diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/chat/ChatStep.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/chat/ChatStep.kt index af9adcc605..ca747ce83b 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/chat/ChatStep.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/chat/ChatStep.kt @@ -1,6 +1,7 @@ package com.flipcash.app.core.chat import android.os.Parcelable +import com.getcode.navigation.HalfSheet import com.getcode.navigation.Sheet import com.getcode.navigation.WrapContentSheet import com.getcode.navigation.flow.FlowStep @@ -96,4 +97,26 @@ sealed interface ChatStep : FlowStep, Parcelable { @Parcelize @Serializable data object EditGroupPicture : ChatStep + + /** + * The full emoji picker for a message's reaction, opened from the pill row's "+" or the + * quick strip's "+". A plain [Sheet] rather than [WrapContentSheet]: the picker's catalog + * needs the screen's height, unlike the short, fixed-content sheets above. [HalfSheet] + * matches iOS's `EmojiPickerSheet.presentationDetents([.medium, .large])` — it rests at half + * height and only grows to the expanded detent once the grid reports it has more than that to + * show (see `AllowSheetExpansionWhenScrollable`). + */ + @Parcelize + @Serializable + data class ReactionPicker(val messageId: Long) : ChatStep, Sheet, HalfSheet + + /** + * Who reacted, and with what — opened by long-pressing a pill. A plain [Sheet] for the same + * reason as [ReactionPicker]: a per-emoji reactor list can run long enough to need the full + * sheet height rather than wrapping its content. [HalfSheet] matches iOS's + * `ReactorsSheet.presentationDetents([.medium, .large])`. + */ + @Parcelize + @Serializable + data class Reactors(val messageId: Long) : ChatStep, Sheet, HalfSheet } diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml index c981d0eff9..d02127ecdd 100644 --- a/apps/flipcash/core/src/main/res/values/strings.xml +++ b/apps/flipcash/core/src/main/res/values/strings.xml @@ -1004,6 +1004,13 @@ Message Not Deleted The message couldn\'t be deleted, so it is still visible to everyone. Message + Couldn\'t add reaction + This message has the maximum number of reactions + %1$d Reactions + Reactions + Search + Clear search + Add reaction Unknown Contact Allow Full Contact Access diff --git a/apps/flipcash/features/messenger/build.gradle.kts b/apps/flipcash/features/messenger/build.gradle.kts index 4c59fcef43..043a468609 100644 --- a/apps/flipcash/features/messenger/build.gradle.kts +++ b/apps/flipcash/features/messenger/build.gradle.kts @@ -18,8 +18,11 @@ dependencies { implementation(project(":apps:flipcash:shared:contacts")) implementation(project(":apps:flipcash:shared:featureflags")) implementation(project(":apps:flipcash:shared:funding")) + implementation(project(":libs:emojis")) implementation(project(":apps:flipcash:shared:menu")) implementation(project(":apps:flipcash:shared:payments")) + // UserProfileDataSource, for the reactors sheet's cached-profile name resolution (decision 4). + implementation(project(":apps:flipcash:shared:persistence:sources")) implementation(project(":apps:flipcash:shared:router")) implementation(project(":apps:flipcash:shared:session")) implementation(project(":apps:flipcash:shared:shareable")) diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt index 621c64f6af..87f878fa87 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -24,6 +25,8 @@ import com.flipcash.app.messenger.internal.ChatViewModel import com.flipcash.app.messenger.internal.StartSendCashOnceReady import com.flipcash.app.messenger.internal.screens.GroupInviteSheet import com.flipcash.app.messenger.internal.screens.MessengerScreen +import com.flipcash.app.messenger.internal.screens.ReactionPickerSheet +import com.flipcash.app.messenger.internal.screens.ReactorsSheet import com.flipcash.app.messenger.internal.screens.cash.ChatAmountEntryContent import com.flipcash.app.messenger.internal.screens.cash.ChatInitPaymentSheet import com.flipcash.app.messenger.internal.screens.profile.ChatProfileScreen @@ -51,6 +54,7 @@ import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch @Composable fun ChatFlowScreen( @@ -116,6 +120,13 @@ private fun chatEntryProvider( annotatedEntry { FlowEditGroupPictureScreen() } + + annotatedEntry { step -> + FlowReactionPickerScreen(step.messageId) + } + annotatedEntry { step -> + FlowReactorsScreen(step.messageId) + } } @Composable @@ -318,3 +329,63 @@ private fun FlowEditGroupNameScreen() { private fun FlowEditGroupPictureScreen() { EditGroupPictureScreen(flowSharedViewModel()) } + +/** The full emoji picker for [messageId] — a tap on any emoji toggles it and dismisses. */ +@Composable +private fun FlowReactionPickerScreen(messageId: Long) { + val viewModel = flowSharedViewModel() + val dismissSheet = LocalBottomSheetDismissDispatcher.current + + ReactionPickerSheet( + loadSections = { query -> viewModel.emojiPickerSections(query) }, + onSelected = { emoji -> + viewModel.dispatchEvent( + ChatViewModel.Event.ToggleReaction(messageId, emoji, clearsSelection = true) + ) + dismissSheet() + }, + onDismiss = dismissSheet, + ) +} + +/** + * Who reacted to [messageId], and with what. Tapping a row dismisses this sheet first, then opens + * the reactor's profile — [ChatViewModel.reactorParticipant] resolves a [ChatParticipant] even for + * a reactor absent from `senderProfiles`. + */ +@Composable +private fun FlowReactorsScreen(messageId: Long) { + val viewModel = flowSharedViewModel() + val navigator = LocalCodeNavigator.current + val dismissSheet = LocalBottomSheetDismissDispatcher.current + val coroutineScope = rememberCoroutineScope() + + val pills by remember(viewModel, messageId) { viewModel.reactionPills(messageId) } + .collectAsStateWithLifecycle(initialValue = emptyList()) + val rows by remember(viewModel, messageId) { viewModel.reactorsRows(messageId) } + .collectAsStateWithLifecycle(initialValue = emptyList()) + val loading by remember(viewModel, messageId) { viewModel.reactorsLoading(messageId) } + .collectAsStateWithLifecycle(initialValue = true) + + ReactorsSheet( + pills = pills, + rows = rows, + loading = loading, + hasMore = { viewModel.reactorsHasMore(messageId) }, + resolveDisplay = { userId -> viewModel.reactorDisplay(userId) }, + onLoadMore = { viewModel.loadMoreReactors(messageId) }, + onOpenProfile = { userId -> + // Dismiss first (decision: profile opens after the sheet closes), then resolve the + // participant — a row is only tappable once reactorDisplay has already resolved it, so + // this is a re-read of state already on screen rather than a fresh network round trip + // in the common case; reactorParticipant only reaches further (member roster, cached + // profile store) for a reactor absent from senderProfiles. + dismissSheet() + coroutineScope.launch { + val participant = viewModel.reactorParticipant(userId) ?: return@launch + navigator.push(ChatStep.Profile(contact = participant)) + } + }, + onDismiss = dismissSheet, + ) +} diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt index 22e41d114b..b20e508889 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt @@ -13,6 +13,7 @@ import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.flatMap +import androidx.paging.map import com.flipcash.analytics.AddMoneySource import com.flipcash.analytics.GroupAccess as AnalyticsGroupAccess import com.flipcash.analytics.GroupGateFunding @@ -31,6 +32,7 @@ import com.flipcash.app.contacts.ContactCoordinator import com.flipcash.app.core.AppRoute import com.flipcash.app.core.chat.ChatIdentifier import com.flipcash.app.core.chat.ChatParticipant +import com.flipcash.app.core.toast.SystemToastController import com.flipcash.app.core.contacts.DeviceContact import com.flipcash.app.core.extensions.setText import com.flipcash.app.core.tokens.brandedName @@ -39,6 +41,7 @@ import com.flipcash.app.core.ui.ConfirmationStyle import com.flipcash.app.core.util.Linkify import com.flipcash.app.funding.PurchaseMethodController import com.flipcash.app.messenger.internal.link.CashCardTap +import com.flipcash.app.persistence.sources.UserProfileDataSource import com.flipcash.libs.coroutines.DispatcherProvider import com.flipcash.shared.chat.UnreadBoundary import com.flipcash.app.messenger.internal.link.ClaimReplyTargets @@ -86,6 +89,20 @@ import com.flipcash.shared.chat.models.splitAroundLinkCard import com.flipcash.shared.chat.models.SenderIdentity import com.flipcash.shared.chat.models.SeparatorConfig import com.flipcash.shared.chat.readOnly +import com.flipcash.shared.chat.MessageReactions +import com.flipcash.shared.chat.reactions.ReactionError +import com.flipcash.shared.chat.reactions.ReactionStrip +import com.flipcash.shared.chat.reactions.ReactionStripComposer +import com.flipcash.shared.chat.reactions.ReactorNameResolver +import com.flipcash.shared.chat.reactions.ReactorsListModel +import com.flipcash.shared.chat.reactions.ReactorsPrefetchCache +import com.flipcash.shared.chat.reactions.SelfReaction +import com.getcode.libs.emojis.reactions.EmojiCatalogLoader +import com.getcode.libs.emojis.reactions.EmojiDrawability +import com.getcode.libs.emojis.reactions.EmojiPickerModel +import com.getcode.libs.emojis.reactions.RecentReactions +import com.getcode.libs.emojis.reactions.RecentReactionsStore +import com.flipcash.shared.chat.canReact import com.flipcash.shared.chat.resolveCapabilities import com.flipcash.shared.chat.ui.detectUrls import com.flipcash.shared.chat.ui.linkableText @@ -178,6 +195,10 @@ internal class ChatViewModel @Inject constructor( private val linkCardResolver: LinkCardResolver, private val cashLinkClaims: CashLinkClaims, private val chatDraftStore: ChatDraftStore, + private val recentReactionsStore: RecentReactionsStore, + private val emojiCatalogLoader: EmojiCatalogLoader, + private val toastController: SystemToastController, + private val userProfileDataSource: UserProfileDataSource, dispatchers: DispatcherProvider, ) : BaseViewModel( initialState = State(), @@ -323,6 +344,12 @@ internal class ChatViewModel @Inject constructor( * the whole round trip and read as dead — which is what it looked like before it had one. */ val joinProgress: LoadingSuccessState = LoadingSuccessState(), + /** + * The quick strip's entries for [selection], recomposed whenever the selected message + * changes — empty while nothing is selected, while the selected message can't be reacted + * to, or before the catalog/recents have loaded for the first selection of this session. + */ + val quickReactionStrip: List = emptyList(), ) { /** * The DM counterparty, or `null` for a group. @@ -558,7 +585,12 @@ internal class ChatViewModel @Inject constructor( data class MessagePolicyChanged(val policy: MessagePolicy) : Event /** Selects [bubble], or leaves selection mode if it is already the selected one. */ - data class ToggleMessageSelection(val bubble: ChatListItem.ContentBubble) : Event + /** [quickReactionStrip] is built ahead by [ChatViewModel.quickReactionStripFor], so the + * strip lands in the same update as the selection; empty leaves it to the strip flow. */ + data class ToggleMessageSelection( + val bubble: ChatListItem.ContentBubble, + val quickReactionStrip: List = emptyList(), + ) : Event data object ClearMessageSelection : Event // The message actions carry what they act on rather than reading it back off the selection: @@ -587,6 +619,34 @@ internal class ChatViewModel @Inject constructor( data class ReplyToMessage(val quote: ChatQuote) : Event data object CancelReply : Event + /** + * Toggles the viewer's reaction with [emoji] on [messageId] — a tap on a pill, on the + * quick strip above a selected bubble, or a pick from the full picker. When it came from + * the strip, [clearsSelection] takes the bubble out of selection mode the same tap it acts + * on; a tap on the pill row under a bubble that isn't selected leaves selection untouched. + */ + data class ToggleReaction( + val messageId: Long, + val emoji: String, + val clearsSelection: Boolean = false, + ) : Event + + /** Opens the full emoji picker for [messageId], from a pill row's or the strip's "+". */ + data class OpenReactionPicker(val messageId: Long) : Event + + /** Opens who-reacted for [messageId], from a long-press on a pill. */ + data class OpenReactors(val messageId: Long) : Event + + /** Internal: the quick strip's entries for the current selection have (re)composed. */ + data class QuickReactionStripComposed(val entries: List) : Event + + /** + * The transcript's own refresh-on-paging hook has ids it wants reaction state refreshed + * for — see [ReactionRefreshPlanner] and the [ChatAction.RefreshReactionIds] that carries + * this down from `MessageList`. Not a reader gesture, so it changes no visible state. + */ + data class RefreshReactionIds(val messageIds: List) : Event + /** * The reader tapped a cash voucher in this transcript, naming the link. * @@ -646,6 +706,25 @@ internal class ChatViewModel @Inject constructor( */ private val viewerCanPost = stateFlow.map { !it.isOutsideGroup }.distinctUntilChanged() + /** + * Live reaction overrides for the open chat — see [ReactionOperations.observeChatReactions]. + * A message missing here falls back to `MessageReactions.from(message.reactions)` in + * [mappedMessages], per that function's contract. + * + * Held as a [StateFlow] (Eagerly, like [senderProfiles]) rather than left cold: the toggle + * handler needs a synchronous read of "is this already self-reacted" to decide whether a tap + * is an add worth ranking for the quick strip, and a cold flow would mean starting a second + * collection just to answer that one question. + */ + @OptIn(ExperimentalCoroutinesApi::class) + private val reactionOverlay: StateFlow> = stateFlow.mapNotNull { it.chatId } + .distinctUntilChanged() + .flatMapLatest { chatCoordinator.observeChatReactions(it) } + .stateIn(viewModelScope, SharingStarted.Eagerly, emptyMap()) + + /** Declared before `init`, which starts filling it. See [StripInputs]. */ + private val stripInputs = MutableStateFlow(null) + /** * Null for a DM, a map for a group — the distinction the transcript needs, because a DM's * counterparty is already named in the title bar and attributing each of their bubbles would be @@ -680,6 +759,111 @@ internal class ChatViewModel @Inject constructor( senderProfiles.value?.get(userId.hexEncodedString()) ?.let { ChatParticipant.TipUser(userId, it) } + /** + * One [ReactorsListModel] per messageId, started on [Event.OpenReactors] (see + * [initMessageActionHandlers]) rather than on the reactors sheet's own composition — loading + * has to begin on the pill long-press itself, and the sheet is a fresh Compose tree on every + * push. `getReactorsPage` is captured against whatever [ChatId] is current at call time + * rather than the chat this view model was built for; a chat never changes under one instance + * in practice, but reading [stateFlow] here keeps the cache correct if that ever changes. + */ + private val reactorsPrefetchCache = ReactorsPrefetchCache(scope = viewModelScope) { messageId, emoji, token -> + val chatId = stateFlow.value.chatId ?: return@ReactorsPrefetchCache Result.failure( + IllegalStateException("No active chat to fetch reactors for") + ) + chatCoordinator.getReactorsPage(chatId, messageId, emoji, token) + } + + fun reactorsRows(messageId: Long): Flow> = reactorsPrefetchCache.rows(messageId) + + fun reactorsLoading(messageId: Long): Flow = reactorsPrefetchCache.loading(messageId) + + fun reactorsHasMore(messageId: Long): Boolean = reactorsPrefetchCache.hasMore(messageId) + + fun loadMoreReactors(messageId: Long) = reactorsPrefetchCache.loadMoreIfNeeded(messageId) + + /** [messageId]'s pills, live — the reactors sheet's summary row (decision 4: title + pills). */ + fun reactionPills(messageId: Long): Flow> = + reactionOverlay.map { it[messageId]?.pills.orEmpty() }.distinctUntilChanged() + + /** + * The full picker's sections for [query] (recents row + one section per catalog category, or a + * single search-results section — see [EmojiPickerModel.sections]), recomposed each time + * [query] changes. Loads the catalog once per call rather than caching it on the view model, + * matching how [emojiCatalogLoader] is already used for the quick strip above — the loader + * itself caches the parsed file, so this is cheap after the first call. + */ + suspend fun emojiPickerSections(query: String): List { + val catalog = emojiCatalogLoader.load() + val undrawable = catalog.entries + .map { it.emoji } + .filterNot { EmojiDrawability.isDrawable(it) } + .toSet() + val recents = recentReactionsStore.rank(undrawable = undrawable) + return EmojiPickerModel.sections(catalog = catalog, undrawable = undrawable, recents = recents, query = query) + } + + /** A reactors-sheet row's resolved identity: the name to show (decision 4's precedence, + * "You" for the viewer), and the profile to draw an avatar from when one is known. */ + data class ReactorDisplay(val name: String, val profile: UserProfile?) + + /** + * The reactors sheet's display name and avatar source for [userId] (decision 4), re-emitting + * as later sources — the member roster, then a server fetch — resolve. Kicks off + * [ChatCoordinator.requestSenderProfile] itself when nothing has an answer yet, mirroring + * [resolveSenderName]'s fallback for the transcript. The self case shows the viewer's own + * picture (so their avatar isn't blank) under the "You" label rather than their own profile + * name. + */ + fun reactorDisplay(userId: ID): Flow { + val chatId = stateFlow.value.chatId + val selfLabel = resources.getString(R.string.title_you) + val selfUserId = userManager.accountId + val members = chatId?.let { chatCoordinator.observeMembers(it) } ?: flowOf(emptyList()) + var requested = false + return combine( + userProfileDataSource.observeProfiles(), + members, + senderProfiles, + ) { cachedProfiles, memberList, sentProfiles -> + val cached = cachedProfiles[userId.hexEncodedString()] + val name = ReactorNameResolver.resolve( + userId = userId, + selfUserId = selfUserId, + selfLabel = selfLabel, + cachedProfile = cached, + members = memberList, + senderProfiles = sentProfiles.orEmpty(), + ) ?: return@combine null + val profile = if (selfUserId != null && userId == selfUserId) { + userManager.profile + } else { + cached + ?: memberList.firstOrNull { it.userId == userId }?.userProfile + ?: sentProfiles?.get(userId.hexEncodedString()) + } + ReactorDisplay(name = name, profile = profile) + }.onEach { display -> + if (display == null && !requested) { + requested = true + chatCoordinator.requestSenderProfile(userId) + } + } + } + + /** [ChatParticipant] for [userId] to open a profile from the reactors sheet, unlike + * [memberParticipant] this also covers a reactor absent from [senderProfiles] by falling back + * to whatever the cached profile store or member roster already has. */ + suspend fun reactorParticipant(userId: ID): ChatParticipant.TipUser? { + memberParticipant(userId)?.let { return it } + val chatId = stateFlow.value.chatId + val fromMembers = chatId?.let { id -> + chatCoordinator.observeMembers(id).first().firstOrNull { it.userId == userId }?.userProfile + } + val profile = fromMembers ?: userProfileDataSource.getCachedProfile(userId) + return profile?.let { ChatParticipant.TipUser(userId, it) } + } + /** * How a drawn card reaches the lookup. Provided to the transcript, read by `LinkCardView`. * @@ -802,6 +986,11 @@ internal class ChatViewModel @Inject constructor( // after the first page does. senderId = message.senderId?.takeIf { !message.isFromSelf }, linkCard = linkCard, + // Stored fallback only — see the reactionOverlay pass below, which fills + // in a live override for whichever row ends up as `isLastRow` once + // `splitAroundLinkCard` has run. + reactionPills = MessageReactions.from(message.reactions).pills, + canReact = canReact(message), ) // A carded link takes a row of its own, with the prose either side of it // on rows above and below. Reversed because the list is: this page runs @@ -810,6 +999,23 @@ internal class ChatViewModel @Inject constructor( .asReversed() } } + } + // A live reaction override — an in-flight tap, or a refreshed/streamed confirmed state — + // beats the stored fallback baked in above. Combined as its own pass over the already-built + // pages rather than folded into the combine above: it needs nothing else that combine reads + // (only the message id already on each row), and keeping it separate means a reaction + // update re-maps pills without re-running sender resolution, link classification or the + // policy/capability pass over every row. + .let { pages -> + combine(pages, reactionOverlay) { data, overlay -> + if (overlay.isEmpty()) return@combine data + data.map { bubble -> + // Only the row the "Edited" marker also lands on carries the pill row, so a + // split message shows one set of pills, not one per row. + val live = if (bubble.isLastRow) overlay[bubble.messageId] else null + if (live == null) bubble else bubble.copy(reactionPills = live.pills) + } + } }.cachedIn(viewModelScope) /** @@ -1642,7 +1848,130 @@ internal class ChatViewModel @Inject constructor( .launchIn(viewModelScope) } + /** + * What the quick strip is built from, other than the message's own reactions. Kept ready ahead + * of a long-press so the selection reducer can build the strip in the same state update that + * lifts the bubble; built on demand, the strip trailed the lift by the catalog's glyph check + * and a DataStore read. Refreshed at init and after each toggle, which is what moves recents. + */ + private class StripInputs( + val recentStats: Map, + val catalogFill: List, + val undrawable: Set, + ) + + private suspend fun refreshStripInputs(): StripInputs { + val catalog = emojiCatalogLoader.load() + val fill = catalog.firstCategoryEntries.map { it.emoji } + val undrawable = withContext(Dispatchers.Default) { + fill.filterNot { EmojiDrawability.isDrawable(it) }.toSet() + } + return StripInputs( + recentStats = recentReactionsStore.stats(), + catalogFill = fill, + undrawable = undrawable, + ).also { stripInputs.value = it } + } + + /** + * The quick strip for [bubble], from the inputs already in hand, or empty if they have not + * loaded yet (the strip flow fills it in then). Called as the bubble is selected, so the strip + * enters with the lift instead of a round trip behind it. + */ + fun quickReactionStripFor(bubble: ChatListItem.ContentBubble): List { + if (!bubble.canReact) return emptyList() + val inputs = stripInputs.value ?: return emptyList() + return composeStrip(inputs, reactionOverlay.value[bubble.messageId]?.selfReactions.orEmpty()) + } + + private fun composeStrip(inputs: StripInputs, selfReactions: List) = + ReactionStripComposer.compose( + recentStats = inputs.recentStats, + selfReactions = selfReactions, + catalogFillSource = inputs.catalogFill, + undrawable = inputs.undrawable, + ) + private fun initMessageActionHandlers() { + viewModelScope.launch { refreshStripInputs() } + + eventFlow.filterIsInstance() + .onEach { event -> + val chatId = stateFlow.value.chatId ?: return@onEach + viewModelScope.launch { + chatCoordinator.toggleReaction(chatId, event.messageId, event.emoji) + refreshStripInputs() + } + } + .launchIn(viewModelScope) + + // The quick strip above a selected, reactable bubble — recomposed each time the selection + // moves to a different message. Reads the live overlay for self-reactions rather than the + // selected bubble's own (possibly stale) snapshot, the same source [mappedMessages] treats + // as authoritative. + stateFlow.map { it.selection?.takeIf { bubble -> bubble.canReact }?.messageId } + .distinctUntilChanged() + .flatMapLatest { messageId -> + if (messageId == null) { + flowOf(emptyList()) + } else { + reactionOverlay.map { overlay -> + overlay[messageId]?.selfReactions.orEmpty() + }.distinctUntilChanged() + } + } + .onEach { selfReactions -> + val entries = if (selfReactions.isEmpty() && stateFlow.value.selection == null) { + emptyList() + } else { + composeStrip(stripInputs.value ?: refreshStripInputs(), selfReactions) + } + dispatchEvent(Event.QuickReactionStripComposed(entries)) + } + .launchIn(viewModelScope) + + // OpenReactionPicker carries no side effect here — the screen's action handler pushes the + // ChatStep, the same as every other transcript-triggered navigation (see MessengerScreen's + // chatActionHandler). The reducer above is a no-op for both too; they exist as events at + // all so a picker pick can reach ToggleReaction through the same dispatch path everything + // else in the transcript uses. + // + // OpenReactors does have one: it starts the reactors fetch (see reactorsPrefetchCache) + // before the navigator pushes ChatStep.Reactors, so loading begins on the long-press + // rather than on the sheet's own composition. + eventFlow.filterIsInstance() + .onEach { event -> + val emojis = reactionOverlay.value[event.messageId]?.pills?.map { it.emoji }.orEmpty() + reactorsPrefetchCache.start(event.messageId, emojis) + } + .launchIn(viewModelScope) + + // See ReactionRefreshPlanner and MessageList's reaction-refresh effects: the transcript + // computes which ids need a refresh (the newest window on open/resume, Room-sourced pages + // as they page in) and hands the batch down here — this just forwards it to the + // coordinator. No reducer case: nothing about visible state changes from this. + eventFlow.filterIsInstance() + .onEach { event -> + val chatId = stateFlow.value.chatId ?: return@onEach + if (event.messageIds.isEmpty()) return@onEach + chatCoordinator.refreshReactions(chatId, event.messageIds) + } + .launchIn(viewModelScope) + + // A failed add/remove the coordinator has already rolled back optimistically — this only + // tells the reader why the pill snapped back. One line, as iOS's toast is. + chatCoordinator.reactionErrors + .onEach { error -> + toastController.showToast( + when (error) { + ReactionError.REACTION_FAILED -> R.string.title_reactionNotAdded + ReactionError.TOO_MANY_REACTION_TYPES -> R.string.title_reactionLimitReached + }, + replacePrevious = true, + ) + } + .launchIn(viewModelScope) + // Read off the state rather than carried on the event, so the row that copies the link and // the row that shares it are handing out the one url [State.groupInviteUrl] builds. eventFlow.filterIsInstance() @@ -2457,6 +2786,7 @@ internal class ChatViewModel @Inject constructor( } state.copy( selection = selected, + quickReactionStrip = if (selected != null) event.quickReactionStrip else emptyList(), confirmingDelete = false, ) } @@ -2505,6 +2835,16 @@ internal class ChatViewModel @Inject constructor( ) } Event.CancelReply -> { state -> state.copy(replyingTo = null) } + // The toggle itself is handled in initMessageActionHandlers (it calls the + // coordinator); the reducer's only job is to take the bubble out of selection mode + // when the tap came from the quick strip. + is Event.ToggleReaction -> { state -> + if (event.clearsSelection) state.copy(selection = null) else state + } + is Event.OpenReactionPicker -> { state -> state } + is Event.OpenReactors -> { state -> state } + is Event.QuickReactionStripComposed -> { state -> state.copy(quickReactionStrip = event.entries) } + is Event.RefreshReactionIds -> { state -> state } // Nothing on screen moves when a voucher is tapped -- the link leaves, the card // keeps saying what it said, and the claim comes back as its own signal. is Event.CashLinkOpened -> { state -> state } diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt index 8c1b733d24..7de666e6b9 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt @@ -127,7 +127,10 @@ internal fun MessengerScreen(viewModel: ChatViewModel) { is ChatAction.ToggleSelection -> { viewModel.dispatchEvent( - ChatViewModel.Event.ToggleMessageSelection(action.bubble) + ChatViewModel.Event.ToggleMessageSelection( + bubble = action.bubble, + quickReactionStrip = viewModel.quickReactionStripFor(action.bubble), + ) ) } @@ -196,6 +199,30 @@ internal fun MessengerScreen(viewModel: ChatViewModel) { keyboard.hideIfVisible { navigator.push(ChatStep.Profile(it)) } } } + + is ChatAction.ToggleReaction -> { + viewModel.dispatchEvent( + ChatViewModel.Event.ToggleReaction( + messageId = action.messageId, + emoji = action.emoji, + clearsSelection = action.fromStrip, + ) + ) + } + + is ChatAction.OpenReactionPicker -> { + viewModel.dispatchEvent(ChatViewModel.Event.OpenReactionPicker(action.messageId)) + navigator.push(ChatStep.ReactionPicker(action.messageId)) + } + + is ChatAction.OpenReactors -> { + viewModel.dispatchEvent(ChatViewModel.Event.OpenReactors(action.messageId)) + navigator.push(ChatStep.Reactors(action.messageId)) + } + + is ChatAction.RefreshReactionIds -> { + viewModel.dispatchEvent(ChatViewModel.Event.RefreshReactionIds(action.messageIds)) + } } Unit @@ -258,6 +285,7 @@ internal fun MessengerScreen(viewModel: ChatViewModel) { linkCardResolution = viewModel.linkCardResolution, canViewProfile = state.canViewProfile, onJumpConsumed = { viewModel.dispatchEvent(ChatViewModel.Event.JumpConsumed) }, + topBarBottom = barHeight, ) } } diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/ReactionPickerSheet.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/ReactionPickerSheet.kt new file mode 100644 index 0000000000..a7ee08a9fb --- /dev/null +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/ReactionPickerSheet.kt @@ -0,0 +1,505 @@ +package com.flipcash.app.messenger.internal.screens + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.spring +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.offset +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.selected +import androidx.compose.ui.unit.IntOffset +import kotlin.math.roundToInt +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Cancel +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.flipcash.features.messenger.R +import com.getcode.libs.emojis.reactions.EmojiCatalogEntry +import com.getcode.libs.emojis.reactions.EmojiPickerModel +import com.getcode.theme.CodeTheme +import com.getcode.ui.core.unboundedClickable +import com.getcode.ui.theme.CodeCircularProgressIndicator +import com.getcode.ui.utils.LocalSheetOverhang +import com.getcode.ui.utils.AllowSheetExpansionWhenScrollable +import dev.chrisbanes.haze.HazeInput +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.blur.HazeBlurStyle +import dev.chrisbanes.haze.blur.HazeColorEffect +import dev.chrisbanes.haze.blur.hazeBlur +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.rememberHazeState +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch + +/** + * The full emoji picker (decision 5: opens as `ChatStep.ReactionPicker`, a [com.getcode.ui.navigation.HalfSheet]). + * A search capsule (not auto-focused — iOS's picker doesn't auto-focus its search field either), + * then every catalog category in a fixed 7-column grid behind a floating glass category bar, all + * through one scrollable list so [AllowSheetExpansionWhenScrollable] sees a single scroll container. + * Mirrors iOS `EmojiPickerSheet` (node 9768:1402, category bar node 9768:1624). There is no title — + * iOS's picker doesn't have one either. + */ +@Composable +internal fun ReactionPickerSheet( + loadSections: suspend (query: String) -> List, + onSelected: (String) -> Unit, + onDismiss: () -> Unit, +) { + val gridState = rememberLazyListState() + AllowSheetExpansionWhenScrollable(gridState) + + val searchState = remember { TextFieldState() } + + Column(modifier = Modifier.fillMaxWidth()) { + SearchField( + state = searchState, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 16.dp, bottom = 12.dp), + ) + + val query by produceState(initialValue = "") { + snapshotFlow { searchState.text.toString() } + .distinctUntilChanged() + .debounce(150) + .collect { value = it } + } + + var hasLoadedOnce by remember { mutableStateOf(false) } + val sections by produceState(initialValue = emptyList(), query, loadSections) { + value = loadSections(query) + hasLoadedOnce = true + } + + when { + !hasLoadedOnce -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CodeCircularProgressIndicator() + } + } + + sections.isEmpty() && query.isNotBlank() -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + text = stringResource(R.string.title_noSearchResults, query), + style = CodeTheme.typography.textMedium, + color = CodeTheme.colors.textSecondary, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 32.dp), + ) + } + } + + else -> { + EmojiGrid( + sections = sections, + query = query, + gridState = gridState, + onSelected = onSelected, + ) + } + } + } +} + +private const val COLUMNS = 7 + +/** One flat entry in the grid's backing list, keyed so a category-bar tap can find a section's row. */ +private sealed interface GridRow { + val sectionId: String + + data class Header(override val sectionId: String, val title: String) : GridRow + data class Emojis(override val sectionId: String, val entries: List) : GridRow +} + +@Composable +private fun EmojiGrid( + sections: List, + query: String, + gridState: LazyListState, + onSelected: (String) -> Unit, +) { + // A LazyColumn of hand-chunked rows rather than LazyVerticalGrid: AllowSheetExpansionWhenScrollable + // only reads a LazyListState, and one flat list keeps every section's header + rows in the same + // scroll container it watches. It also fills the sheet as it expands to 92.5% (no heightIn cap). + val rows = remember(sections) { + buildList { + for (section in sections) { + if (section.id != EmojiPickerModel.SEARCH_RESULTS_ID) { + add(GridRow.Header(section.id, section.title)) + } + for (chunk in section.entries.chunked(COLUMNS)) { + add(GridRow.Emojis(section.id, chunk)) + } + } + } + } + + // The flat-list index a category-bar tap scrolls to — the header when the section has one + // (every catalog category does; search results never show the bar at all). + val sectionStartIndex = remember(rows) { + val map = LinkedHashMap() + rows.forEachIndexed { index, row -> map.putIfAbsent(row.sectionId, index) } + map + } + + val showBar = query.isBlank() && sections.size > 1 + val hazeState = rememberHazeState() + val scope = rememberCoroutineScope() + + // A category chosen from the bar whose section the grid is still scrolling to — the indicator + // holds on it so it doesn't pass through every category the grid scrolls over on the way + // (mirrors iOS's `pendingCategory`). + var pendingCategory by remember { mutableStateOf(null) } + val visibleCategory by remember { + derivedStateOf { + val visibleIndex = gridState.firstVisibleItemIndex + sectionStartIndex.entries + .filter { it.value <= visibleIndex } + .maxByOrNull { it.value } + ?.key + } + } + LaunchedEffect(visibleCategory) { + if (visibleCategory == pendingCategory) pendingCategory = null + } + val selectedCategory = pendingCategory ?: visibleCategory ?: sections.firstOrNull()?.id + + // An expandable sheet is laid out at its expanded height and slid down, so its bottom sits off + // screen until it's fully up. Lifting the bar by that overhang keeps it hugging the screen at + // either detent and mid-drag; it's read at placement so it moves in the same frame as the sheet. + // Collapsing past the half detent would lift it up into the search field, so the lift stops + // once the bar reaches the top of the grid, 12dp under the field, and from there the bar ducks + // off screen with the sheet. + val sheetOverhang = LocalSheetOverhang.current + var gridHeightPx by remember { mutableIntStateOf(0) } + + Box(modifier = Modifier.fillMaxSize().navigationBarsPadding()) { + LazyColumn( + state = gridState, + modifier = Modifier + .fillMaxSize() + .onSizeChanged { gridHeightPx = it.height } + .hazeSource(hazeState), + contentPadding = PaddingValues( + top = 4.dp, + bottom = if (showBar) CATEGORY_BAR_HEIGHT + 24.dp else 16.dp, + ), + ) { + items(rows, key = { row -> + when (row) { + is GridRow.Header -> "header-${row.sectionId}" + is GridRow.Emojis -> "row-${row.sectionId}-${row.entries.first().emoji}" + } + }) { row -> + when (row) { + is GridRow.Header -> { + Text( + text = row.title.uppercase(), + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = CodeTheme.colors.textMain.copy(alpha = 0.5f), + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 20.dp, bottom = 12.dp), + ) + } + + is GridRow.Emojis -> { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 12.dp), + ) { + for (entry in row.entries) { + EmojiCell(entry = entry, onSelected = onSelected) + } + repeat(COLUMNS - row.entries.size) { + Box(modifier = Modifier.weight(1f)) + } + } + } + } + } + } + + if (showBar) { + CategoryBar( + sections = sections, + selected = selectedCategory, + hazeState = hazeState, + onSelect = { id -> + pendingCategory = id + val index = sectionStartIndex[id] ?: return@CategoryBar + scope.launch { gridState.animateScrollToItem(index) } + }, + modifier = Modifier + .align(Alignment.BottomCenter) + .offset { + val maxLift = gridHeightPx - + (CATEGORY_BAR_HEIGHT + CATEGORY_BAR_BOTTOM_GAP).roundToPx() + val lift = sheetOverhang().roundToInt().coerceIn(0, maxLift.coerceAtLeast(0)) + IntOffset(0, -lift) + } + .padding(bottom = CATEGORY_BAR_BOTTOM_GAP), + ) + } + } +} + +@Composable +private fun RowScope.EmojiCell(entry: EmojiCatalogEntry, onSelected: (String) -> Unit) { + Box( + modifier = Modifier + .weight(1f) + .heightIn(min = 41.dp) + .unboundedClickable { onSelected(entry.emoji) } + .semantics { contentDescription = entry.name }, + contentAlignment = Alignment.Center, + ) { + Text(text = entry.emoji, fontSize = 34.sp) + } +} + +/** + * The floating jump bar to each category, a glass capsule over the grid (node 9768:1624), frosted + * with a Haze blur of the grid scrolling beneath it — Android's analogue of iOS's `.glassEffect` + * (see [com.flipcash.app.core.ui.NavigationBar] for the same pattern). The selected category sits on + * its own capsule indicator, which springs between slots. As on iOS, the indicator can be pressed and + * dragged across the bar; [onSelect] fires only once it's let go over a category, so a plain tap is + * the same gesture with no travel. + */ +@Composable +private fun CategoryBar( + sections: List, + selected: String?, + hazeState: HazeState, + onSelect: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val backdrop = CodeTheme.colors.background + val liquidGlass = HazeBlurStyle { + blurRadius(32.dp) + backgroundColor(backdrop) + colorEffects(listOf(HazeColorEffect.tint(backdrop.copy(alpha = 0.72f)))) + } + val haptics = LocalHapticFeedback.current + val density = LocalDensity.current + val selectedIndex = sections.indexOfFirst { it.id == selected }.coerceAtLeast(0) + + // Where the finger is along the bar while it drags the indicator, null otherwise. + var dragX by remember { mutableStateOf(null) } + + Box( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 30.dp) + .height(CATEGORY_BAR_HEIGHT) + .clip(CircleShape) + .hazeBlur(HazeInput.Sources(hazeState), liquidGlass), + ) { + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 6.dp) + .pointerInput(sections) { + val count = sections.size + fun indexAt(x: Float): Int = + (x / (size.width.toFloat() / count)).toInt().coerceIn(0, count - 1) + awaitEachGesture { + val down = awaitFirstDown() + down.consume() + dragX = down.position.x + var hovered = indexAt(down.position.x) + var lastX = down.position.x + while (true) { + val event = awaitPointerEvent() + val change = event.changes.firstOrNull { it.id == down.id } ?: break + if (!change.pressed) { + lastX = change.position.x + change.consume() + break + } + change.consume() + lastX = change.position.x + dragX = lastX + val index = indexAt(lastX) + if (index != hovered) { + hovered = index + haptics.performHapticFeedback(HapticFeedbackType.SegmentTick) + } + } + dragX = null + onSelect(sections[indexAt(lastX)].id) + } + }, + ) { + val barWidth = constraints.maxWidth.toFloat() + val slot = barWidth / sections.size.coerceAtLeast(1) + val indicatorWidth = with(density) { minOf(slot, (INDICATOR_HEIGHT + 6.dp).toPx()) } + val restingX = slot * (selectedIndex + 0.5f) + val targetX = dragX?.coerceIn(slot / 2, barWidth - slot / 2) ?: restingX + // Follow the finger 1:1 while dragging; only the press, release and selection animate. + val indicatorX by animateFloatAsState( + targetValue = targetX, + animationSpec = if (dragX != null) snap() else settleSpring(), + label = "categoryIndicatorX", + ) + val indicatorScale by animateFloatAsState( + targetValue = if (dragX == null) 1f else 1.15f, + animationSpec = settleSpring(), + label = "categoryIndicatorScale", + ) + + Box( + modifier = Modifier + .align(Alignment.CenterStart) + .offset { IntOffset((indicatorX - indicatorWidth / 2).roundToInt(), 0) } + .size(width = with(density) { indicatorWidth.toDp() }, height = INDICATOR_HEIGHT) + .graphicsLayer { + scaleX = indicatorScale + scaleY = indicatorScale + } + .clip(CircleShape) + .background(CodeTheme.colors.textMain.copy(alpha = 0.14f)), + ) + + Row(modifier = Modifier.fillMaxSize()) { + sections.forEachIndexed { index, section -> + Box( + modifier = Modifier + .weight(1f) + .fillMaxSize() + .semantics { + contentDescription = section.title + this.selected = index == selectedIndex + onClick { + onSelect(section.id) + true + } + }, + contentAlignment = Alignment.Center, + ) { + Text(text = section.entries.firstOrNull()?.emoji.orEmpty(), fontSize = 20.sp) + } + } + } + } + } +} + +// iOS's `.spring(duration: 0.35, bounce: 0.2)`. +private fun settleSpring() = spring(dampingRatio = 0.8f, stiffness = 320f) + +private val CATEGORY_BAR_HEIGHT = 45.dp +private val CATEGORY_BAR_BOTTOM_GAP = 4.dp +private val INDICATOR_HEIGHT = 38.dp + +@Composable +private fun SearchField(state: TextFieldState, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .height(44.dp) + .clip(CircleShape) + .background(Color.Black.copy(alpha = 0.26f)) + .padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = null, + tint = CodeTheme.colors.textSecondary, + modifier = Modifier.size(20.dp), + ) + Box( + modifier = Modifier + .weight(1f) + .padding(start = 8.dp), + ) { + BasicTextField( + state = state, + textStyle = CodeTheme.typography.textMedium.copy(color = CodeTheme.colors.textMain), + cursorBrush = SolidColor(CodeTheme.colors.textMain), + lineLimits = TextFieldLineLimits.SingleLine, + decorator = { inner -> + if (state.text.isEmpty()) { + Text( + text = stringResource(R.string.hint_searchEmoji), + style = CodeTheme.typography.textMedium, + color = CodeTheme.colors.textSecondary, + ) + } + inner() + }, + ) + } + if (state.text.isNotEmpty()) { + Icon( + imageVector = Icons.Filled.Cancel, + contentDescription = stringResource(R.string.action_clearEmojiSearch), + tint = CodeTheme.colors.textSecondary, + modifier = Modifier + .size(20.dp) + .unboundedClickable { state.clearText() }, + ) + } + } +} diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/ReactorsSheet.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/ReactorsSheet.kt new file mode 100644 index 0000000000..878c247cb3 --- /dev/null +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/ReactorsSheet.kt @@ -0,0 +1,366 @@ +package com.flipcash.app.messenger.internal.screens + +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.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.flipcash.app.messenger.internal.ChatViewModel +import com.flipcash.features.messenger.R +import com.flipcash.services.models.handle +import com.flipcash.shared.chat.reactions.ReactionPill +import com.flipcash.shared.chat.reactions.ReactorsListModel +import com.flipcash.shared.common.ui.ContactAvatar +import com.getcode.opencode.model.core.ID +import com.getcode.theme.CodeTheme +import com.getcode.ui.components.AppBarDefaults +import com.getcode.ui.components.AppBarWithTitle +import com.getcode.ui.utils.AllowSheetExpansionWhenScrollable +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged + +/** + * Who reacted, and with what (decision 4): one row per person, every emoji they used, no filters. + * Opens unscrolled with nothing focused — there is no search here, unlike the picker. + * + * @param resolveDisplay per-row identity, re-subscribed for the row's lifetime in composition — + * see [ChatViewModel.reactorDisplay] for the source precedence and its live re-resolution as a + * member roster or a server fetch lands. + * @param hasMore read live rather than passed as a snapshot: the last row's visibility effect below + * asks it fresh each time it fires. + */ +@Composable +internal fun ReactorsSheet( + pills: List, + rows: List, + loading: Boolean, + hasMore: () -> Boolean, + resolveDisplay: (ID) -> Flow, + onLoadMore: () -> Unit, + onOpenProfile: (ID) -> Unit, + onDismiss: () -> Unit, +) { + val listState = rememberLazyListState() + AllowSheetExpansionWhenScrollable(listState) + + val totalCount = remember(pills) { pills.sumOf { it.count } } + + Column(modifier = Modifier.fillMaxWidth()) { + AppBarWithTitle( + title = if (totalCount > 0) { + stringResource(R.string.title_reactionsCount, totalCount) + } else { + stringResource(R.string.title_reactorsSheet) + }, + titleAlignment = Alignment.CenterHorizontally, + endContent = { AppBarDefaults.Close(onClick = onDismiss) }, + ) + + if (pills.isNotEmpty()) { + SummaryPillRow(pills = pills) + } + + // Placeholders only if nothing has rendered 300ms in — the first page usually lands + // faster than that, and this keeps a fast fetch from ever flashing skeleton rows. + val showPlaceholders by produceState(initialValue = false, rows, loading) { + value = false + if (rows.isEmpty() && loading) { + delay(300) + value = rows.isEmpty() + } + } + + LoadMoreOnLastRowVisible(listState, rows, hasMore, onLoadMore) + + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding(), + // No horizontal padding: rows inset their own leading edge, and overflowing emoji run + // off the screen edge rather than stopping at a margin. + contentPadding = PaddingValues(vertical = CodeTheme.dimens.grid.x2), + ) { + if (showPlaceholders) { + // min(max pill count, 5) placeholder rows. + val count = (pills.maxOfOrNull { it.count } ?: 1L).coerceAtMost(5L).toInt().coerceAtLeast(1) + items(count) { ReactorRowPlaceholder() } + } else { + items(rows, key = { it.userId.joinToString(",") }) { row -> + ReactorRow( + userId = row.userId, + emojis = row.emojis, + resolveDisplay = resolveDisplay, + onClick = { onOpenProfile(row.userId) }, + ) + } + } + } + } +} + +@Composable +private fun LoadMoreOnLastRowVisible( + listState: LazyListState, + rows: List, + hasMore: () -> Boolean, + onLoadMore: () -> Unit, +) { + LaunchedEffect(listState, rows) { + if (rows.isEmpty()) return@LaunchedEffect + snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index } + .distinctUntilChanged() + .collect { lastVisibleIndex -> + if (lastVisibleIndex != null && lastVisibleIndex >= rows.lastIndex && hasMore()) { + onLoadMore() + } + } + } +} + +/** The non-interactive summary row: every pill from the pill row, unclickable here. */ +@Composable +private fun SummaryPillRow(pills: List) { + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(horizontal = CodeTheme.dimens.inset, vertical = CodeTheme.dimens.grid.x2), + horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), + ) { + for (pill in pills) { + Box( + modifier = Modifier + .height(34.dp) + .background(Color.Black.copy(alpha = 0.3f), RoundedCornerShape(percent = 50)) + .padding(horizontal = CodeTheme.dimens.grid.x3), + contentAlignment = Alignment.Center, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(text = pill.emoji, fontSize = 17.sp) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = pill.count.toString(), + fontSize = 13.sp, + color = Color.White.copy(alpha = 0.5f), + ) + } + } + } + } +} + +/** + * One reactor: avatar and name/handle, then every emoji they reacted with. Only the person opens + * their profile; the emoji are theirs to scroll, not a tap target. + */ +@Composable +private fun ReactorRow( + userId: ID, + emojis: List, + resolveDisplay: (ID) -> Flow, + onClick: () -> Unit, +) { + val displayFlow = remember(userId, resolveDisplay) { resolveDisplay(userId) } + val display by displayFlow.collectAsStateWithLifecycle(initialValue = null) + + BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { + // The person keeps priority over the emoji, which always get at least MinEmojiWidth. + val personMaxWidth = (maxWidth - RowLeadingInset - MinEmojiWidth).coerceAtLeast(0.dp) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = RowLeadingInset) + .padding(vertical = 8.5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = Modifier + .widthIn(max = personMaxWidth) + .clickable(enabled = display != null, onClick = onClick), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(15.dp), + ) { + val profile = display?.profile + if (profile != null) { + ContactAvatar(userProfile = profile, modifier = Modifier.size(48.dp).clip(CircleShape)) + } else { + Box( + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .background(Brush.linearGradient(CodeTheme.colors.contactAvatar.colors)), + ) + } + + Column { + Text( + text = display?.name ?: "", + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = CodeTheme.colors.textMain, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + val handle = profile?.handle + if (!handle.isNullOrBlank()) { + Text( + text = handle, + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = CodeTheme.colors.textMain.copy(alpha = 0.5f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + + ReactorEmojis(emojis = emojis, modifier = Modifier.weight(1f)) + } + } +} + +/** + * A reactor's emoji, trailing-aligned in whatever the name leaves. When there are more than fit + * they scroll sideways, opening on the first emoji, and fade out across the margins instead of + * stopping at a hard edge. The leading margin is also the gap to the name. + */ +@Composable +private fun ReactorEmojis(emojis: List, modifier: Modifier = Modifier) { + val scrollState = rememberScrollState() + val overflows = scrollState.maxValue > 0 + Box( + modifier = modifier + .semantics(mergeDescendants = true) { contentDescription = emojis.joinToString(" ") } + .then(if (overflows) Modifier.edgeFade(EmojiLeadingMargin, EmojiTrailingMargin) else Modifier), + contentAlignment = Alignment.CenterEnd, + ) { + Row( + modifier = Modifier + .horizontalScroll(scrollState) + .padding(start = EmojiLeadingMargin, end = EmojiTrailingMargin) + .clearAndSetSemantics { }, + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + for (emoji in emojis) { + Text(text = emoji, fontSize = 30.sp) + } + } + } +} + +/** Fades content out across [leading] and [trailing], eased so the fade has no visible start line. */ +private fun Modifier.edgeFade(leading: Dp, trailing: Dp): Modifier = this + .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } + .drawWithContent { + drawContent() + val leadingPx = leading.toPx() + val trailingPx = trailing.toPx() + drawRect( + brush = Brush.horizontalGradient(*EdgeFadeStops, startX = 0f, endX = leadingPx), + size = Size(leadingPx, size.height), + blendMode = BlendMode.DstIn, + ) + drawRect( + brush = Brush.horizontalGradient(*EdgeFadeStops, startX = size.width, endX = size.width - trailingPx), + topLeft = Offset(size.width - trailingPx, 0f), + size = Size(trailingPx, size.height), + blendMode = BlendMode.DstIn, + ) + } + +private val EdgeFadeStops = arrayOf( + 0f to Color.Black.copy(alpha = 0f), + 0.25f to Color.Black.copy(alpha = 0.15f), + 0.5f to Color.Black.copy(alpha = 0.5f), + 0.75f to Color.Black.copy(alpha = 0.85f), + 1f to Color.Black, +) + +private val RowLeadingInset = 20.dp +private val MinEmojiWidth = 140.dp +private val EmojiLeadingMargin = 16.dp +private val EmojiTrailingMargin = 20.dp + +@Composable +private fun ReactorRowPlaceholder() { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = RowLeadingInset, vertical = 8.5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(15.dp), + ) { + Box( + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .background(CodeTheme.colors.divider), + ) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) { + Box( + modifier = Modifier + .fillMaxWidth(0.4f) + .height(14.dp) + .clip(RoundedCornerShape(4.dp)) + .background(CodeTheme.colors.divider), + ) + Box( + modifier = Modifier + .fillMaxWidth(0.25f) + .height(11.dp) + .clip(RoundedCornerShape(4.dp)) + .background(CodeTheme.colors.divider), + ) + } + } +} diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ChatBottomBar.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ChatBottomBar.kt index fe0b13faae..b3d7fd2b26 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ChatBottomBar.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ChatBottomBar.kt @@ -50,7 +50,7 @@ import com.flipcash.app.messenger.internal.ChatViewModel import com.flipcash.app.messenger.internal.balanceRequirement import com.flipcash.app.messenger.internal.requiresStaff import com.flipcash.shared.chat.models.ChatActionHandler -import com.flipcash.app.messenger.internal.screens.ChatAnimations +import com.flipcash.shared.chat.ui.ChatAnimations import com.flipcash.services.models.chat.ChatType import com.flipcash.services.models.chat.BlobAccessContext import com.flipcash.features.messenger.R diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt index a8ecbbd21f..43ea29d912 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageList.kt @@ -7,14 +7,14 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListLayoutInfo import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -33,11 +33,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.repeatOnLifecycle import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.itemKey import com.flipcash.app.messenger.internal.ChatViewModel -import com.flipcash.app.messenger.internal.screens.ChatAnimations +import com.flipcash.shared.chat.ui.ChatAnimations import com.flipcash.features.messenger.R import com.flipcash.services.models.chat.ChatType import com.flipcash.services.models.chat.MessagePointer @@ -49,6 +54,7 @@ import com.flipcash.shared.chat.models.LinkCardResolution import com.flipcash.shared.chat.models.LocalChatActionHandler import com.flipcash.shared.chat.models.LocalLinkCardResolution import com.flipcash.shared.chat.models.SeparatorConfig +import com.flipcash.shared.chat.reactions.ReactionRefreshPlanner import com.getcode.theme.CodeTheme import com.getcode.ui.theme.CodeButton import com.getcode.ui.utils.rememberKeyboardController @@ -74,6 +80,7 @@ internal fun MessageList( linkCardResolution: LinkCardResolution, canViewProfile: Boolean, onJumpConsumed: () -> Unit = {}, + topBarBottom: Dp = 0.dp, ) { val keyboard = rememberKeyboardController() val listState = rememberLazyListState() @@ -234,6 +241,55 @@ internal fun MessageList( refreshSettled = true } + // Reaction refresh (decision 6, see ReactionRefreshPlanner): a live overlay override only + // exists for a chat this device is actively watching, so a reaction added while the app was + // backgrounded — or before this chat was ever opened — has to be asked for explicitly. + // + // Two hooks, matching the two ways a message's reaction state can go stale here: + // + // 1. The newest window, on open and every resume. Modeled on ChatViewModel's own + // repeatOnLifecycle(STARTED) pattern (see initLinkCardFreshness) but anchored to this + // screen's own lifecycle owner rather than the process one, because the id list this + // needs only exists in this composition's LazyPagingItems snapshot. Each restart of the + // block re-asks for the same 60 newest ids — refreshing an id twice is a no-op for the + // coordinator, and re-asking is exactly the point: a reader who backgrounded the app is + // the one case an overlay update could have been missed. + // 2. Older pages, as they page in from Room. `messages.loadState.mediator?.append` tells + // apart a page the RemoteMediator just pulled over the network — already current, no + // ask needed — from one the pager served out of the local cache, which may be showing + // reaction state as stale as this device's last sync. The first snapshot this effect + // sees is left unrequested; hook 1 already owns that opening batch. + val lifecycleOwner = LocalLifecycleOwner.current + LaunchedEffect(lifecycleOwner, state.chatId, messages) { + lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { + snapshotFlow { initialLoadComplete }.first { it } + val ids = ReactionRefreshPlanner.initialWindow(messages.newestFirstMessageIds()) + if (ids.isNotEmpty()) onAction(ChatAction.RefreshReactionIds(ids)) + } + } + + LaunchedEffect(state.chatId, messages) { + val refreshedPageIds = mutableSetOf() + var previousIds: Set? = null + snapshotFlow { messages.newestFirstMessageIds().toSet() } + .collect { currentIds -> + val seenBefore = previousIds + previousIds = currentIds + if (seenBefore == null) return@collect + val newIds = (currentIds - seenBefore).toList() + if (newIds.isEmpty()) return@collect + val sourcedFromServer = messages.loadState.mediator?.append is LoadState.Loading + ReactionRefreshPlanner.forLoadedPage( + pageIds = newIds, + sourcedFromServer = sourcedFromServer, + alreadyRefreshed = refreshedPageIds, + ).forEach { chunk -> + onAction(ChatAction.RefreshReactionIds(chunk)) + refreshedPageIds += chunk + } + } + } + LazyColumn( // NB: no sheetResignmentBehavior, unlike every other scrolling list in the app. The // conversation is a full-screen destination, not a sheet, so there is no dismiss drag @@ -306,6 +362,12 @@ internal fun MessageList( focused = focused, animateInsertion = animateInsertion, showsSenderGutter = isGroup, + topBarBottom = topBarBottom, + quickReactionStrip = if (focused && bubble?.messageId == state.selection?.messageId) { + state.quickReactionStrip + } else { + emptyList() + }, attention = if (bubble != null && bubble.messageId == attentionId) { readAttention } else { @@ -468,6 +530,14 @@ internal fun MessageList( private val LazyPagingItems.loadedCount: Int get() = itemSnapshotList.items.size +/** + * Currently loaded message ids, newest first — index 0 is the newest, same order the haptic + * effect above reads [LazyPagingItems.peek] in. Feeds `ReactionRefreshPlanner`, which needs to + * know what "the newest 60" and "a newly loaded page" mean in terms this list already has. + */ +private fun LazyPagingItems.newestFirstMessageIds(): List = + itemSnapshotList.items.mapNotNull { (it as? ChatListItem.ContentBubble)?.messageId } + /** * The first index the pager has nothing for — the hint that drives one more append. * diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageRow.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageRow.kt index 9a9fdb797c..a8ebcefbdd 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageRow.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/MessageRow.kt @@ -5,21 +5,26 @@ import androidx.compose.animation.EnterTransition import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.requiredSize -import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Reply @@ -33,17 +38,24 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.draw.BlurredEdgeTreatment import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.roundToIntRect import androidx.paging.compose.LazyPagingItems -import com.flipcash.app.messenger.internal.screens.ChatAnimations +import com.flipcash.shared.chat.ui.ChatAnimations +import com.flipcash.features.messenger.R import com.flipcash.services.models.chat.BlobAccessContext import com.flipcash.services.models.chat.MessagePointer import com.flipcash.shared.chat.MessageCapability @@ -52,11 +64,13 @@ import com.flipcash.shared.chat.models.ChatListItem import com.flipcash.shared.chat.models.LocalChatActionHandler import com.flipcash.shared.chat.models.ReceiptStatus import com.flipcash.shared.chat.models.SeparatorConfig -import com.flipcash.features.messenger.R +import com.flipcash.shared.chat.reactions.ReactionStrip import com.flipcash.shared.chat.ui.ContentBubble +import com.flipcash.shared.chat.ui.QuickReactionStripPopup +import com.flipcash.shared.chat.ui.ReactionPillRow +import com.flipcash.shared.chat.ui.bubblePositionOf import com.flipcash.shared.chat.ui.rendersBare import com.flipcash.shared.common.ui.ContactAvatar -import com.flipcash.shared.chat.ui.bubblePositionOf import com.getcode.theme.CodeTheme import com.getcode.ui.core.addIf import com.getcode.ui.utils.rememberKeyboardController @@ -86,6 +100,9 @@ internal fun MessageRow( focused: Boolean, animateInsertion: Boolean, showsSenderGutter: Boolean, + quickReactionStrip: List = emptyList(), + /** Where the top bar ends, from the window's top; the strip stays below it. */ + topBarBottom: Dp = 0.dp, attention: () -> Float = { 0f }, ) { val onAction = LocalChatActionHandler.current @@ -154,6 +171,27 @@ internal fun MessageRow( label = "messageLift", ) + // Only the bubble lifts, as iOS lifts only its bubble view out of the cell. Anchored to the + // bubble's own edge, as the insertion animation is, so it grows in place instead of sliding + // inward. + val liftModifier = Modifier.graphicsLayer { + scaleX = lift + scaleY = lift + transformOrigin = if (isOutgoing) TransformOrigin(1f, 0.5f) else TransformOrigin(0f, 0.5f) + } + + // The selected message's own pills stay behind the backdrop with the rest of the transcript, + // so they take the same dim and blur the other rows do. + val pillsBehindBackdrop = selecting && focused + val pillDimAlpha by animateFloatAsState( + targetValue = if (pillsBehindBackdrop) 0.4f else 1f, + label = "pillDim", + ) + val pillDimBlur by animateDpAsState( + targetValue = if (pillsBehindBackdrop) 8.dp else 0.dp, + label = "pillBlur", + ) + val swipe = rememberSwipeToReply( enabled = bubble != null && !selecting && @@ -175,18 +213,7 @@ internal fun MessageRow( // Unbounded: the rectangle treatment would clip the blur at the row's own // edges and leave a hard seam between neighbouring rows. .blur(dimBlur, BlurredEdgeTreatment.Unbounded) - .graphicsLayer { - alpha = dimAlpha - scaleX = lift - scaleY = lift - // Anchored to the bubble's own edge, as the insertion animation is, so - // the lift grows the bubble in place instead of sliding it inward. - transformOrigin = if (isOutgoing) { - TransformOrigin(1f, 0.5f) - } else { - TransformOrigin(0f, 0.5f) - } - } + .graphicsLayer { alpha = dimAlpha } // No row gestures while the backdrop is up: the rows are behind it, and a // press there would move the selection out from under the message the bar — // or the composer — is already acting on. @@ -292,8 +319,19 @@ internal fun MessageRow( modifier = Modifier.weight(1f), horizontalAlignment = if (item.isFromSelf) Alignment.End else Alignment.Start, ) { - Box(insertionModifier) { + Box(insertionModifier.then(liftModifier)) { + val stripShown = selecting && focused && item.canReact && + quickReactionStrip.isNotEmpty() + // The strip lines up with the bubble as drawn, which sits inside + // a full-width layout, so it's measured here rather than taken + // from the popup's anchor. + var bubbleBounds by remember { mutableStateOf(null) } ContentBubble( + modifier = Modifier.addIf(stripShown) { + Modifier.onGloballyPositioned { + bubbleBounds = it.boundsInWindow().roundToIntRect() + } + }, item = item, // The bubble's own targets go with the row's: a cash // bubble behind the backdrop would otherwise open token @@ -311,6 +349,80 @@ internal fun MessageRow( ), attention = attention, ) + + // The quick strip lives above the bubble, only while this exact + // message is the selected one — it replaces the backdrop's own + // reach for a reaction with something faster than opening the + // picker, and disappears the moment selection moves off. + if (stripShown) { + QuickReactionStripPopup( + entries = quickReactionStrip, + bubbleBounds = bubbleBounds, + hugsTrailing = item.isFromSelf, + onToggle = { emoji -> + onAction( + ChatAction.ToggleReaction( + messageId = item.messageId, + emoji = emoji, + fromStrip = true, + ) + ) + }, + // Leave selection first, as iOS dismisses its context + // menu, so the strip doesn't float over the picker. + onOpenPicker = { + onAction(ChatAction.ClearSelection) + onAction(ChatAction.OpenReactionPicker(item.messageId)) + }, + // Clear of the selection bar, measured rather than + // assumed: a guess at its height flipped the strip + // below bubbles that had room above. + minTop = maxOf( + topBarBottom, + WindowInsets.statusBars.asPaddingValues().calculateTopPadding(), + ) + STRIP_BAR_GAP, + ) + } + } + // Same width as the bubble above it (decision 2) — matched here + // against the same fraction MessageBubble sizes a text/reply/deleted + // bubble to, since the row doesn't expose its resolved width outward. + // Kept on screen while selecting, as iOS keeps them under its + // backdrop, but inert like every other target on the row. + // + // Once a message has pills the row stays composed, so the last one can + // animate out; a message that gets its first reaction while on screen + // has its row animate that pill in rather than just appear. + val pillsAtFirstComposition = remember(item.messageId) { item.reactionPills.isNotEmpty() } + val pillRowComposed = remember(item.messageId) { BooleanArray(1) } + if (item.reactionPills.isNotEmpty()) pillRowComposed[0] = true + if (pillRowComposed[0]) { + BoxWithConstraints( + modifier = Modifier + .fillMaxWidth() + .blur(pillDimBlur, BlurredEdgeTreatment.Unbounded) + .graphicsLayer { alpha = pillDimAlpha } + .addIf(selecting) { Modifier.blockPointerInput() }, + ) { + ReactionPillRow( + pills = item.reactionPills, + canReact = item.canReact, + onToggle = { emoji -> + onAction(ChatAction.ToggleReaction(item.messageId, emoji)) + }, + onPillLongClick = { + onAction(ChatAction.OpenReactors(item.messageId)) + }, + onOpenPicker = { + onAction(ChatAction.OpenReactionPicker(item.messageId)) + }, + modifier = Modifier + .align(if (item.isFromSelf) Alignment.TopEnd else Alignment.TopStart) + .width(maxWidth * BUBBLE_ROW_WIDTH_FRACTION), + alignEnd = item.isFromSelf, + animateInitialPills = !pillsAtFirstComposition, + ) + } } val showReceipt = shouldShowReceiptLabel(index, item, messages, otherReadPointer) @@ -427,6 +539,12 @@ private fun senderNameInset(showsGutter: Boolean): Dp = CodeTheme.dimens.grid.x1 } +// Matches MessageBubble's own BUBBLE_MAX_WIDTH_FRACTION for a text/reply/deleted bubble — the row +// doesn't expose its resolved width outward, so the pill row underneath it re-derives the same +// fraction of the shared row width instead. +private const val BUBBLE_ROW_WIDTH_FRACTION = 0.78f +private val STRIP_BAR_GAP = 8.dp + private val AFFORDANCE_SIZE = 32.dp private val AFFORDANCE_INSET = 20.dp private val AFFORDANCE_ICON_INSET = 8.dp @@ -498,3 +616,12 @@ internal fun startsSenderRun(current: ChatListItem.ContentBubble, older: ChatLis val olderBubble = older as? ChatListItem.ContentBubble ?: return true return olderBubble.authorId != author } + +/** Swallows every press before the children see it, so their own targets never fire. */ +private fun Modifier.blockPointerInput(): Modifier = pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + awaitPointerEvent(PointerEventPass.Initial).changes.forEach { it.consume() } + } + } +} diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ReceiptLabel.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ReceiptLabel.kt index fc21283176..82284637ac 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ReceiptLabel.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ReceiptLabel.kt @@ -4,7 +4,7 @@ import android.text.format.DateFormat import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.snap -import com.flipcash.app.messenger.internal.screens.ChatAnimations +import com.flipcash.shared.chat.ui.ChatAnimations import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut diff --git a/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatGroupAnalyticsTest.kt b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatGroupAnalyticsTest.kt index 204e527d77..ffc4aa13ad 100644 --- a/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatGroupAnalyticsTest.kt +++ b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatGroupAnalyticsTest.kt @@ -8,6 +8,7 @@ import com.flipcash.analytics.PropertyValue import com.flipcash.analytics.State as AnalyticsState import com.flipcash.app.analytics.RecordingAnalytics import com.flipcash.app.contacts.ContactCoordinator +import com.flipcash.app.persistence.sources.UserProfileDataSource import com.flipcash.app.core.MainCoroutineRule import com.flipcash.app.funding.PurchaseMethodController import com.flipcash.app.messenger.internal.link.LinkCardClassifier @@ -29,6 +30,8 @@ import com.flipcash.shared.chat.ChatMembership import com.flipcash.shared.chat.GroupAccess import com.flipcash.shared.payments.ContactPaymentDelegate import com.flipcash.shared.payments.TipPaymentDelegate +import com.getcode.libs.emojis.reactions.EmojiCatalogLoader +import com.getcode.libs.emojis.reactions.RecentReactionsStore import com.getcode.manager.BottomBarManager import com.getcode.opencode.controllers.TransactionController import com.getcode.opencode.exchange.Exchange @@ -92,6 +95,9 @@ class ChatGroupAnalyticsTest { private val linkCardResolver = mockk(relaxed = true) private val cashLinkClaims = mockk(relaxed = true) private val chatDraftStore = mockk(relaxed = true) + private val recentReactionsStore = mockk(relaxed = true) + private val emojiCatalogLoader = mockk(relaxed = true) + private val userProfileDataSource = mockk(relaxed = true) private val chatId = ChatId(UUID.randomUUID().bytes) private val mint = Mint("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaaaaaaaaaaa") @@ -141,6 +147,10 @@ class ChatGroupAnalyticsTest { linkCardResolver = linkCardResolver, cashLinkClaims = cashLinkClaims, chatDraftStore = chatDraftStore, + recentReactionsStore = recentReactionsStore, + toastController = mockk(relaxed = true), + emojiCatalogLoader = emojiCatalogLoader, + userProfileDataSource = userProfileDataSource, dispatchers = TestDispatcherProvider(mainCoroutineRule.dispatcher), ) diff --git a/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatMessageActionReducerTest.kt b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatMessageActionReducerTest.kt index 8070348935..8bc3cc2b72 100644 --- a/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatMessageActionReducerTest.kt +++ b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatMessageActionReducerTest.kt @@ -369,4 +369,40 @@ class ChatMessageActionReducerTest { assertNull(state.jumpTarget) assertNull(state.jumpBudget) } + + // ToggleReaction — the reducer itself does nothing to the reaction (that's the coordinator's + // job, run from initMessageActionHandlers); it only decides whether the tap that fired it also + // closes the selection bar, per Event.ToggleReaction.clearsSelection. + + @Test + fun `toggling a reaction from the pill row leaves the selection alone`() { + val target = bubble(1) + val selected = reduce( + ChatViewModel.State(messagePolicy = unbounded), + ChatViewModel.Event.ToggleMessageSelection(target), + ) + + val state = reduce( + selected, + ChatViewModel.Event.ToggleReaction(messageId = 1, emoji = "🔥", clearsSelection = false), + ) + + assertEquals(target, state.selection) + } + + @Test + fun `toggling a reaction from the quick strip clears the selection`() { + val target = bubble(1) + val selected = reduce( + ChatViewModel.State(messagePolicy = unbounded), + ChatViewModel.Event.ToggleMessageSelection(target), + ) + + val state = reduce( + selected, + ChatViewModel.Event.ToggleReaction(messageId = 1, emoji = "🔥", clearsSelection = true), + ) + + assertNull(state.selection) + } } diff --git a/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatSendFailureAnalyticsTest.kt b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatSendFailureAnalyticsTest.kt index 8c63e3cace..5c0d998d51 100644 --- a/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatSendFailureAnalyticsTest.kt +++ b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatSendFailureAnalyticsTest.kt @@ -17,12 +17,15 @@ import com.flipcash.app.tokens.TokenCoordinator import com.flipcash.app.userflags.UserFlagsCoordinator import com.flipcash.libs.coroutines.TestDispatcherProvider import com.flipcash.services.models.UserProfile +import com.flipcash.app.persistence.sources.UserProfileDataSource import com.flipcash.services.models.chat.ChatId import com.flipcash.services.user.UserManager import com.flipcash.shared.chat.ChatCoordinator import com.flipcash.shared.chat.ChatDraftStore import com.flipcash.shared.payments.ContactPaymentDelegate import com.flipcash.shared.payments.TipPaymentDelegate +import com.getcode.libs.emojis.reactions.EmojiCatalogLoader +import com.getcode.libs.emojis.reactions.RecentReactionsStore import com.getcode.manager.BottomBarManager import com.getcode.opencode.controllers.TransactionController import com.getcode.opencode.exchange.Exchange @@ -87,6 +90,9 @@ class ChatSendFailureAnalyticsTest { private val linkCardResolver = mockk(relaxed = true) private val cashLinkClaims = mockk(relaxed = true) private val chatDraftStore = mockk(relaxed = true) + private val recentReactionsStore = mockk(relaxed = true) + private val emojiCatalogLoader = mockk(relaxed = true) + private val userProfileDataSource = mockk(relaxed = true) private val token = mockk(relaxed = true) private val amount = Fiat(5.0) @@ -133,6 +139,10 @@ class ChatSendFailureAnalyticsTest { linkCardResolver = linkCardResolver, cashLinkClaims = cashLinkClaims, chatDraftStore = chatDraftStore, + recentReactionsStore = recentReactionsStore, + toastController = mockk(relaxed = true), + emojiCatalogLoader = emojiCatalogLoader, + userProfileDataSource = userProfileDataSource, dispatchers = TestDispatcherProvider(mainCoroutineRule.dispatcher), ) diff --git a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/models/ChatAction.kt b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/models/ChatAction.kt index 79f76cef51..95a7724b40 100644 --- a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/models/ChatAction.kt +++ b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/models/ChatAction.kt @@ -84,6 +84,31 @@ sealed interface ChatAction { * that boundary — a claim arrives back knowing an entropy and nothing about a transcript. */ data class CashLinkOpened(val entropy: String, val url: String) : ChatAction + + /** + * Toggles the viewer's reaction with [emoji] on [messageId] — a tap on a pill, on a quick-strip + * entry, or a pick from the full picker all resolve to this one action. [fromStrip] is true + * only for the quick strip above a selected bubble, whose tap also exits selection mode. + */ + data class ToggleReaction( + val messageId: Long, + val emoji: String, + val fromStrip: Boolean = false, + ) : ChatAction + + /** Opens the full emoji picker for [messageId], from the pill row's or the quick strip's "+". */ + data class OpenReactionPicker(val messageId: Long) : ChatAction + + /** Opens who-reacted for [messageId], from a long-press on a pill. */ + data class OpenReactors(val messageId: Long) : ChatAction + + /** + * Asks the coordinator to refresh reaction state for [messageIds] — the transcript's own + * refresh-on-paging hook (see `ReactionRefreshPlanner`), not a reader gesture. The transcript + * decides when and which ids; this only carries the request down to the viewmodel that owns + * the coordinator. + */ + data class RefreshReactionIds(val messageIds: List) : ChatAction } typealias ChatActionHandler = (ChatAction) -> Unit diff --git a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/models/ChatListItem.kt b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/models/ChatListItem.kt index dc8bef481d..915b93574a 100644 --- a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/models/ChatListItem.kt +++ b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/models/ChatListItem.kt @@ -2,6 +2,7 @@ package com.flipcash.shared.chat.models import com.flipcash.services.models.chat.MessageContent import com.flipcash.shared.chat.MessageCapability +import com.flipcash.shared.chat.reactions.ReactionPill import com.getcode.opencode.model.core.ID import kotlin.time.Instant @@ -96,6 +97,17 @@ sealed interface ChatListItem { * the receipt go. */ val isLastRow: Boolean = true, + /** + * This message's reaction pills, in display order — the live overlay when one exists, + * otherwise `MessageReactions.from` applied to the message's own stored summary. Empty + * for a message with no reactions. + */ + val reactionPills: List = emptyList(), + /** + * Whether the viewer may react to this message — system messages and messages still + * pending send are excluded. See `com.flipcash.shared.chat.canReact`. + */ + val canReact: Boolean = false, ) : ChatListItem { /** * Who this bubble is attributed to, for grouping. [senderId] is the answer whenever the diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/ChatAnimations.kt b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/ChatAnimations.kt similarity index 68% rename from apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/ChatAnimations.kt rename to apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/ChatAnimations.kt index 7652a2e713..3921cd81bd 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/ChatAnimations.kt +++ b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/ChatAnimations.kt @@ -1,4 +1,4 @@ -package com.flipcash.app.messenger.internal.screens +package com.flipcash.shared.chat.ui import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition @@ -13,9 +13,12 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.ui.Alignment import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.graphics.Color // All chat animation spring specs in one place. -internal object ChatAnimations { +object ChatAnimations { // Message bubble insertion — scale from 0.95 + opacity. // Matches iOS insertionSpring: .spring(duration: 0.23, bounce: 0.27). val insertion: SpringSpec = spring(dampingRatio = 0.73f, stiffness = 746f) @@ -68,4 +71,22 @@ internal object ChatAnimations { // Receipt label exit when a new message is sent — fade out + collapse. private val deliveredIntSize: SpringSpec = spring(dampingRatio = 0.88f, stiffness = 250f) val receiptExit: ExitTransition = shrinkVertically(deliveredIntSize) + fadeOut(delivered) + + // Reaction pills, from iOS ChatMotion's reaction springs and scales. + // A pill arriving — iOS reaction: .spring(duration: 0.32, bounce: 0.35), from 40%. + val reactionEnter: SpringSpec = spring(dampingRatio = 0.65f, stiffness = 386f) + const val reactionEnterScale = 0.4f + // A pill leaving — iOS reactionExit: .spring(duration: 0.2, bounce: 0), to 60%. Quicker than + // its arrival and without overshoot, so it doesn't read as the pill coming back. + val reactionExit: SpringSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = 987f) + const val reactionExitScale = 0.6f + // Pills sliding to make room and the row resizing — iOS reactionReflow, which is insertion. + val reactionReflowOffset: SpringSpec = spring(dampingRatio = 0.73f, stiffness = 746f) + val reactionReflowHeight: SpringSpec = spring(dampingRatio = 0.73f, stiffness = 746f) + // A count or selected state changing in place — iOS reactionChange: .spring(duration: 0.24, bounce: 0). + val reactionChange: SpringSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = 685f) + val reactionChangeColor: SpringSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = 685f) + val reactionChangeDp: SpringSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = 685f) + val reactionChangeOffset: SpringSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = 685f) + val reactionChangeSize: SpringSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = 685f) } diff --git a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/QuickReactionStrip.kt b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/QuickReactionStrip.kt new file mode 100644 index 0000000000..8974cdca14 --- /dev/null +++ b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/QuickReactionStrip.kt @@ -0,0 +1,312 @@ +package com.flipcash.shared.chat.ui + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AddReaction +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.unit.Density +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.selected +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.ui.window.PopupProperties +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import com.flipcash.shared.chat.reactions.ReactionStrip +import androidx.compose.ui.res.stringResource +import com.flipcash.core.R + +// Sizes from the iOS strip (node 9779:105563). +private val StripHeight = 55.dp +private val StripMaxWidth = 313.dp +private val ItemSize = 40.dp +private val ItemSpacing = 4.dp +private val Inset = 8.dp +private val AddSize = 38.dp + +/** How far the fade reaches ahead of the "+" before the row draws fully. */ +private val FadeLead = 28.dp + +/** Gap between one emoji starting to pop in and the next, as in the iOS strip's entrance. */ +private const val ItemStaggerMillis = 18L + +/** Springs matched by eye to the iOS entrance, which settles in about 200ms. */ +private val GrowSpring = spring(dampingRatio = 0.9f, stiffness = 900f) +private val PopSpring = spring(dampingRatio = 0.65f, stiffness = 1200f) + +/** iOS's pre-glass strip surface. */ +private val StripSurface = Color(0xFF303030) + +/** + * The strip a long-press on a reactable, selected bubble shows beside it: up to the 12 entries + * [ReactionStripComposer][com.flipcash.shared.chat.reactions.ReactionStripComposer] composed, + * self-reacted ones highlighted, and a "+" pinned at the trailing end that opens the full picker. + * A tap on an entry both toggles the reaction and clears the selection — [onToggle] is the one + * signal for both. + * + * The capsule grows to fit its entries up to 313dp; past that the emoji scroll under the "+", + * fading out ahead of it. + * + * It enters as iOS's does: the capsule widens out of the side it hugs ([growsFromEnd] for the + * trailing side), the "+" riding its growing edge, and the emoji pop in one after another from + * that side. All of it animates in the draw phase, so the strip's measured size never changes — + * inside a popup, a changing size would resize the window every frame. + */ +@Composable +fun QuickReactionStrip( + entries: List, + onToggle: (emoji: String) -> Unit, + onOpenPicker: () -> Unit, + modifier: Modifier = Modifier, + growsFromEnd: Boolean = false, +) { + if (entries.isEmpty()) return + + val grow = remember { Animatable(0f) } + val pops = remember(entries.size) { List(entries.size) { Animatable(0f) } } + LaunchedEffect(pops) { + launch { grow.animateTo(1f, GrowSpring) } + val order = if (growsFromEnd) pops.asReversed() else pops + order.forEachIndexed { i, pop -> + launch { + delay(i * ItemStaggerMillis) + pop.animateTo(1f, PopSpring) + } + } + } + // How far the capsule's growing edge still has to travel, read in the draw phase only. + fun remaining(fullWidth: Float, minWidth: Float): Float = + (fullWidth - minWidth).coerceAtLeast(0f) * (1f - grow.value).coerceAtLeast(0f) + + val stripWidth = remember { mutableFloatStateOf(0f) } + val rtl = LocalLayoutDirection.current == LayoutDirection.Rtl + + Box( + modifier = modifier + .widthIn(max = StripMaxWidth) + .height(StripHeight) + .onSizeChanged { stripWidth.floatValue = it.width.toFloat() } + .graphicsLayer { + val minWidth = StripHeight.toPx() + shape = RevealCapsule(remaining(size.width, minWidth), growsFromEnd) + clip = true + // Fully opaque a third of the way through the growth, as the iOS capsule is. + alpha = (grow.value * 3f).coerceIn(0f, 1f) + } + .background(StripSurface), + contentAlignment = Alignment.CenterEnd, + ) { + val density = LocalDensity.current + Row( + modifier = Modifier + .fillMaxHeight() + .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } + .drawWithContent { + drawContent() + val width = size.width + val inset = Inset.toPx() + val solidUntil = width - inset - AddSize.toPx() - FadeLead.toPx() + val clearFrom = width - inset - AddSize.toPx() / 2 + drawRect( + brush = Brush.horizontalGradient( + 0f to Color.Transparent, + (inset / width) to Color.Black, + (solidUntil / width).coerceAtLeast(inset / width) to Color.Black, + (clearFrom / width) to Color.Transparent, + 1f to Color.Transparent, + ), + blendMode = BlendMode.DstIn, + ) + } + .testTag("quick_reaction_scroll") + .horizontalScroll(rememberScrollState()) + // Room to bring the last emoji out from under the "+" and its fade. + .padding(start = Inset, end = Inset + AddSize + FadeLead), + horizontalArrangement = Arrangement.spacedBy(ItemSpacing), + verticalAlignment = Alignment.CenterVertically, + ) { + entries.forEachIndexed { index, entry -> + Box( + modifier = Modifier + .graphicsLayer { + val pop = pops[index].value + scaleX = pop + scaleY = pop + alpha = pop.coerceIn(0f, 1f) + } + .testTag("quick_reaction_${entry.emoji}") + .semantics { selected = entry.highlighted } + .size(ItemSize) + .clip(CircleShape) + .background(if (entry.highlighted) Color.White.copy(alpha = 0.18f) else Color.Transparent) + .combinedClickable(onClick = { onToggle(entry.emoji) }, onLongClick = {}), + contentAlignment = Alignment.Center, + ) { + Text(text = entry.emoji, fontSize = with(density) { 28.dp.toSp() }) + } + } + } + Box( + modifier = Modifier + .graphicsLayer { + // Ride the growing edge when that edge is the trailing one; when the capsule + // grows out of the trailing side the "+" is already where it ends up. + if (!growsFromEnd) { + val travel = remaining(stripWidth.floatValue, StripHeight.toPx()) + translationX = if (rtl) travel else -travel + } + } + .padding(end = Inset) + .testTag("quick_reaction_plus") + .semantics { contentDescription = "More reactions" } + .size(AddSize) + .clip(CircleShape) + .background(Color.White.copy(alpha = 0.18f)) + .combinedClickable(onClick = onOpenPicker, onLongClick = {}), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Outlined.AddReaction, + contentDescription = stringResource(R.string.action_addReaction), + tint = Color.White, + modifier = Modifier.size(22.dp), + ) + } + } +} + +/** + * [QuickReactionStrip] in a popup placed against [bubbleBounds], the bubble as drawn in the window + * (lift included), falling back to the layout it sits in until that's measured. Placed the way + * iOS places it: above the bubble with a 16dp gap (below when there's no room under [minTop]), + * hugging the sender's side, and never within 16dp of the window's edges. + */ +@Composable +fun QuickReactionStripPopup( + entries: List, + hugsTrailing: Boolean, + onToggle: (emoji: String) -> Unit, + onOpenPicker: () -> Unit, + minTop: Dp, + bubbleBounds: IntRect? = null, +) { + val density = LocalDensity.current + // Keyed on the bounds so the popup is re-placed as the bubble lifts: the layout it sits in + // is the full-width row, whose edges aren't the bubble's. + val provider = remember(hugsTrailing, minTop, density, bubbleBounds) { + object : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize, + ): IntOffset = with(density) { + QuickReactionStripPlacement.position( + anchor = bubbleBounds ?: anchorBounds, + window = windowSize, + strip = popupContentSize, + hugsTrailing = hugsTrailing, + margin = 16.dp.roundToPx(), + gap = 16.dp.roundToPx(), + minTop = minTop.roundToPx(), + ) + } + } + } + Popup( + popupPositionProvider = provider, + properties = PopupProperties(focusable = false, clippingEnabled = false), + ) { + QuickReactionStrip( + entries = entries, + onToggle = onToggle, + onOpenPicker = onOpenPicker, + growsFromEnd = hugsTrailing, + ) + } +} + +/** + * A capsule over the strip's bounds with [remaining] pixels cut off the growing side: the trailing + * side normally, the leading one when [fromEnd] (the capsule grows out of the trailing side). + */ +private class RevealCapsule( + private val remaining: Float, + private val fromEnd: Boolean, +) : Shape { + override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline { + val growsRight = fromEnd == (layoutDirection == LayoutDirection.Rtl) + val left = if (growsRight) 0f else remaining + val right = if (growsRight) size.width - remaining else size.width + val radius = CornerRadius(size.height / 2f) + return Outline.Rounded(RoundRect(left, 0f, right, size.height, radius)) + } +} + +/** Where [QuickReactionStripPopup] puts the strip, in window pixels. */ +object QuickReactionStripPlacement { + fun position( + anchor: IntRect, + window: IntSize, + strip: IntSize, + hugsTrailing: Boolean, + margin: Int, + gap: Int, + minTop: Int, + ): IntOffset { + val hugged = if (hugsTrailing) { + minOf(anchor.right, window.width - margin) - strip.width + } else { + maxOf(anchor.left, margin) + } + val x = hugged.coerceIn(margin, maxOf(margin, window.width - margin - strip.width)) + val above = anchor.top - gap - strip.height + val y = if (above >= minTop) above else anchor.bottom + gap + return IntOffset(x, y.coerceIn(minTop, maxOf(minTop, window.height - strip.height))) + } +} diff --git a/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/ReactionPillRow.kt b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/ReactionPillRow.kt new file mode 100644 index 0000000000..ed00c35b9b --- /dev/null +++ b/apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/ReactionPillRow.kt @@ -0,0 +1,610 @@ +package com.flipcash.shared.chat.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.AnimationVector2D +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AddReaction +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.node.LayoutModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.selected +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.flipcash.core.R +import com.flipcash.shared.chat.reactions.ReactionPill +import com.getcode.theme.CodeTheme +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * The N-more collapse the pill row uses under a bubble (decision 2): pack pills onto up to + * [maxLines] lines of [containerWidth], and when they don't all fit, show as many as fit + * alongside a trailing "N more" pill (itself sized [moreWidth], resolved by the caller from the + * real count) rather than clipping a pill mid-row. + * + * A pure measure function so the N-more decision is testable without standing up Compose: given + * the same child widths and container width, it always returns the same split. [ReactionPillRow] + * is the only caller — it measures real pill widths, asks this, and lays out accordingly. + */ +object ReactionPillRowLayout { + + data class Result( + /** How many of [pillWidths], from the front, are drawn. */ + val visibleCount: Int, + /** Whether a trailing "N more" pill is needed. */ + val showsMore: Boolean, + /** How many pills the "N more" pill stands in for. Zero when [showsMore] is false. */ + val moreCount: Int, + ) + + /** + * @param pillWidths each reaction pill's measured width, in the same order the row draws them. + * @param moreWidth the "N more" pill's width for the count this call would show. The caller + * only knows that count once this returns, so it re-measures the real chip only when + * [Result.showsMore] comes back true — everything up to then uses an upper-bound estimate + * (a chip sized for the worst case, `pillWidths.size` digits), which never under-reserves. + * @param plusWidth the trailing "+" button's width, or `null` when `canReact` hides it. + * @param expanded true once "N more" has been tapped: every pill draws, unbounded lines, and + * nothing collapses again this composition. + */ + fun compute( + pillWidths: List, + moreWidth: Int, + plusWidth: Int?, + containerWidth: Int, + spacing: Int, + maxLines: Int, + expanded: Boolean, + ): Result { + if (containerWidth <= 0 || pillWidths.isEmpty()) { + return Result(visibleCount = pillWidths.size, showsMore = false, moreCount = 0) + } + + val effectiveMaxLines = if (expanded) Int.MAX_VALUE else maxLines + + if (fits(pillWidths, plusWidth, containerWidth, spacing, effectiveMaxLines)) { + return Result(visibleCount = pillWidths.size, showsMore = false, moreCount = 0) + } + + // Collapsed and everything didn't fit: back off from the full set until what's left, plus + // a trailing "N more" pill (and the "+" beyond it), fits within the line budget. + for (visible in pillWidths.size - 1 downTo 0) { + val widths = pillWidths.subList(0, visible) + moreWidth + if (fits(widths, plusWidth, containerWidth, spacing, effectiveMaxLines)) { + return Result( + visibleCount = visible, + showsMore = true, + moreCount = pillWidths.size - visible, + ) + } + } + return Result(visibleCount = 0, showsMore = true, moreCount = pillWidths.size) + } + + /** + * Whether [widths], followed by [trailingWidth] (the "+" button, when present), packs into + * [maxLines] lines of [containerWidth] with [spacing] between adjacent items on a line — the + * same greedy left-to-right wrap `FlowRow` performs, just run ahead of time on known widths. + */ + private fun fits( + widths: List, + trailingWidth: Int?, + containerWidth: Int, + spacing: Int, + maxLines: Int, + ): Boolean { + var line = 0 + var remaining = containerWidth + var lineHasItem = false + + fun place(width: Int): Boolean { + val needed = width + if (lineHasItem) spacing else 0 + if (needed <= remaining) { + remaining -= needed + lineHasItem = true + return true + } + line++ + if (line >= maxLines) return false + remaining = containerWidth - width + lineHasItem = true + return true + } + + for (width in widths) { + if (!place(width)) return false + } + if (trailingWidth != null && !place(trailingWidth)) return false + return true + } +} + + +/** + * The pill row drawn under a bubble (decision 2): up to two lines of [pills], collapsing into an + * "N more" pill before the trailing "+" when they don't fit, aligned to [alignEnd] (the sender's + * side — end for the viewer's own messages). + * + * A pill tap toggles that reaction; a pill long-press opens the reactors sheet and must not also + * select the message — its own `combinedClickable` handles the long-press so it never reaches the + * bubble's. "N more" expands the row in place — every pill then draws, unbounded lines — and + * stays expanded for the rest of this composition. "+" (hidden when `canReact` is false) opens + * the reaction picker. With no [pills] the row takes no space: the long-press strip is how a + * first reaction gets added, as on iOS. + * + * Changes animate as iOS's `ReactionPillRowView` does: a new pill grows in from 40% on a bouncy + * spring, a removed one shrinks to 60% and fades where it stood while the rest slide over, a count + * rolls up or down, and the row's height follows. The pills showing when the row first composes + * appear without animation, unless [animateInitialPills] says they're new — the caller passes it + * for a row that composes because its message just got its first reaction. + */ +@Composable +fun ReactionPillRow( + pills: List, + canReact: Boolean, + onToggle: (emoji: String) -> Unit, + onPillLongClick: () -> Unit, + onOpenPicker: () -> Unit, + modifier: Modifier = Modifier, + alignEnd: Boolean = false, + maxLines: Int = 2, + animateInitialPills: Boolean = false, +) { + var expanded by remember { mutableStateOf(false) } + + val scope = rememberCoroutineScope() + val motion = remember { PillRowMotion(if (animateInitialPills) emptyList() else pills, canReact) } + val plusTarget = canReact && pills.isNotEmpty() + val rendered = remember(pills, plusTarget, motion.exitsFinished) { motion.update(pills, plusTarget) } + + val pillHeight = 28.dp + val pillPadding = 10.dp + val spacingDp = 6.dp + val plusSize = 28.dp + val topGap = 4.dp + + SubcomposeLayout(modifier = modifier.animateHeightUnclipped()) { constraints -> + val loose = constraints.copy(minWidth = 0, minHeight = 0) + val spacingPx = spacingDp.roundToPx() + val topGapPx = topGap.roundToPx() + val containerWidth = constraints.maxWidth + + val pillPlaceables = subcompose("pills") { + rendered.pills.forEach { item -> + key(item.pill.emoji) { + EnterExit( + entering = item.entering, + exiting = item.exiting, + onExited = { motion.exited(item.pill.emoji) }, + ) { motionModifier -> + ReactionPillChip( + pill = item.pill, + height = pillHeight, + horizontalPadding = pillPadding, + // A previewer (canReact == false) can still open the reactors sheet + // from a long-press, but a tap must not toggle a reaction they're not + // allowed to make. A leaving pill takes neither. + onClick = if (canReact && !item.exiting) ({ onToggle(item.pill.emoji) }) else null, + onLongClick = if (item.exiting) ({}) else onPillLongClick, + modifier = motionModifier, + ) + } + } + } + }.map { it.measure(loose) } + + val plusPlaceable = rendered.plus?.let { plus -> + subcompose("plus") { + EnterExit( + entering = plus.entering, + exiting = plus.exiting, + onExited = { motion.exited(PLUS_KEY) }, + ) { motionModifier -> + PlusChip( + size = plusSize, + onClick = if (plus.exiting) ({}) else onOpenPicker, + modifier = motionModifier, + ) + } + }.first().measure(loose) + } + + // Leaving items keep the spot they held and take no room, so the rest reflow around them. + val flowIndices = rendered.pills.indices.filter { !rendered.pills[it].exiting } + val flowPlus = plusPlaceable?.takeIf { rendered.plus?.exiting == false } + + // Upper-bound estimate: a "more" chip sized for showing every pill, never narrower than + // the real chip this could resolve to. + val moreEstimate = subcompose("more-estimate") { + MorePillChip(count = flowIndices.size, height = pillHeight, horizontalPadding = pillPadding) + }.first().measure(loose) + + val result = ReactionPillRowLayout.compute( + pillWidths = flowIndices.map { pillPlaceables[it].width }, + moreWidth = moreEstimate.width, + plusWidth = flowPlus?.width, + containerWidth = containerWidth, + spacing = spacingPx, + maxLines = maxLines, + expanded = expanded, + ) + + val morePlaceable = if (result.showsMore) { + subcompose("more") { + MorePillChip( + count = result.moreCount, + height = pillHeight, + horizontalPadding = pillPadding, + onClick = { expanded = true }, + ) + }.first().measure(loose) + } else null + + // Greedy wrap identical to ReactionPillRowLayout.fits, now placing real placeables: first + // assign each item an (x, line) within its line, tracking each line's total width and + // height, then place with a per-line offset so an end-aligned row hugs the container's + // trailing edge rather than its leading one. + class Item(val key: String, val placeable: Placeable) + class Placement(val item: Item, val x: Int, val line: Int) + + val items = flowIndices.take(result.visibleCount).map { Item(rendered.pills[it].pill.emoji, pillPlaceables[it]) } + + listOfNotNull(morePlaceable?.let { Item(MORE_KEY, it) }, flowPlus?.let { Item(PLUS_KEY, it) }) + + val placements = ArrayList(items.size) + val lineWidths = ArrayList() + val lineHeights = ArrayList() + var x = 0 + var line = 0 + var lineHasItem = false + var lineHeight = 0 + for (item in items) { + val needed = item.placeable.width + if (lineHasItem) spacingPx else 0 + if (lineHasItem && x + needed > containerWidth) { + lineWidths.add(x) + lineHeights.add(lineHeight) + line++ + x = 0 + lineHeight = 0 + lineHasItem = false + } + if (lineHasItem) x += spacingPx + placements.add(Placement(item, x, line)) + x += item.placeable.width + lineHeight = maxOf(lineHeight, item.placeable.height) + lineHasItem = true + } + lineWidths.add(x) + lineHeights.add(lineHeight) + + val lineY = IntArray(lineHeights.size) + for (i in 1 until lineHeights.size) lineY[i] = lineY[i - 1] + lineHeights[i - 1] + spacingPx + // No pills in the flow means no row: not even the gap above it. + val totalHeight = if (items.isEmpty()) 0 else topGapPx + lineY.last() + lineHeights.last() + + val leaving = rendered.pills.mapIndexedNotNull { index, item -> + if (item.exiting) Item(item.pill.emoji, pillPlaceables[index]) else null + } + listOfNotNull(plusPlaceable?.takeIf { rendered.plus?.exiting == true }?.let { Item(PLUS_KEY, it) }) + + layout(containerWidth, totalHeight) { + val placed = HashSet() + for (placement in placements) { + val lineOffset = if (alignEnd) containerWidth - lineWidths[placement.line] else 0 + val target = IntOffset(placement.x + lineOffset, topGapPx + lineY[placement.line]) + placement.item.placeable.placeRelative(motion.slide(placement.item.key, target, scope)) + placed += placement.item.key + } + for (item in leaving) { + val at = motion.lastPosition(item.key) ?: continue + item.placeable.placeRelative(at) + placed += item.key + } + motion.placed(placed) + } + } +} + +private const val MORE_KEY = "\u0000more" +private const val PLUS_KEY = "\u0000plus" + +private class RenderedPill(val pill: ReactionPill, val entering: Boolean, val exiting: Boolean) +private class RenderedPlus(val entering: Boolean, val exiting: Boolean) +private class RenderedRow(val pills: List, val plus: RenderedPlus?) + +/** + * What the row draws across updates: the current pills plus any still animating out, each at the + * index it last held, and where every item was last placed so a leaving one can fade in place and + * the rest can slide from where they were. Plain fields, not state — they're written as the row + * composes and places, and only [exitsFinished] needs to trigger anything. + */ +private class PillRowMotion(initialPills: List, canReact: Boolean) { + private var shown: List = initialPills.map { RenderedPill(it, entering = false, exiting = false) } + private var plusShown: Boolean = canReact && initialPills.isNotEmpty() + private var plusLeaving: Boolean = false + private val finished = HashSet() + private var placedLastPass: Set = emptySet() + private val offsets = HashMap>() + + var exitsFinished by mutableIntStateOf(0) + private set + + fun update(pills: List, plusTarget: Boolean): RenderedRow { + val current = pills.mapTo(HashSet()) { it.emoji } + val before = shown.filter { !it.exiting }.mapTo(HashSet()) { it.pill.emoji } + val next = pills.mapTo(ArrayList()) { RenderedPill(it, entering = it.emoji !in before, exiting = false) } + shown.forEachIndexed { index, old -> + val emoji = old.pill.emoji + // Only a pill that was actually on screen has somewhere to fade out from; one folded + // into "N more" just goes. + if (emoji in current || emoji in finished || emoji !in placedLastPass) return@forEachIndexed + next.add(index.coerceAtMost(next.size), RenderedPill(old.pill, entering = false, exiting = true)) + } + finished.removeAll(current) + shown = next + + val plus = when { + plusTarget -> RenderedPlus(entering = !plusShown || plusLeaving, exiting = false) + .also { plusShown = true; plusLeaving = false } + plusShown && PLUS_KEY !in finished && PLUS_KEY in placedLastPass -> RenderedPlus(entering = false, exiting = true) + .also { plusLeaving = true } + else -> null.also { plusShown = false; plusLeaving = false; finished.remove(PLUS_KEY) } + } + return RenderedRow(next, plus) + } + + fun exited(key: String) { + finished += key + offsets.remove(key) + exitsFinished++ + } + + /** Where [key] should draw this frame on its way to [target]; a new key starts there. */ + fun slide(key: String, target: IntOffset, scope: CoroutineScope): IntOffset { + val offset = offsets.getOrPut(key) { Animatable(target, IntOffset.VectorConverter) } + if (offset.targetValue != target) scope.launch { offset.animateTo(target, ChatAnimations.reactionReflowOffset) } + return offset.value + } + + fun lastPosition(key: String): IntOffset? = offsets[key]?.value + + fun placed(keys: Set) { + placedLastPass = keys + offsets.keys.retainAll(keys) + } +} + +/** + * Scales and fades [content] in when [entering] (from 40%, overshooting on the arrival spring) and + * out when [exiting] (to 60%), calling [onExited] once it's gone. A return mid-exit springs back. + */ +@Composable +private fun EnterExit( + entering: Boolean, + exiting: Boolean, + onExited: () -> Unit, + content: @Composable (Modifier) -> Unit, +) { + val progress = remember { Animatable(if (entering) 0f else 1f) } + val currentOnExited by rememberUpdatedState(onExited) + LaunchedEffect(exiting) { + if (exiting) { + progress.animateTo(0f, ChatAnimations.reactionExit) + currentOnExited() + } else { + progress.animateTo(1f, ChatAnimations.reactionEnter) + } + } + content( + Modifier.graphicsLayer { + val p = progress.value + val from = if (exiting) ChatAnimations.reactionExitScale else ChatAnimations.reactionEnterScale + val scale = from + (1f - from) * p + scaleX = scale + scaleY = scale + alpha = p.coerceIn(0f, 1f) + }, + ) +} + +/** + * Animates the height this reports on the reflow spring, without the clip `animateContentSize` + * adds — a pill overshooting on arrival, or fading out below a shrinking row, draws past the + * bounds. The first measure (and the first after lazy-list reuse) takes its height as is. + */ +private fun Modifier.animateHeightUnclipped(): Modifier = this then AnimateHeightElement + +private data object AnimateHeightElement : ModifierNodeElement() { + override fun create() = AnimateHeightNode() + override fun update(node: AnimateHeightNode) = Unit +} + +private class AnimateHeightNode : Modifier.Node(), LayoutModifierNode { + private var height: Animatable? = null + + override fun onReset() { + height = null + } + + override fun onDetach() { + height = null + } + + override fun MeasureScope.measure(measurable: Measurable, constraints: Constraints): MeasureResult { + val placeable = measurable.measure(constraints) + val target = placeable.height + val animated = height ?: Animatable(target, Int.VectorConverter).also { height = it } + if (animated.targetValue != target) coroutineScope.launch { animated.animateTo(target, ChatAnimations.reactionReflowHeight) } + // The spring overshoots, and a row collapsing to nothing would dip below zero. + return layout(placeable.width, animated.value.coerceAtLeast(0)) { placeable.place(0, 0) } + } +} + +@Composable +private fun ReactionPillChip( + pill: ReactionPill, + height: Dp, + horizontalPadding: Dp, + onClick: (() -> Unit)?, + onLongClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val shape = RoundedCornerShape(percent = 50) + val fill by animateColorAsState( + targetValue = Color.White.copy(alpha = if (pill.selfReacted) 0.12f else 0.06f), + animationSpec = ChatAnimations.reactionChangeColor, + label = "pill fill", + ) + val borderWidth by animateDpAsState( + targetValue = if (pill.selfReacted) CodeTheme.dimens.thickBorder else 0.dp, + animationSpec = ChatAnimations.reactionChangeDp, + label = "pill border", + ) + Box( + modifier = modifier + .testTag("reaction_pill_${pill.emoji}") + .semantics { selected = pill.selfReacted } + // Clipped first so the press ripple stays inside the capsule. + .clip(shape) + .background(color = fill, shape = shape) + .let { base -> + // A zero-width border would draw as a hairline, so none at all until it grows. + if (borderWidth > 0.dp) { + base.border(width = borderWidth, color = Color.White.copy(alpha = 0.24f), shape = shape) + } else { + base + } + } + .combinedClickable(onClick = onClick ?: {}, onLongClick = onLongClick) + .height(height) + .padding(horizontal = horizontalPadding), + contentAlignment = Alignment.Center, + ) { + // iOS's pill: a 15pt emoji and a 13pt medium count in textMain, 4pt apart. + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = pill.emoji, fontSize = 15.sp) + // The count rolls to its next value like a counter turning: up for a rise, down for a + // fall. + AnimatedContent( + targetState = pill.count, + transitionSpec = { + val direction = if (targetState > initialState) 1 else -1 + (slideInVertically(ChatAnimations.reactionChangeOffset) { it * direction } + fadeIn(ChatAnimations.reactionChange)) + .togetherWith(slideOutVertically(ChatAnimations.reactionChangeOffset) { -it * direction } + fadeOut(ChatAnimations.reactionChange)) + .using(SizeTransform(clip = true) { _, _ -> ChatAnimations.reactionChangeSize }) + }, + label = "pill count", + ) { count -> + Text( + text = count.toString(), + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = CodeTheme.colors.textMain, + ) + } + } + } +} + +@Composable +private fun MorePillChip( + count: Int, + height: Dp, + horizontalPadding: Dp, + onClick: (() -> Unit)? = null, +) { + val shape = RoundedCornerShape(percent = 50) + Box( + modifier = Modifier + // Only the real "more" chip is tagged: the width-estimate slot SubcomposeLayout keeps + // composed (to size the collapse ahead of the real decision) would otherwise carry the + // same tag into the semantics tree even though it's never placed. + .let { base -> if (onClick != null) base.testTag("reaction_pill_more") else base } + .clip(shape) + .background(color = Color.White.copy(alpha = 0.06f), shape = shape) + .let { base -> onClick?.let { base.combinedClickable(onClick = it, onLongClick = {}) } ?: base } + .height(height) + .padding(horizontal = horizontalPadding), + contentAlignment = Alignment.Center, + ) { + Text( + text = "$count more", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = CodeTheme.colors.textMain, + ) + } +} + +@Composable +private fun PlusChip(size: Dp, onClick: () -> Unit, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .testTag("reaction_pill_plus") + .size(size) + .clip(CircleShape) + .background(color = Color.White.copy(alpha = 0.18f), shape = CircleShape) + .combinedClickable(onClick = onClick, onLongClick = {}), + contentAlignment = Alignment.Center, + ) { + // The strip's add-reaction icon, as iOS reuses its strip asset here. + Icon( + imageVector = Icons.Outlined.AddReaction, + contentDescription = stringResource(R.string.action_addReaction), + tint = CodeTheme.colors.textMain, + modifier = Modifier.size(20.dp), + ) + } +} diff --git a/apps/flipcash/shared/chat-ui/src/test/kotlin/com/flipcash/shared/chat/ui/QuickReactionStripPlacementTest.kt b/apps/flipcash/shared/chat-ui/src/test/kotlin/com/flipcash/shared/chat/ui/QuickReactionStripPlacementTest.kt new file mode 100644 index 0000000000..9562c85b6e --- /dev/null +++ b/apps/flipcash/shared/chat-ui/src/test/kotlin/com/flipcash/shared/chat/ui/QuickReactionStripPlacementTest.kt @@ -0,0 +1,69 @@ +package com.flipcash.shared.chat.ui + +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import org.junit.Test +import kotlin.test.assertEquals + +/** [QuickReactionStripPlacement]: iOS's strip placement, beside the long-pressed bubble. */ +class QuickReactionStripPlacementTest { + + private val window = IntSize(1000, 2000) + private val strip = IntSize(600, 100) + + private fun place(anchor: IntRect, hugsTrailing: Boolean, strip: IntSize = this.strip) = + QuickReactionStripPlacement.position( + anchor = anchor, + window = window, + strip = strip, + hugsTrailing = hugsTrailing, + margin = 40, + gap = 40, + minTop = 300, + ) + + @Test + fun `sits above the bubble, leading edges aligned, for an incoming message`() { + assertEquals( + IntOffset(120, 1500 - 40 - 100), + place(IntRect(left = 120, top = 1500, right = 500, bottom = 1600), hugsTrailing = false), + ) + } + + @Test + fun `hugs the trailing edge of an outgoing bubble`() { + assertEquals( + IntOffset(960 - 600, 1360), + place(IntRect(left = 500, top = 1500, right = 960, bottom = 1600), hugsTrailing = true), + ) + } + + @Test + fun `keeps the window margin when the bubble runs to the edge`() { + assertEquals( + IntOffset(40, 1360), + place(IntRect(left = 0, top = 1500, right = 400, bottom = 1600), hugsTrailing = false), + ) + assertEquals( + IntOffset(1000 - 40 - 600, 1360), + place(IntRect(left = 500, top = 1500, right = 1000, bottom = 1600), hugsTrailing = true), + ) + } + + @Test + fun `an incoming strip that would overflow the trailing margin slides back in`() { + assertEquals( + IntOffset(1000 - 40 - 600, 1360), + place(IntRect(left = 700, top = 1500, right = 900, bottom = 1600), hugsTrailing = false), + ) + } + + @Test + fun `goes below the bubble when there is no room above`() { + assertEquals( + IntOffset(120, 600 + 40), + place(IntRect(left = 120, top = 350, right = 500, bottom = 600), hugsTrailing = false), + ) + } +} diff --git a/apps/flipcash/shared/chat-ui/src/test/kotlin/com/flipcash/shared/chat/ui/QuickReactionStripTest.kt b/apps/flipcash/shared/chat-ui/src/test/kotlin/com/flipcash/shared/chat/ui/QuickReactionStripTest.kt new file mode 100644 index 0000000000..dffbc1f1e3 --- /dev/null +++ b/apps/flipcash/shared/chat-ui/src/test/kotlin/com/flipcash/shared/chat/ui/QuickReactionStripTest.kt @@ -0,0 +1,105 @@ +package com.flipcash.shared.chat.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.width +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.assertIsNotSelected +import androidx.compose.ui.test.assertIsSelected +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToNode +import androidx.compose.ui.unit.dp +import com.flipcash.shared.chat.reactions.ReactionStrip +import com.getcode.theme.DesignSystem +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertEquals + +/** + * [QuickReactionStrip]: the horizontally-scrolling strip a long-press on a reactable, selected + * bubble shows (decision 5). Every entry a caller hands it renders, self-reacted ones read as + * selected, and a tap is the one signal that both toggles and (from the caller's side) clears the + * selection. + */ +@RunWith(RobolectricTestRunner::class) +class QuickReactionStripTest { + + @get:Rule + val composeTestRule = createComposeRule() + + private fun entry(emoji: String, highlighted: Boolean = false) = + ReactionStrip.Entry(emoji = emoji, highlighted = highlighted) + + private fun setStrip( + entries: List, + width: androidx.compose.ui.unit.Dp = 200.dp, + onToggle: (String) -> Unit = {}, + onOpenPicker: () -> Unit = {}, + ) { + composeTestRule.setContent { + DesignSystem { + Box(Modifier.width(width)) { + QuickReactionStrip( + entries = entries, + onToggle = onToggle, + onOpenPicker = onOpenPicker, + ) + } + } + } + } + + @Test + fun `every entry renders`() { + val entries = listOf(entry("❤️"), entry("👍"), entry("😂")) + setStrip(entries = entries, width = 2000.dp) + + entries.forEach { composeTestRule.onNodeWithTag("quick_reaction_${it.emoji}").assertExists() } + } + + @Test + fun `self-reacted entries are marked selected`() { + setStrip(entries = listOf(entry("❤️", highlighted = true), entry("👍", highlighted = false))) + + composeTestRule.onNodeWithTag("quick_reaction_❤️").assertIsSelected() + composeTestRule.onNodeWithTag("quick_reaction_👍").assertIsNotSelected() + } + + @Test + fun `tapping an entry toggles that emoji`() { + val toggled = mutableListOf() + setStrip(entries = listOf(entry("❤️"), entry("👍")), onToggle = { toggled += it }) + + composeTestRule.onNodeWithTag("quick_reaction_👍").performClick() + + composeTestRule.runOnIdle { assertEquals(listOf("👍"), toggled) } + } + + @Test + fun `plus opens the picker`() { + var opened = 0 + setStrip(entries = listOf(entry("❤️")), onOpenPicker = { opened++ }) + + composeTestRule.onNodeWithTag("quick_reaction_plus").performClick() + + composeTestRule.runOnIdle { assertEquals(1, opened) } + } + + @Test + fun `overflowing entries are reachable by horizontal scroll`() { + // The strip doesn't dedupe entries, so 29 identical filler emoji plus one distinguishable + // target at the end is enough to force overflow at a narrow width and give the test + // something unique to scroll to. + val entries = List(29) { entry("😀") } + entry("🔥") + setStrip(entries = entries, width = 120.dp) + + composeTestRule.onRoot().performScrollToNode(hasTestTag("quick_reaction_🔥")) + + composeTestRule.onNodeWithTag("quick_reaction_🔥").assertExists() + } +} diff --git a/apps/flipcash/shared/chat-ui/src/test/kotlin/com/flipcash/shared/chat/ui/ReactionPillRowLayoutTest.kt b/apps/flipcash/shared/chat-ui/src/test/kotlin/com/flipcash/shared/chat/ui/ReactionPillRowLayoutTest.kt new file mode 100644 index 0000000000..1daa69e9ab --- /dev/null +++ b/apps/flipcash/shared/chat-ui/src/test/kotlin/com/flipcash/shared/chat/ui/ReactionPillRowLayoutTest.kt @@ -0,0 +1,108 @@ +package com.flipcash.shared.chat.ui + +import org.junit.Test +import kotlin.test.assertEquals + +/** + * [ReactionPillRowLayout.compute]'s N-more collapse: pills wrap onto up to two lines, and when + * they don't all fit, the row shows as many as fit alongside a trailing "N more" pill rather than + * clipping one mid-row. + */ +class ReactionPillRowLayoutTest { + + private val spacing = 6 + + @Test + fun `everything fits on one line shows no more pill`() { + val result = ReactionPillRowLayout.compute( + pillWidths = listOf(40, 40, 40), + moreWidth = 60, + plusWidth = 28, + containerWidth = 300, + spacing = spacing, + maxLines = 2, + expanded = false, + ) + assertEquals(ReactionPillRowLayout.Result(visibleCount = 3, showsMore = false, moreCount = 0), result) + } + + @Test + fun `pills spilling past two lines collapse into a more pill`() { + // Six 60-wide pills at spacing 6 need three lines of 100 width; two lines fit two pills each. + val result = ReactionPillRowLayout.compute( + pillWidths = List(6) { 60 }, + moreWidth = 50, + plusWidth = 28, + containerWidth = 130, + spacing = spacing, + maxLines = 2, + expanded = false, + ) + // Two lines of 130 fit two 60-wide pills each (60, then 60+6=66 more) with 4px to spare — + // not enough for a third 60-wide pill (66 > 4), so line one holds one pill's worth of + // room short of two; the reference math above is why the collapse lands at exactly 2 kept + // pills, 4 folded into "N more". + assertEquals(ReactionPillRowLayout.Result(visibleCount = 2, showsMore = true, moreCount = 4), result) + } + + @Test + fun `expanded ignores the line limit and shows every pill`() { + val result = ReactionPillRowLayout.compute( + pillWidths = List(20) { 60 }, + moreWidth = 50, + plusWidth = 28, + containerWidth = 130, + spacing = spacing, + maxLines = 2, + expanded = true, + ) + assertEquals(ReactionPillRowLayout.Result(visibleCount = 20, showsMore = false, moreCount = 0), result) + } + + @Test + fun `no plus button still collapses correctly`() { + val result = ReactionPillRowLayout.compute( + pillWidths = List(6) { 60 }, + moreWidth = 50, + plusWidth = null, + containerWidth = 130, + spacing = spacing, + maxLines = 2, + expanded = false, + ) + assertEquals(true, result.showsMore) + assertEquals(6, result.visibleCount + result.moreCount) + } + + @Test + fun `empty pills show nothing`() { + val result = ReactionPillRowLayout.compute( + pillWidths = emptyList(), + moreWidth = 50, + plusWidth = 28, + containerWidth = 130, + spacing = spacing, + maxLines = 2, + expanded = false, + ) + assertEquals(ReactionPillRowLayout.Result(visibleCount = 0, showsMore = false, moreCount = 0), result) + } + + @Test + fun `single pill wider than the container still shows alone`() { + val result = ReactionPillRowLayout.compute( + pillWidths = listOf(500), + moreWidth = 50, + plusWidth = 28, + containerWidth = 130, + spacing = spacing, + maxLines = 2, + expanded = false, + ) + // The pill alone already leaves no room for the trailing "+" within two lines, so it + // collapses to a lone "more" pill that does have room for the "+" beside it. + assertEquals(1, result.visibleCount + result.moreCount) + assertEquals(0, result.visibleCount) + assertEquals(true, result.showsMore) + } +} diff --git a/apps/flipcash/shared/chat-ui/src/test/kotlin/com/flipcash/shared/chat/ui/ReactionPillRowTest.kt b/apps/flipcash/shared/chat-ui/src/test/kotlin/com/flipcash/shared/chat/ui/ReactionPillRowTest.kt new file mode 100644 index 0000000000..90755bbfa7 --- /dev/null +++ b/apps/flipcash/shared/chat-ui/src/test/kotlin/com/flipcash/shared/chat/ui/ReactionPillRowTest.kt @@ -0,0 +1,249 @@ +package com.flipcash.shared.chat.ui + +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.width +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.runtime.setValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.getValue +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.assertIsNotSelected +import androidx.compose.ui.test.assertIsSelected +import androidx.compose.ui.test.assertHeightIsEqualTo +import androidx.compose.ui.test.assertWidthIsEqualTo +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.longClick +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.unit.dp +import com.flipcash.shared.chat.reactions.ReactionPill +import com.getcode.theme.DesignSystem +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * [ReactionPillRow]: the pill row a bubble draws its reaction summary in. Behavior, not pixels — + * callback wiring, the previewer's read-only contract (decision 2, #17), and the "N more" collapse + * an narrow bubble forces (the layout math itself is [ReactionPillRowLayoutTest]'s job). + */ +@RunWith(RobolectricTestRunner::class) +class ReactionPillRowTest { + + @get:Rule + val composeTestRule = createComposeRule() + + private fun pill( + emoji: String, + count: Long = 1, + selfReacted: Boolean = false, + pending: Boolean = false, + ) = ReactionPill(emoji = emoji, count = count, selfReacted = selfReacted, pending = pending) + + private fun setRow( + pills: List, + canReact: Boolean = true, + width: androidx.compose.ui.unit.Dp = 400.dp, + onToggle: (String) -> Unit = {}, + onPillLongClick: () -> Unit = {}, + onOpenPicker: () -> Unit = {}, + ) { + composeTestRule.setContent { + DesignSystem { + Box(Modifier.width(width)) { + ReactionPillRow( + pills = pills, + canReact = canReact, + onToggle = onToggle, + onPillLongClick = onPillLongClick, + onOpenPicker = onOpenPicker, + ) + } + } + } + } + + @Test + fun `a message with no reactions draws no row and no plus, matching iOS`() { + setRow(pills = emptyList(), canReact = true) + + composeTestRule.onNodeWithTag("reaction_pill_plus").assertDoesNotExist() + } + + @Test + fun `a removed pill stays on screen while it animates out, then goes`() { + var pills by mutableStateOf(listOf(pill("😀"), pill("🎉"))) + composeTestRule.mainClock.autoAdvance = false + composeTestRule.setContent { + DesignSystem { + Box(Modifier.width(400.dp)) { + ReactionPillRow(pills = pills, canReact = true, onToggle = {}, onPillLongClick = {}, onOpenPicker = {}) + } + } + } + composeTestRule.mainClock.advanceTimeByFrame() + composeTestRule.onNodeWithTag("reaction_pill_🎉").assertIsDisplayed() + + pills = listOf(pill("😀")) + composeTestRule.mainClock.advanceTimeByFrame() + composeTestRule.onNodeWithTag("reaction_pill_🎉").assertExists() + + composeTestRule.mainClock.advanceTimeBy(1_000) + composeTestRule.onNodeWithTag("reaction_pill_🎉").assertDoesNotExist() + composeTestRule.onNodeWithTag("reaction_pill_😀").assertIsDisplayed() + } + + @Test + fun `losing the last pill collapses the row to no height`() { + var pills by mutableStateOf(listOf(pill("😀"))) + composeTestRule.setContent { + DesignSystem { + Box(Modifier.width(400.dp).testTag("container")) { + ReactionPillRow(pills = pills, canReact = true, onToggle = {}, onPillLongClick = {}, onOpenPicker = {}) + } + } + } + composeTestRule.onNodeWithTag("container").assertHeightIsEqualTo(32.dp) + + pills = emptyList() + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("container").assertHeightIsEqualTo(0.dp) + composeTestRule.onNodeWithTag("reaction_pill_😀").assertDoesNotExist() + composeTestRule.onNodeWithTag("reaction_pill_plus").assertDoesNotExist() + } + + @Test + fun `plus is a 28dp circle`() { + setRow(pills = listOf(pill("😀"))) + + composeTestRule.onNodeWithTag("reaction_pill_plus") + .assertWidthIsEqualTo(28.dp) + .assertHeightIsEqualTo(28.dp) + } + + @Test + fun `pills and the N more pill are 28dp tall, level with plus`() { + val pills = listOf( + pill("😀"), pill("😁"), pill("😂"), pill("😃"), + pill("😄"), pill("😅"), pill("😆"), pill("😇"), + ) + setRow(pills = pills, width = 90.dp) + + composeTestRule.onNodeWithTag("reaction_pill_😀").assertHeightIsEqualTo(28.dp) + composeTestRule.onNodeWithTag("reaction_pill_more").assertHeightIsEqualTo(28.dp) + } + + @Test + fun `tapping a pill toggles that emoji`() { + val toggled = mutableListOf() + setRow(pills = listOf(pill("😀"), pill("😂")), onToggle = { toggled += it }) + + composeTestRule.onNodeWithTag("reaction_pill_😂").performClick() + + composeTestRule.runOnIdle { assertEquals(listOf("😂"), toggled) } + } + + @Test + fun `long-pressing a pill only opens reactors, and does not reach a parent long-press`() { + val toggled = mutableListOf() + var reactorsOpened = 0 + var parentLongPresses = 0 + + composeTestRule.setContent { + DesignSystem { + Box( + Modifier + .width(400.dp) + .combinedClickable(onClick = {}, onLongClick = { parentLongPresses++ }), + ) { + ReactionPillRow( + pills = listOf(pill("😀"), pill("😂")), + canReact = true, + onToggle = { toggled += it }, + onPillLongClick = { reactorsOpened++ }, + onOpenPicker = {}, + ) + } + } + } + + composeTestRule.onNodeWithTag("reaction_pill_😂").performTouchInput { longClick() } + + composeTestRule.runOnIdle { + assertEquals(1, reactorsOpened) + assertEquals(emptyList(), toggled) + assertEquals(0, parentLongPresses) + } + } + + @Test + fun `plus opens the picker`() { + var opened = 0 + setRow(pills = listOf(pill("😀")), onOpenPicker = { opened++ }) + + composeTestRule.onNodeWithTag("reaction_pill_plus").performClick() + + composeTestRule.runOnIdle { assertEquals(1, opened) } + } + + @Test + fun `a previewer sees pills and can open reactors, but has no plus and taps do nothing`() { + val toggled = mutableListOf() + var reactorsOpened = 0 + setRow( + pills = listOf(pill("😀")), + canReact = false, + onToggle = { toggled += it }, + onPillLongClick = { reactorsOpened++ }, + ) + + composeTestRule.onNodeWithTag("reaction_pill_plus").assertDoesNotExist() + + composeTestRule.onNodeWithTag("reaction_pill_😀").performClick() + composeTestRule.onNodeWithTag("reaction_pill_😀").performTouchInput { longClick() } + + composeTestRule.runOnIdle { + assertTrue(toggled.isEmpty()) + assertEquals(1, reactorsOpened) + } + } + + @Test + fun `a selected pill is exposed as selected in semantics`() { + setRow(pills = listOf(pill("😀", selfReacted = true), pill("😂", selfReacted = false))) + + composeTestRule.onNodeWithTag("reaction_pill_😀").assertIsSelected() + composeTestRule.onNodeWithTag("reaction_pill_😂").assertIsNotSelected() + } + + @Test + fun `wrapping at a narrow width collapses into an N more pill before plus, which expands on tap`() { + val pills = listOf( + pill("😀"), pill("😁"), pill("😂"), pill("😃"), + pill("😄"), pill("😅"), pill("😆"), pill("😇"), + ) + setRow(pills = pills, width = 90.dp) + + // Narrow enough that not everything fits on two lines: a collapse pill stands in for the + // pills pushed off, placed ahead of "+". The row's SubcomposeLayout still subcomposes every + // pill to measure it, so a collapsed one stays in the semantics tree unplaced — displayed, + // not existence, is the check that actually distinguishes "shown" from "collapsed away". + composeTestRule.onNodeWithTag("reaction_pill_more").assertIsDisplayed() + composeTestRule.onNodeWithTag("reaction_pill_😇").assertIsNotDisplayed() + + composeTestRule.onNodeWithTag("reaction_pill_more").performClick() + + composeTestRule.runOnIdle { + composeTestRule.onNodeWithTag("reaction_pill_more").assertDoesNotExist() + composeTestRule.onNodeWithTag("reaction_pill_😇").assertIsDisplayed() + } + } +} diff --git a/apps/flipcash/shared/chat/build.gradle.kts b/apps/flipcash/shared/chat/build.gradle.kts index a5c83155d4..afa205cec9 100644 --- a/apps/flipcash/shared/chat/build.gradle.kts +++ b/apps/flipcash/shared/chat/build.gradle.kts @@ -6,6 +6,14 @@ android { namespace = "${Gradle.flipcashNamespace}.shared.chat" } +// 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) } + } +} + dependencies { testImplementation(kotlin("test")) testImplementation(libs.bundles.unit.testing) @@ -26,5 +34,6 @@ dependencies { testImplementation(testFixtures(project(":apps:flipcash:shared:analytics"))) implementation(project(":services:flipcash")) implementation(project(":libs:network:connectivity:public")) + implementation(project(":libs:emojis")) implementation(libs.androidx.lifecycle.process) } diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/ChatCoordinator.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/ChatCoordinator.kt index e56f7a696d..c0656b43a2 100644 --- a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/ChatCoordinator.kt +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/ChatCoordinator.kt @@ -15,12 +15,18 @@ import com.flipcash.services.models.chat.EditChatParameters import com.flipcash.services.models.chat.IdempotencyKey import com.flipcash.services.models.chat.MessageContent import com.flipcash.services.models.chat.MessagePointer +import com.flipcash.services.models.PagingToken import com.flipcash.services.models.chat.MuteState import com.flipcash.services.models.chat.ReactionSummary import com.flipcash.services.models.chat.StartChatParameters import com.flipcash.services.models.chat.TypingState +import com.flipcash.services.repository.ReactorsPage +import com.flipcash.shared.chat.reactions.ReactionError +import com.flipcash.shared.chat.reactions.ReactionPill +import com.flipcash.shared.chat.reactions.SelfReaction import com.getcode.opencode.model.core.ID import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow /** @@ -83,6 +89,113 @@ interface EventStreamOperations { fun observeReactions(chatId: ChatId, messageId: Long): Flow } +/** + * One message's reactions as the transcript draws them: pills already in display order, and which + * of them the viewer is shown reacting with — the pending tap, if one has not settled yet, + * otherwise the confirmed state. + */ +data class MessageReactions( + val pills: List, + val selfReactions: List, +) { + companion object { + /** + * Builds [MessageReactions] straight from a persisted [ReactionSummary] — no pending taps, + * no in-flight calls. This is the ViewModel's fallback for a message + * [ReactionOperations.observeChatReactions] has no live override for: apply this to + * [com.flipcash.services.models.chat.ChatMessage.reactions] and let a live override, when + * present, win. Goes through [com.flipcash.shared.chat.reactions.ReactionState] so the + * count-0-tombstone filtering and pill ordering match the live path exactly. + */ + fun from(summary: ReactionSummary?): MessageReactions { + val state = com.flipcash.shared.chat.reactions.ReactionState() + summary?.let { + state.applySummary( + it.reactions.map { reaction -> + com.flipcash.shared.chat.reactions.ReactionState.SummaryEntry( + emoji = reaction.emoji.value, + count = reaction.count, + selfReacted = reaction.selfReactor != null, + version = reaction.version, + selfReactedAt = reaction.selfReactor?.reactedAt, + ) + }, + ) + } + return MessageReactions(pills = state.pills, selfReactions = state.selfReactions) + } + } +} + +/** + * Reaction taps and lookups for a single message: toggling, refreshing confirmed state from the + * server, and paging the "who reacted" sheet. + * + * Implemented by [com.flipcash.shared.chat.internal.delegates.ReactionsDelegate]. + */ +interface ReactionOperations { + /** + * Reactive **live overrides** for reactions in [chatId], keyed by message id — not one entry + * per message in the chat. A message appears here once this device has a pending tap, an + * in-flight call, or a confirmed overlay for it (an [EventStreamOperations] reaction update, or + * an explicit [refreshReactions]); a message whose reactions have only ever been seen via a + * paged Room read, or attached to a server-fetched [com.flipcash.services.models.chat.ChatMessage], + * is absent from this map even though it may well have reactions. + * + * The **ViewModel is responsible for completeness**: for a message missing from this map, fall + * back to [MessageReactions.from] applied to that message's own + * [com.flipcash.services.models.chat.ChatMessage.reactions] (the Room-decoded confirmed + * summary), and let an entry here win when both exist. This works because a write to + * `reactions_json` (a merge, or a plain message upsert that carries a summary) goes through + * Room, and Room invalidates the `chat_messages` `PagingSource` on any write to that table, so + * the paged message list already re-emits with the fresh summary — no separate signal needed + * for the Room-only case. + */ + fun observeChatReactions(chatId: ChatId): Flow> + + /** + * Flips the viewer's reaction to [emoji] on [messageId] and sends the resulting add or remove + * to the server, coalescing a follow-up tap made before the first call answers into one more + * call rather than a queue of them. A tap while a call for the same emoji is already in flight + * is recorded and sent once that call answers, without another call now. + * + * Optimistic: [observeChatReactions] reflects the tap immediately. A successful add records + * [emoji] to the recents ranking; a remove does not. A failure rolls the pill back and, unless + * the failure is [com.flipcash.shared.chat.reactions.ReactionFailure.MESSAGE_NOT_FOUND] or + * [com.flipcash.shared.chat.reactions.ReactionFailure.CANNOT_REACT] (both silent — the message + * or the viewer's standing in the chat has moved on since the tap), is also reported on + * [reactionErrors]. + */ + suspend fun toggleReaction(chatId: ChatId, messageId: Long, emoji: String) + + /** Reaction failures the user should be told about, for a snackbar or similar. */ + val reactionErrors: SharedFlow + + /** + * Fetches confirmed reaction summaries for [messageIds] from the server and merges them into + * both the persisted store and [observeChatReactions] — for a page of messages that just + * entered view without one already arriving via the event stream or the initial load. + */ + suspend fun refreshReactions(chatId: ChatId, messageIds: List): Result + + /** + * One page of the users who reacted to [messageId] with [emoji], for the "who reacted" sheet. + * [limit] defaults to the reactors sheet's own page size (decision 4), not the + * general-purpose [com.flipcash.services.models.QueryOptions] default. + */ + suspend fun getReactorsPage( + chatId: ChatId, + messageId: Long, + emoji: String, + token: PagingToken? = null, + limit: Int = REACTORS_PAGE_SIZE, + ): Result + + companion object { + const val REACTORS_PAGE_SIZE = 50 + } +} + /** * Resolves the [ChatId] of a DM from its participants, independent of any single * conversation. Two ways to arrive at an id: @@ -388,7 +501,8 @@ interface ChatCoordinator : EventStreamOperations, DmChatResolver, MessagingOperations, - GroupOperations { + GroupOperations, + ReactionOperations { /** Full observable snapshot of chat state (feed, typing, reactions, active chat). */ val state: StateFlow diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/MessageCapability.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/MessageCapability.kt index 47a42a722c..fcad28120c 100644 --- a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/MessageCapability.kt +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/MessageCapability.kt @@ -205,3 +205,22 @@ fun Set.withinWindows( /** True while a message sent at [sentAt] is inside this window, or always if there is none. */ private fun Duration?.stillOpen(sentAt: Instant, now: Instant): Boolean = this == null || now - sentAt <= this + +/** + * Whether [message] may be reacted to. + * + * Reactions have no edit/delete-style windows and no report-only carve-out: anyone's message is + * reactable, own or another participant's, text or cash. Only two things rule a message out — + * a system notice (nothing a participant sent) and an unconfirmed send (`eventSequence == 0`, + * the same guard [resolveForParticipant] uses: no valid request can name a message the server + * has not acknowledged). A tombstone has no content left ([MessageContent.Deleted] clears it), + * so the empty-content check below already excludes it. + */ +fun canReact(message: ChatMessage): Boolean { + val contents = message.content + if (contents.isEmpty()) return false + if (contents.any { it is MessageContent.Deleted }) return false + if (message.eventSequence == 0L) return false + if (contents.all { it is MessageContent.System }) return false + return true +} diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/inject/RecentReactionsOwnerModule.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/inject/RecentReactionsOwnerModule.kt new file mode 100644 index 0000000000..928f7dcb90 --- /dev/null +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/inject/RecentReactionsOwnerModule.kt @@ -0,0 +1,18 @@ +package com.flipcash.shared.chat.inject + +import com.flipcash.services.user.UserManager +import com.getcode.libs.emojis.reactions.RecentReactionsOwner +import com.getcode.utils.hexEncodedString +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object RecentReactionsOwnerModule { + + @Provides + fun providesRecentReactionsOwner(userManager: UserManager): RecentReactionsOwner = + RecentReactionsOwner { userManager.accountId?.hexEncodedString() } +} diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/RealChatCoordinator.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/RealChatCoordinator.kt index 6f2a37d78e..dc929f7392 100644 --- a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/RealChatCoordinator.kt +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/RealChatCoordinator.kt @@ -18,11 +18,13 @@ import com.flipcash.shared.chat.EventStreamOperations import com.flipcash.shared.chat.FeedOperations import com.flipcash.shared.chat.GroupOperations import com.flipcash.shared.chat.MessagingOperations +import com.flipcash.shared.chat.ReactionOperations import com.flipcash.shared.chat.internal.delegates.EventStreamDelegate import com.flipcash.shared.chat.internal.delegates.FeedSyncDelegate import com.flipcash.shared.chat.internal.delegates.GroupFeedDelegate import com.flipcash.shared.chat.internal.delegates.DmChatResolverDelegate import com.flipcash.shared.chat.internal.delegates.MessagingDelegate +import com.flipcash.shared.chat.internal.delegates.ReactionsDelegate import com.getcode.opencode.model.accounts.AccountCluster import com.getcode.opencode.providers.SessionListener import com.getcode.utils.TraceType @@ -86,6 +88,7 @@ class RealChatCoordinator @Inject constructor( private val dmChatResolverDelegate: DmChatResolverDelegate, private val messagingDelegate: MessagingDelegate, private val groupFeedDelegate: GroupFeedDelegate, + private val reactionsDelegate: ReactionsDelegate, private val stateHolder: ChatStateHolder, private val draftStore: ChatDraftStore, private val userManager: UserManager, @@ -98,7 +101,8 @@ class RealChatCoordinator @Inject constructor( EventStreamOperations by eventStreamDelegate, DmChatResolver by dmChatResolverDelegate, MessagingOperations by messagingDelegate, - GroupOperations by groupFeedDelegate { + GroupOperations by groupFeedDelegate, + ReactionOperations by reactionsDelegate { companion object { private const val TAG = "ChatCoordinator" @@ -330,6 +334,7 @@ class RealChatCoordinator @Inject constructor( networkObserverJob?.cancel() stateHolder.reset() eventStreamDelegate.clearAll() + reactionsDelegate.clearAll() cluster.value = null supervisorJob.cancel() trace(tag = TAG, message = "teardown complete", type = TraceType.Process) diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/delegates/EventStreamDelegate.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/delegates/EventStreamDelegate.kt index 6e99eef415..2cb0403d0a 100644 --- a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/delegates/EventStreamDelegate.kt +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/delegates/EventStreamDelegate.kt @@ -11,6 +11,7 @@ import com.flipcash.services.controllers.EventStreamingController import com.flipcash.services.models.chat.ChatId import com.flipcash.services.models.chat.ChatMessage import com.flipcash.services.models.chat.ChatUpdate +import com.flipcash.services.models.chat.Emoji import com.flipcash.services.models.chat.EmojiReaction import com.flipcash.services.models.chat.MessageContent import com.flipcash.services.models.chat.MetadataUpdate @@ -18,6 +19,7 @@ import com.flipcash.services.models.chat.ReactionSummary import com.flipcash.services.models.chat.ReactionUpdate import com.flipcash.services.models.chat.Reactor import com.flipcash.services.models.chat.RosterChange +import com.flipcash.shared.chat.reactions.ReactionState import com.flipcash.services.models.chat.TypingNotification import com.flipcash.services.models.chat.TypingState import com.flipcash.services.models.GetDeltaError @@ -27,6 +29,7 @@ import com.flipcash.shared.chat.EventStreamOperations import com.flipcash.shared.chat.internal.ChatStateHolder import com.flipcash.services.user.UserManager import com.getcode.opencode.exchange.Exchange +import com.getcode.opencode.model.core.ID import com.getcode.utils.TraceType import com.getcode.utils.trace import kotlinx.coroutines.CoroutineScope @@ -60,7 +63,7 @@ import kotlin.time.Duration.Companion.seconds * gap is detected, a timed [getDelta][performDeltaSync] backfill is scheduled. * - **Reaction overlays** — merges [ReactionUpdate]s into an in-memory * `Map>` with last-writer-wins on - * `EmojiReaction.sequence`. + * `EmojiReaction.version`. * - **Typing indicators** — maintains per-chat `Set` in [ChatStateHolder]. * - **Eager balance update** — credits incoming cash messages to [TokenCoordinator] * before the server balance refresh arrives. @@ -115,6 +118,40 @@ class EventStreamDelegate @Inject constructor( private var eventStreamCollectJob: Job? = null private var heartbeatJob: Job? = null + /** + * One [ReactionState] per message touched by a stream update, keyed by chat then message id. + * Only [ReactionState.applyUpdate] is called here — server-confirmed state only. A coordinator + * layer above this owns any pending taps, and merges its own [ReactionState] against what this + * delegate persists rather than sharing an instance with it. + */ + private val reactionStates: MutableMap> = mutableMapOf() + + /** + * Gets or creates the message's [ReactionState], seeding a new one from Room. The state is + * written back as a full summary, so an unseeded one would tombstone every stored emoji the + * stream hasn't mentioned this session, at the stored version, where a later summary at that + * same version can't bring it back. + */ + private suspend fun reactionState(chatId: ChatId, messageId: Long): ReactionState { + val chatStates = reactionStates.getOrPut(chatId) { mutableMapOf() } + return chatStates[messageId] ?: ReactionState().also { fresh -> + messageDataSource.getMessage(chatId, messageId)?.reactions?.let { summary -> + fresh.applySummary( + summary.reactions.map { reaction -> + ReactionState.SummaryEntry( + emoji = reaction.emoji.value, + count = reaction.count, + selfReacted = reaction.selfReactor != null, + version = reaction.version, + selfReactedAt = reaction.selfReactor?.reactedAt, + ) + }, + ) + } + chatStates[messageId] = fresh + } + } + // region EventStreamOperations override fun observeTypingIndicators(chatId: ChatId): Flow> { @@ -285,6 +322,7 @@ class EventStreamDelegate @Inject constructor( internal fun clearAll() { sequenceTracker.clearAll() + reactionStates.clear() } private fun ensureCollector(scope: CoroutineScope) { @@ -398,14 +436,31 @@ class EventStreamDelegate @Inject constructor( // --- Process reaction updates --- if (update.reactionUpdates.isNotEmpty()) { - stateHolder.update { state -> - val chatOverlays = state.reactionOverlays[chatId]?.toMutableMap() ?: mutableMapOf() - for (reactionUpdate in update.reactionUpdates) { - applyReactionUpdate(chatOverlays, reactionUpdate) - } - state.copy( - reactionOverlays = state.reactionOverlays + (chatId to chatOverlays.toMap()) + val touchedMessageIds = mutableSetOf() + for (reactionUpdate in update.reactionUpdates) { + reactionState(chatId, reactionUpdate.messageId).applyUpdate( + emoji = reactionUpdate.emoji.value, + actorIsSelf = reactionUpdate.actor == userManager.accountId, + added = reactionUpdate.action == ReactionUpdate.Action.ADDED, + count = reactionUpdate.count, + version = reactionUpdate.version, + reactedAt = reactionUpdate.reactedAt, ) + touchedMessageIds.add(reactionUpdate.messageId) + } + + val selfId = userManager.accountId + val summaries = touchedMessageIds.associateWith { messageId -> + reactionState(chatId, messageId).toReactionSummary(messageId, selfId) + } + + stateHolder.update { state -> + val chatOverlays = (state.reactionOverlays[chatId] ?: emptyMap()) + summaries + state.copy(reactionOverlays = state.reactionOverlays + (chatId to chatOverlays)) + } + + for ((messageId, summary) in summaries) { + messageDataSource.mergeReactions(chatId, messageId, summary) } } @@ -458,60 +513,29 @@ class EventStreamDelegate @Inject constructor( } } - private fun applyReactionUpdate( - overlays: MutableMap, - update: ReactionUpdate, - ) { - val existing = overlays[update.messageId] - val existingReactions = existing?.reactions?.toMutableList() ?: mutableListOf() - - // The server does not report a "self" flag on the update itself, so the client - // maintains EmojiReaction.selfReactor by comparing the update's actor to the - // signed-in user: an ADDED update from self sets it, a REMOVED update from self - // clears it, and an update from anyone else leaves the existing self state alone. - val isSelfActor = update.actor == userManager.accountId - - val idx = existingReactions.indexOfFirst { it.emoji == update.emoji } - if (idx >= 0) { - val current = existingReactions[idx] - if (update.sequence <= current.sequence) return - val selfReactor = when { - isSelfActor && update.action == ReactionUpdate.Action.ADDED -> - Reactor(userId = update.actor, reactedAt = update.reactedAt, version = current.selfReactor?.version ?: 0) - isSelfActor && update.action == ReactionUpdate.Action.REMOVED -> null - else -> current.selfReactor - } - existingReactions[idx] = EmojiReaction( - emoji = update.emoji, - count = update.count, - selfReactor = selfReactor, - sampleReactors = current.sampleReactors, - sequence = update.sequence, - ) - } else { - val selfReactor = if (isSelfActor && update.action == ReactionUpdate.Action.ADDED) { - Reactor(userId = update.actor, reactedAt = update.reactedAt) - } else { - null - } - existingReactions.add( + /** + * [ReactionState.summaryEntries] as a [ReactionSummary] — the shape [observeReactions] exposes + * and [ChatMessageDataSource.mergeReactions] persists. The reducer tracks only whether the + * viewer reacted, not the full [Reactor] the wire type wants for `selfReactor`, so that field + * is rebuilt here from [selfId]; a stream update carries no sample reactors at all (those come + * from the reactors sheet's own paged fetch), so [EmojiReaction.sampleReactors] is always empty + * for a summary built this way. + */ + private fun ReactionState.toReactionSummary(messageId: Long, selfId: ID?): ReactionSummary = + ReactionSummary( + messageId = messageId, + reactions = summaryEntries.map { entry -> EmojiReaction( - emoji = update.emoji, - count = update.count, - selfReactor = selfReactor, + emoji = Emoji(entry.emoji), + count = entry.count, + selfReactor = if (entry.selfReacted && selfId != null) { + Reactor(userId = selfId, reactedAt = entry.selfReactedAt ?: Clock.System.now(), version = entry.version) + } else null, sampleReactors = emptyList(), - sequence = update.sequence, + version = entry.version, ) - ) - } - - existingReactions.removeAll { it.count <= 0 } - - overlays[update.messageId] = ReactionSummary( - messageId = update.messageId, - reactions = existingReactions.toList(), + }, ) - } private fun scheduleGapFill(chatId: ChatId) { val scope = scope ?: return diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/delegates/ReactionsDelegate.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/delegates/ReactionsDelegate.kt new file mode 100644 index 0000000000..dec83b3f93 --- /dev/null +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/delegates/ReactionsDelegate.kt @@ -0,0 +1,237 @@ +package com.flipcash.shared.chat.internal.delegates + +import com.flipcash.app.persistence.sources.ChatMessageDataSource +import com.flipcash.services.controllers.ChatMessagingController +import com.flipcash.services.models.AddReactionError +import com.flipcash.services.models.PagingToken +import com.flipcash.services.models.QueryOptions +import com.flipcash.services.models.RemoveReactionError +import com.flipcash.services.models.chat.ChatId +import com.flipcash.services.models.chat.Emoji +import com.flipcash.services.models.chat.EmojiReaction +import com.flipcash.services.models.chat.ReactionSummary +import com.flipcash.services.models.chat.Reactor +import com.flipcash.services.repository.ReactorsPage +import com.flipcash.services.user.UserManager +import com.flipcash.shared.chat.MessageReactions +import com.flipcash.shared.chat.ReactionOperations +import com.flipcash.shared.chat.internal.ChatStateHolder +import com.flipcash.shared.chat.reactions.ReactionCall +import com.flipcash.shared.chat.reactions.ReactionError +import com.flipcash.shared.chat.reactions.ReactionFailure +import com.flipcash.shared.chat.reactions.ReactionResult +import com.flipcash.shared.chat.reactions.ReactionState +import com.getcode.libs.emojis.reactions.RecentReactionsStore +import com.getcode.opencode.model.core.ID +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.time.Clock + +/** + * Per-message reaction taps and lookups: toggling, refreshing confirmed state from the server, and + * paging the "who reacted" sheet. + * + * Holds one [ReactionState] per message this device has looked at — [snapshot] creates it lazily + * and folds in whatever [ChatStateHolder] currently has confirmed for that message (kept current by + * [EventStreamDelegate]'s own stream-driven reducer, and by [refreshReactions]'s explicit fetch). + * Only that fold is ever repeated; [ReactionState.applySummary] accepts by version, so replaying the + * same or older confirmed state over a pending tap never disturbs it. This mirrors the iOS + * reference (`ConversationReactions`): pending/in-flight state stays in memory only, and only the + * confirmed side is persisted, via [ChatMessageDataSource.mergeReactions]. + * + * @see com.flipcash.shared.chat.internal.RealChatCoordinator + */ +@Singleton +class ReactionsDelegate @Inject constructor( + private val messagingController: ChatMessagingController, + private val messageDataSource: ChatMessageDataSource, + private val recentReactionsStore: RecentReactionsStore, + private val userManager: UserManager, + private val stateHolder: ChatStateHolder, +) : ReactionOperations { + + /** One [ReactionState] per message this device has looked at, keyed by chat then message id. */ + private val liveStates: MutableMap> = mutableMapOf() + + /** + * Bumped on every local tap/respond so [observeChatReactions] re-emits — a pending tap or an + * in-flight call changes [liveStates] without touching [ChatStateHolder.state], which is the + * only thing [observeChatReactions] would otherwise be reacting to. + */ + private val localVersion = MutableStateFlow(0) + + private val _reactionErrors = MutableSharedFlow(extraBufferCapacity = 1) + override val reactionErrors: SharedFlow = _reactionErrors.asSharedFlow() + + override fun observeChatReactions(chatId: ChatId): Flow> = + combine(stateHolder.state, localVersion) { state, _ -> + val overlayIds = state.reactionOverlays[chatId]?.keys.orEmpty() + val liveIds = liveStates[chatId]?.keys.orEmpty() + val ids = overlayIds + liveIds + val result = mutableMapOf() + for (messageId in ids) { + val reactionState = snapshot(chatId, messageId) + result[messageId] = MessageReactions(pills = reactionState.pills, selfReactions = reactionState.selfReactions) + } + result + }.distinctUntilChanged() + + override suspend fun toggleReaction(chatId: ChatId, messageId: Long, emoji: String) { + val state = snapshot(chatId, messageId) + // Recorded at tap time, as iOS does: the snapshot knows reactions stored in Room, so a tap + // that takes back an existing reaction is not counted as a use. + val wasReacted = state.selfReactions.any { it.emoji == emoji } + val call = state.tap(emoji, Clock.System.now()) + if (!wasReacted) recentReactionsStore.record(emoji) + localVersion.update { it + 1 } + if (call != null) sendCall(chatId, messageId, state, call) + } + + override suspend fun refreshReactions(chatId: ChatId, messageIds: List): Result { + val summaries = messagingController.getReactionSummariesByIds(chatId, messageIds) + .getOrElse { return Result.failure(it) } + for (summary in summaries) { + val state = snapshot(chatId, summary.messageId) + state.applySummary(summary.reactions.map { it.toSummaryEntry() }) + persist(chatId, summary.messageId, state) + } + localVersion.update { it + 1 } + return Result.success(Unit) + } + + override suspend fun getReactorsPage( + chatId: ChatId, + messageId: Long, + emoji: String, + token: PagingToken?, + limit: Int, + ): Result = + messagingController.getReactors( + chatId, + messageId, + Emoji(emoji), + QueryOptions(token = token, limit = limit), + ) + + /** Drops all in-memory reaction state (pending taps, in-flight calls, confirmed cache). */ + fun clearAll() { + liveStates.clear() + localVersion.value = 0 + } + + /** + * Sends [call], settles it against [state], persists confirmed state on success, reports a + * non-silent failure on [reactionErrors], and loops once more if settling produced a coalesced follow-up call — the same shape as the iOS + * reference's tap → send → respond cycle. + */ + private suspend fun sendCall(chatId: ChatId, messageId: Long, state: ReactionState, call: ReactionCall) { + val result = performCall(chatId, messageId, call) + val (followUp, error) = state.respond(call.emoji, result) + if (result is ReactionResult.Ok) { + persist(chatId, messageId, state) + } + localVersion.update { it + 1 } + error?.let { _reactionErrors.emit(it) } + if (followUp != null) sendCall(chatId, messageId, state, followUp) + } + + private suspend fun performCall(chatId: ChatId, messageId: Long, call: ReactionCall): ReactionResult { + val emoji = Emoji(call.emoji) + val result = when (call.op) { + ReactionCall.Op.ADD -> messagingController.addReaction(chatId, messageId, emoji) + ReactionCall.Op.REMOVE -> messagingController.removeReaction(chatId, messageId, emoji) + } + return result.fold( + onSuccess = { reaction -> + ReactionResult.Ok( + count = reaction.count, + selfReacted = reaction.selfReactor != null, + version = reaction.version, + selfReactedAt = reaction.selfReactor?.reactedAt, + ) + }, + onFailure = { error -> ReactionResult.Failed(failureOf(call.op, error)) }, + ) + } + + private fun failureOf(op: ReactionCall.Op, error: Throwable): ReactionFailure = when (op) { + ReactionCall.Op.ADD -> when (error) { + is AddReactionError.Denied -> ReactionFailure.DENIED + is AddReactionError.MessageNotFound -> ReactionFailure.MESSAGE_NOT_FOUND + is AddReactionError.CannotReact -> ReactionFailure.CANNOT_REACT + is AddReactionError.TooManyReactionTypes -> ReactionFailure.TOO_MANY_REACTION_TYPES + else -> ReactionFailure.NETWORK + } + ReactionCall.Op.REMOVE -> when (error) { + is RemoveReactionError.Denied -> ReactionFailure.DENIED + is RemoveReactionError.MessageNotFound -> ReactionFailure.MESSAGE_NOT_FOUND + else -> ReactionFailure.NETWORK + } + } + + /** + * Gets or creates the message's [ReactionState] and folds in the latest confirmed overlay. + * + * A freshly-created state is first seeded from Room: after a relaunch the confirmed reactions + * for a message this device already knew about live only on disk, not in [ChatStateHolder] — + * without this, the first tap on an emoji the user already reacted with would compute an ADD + * instead of a REMOVE. Only done once per message (the `getOrPut` below only runs the seed + * block when creating), since after that [ChatStateHolder]'s overlay (kept current by + * [EventStreamDelegate] and [refreshReactions]) is the live source of truth. + */ + private suspend fun snapshot(chatId: ChatId, messageId: Long): ReactionState { + val chatStates = liveStates.getOrPut(chatId) { mutableMapOf() } + val state = chatStates[messageId] ?: ReactionState().also { fresh -> + messageDataSource.getMessage(chatId, messageId)?.reactions?.let { summary -> + fresh.applySummary(summary.reactions.map { it.toSummaryEntry() }) + } + chatStates[messageId] = fresh + } + stateHolder.current.reactionOverlays[chatId]?.get(messageId)?.let { summary -> + state.applySummary(summary.reactions.map { it.toSummaryEntry() }) + } + return state + } + + /** Persists [state]'s confirmed side and mirrors it into [ChatStateHolder.state]'s overlay. */ + private suspend fun persist(chatId: ChatId, messageId: Long, state: ReactionState) { + val selfId = userManager.accountId + val summary = ReactionSummary( + messageId = messageId, + reactions = state.summaryEntries.map { it.toEmojiReaction(selfId) }, + ) + messageDataSource.mergeReactions(chatId, messageId, summary) + stateHolder.update { current -> + val chatOverlays = (current.reactionOverlays[chatId] ?: emptyMap()) + (messageId to summary) + current.copy(reactionOverlays = current.reactionOverlays + (chatId to chatOverlays)) + } + } + + private fun EmojiReaction.toSummaryEntry(): ReactionState.SummaryEntry = ReactionState.SummaryEntry( + emoji = emoji.value, + count = count, + selfReacted = selfReactor != null, + version = version, + selfReactedAt = selfReactor?.reactedAt, + ) + + private fun ReactionState.SummaryEntry.toEmojiReaction(selfId: ID?): EmojiReaction = EmojiReaction( + emoji = Emoji(emoji), + count = count, + selfReactor = if (selfReacted && selfId != null) { + Reactor(userId = selfId, reactedAt = selfReactedAt ?: Clock.System.now(), version = version) + } else { + null + }, + sampleReactors = emptyList(), + version = version, + ) +} diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionOrdering.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionOrdering.kt new file mode 100644 index 0000000000..50369daee6 --- /dev/null +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionOrdering.kt @@ -0,0 +1,37 @@ +package com.flipcash.shared.chat.reactions + +/** + * The order pills are shown in: boost total descending, then count descending, then the emoji's + * UTF-8 bytes ascending. + * + * The last tiebreak matches the server, which sorts summaries with Go's bytewise string compare + * (`flipcash2-server` `messaging/dynamodb/store.go:2277`). Kotlin's `String.compareTo` compares by + * UTF-16 code unit instead, which disagrees with byte order for some multi-scalar emoji, so it + * must not be used here. + */ +object ReactionOrdering { + + /** Returns pills-display order: negative when [lhs] is shown before [rhs]. */ + val comparator: Comparator = Comparator { lhs, rhs -> + val lhsBoost = lhs.boost?.total ?: 0L + val rhsBoost = rhs.boost?.total ?: 0L + if (lhsBoost != rhsBoost) return@Comparator rhsBoost.compareTo(lhsBoost) + if (lhs.count != rhs.count) return@Comparator rhs.count.compareTo(lhs.count) + compareUtf8Bytes(lhs.emoji, rhs.emoji) + } + + /** Returns [pills] in display order. */ + fun sorted(pills: Iterable): List = pills.sortedWith(comparator) + + /** Compares two emoji by their raw UTF-8 bytes, ascending, matching the server's order. */ + fun compareUtf8Bytes(lhs: String, rhs: String): Int { + val a = lhs.encodeToByteArray() + val b = rhs.encodeToByteArray() + val len = minOf(a.size, b.size) + for (i in 0 until len) { + val cmp = (a[i].toInt() and 0xFF) - (b[i].toInt() and 0xFF) + if (cmp != 0) return cmp + } + return a.size - b.size + } +} diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionPill.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionPill.kt new file mode 100644 index 0000000000..9ebe0c7bc7 --- /dev/null +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionPill.kt @@ -0,0 +1,62 @@ +package com.flipcash.shared.chat.reactions + +import kotlin.time.Instant + +/** + * One emoji's displayed reaction on a message, with [count] and [selfReacted] already resolved + * from the pending tap over the confirmed server state. + */ +data class ReactionPill( + val emoji: String, + val count: Long, + val selfReacted: Boolean, + /** True while the user's last tap on this emoji has not been reconciled with the server. */ + val pending: Boolean, + val boost: ReactionBoost? = null, +) + +/** The paid boost on a pill, a phase-2 seam that nothing creates in phase 1. */ +data class ReactionBoost(val total: Long) + +/** An add or remove the caller must send to the server for one emoji. */ +data class ReactionCall(val op: Op, val emoji: String) { + enum class Op { ADD, REMOVE } +} + +/** Why an add or remove call failed, as far as the reaction merge needs to know. */ +enum class ReactionFailure { + NETWORK, + DENIED, + MESSAGE_NOT_FOUND, + CANNOT_REACT, + TOO_MANY_REACTION_TYPES; + + /** The error to show the user for this failure, or null when the rollback is silent. */ + val userError: ReactionError? + get() = when (this) { + NETWORK, DENIED -> ReactionError.REACTION_FAILED + TOO_MANY_REACTION_TYPES -> ReactionError.TOO_MANY_REACTION_TYPES + MESSAGE_NOT_FOUND, CANNOT_REACT -> null + } +} + +/** A reaction failure the user is told about. */ +enum class ReactionError { + REACTION_FAILED, + TOO_MANY_REACTION_TYPES, +} + +/** The outcome of an add or remove call. */ +sealed class ReactionResult { + data class Ok( + val count: Long, + val selfReacted: Boolean, + val version: Long, + val selfReactedAt: Instant?, + ) : ReactionResult() + + data class Failed(val failure: ReactionFailure) : ReactionResult() +} + +/** An emoji the user's displayed state has them reacting with, and when they reacted. */ +data class SelfReaction(val emoji: String, val reactedAt: Instant) diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionRefreshPlanner.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionRefreshPlanner.kt new file mode 100644 index 0000000000..469c4ea743 --- /dev/null +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionRefreshPlanner.kt @@ -0,0 +1,54 @@ +package com.flipcash.shared.chat.reactions + +/** + * Decides which message ids need a reaction refresh (a [com.flipcash.shared.chat.ChatCoordinator.refreshReactions] + * call), and how to batch that call. + * + * Two entry points, matching the two places a hook fires: + * - [initialWindow]: chat opens, or the app/stream comes back to the foreground — refresh the + * newest messages currently loaded, since a live overlay may have been missed while backgrounded. + * - [forLoadedPage]: a new page of *older* messages lands in the paging list. Only messages that + * were read out of Room (already-cached rows the pager attached, not a fresh server page) need a + * refresh — a page the RemoteMediator just fetched from the network already carries current + * reaction state, so refreshing it again would be redundant. + * + * Both are pure: callers own the "already refreshed" bookkeeping and pass in what's still needed. + */ +object ReactionRefreshPlanner { + + const val DEFAULT_WINDOW_SIZE = 60 + const val DEFAULT_CHUNK_SIZE = 40 + + /** + * @param newestFirstIds all currently loaded message ids, newest first. + * @param alreadyRefreshed ids this session has already refreshed and doesn't need to repeat. + */ + fun initialWindow( + newestFirstIds: List, + alreadyRefreshed: Set = emptySet(), + windowSize: Int = DEFAULT_WINDOW_SIZE, + ): List = + newestFirstIds + .asSequence() + .take(windowSize) + .filterNot { it in alreadyRefreshed } + .toList() + + /** + * @param pageIds ids in the page that just loaded. + * @param sourcedFromServer true when this page came from a RemoteMediator network fetch rather + * than an existing Room row; server-fetched pages are skipped. + * @return chunks of ids to refresh, each no larger than [chunkSize]. + */ + fun forLoadedPage( + pageIds: List, + sourcedFromServer: Boolean, + alreadyRefreshed: Set = emptySet(), + chunkSize: Int = DEFAULT_CHUNK_SIZE, + ): List> { + if (sourcedFromServer) return emptyList() + val remaining = pageIds.filterNot { it in alreadyRefreshed } + if (remaining.isEmpty()) return emptyList() + return remaining.chunked(chunkSize) + } +} diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionState.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionState.kt new file mode 100644 index 0000000000..c881f08dd4 --- /dev/null +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionState.kt @@ -0,0 +1,238 @@ +package com.flipcash.shared.chat.reactions + +import kotlin.time.Instant + +/** + * One message's reactions: the server state last accepted per emoji, merged with the user's + * pending taps. + * + * This is `gen_reactions.py`'s `Model`, and `reactions.json`'s `merge` vectors hold it to that. + * Server state (summary, stream update, successful response) is accepted only when its version is + * strictly greater than the one held for that emoji. The displayed self state is the pending tap + * when there is one, otherwise the confirmed one, so the stream never overwrites a pending tap. + * + * Only [confirmed] would be encoded on persistence: pending taps and in-flight calls do not + * survive a relaunch. + */ +class ReactionState { + + /** The last server state accepted for one emoji. */ + data class Confirmed( + val count: Long, + val selfReacted: Boolean, + val version: Long, + /** + * When the user reacted with this emoji, if they have; orders the strip and takes no part + * in the merge. + */ + val selfReactedAt: Instant?, + ) + + /** One emoji as a reaction summary lists it. */ + data class SummaryEntry( + val emoji: String, + val count: Long, + val selfReacted: Boolean, + val version: Long, + val selfReactedAt: Instant?, + ) + + /** + * The accepted server state per emoji; a count of 0 is a tombstone that keeps the emptied + * emoji's version so a late, older add is still rejected. + */ + private val confirmedByEmoji: MutableMap = mutableMapOf() + + /** The self state the user last tapped to, per emoji, until the server settles it. */ + private val desired: MutableMap = mutableMapOf() + + /** Emoji with an add or remove call on the wire. */ + private val inFlightEmoji: MutableSet = mutableSetOf() + + /** When the user tapped each emoji whose [desired] state is on. */ + private val desiredAt: MutableMap = mutableMapOf() + + val confirmed: Map get() = confirmedByEmoji + val inFlight: Set get() = inFlightEmoji + + // region Server state + + /** + * Accepts a reaction summary, which lists only non-empty emoji: a held emoji it omits has + * emptied, unless a tap on it is pending. + */ + fun applySummary(reactions: List) { + val present = reactions.map { it.emoji }.toSet() + for (entry in reactions) { + accept( + entry.emoji, + Confirmed( + count = entry.count, + selfReacted = entry.selfReacted, + version = entry.version, + selfReactedAt = entry.selfReactedAt, + ), + ) + } + for ((emoji, held) in confirmedByEmoji.toMap()) { + if (emoji !in present && desired[emoji] == null && held.count > 0) { + confirmedByEmoji[emoji] = held.copy(count = 0, selfReacted = false, selfReactedAt = null) + } + } + } + + /** Accepts one stream update; [count] is the emoji's new total, not a delta. */ + fun applyUpdate( + emoji: String, + actorIsSelf: Boolean, + added: Boolean, + count: Long, + version: Long, + reactedAt: Instant?, + ) { + val selfReacted: Boolean + val selfReactedAt: Instant? + if (actorIsSelf) { + selfReacted = added + selfReactedAt = if (added) reactedAt else null + } else { + selfReacted = confirmedByEmoji[emoji]?.selfReacted ?: false + selfReactedAt = confirmedByEmoji[emoji]?.selfReactedAt + } + accept(emoji, Confirmed(count = count, selfReacted = selfReacted, version = version, selfReactedAt = selfReactedAt)) + } + + /** + * Accepts every emoji [other] has confirmed, each by version as any server state is, and + * ignores its pending taps. + */ + fun merge(other: ReactionState) { + for ((emoji, state) in other.confirmedByEmoji) { + accept(emoji, state) + } + } + + /** The confirmed non-empty emoji, as a summary would list them. */ + val summaryEntries: List + get() = confirmedByEmoji.mapNotNull { (emoji, state) -> + if (state.count <= 0) return@mapNotNull null + SummaryEntry( + emoji = emoji, + count = state.count, + selfReacted = state.selfReacted, + version = state.version, + selfReactedAt = state.selfReactedAt, + ) + } + + /** Whether no tap is pending and no call is on the wire. */ + val isSettled: Boolean get() = desired.isEmpty() && inFlightEmoji.isEmpty() + + // endregion + + // region User actions + + /** + * Flips the displayed self state of [emoji] and returns the call to send, or null when a call + * for it is already on the wire and the response will send any follow-up. + */ + fun tap(emoji: String, at: Instant): ReactionCall? { + val on = !displayedSelf(emoji) + desired[emoji] = on + if (on) desiredAt[emoji] = at else desiredAt.remove(emoji) + if (emoji in inFlightEmoji) return null + return send(emoji) + } + + /** + * Settles the call in flight for [emoji], returning a coalesced follow-up call and the error + * to show, if any; a response for an emoji with nothing in flight is ignored. + */ + fun respond(emoji: String, result: ReactionResult): Pair { + if (!inFlightEmoji.remove(emoji)) return null to null + return when (result) { + is ReactionResult.Ok -> { + accept( + emoji, + Confirmed( + count = result.count, + selfReacted = result.selfReacted, + version = result.version, + selfReactedAt = result.selfReactedAt, + ), + ) + val wanted = desired[emoji] + if (wanted != null && wanted != (confirmedByEmoji[emoji]?.selfReacted ?: false)) { + send(emoji) to null + } else { + clearDesired(emoji) + null to null + } + } + + is ReactionResult.Failed -> { + clearDesired(emoji) + null to result.failure.userError + } + } + } + + // endregion + + // region Display + + /** The pills to show, in [ReactionOrdering]; an emoji whose displayed count is 0 has none. */ + val pills: List + get() { + val emoji = confirmedByEmoji.keys + desired.keys + val pills = emoji.mapNotNull { emoji -> + val (count, shown) = displayed(emoji) + if (count <= 0) return@mapNotNull null + ReactionPill(emoji = emoji, count = count, selfReacted = shown, pending = desired[emoji] != null) + } + return ReactionOrdering.sorted(pills) + } + + /** The emoji the user is shown reacting with, each with when they reacted. */ + val selfReactions: List + get() = (confirmedByEmoji.keys + desired.keys).mapNotNull { emoji -> + if (!displayedSelf(emoji)) return@mapNotNull null + val reactedAt = desiredAt[emoji] ?: confirmedByEmoji[emoji]?.selfReactedAt ?: Instant.DISTANT_PAST + SelfReaction(emoji = emoji, reactedAt = reactedAt) + } + + // endregion + + // region Private + + private fun accept(emoji: String, state: Confirmed) { + val held = confirmedByEmoji[emoji] + if (held != null && state.version <= held.version) return + confirmedByEmoji[emoji] = state + } + + private fun send(emoji: String): ReactionCall { + inFlightEmoji.add(emoji) + return ReactionCall(op = if (desired[emoji] == true) ReactionCall.Op.ADD else ReactionCall.Op.REMOVE, emoji = emoji) + } + + private fun clearDesired(emoji: String) { + desired.remove(emoji) + desiredAt.remove(emoji) + } + + private fun displayedSelf(emoji: String): Boolean = desired[emoji] ?: confirmedByEmoji[emoji]?.selfReacted ?: false + + /** + * The displayed count, signed because an inconsistent confirmed state (self set on a count of + * 0) must hide the pill rather than underflow. + */ + private fun displayed(emoji: String): Pair { + val held = confirmedByEmoji[emoji] + val shown = displayedSelf(emoji) + val base = (held?.count ?: 0) - (if (held?.selfReacted == true) 1 else 0) + return (base + if (shown) 1 else 0) to shown + } + + // endregion +} diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionStrip.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionStrip.kt new file mode 100644 index 0000000000..cf211ba572 --- /dev/null +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionStrip.kt @@ -0,0 +1,31 @@ +package com.flipcash.shared.chat.reactions + +/** The emoji the long-press strip offers, before the trailing "+" the UI adds. */ +object ReactionStrip { + + /** One emoji in the strip. */ + data class Entry( + val emoji: String, + /** True when the user has reacted to the message with this emoji. */ + val highlighted: Boolean, + ) + + /** + * Returns the recents in rank order, highlighting the ones the user reacted with, followed by + * the user's other reactions, newest first. + */ + fun entries(recents: List, selfReactions: List): List { + val mine = selfReactions.map { it.emoji }.toSet() + val recentSet = recents.toSet() + // Indexed so equal times keep their input order, as the reference's stable sort does. + val extra = selfReactions.withIndex() + .sortedWith( + compareByDescending> { it.value.reactedAt } + .thenBy { it.index }, + ) + .map { it.value.emoji } + .filter { it !in recentSet } + return recents.map { Entry(emoji = it, highlighted = it in mine) } + + extra.map { Entry(emoji = it, highlighted = true) } + } +} diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionStripComposer.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionStripComposer.kt new file mode 100644 index 0000000000..6fc1e346ea --- /dev/null +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactionStripComposer.kt @@ -0,0 +1,52 @@ +package com.flipcash.shared.chat.reactions + +import com.getcode.libs.emojis.reactions.RecentReactions + +/** + * Composes the long-press strip's 12 emoji (the "+" that opens the picker is the UI's, not this + * function's, thirteenth slot). + * + * Mirrors iOS's app-layer composition over the vector-exact `ReactionStrip.entries`: fixed + * defaults (minus undrawable), then up to six most-used beyond the defaults, then every emoji the + * user reacted with on this message (always shown, even past the cap — `ReactionStrip.entries` + * itself guarantees that), then filler from the catalog's first category up to 12 total. + */ +object ReactionStripComposer { + private const val TARGET_SIZE = 12 + private const val MOST_USED_SLOTS = 6 + + /** + * @param recentStats this device's recorded per-emoji use, as [RecentReactions.rank] takes it. + * @param selfReactions the emoji the user reacted to this message with. + * @param catalogFillSource the fill source's emoji, in the catalog's own order (the first + * category, per decision 1 — the caller resolves that; this function only fills from it). + * @param undrawable emoji this device cannot draw as one glyph, left out everywhere. + */ + fun compose( + recentStats: Map, + selfReactions: List, + catalogFillSource: List, + undrawable: Set = emptySet(), + ): List { + val fixed = RecentReactions.DEFAULTS.filter { it !in undrawable } + + val mostUsed = RecentReactions.rank(recentStats, undrawable, fixed.size + MOST_USED_SLOTS) + .filter { it !in fixed } + .take(MOST_USED_SLOTS) + + val entries = ReactionStrip.entries(fixed + mostUsed, selfReactions).toMutableList() + if (entries.size >= TARGET_SIZE) return entries.take(TARGET_SIZE) + + val present = entries.mapTo(mutableSetOf()) { it.emoji } + val selfEmoji = selfReactions.map { it.emoji }.toSet() + + for (emoji in catalogFillSource) { + if (entries.size >= TARGET_SIZE) break + if (emoji in undrawable || emoji in present) continue + entries.add(ReactionStrip.Entry(emoji = emoji, highlighted = emoji in selfEmoji)) + present.add(emoji) + } + + return entries.take(TARGET_SIZE) + } +} diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactorNameResolver.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactorNameResolver.kt new file mode 100644 index 0000000000..4e57d882eb --- /dev/null +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactorNameResolver.kt @@ -0,0 +1,55 @@ +package com.flipcash.shared.chat.reactions + +import com.flipcash.services.models.UserProfile +import com.flipcash.services.models.chat.ChatMember +import com.flipcash.services.models.handle +import com.flipcash.services.models.nameOrHandle +import com.getcode.opencode.model.core.ID +import com.getcode.utils.hexEncodedString + +/** + * The reactors sheet's row name for one userId (decision 4). + * + * Pure and dependency-free so the precedence is unit-testable without a ViewModel, Compose, or a + * database. A caller (`ChatViewModel.reactorProfileFlow`) supplies each source already read — + * [cachedProfile] from a one-shot `UserProfileDataSource` lookup, [members] and [senderProfiles] + * from their own live flows — and re-resolves as those live sources change. + * + * Precedence, matching the identity sources `ChatViewModel.memberParticipant` already reads: + * + * 1. [selfUserId] — the viewer themself. Returns [selfLabel] ("You"), never their own profile + * name, so the sheet doesn't show a reactor a vanity name they gave themselves elsewhere. + * 2. [cachedProfile] — the persisted `user_profiles` cache, resolved without a round trip. + * 3. [members] — the chat's own roster, which a group already holds for everyone in it. + * 4. [senderProfiles] — a server fetch the caller kicked off (`ChatCoordinator.requestSenderProfile`), + * for a reactor absent from both of the above. Answers asynchronously, so a caller re-resolves + * once this map updates. + * + * Returns null when none of the four sources has an answer yet — the caller's cue to request a + * fetch and leave the row showing a placeholder until one of the sources resolves. + */ +object ReactorNameResolver { + + fun resolve( + userId: ID, + selfUserId: ID?, + selfLabel: String, + cachedProfile: UserProfile?, + members: List, + senderProfiles: Map, + ): String? { + if (selfUserId != null && userId == selfUserId) return selfLabel + + cachedProfile?.let { nameOrHandle(it.displayName, it.handle) }?.let { return it } + + members.firstOrNull { it.userId == userId }?.userProfile + ?.let { nameOrHandle(it.displayName, it.handle) } + ?.let { return it } + + senderProfiles[userId.hexEncodedString()] + ?.let { nameOrHandle(it.displayName, it.handle) } + ?.let { return it } + + return null + } +} diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactorsListModel.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactorsListModel.kt new file mode 100644 index 0000000000..8e28b92e28 --- /dev/null +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactorsListModel.kt @@ -0,0 +1,115 @@ +package com.flipcash.shared.chat.reactions + +import com.flipcash.services.models.PagingToken +import com.flipcash.services.repository.ReactorsPage +import com.getcode.opencode.model.core.ID +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope + +/** + * Pure, fake-friendly merge logic for the reactors sheet (decision 4) — mirrors iOS's + * `ReactorsListModel`/`ReactorsSource`. One row per person who reacted, holding every emoji they + * used, in the order [emojis] lists them (pill order); rows sorted by their most recent reaction + * first, then by userId as a tiebreak. + * + * Not itself a ViewModel — a VM or step-scoped holder drives it (loadMoreIfNeeded from a start + * trigger and from "last row visible") and republishes [rows] to its own StateFlow. Kept free of + * Compose/Android types so it is unit-testable with a fake [fetchPage]. + */ +class ReactorsListModel( + private val emojis: List, + private val fetchPage: suspend (emoji: String, token: PagingToken?) -> Result, +) { + + private data class EmojiState( + val reactors: List = emptyList(), + val nextToken: PagingToken? = null, + val hasMore: Boolean = true, + val loading: Boolean = false, + ) + + private val state = emojis.associateWith { EmojiState() }.toMutableMap() + + /** One row per reactor: their userId, every emoji they used (pill order), and their newest reaction time. */ + data class Row( + val userId: ID, + val emojis: List, + val newestReactedAt: kotlin.time.Instant, + ) + + var rows: List = emptyList() + private set + + /** True while at least one emoji still has an unfetched page. */ + val hasMore: Boolean + get() = state.values.any { it.hasMore } + + /** + * Fetches the next page for every emoji that still has more and isn't already loading, waits + * for the whole round to land, then republishes [rows]. A failed page for one emoji only turns + * off that emoji's `hasMore` — the others still complete and are reflected. + */ + suspend fun loadMoreIfNeeded() { + val toFetch = state.filterValues { it.hasMore && !it.loading }.keys + if (toFetch.isEmpty()) return + + toFetch.forEach { emoji -> state[emoji] = state.getValue(emoji).copy(loading = true) } + + coroutineScope { + val results = toFetch.map { emoji -> + emoji to async { fetchPage(emoji, state.getValue(emoji).nextToken) } + } + for ((emoji, deferred) in results) { + val current = state.getValue(emoji) + val result = deferred.await() + state[emoji] = result.fold( + onSuccess = { page -> + current.copy( + reactors = current.reactors + page.reactors, + nextToken = page.nextToken, + hasMore = page.hasMore, + loading = false, + ) + }, + onFailure = { + current.copy(hasMore = false, loading = false) + }, + ) + } + } + + publish() + } + + private fun publish() { + val byUser = LinkedHashMap>>() + for (emoji in emojis) { + val emojiState = state.getValue(emoji) + for (reactor in emojiState.reactors) { + val key = reactor.userId.joinToString(",") + val entries = byUser.getOrPut(key) { mutableListOf() } + entries += emoji to reactor.reactedAt + } + } + + val userIdByKey = mutableMapOf() + for (emoji in emojis) { + for (reactor in state.getValue(emoji).reactors) { + userIdByKey[reactor.userId.joinToString(",")] = reactor.userId + } + } + + rows = byUser.entries + .map { (key, entries) -> + val emojiOrder = entries.map { it.first }.distinct() + val newest = entries.maxOf { it.second } + Row(userId = userIdByKey.getValue(key), emojis = emojiOrder, newestReactedAt = newest) + } + .sortedWith( + compareByDescending { it.newestReactedAt } + .thenByDescending { it.userId.hexEncodedString() }, + ) + } +} + +private fun ID.hexEncodedString(): String = joinToString("") { "%02x".format(it) } diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactorsPrefetchCache.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactorsPrefetchCache.kt new file mode 100644 index 0000000000..3490a0c8de --- /dev/null +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/reactions/ReactorsPrefetchCache.kt @@ -0,0 +1,82 @@ +package com.flipcash.shared.chat.reactions + +import com.flipcash.services.models.PagingToken +import com.flipcash.services.repository.ReactorsPage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +/** + * Owns one [ReactorsListModel] per messageId, keyed so the reactors sheet and whatever started the + * fetch read the same rows/loading state rather than each building their own. + * + * Loading has to start on the pill long-press (decision: `OpenReactors` fires the fetch), not on + * the reactors sheet's first composition — the sheet is a fresh Compose tree on every push, and a + * fetch gated on its `LaunchedEffect` would race the same tap that opened it. [start] is called + * from `ChatViewModel`'s `OpenReactors` handler, one step before the navigator pushes + * `ChatStep.Reactors`; [rows]/[loading] are backed by one shared `StateFlow` each (keyed by + * messageId), rather than a per-messageId flow instance created inside [start] — a composition that + * calls [rows] *before* [start] has run still sees the update once it does, because it collects a + * `.map` over the same always-live backing flow rather than a separate object [start] would + * otherwise swap in. + */ +class ReactorsPrefetchCache( + private val scope: CoroutineScope, + private val fetchPage: suspend (messageId: Long, emoji: String, token: PagingToken?) -> Result, +) { + + private val models = mutableMapOf() + private val allRows = MutableStateFlow>>(emptyMap()) + private val allLoading = MutableStateFlow>(emptyMap()) + + /** The merged rows for [messageId], empty before [start] has produced a first page. */ + fun rows(messageId: Long): Flow> = + allRows.map { it[messageId].orEmpty() }.distinctUntilChanged() + + /** Whether a page is in flight for [messageId]. */ + fun loading(messageId: Long): Flow = + allLoading.map { it[messageId] == true }.distinctUntilChanged() + + /** True while at least one of [messageId]'s emojis still has an unfetched page. */ + fun hasMore(messageId: Long): Boolean = models[messageId]?.hasMore ?: false + + /** + * Starts the first round of pages for [messageId]'s [emojis] if nothing has started yet. + * No-ops for an empty [emojis] list (nothing reacted, so nothing to fetch) and for a messageId + * already in the cache — a second `OpenReactors` for the same message re-opens the existing + * fetch/rows rather than starting over. + */ + fun start(messageId: Long, emojis: List) { + if (emojis.isEmpty() || models.containsKey(messageId)) return + models[messageId] = ReactorsListModel(emojis) { emoji, token -> fetchPage(messageId, emoji, token) } + loadMore(messageId) + } + + /** Fetches the next page for every emoji of [messageId] that still has more, once no load is already in flight. */ + fun loadMoreIfNeeded(messageId: Long) { + val model = models[messageId] ?: return + if (allLoading.value[messageId] == true || !model.hasMore) return + loadMore(messageId) + } + + private fun loadMore(messageId: Long) { + allLoading.update { it + (messageId to true) } + scope.launch { + val model = models.getValue(messageId) + model.loadMoreIfNeeded() + allRows.update { it + (messageId to model.rows) } + allLoading.update { it + (messageId to false) } + } + } + + /** Drops [messageId]'s cached fetch/rows — for a message that leaves the transcript (deleted, etc). */ + fun clear(messageId: Long) { + models.remove(messageId) + allRows.update { it - messageId } + allLoading.update { it - messageId } + } +} diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ActiveChatHandoffTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ActiveChatHandoffTest.kt index 5647d6b0c3..bb8d229e44 100644 --- a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ActiveChatHandoffTest.kt +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ActiveChatHandoffTest.kt @@ -122,6 +122,7 @@ class ActiveChatHandoffTest { networkObserver = mockk(relaxed = true), dispatchers = TestDispatchers(TestCoroutineScheduler()), groupFeedDelegate = mockk(relaxed = true), + reactionsDelegate = mockk(relaxed = true), ) } diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorEagerBalanceTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorEagerBalanceTest.kt index bd82907572..e738df966c 100644 --- a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorEagerBalanceTest.kt +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorEagerBalanceTest.kt @@ -135,6 +135,7 @@ class ChatCoordinatorEagerBalanceTest { networkObserver = mockk(relaxed = true), dispatchers = testDispatchers, groupFeedDelegate = mockk(relaxed = true), + reactionsDelegate = mockk(relaxed = true), ) } diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorEventsTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorEventsTest.kt index 5716d1025f..858cfd7200 100644 --- a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorEventsTest.kt +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorEventsTest.kt @@ -15,7 +15,9 @@ import com.flipcash.services.models.chat.ChatMessage import com.flipcash.services.models.chat.ChatMutation import com.flipcash.services.models.chat.ChatUpdate import com.flipcash.services.models.chat.Emoji +import com.flipcash.services.models.chat.EmojiReaction import com.flipcash.services.models.chat.MessageContent +import com.flipcash.services.models.chat.ReactionSummary import com.flipcash.services.models.chat.ReactionUpdate import com.flipcash.shared.chat.internal.ChatIdGenerator import com.flipcash.shared.chat.internal.ChatStateHolder @@ -138,6 +140,7 @@ class ChatCoordinatorEventsTest { networkObserver = mockk(relaxed = true), dispatchers = testDispatchers, groupFeedDelegate = mockk(relaxed = true), + reactionsDelegate = mockk(relaxed = true), ) } @@ -302,7 +305,7 @@ class ChatCoordinatorEventsTest { actor = otherId, action = ReactionUpdate.Action.ADDED, count = 1, - sequence = 1, + version = 1, reactedAt = Instant.fromEpochSeconds(1000), ), ), @@ -337,7 +340,7 @@ class ChatCoordinatorEventsTest { actor = otherId, action = ReactionUpdate.Action.ADDED, count = 3, - sequence = 5, + version = 5, reactedAt = Instant.fromEpochSeconds(1000), ), ), @@ -355,7 +358,7 @@ class ChatCoordinatorEventsTest { actor = otherId, action = ReactionUpdate.Action.REMOVED, count = 1, - sequence = 2, // older than 5 + version = 2, // older than 5 reactedAt = Instant.fromEpochSeconds(500), ), ), @@ -367,7 +370,7 @@ class ChatCoordinatorEventsTest { assertNotNull(reactions) assertEquals(1, reactions.size) assertEquals(3L, reactions[0].count) // stayed at 3, stale update rejected - assertEquals(5L, reactions[0].sequence) + assertEquals(5L, reactions[0].version) } } @@ -386,7 +389,7 @@ class ChatCoordinatorEventsTest { actor = otherId, action = ReactionUpdate.Action.ADDED, count = 1, - sequence = 1, + version = 1, reactedAt = Instant.fromEpochSeconds(1000), ), ), @@ -404,7 +407,7 @@ class ChatCoordinatorEventsTest { actor = otherId, action = ReactionUpdate.Action.REMOVED, count = 0, - sequence = 2, + version = 2, reactedAt = Instant.fromEpochSeconds(2000), ), ), @@ -418,6 +421,77 @@ class ChatCoordinatorEventsTest { } } + /** + * The reducer's tombstone: a removal-to-zero keeps the emoji's version rather than dropping + * it, so a stale ADDED that arrives after the removal (redelivered, or racing on the wire) + * cannot resurrect the pill. The old ad hoc overlay merge had no tombstone — it forgot the + * emoji's version as soon as the count hit zero — so a late stale ADDED like this one would + * have been accepted. + */ + @Test + fun `stale ADDED after a removal-to-zero is ignored`() = runTest(testDispatchers.dispatcher) { + tornDown { + triggerCollection() + + chatUpdatesChannel.send(ChatUpdate( + chatId = chatId, + reactionUpdates = listOf( + ReactionUpdate( + messageId = 1L, + emoji = Emoji("😀"), + actor = otherId, + action = ReactionUpdate.Action.ADDED, + count = 1, + version = 1, + reactedAt = Instant.fromEpochSeconds(1000), + ), + ), + )) + advanceTimeBy(500.milliseconds) + runCurrent() + + // Removal takes the count to zero at version 2. + chatUpdatesChannel.send(ChatUpdate( + chatId = chatId, + reactionUpdates = listOf( + ReactionUpdate( + messageId = 1L, + emoji = Emoji("😀"), + actor = otherId, + action = ReactionUpdate.Action.REMOVED, + count = 0, + version = 2, + reactedAt = Instant.fromEpochSeconds(2000), + ), + ), + )) + advanceTimeBy(500.milliseconds) + runCurrent() + + // A stale ADDED redelivered at version 1 must not resurrect the pill. + chatUpdatesChannel.send(ChatUpdate( + chatId = chatId, + reactionUpdates = listOf( + ReactionUpdate( + messageId = 1L, + emoji = Emoji("😀"), + actor = otherId, + action = ReactionUpdate.Action.ADDED, + count = 1, + version = 1, + reactedAt = Instant.fromEpochSeconds(1000), + ), + ), + )) + advanceTimeBy(500.milliseconds) + runCurrent() + + val reactions = coordinator.state.value.reactionOverlays[chatId]?.get(1L)?.reactions + assertNotNull(reactions) + assertTrue(reactions.isEmpty()) + } + } + @Test fun `multiple emoji reactions on same message`() = runTest(testDispatchers.dispatcher) { tornDown { @@ -432,7 +506,7 @@ class ChatCoordinatorEventsTest { actor = otherId, action = ReactionUpdate.Action.ADDED, count = 2, - sequence = 1, + version = 1, reactedAt = Instant.fromEpochSeconds(1000), ), ReactionUpdate( @@ -441,7 +515,7 @@ class ChatCoordinatorEventsTest { actor = otherId, action = ReactionUpdate.Action.ADDED, count = 5, - sequence = 2, + version = 2, reactedAt = Instant.fromEpochSeconds(1000), ), ), @@ -455,6 +529,84 @@ class ChatCoordinatorEventsTest { } } + /** A stream reaction update is persisted, not just held in the in-memory overlay. */ + @Test + fun `reaction update is persisted to the message data source`() = runTest(testDispatchers.dispatcher) { + tornDown { + triggerCollection() + + chatUpdatesChannel.send(ChatUpdate( + chatId = chatId, + reactionUpdates = listOf( + ReactionUpdate( + messageId = 1L, + emoji = Emoji("😀"), + actor = otherId, + action = ReactionUpdate.Action.ADDED, + count = 1, + version = 1, + reactedAt = Instant.fromEpochSeconds(1000), + ), + ), + )) + advanceTimeBy(500.milliseconds) + runCurrent() + + coVerify { + messageDataSource.mergeReactions(chatId, 1L, match { summary -> + summary.reactions.singleOrNull()?.let { it.emoji.value == "😀" && it.count == 1L } == true + }) + } + } + } + + /** + * A stream update for a message whose reactions are already stored must not tombstone the + * stored emoji the update doesn't mention: the write is merged as a full summary, and a + * tombstone at the stored version can't be undone by a later summary at that same version. + */ + @Test + fun `reaction update keeps the stored emoji it doesn't mention`() = runTest(testDispatchers.dispatcher) { + tornDown { + coEvery { messageDataSource.getMessage(chatId, 1L) } returns textMessage(1L).copy( + reactions = ReactionSummary( + messageId = 1L, + reactions = listOf( + EmojiReaction(emoji = Emoji("❤️"), count = 1, selfReactor = null, sampleReactors = emptyList(), version = 5), + EmojiReaction(emoji = Emoji("😂"), count = 2, selfReactor = null, sampleReactors = emptyList(), version = 3), + ), + ), + ) + triggerCollection() + + chatUpdatesChannel.send(ChatUpdate( + chatId = chatId, + reactionUpdates = listOf( + ReactionUpdate( + messageId = 1L, + emoji = Emoji("🔥"), + actor = otherId, + action = ReactionUpdate.Action.ADDED, + count = 1, + version = 1, + reactedAt = Instant.fromEpochSeconds(1000), + ), + ), + )) + advanceTimeBy(500.milliseconds) + runCurrent() + + coVerify { + messageDataSource.mergeReactions(chatId, 1L, match { summary -> + summary.reactions.associate { it.emoji.value to it.count } == + mapOf("❤️" to 1L, "😂" to 2L, "🔥" to 1L) + }) + } + val overlay = coordinator.state.value.reactionOverlays[chatId]?.get(1L)?.reactions + assertEquals(setOf("❤️", "😂", "🔥"), overlay?.map { it.emoji.value }?.toSet()) + } + } + // endregion // region Session lifecycle diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorLifecycleStreamTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorLifecycleStreamTest.kt index 1e5be5a24f..189856658a 100644 --- a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorLifecycleStreamTest.kt +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorLifecycleStreamTest.kt @@ -149,6 +149,7 @@ class ChatCoordinatorLifecycleStreamTest { networkObserver = networkObserver, dispatchers = testDispatchers, groupFeedDelegate = mockk(relaxed = true), + reactionsDelegate = mockk(relaxed = true), ) } diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorTeardownTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorTeardownTest.kt index e648ff7a99..290dcdf25a 100644 --- a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorTeardownTest.kt +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatCoordinatorTeardownTest.kt @@ -118,6 +118,7 @@ class ChatCoordinatorTeardownTest { networkObserver = mockk(relaxed = true), dispatchers = TestDispatchers(TestCoroutineScheduler()), groupFeedDelegate = mockk(relaxed = true), + reactionsDelegate = mockk(relaxed = true), ) } diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatHistoryHydrationTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatHistoryHydrationTest.kt index 1c90761e4e..7c24fe5002 100644 --- a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatHistoryHydrationTest.kt +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatHistoryHydrationTest.kt @@ -148,6 +148,7 @@ class ChatHistoryHydrationTest { networkObserver = mockk(relaxed = true), dispatchers = TestDispatchers(testScheduler), groupFeedDelegate = mockk(relaxed = true), + reactionsDelegate = mockk(relaxed = true), ) } diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatMetadataEditStreamTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatMetadataEditStreamTest.kt index cebb67b544..719da56c1d 100644 --- a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatMetadataEditStreamTest.kt +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ChatMetadataEditStreamTest.kt @@ -133,6 +133,7 @@ class ChatMetadataEditStreamTest { networkObserver = mockk(relaxed = true), dispatchers = testDispatchers, groupFeedDelegate = mockk(relaxed = true), + reactionsDelegate = mockk(relaxed = true), ) } diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/GroupChatRoutingTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/GroupChatRoutingTest.kt index cf6ece956d..b9a8d3b64e 100644 --- a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/GroupChatRoutingTest.kt +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/GroupChatRoutingTest.kt @@ -125,6 +125,7 @@ class GroupChatRoutingTest { senderResolver = mockk(relaxed = true), ), groupFeedDelegate = groupFeedDelegate, + reactionsDelegate = mockk(relaxed = true), stateHolder = stateHolder, draftStore = mockk(relaxed = true), userManager = userManager, diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/InterruptedSendRecoveryTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/InterruptedSendRecoveryTest.kt index c3eb10e547..94bf026862 100644 --- a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/InterruptedSendRecoveryTest.kt +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/InterruptedSendRecoveryTest.kt @@ -148,6 +148,7 @@ class InterruptedSendRecoveryTest { networkObserver = mockk(relaxed = true), dispatchers = testDispatchers, groupFeedDelegate = mockk(relaxed = true), + reactionsDelegate = mockk(relaxed = true), ) } diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/MessageCapabilityTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/MessageCapabilityTest.kt index f8d348756b..aa51bc0cd3 100644 --- a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/MessageCapabilityTest.kt +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/MessageCapabilityTest.kt @@ -345,4 +345,46 @@ class MessageCapabilityTest { resolved.readOnly(), ) } + + // canReact — reactions have no window and no report-only carve-out, so the only thing they + // share with resolveCapabilities is which messages don't exist as far as an action is + // concerned: tombstones, system notices, and unconfirmed sends. + @Test + fun `own and another participant's text are both reactable`() { + assertEquals(true, canReact(text())) + assertEquals(true, canReact(text(isFromSelf = false))) + } + + @Test + fun `own and another participant's cash are both reactable`() { + assertEquals(true, canReact(cash())) + assertEquals(true, canReact(cash(isFromSelf = false))) + } + + @Test + fun `an old message stays reactable, unlike edit or delete`() { + assertEquals(true, canReact(text(isFromSelf = false))) + } + + @Test + fun `a tombstone is not reactable`() { + val deleted = message(listOf(MessageContent.Deleted(sentAt, selfId))) + assertEquals(false, canReact(deleted)) + } + + @Test + fun `a system notice is not reactable`() { + val system = message(listOf(MessageContent.System("Anna joined"))) + assertEquals(false, canReact(system)) + } + + @Test + fun `an unconfirmed send is not reactable`() { + assertEquals(false, canReact(text(eventSequence = 0))) + } + + @Test + fun `empty content is not reactable`() { + assertEquals(false, canReact(message(emptyList()))) + } } diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/MessageReactionsTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/MessageReactionsTest.kt new file mode 100644 index 0000000000..1cb3248dc6 --- /dev/null +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/MessageReactionsTest.kt @@ -0,0 +1,74 @@ +package com.flipcash.shared.chat + +import com.flipcash.services.models.chat.Emoji +import com.flipcash.services.models.chat.EmojiReaction +import com.flipcash.services.models.chat.ReactionSummary +import com.flipcash.services.models.chat.Reactor +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Instant + +/** + * Covers [MessageReactions.from] — the pure fallback the ViewModel applies to a [ChatMessage]'s + * persisted `reactions` for a message [ReactionOperations.observeChatReactions] has no live + * override for (see that function's KDoc for the full contract). + */ +class MessageReactionsTest { + + @Test + fun `from builds pills from a persisted summary with no live overrides`() { + val summary = ReactionSummary( + messageId = 1L, + reactions = listOf( + EmojiReaction( + emoji = Emoji("👍"), + count = 2, + selfReactor = Reactor( + userId = listOf(1), + reactedAt = Instant.fromEpochSeconds(10), + version = 1, + ), + sampleReactors = emptyList(), + version = 1, + ), + ), + ) + + val reactions = MessageReactions.from(summary) + + assertEquals(1, reactions.pills.size) + assertEquals("👍", reactions.pills.first().emoji) + assertEquals(2, reactions.pills.first().count) + assertTrue(reactions.pills.first().selfReacted) + assertEquals(listOf("👍"), reactions.selfReactions.map { it.emoji }) + } + + @Test + fun `from a null summary is empty`() { + val reactions = MessageReactions.from(null) + + assertTrue(reactions.pills.isEmpty()) + assertTrue(reactions.selfReactions.isEmpty()) + } + + @Test + fun `from drops a tombstoned emoji`() { + val summary = ReactionSummary( + messageId = 1L, + reactions = listOf( + EmojiReaction( + emoji = Emoji("❤️"), + count = 0, + selfReactor = null, + sampleReactors = emptyList(), + version = 3, + ), + ), + ) + + val reactions = MessageReactions.from(summary) + + assertTrue(reactions.pills.isEmpty()) + } +} diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ReactionsDelegateTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ReactionsDelegateTest.kt new file mode 100644 index 0000000000..c92adbbb90 --- /dev/null +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ReactionsDelegateTest.kt @@ -0,0 +1,215 @@ +package com.flipcash.shared.chat + +import com.flipcash.app.persistence.sources.ChatMessageDataSource +import com.flipcash.services.controllers.ChatMessagingController +import com.flipcash.services.models.AddReactionError +import com.flipcash.services.models.chat.ChatId +import com.flipcash.services.models.chat.ChatMessage +import com.flipcash.services.models.chat.Emoji +import com.flipcash.services.models.chat.EmojiReaction +import com.flipcash.services.models.chat.ReactionSummary +import com.flipcash.services.models.chat.Reactor +import com.flipcash.services.repository.ReactorsPage +import com.flipcash.services.user.UserManager +import com.flipcash.shared.chat.internal.ChatStateHolder +import com.flipcash.shared.chat.internal.delegates.ReactionsDelegate +import com.getcode.libs.emojis.reactions.RecentReactionsStore +import com.getcode.opencode.model.core.ID +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.yield +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.time.Instant + +class ReactionsDelegateTest { + + private lateinit var messagingController: ChatMessagingController + private lateinit var messageDataSource: ChatMessageDataSource + private lateinit var recentReactionsStore: RecentReactionsStore + private lateinit var stateHolder: ChatStateHolder + private lateinit var delegate: ReactionsDelegate + + private val chatId: ChatId = ChatId("aabbccdd") + private val selfId: ID = "self-user-id".encodeToByteArray().toList() + private val emoji = "😀" + + @Before + fun setup() { + messagingController = mockk(relaxed = true) + messageDataSource = mockk(relaxed = true) + recentReactionsStore = mockk(relaxed = true) + stateHolder = ChatStateHolder() + val userManager = mockk(relaxed = true) + every { userManager.accountId } returns selfId + + delegate = ReactionsDelegate( + messagingController = messagingController, + messageDataSource = messageDataSource, + recentReactionsStore = recentReactionsStore, + userManager = userManager, + stateHolder = stateHolder, + ) + } + + @Test + fun `toggle success adds a self pill, persists, and records recents`() = runTest { + coEvery { messagingController.addReaction(chatId, 1L, Emoji(emoji)) } returns Result.success( + EmojiReaction( + emoji = Emoji(emoji), + count = 1, + selfReactor = Reactor(userId = selfId, reactedAt = Instant.fromEpochSeconds(1000), version = 1), + sampleReactors = emptyList(), + version = 1, + ) + ) + + delegate.toggleReaction(chatId, 1L, emoji) + + val reactions = delegate.observeChatReactions(chatId).first() + val pills = reactions[1L]?.pills + assertEquals(1, pills?.size) + assertEquals(emoji, pills?.first()?.emoji) + assertTrue(pills?.first()?.selfReacted == true) + + coVerify { recentReactionsStore.record(emoji) } + coVerify { messageDataSource.mergeReactions(chatId, 1L, any()) } + } + + @Test + fun `toggle failure rolls back the pill and reports a non-silent error`() = runTest { + coEvery { messagingController.addReaction(chatId, 1L, Emoji(emoji)) } returns + Result.failure(AddReactionError.Denied()) + + var error: com.flipcash.shared.chat.reactions.ReactionError? = null + val collector = launch { error = delegate.reactionErrors.first() } + yield() + + delegate.toggleReaction(chatId, 1L, emoji) + collector.join() + + // Denied maps to REACTION_FAILED via ReactionFailure.userError. + assertEquals(com.flipcash.shared.chat.reactions.ReactionError.REACTION_FAILED, error) + + coVerify(exactly = 0) { messageDataSource.mergeReactions(any(), any(), any()) } + // Recents count the tap, as on iOS, whether or not the server takes it. + coVerify(exactly = 1) { recentReactionsStore.record(emoji) } + } + + @Test + fun `a second tap while the first call is in flight coalesces into one follow-up call`() = runTest { + // The add call never completes within this test body — it's an uncompleted deferred result — + // so the second, opposite tap must be recorded rather than sent immediately. + val addDeferred = CompletableDeferred>() + coEvery { messagingController.addReaction(chatId, 1L, Emoji(emoji)) } coAnswers { addDeferred.await() } + coEvery { messagingController.removeReaction(chatId, 1L, Emoji(emoji)) } returns Result.success( + EmojiReaction(emoji = Emoji(emoji), count = 0, selfReactor = null, sampleReactors = emptyList(), version = 2) + ) + + val firstTap = launch { delegate.toggleReaction(chatId, 1L, emoji) } + yield() + // Second tap: toggles back off while the ADD is still in flight — must not send REMOVE yet. + delegate.toggleReaction(chatId, 1L, emoji) + coVerify(exactly = 0) { messagingController.removeReaction(any(), any(), any()) } + + addDeferred.complete( + Result.success( + EmojiReaction( + emoji = Emoji(emoji), + count = 1, + selfReactor = Reactor(userId = selfId, reactedAt = Instant.fromEpochSeconds(1000), version = 1), + sampleReactors = emptyList(), + version = 1, + ) + ) + ) + firstTap.join() + + // The coalesced REMOVE follow-up fires once the ADD settles. + coVerify { messagingController.removeReaction(chatId, 1L, Emoji(emoji)) } + } + + @Test + fun `refresh merges server summaries into the persisted store and the overlay`() = runTest { + val summary = ReactionSummary( + messageId = 1L, + reactions = listOf( + EmojiReaction( + emoji = Emoji(emoji), + count = 3, + selfReactor = null, + sampleReactors = emptyList(), + version = 5, + ) + ), + ) + coEvery { messagingController.getReactionSummariesByIds(chatId, listOf(1L)) } returns + Result.success(listOf(summary)) + + val result = delegate.refreshReactions(chatId, listOf(1L)) + + assertTrue(result.isSuccess) + coVerify { messageDataSource.mergeReactions(chatId, 1L, any()) } + val overlay = stateHolder.current.reactionOverlays[chatId]?.get(1L) + assertEquals(3, overlay?.reactions?.first()?.count) + } + + /** + * After relaunch, a confirmed self-reaction lives only in Room — [ChatStateHolder]'s in-memory + * overlay is empty. Tapping that emoji must still compute a REMOVE, not an ADD: `snapshot` has + * to seed the fresh [com.flipcash.shared.chat.reactions.ReactionState] from the stored summary, + * not just from the overlay. + */ + @Test + fun `toggling an emoji already reacted to in Room, with no overlay, sends a remove`() = runTest { + coEvery { messageDataSource.getMessage(chatId, 1L) } returns ChatMessage( + messageId = 1L, + senderId = selfId, + content = emptyList(), + timestamp = Instant.fromEpochSeconds(1000), + unreadSeq = 0, + reactions = ReactionSummary( + messageId = 1L, + reactions = listOf( + EmojiReaction( + emoji = Emoji(emoji), + count = 1, + selfReactor = Reactor(userId = selfId, reactedAt = Instant.fromEpochSeconds(1000), version = 1), + sampleReactors = emptyList(), + version = 1, + ) + ), + ), + ) + coEvery { messagingController.removeReaction(chatId, 1L, Emoji(emoji)) } returns Result.success( + EmojiReaction(emoji = Emoji(emoji), count = 0, selfReactor = null, sampleReactors = emptyList(), version = 2) + ) + + delegate.toggleReaction(chatId, 1L, emoji) + + coVerify { messagingController.removeReaction(chatId, 1L, Emoji(emoji)) } + coVerify(exactly = 0) { messagingController.addReaction(any(), any(), any()) } + // Taking a reaction back is not a use of it. + coVerify(exactly = 0) { recentReactionsStore.record(any()) } + } + + @Test + fun `getReactorsPage passes through to the controller`() = runTest { + coEvery { messagingController.getReactors(chatId, 1L, Emoji(emoji), any()) } returns + Result.success(ReactorsPage(reactors = emptyList(), hasMore = false)) + + val result = delegate.getReactorsPage(chatId, 1L, emoji) + + assertTrue(result.isSuccess) + assertNull(result.getOrNull()?.nextToken) + } +} diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ReceivedCounterTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ReceivedCounterTest.kt index 8ba346eeba..91677838ea 100644 --- a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ReceivedCounterTest.kt +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ReceivedCounterTest.kt @@ -156,6 +156,7 @@ class ReceivedCounterTest { networkObserver = mockk(relaxed = true), dispatchers = testDispatchers, groupFeedDelegate = mockk(relaxed = true), + reactionsDelegate = mockk(relaxed = true), ) } diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ReceivedEventTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ReceivedEventTest.kt index 0717b984b1..3f86b24c7e 100644 --- a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ReceivedEventTest.kt +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ReceivedEventTest.kt @@ -162,6 +162,7 @@ class ReceivedEventTest { networkObserver = mockk(relaxed = true), dispatchers = testDispatchers, groupFeedDelegate = mockk(relaxed = true), + reactionsDelegate = mockk(relaxed = true), ) } diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ViewerStateStreamTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ViewerStateStreamTest.kt index f7d535cf7e..65c5d085f7 100644 --- a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ViewerStateStreamTest.kt +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/ViewerStateStreamTest.kt @@ -136,6 +136,7 @@ class ViewerStateStreamTest { networkObserver = mockk(relaxed = true), dispatchers = testDispatchers, groupFeedDelegate = mockk(relaxed = true), + reactionsDelegate = mockk(relaxed = true), ) } diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactionRefreshPlannerTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactionRefreshPlannerTest.kt new file mode 100644 index 0000000000..afecbb764f --- /dev/null +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactionRefreshPlannerTest.kt @@ -0,0 +1,63 @@ +package com.flipcash.shared.chat.reactions + +import org.junit.Assert.assertEquals +import org.junit.Test + +class ReactionRefreshPlannerTest { + + @Test + fun `initial window caps at window size`() { + val ids = (1L..100L).toList() + val window = ReactionRefreshPlanner.initialWindow(ids, windowSize = 60) + assertEquals(60, window.size) + assertEquals(ids.take(60), window) + } + + @Test + fun `initial window under window size returns all`() { + val ids = (1L..10L).toList() + assertEquals(ids, ReactionRefreshPlanner.initialWindow(ids, windowSize = 60)) + } + + @Test + fun `initial window excludes already refreshed ids`() { + val ids = listOf(1L, 2L, 3L, 4L, 5L) + val window = ReactionRefreshPlanner.initialWindow(ids, alreadyRefreshed = setOf(2L, 4L), windowSize = 60) + assertEquals(listOf(1L, 3L, 5L), window) + } + + @Test + fun `loaded page from server is skipped entirely`() { + val ids = (1L..100L).toList() + val chunks = ReactionRefreshPlanner.forLoadedPage(ids, sourcedFromServer = true) + assertEquals(emptyList>(), chunks) + } + + @Test + fun `loaded page from room is chunked`() { + val ids = (1L..100L).toList() + val chunks = ReactionRefreshPlanner.forLoadedPage(ids, sourcedFromServer = false, chunkSize = 40) + assertEquals(3, chunks.size) + assertEquals(40, chunks[0].size) + assertEquals(40, chunks[1].size) + assertEquals(20, chunks[2].size) + assertEquals(ids, chunks.flatten()) + } + + @Test + fun `loaded page excludes already refreshed and drops empty result`() { + val ids = listOf(1L, 2L, 3L) + val chunks = ReactionRefreshPlanner.forLoadedPage( + ids, + sourcedFromServer = false, + alreadyRefreshed = setOf(1L, 2L, 3L), + ) + assertEquals(emptyList>(), chunks) + } + + @Test + fun `loaded page empty input yields no chunks`() { + val chunks = ReactionRefreshPlanner.forLoadedPage(emptyList(), sourcedFromServer = false) + assertEquals(emptyList>(), chunks) + } +} diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactionStripComposerTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactionStripComposerTest.kt new file mode 100644 index 0000000000..b7b2ffa18c --- /dev/null +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactionStripComposerTest.kt @@ -0,0 +1,100 @@ +package com.flipcash.shared.chat.reactions + +import com.getcode.libs.emojis.reactions.RecentReactions +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Instant + +class ReactionStripComposerTest { + + private val catalogFillSource = listOf("😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃", "😉", "😊", "😇", "😍") + + @Test + fun `with no history the strip is the six defaults padded from the catalog`() { + val entries = ReactionStripComposer.compose( + recentStats = emptyMap(), + selfReactions = emptyList(), + catalogFillSource = catalogFillSource, + ) + + assertEquals(12, entries.size) + assertEquals(RecentReactions.DEFAULTS, entries.take(6).map { it.emoji }) + assertTrue(entries.none { it.highlighted }) + // The fill is catalog emoji not already offered, in the catalog's own order. + val fill = entries.drop(6).map { it.emoji } + assertEquals(catalogFillSource.filter { it !in RecentReactions.DEFAULTS }.take(6), fill) + } + + @Test + fun `undrawable defaults are dropped, not replaced by a most-used or filler emoji in their slot`() { + val undrawable = setOf(RecentReactions.DEFAULTS[1]) + + val entries = ReactionStripComposer.compose( + recentStats = emptyMap(), + selfReactions = emptyList(), + catalogFillSource = catalogFillSource, + undrawable = undrawable, + ) + + assertTrue(entries.none { it.emoji in undrawable }) + assertEquals(RecentReactions.DEFAULTS.filter { it !in undrawable }, entries.take(5).map { it.emoji }) + } + + @Test + fun `most-used emoji beyond the defaults fill the next six slots, ranked`() { + val stats = mapOf( + "🥳" to RecentReactions.Usage(count = 10, lastUsed = Instant.fromEpochSeconds(100)), + "🤔" to RecentReactions.Usage(count = 5, lastUsed = Instant.fromEpochSeconds(200)), + ) + + val entries = ReactionStripComposer.compose( + recentStats = stats, + selfReactions = emptyList(), + catalogFillSource = catalogFillSource, + ) + + val afterDefaults = entries.drop(6).map { it.emoji } + assertEquals(listOf("🥳", "🤔"), afterDefaults.take(2)) + } + + @Test + fun `a self reaction outside the defaults and most-used is always included, highlighted`() { + val entries = ReactionStripComposer.compose( + recentStats = emptyMap(), + selfReactions = listOf(SelfReaction(emoji = "🦄", reactedAt = Instant.fromEpochSeconds(1))), + catalogFillSource = catalogFillSource, + ) + + val unicorn = entries.firstOrNull { it.emoji == "🦄" } + assertTrue(unicorn != null, "self reaction was dropped from the strip") + assertTrue(unicorn.highlighted) + } + + @Test + fun `never returns more than 12 entries`() { + val manyStats = (1..20).associate { "emoji$it" to RecentReactions.Usage(count = it, lastUsed = Instant.fromEpochSeconds(it.toLong())) } + val manySelf = (1..20).map { SelfReaction(emoji = "self$it", reactedAt = Instant.fromEpochSeconds(it.toLong())) } + + val entries = ReactionStripComposer.compose( + recentStats = manyStats, + selfReactions = manySelf, + catalogFillSource = catalogFillSource, + ) + + assertEquals(12, entries.size) + } + + @Test + fun `a default already ranked as most-used is not duplicated`() { + val stats = mapOf(RecentReactions.DEFAULTS[0] to RecentReactions.Usage(count = 100, lastUsed = Instant.fromEpochSeconds(1))) + + val entries = ReactionStripComposer.compose( + recentStats = stats, + selfReactions = emptyList(), + catalogFillSource = catalogFillSource, + ) + + assertEquals(1, entries.count { it.emoji == RecentReactions.DEFAULTS[0] }) + } +} diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactionVectorsTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactionVectorsTest.kt new file mode 100644 index 0000000000..d8ed1c156b --- /dev/null +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactionVectorsTest.kt @@ -0,0 +1,223 @@ +package com.flipcash.shared.chat.reactions + +import org.json.JSONArray +import org.json.JSONObject +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Instant + +/** + * `test-vectors/reactions.json`, read in place through this module's test resources. It is a + * verbatim copy of the orchestrator's canonical fixture (#17). A failure here is either a real regression or a cross-platform decision that + * has to be made in the canonical fixture and re-synced to both platforms — never a local edit. + * + * Covers the `merge`, `order` and `strip` sections; `defaults` and `recents` are covered by + * `libs:emojis`'s `RecentReactionsVectorTest`, and `drawability` by its instrumented + * `EmojiDrawabilityVectorTest`. + * + * `merge` replays each vector's steps against a fresh [ReactionState] — the same state machine as + * iOS's `FlipcashCore` `ReactionState` and `gen_reactions.py`'s reference `Model` — and checks the + * resulting pills, the calls it would send, and the errors it would show. + */ +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class ReactionVectorsTest { + + @Test + fun `merge vectors match the reactions fixture`() { + val vectors = section("merge") + assertTrue(vectors.length() > 0, "reactions.json loaded no merge vectors") + + for (i in 0 until vectors.length()) { + val vector = vectors.getJSONObject(i) + val name = vector.getString("name") + val note = vector.getString("note") + val self = vector.getString("self") + + val state = ReactionState() + val calls = mutableListOf() + val errors = mutableListOf() + + val steps = vector.getJSONArray("steps") + for (s in 0 until steps.length()) { + val step = steps.getJSONObject(s) + when (val op = step.getString("op")) { + "summary" -> { + val reactions = step.getJSONArray("reactions") + val entries = (0 until reactions.length()).map { k -> + val r = reactions.getJSONObject(k) + ReactionState.SummaryEntry( + emoji = r.getString("emoji"), + count = r.getLong("count"), + selfReacted = r.getBoolean("self"), + version = r.getLong("version"), + selfReactedAt = null, + ) + } + state.applySummary(entries) + } + + "update" -> { + state.applyUpdate( + emoji = step.getString("emoji"), + actorIsSelf = step.getString("actor") == self, + added = step.getString("action") == "ADDED", + count = step.getLong("count"), + version = step.getLong("version"), + reactedAt = EPOCH, + ) + } + + "tap" -> { + val call = state.tap(step.getString("emoji"), at = EPOCH) + if (call != null) calls.add(call) + } + + "respond" -> { + val result = step.getString("result") + val outcome = if (result == "OK") { + val reaction = step.getJSONObject("reaction") + ReactionResult.Ok( + count = reaction.getLong("count"), + selfReacted = reaction.getBoolean("self"), + version = reaction.getLong("version"), + selfReactedAt = null, + ) + } else { + ReactionResult.Failed(failureOf(result)) + } + val (call, error) = state.respond(step.getString("emoji"), outcome) + if (call != null) calls.add(call) + if (error != null) errors.add(error) + } + + else -> error("unknown op `$op`") + } + } + + val expect = vector.getJSONObject("expect") + assertPills(expect.getJSONArray("pills"), state.pills, name, note) + assertCalls(expect.getJSONArray("calls"), calls, name, note) + assertErrors(expect.getJSONArray("errors"), errors, name, note) + } + } + + @Test + fun `order vectors match the reactions fixture`() { + val vectors = section("order") + assertTrue(vectors.length() > 0, "reactions.json loaded no order vectors") + + for (i in 0 until vectors.length()) { + val vector = vectors.getJSONObject(i) + val name = vector.getString("name") + val note = vector.getString("note") + + val pillsJson = vector.getJSONArray("pills") + val pills = (0 until pillsJson.length()).map { k -> + val p = pillsJson.getJSONObject(k) + val boostTotal = p.getLong("boostTotal") + ReactionPill( + emoji = p.getString("emoji"), + count = p.getLong("count"), + selfReacted = false, + pending = false, + boost = if (boostTotal != 0L) ReactionBoost(boostTotal) else null, + ) + } + + val expected = vector.getJSONArray("expect") + val expectedOrder = (0 until expected.length()).map { expected.getString(it) } + val actualOrder = ReactionOrdering.sorted(pills).map { it.emoji } + assertEquals(expectedOrder, actualOrder, "vector `$name`: $note") + } + } + + @Test + fun `strip vectors match the reactions fixture`() { + val vectors = section("strip") + assertTrue(vectors.length() > 0, "reactions.json loaded no strip vectors") + + for (i in 0 until vectors.length()) { + val vector = vectors.getJSONObject(i) + val name = vector.getString("name") + val note = vector.getString("note") + + val recentsJson = vector.getJSONArray("recents") + val recents = (0 until recentsJson.length()).map { recentsJson.getString(it) } + + val selfReactionsJson = vector.getJSONArray("selfReactions") + val selfReactions = (0 until selfReactionsJson.length()).map { k -> + val r = selfReactionsJson.getJSONObject(k) + SelfReaction(emoji = r.getString("emoji"), reactedAt = Instant.fromEpochSeconds(r.getLong("reactedAt"))) + } + + val expectJson = vector.getJSONArray("expect") + val expected = (0 until expectJson.length()).map { k -> + val e = expectJson.getJSONObject(k) + ReactionStrip.Entry(emoji = e.getString("emoji"), highlighted = e.getBoolean("highlighted")) + } + + val actual = ReactionStrip.entries(recents, selfReactions) + assertEquals(expected, actual, "vector `$name`: $note") + } + } + + private fun assertPills(expected: JSONArray, actual: List, name: String, note: String) { + assertEquals(expected.length(), actual.size, "vector `$name` pill count: $note") + for (k in 0 until expected.length()) { + val e = expected.getJSONObject(k) + val a = actual[k] + assertEquals(e.getString("emoji"), a.emoji, "vector `$name` pill[$k].emoji: $note") + assertEquals(e.getLong("count"), a.count, "vector `$name` pill[$k].count: $note") + assertEquals(e.getBoolean("selfReacted"), a.selfReacted, "vector `$name` pill[$k].selfReacted: $note") + assertEquals(e.getBoolean("pending"), a.pending, "vector `$name` pill[$k].pending: $note") + } + } + + private fun assertCalls(expected: JSONArray, actual: List, name: String, note: String) { + assertEquals(expected.length(), actual.size, "vector `$name` call count: $note") + for (k in 0 until expected.length()) { + val e = expected.getJSONObject(k) + val a = actual[k] + val expectedOp = if (e.getString("op") == "add") ReactionCall.Op.ADD else ReactionCall.Op.REMOVE + assertEquals(expectedOp, a.op, "vector `$name` call[$k].op: $note") + assertEquals(e.getString("emoji"), a.emoji, "vector `$name` call[$k].emoji: $note") + } + } + + private fun assertErrors(expected: JSONArray, actual: List, name: String, note: String) { + assertEquals(expected.length(), actual.size, "vector `$name` error count: $note") + for (k in 0 until expected.length()) { + val expectedError = when (val wire = expected.getString(k)) { + "reactionFailed" -> ReactionError.REACTION_FAILED + "tooManyReactionTypes" -> ReactionError.TOO_MANY_REACTION_TYPES + else -> error("unknown error `$wire`") + } + assertEquals(expectedError, actual[k], "vector `$name` error[$k]: $note") + } + } + + private fun failureOf(result: String): ReactionFailure = when (result) { + "NETWORK" -> ReactionFailure.NETWORK + "DENIED" -> ReactionFailure.DENIED + "MESSAGE_NOT_FOUND" -> ReactionFailure.MESSAGE_NOT_FOUND + "CANNOT_REACT" -> ReactionFailure.CANNOT_REACT + "TOO_MANY_REACTION_TYPES" -> ReactionFailure.TOO_MANY_REACTION_TYPES + else -> error("unknown result `$result`") + } + + private fun section(name: String): JSONArray { + val json = javaClass.classLoader!! + .getResourceAsStream("reactions.json")!! + .bufferedReader().use { it.readText() } + return JSONObject(json).getJSONArray(name) + } + + private companion object { + val EPOCH: Instant = Instant.fromEpochSeconds(0) + } +} diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactorNameResolverTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactorNameResolverTest.kt new file mode 100644 index 0000000000..664f062f8b --- /dev/null +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactorNameResolverTest.kt @@ -0,0 +1,122 @@ +package com.flipcash.shared.chat.reactions + +import com.flipcash.services.models.UserProfile +import com.flipcash.services.models.chat.ChatMember +import com.getcode.opencode.model.core.ID +import com.getcode.utils.hexEncodedString +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +private fun id(byte: Int): ID = listOf(byte.toByte()) + +private fun profile(name: String = "", username: String? = null): UserProfile = UserProfile( + displayName = name, + socialAccounts = emptyList(), + phoneNumber = null, + email = null, + username = username, +) + +private fun member(userId: ID, name: String) = ChatMember( + userId = userId, + userProfile = profile(name), + pointers = emptyList(), +) + +class ReactorNameResolverTest { + + private val self = id(1) + private val alice = id(2) + + @Test + fun `self always resolves to the self label, ignoring every other source`() { + val resolved = ReactorNameResolver.resolve( + userId = self, + selfUserId = self, + selfLabel = "You", + cachedProfile = profile("Self Vanity Name"), + members = listOf(member(self, "Self Member Name")), + senderProfiles = mapOf(self.hexEncodedString() to profile("Self Sender Name")), + ) + assertEquals("You", resolved) + } + + @Test + fun `cached profile wins over the member roster and sender profiles`() { + val resolved = ReactorNameResolver.resolve( + userId = alice, + selfUserId = self, + selfLabel = "You", + cachedProfile = profile("Cached Alice"), + members = listOf(member(alice, "Member Alice")), + senderProfiles = mapOf(alice.hexEncodedString() to profile("Sender Alice")), + ) + assertEquals("Cached Alice", resolved) + } + + @Test + fun `member roster wins over sender profiles when there is no cached profile`() { + val resolved = ReactorNameResolver.resolve( + userId = alice, + selfUserId = self, + selfLabel = "You", + cachedProfile = null, + members = listOf(member(alice, "Member Alice")), + senderProfiles = mapOf(alice.hexEncodedString() to profile("Sender Alice")), + ) + assertEquals("Member Alice", resolved) + } + + @Test + fun `sender profiles resolve a reactor absent from the cache and the roster`() { + val resolved = ReactorNameResolver.resolve( + userId = alice, + selfUserId = self, + selfLabel = "You", + cachedProfile = null, + members = emptyList(), + senderProfiles = mapOf(alice.hexEncodedString() to profile("Sender Alice")), + ) + assertEquals("Sender Alice", resolved) + } + + @Test + fun `null when none of the four sources has an answer yet`() { + val resolved = ReactorNameResolver.resolve( + userId = alice, + selfUserId = self, + selfLabel = "You", + cachedProfile = null, + members = emptyList(), + senderProfiles = emptyMap(), + ) + assertNull(resolved) + } + + @Test + fun `a source with a blank name and no handle is treated as not resolving`() { + val resolved = ReactorNameResolver.resolve( + userId = alice, + selfUserId = self, + selfLabel = "You", + cachedProfile = profile(name = "", username = null), + members = listOf(member(alice, "Member Alice")), + senderProfiles = emptyMap(), + ) + assertEquals("Member Alice", resolved) + } + + @Test + fun `a blank display name falls back to the handle within the same source`() { + val resolved = ReactorNameResolver.resolve( + userId = alice, + selfUserId = self, + selfLabel = "You", + cachedProfile = profile(name = "", username = "alice"), + members = emptyList(), + senderProfiles = emptyMap(), + ) + assertEquals("@alice", resolved) + } +} diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactorsListModelTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactorsListModelTest.kt new file mode 100644 index 0000000000..b7ef4307ef --- /dev/null +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactorsListModelTest.kt @@ -0,0 +1,146 @@ +package com.flipcash.shared.chat.reactions + +import com.flipcash.services.repository.ReactorsPage +import com.flipcash.services.models.chat.Reactor +import com.getcode.opencode.model.core.ID +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.time.Instant + +private fun id(byte: Int): ID = listOf(byte.toByte()) + +class ReactorsListModelTest { + + private val alice = id(1) + private val bob = id(2) + private val carol = id(3) + + private fun reactor(userId: ID, seconds: Long) = + Reactor(userId = userId, reactedAt = Instant.fromEpochSeconds(seconds)) + + @Test + fun `merges every emoji's first page into one row per person, in pill order`() = runTest { + val model = ReactorsListModel( + emojis = listOf("❤️", "👍"), // heart, thumbsup + fetchPage = { emoji, _ -> + when (emoji) { + "❤️" -> Result.success( + ReactorsPage(reactors = listOf(reactor(alice, 10), reactor(bob, 5)), hasMore = false), + ) + else -> Result.success( + ReactorsPage(reactors = listOf(reactor(bob, 20)), hasMore = false), + ) + } + }, + ) + + model.loadMoreIfNeeded() + + assertEquals(2, model.rows.size) + val bobRow = model.rows.first { it.userId == bob } + assertEquals(listOf("❤️", "👍"), bobRow.emojis) + val aliceRow = model.rows.first { it.userId == alice } + assertEquals(listOf("❤️"), aliceRow.emojis) + } + + @Test + fun `rows sort by newest reaction first, then userId descending`() = runTest { + val model = ReactorsListModel( + emojis = listOf("❤️"), + fetchPage = { _, _ -> + Result.success( + ReactorsPage( + reactors = listOf(reactor(alice, 5), reactor(bob, 20), reactor(carol, 20)), + hasMore = false, + ), + ) + }, + ) + + model.loadMoreIfNeeded() + + // bob and carol tie at 20s -> higher userId (carol) first; alice (5s) last. + assertEquals(listOf(carol, bob, alice), model.rows.map { it.userId }) + } + + @Test + fun `publishes rows only once every emoji's round of pages is in`() = runTest { + var bobReleased = false + val model = ReactorsListModel( + emojis = listOf("❤️", "👍"), + fetchPage = { emoji, _ -> + if (emoji == "👍" && !bobReleased) { + // Simulate the slower of the two calls. + } + when (emoji) { + "❤️" -> Result.success(ReactorsPage(reactors = listOf(reactor(alice, 1)), hasMore = false)) + else -> Result.success(ReactorsPage(reactors = listOf(reactor(bob, 1)), hasMore = false)) + } + }, + ) + + assertTrue(model.rows.isEmpty()) + model.loadMoreIfNeeded() + assertEquals(2, model.rows.size) + } + + @Test + fun `a failed page stops asking for more of that emoji only`() = runTest { + var heartCalls = 0 + var thumbsCalls = 0 + val model = ReactorsListModel( + emojis = listOf("❤️", "👍"), + fetchPage = { emoji, _ -> + when (emoji) { + "❤️" -> { + heartCalls++ + Result.failure(RuntimeException("boom")) + } + else -> { + thumbsCalls++ + Result.success(ReactorsPage(reactors = listOf(reactor(bob, 1)), hasMore = true, nextToken = listOf(1))) + } + } + }, + ) + + model.loadMoreIfNeeded() + model.loadMoreIfNeeded() + + assertEquals(1, heartCalls, "should not retry a failed emoji's page") + assertEquals(2, thumbsCalls) + assertTrue(model.hasMore, "thumbsup still has more") + } + + @Test + fun `hasMore is false once every emoji is exhausted`() = runTest { + val model = ReactorsListModel( + emojis = listOf("❤️"), + fetchPage = { _, _ -> Result.success(ReactorsPage(reactors = emptyList(), hasMore = false)) }, + ) + + assertTrue(model.hasMore) + model.loadMoreIfNeeded() + assertFalse(model.hasMore) + } + + @Test + fun `does not re-fetch an emoji whose page is already loading or exhausted`() = runTest { + var calls = 0 + val model = ReactorsListModel( + emojis = listOf("❤️"), + fetchPage = { _, _ -> + calls++ + Result.success(ReactorsPage(reactors = emptyList(), hasMore = false)) + }, + ) + + model.loadMoreIfNeeded() + model.loadMoreIfNeeded() + + assertEquals(1, calls, "exhausted emoji should not be asked again") + } +} diff --git a/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactorsPrefetchCacheTest.kt b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactorsPrefetchCacheTest.kt new file mode 100644 index 0000000000..9b0ef49832 --- /dev/null +++ b/apps/flipcash/shared/chat/src/test/kotlin/com/flipcash/shared/chat/reactions/ReactorsPrefetchCacheTest.kt @@ -0,0 +1,101 @@ +package com.flipcash.shared.chat.reactions + +import com.flipcash.services.models.PagingToken +import com.flipcash.services.models.chat.Reactor +import com.flipcash.services.repository.ReactorsPage +import com.getcode.opencode.model.core.ID +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.Instant + +private fun id(byte: Int): ID = listOf(byte.toByte()) + +private fun reactor(userId: ID, reactedAt: Instant) = Reactor(userId = userId, reactedAt = reactedAt) + +class ReactorsPrefetchCacheTest { + + @Test + fun `start calls fetchPage for every emoji before the sheet would read rows`() = runTest { + val calls = mutableListOf>() + val cache = ReactorsPrefetchCache(scope = this) { messageId, emoji, _ -> + calls += messageId to emoji + Result.success(ReactorsPage(reactors = emptyList(), hasMore = false)) + } + + // OpenReactors calls start() synchronously, one step before the navigator pushes the sheet + // (see ChatViewModel's OpenReactors handler) — so by the time a sheet would compose and + // read rows(), the fetch is already in flight. + cache.start(messageId = 1L, emojis = listOf("👍", "🔥")) + advanceUntilIdle() + + assertEquals(setOf(1L to "👍", 1L to "🔥"), calls.toSet()) + } + + @Test + fun `rows publish once the fetch completes`() = runTest { + val alice = id(2) + val reactedAt = Instant.fromEpochSeconds(1_000) + val cache = ReactorsPrefetchCache(scope = this) { _, _, _ -> + Result.success(ReactorsPage(reactors = listOf(reactor(alice, reactedAt)), hasMore = false)) + } + + cache.start(messageId = 1L, emojis = listOf("👍")) + advanceUntilIdle() + + assertEquals(false, cache.loading(1L).first()) + assertEquals(listOf(alice), cache.rows(1L).first().map { it.userId }) + } + + @Test + fun `a rows subscriber that reads before start still sees the fetch complete`() = runTest { + val alice = id(2) + val reactedAt = Instant.fromEpochSeconds(1_000) + val cache = ReactorsPrefetchCache(scope = this) { _, _, _ -> + Result.success(ReactorsPage(reactors = listOf(reactor(alice, reactedAt)), hasMore = false)) + } + + // The sheet's own composition can read rows()/loading() before ChatViewModel's OpenReactors + // handler has run start() — both derive from the same backing StateFlow, so a flow obtained + // now still reflects the fetch once it lands, rather than being a stale placeholder. + val rowsFlow = cache.rows(1L) + + cache.start(messageId = 1L, emojis = listOf("👍")) + advanceUntilIdle() + + assertEquals(listOf(alice), rowsFlow.first().map { it.userId }) + } + + @Test + fun `start is idempotent per messageId`() = runTest { + var callCount = 0 + val cache = ReactorsPrefetchCache(scope = this) { _, _, _ -> + callCount++ + Result.success(ReactorsPage(reactors = emptyList(), hasMore = false)) + } + + cache.start(messageId = 1L, emojis = listOf("👍")) + advanceUntilIdle() + cache.start(messageId = 1L, emojis = listOf("👍")) + advanceUntilIdle() + + assertEquals(1, callCount) + } + + @Test + fun `start no-ops for a message with no reactions`() = runTest { + var callCount = 0 + val cache = ReactorsPrefetchCache(scope = this) { _, _, _ -> + callCount++ + Result.success(ReactorsPage(reactors = emptyList(), hasMore = false)) + } + + cache.start(messageId = 1L, emojis = emptyList()) + advanceUntilIdle() + + assertEquals(0, callCount) + assertEquals(emptyList(), cache.rows(1L).first()) + } +} diff --git a/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/converters/ChatTypeConverters.kt b/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/converters/ChatTypeConverters.kt index adae839637..bc631733ab 100644 --- a/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/converters/ChatTypeConverters.kt +++ b/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/converters/ChatTypeConverters.kt @@ -4,15 +4,53 @@ import androidx.room.TypeConverter import com.flipcash.app.persistence.entities.MessageStatus import com.flipcash.services.models.VerifiableContactMethod import com.flipcash.services.models.chat.MediaItem +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNames private val json = Json { ignoreUnknownKeys = true encodeDefaults = true } +/** + * Merges a message's stored `reactions_json` with an incoming payload, per emoji — mirrors iOS's + * `applySummary`. `incoming` is treated as a full confirmed summary: an emoji [storedJson] holds + * that [incomingJson] omits has emptied, so it is kept as a tombstone (count 0, self/sample + * reactors cleared, `version` retained so a later, older add for it is still rejected) rather than + * left stale. An emoji present on both sides keeps whichever entry's `version` is strictly higher; + * a tie keeps [storedJson]'s entry, matching [com.flipcash.shared.chat.reactions.ReactionState]'s + * accept-by-version rule (`accept` rejects `<=`, so only a strictly newer version replaces it). + * `incomingJson == null` keeps [storedJson] as-is: a write that carries no reaction data (an + * ordinary content upsert) must not erase confirmed reactions already on disk. `storedJson == + * null` (nothing stored yet) takes [incomingJson] outright. + */ +internal fun mergeReactionsJson(storedJson: String?, incomingJson: String?): String? { + if (incomingJson == null) return storedJson + if (storedJson == null) return incomingJson + val stored = json.decodeFromString(storedJson) + val incoming = json.decodeFromString(incomingJson) + val storedByEmoji = stored.reactions.associateBy { it.emoji } + val incomingByEmoji = incoming.reactions.associateBy { it.emoji } + val merged = (storedByEmoji.keys + incomingByEmoji.keys).mapNotNull { emoji -> + val storedEntry = storedByEmoji[emoji] + val incomingEntry = incomingByEmoji[emoji] + when { + incomingEntry == null -> storedEntry?.copy( + count = 0, + selfReactor = null, + sampleReactors = emptyList(), + ) + storedEntry == null -> incomingEntry + incomingEntry.version > storedEntry.version -> incomingEntry + else -> storedEntry + } + } + return json.encodeToString(incoming.copy(reactions = merged)) +} + class ChatTypeConverters { // region MessageContent @@ -253,13 +291,15 @@ data class ReactionSummarySerialized( val reactions: List, ) +@OptIn(ExperimentalSerializationApi::class) @Serializable data class EmojiReactionSerialized( val emoji: String, val count: Long, val selfReactor: ReactorSerialized? = null, val sampleReactors: List, - val sequence: Long, + // Rows written before the field was renamed to match the proto store it as `sequence`. + @JsonNames("sequence") val version: Long, ) @Serializable diff --git a/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/dao/ChatMessageDao.kt b/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/dao/ChatMessageDao.kt index a4ae030d26..76f97c05a9 100644 --- a/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/dao/ChatMessageDao.kt +++ b/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/dao/ChatMessageDao.kt @@ -6,6 +6,7 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction +import com.flipcash.app.persistence.converters.mergeReactionsJson import com.flipcash.app.persistence.entities.ChatMessageEntity import com.flipcash.app.persistence.entities.MessageStatus import kotlinx.coroutines.flow.Flow @@ -140,6 +141,36 @@ interface ChatMessageDao { @Query("SELECT event_sequence FROM chat_messages WHERE chat_id_hex = :chatIdHex AND message_id = :messageId") suspend fun getEventSequence(chatIdHex: String, messageId: Long): Long? + @Query("SELECT reactions_json FROM chat_messages WHERE chat_id_hex = :chatIdHex AND message_id = :messageId") + suspend fun getReactionsJson(chatIdHex: String, messageId: Long): String? + + @Query("SELECT EXISTS(SELECT 1 FROM chat_messages WHERE chat_id_hex = :chatIdHex AND message_id = :messageId)") + suspend fun exists(chatIdHex: String, messageId: Long): Boolean + + /** + * Reactions-only write: touches `reactions_json` alone, leaving every other column — content, + * status, event_sequence — untouched. A row must already exist; a stream reaction event for a + * message not yet stored is dropped by the caller (mirrors iOS's `updateReactions`, which skips + * unstored messages rather than writing a bare row). + */ + @Query("UPDATE chat_messages SET reactions_json = :reactionsJson WHERE chat_id_hex = :chatIdHex AND message_id = :messageId") + suspend fun updateReactionsJson(chatIdHex: String, messageId: Long, reactionsJson: String?) + + /** + * Merges [incomingJson] onto the message's stored `reactions_json`, per emoji, keeping + * whichever side has the higher `version` (see [mergeReactionsJson]), then writes the result + * back. Does nothing if the message isn't stored. This is the reactions-only counterpart of + * the merge [upsert] already does for a full-row write — used when a reaction summary arrives + * on its own (a stream `ReactionUpdate`, or a batch summary refresh) rather than attached to a + * message. + */ + @Transaction + suspend fun mergeReactionsJson(chatIdHex: String, messageId: Long, incomingJson: String) { + if (!exists(chatIdHex, messageId)) return + val stored = getReactionsJson(chatIdHex, messageId) + updateReactionsJson(chatIdHex, messageId, mergeReactionsJson(stored, incomingJson)) + } + @Query("SELECT unread_seq FROM chat_messages WHERE chat_id_hex = :chatIdHex AND message_id = :messageId") suspend fun getUnreadSeq(chatIdHex: String, messageId: Long): Long? @@ -161,9 +192,18 @@ interface ChatMessageDao { } val existingPendingId = getPendingClientId(entity.chatIdHex, entity.messageId) - val merged = if (existingPendingId != null && entity.pendingClientIdHex == null) { + var merged = if (existingPendingId != null && entity.pendingClientIdHex == null) { entity.copy(pendingClientIdHex = existingPendingId) } else entity + + // Reactions: never let this write clobber a newer confirmed reaction with an older or + // absent one. mergeReactionsJson keeps the stored side when entity carries none, and + // per-emoji picks whichever side's `version` is higher otherwise. + val storedReactionsJson = getReactionsJson(entity.chatIdHex, entity.messageId) + merged = merged.copy( + reactionsJson = mergeReactionsJson(storedReactionsJson, entity.reactionsJson), + ) + insert(merged) } diff --git a/apps/flipcash/shared/persistence/db/src/test/kotlin/com/flipcash/app/persistence/converters/ChatTypeConvertersTest.kt b/apps/flipcash/shared/persistence/db/src/test/kotlin/com/flipcash/app/persistence/converters/ChatTypeConvertersTest.kt index a752a6b171..50fc96b43f 100644 --- a/apps/flipcash/shared/persistence/db/src/test/kotlin/com/flipcash/app/persistence/converters/ChatTypeConvertersTest.kt +++ b/apps/flipcash/shared/persistence/db/src/test/kotlin/com/flipcash/app/persistence/converters/ChatTypeConvertersTest.kt @@ -24,14 +24,14 @@ class ChatTypeConvertersTest { ReactorSerialized(userIdHex = "aabb", reactedAtEpochSeconds = 1000L), ReactorSerialized(userIdHex = "ccdd", reactedAtEpochSeconds = 2000L), ), - sequence = 5, + version = 5, ), EmojiReactionSerialized( emoji = "\uD83D\uDC4D", count = 1, selfReactor = null, sampleReactors = emptyList(), - sequence = 3, + version = 3, ), ), ) @@ -40,6 +40,16 @@ class ChatTypeConvertersTest { assertEquals(original, deserialized) } + @Test + fun `fromReactionSummary reads rows stored before sequence was renamed to version`() { + val stored = """{"messageId":42,"reactions":[{"emoji":"\uD83D\uDC4D","count":1,""" + + """"selfReactor":null,"sampleReactors":[],"sequence":7}]}""" + + val deserialized = converter.fromReactionSummary(stored) + + assertEquals(7L, deserialized?.reactions?.single()?.version) + } + @Test fun `fromReactionSummary returns null for null input`() { assertNull(converter.fromReactionSummary(null)) @@ -183,4 +193,53 @@ class ChatTypeConvertersTest { } // endregion + + // region mergeReactionsJson + + private fun reactionsJson(vararg entries: Triple) = converter.toReactionSummary( + ReactionSummarySerialized( + messageId = 1, + reactions = entries.map { (emoji, count, version) -> + EmojiReactionSerialized( + emoji = emoji, + count = count, + selfReactor = if (count > 0) ReactorSerialized(userIdHex = "self", reactedAtEpochSeconds = 1L) else null, + sampleReactors = emptyList(), + version = version, + ) + }, + ) + )!! + + private fun decode(json: String?) = json?.let { converter.fromReactionSummary(it) } + + @Test + fun `mergeReactionsJson tombstones a stored emoji the incoming summary omits`() { + val stored = reactionsJson(Triple("👍", 1L, 1L), Triple("❤️", 1L, 3L)) + val incoming = reactionsJson(Triple("👍", 2L, 2L)) + + val merged = decode(mergeReactionsJson(stored, incoming))!! + val byEmoji = merged.reactions.associateBy { it.emoji } + + // The still-present emoji takes the incoming (newer) entry outright. + assertEquals(EmojiReactionSerialized("👍", 2L, byEmoji["👍"]?.selfReactor, emptyList(), 2L), byEmoji["👍"]) + // The omitted emoji becomes a tombstone: count 0, self/sample reactors cleared, version kept. + assertEquals( + EmojiReactionSerialized(emoji = "❤️", count = 0L, selfReactor = null, sampleReactors = emptyList(), version = 3L), + byEmoji["❤️"], + ) + } + + @Test + fun `mergeReactionsJson keeps the stored entry on a version tie`() { + val stored = reactionsJson(Triple("👍", 5L, 3L)) + val incoming = reactionsJson(Triple("👍", 1L, 3L)) + + val merged = decode(mergeReactionsJson(stored, incoming))!! + + // Strictly-greater only, matching ReactionState.accept: a tie does not let incoming win. + assertEquals(5L, merged.reactions.single { it.emoji == "👍" }.count) + } + + // endregion } diff --git a/apps/flipcash/shared/persistence/db/src/test/kotlin/com/flipcash/app/persistence/dao/ChatMessageDaoTest.kt b/apps/flipcash/shared/persistence/db/src/test/kotlin/com/flipcash/app/persistence/dao/ChatMessageDaoTest.kt index 10eb6c0373..4e27f053cb 100644 --- a/apps/flipcash/shared/persistence/db/src/test/kotlin/com/flipcash/app/persistence/dao/ChatMessageDaoTest.kt +++ b/apps/flipcash/shared/persistence/db/src/test/kotlin/com/flipcash/app/persistence/dao/ChatMessageDaoTest.kt @@ -5,10 +5,13 @@ import androidx.paging.PagingSource import androidx.room.Room import androidx.test.core.app.ApplicationProvider import com.flipcash.app.persistence.FlipcashDatabase +import com.flipcash.app.persistence.converters.EmojiReactionSerialized import com.flipcash.app.persistence.converters.MessageContentSerialized +import com.flipcash.app.persistence.converters.ReactionSummarySerialized import com.flipcash.app.persistence.entities.ChatMessageEntity import com.flipcash.app.persistence.entities.MessageStatus import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json import org.junit.After import org.junit.Before import org.junit.Test @@ -25,6 +28,8 @@ import kotlin.test.assertNull @RunWith(RobolectricTestRunner::class) class ChatMessageDaoTest { + private val reactionJsonCodec = Json { ignoreUnknownKeys = true } + private lateinit var db: FlipcashDatabase private lateinit var dao: ChatMessageDao @@ -365,6 +370,106 @@ class ChatMessageDaoTest { assertEquals(MessageStatus.SENT, dao.getMessage(CHAT_HEX, 1)?.status) } + // region Reactions + + private fun reactionsJson(vararg entries: Pair) = reactionJsonCodec.encodeToString( + ReactionSummarySerialized( + messageId = 1, + reactions = entries.map { (emoji, version) -> + EmojiReactionSerialized(emoji = emoji, count = version, sampleReactors = emptyList(), version = version) + }, + ) + ) + + private fun versionsByEmoji(json: String?) = json + ?.let { reactionJsonCodec.decodeFromString(it) } + ?.reactions + ?.associate { it.emoji to it.version } + .orEmpty() + + private fun countsByEmoji(json: String?) = json + ?.let { reactionJsonCodec.decodeFromString(it) } + ?.reactions + ?.associate { it.emoji to it.count } + .orEmpty() + + /** + * A plain content upsert (edit, delivery-status refresh, server echo) carries no reaction data + * of its own. It must not wipe out a reaction a prior `ReactionUpdate` already confirmed. + */ + @Test + fun `upsert without reactions keeps the reactions already stored`() = runTest { + dao.upsert(text(1, "hi")) + dao.mergeReactionsJson(CHAT_HEX, 1, reactionsJson("👍" to 1)) + + dao.upsert(text(1, "hi edited")) + + assertEquals(mapOf("👍" to 1L), versionsByEmoji(dao.getReactionsJson(CHAT_HEX, 1))) + } + + /** An upsert carrying a newer reaction summary wins over the older one on disk. */ + @Test + fun `upsert with a newer reaction summary replaces the stale one`() = runTest { + dao.upsert(text(1, "hi")) + dao.mergeReactionsJson(CHAT_HEX, 1, reactionsJson("👍" to 1)) + + dao.upsert(text(1, "hi").copy(reactionsJson = reactionsJson("👍" to 2))) + + assertEquals(mapOf("👍" to 2L), versionsByEmoji(dao.getReactionsJson(CHAT_HEX, 1))) + } + + /** An upsert carrying a stale reaction summary does not regress the newer one on disk. */ + @Test + fun `upsert with a stale reaction summary is ignored`() = runTest { + dao.upsert(text(1, "hi")) + dao.mergeReactionsJson(CHAT_HEX, 1, reactionsJson("👍" to 5)) + + dao.upsert(text(1, "hi").copy(reactionsJson = reactionsJson("👍" to 2))) + + assertEquals(mapOf("👍" to 5L), versionsByEmoji(dao.getReactionsJson(CHAT_HEX, 1))) + } + + @Test + fun `updateReactionsJson writes reactions without touching other columns`() = runTest { + dao.upsert(text(1, "hi")) + + dao.updateReactionsJson(CHAT_HEX, 1, reactionsJson("❤️" to 1)) + + val stored = dao.getMessage(CHAT_HEX, 1)!! + assertEquals("hi", (stored.contentJson?.first() as MessageContentSerialized.Text).text) + assertEquals(mapOf("❤️" to 1L), versionsByEmoji(stored.reactionsJson)) + } + + @Test + fun `mergeReactionsJson does nothing for a message that isn't stored`() = runTest { + dao.mergeReactionsJson(CHAT_HEX, 99, reactionsJson("👍" to 1)) + + assertNull(dao.getReactionsJson(CHAT_HEX, 99)) + } + + /** + * A stored emoji the incoming payload omits is treated as emptied on the server (mirrors iOS's + * `applySummary`), not left stale: it tombstones to count 0 while keeping its version. + */ + @Test + fun `mergeReactionsJson tombstones a stored emoji the incoming payload omits`() = runTest { + dao.upsert(text(1, "hi")) + dao.mergeReactionsJson(CHAT_HEX, 1, reactionsJson("👍" to 1, "❤️" to 1)) + + dao.mergeReactionsJson(CHAT_HEX, 1, reactionsJson("👍" to 2)) + + assertEquals( + mapOf("👍" to 2L, "❤️" to 1L), + versionsByEmoji(dao.getReactionsJson(CHAT_HEX, 1)), + ) + assertEquals( + mapOf("👍" to 2L, "❤️" to 0L), + countsByEmoji(dao.getReactionsJson(CHAT_HEX, 1)), + ) + } + + // endregion + private companion object { const val CHAT_HEX = "aabb" const val OTHER_HEX = "ccdd" diff --git a/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/ChatMessageDataSource.kt b/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/ChatMessageDataSource.kt index 9aed3ba0e8..3e09ddc12e 100644 --- a/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/ChatMessageDataSource.kt +++ b/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/ChatMessageDataSource.kt @@ -8,6 +8,7 @@ import com.flipcash.app.persistence.sources.mapper.chat.ChatEntityMapper import com.flipcash.services.models.chat.ChatId import com.flipcash.services.models.chat.ChatMessage import com.flipcash.services.models.chat.ClientMessageId +import com.flipcash.services.models.chat.ReactionSummary import com.flipcash.app.persistence.entities.MessageStatus import com.flipcash.services.models.chat.MessageContent import com.flipcash.services.persistence.PagingDataSource @@ -271,6 +272,17 @@ class ChatMessageDataSource @Inject constructor( return clientMessageId } + /** + * Merges [summary] onto the message's stored `reactions_json`, per emoji keeping whichever + * side has the higher `version` (see [ChatMessageDao.mergeReactionsJson]). A no-op if the + * message isn't stored yet — a reaction event for a message this device hasn't seen is + * dropped rather than written as a bare row. + */ + suspend fun mergeReactions(chatId: ChatId, messageId: Long, summary: ReactionSummary) { + val json = mapper.encodeReactions(summary) ?: return + db?.chatMessageDao()?.mergeReactionsJson(mapper.chatIdHex(chatId), messageId, json) + } + fun toChatMessage(entity: ChatMessageEntity): ChatMessage { val message = mapper.toMessage(entity) val selfId = userManager.accountId diff --git a/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mapper/chat/ChatEntityMapper.kt b/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mapper/chat/ChatEntityMapper.kt index f2b3bb2f10..fd8ef7f47d 100644 --- a/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mapper/chat/ChatEntityMapper.kt +++ b/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mapper/chat/ChatEntityMapper.kt @@ -57,6 +57,8 @@ import kotlin.time.Instant @Singleton class ChatEntityMapper @Inject constructor() { + private val reactionJson = kotlinx.serialization.json.Json { ignoreUnknownKeys = true } + // region ChatMetadata /** @@ -171,9 +173,7 @@ class ChatEntityMapper @Inject constructor() { unreadSeq = message.unreadSeq, eventSequence = message.eventSequence, lastEditedTsEpochMs = message.lastEditedTs?.toEpochMilliseconds(), - reactionsJson = message.reactions?.toSerialized()?.let { - kotlinx.serialization.json.Json.encodeToString(it) - }, + reactionsJson = encodeReactions(message.reactions), isDeleted = message.content.any { it is MessageContent.Deleted }, ) } @@ -187,9 +187,7 @@ class ChatEntityMapper @Inject constructor() { unreadSeq = entity.unreadSeq, eventSequence = entity.eventSequence, lastEditedTs = entity.lastEditedTsEpochMs?.let { Instant.fromEpochMilliseconds(it) }, - reactions = entity.reactionsJson?.let { - kotlinx.serialization.json.Json.decodeFromString(it).toDomain() - }, + reactions = decodeReactions(entity.reactionsJson), deliveryStatus = when (entity.status) { MessageStatus.SENDING -> DeliveryStatus.SENDING MessageStatus.SENT -> DeliveryStatus.SENT @@ -218,6 +216,38 @@ class ChatEntityMapper @Inject constructor() { ) } + /** Encodes a [ReactionSummary] for [ChatMessageEntity.reactionsJson], or `null` for none. */ + fun encodeReactions(summary: ReactionSummary?): String? = + summary?.toSerialized()?.let { reactionJson.encodeToString(it) } + + /** Decodes [ChatMessageEntity.reactionsJson] back to a [ReactionSummary], or `null` for none. */ + fun decodeReactions(json: String?): ReactionSummary? = + json?.let { reactionJson.decodeFromString(it).toDomain() } + + /** + * Merges [incoming] onto [stored], per emoji, keeping whichever entry carries the higher + * `version` — mirrors [com.flipcash.shared.chat.reactions.ReactionState]'s accept-by-version + * rule so a message upsert can't clobber a newer confirmed reaction with a stale one. An emoji + * [stored] holds that [incoming] omits is kept: [incoming] is a live payload attached to one + * message write, not an authoritative "this and only this" summary. + */ + fun mergeReactions(stored: ReactionSummary?, incoming: ReactionSummary): ReactionSummary { + if (stored == null) return incoming + val storedByEmoji = stored.reactions.associateBy { it.emoji } + val incomingByEmoji = incoming.reactions.associateBy { it.emoji } + val merged = (storedByEmoji.keys + incomingByEmoji.keys).mapNotNull { emoji -> + val storedEntry = storedByEmoji[emoji] + val incomingEntry = incomingByEmoji[emoji] + when { + incomingEntry == null -> storedEntry + storedEntry == null -> incomingEntry + incomingEntry.version >= storedEntry.version -> incomingEntry + else -> storedEntry + } + } + return ReactionSummary(messageId = incoming.messageId, reactions = merged) + } + // endregion // region ChatMember @@ -436,7 +466,7 @@ private fun EmojiReaction.toSerialized(): EmojiReactionSerialized = EmojiReactio count = count, selfReactor = selfReactor?.toSerialized(), sampleReactors = sampleReactors.map { it.toSerialized() }, - sequence = sequence, + version = version, ) private fun Reactor.toSerialized(): ReactorSerialized = ReactorSerialized( @@ -455,7 +485,7 @@ private fun EmojiReactionSerialized.toDomain(): EmojiReaction = EmojiReaction( count = count, selfReactor = selfReactor?.toDomain(), sampleReactors = sampleReactors.map { it.toDomain() }, - sequence = sequence, + version = version, ) private fun ReactorSerialized.toDomain(): Reactor = Reactor( diff --git a/apps/flipcash/shared/persistence/sources/src/test/kotlin/com/flipcash/app/persistence/sources/mapper/chat/ChatEntityMapperTest.kt b/apps/flipcash/shared/persistence/sources/src/test/kotlin/com/flipcash/app/persistence/sources/mapper/chat/ChatEntityMapperTest.kt index 2fe69492de..28d2e29fae 100644 --- a/apps/flipcash/shared/persistence/sources/src/test/kotlin/com/flipcash/app/persistence/sources/mapper/chat/ChatEntityMapperTest.kt +++ b/apps/flipcash/shared/persistence/sources/src/test/kotlin/com/flipcash/app/persistence/sources/mapper/chat/ChatEntityMapperTest.kt @@ -301,6 +301,70 @@ class ChatEntityMapperTest { assertEquals(true, restored.viewerState?.permissions?.canEdit) } + // region Reactions + + @Test + fun `null reactions round-trip through encode and decode as null`() { + assertEquals(null, mapper.encodeReactions(null)) + assertEquals(null, mapper.decodeReactions(null)) + } + + @Test + fun `a reaction summary round-trips through encode and decode`() { + val summary = reactionSummary(emoji("👍") to 3L) + + val decoded = mapper.decodeReactions(mapper.encodeReactions(summary)) + + assertEquals(summary, decoded) + } + + @Test + fun `merge keeps the stored entry when incoming omits its emoji`() { + val stored = reactionSummary(emoji("👍") to 1L, emoji("❤️") to 1L) + val incoming = reactionSummary(emoji("👍") to 2L) + + val merged = mapper.mergeReactions(stored, incoming) + + assertEquals(setOf("👍", "❤️"), merged.reactions.map { it.emoji.value }.toSet()) + } + + @Test + fun `merge keeps the higher-version entry per emoji`() { + val stored = reactionSummary(emoji("👍") to 5L) + val incoming = reactionSummary(emoji("👍") to 2L) + + val merged = mapper.mergeReactions(stored, incoming) + + assertEquals(5L, merged.reactions.single().count) + } + + @Test + fun `merge onto null stored state returns the incoming summary unchanged`() { + val incoming = reactionSummary(emoji("👍") to 1L) + + assertEquals(incoming, mapper.mergeReactions(null, incoming)) + } + + private fun emoji(value: String) = com.flipcash.services.models.chat.Emoji(value) + + /** Builds a [ReactionSummary] with one [EmojiReaction] per (emoji, version) pair, version doubling as count for easy assertions. */ + private fun reactionSummary( + vararg entries: Pair, + ) = com.flipcash.services.models.chat.ReactionSummary( + messageId = 42L, + reactions = entries.map { (emoji, version) -> + com.flipcash.services.models.chat.EmojiReaction( + emoji = emoji, + count = version, + selfReactor = null, + sampleReactors = emptyList(), + version = version, + ) + }, + ) + + // endregion + private companion object { const val CHAT_HEX = "aabbccdd" } diff --git a/libs/emojis/build.gradle.kts b/libs/emojis/build.gradle.kts index e3aa087d95..370070ee78 100644 --- a/libs/emojis/build.gradle.kts +++ b/libs/emojis/build.gradle.kts @@ -1,16 +1,36 @@ plugins { alias(libs.plugins.flipcash.android.library) + id("com.google.devtools.ksp") + id("dagger.hilt.android.plugin") } android { namespace = "${Gradle.codeNamespace}.libs.emojis" } +// 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) } + } +} + 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) } // Define the task to fetch and generate emoji data diff --git a/libs/emojis/src/androidTest/java/com/getcode/libs/emojis/reactions/EmojiDrawabilityVectorTest.kt b/libs/emojis/src/androidTest/java/com/getcode/libs/emojis/reactions/EmojiDrawabilityVectorTest.kt new file mode 100644 index 0000000000..5efb2e963e --- /dev/null +++ b/libs/emojis/src/androidTest/java/com/getcode/libs/emojis/reactions/EmojiDrawabilityVectorTest.kt @@ -0,0 +1,32 @@ +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 +import org.junit.runner.RunWith + +/** + * Runs the `drawability` vectors from flipcash-client-orchestrator#17 against the device's emoji + * font. Instrumented because Robolectric's Paint has no real font to shape a ZWJ sequence with. + * + * The mixed-skin-tone vector needs Emoji 12.1, which Android ships from API 30. + */ +@RunWith(AndroidJUnit4::class) +class EmojiDrawabilityVectorTest { + + @Test + fun drawabilityVectors() { + val json = InstrumentationRegistry.getInstrumentation().context.assets + .open("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") + val expected = vector.getBoolean("drawable") + val actual = EmojiDrawability.isDrawable(emoji) + if (actual == expected) null else "$emoji (${vector.getString("note")}): expected $expected, got $actual" + } + assertEquals("drawability vectors failed:\n" + failures.joinToString("\n"), emptyList(), failures) + } +} diff --git a/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/EmojiUsageController.kt b/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/EmojiUsageController.kt index d47b4cec90..4fd032dfd1 100644 --- a/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/EmojiUsageController.kt +++ b/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/EmojiUsageController.kt @@ -1,5 +1,6 @@ package com.getcode.libs.emojis +import com.getcode.libs.emojis.reactions.RecentReactions import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -7,8 +8,10 @@ class EmojiUsageController( private val queryProvider: EmojiQueryProvider ): EmojiUsageTracker { private companion object { - val DEFAULT_TOP = listOf("👍", "👎", "❤️", "🙌", "😂") - private const val MAX_EMOJIS = 5 + // The reaction strip's six defaults, so the picker's "frequently used" filler and the + // strip agree on what an emoji-less history falls back to. + val DEFAULT_TOP = RecentReactions.DEFAULTS + private val MAX_EMOJIS = RecentReactions.DEFAULTS.size } override suspend fun mostUsedEmojis(includeFiller: Boolean): List { diff --git a/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/EmojiCatalog.kt b/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/EmojiCatalog.kt new file mode 100644 index 0000000000..79d32ec758 --- /dev/null +++ b/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/EmojiCatalog.kt @@ -0,0 +1,76 @@ +package com.getcode.libs.emojis.reactions + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +/** One emoji as `emoji_catalog.json` (bundled verbatim from the orchestrator repo) lists it. */ +@Serializable +data class EmojiCatalogEntry( + val emoji: String, + val name: String, + val category: String, + val version: String, + val skinTone: Boolean, + val keywords: List = emptyList(), +) + +/** `emoji_catalog.json`'s categories, in the file's own order, and the entries in each. */ +data class EmojiCatalog( + val categories: List, + val entries: List, +) { + val byCategory: Map> by lazy { + entries.groupBy { it.category } + } + + /** The first category's entries, the app-layer strip's fill source (decision 1). */ + val firstCategoryEntries: List + get() = categories.firstOrNull()?.let { byCategory[it] }.orEmpty() +} + +@Serializable +private data class EmojiCatalogFile( + val categories: List, + val emoji: List, +) + +/** + * Loads [EmojiCatalog] from the bundled `emoji_catalog.json` resource once, on + * [dispatcher] (default [Dispatchers.Default], since parsing is CPU-bound, not I/O), and caches + * the result for the loader's lifetime. + */ +class EmojiCatalogLoader( + private val dispatcher: CoroutineDispatcher = Dispatchers.Default, + private val resourceLoader: () -> String = { defaultResource() }, +) { + private val mutex = Mutex() + private var cached: EmojiCatalog? = null + + suspend fun load(): EmojiCatalog { + cached?.let { return it } + return mutex.withLock { + cached?.let { return@withLock it } + withContext(dispatcher) { + val text = resourceLoader() + val file = json.decodeFromString(EmojiCatalogFile.serializer(), text) + EmojiCatalog(categories = file.categories, entries = file.emoji).also { cached = it } + } + } + } + + private companion object { + val json = Json { ignoreUnknownKeys = true } + + fun defaultResource(): String { + val stream = EmojiCatalogLoader::class.java.classLoader + ?.getResourceAsStream("emoji_catalog.json") + ?: error("emoji_catalog.json resource not found") + return stream.bufferedReader().use { it.readText() } + } + } +} diff --git a/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawability.kt b/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawability.kt new file mode 100644 index 0000000000..e5d9868729 --- /dev/null +++ b/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawability.kt @@ -0,0 +1,29 @@ +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. + */ + fun isDrawable(emoji: String, probe: GlyphProbe = PaintGlyphProbe): 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) + } +} diff --git a/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/EmojiPickerModel.kt b/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/EmojiPickerModel.kt new file mode 100644 index 0000000000..f59387ba38 --- /dev/null +++ b/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/EmojiPickerModel.kt @@ -0,0 +1,77 @@ +package com.getcode.libs.emojis.reactions + +/** + * The picker's pure grouping logic — search, category sections, the Frequently Used row, and the + * undrawable filter — kept apart from the Compose sheet so it is testable without loading the real + * catalog or standing up a screen. Mirrors iOS's `EmojiPickerModel`. + */ +object EmojiPickerModel { + + /** One section the grid draws, in order. */ + data class Section( + val id: String, + val title: String, + val entries: List, + ) + + /** The id [sections] gives the Frequently Used row, so the category bar can skip past it. */ + const val FREQUENTLY_USED_ID = "frequently-used" + const val FREQUENTLY_USED_TITLE = "Frequently Used" + + /** The id [sections] gives the single section a non-empty search produces. */ + const val SEARCH_RESULTS_ID = "search-results" + const val SEARCH_RESULTS_TITLE = "Search Results" + + /** + * Builds the sections the grid draws. + * + * With an empty (or blank) [query]: a Frequently Used row built from [recents] — deduped and + * left in the caller's order (the caller is expected to have already capped it to the picker's + * row limit) — followed by one section per [catalog] category, in catalog order. + * + * With a non-empty [query]: a single "search results" section, catalog order, matched + * case-insensitively against an entry's name or keywords — no Frequently Used row, since a + * search is the user looking for something specific rather than reaching for the recent set. + * + * [undrawable] is filtered out of every section, including the Frequently Used row — a recent + * pick this OS build can no longer render is worth dropping silently rather than showing a tofu + * box for. + */ + fun sections( + catalog: EmojiCatalog, + undrawable: Set, + recents: List, + query: String, + ): List
{ + val drawable = catalog.entries.filter { it.emoji !in undrawable } + val trimmedQuery = query.trim() + + if (trimmedQuery.isNotEmpty()) { + val needle = trimmedQuery.lowercase() + val matches = drawable.filter { entry -> + entry.name.lowercase().contains(needle) || + entry.keywords.any { it.lowercase().contains(needle) } + } + return listOf(Section(id = SEARCH_RESULTS_ID, title = SEARCH_RESULTS_TITLE, entries = matches)) + } + + val sections = mutableListOf
() + if (recents.isNotEmpty()) { + val byEmoji = drawable.associateBy { it.emoji } + val seen = mutableSetOf() + val frequent = recents.mapNotNull { emoji -> + if (!seen.add(emoji)) return@mapNotNull null + byEmoji[emoji] + } + if (frequent.isNotEmpty()) { + sections += Section(id = FREQUENTLY_USED_ID, title = FREQUENTLY_USED_TITLE, entries = frequent) + } + } + for (category in catalog.categories) { + val entries = drawable.filter { it.category == category } + if (entries.isEmpty()) continue + sections += Section(id = category, title = category, entries = entries) + } + return sections + } +} diff --git a/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/ReactionsModule.kt b/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/ReactionsModule.kt new file mode 100644 index 0000000000..0040ce4a1b --- /dev/null +++ b/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/ReactionsModule.kt @@ -0,0 +1,25 @@ +package com.getcode.libs.emojis.reactions + +import android.content.Context +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 + +@Module +@InstallIn(SingletonComponent::class) +object ReactionsModule { + + @Provides + @Singleton + fun providesRecentReactionsStore( + @ApplicationContext context: Context, + owner: RecentReactionsOwner, + ): RecentReactionsStore = DataStoreRecentReactionsStore(context, owner) + + @Provides + @Singleton + fun providesEmojiCatalogLoader(): EmojiCatalogLoader = EmojiCatalogLoader() +} diff --git a/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/RecentReactions.kt b/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/RecentReactions.kt new file mode 100644 index 0000000000..04221bfd10 --- /dev/null +++ b/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/RecentReactions.kt @@ -0,0 +1,60 @@ +package com.getcode.libs.emojis.reactions + +import kotlin.time.Instant + +/** Ranks the emoji the strip and picker offer first, from how often and how recently each was used. */ +object RecentReactions { + + /** The emoji offered before any have been used, in their order. */ + val DEFAULTS: List = listOf("❤️", "👍", "😂", "😮", "😢", "🔥") + + /** How often and how recently one emoji was reacted with. */ + data class Usage(val count: Int, val lastUsed: Instant) + + /** Records one use of [emoji] at [date] in [stats], returning the updated map. */ + fun record(emoji: String, at: Instant, stats: Map): Map { + val existing = stats[emoji] + val updated = if (existing != null) { + existing.copy(count = existing.count + 1, lastUsed = maxOf(existing.lastUsed, at)) + } else { + Usage(count = 1, lastUsed = at) + } + return stats + (emoji to updated) + } + + /** + * Returns up to [limit] emoji: the used ones by count descending, then last use descending, + * padded with [DEFAULTS], leaving out any in [undrawable]. + * + * Padding stops at the defaults, so an unused picker asking for more than six gets six. + */ + fun rank(stats: Map, undrawable: Set, limit: Int): List { + val ranked = stats.entries + .sortedWith( + compareByDescending> { it.value.count } + .thenByDescending { it.value.lastUsed } + // Only for a map's lack of order; two uses at the same instant do not happen. + .thenComparator { a, b -> compareUtf8Bytes(a.key, b.key) }, + ) + .map { it.key } + .filter { it !in undrawable } + .toMutableList() + for (emoji in DEFAULTS) { + if (emoji !in ranked && emoji !in undrawable) { + ranked.add(emoji) + } + } + return ranked.take(maxOf(limit, 0)) + } + + private fun compareUtf8Bytes(lhs: String, rhs: String): Int { + val a = lhs.encodeToByteArray() + val b = rhs.encodeToByteArray() + val len = minOf(a.size, b.size) + for (i in 0 until len) { + val cmp = (a[i].toInt() and 0xFF) - (b[i].toInt() and 0xFF) + if (cmp != 0) return cmp + } + return a.size - b.size + } +} diff --git a/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStore.kt b/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStore.kt new file mode 100644 index 0000000000..c78fe9f87c --- /dev/null +++ b/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStore.kt @@ -0,0 +1,91 @@ +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 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 { + + /** Records one use of [emoji] now. Only an add records — a remove does not. */ + suspend fun record(emoji: String) + + /** Returns up to [limit] emoji in rank order, leaving out [undrawable]. */ + suspend fun rank(undrawable: Set = emptySet(), limit: Int = RecentReactions.DEFAULTS.size): List + + /** The raw per-emoji use stats, for a caller (e.g. [com.flipcash.shared.chat.reactions.ReactionStripComposer]) that ranks them itself. */ + suspend fun stats(): Map +} + +/** + * The signed-in account whose recents [DataStoreRecentReactionsStore] reads and writes, or null + * when nobody is signed in. One device serves every account, and switching back to one should find + * its recents where it left them, so each account's usage lives under its own keys. + */ +fun interface RecentReactionsOwner { + fun current(): String? +} + +@Singleton +class DataStoreRecentReactionsStore @Inject constructor( + @ApplicationContext context: Context, + private val owner: RecentReactionsOwner, +) : RecentReactionsStore { + + private val dataStore = context.recentReactionsDataStore + + override suspend fun record(emoji: String) { + val owner = owner.current() ?: return + val now = Clock.System.now() + dataStore.edit { prefs -> + val key = key(owner, emoji) + val current = decode(prefs[key]) + prefs[key] = encode(RecentReactions.Usage(count = current.count + 1, lastUsed = now)) + } + } + + override suspend fun rank(undrawable: Set, limit: Int): List = + RecentReactions.rank(stats(), undrawable, limit) + + override suspend fun stats(): Map { + val prefix = prefix(owner.current() ?: return emptyMap()) + val prefs = dataStore.data.first() + return prefs.asMap().mapNotNull { (prefsKey, raw) -> + val emoji = prefsKey.name.removePrefix(prefix) + if (emoji == prefsKey.name) return@mapNotNull null // not one of ours + val usage = decode(raw as? String) ?: return@mapNotNull null + emoji to usage + }.toMap() + } + + private fun prefix(owner: String) = "$owner:$KEY_PREFIX" + + private fun key(owner: String, emoji: String) = stringPreferencesKey("${prefix(owner)}$emoji") + + private fun encode(usage: RecentReactions.Usage): String = "${usage.count}:${usage.lastUsed.epochSeconds}" + + private fun decode(raw: String?): RecentReactions.Usage { + if (raw == null) return RecentReactions.Usage(count = 0, lastUsed = Instant.fromEpochSeconds(0)) + val separator = raw.indexOf(':') + if (separator < 0) return RecentReactions.Usage(count = 0, lastUsed = Instant.fromEpochSeconds(0)) + val count = raw.substring(0, separator).toIntOrNull() ?: 0 + val lastUsed = raw.substring(separator + 1).toLongOrNull()?.let { Instant.fromEpochSeconds(it) } + ?: Instant.fromEpochSeconds(0) + return RecentReactions.Usage(count = count, lastUsed = lastUsed) + } + + private companion object { + const val KEY_PREFIX = "emoji:" + } +} diff --git a/libs/emojis/src/main/resources/emoji_catalog.json b/libs/emojis/src/main/resources/emoji_catalog.json new file mode 100644 index 0000000000..09ecb2b8e3 --- /dev/null +++ b/libs/emojis/src/main/resources/emoji_catalog.json @@ -0,0 +1,3953 @@ +{ + "emojiVersion":"17.0.0", + "cldr":"48.2.2", + "gomoji":"v1.4.1", + "categories":["Smileys & People","Animals & Nature","Food & Drink","Travel & Places","Activities","Objects","Symbols","Flags"], + "dropped":[], + "emoji":[ + {"emoji":"😀","name":"grinning face","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["grinning","face","cheerful","cheery","grin","happy","laugh","nice","smile","smiling","teeth"]}, + {"emoji":"😃","name":"grinning face with big eyes","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["grinning","face","with","big","eyes","awesome","grin","happy","mouth","open","smile","smiling","teeth","yay"]}, + {"emoji":"😄","name":"grinning face with smiling eyes","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["grinning","face","with","smiling","eyes","eye","grin","happy","laugh","lol","mouth","open","smile"]}, + {"emoji":"😁","name":"beaming face with smiling eyes","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["beaming","face","with","smiling","eyes","eye","grin","grinning","happy","nice","smile","teeth"]}, + {"emoji":"😆","name":"grinning squinting face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["grinning","squinting","face","closed","eyes","haha","hahaha","happy","laugh","lol","mouth","open","rofl","smile","smiling"]}, + {"emoji":"😅","name":"grinning face with sweat","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["grinning","face","with","sweat","cold","dejected","excited","mouth","nervous","open","smile","smiling","stress","stressed"]}, + {"emoji":"🤣","name":"rolling on the floor laughing","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["rolling","on","the","floor","laughing","crying","face","funny","haha","happy","hehe","hilarious","joy","laugh","lmao","lol","rofl","roflmao","tear"]}, + {"emoji":"😂","name":"face with tears of joy","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["face","with","tears","of","joy","crying","feels","funny","haha","happy","hehe","hilarious","laugh","lmao","lol","rofl","roflmao","tear"]}, + {"emoji":"🙂","name":"slightly smiling face","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["slightly","smiling","face","happy","smile"]}, + {"emoji":"🙃","name":"upside-down face","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["upside-down","face","hehe","smile"]}, + {"emoji":"🫠","name":"melting face","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["melting","face","disappear","dissolve","embarrassed","haha","heat","hot","liquid","lol","melt","sarcasm","sarcastic"]}, + {"emoji":"😉","name":"winking face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["winking","face","flirt","heartbreaker","sexy","slide","tease","wink","winks"]}, + {"emoji":"😊","name":"smiling face with smiling eyes","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["smiling","face","with","eyes","blush","eye","glad","satisfied","smile"]}, + {"emoji":"😇","name":"smiling face with halo","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["smiling","face","with","halo","angel","angelic","angels","blessed","fairy","fairytale","fantasy","happy","innocent","peaceful","smile","spirit","tale"]}, + {"emoji":"🥰","name":"smiling face with hearts","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["smiling","face","with","hearts","3","adore","crush","heart","ily","love","romance","smile","you"]}, + {"emoji":"😍","name":"smiling face with heart-eyes","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["smiling","face","with","heart-eyes","143","bae","eye","feels","hearts","ily","kisses","love","romance","romantic","smile","xoxo"]}, + {"emoji":"🤩","name":"star-struck","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["star-struck","excited","eyes","face","grinning","smile","star","starry-eyed","wow"]}, + {"emoji":"😘","name":"face blowing a kiss","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["face","blowing","a","kiss","adorbs","bae","flirt","heart","ily","love","lover","miss","muah","romantic","smooch","xoxo","you"]}, + {"emoji":"😗","name":"kissing face","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["kissing","face","143","date","dating","flirt","ily","kiss","love","smooch","smooches","xoxo","you"]}, + {"emoji":"☺️","name":"smiling face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["smiling","face"]}, + {"emoji":"😚","name":"kissing face with closed eyes","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["kissing","face","with","closed","eyes","143","bae","blush","date","dating","eye","flirt","ily","kisses","smooches","xoxo"]}, + {"emoji":"😙","name":"kissing face with smiling eyes","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["kissing","face","with","smiling","eyes","143","closed","date","dating","eye","flirt","ily","kiss","kisses","love","night","smile"]}, + {"emoji":"🥲","name":"smiling face with tear","category":"Smileys & People","version":"13.0","skinTone":false,"keywords":["smiling","face","with","tear","glad","grateful","happy","joy","pain","proud","relieved","smile","smiley","touched"]}, + {"emoji":"😋","name":"face savoring food","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["face","savoring","food","delicious","eat","full","hungry","savor","smile","smiling","tasty","um","yum","yummy"]}, + {"emoji":"😛","name":"face with tongue","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["face","with","tongue","awesome","cool","nice","party","stuck-out","sweet"]}, + {"emoji":"😜","name":"winking face with tongue","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["winking","face","with","tongue","crazy","epic","eye","funny","joke","loopy","nutty","party","stuck-out","wacky","weirdo","wink","yolo"]}, + {"emoji":"🤪","name":"zany face","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["zany","face","crazy","eye","eyes","goofy","large","small"]}, + {"emoji":"😝","name":"squinting face with tongue","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["squinting","face","with","tongue","closed","eye","eyes","gross","horrible","omg","stuck-out","taste","whatever","yolo"]}, + {"emoji":"🤑","name":"money-mouth face","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["money-mouth","face","money","mouth","paid"]}, + {"emoji":"🤗","name":"smiling face with open hands","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["smiling","face","with","open","hands","hug","hugging"]}, + {"emoji":"🤭","name":"face with hand over mouth","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["face","with","hand","over","mouth","giggle","giggling","oops","realization","secret","shock","sudden","surprise","whoops"]}, + {"emoji":"🫢","name":"face with open eyes and hand over mouth","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["face","with","open","eyes","and","hand","over","mouth","amazement","awe","disbelief","embarrass","gasp","omg","quiet","scared","shock","surprise"]}, + {"emoji":"🫣","name":"face with peeking eye","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["face","with","peeking","eye","captivated","embarrass","hide","hiding","peek","peep","scared","shy","stare"]}, + {"emoji":"🤫","name":"shushing face","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["shushing","face","quiet","shh","shush"]}, + {"emoji":"🤔","name":"thinking face","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["thinking","face","chin","consider","hmm","ponder","pondering","wondering"]}, + {"emoji":"🫡","name":"saluting face","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["saluting","face","good","luck","ma’am","OK","respect","salute","sir","troops","yes"]}, + {"emoji":"🤐","name":"zipper-mouth face","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["zipper-mouth","face","keep","mouth","quiet","secret","shut","zip","zipper"]}, + {"emoji":"🤨","name":"face with raised eyebrow","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["face","with","raised","eyebrow","disapproval","disbelief","distrust","emoji","hmm","mild","skeptic","skeptical","skepticism","surprise","what"]}, + {"emoji":"😐","name":"neutral face","category":"Smileys & People","version":"0.7","skinTone":false,"keywords":["neutral","face","awkward","blank","deadpan","expressionless","fine","jealous","meh","oh","shade","straight","unamused","unhappy","unimpressed","whatever"]}, + {"emoji":"😑","name":"expressionless face","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["expressionless","face","awkward","dead","fine","inexpressive","jealous","meh","not","oh","omg","straight","uh","unhappy","unimpressed","whatever"]}, + {"emoji":"😶","name":"face without mouth","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["face","without","mouth","awkward","blank","expressionless","mouthless","mute","quiet","secret","silence","silent","speechless"]}, + {"emoji":"🫥","name":"dotted line face","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["dotted","line","face","depressed","disappear","hidden","hide","introvert","invisible","meh","whatever","wtv"]}, + {"emoji":"😶‍🌫️","name":"face in clouds","category":"Smileys & People","version":"13.1","skinTone":false,"keywords":["face","in","clouds"]}, + {"emoji":"😏","name":"smirking face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["smirking","face","boss","dapper","flirt","homie","kidding","leer","shade","slick","sly","smirk","smug","snicker","suave","suspicious","swag"]}, + {"emoji":"😒","name":"unamused face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["unamused","face","...","bored","fine","jealous","jel","jelly","pissed","smh","ugh","uhh","unhappy","weird","whatever"]}, + {"emoji":"🙄","name":"face with rolling eyes","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["face","with","rolling","eyes","eyeroll","shade","ugh","whatever"]}, + {"emoji":"😬","name":"grimacing face","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["grimacing","face","awk","awkward","dentist","grimace","grinning","smile","smiling"]}, + {"emoji":"😮‍💨","name":"face exhaling","category":"Smileys & People","version":"13.1","skinTone":false,"keywords":["face","exhaling","blow","blowing","exhale","exhausted","gasp","groan","relief","sigh","smiley","smoke","whisper","whistle"]}, + {"emoji":"🤥","name":"lying face","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["lying","face","liar","lie","pinocchio"]}, + {"emoji":"🫨","name":"shaking face","category":"Smileys & People","version":"15.0","skinTone":false,"keywords":["shaking","face","crazy","daze","earthquake","omg","panic","shock","surprise","vibrate","whoa","wow"]}, + {"emoji":"🙂‍↔️","name":"head shaking horizontally","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["head","shaking","horizontally"]}, + {"emoji":"🙂‍↕️","name":"head shaking vertically","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["head","shaking","vertically"]}, + {"emoji":"😌","name":"relieved face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["relieved","face","calm","peace","relief","zen"]}, + {"emoji":"😔","name":"pensive face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["pensive","face","awful","bored","dejected","died","disappointed","losing","lost","sad","sucks"]}, + {"emoji":"😪","name":"sleepy face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["sleepy","face","crying","good","night","sad","sleep","sleeping","tired"]}, + {"emoji":"🤤","name":"drooling face","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["drooling","face"]}, + {"emoji":"😴","name":"sleeping face","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["sleeping","face","bed","bedtime","good","goodnight","nap","night","sleep","tired","whatever","yawn","zzz"]}, + {"emoji":"🫩","name":"face with bags under eyes","category":"Smileys & People","version":"16.0","skinTone":false,"keywords":["face","with","bags","under","eyes","bored","exhausted","fatigued","late","sleepy","tired","weary"]}, + {"emoji":"😷","name":"face with medical mask","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["face","with","medical","mask","cold","dentist","dermatologist","doctor","dr","germs","medicine","sick"]}, + {"emoji":"🤒","name":"face with thermometer","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["face","with","thermometer","ill","sick"]}, + {"emoji":"🤕","name":"face with head-bandage","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["face","with","head-bandage","bandage","hurt","injury","ouch"]}, + {"emoji":"🤢","name":"nauseated face","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["nauseated","face","gross","nasty","sick","vomit"]}, + {"emoji":"🤮","name":"face vomiting","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["face","vomiting","barf","ew","gross","puke","sick","spew","throw","up","vomit"]}, + {"emoji":"🤧","name":"sneezing face","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["sneezing","face","fever","flu","gesundheit","sick","sneeze"]}, + {"emoji":"🥵","name":"hot face","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["hot","face","dying","feverish","heat","panting","red-faced","stroke","sweating","tongue"]}, + {"emoji":"🥶","name":"cold face","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["cold","face","blue","blue-faced","freezing","frostbite","icicles","subzero","teeth"]}, + {"emoji":"🥴","name":"woozy face","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["woozy","face","dizzy","drunk","eyes","intoxicated","mouth","tipsy","uneven","wavy"]}, + {"emoji":"😵","name":"face with crossed-out eyes","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["face","with","crossed-out","eyes","dead","dizzy","feels","knocked","out","sick","tired"]}, + {"emoji":"😵‍💫","name":"face with spiral eyes","category":"Smileys & People","version":"13.1","skinTone":false,"keywords":["face","with","spiral","eyes","confused","dizzy","hypnotized","omg","smiley","trouble","whoa","woah","woozy"]}, + {"emoji":"🤯","name":"exploding head","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["exploding","head","blown","explode","mind","mindblown","no","shocked","way"]}, + {"emoji":"🤠","name":"cowboy hat face","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["cowboy","hat","face","cowgirl"]}, + {"emoji":"🥳","name":"partying face","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["partying","face","bday","birthday","celebrate","celebration","excited","happy","hat","hooray","horn","party"]}, + {"emoji":"🥸","name":"disguised face","category":"Smileys & People","version":"13.0","skinTone":false,"keywords":["disguised","face","disguise","eyebrow","glasses","incognito","moustache","mustache","nose","person","spy","tache","tash"]}, + {"emoji":"😎","name":"smiling face with sunglasses","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["smiling","face","with","sunglasses","awesome","beach","bright","bro","chilling","cool","rad","relaxed","shades","slay","smile","style","swag","win"]}, + {"emoji":"🤓","name":"nerd face","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["nerd","face","brainy","clever","expert","geek","gifted","glasses","intelligent","smart"]}, + {"emoji":"🧐","name":"face with monocle","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["face","with","monocle","classy","fancy","rich","stuffy","wealthy"]}, + {"emoji":"😕","name":"confused face","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["confused","face","befuddled","confusing","dunno","frown","hm","meh","not","sad","sorry","sure"]}, + {"emoji":"🫤","name":"face with diagonal mouth","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["face","with","diagonal","mouth","confused","confusion","disappointed","doubt","doubtful","frustrated","frustration","meh","skeptical","unsure","whatever","wtv"]}, + {"emoji":"😟","name":"worried face","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["worried","face","anxious","butterflies","nerves","nervous","sad","stress","stressed","surprised","worry"]}, + {"emoji":"🙁","name":"slightly frowning face","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["slightly","frowning","face","frown","sad"]}, + {"emoji":"☹️","name":"frowning face","category":"Smileys & People","version":"0.7","skinTone":false,"keywords":["frowning","face"]}, + {"emoji":"😮","name":"face with open mouth","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["face","with","open","mouth","believe","forgot","omg","shocked","surprised","sympathy","unbelievable","unreal","whoa","wow","you"]}, + {"emoji":"😯","name":"hushed face","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["hushed","face","epic","omg","stunned","surprised","whoa","woah"]}, + {"emoji":"😲","name":"astonished face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["astonished","face","cost","no","omg","shocked","totally","way"]}, + {"emoji":"😳","name":"flushed face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["flushed","face","amazed","awkward","crazy","dazed","dead","disbelief","embarrassed","geez","heat","hot","impressed","jeez","what","wow"]}, + {"emoji":"🫪","name":"distorted face","category":"Smileys & People","version":"17.0","skinTone":false,"keywords":["distorted","face","anxiety","bloated","panic","shocked","surprised","vulnerable"]}, + {"emoji":"🥺","name":"pleading face","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["pleading","face","begging","big","eyes","mercy","not","please","pretty","puppy","sad","why"]}, + {"emoji":"🥹","name":"face holding back tears","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["face","holding","back","tears","admiration","aww","cry","embarrassed","feelings","grateful","gratitude","joy","please","proud","resist","sad"]}, + {"emoji":"😦","name":"frowning face with open mouth","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["frowning","face","with","open","mouth","caught","frown","guard","scared","scary","surprise","what","wow"]}, + {"emoji":"😧","name":"anguished face","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["anguished","face","forgot","scared","scary","stressed","surprise","unhappy","what","wow"]}, + {"emoji":"😨","name":"fearful face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["fearful","face","afraid","anxious","blame","fear","scared","worried"]}, + {"emoji":"😰","name":"anxious face with sweat","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["anxious","face","with","sweat","blue","cold","eek","mouth","nervous","open","rushed","scared","yikes"]}, + {"emoji":"😥","name":"sad but relieved face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["sad","but","relieved","face","anxious","call","close","complicated","disappointed","not","sweat","time","whew"]}, + {"emoji":"😢","name":"crying face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["crying","face","awful","cry","feels","miss","sad","tear","triste","unhappy"]}, + {"emoji":"😭","name":"loudly crying face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["loudly","crying","face","bawling","cry","sad","sob","tear","tears","unhappy"]}, + {"emoji":"😱","name":"face screaming in fear","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["face","screaming","in","fear","epic","fearful","munch","scared","scream","screamer","shocked","surprised","woah"]}, + {"emoji":"😖","name":"confounded face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["confounded","face","annoyed","confused","cringe","distraught","feels","frustrated","mad","sad"]}, + {"emoji":"😣","name":"persevering face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["persevering","face","concentrate","concentration","focus","headache","persevere"]}, + {"emoji":"😞","name":"disappointed face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["disappointed","face","awful","blame","dejected","fail","losing","sad","unhappy"]}, + {"emoji":"😓","name":"downcast face with sweat","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["downcast","face","with","sweat","close","cold","feels","headache","nervous","sad","scared","yikes"]}, + {"emoji":"😩","name":"weary face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["weary","face","crying","fail","feels","hungry","mad","nooo","sad","sleepy","tired","unhappy"]}, + {"emoji":"😫","name":"tired face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["tired","face","cost","feels","nap","sad","sneeze"]}, + {"emoji":"🥱","name":"yawning face","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["yawning","face","bedtime","bored","goodnight","nap","night","sleep","sleepy","tired","whatever","yawn","zzz"]}, + {"emoji":"😤","name":"face with steam from nose","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["face","with","steam","from","nose","anger","angry","feels","fume","fuming","furious","fury","mad","triumph","unhappy","won"]}, + {"emoji":"😡","name":"enraged face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["enraged","face","anger","angry","feels","mad","maddening","pouting","rage","red","shade","unhappy","upset"]}, + {"emoji":"😠","name":"angry face","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["angry","face","anger","blame","feels","frustrated","mad","maddening","rage","shade","unhappy","upset"]}, + {"emoji":"🤬","name":"face with symbols on mouth","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["face","with","symbols","on","mouth","censor","cursing","cussing","mad","pissed","swearing"]}, + {"emoji":"😈","name":"smiling face with horns","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["smiling","face","with","horns","demon","devil","evil","fairy","fairytale","fantasy","purple","shade","smile","tale"]}, + {"emoji":"👿","name":"angry face with horns","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["angry","face","with","horns","demon","devil","evil","fairy","fairytale","fantasy","imp","mischievous","purple","shade","tale"]}, + {"emoji":"💀","name":"skull","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["skull","body","dead","death","face","fairy","fairytale","i’m","lmao","monster","tale","yolo"]}, + {"emoji":"☠️","name":"skull and crossbones","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["skull","and","crossbones"]}, + {"emoji":"💩","name":"pile of poo","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["pile","of","poo","bs","comic","doo","dung","face","fml","monster","poop","smelly","smh","stink","stinks","stinky","turd"]}, + {"emoji":"🤡","name":"clown face","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["clown","face"]}, + {"emoji":"👹","name":"ogre","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["ogre","creature","devil","face","fairy","fairytale","fantasy","mask","monster","scary","tale"]}, + {"emoji":"👺","name":"goblin","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["goblin","angry","creature","face","fairy","fairytale","fantasy","mask","mean","monster","tale"]}, + {"emoji":"👻","name":"ghost","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["ghost","boo","creature","excited","face","fairy","fairytale","fantasy","halloween","haunting","monster","scary","silly","tale"]}, + {"emoji":"👽","name":"alien","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["alien","creature","extraterrestrial","face","fairy","fairytale","fantasy","monster","space","tale","ufo"]}, + {"emoji":"👾","name":"alien monster","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["alien","monster","creature","extraterrestrial","face","fairy","fairytale","fantasy","game","gamer","games","pixelated","space","tale","ufo"]}, + {"emoji":"🤖","name":"robot","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["robot","face","monster"]}, + {"emoji":"😺","name":"grinning cat","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["grinning","cat","animal","face","mouth","open","smile","smiling"]}, + {"emoji":"😸","name":"grinning cat with smiling eyes","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["grinning","cat","with","smiling","eyes","animal","eye","face","grin","smile"]}, + {"emoji":"😹","name":"cat with tears of joy","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["cat","with","tears","of","joy","animal","face","laugh","laughing","lol","tear"]}, + {"emoji":"😻","name":"smiling cat with heart-eyes","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["smiling","cat","with","heart-eyes","animal","eye","face","heart","love","smile"]}, + {"emoji":"😼","name":"cat with wry smile","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["cat","with","wry","smile","animal","face","ironic"]}, + {"emoji":"😽","name":"kissing cat","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["kissing","cat","animal","closed","eye","eyes","face","kiss"]}, + {"emoji":"🙀","name":"weary cat","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["weary","cat","animal","face","oh","surprised"]}, + {"emoji":"😿","name":"crying cat","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["crying","cat","animal","cry","face","sad","tear"]}, + {"emoji":"😾","name":"pouting cat","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["pouting","cat","animal","face"]}, + {"emoji":"🙈","name":"see-no-evil monkey","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["see-no-evil","monkey","embarrassed","evil","face","forbidden","forgot","gesture","hide","no","omg","prohibited","scared","secret","smh","watch"]}, + {"emoji":"🙉","name":"hear-no-evil monkey","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["hear-no-evil","monkey","animal","ears","evil","face","forbidden","gesture","hear","listen","no","not","prohibited","secret","shh","tmi"]}, + {"emoji":"🙊","name":"speak-no-evil monkey","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["speak-no-evil","monkey","animal","evil","face","forbidden","gesture","no","not","oops","prohibited","quiet","secret","speak","stealth"]}, + {"emoji":"💌","name":"love letter","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["love","letter","heart","mail","romance","valentine"]}, + {"emoji":"💘","name":"heart with arrow","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["heart","with","arrow","143","adorbs","cupid","date","emotion","ily","love","romance","valentine"]}, + {"emoji":"💝","name":"heart with ribbon","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["heart","with","ribbon","143","anniversary","emotion","ily","kisses","valentine","xoxo"]}, + {"emoji":"💖","name":"sparkling heart","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["sparkling","heart","143","emotion","excited","good","ily","kisses","morning","night","sparkle","xoxo"]}, + {"emoji":"💗","name":"growing heart","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["growing","heart","143","emotion","excited","heartpulse","ily","kisses","muah","nervous","pulse","xoxo"]}, + {"emoji":"💓","name":"beating heart","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["beating","heart","143","cardio","emotion","heartbeat","ily","love","pulsating","pulse"]}, + {"emoji":"💞","name":"revolving hearts","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["revolving","hearts","143","adorbs","anniversary","emotion","heart"]}, + {"emoji":"💕","name":"two hearts","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["two","hearts","143","anniversary","date","dating","emotion","heart","ily","kisses","love","loving","xoxo"]}, + {"emoji":"💟","name":"heart decoration","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["heart","decoration","143","emotion","hearth","purple","white"]}, + {"emoji":"❣️","name":"heart exclamation","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["heart","exclamation"]}, + {"emoji":"💔","name":"broken heart","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["broken","heart","break","crushed","emotion","heartbroken","lonely","sad"]}, + {"emoji":"❤️‍🔥","name":"heart on fire","category":"Smileys & People","version":"13.1","skinTone":false,"keywords":["heart","on","fire"]}, + {"emoji":"❤️‍🩹","name":"mending heart","category":"Smileys & People","version":"13.1","skinTone":false,"keywords":["mending","heart"]}, + {"emoji":"❤️","name":"red heart","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["red","heart"]}, + {"emoji":"🩷","name":"pink heart","category":"Smileys & People","version":"15.0","skinTone":false,"keywords":["pink","heart","143","adorable","cute","emotion","ily","like","love","special","sweet"]}, + {"emoji":"🧡","name":"orange heart","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["orange","heart","143"]}, + {"emoji":"💛","name":"yellow heart","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["yellow","heart","143","cardiac","emotion","ily","love"]}, + {"emoji":"💚","name":"green heart","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["green","heart","143","emotion","ily","love","romantic"]}, + {"emoji":"💙","name":"blue heart","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["blue","heart","143","emotion","ily","love","romance"]}, + {"emoji":"🩵","name":"light blue heart","category":"Smileys & People","version":"15.0","skinTone":false,"keywords":["light","blue","heart","143","cute","cyan","emotion","ily","like","love","sky","special","teal"]}, + {"emoji":"💜","name":"purple heart","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["purple","heart","143","bestest","emotion","ily","love"]}, + {"emoji":"🤎","name":"brown heart","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["brown","heart","143"]}, + {"emoji":"🖤","name":"black heart","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["black","heart","evil","wicked"]}, + {"emoji":"🩶","name":"grey heart","category":"Smileys & People","version":"15.0","skinTone":false,"keywords":["grey","heart","143","emotion","gray","ily","love","silver","slate","special"]}, + {"emoji":"🤍","name":"white heart","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["white","heart","143"]}, + {"emoji":"💋","name":"kiss mark","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["kiss","mark","dating","emotion","heart","kissing","lips","romance","sexy"]}, + {"emoji":"💯","name":"hundred points","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["hundred","points","100","a+","agree","clearly","definitely","faithful","fleek","full","keep","perfect","point","score","TRUE","truth","yup"]}, + {"emoji":"💢","name":"anger symbol","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["anger","symbol","angry","comic","mad","upset"]}, + {"emoji":"🫯","name":"fight cloud","category":"Smileys & People","version":"17.0","skinTone":false,"keywords":["fight","cloud","argument","brawl","debate","disagreement","ruckus","wrestle"]}, + {"emoji":"💥","name":"collision","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["collision","bomb","boom","collide","comic","explode"]}, + {"emoji":"💫","name":"dizzy","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["dizzy","comic","shining","shooting","star","stars"]}, + {"emoji":"💦","name":"sweat droplets","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["sweat","droplets","comic","drip","droplet","drops","splashing","squirt","water","wet","work","workout"]}, + {"emoji":"💨","name":"dashing away","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["dashing","away","cloud","comic","dash","fart","fast","go","gone","gotta","running","smoke"]}, + {"emoji":"🕳️","name":"hole","category":"Smileys & People","version":"0.7","skinTone":false,"keywords":["hole"]}, + {"emoji":"💬","name":"speech balloon","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["speech","balloon","bubble","comic","dialog","message","sms","talk","text","typing"]}, + {"emoji":"👁️‍🗨️","name":"eye in speech bubble","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["eye","in","speech","bubble"]}, + {"emoji":"🗨️","name":"left speech bubble","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["left","speech","bubble"]}, + {"emoji":"🗯️","name":"right anger bubble","category":"Smileys & People","version":"0.7","skinTone":false,"keywords":["right","anger","bubble"]}, + {"emoji":"💭","name":"thought balloon","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["thought","balloon","bubble","cartoon","cloud","comic","daydream","decisions","dream","idea","invent","invention","realize","think","thoughts","wonder"]}, + {"emoji":"💤","name":"ZZZ","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["ZZZ","comic","good","goodnight","night","sleep","sleeping","sleepy","tired","zzz"]}, + {"emoji":"👋","name":"waving hand","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["waving","hand","bye","cya","g2g","greetings","gtg","hello","hey","hi","later","outtie","ttfn","ttyl","wave","yo","you"]}, + {"emoji":"👋🏻","name":"waving hand: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["waving","hand:","light","skin","tone"]}, + {"emoji":"👋🏼","name":"waving hand: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["waving","hand:","medium-light","skin","tone"]}, + {"emoji":"👋🏽","name":"waving hand: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["waving","hand:","medium","skin","tone"]}, + {"emoji":"👋🏾","name":"waving hand: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["waving","hand:","medium-dark","skin","tone"]}, + {"emoji":"👋🏿","name":"waving hand: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["waving","hand:","dark","skin","tone"]}, + {"emoji":"🤚","name":"raised back of hand","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["raised","back","of","hand","backhand"]}, + {"emoji":"🤚🏻","name":"raised back of hand: light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["raised","back","of","hand:","light","skin","tone"]}, + {"emoji":"🤚🏼","name":"raised back of hand: medium-light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["raised","back","of","hand:","medium-light","skin","tone"]}, + {"emoji":"🤚🏽","name":"raised back of hand: medium skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["raised","back","of","hand:","medium","skin","tone"]}, + {"emoji":"🤚🏾","name":"raised back of hand: medium-dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["raised","back","of","hand:","medium-dark","skin","tone"]}, + {"emoji":"🤚🏿","name":"raised back of hand: dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["raised","back","of","hand:","dark","skin","tone"]}, + {"emoji":"🖐️","name":"hand with fingers splayed","category":"Smileys & People","version":"0.7","skinTone":false,"keywords":["hand","with","fingers","splayed"]}, + {"emoji":"🖐🏻","name":"hand with fingers splayed: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["hand","with","fingers","splayed:","light","skin","tone"]}, + {"emoji":"🖐🏼","name":"hand with fingers splayed: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["hand","with","fingers","splayed:","medium-light","skin","tone"]}, + {"emoji":"🖐🏽","name":"hand with fingers splayed: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["hand","with","fingers","splayed:","medium","skin","tone"]}, + {"emoji":"🖐🏾","name":"hand with fingers splayed: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["hand","with","fingers","splayed:","medium-dark","skin","tone"]}, + {"emoji":"🖐🏿","name":"hand with fingers splayed: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["hand","with","fingers","splayed:","dark","skin","tone"]}, + {"emoji":"✋","name":"raised hand","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["raised","hand","5","five","high","stop"]}, + {"emoji":"✋🏻","name":"raised hand: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["raised","hand:","light","skin","tone"]}, + {"emoji":"✋🏼","name":"raised hand: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["raised","hand:","medium-light","skin","tone"]}, + {"emoji":"✋🏽","name":"raised hand: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["raised","hand:","medium","skin","tone"]}, + {"emoji":"✋🏾","name":"raised hand: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["raised","hand:","medium-dark","skin","tone"]}, + {"emoji":"✋🏿","name":"raised hand: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["raised","hand:","dark","skin","tone"]}, + {"emoji":"🖖","name":"vulcan salute","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["vulcan","salute","finger","hand","hands","Vulcan"]}, + {"emoji":"🖖🏻","name":"vulcan salute: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["vulcan","salute:","light","skin","tone"]}, + {"emoji":"🖖🏼","name":"vulcan salute: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["vulcan","salute:","medium-light","skin","tone"]}, + {"emoji":"🖖🏽","name":"vulcan salute: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["vulcan","salute:","medium","skin","tone"]}, + {"emoji":"🖖🏾","name":"vulcan salute: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["vulcan","salute:","medium-dark","skin","tone"]}, + {"emoji":"🖖🏿","name":"vulcan salute: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["vulcan","salute:","dark","skin","tone"]}, + {"emoji":"🫱","name":"rightwards hand","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["rightwards","hand","handshake","hold","reach","right","rightward","shake"]}, + {"emoji":"🫱🏻","name":"rightwards hand: light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["rightwards","hand:","light","skin","tone"]}, + {"emoji":"🫱🏼","name":"rightwards hand: medium-light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["rightwards","hand:","medium-light","skin","tone"]}, + {"emoji":"🫱🏽","name":"rightwards hand: medium skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["rightwards","hand:","medium","skin","tone"]}, + {"emoji":"🫱🏾","name":"rightwards hand: medium-dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["rightwards","hand:","medium-dark","skin","tone"]}, + {"emoji":"🫱🏿","name":"rightwards hand: dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["rightwards","hand:","dark","skin","tone"]}, + {"emoji":"🫲","name":"leftwards hand","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["leftwards","hand","handshake","hold","left","leftward","reach","shake"]}, + {"emoji":"🫲🏻","name":"leftwards hand: light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["leftwards","hand:","light","skin","tone"]}, + {"emoji":"🫲🏼","name":"leftwards hand: medium-light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["leftwards","hand:","medium-light","skin","tone"]}, + {"emoji":"🫲🏽","name":"leftwards hand: medium skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["leftwards","hand:","medium","skin","tone"]}, + {"emoji":"🫲🏾","name":"leftwards hand: medium-dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["leftwards","hand:","medium-dark","skin","tone"]}, + {"emoji":"🫲🏿","name":"leftwards hand: dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["leftwards","hand:","dark","skin","tone"]}, + {"emoji":"🫳","name":"palm down hand","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["palm","down","hand","dismiss","drop","dropped","pick","shoo","up"]}, + {"emoji":"🫳🏻","name":"palm down hand: light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["palm","down","hand:","light","skin","tone"]}, + {"emoji":"🫳🏼","name":"palm down hand: medium-light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["palm","down","hand:","medium-light","skin","tone"]}, + {"emoji":"🫳🏽","name":"palm down hand: medium skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["palm","down","hand:","medium","skin","tone"]}, + {"emoji":"🫳🏾","name":"palm down hand: medium-dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["palm","down","hand:","medium-dark","skin","tone"]}, + {"emoji":"🫳🏿","name":"palm down hand: dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["palm","down","hand:","dark","skin","tone"]}, + {"emoji":"🫴","name":"palm up hand","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["palm","up","hand","beckon","catch","come","hold","know","lift","me","offer","tell"]}, + {"emoji":"🫴🏻","name":"palm up hand: light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["palm","up","hand:","light","skin","tone"]}, + {"emoji":"🫴🏼","name":"palm up hand: medium-light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["palm","up","hand:","medium-light","skin","tone"]}, + {"emoji":"🫴🏽","name":"palm up hand: medium skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["palm","up","hand:","medium","skin","tone"]}, + {"emoji":"🫴🏾","name":"palm up hand: medium-dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["palm","up","hand:","medium-dark","skin","tone"]}, + {"emoji":"🫴🏿","name":"palm up hand: dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["palm","up","hand:","dark","skin","tone"]}, + {"emoji":"🫷","name":"leftwards pushing hand","category":"Smileys & People","version":"15.0","skinTone":false,"keywords":["leftwards","pushing","hand","block","five","halt","high","hold","leftward","pause","push","refuse","slap","stop","wait"]}, + {"emoji":"🫷🏻","name":"leftwards pushing hand: light skin tone","category":"Smileys & People","version":"15.0","skinTone":true,"keywords":["leftwards","pushing","hand:","light","skin","tone"]}, + {"emoji":"🫷🏼","name":"leftwards pushing hand: medium-light skin tone","category":"Smileys & People","version":"15.0","skinTone":true,"keywords":["leftwards","pushing","hand:","medium-light","skin","tone"]}, + {"emoji":"🫷🏽","name":"leftwards pushing hand: medium skin tone","category":"Smileys & People","version":"15.0","skinTone":true,"keywords":["leftwards","pushing","hand:","medium","skin","tone"]}, + {"emoji":"🫷🏾","name":"leftwards pushing hand: medium-dark skin tone","category":"Smileys & People","version":"15.0","skinTone":true,"keywords":["leftwards","pushing","hand:","medium-dark","skin","tone"]}, + {"emoji":"🫷🏿","name":"leftwards pushing hand: dark skin tone","category":"Smileys & People","version":"15.0","skinTone":true,"keywords":["leftwards","pushing","hand:","dark","skin","tone"]}, + {"emoji":"🫸","name":"rightwards pushing hand","category":"Smileys & People","version":"15.0","skinTone":false,"keywords":["rightwards","pushing","hand","block","five","halt","high","hold","pause","push","refuse","rightward","slap","stop","wait"]}, + {"emoji":"🫸🏻","name":"rightwards pushing hand: light skin tone","category":"Smileys & People","version":"15.0","skinTone":true,"keywords":["rightwards","pushing","hand:","light","skin","tone"]}, + {"emoji":"🫸🏼","name":"rightwards pushing hand: medium-light skin tone","category":"Smileys & People","version":"15.0","skinTone":true,"keywords":["rightwards","pushing","hand:","medium-light","skin","tone"]}, + {"emoji":"🫸🏽","name":"rightwards pushing hand: medium skin tone","category":"Smileys & People","version":"15.0","skinTone":true,"keywords":["rightwards","pushing","hand:","medium","skin","tone"]}, + {"emoji":"🫸🏾","name":"rightwards pushing hand: medium-dark skin tone","category":"Smileys & People","version":"15.0","skinTone":true,"keywords":["rightwards","pushing","hand:","medium-dark","skin","tone"]}, + {"emoji":"🫸🏿","name":"rightwards pushing hand: dark skin tone","category":"Smileys & People","version":"15.0","skinTone":true,"keywords":["rightwards","pushing","hand:","dark","skin","tone"]}, + {"emoji":"👌","name":"OK hand","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["OK","hand","awesome","bet","dope","fleek","fosho","got","gotcha","legit","okay","pinch","rad","sure","sweet","three"]}, + {"emoji":"👌🏻","name":"OK hand: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["OK","hand:","light","skin","tone"]}, + {"emoji":"👌🏼","name":"OK hand: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["OK","hand:","medium-light","skin","tone"]}, + {"emoji":"👌🏽","name":"OK hand: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["OK","hand:","medium","skin","tone"]}, + {"emoji":"👌🏾","name":"OK hand: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["OK","hand:","medium-dark","skin","tone"]}, + {"emoji":"👌🏿","name":"OK hand: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["OK","hand:","dark","skin","tone"]}, + {"emoji":"🤌","name":"pinched fingers","category":"Smileys & People","version":"13.0","skinTone":false,"keywords":["pinched","fingers","gesture","hand","hold","huh","interrogation","patience","relax","sarcastic","ugh","what","zip"]}, + {"emoji":"🤌🏻","name":"pinched fingers: light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["pinched","fingers:","light","skin","tone"]}, + {"emoji":"🤌🏼","name":"pinched fingers: medium-light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["pinched","fingers:","medium-light","skin","tone"]}, + {"emoji":"🤌🏽","name":"pinched fingers: medium skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["pinched","fingers:","medium","skin","tone"]}, + {"emoji":"🤌🏾","name":"pinched fingers: medium-dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["pinched","fingers:","medium-dark","skin","tone"]}, + {"emoji":"🤌🏿","name":"pinched fingers: dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["pinched","fingers:","dark","skin","tone"]}, + {"emoji":"🤏","name":"pinching hand","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["pinching","hand","amount","bit","fingers","little","small","sort"]}, + {"emoji":"🤏🏻","name":"pinching hand: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["pinching","hand:","light","skin","tone"]}, + {"emoji":"🤏🏼","name":"pinching hand: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["pinching","hand:","medium-light","skin","tone"]}, + {"emoji":"🤏🏽","name":"pinching hand: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["pinching","hand:","medium","skin","tone"]}, + {"emoji":"🤏🏾","name":"pinching hand: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["pinching","hand:","medium-dark","skin","tone"]}, + {"emoji":"🤏🏿","name":"pinching hand: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["pinching","hand:","dark","skin","tone"]}, + {"emoji":"✌️","name":"victory hand","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["victory","hand"]}, + {"emoji":"✌🏻","name":"victory hand: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["victory","hand:","light","skin","tone"]}, + {"emoji":"✌🏼","name":"victory hand: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["victory","hand:","medium-light","skin","tone"]}, + {"emoji":"✌🏽","name":"victory hand: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["victory","hand:","medium","skin","tone"]}, + {"emoji":"✌🏾","name":"victory hand: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["victory","hand:","medium-dark","skin","tone"]}, + {"emoji":"✌🏿","name":"victory hand: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["victory","hand:","dark","skin","tone"]}, + {"emoji":"🤞","name":"crossed fingers","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["crossed","fingers","cross","finger","hand","luck"]}, + {"emoji":"🤞🏻","name":"crossed fingers: light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["crossed","fingers:","light","skin","tone"]}, + {"emoji":"🤞🏼","name":"crossed fingers: medium-light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["crossed","fingers:","medium-light","skin","tone"]}, + {"emoji":"🤞🏽","name":"crossed fingers: medium skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["crossed","fingers:","medium","skin","tone"]}, + {"emoji":"🤞🏾","name":"crossed fingers: medium-dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["crossed","fingers:","medium-dark","skin","tone"]}, + {"emoji":"🤞🏿","name":"crossed fingers: dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["crossed","fingers:","dark","skin","tone"]}, + {"emoji":"🫰","name":"hand with index finger and thumb crossed","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["hand","with","index","finger","and","thumb","crossed","<3","expensive","heart","love","money","snap"]}, + {"emoji":"🫰🏻","name":"hand with index finger and thumb crossed: light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["hand","with","index","finger","and","thumb","crossed:","light","skin","tone"]}, + {"emoji":"🫰🏼","name":"hand with index finger and thumb crossed: medium-light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["hand","with","index","finger","and","thumb","crossed:","medium-light","skin","tone"]}, + {"emoji":"🫰🏽","name":"hand with index finger and thumb crossed: medium skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["hand","with","index","finger","and","thumb","crossed:","medium","skin","tone"]}, + {"emoji":"🫰🏾","name":"hand with index finger and thumb crossed: medium-dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["hand","with","index","finger","and","thumb","crossed:","medium-dark","skin","tone"]}, + {"emoji":"🫰🏿","name":"hand with index finger and thumb crossed: dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["hand","with","index","finger","and","thumb","crossed:","dark","skin","tone"]}, + {"emoji":"🤟","name":"love-you gesture","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["love-you","gesture","fingers","hand","ILY","love","three","you"]}, + {"emoji":"🤟🏻","name":"love-you gesture: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["love-you","gesture:","light","skin","tone"]}, + {"emoji":"🤟🏼","name":"love-you gesture: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["love-you","gesture:","medium-light","skin","tone"]}, + {"emoji":"🤟🏽","name":"love-you gesture: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["love-you","gesture:","medium","skin","tone"]}, + {"emoji":"🤟🏾","name":"love-you gesture: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["love-you","gesture:","medium-dark","skin","tone"]}, + {"emoji":"🤟🏿","name":"love-you gesture: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["love-you","gesture:","dark","skin","tone"]}, + {"emoji":"🤘","name":"sign of the horns","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["sign","of","the","horns","finger","hand","rock-on"]}, + {"emoji":"🤘🏻","name":"sign of the horns: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["sign","of","the","horns:","light","skin","tone"]}, + {"emoji":"🤘🏼","name":"sign of the horns: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["sign","of","the","horns:","medium-light","skin","tone"]}, + {"emoji":"🤘🏽","name":"sign of the horns: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["sign","of","the","horns:","medium","skin","tone"]}, + {"emoji":"🤘🏾","name":"sign of the horns: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["sign","of","the","horns:","medium-dark","skin","tone"]}, + {"emoji":"🤘🏿","name":"sign of the horns: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["sign","of","the","horns:","dark","skin","tone"]}, + {"emoji":"🤙","name":"call me hand","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["call","me","hand","hang","loose","Shaka"]}, + {"emoji":"🤙🏻","name":"call me hand: light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["call","me","hand:","light","skin","tone"]}, + {"emoji":"🤙🏼","name":"call me hand: medium-light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["call","me","hand:","medium-light","skin","tone"]}, + {"emoji":"🤙🏽","name":"call me hand: medium skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["call","me","hand:","medium","skin","tone"]}, + {"emoji":"🤙🏾","name":"call me hand: medium-dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["call","me","hand:","medium-dark","skin","tone"]}, + {"emoji":"🤙🏿","name":"call me hand: dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["call","me","hand:","dark","skin","tone"]}, + {"emoji":"👈","name":"backhand index pointing left","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["backhand","index","pointing","left","finger","hand","point"]}, + {"emoji":"👈🏻","name":"backhand index pointing left: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","left:","light","skin","tone"]}, + {"emoji":"👈🏼","name":"backhand index pointing left: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","left:","medium-light","skin","tone"]}, + {"emoji":"👈🏽","name":"backhand index pointing left: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","left:","medium","skin","tone"]}, + {"emoji":"👈🏾","name":"backhand index pointing left: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","left:","medium-dark","skin","tone"]}, + {"emoji":"👈🏿","name":"backhand index pointing left: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","left:","dark","skin","tone"]}, + {"emoji":"👉","name":"backhand index pointing right","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["backhand","index","pointing","right","finger","hand","point"]}, + {"emoji":"👉🏻","name":"backhand index pointing right: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","right:","light","skin","tone"]}, + {"emoji":"👉🏼","name":"backhand index pointing right: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","right:","medium-light","skin","tone"]}, + {"emoji":"👉🏽","name":"backhand index pointing right: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","right:","medium","skin","tone"]}, + {"emoji":"👉🏾","name":"backhand index pointing right: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","right:","medium-dark","skin","tone"]}, + {"emoji":"👉🏿","name":"backhand index pointing right: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","right:","dark","skin","tone"]}, + {"emoji":"👆","name":"backhand index pointing up","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["backhand","index","pointing","up","finger","hand","point"]}, + {"emoji":"👆🏻","name":"backhand index pointing up: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","up:","light","skin","tone"]}, + {"emoji":"👆🏼","name":"backhand index pointing up: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","up:","medium-light","skin","tone"]}, + {"emoji":"👆🏽","name":"backhand index pointing up: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","up:","medium","skin","tone"]}, + {"emoji":"👆🏾","name":"backhand index pointing up: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","up:","medium-dark","skin","tone"]}, + {"emoji":"👆🏿","name":"backhand index pointing up: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","up:","dark","skin","tone"]}, + {"emoji":"🖕","name":"middle finger","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["middle","finger","hand"]}, + {"emoji":"🖕🏻","name":"middle finger: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["middle","finger:","light","skin","tone"]}, + {"emoji":"🖕🏼","name":"middle finger: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["middle","finger:","medium-light","skin","tone"]}, + {"emoji":"🖕🏽","name":"middle finger: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["middle","finger:","medium","skin","tone"]}, + {"emoji":"🖕🏾","name":"middle finger: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["middle","finger:","medium-dark","skin","tone"]}, + {"emoji":"🖕🏿","name":"middle finger: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["middle","finger:","dark","skin","tone"]}, + {"emoji":"👇","name":"backhand index pointing down","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["backhand","index","pointing","down","finger","hand","point"]}, + {"emoji":"👇🏻","name":"backhand index pointing down: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","down:","light","skin","tone"]}, + {"emoji":"👇🏼","name":"backhand index pointing down: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","down:","medium-light","skin","tone"]}, + {"emoji":"👇🏽","name":"backhand index pointing down: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","down:","medium","skin","tone"]}, + {"emoji":"👇🏾","name":"backhand index pointing down: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","down:","medium-dark","skin","tone"]}, + {"emoji":"👇🏿","name":"backhand index pointing down: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["backhand","index","pointing","down:","dark","skin","tone"]}, + {"emoji":"☝️","name":"index pointing up","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["index","pointing","up"]}, + {"emoji":"☝🏻","name":"index pointing up: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["index","pointing","up:","light","skin","tone"]}, + {"emoji":"☝🏼","name":"index pointing up: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["index","pointing","up:","medium-light","skin","tone"]}, + {"emoji":"☝🏽","name":"index pointing up: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["index","pointing","up:","medium","skin","tone"]}, + {"emoji":"☝🏾","name":"index pointing up: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["index","pointing","up:","medium-dark","skin","tone"]}, + {"emoji":"☝🏿","name":"index pointing up: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["index","pointing","up:","dark","skin","tone"]}, + {"emoji":"🫵","name":"index pointing at the viewer","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["index","pointing","at","the","viewer","finger","hand","poke","you"]}, + {"emoji":"🫵🏻","name":"index pointing at the viewer: light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["index","pointing","at","the","viewer:","light","skin","tone"]}, + {"emoji":"🫵🏼","name":"index pointing at the viewer: medium-light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["index","pointing","at","the","viewer:","medium-light","skin","tone"]}, + {"emoji":"🫵🏽","name":"index pointing at the viewer: medium skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["index","pointing","at","the","viewer:","medium","skin","tone"]}, + {"emoji":"🫵🏾","name":"index pointing at the viewer: medium-dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["index","pointing","at","the","viewer:","medium-dark","skin","tone"]}, + {"emoji":"🫵🏿","name":"index pointing at the viewer: dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["index","pointing","at","the","viewer:","dark","skin","tone"]}, + {"emoji":"👍","name":"thumbs up","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["thumbs","up","+1","good","hand","like","thumb","yes"]}, + {"emoji":"👍🏻","name":"thumbs up: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["thumbs","up:","light","skin","tone"]}, + {"emoji":"👍🏼","name":"thumbs up: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["thumbs","up:","medium-light","skin","tone"]}, + {"emoji":"👍🏽","name":"thumbs up: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["thumbs","up:","medium","skin","tone"]}, + {"emoji":"👍🏾","name":"thumbs up: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["thumbs","up:","medium-dark","skin","tone"]}, + {"emoji":"👍🏿","name":"thumbs up: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["thumbs","up:","dark","skin","tone"]}, + {"emoji":"👎","name":"thumbs down","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["thumbs","down","-1","bad","dislike","good","hand","no","nope","thumb"]}, + {"emoji":"👎🏻","name":"thumbs down: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["thumbs","down:","light","skin","tone"]}, + {"emoji":"👎🏼","name":"thumbs down: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["thumbs","down:","medium-light","skin","tone"]}, + {"emoji":"👎🏽","name":"thumbs down: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["thumbs","down:","medium","skin","tone"]}, + {"emoji":"👎🏾","name":"thumbs down: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["thumbs","down:","medium-dark","skin","tone"]}, + {"emoji":"👎🏿","name":"thumbs down: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["thumbs","down:","dark","skin","tone"]}, + {"emoji":"✊","name":"raised fist","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["raised","fist","clenched","hand","punch","solidarity"]}, + {"emoji":"✊🏻","name":"raised fist: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["raised","fist:","light","skin","tone"]}, + {"emoji":"✊🏼","name":"raised fist: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["raised","fist:","medium-light","skin","tone"]}, + {"emoji":"✊🏽","name":"raised fist: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["raised","fist:","medium","skin","tone"]}, + {"emoji":"✊🏾","name":"raised fist: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["raised","fist:","medium-dark","skin","tone"]}, + {"emoji":"✊🏿","name":"raised fist: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["raised","fist:","dark","skin","tone"]}, + {"emoji":"👊","name":"oncoming fist","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["oncoming","fist","absolutely","agree","boom","bro","bruh","bump","clenched","correct","hand","knuckle","pound","punch","rock","ttyl"]}, + {"emoji":"👊🏻","name":"oncoming fist: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["oncoming","fist:","light","skin","tone"]}, + {"emoji":"👊🏼","name":"oncoming fist: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["oncoming","fist:","medium-light","skin","tone"]}, + {"emoji":"👊🏽","name":"oncoming fist: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["oncoming","fist:","medium","skin","tone"]}, + {"emoji":"👊🏾","name":"oncoming fist: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["oncoming","fist:","medium-dark","skin","tone"]}, + {"emoji":"👊🏿","name":"oncoming fist: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["oncoming","fist:","dark","skin","tone"]}, + {"emoji":"🤛","name":"left-facing fist","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["left-facing","fist","leftwards"]}, + {"emoji":"🤛🏻","name":"left-facing fist: light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["left-facing","fist:","light","skin","tone"]}, + {"emoji":"🤛🏼","name":"left-facing fist: medium-light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["left-facing","fist:","medium-light","skin","tone"]}, + {"emoji":"🤛🏽","name":"left-facing fist: medium skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["left-facing","fist:","medium","skin","tone"]}, + {"emoji":"🤛🏾","name":"left-facing fist: medium-dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["left-facing","fist:","medium-dark","skin","tone"]}, + {"emoji":"🤛🏿","name":"left-facing fist: dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["left-facing","fist:","dark","skin","tone"]}, + {"emoji":"🤜","name":"right-facing fist","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["right-facing","fist","rightwards"]}, + {"emoji":"🤜🏻","name":"right-facing fist: light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["right-facing","fist:","light","skin","tone"]}, + {"emoji":"🤜🏼","name":"right-facing fist: medium-light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["right-facing","fist:","medium-light","skin","tone"]}, + {"emoji":"🤜🏽","name":"right-facing fist: medium skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["right-facing","fist:","medium","skin","tone"]}, + {"emoji":"🤜🏾","name":"right-facing fist: medium-dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["right-facing","fist:","medium-dark","skin","tone"]}, + {"emoji":"🤜🏿","name":"right-facing fist: dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["right-facing","fist:","dark","skin","tone"]}, + {"emoji":"👏","name":"clapping hands","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["clapping","hands","applause","approval","awesome","clap","congrats","congratulations","excited","good","great","hand","homie","job","nice","prayed","well","yay"]}, + {"emoji":"👏🏻","name":"clapping hands: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["clapping","hands:","light","skin","tone"]}, + {"emoji":"👏🏼","name":"clapping hands: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["clapping","hands:","medium-light","skin","tone"]}, + {"emoji":"👏🏽","name":"clapping hands: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["clapping","hands:","medium","skin","tone"]}, + {"emoji":"👏🏾","name":"clapping hands: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["clapping","hands:","medium-dark","skin","tone"]}, + {"emoji":"👏🏿","name":"clapping hands: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["clapping","hands:","dark","skin","tone"]}, + {"emoji":"🙌","name":"raising hands","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["raising","hands","celebration","gesture","hand","hooray","praise","raised"]}, + {"emoji":"🙌🏻","name":"raising hands: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["raising","hands:","light","skin","tone"]}, + {"emoji":"🙌🏼","name":"raising hands: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["raising","hands:","medium-light","skin","tone"]}, + {"emoji":"🙌🏽","name":"raising hands: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["raising","hands:","medium","skin","tone"]}, + {"emoji":"🙌🏾","name":"raising hands: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["raising","hands:","medium-dark","skin","tone"]}, + {"emoji":"🙌🏿","name":"raising hands: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["raising","hands:","dark","skin","tone"]}, + {"emoji":"🫶","name":"heart hands","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["heart","hands","<3","love","you"]}, + {"emoji":"🫶🏻","name":"heart hands: light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["heart","hands:","light","skin","tone"]}, + {"emoji":"🫶🏼","name":"heart hands: medium-light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["heart","hands:","medium-light","skin","tone"]}, + {"emoji":"🫶🏽","name":"heart hands: medium skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["heart","hands:","medium","skin","tone"]}, + {"emoji":"🫶🏾","name":"heart hands: medium-dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["heart","hands:","medium-dark","skin","tone"]}, + {"emoji":"🫶🏿","name":"heart hands: dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["heart","hands:","dark","skin","tone"]}, + {"emoji":"👐","name":"open hands","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["open","hands","hand","hug","jazz","swerve"]}, + {"emoji":"👐🏻","name":"open hands: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["open","hands:","light","skin","tone"]}, + {"emoji":"👐🏼","name":"open hands: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["open","hands:","medium-light","skin","tone"]}, + {"emoji":"👐🏽","name":"open hands: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["open","hands:","medium","skin","tone"]}, + {"emoji":"👐🏾","name":"open hands: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["open","hands:","medium-dark","skin","tone"]}, + {"emoji":"👐🏿","name":"open hands: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["open","hands:","dark","skin","tone"]}, + {"emoji":"🤲","name":"palms up together","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["palms","up","together","cupped","dua","hands","pray","prayer","wish"]}, + {"emoji":"🤲🏻","name":"palms up together: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["palms","up","together:","light","skin","tone"]}, + {"emoji":"🤲🏼","name":"palms up together: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["palms","up","together:","medium-light","skin","tone"]}, + {"emoji":"🤲🏽","name":"palms up together: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["palms","up","together:","medium","skin","tone"]}, + {"emoji":"🤲🏾","name":"palms up together: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["palms","up","together:","medium-dark","skin","tone"]}, + {"emoji":"🤲🏿","name":"palms up together: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["palms","up","together:","dark","skin","tone"]}, + {"emoji":"🤝","name":"handshake","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["handshake","agreement","deal","hand","meeting","shake"]}, + {"emoji":"🤝🏻","name":"handshake: light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","light","skin","tone"]}, + {"emoji":"🤝🏼","name":"handshake: medium-light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","medium-light","skin","tone"]}, + {"emoji":"🤝🏽","name":"handshake: medium skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","medium","skin","tone"]}, + {"emoji":"🤝🏾","name":"handshake: medium-dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","medium-dark","skin","tone"]}, + {"emoji":"🤝🏿","name":"handshake: dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","dark","skin","tone"]}, + {"emoji":"🫱🏻‍🫲🏼","name":"handshake: light skin tone, medium-light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","light","skin","tone,","medium-light","tone"]}, + {"emoji":"🫱🏻‍🫲🏽","name":"handshake: light skin tone, medium skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","light","skin","tone,","medium","tone"]}, + {"emoji":"🫱🏻‍🫲🏾","name":"handshake: light skin tone, medium-dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"🫱🏻‍🫲🏿","name":"handshake: light skin tone, dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","light","skin","tone,","dark","tone"]}, + {"emoji":"🫱🏼‍🫲🏻","name":"handshake: medium-light skin tone, light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","medium-light","skin","tone,","light","tone"]}, + {"emoji":"🫱🏼‍🫲🏽","name":"handshake: medium-light skin tone, medium skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"🫱🏼‍🫲🏾","name":"handshake: medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"🫱🏼‍🫲🏿","name":"handshake: medium-light skin tone, dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"🫱🏽‍🫲🏻","name":"handshake: medium skin tone, light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","medium","skin","tone,","light","tone"]}, + {"emoji":"🫱🏽‍🫲🏼","name":"handshake: medium skin tone, medium-light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"🫱🏽‍🫲🏾","name":"handshake: medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"🫱🏽‍🫲🏿","name":"handshake: medium skin tone, dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","medium","skin","tone,","dark","tone"]}, + {"emoji":"🫱🏾‍🫲🏻","name":"handshake: medium-dark skin tone, light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"🫱🏾‍🫲🏼","name":"handshake: medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"🫱🏾‍🫲🏽","name":"handshake: medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"🫱🏾‍🫲🏿","name":"handshake: medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"🫱🏿‍🫲🏻","name":"handshake: dark skin tone, light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","dark","skin","tone,","light","tone"]}, + {"emoji":"🫱🏿‍🫲🏼","name":"handshake: dark skin tone, medium-light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"🫱🏿‍🫲🏽","name":"handshake: dark skin tone, medium skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","dark","skin","tone,","medium","tone"]}, + {"emoji":"🫱🏿‍🫲🏾","name":"handshake: dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["handshake:","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"🙏","name":"folded hands","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["folded","hands","appreciate","ask","beg","blessed","bow","cmon","five","gesture","hand","high","please","pray","thanks","thx"]}, + {"emoji":"🙏🏻","name":"folded hands: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["folded","hands:","light","skin","tone"]}, + {"emoji":"🙏🏼","name":"folded hands: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["folded","hands:","medium-light","skin","tone"]}, + {"emoji":"🙏🏽","name":"folded hands: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["folded","hands:","medium","skin","tone"]}, + {"emoji":"🙏🏾","name":"folded hands: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["folded","hands:","medium-dark","skin","tone"]}, + {"emoji":"🙏🏿","name":"folded hands: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["folded","hands:","dark","skin","tone"]}, + {"emoji":"✍️","name":"writing hand","category":"Smileys & People","version":"0.7","skinTone":false,"keywords":["writing","hand"]}, + {"emoji":"✍🏻","name":"writing hand: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["writing","hand:","light","skin","tone"]}, + {"emoji":"✍🏼","name":"writing hand: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["writing","hand:","medium-light","skin","tone"]}, + {"emoji":"✍🏽","name":"writing hand: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["writing","hand:","medium","skin","tone"]}, + {"emoji":"✍🏾","name":"writing hand: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["writing","hand:","medium-dark","skin","tone"]}, + {"emoji":"✍🏿","name":"writing hand: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["writing","hand:","dark","skin","tone"]}, + {"emoji":"💅","name":"nail polish","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["nail","polish","bored","care","cosmetics","done","makeup","manicure","whatever"]}, + {"emoji":"💅🏻","name":"nail polish: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["nail","polish:","light","skin","tone"]}, + {"emoji":"💅🏼","name":"nail polish: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["nail","polish:","medium-light","skin","tone"]}, + {"emoji":"💅🏽","name":"nail polish: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["nail","polish:","medium","skin","tone"]}, + {"emoji":"💅🏾","name":"nail polish: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["nail","polish:","medium-dark","skin","tone"]}, + {"emoji":"💅🏿","name":"nail polish: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["nail","polish:","dark","skin","tone"]}, + {"emoji":"🤳","name":"selfie","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["selfie","camera","phone"]}, + {"emoji":"🤳🏻","name":"selfie: light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["selfie:","light","skin","tone"]}, + {"emoji":"🤳🏼","name":"selfie: medium-light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["selfie:","medium-light","skin","tone"]}, + {"emoji":"🤳🏽","name":"selfie: medium skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["selfie:","medium","skin","tone"]}, + {"emoji":"🤳🏾","name":"selfie: medium-dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["selfie:","medium-dark","skin","tone"]}, + {"emoji":"🤳🏿","name":"selfie: dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["selfie:","dark","skin","tone"]}, + {"emoji":"💪","name":"flexed biceps","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["flexed","biceps","arm","beast","bench","bodybuilder","bro","curls","flex","gains","gym","jacked","muscle","press","ripped","strong","weightlift"]}, + {"emoji":"💪🏻","name":"flexed biceps: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["flexed","biceps:","light","skin","tone"]}, + {"emoji":"💪🏼","name":"flexed biceps: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["flexed","biceps:","medium-light","skin","tone"]}, + {"emoji":"💪🏽","name":"flexed biceps: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["flexed","biceps:","medium","skin","tone"]}, + {"emoji":"💪🏾","name":"flexed biceps: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["flexed","biceps:","medium-dark","skin","tone"]}, + {"emoji":"💪🏿","name":"flexed biceps: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["flexed","biceps:","dark","skin","tone"]}, + {"emoji":"🦾","name":"mechanical arm","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["mechanical","arm","accessibility","prosthetic"]}, + {"emoji":"🦿","name":"mechanical leg","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["mechanical","leg","accessibility","prosthetic"]}, + {"emoji":"🦵","name":"leg","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["leg","bent","foot","kick","knee","limb"]}, + {"emoji":"🦵🏻","name":"leg: light skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["leg:","light","skin","tone"]}, + {"emoji":"🦵🏼","name":"leg: medium-light skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["leg:","medium-light","skin","tone"]}, + {"emoji":"🦵🏽","name":"leg: medium skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["leg:","medium","skin","tone"]}, + {"emoji":"🦵🏾","name":"leg: medium-dark skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["leg:","medium-dark","skin","tone"]}, + {"emoji":"🦵🏿","name":"leg: dark skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["leg:","dark","skin","tone"]}, + {"emoji":"🦶","name":"foot","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["foot","ankle","feet","kick","stomp"]}, + {"emoji":"🦶🏻","name":"foot: light skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["foot:","light","skin","tone"]}, + {"emoji":"🦶🏼","name":"foot: medium-light skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["foot:","medium-light","skin","tone"]}, + {"emoji":"🦶🏽","name":"foot: medium skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["foot:","medium","skin","tone"]}, + {"emoji":"🦶🏾","name":"foot: medium-dark skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["foot:","medium-dark","skin","tone"]}, + {"emoji":"🦶🏿","name":"foot: dark skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["foot:","dark","skin","tone"]}, + {"emoji":"👂","name":"ear","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["ear","body","ears","hear","hearing","listen","listening","sound"]}, + {"emoji":"👂🏻","name":"ear: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["ear:","light","skin","tone"]}, + {"emoji":"👂🏼","name":"ear: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["ear:","medium-light","skin","tone"]}, + {"emoji":"👂🏽","name":"ear: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["ear:","medium","skin","tone"]}, + {"emoji":"👂🏾","name":"ear: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["ear:","medium-dark","skin","tone"]}, + {"emoji":"👂🏿","name":"ear: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["ear:","dark","skin","tone"]}, + {"emoji":"🦻","name":"ear with hearing aid","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["ear","with","hearing","aid","accessibility","hard"]}, + {"emoji":"🦻🏻","name":"ear with hearing aid: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["ear","with","hearing","aid:","light","skin","tone"]}, + {"emoji":"🦻🏼","name":"ear with hearing aid: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["ear","with","hearing","aid:","medium-light","skin","tone"]}, + {"emoji":"🦻🏽","name":"ear with hearing aid: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["ear","with","hearing","aid:","medium","skin","tone"]}, + {"emoji":"🦻🏾","name":"ear with hearing aid: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["ear","with","hearing","aid:","medium-dark","skin","tone"]}, + {"emoji":"🦻🏿","name":"ear with hearing aid: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["ear","with","hearing","aid:","dark","skin","tone"]}, + {"emoji":"👃","name":"nose","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["nose","body","noses","nosey","odor","smell","smells"]}, + {"emoji":"👃🏻","name":"nose: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["nose:","light","skin","tone"]}, + {"emoji":"👃🏼","name":"nose: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["nose:","medium-light","skin","tone"]}, + {"emoji":"👃🏽","name":"nose: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["nose:","medium","skin","tone"]}, + {"emoji":"👃🏾","name":"nose: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["nose:","medium-dark","skin","tone"]}, + {"emoji":"👃🏿","name":"nose: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["nose:","dark","skin","tone"]}, + {"emoji":"🧠","name":"brain","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["brain","intelligent","smart"]}, + {"emoji":"🫀","name":"anatomical heart","category":"Smileys & People","version":"13.0","skinTone":false,"keywords":["anatomical","heart","beat","cardiology","heartbeat","organ","pulse","real","red"]}, + {"emoji":"🫁","name":"lungs","category":"Smileys & People","version":"13.0","skinTone":false,"keywords":["lungs","breath","breathe","exhalation","inhalation","lung","organ","respiration"]}, + {"emoji":"🦷","name":"tooth","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["tooth","dentist","pearly","teeth","white"]}, + {"emoji":"🦴","name":"bone","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["bone","bones","dog","skeleton","wishbone"]}, + {"emoji":"👀","name":"eyes","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["eyes","body","eye","face","googly","look","looking","omg","peep","see","seeing"]}, + {"emoji":"👁️","name":"eye","category":"Smileys & People","version":"0.7","skinTone":false,"keywords":["eye"]}, + {"emoji":"👅","name":"tongue","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["tongue","body","lick","slurp"]}, + {"emoji":"👄","name":"mouth","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["mouth","beauty","body","kiss","kissing","lips","lipstick"]}, + {"emoji":"🫦","name":"biting lip","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["biting","lip","anxious","bite","fear","flirt","flirting","kiss","lipstick","nervous","sexy","uncomfortable","worried","worry"]}, + {"emoji":"👶","name":"baby","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["baby","babies","children","goo","infant","newborn","pregnant","young"]}, + {"emoji":"👶🏻","name":"baby: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["baby:","light","skin","tone"]}, + {"emoji":"👶🏼","name":"baby: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["baby:","medium-light","skin","tone"]}, + {"emoji":"👶🏽","name":"baby: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["baby:","medium","skin","tone"]}, + {"emoji":"👶🏾","name":"baby: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["baby:","medium-dark","skin","tone"]}, + {"emoji":"👶🏿","name":"baby: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["baby:","dark","skin","tone"]}, + {"emoji":"🧒","name":"child","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["child","bright-eyed","grandchild","kid","young","younger"]}, + {"emoji":"🧒🏻","name":"child: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["child:","light","skin","tone"]}, + {"emoji":"🧒🏼","name":"child: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["child:","medium-light","skin","tone"]}, + {"emoji":"🧒🏽","name":"child: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["child:","medium","skin","tone"]}, + {"emoji":"🧒🏾","name":"child: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["child:","medium-dark","skin","tone"]}, + {"emoji":"🧒🏿","name":"child: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["child:","dark","skin","tone"]}, + {"emoji":"👦","name":"boy","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["boy","bright-eyed","child","grandson","kid","son","young","younger"]}, + {"emoji":"👦🏻","name":"boy: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["boy:","light","skin","tone"]}, + {"emoji":"👦🏼","name":"boy: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["boy:","medium-light","skin","tone"]}, + {"emoji":"👦🏽","name":"boy: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["boy:","medium","skin","tone"]}, + {"emoji":"👦🏾","name":"boy: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["boy:","medium-dark","skin","tone"]}, + {"emoji":"👦🏿","name":"boy: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["boy:","dark","skin","tone"]}, + {"emoji":"👧","name":"girl","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["girl","bright-eyed","child","daughter","granddaughter","kid","Virgo","young","younger","zodiac"]}, + {"emoji":"👧🏻","name":"girl: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["girl:","light","skin","tone"]}, + {"emoji":"👧🏼","name":"girl: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["girl:","medium-light","skin","tone"]}, + {"emoji":"👧🏽","name":"girl: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["girl:","medium","skin","tone"]}, + {"emoji":"👧🏾","name":"girl: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["girl:","medium-dark","skin","tone"]}, + {"emoji":"👧🏿","name":"girl: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["girl:","dark","skin","tone"]}, + {"emoji":"🧑","name":"person","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["person","adult"]}, + {"emoji":"🧑🏻","name":"person: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person:","light","skin","tone"]}, + {"emoji":"🧑🏼","name":"person: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽","name":"person: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person:","medium","skin","tone"]}, + {"emoji":"🧑🏾","name":"person: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿","name":"person: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person:","dark","skin","tone"]}, + {"emoji":"👱","name":"person: blond hair","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person:","blond","hair","blond-haired","human","person"]}, + {"emoji":"👱🏻","name":"person: light skin tone, blond hair","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person:","light","skin","tone,","blond","hair"]}, + {"emoji":"👱🏼","name":"person: medium-light skin tone, blond hair","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person:","medium-light","skin","tone,","blond","hair"]}, + {"emoji":"👱🏽","name":"person: medium skin tone, blond hair","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person:","medium","skin","tone,","blond","hair"]}, + {"emoji":"👱🏾","name":"person: medium-dark skin tone, blond hair","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person:","medium-dark","skin","tone,","blond","hair"]}, + {"emoji":"👱🏿","name":"person: dark skin tone, blond hair","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person:","dark","skin","tone,","blond","hair"]}, + {"emoji":"👨","name":"man","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["man","adult","bro"]}, + {"emoji":"👨🏻","name":"man: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["man:","light","skin","tone"]}, + {"emoji":"👨🏼","name":"man: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["man:","medium-light","skin","tone"]}, + {"emoji":"👨🏽","name":"man: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["man:","medium","skin","tone"]}, + {"emoji":"👨🏾","name":"man: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["man:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿","name":"man: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["man:","dark","skin","tone"]}, + {"emoji":"🧔","name":"person: beard","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["person:","beard","bearded","person","whiskers"]}, + {"emoji":"🧔🏻","name":"person: light skin tone, beard","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person:","light","skin","tone,","beard"]}, + {"emoji":"🧔🏼","name":"person: medium-light skin tone, beard","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person:","medium-light","skin","tone,","beard"]}, + {"emoji":"🧔🏽","name":"person: medium skin tone, beard","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person:","medium","skin","tone,","beard"]}, + {"emoji":"🧔🏾","name":"person: medium-dark skin tone, beard","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person:","medium-dark","skin","tone,","beard"]}, + {"emoji":"🧔🏿","name":"person: dark skin tone, beard","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person:","dark","skin","tone,","beard"]}, + {"emoji":"🧔‍♂️","name":"man: beard","category":"Smileys & People","version":"13.1","skinTone":false,"keywords":["man:","beard"]}, + {"emoji":"🧔🏻‍♂️","name":"man: light skin tone, beard","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["man:","light","skin","tone,","beard"]}, + {"emoji":"🧔🏼‍♂️","name":"man: medium-light skin tone, beard","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["man:","medium-light","skin","tone,","beard"]}, + {"emoji":"🧔🏽‍♂️","name":"man: medium skin tone, beard","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["man:","medium","skin","tone,","beard"]}, + {"emoji":"🧔🏾‍♂️","name":"man: medium-dark skin tone, beard","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["man:","medium-dark","skin","tone,","beard"]}, + {"emoji":"🧔🏿‍♂️","name":"man: dark skin tone, beard","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["man:","dark","skin","tone,","beard"]}, + {"emoji":"🧔‍♀️","name":"woman: beard","category":"Smileys & People","version":"13.1","skinTone":false,"keywords":["woman:","beard"]}, + {"emoji":"🧔🏻‍♀️","name":"woman: light skin tone, beard","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["woman:","light","skin","tone,","beard"]}, + {"emoji":"🧔🏼‍♀️","name":"woman: medium-light skin tone, beard","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["woman:","medium-light","skin","tone,","beard"]}, + {"emoji":"🧔🏽‍♀️","name":"woman: medium skin tone, beard","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["woman:","medium","skin","tone,","beard"]}, + {"emoji":"🧔🏾‍♀️","name":"woman: medium-dark skin tone, beard","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["woman:","medium-dark","skin","tone,","beard"]}, + {"emoji":"🧔🏿‍♀️","name":"woman: dark skin tone, beard","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["woman:","dark","skin","tone,","beard"]}, + {"emoji":"👨‍🦰","name":"man: red hair","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["man:","red","hair"]}, + {"emoji":"👨🏻‍🦰","name":"man: light skin tone, red hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","light","skin","tone,","red","hair"]}, + {"emoji":"👨🏼‍🦰","name":"man: medium-light skin tone, red hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","medium-light","skin","tone,","red","hair"]}, + {"emoji":"👨🏽‍🦰","name":"man: medium skin tone, red hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","medium","skin","tone,","red","hair"]}, + {"emoji":"👨🏾‍🦰","name":"man: medium-dark skin tone, red hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","medium-dark","skin","tone,","red","hair"]}, + {"emoji":"👨🏿‍🦰","name":"man: dark skin tone, red hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","dark","skin","tone,","red","hair"]}, + {"emoji":"👨‍🦱","name":"man: curly hair","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["man:","curly","hair"]}, + {"emoji":"👨🏻‍🦱","name":"man: light skin tone, curly hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","light","skin","tone,","curly","hair"]}, + {"emoji":"👨🏼‍🦱","name":"man: medium-light skin tone, curly hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","medium-light","skin","tone,","curly","hair"]}, + {"emoji":"👨🏽‍🦱","name":"man: medium skin tone, curly hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","medium","skin","tone,","curly","hair"]}, + {"emoji":"👨🏾‍🦱","name":"man: medium-dark skin tone, curly hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","medium-dark","skin","tone,","curly","hair"]}, + {"emoji":"👨🏿‍🦱","name":"man: dark skin tone, curly hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","dark","skin","tone,","curly","hair"]}, + {"emoji":"👨‍🦳","name":"man: white hair","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["man:","white","hair"]}, + {"emoji":"👨🏻‍🦳","name":"man: light skin tone, white hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","light","skin","tone,","white","hair"]}, + {"emoji":"👨🏼‍🦳","name":"man: medium-light skin tone, white hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","medium-light","skin","tone,","white","hair"]}, + {"emoji":"👨🏽‍🦳","name":"man: medium skin tone, white hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","medium","skin","tone,","white","hair"]}, + {"emoji":"👨🏾‍🦳","name":"man: medium-dark skin tone, white hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","medium-dark","skin","tone,","white","hair"]}, + {"emoji":"👨🏿‍🦳","name":"man: dark skin tone, white hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","dark","skin","tone,","white","hair"]}, + {"emoji":"👨‍🦲","name":"man: bald","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["man:","bald"]}, + {"emoji":"👨🏻‍🦲","name":"man: light skin tone, bald","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","light","skin","tone,","bald"]}, + {"emoji":"👨🏼‍🦲","name":"man: medium-light skin tone, bald","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","medium-light","skin","tone,","bald"]}, + {"emoji":"👨🏽‍🦲","name":"man: medium skin tone, bald","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","medium","skin","tone,","bald"]}, + {"emoji":"👨🏾‍🦲","name":"man: medium-dark skin tone, bald","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","medium-dark","skin","tone,","bald"]}, + {"emoji":"👨🏿‍🦲","name":"man: dark skin tone, bald","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man:","dark","skin","tone,","bald"]}, + {"emoji":"👩","name":"woman","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["woman","adult","lady"]}, + {"emoji":"👩🏻","name":"woman: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["woman:","light","skin","tone"]}, + {"emoji":"👩🏼","name":"woman: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["woman:","medium-light","skin","tone"]}, + {"emoji":"👩🏽","name":"woman: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["woman:","medium","skin","tone"]}, + {"emoji":"👩🏾","name":"woman: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["woman:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿","name":"woman: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["woman:","dark","skin","tone"]}, + {"emoji":"👩‍🦰","name":"woman: red hair","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["woman:","red","hair"]}, + {"emoji":"👩🏻‍🦰","name":"woman: light skin tone, red hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","light","skin","tone,","red","hair"]}, + {"emoji":"👩🏼‍🦰","name":"woman: medium-light skin tone, red hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","medium-light","skin","tone,","red","hair"]}, + {"emoji":"👩🏽‍🦰","name":"woman: medium skin tone, red hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","medium","skin","tone,","red","hair"]}, + {"emoji":"👩🏾‍🦰","name":"woman: medium-dark skin tone, red hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","medium-dark","skin","tone,","red","hair"]}, + {"emoji":"👩🏿‍🦰","name":"woman: dark skin tone, red hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","dark","skin","tone,","red","hair"]}, + {"emoji":"🧑‍🦰","name":"person: red hair","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["person:","red","hair"]}, + {"emoji":"🧑🏻‍🦰","name":"person: light skin tone, red hair","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","light","skin","tone,","red","hair"]}, + {"emoji":"🧑🏼‍🦰","name":"person: medium-light skin tone, red hair","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","medium-light","skin","tone,","red","hair"]}, + {"emoji":"🧑🏽‍🦰","name":"person: medium skin tone, red hair","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","medium","skin","tone,","red","hair"]}, + {"emoji":"🧑🏾‍🦰","name":"person: medium-dark skin tone, red hair","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","medium-dark","skin","tone,","red","hair"]}, + {"emoji":"🧑🏿‍🦰","name":"person: dark skin tone, red hair","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","dark","skin","tone,","red","hair"]}, + {"emoji":"👩‍🦱","name":"woman: curly hair","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["woman:","curly","hair"]}, + {"emoji":"👩🏻‍🦱","name":"woman: light skin tone, curly hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","light","skin","tone,","curly","hair"]}, + {"emoji":"👩🏼‍🦱","name":"woman: medium-light skin tone, curly hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","medium-light","skin","tone,","curly","hair"]}, + {"emoji":"👩🏽‍🦱","name":"woman: medium skin tone, curly hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","medium","skin","tone,","curly","hair"]}, + {"emoji":"👩🏾‍🦱","name":"woman: medium-dark skin tone, curly hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","medium-dark","skin","tone,","curly","hair"]}, + {"emoji":"👩🏿‍🦱","name":"woman: dark skin tone, curly hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","dark","skin","tone,","curly","hair"]}, + {"emoji":"🧑‍🦱","name":"person: curly hair","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["person:","curly","hair"]}, + {"emoji":"🧑🏻‍🦱","name":"person: light skin tone, curly hair","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","light","skin","tone,","curly","hair"]}, + {"emoji":"🧑🏼‍🦱","name":"person: medium-light skin tone, curly hair","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","medium-light","skin","tone,","curly","hair"]}, + {"emoji":"🧑🏽‍🦱","name":"person: medium skin tone, curly hair","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","medium","skin","tone,","curly","hair"]}, + {"emoji":"🧑🏾‍🦱","name":"person: medium-dark skin tone, curly hair","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","medium-dark","skin","tone,","curly","hair"]}, + {"emoji":"🧑🏿‍🦱","name":"person: dark skin tone, curly hair","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","dark","skin","tone,","curly","hair"]}, + {"emoji":"👩‍🦳","name":"woman: white hair","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["woman:","white","hair"]}, + {"emoji":"👩🏻‍🦳","name":"woman: light skin tone, white hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","light","skin","tone,","white","hair"]}, + {"emoji":"👩🏼‍🦳","name":"woman: medium-light skin tone, white hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","medium-light","skin","tone,","white","hair"]}, + {"emoji":"👩🏽‍🦳","name":"woman: medium skin tone, white hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","medium","skin","tone,","white","hair"]}, + {"emoji":"👩🏾‍🦳","name":"woman: medium-dark skin tone, white hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","medium-dark","skin","tone,","white","hair"]}, + {"emoji":"👩🏿‍🦳","name":"woman: dark skin tone, white hair","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","dark","skin","tone,","white","hair"]}, + {"emoji":"🧑‍🦳","name":"person: white hair","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["person:","white","hair"]}, + {"emoji":"🧑🏻‍🦳","name":"person: light skin tone, white hair","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","light","skin","tone,","white","hair"]}, + {"emoji":"🧑🏼‍🦳","name":"person: medium-light skin tone, white hair","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","medium-light","skin","tone,","white","hair"]}, + {"emoji":"🧑🏽‍🦳","name":"person: medium skin tone, white hair","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","medium","skin","tone,","white","hair"]}, + {"emoji":"🧑🏾‍🦳","name":"person: medium-dark skin tone, white hair","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","medium-dark","skin","tone,","white","hair"]}, + {"emoji":"🧑🏿‍🦳","name":"person: dark skin tone, white hair","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","dark","skin","tone,","white","hair"]}, + {"emoji":"👩‍🦲","name":"woman: bald","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["woman:","bald"]}, + {"emoji":"👩🏻‍🦲","name":"woman: light skin tone, bald","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","light","skin","tone,","bald"]}, + {"emoji":"👩🏼‍🦲","name":"woman: medium-light skin tone, bald","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","medium-light","skin","tone,","bald"]}, + {"emoji":"👩🏽‍🦲","name":"woman: medium skin tone, bald","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","medium","skin","tone,","bald"]}, + {"emoji":"👩🏾‍🦲","name":"woman: medium-dark skin tone, bald","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","medium-dark","skin","tone,","bald"]}, + {"emoji":"👩🏿‍🦲","name":"woman: dark skin tone, bald","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman:","dark","skin","tone,","bald"]}, + {"emoji":"🧑‍🦲","name":"person: bald","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["person:","bald"]}, + {"emoji":"🧑🏻‍🦲","name":"person: light skin tone, bald","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","light","skin","tone,","bald"]}, + {"emoji":"🧑🏼‍🦲","name":"person: medium-light skin tone, bald","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","medium-light","skin","tone,","bald"]}, + {"emoji":"🧑🏽‍🦲","name":"person: medium skin tone, bald","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","medium","skin","tone,","bald"]}, + {"emoji":"🧑🏾‍🦲","name":"person: medium-dark skin tone, bald","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","medium-dark","skin","tone,","bald"]}, + {"emoji":"🧑🏿‍🦲","name":"person: dark skin tone, bald","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person:","dark","skin","tone,","bald"]}, + {"emoji":"👱‍♀️","name":"woman: blond hair","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman:","blond","hair"]}, + {"emoji":"👱🏻‍♀️","name":"woman: light skin tone, blond hair","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman:","light","skin","tone,","blond","hair"]}, + {"emoji":"👱🏼‍♀️","name":"woman: medium-light skin tone, blond hair","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman:","medium-light","skin","tone,","blond","hair"]}, + {"emoji":"👱🏽‍♀️","name":"woman: medium skin tone, blond hair","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman:","medium","skin","tone,","blond","hair"]}, + {"emoji":"👱🏾‍♀️","name":"woman: medium-dark skin tone, blond hair","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman:","medium-dark","skin","tone,","blond","hair"]}, + {"emoji":"👱🏿‍♀️","name":"woman: dark skin tone, blond hair","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman:","dark","skin","tone,","blond","hair"]}, + {"emoji":"👱‍♂️","name":"man: blond hair","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man:","blond","hair"]}, + {"emoji":"👱🏻‍♂️","name":"man: light skin tone, blond hair","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man:","light","skin","tone,","blond","hair"]}, + {"emoji":"👱🏼‍♂️","name":"man: medium-light skin tone, blond hair","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man:","medium-light","skin","tone,","blond","hair"]}, + {"emoji":"👱🏽‍♂️","name":"man: medium skin tone, blond hair","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man:","medium","skin","tone,","blond","hair"]}, + {"emoji":"👱🏾‍♂️","name":"man: medium-dark skin tone, blond hair","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man:","medium-dark","skin","tone,","blond","hair"]}, + {"emoji":"👱🏿‍♂️","name":"man: dark skin tone, blond hair","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man:","dark","skin","tone,","blond","hair"]}, + {"emoji":"🧓","name":"older person","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["older","person","adult","elderly","grandparent","old","wise"]}, + {"emoji":"🧓🏻","name":"older person: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["older","person:","light","skin","tone"]}, + {"emoji":"🧓🏼","name":"older person: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["older","person:","medium-light","skin","tone"]}, + {"emoji":"🧓🏽","name":"older person: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["older","person:","medium","skin","tone"]}, + {"emoji":"🧓🏾","name":"older person: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["older","person:","medium-dark","skin","tone"]}, + {"emoji":"🧓🏿","name":"older person: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["older","person:","dark","skin","tone"]}, + {"emoji":"👴","name":"old man","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["old","man","adult","bald","elderly","gramps","grandfather","grandpa","wise"]}, + {"emoji":"👴🏻","name":"old man: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["old","man:","light","skin","tone"]}, + {"emoji":"👴🏼","name":"old man: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["old","man:","medium-light","skin","tone"]}, + {"emoji":"👴🏽","name":"old man: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["old","man:","medium","skin","tone"]}, + {"emoji":"👴🏾","name":"old man: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["old","man:","medium-dark","skin","tone"]}, + {"emoji":"👴🏿","name":"old man: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["old","man:","dark","skin","tone"]}, + {"emoji":"👵","name":"old woman","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["old","woman","adult","elderly","grandma","grandmother","granny","lady","wise"]}, + {"emoji":"👵🏻","name":"old woman: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["old","woman:","light","skin","tone"]}, + {"emoji":"👵🏼","name":"old woman: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["old","woman:","medium-light","skin","tone"]}, + {"emoji":"👵🏽","name":"old woman: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["old","woman:","medium","skin","tone"]}, + {"emoji":"👵🏾","name":"old woman: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["old","woman:","medium-dark","skin","tone"]}, + {"emoji":"👵🏿","name":"old woman: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["old","woman:","dark","skin","tone"]}, + {"emoji":"🙍","name":"person frowning","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person","frowning","annoyed","disappointed","disgruntled","disturbed","frown","frustrated","gesture","irritated","upset"]}, + {"emoji":"🙍🏻","name":"person frowning: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","frowning:","light","skin","tone"]}, + {"emoji":"🙍🏼","name":"person frowning: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","frowning:","medium-light","skin","tone"]}, + {"emoji":"🙍🏽","name":"person frowning: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","frowning:","medium","skin","tone"]}, + {"emoji":"🙍🏾","name":"person frowning: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","frowning:","medium-dark","skin","tone"]}, + {"emoji":"🙍🏿","name":"person frowning: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","frowning:","dark","skin","tone"]}, + {"emoji":"🙍‍♂️","name":"man frowning","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","frowning"]}, + {"emoji":"🙍🏻‍♂️","name":"man frowning: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","frowning:","light","skin","tone"]}, + {"emoji":"🙍🏼‍♂️","name":"man frowning: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","frowning:","medium-light","skin","tone"]}, + {"emoji":"🙍🏽‍♂️","name":"man frowning: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","frowning:","medium","skin","tone"]}, + {"emoji":"🙍🏾‍♂️","name":"man frowning: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","frowning:","medium-dark","skin","tone"]}, + {"emoji":"🙍🏿‍♂️","name":"man frowning: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","frowning:","dark","skin","tone"]}, + {"emoji":"🙍‍♀️","name":"woman frowning","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","frowning"]}, + {"emoji":"🙍🏻‍♀️","name":"woman frowning: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","frowning:","light","skin","tone"]}, + {"emoji":"🙍🏼‍♀️","name":"woman frowning: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","frowning:","medium-light","skin","tone"]}, + {"emoji":"🙍🏽‍♀️","name":"woman frowning: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","frowning:","medium","skin","tone"]}, + {"emoji":"🙍🏾‍♀️","name":"woman frowning: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","frowning:","medium-dark","skin","tone"]}, + {"emoji":"🙍🏿‍♀️","name":"woman frowning: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","frowning:","dark","skin","tone"]}, + {"emoji":"🙎","name":"person pouting","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person","pouting","disappointed","downtrodden","frown","grimace","scowl","sulk","upset","whine"]}, + {"emoji":"🙎🏻","name":"person pouting: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","pouting:","light","skin","tone"]}, + {"emoji":"🙎🏼","name":"person pouting: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","pouting:","medium-light","skin","tone"]}, + {"emoji":"🙎🏽","name":"person pouting: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","pouting:","medium","skin","tone"]}, + {"emoji":"🙎🏾","name":"person pouting: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","pouting:","medium-dark","skin","tone"]}, + {"emoji":"🙎🏿","name":"person pouting: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","pouting:","dark","skin","tone"]}, + {"emoji":"🙎‍♂️","name":"man pouting","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","pouting"]}, + {"emoji":"🙎🏻‍♂️","name":"man pouting: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","pouting:","light","skin","tone"]}, + {"emoji":"🙎🏼‍♂️","name":"man pouting: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","pouting:","medium-light","skin","tone"]}, + {"emoji":"🙎🏽‍♂️","name":"man pouting: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","pouting:","medium","skin","tone"]}, + {"emoji":"🙎🏾‍♂️","name":"man pouting: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","pouting:","medium-dark","skin","tone"]}, + {"emoji":"🙎🏿‍♂️","name":"man pouting: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","pouting:","dark","skin","tone"]}, + {"emoji":"🙎‍♀️","name":"woman pouting","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","pouting"]}, + {"emoji":"🙎🏻‍♀️","name":"woman pouting: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","pouting:","light","skin","tone"]}, + {"emoji":"🙎🏼‍♀️","name":"woman pouting: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","pouting:","medium-light","skin","tone"]}, + {"emoji":"🙎🏽‍♀️","name":"woman pouting: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","pouting:","medium","skin","tone"]}, + {"emoji":"🙎🏾‍♀️","name":"woman pouting: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","pouting:","medium-dark","skin","tone"]}, + {"emoji":"🙎🏿‍♀️","name":"woman pouting: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","pouting:","dark","skin","tone"]}, + {"emoji":"🙅","name":"person gesturing NO","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person","gesturing","NO","forbidden","gesture","hand","not","prohibit"]}, + {"emoji":"🙅🏻","name":"person gesturing NO: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","gesturing","NO:","light","skin","tone"]}, + {"emoji":"🙅🏼","name":"person gesturing NO: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","gesturing","NO:","medium-light","skin","tone"]}, + {"emoji":"🙅🏽","name":"person gesturing NO: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","gesturing","NO:","medium","skin","tone"]}, + {"emoji":"🙅🏾","name":"person gesturing NO: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","gesturing","NO:","medium-dark","skin","tone"]}, + {"emoji":"🙅🏿","name":"person gesturing NO: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","gesturing","NO:","dark","skin","tone"]}, + {"emoji":"🙅‍♂️","name":"man gesturing NO","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","gesturing","NO"]}, + {"emoji":"🙅🏻‍♂️","name":"man gesturing NO: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","gesturing","NO:","light","skin","tone"]}, + {"emoji":"🙅🏼‍♂️","name":"man gesturing NO: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","gesturing","NO:","medium-light","skin","tone"]}, + {"emoji":"🙅🏽‍♂️","name":"man gesturing NO: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","gesturing","NO:","medium","skin","tone"]}, + {"emoji":"🙅🏾‍♂️","name":"man gesturing NO: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","gesturing","NO:","medium-dark","skin","tone"]}, + {"emoji":"🙅🏿‍♂️","name":"man gesturing NO: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","gesturing","NO:","dark","skin","tone"]}, + {"emoji":"🙅‍♀️","name":"woman gesturing NO","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","gesturing","NO"]}, + {"emoji":"🙅🏻‍♀️","name":"woman gesturing NO: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","gesturing","NO:","light","skin","tone"]}, + {"emoji":"🙅🏼‍♀️","name":"woman gesturing NO: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","gesturing","NO:","medium-light","skin","tone"]}, + {"emoji":"🙅🏽‍♀️","name":"woman gesturing NO: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","gesturing","NO:","medium","skin","tone"]}, + {"emoji":"🙅🏾‍♀️","name":"woman gesturing NO: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","gesturing","NO:","medium-dark","skin","tone"]}, + {"emoji":"🙅🏿‍♀️","name":"woman gesturing NO: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","gesturing","NO:","dark","skin","tone"]}, + {"emoji":"🙆","name":"person gesturing OK","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person","gesturing","OK","exercise","gesture","hand","omg"]}, + {"emoji":"🙆🏻","name":"person gesturing OK: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","gesturing","OK:","light","skin","tone"]}, + {"emoji":"🙆🏼","name":"person gesturing OK: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","gesturing","OK:","medium-light","skin","tone"]}, + {"emoji":"🙆🏽","name":"person gesturing OK: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","gesturing","OK:","medium","skin","tone"]}, + {"emoji":"🙆🏾","name":"person gesturing OK: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","gesturing","OK:","medium-dark","skin","tone"]}, + {"emoji":"🙆🏿","name":"person gesturing OK: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","gesturing","OK:","dark","skin","tone"]}, + {"emoji":"🙆‍♂️","name":"man gesturing OK","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","gesturing","OK"]}, + {"emoji":"🙆🏻‍♂️","name":"man gesturing OK: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","gesturing","OK:","light","skin","tone"]}, + {"emoji":"🙆🏼‍♂️","name":"man gesturing OK: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","gesturing","OK:","medium-light","skin","tone"]}, + {"emoji":"🙆🏽‍♂️","name":"man gesturing OK: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","gesturing","OK:","medium","skin","tone"]}, + {"emoji":"🙆🏾‍♂️","name":"man gesturing OK: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","gesturing","OK:","medium-dark","skin","tone"]}, + {"emoji":"🙆🏿‍♂️","name":"man gesturing OK: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","gesturing","OK:","dark","skin","tone"]}, + {"emoji":"🙆‍♀️","name":"woman gesturing OK","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","gesturing","OK"]}, + {"emoji":"🙆🏻‍♀️","name":"woman gesturing OK: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","gesturing","OK:","light","skin","tone"]}, + {"emoji":"🙆🏼‍♀️","name":"woman gesturing OK: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","gesturing","OK:","medium-light","skin","tone"]}, + {"emoji":"🙆🏽‍♀️","name":"woman gesturing OK: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","gesturing","OK:","medium","skin","tone"]}, + {"emoji":"🙆🏾‍♀️","name":"woman gesturing OK: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","gesturing","OK:","medium-dark","skin","tone"]}, + {"emoji":"🙆🏿‍♀️","name":"woman gesturing OK: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","gesturing","OK:","dark","skin","tone"]}, + {"emoji":"💁","name":"person tipping hand","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person","tipping","hand","fetch","flick","flip","gossip","sarcasm","sarcastic","sassy","seriously","whatever"]}, + {"emoji":"💁🏻","name":"person tipping hand: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","tipping","hand:","light","skin","tone"]}, + {"emoji":"💁🏼","name":"person tipping hand: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","tipping","hand:","medium-light","skin","tone"]}, + {"emoji":"💁🏽","name":"person tipping hand: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","tipping","hand:","medium","skin","tone"]}, + {"emoji":"💁🏾","name":"person tipping hand: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","tipping","hand:","medium-dark","skin","tone"]}, + {"emoji":"💁🏿","name":"person tipping hand: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","tipping","hand:","dark","skin","tone"]}, + {"emoji":"💁‍♂️","name":"man tipping hand","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","tipping","hand"]}, + {"emoji":"💁🏻‍♂️","name":"man tipping hand: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","tipping","hand:","light","skin","tone"]}, + {"emoji":"💁🏼‍♂️","name":"man tipping hand: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","tipping","hand:","medium-light","skin","tone"]}, + {"emoji":"💁🏽‍♂️","name":"man tipping hand: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","tipping","hand:","medium","skin","tone"]}, + {"emoji":"💁🏾‍♂️","name":"man tipping hand: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","tipping","hand:","medium-dark","skin","tone"]}, + {"emoji":"💁🏿‍♂️","name":"man tipping hand: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","tipping","hand:","dark","skin","tone"]}, + {"emoji":"💁‍♀️","name":"woman tipping hand","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","tipping","hand"]}, + {"emoji":"💁🏻‍♀️","name":"woman tipping hand: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","tipping","hand:","light","skin","tone"]}, + {"emoji":"💁🏼‍♀️","name":"woman tipping hand: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","tipping","hand:","medium-light","skin","tone"]}, + {"emoji":"💁🏽‍♀️","name":"woman tipping hand: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","tipping","hand:","medium","skin","tone"]}, + {"emoji":"💁🏾‍♀️","name":"woman tipping hand: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","tipping","hand:","medium-dark","skin","tone"]}, + {"emoji":"💁🏿‍♀️","name":"woman tipping hand: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","tipping","hand:","dark","skin","tone"]}, + {"emoji":"🙋","name":"person raising hand","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person","raising","hand","gesture","here","know","me","pick","question","raise"]}, + {"emoji":"🙋🏻","name":"person raising hand: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","raising","hand:","light","skin","tone"]}, + {"emoji":"🙋🏼","name":"person raising hand: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","raising","hand:","medium-light","skin","tone"]}, + {"emoji":"🙋🏽","name":"person raising hand: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","raising","hand:","medium","skin","tone"]}, + {"emoji":"🙋🏾","name":"person raising hand: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","raising","hand:","medium-dark","skin","tone"]}, + {"emoji":"🙋🏿","name":"person raising hand: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","raising","hand:","dark","skin","tone"]}, + {"emoji":"🙋‍♂️","name":"man raising hand","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","raising","hand"]}, + {"emoji":"🙋🏻‍♂️","name":"man raising hand: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","raising","hand:","light","skin","tone"]}, + {"emoji":"🙋🏼‍♂️","name":"man raising hand: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","raising","hand:","medium-light","skin","tone"]}, + {"emoji":"🙋🏽‍♂️","name":"man raising hand: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","raising","hand:","medium","skin","tone"]}, + {"emoji":"🙋🏾‍♂️","name":"man raising hand: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","raising","hand:","medium-dark","skin","tone"]}, + {"emoji":"🙋🏿‍♂️","name":"man raising hand: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","raising","hand:","dark","skin","tone"]}, + {"emoji":"🙋‍♀️","name":"woman raising hand","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","raising","hand"]}, + {"emoji":"🙋🏻‍♀️","name":"woman raising hand: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","raising","hand:","light","skin","tone"]}, + {"emoji":"🙋🏼‍♀️","name":"woman raising hand: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","raising","hand:","medium-light","skin","tone"]}, + {"emoji":"🙋🏽‍♀️","name":"woman raising hand: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","raising","hand:","medium","skin","tone"]}, + {"emoji":"🙋🏾‍♀️","name":"woman raising hand: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","raising","hand:","medium-dark","skin","tone"]}, + {"emoji":"🙋🏿‍♀️","name":"woman raising hand: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","raising","hand:","dark","skin","tone"]}, + {"emoji":"🧏","name":"deaf person","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["deaf","person","accessibility","ear","gesture","hear"]}, + {"emoji":"🧏🏻","name":"deaf person: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["deaf","person:","light","skin","tone"]}, + {"emoji":"🧏🏼","name":"deaf person: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["deaf","person:","medium-light","skin","tone"]}, + {"emoji":"🧏🏽","name":"deaf person: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["deaf","person:","medium","skin","tone"]}, + {"emoji":"🧏🏾","name":"deaf person: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["deaf","person:","medium-dark","skin","tone"]}, + {"emoji":"🧏🏿","name":"deaf person: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["deaf","person:","dark","skin","tone"]}, + {"emoji":"🧏‍♂️","name":"deaf man","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["deaf","man"]}, + {"emoji":"🧏🏻‍♂️","name":"deaf man: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["deaf","man:","light","skin","tone"]}, + {"emoji":"🧏🏼‍♂️","name":"deaf man: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["deaf","man:","medium-light","skin","tone"]}, + {"emoji":"🧏🏽‍♂️","name":"deaf man: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["deaf","man:","medium","skin","tone"]}, + {"emoji":"🧏🏾‍♂️","name":"deaf man: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["deaf","man:","medium-dark","skin","tone"]}, + {"emoji":"🧏🏿‍♂️","name":"deaf man: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["deaf","man:","dark","skin","tone"]}, + {"emoji":"🧏‍♀️","name":"deaf woman","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["deaf","woman"]}, + {"emoji":"🧏🏻‍♀️","name":"deaf woman: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["deaf","woman:","light","skin","tone"]}, + {"emoji":"🧏🏼‍♀️","name":"deaf woman: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["deaf","woman:","medium-light","skin","tone"]}, + {"emoji":"🧏🏽‍♀️","name":"deaf woman: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["deaf","woman:","medium","skin","tone"]}, + {"emoji":"🧏🏾‍♀️","name":"deaf woman: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["deaf","woman:","medium-dark","skin","tone"]}, + {"emoji":"🧏🏿‍♀️","name":"deaf woman: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["deaf","woman:","dark","skin","tone"]}, + {"emoji":"🙇","name":"person bowing","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person","bowing","apology","ask","beg","bow","favor","forgive","gesture","meditate","meditation","pity","regret","sorry"]}, + {"emoji":"🙇🏻","name":"person bowing: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","bowing:","light","skin","tone"]}, + {"emoji":"🙇🏼","name":"person bowing: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","bowing:","medium-light","skin","tone"]}, + {"emoji":"🙇🏽","name":"person bowing: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","bowing:","medium","skin","tone"]}, + {"emoji":"🙇🏾","name":"person bowing: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","bowing:","medium-dark","skin","tone"]}, + {"emoji":"🙇🏿","name":"person bowing: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","bowing:","dark","skin","tone"]}, + {"emoji":"🙇‍♂️","name":"man bowing","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","bowing"]}, + {"emoji":"🙇🏻‍♂️","name":"man bowing: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","bowing:","light","skin","tone"]}, + {"emoji":"🙇🏼‍♂️","name":"man bowing: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","bowing:","medium-light","skin","tone"]}, + {"emoji":"🙇🏽‍♂️","name":"man bowing: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","bowing:","medium","skin","tone"]}, + {"emoji":"🙇🏾‍♂️","name":"man bowing: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","bowing:","medium-dark","skin","tone"]}, + {"emoji":"🙇🏿‍♂️","name":"man bowing: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","bowing:","dark","skin","tone"]}, + {"emoji":"🙇‍♀️","name":"woman bowing","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","bowing"]}, + {"emoji":"🙇🏻‍♀️","name":"woman bowing: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","bowing:","light","skin","tone"]}, + {"emoji":"🙇🏼‍♀️","name":"woman bowing: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","bowing:","medium-light","skin","tone"]}, + {"emoji":"🙇🏽‍♀️","name":"woman bowing: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","bowing:","medium","skin","tone"]}, + {"emoji":"🙇🏾‍♀️","name":"woman bowing: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","bowing:","medium-dark","skin","tone"]}, + {"emoji":"🙇🏿‍♀️","name":"woman bowing: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","bowing:","dark","skin","tone"]}, + {"emoji":"🤦","name":"person facepalming","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["person","facepalming","again","bewilder","disbelief","exasperation","facepalm","no","not","oh","omg","shock","smh"]}, + {"emoji":"🤦🏻","name":"person facepalming: light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","facepalming:","light","skin","tone"]}, + {"emoji":"🤦🏼","name":"person facepalming: medium-light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","facepalming:","medium-light","skin","tone"]}, + {"emoji":"🤦🏽","name":"person facepalming: medium skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","facepalming:","medium","skin","tone"]}, + {"emoji":"🤦🏾","name":"person facepalming: medium-dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","facepalming:","medium-dark","skin","tone"]}, + {"emoji":"🤦🏿","name":"person facepalming: dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","facepalming:","dark","skin","tone"]}, + {"emoji":"🤦‍♂️","name":"man facepalming","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","facepalming"]}, + {"emoji":"🤦🏻‍♂️","name":"man facepalming: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","facepalming:","light","skin","tone"]}, + {"emoji":"🤦🏼‍♂️","name":"man facepalming: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","facepalming:","medium-light","skin","tone"]}, + {"emoji":"🤦🏽‍♂️","name":"man facepalming: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","facepalming:","medium","skin","tone"]}, + {"emoji":"🤦🏾‍♂️","name":"man facepalming: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","facepalming:","medium-dark","skin","tone"]}, + {"emoji":"🤦🏿‍♂️","name":"man facepalming: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","facepalming:","dark","skin","tone"]}, + {"emoji":"🤦‍♀️","name":"woman facepalming","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","facepalming"]}, + {"emoji":"🤦🏻‍♀️","name":"woman facepalming: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","facepalming:","light","skin","tone"]}, + {"emoji":"🤦🏼‍♀️","name":"woman facepalming: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","facepalming:","medium-light","skin","tone"]}, + {"emoji":"🤦🏽‍♀️","name":"woman facepalming: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","facepalming:","medium","skin","tone"]}, + {"emoji":"🤦🏾‍♀️","name":"woman facepalming: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","facepalming:","medium-dark","skin","tone"]}, + {"emoji":"🤦🏿‍♀️","name":"woman facepalming: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","facepalming:","dark","skin","tone"]}, + {"emoji":"🤷","name":"person shrugging","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["person","shrugging","doubt","dunno","guess","idk","ignorance","indifference","knows","maybe","shrug","whatever","who"]}, + {"emoji":"🤷🏻","name":"person shrugging: light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","shrugging:","light","skin","tone"]}, + {"emoji":"🤷🏼","name":"person shrugging: medium-light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","shrugging:","medium-light","skin","tone"]}, + {"emoji":"🤷🏽","name":"person shrugging: medium skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","shrugging:","medium","skin","tone"]}, + {"emoji":"🤷🏾","name":"person shrugging: medium-dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","shrugging:","medium-dark","skin","tone"]}, + {"emoji":"🤷🏿","name":"person shrugging: dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","shrugging:","dark","skin","tone"]}, + {"emoji":"🤷‍♂️","name":"man shrugging","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","shrugging"]}, + {"emoji":"🤷🏻‍♂️","name":"man shrugging: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","shrugging:","light","skin","tone"]}, + {"emoji":"🤷🏼‍♂️","name":"man shrugging: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","shrugging:","medium-light","skin","tone"]}, + {"emoji":"🤷🏽‍♂️","name":"man shrugging: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","shrugging:","medium","skin","tone"]}, + {"emoji":"🤷🏾‍♂️","name":"man shrugging: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","shrugging:","medium-dark","skin","tone"]}, + {"emoji":"🤷🏿‍♂️","name":"man shrugging: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","shrugging:","dark","skin","tone"]}, + {"emoji":"🤷‍♀️","name":"woman shrugging","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","shrugging"]}, + {"emoji":"🤷🏻‍♀️","name":"woman shrugging: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","shrugging:","light","skin","tone"]}, + {"emoji":"🤷🏼‍♀️","name":"woman shrugging: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","shrugging:","medium-light","skin","tone"]}, + {"emoji":"🤷🏽‍♀️","name":"woman shrugging: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","shrugging:","medium","skin","tone"]}, + {"emoji":"🤷🏾‍♀️","name":"woman shrugging: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","shrugging:","medium-dark","skin","tone"]}, + {"emoji":"🤷🏿‍♀️","name":"woman shrugging: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","shrugging:","dark","skin","tone"]}, + {"emoji":"🧑‍⚕️","name":"health worker","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["health","worker"]}, + {"emoji":"🧑🏻‍⚕️","name":"health worker: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["health","worker:","light","skin","tone"]}, + {"emoji":"🧑🏼‍⚕️","name":"health worker: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["health","worker:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍⚕️","name":"health worker: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["health","worker:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍⚕️","name":"health worker: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["health","worker:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍⚕️","name":"health worker: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["health","worker:","dark","skin","tone"]}, + {"emoji":"👨‍⚕️","name":"man health worker","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","health","worker"]}, + {"emoji":"👨🏻‍⚕️","name":"man health worker: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","health","worker:","light","skin","tone"]}, + {"emoji":"👨🏼‍⚕️","name":"man health worker: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","health","worker:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍⚕️","name":"man health worker: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","health","worker:","medium","skin","tone"]}, + {"emoji":"👨🏾‍⚕️","name":"man health worker: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","health","worker:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍⚕️","name":"man health worker: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","health","worker:","dark","skin","tone"]}, + {"emoji":"👩‍⚕️","name":"woman health worker","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","health","worker"]}, + {"emoji":"👩🏻‍⚕️","name":"woman health worker: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","health","worker:","light","skin","tone"]}, + {"emoji":"👩🏼‍⚕️","name":"woman health worker: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","health","worker:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍⚕️","name":"woman health worker: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","health","worker:","medium","skin","tone"]}, + {"emoji":"👩🏾‍⚕️","name":"woman health worker: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","health","worker:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍⚕️","name":"woman health worker: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","health","worker:","dark","skin","tone"]}, + {"emoji":"🧑‍🎓","name":"student","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["student","graduate"]}, + {"emoji":"🧑🏻‍🎓","name":"student: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["student:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🎓","name":"student: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["student:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🎓","name":"student: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["student:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🎓","name":"student: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["student:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🎓","name":"student: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["student:","dark","skin","tone"]}, + {"emoji":"👨‍🎓","name":"man student","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","student","graduate"]}, + {"emoji":"👨🏻‍🎓","name":"man student: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","student:","light","skin","tone"]}, + {"emoji":"👨🏼‍🎓","name":"man student: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","student:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🎓","name":"man student: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","student:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🎓","name":"man student: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","student:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🎓","name":"man student: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","student:","dark","skin","tone"]}, + {"emoji":"👩‍🎓","name":"woman student","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","student","graduate"]}, + {"emoji":"👩🏻‍🎓","name":"woman student: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","student:","light","skin","tone"]}, + {"emoji":"👩🏼‍🎓","name":"woman student: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","student:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🎓","name":"woman student: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","student:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🎓","name":"woman student: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","student:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🎓","name":"woman student: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","student:","dark","skin","tone"]}, + {"emoji":"🧑‍🏫","name":"teacher","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["teacher","instructor","lecturer","professor"]}, + {"emoji":"🧑🏻‍🏫","name":"teacher: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["teacher:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🏫","name":"teacher: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["teacher:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🏫","name":"teacher: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["teacher:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🏫","name":"teacher: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["teacher:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🏫","name":"teacher: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["teacher:","dark","skin","tone"]}, + {"emoji":"👨‍🏫","name":"man teacher","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","teacher","instructor","lecturer","professor"]}, + {"emoji":"👨🏻‍🏫","name":"man teacher: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","teacher:","light","skin","tone"]}, + {"emoji":"👨🏼‍🏫","name":"man teacher: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","teacher:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🏫","name":"man teacher: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","teacher:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🏫","name":"man teacher: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","teacher:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🏫","name":"man teacher: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","teacher:","dark","skin","tone"]}, + {"emoji":"👩‍🏫","name":"woman teacher","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","teacher","instructor","lecturer","professor"]}, + {"emoji":"👩🏻‍🏫","name":"woman teacher: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","teacher:","light","skin","tone"]}, + {"emoji":"👩🏼‍🏫","name":"woman teacher: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","teacher:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🏫","name":"woman teacher: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","teacher:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🏫","name":"woman teacher: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","teacher:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🏫","name":"woman teacher: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","teacher:","dark","skin","tone"]}, + {"emoji":"🧑‍⚖️","name":"judge","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["judge"]}, + {"emoji":"🧑🏻‍⚖️","name":"judge: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["judge:","light","skin","tone"]}, + {"emoji":"🧑🏼‍⚖️","name":"judge: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["judge:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍⚖️","name":"judge: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["judge:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍⚖️","name":"judge: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["judge:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍⚖️","name":"judge: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["judge:","dark","skin","tone"]}, + {"emoji":"👨‍⚖️","name":"man judge","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","judge"]}, + {"emoji":"👨🏻‍⚖️","name":"man judge: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","judge:","light","skin","tone"]}, + {"emoji":"👨🏼‍⚖️","name":"man judge: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","judge:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍⚖️","name":"man judge: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","judge:","medium","skin","tone"]}, + {"emoji":"👨🏾‍⚖️","name":"man judge: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","judge:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍⚖️","name":"man judge: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","judge:","dark","skin","tone"]}, + {"emoji":"👩‍⚖️","name":"woman judge","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","judge"]}, + {"emoji":"👩🏻‍⚖️","name":"woman judge: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","judge:","light","skin","tone"]}, + {"emoji":"👩🏼‍⚖️","name":"woman judge: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","judge:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍⚖️","name":"woman judge: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","judge:","medium","skin","tone"]}, + {"emoji":"👩🏾‍⚖️","name":"woman judge: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","judge:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍⚖️","name":"woman judge: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","judge:","dark","skin","tone"]}, + {"emoji":"🧑‍🌾","name":"farmer","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["farmer","gardener","rancher"]}, + {"emoji":"🧑🏻‍🌾","name":"farmer: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["farmer:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🌾","name":"farmer: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["farmer:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🌾","name":"farmer: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["farmer:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🌾","name":"farmer: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["farmer:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🌾","name":"farmer: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["farmer:","dark","skin","tone"]}, + {"emoji":"👨‍🌾","name":"man farmer","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","farmer","gardener","rancher"]}, + {"emoji":"👨🏻‍🌾","name":"man farmer: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","farmer:","light","skin","tone"]}, + {"emoji":"👨🏼‍🌾","name":"man farmer: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","farmer:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🌾","name":"man farmer: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","farmer:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🌾","name":"man farmer: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","farmer:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🌾","name":"man farmer: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","farmer:","dark","skin","tone"]}, + {"emoji":"👩‍🌾","name":"woman farmer","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","farmer","gardener","rancher"]}, + {"emoji":"👩🏻‍🌾","name":"woman farmer: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","farmer:","light","skin","tone"]}, + {"emoji":"👩🏼‍🌾","name":"woman farmer: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","farmer:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🌾","name":"woman farmer: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","farmer:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🌾","name":"woman farmer: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","farmer:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🌾","name":"woman farmer: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","farmer:","dark","skin","tone"]}, + {"emoji":"🧑‍🍳","name":"cook","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["cook","chef"]}, + {"emoji":"🧑🏻‍🍳","name":"cook: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["cook:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🍳","name":"cook: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["cook:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🍳","name":"cook: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["cook:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🍳","name":"cook: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["cook:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🍳","name":"cook: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["cook:","dark","skin","tone"]}, + {"emoji":"👨‍🍳","name":"man cook","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","cook","chef"]}, + {"emoji":"👨🏻‍🍳","name":"man cook: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","cook:","light","skin","tone"]}, + {"emoji":"👨🏼‍🍳","name":"man cook: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","cook:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🍳","name":"man cook: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","cook:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🍳","name":"man cook: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","cook:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🍳","name":"man cook: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","cook:","dark","skin","tone"]}, + {"emoji":"👩‍🍳","name":"woman cook","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","cook","chef"]}, + {"emoji":"👩🏻‍🍳","name":"woman cook: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","cook:","light","skin","tone"]}, + {"emoji":"👩🏼‍🍳","name":"woman cook: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","cook:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🍳","name":"woman cook: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","cook:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🍳","name":"woman cook: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","cook:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🍳","name":"woman cook: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","cook:","dark","skin","tone"]}, + {"emoji":"🧑‍🔧","name":"mechanic","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["mechanic","electrician","plumber","tradesperson"]}, + {"emoji":"🧑🏻‍🔧","name":"mechanic: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["mechanic:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🔧","name":"mechanic: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["mechanic:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🔧","name":"mechanic: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["mechanic:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🔧","name":"mechanic: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["mechanic:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🔧","name":"mechanic: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["mechanic:","dark","skin","tone"]}, + {"emoji":"👨‍🔧","name":"man mechanic","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","mechanic","electrician","plumber","tradesperson"]}, + {"emoji":"👨🏻‍🔧","name":"man mechanic: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","mechanic:","light","skin","tone"]}, + {"emoji":"👨🏼‍🔧","name":"man mechanic: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","mechanic:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🔧","name":"man mechanic: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","mechanic:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🔧","name":"man mechanic: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","mechanic:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🔧","name":"man mechanic: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","mechanic:","dark","skin","tone"]}, + {"emoji":"👩‍🔧","name":"woman mechanic","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","mechanic","electrician","plumber","tradesperson"]}, + {"emoji":"👩🏻‍🔧","name":"woman mechanic: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","mechanic:","light","skin","tone"]}, + {"emoji":"👩🏼‍🔧","name":"woman mechanic: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","mechanic:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🔧","name":"woman mechanic: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","mechanic:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🔧","name":"woman mechanic: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","mechanic:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🔧","name":"woman mechanic: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","mechanic:","dark","skin","tone"]}, + {"emoji":"🧑‍🏭","name":"factory worker","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["factory","worker","assembly","industrial"]}, + {"emoji":"🧑🏻‍🏭","name":"factory worker: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["factory","worker:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🏭","name":"factory worker: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["factory","worker:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🏭","name":"factory worker: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["factory","worker:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🏭","name":"factory worker: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["factory","worker:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🏭","name":"factory worker: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["factory","worker:","dark","skin","tone"]}, + {"emoji":"👨‍🏭","name":"man factory worker","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","factory","worker","assembly","industrial"]}, + {"emoji":"👨🏻‍🏭","name":"man factory worker: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","factory","worker:","light","skin","tone"]}, + {"emoji":"👨🏼‍🏭","name":"man factory worker: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","factory","worker:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🏭","name":"man factory worker: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","factory","worker:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🏭","name":"man factory worker: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","factory","worker:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🏭","name":"man factory worker: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","factory","worker:","dark","skin","tone"]}, + {"emoji":"👩‍🏭","name":"woman factory worker","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","factory","worker","assembly","industrial"]}, + {"emoji":"👩🏻‍🏭","name":"woman factory worker: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","factory","worker:","light","skin","tone"]}, + {"emoji":"👩🏼‍🏭","name":"woman factory worker: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","factory","worker:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🏭","name":"woman factory worker: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","factory","worker:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🏭","name":"woman factory worker: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","factory","worker:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🏭","name":"woman factory worker: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","factory","worker:","dark","skin","tone"]}, + {"emoji":"🧑‍💼","name":"office worker","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["office","worker","architect","business","manager","white-collar"]}, + {"emoji":"🧑🏻‍💼","name":"office worker: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["office","worker:","light","skin","tone"]}, + {"emoji":"🧑🏼‍💼","name":"office worker: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["office","worker:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍💼","name":"office worker: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["office","worker:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍💼","name":"office worker: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["office","worker:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍💼","name":"office worker: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["office","worker:","dark","skin","tone"]}, + {"emoji":"👨‍💼","name":"man office worker","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","office","worker","architect","business","manager","white-collar"]}, + {"emoji":"👨🏻‍💼","name":"man office worker: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","office","worker:","light","skin","tone"]}, + {"emoji":"👨🏼‍💼","name":"man office worker: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","office","worker:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍💼","name":"man office worker: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","office","worker:","medium","skin","tone"]}, + {"emoji":"👨🏾‍💼","name":"man office worker: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","office","worker:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍💼","name":"man office worker: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","office","worker:","dark","skin","tone"]}, + {"emoji":"👩‍💼","name":"woman office worker","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","office","worker","architect","business","manager","white-collar"]}, + {"emoji":"👩🏻‍💼","name":"woman office worker: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","office","worker:","light","skin","tone"]}, + {"emoji":"👩🏼‍💼","name":"woman office worker: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","office","worker:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍💼","name":"woman office worker: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","office","worker:","medium","skin","tone"]}, + {"emoji":"👩🏾‍💼","name":"woman office worker: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","office","worker:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍💼","name":"woman office worker: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","office","worker:","dark","skin","tone"]}, + {"emoji":"🧑‍🔬","name":"scientist","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["scientist","biologist","chemist","engineer","mathematician","physicist"]}, + {"emoji":"🧑🏻‍🔬","name":"scientist: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["scientist:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🔬","name":"scientist: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["scientist:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🔬","name":"scientist: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["scientist:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🔬","name":"scientist: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["scientist:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🔬","name":"scientist: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["scientist:","dark","skin","tone"]}, + {"emoji":"👨‍🔬","name":"man scientist","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","scientist","biologist","chemist","engineer","mathematician","physicist"]}, + {"emoji":"👨🏻‍🔬","name":"man scientist: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","scientist:","light","skin","tone"]}, + {"emoji":"👨🏼‍🔬","name":"man scientist: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","scientist:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🔬","name":"man scientist: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","scientist:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🔬","name":"man scientist: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","scientist:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🔬","name":"man scientist: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","scientist:","dark","skin","tone"]}, + {"emoji":"👩‍🔬","name":"woman scientist","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","scientist","biologist","chemist","engineer","mathematician","physicist"]}, + {"emoji":"👩🏻‍🔬","name":"woman scientist: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","scientist:","light","skin","tone"]}, + {"emoji":"👩🏼‍🔬","name":"woman scientist: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","scientist:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🔬","name":"woman scientist: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","scientist:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🔬","name":"woman scientist: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","scientist:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🔬","name":"woman scientist: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","scientist:","dark","skin","tone"]}, + {"emoji":"🧑‍💻","name":"technologist","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["technologist","coder","computer","developer","inventor","software"]}, + {"emoji":"🧑🏻‍💻","name":"technologist: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["technologist:","light","skin","tone"]}, + {"emoji":"🧑🏼‍💻","name":"technologist: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["technologist:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍💻","name":"technologist: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["technologist:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍💻","name":"technologist: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["technologist:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍💻","name":"technologist: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["technologist:","dark","skin","tone"]}, + {"emoji":"👨‍💻","name":"man technologist","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","technologist","coder","computer","developer","inventor","software"]}, + {"emoji":"👨🏻‍💻","name":"man technologist: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","technologist:","light","skin","tone"]}, + {"emoji":"👨🏼‍💻","name":"man technologist: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","technologist:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍💻","name":"man technologist: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","technologist:","medium","skin","tone"]}, + {"emoji":"👨🏾‍💻","name":"man technologist: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","technologist:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍💻","name":"man technologist: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","technologist:","dark","skin","tone"]}, + {"emoji":"👩‍💻","name":"woman technologist","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","technologist","coder","computer","developer","inventor","software"]}, + {"emoji":"👩🏻‍💻","name":"woman technologist: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","technologist:","light","skin","tone"]}, + {"emoji":"👩🏼‍💻","name":"woman technologist: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","technologist:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍💻","name":"woman technologist: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","technologist:","medium","skin","tone"]}, + {"emoji":"👩🏾‍💻","name":"woman technologist: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","technologist:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍💻","name":"woman technologist: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","technologist:","dark","skin","tone"]}, + {"emoji":"🧑‍🎤","name":"singer","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["singer","actor","entertainer","rock","rockstar","star"]}, + {"emoji":"🧑🏻‍🎤","name":"singer: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["singer:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🎤","name":"singer: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["singer:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🎤","name":"singer: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["singer:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🎤","name":"singer: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["singer:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🎤","name":"singer: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["singer:","dark","skin","tone"]}, + {"emoji":"👨‍🎤","name":"man singer","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","singer","actor","entertainer","rock","rockstar","star"]}, + {"emoji":"👨🏻‍🎤","name":"man singer: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","singer:","light","skin","tone"]}, + {"emoji":"👨🏼‍🎤","name":"man singer: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","singer:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🎤","name":"man singer: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","singer:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🎤","name":"man singer: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","singer:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🎤","name":"man singer: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","singer:","dark","skin","tone"]}, + {"emoji":"👩‍🎤","name":"woman singer","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","singer","actor","entertainer","rock","rockstar","star"]}, + {"emoji":"👩🏻‍🎤","name":"woman singer: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","singer:","light","skin","tone"]}, + {"emoji":"👩🏼‍🎤","name":"woman singer: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","singer:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🎤","name":"woman singer: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","singer:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🎤","name":"woman singer: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","singer:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🎤","name":"woman singer: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","singer:","dark","skin","tone"]}, + {"emoji":"🧑‍🎨","name":"artist","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["artist","palette"]}, + {"emoji":"🧑🏻‍🎨","name":"artist: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["artist:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🎨","name":"artist: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["artist:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🎨","name":"artist: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["artist:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🎨","name":"artist: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["artist:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🎨","name":"artist: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["artist:","dark","skin","tone"]}, + {"emoji":"👨‍🎨","name":"man artist","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","artist","palette"]}, + {"emoji":"👨🏻‍🎨","name":"man artist: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","artist:","light","skin","tone"]}, + {"emoji":"👨🏼‍🎨","name":"man artist: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","artist:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🎨","name":"man artist: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","artist:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🎨","name":"man artist: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","artist:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🎨","name":"man artist: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","artist:","dark","skin","tone"]}, + {"emoji":"👩‍🎨","name":"woman artist","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","artist","palette"]}, + {"emoji":"👩🏻‍🎨","name":"woman artist: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","artist:","light","skin","tone"]}, + {"emoji":"👩🏼‍🎨","name":"woman artist: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","artist:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🎨","name":"woman artist: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","artist:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🎨","name":"woman artist: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","artist:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🎨","name":"woman artist: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","artist:","dark","skin","tone"]}, + {"emoji":"🧑‍✈️","name":"pilot","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["pilot"]}, + {"emoji":"🧑🏻‍✈️","name":"pilot: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["pilot:","light","skin","tone"]}, + {"emoji":"🧑🏼‍✈️","name":"pilot: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["pilot:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍✈️","name":"pilot: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["pilot:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍✈️","name":"pilot: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["pilot:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍✈️","name":"pilot: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["pilot:","dark","skin","tone"]}, + {"emoji":"👨‍✈️","name":"man pilot","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","pilot"]}, + {"emoji":"👨🏻‍✈️","name":"man pilot: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","pilot:","light","skin","tone"]}, + {"emoji":"👨🏼‍✈️","name":"man pilot: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","pilot:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍✈️","name":"man pilot: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","pilot:","medium","skin","tone"]}, + {"emoji":"👨🏾‍✈️","name":"man pilot: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","pilot:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍✈️","name":"man pilot: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","pilot:","dark","skin","tone"]}, + {"emoji":"👩‍✈️","name":"woman pilot","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","pilot"]}, + {"emoji":"👩🏻‍✈️","name":"woman pilot: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","pilot:","light","skin","tone"]}, + {"emoji":"👩🏼‍✈️","name":"woman pilot: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","pilot:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍✈️","name":"woman pilot: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","pilot:","medium","skin","tone"]}, + {"emoji":"👩🏾‍✈️","name":"woman pilot: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","pilot:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍✈️","name":"woman pilot: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","pilot:","dark","skin","tone"]}, + {"emoji":"🧑‍🚀","name":"astronaut","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["astronaut","rocket","space"]}, + {"emoji":"🧑🏻‍🚀","name":"astronaut: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["astronaut:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🚀","name":"astronaut: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["astronaut:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🚀","name":"astronaut: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["astronaut:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🚀","name":"astronaut: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["astronaut:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🚀","name":"astronaut: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["astronaut:","dark","skin","tone"]}, + {"emoji":"👨‍🚀","name":"man astronaut","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","astronaut","rocket","space"]}, + {"emoji":"👨🏻‍🚀","name":"man astronaut: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","astronaut:","light","skin","tone"]}, + {"emoji":"👨🏼‍🚀","name":"man astronaut: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","astronaut:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🚀","name":"man astronaut: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","astronaut:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🚀","name":"man astronaut: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","astronaut:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🚀","name":"man astronaut: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","astronaut:","dark","skin","tone"]}, + {"emoji":"👩‍🚀","name":"woman astronaut","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","astronaut","rocket","space"]}, + {"emoji":"👩🏻‍🚀","name":"woman astronaut: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","astronaut:","light","skin","tone"]}, + {"emoji":"👩🏼‍🚀","name":"woman astronaut: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","astronaut:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🚀","name":"woman astronaut: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","astronaut:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🚀","name":"woman astronaut: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","astronaut:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🚀","name":"woman astronaut: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","astronaut:","dark","skin","tone"]}, + {"emoji":"🧑‍🚒","name":"firefighter","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["firefighter","fire","firetruck"]}, + {"emoji":"🧑🏻‍🚒","name":"firefighter: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["firefighter:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🚒","name":"firefighter: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["firefighter:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🚒","name":"firefighter: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["firefighter:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🚒","name":"firefighter: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["firefighter:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🚒","name":"firefighter: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["firefighter:","dark","skin","tone"]}, + {"emoji":"👨‍🚒","name":"man firefighter","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","firefighter","fire","firetruck"]}, + {"emoji":"👨🏻‍🚒","name":"man firefighter: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","firefighter:","light","skin","tone"]}, + {"emoji":"👨🏼‍🚒","name":"man firefighter: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","firefighter:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🚒","name":"man firefighter: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","firefighter:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🚒","name":"man firefighter: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","firefighter:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🚒","name":"man firefighter: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","firefighter:","dark","skin","tone"]}, + {"emoji":"👩‍🚒","name":"woman firefighter","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","firefighter","fire","firetruck"]}, + {"emoji":"👩🏻‍🚒","name":"woman firefighter: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","firefighter:","light","skin","tone"]}, + {"emoji":"👩🏼‍🚒","name":"woman firefighter: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","firefighter:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🚒","name":"woman firefighter: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","firefighter:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🚒","name":"woman firefighter: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","firefighter:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🚒","name":"woman firefighter: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","firefighter:","dark","skin","tone"]}, + {"emoji":"👮","name":"police officer","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["police","officer","apprehend","arrest","citation","cop","law","over","pulled","undercover"]}, + {"emoji":"👮🏻","name":"police officer: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["police","officer:","light","skin","tone"]}, + {"emoji":"👮🏼","name":"police officer: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["police","officer:","medium-light","skin","tone"]}, + {"emoji":"👮🏽","name":"police officer: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["police","officer:","medium","skin","tone"]}, + {"emoji":"👮🏾","name":"police officer: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["police","officer:","medium-dark","skin","tone"]}, + {"emoji":"👮🏿","name":"police officer: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["police","officer:","dark","skin","tone"]}, + {"emoji":"👮‍♂️","name":"man police officer","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","police","officer"]}, + {"emoji":"👮🏻‍♂️","name":"man police officer: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","police","officer:","light","skin","tone"]}, + {"emoji":"👮🏼‍♂️","name":"man police officer: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","police","officer:","medium-light","skin","tone"]}, + {"emoji":"👮🏽‍♂️","name":"man police officer: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","police","officer:","medium","skin","tone"]}, + {"emoji":"👮🏾‍♂️","name":"man police officer: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","police","officer:","medium-dark","skin","tone"]}, + {"emoji":"👮🏿‍♂️","name":"man police officer: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","police","officer:","dark","skin","tone"]}, + {"emoji":"👮‍♀️","name":"woman police officer","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","police","officer"]}, + {"emoji":"👮🏻‍♀️","name":"woman police officer: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","police","officer:","light","skin","tone"]}, + {"emoji":"👮🏼‍♀️","name":"woman police officer: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","police","officer:","medium-light","skin","tone"]}, + {"emoji":"👮🏽‍♀️","name":"woman police officer: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","police","officer:","medium","skin","tone"]}, + {"emoji":"👮🏾‍♀️","name":"woman police officer: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","police","officer:","medium-dark","skin","tone"]}, + {"emoji":"👮🏿‍♀️","name":"woman police officer: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","police","officer:","dark","skin","tone"]}, + {"emoji":"🕵️","name":"detective","category":"Smileys & People","version":"0.7","skinTone":false,"keywords":["detective"]}, + {"emoji":"🕵🏻","name":"detective: light skin tone","category":"Smileys & People","version":"2.0","skinTone":true,"keywords":["detective:","light","skin","tone"]}, + {"emoji":"🕵🏼","name":"detective: medium-light skin tone","category":"Smileys & People","version":"2.0","skinTone":true,"keywords":["detective:","medium-light","skin","tone"]}, + {"emoji":"🕵🏽","name":"detective: medium skin tone","category":"Smileys & People","version":"2.0","skinTone":true,"keywords":["detective:","medium","skin","tone"]}, + {"emoji":"🕵🏾","name":"detective: medium-dark skin tone","category":"Smileys & People","version":"2.0","skinTone":true,"keywords":["detective:","medium-dark","skin","tone"]}, + {"emoji":"🕵🏿","name":"detective: dark skin tone","category":"Smileys & People","version":"2.0","skinTone":true,"keywords":["detective:","dark","skin","tone"]}, + {"emoji":"🕵️‍♂️","name":"man detective","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","detective"]}, + {"emoji":"🕵🏻‍♂️","name":"man detective: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","detective:","light","skin","tone"]}, + {"emoji":"🕵🏼‍♂️","name":"man detective: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","detective:","medium-light","skin","tone"]}, + {"emoji":"🕵🏽‍♂️","name":"man detective: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","detective:","medium","skin","tone"]}, + {"emoji":"🕵🏾‍♂️","name":"man detective: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","detective:","medium-dark","skin","tone"]}, + {"emoji":"🕵🏿‍♂️","name":"man detective: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","detective:","dark","skin","tone"]}, + {"emoji":"🕵️‍♀️","name":"woman detective","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","detective"]}, + {"emoji":"🕵🏻‍♀️","name":"woman detective: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","detective:","light","skin","tone"]}, + {"emoji":"🕵🏼‍♀️","name":"woman detective: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","detective:","medium-light","skin","tone"]}, + {"emoji":"🕵🏽‍♀️","name":"woman detective: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","detective:","medium","skin","tone"]}, + {"emoji":"🕵🏾‍♀️","name":"woman detective: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","detective:","medium-dark","skin","tone"]}, + {"emoji":"🕵🏿‍♀️","name":"woman detective: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","detective:","dark","skin","tone"]}, + {"emoji":"💂","name":"guard","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["guard","buckingham","helmet","london","palace"]}, + {"emoji":"💂🏻","name":"guard: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["guard:","light","skin","tone"]}, + {"emoji":"💂🏼","name":"guard: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["guard:","medium-light","skin","tone"]}, + {"emoji":"💂🏽","name":"guard: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["guard:","medium","skin","tone"]}, + {"emoji":"💂🏾","name":"guard: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["guard:","medium-dark","skin","tone"]}, + {"emoji":"💂🏿","name":"guard: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["guard:","dark","skin","tone"]}, + {"emoji":"💂‍♂️","name":"man guard","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","guard"]}, + {"emoji":"💂🏻‍♂️","name":"man guard: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","guard:","light","skin","tone"]}, + {"emoji":"💂🏼‍♂️","name":"man guard: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","guard:","medium-light","skin","tone"]}, + {"emoji":"💂🏽‍♂️","name":"man guard: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","guard:","medium","skin","tone"]}, + {"emoji":"💂🏾‍♂️","name":"man guard: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","guard:","medium-dark","skin","tone"]}, + {"emoji":"💂🏿‍♂️","name":"man guard: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","guard:","dark","skin","tone"]}, + {"emoji":"💂‍♀️","name":"woman guard","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","guard"]}, + {"emoji":"💂🏻‍♀️","name":"woman guard: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","guard:","light","skin","tone"]}, + {"emoji":"💂🏼‍♀️","name":"woman guard: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","guard:","medium-light","skin","tone"]}, + {"emoji":"💂🏽‍♀️","name":"woman guard: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","guard:","medium","skin","tone"]}, + {"emoji":"💂🏾‍♀️","name":"woman guard: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","guard:","medium-dark","skin","tone"]}, + {"emoji":"💂🏿‍♀️","name":"woman guard: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","guard:","dark","skin","tone"]}, + {"emoji":"🥷","name":"ninja","category":"Smileys & People","version":"13.0","skinTone":false,"keywords":["ninja","assassin","fight","fighter","hidden","person","secret","skills","sly","soldier","stealth","war"]}, + {"emoji":"🥷🏻","name":"ninja: light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["ninja:","light","skin","tone"]}, + {"emoji":"🥷🏼","name":"ninja: medium-light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["ninja:","medium-light","skin","tone"]}, + {"emoji":"🥷🏽","name":"ninja: medium skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["ninja:","medium","skin","tone"]}, + {"emoji":"🥷🏾","name":"ninja: medium-dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["ninja:","medium-dark","skin","tone"]}, + {"emoji":"🥷🏿","name":"ninja: dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["ninja:","dark","skin","tone"]}, + {"emoji":"👷","name":"construction worker","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["construction","worker","build","fix","hardhat","hat","man","person","rebuild","remodel","repair","work"]}, + {"emoji":"👷🏻","name":"construction worker: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["construction","worker:","light","skin","tone"]}, + {"emoji":"👷🏼","name":"construction worker: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["construction","worker:","medium-light","skin","tone"]}, + {"emoji":"👷🏽","name":"construction worker: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["construction","worker:","medium","skin","tone"]}, + {"emoji":"👷🏾","name":"construction worker: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["construction","worker:","medium-dark","skin","tone"]}, + {"emoji":"👷🏿","name":"construction worker: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["construction","worker:","dark","skin","tone"]}, + {"emoji":"👷‍♂️","name":"man construction worker","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","construction","worker"]}, + {"emoji":"👷🏻‍♂️","name":"man construction worker: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","construction","worker:","light","skin","tone"]}, + {"emoji":"👷🏼‍♂️","name":"man construction worker: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","construction","worker:","medium-light","skin","tone"]}, + {"emoji":"👷🏽‍♂️","name":"man construction worker: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","construction","worker:","medium","skin","tone"]}, + {"emoji":"👷🏾‍♂️","name":"man construction worker: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","construction","worker:","medium-dark","skin","tone"]}, + {"emoji":"👷🏿‍♂️","name":"man construction worker: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","construction","worker:","dark","skin","tone"]}, + {"emoji":"👷‍♀️","name":"woman construction worker","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","construction","worker"]}, + {"emoji":"👷🏻‍♀️","name":"woman construction worker: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","construction","worker:","light","skin","tone"]}, + {"emoji":"👷🏼‍♀️","name":"woman construction worker: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","construction","worker:","medium-light","skin","tone"]}, + {"emoji":"👷🏽‍♀️","name":"woman construction worker: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","construction","worker:","medium","skin","tone"]}, + {"emoji":"👷🏾‍♀️","name":"woman construction worker: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","construction","worker:","medium-dark","skin","tone"]}, + {"emoji":"👷🏿‍♀️","name":"woman construction worker: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","construction","worker:","dark","skin","tone"]}, + {"emoji":"🫅","name":"person with crown","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["person","with","crown","monarch","noble","regal","royal","royalty"]}, + {"emoji":"🫅🏻","name":"person with crown: light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["person","with","crown:","light","skin","tone"]}, + {"emoji":"🫅🏼","name":"person with crown: medium-light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["person","with","crown:","medium-light","skin","tone"]}, + {"emoji":"🫅🏽","name":"person with crown: medium skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["person","with","crown:","medium","skin","tone"]}, + {"emoji":"🫅🏾","name":"person with crown: medium-dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["person","with","crown:","medium-dark","skin","tone"]}, + {"emoji":"🫅🏿","name":"person with crown: dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["person","with","crown:","dark","skin","tone"]}, + {"emoji":"🤴","name":"prince","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["prince","crown","fairy","fairytale","fantasy","king","royal","royalty","tale"]}, + {"emoji":"🤴🏻","name":"prince: light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["prince:","light","skin","tone"]}, + {"emoji":"🤴🏼","name":"prince: medium-light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["prince:","medium-light","skin","tone"]}, + {"emoji":"🤴🏽","name":"prince: medium skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["prince:","medium","skin","tone"]}, + {"emoji":"🤴🏾","name":"prince: medium-dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["prince:","medium-dark","skin","tone"]}, + {"emoji":"🤴🏿","name":"prince: dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["prince:","dark","skin","tone"]}, + {"emoji":"👸","name":"princess","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["princess","crown","fairy","fairytale","fantasy","queen","royal","royalty","tale"]}, + {"emoji":"👸🏻","name":"princess: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["princess:","light","skin","tone"]}, + {"emoji":"👸🏼","name":"princess: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["princess:","medium-light","skin","tone"]}, + {"emoji":"👸🏽","name":"princess: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["princess:","medium","skin","tone"]}, + {"emoji":"👸🏾","name":"princess: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["princess:","medium-dark","skin","tone"]}, + {"emoji":"👸🏿","name":"princess: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["princess:","dark","skin","tone"]}, + {"emoji":"👳","name":"person wearing turban","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person","wearing","turban"]}, + {"emoji":"👳🏻","name":"person wearing turban: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","wearing","turban:","light","skin","tone"]}, + {"emoji":"👳🏼","name":"person wearing turban: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","wearing","turban:","medium-light","skin","tone"]}, + {"emoji":"👳🏽","name":"person wearing turban: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","wearing","turban:","medium","skin","tone"]}, + {"emoji":"👳🏾","name":"person wearing turban: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","wearing","turban:","medium-dark","skin","tone"]}, + {"emoji":"👳🏿","name":"person wearing turban: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","wearing","turban:","dark","skin","tone"]}, + {"emoji":"👳‍♂️","name":"man wearing turban","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","wearing","turban"]}, + {"emoji":"👳🏻‍♂️","name":"man wearing turban: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","wearing","turban:","light","skin","tone"]}, + {"emoji":"👳🏼‍♂️","name":"man wearing turban: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","wearing","turban:","medium-light","skin","tone"]}, + {"emoji":"👳🏽‍♂️","name":"man wearing turban: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","wearing","turban:","medium","skin","tone"]}, + {"emoji":"👳🏾‍♂️","name":"man wearing turban: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","wearing","turban:","medium-dark","skin","tone"]}, + {"emoji":"👳🏿‍♂️","name":"man wearing turban: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","wearing","turban:","dark","skin","tone"]}, + {"emoji":"👳‍♀️","name":"woman wearing turban","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","wearing","turban"]}, + {"emoji":"👳🏻‍♀️","name":"woman wearing turban: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","wearing","turban:","light","skin","tone"]}, + {"emoji":"👳🏼‍♀️","name":"woman wearing turban: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","wearing","turban:","medium-light","skin","tone"]}, + {"emoji":"👳🏽‍♀️","name":"woman wearing turban: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","wearing","turban:","medium","skin","tone"]}, + {"emoji":"👳🏾‍♀️","name":"woman wearing turban: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","wearing","turban:","medium-dark","skin","tone"]}, + {"emoji":"👳🏿‍♀️","name":"woman wearing turban: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","wearing","turban:","dark","skin","tone"]}, + {"emoji":"👲","name":"person with skullcap","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person","with","skullcap","cap","Chinese","gua","guapi","hat","mao","pi"]}, + {"emoji":"👲🏻","name":"person with skullcap: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","with","skullcap:","light","skin","tone"]}, + {"emoji":"👲🏼","name":"person with skullcap: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","with","skullcap:","medium-light","skin","tone"]}, + {"emoji":"👲🏽","name":"person with skullcap: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","with","skullcap:","medium","skin","tone"]}, + {"emoji":"👲🏾","name":"person with skullcap: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","with","skullcap:","medium-dark","skin","tone"]}, + {"emoji":"👲🏿","name":"person with skullcap: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","with","skullcap:","dark","skin","tone"]}, + {"emoji":"🧕","name":"woman with headscarf","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["woman","with","headscarf","bandana","head","hijab","kerchief","mantilla","tichel"]}, + {"emoji":"🧕🏻","name":"woman with headscarf: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","with","headscarf:","light","skin","tone"]}, + {"emoji":"🧕🏼","name":"woman with headscarf: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","with","headscarf:","medium-light","skin","tone"]}, + {"emoji":"🧕🏽","name":"woman with headscarf: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","with","headscarf:","medium","skin","tone"]}, + {"emoji":"🧕🏾","name":"woman with headscarf: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","with","headscarf:","medium-dark","skin","tone"]}, + {"emoji":"🧕🏿","name":"woman with headscarf: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","with","headscarf:","dark","skin","tone"]}, + {"emoji":"🤵","name":"person in tuxedo","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["person","in","tuxedo","formal","wedding"]}, + {"emoji":"🤵🏻","name":"person in tuxedo: light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","in","tuxedo:","light","skin","tone"]}, + {"emoji":"🤵🏼","name":"person in tuxedo: medium-light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","in","tuxedo:","medium-light","skin","tone"]}, + {"emoji":"🤵🏽","name":"person in tuxedo: medium skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","in","tuxedo:","medium","skin","tone"]}, + {"emoji":"🤵🏾","name":"person in tuxedo: medium-dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","in","tuxedo:","medium-dark","skin","tone"]}, + {"emoji":"🤵🏿","name":"person in tuxedo: dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","in","tuxedo:","dark","skin","tone"]}, + {"emoji":"🤵‍♂️","name":"man in tuxedo","category":"Smileys & People","version":"13.0","skinTone":false,"keywords":["man","in","tuxedo"]}, + {"emoji":"🤵🏻‍♂️","name":"man in tuxedo: light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["man","in","tuxedo:","light","skin","tone"]}, + {"emoji":"🤵🏼‍♂️","name":"man in tuxedo: medium-light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["man","in","tuxedo:","medium-light","skin","tone"]}, + {"emoji":"🤵🏽‍♂️","name":"man in tuxedo: medium skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["man","in","tuxedo:","medium","skin","tone"]}, + {"emoji":"🤵🏾‍♂️","name":"man in tuxedo: medium-dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["man","in","tuxedo:","medium-dark","skin","tone"]}, + {"emoji":"🤵🏿‍♂️","name":"man in tuxedo: dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["man","in","tuxedo:","dark","skin","tone"]}, + {"emoji":"🤵‍♀️","name":"woman in tuxedo","category":"Smileys & People","version":"13.0","skinTone":false,"keywords":["woman","in","tuxedo"]}, + {"emoji":"🤵🏻‍♀️","name":"woman in tuxedo: light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["woman","in","tuxedo:","light","skin","tone"]}, + {"emoji":"🤵🏼‍♀️","name":"woman in tuxedo: medium-light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["woman","in","tuxedo:","medium-light","skin","tone"]}, + {"emoji":"🤵🏽‍♀️","name":"woman in tuxedo: medium skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["woman","in","tuxedo:","medium","skin","tone"]}, + {"emoji":"🤵🏾‍♀️","name":"woman in tuxedo: medium-dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["woman","in","tuxedo:","medium-dark","skin","tone"]}, + {"emoji":"🤵🏿‍♀️","name":"woman in tuxedo: dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["woman","in","tuxedo:","dark","skin","tone"]}, + {"emoji":"👰","name":"person with veil","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person","with","veil","wedding"]}, + {"emoji":"👰🏻","name":"person with veil: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","with","veil:","light","skin","tone"]}, + {"emoji":"👰🏼","name":"person with veil: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","with","veil:","medium-light","skin","tone"]}, + {"emoji":"👰🏽","name":"person with veil: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","with","veil:","medium","skin","tone"]}, + {"emoji":"👰🏾","name":"person with veil: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","with","veil:","medium-dark","skin","tone"]}, + {"emoji":"👰🏿","name":"person with veil: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","with","veil:","dark","skin","tone"]}, + {"emoji":"👰‍♂️","name":"man with veil","category":"Smileys & People","version":"13.0","skinTone":false,"keywords":["man","with","veil"]}, + {"emoji":"👰🏻‍♂️","name":"man with veil: light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["man","with","veil:","light","skin","tone"]}, + {"emoji":"👰🏼‍♂️","name":"man with veil: medium-light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["man","with","veil:","medium-light","skin","tone"]}, + {"emoji":"👰🏽‍♂️","name":"man with veil: medium skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["man","with","veil:","medium","skin","tone"]}, + {"emoji":"👰🏾‍♂️","name":"man with veil: medium-dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["man","with","veil:","medium-dark","skin","tone"]}, + {"emoji":"👰🏿‍♂️","name":"man with veil: dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["man","with","veil:","dark","skin","tone"]}, + {"emoji":"👰‍♀️","name":"woman with veil","category":"Smileys & People","version":"13.0","skinTone":false,"keywords":["woman","with","veil"]}, + {"emoji":"👰🏻‍♀️","name":"woman with veil: light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["woman","with","veil:","light","skin","tone"]}, + {"emoji":"👰🏼‍♀️","name":"woman with veil: medium-light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["woman","with","veil:","medium-light","skin","tone"]}, + {"emoji":"👰🏽‍♀️","name":"woman with veil: medium skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["woman","with","veil:","medium","skin","tone"]}, + {"emoji":"👰🏾‍♀️","name":"woman with veil: medium-dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["woman","with","veil:","medium-dark","skin","tone"]}, + {"emoji":"👰🏿‍♀️","name":"woman with veil: dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["woman","with","veil:","dark","skin","tone"]}, + {"emoji":"🤰","name":"pregnant woman","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["pregnant","woman"]}, + {"emoji":"🤰🏻","name":"pregnant woman: light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["pregnant","woman:","light","skin","tone"]}, + {"emoji":"🤰🏼","name":"pregnant woman: medium-light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["pregnant","woman:","medium-light","skin","tone"]}, + {"emoji":"🤰🏽","name":"pregnant woman: medium skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["pregnant","woman:","medium","skin","tone"]}, + {"emoji":"🤰🏾","name":"pregnant woman: medium-dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["pregnant","woman:","medium-dark","skin","tone"]}, + {"emoji":"🤰🏿","name":"pregnant woman: dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["pregnant","woman:","dark","skin","tone"]}, + {"emoji":"🫃","name":"pregnant man","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["pregnant","man","belly","bloated","full","overeat"]}, + {"emoji":"🫃🏻","name":"pregnant man: light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["pregnant","man:","light","skin","tone"]}, + {"emoji":"🫃🏼","name":"pregnant man: medium-light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["pregnant","man:","medium-light","skin","tone"]}, + {"emoji":"🫃🏽","name":"pregnant man: medium skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["pregnant","man:","medium","skin","tone"]}, + {"emoji":"🫃🏾","name":"pregnant man: medium-dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["pregnant","man:","medium-dark","skin","tone"]}, + {"emoji":"🫃🏿","name":"pregnant man: dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["pregnant","man:","dark","skin","tone"]}, + {"emoji":"🫄","name":"pregnant person","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["pregnant","person","belly","bloated","full","overeat","stuffed"]}, + {"emoji":"🫄🏻","name":"pregnant person: light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["pregnant","person:","light","skin","tone"]}, + {"emoji":"🫄🏼","name":"pregnant person: medium-light skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["pregnant","person:","medium-light","skin","tone"]}, + {"emoji":"🫄🏽","name":"pregnant person: medium skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["pregnant","person:","medium","skin","tone"]}, + {"emoji":"🫄🏾","name":"pregnant person: medium-dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["pregnant","person:","medium-dark","skin","tone"]}, + {"emoji":"🫄🏿","name":"pregnant person: dark skin tone","category":"Smileys & People","version":"14.0","skinTone":true,"keywords":["pregnant","person:","dark","skin","tone"]}, + {"emoji":"🤱","name":"breast-feeding","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["breast-feeding","baby","breast","feeding","mom","mother","nursing","woman"]}, + {"emoji":"🤱🏻","name":"breast-feeding: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["breast-feeding:","light","skin","tone"]}, + {"emoji":"🤱🏼","name":"breast-feeding: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["breast-feeding:","medium-light","skin","tone"]}, + {"emoji":"🤱🏽","name":"breast-feeding: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["breast-feeding:","medium","skin","tone"]}, + {"emoji":"🤱🏾","name":"breast-feeding: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["breast-feeding:","medium-dark","skin","tone"]}, + {"emoji":"🤱🏿","name":"breast-feeding: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["breast-feeding:","dark","skin","tone"]}, + {"emoji":"👩‍🍼","name":"woman feeding baby","category":"Smileys & People","version":"13.0","skinTone":false,"keywords":["woman","feeding","baby","feed","mom","mother","nanny","newborn","nursing"]}, + {"emoji":"👩🏻‍🍼","name":"woman feeding baby: light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["woman","feeding","baby:","light","skin","tone"]}, + {"emoji":"👩🏼‍🍼","name":"woman feeding baby: medium-light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["woman","feeding","baby:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🍼","name":"woman feeding baby: medium skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["woman","feeding","baby:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🍼","name":"woman feeding baby: medium-dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["woman","feeding","baby:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🍼","name":"woman feeding baby: dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["woman","feeding","baby:","dark","skin","tone"]}, + {"emoji":"👨‍🍼","name":"man feeding baby","category":"Smileys & People","version":"13.0","skinTone":false,"keywords":["man","feeding","baby","dad","father","feed","nanny","newborn","nursing"]}, + {"emoji":"👨🏻‍🍼","name":"man feeding baby: light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["man","feeding","baby:","light","skin","tone"]}, + {"emoji":"👨🏼‍🍼","name":"man feeding baby: medium-light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["man","feeding","baby:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🍼","name":"man feeding baby: medium skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["man","feeding","baby:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🍼","name":"man feeding baby: medium-dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["man","feeding","baby:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🍼","name":"man feeding baby: dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["man","feeding","baby:","dark","skin","tone"]}, + {"emoji":"🧑‍🍼","name":"person feeding baby","category":"Smileys & People","version":"13.0","skinTone":false,"keywords":["person","feeding","baby","feed","nanny","newborn","nursing","parent"]}, + {"emoji":"🧑🏻‍🍼","name":"person feeding baby: light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["person","feeding","baby:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🍼","name":"person feeding baby: medium-light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["person","feeding","baby:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🍼","name":"person feeding baby: medium skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["person","feeding","baby:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🍼","name":"person feeding baby: medium-dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["person","feeding","baby:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🍼","name":"person feeding baby: dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["person","feeding","baby:","dark","skin","tone"]}, + {"emoji":"👼","name":"baby angel","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["baby","angel","church","face","fairy","fairytale","fantasy","tale"]}, + {"emoji":"👼🏻","name":"baby angel: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["baby","angel:","light","skin","tone"]}, + {"emoji":"👼🏼","name":"baby angel: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["baby","angel:","medium-light","skin","tone"]}, + {"emoji":"👼🏽","name":"baby angel: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["baby","angel:","medium","skin","tone"]}, + {"emoji":"👼🏾","name":"baby angel: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["baby","angel:","medium-dark","skin","tone"]}, + {"emoji":"👼🏿","name":"baby angel: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["baby","angel:","dark","skin","tone"]}, + {"emoji":"🎅","name":"Santa Claus","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["Santa","Claus","celebration","Christmas","claus","fairy","fantasy","father","holiday","merry","santa","tale","xmas"]}, + {"emoji":"🎅🏻","name":"Santa Claus: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["Santa","Claus:","light","skin","tone"]}, + {"emoji":"🎅🏼","name":"Santa Claus: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["Santa","Claus:","medium-light","skin","tone"]}, + {"emoji":"🎅🏽","name":"Santa Claus: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["Santa","Claus:","medium","skin","tone"]}, + {"emoji":"🎅🏾","name":"Santa Claus: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["Santa","Claus:","medium-dark","skin","tone"]}, + {"emoji":"🎅🏿","name":"Santa Claus: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["Santa","Claus:","dark","skin","tone"]}, + {"emoji":"🤶","name":"Mrs. Claus","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["Mrs.","Claus","celebration","Christmas","claus","fairy","fantasy","holiday","merry","mother","Mrs","santa","tale","xmas"]}, + {"emoji":"🤶🏻","name":"Mrs. Claus: light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["Mrs.","Claus:","light","skin","tone"]}, + {"emoji":"🤶🏼","name":"Mrs. Claus: medium-light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["Mrs.","Claus:","medium-light","skin","tone"]}, + {"emoji":"🤶🏽","name":"Mrs. Claus: medium skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["Mrs.","Claus:","medium","skin","tone"]}, + {"emoji":"🤶🏾","name":"Mrs. Claus: medium-dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["Mrs.","Claus:","medium-dark","skin","tone"]}, + {"emoji":"🤶🏿","name":"Mrs. Claus: dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["Mrs.","Claus:","dark","skin","tone"]}, + {"emoji":"🧑‍🎄","name":"Mx Claus","category":"Smileys & People","version":"13.0","skinTone":false,"keywords":["Mx","Claus","celebration","Christmas","claus","fairy","fantasy","holiday","merry","santa","tale","xmas"]}, + {"emoji":"🧑🏻‍🎄","name":"Mx Claus: light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["Mx","Claus:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🎄","name":"Mx Claus: medium-light skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["Mx","Claus:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🎄","name":"Mx Claus: medium skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["Mx","Claus:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🎄","name":"Mx Claus: medium-dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["Mx","Claus:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🎄","name":"Mx Claus: dark skin tone","category":"Smileys & People","version":"13.0","skinTone":true,"keywords":["Mx","Claus:","dark","skin","tone"]}, + {"emoji":"🦸","name":"superhero","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["superhero","good","hero","superpower"]}, + {"emoji":"🦸🏻","name":"superhero: light skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["superhero:","light","skin","tone"]}, + {"emoji":"🦸🏼","name":"superhero: medium-light skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["superhero:","medium-light","skin","tone"]}, + {"emoji":"🦸🏽","name":"superhero: medium skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["superhero:","medium","skin","tone"]}, + {"emoji":"🦸🏾","name":"superhero: medium-dark skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["superhero:","medium-dark","skin","tone"]}, + {"emoji":"🦸🏿","name":"superhero: dark skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["superhero:","dark","skin","tone"]}, + {"emoji":"🦸‍♂️","name":"man superhero","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["man","superhero"]}, + {"emoji":"🦸🏻‍♂️","name":"man superhero: light skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man","superhero:","light","skin","tone"]}, + {"emoji":"🦸🏼‍♂️","name":"man superhero: medium-light skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man","superhero:","medium-light","skin","tone"]}, + {"emoji":"🦸🏽‍♂️","name":"man superhero: medium skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man","superhero:","medium","skin","tone"]}, + {"emoji":"🦸🏾‍♂️","name":"man superhero: medium-dark skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man","superhero:","medium-dark","skin","tone"]}, + {"emoji":"🦸🏿‍♂️","name":"man superhero: dark skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man","superhero:","dark","skin","tone"]}, + {"emoji":"🦸‍♀️","name":"woman superhero","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["woman","superhero"]}, + {"emoji":"🦸🏻‍♀️","name":"woman superhero: light skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman","superhero:","light","skin","tone"]}, + {"emoji":"🦸🏼‍♀️","name":"woman superhero: medium-light skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman","superhero:","medium-light","skin","tone"]}, + {"emoji":"🦸🏽‍♀️","name":"woman superhero: medium skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman","superhero:","medium","skin","tone"]}, + {"emoji":"🦸🏾‍♀️","name":"woman superhero: medium-dark skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman","superhero:","medium-dark","skin","tone"]}, + {"emoji":"🦸🏿‍♀️","name":"woman superhero: dark skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman","superhero:","dark","skin","tone"]}, + {"emoji":"🦹","name":"supervillain","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["supervillain","bad","criminal","evil","superpower","villain"]}, + {"emoji":"🦹🏻","name":"supervillain: light skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["supervillain:","light","skin","tone"]}, + {"emoji":"🦹🏼","name":"supervillain: medium-light skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["supervillain:","medium-light","skin","tone"]}, + {"emoji":"🦹🏽","name":"supervillain: medium skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["supervillain:","medium","skin","tone"]}, + {"emoji":"🦹🏾","name":"supervillain: medium-dark skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["supervillain:","medium-dark","skin","tone"]}, + {"emoji":"🦹🏿","name":"supervillain: dark skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["supervillain:","dark","skin","tone"]}, + {"emoji":"🦹‍♂️","name":"man supervillain","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["man","supervillain"]}, + {"emoji":"🦹🏻‍♂️","name":"man supervillain: light skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man","supervillain:","light","skin","tone"]}, + {"emoji":"🦹🏼‍♂️","name":"man supervillain: medium-light skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man","supervillain:","medium-light","skin","tone"]}, + {"emoji":"🦹🏽‍♂️","name":"man supervillain: medium skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man","supervillain:","medium","skin","tone"]}, + {"emoji":"🦹🏾‍♂️","name":"man supervillain: medium-dark skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man","supervillain:","medium-dark","skin","tone"]}, + {"emoji":"🦹🏿‍♂️","name":"man supervillain: dark skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["man","supervillain:","dark","skin","tone"]}, + {"emoji":"🦹‍♀️","name":"woman supervillain","category":"Smileys & People","version":"11.0","skinTone":false,"keywords":["woman","supervillain"]}, + {"emoji":"🦹🏻‍♀️","name":"woman supervillain: light skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman","supervillain:","light","skin","tone"]}, + {"emoji":"🦹🏼‍♀️","name":"woman supervillain: medium-light skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman","supervillain:","medium-light","skin","tone"]}, + {"emoji":"🦹🏽‍♀️","name":"woman supervillain: medium skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman","supervillain:","medium","skin","tone"]}, + {"emoji":"🦹🏾‍♀️","name":"woman supervillain: medium-dark skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman","supervillain:","medium-dark","skin","tone"]}, + {"emoji":"🦹🏿‍♀️","name":"woman supervillain: dark skin tone","category":"Smileys & People","version":"11.0","skinTone":true,"keywords":["woman","supervillain:","dark","skin","tone"]}, + {"emoji":"🧙","name":"mage","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["mage","fantasy","magic","play","sorcerer","sorceress","sorcery","spell","summon","witch","wizard"]}, + {"emoji":"🧙🏻","name":"mage: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["mage:","light","skin","tone"]}, + {"emoji":"🧙🏼","name":"mage: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["mage:","medium-light","skin","tone"]}, + {"emoji":"🧙🏽","name":"mage: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["mage:","medium","skin","tone"]}, + {"emoji":"🧙🏾","name":"mage: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["mage:","medium-dark","skin","tone"]}, + {"emoji":"🧙🏿","name":"mage: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["mage:","dark","skin","tone"]}, + {"emoji":"🧙‍♂️","name":"man mage","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["man","mage"]}, + {"emoji":"🧙🏻‍♂️","name":"man mage: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","mage:","light","skin","tone"]}, + {"emoji":"🧙🏼‍♂️","name":"man mage: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","mage:","medium-light","skin","tone"]}, + {"emoji":"🧙🏽‍♂️","name":"man mage: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","mage:","medium","skin","tone"]}, + {"emoji":"🧙🏾‍♂️","name":"man mage: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","mage:","medium-dark","skin","tone"]}, + {"emoji":"🧙🏿‍♂️","name":"man mage: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","mage:","dark","skin","tone"]}, + {"emoji":"🧙‍♀️","name":"woman mage","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["woman","mage"]}, + {"emoji":"🧙🏻‍♀️","name":"woman mage: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","mage:","light","skin","tone"]}, + {"emoji":"🧙🏼‍♀️","name":"woman mage: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","mage:","medium-light","skin","tone"]}, + {"emoji":"🧙🏽‍♀️","name":"woman mage: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","mage:","medium","skin","tone"]}, + {"emoji":"🧙🏾‍♀️","name":"woman mage: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","mage:","medium-dark","skin","tone"]}, + {"emoji":"🧙🏿‍♀️","name":"woman mage: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","mage:","dark","skin","tone"]}, + {"emoji":"🧚","name":"fairy","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["fairy","fairytale","fantasy","myth","person","pixie","tale","wings"]}, + {"emoji":"🧚🏻","name":"fairy: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["fairy:","light","skin","tone"]}, + {"emoji":"🧚🏼","name":"fairy: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["fairy:","medium-light","skin","tone"]}, + {"emoji":"🧚🏽","name":"fairy: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["fairy:","medium","skin","tone"]}, + {"emoji":"🧚🏾","name":"fairy: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["fairy:","medium-dark","skin","tone"]}, + {"emoji":"🧚🏿","name":"fairy: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["fairy:","dark","skin","tone"]}, + {"emoji":"🧚‍♂️","name":"man fairy","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["man","fairy"]}, + {"emoji":"🧚🏻‍♂️","name":"man fairy: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","fairy:","light","skin","tone"]}, + {"emoji":"🧚🏼‍♂️","name":"man fairy: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","fairy:","medium-light","skin","tone"]}, + {"emoji":"🧚🏽‍♂️","name":"man fairy: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","fairy:","medium","skin","tone"]}, + {"emoji":"🧚🏾‍♂️","name":"man fairy: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","fairy:","medium-dark","skin","tone"]}, + {"emoji":"🧚🏿‍♂️","name":"man fairy: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","fairy:","dark","skin","tone"]}, + {"emoji":"🧚‍♀️","name":"woman fairy","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["woman","fairy"]}, + {"emoji":"🧚🏻‍♀️","name":"woman fairy: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","fairy:","light","skin","tone"]}, + {"emoji":"🧚🏼‍♀️","name":"woman fairy: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","fairy:","medium-light","skin","tone"]}, + {"emoji":"🧚🏽‍♀️","name":"woman fairy: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","fairy:","medium","skin","tone"]}, + {"emoji":"🧚🏾‍♀️","name":"woman fairy: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","fairy:","medium-dark","skin","tone"]}, + {"emoji":"🧚🏿‍♀️","name":"woman fairy: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","fairy:","dark","skin","tone"]}, + {"emoji":"🧛","name":"vampire","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["vampire","blood","Dracula","fangs","halloween","scary","supernatural","teeth","undead"]}, + {"emoji":"🧛🏻","name":"vampire: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["vampire:","light","skin","tone"]}, + {"emoji":"🧛🏼","name":"vampire: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["vampire:","medium-light","skin","tone"]}, + {"emoji":"🧛🏽","name":"vampire: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["vampire:","medium","skin","tone"]}, + {"emoji":"🧛🏾","name":"vampire: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["vampire:","medium-dark","skin","tone"]}, + {"emoji":"🧛🏿","name":"vampire: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["vampire:","dark","skin","tone"]}, + {"emoji":"🧛‍♂️","name":"man vampire","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["man","vampire"]}, + {"emoji":"🧛🏻‍♂️","name":"man vampire: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","vampire:","light","skin","tone"]}, + {"emoji":"🧛🏼‍♂️","name":"man vampire: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","vampire:","medium-light","skin","tone"]}, + {"emoji":"🧛🏽‍♂️","name":"man vampire: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","vampire:","medium","skin","tone"]}, + {"emoji":"🧛🏾‍♂️","name":"man vampire: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","vampire:","medium-dark","skin","tone"]}, + {"emoji":"🧛🏿‍♂️","name":"man vampire: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","vampire:","dark","skin","tone"]}, + {"emoji":"🧛‍♀️","name":"woman vampire","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["woman","vampire"]}, + {"emoji":"🧛🏻‍♀️","name":"woman vampire: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","vampire:","light","skin","tone"]}, + {"emoji":"🧛🏼‍♀️","name":"woman vampire: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","vampire:","medium-light","skin","tone"]}, + {"emoji":"🧛🏽‍♀️","name":"woman vampire: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","vampire:","medium","skin","tone"]}, + {"emoji":"🧛🏾‍♀️","name":"woman vampire: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","vampire:","medium-dark","skin","tone"]}, + {"emoji":"🧛🏿‍♀️","name":"woman vampire: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","vampire:","dark","skin","tone"]}, + {"emoji":"🧜","name":"merperson","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["merperson","creature","fairytale","folklore","ocean","sea","siren","trident"]}, + {"emoji":"🧜🏻","name":"merperson: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["merperson:","light","skin","tone"]}, + {"emoji":"🧜🏼","name":"merperson: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["merperson:","medium-light","skin","tone"]}, + {"emoji":"🧜🏽","name":"merperson: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["merperson:","medium","skin","tone"]}, + {"emoji":"🧜🏾","name":"merperson: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["merperson:","medium-dark","skin","tone"]}, + {"emoji":"🧜🏿","name":"merperson: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["merperson:","dark","skin","tone"]}, + {"emoji":"🧜‍♂️","name":"merman","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["merman"]}, + {"emoji":"🧜🏻‍♂️","name":"merman: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["merman:","light","skin","tone"]}, + {"emoji":"🧜🏼‍♂️","name":"merman: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["merman:","medium-light","skin","tone"]}, + {"emoji":"🧜🏽‍♂️","name":"merman: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["merman:","medium","skin","tone"]}, + {"emoji":"🧜🏾‍♂️","name":"merman: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["merman:","medium-dark","skin","tone"]}, + {"emoji":"🧜🏿‍♂️","name":"merman: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["merman:","dark","skin","tone"]}, + {"emoji":"🧜‍♀️","name":"mermaid","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["mermaid"]}, + {"emoji":"🧜🏻‍♀️","name":"mermaid: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["mermaid:","light","skin","tone"]}, + {"emoji":"🧜🏼‍♀️","name":"mermaid: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["mermaid:","medium-light","skin","tone"]}, + {"emoji":"🧜🏽‍♀️","name":"mermaid: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["mermaid:","medium","skin","tone"]}, + {"emoji":"🧜🏾‍♀️","name":"mermaid: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["mermaid:","medium-dark","skin","tone"]}, + {"emoji":"🧜🏿‍♀️","name":"mermaid: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["mermaid:","dark","skin","tone"]}, + {"emoji":"🧝","name":"elf","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["elf","elves","enchantment","fantasy","folklore","magic","magical","myth"]}, + {"emoji":"🧝🏻","name":"elf: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["elf:","light","skin","tone"]}, + {"emoji":"🧝🏼","name":"elf: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["elf:","medium-light","skin","tone"]}, + {"emoji":"🧝🏽","name":"elf: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["elf:","medium","skin","tone"]}, + {"emoji":"🧝🏾","name":"elf: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["elf:","medium-dark","skin","tone"]}, + {"emoji":"🧝🏿","name":"elf: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["elf:","dark","skin","tone"]}, + {"emoji":"🧝‍♂️","name":"man elf","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["man","elf"]}, + {"emoji":"🧝🏻‍♂️","name":"man elf: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","elf:","light","skin","tone"]}, + {"emoji":"🧝🏼‍♂️","name":"man elf: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","elf:","medium-light","skin","tone"]}, + {"emoji":"🧝🏽‍♂️","name":"man elf: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","elf:","medium","skin","tone"]}, + {"emoji":"🧝🏾‍♂️","name":"man elf: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","elf:","medium-dark","skin","tone"]}, + {"emoji":"🧝🏿‍♂️","name":"man elf: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","elf:","dark","skin","tone"]}, + {"emoji":"🧝‍♀️","name":"woman elf","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["woman","elf"]}, + {"emoji":"🧝🏻‍♀️","name":"woman elf: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","elf:","light","skin","tone"]}, + {"emoji":"🧝🏼‍♀️","name":"woman elf: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","elf:","medium-light","skin","tone"]}, + {"emoji":"🧝🏽‍♀️","name":"woman elf: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","elf:","medium","skin","tone"]}, + {"emoji":"🧝🏾‍♀️","name":"woman elf: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","elf:","medium-dark","skin","tone"]}, + {"emoji":"🧝🏿‍♀️","name":"woman elf: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","elf:","dark","skin","tone"]}, + {"emoji":"🧞","name":"genie","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["genie","djinn","fantasy","jinn","lamp","myth","rub","wishes"]}, + {"emoji":"🧞‍♂️","name":"man genie","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["man","genie"]}, + {"emoji":"🧞‍♀️","name":"woman genie","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["woman","genie"]}, + {"emoji":"🧟","name":"zombie","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["zombie","apocalypse","dead","halloween","horror","scary","undead","walking"]}, + {"emoji":"🧟‍♂️","name":"man zombie","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["man","zombie"]}, + {"emoji":"🧟‍♀️","name":"woman zombie","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["woman","zombie"]}, + {"emoji":"🧌","name":"troll","category":"Smileys & People","version":"14.0","skinTone":false,"keywords":["troll","fairy","fantasy","monster","tale","trolling"]}, + {"emoji":"🫈","name":"hairy creature","category":"Smileys & People","version":"17.0","skinTone":false,"keywords":["hairy","creature","bigfoot","cryptid","forest","giant","sasquatch","woodwose","yeti"]}, + {"emoji":"💆","name":"person getting massage","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person","getting","massage","face","headache","relax","relaxing","salon","soothe","spa","tension","therapy","treatment"]}, + {"emoji":"💆🏻","name":"person getting massage: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","getting","massage:","light","skin","tone"]}, + {"emoji":"💆🏼","name":"person getting massage: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","getting","massage:","medium-light","skin","tone"]}, + {"emoji":"💆🏽","name":"person getting massage: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","getting","massage:","medium","skin","tone"]}, + {"emoji":"💆🏾","name":"person getting massage: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","getting","massage:","medium-dark","skin","tone"]}, + {"emoji":"💆🏿","name":"person getting massage: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","getting","massage:","dark","skin","tone"]}, + {"emoji":"💆‍♂️","name":"man getting massage","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","getting","massage"]}, + {"emoji":"💆🏻‍♂️","name":"man getting massage: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","getting","massage:","light","skin","tone"]}, + {"emoji":"💆🏼‍♂️","name":"man getting massage: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","getting","massage:","medium-light","skin","tone"]}, + {"emoji":"💆🏽‍♂️","name":"man getting massage: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","getting","massage:","medium","skin","tone"]}, + {"emoji":"💆🏾‍♂️","name":"man getting massage: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","getting","massage:","medium-dark","skin","tone"]}, + {"emoji":"💆🏿‍♂️","name":"man getting massage: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","getting","massage:","dark","skin","tone"]}, + {"emoji":"💆‍♀️","name":"woman getting massage","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","getting","massage"]}, + {"emoji":"💆🏻‍♀️","name":"woman getting massage: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","getting","massage:","light","skin","tone"]}, + {"emoji":"💆🏼‍♀️","name":"woman getting massage: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","getting","massage:","medium-light","skin","tone"]}, + {"emoji":"💆🏽‍♀️","name":"woman getting massage: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","getting","massage:","medium","skin","tone"]}, + {"emoji":"💆🏾‍♀️","name":"woman getting massage: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","getting","massage:","medium-dark","skin","tone"]}, + {"emoji":"💆🏿‍♀️","name":"woman getting massage: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","getting","massage:","dark","skin","tone"]}, + {"emoji":"💇","name":"person getting haircut","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person","getting","haircut","barber","beauty","chop","cosmetology","cut","groom","hair","parlor","shears","style"]}, + {"emoji":"💇🏻","name":"person getting haircut: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","getting","haircut:","light","skin","tone"]}, + {"emoji":"💇🏼","name":"person getting haircut: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","getting","haircut:","medium-light","skin","tone"]}, + {"emoji":"💇🏽","name":"person getting haircut: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","getting","haircut:","medium","skin","tone"]}, + {"emoji":"💇🏾","name":"person getting haircut: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","getting","haircut:","medium-dark","skin","tone"]}, + {"emoji":"💇🏿","name":"person getting haircut: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","getting","haircut:","dark","skin","tone"]}, + {"emoji":"💇‍♂️","name":"man getting haircut","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","getting","haircut"]}, + {"emoji":"💇🏻‍♂️","name":"man getting haircut: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","getting","haircut:","light","skin","tone"]}, + {"emoji":"💇🏼‍♂️","name":"man getting haircut: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","getting","haircut:","medium-light","skin","tone"]}, + {"emoji":"💇🏽‍♂️","name":"man getting haircut: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","getting","haircut:","medium","skin","tone"]}, + {"emoji":"💇🏾‍♂️","name":"man getting haircut: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","getting","haircut:","medium-dark","skin","tone"]}, + {"emoji":"💇🏿‍♂️","name":"man getting haircut: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","getting","haircut:","dark","skin","tone"]}, + {"emoji":"💇‍♀️","name":"woman getting haircut","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","getting","haircut"]}, + {"emoji":"💇🏻‍♀️","name":"woman getting haircut: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","getting","haircut:","light","skin","tone"]}, + {"emoji":"💇🏼‍♀️","name":"woman getting haircut: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","getting","haircut:","medium-light","skin","tone"]}, + {"emoji":"💇🏽‍♀️","name":"woman getting haircut: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","getting","haircut:","medium","skin","tone"]}, + {"emoji":"💇🏾‍♀️","name":"woman getting haircut: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","getting","haircut:","medium-dark","skin","tone"]}, + {"emoji":"💇🏿‍♀️","name":"woman getting haircut: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","getting","haircut:","dark","skin","tone"]}, + {"emoji":"🚶","name":"person walking","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person","walking","amble","gait","hike","man","pace","pedestrian","stride","stroll","walk"]}, + {"emoji":"🚶🏻","name":"person walking: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","walking:","light","skin","tone"]}, + {"emoji":"🚶🏼","name":"person walking: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","walking:","medium-light","skin","tone"]}, + {"emoji":"🚶🏽","name":"person walking: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","walking:","medium","skin","tone"]}, + {"emoji":"🚶🏾","name":"person walking: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","walking:","medium-dark","skin","tone"]}, + {"emoji":"🚶🏿","name":"person walking: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","walking:","dark","skin","tone"]}, + {"emoji":"🚶‍♂️","name":"man walking","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","walking"]}, + {"emoji":"🚶🏻‍♂️","name":"man walking: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","walking:","light","skin","tone"]}, + {"emoji":"🚶🏼‍♂️","name":"man walking: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","walking:","medium-light","skin","tone"]}, + {"emoji":"🚶🏽‍♂️","name":"man walking: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","walking:","medium","skin","tone"]}, + {"emoji":"🚶🏾‍♂️","name":"man walking: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","walking:","medium-dark","skin","tone"]}, + {"emoji":"🚶🏿‍♂️","name":"man walking: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","walking:","dark","skin","tone"]}, + {"emoji":"🚶‍♀️","name":"woman walking","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","walking"]}, + {"emoji":"🚶🏻‍♀️","name":"woman walking: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","walking:","light","skin","tone"]}, + {"emoji":"🚶🏼‍♀️","name":"woman walking: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","walking:","medium-light","skin","tone"]}, + {"emoji":"🚶🏽‍♀️","name":"woman walking: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","walking:","medium","skin","tone"]}, + {"emoji":"🚶🏾‍♀️","name":"woman walking: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","walking:","medium-dark","skin","tone"]}, + {"emoji":"🚶🏿‍♀️","name":"woman walking: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","walking:","dark","skin","tone"]}, + {"emoji":"🚶‍➡️","name":"person walking facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["person","walking","facing","right"]}, + {"emoji":"🚶🏻‍➡️","name":"person walking facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","walking","facing","right:","light","skin","tone"]}, + {"emoji":"🚶🏼‍➡️","name":"person walking facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","walking","facing","right:","medium-light","skin","tone"]}, + {"emoji":"🚶🏽‍➡️","name":"person walking facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","walking","facing","right:","medium","skin","tone"]}, + {"emoji":"🚶🏾‍➡️","name":"person walking facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","walking","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"🚶🏿‍➡️","name":"person walking facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","walking","facing","right:","dark","skin","tone"]}, + {"emoji":"🚶‍♀️‍➡️","name":"woman walking facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["woman","walking","facing","right"]}, + {"emoji":"🚶🏻‍♀️‍➡️","name":"woman walking facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","walking","facing","right:","light","skin","tone"]}, + {"emoji":"🚶🏼‍♀️‍➡️","name":"woman walking facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","walking","facing","right:","medium-light","skin","tone"]}, + {"emoji":"🚶🏽‍♀️‍➡️","name":"woman walking facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","walking","facing","right:","medium","skin","tone"]}, + {"emoji":"🚶🏾‍♀️‍➡️","name":"woman walking facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","walking","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"🚶🏿‍♀️‍➡️","name":"woman walking facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","walking","facing","right:","dark","skin","tone"]}, + {"emoji":"🚶‍♂️‍➡️","name":"man walking facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["man","walking","facing","right"]}, + {"emoji":"🚶🏻‍♂️‍➡️","name":"man walking facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","walking","facing","right:","light","skin","tone"]}, + {"emoji":"🚶🏼‍♂️‍➡️","name":"man walking facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","walking","facing","right:","medium-light","skin","tone"]}, + {"emoji":"🚶🏽‍♂️‍➡️","name":"man walking facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","walking","facing","right:","medium","skin","tone"]}, + {"emoji":"🚶🏾‍♂️‍➡️","name":"man walking facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","walking","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"🚶🏿‍♂️‍➡️","name":"man walking facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","walking","facing","right:","dark","skin","tone"]}, + {"emoji":"🧍","name":"person standing","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["person","standing","stand"]}, + {"emoji":"🧍🏻","name":"person standing: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["person","standing:","light","skin","tone"]}, + {"emoji":"🧍🏼","name":"person standing: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["person","standing:","medium-light","skin","tone"]}, + {"emoji":"🧍🏽","name":"person standing: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["person","standing:","medium","skin","tone"]}, + {"emoji":"🧍🏾","name":"person standing: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["person","standing:","medium-dark","skin","tone"]}, + {"emoji":"🧍🏿","name":"person standing: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["person","standing:","dark","skin","tone"]}, + {"emoji":"🧍‍♂️","name":"man standing","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["man","standing"]}, + {"emoji":"🧍🏻‍♂️","name":"man standing: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","standing:","light","skin","tone"]}, + {"emoji":"🧍🏼‍♂️","name":"man standing: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","standing:","medium-light","skin","tone"]}, + {"emoji":"🧍🏽‍♂️","name":"man standing: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","standing:","medium","skin","tone"]}, + {"emoji":"🧍🏾‍♂️","name":"man standing: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","standing:","medium-dark","skin","tone"]}, + {"emoji":"🧍🏿‍♂️","name":"man standing: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","standing:","dark","skin","tone"]}, + {"emoji":"🧍‍♀️","name":"woman standing","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["woman","standing"]}, + {"emoji":"🧍🏻‍♀️","name":"woman standing: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","standing:","light","skin","tone"]}, + {"emoji":"🧍🏼‍♀️","name":"woman standing: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","standing:","medium-light","skin","tone"]}, + {"emoji":"🧍🏽‍♀️","name":"woman standing: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","standing:","medium","skin","tone"]}, + {"emoji":"🧍🏾‍♀️","name":"woman standing: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","standing:","medium-dark","skin","tone"]}, + {"emoji":"🧍🏿‍♀️","name":"woman standing: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","standing:","dark","skin","tone"]}, + {"emoji":"🧎","name":"person kneeling","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["person","kneeling","kneel","knees"]}, + {"emoji":"🧎🏻","name":"person kneeling: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["person","kneeling:","light","skin","tone"]}, + {"emoji":"🧎🏼","name":"person kneeling: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["person","kneeling:","medium-light","skin","tone"]}, + {"emoji":"🧎🏽","name":"person kneeling: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["person","kneeling:","medium","skin","tone"]}, + {"emoji":"🧎🏾","name":"person kneeling: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["person","kneeling:","medium-dark","skin","tone"]}, + {"emoji":"🧎🏿","name":"person kneeling: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["person","kneeling:","dark","skin","tone"]}, + {"emoji":"🧎‍♂️","name":"man kneeling","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["man","kneeling"]}, + {"emoji":"🧎🏻‍♂️","name":"man kneeling: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","kneeling:","light","skin","tone"]}, + {"emoji":"🧎🏼‍♂️","name":"man kneeling: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","kneeling:","medium-light","skin","tone"]}, + {"emoji":"🧎🏽‍♂️","name":"man kneeling: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","kneeling:","medium","skin","tone"]}, + {"emoji":"🧎🏾‍♂️","name":"man kneeling: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","kneeling:","medium-dark","skin","tone"]}, + {"emoji":"🧎🏿‍♂️","name":"man kneeling: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","kneeling:","dark","skin","tone"]}, + {"emoji":"🧎‍♀️","name":"woman kneeling","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["woman","kneeling"]}, + {"emoji":"🧎🏻‍♀️","name":"woman kneeling: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","kneeling:","light","skin","tone"]}, + {"emoji":"🧎🏼‍♀️","name":"woman kneeling: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","kneeling:","medium-light","skin","tone"]}, + {"emoji":"🧎🏽‍♀️","name":"woman kneeling: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","kneeling:","medium","skin","tone"]}, + {"emoji":"🧎🏾‍♀️","name":"woman kneeling: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","kneeling:","medium-dark","skin","tone"]}, + {"emoji":"🧎🏿‍♀️","name":"woman kneeling: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","kneeling:","dark","skin","tone"]}, + {"emoji":"🧎‍➡️","name":"person kneeling facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["person","kneeling","facing","right"]}, + {"emoji":"🧎🏻‍➡️","name":"person kneeling facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","kneeling","facing","right:","light","skin","tone"]}, + {"emoji":"🧎🏼‍➡️","name":"person kneeling facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","kneeling","facing","right:","medium-light","skin","tone"]}, + {"emoji":"🧎🏽‍➡️","name":"person kneeling facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","kneeling","facing","right:","medium","skin","tone"]}, + {"emoji":"🧎🏾‍➡️","name":"person kneeling facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","kneeling","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"🧎🏿‍➡️","name":"person kneeling facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","kneeling","facing","right:","dark","skin","tone"]}, + {"emoji":"🧎‍♀️‍➡️","name":"woman kneeling facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["woman","kneeling","facing","right"]}, + {"emoji":"🧎🏻‍♀️‍➡️","name":"woman kneeling facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","kneeling","facing","right:","light","skin","tone"]}, + {"emoji":"🧎🏼‍♀️‍➡️","name":"woman kneeling facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","kneeling","facing","right:","medium-light","skin","tone"]}, + {"emoji":"🧎🏽‍♀️‍➡️","name":"woman kneeling facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","kneeling","facing","right:","medium","skin","tone"]}, + {"emoji":"🧎🏾‍♀️‍➡️","name":"woman kneeling facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","kneeling","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"🧎🏿‍♀️‍➡️","name":"woman kneeling facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","kneeling","facing","right:","dark","skin","tone"]}, + {"emoji":"🧎‍♂️‍➡️","name":"man kneeling facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["man","kneeling","facing","right"]}, + {"emoji":"🧎🏻‍♂️‍➡️","name":"man kneeling facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","kneeling","facing","right:","light","skin","tone"]}, + {"emoji":"🧎🏼‍♂️‍➡️","name":"man kneeling facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","kneeling","facing","right:","medium-light","skin","tone"]}, + {"emoji":"🧎🏽‍♂️‍➡️","name":"man kneeling facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","kneeling","facing","right:","medium","skin","tone"]}, + {"emoji":"🧎🏾‍♂️‍➡️","name":"man kneeling facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","kneeling","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"🧎🏿‍♂️‍➡️","name":"man kneeling facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","kneeling","facing","right:","dark","skin","tone"]}, + {"emoji":"🧑‍🦯","name":"person with white cane","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["person","with","white","cane","accessibility","blind","probing"]}, + {"emoji":"🧑🏻‍🦯","name":"person with white cane: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person","with","white","cane:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🦯","name":"person with white cane: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person","with","white","cane:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🦯","name":"person with white cane: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person","with","white","cane:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🦯","name":"person with white cane: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person","with","white","cane:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🦯","name":"person with white cane: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person","with","white","cane:","dark","skin","tone"]}, + {"emoji":"🧑‍🦯‍➡️","name":"person with white cane facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["person","with","white","cane","facing","right"]}, + {"emoji":"🧑🏻‍🦯‍➡️","name":"person with white cane facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","with","white","cane","facing","right:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🦯‍➡️","name":"person with white cane facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","with","white","cane","facing","right:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🦯‍➡️","name":"person with white cane facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","with","white","cane","facing","right:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🦯‍➡️","name":"person with white cane facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","with","white","cane","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🦯‍➡️","name":"person with white cane facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","with","white","cane","facing","right:","dark","skin","tone"]}, + {"emoji":"👨‍🦯","name":"man with white cane","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["man","with","white","cane","accessibility","blind","probing"]}, + {"emoji":"👨🏻‍🦯","name":"man with white cane: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","with","white","cane:","light","skin","tone"]}, + {"emoji":"👨🏼‍🦯","name":"man with white cane: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","with","white","cane:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🦯","name":"man with white cane: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","with","white","cane:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🦯","name":"man with white cane: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","with","white","cane:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🦯","name":"man with white cane: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","with","white","cane:","dark","skin","tone"]}, + {"emoji":"👨‍🦯‍➡️","name":"man with white cane facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["man","with","white","cane","facing","right"]}, + {"emoji":"👨🏻‍🦯‍➡️","name":"man with white cane facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","with","white","cane","facing","right:","light","skin","tone"]}, + {"emoji":"👨🏼‍🦯‍➡️","name":"man with white cane facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","with","white","cane","facing","right:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🦯‍➡️","name":"man with white cane facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","with","white","cane","facing","right:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🦯‍➡️","name":"man with white cane facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","with","white","cane","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🦯‍➡️","name":"man with white cane facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","with","white","cane","facing","right:","dark","skin","tone"]}, + {"emoji":"👩‍🦯","name":"woman with white cane","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["woman","with","white","cane","accessibility","blind","probing"]}, + {"emoji":"👩🏻‍🦯","name":"woman with white cane: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","with","white","cane:","light","skin","tone"]}, + {"emoji":"👩🏼‍🦯","name":"woman with white cane: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","with","white","cane:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🦯","name":"woman with white cane: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","with","white","cane:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🦯","name":"woman with white cane: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","with","white","cane:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🦯","name":"woman with white cane: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","with","white","cane:","dark","skin","tone"]}, + {"emoji":"👩‍🦯‍➡️","name":"woman with white cane facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["woman","with","white","cane","facing","right"]}, + {"emoji":"👩🏻‍🦯‍➡️","name":"woman with white cane facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","with","white","cane","facing","right:","light","skin","tone"]}, + {"emoji":"👩🏼‍🦯‍➡️","name":"woman with white cane facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","with","white","cane","facing","right:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🦯‍➡️","name":"woman with white cane facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","with","white","cane","facing","right:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🦯‍➡️","name":"woman with white cane facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","with","white","cane","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🦯‍➡️","name":"woman with white cane facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","with","white","cane","facing","right:","dark","skin","tone"]}, + {"emoji":"🧑‍🦼","name":"person in motorized wheelchair","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["person","in","motorized","wheelchair","accessibility"]}, + {"emoji":"🧑🏻‍🦼","name":"person in motorized wheelchair: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person","in","motorized","wheelchair:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🦼","name":"person in motorized wheelchair: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person","in","motorized","wheelchair:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🦼","name":"person in motorized wheelchair: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person","in","motorized","wheelchair:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🦼","name":"person in motorized wheelchair: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person","in","motorized","wheelchair:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🦼","name":"person in motorized wheelchair: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person","in","motorized","wheelchair:","dark","skin","tone"]}, + {"emoji":"🧑‍🦼‍➡️","name":"person in motorized wheelchair facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["person","in","motorized","wheelchair","facing","right"]}, + {"emoji":"🧑🏻‍🦼‍➡️","name":"person in motorized wheelchair facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","in","motorized","wheelchair","facing","right:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🦼‍➡️","name":"person in motorized wheelchair facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","in","motorized","wheelchair","facing","right:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🦼‍➡️","name":"person in motorized wheelchair facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","in","motorized","wheelchair","facing","right:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🦼‍➡️","name":"person in motorized wheelchair facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","in","motorized","wheelchair","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🦼‍➡️","name":"person in motorized wheelchair facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","in","motorized","wheelchair","facing","right:","dark","skin","tone"]}, + {"emoji":"👨‍🦼","name":"man in motorized wheelchair","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["man","in","motorized","wheelchair","accessibility"]}, + {"emoji":"👨🏻‍🦼","name":"man in motorized wheelchair: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","in","motorized","wheelchair:","light","skin","tone"]}, + {"emoji":"👨🏼‍🦼","name":"man in motorized wheelchair: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","in","motorized","wheelchair:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🦼","name":"man in motorized wheelchair: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","in","motorized","wheelchair:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🦼","name":"man in motorized wheelchair: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","in","motorized","wheelchair:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🦼","name":"man in motorized wheelchair: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","in","motorized","wheelchair:","dark","skin","tone"]}, + {"emoji":"👨‍🦼‍➡️","name":"man in motorized wheelchair facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["man","in","motorized","wheelchair","facing","right"]}, + {"emoji":"👨🏻‍🦼‍➡️","name":"man in motorized wheelchair facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","in","motorized","wheelchair","facing","right:","light","skin","tone"]}, + {"emoji":"👨🏼‍🦼‍➡️","name":"man in motorized wheelchair facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","in","motorized","wheelchair","facing","right:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🦼‍➡️","name":"man in motorized wheelchair facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","in","motorized","wheelchair","facing","right:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🦼‍➡️","name":"man in motorized wheelchair facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","in","motorized","wheelchair","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🦼‍➡️","name":"man in motorized wheelchair facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","in","motorized","wheelchair","facing","right:","dark","skin","tone"]}, + {"emoji":"👩‍🦼","name":"woman in motorized wheelchair","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["woman","in","motorized","wheelchair","accessibility"]}, + {"emoji":"👩🏻‍🦼","name":"woman in motorized wheelchair: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","in","motorized","wheelchair:","light","skin","tone"]}, + {"emoji":"👩🏼‍🦼","name":"woman in motorized wheelchair: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","in","motorized","wheelchair:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🦼","name":"woman in motorized wheelchair: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","in","motorized","wheelchair:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🦼","name":"woman in motorized wheelchair: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","in","motorized","wheelchair:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🦼","name":"woman in motorized wheelchair: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","in","motorized","wheelchair:","dark","skin","tone"]}, + {"emoji":"👩‍🦼‍➡️","name":"woman in motorized wheelchair facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["woman","in","motorized","wheelchair","facing","right"]}, + {"emoji":"👩🏻‍🦼‍➡️","name":"woman in motorized wheelchair facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","in","motorized","wheelchair","facing","right:","light","skin","tone"]}, + {"emoji":"👩🏼‍🦼‍➡️","name":"woman in motorized wheelchair facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","in","motorized","wheelchair","facing","right:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🦼‍➡️","name":"woman in motorized wheelchair facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","in","motorized","wheelchair","facing","right:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🦼‍➡️","name":"woman in motorized wheelchair facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","in","motorized","wheelchair","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🦼‍➡️","name":"woman in motorized wheelchair facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","in","motorized","wheelchair","facing","right:","dark","skin","tone"]}, + {"emoji":"🧑‍🦽","name":"person in manual wheelchair","category":"Smileys & People","version":"12.1","skinTone":false,"keywords":["person","in","manual","wheelchair","accessibility"]}, + {"emoji":"🧑🏻‍🦽","name":"person in manual wheelchair: light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person","in","manual","wheelchair:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🦽","name":"person in manual wheelchair: medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person","in","manual","wheelchair:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🦽","name":"person in manual wheelchair: medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person","in","manual","wheelchair:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🦽","name":"person in manual wheelchair: medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person","in","manual","wheelchair:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🦽","name":"person in manual wheelchair: dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["person","in","manual","wheelchair:","dark","skin","tone"]}, + {"emoji":"🧑‍🦽‍➡️","name":"person in manual wheelchair facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["person","in","manual","wheelchair","facing","right"]}, + {"emoji":"🧑🏻‍🦽‍➡️","name":"person in manual wheelchair facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","in","manual","wheelchair","facing","right:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🦽‍➡️","name":"person in manual wheelchair facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","in","manual","wheelchair","facing","right:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🦽‍➡️","name":"person in manual wheelchair facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","in","manual","wheelchair","facing","right:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🦽‍➡️","name":"person in manual wheelchair facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","in","manual","wheelchair","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🦽‍➡️","name":"person in manual wheelchair facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","in","manual","wheelchair","facing","right:","dark","skin","tone"]}, + {"emoji":"👨‍🦽","name":"man in manual wheelchair","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["man","in","manual","wheelchair","accessibility"]}, + {"emoji":"👨🏻‍🦽","name":"man in manual wheelchair: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","in","manual","wheelchair:","light","skin","tone"]}, + {"emoji":"👨🏼‍🦽","name":"man in manual wheelchair: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","in","manual","wheelchair:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🦽","name":"man in manual wheelchair: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","in","manual","wheelchair:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🦽","name":"man in manual wheelchair: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","in","manual","wheelchair:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🦽","name":"man in manual wheelchair: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["man","in","manual","wheelchair:","dark","skin","tone"]}, + {"emoji":"👨‍🦽‍➡️","name":"man in manual wheelchair facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["man","in","manual","wheelchair","facing","right"]}, + {"emoji":"👨🏻‍🦽‍➡️","name":"man in manual wheelchair facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","in","manual","wheelchair","facing","right:","light","skin","tone"]}, + {"emoji":"👨🏼‍🦽‍➡️","name":"man in manual wheelchair facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","in","manual","wheelchair","facing","right:","medium-light","skin","tone"]}, + {"emoji":"👨🏽‍🦽‍➡️","name":"man in manual wheelchair facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","in","manual","wheelchair","facing","right:","medium","skin","tone"]}, + {"emoji":"👨🏾‍🦽‍➡️","name":"man in manual wheelchair facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","in","manual","wheelchair","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"👨🏿‍🦽‍➡️","name":"man in manual wheelchair facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","in","manual","wheelchair","facing","right:","dark","skin","tone"]}, + {"emoji":"👩‍🦽","name":"woman in manual wheelchair","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["woman","in","manual","wheelchair","accessibility"]}, + {"emoji":"👩🏻‍🦽","name":"woman in manual wheelchair: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","in","manual","wheelchair:","light","skin","tone"]}, + {"emoji":"👩🏼‍🦽","name":"woman in manual wheelchair: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","in","manual","wheelchair:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🦽","name":"woman in manual wheelchair: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","in","manual","wheelchair:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🦽","name":"woman in manual wheelchair: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","in","manual","wheelchair:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🦽","name":"woman in manual wheelchair: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","in","manual","wheelchair:","dark","skin","tone"]}, + {"emoji":"👩‍🦽‍➡️","name":"woman in manual wheelchair facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["woman","in","manual","wheelchair","facing","right"]}, + {"emoji":"👩🏻‍🦽‍➡️","name":"woman in manual wheelchair facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","in","manual","wheelchair","facing","right:","light","skin","tone"]}, + {"emoji":"👩🏼‍🦽‍➡️","name":"woman in manual wheelchair facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","in","manual","wheelchair","facing","right:","medium-light","skin","tone"]}, + {"emoji":"👩🏽‍🦽‍➡️","name":"woman in manual wheelchair facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","in","manual","wheelchair","facing","right:","medium","skin","tone"]}, + {"emoji":"👩🏾‍🦽‍➡️","name":"woman in manual wheelchair facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","in","manual","wheelchair","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"👩🏿‍🦽‍➡️","name":"woman in manual wheelchair facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","in","manual","wheelchair","facing","right:","dark","skin","tone"]}, + {"emoji":"🏃","name":"person running","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person","running","fast","hurry","marathon","move","quick","race","racing","run","rush","speed"]}, + {"emoji":"🏃🏻","name":"person running: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","running:","light","skin","tone"]}, + {"emoji":"🏃🏼","name":"person running: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","running:","medium-light","skin","tone"]}, + {"emoji":"🏃🏽","name":"person running: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","running:","medium","skin","tone"]}, + {"emoji":"🏃🏾","name":"person running: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","running:","medium-dark","skin","tone"]}, + {"emoji":"🏃🏿","name":"person running: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","running:","dark","skin","tone"]}, + {"emoji":"🏃‍♂️","name":"man running","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","running"]}, + {"emoji":"🏃🏻‍♂️","name":"man running: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","running:","light","skin","tone"]}, + {"emoji":"🏃🏼‍♂️","name":"man running: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","running:","medium-light","skin","tone"]}, + {"emoji":"🏃🏽‍♂️","name":"man running: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","running:","medium","skin","tone"]}, + {"emoji":"🏃🏾‍♂️","name":"man running: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","running:","medium-dark","skin","tone"]}, + {"emoji":"🏃🏿‍♂️","name":"man running: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","running:","dark","skin","tone"]}, + {"emoji":"🏃‍♀️","name":"woman running","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","running"]}, + {"emoji":"🏃🏻‍♀️","name":"woman running: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","running:","light","skin","tone"]}, + {"emoji":"🏃🏼‍♀️","name":"woman running: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","running:","medium-light","skin","tone"]}, + {"emoji":"🏃🏽‍♀️","name":"woman running: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","running:","medium","skin","tone"]}, + {"emoji":"🏃🏾‍♀️","name":"woman running: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","running:","medium-dark","skin","tone"]}, + {"emoji":"🏃🏿‍♀️","name":"woman running: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","running:","dark","skin","tone"]}, + {"emoji":"🏃‍➡️","name":"person running facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["person","running","facing","right"]}, + {"emoji":"🏃🏻‍➡️","name":"person running facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","running","facing","right:","light","skin","tone"]}, + {"emoji":"🏃🏼‍➡️","name":"person running facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","running","facing","right:","medium-light","skin","tone"]}, + {"emoji":"🏃🏽‍➡️","name":"person running facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","running","facing","right:","medium","skin","tone"]}, + {"emoji":"🏃🏾‍➡️","name":"person running facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","running","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"🏃🏿‍➡️","name":"person running facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["person","running","facing","right:","dark","skin","tone"]}, + {"emoji":"🏃‍♀️‍➡️","name":"woman running facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["woman","running","facing","right"]}, + {"emoji":"🏃🏻‍♀️‍➡️","name":"woman running facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","running","facing","right:","light","skin","tone"]}, + {"emoji":"🏃🏼‍♀️‍➡️","name":"woman running facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","running","facing","right:","medium-light","skin","tone"]}, + {"emoji":"🏃🏽‍♀️‍➡️","name":"woman running facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","running","facing","right:","medium","skin","tone"]}, + {"emoji":"🏃🏾‍♀️‍➡️","name":"woman running facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","running","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"🏃🏿‍♀️‍➡️","name":"woman running facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["woman","running","facing","right:","dark","skin","tone"]}, + {"emoji":"🏃‍♂️‍➡️","name":"man running facing right","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["man","running","facing","right"]}, + {"emoji":"🏃🏻‍♂️‍➡️","name":"man running facing right: light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","running","facing","right:","light","skin","tone"]}, + {"emoji":"🏃🏼‍♂️‍➡️","name":"man running facing right: medium-light skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","running","facing","right:","medium-light","skin","tone"]}, + {"emoji":"🏃🏽‍♂️‍➡️","name":"man running facing right: medium skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","running","facing","right:","medium","skin","tone"]}, + {"emoji":"🏃🏾‍♂️‍➡️","name":"man running facing right: medium-dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","running","facing","right:","medium-dark","skin","tone"]}, + {"emoji":"🏃🏿‍♂️‍➡️","name":"man running facing right: dark skin tone","category":"Smileys & People","version":"15.1","skinTone":true,"keywords":["man","running","facing","right:","dark","skin","tone"]}, + {"emoji":"🧑‍🩰","name":"ballet dancer","category":"Smileys & People","version":"17.0","skinTone":false,"keywords":["ballet","dancer"]}, + {"emoji":"🧑🏻‍🩰","name":"ballet dancer: light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["ballet","dancer:","light","skin","tone"]}, + {"emoji":"🧑🏼‍🩰","name":"ballet dancer: medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["ballet","dancer:","medium-light","skin","tone"]}, + {"emoji":"🧑🏽‍🩰","name":"ballet dancer: medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["ballet","dancer:","medium","skin","tone"]}, + {"emoji":"🧑🏾‍🩰","name":"ballet dancer: medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["ballet","dancer:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏿‍🩰","name":"ballet dancer: dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["ballet","dancer:","dark","skin","tone"]}, + {"emoji":"💃","name":"woman dancing","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["woman","dancing","dance","dancer","elegant","festive","flair","flamenco","groove","let’s","salsa","tango"]}, + {"emoji":"💃🏻","name":"woman dancing: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["woman","dancing:","light","skin","tone"]}, + {"emoji":"💃🏼","name":"woman dancing: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["woman","dancing:","medium-light","skin","tone"]}, + {"emoji":"💃🏽","name":"woman dancing: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["woman","dancing:","medium","skin","tone"]}, + {"emoji":"💃🏾","name":"woman dancing: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["woman","dancing:","medium-dark","skin","tone"]}, + {"emoji":"💃🏿","name":"woman dancing: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["woman","dancing:","dark","skin","tone"]}, + {"emoji":"🕺","name":"man dancing","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["man","dancing","dance","dancer","elegant","festive","flair","flamenco","groove","let’s","salsa","tango"]}, + {"emoji":"🕺🏻","name":"man dancing: light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["man","dancing:","light","skin","tone"]}, + {"emoji":"🕺🏼","name":"man dancing: medium-light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["man","dancing:","medium-light","skin","tone"]}, + {"emoji":"🕺🏽","name":"man dancing: medium skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["man","dancing:","medium","skin","tone"]}, + {"emoji":"🕺🏾","name":"man dancing: medium-dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["man","dancing:","medium-dark","skin","tone"]}, + {"emoji":"🕺🏿","name":"man dancing: dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["man","dancing:","dark","skin","tone"]}, + {"emoji":"🕴️","name":"person in suit levitating","category":"Smileys & People","version":"0.7","skinTone":false,"keywords":["person","in","suit","levitating"]}, + {"emoji":"🕴🏻","name":"person in suit levitating: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["person","in","suit","levitating:","light","skin","tone"]}, + {"emoji":"🕴🏼","name":"person in suit levitating: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["person","in","suit","levitating:","medium-light","skin","tone"]}, + {"emoji":"🕴🏽","name":"person in suit levitating: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["person","in","suit","levitating:","medium","skin","tone"]}, + {"emoji":"🕴🏾","name":"person in suit levitating: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["person","in","suit","levitating:","medium-dark","skin","tone"]}, + {"emoji":"🕴🏿","name":"person in suit levitating: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["person","in","suit","levitating:","dark","skin","tone"]}, + {"emoji":"👯","name":"people with bunny ears","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["people","with","bunny","ears","bestie","bff","counterpart","dancer","double","ear","identical","pair","party","partying","soulmate","twin","twinsies"]}, + {"emoji":"👯🏻","name":"people with bunny ears: light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","light","skin","tone"]}, + {"emoji":"👯🏼","name":"people with bunny ears: medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","medium-light","skin","tone"]}, + {"emoji":"👯🏽","name":"people with bunny ears: medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","medium","skin","tone"]}, + {"emoji":"👯🏾","name":"people with bunny ears: medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","medium-dark","skin","tone"]}, + {"emoji":"👯🏿","name":"people with bunny ears: dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","dark","skin","tone"]}, + {"emoji":"👯‍♂️","name":"men with bunny ears","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["men","with","bunny","ears"]}, + {"emoji":"👯🏻‍♂️","name":"men with bunny ears: light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","light","skin","tone"]}, + {"emoji":"👯🏼‍♂️","name":"men with bunny ears: medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","medium-light","skin","tone"]}, + {"emoji":"👯🏽‍♂️","name":"men with bunny ears: medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","medium","skin","tone"]}, + {"emoji":"👯🏾‍♂️","name":"men with bunny ears: medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","medium-dark","skin","tone"]}, + {"emoji":"👯🏿‍♂️","name":"men with bunny ears: dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","dark","skin","tone"]}, + {"emoji":"👯‍♀️","name":"women with bunny ears","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["women","with","bunny","ears"]}, + {"emoji":"👯🏻‍♀️","name":"women with bunny ears: light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","light","skin","tone"]}, + {"emoji":"👯🏼‍♀️","name":"women with bunny ears: medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","medium-light","skin","tone"]}, + {"emoji":"👯🏽‍♀️","name":"women with bunny ears: medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","medium","skin","tone"]}, + {"emoji":"👯🏾‍♀️","name":"women with bunny ears: medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","medium-dark","skin","tone"]}, + {"emoji":"👯🏿‍♀️","name":"women with bunny ears: dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","dark","skin","tone"]}, + {"emoji":"🧑🏻‍🐰‍🧑🏼","name":"people with bunny ears: light skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","light","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏻‍🐰‍🧑🏽","name":"people with bunny ears: light skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","light","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏻‍🐰‍🧑🏾","name":"people with bunny ears: light skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"🧑🏻‍🐰‍🧑🏿","name":"people with bunny ears: light skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","light","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏼‍🐰‍🧑🏻","name":"people with bunny ears: medium-light skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","medium-light","skin","tone,","light","tone"]}, + {"emoji":"🧑🏼‍🐰‍🧑🏽","name":"people with bunny ears: medium-light skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏼‍🐰‍🧑🏾","name":"people with bunny ears: medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"🧑🏼‍🐰‍🧑🏿","name":"people with bunny ears: medium-light skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏽‍🐰‍🧑🏻","name":"people with bunny ears: medium skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","medium","skin","tone,","light","tone"]}, + {"emoji":"🧑🏽‍🐰‍🧑🏼","name":"people with bunny ears: medium skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏽‍🐰‍🧑🏾","name":"people with bunny ears: medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"🧑🏽‍🐰‍🧑🏿","name":"people with bunny ears: medium skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","medium","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏾‍🐰‍🧑🏻","name":"people with bunny ears: medium-dark skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"🧑🏾‍🐰‍🧑🏼","name":"people with bunny ears: medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏾‍🐰‍🧑🏽","name":"people with bunny ears: medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏾‍🐰‍🧑🏿","name":"people with bunny ears: medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏿‍🐰‍🧑🏻","name":"people with bunny ears: dark skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","dark","skin","tone,","light","tone"]}, + {"emoji":"🧑🏿‍🐰‍🧑🏼","name":"people with bunny ears: dark skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏿‍🐰‍🧑🏽","name":"people with bunny ears: dark skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","dark","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏿‍🐰‍🧑🏾","name":"people with bunny ears: dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","with","bunny","ears:","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏻‍🐰‍👨🏼","name":"men with bunny ears: light skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","light","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏻‍🐰‍👨🏽","name":"men with bunny ears: light skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","light","skin","tone,","medium","tone"]}, + {"emoji":"👨🏻‍🐰‍👨🏾","name":"men with bunny ears: light skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏻‍🐰‍👨🏿","name":"men with bunny ears: light skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","light","skin","tone,","dark","tone"]}, + {"emoji":"👨🏼‍🐰‍👨🏻","name":"men with bunny ears: medium-light skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","medium-light","skin","tone,","light","tone"]}, + {"emoji":"👨🏼‍🐰‍👨🏽","name":"men with bunny ears: medium-light skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"👨🏼‍🐰‍👨🏾","name":"men with bunny ears: medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏼‍🐰‍👨🏿","name":"men with bunny ears: medium-light skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"👨🏽‍🐰‍👨🏻","name":"men with bunny ears: medium skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","medium","skin","tone,","light","tone"]}, + {"emoji":"👨🏽‍🐰‍👨🏼","name":"men with bunny ears: medium skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏽‍🐰‍👨🏾","name":"men with bunny ears: medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏽‍🐰‍👨🏿","name":"men with bunny ears: medium skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","medium","skin","tone,","dark","tone"]}, + {"emoji":"👨🏾‍🐰‍👨🏻","name":"men with bunny ears: medium-dark skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"👨🏾‍🐰‍👨🏼","name":"men with bunny ears: medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏾‍🐰‍👨🏽","name":"men with bunny ears: medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"👨🏾‍🐰‍👨🏿","name":"men with bunny ears: medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"👨🏿‍🐰‍👨🏻","name":"men with bunny ears: dark skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","dark","skin","tone,","light","tone"]}, + {"emoji":"👨🏿‍🐰‍👨🏼","name":"men with bunny ears: dark skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏿‍🐰‍👨🏽","name":"men with bunny ears: dark skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","dark","skin","tone,","medium","tone"]}, + {"emoji":"👨🏿‍🐰‍👨🏾","name":"men with bunny ears: dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","with","bunny","ears:","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏻‍🐰‍👩🏼","name":"women with bunny ears: light skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","light","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏻‍🐰‍👩🏽","name":"women with bunny ears: light skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","light","skin","tone,","medium","tone"]}, + {"emoji":"👩🏻‍🐰‍👩🏾","name":"women with bunny ears: light skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏻‍🐰‍👩🏿","name":"women with bunny ears: light skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","light","skin","tone,","dark","tone"]}, + {"emoji":"👩🏼‍🐰‍👩🏻","name":"women with bunny ears: medium-light skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","medium-light","skin","tone,","light","tone"]}, + {"emoji":"👩🏼‍🐰‍👩🏽","name":"women with bunny ears: medium-light skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"👩🏼‍🐰‍👩🏾","name":"women with bunny ears: medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏼‍🐰‍👩🏿","name":"women with bunny ears: medium-light skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"👩🏽‍🐰‍👩🏻","name":"women with bunny ears: medium skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","medium","skin","tone,","light","tone"]}, + {"emoji":"👩🏽‍🐰‍👩🏼","name":"women with bunny ears: medium skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏽‍🐰‍👩🏾","name":"women with bunny ears: medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏽‍🐰‍👩🏿","name":"women with bunny ears: medium skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","medium","skin","tone,","dark","tone"]}, + {"emoji":"👩🏾‍🐰‍👩🏻","name":"women with bunny ears: medium-dark skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"👩🏾‍🐰‍👩🏼","name":"women with bunny ears: medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏾‍🐰‍👩🏽","name":"women with bunny ears: medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"👩🏾‍🐰‍👩🏿","name":"women with bunny ears: medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"👩🏿‍🐰‍👩🏻","name":"women with bunny ears: dark skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","dark","skin","tone,","light","tone"]}, + {"emoji":"👩🏿‍🐰‍👩🏼","name":"women with bunny ears: dark skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏿‍🐰‍👩🏽","name":"women with bunny ears: dark skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","dark","skin","tone,","medium","tone"]}, + {"emoji":"👩🏿‍🐰‍👩🏾","name":"women with bunny ears: dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","with","bunny","ears:","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"🧖","name":"person in steamy room","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["person","in","steamy","room","day","luxurious","pamper","relax","sauna","spa","steam","steambath","unwind"]}, + {"emoji":"🧖🏻","name":"person in steamy room: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person","in","steamy","room:","light","skin","tone"]}, + {"emoji":"🧖🏼","name":"person in steamy room: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person","in","steamy","room:","medium-light","skin","tone"]}, + {"emoji":"🧖🏽","name":"person in steamy room: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person","in","steamy","room:","medium","skin","tone"]}, + {"emoji":"🧖🏾","name":"person in steamy room: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person","in","steamy","room:","medium-dark","skin","tone"]}, + {"emoji":"🧖🏿","name":"person in steamy room: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person","in","steamy","room:","dark","skin","tone"]}, + {"emoji":"🧖‍♂️","name":"man in steamy room","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["man","in","steamy","room"]}, + {"emoji":"🧖🏻‍♂️","name":"man in steamy room: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","in","steamy","room:","light","skin","tone"]}, + {"emoji":"🧖🏼‍♂️","name":"man in steamy room: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","in","steamy","room:","medium-light","skin","tone"]}, + {"emoji":"🧖🏽‍♂️","name":"man in steamy room: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","in","steamy","room:","medium","skin","tone"]}, + {"emoji":"🧖🏾‍♂️","name":"man in steamy room: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","in","steamy","room:","medium-dark","skin","tone"]}, + {"emoji":"🧖🏿‍♂️","name":"man in steamy room: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","in","steamy","room:","dark","skin","tone"]}, + {"emoji":"🧖‍♀️","name":"woman in steamy room","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["woman","in","steamy","room"]}, + {"emoji":"🧖🏻‍♀️","name":"woman in steamy room: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","in","steamy","room:","light","skin","tone"]}, + {"emoji":"🧖🏼‍♀️","name":"woman in steamy room: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","in","steamy","room:","medium-light","skin","tone"]}, + {"emoji":"🧖🏽‍♀️","name":"woman in steamy room: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","in","steamy","room:","medium","skin","tone"]}, + {"emoji":"🧖🏾‍♀️","name":"woman in steamy room: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","in","steamy","room:","medium-dark","skin","tone"]}, + {"emoji":"🧖🏿‍♀️","name":"woman in steamy room: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","in","steamy","room:","dark","skin","tone"]}, + {"emoji":"🧗","name":"person climbing","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["person","climbing","climb","climber","mountain","rock","scale","up"]}, + {"emoji":"🧗🏻","name":"person climbing: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person","climbing:","light","skin","tone"]}, + {"emoji":"🧗🏼","name":"person climbing: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person","climbing:","medium-light","skin","tone"]}, + {"emoji":"🧗🏽","name":"person climbing: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person","climbing:","medium","skin","tone"]}, + {"emoji":"🧗🏾","name":"person climbing: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person","climbing:","medium-dark","skin","tone"]}, + {"emoji":"🧗🏿","name":"person climbing: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person","climbing:","dark","skin","tone"]}, + {"emoji":"🧗‍♂️","name":"man climbing","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["man","climbing"]}, + {"emoji":"🧗🏻‍♂️","name":"man climbing: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","climbing:","light","skin","tone"]}, + {"emoji":"🧗🏼‍♂️","name":"man climbing: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","climbing:","medium-light","skin","tone"]}, + {"emoji":"🧗🏽‍♂️","name":"man climbing: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","climbing:","medium","skin","tone"]}, + {"emoji":"🧗🏾‍♂️","name":"man climbing: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","climbing:","medium-dark","skin","tone"]}, + {"emoji":"🧗🏿‍♂️","name":"man climbing: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","climbing:","dark","skin","tone"]}, + {"emoji":"🧗‍♀️","name":"woman climbing","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["woman","climbing"]}, + {"emoji":"🧗🏻‍♀️","name":"woman climbing: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","climbing:","light","skin","tone"]}, + {"emoji":"🧗🏼‍♀️","name":"woman climbing: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","climbing:","medium-light","skin","tone"]}, + {"emoji":"🧗🏽‍♀️","name":"woman climbing: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","climbing:","medium","skin","tone"]}, + {"emoji":"🧗🏾‍♀️","name":"woman climbing: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","climbing:","medium-dark","skin","tone"]}, + {"emoji":"🧗🏿‍♀️","name":"woman climbing: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","climbing:","dark","skin","tone"]}, + {"emoji":"🤺","name":"person fencing","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["person","fencing","fencer","sword"]}, + {"emoji":"🏇","name":"horse racing","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["horse","racing","jockey","racehorse","riding","sport"]}, + {"emoji":"🏇🏻","name":"horse racing: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["horse","racing:","light","skin","tone"]}, + {"emoji":"🏇🏼","name":"horse racing: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["horse","racing:","medium-light","skin","tone"]}, + {"emoji":"🏇🏽","name":"horse racing: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["horse","racing:","medium","skin","tone"]}, + {"emoji":"🏇🏾","name":"horse racing: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["horse","racing:","medium-dark","skin","tone"]}, + {"emoji":"🏇🏿","name":"horse racing: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["horse","racing:","dark","skin","tone"]}, + {"emoji":"⛷️","name":"skier","category":"Smileys & People","version":"0.7","skinTone":false,"keywords":["skier"]}, + {"emoji":"🏂","name":"snowboarder","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["snowboarder","ski","snow","snowboard","sport"]}, + {"emoji":"🏂🏻","name":"snowboarder: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["snowboarder:","light","skin","tone"]}, + {"emoji":"🏂🏼","name":"snowboarder: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["snowboarder:","medium-light","skin","tone"]}, + {"emoji":"🏂🏽","name":"snowboarder: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["snowboarder:","medium","skin","tone"]}, + {"emoji":"🏂🏾","name":"snowboarder: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["snowboarder:","medium-dark","skin","tone"]}, + {"emoji":"🏂🏿","name":"snowboarder: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["snowboarder:","dark","skin","tone"]}, + {"emoji":"🏌️","name":"person golfing","category":"Smileys & People","version":"0.7","skinTone":false,"keywords":["person","golfing"]}, + {"emoji":"🏌🏻","name":"person golfing: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["person","golfing:","light","skin","tone"]}, + {"emoji":"🏌🏼","name":"person golfing: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["person","golfing:","medium-light","skin","tone"]}, + {"emoji":"🏌🏽","name":"person golfing: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["person","golfing:","medium","skin","tone"]}, + {"emoji":"🏌🏾","name":"person golfing: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["person","golfing:","medium-dark","skin","tone"]}, + {"emoji":"🏌🏿","name":"person golfing: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["person","golfing:","dark","skin","tone"]}, + {"emoji":"🏌️‍♂️","name":"man golfing","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","golfing"]}, + {"emoji":"🏌🏻‍♂️","name":"man golfing: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","golfing:","light","skin","tone"]}, + {"emoji":"🏌🏼‍♂️","name":"man golfing: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","golfing:","medium-light","skin","tone"]}, + {"emoji":"🏌🏽‍♂️","name":"man golfing: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","golfing:","medium","skin","tone"]}, + {"emoji":"🏌🏾‍♂️","name":"man golfing: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","golfing:","medium-dark","skin","tone"]}, + {"emoji":"🏌🏿‍♂️","name":"man golfing: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","golfing:","dark","skin","tone"]}, + {"emoji":"🏌️‍♀️","name":"woman golfing","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","golfing"]}, + {"emoji":"🏌🏻‍♀️","name":"woman golfing: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","golfing:","light","skin","tone"]}, + {"emoji":"🏌🏼‍♀️","name":"woman golfing: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","golfing:","medium-light","skin","tone"]}, + {"emoji":"🏌🏽‍♀️","name":"woman golfing: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","golfing:","medium","skin","tone"]}, + {"emoji":"🏌🏾‍♀️","name":"woman golfing: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","golfing:","medium-dark","skin","tone"]}, + {"emoji":"🏌🏿‍♀️","name":"woman golfing: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","golfing:","dark","skin","tone"]}, + {"emoji":"🏄","name":"person surfing","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person","surfing","beach","ocean","sport","surf","surfer","swell","waves"]}, + {"emoji":"🏄🏻","name":"person surfing: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","surfing:","light","skin","tone"]}, + {"emoji":"🏄🏼","name":"person surfing: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","surfing:","medium-light","skin","tone"]}, + {"emoji":"🏄🏽","name":"person surfing: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","surfing:","medium","skin","tone"]}, + {"emoji":"🏄🏾","name":"person surfing: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","surfing:","medium-dark","skin","tone"]}, + {"emoji":"🏄🏿","name":"person surfing: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","surfing:","dark","skin","tone"]}, + {"emoji":"🏄‍♂️","name":"man surfing","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","surfing"]}, + {"emoji":"🏄🏻‍♂️","name":"man surfing: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","surfing:","light","skin","tone"]}, + {"emoji":"🏄🏼‍♂️","name":"man surfing: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","surfing:","medium-light","skin","tone"]}, + {"emoji":"🏄🏽‍♂️","name":"man surfing: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","surfing:","medium","skin","tone"]}, + {"emoji":"🏄🏾‍♂️","name":"man surfing: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","surfing:","medium-dark","skin","tone"]}, + {"emoji":"🏄🏿‍♂️","name":"man surfing: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","surfing:","dark","skin","tone"]}, + {"emoji":"🏄‍♀️","name":"woman surfing","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","surfing"]}, + {"emoji":"🏄🏻‍♀️","name":"woman surfing: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","surfing:","light","skin","tone"]}, + {"emoji":"🏄🏼‍♀️","name":"woman surfing: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","surfing:","medium-light","skin","tone"]}, + {"emoji":"🏄🏽‍♀️","name":"woman surfing: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","surfing:","medium","skin","tone"]}, + {"emoji":"🏄🏾‍♀️","name":"woman surfing: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","surfing:","medium-dark","skin","tone"]}, + {"emoji":"🏄🏿‍♀️","name":"woman surfing: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","surfing:","dark","skin","tone"]}, + {"emoji":"🚣","name":"person rowing boat","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["person","rowing","boat","canoe","cruise","fishing","lake","oar","paddle","raft","river","row","rowboat"]}, + {"emoji":"🚣🏻","name":"person rowing boat: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","rowing","boat:","light","skin","tone"]}, + {"emoji":"🚣🏼","name":"person rowing boat: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","rowing","boat:","medium-light","skin","tone"]}, + {"emoji":"🚣🏽","name":"person rowing boat: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","rowing","boat:","medium","skin","tone"]}, + {"emoji":"🚣🏾","name":"person rowing boat: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","rowing","boat:","medium-dark","skin","tone"]}, + {"emoji":"🚣🏿","name":"person rowing boat: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","rowing","boat:","dark","skin","tone"]}, + {"emoji":"🚣‍♂️","name":"man rowing boat","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","rowing","boat"]}, + {"emoji":"🚣🏻‍♂️","name":"man rowing boat: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","rowing","boat:","light","skin","tone"]}, + {"emoji":"🚣🏼‍♂️","name":"man rowing boat: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","rowing","boat:","medium-light","skin","tone"]}, + {"emoji":"🚣🏽‍♂️","name":"man rowing boat: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","rowing","boat:","medium","skin","tone"]}, + {"emoji":"🚣🏾‍♂️","name":"man rowing boat: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","rowing","boat:","medium-dark","skin","tone"]}, + {"emoji":"🚣🏿‍♂️","name":"man rowing boat: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","rowing","boat:","dark","skin","tone"]}, + {"emoji":"🚣‍♀️","name":"woman rowing boat","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","rowing","boat"]}, + {"emoji":"🚣🏻‍♀️","name":"woman rowing boat: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","rowing","boat:","light","skin","tone"]}, + {"emoji":"🚣🏼‍♀️","name":"woman rowing boat: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","rowing","boat:","medium-light","skin","tone"]}, + {"emoji":"🚣🏽‍♀️","name":"woman rowing boat: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","rowing","boat:","medium","skin","tone"]}, + {"emoji":"🚣🏾‍♀️","name":"woman rowing boat: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","rowing","boat:","medium-dark","skin","tone"]}, + {"emoji":"🚣🏿‍♀️","name":"woman rowing boat: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","rowing","boat:","dark","skin","tone"]}, + {"emoji":"🏊","name":"person swimming","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person","swimming","freestyle","sport","swim","swimmer","triathlon"]}, + {"emoji":"🏊🏻","name":"person swimming: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","swimming:","light","skin","tone"]}, + {"emoji":"🏊🏼","name":"person swimming: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","swimming:","medium-light","skin","tone"]}, + {"emoji":"🏊🏽","name":"person swimming: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","swimming:","medium","skin","tone"]}, + {"emoji":"🏊🏾","name":"person swimming: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","swimming:","medium-dark","skin","tone"]}, + {"emoji":"🏊🏿","name":"person swimming: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","swimming:","dark","skin","tone"]}, + {"emoji":"🏊‍♂️","name":"man swimming","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","swimming"]}, + {"emoji":"🏊🏻‍♂️","name":"man swimming: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","swimming:","light","skin","tone"]}, + {"emoji":"🏊🏼‍♂️","name":"man swimming: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","swimming:","medium-light","skin","tone"]}, + {"emoji":"🏊🏽‍♂️","name":"man swimming: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","swimming:","medium","skin","tone"]}, + {"emoji":"🏊🏾‍♂️","name":"man swimming: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","swimming:","medium-dark","skin","tone"]}, + {"emoji":"🏊🏿‍♂️","name":"man swimming: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","swimming:","dark","skin","tone"]}, + {"emoji":"🏊‍♀️","name":"woman swimming","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","swimming"]}, + {"emoji":"🏊🏻‍♀️","name":"woman swimming: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","swimming:","light","skin","tone"]}, + {"emoji":"🏊🏼‍♀️","name":"woman swimming: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","swimming:","medium-light","skin","tone"]}, + {"emoji":"🏊🏽‍♀️","name":"woman swimming: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","swimming:","medium","skin","tone"]}, + {"emoji":"🏊🏾‍♀️","name":"woman swimming: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","swimming:","medium-dark","skin","tone"]}, + {"emoji":"🏊🏿‍♀️","name":"woman swimming: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","swimming:","dark","skin","tone"]}, + {"emoji":"⛹️","name":"person bouncing ball","category":"Smileys & People","version":"0.7","skinTone":false,"keywords":["person","bouncing","ball"]}, + {"emoji":"⛹🏻","name":"person bouncing ball: light skin tone","category":"Smileys & People","version":"2.0","skinTone":true,"keywords":["person","bouncing","ball:","light","skin","tone"]}, + {"emoji":"⛹🏼","name":"person bouncing ball: medium-light skin tone","category":"Smileys & People","version":"2.0","skinTone":true,"keywords":["person","bouncing","ball:","medium-light","skin","tone"]}, + {"emoji":"⛹🏽","name":"person bouncing ball: medium skin tone","category":"Smileys & People","version":"2.0","skinTone":true,"keywords":["person","bouncing","ball:","medium","skin","tone"]}, + {"emoji":"⛹🏾","name":"person bouncing ball: medium-dark skin tone","category":"Smileys & People","version":"2.0","skinTone":true,"keywords":["person","bouncing","ball:","medium-dark","skin","tone"]}, + {"emoji":"⛹🏿","name":"person bouncing ball: dark skin tone","category":"Smileys & People","version":"2.0","skinTone":true,"keywords":["person","bouncing","ball:","dark","skin","tone"]}, + {"emoji":"⛹️‍♂️","name":"man bouncing ball","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","bouncing","ball"]}, + {"emoji":"⛹🏻‍♂️","name":"man bouncing ball: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","bouncing","ball:","light","skin","tone"]}, + {"emoji":"⛹🏼‍♂️","name":"man bouncing ball: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","bouncing","ball:","medium-light","skin","tone"]}, + {"emoji":"⛹🏽‍♂️","name":"man bouncing ball: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","bouncing","ball:","medium","skin","tone"]}, + {"emoji":"⛹🏾‍♂️","name":"man bouncing ball: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","bouncing","ball:","medium-dark","skin","tone"]}, + {"emoji":"⛹🏿‍♂️","name":"man bouncing ball: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","bouncing","ball:","dark","skin","tone"]}, + {"emoji":"⛹️‍♀️","name":"woman bouncing ball","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","bouncing","ball"]}, + {"emoji":"⛹🏻‍♀️","name":"woman bouncing ball: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","bouncing","ball:","light","skin","tone"]}, + {"emoji":"⛹🏼‍♀️","name":"woman bouncing ball: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","bouncing","ball:","medium-light","skin","tone"]}, + {"emoji":"⛹🏽‍♀️","name":"woman bouncing ball: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","bouncing","ball:","medium","skin","tone"]}, + {"emoji":"⛹🏾‍♀️","name":"woman bouncing ball: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","bouncing","ball:","medium-dark","skin","tone"]}, + {"emoji":"⛹🏿‍♀️","name":"woman bouncing ball: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","bouncing","ball:","dark","skin","tone"]}, + {"emoji":"🏋️","name":"person lifting weights","category":"Smileys & People","version":"0.7","skinTone":false,"keywords":["person","lifting","weights"]}, + {"emoji":"🏋🏻","name":"person lifting weights: light skin tone","category":"Smileys & People","version":"2.0","skinTone":true,"keywords":["person","lifting","weights:","light","skin","tone"]}, + {"emoji":"🏋🏼","name":"person lifting weights: medium-light skin tone","category":"Smileys & People","version":"2.0","skinTone":true,"keywords":["person","lifting","weights:","medium-light","skin","tone"]}, + {"emoji":"🏋🏽","name":"person lifting weights: medium skin tone","category":"Smileys & People","version":"2.0","skinTone":true,"keywords":["person","lifting","weights:","medium","skin","tone"]}, + {"emoji":"🏋🏾","name":"person lifting weights: medium-dark skin tone","category":"Smileys & People","version":"2.0","skinTone":true,"keywords":["person","lifting","weights:","medium-dark","skin","tone"]}, + {"emoji":"🏋🏿","name":"person lifting weights: dark skin tone","category":"Smileys & People","version":"2.0","skinTone":true,"keywords":["person","lifting","weights:","dark","skin","tone"]}, + {"emoji":"🏋️‍♂️","name":"man lifting weights","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","lifting","weights"]}, + {"emoji":"🏋🏻‍♂️","name":"man lifting weights: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","lifting","weights:","light","skin","tone"]}, + {"emoji":"🏋🏼‍♂️","name":"man lifting weights: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","lifting","weights:","medium-light","skin","tone"]}, + {"emoji":"🏋🏽‍♂️","name":"man lifting weights: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","lifting","weights:","medium","skin","tone"]}, + {"emoji":"🏋🏾‍♂️","name":"man lifting weights: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","lifting","weights:","medium-dark","skin","tone"]}, + {"emoji":"🏋🏿‍♂️","name":"man lifting weights: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","lifting","weights:","dark","skin","tone"]}, + {"emoji":"🏋️‍♀️","name":"woman lifting weights","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","lifting","weights"]}, + {"emoji":"🏋🏻‍♀️","name":"woman lifting weights: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","lifting","weights:","light","skin","tone"]}, + {"emoji":"🏋🏼‍♀️","name":"woman lifting weights: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","lifting","weights:","medium-light","skin","tone"]}, + {"emoji":"🏋🏽‍♀️","name":"woman lifting weights: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","lifting","weights:","medium","skin","tone"]}, + {"emoji":"🏋🏾‍♀️","name":"woman lifting weights: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","lifting","weights:","medium-dark","skin","tone"]}, + {"emoji":"🏋🏿‍♀️","name":"woman lifting weights: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","lifting","weights:","dark","skin","tone"]}, + {"emoji":"🚴","name":"person biking","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["person","biking","bicycle","bicyclist","bike","cycle","cyclist","riding","sport"]}, + {"emoji":"🚴🏻","name":"person biking: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","biking:","light","skin","tone"]}, + {"emoji":"🚴🏼","name":"person biking: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","biking:","medium-light","skin","tone"]}, + {"emoji":"🚴🏽","name":"person biking: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","biking:","medium","skin","tone"]}, + {"emoji":"🚴🏾","name":"person biking: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","biking:","medium-dark","skin","tone"]}, + {"emoji":"🚴🏿","name":"person biking: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","biking:","dark","skin","tone"]}, + {"emoji":"🚴‍♂️","name":"man biking","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","biking"]}, + {"emoji":"🚴🏻‍♂️","name":"man biking: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","biking:","light","skin","tone"]}, + {"emoji":"🚴🏼‍♂️","name":"man biking: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","biking:","medium-light","skin","tone"]}, + {"emoji":"🚴🏽‍♂️","name":"man biking: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","biking:","medium","skin","tone"]}, + {"emoji":"🚴🏾‍♂️","name":"man biking: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","biking:","medium-dark","skin","tone"]}, + {"emoji":"🚴🏿‍♂️","name":"man biking: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","biking:","dark","skin","tone"]}, + {"emoji":"🚴‍♀️","name":"woman biking","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","biking"]}, + {"emoji":"🚴🏻‍♀️","name":"woman biking: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","biking:","light","skin","tone"]}, + {"emoji":"🚴🏼‍♀️","name":"woman biking: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","biking:","medium-light","skin","tone"]}, + {"emoji":"🚴🏽‍♀️","name":"woman biking: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","biking:","medium","skin","tone"]}, + {"emoji":"🚴🏾‍♀️","name":"woman biking: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","biking:","medium-dark","skin","tone"]}, + {"emoji":"🚴🏿‍♀️","name":"woman biking: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","biking:","dark","skin","tone"]}, + {"emoji":"🚵","name":"person mountain biking","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["person","mountain","biking","bicycle","bicyclist","bike","cycle","cyclist","riding","sport"]}, + {"emoji":"🚵🏻","name":"person mountain biking: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","mountain","biking:","light","skin","tone"]}, + {"emoji":"🚵🏼","name":"person mountain biking: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","mountain","biking:","medium-light","skin","tone"]}, + {"emoji":"🚵🏽","name":"person mountain biking: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","mountain","biking:","medium","skin","tone"]}, + {"emoji":"🚵🏾","name":"person mountain biking: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","mountain","biking:","medium-dark","skin","tone"]}, + {"emoji":"🚵🏿","name":"person mountain biking: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","mountain","biking:","dark","skin","tone"]}, + {"emoji":"🚵‍♂️","name":"man mountain biking","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","mountain","biking"]}, + {"emoji":"🚵🏻‍♂️","name":"man mountain biking: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","mountain","biking:","light","skin","tone"]}, + {"emoji":"🚵🏼‍♂️","name":"man mountain biking: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","mountain","biking:","medium-light","skin","tone"]}, + {"emoji":"🚵🏽‍♂️","name":"man mountain biking: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","mountain","biking:","medium","skin","tone"]}, + {"emoji":"🚵🏾‍♂️","name":"man mountain biking: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","mountain","biking:","medium-dark","skin","tone"]}, + {"emoji":"🚵🏿‍♂️","name":"man mountain biking: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","mountain","biking:","dark","skin","tone"]}, + {"emoji":"🚵‍♀️","name":"woman mountain biking","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","mountain","biking"]}, + {"emoji":"🚵🏻‍♀️","name":"woman mountain biking: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","mountain","biking:","light","skin","tone"]}, + {"emoji":"🚵🏼‍♀️","name":"woman mountain biking: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","mountain","biking:","medium-light","skin","tone"]}, + {"emoji":"🚵🏽‍♀️","name":"woman mountain biking: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","mountain","biking:","medium","skin","tone"]}, + {"emoji":"🚵🏾‍♀️","name":"woman mountain biking: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","mountain","biking:","medium-dark","skin","tone"]}, + {"emoji":"🚵🏿‍♀️","name":"woman mountain biking: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","mountain","biking:","dark","skin","tone"]}, + {"emoji":"🤸","name":"person cartwheeling","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["person","cartwheeling","active","cartwheel","excited","flip","gymnastics","happy","somersault"]}, + {"emoji":"🤸🏻","name":"person cartwheeling: light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","cartwheeling:","light","skin","tone"]}, + {"emoji":"🤸🏼","name":"person cartwheeling: medium-light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","cartwheeling:","medium-light","skin","tone"]}, + {"emoji":"🤸🏽","name":"person cartwheeling: medium skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","cartwheeling:","medium","skin","tone"]}, + {"emoji":"🤸🏾","name":"person cartwheeling: medium-dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","cartwheeling:","medium-dark","skin","tone"]}, + {"emoji":"🤸🏿","name":"person cartwheeling: dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","cartwheeling:","dark","skin","tone"]}, + {"emoji":"🤸‍♂️","name":"man cartwheeling","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","cartwheeling"]}, + {"emoji":"🤸🏻‍♂️","name":"man cartwheeling: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","cartwheeling:","light","skin","tone"]}, + {"emoji":"🤸🏼‍♂️","name":"man cartwheeling: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","cartwheeling:","medium-light","skin","tone"]}, + {"emoji":"🤸🏽‍♂️","name":"man cartwheeling: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","cartwheeling:","medium","skin","tone"]}, + {"emoji":"🤸🏾‍♂️","name":"man cartwheeling: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","cartwheeling:","medium-dark","skin","tone"]}, + {"emoji":"🤸🏿‍♂️","name":"man cartwheeling: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","cartwheeling:","dark","skin","tone"]}, + {"emoji":"🤸‍♀️","name":"woman cartwheeling","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","cartwheeling"]}, + {"emoji":"🤸🏻‍♀️","name":"woman cartwheeling: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","cartwheeling:","light","skin","tone"]}, + {"emoji":"🤸🏼‍♀️","name":"woman cartwheeling: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","cartwheeling:","medium-light","skin","tone"]}, + {"emoji":"🤸🏽‍♀️","name":"woman cartwheeling: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","cartwheeling:","medium","skin","tone"]}, + {"emoji":"🤸🏾‍♀️","name":"woman cartwheeling: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","cartwheeling:","medium-dark","skin","tone"]}, + {"emoji":"🤸🏿‍♀️","name":"woman cartwheeling: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","cartwheeling:","dark","skin","tone"]}, + {"emoji":"🤼","name":"people wrestling","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["people","wrestling","combat","duel","grapple","ring","tournament","wrestle"]}, + {"emoji":"🤼🏻","name":"people wrestling: light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","light","skin","tone"]}, + {"emoji":"🤼🏼","name":"people wrestling: medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","medium-light","skin","tone"]}, + {"emoji":"🤼🏽","name":"people wrestling: medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","medium","skin","tone"]}, + {"emoji":"🤼🏾","name":"people wrestling: medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","medium-dark","skin","tone"]}, + {"emoji":"🤼🏿","name":"people wrestling: dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","dark","skin","tone"]}, + {"emoji":"🤼‍♂️","name":"men wrestling","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["men","wrestling"]}, + {"emoji":"🤼🏻‍♂️","name":"men wrestling: light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","light","skin","tone"]}, + {"emoji":"🤼🏼‍♂️","name":"men wrestling: medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","medium-light","skin","tone"]}, + {"emoji":"🤼🏽‍♂️","name":"men wrestling: medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","medium","skin","tone"]}, + {"emoji":"🤼🏾‍♂️","name":"men wrestling: medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","medium-dark","skin","tone"]}, + {"emoji":"🤼🏿‍♂️","name":"men wrestling: dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","dark","skin","tone"]}, + {"emoji":"🤼‍♀️","name":"women wrestling","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["women","wrestling"]}, + {"emoji":"🤼🏻‍♀️","name":"women wrestling: light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","light","skin","tone"]}, + {"emoji":"🤼🏼‍♀️","name":"women wrestling: medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","medium-light","skin","tone"]}, + {"emoji":"🤼🏽‍♀️","name":"women wrestling: medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","medium","skin","tone"]}, + {"emoji":"🤼🏾‍♀️","name":"women wrestling: medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","medium-dark","skin","tone"]}, + {"emoji":"🤼🏿‍♀️","name":"women wrestling: dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","dark","skin","tone"]}, + {"emoji":"🧑🏻‍🫯‍🧑🏼","name":"people wrestling: light skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","light","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏻‍🫯‍🧑🏽","name":"people wrestling: light skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","light","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏻‍🫯‍🧑🏾","name":"people wrestling: light skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"🧑🏻‍🫯‍🧑🏿","name":"people wrestling: light skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","light","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏼‍🫯‍🧑🏻","name":"people wrestling: medium-light skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","medium-light","skin","tone,","light","tone"]}, + {"emoji":"🧑🏼‍🫯‍🧑🏽","name":"people wrestling: medium-light skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏼‍🫯‍🧑🏾","name":"people wrestling: medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"🧑🏼‍🫯‍🧑🏿","name":"people wrestling: medium-light skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏽‍🫯‍🧑🏻","name":"people wrestling: medium skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","medium","skin","tone,","light","tone"]}, + {"emoji":"🧑🏽‍🫯‍🧑🏼","name":"people wrestling: medium skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏽‍🫯‍🧑🏾","name":"people wrestling: medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"🧑🏽‍🫯‍🧑🏿","name":"people wrestling: medium skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","medium","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏾‍🫯‍🧑🏻","name":"people wrestling: medium-dark skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"🧑🏾‍🫯‍🧑🏼","name":"people wrestling: medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏾‍🫯‍🧑🏽","name":"people wrestling: medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏾‍🫯‍🧑🏿","name":"people wrestling: medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏿‍🫯‍🧑🏻","name":"people wrestling: dark skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","dark","skin","tone,","light","tone"]}, + {"emoji":"🧑🏿‍🫯‍🧑🏼","name":"people wrestling: dark skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏿‍🫯‍🧑🏽","name":"people wrestling: dark skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","dark","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏿‍🫯‍🧑🏾","name":"people wrestling: dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["people","wrestling:","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏻‍🫯‍👨🏼","name":"men wrestling: light skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","light","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏻‍🫯‍👨🏽","name":"men wrestling: light skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","light","skin","tone,","medium","tone"]}, + {"emoji":"👨🏻‍🫯‍👨🏾","name":"men wrestling: light skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏻‍🫯‍👨🏿","name":"men wrestling: light skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","light","skin","tone,","dark","tone"]}, + {"emoji":"👨🏼‍🫯‍👨🏻","name":"men wrestling: medium-light skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","medium-light","skin","tone,","light","tone"]}, + {"emoji":"👨🏼‍🫯‍👨🏽","name":"men wrestling: medium-light skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"👨🏼‍🫯‍👨🏾","name":"men wrestling: medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏼‍🫯‍👨🏿","name":"men wrestling: medium-light skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"👨🏽‍🫯‍👨🏻","name":"men wrestling: medium skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","medium","skin","tone,","light","tone"]}, + {"emoji":"👨🏽‍🫯‍👨🏼","name":"men wrestling: medium skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏽‍🫯‍👨🏾","name":"men wrestling: medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏽‍🫯‍👨🏿","name":"men wrestling: medium skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","medium","skin","tone,","dark","tone"]}, + {"emoji":"👨🏾‍🫯‍👨🏻","name":"men wrestling: medium-dark skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"👨🏾‍🫯‍👨🏼","name":"men wrestling: medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏾‍🫯‍👨🏽","name":"men wrestling: medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"👨🏾‍🫯‍👨🏿","name":"men wrestling: medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"👨🏿‍🫯‍👨🏻","name":"men wrestling: dark skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","dark","skin","tone,","light","tone"]}, + {"emoji":"👨🏿‍🫯‍👨🏼","name":"men wrestling: dark skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏿‍🫯‍👨🏽","name":"men wrestling: dark skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","dark","skin","tone,","medium","tone"]}, + {"emoji":"👨🏿‍🫯‍👨🏾","name":"men wrestling: dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["men","wrestling:","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏻‍🫯‍👩🏼","name":"women wrestling: light skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","light","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏻‍🫯‍👩🏽","name":"women wrestling: light skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","light","skin","tone,","medium","tone"]}, + {"emoji":"👩🏻‍🫯‍👩🏾","name":"women wrestling: light skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏻‍🫯‍👩🏿","name":"women wrestling: light skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","light","skin","tone,","dark","tone"]}, + {"emoji":"👩🏼‍🫯‍👩🏻","name":"women wrestling: medium-light skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","medium-light","skin","tone,","light","tone"]}, + {"emoji":"👩🏼‍🫯‍👩🏽","name":"women wrestling: medium-light skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"👩🏼‍🫯‍👩🏾","name":"women wrestling: medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏼‍🫯‍👩🏿","name":"women wrestling: medium-light skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"👩🏽‍🫯‍👩🏻","name":"women wrestling: medium skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","medium","skin","tone,","light","tone"]}, + {"emoji":"👩🏽‍🫯‍👩🏼","name":"women wrestling: medium skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏽‍🫯‍👩🏾","name":"women wrestling: medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏽‍🫯‍👩🏿","name":"women wrestling: medium skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","medium","skin","tone,","dark","tone"]}, + {"emoji":"👩🏾‍🫯‍👩🏻","name":"women wrestling: medium-dark skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"👩🏾‍🫯‍👩🏼","name":"women wrestling: medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏾‍🫯‍👩🏽","name":"women wrestling: medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"👩🏾‍🫯‍👩🏿","name":"women wrestling: medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"👩🏿‍🫯‍👩🏻","name":"women wrestling: dark skin tone, light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","dark","skin","tone,","light","tone"]}, + {"emoji":"👩🏿‍🫯‍👩🏼","name":"women wrestling: dark skin tone, medium-light skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏿‍🫯‍👩🏽","name":"women wrestling: dark skin tone, medium skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","dark","skin","tone,","medium","tone"]}, + {"emoji":"👩🏿‍🫯‍👩🏾","name":"women wrestling: dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"17.0","skinTone":true,"keywords":["women","wrestling:","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"🤽","name":"person playing water polo","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["person","playing","water","polo","sport","swimming","waterpolo"]}, + {"emoji":"🤽🏻","name":"person playing water polo: light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","playing","water","polo:","light","skin","tone"]}, + {"emoji":"🤽🏼","name":"person playing water polo: medium-light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","playing","water","polo:","medium-light","skin","tone"]}, + {"emoji":"🤽🏽","name":"person playing water polo: medium skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","playing","water","polo:","medium","skin","tone"]}, + {"emoji":"🤽🏾","name":"person playing water polo: medium-dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","playing","water","polo:","medium-dark","skin","tone"]}, + {"emoji":"🤽🏿","name":"person playing water polo: dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","playing","water","polo:","dark","skin","tone"]}, + {"emoji":"🤽‍♂️","name":"man playing water polo","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","playing","water","polo"]}, + {"emoji":"🤽🏻‍♂️","name":"man playing water polo: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","playing","water","polo:","light","skin","tone"]}, + {"emoji":"🤽🏼‍♂️","name":"man playing water polo: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","playing","water","polo:","medium-light","skin","tone"]}, + {"emoji":"🤽🏽‍♂️","name":"man playing water polo: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","playing","water","polo:","medium","skin","tone"]}, + {"emoji":"🤽🏾‍♂️","name":"man playing water polo: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","playing","water","polo:","medium-dark","skin","tone"]}, + {"emoji":"🤽🏿‍♂️","name":"man playing water polo: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","playing","water","polo:","dark","skin","tone"]}, + {"emoji":"🤽‍♀️","name":"woman playing water polo","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","playing","water","polo"]}, + {"emoji":"🤽🏻‍♀️","name":"woman playing water polo: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","playing","water","polo:","light","skin","tone"]}, + {"emoji":"🤽🏼‍♀️","name":"woman playing water polo: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","playing","water","polo:","medium-light","skin","tone"]}, + {"emoji":"🤽🏽‍♀️","name":"woman playing water polo: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","playing","water","polo:","medium","skin","tone"]}, + {"emoji":"🤽🏾‍♀️","name":"woman playing water polo: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","playing","water","polo:","medium-dark","skin","tone"]}, + {"emoji":"🤽🏿‍♀️","name":"woman playing water polo: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","playing","water","polo:","dark","skin","tone"]}, + {"emoji":"🤾","name":"person playing handball","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["person","playing","handball","athletics","ball","catch","chuck","hurl","lob","pitch","sport","throw","toss"]}, + {"emoji":"🤾🏻","name":"person playing handball: light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","playing","handball:","light","skin","tone"]}, + {"emoji":"🤾🏼","name":"person playing handball: medium-light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","playing","handball:","medium-light","skin","tone"]}, + {"emoji":"🤾🏽","name":"person playing handball: medium skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","playing","handball:","medium","skin","tone"]}, + {"emoji":"🤾🏾","name":"person playing handball: medium-dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","playing","handball:","medium-dark","skin","tone"]}, + {"emoji":"🤾🏿","name":"person playing handball: dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","playing","handball:","dark","skin","tone"]}, + {"emoji":"🤾‍♂️","name":"man playing handball","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","playing","handball"]}, + {"emoji":"🤾🏻‍♂️","name":"man playing handball: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","playing","handball:","light","skin","tone"]}, + {"emoji":"🤾🏼‍♂️","name":"man playing handball: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","playing","handball:","medium-light","skin","tone"]}, + {"emoji":"🤾🏽‍♂️","name":"man playing handball: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","playing","handball:","medium","skin","tone"]}, + {"emoji":"🤾🏾‍♂️","name":"man playing handball: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","playing","handball:","medium-dark","skin","tone"]}, + {"emoji":"🤾🏿‍♂️","name":"man playing handball: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","playing","handball:","dark","skin","tone"]}, + {"emoji":"🤾‍♀️","name":"woman playing handball","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","playing","handball"]}, + {"emoji":"🤾🏻‍♀️","name":"woman playing handball: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","playing","handball:","light","skin","tone"]}, + {"emoji":"🤾🏼‍♀️","name":"woman playing handball: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","playing","handball:","medium-light","skin","tone"]}, + {"emoji":"🤾🏽‍♀️","name":"woman playing handball: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","playing","handball:","medium","skin","tone"]}, + {"emoji":"🤾🏾‍♀️","name":"woman playing handball: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","playing","handball:","medium-dark","skin","tone"]}, + {"emoji":"🤾🏿‍♀️","name":"woman playing handball: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","playing","handball:","dark","skin","tone"]}, + {"emoji":"🤹","name":"person juggling","category":"Smileys & People","version":"3.0","skinTone":false,"keywords":["person","juggling","act","balance","balancing","handle","juggle","manage","multitask","skill"]}, + {"emoji":"🤹🏻","name":"person juggling: light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","juggling:","light","skin","tone"]}, + {"emoji":"🤹🏼","name":"person juggling: medium-light skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","juggling:","medium-light","skin","tone"]}, + {"emoji":"🤹🏽","name":"person juggling: medium skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","juggling:","medium","skin","tone"]}, + {"emoji":"🤹🏾","name":"person juggling: medium-dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","juggling:","medium-dark","skin","tone"]}, + {"emoji":"🤹🏿","name":"person juggling: dark skin tone","category":"Smileys & People","version":"3.0","skinTone":true,"keywords":["person","juggling:","dark","skin","tone"]}, + {"emoji":"🤹‍♂️","name":"man juggling","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["man","juggling"]}, + {"emoji":"🤹🏻‍♂️","name":"man juggling: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","juggling:","light","skin","tone"]}, + {"emoji":"🤹🏼‍♂️","name":"man juggling: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","juggling:","medium-light","skin","tone"]}, + {"emoji":"🤹🏽‍♂️","name":"man juggling: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","juggling:","medium","skin","tone"]}, + {"emoji":"🤹🏾‍♂️","name":"man juggling: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","juggling:","medium-dark","skin","tone"]}, + {"emoji":"🤹🏿‍♂️","name":"man juggling: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["man","juggling:","dark","skin","tone"]}, + {"emoji":"🤹‍♀️","name":"woman juggling","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["woman","juggling"]}, + {"emoji":"🤹🏻‍♀️","name":"woman juggling: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","juggling:","light","skin","tone"]}, + {"emoji":"🤹🏼‍♀️","name":"woman juggling: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","juggling:","medium-light","skin","tone"]}, + {"emoji":"🤹🏽‍♀️","name":"woman juggling: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","juggling:","medium","skin","tone"]}, + {"emoji":"🤹🏾‍♀️","name":"woman juggling: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","juggling:","medium-dark","skin","tone"]}, + {"emoji":"🤹🏿‍♀️","name":"woman juggling: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["woman","juggling:","dark","skin","tone"]}, + {"emoji":"🧘","name":"person in lotus position","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["person","in","lotus","position","cross","legged","legs","meditation","peace","relax","serenity","yoga","yogi","zen"]}, + {"emoji":"🧘🏻","name":"person in lotus position: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person","in","lotus","position:","light","skin","tone"]}, + {"emoji":"🧘🏼","name":"person in lotus position: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person","in","lotus","position:","medium-light","skin","tone"]}, + {"emoji":"🧘🏽","name":"person in lotus position: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person","in","lotus","position:","medium","skin","tone"]}, + {"emoji":"🧘🏾","name":"person in lotus position: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person","in","lotus","position:","medium-dark","skin","tone"]}, + {"emoji":"🧘🏿","name":"person in lotus position: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["person","in","lotus","position:","dark","skin","tone"]}, + {"emoji":"🧘‍♂️","name":"man in lotus position","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["man","in","lotus","position"]}, + {"emoji":"🧘🏻‍♂️","name":"man in lotus position: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","in","lotus","position:","light","skin","tone"]}, + {"emoji":"🧘🏼‍♂️","name":"man in lotus position: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","in","lotus","position:","medium-light","skin","tone"]}, + {"emoji":"🧘🏽‍♂️","name":"man in lotus position: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","in","lotus","position:","medium","skin","tone"]}, + {"emoji":"🧘🏾‍♂️","name":"man in lotus position: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","in","lotus","position:","medium-dark","skin","tone"]}, + {"emoji":"🧘🏿‍♂️","name":"man in lotus position: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["man","in","lotus","position:","dark","skin","tone"]}, + {"emoji":"🧘‍♀️","name":"woman in lotus position","category":"Smileys & People","version":"5.0","skinTone":false,"keywords":["woman","in","lotus","position"]}, + {"emoji":"🧘🏻‍♀️","name":"woman in lotus position: light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","in","lotus","position:","light","skin","tone"]}, + {"emoji":"🧘🏼‍♀️","name":"woman in lotus position: medium-light skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","in","lotus","position:","medium-light","skin","tone"]}, + {"emoji":"🧘🏽‍♀️","name":"woman in lotus position: medium skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","in","lotus","position:","medium","skin","tone"]}, + {"emoji":"🧘🏾‍♀️","name":"woman in lotus position: medium-dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","in","lotus","position:","medium-dark","skin","tone"]}, + {"emoji":"🧘🏿‍♀️","name":"woman in lotus position: dark skin tone","category":"Smileys & People","version":"5.0","skinTone":true,"keywords":["woman","in","lotus","position:","dark","skin","tone"]}, + {"emoji":"🛀","name":"person taking bath","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["person","taking","bath","bathtub","tub"]}, + {"emoji":"🛀🏻","name":"person taking bath: light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","taking","bath:","light","skin","tone"]}, + {"emoji":"🛀🏼","name":"person taking bath: medium-light skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","taking","bath:","medium-light","skin","tone"]}, + {"emoji":"🛀🏽","name":"person taking bath: medium skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","taking","bath:","medium","skin","tone"]}, + {"emoji":"🛀🏾","name":"person taking bath: medium-dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","taking","bath:","medium-dark","skin","tone"]}, + {"emoji":"🛀🏿","name":"person taking bath: dark skin tone","category":"Smileys & People","version":"1.0","skinTone":true,"keywords":["person","taking","bath:","dark","skin","tone"]}, + {"emoji":"🛌","name":"person in bed","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["person","in","bed","bedtime","good","goodnight","hotel","nap","night","sleep","tired","zzz"]}, + {"emoji":"🛌🏻","name":"person in bed: light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["person","in","bed:","light","skin","tone"]}, + {"emoji":"🛌🏼","name":"person in bed: medium-light skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["person","in","bed:","medium-light","skin","tone"]}, + {"emoji":"🛌🏽","name":"person in bed: medium skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["person","in","bed:","medium","skin","tone"]}, + {"emoji":"🛌🏾","name":"person in bed: medium-dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["person","in","bed:","medium-dark","skin","tone"]}, + {"emoji":"🛌🏿","name":"person in bed: dark skin tone","category":"Smileys & People","version":"4.0","skinTone":true,"keywords":["person","in","bed:","dark","skin","tone"]}, + {"emoji":"🧑‍🤝‍🧑","name":"people holding hands","category":"Smileys & People","version":"12.0","skinTone":false,"keywords":["people","holding","hands","bae","bestie","bff","couple","dating","flirt","friends","hand","hold","twins"]}, + {"emoji":"🧑🏻‍🤝‍🧑🏻","name":"people holding hands: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["people","holding","hands:","light","skin","tone"]}, + {"emoji":"🧑🏻‍🤝‍🧑🏼","name":"people holding hands: light skin tone, medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["people","holding","hands:","light","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏻‍🤝‍🧑🏽","name":"people holding hands: light skin tone, medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["people","holding","hands:","light","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏻‍🤝‍🧑🏾","name":"people holding hands: light skin tone, medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["people","holding","hands:","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"🧑🏻‍🤝‍🧑🏿","name":"people holding hands: light skin tone, dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["people","holding","hands:","light","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏼‍🤝‍🧑🏻","name":"people holding hands: medium-light skin tone, light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["people","holding","hands:","medium-light","skin","tone,","light","tone"]}, + {"emoji":"🧑🏼‍🤝‍🧑🏼","name":"people holding hands: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["people","holding","hands:","medium-light","skin","tone"]}, + {"emoji":"🧑🏼‍🤝‍🧑🏽","name":"people holding hands: medium-light skin tone, medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["people","holding","hands:","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏼‍🤝‍🧑🏾","name":"people holding hands: medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["people","holding","hands:","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"🧑🏼‍🤝‍🧑🏿","name":"people holding hands: medium-light skin tone, dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["people","holding","hands:","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏽‍🤝‍🧑🏻","name":"people holding hands: medium skin tone, light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["people","holding","hands:","medium","skin","tone,","light","tone"]}, + {"emoji":"🧑🏽‍🤝‍🧑🏼","name":"people holding hands: medium skin tone, medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["people","holding","hands:","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏽‍🤝‍🧑🏽","name":"people holding hands: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["people","holding","hands:","medium","skin","tone"]}, + {"emoji":"🧑🏽‍🤝‍🧑🏾","name":"people holding hands: medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["people","holding","hands:","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"🧑🏽‍🤝‍🧑🏿","name":"people holding hands: medium skin tone, dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["people","holding","hands:","medium","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏾‍🤝‍🧑🏻","name":"people holding hands: medium-dark skin tone, light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["people","holding","hands:","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"🧑🏾‍🤝‍🧑🏼","name":"people holding hands: medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["people","holding","hands:","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏾‍🤝‍🧑🏽","name":"people holding hands: medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["people","holding","hands:","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏾‍🤝‍🧑🏾","name":"people holding hands: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["people","holding","hands:","medium-dark","skin","tone"]}, + {"emoji":"🧑🏾‍🤝‍🧑🏿","name":"people holding hands: medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["people","holding","hands:","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏿‍🤝‍🧑🏻","name":"people holding hands: dark skin tone, light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["people","holding","hands:","dark","skin","tone,","light","tone"]}, + {"emoji":"🧑🏿‍🤝‍🧑🏼","name":"people holding hands: dark skin tone, medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["people","holding","hands:","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏿‍🤝‍🧑🏽","name":"people holding hands: dark skin tone, medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["people","holding","hands:","dark","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏿‍🤝‍🧑🏾","name":"people holding hands: dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["people","holding","hands:","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"🧑🏿‍🤝‍🧑🏿","name":"people holding hands: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["people","holding","hands:","dark","skin","tone"]}, + {"emoji":"👭","name":"women holding hands","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["women","holding","hands","bae","bestie","bff","couple","dating","flirt","friends","girls","hand","hold","sisters","twins"]}, + {"emoji":"👭🏻","name":"women holding hands: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["women","holding","hands:","light","skin","tone"]}, + {"emoji":"👩🏻‍🤝‍👩🏼","name":"women holding hands: light skin tone, medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["women","holding","hands:","light","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏻‍🤝‍👩🏽","name":"women holding hands: light skin tone, medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["women","holding","hands:","light","skin","tone,","medium","tone"]}, + {"emoji":"👩🏻‍🤝‍👩🏾","name":"women holding hands: light skin tone, medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["women","holding","hands:","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏻‍🤝‍👩🏿","name":"women holding hands: light skin tone, dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["women","holding","hands:","light","skin","tone,","dark","tone"]}, + {"emoji":"👩🏼‍🤝‍👩🏻","name":"women holding hands: medium-light skin tone, light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["women","holding","hands:","medium-light","skin","tone,","light","tone"]}, + {"emoji":"👭🏼","name":"women holding hands: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["women","holding","hands:","medium-light","skin","tone"]}, + {"emoji":"👩🏼‍🤝‍👩🏽","name":"women holding hands: medium-light skin tone, medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["women","holding","hands:","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"👩🏼‍🤝‍👩🏾","name":"women holding hands: medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["women","holding","hands:","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏼‍🤝‍👩🏿","name":"women holding hands: medium-light skin tone, dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["women","holding","hands:","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"👩🏽‍🤝‍👩🏻","name":"women holding hands: medium skin tone, light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["women","holding","hands:","medium","skin","tone,","light","tone"]}, + {"emoji":"👩🏽‍🤝‍👩🏼","name":"women holding hands: medium skin tone, medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["women","holding","hands:","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"👭🏽","name":"women holding hands: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["women","holding","hands:","medium","skin","tone"]}, + {"emoji":"👩🏽‍🤝‍👩🏾","name":"women holding hands: medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["women","holding","hands:","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏽‍🤝‍👩🏿","name":"women holding hands: medium skin tone, dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["women","holding","hands:","medium","skin","tone,","dark","tone"]}, + {"emoji":"👩🏾‍🤝‍👩🏻","name":"women holding hands: medium-dark skin tone, light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["women","holding","hands:","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"👩🏾‍🤝‍👩🏼","name":"women holding hands: medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["women","holding","hands:","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏾‍🤝‍👩🏽","name":"women holding hands: medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["women","holding","hands:","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"👭🏾","name":"women holding hands: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["women","holding","hands:","medium-dark","skin","tone"]}, + {"emoji":"👩🏾‍🤝‍👩🏿","name":"women holding hands: medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["women","holding","hands:","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"👩🏿‍🤝‍👩🏻","name":"women holding hands: dark skin tone, light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["women","holding","hands:","dark","skin","tone,","light","tone"]}, + {"emoji":"👩🏿‍🤝‍👩🏼","name":"women holding hands: dark skin tone, medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["women","holding","hands:","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏿‍🤝‍👩🏽","name":"women holding hands: dark skin tone, medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["women","holding","hands:","dark","skin","tone,","medium","tone"]}, + {"emoji":"👩🏿‍🤝‍👩🏾","name":"women holding hands: dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["women","holding","hands:","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"👭🏿","name":"women holding hands: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["women","holding","hands:","dark","skin","tone"]}, + {"emoji":"👫","name":"woman and man holding hands","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["woman","and","man","holding","hands","bae","bestie","bff","couple","dating","flirt","friends","hand","hold","twins"]}, + {"emoji":"👫🏻","name":"woman and man holding hands: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","light","skin","tone"]}, + {"emoji":"👩🏻‍🤝‍👨🏼","name":"woman and man holding hands: light skin tone, medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","light","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏻‍🤝‍👨🏽","name":"woman and man holding hands: light skin tone, medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","light","skin","tone,","medium","tone"]}, + {"emoji":"👩🏻‍🤝‍👨🏾","name":"woman and man holding hands: light skin tone, medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏻‍🤝‍👨🏿","name":"woman and man holding hands: light skin tone, dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","light","skin","tone,","dark","tone"]}, + {"emoji":"👩🏼‍🤝‍👨🏻","name":"woman and man holding hands: medium-light skin tone, light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","medium-light","skin","tone,","light","tone"]}, + {"emoji":"👫🏼","name":"woman and man holding hands: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","medium-light","skin","tone"]}, + {"emoji":"👩🏼‍🤝‍👨🏽","name":"woman and man holding hands: medium-light skin tone, medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"👩🏼‍🤝‍👨🏾","name":"woman and man holding hands: medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏼‍🤝‍👨🏿","name":"woman and man holding hands: medium-light skin tone, dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"👩🏽‍🤝‍👨🏻","name":"woman and man holding hands: medium skin tone, light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","medium","skin","tone,","light","tone"]}, + {"emoji":"👩🏽‍🤝‍👨🏼","name":"woman and man holding hands: medium skin tone, medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"👫🏽","name":"woman and man holding hands: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","medium","skin","tone"]}, + {"emoji":"👩🏽‍🤝‍👨🏾","name":"woman and man holding hands: medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏽‍🤝‍👨🏿","name":"woman and man holding hands: medium skin tone, dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","medium","skin","tone,","dark","tone"]}, + {"emoji":"👩🏾‍🤝‍👨🏻","name":"woman and man holding hands: medium-dark skin tone, light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"👩🏾‍🤝‍👨🏼","name":"woman and man holding hands: medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏾‍🤝‍👨🏽","name":"woman and man holding hands: medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"👫🏾","name":"woman and man holding hands: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","medium-dark","skin","tone"]}, + {"emoji":"👩🏾‍🤝‍👨🏿","name":"woman and man holding hands: medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"👩🏿‍🤝‍👨🏻","name":"woman and man holding hands: dark skin tone, light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","dark","skin","tone,","light","tone"]}, + {"emoji":"👩🏿‍🤝‍👨🏼","name":"woman and man holding hands: dark skin tone, medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏿‍🤝‍👨🏽","name":"woman and man holding hands: dark skin tone, medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","dark","skin","tone,","medium","tone"]}, + {"emoji":"👩🏿‍🤝‍👨🏾","name":"woman and man holding hands: dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"👫🏿","name":"woman and man holding hands: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["woman","and","man","holding","hands:","dark","skin","tone"]}, + {"emoji":"👬","name":"men holding hands","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["men","holding","hands","bae","bestie","bff","boys","brothers","couple","dating","flirt","friends","hand","hold","twins"]}, + {"emoji":"👬🏻","name":"men holding hands: light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["men","holding","hands:","light","skin","tone"]}, + {"emoji":"👨🏻‍🤝‍👨🏼","name":"men holding hands: light skin tone, medium-light skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["men","holding","hands:","light","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏻‍🤝‍👨🏽","name":"men holding hands: light skin tone, medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["men","holding","hands:","light","skin","tone,","medium","tone"]}, + {"emoji":"👨🏻‍🤝‍👨🏾","name":"men holding hands: light skin tone, medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["men","holding","hands:","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏻‍🤝‍👨🏿","name":"men holding hands: light skin tone, dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["men","holding","hands:","light","skin","tone,","dark","tone"]}, + {"emoji":"👨🏼‍🤝‍👨🏻","name":"men holding hands: medium-light skin tone, light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["men","holding","hands:","medium-light","skin","tone,","light","tone"]}, + {"emoji":"👬🏼","name":"men holding hands: medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["men","holding","hands:","medium-light","skin","tone"]}, + {"emoji":"👨🏼‍🤝‍👨🏽","name":"men holding hands: medium-light skin tone, medium skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["men","holding","hands:","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"👨🏼‍🤝‍👨🏾","name":"men holding hands: medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["men","holding","hands:","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏼‍🤝‍👨🏿","name":"men holding hands: medium-light skin tone, dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["men","holding","hands:","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"👨🏽‍🤝‍👨🏻","name":"men holding hands: medium skin tone, light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["men","holding","hands:","medium","skin","tone,","light","tone"]}, + {"emoji":"👨🏽‍🤝‍👨🏼","name":"men holding hands: medium skin tone, medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["men","holding","hands:","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"👬🏽","name":"men holding hands: medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["men","holding","hands:","medium","skin","tone"]}, + {"emoji":"👨🏽‍🤝‍👨🏾","name":"men holding hands: medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["men","holding","hands:","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏽‍🤝‍👨🏿","name":"men holding hands: medium skin tone, dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["men","holding","hands:","medium","skin","tone,","dark","tone"]}, + {"emoji":"👨🏾‍🤝‍👨🏻","name":"men holding hands: medium-dark skin tone, light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["men","holding","hands:","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"👨🏾‍🤝‍👨🏼","name":"men holding hands: medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["men","holding","hands:","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏾‍🤝‍👨🏽","name":"men holding hands: medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["men","holding","hands:","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"👬🏾","name":"men holding hands: medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["men","holding","hands:","medium-dark","skin","tone"]}, + {"emoji":"👨🏾‍🤝‍👨🏿","name":"men holding hands: medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"12.1","skinTone":true,"keywords":["men","holding","hands:","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"👨🏿‍🤝‍👨🏻","name":"men holding hands: dark skin tone, light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["men","holding","hands:","dark","skin","tone,","light","tone"]}, + {"emoji":"👨🏿‍🤝‍👨🏼","name":"men holding hands: dark skin tone, medium-light skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["men","holding","hands:","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏿‍🤝‍👨🏽","name":"men holding hands: dark skin tone, medium skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["men","holding","hands:","dark","skin","tone,","medium","tone"]}, + {"emoji":"👨🏿‍🤝‍👨🏾","name":"men holding hands: dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["men","holding","hands:","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"👬🏿","name":"men holding hands: dark skin tone","category":"Smileys & People","version":"12.0","skinTone":true,"keywords":["men","holding","hands:","dark","skin","tone"]}, + {"emoji":"💏","name":"kiss","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["kiss","anniversary","babe","bae","couple","date","dating","heart","love","mwah","person","romance","together","xoxo"]}, + {"emoji":"💏🏻","name":"kiss: light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","light","skin","tone"]}, + {"emoji":"💏🏼","name":"kiss: medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","medium-light","skin","tone"]}, + {"emoji":"💏🏽","name":"kiss: medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","medium","skin","tone"]}, + {"emoji":"💏🏾","name":"kiss: medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","medium-dark","skin","tone"]}, + {"emoji":"💏🏿","name":"kiss: dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","dark","skin","tone"]}, + {"emoji":"🧑🏻‍❤️‍💋‍🧑🏼","name":"kiss: person, person, light skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","light","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏻‍❤️‍💋‍🧑🏽","name":"kiss: person, person, light skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","light","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏻‍❤️‍💋‍🧑🏾","name":"kiss: person, person, light skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"🧑🏻‍❤️‍💋‍🧑🏿","name":"kiss: person, person, light skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","light","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏼‍❤️‍💋‍🧑🏻","name":"kiss: person, person, medium-light skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","medium-light","skin","tone,","light","tone"]}, + {"emoji":"🧑🏼‍❤️‍💋‍🧑🏽","name":"kiss: person, person, medium-light skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏼‍❤️‍💋‍🧑🏾","name":"kiss: person, person, medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"🧑🏼‍❤️‍💋‍🧑🏿","name":"kiss: person, person, medium-light skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏽‍❤️‍💋‍🧑🏻","name":"kiss: person, person, medium skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","medium","skin","tone,","light","tone"]}, + {"emoji":"🧑🏽‍❤️‍💋‍🧑🏼","name":"kiss: person, person, medium skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏽‍❤️‍💋‍🧑🏾","name":"kiss: person, person, medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"🧑🏽‍❤️‍💋‍🧑🏿","name":"kiss: person, person, medium skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","medium","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏾‍❤️‍💋‍🧑🏻","name":"kiss: person, person, medium-dark skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"🧑🏾‍❤️‍💋‍🧑🏼","name":"kiss: person, person, medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏾‍❤️‍💋‍🧑🏽","name":"kiss: person, person, medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏾‍❤️‍💋‍🧑🏿","name":"kiss: person, person, medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏿‍❤️‍💋‍🧑🏻","name":"kiss: person, person, dark skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","dark","skin","tone,","light","tone"]}, + {"emoji":"🧑🏿‍❤️‍💋‍🧑🏼","name":"kiss: person, person, dark skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏿‍❤️‍💋‍🧑🏽","name":"kiss: person, person, dark skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","dark","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏿‍❤️‍💋‍🧑🏾","name":"kiss: person, person, dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","person,","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩‍❤️‍💋‍👨","name":"kiss: woman, man","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["kiss:","woman,","man"]}, + {"emoji":"👩🏻‍❤️‍💋‍👨🏻","name":"kiss: woman, man, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","light","skin","tone"]}, + {"emoji":"👩🏻‍❤️‍💋‍👨🏼","name":"kiss: woman, man, light skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","light","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏻‍❤️‍💋‍👨🏽","name":"kiss: woman, man, light skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","light","skin","tone,","medium","tone"]}, + {"emoji":"👩🏻‍❤️‍💋‍👨🏾","name":"kiss: woman, man, light skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏻‍❤️‍💋‍👨🏿","name":"kiss: woman, man, light skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","light","skin","tone,","dark","tone"]}, + {"emoji":"👩🏼‍❤️‍💋‍👨🏻","name":"kiss: woman, man, medium-light skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","medium-light","skin","tone,","light","tone"]}, + {"emoji":"👩🏼‍❤️‍💋‍👨🏼","name":"kiss: woman, man, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","medium-light","skin","tone"]}, + {"emoji":"👩🏼‍❤️‍💋‍👨🏽","name":"kiss: woman, man, medium-light skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"👩🏼‍❤️‍💋‍👨🏾","name":"kiss: woman, man, medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏼‍❤️‍💋‍👨🏿","name":"kiss: woman, man, medium-light skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"👩🏽‍❤️‍💋‍👨🏻","name":"kiss: woman, man, medium skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","medium","skin","tone,","light","tone"]}, + {"emoji":"👩🏽‍❤️‍💋‍👨🏼","name":"kiss: woman, man, medium skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏽‍❤️‍💋‍👨🏽","name":"kiss: woman, man, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","medium","skin","tone"]}, + {"emoji":"👩🏽‍❤️‍💋‍👨🏾","name":"kiss: woman, man, medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏽‍❤️‍💋‍👨🏿","name":"kiss: woman, man, medium skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","medium","skin","tone,","dark","tone"]}, + {"emoji":"👩🏾‍❤️‍💋‍👨🏻","name":"kiss: woman, man, medium-dark skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"👩🏾‍❤️‍💋‍👨🏼","name":"kiss: woman, man, medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏾‍❤️‍💋‍👨🏽","name":"kiss: woman, man, medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"👩🏾‍❤️‍💋‍👨🏾","name":"kiss: woman, man, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","medium-dark","skin","tone"]}, + {"emoji":"👩🏾‍❤️‍💋‍👨🏿","name":"kiss: woman, man, medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"👩🏿‍❤️‍💋‍👨🏻","name":"kiss: woman, man, dark skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","dark","skin","tone,","light","tone"]}, + {"emoji":"👩🏿‍❤️‍💋‍👨🏼","name":"kiss: woman, man, dark skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏿‍❤️‍💋‍👨🏽","name":"kiss: woman, man, dark skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","dark","skin","tone,","medium","tone"]}, + {"emoji":"👩🏿‍❤️‍💋‍👨🏾","name":"kiss: woman, man, dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏿‍❤️‍💋‍👨🏿","name":"kiss: woman, man, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","man,","dark","skin","tone"]}, + {"emoji":"👨‍❤️‍💋‍👨","name":"kiss: man, man","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["kiss:","man,","man"]}, + {"emoji":"👨🏻‍❤️‍💋‍👨🏻","name":"kiss: man, man, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","light","skin","tone"]}, + {"emoji":"👨🏻‍❤️‍💋‍👨🏼","name":"kiss: man, man, light skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","light","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏻‍❤️‍💋‍👨🏽","name":"kiss: man, man, light skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","light","skin","tone,","medium","tone"]}, + {"emoji":"👨🏻‍❤️‍💋‍👨🏾","name":"kiss: man, man, light skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏻‍❤️‍💋‍👨🏿","name":"kiss: man, man, light skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","light","skin","tone,","dark","tone"]}, + {"emoji":"👨🏼‍❤️‍💋‍👨🏻","name":"kiss: man, man, medium-light skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","medium-light","skin","tone,","light","tone"]}, + {"emoji":"👨🏼‍❤️‍💋‍👨🏼","name":"kiss: man, man, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","medium-light","skin","tone"]}, + {"emoji":"👨🏼‍❤️‍💋‍👨🏽","name":"kiss: man, man, medium-light skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"👨🏼‍❤️‍💋‍👨🏾","name":"kiss: man, man, medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏼‍❤️‍💋‍👨🏿","name":"kiss: man, man, medium-light skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"👨🏽‍❤️‍💋‍👨🏻","name":"kiss: man, man, medium skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","medium","skin","tone,","light","tone"]}, + {"emoji":"👨🏽‍❤️‍💋‍👨🏼","name":"kiss: man, man, medium skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏽‍❤️‍💋‍👨🏽","name":"kiss: man, man, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","medium","skin","tone"]}, + {"emoji":"👨🏽‍❤️‍💋‍👨🏾","name":"kiss: man, man, medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏽‍❤️‍💋‍👨🏿","name":"kiss: man, man, medium skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","medium","skin","tone,","dark","tone"]}, + {"emoji":"👨🏾‍❤️‍💋‍👨🏻","name":"kiss: man, man, medium-dark skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"👨🏾‍❤️‍💋‍👨🏼","name":"kiss: man, man, medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏾‍❤️‍💋‍👨🏽","name":"kiss: man, man, medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"👨🏾‍❤️‍💋‍👨🏾","name":"kiss: man, man, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","medium-dark","skin","tone"]}, + {"emoji":"👨🏾‍❤️‍💋‍👨🏿","name":"kiss: man, man, medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"👨🏿‍❤️‍💋‍👨🏻","name":"kiss: man, man, dark skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","dark","skin","tone,","light","tone"]}, + {"emoji":"👨🏿‍❤️‍💋‍👨🏼","name":"kiss: man, man, dark skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏿‍❤️‍💋‍👨🏽","name":"kiss: man, man, dark skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","dark","skin","tone,","medium","tone"]}, + {"emoji":"👨🏿‍❤️‍💋‍👨🏾","name":"kiss: man, man, dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏿‍❤️‍💋‍👨🏿","name":"kiss: man, man, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","man,","dark","skin","tone"]}, + {"emoji":"👩‍❤️‍💋‍👩","name":"kiss: woman, woman","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["kiss:","woman,","woman"]}, + {"emoji":"👩🏻‍❤️‍💋‍👩🏻","name":"kiss: woman, woman, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","light","skin","tone"]}, + {"emoji":"👩🏻‍❤️‍💋‍👩🏼","name":"kiss: woman, woman, light skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","light","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏻‍❤️‍💋‍👩🏽","name":"kiss: woman, woman, light skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","light","skin","tone,","medium","tone"]}, + {"emoji":"👩🏻‍❤️‍💋‍👩🏾","name":"kiss: woman, woman, light skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏻‍❤️‍💋‍👩🏿","name":"kiss: woman, woman, light skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","light","skin","tone,","dark","tone"]}, + {"emoji":"👩🏼‍❤️‍💋‍👩🏻","name":"kiss: woman, woman, medium-light skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","medium-light","skin","tone,","light","tone"]}, + {"emoji":"👩🏼‍❤️‍💋‍👩🏼","name":"kiss: woman, woman, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","medium-light","skin","tone"]}, + {"emoji":"👩🏼‍❤️‍💋‍👩🏽","name":"kiss: woman, woman, medium-light skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"👩🏼‍❤️‍💋‍👩🏾","name":"kiss: woman, woman, medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏼‍❤️‍💋‍👩🏿","name":"kiss: woman, woman, medium-light skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"👩🏽‍❤️‍💋‍👩🏻","name":"kiss: woman, woman, medium skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","medium","skin","tone,","light","tone"]}, + {"emoji":"👩🏽‍❤️‍💋‍👩🏼","name":"kiss: woman, woman, medium skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏽‍❤️‍💋‍👩🏽","name":"kiss: woman, woman, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","medium","skin","tone"]}, + {"emoji":"👩🏽‍❤️‍💋‍👩🏾","name":"kiss: woman, woman, medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏽‍❤️‍💋‍👩🏿","name":"kiss: woman, woman, medium skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","medium","skin","tone,","dark","tone"]}, + {"emoji":"👩🏾‍❤️‍💋‍👩🏻","name":"kiss: woman, woman, medium-dark skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"👩🏾‍❤️‍💋‍👩🏼","name":"kiss: woman, woman, medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏾‍❤️‍💋‍👩🏽","name":"kiss: woman, woman, medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"👩🏾‍❤️‍💋‍👩🏾","name":"kiss: woman, woman, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","medium-dark","skin","tone"]}, + {"emoji":"👩🏾‍❤️‍💋‍👩🏿","name":"kiss: woman, woman, medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"👩🏿‍❤️‍💋‍👩🏻","name":"kiss: woman, woman, dark skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","dark","skin","tone,","light","tone"]}, + {"emoji":"👩🏿‍❤️‍💋‍👩🏼","name":"kiss: woman, woman, dark skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏿‍❤️‍💋‍👩🏽","name":"kiss: woman, woman, dark skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","dark","skin","tone,","medium","tone"]}, + {"emoji":"👩🏿‍❤️‍💋‍👩🏾","name":"kiss: woman, woman, dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏿‍❤️‍💋‍👩🏿","name":"kiss: woman, woman, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["kiss:","woman,","dark","skin","tone"]}, + {"emoji":"💑","name":"couple with heart","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["couple","with","heart","anniversary","babe","bae","dating","kiss","love","person","relationship","romance","together","you"]}, + {"emoji":"💑🏻","name":"couple with heart: light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","light","skin","tone"]}, + {"emoji":"💑🏼","name":"couple with heart: medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","medium-light","skin","tone"]}, + {"emoji":"💑🏽","name":"couple with heart: medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","medium","skin","tone"]}, + {"emoji":"💑🏾","name":"couple with heart: medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","medium-dark","skin","tone"]}, + {"emoji":"💑🏿","name":"couple with heart: dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","dark","skin","tone"]}, + {"emoji":"🧑🏻‍❤️‍🧑🏼","name":"couple with heart: person, person, light skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","light","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏻‍❤️‍🧑🏽","name":"couple with heart: person, person, light skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","light","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏻‍❤️‍🧑🏾","name":"couple with heart: person, person, light skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"🧑🏻‍❤️‍🧑🏿","name":"couple with heart: person, person, light skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","light","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏼‍❤️‍🧑🏻","name":"couple with heart: person, person, medium-light skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","medium-light","skin","tone,","light","tone"]}, + {"emoji":"🧑🏼‍❤️‍🧑🏽","name":"couple with heart: person, person, medium-light skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏼‍❤️‍🧑🏾","name":"couple with heart: person, person, medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"🧑🏼‍❤️‍🧑🏿","name":"couple with heart: person, person, medium-light skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏽‍❤️‍🧑🏻","name":"couple with heart: person, person, medium skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","medium","skin","tone,","light","tone"]}, + {"emoji":"🧑🏽‍❤️‍🧑🏼","name":"couple with heart: person, person, medium skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏽‍❤️‍🧑🏾","name":"couple with heart: person, person, medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"🧑🏽‍❤️‍🧑🏿","name":"couple with heart: person, person, medium skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","medium","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏾‍❤️‍🧑🏻","name":"couple with heart: person, person, medium-dark skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"🧑🏾‍❤️‍🧑🏼","name":"couple with heart: person, person, medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏾‍❤️‍🧑🏽","name":"couple with heart: person, person, medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏾‍❤️‍🧑🏿","name":"couple with heart: person, person, medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"🧑🏿‍❤️‍🧑🏻","name":"couple with heart: person, person, dark skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","dark","skin","tone,","light","tone"]}, + {"emoji":"🧑🏿‍❤️‍🧑🏼","name":"couple with heart: person, person, dark skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"🧑🏿‍❤️‍🧑🏽","name":"couple with heart: person, person, dark skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","dark","skin","tone,","medium","tone"]}, + {"emoji":"🧑🏿‍❤️‍🧑🏾","name":"couple with heart: person, person, dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","person,","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩‍❤️‍👨","name":"couple with heart: woman, man","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["couple","with","heart:","woman,","man"]}, + {"emoji":"👩🏻‍❤️‍👨🏻","name":"couple with heart: woman, man, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","light","skin","tone"]}, + {"emoji":"👩🏻‍❤️‍👨🏼","name":"couple with heart: woman, man, light skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","light","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏻‍❤️‍👨🏽","name":"couple with heart: woman, man, light skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","light","skin","tone,","medium","tone"]}, + {"emoji":"👩🏻‍❤️‍👨🏾","name":"couple with heart: woman, man, light skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏻‍❤️‍👨🏿","name":"couple with heart: woman, man, light skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","light","skin","tone,","dark","tone"]}, + {"emoji":"👩🏼‍❤️‍👨🏻","name":"couple with heart: woman, man, medium-light skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","medium-light","skin","tone,","light","tone"]}, + {"emoji":"👩🏼‍❤️‍👨🏼","name":"couple with heart: woman, man, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","medium-light","skin","tone"]}, + {"emoji":"👩🏼‍❤️‍👨🏽","name":"couple with heart: woman, man, medium-light skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"👩🏼‍❤️‍👨🏾","name":"couple with heart: woman, man, medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏼‍❤️‍👨🏿","name":"couple with heart: woman, man, medium-light skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"👩🏽‍❤️‍👨🏻","name":"couple with heart: woman, man, medium skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","medium","skin","tone,","light","tone"]}, + {"emoji":"👩🏽‍❤️‍👨🏼","name":"couple with heart: woman, man, medium skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏽‍❤️‍👨🏽","name":"couple with heart: woman, man, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","medium","skin","tone"]}, + {"emoji":"👩🏽‍❤️‍👨🏾","name":"couple with heart: woman, man, medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏽‍❤️‍👨🏿","name":"couple with heart: woman, man, medium skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","medium","skin","tone,","dark","tone"]}, + {"emoji":"👩🏾‍❤️‍👨🏻","name":"couple with heart: woman, man, medium-dark skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"👩🏾‍❤️‍👨🏼","name":"couple with heart: woman, man, medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏾‍❤️‍👨🏽","name":"couple with heart: woman, man, medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"👩🏾‍❤️‍👨🏾","name":"couple with heart: woman, man, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","medium-dark","skin","tone"]}, + {"emoji":"👩🏾‍❤️‍👨🏿","name":"couple with heart: woman, man, medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"👩🏿‍❤️‍👨🏻","name":"couple with heart: woman, man, dark skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","dark","skin","tone,","light","tone"]}, + {"emoji":"👩🏿‍❤️‍👨🏼","name":"couple with heart: woman, man, dark skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏿‍❤️‍👨🏽","name":"couple with heart: woman, man, dark skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","dark","skin","tone,","medium","tone"]}, + {"emoji":"👩🏿‍❤️‍👨🏾","name":"couple with heart: woman, man, dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏿‍❤️‍👨🏿","name":"couple with heart: woman, man, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","man,","dark","skin","tone"]}, + {"emoji":"👨‍❤️‍👨","name":"couple with heart: man, man","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["couple","with","heart:","man,","man"]}, + {"emoji":"👨🏻‍❤️‍👨🏻","name":"couple with heart: man, man, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","light","skin","tone"]}, + {"emoji":"👨🏻‍❤️‍👨🏼","name":"couple with heart: man, man, light skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","light","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏻‍❤️‍👨🏽","name":"couple with heart: man, man, light skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","light","skin","tone,","medium","tone"]}, + {"emoji":"👨🏻‍❤️‍👨🏾","name":"couple with heart: man, man, light skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏻‍❤️‍👨🏿","name":"couple with heart: man, man, light skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","light","skin","tone,","dark","tone"]}, + {"emoji":"👨🏼‍❤️‍👨🏻","name":"couple with heart: man, man, medium-light skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","medium-light","skin","tone,","light","tone"]}, + {"emoji":"👨🏼‍❤️‍👨🏼","name":"couple with heart: man, man, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","medium-light","skin","tone"]}, + {"emoji":"👨🏼‍❤️‍👨🏽","name":"couple with heart: man, man, medium-light skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"👨🏼‍❤️‍👨🏾","name":"couple with heart: man, man, medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏼‍❤️‍👨🏿","name":"couple with heart: man, man, medium-light skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"👨🏽‍❤️‍👨🏻","name":"couple with heart: man, man, medium skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","medium","skin","tone,","light","tone"]}, + {"emoji":"👨🏽‍❤️‍👨🏼","name":"couple with heart: man, man, medium skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏽‍❤️‍👨🏽","name":"couple with heart: man, man, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","medium","skin","tone"]}, + {"emoji":"👨🏽‍❤️‍👨🏾","name":"couple with heart: man, man, medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏽‍❤️‍👨🏿","name":"couple with heart: man, man, medium skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","medium","skin","tone,","dark","tone"]}, + {"emoji":"👨🏾‍❤️‍👨🏻","name":"couple with heart: man, man, medium-dark skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"👨🏾‍❤️‍👨🏼","name":"couple with heart: man, man, medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏾‍❤️‍👨🏽","name":"couple with heart: man, man, medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"👨🏾‍❤️‍👨🏾","name":"couple with heart: man, man, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","medium-dark","skin","tone"]}, + {"emoji":"👨🏾‍❤️‍👨🏿","name":"couple with heart: man, man, medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"👨🏿‍❤️‍👨🏻","name":"couple with heart: man, man, dark skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","dark","skin","tone,","light","tone"]}, + {"emoji":"👨🏿‍❤️‍👨🏼","name":"couple with heart: man, man, dark skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👨🏿‍❤️‍👨🏽","name":"couple with heart: man, man, dark skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","dark","skin","tone,","medium","tone"]}, + {"emoji":"👨🏿‍❤️‍👨🏾","name":"couple with heart: man, man, dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"👨🏿‍❤️‍👨🏿","name":"couple with heart: man, man, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","man,","dark","skin","tone"]}, + {"emoji":"👩‍❤️‍👩","name":"couple with heart: woman, woman","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["couple","with","heart:","woman,","woman"]}, + {"emoji":"👩🏻‍❤️‍👩🏻","name":"couple with heart: woman, woman, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","light","skin","tone"]}, + {"emoji":"👩🏻‍❤️‍👩🏼","name":"couple with heart: woman, woman, light skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","light","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏻‍❤️‍👩🏽","name":"couple with heart: woman, woman, light skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","light","skin","tone,","medium","tone"]}, + {"emoji":"👩🏻‍❤️‍👩🏾","name":"couple with heart: woman, woman, light skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏻‍❤️‍👩🏿","name":"couple with heart: woman, woman, light skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","light","skin","tone,","dark","tone"]}, + {"emoji":"👩🏼‍❤️‍👩🏻","name":"couple with heart: woman, woman, medium-light skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","medium-light","skin","tone,","light","tone"]}, + {"emoji":"👩🏼‍❤️‍👩🏼","name":"couple with heart: woman, woman, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","medium-light","skin","tone"]}, + {"emoji":"👩🏼‍❤️‍👩🏽","name":"couple with heart: woman, woman, medium-light skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","medium-light","skin","tone,","medium","tone"]}, + {"emoji":"👩🏼‍❤️‍👩🏾","name":"couple with heart: woman, woman, medium-light skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","medium-light","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏼‍❤️‍👩🏿","name":"couple with heart: woman, woman, medium-light skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","medium-light","skin","tone,","dark","tone"]}, + {"emoji":"👩🏽‍❤️‍👩🏻","name":"couple with heart: woman, woman, medium skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","medium","skin","tone,","light","tone"]}, + {"emoji":"👩🏽‍❤️‍👩🏼","name":"couple with heart: woman, woman, medium skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","medium","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏽‍❤️‍👩🏽","name":"couple with heart: woman, woman, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","medium","skin","tone"]}, + {"emoji":"👩🏽‍❤️‍👩🏾","name":"couple with heart: woman, woman, medium skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","medium","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏽‍❤️‍👩🏿","name":"couple with heart: woman, woman, medium skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","medium","skin","tone,","dark","tone"]}, + {"emoji":"👩🏾‍❤️‍👩🏻","name":"couple with heart: woman, woman, medium-dark skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","medium-dark","skin","tone,","light","tone"]}, + {"emoji":"👩🏾‍❤️‍👩🏼","name":"couple with heart: woman, woman, medium-dark skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","medium-dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏾‍❤️‍👩🏽","name":"couple with heart: woman, woman, medium-dark skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","medium-dark","skin","tone,","medium","tone"]}, + {"emoji":"👩🏾‍❤️‍👩🏾","name":"couple with heart: woman, woman, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","medium-dark","skin","tone"]}, + {"emoji":"👩🏾‍❤️‍👩🏿","name":"couple with heart: woman, woman, medium-dark skin tone, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","medium-dark","skin","tone,","dark","tone"]}, + {"emoji":"👩🏿‍❤️‍👩🏻","name":"couple with heart: woman, woman, dark skin tone, light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","dark","skin","tone,","light","tone"]}, + {"emoji":"👩🏿‍❤️‍👩🏼","name":"couple with heart: woman, woman, dark skin tone, medium-light skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","dark","skin","tone,","medium-light","tone"]}, + {"emoji":"👩🏿‍❤️‍👩🏽","name":"couple with heart: woman, woman, dark skin tone, medium skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","dark","skin","tone,","medium","tone"]}, + {"emoji":"👩🏿‍❤️‍👩🏾","name":"couple with heart: woman, woman, dark skin tone, medium-dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","dark","skin","tone,","medium-dark","tone"]}, + {"emoji":"👩🏿‍❤️‍👩🏿","name":"couple with heart: woman, woman, dark skin tone","category":"Smileys & People","version":"13.1","skinTone":true,"keywords":["couple","with","heart:","woman,","dark","skin","tone"]}, + {"emoji":"👨‍👩‍👦","name":"family: man, woman, boy","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["family:","man,","woman,","boy"]}, + {"emoji":"👨‍👩‍👧","name":"family: man, woman, girl","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["family:","man,","woman,","girl"]}, + {"emoji":"👨‍👩‍👧‍👦","name":"family: man, woman, girl, boy","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["family:","man,","woman,","girl,","boy"]}, + {"emoji":"👨‍👩‍👦‍👦","name":"family: man, woman, boy, boy","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["family:","man,","woman,","boy,","boy"]}, + {"emoji":"👨‍👩‍👧‍👧","name":"family: man, woman, girl, girl","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["family:","man,","woman,","girl,","girl"]}, + {"emoji":"👨‍👨‍👦","name":"family: man, man, boy","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["family:","man,","boy"]}, + {"emoji":"👨‍👨‍👧","name":"family: man, man, girl","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["family:","man,","girl"]}, + {"emoji":"👨‍👨‍👧‍👦","name":"family: man, man, girl, boy","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["family:","man,","girl,","boy"]}, + {"emoji":"👨‍👨‍👦‍👦","name":"family: man, man, boy, boy","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["family:","man,","boy,","boy"]}, + {"emoji":"👨‍👨‍👧‍👧","name":"family: man, man, girl, girl","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["family:","man,","girl,","girl"]}, + {"emoji":"👩‍👩‍👦","name":"family: woman, woman, boy","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["family:","woman,","boy"]}, + {"emoji":"👩‍👩‍👧","name":"family: woman, woman, girl","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["family:","woman,","girl"]}, + {"emoji":"👩‍👩‍👧‍👦","name":"family: woman, woman, girl, boy","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["family:","woman,","girl,","boy"]}, + {"emoji":"👩‍👩‍👦‍👦","name":"family: woman, woman, boy, boy","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["family:","woman,","boy,","boy"]}, + {"emoji":"👩‍👩‍👧‍👧","name":"family: woman, woman, girl, girl","category":"Smileys & People","version":"2.0","skinTone":false,"keywords":["family:","woman,","girl,","girl"]}, + {"emoji":"👨‍👦","name":"family: man, boy","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["family:","man,","boy"]}, + {"emoji":"👨‍👦‍👦","name":"family: man, boy, boy","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["family:","man,","boy,","boy"]}, + {"emoji":"👨‍👧","name":"family: man, girl","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["family:","man,","girl"]}, + {"emoji":"👨‍👧‍👦","name":"family: man, girl, boy","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["family:","man,","girl,","boy"]}, + {"emoji":"👨‍👧‍👧","name":"family: man, girl, girl","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["family:","man,","girl,","girl"]}, + {"emoji":"👩‍👦","name":"family: woman, boy","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["family:","woman,","boy"]}, + {"emoji":"👩‍👦‍👦","name":"family: woman, boy, boy","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["family:","woman,","boy,","boy"]}, + {"emoji":"👩‍👧","name":"family: woman, girl","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["family:","woman,","girl"]}, + {"emoji":"👩‍👧‍👦","name":"family: woman, girl, boy","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["family:","woman,","girl,","boy"]}, + {"emoji":"👩‍👧‍👧","name":"family: woman, girl, girl","category":"Smileys & People","version":"4.0","skinTone":false,"keywords":["family:","woman,","girl,","girl"]}, + {"emoji":"🗣️","name":"speaking head","category":"Smileys & People","version":"0.7","skinTone":false,"keywords":["speaking","head"]}, + {"emoji":"👤","name":"bust in silhouette","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["bust","in","silhouette","mysterious","shadow"]}, + {"emoji":"👥","name":"busts in silhouette","category":"Smileys & People","version":"1.0","skinTone":false,"keywords":["busts","in","silhouette","bff","bust","everyone","friend","friends","people"]}, + {"emoji":"🫂","name":"people hugging","category":"Smileys & People","version":"13.0","skinTone":false,"keywords":["people","hugging","comfort","embrace","farewell","friendship","goodbye","hello","hug","love","thanks"]}, + {"emoji":"👪","name":"family","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["family","child"]}, + {"emoji":"🧑‍🧑‍🧒","name":"family: adult, adult, child","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["family:","adult,","child","adult","family"]}, + {"emoji":"🧑‍🧑‍🧒‍🧒","name":"family: adult, adult, child, child","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["family:","adult,","child,","child","adult","family"]}, + {"emoji":"🧑‍🧒","name":"family: adult, child","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["family:","adult,","child","adult","family"]}, + {"emoji":"🧑‍🧒‍🧒","name":"family: adult, child, child","category":"Smileys & People","version":"15.1","skinTone":false,"keywords":["family:","adult,","child,","child","adult","family"]}, + {"emoji":"👣","name":"footprints","category":"Smileys & People","version":"0.6","skinTone":false,"keywords":["footprints","barefoot","clothing","footprint","omw","print","walk"]}, + {"emoji":"🫆","name":"fingerprint","category":"Smileys & People","version":"16.0","skinTone":false,"keywords":["fingerprint","clue","crime","detective","forensics","identity","mystery","print","safety","trace"]}, + {"emoji":"🐵","name":"monkey face","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["monkey","face","animal","banana"]}, + {"emoji":"🐒","name":"monkey","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["monkey","animal","banana"]}, + {"emoji":"🦍","name":"gorilla","category":"Animals & Nature","version":"3.0","skinTone":false,"keywords":["gorilla","animal"]}, + {"emoji":"🦧","name":"orangutan","category":"Animals & Nature","version":"12.0","skinTone":false,"keywords":["orangutan","animal","ape","monkey"]}, + {"emoji":"🐶","name":"dog face","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["dog","face","adorbs","animal","pet","puppies","puppy"]}, + {"emoji":"🐕","name":"dog","category":"Animals & Nature","version":"0.7","skinTone":false,"keywords":["dog","animal","animals","dogs","pet"]}, + {"emoji":"🦮","name":"guide dog","category":"Animals & Nature","version":"12.0","skinTone":false,"keywords":["guide","dog","accessibility","animal","blind"]}, + {"emoji":"🐕‍🦺","name":"service dog","category":"Animals & Nature","version":"12.0","skinTone":false,"keywords":["service","dog","accessibility","animal","assistance"]}, + {"emoji":"🐩","name":"poodle","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["poodle","animal","dog","fluffy"]}, + {"emoji":"🐺","name":"wolf","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["wolf","animal","face"]}, + {"emoji":"🦊","name":"fox","category":"Animals & Nature","version":"3.0","skinTone":false,"keywords":["fox","animal","face"]}, + {"emoji":"🦝","name":"raccoon","category":"Animals & Nature","version":"11.0","skinTone":false,"keywords":["raccoon","animal","curious","sly"]}, + {"emoji":"🐱","name":"cat face","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["cat","face","animal","kitten","kitty","pet"]}, + {"emoji":"🐈","name":"cat","category":"Animals & Nature","version":"0.7","skinTone":false,"keywords":["cat","animal","animals","cats","kitten","pet"]}, + {"emoji":"🐈‍⬛","name":"black cat","category":"Animals & Nature","version":"13.0","skinTone":false,"keywords":["black","cat","animal","feline","halloween","meow","unlucky"]}, + {"emoji":"🦁","name":"lion","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["lion","alpha","animal","face","Leo","mane","order","rawr","roar","safari","strong","zodiac"]}, + {"emoji":"🐯","name":"tiger face","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["tiger","face","animal","big","cat","predator"]}, + {"emoji":"🐅","name":"tiger","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["tiger","animal","big","cat","predator","zoo"]}, + {"emoji":"🐆","name":"leopard","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["leopard","animal","big","cat","predator","zoo"]}, + {"emoji":"🐴","name":"horse face","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["horse","face","animal","dressage","equine","farm","horses"]}, + {"emoji":"🫎","name":"moose","category":"Animals & Nature","version":"15.0","skinTone":false,"keywords":["moose","alces","animal","antlers","elk","mammal"]}, + {"emoji":"🫏","name":"donkey","category":"Animals & Nature","version":"15.0","skinTone":false,"keywords":["donkey","animal","ass","burro","hinny","mammal","mule","stubborn"]}, + {"emoji":"🐎","name":"horse","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["horse","animal","equestrian","farm","racehorse","racing"]}, + {"emoji":"🦄","name":"unicorn","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["unicorn","face"]}, + {"emoji":"🦓","name":"zebra","category":"Animals & Nature","version":"5.0","skinTone":false,"keywords":["zebra","animal","stripe"]}, + {"emoji":"🦌","name":"deer","category":"Animals & Nature","version":"3.0","skinTone":false,"keywords":["deer","animal"]}, + {"emoji":"🦬","name":"bison","category":"Animals & Nature","version":"13.0","skinTone":false,"keywords":["bison","animal","buffalo","herd","wisent"]}, + {"emoji":"🐮","name":"cow face","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["cow","face","animal","farm","milk","moo"]}, + {"emoji":"🐂","name":"ox","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["ox","animal","animals","bull","farm","Taurus","zodiac"]}, + {"emoji":"🐃","name":"water buffalo","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["water","buffalo","animal","zoo"]}, + {"emoji":"🐄","name":"cow","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["cow","animal","animals","farm","milk","moo"]}, + {"emoji":"🐷","name":"pig face","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["pig","face","animal","bacon","farm","pork"]}, + {"emoji":"🐖","name":"pig","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["pig","animal","bacon","farm","pork","sow"]}, + {"emoji":"🐗","name":"boar","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["boar","animal","pig"]}, + {"emoji":"🐽","name":"pig nose","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["pig","nose","animal","face","farm","smell","snout"]}, + {"emoji":"🐏","name":"ram","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["ram","animal","Aries","horns","male","sheep","zodiac","zoo"]}, + {"emoji":"🐑","name":"ewe","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["ewe","animal","baa","farm","female","fluffy","lamb","sheep","wool"]}, + {"emoji":"🐐","name":"goat","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["goat","animal","Capricorn","farm","milk","zodiac"]}, + {"emoji":"🐪","name":"camel","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["camel","animal","desert","dromedary","hump","one"]}, + {"emoji":"🐫","name":"two-hump camel","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["two-hump","camel","animal","bactrian","desert","hump","two"]}, + {"emoji":"🦙","name":"llama","category":"Animals & Nature","version":"11.0","skinTone":false,"keywords":["llama","alpaca","animal","guanaco","vicuña","wool"]}, + {"emoji":"🦒","name":"giraffe","category":"Animals & Nature","version":"5.0","skinTone":false,"keywords":["giraffe","animal","spots"]}, + {"emoji":"🐘","name":"elephant","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["elephant","animal"]}, + {"emoji":"🦣","name":"mammoth","category":"Animals & Nature","version":"13.0","skinTone":false,"keywords":["mammoth","animal","extinction","large","tusk","wooly"]}, + {"emoji":"🦏","name":"rhinoceros","category":"Animals & Nature","version":"3.0","skinTone":false,"keywords":["rhinoceros","animal"]}, + {"emoji":"🦛","name":"hippopotamus","category":"Animals & Nature","version":"11.0","skinTone":false,"keywords":["hippopotamus","animal","hippo"]}, + {"emoji":"🐭","name":"mouse face","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["mouse","face","animal"]}, + {"emoji":"🐁","name":"mouse","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["mouse","animal","animals"]}, + {"emoji":"🐀","name":"rat","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["rat","animal"]}, + {"emoji":"🐹","name":"hamster","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["hamster","animal","face","pet"]}, + {"emoji":"🐰","name":"rabbit face","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["rabbit","face","animal","bunny","pet"]}, + {"emoji":"🐇","name":"rabbit","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["rabbit","animal","bunny","pet"]}, + {"emoji":"🐿️","name":"chipmunk","category":"Animals & Nature","version":"0.7","skinTone":false,"keywords":["chipmunk"]}, + {"emoji":"🦫","name":"beaver","category":"Animals & Nature","version":"13.0","skinTone":false,"keywords":["beaver","animal","dam","teeth"]}, + {"emoji":"🦔","name":"hedgehog","category":"Animals & Nature","version":"5.0","skinTone":false,"keywords":["hedgehog","animal","spiny"]}, + {"emoji":"🦇","name":"bat","category":"Animals & Nature","version":"3.0","skinTone":false,"keywords":["bat","animal","vampire"]}, + {"emoji":"🐻","name":"bear","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["bear","animal","face","grizzly","growl","honey"]}, + {"emoji":"🐻‍❄️","name":"polar bear","category":"Animals & Nature","version":"13.0","skinTone":false,"keywords":["polar","bear"]}, + {"emoji":"🐨","name":"koala","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["koala","animal","australia","bear","down","face","marsupial","under"]}, + {"emoji":"🐼","name":"panda","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["panda","animal","bamboo","face"]}, + {"emoji":"🦥","name":"sloth","category":"Animals & Nature","version":"12.0","skinTone":false,"keywords":["sloth","lazy","slow"]}, + {"emoji":"🦦","name":"otter","category":"Animals & Nature","version":"12.0","skinTone":false,"keywords":["otter","animal","fishing","playful"]}, + {"emoji":"🦨","name":"skunk","category":"Animals & Nature","version":"12.0","skinTone":false,"keywords":["skunk","animal","stink"]}, + {"emoji":"🦘","name":"kangaroo","category":"Animals & Nature","version":"11.0","skinTone":false,"keywords":["kangaroo","animal","joey","jump","marsupial"]}, + {"emoji":"🦡","name":"badger","category":"Animals & Nature","version":"11.0","skinTone":false,"keywords":["badger","animal","honey","pester"]}, + {"emoji":"🐾","name":"paw prints","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["paw","prints","feet","paws","print"]}, + {"emoji":"🦃","name":"turkey","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["turkey","bird","gobble","thanksgiving"]}, + {"emoji":"🐔","name":"chicken","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["chicken","animal","bird","ornithology"]}, + {"emoji":"🐓","name":"rooster","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["rooster","animal","bird","ornithology"]}, + {"emoji":"🐣","name":"hatching chick","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["hatching","chick","animal","baby","bird","egg"]}, + {"emoji":"🐤","name":"baby chick","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["baby","chick","animal","bird","ornithology"]}, + {"emoji":"🐥","name":"front-facing baby chick","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["front-facing","baby","chick","animal","bird","newborn","ornithology"]}, + {"emoji":"🐦","name":"bird","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["bird","animal","ornithology"]}, + {"emoji":"🐧","name":"penguin","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["penguin","animal","antarctica","bird","ornithology"]}, + {"emoji":"🕊️","name":"dove","category":"Animals & Nature","version":"0.7","skinTone":false,"keywords":["dove"]}, + {"emoji":"🦅","name":"eagle","category":"Animals & Nature","version":"3.0","skinTone":false,"keywords":["eagle","animal","bird","ornithology"]}, + {"emoji":"🦆","name":"duck","category":"Animals & Nature","version":"3.0","skinTone":false,"keywords":["duck","animal","bird","ornithology"]}, + {"emoji":"🦢","name":"swan","category":"Animals & Nature","version":"11.0","skinTone":false,"keywords":["swan","animal","bird","cygnet","duckling","ornithology","ugly"]}, + {"emoji":"🦉","name":"owl","category":"Animals & Nature","version":"3.0","skinTone":false,"keywords":["owl","animal","bird","ornithology","wise"]}, + {"emoji":"🦤","name":"dodo","category":"Animals & Nature","version":"13.0","skinTone":false,"keywords":["dodo","animal","bird","extinction","large","ornithology"]}, + {"emoji":"🪶","name":"feather","category":"Animals & Nature","version":"13.0","skinTone":false,"keywords":["feather","bird","flight","light","plumage"]}, + {"emoji":"🦩","name":"flamingo","category":"Animals & Nature","version":"12.0","skinTone":false,"keywords":["flamingo","animal","bird","flamboyant","ornithology","tropical"]}, + {"emoji":"🦚","name":"peacock","category":"Animals & Nature","version":"11.0","skinTone":false,"keywords":["peacock","animal","bird","colorful","ornithology","ostentatious","peahen","pretty","proud"]}, + {"emoji":"🦜","name":"parrot","category":"Animals & Nature","version":"11.0","skinTone":false,"keywords":["parrot","animal","bird","ornithology","pirate","talk"]}, + {"emoji":"🪽","name":"wing","category":"Animals & Nature","version":"15.0","skinTone":false,"keywords":["wing","angelic","ascend","aviation","bird","fly","flying","heavenly","mythology","soar"]}, + {"emoji":"🐦‍⬛","name":"black bird","category":"Animals & Nature","version":"15.0","skinTone":false,"keywords":["black","bird","animal","beak","caw","corvid","crow","ornithology","raven","rook"]}, + {"emoji":"🪿","name":"goose","category":"Animals & Nature","version":"15.0","skinTone":false,"keywords":["goose","animal","bird","duck","flock","fowl","gaggle","gander","geese","honk","ornithology","silly"]}, + {"emoji":"🐦‍🔥","name":"phoenix","category":"Animals & Nature","version":"15.1","skinTone":false,"keywords":["phoenix","ascend","ascension","emerge","fantasy","firebird","glory","immortal","rebirth","reincarnation","reinvent","renewal","revival","revive","rise","transform"]}, + {"emoji":"🐸","name":"frog","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["frog","animal","face"]}, + {"emoji":"🐊","name":"crocodile","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["crocodile","animal","zoo"]}, + {"emoji":"🐢","name":"turtle","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["turtle","animal","terrapin","tortoise"]}, + {"emoji":"🦎","name":"lizard","category":"Animals & Nature","version":"3.0","skinTone":false,"keywords":["lizard","animal","reptile"]}, + {"emoji":"🐍","name":"snake","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["snake","animal","bearer","Ophiuchus","serpent","zodiac"]}, + {"emoji":"🐲","name":"dragon face","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["dragon","face","animal","fairy","fairytale","tale"]}, + {"emoji":"🐉","name":"dragon","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["dragon","animal","fairy","fairytale","knights","tale"]}, + {"emoji":"🦕","name":"sauropod","category":"Animals & Nature","version":"5.0","skinTone":false,"keywords":["sauropod","brachiosaurus","brontosaurus","dinosaur","diplodocus"]}, + {"emoji":"🦖","name":"T-Rex","category":"Animals & Nature","version":"5.0","skinTone":false,"keywords":["T-Rex","dinosaur","Rex","T","Tyrannosaurus"]}, + {"emoji":"🐳","name":"spouting whale","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["spouting","whale","animal","beach","face","ocean"]}, + {"emoji":"🐋","name":"whale","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["whale","animal","beach","ocean"]}, + {"emoji":"🐬","name":"dolphin","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["dolphin","animal","beach","flipper","ocean"]}, + {"emoji":"🫍","name":"orca","category":"Animals & Nature","version":"17.0","skinTone":false,"keywords":["orca","marine","ocean","whale"]}, + {"emoji":"🦭","name":"seal","category":"Animals & Nature","version":"13.0","skinTone":false,"keywords":["seal","animal","lion","ocean","sea"]}, + {"emoji":"🐟","name":"fish","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["fish","animal","dinner","fishes","fishing","Pisces","zodiac"]}, + {"emoji":"🐠","name":"tropical fish","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["tropical","fish","animal","fishes"]}, + {"emoji":"🐡","name":"blowfish","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["blowfish","animal","fish"]}, + {"emoji":"🦈","name":"shark","category":"Animals & Nature","version":"3.0","skinTone":false,"keywords":["shark","animal","fish"]}, + {"emoji":"🐙","name":"octopus","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["octopus","animal","creature","ocean"]}, + {"emoji":"🐚","name":"spiral shell","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["spiral","shell","animal","beach","conch","sea"]}, + {"emoji":"🪸","name":"coral","category":"Animals & Nature","version":"14.0","skinTone":false,"keywords":["coral","change","climate","ocean","reef","sea"]}, + {"emoji":"🪼","name":"jellyfish","category":"Animals & Nature","version":"15.0","skinTone":false,"keywords":["jellyfish","animal","aquarium","burn","invertebrate","jelly","life","marine","ocean","ouch","plankton","sea","sting","stinger","tentacles"]}, + {"emoji":"🦀","name":"crab","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["crab","Cancer","zodiac"]}, + {"emoji":"🦞","name":"lobster","category":"Animals & Nature","version":"11.0","skinTone":false,"keywords":["lobster","animal","bisque","claws","seafood"]}, + {"emoji":"🦐","name":"shrimp","category":"Animals & Nature","version":"3.0","skinTone":false,"keywords":["shrimp","food","shellfish","small"]}, + {"emoji":"🦑","name":"squid","category":"Animals & Nature","version":"3.0","skinTone":false,"keywords":["squid","animal","food","mollusk"]}, + {"emoji":"🦪","name":"oyster","category":"Animals & Nature","version":"12.0","skinTone":false,"keywords":["oyster","diving","pearl"]}, + {"emoji":"🐌","name":"snail","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["snail","animal","escargot","garden","nature","slug"]}, + {"emoji":"🦋","name":"butterfly","category":"Animals & Nature","version":"3.0","skinTone":false,"keywords":["butterfly","insect","pretty"]}, + {"emoji":"🐛","name":"bug","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["bug","animal","garden","insect"]}, + {"emoji":"🐜","name":"ant","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["ant","animal","garden","insect"]}, + {"emoji":"🐝","name":"honeybee","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["honeybee","animal","bee","bumblebee","honey","insect","nature","spring"]}, + {"emoji":"🪲","name":"beetle","category":"Animals & Nature","version":"13.0","skinTone":false,"keywords":["beetle","animal","bug","insect"]}, + {"emoji":"🐞","name":"lady beetle","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["lady","beetle","animal","garden","insect","ladybird","ladybug","nature"]}, + {"emoji":"🦗","name":"cricket","category":"Animals & Nature","version":"5.0","skinTone":false,"keywords":["cricket","animal","bug","grasshopper","insect","Orthoptera"]}, + {"emoji":"🪳","name":"cockroach","category":"Animals & Nature","version":"13.0","skinTone":false,"keywords":["cockroach","animal","insect","pest","roach"]}, + {"emoji":"🕷️","name":"spider","category":"Animals & Nature","version":"0.7","skinTone":false,"keywords":["spider"]}, + {"emoji":"🕸️","name":"spider web","category":"Animals & Nature","version":"0.7","skinTone":false,"keywords":["spider","web"]}, + {"emoji":"🦂","name":"scorpion","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["scorpion","Scorpio","Scorpius","zodiac"]}, + {"emoji":"🦟","name":"mosquito","category":"Animals & Nature","version":"11.0","skinTone":false,"keywords":["mosquito","bite","disease","fever","insect","malaria","pest","virus"]}, + {"emoji":"🪰","name":"fly","category":"Animals & Nature","version":"13.0","skinTone":false,"keywords":["fly","animal","disease","insect","maggot","pest","rotting"]}, + {"emoji":"🪱","name":"worm","category":"Animals & Nature","version":"13.0","skinTone":false,"keywords":["worm","animal","annelid","earthworm","parasite"]}, + {"emoji":"🦠","name":"microbe","category":"Animals & Nature","version":"11.0","skinTone":false,"keywords":["microbe","amoeba","bacteria","science","virus"]}, + {"emoji":"💐","name":"bouquet","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["bouquet","anniversary","birthday","date","flower","love","plant","romance"]}, + {"emoji":"🌸","name":"cherry blossom","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["cherry","blossom","flower","plant","spring","springtime"]}, + {"emoji":"💮","name":"white flower","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["white","flower"]}, + {"emoji":"🪷","name":"lotus","category":"Animals & Nature","version":"14.0","skinTone":false,"keywords":["lotus","beauty","Buddhism","calm","flower","Hinduism","peace","purity","serenity"]}, + {"emoji":"🏵️","name":"rosette","category":"Animals & Nature","version":"0.7","skinTone":false,"keywords":["rosette"]}, + {"emoji":"🌹","name":"rose","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["rose","beauty","elegant","flower","love","plant","red","valentine"]}, + {"emoji":"🥀","name":"wilted flower","category":"Animals & Nature","version":"3.0","skinTone":false,"keywords":["wilted","flower","dying"]}, + {"emoji":"🌺","name":"hibiscus","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["hibiscus","flower","plant"]}, + {"emoji":"🌻","name":"sunflower","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["sunflower","flower","outdoors","plant","sun"]}, + {"emoji":"🌼","name":"blossom","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["blossom","buttercup","dandelion","flower","plant"]}, + {"emoji":"🌷","name":"tulip","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["tulip","blossom","flower","growth","plant"]}, + {"emoji":"🪻","name":"hyacinth","category":"Animals & Nature","version":"15.0","skinTone":false,"keywords":["hyacinth","bloom","bluebonnet","flower","indigo","lavender","lilac","lupine","plant","purple","shrub","snapdragon","spring","violet"]}, + {"emoji":"🌱","name":"seedling","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["seedling","plant","sapling","sprout","young"]}, + {"emoji":"🪴","name":"potted plant","category":"Animals & Nature","version":"13.0","skinTone":false,"keywords":["potted","plant","decor","grow","house","nurturing","pot"]}, + {"emoji":"🌲","name":"evergreen tree","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["evergreen","tree","christmas","forest","pine"]}, + {"emoji":"🌳","name":"deciduous tree","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["deciduous","tree","forest","green","habitat","shedding"]}, + {"emoji":"🌴","name":"palm tree","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["palm","tree","beach","plant","tropical"]}, + {"emoji":"🌵","name":"cactus","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["cactus","desert","drought","nature","plant"]}, + {"emoji":"🌾","name":"sheaf of rice","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["sheaf","of","rice","ear","grain","grains","plant"]}, + {"emoji":"🌿","name":"herb","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["herb","leaf","plant"]}, + {"emoji":"☘️","name":"shamrock","category":"Animals & Nature","version":"1.0","skinTone":false,"keywords":["shamrock"]}, + {"emoji":"🍀","name":"four leaf clover","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["four","leaf","clover","4","four-leaf","irish","lucky","plant"]}, + {"emoji":"🍁","name":"maple leaf","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["maple","leaf","falling"]}, + {"emoji":"🍂","name":"fallen leaf","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["fallen","leaf","autumn","fall","falling"]}, + {"emoji":"🍃","name":"leaf fluttering in wind","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["leaf","fluttering","in","wind","blow","flutter"]}, + {"emoji":"🪹","name":"empty nest","category":"Animals & Nature","version":"14.0","skinTone":false,"keywords":["empty","nest","branch","home","nesting"]}, + {"emoji":"🪺","name":"nest with eggs","category":"Animals & Nature","version":"14.0","skinTone":false,"keywords":["nest","with","eggs","bird","branch","egg","nesting"]}, + {"emoji":"🍄","name":"mushroom","category":"Animals & Nature","version":"0.6","skinTone":false,"keywords":["mushroom","fungus","toadstool"]}, + {"emoji":"🪾","name":"leafless tree","category":"Animals & Nature","version":"16.0","skinTone":false,"keywords":["leafless","tree","bare","barren","branches","dead","drought","trunk","winter","wood"]}, + {"emoji":"🍇","name":"grapes","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["grapes","Dionysus","fruit","grape"]}, + {"emoji":"🍈","name":"melon","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["melon","cantaloupe","fruit"]}, + {"emoji":"🍉","name":"watermelon","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["watermelon","fruit"]}, + {"emoji":"🍊","name":"tangerine","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["tangerine","c","citrus","fruit","nectarine","orange","vitamin"]}, + {"emoji":"🍋","name":"lemon","category":"Food & Drink","version":"1.0","skinTone":false,"keywords":["lemon","citrus","fruit","sour"]}, + {"emoji":"🍋‍🟩","name":"lime","category":"Food & Drink","version":"15.1","skinTone":false,"keywords":["lime","acidity","citrus","cocktail","fruit","garnish","key","margarita","mojito","refreshing","salsa","sour","tangy","tequila","tropical","zest"]}, + {"emoji":"🍌","name":"banana","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["banana","fruit","potassium"]}, + {"emoji":"🍍","name":"pineapple","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["pineapple","colada","fruit","pina","tropical"]}, + {"emoji":"🥭","name":"mango","category":"Food & Drink","version":"11.0","skinTone":false,"keywords":["mango","food","fruit","tropical"]}, + {"emoji":"🍎","name":"red apple","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["red","apple","diet","food","fruit","health","ripe"]}, + {"emoji":"🍏","name":"green apple","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["green","apple","fruit"]}, + {"emoji":"🍐","name":"pear","category":"Food & Drink","version":"1.0","skinTone":false,"keywords":["pear","fruit"]}, + {"emoji":"🍑","name":"peach","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["peach","fruit"]}, + {"emoji":"🍒","name":"cherries","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["cherries","berries","cherry","fruit","red"]}, + {"emoji":"🍓","name":"strawberry","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["strawberry","berry","fruit"]}, + {"emoji":"🫐","name":"blueberries","category":"Food & Drink","version":"13.0","skinTone":false,"keywords":["blueberries","berries","berry","bilberry","blue","blueberry","food","fruit"]}, + {"emoji":"🥝","name":"kiwi fruit","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["kiwi","fruit","food"]}, + {"emoji":"🍅","name":"tomato","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["tomato","food","fruit","vegetable"]}, + {"emoji":"🫒","name":"olive","category":"Food & Drink","version":"13.0","skinTone":false,"keywords":["olive","food"]}, + {"emoji":"🥥","name":"coconut","category":"Food & Drink","version":"5.0","skinTone":false,"keywords":["coconut","colada","palm","piña"]}, + {"emoji":"🥑","name":"avocado","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["avocado","food","fruit"]}, + {"emoji":"🍆","name":"eggplant","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["eggplant","aubergine","vegetable"]}, + {"emoji":"🥔","name":"potato","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["potato","food","vegetable"]}, + {"emoji":"🥕","name":"carrot","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["carrot","food","vegetable"]}, + {"emoji":"🌽","name":"ear of corn","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["ear","of","corn","crops","farm","maize","maze"]}, + {"emoji":"🌶️","name":"hot pepper","category":"Food & Drink","version":"0.7","skinTone":false,"keywords":["hot","pepper"]}, + {"emoji":"🫑","name":"bell pepper","category":"Food & Drink","version":"13.0","skinTone":false,"keywords":["bell","pepper","capsicum","food","vegetable"]}, + {"emoji":"🥒","name":"cucumber","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["cucumber","food","pickle","vegetable"]}, + {"emoji":"🥬","name":"leafy green","category":"Food & Drink","version":"11.0","skinTone":false,"keywords":["leafy","green","bok","burgers","cabbage","choy","kale","lettuce","salad"]}, + {"emoji":"🥦","name":"broccoli","category":"Food & Drink","version":"5.0","skinTone":false,"keywords":["broccoli","cabbage","wild"]}, + {"emoji":"🧄","name":"garlic","category":"Food & Drink","version":"12.0","skinTone":false,"keywords":["garlic","flavoring"]}, + {"emoji":"🧅","name":"onion","category":"Food & Drink","version":"12.0","skinTone":false,"keywords":["onion","flavoring"]}, + {"emoji":"🥜","name":"peanuts","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["peanuts","food","nut","peanut","vegetable"]}, + {"emoji":"🫘","name":"beans","category":"Food & Drink","version":"14.0","skinTone":false,"keywords":["beans","food","kidney","legume","small"]}, + {"emoji":"🌰","name":"chestnut","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["chestnut","almond","plant"]}, + {"emoji":"🫚","name":"ginger root","category":"Food & Drink","version":"15.0","skinTone":false,"keywords":["ginger","root","beer","health","herb","natural","spice"]}, + {"emoji":"🫛","name":"pea pod","category":"Food & Drink","version":"15.0","skinTone":false,"keywords":["pea","pod","beans","beanstalk","edamame","legume","soybean","vegetable","veggie"]}, + {"emoji":"🍄‍🟫","name":"brown mushroom","category":"Food & Drink","version":"15.1","skinTone":false,"keywords":["brown","mushroom","food","fungi","fungus","nature","pizza","portobello","shiitake","shroom","spore","sprout","toppings","truffle","vegetable","vegetarian","veggie"]}, + {"emoji":"🫜","name":"root vegetable","category":"Food & Drink","version":"16.0","skinTone":false,"keywords":["root","vegetable","beet","food","garden","radish","salad","turnip","vegetarian"]}, + {"emoji":"🍞","name":"bread","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["bread","carbs","food","grain","loaf","restaurant","toast","wheat"]}, + {"emoji":"🥐","name":"croissant","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["croissant","bread","breakfast","crescent","food","french","roll"]}, + {"emoji":"🥖","name":"baguette bread","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["baguette","bread","food","french"]}, + {"emoji":"🫓","name":"flatbread","category":"Food & Drink","version":"13.0","skinTone":false,"keywords":["flatbread","arepa","bread","food","gordita","lavash","naan","pita"]}, + {"emoji":"🥨","name":"pretzel","category":"Food & Drink","version":"5.0","skinTone":false,"keywords":["pretzel","convoluted","twisted"]}, + {"emoji":"🥯","name":"bagel","category":"Food & Drink","version":"11.0","skinTone":false,"keywords":["bagel","bakery","bread","breakfast","schmear"]}, + {"emoji":"🥞","name":"pancakes","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["pancakes","breakfast","crêpe","food","hotcake","pancake"]}, + {"emoji":"🧇","name":"waffle","category":"Food & Drink","version":"12.0","skinTone":false,"keywords":["waffle","breakfast","indecisive","iron"]}, + {"emoji":"🧀","name":"cheese wedge","category":"Food & Drink","version":"1.0","skinTone":false,"keywords":["cheese","wedge"]}, + {"emoji":"🍖","name":"meat on bone","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["meat","on","bone"]}, + {"emoji":"🍗","name":"poultry leg","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["poultry","leg","bone","chicken","drumstick","hungry","turkey"]}, + {"emoji":"🥩","name":"cut of meat","category":"Food & Drink","version":"5.0","skinTone":false,"keywords":["cut","of","meat","chop","lambchop","porkchop","red","steak"]}, + {"emoji":"🥓","name":"bacon","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["bacon","breakfast","food","meat"]}, + {"emoji":"🍔","name":"hamburger","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["hamburger","burger","eat","fast","food","hungry"]}, + {"emoji":"🍟","name":"french fries","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["french","fries","fast","food"]}, + {"emoji":"🍕","name":"pizza","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["pizza","cheese","food","hungry","pepperoni","slice"]}, + {"emoji":"🌭","name":"hot dog","category":"Food & Drink","version":"1.0","skinTone":false,"keywords":["hot","dog","frankfurter","hotdog","sausage"]}, + {"emoji":"🥪","name":"sandwich","category":"Food & Drink","version":"5.0","skinTone":false,"keywords":["sandwich","bread"]}, + {"emoji":"🌮","name":"taco","category":"Food & Drink","version":"1.0","skinTone":false,"keywords":["taco","mexican"]}, + {"emoji":"🌯","name":"burrito","category":"Food & Drink","version":"1.0","skinTone":false,"keywords":["burrito","mexican","wrap"]}, + {"emoji":"🫔","name":"tamale","category":"Food & Drink","version":"13.0","skinTone":false,"keywords":["tamale","food","mexican","pamonha","wrapped"]}, + {"emoji":"🥙","name":"stuffed flatbread","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["stuffed","flatbread","falafel","food","gyro","kebab"]}, + {"emoji":"🧆","name":"falafel","category":"Food & Drink","version":"12.0","skinTone":false,"keywords":["falafel","chickpea","meatball"]}, + {"emoji":"🥚","name":"egg","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["egg","breakfast","food"]}, + {"emoji":"🍳","name":"cooking","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["cooking","breakfast","easy","egg","fry","frying","over","pan","restaurant","side","sunny","up"]}, + {"emoji":"🥘","name":"shallow pan of food","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["shallow","pan","of","food","casserole","paella"]}, + {"emoji":"🍲","name":"pot of food","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["pot","of","food","soup","stew"]}, + {"emoji":"🫕","name":"fondue","category":"Food & Drink","version":"13.0","skinTone":false,"keywords":["fondue","cheese","chocolate","food","melted","pot","ski"]}, + {"emoji":"🥣","name":"bowl with spoon","category":"Food & Drink","version":"5.0","skinTone":false,"keywords":["bowl","with","spoon","breakfast","cereal","congee","oatmeal","porridge"]}, + {"emoji":"🥗","name":"green salad","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["green","salad","food"]}, + {"emoji":"🍿","name":"popcorn","category":"Food & Drink","version":"1.0","skinTone":false,"keywords":["popcorn","corn","movie","pop"]}, + {"emoji":"🧈","name":"butter","category":"Food & Drink","version":"12.0","skinTone":false,"keywords":["butter","dairy"]}, + {"emoji":"🧂","name":"salt","category":"Food & Drink","version":"11.0","skinTone":false,"keywords":["salt","condiment","flavor","mad","salty","shaker","taste","upset"]}, + {"emoji":"🥫","name":"canned food","category":"Food & Drink","version":"5.0","skinTone":false,"keywords":["canned","food","can"]}, + {"emoji":"🍱","name":"bento box","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["bento","box","food"]}, + {"emoji":"🍘","name":"rice cracker","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["rice","cracker","food"]}, + {"emoji":"🍙","name":"rice ball","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["rice","ball","food","Japanese"]}, + {"emoji":"🍚","name":"cooked rice","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["cooked","rice","food"]}, + {"emoji":"🍛","name":"curry rice","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["curry","rice","food"]}, + {"emoji":"🍜","name":"steaming bowl","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["steaming","bowl","chopsticks","food","noodle","pho","ramen","soup"]}, + {"emoji":"🍝","name":"spaghetti","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["spaghetti","food","meatballs","pasta","restaurant"]}, + {"emoji":"🍠","name":"roasted sweet potato","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["roasted","sweet","potato","food"]}, + {"emoji":"🍢","name":"oden","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["oden","food","kebab","restaurant","seafood","skewer","stick"]}, + {"emoji":"🍣","name":"sushi","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["sushi","food"]}, + {"emoji":"🍤","name":"fried shrimp","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["fried","shrimp","prawn","tempura"]}, + {"emoji":"🍥","name":"fish cake with swirl","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["fish","cake","with","swirl","food","pastry","restaurant"]}, + {"emoji":"🥮","name":"moon cake","category":"Food & Drink","version":"11.0","skinTone":false,"keywords":["moon","cake","autumn","festival","yuèbǐng"]}, + {"emoji":"🍡","name":"dango","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["dango","dessert","Japanese","skewer","stick","sweet"]}, + {"emoji":"🥟","name":"dumpling","category":"Food & Drink","version":"5.0","skinTone":false,"keywords":["dumpling","empanada","gyōza","jiaozi","pierogi","potsticker"]}, + {"emoji":"🥠","name":"fortune cookie","category":"Food & Drink","version":"5.0","skinTone":false,"keywords":["fortune","cookie","prophecy"]}, + {"emoji":"🥡","name":"takeout box","category":"Food & Drink","version":"5.0","skinTone":false,"keywords":["takeout","box","chopsticks","delivery","food","oyster","pail"]}, + {"emoji":"🍦","name":"soft ice cream","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["soft","ice","cream","dessert","food","icecream","restaurant","serve","sweet"]}, + {"emoji":"🍧","name":"shaved ice","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["shaved","ice","dessert","restaurant","sweet"]}, + {"emoji":"🍨","name":"ice cream","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["ice","cream","dessert","food","restaurant","sweet"]}, + {"emoji":"🍩","name":"doughnut","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["doughnut","breakfast","dessert","donut","food","sweet"]}, + {"emoji":"🍪","name":"cookie","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["cookie","chip","chocolate","dessert","sweet"]}, + {"emoji":"🎂","name":"birthday cake","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["birthday","cake","bday","celebration","dessert","happy","pastry","sweet"]}, + {"emoji":"🍰","name":"shortcake","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["shortcake","cake","dessert","pastry","slice","sweet"]}, + {"emoji":"🧁","name":"cupcake","category":"Food & Drink","version":"11.0","skinTone":false,"keywords":["cupcake","bakery","dessert","sprinkles","sugar","sweet","treat"]}, + {"emoji":"🥧","name":"pie","category":"Food & Drink","version":"5.0","skinTone":false,"keywords":["pie","apple","filling","fruit","meat","pastry","pumpkin","slice"]}, + {"emoji":"🍫","name":"chocolate bar","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["chocolate","bar","candy","dessert","halloween","sweet","tooth"]}, + {"emoji":"🍬","name":"candy","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["candy","cavities","dessert","halloween","restaurant","sweet","tooth","wrapper"]}, + {"emoji":"🍭","name":"lollipop","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["lollipop","candy","dessert","food","restaurant","sweet"]}, + {"emoji":"🍮","name":"custard","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["custard","dessert","pudding","sweet"]}, + {"emoji":"🍯","name":"honey pot","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["honey","pot","barrel","bear","food","honeypot","jar","sweet"]}, + {"emoji":"🍼","name":"baby bottle","category":"Food & Drink","version":"1.0","skinTone":false,"keywords":["baby","bottle","babies","birth","born","drink","infant","milk","newborn"]}, + {"emoji":"🥛","name":"glass of milk","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["glass","of","milk","drink"]}, + {"emoji":"☕","name":"hot beverage","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["hot","beverage","cafe","caffeine","chai","coffee","drink","morning","steaming","tea"]}, + {"emoji":"🫖","name":"teapot","category":"Food & Drink","version":"13.0","skinTone":false,"keywords":["teapot","brew","drink","food","pot","tea"]}, + {"emoji":"🍵","name":"teacup without handle","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["teacup","without","handle","beverage","cup","drink","oolong","tea"]}, + {"emoji":"🍶","name":"sake","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["sake","bar","beverage","bottle","cup","drink","restaurant"]}, + {"emoji":"🍾","name":"bottle with popping cork","category":"Food & Drink","version":"1.0","skinTone":false,"keywords":["bottle","with","popping","cork","bar","drink"]}, + {"emoji":"🍷","name":"wine glass","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["wine","glass","alcohol","bar","beverage","booze","club","drink","drinking","drinks","restaurant"]}, + {"emoji":"🍸","name":"cocktail glass","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["cocktail","glass","alcohol","bar","booze","club","drink","drinking","drinks","mad","martini","men"]}, + {"emoji":"🍹","name":"tropical drink","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["tropical","drink","alcohol","bar","booze","club","cocktail","drinking","drinks","drunk","mai","party","tai","tropics"]}, + {"emoji":"🍺","name":"beer mug","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["beer","mug","alcohol","ale","bar","booze","drink","drinking","drinks","octoberfest","oktoberfest","pint","stein","summer"]}, + {"emoji":"🍻","name":"clinking beer mugs","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["clinking","beer","mugs","alcohol","bar","booze","bottoms","cheers","clink","drinking","drinks"]}, + {"emoji":"🥂","name":"clinking glasses","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["clinking","glasses","celebrate","clink","drink","glass"]}, + {"emoji":"🥃","name":"tumbler glass","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["tumbler","glass","liquor","scotch","shot","whiskey","whisky"]}, + {"emoji":"🫗","name":"pouring liquid","category":"Food & Drink","version":"14.0","skinTone":false,"keywords":["pouring","liquid","accident","drink","empty","glass","oops","pour","spill","water"]}, + {"emoji":"🥤","name":"cup with straw","category":"Food & Drink","version":"5.0","skinTone":false,"keywords":["cup","with","straw","drink","juice","malt","soda","soft","water"]}, + {"emoji":"🧋","name":"bubble tea","category":"Food & Drink","version":"13.0","skinTone":false,"keywords":["bubble","tea","boba","food","milk","pearl"]}, + {"emoji":"🧃","name":"beverage box","category":"Food & Drink","version":"12.0","skinTone":false,"keywords":["beverage","box","juice","straw","sweet"]}, + {"emoji":"🧉","name":"mate","category":"Food & Drink","version":"12.0","skinTone":false,"keywords":["mate","drink"]}, + {"emoji":"🧊","name":"ice","category":"Food & Drink","version":"12.0","skinTone":false,"keywords":["ice","cold","cube","iceberg"]}, + {"emoji":"🥢","name":"chopsticks","category":"Food & Drink","version":"5.0","skinTone":false,"keywords":["chopsticks","hashi","jeotgarak","kuaizi"]}, + {"emoji":"🍽️","name":"fork and knife with plate","category":"Food & Drink","version":"0.7","skinTone":false,"keywords":["fork","and","knife","with","plate"]}, + {"emoji":"🍴","name":"fork and knife","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["fork","and","knife","breakfast","breaky","cooking","cutlery","delicious","dinner","eat","feed","food","hungry","lunch","restaurant","yum","yummy"]}, + {"emoji":"🥄","name":"spoon","category":"Food & Drink","version":"3.0","skinTone":false,"keywords":["spoon","eat","tableware"]}, + {"emoji":"🔪","name":"kitchen knife","category":"Food & Drink","version":"0.6","skinTone":false,"keywords":["kitchen","knife","chef","cooking","hocho","tool","weapon"]}, + {"emoji":"🫙","name":"jar","category":"Food & Drink","version":"14.0","skinTone":false,"keywords":["jar","condiment","container","empty","nothing","sauce","store"]}, + {"emoji":"🏺","name":"amphora","category":"Food & Drink","version":"1.0","skinTone":false,"keywords":["amphora","Aquarius","cooking","drink","jug","tool","weapon","zodiac"]}, + {"emoji":"🌍","name":"globe showing Europe-Africa","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["globe","showing","Europe-Africa","Africa","earth","Europe","world"]}, + {"emoji":"🌎","name":"globe showing Americas","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["globe","showing","Americas","earth","world"]}, + {"emoji":"🌏","name":"globe showing Asia-Australia","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["globe","showing","Asia-Australia","Asia","Australia","earth","world"]}, + {"emoji":"🌐","name":"globe with meridians","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["globe","with","meridians","earth","internet","web","world","worldwide"]}, + {"emoji":"🗺️","name":"world map","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["world","map"]}, + {"emoji":"🗾","name":"map of Japan","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["map","of","Japan"]}, + {"emoji":"🧭","name":"compass","category":"Travel & Places","version":"11.0","skinTone":false,"keywords":["compass","direction","magnetic","navigation","orienteering"]}, + {"emoji":"🏔️","name":"snow-capped mountain","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["snow-capped","mountain"]}, + {"emoji":"⛰️","name":"mountain","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["mountain"]}, + {"emoji":"🛘","name":"landslide","category":"Travel & Places","version":"17.0","skinTone":false,"keywords":["landslide","avalanche","danger","disaster","earthquake","mountain","mudslide","rocks"]}, + {"emoji":"🌋","name":"volcano","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["volcano","eruption","mountain","nature"]}, + {"emoji":"🗻","name":"mount fuji","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["mount","fuji","mountain","nature"]}, + {"emoji":"🏕️","name":"camping","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["camping"]}, + {"emoji":"🏖️","name":"beach with umbrella","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["beach","with","umbrella"]}, + {"emoji":"🏜️","name":"desert","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["desert"]}, + {"emoji":"🏝️","name":"desert island","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["desert","island"]}, + {"emoji":"🏞️","name":"national park","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["national","park"]}, + {"emoji":"🏟️","name":"stadium","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["stadium"]}, + {"emoji":"🏛️","name":"classical building","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["classical","building"]}, + {"emoji":"🏗️","name":"building construction","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["building","construction"]}, + {"emoji":"🧱","name":"brick","category":"Travel & Places","version":"11.0","skinTone":false,"keywords":["brick","bricks","clay","mortar","wall"]}, + {"emoji":"🪨","name":"rock","category":"Travel & Places","version":"13.0","skinTone":false,"keywords":["rock","boulder","heavy","solid","stone","tough"]}, + {"emoji":"🪵","name":"wood","category":"Travel & Places","version":"13.0","skinTone":false,"keywords":["wood","log","lumber","timber"]}, + {"emoji":"🛖","name":"hut","category":"Travel & Places","version":"13.0","skinTone":false,"keywords":["hut","home","house","roundhouse","shelter","yurt"]}, + {"emoji":"🏘️","name":"houses","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["houses"]}, + {"emoji":"🏚️","name":"derelict house","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["derelict","house"]}, + {"emoji":"🏠","name":"house","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["house","building","country","heart","home","ranch","settle","simple","suburban","suburbia","where"]}, + {"emoji":"🏡","name":"house with garden","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["house","with","garden","building","country","heart","home","ranch","settle","simple","suburban","suburbia","where"]}, + {"emoji":"🏢","name":"office building","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["office","building","city","cubical","job"]}, + {"emoji":"🏣","name":"Japanese post office","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["Japanese","post","office","building"]}, + {"emoji":"🏤","name":"post office","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["post","office","building","European"]}, + {"emoji":"🏥","name":"hospital","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["hospital","building","doctor","medicine"]}, + {"emoji":"🏦","name":"bank","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["bank","building"]}, + {"emoji":"🏨","name":"hotel","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["hotel","building"]}, + {"emoji":"🏩","name":"love hotel","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["love","hotel","building"]}, + {"emoji":"🏪","name":"convenience store","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["convenience","store","24","building","hours"]}, + {"emoji":"🏫","name":"school","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["school","building"]}, + {"emoji":"🏬","name":"department store","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["department","store","building"]}, + {"emoji":"🏭","name":"factory","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["factory","building"]}, + {"emoji":"🏯","name":"Japanese castle","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["Japanese","castle","building"]}, + {"emoji":"🏰","name":"castle","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["castle","building","European"]}, + {"emoji":"💒","name":"wedding","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["wedding","chapel","hitched","nuptials","romance"]}, + {"emoji":"🗼","name":"Tokyo tower","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["Tokyo","tower"]}, + {"emoji":"🗽","name":"Statue of Liberty","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["Statue","of","Liberty","liberty","new","ny","nyc","statue","york"]}, + {"emoji":"⛪","name":"church","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["church","bless","chapel","Christian","cross","religion"]}, + {"emoji":"🕌","name":"mosque","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["mosque","islam","masjid","Muslim","religion"]}, + {"emoji":"🛕","name":"hindu temple","category":"Travel & Places","version":"12.0","skinTone":false,"keywords":["hindu","temple"]}, + {"emoji":"🕍","name":"synagogue","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["synagogue","Jew","Jewish","judaism","religion","temple"]}, + {"emoji":"⛩️","name":"shinto shrine","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["shinto","shrine"]}, + {"emoji":"🕋","name":"kaaba","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["kaaba","hajj","islam","Muslim","religion","umrah"]}, + {"emoji":"⛲","name":"fountain","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["fountain"]}, + {"emoji":"⛺","name":"tent","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["tent","camping"]}, + {"emoji":"🌁","name":"foggy","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["foggy","fog"]}, + {"emoji":"🌃","name":"night with stars","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["night","with","stars","star"]}, + {"emoji":"🏙️","name":"cityscape","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["cityscape"]}, + {"emoji":"🌄","name":"sunrise over mountains","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["sunrise","over","mountains","morning","sun"]}, + {"emoji":"🌅","name":"sunrise","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["sunrise","morning","nature","sun"]}, + {"emoji":"🌆","name":"cityscape at dusk","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["cityscape","at","dusk","building","city","evening","landscape","sun","sunset"]}, + {"emoji":"🌇","name":"sunset","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["sunset","building","dusk","sun"]}, + {"emoji":"🌉","name":"bridge at night","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["bridge","at","night"]}, + {"emoji":"♨️","name":"hot springs","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["hot","springs"]}, + {"emoji":"🎠","name":"carousel horse","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["carousel","horse","entertainment"]}, + {"emoji":"🛝","name":"playground slide","category":"Travel & Places","version":"14.0","skinTone":false,"keywords":["playground","slide","amusement","park","play","playing","sliding","theme"]}, + {"emoji":"🎡","name":"ferris wheel","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["ferris","wheel","amusement","park","theme"]}, + {"emoji":"🎢","name":"roller coaster","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["roller","coaster","amusement","park","theme"]}, + {"emoji":"💈","name":"barber pole","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["barber","pole","cut","fresh","haircut","shave"]}, + {"emoji":"🎪","name":"circus tent","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["circus","tent"]}, + {"emoji":"🚂","name":"locomotive","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["locomotive","caboose","engine","railway","steam","train","trains","travel"]}, + {"emoji":"🚃","name":"railway car","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["railway","car","electric","train","tram","travel","trolleybus"]}, + {"emoji":"🚄","name":"high-speed train","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["high-speed","train","railway","shinkansen","speed"]}, + {"emoji":"🚅","name":"bullet train","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["bullet","train","high-speed","nose","railway","shinkansen","speed","travel"]}, + {"emoji":"🚆","name":"train","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["train","arrived","choo","railway"]}, + {"emoji":"🚇","name":"metro","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["metro","subway","travel"]}, + {"emoji":"🚈","name":"light rail","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["light","rail","arrived","monorail","railway"]}, + {"emoji":"🚉","name":"station","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["station","railway","train"]}, + {"emoji":"🚊","name":"tram","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["tram","trolleybus"]}, + {"emoji":"🚝","name":"monorail","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["monorail","vehicle"]}, + {"emoji":"🚞","name":"mountain railway","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["mountain","railway","car","trip"]}, + {"emoji":"🚋","name":"tram car","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["tram","car","bus","trolley","trolleybus"]}, + {"emoji":"🚌","name":"bus","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["bus","school","vehicle"]}, + {"emoji":"🚍","name":"oncoming bus","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["oncoming","bus","cars"]}, + {"emoji":"🚎","name":"trolleybus","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["trolleybus","bus","tram","trolley"]}, + {"emoji":"🚐","name":"minibus","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["minibus","bus","drive","van","vehicle"]}, + {"emoji":"🚑","name":"ambulance","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["ambulance","emergency","vehicle"]}, + {"emoji":"🚒","name":"fire engine","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["fire","engine","truck"]}, + {"emoji":"🚓","name":"police car","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["police","car","5–0","cops","patrol"]}, + {"emoji":"🚔","name":"oncoming police car","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["oncoming","police","car"]}, + {"emoji":"🚕","name":"taxi","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["taxi","cab","cabbie","car","drive","vehicle","yellow"]}, + {"emoji":"🚖","name":"oncoming taxi","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["oncoming","taxi","cab","cabbie","cars","drove","hail","yellow"]}, + {"emoji":"🚗","name":"automobile","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["automobile","car","driving","vehicle"]}, + {"emoji":"🚘","name":"oncoming automobile","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["oncoming","automobile","car","cars","drove","vehicle"]}, + {"emoji":"🚙","name":"sport utility vehicle","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["sport","utility","vehicle","car","drive","recreational","sportutility"]}, + {"emoji":"🛻","name":"pickup truck","category":"Travel & Places","version":"13.0","skinTone":false,"keywords":["pickup","truck","automobile","car","flatbed","pick-up","transportation"]}, + {"emoji":"🚚","name":"delivery truck","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["delivery","truck","car","drive","vehicle"]}, + {"emoji":"🚛","name":"articulated lorry","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["articulated","lorry","car","drive","move","semi","truck","vehicle"]}, + {"emoji":"🚜","name":"tractor","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["tractor","vehicle"]}, + {"emoji":"🏎️","name":"racing car","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["racing","car"]}, + {"emoji":"🏍️","name":"motorcycle","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["motorcycle"]}, + {"emoji":"🛵","name":"motor scooter","category":"Travel & Places","version":"3.0","skinTone":false,"keywords":["motor","scooter"]}, + {"emoji":"🦽","name":"manual wheelchair","category":"Travel & Places","version":"12.0","skinTone":false,"keywords":["manual","wheelchair","accessibility"]}, + {"emoji":"🦼","name":"motorized wheelchair","category":"Travel & Places","version":"12.0","skinTone":false,"keywords":["motorized","wheelchair","accessibility"]}, + {"emoji":"🛺","name":"auto rickshaw","category":"Travel & Places","version":"12.0","skinTone":false,"keywords":["auto","rickshaw","tuk"]}, + {"emoji":"🚲","name":"bicycle","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["bicycle","bike","class","cycle","cycling","cyclist","gang","ride","spin","spinning"]}, + {"emoji":"🛴","name":"kick scooter","category":"Travel & Places","version":"3.0","skinTone":false,"keywords":["kick","scooter"]}, + {"emoji":"🛹","name":"skateboard","category":"Travel & Places","version":"11.0","skinTone":false,"keywords":["skateboard","board","skate","skater","wheels"]}, + {"emoji":"🛼","name":"roller skate","category":"Travel & Places","version":"13.0","skinTone":false,"keywords":["roller","skate","blades","skates","sport"]}, + {"emoji":"🚏","name":"bus stop","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["bus","stop","busstop"]}, + {"emoji":"🛣️","name":"motorway","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["motorway"]}, + {"emoji":"🛤️","name":"railway track","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["railway","track"]}, + {"emoji":"🛢️","name":"oil drum","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["oil","drum"]}, + {"emoji":"⛽","name":"fuel pump","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["fuel","pump","diesel","fuelpump","gas","gasoline","station"]}, + {"emoji":"🛞","name":"wheel","category":"Travel & Places","version":"14.0","skinTone":false,"keywords":["wheel","car","circle","tire","turn","vehicle"]}, + {"emoji":"🚨","name":"police car light","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["police","car","light","alarm","alert","beacon","emergency","revolving","siren"]}, + {"emoji":"🚥","name":"horizontal traffic light","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["horizontal","traffic","light","intersection","signal","stop","stoplight"]}, + {"emoji":"🚦","name":"vertical traffic light","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["vertical","traffic","light","drove","intersection","signal","stop","stoplight"]}, + {"emoji":"🛑","name":"stop sign","category":"Travel & Places","version":"3.0","skinTone":false,"keywords":["stop","sign","octagonal"]}, + {"emoji":"🚧","name":"construction","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["construction","barrier"]}, + {"emoji":"⚓","name":"anchor","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["anchor","ship","tool"]}, + {"emoji":"🛟","name":"ring buoy","category":"Travel & Places","version":"14.0","skinTone":false,"keywords":["ring","buoy","float","life","lifesaver","preserver","rescue","safety","save","saver","swim"]}, + {"emoji":"⛵","name":"sailboat","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["sailboat","boat","resort","sailing","sea","yacht"]}, + {"emoji":"🛶","name":"canoe","category":"Travel & Places","version":"3.0","skinTone":false,"keywords":["canoe","boat"]}, + {"emoji":"🚤","name":"speedboat","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["speedboat","billionaire","boat","lake","luxury","millionaire","summer","travel"]}, + {"emoji":"🛳️","name":"passenger ship","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["passenger","ship"]}, + {"emoji":"⛴️","name":"ferry","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["ferry"]}, + {"emoji":"🛥️","name":"motor boat","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["motor","boat"]}, + {"emoji":"🚢","name":"ship","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["ship","boat","passenger","travel"]}, + {"emoji":"✈️","name":"airplane","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["airplane"]}, + {"emoji":"🛩️","name":"small airplane","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["small","airplane"]}, + {"emoji":"🛫","name":"airplane departure","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["airplane","departure","aeroplane","check-in","departures","plane"]}, + {"emoji":"🛬","name":"airplane arrival","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["airplane","arrival","aeroplane","arrivals","arriving","landing","plane"]}, + {"emoji":"🪂","name":"parachute","category":"Travel & Places","version":"12.0","skinTone":false,"keywords":["parachute","hang-glide","parasail","skydive"]}, + {"emoji":"💺","name":"seat","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["seat","chair"]}, + {"emoji":"🚁","name":"helicopter","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["helicopter","copter","roflcopter","travel","vehicle"]}, + {"emoji":"🚟","name":"suspension railway","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["suspension","railway"]}, + {"emoji":"🚠","name":"mountain cableway","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["mountain","cableway","cable","gondola","lift","ski"]}, + {"emoji":"🚡","name":"aerial tramway","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["aerial","tramway","cable","car","gondola","ropeway"]}, + {"emoji":"🛰️","name":"satellite","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["satellite"]}, + {"emoji":"🚀","name":"rocket","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["rocket","launch","rockets","space","travel"]}, + {"emoji":"🛸","name":"flying saucer","category":"Travel & Places","version":"5.0","skinTone":false,"keywords":["flying","saucer","aliens","extra","terrestrial","UFO"]}, + {"emoji":"🛎️","name":"bellhop bell","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["bellhop","bell"]}, + {"emoji":"🧳","name":"luggage","category":"Travel & Places","version":"11.0","skinTone":false,"keywords":["luggage","bag","packing","roller","suitcase","travel"]}, + {"emoji":"⌛","name":"hourglass done","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["hourglass","done","sand","time","timer"]}, + {"emoji":"⏳","name":"hourglass not done","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["hourglass","not","done","flowing","hours","sand","timer","waiting","yolo"]}, + {"emoji":"⌚","name":"watch","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["watch","clock","time"]}, + {"emoji":"⏰","name":"alarm clock","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["alarm","clock","hours","hrs","late","time","waiting"]}, + {"emoji":"⏱️","name":"stopwatch","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["stopwatch"]}, + {"emoji":"⏲️","name":"timer clock","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["timer","clock"]}, + {"emoji":"🕰️","name":"mantelpiece clock","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["mantelpiece","clock"]}, + {"emoji":"🕛","name":"twelve o’clock","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["twelve","o’clock","12","12:00","clock","time"]}, + {"emoji":"🕧","name":"twelve-thirty","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["twelve-thirty","12","12:30","30","clock","thirty","time","twelve"]}, + {"emoji":"🕐","name":"one o’clock","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["one","o’clock","1","1:00","clock","time"]}, + {"emoji":"🕜","name":"one-thirty","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["one-thirty","1","1:30","30","clock","one","thirty","time"]}, + {"emoji":"🕑","name":"two o’clock","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["two","o’clock","2","2:00","clock","time"]}, + {"emoji":"🕝","name":"two-thirty","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["two-thirty","2","2:30","30","clock","thirty","time","two"]}, + {"emoji":"🕒","name":"three o’clock","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["three","o’clock","3","3:00","clock","time"]}, + {"emoji":"🕞","name":"three-thirty","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["three-thirty","3","3:30","30","clock","thirty","three","time"]}, + {"emoji":"🕓","name":"four o’clock","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["four","o’clock","4","4:00","clock","time"]}, + {"emoji":"🕟","name":"four-thirty","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["four-thirty","30","4","4:30","clock","four","thirty","time"]}, + {"emoji":"🕔","name":"five o’clock","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["five","o’clock","5","5:00","clock","time"]}, + {"emoji":"🕠","name":"five-thirty","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["five-thirty","30","5","5:30","clock","five","thirty","time"]}, + {"emoji":"🕕","name":"six o’clock","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["six","o’clock","6","6:00","clock","time"]}, + {"emoji":"🕡","name":"six-thirty","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["six-thirty","30","6","6:30","clock","six","thirty"]}, + {"emoji":"🕖","name":"seven o’clock","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["seven","o’clock","0","7","7:00","clock"]}, + {"emoji":"🕢","name":"seven-thirty","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["seven-thirty","30","7","7:30","clock","seven","thirty"]}, + {"emoji":"🕗","name":"eight o’clock","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["eight","o’clock","8","8:00","clock","time"]}, + {"emoji":"🕣","name":"eight-thirty","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["eight-thirty","30","8","8:30","clock","eight","thirty","time"]}, + {"emoji":"🕘","name":"nine o’clock","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["nine","o’clock","9","9:00","clock","time"]}, + {"emoji":"🕤","name":"nine-thirty","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["nine-thirty","30","9","9:30","clock","nine","thirty","time"]}, + {"emoji":"🕙","name":"ten o’clock","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["ten","o’clock","0","10","10:00","clock"]}, + {"emoji":"🕥","name":"ten-thirty","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["ten-thirty","10","10:30","30","clock","ten","thirty","time"]}, + {"emoji":"🕚","name":"eleven o’clock","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["eleven","o’clock","11","11:00","clock","time"]}, + {"emoji":"🕦","name":"eleven-thirty","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["eleven-thirty","11","11:30","30","clock","eleven","thirty","time"]}, + {"emoji":"🌑","name":"new moon","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["new","moon","dark","space"]}, + {"emoji":"🌒","name":"waxing crescent moon","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["waxing","crescent","moon","dreams","space"]}, + {"emoji":"🌓","name":"first quarter moon","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["first","quarter","moon","space"]}, + {"emoji":"🌔","name":"waxing gibbous moon","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["waxing","gibbous","moon","space"]}, + {"emoji":"🌕","name":"full moon","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["full","moon","space"]}, + {"emoji":"🌖","name":"waning gibbous moon","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["waning","gibbous","moon","space"]}, + {"emoji":"🌗","name":"last quarter moon","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["last","quarter","moon","space"]}, + {"emoji":"🌘","name":"waning crescent moon","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["waning","crescent","moon","space"]}, + {"emoji":"🌙","name":"crescent moon","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["crescent","moon","ramadan","space"]}, + {"emoji":"🌚","name":"new moon face","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["new","moon","face","space"]}, + {"emoji":"🌛","name":"first quarter moon face","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["first","quarter","moon","face","space"]}, + {"emoji":"🌜","name":"last quarter moon face","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["last","quarter","moon","face","dreams"]}, + {"emoji":"🌡️","name":"thermometer","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["thermometer"]}, + {"emoji":"☀️","name":"sun","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["sun"]}, + {"emoji":"🌝","name":"full moon face","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["full","moon","face","bright"]}, + {"emoji":"🌞","name":"sun with face","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["sun","with","face","beach","bright","day","heat","shine","sunny","sunshine","weather"]}, + {"emoji":"🪐","name":"ringed planet","category":"Travel & Places","version":"12.0","skinTone":false,"keywords":["ringed","planet","saturn","saturnine"]}, + {"emoji":"⭐","name":"star","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["star","astronomy","medium","stars","white"]}, + {"emoji":"🌟","name":"glowing star","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["glowing","star","glittery","glow","night","shining","sparkle","win"]}, + {"emoji":"🌠","name":"shooting star","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["shooting","star","falling","night","space"]}, + {"emoji":"🌌","name":"milky way","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["milky","way","space"]}, + {"emoji":"☁️","name":"cloud","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["cloud"]}, + {"emoji":"⛅","name":"sun behind cloud","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["sun","behind","cloud","cloudy","weather"]}, + {"emoji":"⛈️","name":"cloud with lightning and rain","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["cloud","with","lightning","and","rain"]}, + {"emoji":"🌤️","name":"sun behind small cloud","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["sun","behind","small","cloud"]}, + {"emoji":"🌥️","name":"sun behind large cloud","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["sun","behind","large","cloud"]}, + {"emoji":"🌦️","name":"sun behind rain cloud","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["sun","behind","rain","cloud"]}, + {"emoji":"🌧️","name":"cloud with rain","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["cloud","with","rain"]}, + {"emoji":"🌨️","name":"cloud with snow","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["cloud","with","snow"]}, + {"emoji":"🌩️","name":"cloud with lightning","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["cloud","with","lightning"]}, + {"emoji":"🌪️","name":"tornado","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["tornado"]}, + {"emoji":"🌫️","name":"fog","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["fog"]}, + {"emoji":"🌬️","name":"wind face","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["wind","face"]}, + {"emoji":"🌀","name":"cyclone","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["cyclone","dizzy","hurricane","twister","typhoon","weather"]}, + {"emoji":"🌈","name":"rainbow","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["rainbow","gay","genderqueer","glbt","glbtq","lesbian","lgbt","lgbtq","lgbtqia","nature","pride","queer","rain","trans","transgender","weather"]}, + {"emoji":"🌂","name":"closed umbrella","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["closed","umbrella","clothing","rain"]}, + {"emoji":"☂️","name":"umbrella","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["umbrella"]}, + {"emoji":"☔","name":"umbrella with rain drops","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["umbrella","with","rain","drops","clothing","drop","weather"]}, + {"emoji":"⛱️","name":"umbrella on ground","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["umbrella","on","ground"]}, + {"emoji":"⚡","name":"high voltage","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["high","voltage","danger","electric","electricity","lightning","nature","thunder","thunderbolt","zap"]}, + {"emoji":"❄️","name":"snowflake","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["snowflake"]}, + {"emoji":"☃️","name":"snowman","category":"Travel & Places","version":"0.7","skinTone":false,"keywords":["snowman"]}, + {"emoji":"⛄","name":"snowman without snow","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["snowman","without","snow","cold","man"]}, + {"emoji":"☄️","name":"comet","category":"Travel & Places","version":"1.0","skinTone":false,"keywords":["comet"]}, + {"emoji":"🔥","name":"fire","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["fire","af","burn","flame","hot","lit","litaf","tool"]}, + {"emoji":"💧","name":"droplet","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["droplet","cold","comic","drop","nature","sad","sweat","tear","water","weather"]}, + {"emoji":"🌊","name":"water wave","category":"Travel & Places","version":"0.6","skinTone":false,"keywords":["water","wave","nature","ocean","surf","surfer","surfing"]}, + {"emoji":"🎃","name":"jack-o-lantern","category":"Activities","version":"0.6","skinTone":false,"keywords":["jack-o-lantern","celebration","halloween","jack","lantern","pumpkin"]}, + {"emoji":"🎄","name":"Christmas tree","category":"Activities","version":"0.6","skinTone":false,"keywords":["Christmas","tree","celebration"]}, + {"emoji":"🎆","name":"fireworks","category":"Activities","version":"0.6","skinTone":false,"keywords":["fireworks","boom","celebration","entertainment","yolo"]}, + {"emoji":"🎇","name":"sparkler","category":"Activities","version":"0.6","skinTone":false,"keywords":["sparkler","boom","celebration","fireworks","sparkle"]}, + {"emoji":"🧨","name":"firecracker","category":"Activities","version":"11.0","skinTone":false,"keywords":["firecracker","dynamite","explosive","fire","fireworks","light","pop","popping","spark"]}, + {"emoji":"✨","name":"sparkles","category":"Activities","version":"0.6","skinTone":false,"keywords":["sparkles","*","magic","sparkle","star"]}, + {"emoji":"🎈","name":"balloon","category":"Activities","version":"0.6","skinTone":false,"keywords":["balloon","birthday","celebrate","celebration"]}, + {"emoji":"🎉","name":"party popper","category":"Activities","version":"0.6","skinTone":false,"keywords":["party","popper","awesome","birthday","celebrate","celebration","excited","hooray","tada","woohoo"]}, + {"emoji":"🎊","name":"confetti ball","category":"Activities","version":"0.6","skinTone":false,"keywords":["confetti","ball","celebrate","celebration","party","woohoo"]}, + {"emoji":"🎋","name":"tanabata tree","category":"Activities","version":"0.6","skinTone":false,"keywords":["tanabata","tree","banner","celebration","Japanese"]}, + {"emoji":"🎍","name":"pine decoration","category":"Activities","version":"0.6","skinTone":false,"keywords":["pine","decoration","bamboo","celebration","Japanese","plant"]}, + {"emoji":"🎎","name":"Japanese dolls","category":"Activities","version":"0.6","skinTone":false,"keywords":["Japanese","dolls","celebration","doll","festival"]}, + {"emoji":"🎏","name":"carp streamer","category":"Activities","version":"0.6","skinTone":false,"keywords":["carp","streamer","celebration"]}, + {"emoji":"🎐","name":"wind chime","category":"Activities","version":"0.6","skinTone":false,"keywords":["wind","chime","bell","celebration"]}, + {"emoji":"🎑","name":"moon viewing ceremony","category":"Activities","version":"0.6","skinTone":false,"keywords":["moon","viewing","ceremony","celebration"]}, + {"emoji":"🧧","name":"red envelope","category":"Activities","version":"11.0","skinTone":false,"keywords":["red","envelope","gift","good","hóngbāo","lai","luck","money","see"]}, + {"emoji":"🎀","name":"ribbon","category":"Activities","version":"0.6","skinTone":false,"keywords":["ribbon","celebration"]}, + {"emoji":"🎁","name":"wrapped gift","category":"Activities","version":"0.6","skinTone":false,"keywords":["wrapped","gift","birthday","bow","box","celebration","christmas","present","surprise"]}, + {"emoji":"🎗️","name":"reminder ribbon","category":"Activities","version":"0.7","skinTone":false,"keywords":["reminder","ribbon"]}, + {"emoji":"🎟️","name":"admission tickets","category":"Activities","version":"0.7","skinTone":false,"keywords":["admission","tickets"]}, + {"emoji":"🎫","name":"ticket","category":"Activities","version":"0.6","skinTone":false,"keywords":["ticket","admission","stub"]}, + {"emoji":"🎖️","name":"military medal","category":"Activities","version":"0.7","skinTone":false,"keywords":["military","medal"]}, + {"emoji":"🏆","name":"trophy","category":"Activities","version":"0.6","skinTone":false,"keywords":["trophy","champion","champs","prize","slay","sport","victory","win","winning"]}, + {"emoji":"🏅","name":"sports medal","category":"Activities","version":"1.0","skinTone":false,"keywords":["sports","medal","award","gold","winner"]}, + {"emoji":"🥇","name":"1st place medal","category":"Activities","version":"3.0","skinTone":false,"keywords":["1st","place","medal","first","gold"]}, + {"emoji":"🥈","name":"2nd place medal","category":"Activities","version":"3.0","skinTone":false,"keywords":["2nd","place","medal","second","silver"]}, + {"emoji":"🥉","name":"3rd place medal","category":"Activities","version":"3.0","skinTone":false,"keywords":["3rd","place","medal","bronze","third"]}, + {"emoji":"⚽","name":"soccer ball","category":"Activities","version":"0.6","skinTone":false,"keywords":["soccer","ball","football","futbol","sport"]}, + {"emoji":"⚾","name":"baseball","category":"Activities","version":"0.6","skinTone":false,"keywords":["baseball","ball","sport"]}, + {"emoji":"🥎","name":"softball","category":"Activities","version":"11.0","skinTone":false,"keywords":["softball","ball","glove","sports","underarm"]}, + {"emoji":"🏀","name":"basketball","category":"Activities","version":"0.6","skinTone":false,"keywords":["basketball","ball","hoop","sport"]}, + {"emoji":"🏐","name":"volleyball","category":"Activities","version":"1.0","skinTone":false,"keywords":["volleyball","ball","game"]}, + {"emoji":"🏈","name":"american football","category":"Activities","version":"0.6","skinTone":false,"keywords":["american","football","ball","bowl","sport","super"]}, + {"emoji":"🏉","name":"rugby football","category":"Activities","version":"1.0","skinTone":false,"keywords":["rugby","football","ball","sport"]}, + {"emoji":"🎾","name":"tennis","category":"Activities","version":"0.6","skinTone":false,"keywords":["tennis","ball","racquet","sport"]}, + {"emoji":"🥏","name":"flying disc","category":"Activities","version":"11.0","skinTone":false,"keywords":["flying","disc","ultimate"]}, + {"emoji":"🎳","name":"bowling","category":"Activities","version":"0.6","skinTone":false,"keywords":["bowling","ball","game","sport","strike"]}, + {"emoji":"🏏","name":"cricket game","category":"Activities","version":"1.0","skinTone":false,"keywords":["cricket","game","ball","bat"]}, + {"emoji":"🏑","name":"field hockey","category":"Activities","version":"1.0","skinTone":false,"keywords":["field","hockey","ball","game","stick"]}, + {"emoji":"🏒","name":"ice hockey","category":"Activities","version":"1.0","skinTone":false,"keywords":["ice","hockey","game","puck","stick"]}, + {"emoji":"🥍","name":"lacrosse","category":"Activities","version":"11.0","skinTone":false,"keywords":["lacrosse","ball","goal","sports","stick"]}, + {"emoji":"🏓","name":"ping pong","category":"Activities","version":"1.0","skinTone":false,"keywords":["ping","pong","ball","bat","game","paddle","pingpong","table","tennis"]}, + {"emoji":"🏸","name":"badminton","category":"Activities","version":"1.0","skinTone":false,"keywords":["badminton","birdie","game","racquet","shuttlecock"]}, + {"emoji":"🥊","name":"boxing glove","category":"Activities","version":"3.0","skinTone":false,"keywords":["boxing","glove"]}, + {"emoji":"🥋","name":"martial arts uniform","category":"Activities","version":"3.0","skinTone":false,"keywords":["martial","arts","uniform","judo","karate","taekwondo"]}, + {"emoji":"🥅","name":"goal net","category":"Activities","version":"3.0","skinTone":false,"keywords":["goal","net"]}, + {"emoji":"⛳","name":"flag in hole","category":"Activities","version":"0.6","skinTone":false,"keywords":["flag","in","hole","golf","sport"]}, + {"emoji":"⛸️","name":"ice skate","category":"Activities","version":"0.7","skinTone":false,"keywords":["ice","skate"]}, + {"emoji":"🎣","name":"fishing pole","category":"Activities","version":"0.6","skinTone":false,"keywords":["fishing","pole","entertainment","fish","sport"]}, + {"emoji":"🤿","name":"diving mask","category":"Activities","version":"12.0","skinTone":false,"keywords":["diving","mask","scuba","snorkeling"]}, + {"emoji":"🎽","name":"running shirt","category":"Activities","version":"0.6","skinTone":false,"keywords":["running","shirt","athletics","sash"]}, + {"emoji":"🎿","name":"skis","category":"Activities","version":"0.6","skinTone":false,"keywords":["skis","ski","snow","sport"]}, + {"emoji":"🛷","name":"sled","category":"Activities","version":"5.0","skinTone":false,"keywords":["sled","luge","sledge","sleigh","snow","toboggan"]}, + {"emoji":"🥌","name":"curling stone","category":"Activities","version":"5.0","skinTone":false,"keywords":["curling","stone","game","rock"]}, + {"emoji":"🎯","name":"bullseye","category":"Activities","version":"0.6","skinTone":false,"keywords":["bullseye","bull","dart","direct","entertainment","game","hit","target"]}, + {"emoji":"🪀","name":"yo-yo","category":"Activities","version":"12.0","skinTone":false,"keywords":["yo-yo","fluctuate","toy"]}, + {"emoji":"🪁","name":"kite","category":"Activities","version":"12.0","skinTone":false,"keywords":["kite","fly","soar"]}, + {"emoji":"🔫","name":"water pistol","category":"Activities","version":"0.6","skinTone":false,"keywords":["water","pistol","gun","handgun","revolver","tool","weapon"]}, + {"emoji":"🎱","name":"pool 8 ball","category":"Activities","version":"0.6","skinTone":false,"keywords":["pool","8","ball","8ball","billiard","eight","game"]}, + {"emoji":"🔮","name":"crystal ball","category":"Activities","version":"0.6","skinTone":false,"keywords":["crystal","ball","fairy","fairytale","fantasy","fortune","future","magic","tale","tool"]}, + {"emoji":"🪄","name":"magic wand","category":"Activities","version":"13.0","skinTone":false,"keywords":["magic","wand","magician","witch","wizard"]}, + {"emoji":"🎮","name":"video game","category":"Activities","version":"0.6","skinTone":false,"keywords":["video","game","controller","entertainment"]}, + {"emoji":"🕹️","name":"joystick","category":"Activities","version":"0.7","skinTone":false,"keywords":["joystick"]}, + {"emoji":"🎰","name":"slot machine","category":"Activities","version":"0.6","skinTone":false,"keywords":["slot","machine","casino","gamble","gambling","game","slots"]}, + {"emoji":"🎲","name":"game die","category":"Activities","version":"0.6","skinTone":false,"keywords":["game","die","dice","entertainment"]}, + {"emoji":"🧩","name":"puzzle piece","category":"Activities","version":"11.0","skinTone":false,"keywords":["puzzle","piece","clue","interlocking","jigsaw"]}, + {"emoji":"🧸","name":"teddy bear","category":"Activities","version":"11.0","skinTone":false,"keywords":["teddy","bear","plaything","plush","stuffed","toy"]}, + {"emoji":"🪅","name":"piñata","category":"Activities","version":"13.0","skinTone":false,"keywords":["piñata","candy","celebrate","celebration","cinco","de","festive","mayo","party","pinada","pinata"]}, + {"emoji":"🪩","name":"mirror ball","category":"Activities","version":"14.0","skinTone":false,"keywords":["mirror","ball","dance","disco","glitter","party"]}, + {"emoji":"🪆","name":"nesting dolls","category":"Activities","version":"13.0","skinTone":false,"keywords":["nesting","dolls","babooshka","baboushka","babushka","doll","matryoshka","russia"]}, + {"emoji":"♠️","name":"spade suit","category":"Activities","version":"0.6","skinTone":false,"keywords":["spade","suit"]}, + {"emoji":"♥️","name":"heart suit","category":"Activities","version":"0.6","skinTone":false,"keywords":["heart","suit"]}, + {"emoji":"♦️","name":"diamond suit","category":"Activities","version":"0.6","skinTone":false,"keywords":["diamond","suit"]}, + {"emoji":"♣️","name":"club suit","category":"Activities","version":"0.6","skinTone":false,"keywords":["club","suit"]}, + {"emoji":"♟️","name":"chess pawn","category":"Activities","version":"11.0","skinTone":false,"keywords":["chess","pawn"]}, + {"emoji":"🃏","name":"joker","category":"Activities","version":"0.6","skinTone":false,"keywords":["joker","card","game","wildcard"]}, + {"emoji":"🀄","name":"mahjong red dragon","category":"Activities","version":"0.6","skinTone":false,"keywords":["mahjong","red","dragon","game"]}, + {"emoji":"🎴","name":"flower playing cards","category":"Activities","version":"0.6","skinTone":false,"keywords":["flower","playing","cards","card","game","Japanese"]}, + {"emoji":"🎭","name":"performing arts","category":"Activities","version":"0.6","skinTone":false,"keywords":["performing","arts","actor","actress","art","entertainment","mask","theater","theatre","thespian"]}, + {"emoji":"🖼️","name":"framed picture","category":"Activities","version":"0.7","skinTone":false,"keywords":["framed","picture"]}, + {"emoji":"🎨","name":"artist palette","category":"Activities","version":"0.6","skinTone":false,"keywords":["artist","palette","art","artsy","arty","colorful","creative","entertainment","museum","painter","painting"]}, + {"emoji":"🧵","name":"thread","category":"Activities","version":"11.0","skinTone":false,"keywords":["thread","needle","sewing","spool","string"]}, + {"emoji":"🪡","name":"sewing needle","category":"Activities","version":"13.0","skinTone":false,"keywords":["sewing","needle","embroidery","sew","stitches","sutures","tailoring","thread"]}, + {"emoji":"🧶","name":"yarn","category":"Activities","version":"11.0","skinTone":false,"keywords":["yarn","ball","crochet","knit"]}, + {"emoji":"🪢","name":"knot","category":"Activities","version":"13.0","skinTone":false,"keywords":["knot","cord","rope","tangled","tie","twine","twist"]}, + {"emoji":"👓","name":"glasses","category":"Objects","version":"0.6","skinTone":false,"keywords":["glasses","clothing","eye","eyeglasses","eyewear"]}, + {"emoji":"🕶️","name":"sunglasses","category":"Objects","version":"0.7","skinTone":false,"keywords":["sunglasses"]}, + {"emoji":"🥽","name":"goggles","category":"Objects","version":"11.0","skinTone":false,"keywords":["goggles","dive","eye","protection","scuba","swimming","welding"]}, + {"emoji":"🥼","name":"lab coat","category":"Objects","version":"11.0","skinTone":false,"keywords":["lab","coat","clothes","doctor","dr","experiment","jacket","scientist","white"]}, + {"emoji":"🦺","name":"safety vest","category":"Objects","version":"12.0","skinTone":false,"keywords":["safety","vest","emergency"]}, + {"emoji":"👔","name":"necktie","category":"Objects","version":"0.6","skinTone":false,"keywords":["necktie","clothing","employed","serious","shirt","tie"]}, + {"emoji":"👕","name":"t-shirt","category":"Objects","version":"0.6","skinTone":false,"keywords":["t-shirt","blue","casual","clothes","clothing","collar","dressed","shirt","shopping","tshirt","weekend"]}, + {"emoji":"👖","name":"jeans","category":"Objects","version":"0.6","skinTone":false,"keywords":["jeans","blue","casual","clothes","clothing","denim","dressed","pants","shopping","trousers","weekend"]}, + {"emoji":"🧣","name":"scarf","category":"Objects","version":"5.0","skinTone":false,"keywords":["scarf","bundle","cold","neck","up"]}, + {"emoji":"🧤","name":"gloves","category":"Objects","version":"5.0","skinTone":false,"keywords":["gloves","hand"]}, + {"emoji":"🧥","name":"coat","category":"Objects","version":"5.0","skinTone":false,"keywords":["coat","brr","bundle","cold","jacket","up"]}, + {"emoji":"🧦","name":"socks","category":"Objects","version":"5.0","skinTone":false,"keywords":["socks","stocking"]}, + {"emoji":"👗","name":"dress","category":"Objects","version":"0.6","skinTone":false,"keywords":["dress","clothes","clothing","dressed","fancy","shopping"]}, + {"emoji":"👘","name":"kimono","category":"Objects","version":"0.6","skinTone":false,"keywords":["kimono","clothing","comfortable"]}, + {"emoji":"🥻","name":"sari","category":"Objects","version":"12.0","skinTone":false,"keywords":["sari","clothing","dress"]}, + {"emoji":"🩱","name":"one-piece swimsuit","category":"Objects","version":"12.0","skinTone":false,"keywords":["one-piece","swimsuit","bathing","suit"]}, + {"emoji":"🩲","name":"briefs","category":"Objects","version":"12.0","skinTone":false,"keywords":["briefs","bathing","one-piece","suit","swimsuit","underwear"]}, + {"emoji":"🩳","name":"shorts","category":"Objects","version":"12.0","skinTone":false,"keywords":["shorts","bathing","pants","suit","swimsuit","underwear"]}, + {"emoji":"👙","name":"bikini","category":"Objects","version":"0.6","skinTone":false,"keywords":["bikini","bathing","beach","clothing","pool","suit","swim"]}, + {"emoji":"👚","name":"woman’s clothes","category":"Objects","version":"0.6","skinTone":false,"keywords":["woman’s","clothes","blouse","clothing","collar","dress","dressed","lady","shirt","shopping","woman"]}, + {"emoji":"🪭","name":"folding hand fan","category":"Objects","version":"15.0","skinTone":false,"keywords":["folding","hand","fan","clack","clap","cool","cooling","dance","flirt","flutter","hot","shy"]}, + {"emoji":"👛","name":"purse","category":"Objects","version":"0.6","skinTone":false,"keywords":["purse","clothes","clothing","coin","dress","fancy","handbag","shopping"]}, + {"emoji":"👜","name":"handbag","category":"Objects","version":"0.6","skinTone":false,"keywords":["handbag","bag","clothes","clothing","dress","lady","purse","shopping"]}, + {"emoji":"👝","name":"clutch bag","category":"Objects","version":"0.6","skinTone":false,"keywords":["clutch","bag","clothes","clothing","dress","handbag","pouch","purse"]}, + {"emoji":"🛍️","name":"shopping bags","category":"Objects","version":"0.7","skinTone":false,"keywords":["shopping","bags"]}, + {"emoji":"🎒","name":"backpack","category":"Objects","version":"0.6","skinTone":false,"keywords":["backpack","backpacking","bag","bookbag","education","rucksack","satchel","school"]}, + {"emoji":"🩴","name":"thong sandal","category":"Objects","version":"13.0","skinTone":false,"keywords":["thong","sandal","beach","flip","flop","sandals","shoe","thongs","zōri"]}, + {"emoji":"👞","name":"man’s shoe","category":"Objects","version":"0.6","skinTone":false,"keywords":["man’s","shoe","brown","clothes","clothing","feet","foot","kick","man","shoes","shopping"]}, + {"emoji":"👟","name":"running shoe","category":"Objects","version":"0.6","skinTone":false,"keywords":["running","shoe","athletic","clothes","clothing","fast","kick","shoes","shopping","sneaker","tennis"]}, + {"emoji":"🥾","name":"hiking boot","category":"Objects","version":"11.0","skinTone":false,"keywords":["hiking","boot","backpacking","brown","camping","outdoors","shoe"]}, + {"emoji":"🥿","name":"flat shoe","category":"Objects","version":"11.0","skinTone":false,"keywords":["flat","shoe","ballet","comfy","flats","slip-on","slipper"]}, + {"emoji":"👠","name":"high-heeled shoe","category":"Objects","version":"0.6","skinTone":false,"keywords":["high-heeled","shoe","clothes","clothing","dress","fashion","heel","heels","shoes","shopping","stiletto","woman"]}, + {"emoji":"👡","name":"woman’s sandal","category":"Objects","version":"0.6","skinTone":false,"keywords":["woman’s","sandal","clothing","shoe","woman"]}, + {"emoji":"🩰","name":"ballet shoes","category":"Objects","version":"12.0","skinTone":false,"keywords":["ballet","shoes","dance"]}, + {"emoji":"👢","name":"woman’s boot","category":"Objects","version":"0.6","skinTone":false,"keywords":["woman’s","boot","clothes","clothing","dress","shoe","shoes","shopping","woman"]}, + {"emoji":"🪮","name":"hair pick","category":"Objects","version":"15.0","skinTone":false,"keywords":["hair","pick","Afro","comb","groom"]}, + {"emoji":"👑","name":"crown","category":"Objects","version":"0.6","skinTone":false,"keywords":["crown","clothing","family","king","medieval","queen","royal","royalty","win"]}, + {"emoji":"👒","name":"woman’s hat","category":"Objects","version":"0.6","skinTone":false,"keywords":["woman’s","hat","clothes","clothing","garden","hats","party","woman"]}, + {"emoji":"🎩","name":"top hat","category":"Objects","version":"0.6","skinTone":false,"keywords":["top","hat","clothes","clothing","fancy","formal","magic","tophat"]}, + {"emoji":"🎓","name":"graduation cap","category":"Objects","version":"0.6","skinTone":false,"keywords":["graduation","cap","celebration","clothing","education","hat","scholar"]}, + {"emoji":"🧢","name":"billed cap","category":"Objects","version":"5.0","skinTone":false,"keywords":["billed","cap","baseball","bent","dad","hat"]}, + {"emoji":"🪖","name":"military helmet","category":"Objects","version":"13.0","skinTone":false,"keywords":["military","helmet","army","soldier","war","warrior"]}, + {"emoji":"⛑️","name":"rescue worker’s helmet","category":"Objects","version":"0.7","skinTone":false,"keywords":["rescue","worker’s","helmet"]}, + {"emoji":"📿","name":"prayer beads","category":"Objects","version":"1.0","skinTone":false,"keywords":["prayer","beads","clothing","necklace","religion"]}, + {"emoji":"💄","name":"lipstick","category":"Objects","version":"0.6","skinTone":false,"keywords":["lipstick","cosmetics","date","makeup"]}, + {"emoji":"💍","name":"ring","category":"Objects","version":"0.6","skinTone":false,"keywords":["ring","diamond","engaged","engagement","married","romance","shiny","sparkling","wedding"]}, + {"emoji":"💎","name":"gem stone","category":"Objects","version":"0.6","skinTone":false,"keywords":["gem","stone","diamond","engagement","jewel","money","romance","wedding"]}, + {"emoji":"🔇","name":"muted speaker","category":"Objects","version":"1.0","skinTone":false,"keywords":["muted","speaker","mute","quiet","silent","sound"]}, + {"emoji":"🔈","name":"speaker low volume","category":"Objects","version":"0.7","skinTone":false,"keywords":["speaker","low","volume","soft","sound"]}, + {"emoji":"🔉","name":"speaker medium volume","category":"Objects","version":"1.0","skinTone":false,"keywords":["speaker","medium","volume","sound"]}, + {"emoji":"🔊","name":"speaker high volume","category":"Objects","version":"0.6","skinTone":false,"keywords":["speaker","high","volume","loud","music","sound"]}, + {"emoji":"📢","name":"loudspeaker","category":"Objects","version":"0.6","skinTone":false,"keywords":["loudspeaker","address","communication","loud","public","sound"]}, + {"emoji":"📣","name":"megaphone","category":"Objects","version":"0.6","skinTone":false,"keywords":["megaphone","cheering","sound"]}, + {"emoji":"📯","name":"postal horn","category":"Objects","version":"1.0","skinTone":false,"keywords":["postal","horn","post"]}, + {"emoji":"🔔","name":"bell","category":"Objects","version":"0.6","skinTone":false,"keywords":["bell","break","church","sound"]}, + {"emoji":"🔕","name":"bell with slash","category":"Objects","version":"1.0","skinTone":false,"keywords":["bell","with","slash","forbidden","mute","no","not","prohibited","quiet","silent","sound"]}, + {"emoji":"🎼","name":"musical score","category":"Objects","version":"0.6","skinTone":false,"keywords":["musical","score","music","note"]}, + {"emoji":"🎵","name":"musical note","category":"Objects","version":"0.6","skinTone":false,"keywords":["musical","note","music","sound"]}, + {"emoji":"🎶","name":"musical notes","category":"Objects","version":"0.6","skinTone":false,"keywords":["musical","notes","music","note","sound"]}, + {"emoji":"🎙️","name":"studio microphone","category":"Objects","version":"0.7","skinTone":false,"keywords":["studio","microphone"]}, + {"emoji":"🎚️","name":"level slider","category":"Objects","version":"0.7","skinTone":false,"keywords":["level","slider"]}, + {"emoji":"🎛️","name":"control knobs","category":"Objects","version":"0.7","skinTone":false,"keywords":["control","knobs"]}, + {"emoji":"🎤","name":"microphone","category":"Objects","version":"0.6","skinTone":false,"keywords":["microphone","karaoke","mic","music","sing","sound"]}, + {"emoji":"🎧","name":"headphone","category":"Objects","version":"0.6","skinTone":false,"keywords":["headphone","earbud","sound"]}, + {"emoji":"📻","name":"radio","category":"Objects","version":"0.6","skinTone":false,"keywords":["radio","entertainment","tbt","video"]}, + {"emoji":"🎷","name":"saxophone","category":"Objects","version":"0.6","skinTone":false,"keywords":["saxophone","instrument","music","sax"]}, + {"emoji":"🎺","name":"trumpet","category":"Objects","version":"0.6","skinTone":false,"keywords":["trumpet","instrument","music"]}, + {"emoji":"🪊","name":"trombone","category":"Objects","version":"17.0","skinTone":false,"keywords":["trombone","brass","instrument","jazz","music","sad","slide"]}, + {"emoji":"🪗","name":"accordion","category":"Objects","version":"13.0","skinTone":false,"keywords":["accordion","box","concertina","instrument","music","squeeze","squeezebox"]}, + {"emoji":"🎸","name":"guitar","category":"Objects","version":"0.6","skinTone":false,"keywords":["guitar","instrument","music","strat"]}, + {"emoji":"🎹","name":"musical keyboard","category":"Objects","version":"0.6","skinTone":false,"keywords":["musical","keyboard","instrument","music","piano"]}, + {"emoji":"🎻","name":"violin","category":"Objects","version":"0.6","skinTone":false,"keywords":["violin","instrument","music"]}, + {"emoji":"🪕","name":"banjo","category":"Objects","version":"12.0","skinTone":false,"keywords":["banjo","music","stringed"]}, + {"emoji":"🥁","name":"drum","category":"Objects","version":"3.0","skinTone":false,"keywords":["drum","drumsticks","music"]}, + {"emoji":"🪘","name":"long drum","category":"Objects","version":"13.0","skinTone":false,"keywords":["long","drum","beat","conga","instrument","rhythm"]}, + {"emoji":"🪇","name":"maracas","category":"Objects","version":"15.0","skinTone":false,"keywords":["maracas","cha","dance","instrument","music","party","percussion","rattle","shake","shaker"]}, + {"emoji":"🪈","name":"flute","category":"Objects","version":"15.0","skinTone":false,"keywords":["flute","band","fife","flautist","instrument","marching","music","orchestra","piccolo","pipe","recorder","woodwind"]}, + {"emoji":"🪉","name":"harp","category":"Objects","version":"16.0","skinTone":false,"keywords":["harp","cupid","instrument","love","music","orchestra"]}, + {"emoji":"📱","name":"mobile phone","category":"Objects","version":"0.6","skinTone":false,"keywords":["mobile","phone","cell","communication","telephone"]}, + {"emoji":"📲","name":"mobile phone with arrow","category":"Objects","version":"0.6","skinTone":false,"keywords":["mobile","phone","with","arrow","build","call","cell","communication","receive","telephone"]}, + {"emoji":"☎️","name":"telephone","category":"Objects","version":"0.6","skinTone":false,"keywords":["telephone"]}, + {"emoji":"📞","name":"telephone receiver","category":"Objects","version":"0.6","skinTone":false,"keywords":["telephone","receiver","communication","phone","voip"]}, + {"emoji":"📟","name":"pager","category":"Objects","version":"0.6","skinTone":false,"keywords":["pager","communication"]}, + {"emoji":"📠","name":"fax machine","category":"Objects","version":"0.6","skinTone":false,"keywords":["fax","machine","communication"]}, + {"emoji":"🔋","name":"battery","category":"Objects","version":"0.6","skinTone":false,"keywords":["battery"]}, + {"emoji":"🪫","name":"low battery","category":"Objects","version":"14.0","skinTone":false,"keywords":["low","battery","drained","electronic","energy","power"]}, + {"emoji":"🔌","name":"electric plug","category":"Objects","version":"0.6","skinTone":false,"keywords":["electric","plug","electricity"]}, + {"emoji":"💻","name":"laptop","category":"Objects","version":"0.6","skinTone":false,"keywords":["laptop","computer","office","pc","personal"]}, + {"emoji":"🖥️","name":"desktop computer","category":"Objects","version":"0.7","skinTone":false,"keywords":["desktop","computer"]}, + {"emoji":"🖨️","name":"printer","category":"Objects","version":"0.7","skinTone":false,"keywords":["printer"]}, + {"emoji":"⌨️","name":"keyboard","category":"Objects","version":"1.0","skinTone":false,"keywords":["keyboard"]}, + {"emoji":"🖱️","name":"computer mouse","category":"Objects","version":"0.7","skinTone":false,"keywords":["computer","mouse"]}, + {"emoji":"🖲️","name":"trackball","category":"Objects","version":"0.7","skinTone":false,"keywords":["trackball"]}, + {"emoji":"💽","name":"computer disk","category":"Objects","version":"0.6","skinTone":false,"keywords":["computer","disk","minidisk","optical"]}, + {"emoji":"💾","name":"floppy disk","category":"Objects","version":"0.6","skinTone":false,"keywords":["floppy","disk","computer"]}, + {"emoji":"💿","name":"optical disk","category":"Objects","version":"0.6","skinTone":false,"keywords":["optical","disk","blu-ray","CD","computer","dvd"]}, + {"emoji":"📀","name":"dvd","category":"Objects","version":"0.6","skinTone":false,"keywords":["dvd","Blu-ray","cd","computer","disk","DVD","optical"]}, + {"emoji":"🧮","name":"abacus","category":"Objects","version":"11.0","skinTone":false,"keywords":["abacus","calculation","calculator"]}, + {"emoji":"🎥","name":"movie camera","category":"Objects","version":"0.6","skinTone":false,"keywords":["movie","camera","bollywood","cinema","film","hollywood","record"]}, + {"emoji":"🎞️","name":"film frames","category":"Objects","version":"0.7","skinTone":false,"keywords":["film","frames"]}, + {"emoji":"📽️","name":"film projector","category":"Objects","version":"0.7","skinTone":false,"keywords":["film","projector"]}, + {"emoji":"🎬","name":"clapper board","category":"Objects","version":"0.6","skinTone":false,"keywords":["clapper","board","action","movie"]}, + {"emoji":"📺","name":"television","category":"Objects","version":"0.6","skinTone":false,"keywords":["television","tv","video"]}, + {"emoji":"📷","name":"camera","category":"Objects","version":"0.6","skinTone":false,"keywords":["camera","photo","selfie","snap","tbt","trip","video"]}, + {"emoji":"📸","name":"camera with flash","category":"Objects","version":"1.0","skinTone":false,"keywords":["camera","with","flash","video"]}, + {"emoji":"📹","name":"video camera","category":"Objects","version":"0.6","skinTone":false,"keywords":["video","camera","camcorder","tbt"]}, + {"emoji":"📼","name":"videocassette","category":"Objects","version":"0.6","skinTone":false,"keywords":["videocassette","old","school","tape","vcr","vhs","video"]}, + {"emoji":"🔍","name":"magnifying glass tilted left","category":"Objects","version":"0.6","skinTone":false,"keywords":["magnifying","glass","tilted","left","lab","left-pointing","science","search","tool"]}, + {"emoji":"🔎","name":"magnifying glass tilted right","category":"Objects","version":"0.6","skinTone":false,"keywords":["magnifying","glass","tilted","right","contact","lab","right-pointing","science","search","tool"]}, + {"emoji":"🕯️","name":"candle","category":"Objects","version":"0.7","skinTone":false,"keywords":["candle"]}, + {"emoji":"💡","name":"light bulb","category":"Objects","version":"0.6","skinTone":false,"keywords":["light","bulb","comic","electric","idea"]}, + {"emoji":"🔦","name":"flashlight","category":"Objects","version":"0.6","skinTone":false,"keywords":["flashlight","electric","light","tool","torch"]}, + {"emoji":"🏮","name":"red paper lantern","category":"Objects","version":"0.6","skinTone":false,"keywords":["red","paper","lantern","bar","light","restaurant"]}, + {"emoji":"🪔","name":"diya lamp","category":"Objects","version":"12.0","skinTone":false,"keywords":["diya","lamp","light","oil"]}, + {"emoji":"📔","name":"notebook with decorative cover","category":"Objects","version":"0.6","skinTone":false,"keywords":["notebook","with","decorative","cover","book","decorated","education","school","writing"]}, + {"emoji":"📕","name":"closed book","category":"Objects","version":"0.6","skinTone":false,"keywords":["closed","book","education"]}, + {"emoji":"📖","name":"open book","category":"Objects","version":"0.6","skinTone":false,"keywords":["open","book","education","fantasy","knowledge","library","novels","reading"]}, + {"emoji":"📗","name":"green book","category":"Objects","version":"0.6","skinTone":false,"keywords":["green","book","education","fantasy","library","reading"]}, + {"emoji":"📘","name":"blue book","category":"Objects","version":"0.6","skinTone":false,"keywords":["blue","book","education","fantasy","library","reading"]}, + {"emoji":"📙","name":"orange book","category":"Objects","version":"0.6","skinTone":false,"keywords":["orange","book","education","fantasy","library","reading"]}, + {"emoji":"📚","name":"books","category":"Objects","version":"0.6","skinTone":false,"keywords":["books","book","education","fantasy","knowledge","library","novels","reading","school","study"]}, + {"emoji":"📓","name":"notebook","category":"Objects","version":"0.6","skinTone":false,"keywords":["notebook"]}, + {"emoji":"📒","name":"ledger","category":"Objects","version":"0.6","skinTone":false,"keywords":["ledger","notebook"]}, + {"emoji":"📃","name":"page with curl","category":"Objects","version":"0.6","skinTone":false,"keywords":["page","with","curl","document","paper"]}, + {"emoji":"📜","name":"scroll","category":"Objects","version":"0.6","skinTone":false,"keywords":["scroll","paper"]}, + {"emoji":"📄","name":"page facing up","category":"Objects","version":"0.6","skinTone":false,"keywords":["page","facing","up","document","paper"]}, + {"emoji":"📰","name":"newspaper","category":"Objects","version":"0.6","skinTone":false,"keywords":["newspaper","communication","news","paper"]}, + {"emoji":"🗞️","name":"rolled-up newspaper","category":"Objects","version":"0.7","skinTone":false,"keywords":["rolled-up","newspaper"]}, + {"emoji":"📑","name":"bookmark tabs","category":"Objects","version":"0.6","skinTone":false,"keywords":["bookmark","tabs","mark","marker"]}, + {"emoji":"🔖","name":"bookmark","category":"Objects","version":"0.6","skinTone":false,"keywords":["bookmark","mark"]}, + {"emoji":"🏷️","name":"label","category":"Objects","version":"0.7","skinTone":false,"keywords":["label"]}, + {"emoji":"🪙","name":"coin","category":"Objects","version":"13.0","skinTone":false,"keywords":["coin","dollar","euro","gold","metal","money","rich","silver","treasure"]}, + {"emoji":"💰","name":"money bag","category":"Objects","version":"0.6","skinTone":false,"keywords":["money","bag","bank","bet","billion","cash","cost","dollar","gold","million","moneybag","paid","paying","pot","rich","win"]}, + {"emoji":"🪎","name":"treasure chest","category":"Objects","version":"17.0","skinTone":false,"keywords":["treasure","chest","gem","gold","jewels","loot","money","prize","silver","valuables","wealth"]}, + {"emoji":"💴","name":"yen banknote","category":"Objects","version":"0.6","skinTone":false,"keywords":["yen","banknote","bank","bill","currency","money","note"]}, + {"emoji":"💵","name":"dollar banknote","category":"Objects","version":"0.6","skinTone":false,"keywords":["dollar","banknote","bank","bill","currency","money","note"]}, + {"emoji":"💶","name":"euro banknote","category":"Objects","version":"1.0","skinTone":false,"keywords":["euro","banknote","100","bank","bill","currency","money","note","rich"]}, + {"emoji":"💷","name":"pound banknote","category":"Objects","version":"1.0","skinTone":false,"keywords":["pound","banknote","bank","bill","billion","cash","currency","money","note","pounds"]}, + {"emoji":"💸","name":"money with wings","category":"Objects","version":"0.6","skinTone":false,"keywords":["money","with","wings","bank","banknote","bill","billion","cash","dollar","fly","million","note","pay"]}, + {"emoji":"💳","name":"credit card","category":"Objects","version":"0.6","skinTone":false,"keywords":["credit","card","bank","cash","charge","money","pay"]}, + {"emoji":"🧾","name":"receipt","category":"Objects","version":"11.0","skinTone":false,"keywords":["receipt","accounting","bookkeeping","evidence","invoice","proof"]}, + {"emoji":"💹","name":"chart increasing with yen","category":"Objects","version":"0.6","skinTone":false,"keywords":["chart","increasing","with","yen","bank","currency","graph","growth","market","money","rise","trend","upward"]}, + {"emoji":"✉️","name":"envelope","category":"Objects","version":"0.6","skinTone":false,"keywords":["envelope"]}, + {"emoji":"📧","name":"e-mail","category":"Objects","version":"0.6","skinTone":false,"keywords":["e-mail","email","letter","mail"]}, + {"emoji":"📨","name":"incoming envelope","category":"Objects","version":"0.6","skinTone":false,"keywords":["incoming","envelope","delivering","e-mail","email","letter","mail","receive","sent"]}, + {"emoji":"📩","name":"envelope with arrow","category":"Objects","version":"0.6","skinTone":false,"keywords":["envelope","with","arrow","communication","down","e-mail","email","letter","mail","outgoing","send","sent"]}, + {"emoji":"📤","name":"outbox tray","category":"Objects","version":"0.6","skinTone":false,"keywords":["outbox","tray","box","email","letter","mail","sent"]}, + {"emoji":"📥","name":"inbox tray","category":"Objects","version":"0.6","skinTone":false,"keywords":["inbox","tray","box","email","letter","mail","receive","zero"]}, + {"emoji":"📦","name":"package","category":"Objects","version":"0.6","skinTone":false,"keywords":["package","box","communication","delivery","parcel","shipping"]}, + {"emoji":"📫","name":"closed mailbox with raised flag","category":"Objects","version":"0.6","skinTone":false,"keywords":["closed","mailbox","with","raised","flag","communication","mail","postbox"]}, + {"emoji":"📪","name":"closed mailbox with lowered flag","category":"Objects","version":"0.6","skinTone":false,"keywords":["closed","mailbox","with","lowered","flag","mail","postbox"]}, + {"emoji":"📬","name":"open mailbox with raised flag","category":"Objects","version":"0.7","skinTone":false,"keywords":["open","mailbox","with","raised","flag","mail","postbox"]}, + {"emoji":"📭","name":"open mailbox with lowered flag","category":"Objects","version":"0.7","skinTone":false,"keywords":["open","mailbox","with","lowered","flag","mail","postbox"]}, + {"emoji":"📮","name":"postbox","category":"Objects","version":"0.6","skinTone":false,"keywords":["postbox","mail","mailbox"]}, + {"emoji":"🗳️","name":"ballot box with ballot","category":"Objects","version":"0.7","skinTone":false,"keywords":["ballot","box","with"]}, + {"emoji":"✏️","name":"pencil","category":"Objects","version":"0.6","skinTone":false,"keywords":["pencil"]}, + {"emoji":"✒️","name":"black nib","category":"Objects","version":"0.6","skinTone":false,"keywords":["black","nib"]}, + {"emoji":"🖋️","name":"fountain pen","category":"Objects","version":"0.7","skinTone":false,"keywords":["fountain","pen"]}, + {"emoji":"🖊️","name":"pen","category":"Objects","version":"0.7","skinTone":false,"keywords":["pen"]}, + {"emoji":"🖌️","name":"paintbrush","category":"Objects","version":"0.7","skinTone":false,"keywords":["paintbrush"]}, + {"emoji":"🖍️","name":"crayon","category":"Objects","version":"0.7","skinTone":false,"keywords":["crayon"]}, + {"emoji":"📝","name":"memo","category":"Objects","version":"0.6","skinTone":false,"keywords":["memo","communication","media","notes","pencil"]}, + {"emoji":"💼","name":"briefcase","category":"Objects","version":"0.6","skinTone":false,"keywords":["briefcase","office"]}, + {"emoji":"📁","name":"file folder","category":"Objects","version":"0.6","skinTone":false,"keywords":["file","folder"]}, + {"emoji":"📂","name":"open file folder","category":"Objects","version":"0.6","skinTone":false,"keywords":["open","file","folder"]}, + {"emoji":"🗂️","name":"card index dividers","category":"Objects","version":"0.7","skinTone":false,"keywords":["card","index","dividers"]}, + {"emoji":"📅","name":"calendar","category":"Objects","version":"0.6","skinTone":false,"keywords":["calendar","date"]}, + {"emoji":"📆","name":"tear-off calendar","category":"Objects","version":"0.6","skinTone":false,"keywords":["tear-off","calendar"]}, + {"emoji":"🗒️","name":"spiral notepad","category":"Objects","version":"0.7","skinTone":false,"keywords":["spiral","notepad"]}, + {"emoji":"🗓️","name":"spiral calendar","category":"Objects","version":"0.7","skinTone":false,"keywords":["spiral","calendar"]}, + {"emoji":"📇","name":"card index","category":"Objects","version":"0.6","skinTone":false,"keywords":["card","index","old","rolodex","school"]}, + {"emoji":"📈","name":"chart increasing","category":"Objects","version":"0.6","skinTone":false,"keywords":["chart","increasing","data","graph","growth","right","trend","up","upward"]}, + {"emoji":"📉","name":"chart decreasing","category":"Objects","version":"0.6","skinTone":false,"keywords":["chart","decreasing","data","down","downward","graph","negative","trend"]}, + {"emoji":"📊","name":"bar chart","category":"Objects","version":"0.6","skinTone":false,"keywords":["bar","chart","data","graph"]}, + {"emoji":"📋","name":"clipboard","category":"Objects","version":"0.6","skinTone":false,"keywords":["clipboard","do","list","notes"]}, + {"emoji":"📌","name":"pushpin","category":"Objects","version":"0.6","skinTone":false,"keywords":["pushpin","collage","pin"]}, + {"emoji":"📍","name":"round pushpin","category":"Objects","version":"0.6","skinTone":false,"keywords":["round","pushpin","location","map","pin"]}, + {"emoji":"📎","name":"paperclip","category":"Objects","version":"0.6","skinTone":false,"keywords":["paperclip"]}, + {"emoji":"🖇️","name":"linked paperclips","category":"Objects","version":"0.7","skinTone":false,"keywords":["linked","paperclips"]}, + {"emoji":"📏","name":"straight ruler","category":"Objects","version":"0.6","skinTone":false,"keywords":["straight","ruler","angle","edge","math","straightedge"]}, + {"emoji":"📐","name":"triangular ruler","category":"Objects","version":"0.6","skinTone":false,"keywords":["triangular","ruler","angle","math","rule","set","slide","triangle"]}, + {"emoji":"✂️","name":"scissors","category":"Objects","version":"0.6","skinTone":false,"keywords":["scissors"]}, + {"emoji":"🗃️","name":"card file box","category":"Objects","version":"0.7","skinTone":false,"keywords":["card","file","box"]}, + {"emoji":"🗄️","name":"file cabinet","category":"Objects","version":"0.7","skinTone":false,"keywords":["file","cabinet"]}, + {"emoji":"🗑️","name":"wastebasket","category":"Objects","version":"0.7","skinTone":false,"keywords":["wastebasket"]}, + {"emoji":"🔒","name":"locked","category":"Objects","version":"0.6","skinTone":false,"keywords":["locked","closed","lock","private"]}, + {"emoji":"🔓","name":"unlocked","category":"Objects","version":"0.6","skinTone":false,"keywords":["unlocked","cracked","lock","open","unlock"]}, + {"emoji":"🔏","name":"locked with pen","category":"Objects","version":"0.6","skinTone":false,"keywords":["locked","with","pen","ink","lock","nib","privacy"]}, + {"emoji":"🔐","name":"locked with key","category":"Objects","version":"0.6","skinTone":false,"keywords":["locked","with","key","bike","closed","lock","secure"]}, + {"emoji":"🔑","name":"key","category":"Objects","version":"0.6","skinTone":false,"keywords":["key","keys","lock","major","password","unlock"]}, + {"emoji":"🗝️","name":"old key","category":"Objects","version":"0.7","skinTone":false,"keywords":["old","key"]}, + {"emoji":"🔨","name":"hammer","category":"Objects","version":"0.6","skinTone":false,"keywords":["hammer","home","improvement","repairs","tool"]}, + {"emoji":"🪓","name":"axe","category":"Objects","version":"12.0","skinTone":false,"keywords":["axe","ax","chop","hatchet","split","wood"]}, + {"emoji":"⛏️","name":"pick","category":"Objects","version":"0.7","skinTone":false,"keywords":["pick"]}, + {"emoji":"⚒️","name":"hammer and pick","category":"Objects","version":"1.0","skinTone":false,"keywords":["hammer","and","pick"]}, + {"emoji":"🛠️","name":"hammer and wrench","category":"Objects","version":"0.7","skinTone":false,"keywords":["hammer","and","wrench"]}, + {"emoji":"🗡️","name":"dagger","category":"Objects","version":"0.7","skinTone":false,"keywords":["dagger"]}, + {"emoji":"⚔️","name":"crossed swords","category":"Objects","version":"1.0","skinTone":false,"keywords":["crossed","swords"]}, + {"emoji":"💣","name":"bomb","category":"Objects","version":"0.6","skinTone":false,"keywords":["bomb","boom","comic","dangerous","explosion","hot"]}, + {"emoji":"🪃","name":"boomerang","category":"Objects","version":"13.0","skinTone":false,"keywords":["boomerang","rebound","repercussion","weapon"]}, + {"emoji":"🏹","name":"bow and arrow","category":"Objects","version":"1.0","skinTone":false,"keywords":["bow","and","arrow","archer","archery","Sagittarius","tool","weapon","zodiac"]}, + {"emoji":"🛡️","name":"shield","category":"Objects","version":"0.7","skinTone":false,"keywords":["shield"]}, + {"emoji":"🪚","name":"carpentry saw","category":"Objects","version":"13.0","skinTone":false,"keywords":["carpentry","saw","carpenter","cut","lumber","tool","trim"]}, + {"emoji":"🔧","name":"wrench","category":"Objects","version":"0.6","skinTone":false,"keywords":["wrench","home","improvement","spanner","tool"]}, + {"emoji":"🪛","name":"screwdriver","category":"Objects","version":"13.0","skinTone":false,"keywords":["screwdriver","flathead","handy","screw","tool"]}, + {"emoji":"🔩","name":"nut and bolt","category":"Objects","version":"0.6","skinTone":false,"keywords":["nut","and","bolt","home","improvement","tool"]}, + {"emoji":"⚙️","name":"gear","category":"Objects","version":"1.0","skinTone":false,"keywords":["gear"]}, + {"emoji":"🗜️","name":"clamp","category":"Objects","version":"0.7","skinTone":false,"keywords":["clamp"]}, + {"emoji":"⚖️","name":"balance scale","category":"Objects","version":"1.0","skinTone":false,"keywords":["balance","scale"]}, + {"emoji":"🦯","name":"white cane","category":"Objects","version":"12.0","skinTone":false,"keywords":["white","cane","accessibility","blind","probing"]}, + {"emoji":"🔗","name":"link","category":"Objects","version":"0.6","skinTone":false,"keywords":["link","links"]}, + {"emoji":"⛓️‍💥","name":"broken chain","category":"Objects","version":"15.1","skinTone":false,"keywords":["broken","chain"]}, + {"emoji":"⛓️","name":"chains","category":"Objects","version":"0.7","skinTone":false,"keywords":["chains"]}, + {"emoji":"🪝","name":"hook","category":"Objects","version":"13.0","skinTone":false,"keywords":["hook","catch","crook","curve","ensnare","point","selling"]}, + {"emoji":"🧰","name":"toolbox","category":"Objects","version":"11.0","skinTone":false,"keywords":["toolbox","box","chest","mechanic","red","tool"]}, + {"emoji":"🧲","name":"magnet","category":"Objects","version":"11.0","skinTone":false,"keywords":["magnet","attraction","horseshoe","magnetic","negative","positive","shape","u"]}, + {"emoji":"🪜","name":"ladder","category":"Objects","version":"13.0","skinTone":false,"keywords":["ladder","climb","rung","step"]}, + {"emoji":"🪏","name":"shovel","category":"Objects","version":"16.0","skinTone":false,"keywords":["shovel","bury","dig","garden","hole","plant","scoop","snow","spade"]}, + {"emoji":"⚗️","name":"alembic","category":"Objects","version":"1.0","skinTone":false,"keywords":["alembic"]}, + {"emoji":"🧪","name":"test tube","category":"Objects","version":"11.0","skinTone":false,"keywords":["test","tube","chemist","chemistry","experiment","lab","science"]}, + {"emoji":"🧫","name":"petri dish","category":"Objects","version":"11.0","skinTone":false,"keywords":["petri","dish","bacteria","biologist","biology","culture","lab"]}, + {"emoji":"🧬","name":"dna","category":"Objects","version":"11.0","skinTone":false,"keywords":["dna","biologist","evolution","gene","genetics","life"]}, + {"emoji":"🔬","name":"microscope","category":"Objects","version":"1.0","skinTone":false,"keywords":["microscope","experiment","lab","science","tool"]}, + {"emoji":"🔭","name":"telescope","category":"Objects","version":"1.0","skinTone":false,"keywords":["telescope","contact","extraterrestrial","science","tool"]}, + {"emoji":"📡","name":"satellite antenna","category":"Objects","version":"0.6","skinTone":false,"keywords":["satellite","antenna","aliens","contact","dish","science"]}, + {"emoji":"💉","name":"syringe","category":"Objects","version":"0.6","skinTone":false,"keywords":["syringe","doctor","flu","medicine","needle","shot","sick","tool","vaccination"]}, + {"emoji":"🩸","name":"drop of blood","category":"Objects","version":"12.0","skinTone":false,"keywords":["drop","of","blood","bleed","donation","injury","medicine","menstruation"]}, + {"emoji":"💊","name":"pill","category":"Objects","version":"0.6","skinTone":false,"keywords":["pill","doctor","drugs","medicated","medicine","pills","sick","vitamin"]}, + {"emoji":"🩹","name":"adhesive bandage","category":"Objects","version":"12.0","skinTone":false,"keywords":["adhesive","bandage"]}, + {"emoji":"🩼","name":"crutch","category":"Objects","version":"14.0","skinTone":false,"keywords":["crutch","aid","cane","disability","help","hurt","injured","mobility","stick"]}, + {"emoji":"🩺","name":"stethoscope","category":"Objects","version":"12.0","skinTone":false,"keywords":["stethoscope","doctor","heart","medicine"]}, + {"emoji":"🩻","name":"x-ray","category":"Objects","version":"14.0","skinTone":false,"keywords":["x-ray","bones","doctor","medical","skeleton","skull","xray"]}, + {"emoji":"🚪","name":"door","category":"Objects","version":"0.6","skinTone":false,"keywords":["door","back","closet","front"]}, + {"emoji":"🛗","name":"elevator","category":"Objects","version":"13.0","skinTone":false,"keywords":["elevator","accessibility","hoist","lift"]}, + {"emoji":"🪞","name":"mirror","category":"Objects","version":"13.0","skinTone":false,"keywords":["mirror","makeup","reflection","reflector","speculum"]}, + {"emoji":"🪟","name":"window","category":"Objects","version":"13.0","skinTone":false,"keywords":["window","air","frame","fresh","opening","transparent","view"]}, + {"emoji":"🛏️","name":"bed","category":"Objects","version":"0.7","skinTone":false,"keywords":["bed"]}, + {"emoji":"🛋️","name":"couch and lamp","category":"Objects","version":"0.7","skinTone":false,"keywords":["couch","and","lamp"]}, + {"emoji":"🪑","name":"chair","category":"Objects","version":"12.0","skinTone":false,"keywords":["chair","seat","sit"]}, + {"emoji":"🚽","name":"toilet","category":"Objects","version":"0.6","skinTone":false,"keywords":["toilet","bathroom"]}, + {"emoji":"🪠","name":"plunger","category":"Objects","version":"13.0","skinTone":false,"keywords":["plunger","cup","force","plumber","poop","suction","toilet"]}, + {"emoji":"🚿","name":"shower","category":"Objects","version":"1.0","skinTone":false,"keywords":["shower","water"]}, + {"emoji":"🛁","name":"bathtub","category":"Objects","version":"1.0","skinTone":false,"keywords":["bathtub","bath"]}, + {"emoji":"🪤","name":"mouse trap","category":"Objects","version":"13.0","skinTone":false,"keywords":["mouse","trap","bait","cheese","lure","mousetrap","snare"]}, + {"emoji":"🪒","name":"razor","category":"Objects","version":"12.0","skinTone":false,"keywords":["razor","sharp","shave"]}, + {"emoji":"🧴","name":"lotion bottle","category":"Objects","version":"11.0","skinTone":false,"keywords":["lotion","bottle","moisturizer","shampoo","sunscreen"]}, + {"emoji":"🧷","name":"safety pin","category":"Objects","version":"11.0","skinTone":false,"keywords":["safety","pin","diaper","punk","rock"]}, + {"emoji":"🧹","name":"broom","category":"Objects","version":"11.0","skinTone":false,"keywords":["broom","cleaning","sweeping","witch"]}, + {"emoji":"🧺","name":"basket","category":"Objects","version":"11.0","skinTone":false,"keywords":["basket","farming","laundry","picnic"]}, + {"emoji":"🧻","name":"roll of paper","category":"Objects","version":"11.0","skinTone":false,"keywords":["roll","of","paper","toilet","towels"]}, + {"emoji":"🪣","name":"bucket","category":"Objects","version":"13.0","skinTone":false,"keywords":["bucket","cask","pail","vat"]}, + {"emoji":"🧼","name":"soap","category":"Objects","version":"11.0","skinTone":false,"keywords":["soap","bar","bathing","clean","cleaning","lather","soapdish"]}, + {"emoji":"🫧","name":"bubbles","category":"Objects","version":"14.0","skinTone":false,"keywords":["bubbles","bubble","burp","clean","floating","pearl","soap","underwater"]}, + {"emoji":"🪥","name":"toothbrush","category":"Objects","version":"13.0","skinTone":false,"keywords":["toothbrush","bathroom","brush","clean","dental","hygiene","teeth","toiletry"]}, + {"emoji":"🧽","name":"sponge","category":"Objects","version":"11.0","skinTone":false,"keywords":["sponge","absorbing","cleaning","porous","soak"]}, + {"emoji":"🧯","name":"fire extinguisher","category":"Objects","version":"11.0","skinTone":false,"keywords":["fire","extinguisher","extinguish","quench"]}, + {"emoji":"🛒","name":"shopping cart","category":"Objects","version":"3.0","skinTone":false,"keywords":["shopping","cart","trolley"]}, + {"emoji":"🚬","name":"cigarette","category":"Objects","version":"0.6","skinTone":false,"keywords":["cigarette","smoking"]}, + {"emoji":"⚰️","name":"coffin","category":"Objects","version":"1.0","skinTone":false,"keywords":["coffin"]}, + {"emoji":"🪦","name":"headstone","category":"Objects","version":"13.0","skinTone":false,"keywords":["headstone","cemetery","dead","grave","graveyard","memorial","rip","tomb","tombstone"]}, + {"emoji":"⚱️","name":"funeral urn","category":"Objects","version":"1.0","skinTone":false,"keywords":["funeral","urn"]}, + {"emoji":"🧿","name":"nazar amulet","category":"Objects","version":"11.0","skinTone":false,"keywords":["nazar","amulet","bead","blue","charm","evil-eye","talisman"]}, + {"emoji":"🪬","name":"hamsa","category":"Objects","version":"14.0","skinTone":false,"keywords":["hamsa","amulet","Fatima","fortune","guide","hand","Mary","Miriam","palm","protect","protection"]}, + {"emoji":"🗿","name":"moai","category":"Objects","version":"0.6","skinTone":false,"keywords":["moai","face","moyai","statue","stoneface","travel"]}, + {"emoji":"🪧","name":"placard","category":"Objects","version":"13.0","skinTone":false,"keywords":["placard","card","demonstration","notice","picket","plaque","protest","sign"]}, + {"emoji":"🪪","name":"identification card","category":"Objects","version":"14.0","skinTone":false,"keywords":["identification","card","credentials","document","ID","license","security"]}, + {"emoji":"🏧","name":"ATM sign","category":"Symbols","version":"0.6","skinTone":false,"keywords":["ATM","sign","automated","bank","cash","money","teller"]}, + {"emoji":"🚮","name":"litter in bin sign","category":"Symbols","version":"1.0","skinTone":false,"keywords":["litter","in","bin","sign","litterbin"]}, + {"emoji":"🚰","name":"potable water","category":"Symbols","version":"1.0","skinTone":false,"keywords":["potable","water","drinking"]}, + {"emoji":"♿","name":"wheelchair symbol","category":"Symbols","version":"0.6","skinTone":false,"keywords":["wheelchair","symbol","access","handicap"]}, + {"emoji":"🚹","name":"men’s room","category":"Symbols","version":"0.6","skinTone":false,"keywords":["men’s","room","bathroom","lavatory","man","restroom","toilet","WC"]}, + {"emoji":"🚺","name":"women’s room","category":"Symbols","version":"0.6","skinTone":false,"keywords":["women’s","room","bathroom","lavatory","restroom","toilet","WC","woman"]}, + {"emoji":"🚻","name":"restroom","category":"Symbols","version":"0.6","skinTone":false,"keywords":["restroom","bathroom","lavatory","toilet","WC"]}, + {"emoji":"🚼","name":"baby symbol","category":"Symbols","version":"0.6","skinTone":false,"keywords":["baby","symbol","changing"]}, + {"emoji":"🚾","name":"water closet","category":"Symbols","version":"0.6","skinTone":false,"keywords":["water","closet","bathroom","lavatory","restroom","toilet","WC"]}, + {"emoji":"🛂","name":"passport control","category":"Symbols","version":"1.0","skinTone":false,"keywords":["passport","control"]}, + {"emoji":"🛃","name":"customs","category":"Symbols","version":"1.0","skinTone":false,"keywords":["customs","packing"]}, + {"emoji":"🛄","name":"baggage claim","category":"Symbols","version":"1.0","skinTone":false,"keywords":["baggage","claim","arrived","bags","case","checked","journey","packing","plane","ready","travel","trip"]}, + {"emoji":"🛅","name":"left luggage","category":"Symbols","version":"1.0","skinTone":false,"keywords":["left","luggage","baggage","case","locker"]}, + {"emoji":"⚠️","name":"warning","category":"Symbols","version":"0.6","skinTone":false,"keywords":["warning"]}, + {"emoji":"🚸","name":"children crossing","category":"Symbols","version":"1.0","skinTone":false,"keywords":["children","crossing","child","pedestrian","traffic"]}, + {"emoji":"⛔","name":"no entry","category":"Symbols","version":"0.6","skinTone":false,"keywords":["no","entry","do","fail","forbidden","not","pass","prohibited","traffic"]}, + {"emoji":"🚫","name":"prohibited","category":"Symbols","version":"0.6","skinTone":false,"keywords":["prohibited","entry","forbidden","no","not","smoke"]}, + {"emoji":"🚳","name":"no bicycles","category":"Symbols","version":"1.0","skinTone":false,"keywords":["no","bicycles","bicycle","bike","forbidden","not","prohibited"]}, + {"emoji":"🚭","name":"no smoking","category":"Symbols","version":"0.6","skinTone":false,"keywords":["no","smoking","forbidden","not","prohibited","smoke"]}, + {"emoji":"🚯","name":"no littering","category":"Symbols","version":"1.0","skinTone":false,"keywords":["no","littering","forbidden","litter","not","prohibited"]}, + {"emoji":"🚱","name":"non-potable water","category":"Symbols","version":"1.0","skinTone":false,"keywords":["non-potable","water","dry","non-drinking","prohibited"]}, + {"emoji":"🚷","name":"no pedestrians","category":"Symbols","version":"1.0","skinTone":false,"keywords":["no","pedestrians","forbidden","not","pedestrian","prohibited"]}, + {"emoji":"📵","name":"no mobile phones","category":"Symbols","version":"1.0","skinTone":false,"keywords":["no","mobile","phones","cell","forbidden","not","phone","prohibited","telephone"]}, + {"emoji":"🔞","name":"no one under eighteen","category":"Symbols","version":"0.6","skinTone":false,"keywords":["no","one","under","eighteen","18","age","forbidden","not","prohibited","restriction","underage"]}, + {"emoji":"☢️","name":"radioactive","category":"Symbols","version":"1.0","skinTone":false,"keywords":["radioactive"]}, + {"emoji":"☣️","name":"biohazard","category":"Symbols","version":"1.0","skinTone":false,"keywords":["biohazard"]}, + {"emoji":"⬆️","name":"up arrow","category":"Symbols","version":"0.6","skinTone":false,"keywords":["up","arrow"]}, + {"emoji":"↗️","name":"up-right arrow","category":"Symbols","version":"0.6","skinTone":false,"keywords":["up-right","arrow"]}, + {"emoji":"➡️","name":"right arrow","category":"Symbols","version":"0.6","skinTone":false,"keywords":["right","arrow"]}, + {"emoji":"↘️","name":"down-right arrow","category":"Symbols","version":"0.6","skinTone":false,"keywords":["down-right","arrow"]}, + {"emoji":"⬇️","name":"down arrow","category":"Symbols","version":"0.6","skinTone":false,"keywords":["down","arrow"]}, + {"emoji":"↙️","name":"down-left arrow","category":"Symbols","version":"0.6","skinTone":false,"keywords":["down-left","arrow"]}, + {"emoji":"⬅️","name":"left arrow","category":"Symbols","version":"0.6","skinTone":false,"keywords":["left","arrow"]}, + {"emoji":"↖️","name":"up-left arrow","category":"Symbols","version":"0.6","skinTone":false,"keywords":["up-left","arrow"]}, + {"emoji":"↕️","name":"up-down arrow","category":"Symbols","version":"0.6","skinTone":false,"keywords":["up-down","arrow"]}, + {"emoji":"↔️","name":"left-right arrow","category":"Symbols","version":"0.6","skinTone":false,"keywords":["left-right","arrow"]}, + {"emoji":"↩️","name":"right arrow curving left","category":"Symbols","version":"0.6","skinTone":false,"keywords":["right","arrow","curving","left"]}, + {"emoji":"↪️","name":"left arrow curving right","category":"Symbols","version":"0.6","skinTone":false,"keywords":["left","arrow","curving","right"]}, + {"emoji":"⤴️","name":"right arrow curving up","category":"Symbols","version":"0.6","skinTone":false,"keywords":["right","arrow","curving","up"]}, + {"emoji":"⤵️","name":"right arrow curving down","category":"Symbols","version":"0.6","skinTone":false,"keywords":["right","arrow","curving","down"]}, + {"emoji":"🔃","name":"clockwise vertical arrows","category":"Symbols","version":"0.6","skinTone":false,"keywords":["clockwise","vertical","arrows","arrow","refresh","reload"]}, + {"emoji":"🔄","name":"counterclockwise arrows button","category":"Symbols","version":"1.0","skinTone":false,"keywords":["counterclockwise","arrows","button","again","anticlockwise","arrow","deja","refresh","rewindershins","vu"]}, + {"emoji":"🔙","name":"BACK arrow","category":"Symbols","version":"0.6","skinTone":false,"keywords":["BACK","arrow"]}, + {"emoji":"🔚","name":"END arrow","category":"Symbols","version":"0.6","skinTone":false,"keywords":["END","arrow"]}, + {"emoji":"🔛","name":"ON! arrow","category":"Symbols","version":"0.6","skinTone":false,"keywords":["ON!","arrow","mark"]}, + {"emoji":"🔜","name":"SOON arrow","category":"Symbols","version":"0.6","skinTone":false,"keywords":["SOON","arrow","brb","omw"]}, + {"emoji":"🔝","name":"TOP arrow","category":"Symbols","version":"0.6","skinTone":false,"keywords":["TOP","arrow","homie","up"]}, + {"emoji":"🛐","name":"place of worship","category":"Symbols","version":"1.0","skinTone":false,"keywords":["place","of","worship","pray","religion"]}, + {"emoji":"⚛️","name":"atom symbol","category":"Symbols","version":"1.0","skinTone":false,"keywords":["atom","symbol"]}, + {"emoji":"🕉️","name":"om","category":"Symbols","version":"0.7","skinTone":false,"keywords":["om"]}, + {"emoji":"✡️","name":"star of David","category":"Symbols","version":"0.7","skinTone":false,"keywords":["star","of","David"]}, + {"emoji":"☸️","name":"wheel of dharma","category":"Symbols","version":"0.7","skinTone":false,"keywords":["wheel","of","dharma"]}, + {"emoji":"☯️","name":"yin yang","category":"Symbols","version":"0.7","skinTone":false,"keywords":["yin","yang"]}, + {"emoji":"✝️","name":"latin cross","category":"Symbols","version":"0.7","skinTone":false,"keywords":["latin","cross"]}, + {"emoji":"☦️","name":"orthodox cross","category":"Symbols","version":"1.0","skinTone":false,"keywords":["orthodox","cross"]}, + {"emoji":"☪️","name":"star and crescent","category":"Symbols","version":"0.7","skinTone":false,"keywords":["star","and","crescent"]}, + {"emoji":"☮️","name":"peace symbol","category":"Symbols","version":"1.0","skinTone":false,"keywords":["peace","symbol"]}, + {"emoji":"🕎","name":"menorah","category":"Symbols","version":"1.0","skinTone":false,"keywords":["menorah","candelabrum","candlestick","hanukkah","jewish","judaism","religion"]}, + {"emoji":"🔯","name":"dotted six-pointed star","category":"Symbols","version":"0.6","skinTone":false,"keywords":["dotted","six-pointed","star","fortune","jewish","judaism"]}, + {"emoji":"🪯","name":"khanda","category":"Symbols","version":"15.0","skinTone":false,"keywords":["khanda","Deg","Fateh","Khalsa","Khanda","religion","Sikh","Sikhism","Tegh"]}, + {"emoji":"♈","name":"Aries","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Aries","horoscope","ram","zodiac"]}, + {"emoji":"♉","name":"Taurus","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Taurus","bull","horoscope","ox","zodiac"]}, + {"emoji":"♊","name":"Gemini","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Gemini","horoscope","twins","zodiac"]}, + {"emoji":"♋","name":"Cancer","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Cancer","crab","horoscope","zodiac"]}, + {"emoji":"♌","name":"Leo","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Leo","horoscope","lion","zodiac"]}, + {"emoji":"♍","name":"Virgo","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Virgo","horoscope","zodiac"]}, + {"emoji":"♎","name":"Libra","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Libra","balance","horoscope","justice","scales","zodiac"]}, + {"emoji":"♏","name":"Scorpio","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Scorpio","horoscope","scorpion","Scorpius","zodiac"]}, + {"emoji":"♐","name":"Sagittarius","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Sagittarius","archer","horoscope","zodiac"]}, + {"emoji":"♑","name":"Capricorn","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Capricorn","goat","horoscope","zodiac"]}, + {"emoji":"♒","name":"Aquarius","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Aquarius","bearer","horoscope","water","zodiac"]}, + {"emoji":"♓","name":"Pisces","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Pisces","fish","horoscope","zodiac"]}, + {"emoji":"⛎","name":"Ophiuchus","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Ophiuchus","bearer","serpent","snake","zodiac"]}, + {"emoji":"🔀","name":"shuffle tracks button","category":"Symbols","version":"1.0","skinTone":false,"keywords":["shuffle","tracks","button","arrow","crossed"]}, + {"emoji":"🔁","name":"repeat button","category":"Symbols","version":"1.0","skinTone":false,"keywords":["repeat","button","arrow","clockwise"]}, + {"emoji":"🔂","name":"repeat single button","category":"Symbols","version":"1.0","skinTone":false,"keywords":["repeat","single","button","arrow","clockwise","once"]}, + {"emoji":"▶️","name":"play button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["play","button"]}, + {"emoji":"⏩","name":"fast-forward button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["fast-forward","button","arrow","double","fast","forward"]}, + {"emoji":"⏭️","name":"next track button","category":"Symbols","version":"0.7","skinTone":false,"keywords":["next","track","button"]}, + {"emoji":"⏯️","name":"play or pause button","category":"Symbols","version":"1.0","skinTone":false,"keywords":["play","or","pause","button"]}, + {"emoji":"◀️","name":"reverse button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["reverse","button"]}, + {"emoji":"⏪","name":"fast reverse button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["fast","reverse","button","arrow","double","rewind"]}, + {"emoji":"⏮️","name":"last track button","category":"Symbols","version":"0.7","skinTone":false,"keywords":["last","track","button"]}, + {"emoji":"🔼","name":"upwards button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["upwards","button","arrow","red","up"]}, + {"emoji":"⏫","name":"fast up button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["fast","up","button","arrow","double"]}, + {"emoji":"🔽","name":"downwards button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["downwards","button","arrow","down","red"]}, + {"emoji":"⏬","name":"fast down button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["fast","down","button","arrow","double"]}, + {"emoji":"⏸️","name":"pause button","category":"Symbols","version":"0.7","skinTone":false,"keywords":["pause","button"]}, + {"emoji":"⏹️","name":"stop button","category":"Symbols","version":"0.7","skinTone":false,"keywords":["stop","button"]}, + {"emoji":"⏺️","name":"record button","category":"Symbols","version":"0.7","skinTone":false,"keywords":["record","button"]}, + {"emoji":"⏏️","name":"eject button","category":"Symbols","version":"1.0","skinTone":false,"keywords":["eject","button"]}, + {"emoji":"🎦","name":"cinema","category":"Symbols","version":"0.6","skinTone":false,"keywords":["cinema","camera","film","movie"]}, + {"emoji":"🔅","name":"dim button","category":"Symbols","version":"1.0","skinTone":false,"keywords":["dim","button","brightness","low"]}, + {"emoji":"🔆","name":"bright button","category":"Symbols","version":"1.0","skinTone":false,"keywords":["bright","button","brightness","light"]}, + {"emoji":"📶","name":"antenna bars","category":"Symbols","version":"0.6","skinTone":false,"keywords":["antenna","bars","bar","cell","communication","mobile","phone","signal","telephone"]}, + {"emoji":"🛜","name":"wireless","category":"Symbols","version":"15.0","skinTone":false,"keywords":["wireless","broadband","computer","connectivity","hotspot","internet","network","router","smartphone","wi-fi","wifi","wlan"]}, + {"emoji":"📳","name":"vibration mode","category":"Symbols","version":"0.6","skinTone":false,"keywords":["vibration","mode","cell","communication","mobile","phone","telephone"]}, + {"emoji":"📴","name":"mobile phone off","category":"Symbols","version":"0.6","skinTone":false,"keywords":["mobile","phone","off","cell","telephone"]}, + {"emoji":"♀️","name":"female sign","category":"Symbols","version":"4.0","skinTone":false,"keywords":["female","sign"]}, + {"emoji":"♂️","name":"male sign","category":"Symbols","version":"4.0","skinTone":false,"keywords":["male","sign"]}, + {"emoji":"⚧️","name":"transgender symbol","category":"Symbols","version":"13.0","skinTone":false,"keywords":["transgender","symbol"]}, + {"emoji":"✖️","name":"multiply","category":"Symbols","version":"0.6","skinTone":false,"keywords":["multiply"]}, + {"emoji":"➕","name":"plus","category":"Symbols","version":"0.6","skinTone":false,"keywords":["plus","+"]}, + {"emoji":"➖","name":"minus","category":"Symbols","version":"0.6","skinTone":false,"keywords":["minus","-","−","heavy","math","sign"]}, + {"emoji":"➗","name":"divide","category":"Symbols","version":"0.6","skinTone":false,"keywords":["divide","÷","division","heavy","math","sign"]}, + {"emoji":"🟰","name":"heavy equals sign","category":"Symbols","version":"14.0","skinTone":false,"keywords":["heavy","equals","sign","answer","equal","equality","math"]}, + {"emoji":"♾️","name":"infinity","category":"Symbols","version":"11.0","skinTone":false,"keywords":["infinity"]}, + {"emoji":"‼️","name":"double exclamation mark","category":"Symbols","version":"0.6","skinTone":false,"keywords":["double","exclamation","mark"]}, + {"emoji":"⁉️","name":"exclamation question mark","category":"Symbols","version":"0.6","skinTone":false,"keywords":["exclamation","question","mark"]}, + {"emoji":"❓","name":"red question mark","category":"Symbols","version":"0.6","skinTone":false,"keywords":["red","question","mark","?","punctuation"]}, + {"emoji":"❔","name":"white question mark","category":"Symbols","version":"0.6","skinTone":false,"keywords":["white","question","mark","?","outlined","punctuation"]}, + {"emoji":"❕","name":"white exclamation mark","category":"Symbols","version":"0.6","skinTone":false,"keywords":["white","exclamation","mark","!","outlined","punctuation"]}, + {"emoji":"❗","name":"red exclamation mark","category":"Symbols","version":"0.6","skinTone":false,"keywords":["red","exclamation","mark","!","punctuation"]}, + {"emoji":"〰️","name":"wavy dash","category":"Symbols","version":"0.6","skinTone":false,"keywords":["wavy","dash"]}, + {"emoji":"💱","name":"currency exchange","category":"Symbols","version":"0.6","skinTone":false,"keywords":["currency","exchange","bank","money"]}, + {"emoji":"💲","name":"heavy dollar sign","category":"Symbols","version":"0.6","skinTone":false,"keywords":["heavy","dollar","sign","billion","cash","charge","currency","million","money","pay"]}, + {"emoji":"⚕️","name":"medical symbol","category":"Symbols","version":"4.0","skinTone":false,"keywords":["medical","symbol"]}, + {"emoji":"♻️","name":"recycling symbol","category":"Symbols","version":"0.6","skinTone":false,"keywords":["recycling","symbol"]}, + {"emoji":"⚜️","name":"fleur-de-lis","category":"Symbols","version":"1.0","skinTone":false,"keywords":["fleur-de-lis"]}, + {"emoji":"🔱","name":"trident emblem","category":"Symbols","version":"0.6","skinTone":false,"keywords":["trident","emblem","anchor","poseidon","ship","tool"]}, + {"emoji":"📛","name":"name badge","category":"Symbols","version":"0.6","skinTone":false,"keywords":["name","badge"]}, + {"emoji":"🔰","name":"Japanese symbol for beginner","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","symbol","for","beginner","chevron","green","leaf","tool","yellow"]}, + {"emoji":"⭕","name":"hollow red circle","category":"Symbols","version":"0.6","skinTone":false,"keywords":["hollow","red","circle","heavy","large","o"]}, + {"emoji":"✅","name":"check mark button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["check","mark","button","✓","checked","checkmark","complete","completed","done","fixed","tick"]}, + {"emoji":"☑️","name":"check box with check","category":"Symbols","version":"0.6","skinTone":false,"keywords":["check","box","with"]}, + {"emoji":"✔️","name":"check mark","category":"Symbols","version":"0.6","skinTone":false,"keywords":["check","mark"]}, + {"emoji":"❌","name":"cross mark","category":"Symbols","version":"0.6","skinTone":false,"keywords":["cross","mark","×","cancel","multiplication","multiply","x"]}, + {"emoji":"❎","name":"cross mark button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["cross","mark","button","×","multiplication","multiply","square","x"]}, + {"emoji":"➰","name":"curly loop","category":"Symbols","version":"0.6","skinTone":false,"keywords":["curly","loop","curl"]}, + {"emoji":"➿","name":"double curly loop","category":"Symbols","version":"1.0","skinTone":false,"keywords":["double","curly","loop","curl"]}, + {"emoji":"〽️","name":"part alternation mark","category":"Symbols","version":"0.6","skinTone":false,"keywords":["part","alternation","mark"]}, + {"emoji":"✳️","name":"eight-spoked asterisk","category":"Symbols","version":"0.6","skinTone":false,"keywords":["eight-spoked","asterisk"]}, + {"emoji":"✴️","name":"eight-pointed star","category":"Symbols","version":"0.6","skinTone":false,"keywords":["eight-pointed","star"]}, + {"emoji":"❇️","name":"sparkle","category":"Symbols","version":"0.6","skinTone":false,"keywords":["sparkle"]}, + {"emoji":"©️","name":"copyright","category":"Symbols","version":"0.6","skinTone":false,"keywords":["copyright"]}, + {"emoji":"®️","name":"registered","category":"Symbols","version":"0.6","skinTone":false,"keywords":["registered"]}, + {"emoji":"™️","name":"trade mark","category":"Symbols","version":"0.6","skinTone":false,"keywords":["trade","mark"]}, + {"emoji":"🫟","name":"splatter","category":"Symbols","version":"16.0","skinTone":false,"keywords":["splatter","drip","holi","ink","liquid","mess","paint","spill","stain"]}, + {"emoji":"#️⃣","name":"keycap: #","category":"Symbols","version":"0.6","skinTone":false,"keywords":["keycap:","#"]}, + {"emoji":"*️⃣","name":"keycap: *","category":"Symbols","version":"2.0","skinTone":false,"keywords":["keycap:","*"]}, + {"emoji":"0️⃣","name":"keycap: 0","category":"Symbols","version":"0.6","skinTone":false,"keywords":["keycap:","0"]}, + {"emoji":"1️⃣","name":"keycap: 1","category":"Symbols","version":"0.6","skinTone":false,"keywords":["keycap:","1"]}, + {"emoji":"2️⃣","name":"keycap: 2","category":"Symbols","version":"0.6","skinTone":false,"keywords":["keycap:","2"]}, + {"emoji":"3️⃣","name":"keycap: 3","category":"Symbols","version":"0.6","skinTone":false,"keywords":["keycap:","3"]}, + {"emoji":"4️⃣","name":"keycap: 4","category":"Symbols","version":"0.6","skinTone":false,"keywords":["keycap:","4"]}, + {"emoji":"5️⃣","name":"keycap: 5","category":"Symbols","version":"0.6","skinTone":false,"keywords":["keycap:","5"]}, + {"emoji":"6️⃣","name":"keycap: 6","category":"Symbols","version":"0.6","skinTone":false,"keywords":["keycap:","6"]}, + {"emoji":"7️⃣","name":"keycap: 7","category":"Symbols","version":"0.6","skinTone":false,"keywords":["keycap:","7"]}, + {"emoji":"8️⃣","name":"keycap: 8","category":"Symbols","version":"0.6","skinTone":false,"keywords":["keycap:","8"]}, + {"emoji":"9️⃣","name":"keycap: 9","category":"Symbols","version":"0.6","skinTone":false,"keywords":["keycap:","9"]}, + {"emoji":"🔟","name":"keycap: 10","category":"Symbols","version":"0.6","skinTone":false,"keywords":["keycap:","10"]}, + {"emoji":"🔠","name":"input latin uppercase","category":"Symbols","version":"0.6","skinTone":false,"keywords":["input","latin","uppercase","ABCD","letters"]}, + {"emoji":"🔡","name":"input latin lowercase","category":"Symbols","version":"0.6","skinTone":false,"keywords":["input","latin","lowercase","abcd","letters"]}, + {"emoji":"🔢","name":"input numbers","category":"Symbols","version":"0.6","skinTone":false,"keywords":["input","numbers","1234"]}, + {"emoji":"🔣","name":"input symbols","category":"Symbols","version":"0.6","skinTone":false,"keywords":["input","symbols","&","%","♪","〒"]}, + {"emoji":"🔤","name":"input latin letters","category":"Symbols","version":"0.6","skinTone":false,"keywords":["input","latin","letters","abc","alphabet"]}, + {"emoji":"🅰️","name":"A button (blood type)","category":"Symbols","version":"0.6","skinTone":false,"keywords":["A","button","(blood","type)"]}, + {"emoji":"🆎","name":"AB button (blood type)","category":"Symbols","version":"0.6","skinTone":false,"keywords":["AB","button","(blood","type)","blood","type"]}, + {"emoji":"🅱️","name":"B button (blood type)","category":"Symbols","version":"0.6","skinTone":false,"keywords":["B","button","(blood","type)"]}, + {"emoji":"🆑","name":"CL button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["CL","button"]}, + {"emoji":"🆒","name":"COOL button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["COOL","button"]}, + {"emoji":"🆓","name":"FREE button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["FREE","button"]}, + {"emoji":"ℹ️","name":"information","category":"Symbols","version":"0.6","skinTone":false,"keywords":["information"]}, + {"emoji":"🆔","name":"ID button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["ID","button","identity"]}, + {"emoji":"Ⓜ️","name":"circled M","category":"Symbols","version":"0.6","skinTone":false,"keywords":["circled","M"]}, + {"emoji":"🆕","name":"NEW button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["NEW","button"]}, + {"emoji":"🆖","name":"NG button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["NG","button"]}, + {"emoji":"🅾️","name":"O button (blood type)","category":"Symbols","version":"0.6","skinTone":false,"keywords":["O","button","(blood","type)"]}, + {"emoji":"🆗","name":"OK button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["OK","button","okay"]}, + {"emoji":"🅿️","name":"P button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["P","button"]}, + {"emoji":"🆘","name":"SOS button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["SOS","button","help"]}, + {"emoji":"🆙","name":"UP! button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["UP!","button","mark","UP"]}, + {"emoji":"🆚","name":"VS button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["VS","button","versus"]}, + {"emoji":"🈁","name":"Japanese “here” button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","“here”","button","here","katakana"]}, + {"emoji":"🈂️","name":"Japanese “service charge” button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","“service","charge”","button"]}, + {"emoji":"🈷️","name":"Japanese “monthly amount” button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","“monthly","amount”","button"]}, + {"emoji":"🈶","name":"Japanese “not free of charge” button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","“not","free","of","charge”","button","charge","ideograph","not"]}, + {"emoji":"🈯","name":"Japanese “reserved” button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","“reserved”","button","ideograph","reserved"]}, + {"emoji":"🉐","name":"Japanese “bargain” button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","“bargain”","button","bargain","ideograph"]}, + {"emoji":"🈹","name":"Japanese “discount” button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","“discount”","button","discount","ideograph"]}, + {"emoji":"🈚","name":"Japanese “free of charge” button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","“free","of","charge”","button","charge","free","ideograph"]}, + {"emoji":"🈲","name":"Japanese “prohibited” button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","“prohibited”","button","ideograph","prohibited"]}, + {"emoji":"🉑","name":"Japanese “acceptable” button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","“acceptable”","button","acceptable","ideograph"]}, + {"emoji":"🈸","name":"Japanese “application” button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","“application”","button","application","ideograph"]}, + {"emoji":"🈴","name":"Japanese “passing grade” button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","“passing","grade”","button","grade","ideograph","passing"]}, + {"emoji":"🈳","name":"Japanese “vacancy” button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","“vacancy”","button","ideograph","vacancy"]}, + {"emoji":"㊗️","name":"Japanese “congratulations” button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","“congratulations”","button"]}, + {"emoji":"㊙️","name":"Japanese “secret” button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","“secret”","button"]}, + {"emoji":"🈺","name":"Japanese “open for business” button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","“open","for","business”","button","business","ideograph","open"]}, + {"emoji":"🈵","name":"Japanese “no vacancy” button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["Japanese","“no","vacancy”","button","ideograph","no","vacancy"]}, + {"emoji":"🔴","name":"red circle","category":"Symbols","version":"0.6","skinTone":false,"keywords":["red","circle","geometric"]}, + {"emoji":"🟠","name":"orange circle","category":"Symbols","version":"12.0","skinTone":false,"keywords":["orange","circle"]}, + {"emoji":"🟡","name":"yellow circle","category":"Symbols","version":"12.0","skinTone":false,"keywords":["yellow","circle"]}, + {"emoji":"🟢","name":"green circle","category":"Symbols","version":"12.0","skinTone":false,"keywords":["green","circle"]}, + {"emoji":"🔵","name":"blue circle","category":"Symbols","version":"0.6","skinTone":false,"keywords":["blue","circle","geometric"]}, + {"emoji":"🟣","name":"purple circle","category":"Symbols","version":"12.0","skinTone":false,"keywords":["purple","circle"]}, + {"emoji":"🟤","name":"brown circle","category":"Symbols","version":"12.0","skinTone":false,"keywords":["brown","circle"]}, + {"emoji":"⚫","name":"black circle","category":"Symbols","version":"0.6","skinTone":false,"keywords":["black","circle","geometric"]}, + {"emoji":"⚪","name":"white circle","category":"Symbols","version":"0.6","skinTone":false,"keywords":["white","circle","geometric"]}, + {"emoji":"🟥","name":"red square","category":"Symbols","version":"12.0","skinTone":false,"keywords":["red","square","card","penalty"]}, + {"emoji":"🟧","name":"orange square","category":"Symbols","version":"12.0","skinTone":false,"keywords":["orange","square"]}, + {"emoji":"🟨","name":"yellow square","category":"Symbols","version":"12.0","skinTone":false,"keywords":["yellow","square","card","penalty"]}, + {"emoji":"🟩","name":"green square","category":"Symbols","version":"12.0","skinTone":false,"keywords":["green","square"]}, + {"emoji":"🟦","name":"blue square","category":"Symbols","version":"12.0","skinTone":false,"keywords":["blue","square"]}, + {"emoji":"🟪","name":"purple square","category":"Symbols","version":"12.0","skinTone":false,"keywords":["purple","square"]}, + {"emoji":"🟫","name":"brown square","category":"Symbols","version":"12.0","skinTone":false,"keywords":["brown","square"]}, + {"emoji":"⬛","name":"black large square","category":"Symbols","version":"0.6","skinTone":false,"keywords":["black","large","square","geometric"]}, + {"emoji":"⬜","name":"white large square","category":"Symbols","version":"0.6","skinTone":false,"keywords":["white","large","square","geometric"]}, + {"emoji":"◼️","name":"black medium square","category":"Symbols","version":"0.6","skinTone":false,"keywords":["black","medium","square"]}, + {"emoji":"◻️","name":"white medium square","category":"Symbols","version":"0.6","skinTone":false,"keywords":["white","medium","square"]}, + {"emoji":"◾","name":"black medium-small square","category":"Symbols","version":"0.6","skinTone":false,"keywords":["black","medium-small","square","geometric"]}, + {"emoji":"◽","name":"white medium-small square","category":"Symbols","version":"0.6","skinTone":false,"keywords":["white","medium-small","square","geometric"]}, + {"emoji":"▪️","name":"black small square","category":"Symbols","version":"0.6","skinTone":false,"keywords":["black","small","square"]}, + {"emoji":"▫️","name":"white small square","category":"Symbols","version":"0.6","skinTone":false,"keywords":["white","small","square"]}, + {"emoji":"🔶","name":"large orange diamond","category":"Symbols","version":"0.6","skinTone":false,"keywords":["large","orange","diamond","geometric"]}, + {"emoji":"🔷","name":"large blue diamond","category":"Symbols","version":"0.6","skinTone":false,"keywords":["large","blue","diamond","geometric"]}, + {"emoji":"🔸","name":"small orange diamond","category":"Symbols","version":"0.6","skinTone":false,"keywords":["small","orange","diamond","geometric"]}, + {"emoji":"🔹","name":"small blue diamond","category":"Symbols","version":"0.6","skinTone":false,"keywords":["small","blue","diamond","geometric"]}, + {"emoji":"🔺","name":"red triangle pointed up","category":"Symbols","version":"0.6","skinTone":false,"keywords":["red","triangle","pointed","up","geometric"]}, + {"emoji":"🔻","name":"red triangle pointed down","category":"Symbols","version":"0.6","skinTone":false,"keywords":["red","triangle","pointed","down","geometric"]}, + {"emoji":"💠","name":"diamond with a dot","category":"Symbols","version":"0.6","skinTone":false,"keywords":["diamond","with","a","dot","comic","geometric"]}, + {"emoji":"🔘","name":"radio button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["radio","button","geometric"]}, + {"emoji":"🔳","name":"white square button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["white","square","button","geometric","outlined"]}, + {"emoji":"🔲","name":"black square button","category":"Symbols","version":"0.6","skinTone":false,"keywords":["black","square","button","geometric"]}, + {"emoji":"🏁","name":"chequered flag","category":"Flags","version":"0.6","skinTone":false,"keywords":["chequered","flag","checkered","finish","flags","game","race","racing","sport","win"]}, + {"emoji":"🚩","name":"triangular flag","category":"Flags","version":"0.6","skinTone":false,"keywords":["triangular","flag","construction","golf","post"]}, + {"emoji":"🎌","name":"crossed flags","category":"Flags","version":"0.6","skinTone":false,"keywords":["crossed","flags","celebration","cross","Japanese"]}, + {"emoji":"🏴","name":"black flag","category":"Flags","version":"1.0","skinTone":false,"keywords":["black","flag","waving"]}, + {"emoji":"🏳️","name":"white flag","category":"Flags","version":"0.7","skinTone":false,"keywords":["white","flag"]}, + {"emoji":"🏳️‍🌈","name":"rainbow flag","category":"Flags","version":"4.0","skinTone":false,"keywords":["rainbow","flag"]}, + {"emoji":"🏳️‍⚧️","name":"transgender flag","category":"Flags","version":"13.0","skinTone":false,"keywords":["transgender","flag"]}, + {"emoji":"🏴‍☠️","name":"pirate flag","category":"Flags","version":"11.0","skinTone":false,"keywords":["pirate","flag"]}, + {"emoji":"🇦🇨","name":"flag: Ascension Island","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Ascension","Island"]}, + {"emoji":"🇦🇩","name":"flag: Andorra","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Andorra"]}, + {"emoji":"🇦🇪","name":"flag: United Arab Emirates","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","United","Arab","Emirates"]}, + {"emoji":"🇦🇫","name":"flag: Afghanistan","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Afghanistan"]}, + {"emoji":"🇦🇬","name":"flag: Antigua & Barbuda","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Antigua","&","Barbuda"]}, + {"emoji":"🇦🇮","name":"flag: Anguilla","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Anguilla"]}, + {"emoji":"🇦🇱","name":"flag: Albania","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Albania"]}, + {"emoji":"🇦🇲","name":"flag: Armenia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Armenia"]}, + {"emoji":"🇦🇴","name":"flag: Angola","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Angola"]}, + {"emoji":"🇦🇶","name":"flag: Antarctica","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Antarctica"]}, + {"emoji":"🇦🇷","name":"flag: Argentina","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Argentina"]}, + {"emoji":"🇦🇸","name":"flag: American Samoa","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","American","Samoa"]}, + {"emoji":"🇦🇹","name":"flag: Austria","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Austria"]}, + {"emoji":"🇦🇺","name":"flag: Australia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Australia"]}, + {"emoji":"🇦🇼","name":"flag: Aruba","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Aruba"]}, + {"emoji":"🇦🇽","name":"flag: Åland Islands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Åland","Islands"]}, + {"emoji":"🇦🇿","name":"flag: Azerbaijan","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Azerbaijan"]}, + {"emoji":"🇧🇦","name":"flag: Bosnia & Herzegovina","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Bosnia","&","Herzegovina"]}, + {"emoji":"🇧🇧","name":"flag: Barbados","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Barbados"]}, + {"emoji":"🇧🇩","name":"flag: Bangladesh","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Bangladesh"]}, + {"emoji":"🇧🇪","name":"flag: Belgium","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Belgium"]}, + {"emoji":"🇧🇫","name":"flag: Burkina Faso","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Burkina","Faso"]}, + {"emoji":"🇧🇬","name":"flag: Bulgaria","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Bulgaria"]}, + {"emoji":"🇧🇭","name":"flag: Bahrain","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Bahrain"]}, + {"emoji":"🇧🇮","name":"flag: Burundi","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Burundi"]}, + {"emoji":"🇧🇯","name":"flag: Benin","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Benin"]}, + {"emoji":"🇧🇱","name":"flag: St. Barthélemy","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","St.","Barthélemy"]}, + {"emoji":"🇧🇲","name":"flag: Bermuda","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Bermuda"]}, + {"emoji":"🇧🇳","name":"flag: Brunei","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Brunei"]}, + {"emoji":"🇧🇴","name":"flag: Bolivia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Bolivia"]}, + {"emoji":"🇧🇶","name":"flag: Caribbean Netherlands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Caribbean","Netherlands"]}, + {"emoji":"🇧🇷","name":"flag: Brazil","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Brazil"]}, + {"emoji":"🇧🇸","name":"flag: Bahamas","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Bahamas"]}, + {"emoji":"🇧🇹","name":"flag: Bhutan","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Bhutan"]}, + {"emoji":"🇧🇻","name":"flag: Bouvet Island","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Bouvet","Island"]}, + {"emoji":"🇧🇼","name":"flag: Botswana","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Botswana"]}, + {"emoji":"🇧🇾","name":"flag: Belarus","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Belarus"]}, + {"emoji":"🇧🇿","name":"flag: Belize","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Belize"]}, + {"emoji":"🇨🇦","name":"flag: Canada","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Canada"]}, + {"emoji":"🇨🇨","name":"flag: Cocos (Keeling) Islands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Cocos","(Keeling)","Islands"]}, + {"emoji":"🇨🇩","name":"flag: Congo - Kinshasa","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Congo","-","Kinshasa"]}, + {"emoji":"🇨🇫","name":"flag: Central African Republic","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Central","African","Republic"]}, + {"emoji":"🇨🇬","name":"flag: Congo - Brazzaville","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Congo","-","Brazzaville"]}, + {"emoji":"🇨🇭","name":"flag: Switzerland","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Switzerland"]}, + {"emoji":"🇨🇮","name":"flag: Côte d’Ivoire","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Côte","d’Ivoire"]}, + {"emoji":"🇨🇰","name":"flag: Cook Islands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Cook","Islands"]}, + {"emoji":"🇨🇱","name":"flag: Chile","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Chile"]}, + {"emoji":"🇨🇲","name":"flag: Cameroon","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Cameroon"]}, + {"emoji":"🇨🇳","name":"flag: China","category":"Flags","version":"0.6","skinTone":false,"keywords":["flag:","China"]}, + {"emoji":"🇨🇴","name":"flag: Colombia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Colombia"]}, + {"emoji":"🇨🇵","name":"flag: Clipperton Island","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Clipperton","Island"]}, + {"emoji":"🇨🇶","name":"flag: Sark","category":"Flags","version":"16.0","skinTone":false,"keywords":["flag:","Sark"]}, + {"emoji":"🇨🇷","name":"flag: Costa Rica","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Costa","Rica"]}, + {"emoji":"🇨🇺","name":"flag: Cuba","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Cuba"]}, + {"emoji":"🇨🇻","name":"flag: Cape Verde","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Cape","Verde"]}, + {"emoji":"🇨🇼","name":"flag: Curaçao","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Curaçao"]}, + {"emoji":"🇨🇽","name":"flag: Christmas Island","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Christmas","Island"]}, + {"emoji":"🇨🇾","name":"flag: Cyprus","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Cyprus"]}, + {"emoji":"🇨🇿","name":"flag: Czechia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Czechia"]}, + {"emoji":"🇩🇪","name":"flag: Germany","category":"Flags","version":"0.6","skinTone":false,"keywords":["flag:","Germany"]}, + {"emoji":"🇩🇬","name":"flag: Diego Garcia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Diego","Garcia"]}, + {"emoji":"🇩🇯","name":"flag: Djibouti","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Djibouti"]}, + {"emoji":"🇩🇰","name":"flag: Denmark","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Denmark"]}, + {"emoji":"🇩🇲","name":"flag: Dominica","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Dominica"]}, + {"emoji":"🇩🇴","name":"flag: Dominican Republic","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Dominican","Republic"]}, + {"emoji":"🇩🇿","name":"flag: Algeria","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Algeria"]}, + {"emoji":"🇪🇦","name":"flag: Ceuta & Melilla","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Ceuta","&","Melilla"]}, + {"emoji":"🇪🇨","name":"flag: Ecuador","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Ecuador"]}, + {"emoji":"🇪🇪","name":"flag: Estonia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Estonia"]}, + {"emoji":"🇪🇬","name":"flag: Egypt","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Egypt"]}, + {"emoji":"🇪🇭","name":"flag: Western Sahara","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Western","Sahara"]}, + {"emoji":"🇪🇷","name":"flag: Eritrea","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Eritrea"]}, + {"emoji":"🇪🇸","name":"flag: Spain","category":"Flags","version":"0.6","skinTone":false,"keywords":["flag:","Spain"]}, + {"emoji":"🇪🇹","name":"flag: Ethiopia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Ethiopia"]}, + {"emoji":"🇪🇺","name":"flag: European Union","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","European","Union"]}, + {"emoji":"🇫🇮","name":"flag: Finland","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Finland"]}, + {"emoji":"🇫🇯","name":"flag: Fiji","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Fiji"]}, + {"emoji":"🇫🇰","name":"flag: Falkland Islands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Falkland","Islands"]}, + {"emoji":"🇫🇲","name":"flag: Micronesia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Micronesia"]}, + {"emoji":"🇫🇴","name":"flag: Faroe Islands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Faroe","Islands"]}, + {"emoji":"🇫🇷","name":"flag: France","category":"Flags","version":"0.6","skinTone":false,"keywords":["flag:","France"]}, + {"emoji":"🇬🇦","name":"flag: Gabon","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Gabon"]}, + {"emoji":"🇬🇧","name":"flag: United Kingdom","category":"Flags","version":"0.6","skinTone":false,"keywords":["flag:","United","Kingdom"]}, + {"emoji":"🇬🇩","name":"flag: Grenada","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Grenada"]}, + {"emoji":"🇬🇪","name":"flag: Georgia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Georgia"]}, + {"emoji":"🇬🇫","name":"flag: French Guiana","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","French","Guiana"]}, + {"emoji":"🇬🇬","name":"flag: Guernsey","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Guernsey"]}, + {"emoji":"🇬🇭","name":"flag: Ghana","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Ghana"]}, + {"emoji":"🇬🇮","name":"flag: Gibraltar","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Gibraltar"]}, + {"emoji":"🇬🇱","name":"flag: Greenland","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Greenland"]}, + {"emoji":"🇬🇲","name":"flag: Gambia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Gambia"]}, + {"emoji":"🇬🇳","name":"flag: Guinea","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Guinea"]}, + {"emoji":"🇬🇵","name":"flag: Guadeloupe","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Guadeloupe"]}, + {"emoji":"🇬🇶","name":"flag: Equatorial Guinea","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Equatorial","Guinea"]}, + {"emoji":"🇬🇷","name":"flag: Greece","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Greece"]}, + {"emoji":"🇬🇸","name":"flag: South Georgia & South Sandwich Islands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","South","Georgia","&","Sandwich","Islands"]}, + {"emoji":"🇬🇹","name":"flag: Guatemala","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Guatemala"]}, + {"emoji":"🇬🇺","name":"flag: Guam","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Guam"]}, + {"emoji":"🇬🇼","name":"flag: Guinea-Bissau","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Guinea-Bissau"]}, + {"emoji":"🇬🇾","name":"flag: Guyana","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Guyana"]}, + {"emoji":"🇭🇰","name":"flag: Hong Kong SAR China","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Hong","Kong","SAR","China"]}, + {"emoji":"🇭🇲","name":"flag: Heard & McDonald Islands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Heard","&","McDonald","Islands"]}, + {"emoji":"🇭🇳","name":"flag: Honduras","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Honduras"]}, + {"emoji":"🇭🇷","name":"flag: Croatia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Croatia"]}, + {"emoji":"🇭🇹","name":"flag: Haiti","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Haiti"]}, + {"emoji":"🇭🇺","name":"flag: Hungary","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Hungary"]}, + {"emoji":"🇮🇨","name":"flag: Canary Islands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Canary","Islands"]}, + {"emoji":"🇮🇩","name":"flag: Indonesia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Indonesia"]}, + {"emoji":"🇮🇪","name":"flag: Ireland","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Ireland"]}, + {"emoji":"🇮🇱","name":"flag: Israel","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Israel"]}, + {"emoji":"🇮🇲","name":"flag: Isle of Man","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Isle","of","Man"]}, + {"emoji":"🇮🇳","name":"flag: India","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","India"]}, + {"emoji":"🇮🇴","name":"flag: British Indian Ocean Territory","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","British","Indian","Ocean","Territory"]}, + {"emoji":"🇮🇶","name":"flag: Iraq","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Iraq"]}, + {"emoji":"🇮🇷","name":"flag: Iran","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Iran"]}, + {"emoji":"🇮🇸","name":"flag: Iceland","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Iceland"]}, + {"emoji":"🇮🇹","name":"flag: Italy","category":"Flags","version":"0.6","skinTone":false,"keywords":["flag:","Italy"]}, + {"emoji":"🇯🇪","name":"flag: Jersey","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Jersey"]}, + {"emoji":"🇯🇲","name":"flag: Jamaica","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Jamaica"]}, + {"emoji":"🇯🇴","name":"flag: Jordan","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Jordan"]}, + {"emoji":"🇯🇵","name":"flag: Japan","category":"Flags","version":"0.6","skinTone":false,"keywords":["flag:","Japan"]}, + {"emoji":"🇰🇪","name":"flag: Kenya","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Kenya"]}, + {"emoji":"🇰🇬","name":"flag: Kyrgyzstan","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Kyrgyzstan"]}, + {"emoji":"🇰🇭","name":"flag: Cambodia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Cambodia"]}, + {"emoji":"🇰🇮","name":"flag: Kiribati","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Kiribati"]}, + {"emoji":"🇰🇲","name":"flag: Comoros","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Comoros"]}, + {"emoji":"🇰🇳","name":"flag: St. Kitts & Nevis","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","St.","Kitts","&","Nevis"]}, + {"emoji":"🇰🇵","name":"flag: North Korea","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","North","Korea"]}, + {"emoji":"🇰🇷","name":"flag: South Korea","category":"Flags","version":"0.6","skinTone":false,"keywords":["flag:","South","Korea"]}, + {"emoji":"🇰🇼","name":"flag: Kuwait","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Kuwait"]}, + {"emoji":"🇰🇾","name":"flag: Cayman Islands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Cayman","Islands"]}, + {"emoji":"🇰🇿","name":"flag: Kazakhstan","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Kazakhstan"]}, + {"emoji":"🇱🇦","name":"flag: Laos","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Laos"]}, + {"emoji":"🇱🇧","name":"flag: Lebanon","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Lebanon"]}, + {"emoji":"🇱🇨","name":"flag: St. Lucia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","St.","Lucia"]}, + {"emoji":"🇱🇮","name":"flag: Liechtenstein","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Liechtenstein"]}, + {"emoji":"🇱🇰","name":"flag: Sri Lanka","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Sri","Lanka"]}, + {"emoji":"🇱🇷","name":"flag: Liberia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Liberia"]}, + {"emoji":"🇱🇸","name":"flag: Lesotho","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Lesotho"]}, + {"emoji":"🇱🇹","name":"flag: Lithuania","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Lithuania"]}, + {"emoji":"🇱🇺","name":"flag: Luxembourg","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Luxembourg"]}, + {"emoji":"🇱🇻","name":"flag: Latvia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Latvia"]}, + {"emoji":"🇱🇾","name":"flag: Libya","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Libya"]}, + {"emoji":"🇲🇦","name":"flag: Morocco","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Morocco"]}, + {"emoji":"🇲🇨","name":"flag: Monaco","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Monaco"]}, + {"emoji":"🇲🇩","name":"flag: Moldova","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Moldova"]}, + {"emoji":"🇲🇪","name":"flag: Montenegro","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Montenegro"]}, + {"emoji":"🇲🇫","name":"flag: St. Martin","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","St.","Martin"]}, + {"emoji":"🇲🇬","name":"flag: Madagascar","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Madagascar"]}, + {"emoji":"🇲🇭","name":"flag: Marshall Islands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Marshall","Islands"]}, + {"emoji":"🇲🇰","name":"flag: North Macedonia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","North","Macedonia"]}, + {"emoji":"🇲🇱","name":"flag: Mali","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Mali"]}, + {"emoji":"🇲🇲","name":"flag: Myanmar (Burma)","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Myanmar","(Burma)"]}, + {"emoji":"🇲🇳","name":"flag: Mongolia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Mongolia"]}, + {"emoji":"🇲🇴","name":"flag: Macao SAR China","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Macao","SAR","China"]}, + {"emoji":"🇲🇵","name":"flag: Northern Mariana Islands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Northern","Mariana","Islands"]}, + {"emoji":"🇲🇶","name":"flag: Martinique","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Martinique"]}, + {"emoji":"🇲🇷","name":"flag: Mauritania","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Mauritania"]}, + {"emoji":"🇲🇸","name":"flag: Montserrat","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Montserrat"]}, + {"emoji":"🇲🇹","name":"flag: Malta","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Malta"]}, + {"emoji":"🇲🇺","name":"flag: Mauritius","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Mauritius"]}, + {"emoji":"🇲🇻","name":"flag: Maldives","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Maldives"]}, + {"emoji":"🇲🇼","name":"flag: Malawi","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Malawi"]}, + {"emoji":"🇲🇽","name":"flag: Mexico","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Mexico"]}, + {"emoji":"🇲🇾","name":"flag: Malaysia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Malaysia"]}, + {"emoji":"🇲🇿","name":"flag: Mozambique","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Mozambique"]}, + {"emoji":"🇳🇦","name":"flag: Namibia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Namibia"]}, + {"emoji":"🇳🇨","name":"flag: New Caledonia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","New","Caledonia"]}, + {"emoji":"🇳🇪","name":"flag: Niger","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Niger"]}, + {"emoji":"🇳🇫","name":"flag: Norfolk Island","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Norfolk","Island"]}, + {"emoji":"🇳🇬","name":"flag: Nigeria","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Nigeria"]}, + {"emoji":"🇳🇮","name":"flag: Nicaragua","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Nicaragua"]}, + {"emoji":"🇳🇱","name":"flag: Netherlands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Netherlands"]}, + {"emoji":"🇳🇴","name":"flag: Norway","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Norway"]}, + {"emoji":"🇳🇵","name":"flag: Nepal","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Nepal"]}, + {"emoji":"🇳🇷","name":"flag: Nauru","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Nauru"]}, + {"emoji":"🇳🇺","name":"flag: Niue","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Niue"]}, + {"emoji":"🇳🇿","name":"flag: New Zealand","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","New","Zealand"]}, + {"emoji":"🇴🇲","name":"flag: Oman","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Oman"]}, + {"emoji":"🇵🇦","name":"flag: Panama","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Panama"]}, + {"emoji":"🇵🇪","name":"flag: Peru","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Peru"]}, + {"emoji":"🇵🇫","name":"flag: French Polynesia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","French","Polynesia"]}, + {"emoji":"🇵🇬","name":"flag: Papua New Guinea","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Papua","New","Guinea"]}, + {"emoji":"🇵🇭","name":"flag: Philippines","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Philippines"]}, + {"emoji":"🇵🇰","name":"flag: Pakistan","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Pakistan"]}, + {"emoji":"🇵🇱","name":"flag: Poland","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Poland"]}, + {"emoji":"🇵🇲","name":"flag: St. Pierre & Miquelon","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","St.","Pierre","&","Miquelon"]}, + {"emoji":"🇵🇳","name":"flag: Pitcairn Islands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Pitcairn","Islands"]}, + {"emoji":"🇵🇷","name":"flag: Puerto Rico","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Puerto","Rico"]}, + {"emoji":"🇵🇸","name":"flag: Palestinian Territories","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Palestinian","Territories"]}, + {"emoji":"🇵🇹","name":"flag: Portugal","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Portugal"]}, + {"emoji":"🇵🇼","name":"flag: Palau","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Palau"]}, + {"emoji":"🇵🇾","name":"flag: Paraguay","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Paraguay"]}, + {"emoji":"🇶🇦","name":"flag: Qatar","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Qatar"]}, + {"emoji":"🇷🇪","name":"flag: Réunion","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Réunion"]}, + {"emoji":"🇷🇴","name":"flag: Romania","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Romania"]}, + {"emoji":"🇷🇸","name":"flag: Serbia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Serbia"]}, + {"emoji":"🇷🇺","name":"flag: Russia","category":"Flags","version":"0.6","skinTone":false,"keywords":["flag:","Russia"]}, + {"emoji":"🇷🇼","name":"flag: Rwanda","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Rwanda"]}, + {"emoji":"🇸🇦","name":"flag: Saudi Arabia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Saudi","Arabia"]}, + {"emoji":"🇸🇧","name":"flag: Solomon Islands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Solomon","Islands"]}, + {"emoji":"🇸🇨","name":"flag: Seychelles","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Seychelles"]}, + {"emoji":"🇸🇩","name":"flag: Sudan","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Sudan"]}, + {"emoji":"🇸🇪","name":"flag: Sweden","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Sweden"]}, + {"emoji":"🇸🇬","name":"flag: Singapore","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Singapore"]}, + {"emoji":"🇸🇭","name":"flag: St. Helena","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","St.","Helena"]}, + {"emoji":"🇸🇮","name":"flag: Slovenia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Slovenia"]}, + {"emoji":"🇸🇯","name":"flag: Svalbard & Jan Mayen","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Svalbard","&","Jan","Mayen"]}, + {"emoji":"🇸🇰","name":"flag: Slovakia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Slovakia"]}, + {"emoji":"🇸🇱","name":"flag: Sierra Leone","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Sierra","Leone"]}, + {"emoji":"🇸🇲","name":"flag: San Marino","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","San","Marino"]}, + {"emoji":"🇸🇳","name":"flag: Senegal","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Senegal"]}, + {"emoji":"🇸🇴","name":"flag: Somalia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Somalia"]}, + {"emoji":"🇸🇷","name":"flag: Suriname","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Suriname"]}, + {"emoji":"🇸🇸","name":"flag: South Sudan","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","South","Sudan"]}, + {"emoji":"🇸🇹","name":"flag: São Tomé & Príncipe","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","São","Tomé","&","Príncipe"]}, + {"emoji":"🇸🇻","name":"flag: El Salvador","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","El","Salvador"]}, + {"emoji":"🇸🇽","name":"flag: Sint Maarten","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Sint","Maarten"]}, + {"emoji":"🇸🇾","name":"flag: Syria","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Syria"]}, + {"emoji":"🇸🇿","name":"flag: Eswatini","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Eswatini"]}, + {"emoji":"🇹🇦","name":"flag: Tristan da Cunha","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Tristan","da","Cunha"]}, + {"emoji":"🇹🇨","name":"flag: Turks & Caicos Islands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Turks","&","Caicos","Islands"]}, + {"emoji":"🇹🇩","name":"flag: Chad","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Chad"]}, + {"emoji":"🇹🇫","name":"flag: French Southern Territories","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","French","Southern","Territories"]}, + {"emoji":"🇹🇬","name":"flag: Togo","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Togo"]}, + {"emoji":"🇹🇭","name":"flag: Thailand","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Thailand"]}, + {"emoji":"🇹🇯","name":"flag: Tajikistan","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Tajikistan"]}, + {"emoji":"🇹🇰","name":"flag: Tokelau","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Tokelau"]}, + {"emoji":"🇹🇱","name":"flag: Timor-Leste","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Timor-Leste"]}, + {"emoji":"🇹🇲","name":"flag: Turkmenistan","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Turkmenistan"]}, + {"emoji":"🇹🇳","name":"flag: Tunisia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Tunisia"]}, + {"emoji":"🇹🇴","name":"flag: Tonga","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Tonga"]}, + {"emoji":"🇹🇷","name":"flag: Türkiye","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Türkiye"]}, + {"emoji":"🇹🇹","name":"flag: Trinidad & Tobago","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Trinidad","&","Tobago"]}, + {"emoji":"🇹🇻","name":"flag: Tuvalu","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Tuvalu"]}, + {"emoji":"🇹🇼","name":"flag: Taiwan","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Taiwan"]}, + {"emoji":"🇹🇿","name":"flag: Tanzania","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Tanzania"]}, + {"emoji":"🇺🇦","name":"flag: Ukraine","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Ukraine"]}, + {"emoji":"🇺🇬","name":"flag: Uganda","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Uganda"]}, + {"emoji":"🇺🇲","name":"flag: U.S. Outlying Islands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","U.S.","Outlying","Islands"]}, + {"emoji":"🇺🇳","name":"flag: United Nations","category":"Flags","version":"4.0","skinTone":false,"keywords":["flag:","United","Nations"]}, + {"emoji":"🇺🇸","name":"flag: United States","category":"Flags","version":"0.6","skinTone":false,"keywords":["flag:","United","States"]}, + {"emoji":"🇺🇾","name":"flag: Uruguay","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Uruguay"]}, + {"emoji":"🇺🇿","name":"flag: Uzbekistan","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Uzbekistan"]}, + {"emoji":"🇻🇦","name":"flag: Vatican City","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Vatican","City"]}, + {"emoji":"🇻🇨","name":"flag: St. Vincent & Grenadines","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","St.","Vincent","&","Grenadines"]}, + {"emoji":"🇻🇪","name":"flag: Venezuela","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Venezuela"]}, + {"emoji":"🇻🇬","name":"flag: British Virgin Islands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","British","Virgin","Islands"]}, + {"emoji":"🇻🇮","name":"flag: U.S. Virgin Islands","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","U.S.","Virgin","Islands"]}, + {"emoji":"🇻🇳","name":"flag: Vietnam","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Vietnam"]}, + {"emoji":"🇻🇺","name":"flag: Vanuatu","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Vanuatu"]}, + {"emoji":"🇼🇫","name":"flag: Wallis & Futuna","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Wallis","&","Futuna"]}, + {"emoji":"🇼🇸","name":"flag: Samoa","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Samoa"]}, + {"emoji":"🇽🇰","name":"flag: Kosovo","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Kosovo"]}, + {"emoji":"🇾🇪","name":"flag: Yemen","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Yemen"]}, + {"emoji":"🇾🇹","name":"flag: Mayotte","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Mayotte"]}, + {"emoji":"🇿🇦","name":"flag: South Africa","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","South","Africa"]}, + {"emoji":"🇿🇲","name":"flag: Zambia","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Zambia"]}, + {"emoji":"🇿🇼","name":"flag: Zimbabwe","category":"Flags","version":"2.0","skinTone":false,"keywords":["flag:","Zimbabwe"]}, + {"emoji":"🏴󠁧󠁢󠁥󠁮󠁧󠁿","name":"flag: England","category":"Flags","version":"5.0","skinTone":false,"keywords":["flag:","England"]}, + {"emoji":"🏴󠁧󠁢󠁳󠁣󠁴󠁿","name":"flag: Scotland","category":"Flags","version":"5.0","skinTone":false,"keywords":["flag:","Scotland"]}, + {"emoji":"🏴󠁧󠁢󠁷󠁬󠁳󠁿","name":"flag: Wales","category":"Flags","version":"5.0","skinTone":false,"keywords":["flag:","Wales"]} + ] +} diff --git a/libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/EmojiCatalogLoaderTest.kt b/libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/EmojiCatalogLoaderTest.kt new file mode 100644 index 0000000000..66a133d612 --- /dev/null +++ b/libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/EmojiCatalogLoaderTest.kt @@ -0,0 +1,52 @@ +package com.getcode.libs.emojis.reactions + +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class EmojiCatalogLoaderTest { + + @Test + fun `loads the bundled catalog and preserves category order`() = runBlocking { + val loader = EmojiCatalogLoader() + val catalog = loader.load() + + assertTrue(catalog.categories.isNotEmpty(), "catalog has no categories") + assertTrue(catalog.entries.isNotEmpty(), "catalog has no entries") + assertEquals( + listOf( + "Smileys & People", + "Animals & Nature", + "Food & Drink", + "Travel & Places", + "Activities", + "Objects", + "Symbols", + "Flags", + ), + catalog.categories, + ) + + // Every entry's category is one the file declared, in the same order it declared them. + val declared = catalog.categories.toSet() + assertTrue(catalog.entries.all { it.category in declared }, "an entry's category is not in the declared list") + + assertEquals(catalog.categories.first(), "Smileys & People") + assertTrue(catalog.firstCategoryEntries.isNotEmpty(), "first category has no entries") + assertTrue(catalog.firstCategoryEntries.all { it.category == catalog.categories.first() }) + } + + @Test + fun `caches after the first load`() = runBlocking { + val loader = EmojiCatalogLoader() + val first = loader.load() + val second = loader.load() + assertTrue(first === second, "second load did not return the cached instance") + } +} diff --git a/libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/EmojiPickerModelTest.kt b/libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/EmojiPickerModelTest.kt new file mode 100644 index 0000000000..aa58b12374 --- /dev/null +++ b/libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/EmojiPickerModelTest.kt @@ -0,0 +1,126 @@ +package com.getcode.libs.emojis.reactions + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class EmojiPickerModelTest { + + private val grin = EmojiCatalogEntry( + emoji = "😀", + name = "grinning face", + category = "Smileys & People", + version = "1.0", + skinTone = false, + keywords = listOf("happy", "smile"), + ) + private val heart = EmojiCatalogEntry( + emoji = "❤️", + name = "red heart", + category = "Smileys & People", + version = "1.0", + skinTone = false, + keywords = listOf("love"), + ) + private val pizza = EmojiCatalogEntry( + emoji = "🍕", + name = "pizza", + category = "Food & Drink", + version = "1.0", + skinTone = false, + keywords = listOf("food", "slice"), + ) + private val undrawableEntry = EmojiCatalogEntry( + emoji = "🫠", + name = "melting face", + category = "Smileys & People", + version = "14.0", + skinTone = false, + keywords = emptyList(), + ) + + private val catalog = EmojiCatalog( + categories = listOf("Smileys & People", "Food & Drink"), + entries = listOf(grin, heart, pizza, undrawableEntry), + ) + + @Test + fun `empty query returns frequently used row then one section per category, in catalog order`() { + val sections = EmojiPickerModel.sections( + catalog = catalog, + undrawable = emptySet(), + recents = listOf(heart.emoji, grin.emoji), + query = "", + ) + + assertEquals( + listOf(EmojiPickerModel.FREQUENTLY_USED_ID, "Smileys & People", "Food & Drink"), + sections.map { it.id }, + ) + assertEquals(listOf(heart, grin), sections.first().entries) + assertEquals(listOf(grin, heart, undrawableEntry), sections[1].entries) + assertEquals(listOf(pizza), sections[2].entries) + } + + @Test + fun `frequently used row is deduped and undrawable entries are dropped from it and every section`() { + val sections = EmojiPickerModel.sections( + catalog = catalog, + undrawable = setOf(undrawableEntry.emoji), + recents = listOf(heart.emoji, heart.emoji, undrawableEntry.emoji), + query = "", + ) + + assertEquals(listOf(heart), sections.first { it.id == EmojiPickerModel.FREQUENTLY_USED_ID }.entries) + assertTrue(sections.none { entry -> entry.entries.any { it.emoji == undrawableEntry.emoji } }) + } + + @Test + fun `no frequently used row when recents is empty`() { + val sections = EmojiPickerModel.sections( + catalog = catalog, + undrawable = emptySet(), + recents = emptyList(), + query = "", + ) + + assertEquals(listOf("Smileys & People", "Food & Drink"), sections.map { it.id }) + } + + @Test + fun `search matches name or keyword case-insensitively and returns one section, no frequently used row`() { + val sections = EmojiPickerModel.sections( + catalog = catalog, + undrawable = emptySet(), + recents = listOf(heart.emoji), + query = "SLICE", + ) + + assertEquals(listOf(EmojiPickerModel.SEARCH_RESULTS_ID), sections.map { it.id }) + assertEquals(listOf(pizza), sections.first().entries) + } + + @Test + fun `blank query (whitespace only) behaves as empty`() { + val sections = EmojiPickerModel.sections( + catalog = catalog, + undrawable = emptySet(), + recents = emptyList(), + query = " ", + ) + + assertEquals(listOf("Smileys & People", "Food & Drink"), sections.map { it.id }) + } + + @Test + fun `search excludes undrawable entries`() { + val sections = EmojiPickerModel.sections( + catalog = catalog, + undrawable = setOf(undrawableEntry.emoji), + recents = emptyList(), + query = "face", + ) + + assertEquals(listOf(grin), sections.first().entries) + } +} diff --git a/libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStoreTest.kt b/libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStoreTest.kt new file mode 100644 index 0000000000..dbeb64d90d --- /dev/null +++ b/libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStoreTest.kt @@ -0,0 +1,45 @@ +package com.getcode.libs.emojis.reactions + +import androidx.test.core.app.ApplicationProvider +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@RunWith(RobolectricTestRunner::class) +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 store = DataStoreRecentReactionsStore( + context = ApplicationProvider.getApplicationContext(), + owner = { owner }, + ) + + @Test + fun `each account keeps its own recents`() = runTest { + owner = "alice" + store.record("🎉") + store.record("🎉") + + owner = "bob" + assertTrue(store.stats().isEmpty(), "bob sees alice's recents") + store.record("🍕") + + owner = "alice" + assertEquals(setOf("🎉"), store.stats().keys) + assertEquals(2, store.stats().getValue("🎉").count) + } + + @Test + fun `nothing is recorded or read without a signed-in account`() = runTest { + owner = null + store.record("🎉") + assertTrue(store.stats().isEmpty()) + + owner = "carol" + assertTrue(store.stats().isEmpty()) + } +} diff --git a/libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsVectorTest.kt b/libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsVectorTest.kt new file mode 100644 index 0000000000..c219156b13 --- /dev/null +++ b/libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsVectorTest.kt @@ -0,0 +1,74 @@ +package com.getcode.libs.emojis.reactions + +import org.json.JSONArray +import org.json.JSONObject +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Instant + +/** + * `test-vectors/reactions.json`, read in place through this module's test resources. It is a + * verbatim copy of the orchestrator's canonical fixture (#17). A failure here is either a real regression or a cross-platform decision that + * has to be made in the canonical fixture and re-synced to both platforms — never a local edit. + * + * Covers `defaults` and `recents`; `merge`, `order` and `strip` are covered by + * `apps:flipcash:shared:chat`'s `ReactionVectorsTest`, next to where `ReactionState` lives, and + * `drawability` by the instrumented `EmojiDrawabilityVectorTest`, which needs a real emoji font. + */ +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class RecentReactionsVectorTest { + + @Test + fun `defaults match the reactions fixture`() { + val expected = section("defaults") + assertTrue(expected.length() > 0, "reactions.json loaded no defaults") + val expectedList = (0 until expected.length()).map { expected.getString(it) } + assertEquals(expectedList, RecentReactions.DEFAULTS, "RecentReactions.DEFAULTS") + } + + @Test + fun `recents vectors match the reactions fixture`() { + val vectors = section("recents") + assertTrue(vectors.length() > 0, "reactions.json loaded no recents vectors") + + for (i in 0 until vectors.length()) { + val vector = vectors.getJSONObject(i) + val name = vector.getString("name") + val note = vector.getString("note") + + val usesJson = vector.getJSONArray("uses") + var stats: Map = emptyMap() + for (k in 0 until usesJson.length()) { + val use = usesJson.getJSONObject(k) + stats = RecentReactions.record( + emoji = use.getString("emoji"), + at = Instant.fromEpochSeconds(use.getLong("usedAt")), + stats = stats, + ) + } + + val undrawableJson = vector.getJSONArray("undrawable") + val undrawable = (0 until undrawableJson.length()).map { undrawableJson.getString(it) }.toSet() + + val limit = vector.getInt("limit") + + val expectedJson = vector.getJSONArray("expect") + val expected = (0 until expectedJson.length()).map { expectedJson.getString(it) } + + val actual = RecentReactions.rank(stats, undrawable, limit) + assertEquals(expected, actual, "vector `$name`: $note") + } + } + + private fun section(name: String): JSONArray { + val json = javaClass.classLoader!! + .getResourceAsStream("reactions.json")!! + .bufferedReader().use { it.readText() } + return JSONObject(json).getJSONArray(name) + } +} diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ChatMessagingController.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ChatMessagingController.kt index 98d3105e01..8813c0d939 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ChatMessagingController.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/controllers/ChatMessagingController.kt @@ -109,6 +109,7 @@ class ChatMessagingController @Inject constructor( return repository.removeReaction(owner, chatId, messageId, emoji) } + /** [queryOptions]`.limit` (page size) is clamped at [REACTORS_PAGE_SIZE_LIMIT], the server cap. */ suspend fun getReactors( chatId: ChatId, messageId: Long, @@ -116,7 +117,8 @@ class ChatMessagingController @Inject constructor( queryOptions: QueryOptions = QueryOptions(), ): Result { val owner = runCatching { requireOwner() }.getOrElse { return Result.failure(it) } - return repository.getReactors(owner, chatId, messageId, emoji, queryOptions) + val clamped = queryOptions.copy(limit = queryOptions.limit.coerceAtMost(REACTORS_PAGE_SIZE_LIMIT)) + return repository.getReactors(owner, chatId, messageId, emoji, clamped) } suspend fun getReactionSummary( @@ -135,6 +137,32 @@ class ChatMessagingController @Inject constructor( return repository.getReactionSummaries(owner, chatId, queryOptions) } + /** + * Fetches reaction summaries for [messageIds], chunking into batches of at most + * [MESSAGE_ID_BATCH_LIMIT] (the server's `MessageIdBatch` cap) and merging the results. + * Fails fast on the first chunk that fails; earlier chunks already fetched are discarded + * with it since a partial summary set isn't safe to seed reaction state from. + */ + suspend fun getReactionSummariesByIds( + chatId: ChatId, + messageIds: List, + ): Result> { + if (messageIds.isEmpty()) return Result.success(emptyList()) + val owner = runCatching { requireOwner() }.getOrElse { return Result.failure(it) } + val merged = mutableListOf() + for (chunk in messageIds.chunked(MESSAGE_ID_BATCH_LIMIT)) { + val result = repository.getReactionSummariesByIds(owner, chatId, chunk) + val summaries = result.getOrElse { return Result.failure(it) } + merged += summaries + } + return Result.success(merged) + } + + companion object { + private const val MESSAGE_ID_BATCH_LIMIT = 100 + private const val REACTORS_PAGE_SIZE_LIMIT = 100 + } + suspend fun advancePointer( chatId: ChatId, pointerType: PointerType, diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ChatMessagingApi.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ChatMessagingApi.kt index 70222b90b9..3563dfd296 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ChatMessagingApi.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/ChatMessagingApi.kt @@ -323,4 +323,26 @@ internal class ChatMessagingApi @Inject constructor( api.getReactionSummaries(request) } } + + /** [messageIds] must be non-empty and at most 100 — the `MessageIdBatch` proto cap. */ + suspend fun getReactionSummariesByIds( + owner: KeyPair, + chatId: ChatId, + messageIds: List, + ): RpcMessagingService.GetReactionSummariesResponse { + val request = RpcMessagingService.GetReactionSummariesRequest.newBuilder() + .setChatId(chatId.asChatId()) + .setMessageIds( + MessagingModel.MessageIdBatch.newBuilder() + .addAllMessageIds(messageIds.map { MessagingModel.MessageId.newBuilder().setValue(it).build() }) + ) + .apply { setAuth(authenticate(owner)) } + .build() + + request.validate().orThrow() + + return withContext(Dispatchers.IO) { + api.getReactionSummaries(request) + } + } } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt index 73353bca7a..b93ccbdbd2 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt @@ -266,7 +266,7 @@ internal fun MessagingModel.EmojiReaction.toEmojiReaction(): EmojiReaction { count = count, selfReactor = if (hasSelfReactor()) selfReactor.toReactor() else null, sampleReactors = sampleReactorsList.map { it.toReactor() }, - sequence = version, + version = version, ) } @@ -289,7 +289,7 @@ internal fun MessagingModel.ReactionUpdate.toReactionUpdate(): ReactionUpdate { else -> ReactionUpdate.Action.UNKNOWN }, count = count, - sequence = version, + version = version, reactedAt = Instant.fromEpochSeconds(reactedTs.seconds, reactedTs.nanos), ) } diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ChatMessagingService.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ChatMessagingService.kt index 0b07b0459d..dd8a5a5758 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ChatMessagingService.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/services/ChatMessagingService.kt @@ -18,7 +18,9 @@ import com.flipcash.services.models.SendMessageError import com.flipcash.services.models.GetMessageError import com.flipcash.services.models.GetMessagesError import com.flipcash.services.models.NotifyIsTypingError +import com.flipcash.services.models.PagingToken import com.flipcash.services.models.QueryOptions +import com.flipcash.services.internal.network.extensions.toPagingToken import com.flipcash.services.models.RemoveReactionError import com.flipcash.services.models.chat.ChatId import com.flipcash.services.models.chat.ClientMessageId @@ -325,6 +327,7 @@ internal class ChatMessagingService @Inject constructor( GetReactorsResult( reactors = response.reactorsList.map { it.toReactor() }, hasMore = response.hasMore, + pagingToken = if (response.hasPagingToken()) response.pagingToken.toPagingToken() else null, ) ) RpcMessagingService.GetReactorsResponse.Result.DENIED -> Result.failure(GetReactorsError.Denied()) @@ -384,6 +387,30 @@ internal class ChatMessagingService @Inject constructor( } ) } + + /** One `MessageIdBatch` call; [messageIds] must be non-empty and at most 100 (server cap). */ + suspend fun getReactionSummariesByIds( + owner: KeyPair, + chatId: ChatId, + messageIds: List, + ): Result> { + return runCatching { + api.getReactionSummariesByIds(owner, chatId, messageIds) + }.foldWithSuppression( + onSuccess = { response -> + when (response.result) { + RpcMessagingService.GetReactionSummariesResponse.Result.OK -> + Result.success(response.summariesList.map { it.toReactionSummary() }) + RpcMessagingService.GetReactionSummariesResponse.Result.DENIED -> Result.failure(GetReactionSummariesError.Denied()) + RpcMessagingService.GetReactionSummariesResponse.Result.UNRECOGNIZED -> Result.failure(GetReactionSummariesError.Unrecognized()) + else -> Result.failure(GetReactionSummariesError.Other()) + } + }, + onFailure = { cause -> + Result.failure(cause.toValidationOrElse { GetReactionSummariesError.Other(cause = it) }) + } + ) + } } data class GetDeltaResult( @@ -395,4 +422,5 @@ data class GetDeltaResult( data class GetReactorsResult( val reactors: List, val hasMore: Boolean, + val pagingToken: PagingToken? = null, ) diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalChatMessagingRepository.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalChatMessagingRepository.kt index 5ba26a2157..1e8ae54c97 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalChatMessagingRepository.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/internal/repositories/InternalChatMessagingRepository.kt @@ -122,7 +122,7 @@ internal class InternalChatMessagingRepository( queryOptions: QueryOptions, ): Result = service.getReactors(owner, chatId, messageId, emoji, queryOptions) .onFailure { ErrorUtils.handleError(it) } - .map { ReactorsPage(reactors = it.reactors, hasMore = it.hasMore) } + .map { ReactorsPage(reactors = it.reactors, hasMore = it.hasMore, nextToken = it.pagingToken) } override suspend fun getReactionSummary( owner: KeyPair, @@ -138,6 +138,13 @@ internal class InternalChatMessagingRepository( ): Result> = service.getReactionSummaries(owner, chatId, queryOptions) .onFailure { ErrorUtils.handleError(it) } + override suspend fun getReactionSummariesByIds( + owner: KeyPair, + chatId: ChatId, + messageIds: List, + ): Result> = service.getReactionSummariesByIds(owner, chatId, messageIds) + .onFailure { ErrorUtils.handleError(it) } + override suspend fun advancePointer( owner: KeyPair, chatId: ChatId, diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/EmojiReaction.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/EmojiReaction.kt index cfba62d242..e8a4e39ab9 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/EmojiReaction.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/EmojiReaction.kt @@ -5,5 +5,5 @@ data class EmojiReaction( val count: Long, val selfReactor: Reactor?, val sampleReactors: List, - val sequence: Long, + val version: Long, ) diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ReactionUpdate.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ReactionUpdate.kt index b99e96eb9f..f81524b896 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ReactionUpdate.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/models/chat/ReactionUpdate.kt @@ -9,7 +9,7 @@ data class ReactionUpdate( val actor: ID, val action: Action, val count: Long, - val sequence: Long, + val version: Long, val reactedAt: Instant, ) { enum class Action { diff --git a/services/flipcash/src/main/kotlin/com/flipcash/services/repository/ChatMessagingRepository.kt b/services/flipcash/src/main/kotlin/com/flipcash/services/repository/ChatMessagingRepository.kt index 3b472b5f8f..56a5086347 100644 --- a/services/flipcash/src/main/kotlin/com/flipcash/services/repository/ChatMessagingRepository.kt +++ b/services/flipcash/src/main/kotlin/com/flipcash/services/repository/ChatMessagingRepository.kt @@ -100,6 +100,17 @@ interface ChatMessagingRepository { queryOptions: QueryOptions, ): Result> + /** + * Fetches reaction summaries for specific messages. [messageIds] must be non-empty and at + * most 100 long — the server rejects a larger batch; callers with more ids chunk and merge + * (see [com.flipcash.services.controllers.ChatMessagingController.getReactionSummariesByIds]). + */ + suspend fun getReactionSummariesByIds( + owner: KeyPair, + chatId: ChatId, + messageIds: List, + ): Result> + suspend fun advancePointer( owner: KeyPair, chatId: ChatId, @@ -123,4 +134,6 @@ data class DeltaUpdate( data class ReactorsPage( val reactors: List, val hasMore: Boolean, + /** Pass as `queryOptions.token` on the next [ChatMessagingRepository.getReactors] call. Set only when [hasMore]. */ + val nextToken: com.flipcash.services.models.PagingToken? = null, ) diff --git a/services/flipcash/src/test/kotlin/com/flipcash/services/controllers/ChatMessagingControllerTest.kt b/services/flipcash/src/test/kotlin/com/flipcash/services/controllers/ChatMessagingControllerTest.kt index 9a69a0681c..3ddd48f844 100644 --- a/services/flipcash/src/test/kotlin/com/flipcash/services/controllers/ChatMessagingControllerTest.kt +++ b/services/flipcash/src/test/kotlin/com/flipcash/services/controllers/ChatMessagingControllerTest.kt @@ -61,7 +61,7 @@ class ChatMessagingControllerTest { count = count, selfReactor = null, sampleReactors = emptyList(), - sequence = 1, + version = 1, ) // region getMessage @@ -454,6 +454,77 @@ class ChatMessagingControllerTest { // endregion + // region getReactionSummariesByIds + + @Test + fun `getReactionSummariesByIds fails when no account cluster`() = runTest { + every { userManager.accountCluster } returns null + val result = controller.getReactionSummariesByIds(testChatId, listOf(1L)) + assertTrue(result.isFailure) + assertIs(result.exceptionOrNull()) + } + + @Test + fun `getReactionSummariesByIds succeeds without touching the repository when empty`() = runTest { + stubOwner() + + val result = controller.getReactionSummariesByIds(testChatId, emptyList()) + + assertEquals(emptyList(), result.getOrThrow()) + assertEquals(emptyList>(), repository.getReactionSummariesByIdsCalls) + } + + @Test + fun `getReactionSummariesByIds sends ids under 100 as a single chunk`() = runTest { + stubOwner() + val ids = (1L..5L).toList() + repository.getReactionSummariesByIdsResult = { chunk -> Result.success(chunk.map { ReactionSummary(messageId = it, reactions = emptyList()) }) } + + val result = controller.getReactionSummariesByIds(testChatId, ids) + + assertEquals(listOf(ids), repository.getReactionSummariesByIdsCalls) + assertEquals(5, result.getOrThrow().size) + } + + @Test + fun `getReactionSummariesByIds chunks over 100 ids and merges results`() = runTest { + stubOwner() + val ids = (1L..150L).toList() + repository.getReactionSummariesByIdsResult = { chunk -> Result.success(chunk.map { ReactionSummary(messageId = it, reactions = emptyList()) }) } + + val result = controller.getReactionSummariesByIds(testChatId, ids) + + assertEquals(2, repository.getReactionSummariesByIdsCalls.size) + assertEquals(100, repository.getReactionSummariesByIdsCalls[0].size) + assertEquals(50, repository.getReactionSummariesByIdsCalls[1].size) + assertEquals(150, result.getOrThrow().size) + assertEquals(ids, result.getOrThrow().map { it.messageId }) + } + + @Test + fun `getReactionSummariesByIds fails fast on the first failing chunk`() = runTest { + stubOwner() + val ids = (1L..150L).toList() + val cause = RuntimeException("boom") + var calls = 0 + repository.getReactionSummariesByIdsResult = { chunk -> + calls++ + if (calls == 1) { + Result.success(chunk.map { ReactionSummary(messageId = it, reactions = emptyList()) }) + } else { + Result.failure(cause) + } + } + + val result = controller.getReactionSummariesByIds(testChatId, ids) + + assertTrue(result.isFailure) + assertSame(cause, result.exceptionOrNull()) + assertEquals(2, repository.getReactionSummariesByIdsCalls.size) + } + + // endregion + // region getDelta @Test @@ -491,6 +562,8 @@ private class FakeChatMessagingRepository : ChatMessagingRepository { var getReactorsResult: Result = Result.failure(RuntimeException("not configured")) var getReactionSummaryResult: Result = Result.failure(RuntimeException("not configured")) var getReactionSummariesResult: Result> = Result.failure(RuntimeException("not configured")) + var getReactionSummariesByIdsResult: (List) -> Result> = { Result.failure(RuntimeException("not configured")) } + val getReactionSummariesByIdsCalls: MutableList> = mutableListOf() var advancePointerResult: Result = Result.failure(RuntimeException("not configured")) var notifyIsTypingResult: Result = Result.failure(RuntimeException("not configured")) @@ -566,6 +639,12 @@ private class FakeChatMessagingRepository : ChatMessagingRepository { return getReactionSummariesResult } + override suspend fun getReactionSummariesByIds(owner: Ed25519.KeyPair, chatId: ChatId, messageIds: List): Result> { + lastChatId = chatId + getReactionSummariesByIdsCalls += messageIds + return getReactionSummariesByIdsResult(messageIds) + } + override suspend fun advancePointer(owner: Ed25519.KeyPair, chatId: ChatId, pointerType: PointerType, messageId: Long): Result { lastChatId = chatId; lastPointerType = pointerType; lastMessageId = messageId return advancePointerResult diff --git a/test-vectors/README.md b/test-vectors/README.md index a1088bdd98..74d0f6f945 100644 --- a/test-vectors/README.md +++ b/test-vectors/README.md @@ -37,6 +37,7 @@ impl must reproduce the fixtures before the native duplicates are deleted. | `solana_message.json` | `:services:opencode` → `testDebugUnitTest` (host JVM) — **green** | `FlipcashCoreVectors` → xcodebuild on iOS Simulator — **green** | | `compact_message.json` | `:services:opencode` → `testDebugUnitTest` (host JVM) — **green** | `FlipcashCoreVectors` → xcodebuild on iOS Simulator — **green** | | `kikcode.json` + `kikcode_golden.svg` | `:libs:codes:kikcode` → `testAndroidHostTest` (host JVM) — **green** | *same Kotlin source* → `iosSimulatorArm64Test` (Kotlin/Native) — **green** | +| `reactions.json` | `:libs:emojis` + `:apps:flipcash:shared:chat` → `testDebugUnitTest` (host JVM); `drawability` via `:libs:emojis` androidTest → `connectedAndroidTest`. Read in place, no per-module copy. Canonical in the orchestrator (#17), not regenerated here | `FlipcashCoreVectors` → xcodebuild on iOS Simulator (copy of the orchestrator fixture) | Why the iOS split: **ed25519** lives in the standalone `CodeCurves` C package → host `swift test`. Everything else (**base58**, and later derivation/bonding curve) lives in **FlipcashCore**, whose diff --git a/test-vectors/reactions.json b/test-vectors/reactions.json new file mode 100644 index 0000000000..b8b86ea67d --- /dev/null +++ b/test-vectors/reactions.json @@ -0,0 +1,1766 @@ +{ + "defaults": [ + "❤️", + "👍", + "😂", + "😮", + "😢", + "🔥" + ], + "merge": [ + { + "name": "summary-renders", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 2, + "self": false, + "version": 4 + }, + { + "emoji": "🔥", + "count": 1, + "self": true, + "version": 1 + } + ] + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 2, + "selfReacted": false, + "pending": false + }, + { + "emoji": "🔥", + "count": 1, + "selfReacted": true, + "pending": false + } + ], + "calls": [], + "errors": [] + }, + "note": "A summary alone. Counts as given; self from self_reactor." + }, + { + "name": "stale-update-dropped", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 3, + "self": false, + "version": 7 + } + ] + }, + { + "op": "update", + "emoji": "👍", + "actor": "ana", + "action": "REMOVED", + "count": 2, + "version": 6 + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 3, + "selfReacted": false, + "pending": false + } + ], + "calls": [], + "errors": [] + }, + "note": "Version 6 is below the held 7, so it is an update the summary already folded in." + }, + { + "name": "equal-version-dropped", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 3, + "self": false, + "version": 7 + } + ] + }, + { + "op": "update", + "emoji": "👍", + "actor": "ana", + "action": "REMOVED", + "count": 2, + "version": 7 + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 3, + "selfReacted": false, + "pending": false + } + ], + "calls": [], + "errors": [] + }, + "note": "Accept strictly greater. The same version is the same state." + }, + { + "name": "newer-update-applied", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 3, + "self": false, + "version": 7 + } + ] + }, + { + "op": "update", + "emoji": "👍", + "actor": "ana", + "action": "ADDED", + "count": 4, + "version": 8 + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 4, + "selfReacted": false, + "pending": false + } + ], + "calls": [], + "errors": [] + }, + "note": "Count comes from the update, not from adding one: an update is state, not a delta." + }, + { + "name": "update-to-zero-removes-pill", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": false, + "version": 3 + } + ] + }, + { + "op": "update", + "emoji": "👍", + "actor": "ana", + "action": "REMOVED", + "count": 0, + "version": 4 + } + ], + "expect": { + "pills": [], + "calls": [], + "errors": [] + }, + "note": "Zero count, no pill." + }, + { + "name": "tombstone-rejects-late-add", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": false, + "version": 3 + } + ] + }, + { + "op": "update", + "emoji": "👍", + "actor": "ana", + "action": "REMOVED", + "count": 0, + "version": 4 + }, + { + "op": "update", + "emoji": "👍", + "actor": "ana", + "action": "ADDED", + "count": 1, + "version": 3 + } + ], + "expect": { + "pills": [], + "calls": [], + "errors": [] + }, + "note": "The server keeps an emptied emoji's version, so the client must too. Without the tombstone the late ADDED at 3 would resurrect the pill." + }, + { + "name": "readd-after-empty", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": false, + "version": 3 + } + ] + }, + { + "op": "update", + "emoji": "👍", + "actor": "ana", + "action": "REMOVED", + "count": 0, + "version": 4 + }, + { + "op": "update", + "emoji": "👍", + "actor": "ben", + "action": "ADDED", + "count": 1, + "version": 5 + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 1, + "selfReacted": false, + "pending": false + } + ], + "calls": [], + "errors": [] + }, + "note": "A re-add comes above the tombstone and is accepted." + }, + { + "name": "new-emoji-from-stream", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": false, + "version": 1 + } + ] + }, + { + "op": "update", + "emoji": "🎉", + "actor": "ana", + "action": "ADDED", + "count": 1, + "version": 1 + } + ], + "expect": { + "pills": [ + { + "emoji": "🎉", + "count": 1, + "selfReacted": false, + "pending": false + }, + { + "emoji": "👍", + "count": 1, + "selfReacted": false, + "pending": false + } + ], + "calls": [], + "errors": [] + }, + "note": "An emoji we have never held has no watermark; any version is accepted. Equal counts fall back to UTF-8 byte order: 🎉 (F0 9F 8E 89) before 👍 (F0 9F 91 8D)." + }, + { + "name": "own-update-from-another-device", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": false, + "version": 1 + } + ] + }, + { + "op": "update", + "emoji": "👍", + "actor": "me", + "action": "ADDED", + "count": 2, + "version": 2 + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 2, + "selfReacted": true, + "pending": false + } + ], + "calls": [], + "errors": [] + }, + "note": "Our own reaction made on another device arrives on the stream and highlights the pill." + }, + { + "name": "tap-adds-optimistically", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": false, + "version": 1 + } + ] + }, + { + "op": "tap", + "emoji": "👍" + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 2, + "selfReacted": true, + "pending": true + } + ], + "calls": [ + { + "op": "add", + "emoji": "👍" + } + ], + "errors": [] + }, + "note": "The pill changes before the server answers, and is marked pending." + }, + { + "name": "tap-new-emoji", + "self": "me", + "steps": [ + { + "op": "tap", + "emoji": "🔥" + } + ], + "expect": { + "pills": [ + { + "emoji": "🔥", + "count": 1, + "selfReacted": true, + "pending": true + } + ], + "calls": [ + { + "op": "add", + "emoji": "🔥" + } + ], + "errors": [] + }, + "note": "Tapping an emoji the message does not have yet, from the strip or picker." + }, + { + "name": "tap-confirmed", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": false, + "version": 1 + } + ] + }, + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "respond", + "emoji": "👍", + "result": "OK", + "reaction": { + "count": 2, + "self": true, + "version": 2 + } + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 2, + "selfReacted": true, + "pending": false + } + ], + "calls": [ + { + "op": "add", + "emoji": "👍" + } + ], + "errors": [] + }, + "note": "The response's EmojiReaction replaces the pending state." + }, + { + "name": "tap-removes", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 2, + "self": true, + "version": 5 + } + ] + }, + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "respond", + "emoji": "👍", + "result": "OK", + "reaction": { + "count": 1, + "self": false, + "version": 6 + } + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 1, + "selfReacted": false, + "pending": false + } + ], + "calls": [ + { + "op": "remove", + "emoji": "👍" + } + ], + "errors": [] + }, + "note": "Tapping a highlighted pill removes." + }, + { + "name": "remove-last-reaction", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": true, + "version": 5 + } + ] + }, + { + "op": "tap", + "emoji": "👍" + } + ], + "expect": { + "pills": [], + "calls": [ + { + "op": "remove", + "emoji": "👍" + } + ], + "errors": [] + }, + "note": "Removing the only reaction hides the pill immediately, before the response." + }, + { + "name": "stream-during-pending-keeps-highlight", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": false, + "version": 1 + } + ] + }, + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "update", + "emoji": "👍", + "actor": "ana", + "action": "ADDED", + "count": 2, + "version": 2 + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 3, + "selfReacted": true, + "pending": true + } + ], + "calls": [ + { + "op": "add", + "emoji": "👍" + } + ], + "errors": [] + }, + "note": "Someone else's reaction lands while ours is on the wire: their count shows, our highlight stays." + }, + { + "name": "own-echo-before-response", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": false, + "version": 1 + } + ] + }, + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "update", + "emoji": "👍", + "actor": "me", + "action": "ADDED", + "count": 2, + "version": 2 + }, + { + "op": "respond", + "emoji": "👍", + "result": "OK", + "reaction": { + "count": 2, + "self": true, + "version": 2 + } + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 2, + "selfReacted": true, + "pending": false + } + ], + "calls": [ + { + "op": "add", + "emoji": "👍" + } + ], + "errors": [] + }, + "note": "The stream can deliver our own add before the RPC returns. It must not count us twice (3), and the response at the same version is then a no-op." + }, + { + "name": "coalesce-on-off-on", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": false, + "version": 1 + } + ] + }, + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "tap", + "emoji": "👍" + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 2, + "selfReacted": true, + "pending": true + } + ], + "calls": [ + { + "op": "add", + "emoji": "👍" + } + ], + "errors": [] + }, + "note": "Three taps while the first add is in flight: one call. The later taps only move the displayed state." + }, + { + "name": "coalesce-on-off-then-settle", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": false, + "version": 1 + } + ] + }, + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "respond", + "emoji": "👍", + "result": "OK", + "reaction": { + "count": 2, + "self": true, + "version": 2 + } + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 1, + "selfReacted": false, + "pending": true + } + ], + "calls": [ + { + "op": "add", + "emoji": "👍" + }, + { + "op": "remove", + "emoji": "👍" + } + ], + "errors": [] + }, + "note": "On, then off, while the add is in flight. The add lands; the final state (off) differs from the server's (on), so a remove follows, and the pill shows the off state as pending until it returns." + }, + { + "name": "coalesce-on-off-on-settle", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": false, + "version": 1 + } + ] + }, + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "respond", + "emoji": "👍", + "result": "OK", + "reaction": { + "count": 2, + "self": true, + "version": 2 + } + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 2, + "selfReacted": true, + "pending": false + } + ], + "calls": [ + { + "op": "add", + "emoji": "👍" + } + ], + "errors": [] + }, + "note": "On, off, on. The add lands and the final state already matches, so nothing more is sent." + }, + { + "name": "network-failure-rolls-back", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": false, + "version": 1 + } + ] + }, + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "respond", + "emoji": "👍", + "result": "NETWORK" + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 1, + "selfReacted": false, + "pending": false + } + ], + "calls": [ + { + "op": "add", + "emoji": "👍" + } + ], + "errors": [ + "reactionFailed" + ] + }, + "note": "Back to the confirmed state, with the generic error." + }, + { + "name": "denied-rolls-back", + "self": "me", + "steps": [ + { + "op": "tap", + "emoji": "🫩" + }, + { + "op": "respond", + "emoji": "🫩", + "result": "DENIED" + } + ], + "expect": { + "pills": [], + "calls": [ + { + "op": "add", + "emoji": "🫩" + } + ], + "errors": [ + "reactionFailed" + ] + }, + "note": "DENIED is shown like a network failure; the catalog should make it rare." + }, + { + "name": "too-many-types", + "self": "me", + "steps": [ + { + "op": "tap", + "emoji": "🦖" + }, + { + "op": "respond", + "emoji": "🦖", + "result": "TOO_MANY_REACTION_TYPES" + } + ], + "expect": { + "pills": [], + "calls": [ + { + "op": "add", + "emoji": "🦖" + } + ], + "errors": [ + "tooManyReactionTypes" + ] + }, + "note": "The 21st distinct emoji on a message." + }, + { + "name": "message-not-found-is-silent", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": false, + "version": 1 + } + ] + }, + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "respond", + "emoji": "👍", + "result": "MESSAGE_NOT_FOUND" + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 1, + "selfReacted": false, + "pending": false + } + ], + "calls": [ + { + "op": "add", + "emoji": "👍" + } + ], + "errors": [] + }, + "note": "The message was deleted; the stream is removing it, so no error is shown." + }, + { + "name": "cannot-react-is-silent", + "self": "me", + "steps": [ + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "respond", + "emoji": "👍", + "result": "CANNOT_REACT" + } + ], + "expect": { + "pills": [], + "calls": [ + { + "op": "add", + "emoji": "👍" + } + ], + "errors": [] + }, + "note": "The strip should not have been offered. Roll back quietly." + }, + { + "name": "failure-drops-coalesced-taps", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": false, + "version": 1 + } + ] + }, + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "respond", + "emoji": "👍", + "result": "NETWORK" + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 1, + "selfReacted": false, + "pending": false + } + ], + "calls": [ + { + "op": "add", + "emoji": "👍" + } + ], + "errors": [ + "reactionFailed" + ] + }, + "note": "Nothing is queued: a failure discards the taps made while it was in flight too." + }, + { + "name": "summary-omits-emptied-emoji", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": false, + "version": 1 + }, + { + "emoji": "🔥", + "count": 2, + "self": false, + "version": 3 + } + ] + }, + { + "op": "summary", + "reactions": [ + { + "emoji": "🔥", + "count": 2, + "self": false, + "version": 3 + } + ] + } + ], + "expect": { + "pills": [ + { + "emoji": "🔥", + "count": 2, + "selfReacted": false, + "pending": false + } + ], + "calls": [], + "errors": [] + }, + "note": "A later summary without 👍 means it emptied." + }, + { + "name": "summary-keeps-pending-emoji", + "self": "me", + "steps": [ + { + "op": "tap", + "emoji": "👍" + }, + { + "op": "summary", + "reactions": [ + { + "emoji": "🔥", + "count": 1, + "self": false, + "version": 1 + } + ] + } + ], + "expect": { + "pills": [ + { + "emoji": "👍", + "count": 1, + "selfReacted": true, + "pending": true + }, + { + "emoji": "🔥", + "count": 1, + "selfReacted": false, + "pending": false + } + ], + "calls": [ + { + "op": "add", + "emoji": "👍" + } + ], + "errors": [] + }, + "note": "A summary fetched before our add landed must not wipe the pending tap." + }, + { + "name": "reactions-are-per-emoji", + "self": "me", + "steps": [ + { + "op": "summary", + "reactions": [ + { + "emoji": "👍", + "count": 1, + "self": false, + "version": 9 + }, + { + "emoji": "🔥", + "count": 1, + "self": false, + "version": 2 + } + ] + }, + { + "op": "update", + "emoji": "🔥", + "actor": "ana", + "action": "ADDED", + "count": 2, + "version": 3 + } + ], + "expect": { + "pills": [ + { + "emoji": "🔥", + "count": 2, + "selfReacted": false, + "pending": false + }, + { + "emoji": "👍", + "count": 1, + "selfReacted": false, + "pending": false + } + ], + "calls": [], + "errors": [] + }, + "note": "Versions are per (message, emoji). 🔥 at 3 is fresh even though 👍 is at 9." + } + ], + "order": [ + { + "name": "count-descending", + "pills": [ + { + "emoji": "👍", + "count": 1, + "boostTotal": 0 + }, + { + "emoji": "🔥", + "count": 3, + "boostTotal": 0 + }, + { + "emoji": "😂", + "count": 2, + "boostTotal": 0 + } + ], + "expect": [ + "🔥", + "😂", + "👍" + ], + "note": "Phase 1: every boost total is 0, so count decides." + }, + { + "name": "boost-before-count", + "pills": [ + { + "emoji": "👍", + "count": 5, + "boostTotal": 0 + }, + { + "emoji": "💯", + "count": 2, + "boostTotal": 1000 + }, + { + "emoji": "👎", + "count": 1, + "boostTotal": 500 + } + ], + "expect": [ + "💯", + "👎", + "👍" + ], + "note": "Phase 2: any boost outranks any count. The unit is opaque here, only its order matters." + }, + { + "name": "tie-on-utf8-bytes", + "pills": [ + { + "emoji": "😂", + "count": 1, + "boostTotal": 0 + }, + { + "emoji": "👍", + "count": 1, + "boostTotal": 0 + }, + { + "emoji": "❤️", + "count": 1, + "boostTotal": 0 + } + ], + "expect": [ + "❤️", + "👍", + "😂" + ], + "note": "Ties fall back to the server's order, UTF-8 byte order. ❤️ starts E2, the others F0." + }, + { + "name": "tie-utf8-not-utf16", + "pills": [ + { + "emoji": "☝🏻", + "count": 1, + "boostTotal": 0 + }, + { + "emoji": "☝️", + "count": 1, + "boostTotal": 0 + } + ], + "expect": [ + "☝️", + "☝🏻" + ], + "note": "The case that separates byte order from a platform's default string compare. Both start U+261D. Next, ☝️ has U+FE0F (UTF-8 EF B8 8F) and ☝🏻 has U+1F3FB (F0 9F 8F BB): EF < F0 in UTF-8, but in UTF-16 the surrogate D83C < FE0F, which would flip them." + } + ], + "strip": [ + { + "name": "no-reactions", + "recents": [ + "😂", + "❤️", + "🔥", + "👍", + "😭", + "✨" + ], + "selfReactions": [], + "expect": [ + { + "emoji": "😂", + "highlighted": false + }, + { + "emoji": "❤️", + "highlighted": false + }, + { + "emoji": "🔥", + "highlighted": false + }, + { + "emoji": "👍", + "highlighted": false + }, + { + "emoji": "😭", + "highlighted": false + }, + { + "emoji": "✨", + "highlighted": false + } + ], + "note": "Recents, in rank order." + }, + { + "name": "reacted-with-a-recent", + "recents": [ + "😂", + "❤️", + "🔥", + "👍", + "😭", + "✨" + ], + "selfReactions": [ + { + "emoji": "🔥", + "reactedAt": 100 + } + ], + "expect": [ + { + "emoji": "😂", + "highlighted": false + }, + { + "emoji": "❤️", + "highlighted": false + }, + { + "emoji": "🔥", + "highlighted": true + }, + { + "emoji": "👍", + "highlighted": false + }, + { + "emoji": "😭", + "highlighted": false + }, + { + "emoji": "✨", + "highlighted": false + } + ], + "note": "A reaction already in the recents is highlighted in place, not moved." + }, + { + "name": "reacted-outside-recents", + "recents": [ + "😂", + "❤️", + "🔥", + "👍", + "😭", + "✨" + ], + "selfReactions": [ + { + "emoji": "💥", + "reactedAt": 100 + } + ], + "expect": [ + { + "emoji": "😂", + "highlighted": false + }, + { + "emoji": "❤️", + "highlighted": false + }, + { + "emoji": "🔥", + "highlighted": false + }, + { + "emoji": "👍", + "highlighted": false + }, + { + "emoji": "😭", + "highlighted": false + }, + { + "emoji": "✨", + "highlighted": false + }, + { + "emoji": "💥", + "highlighted": true + } + ], + "note": "After the six, next to +. The WhatsApp screenshot in the design review." + }, + { + "name": "several-outside-newest-first", + "recents": [ + "😂", + "❤️", + "🔥", + "👍", + "😭", + "✨" + ], + "selfReactions": [ + { + "emoji": "💥", + "reactedAt": 100 + }, + { + "emoji": "🦖", + "reactedAt": 300 + }, + { + "emoji": "🫠", + "reactedAt": 200 + } + ], + "expect": [ + { + "emoji": "😂", + "highlighted": false + }, + { + "emoji": "❤️", + "highlighted": false + }, + { + "emoji": "🔥", + "highlighted": false + }, + { + "emoji": "👍", + "highlighted": false + }, + { + "emoji": "😭", + "highlighted": false + }, + { + "emoji": "✨", + "highlighted": false + }, + { + "emoji": "🦖", + "highlighted": true + }, + { + "emoji": "🫠", + "highlighted": true + }, + { + "emoji": "💥", + "highlighted": true + } + ], + "note": "Newest reaction first, so the one you just made is the one visible before scrolling." + }, + { + "name": "mixed", + "recents": [ + "😂", + "❤️", + "🔥", + "👍", + "😭", + "✨" + ], + "selfReactions": [ + { + "emoji": "❤️", + "reactedAt": 50 + }, + { + "emoji": "🦖", + "reactedAt": 60 + } + ], + "expect": [ + { + "emoji": "😂", + "highlighted": false + }, + { + "emoji": "❤️", + "highlighted": true + }, + { + "emoji": "🔥", + "highlighted": false + }, + { + "emoji": "👍", + "highlighted": false + }, + { + "emoji": "😭", + "highlighted": false + }, + { + "emoji": "✨", + "highlighted": false + }, + { + "emoji": "🦖", + "highlighted": true + } + ], + "note": "Only the reaction outside the recents is appended." + } + ], + "recents": [ + { + "name": "empty-pads-defaults", + "uses": [], + "undrawable": [], + "limit": 6, + "expect": [ + "❤️", + "👍", + "😂", + "😮", + "😢", + "🔥" + ], + "note": "Nothing used yet: the defaults from node 10286:2573, in their order." + }, + { + "name": "frequency-first", + "uses": [ + { + "emoji": "🦖", + "usedAt": 1 + }, + { + "emoji": "🦖", + "usedAt": 2 + }, + { + "emoji": "🦖", + "usedAt": 3 + }, + { + "emoji": "🫠", + "usedAt": 4 + } + ], + "undrawable": [], + "limit": 6, + "expect": [ + "🦖", + "🫠", + "❤️", + "👍", + "😂", + "😮" + ], + "note": "Most used first, then padding with the defaults." + }, + { + "name": "recency-breaks-ties", + "uses": [ + { + "emoji": "🦖", + "usedAt": 1 + }, + { + "emoji": "🫠", + "usedAt": 2 + }, + { + "emoji": "🎉", + "usedAt": 3 + } + ], + "undrawable": [], + "limit": 6, + "expect": [ + "🎉", + "🫠", + "🦖", + "❤️", + "👍", + "😂" + ], + "note": "Equal counts: most recently used first." + }, + { + "name": "padding-skips-used-defaults", + "uses": [ + { + "emoji": "👍", + "usedAt": 1 + }, + { + "emoji": "👍", + "usedAt": 2 + } + ], + "undrawable": [], + "limit": 6, + "expect": [ + "👍", + "❤️", + "😂", + "😮", + "😢", + "🔥" + ], + "note": "A default already ranked is not repeated." + }, + { + "name": "undrawable-skipped", + "uses": [ + { + "emoji": "🫩", + "usedAt": 1 + }, + { + "emoji": "🫩", + "usedAt": 2 + }, + { + "emoji": "🦖", + "usedAt": 3 + } + ], + "undrawable": [ + "🫩" + ], + "limit": 6, + "expect": [ + "🦖", + "❤️", + "👍", + "😂", + "😮", + "😢" + ], + "note": "Emoji this device cannot draw are not offered, even if used on another OS version." + }, + { + "name": "picker-row-of-seven", + "uses": [], + "undrawable": [], + "limit": 7, + "expect": [ + "❤️", + "👍", + "😂", + "😮", + "😢", + "🔥" + ], + "note": "The picker asks for 7 and there are 6 defaults, so an unused picker shows 6. Not padded further: the row is not worth an invented seventh default." + }, + { + "name": "limit-truncates", + "uses": [ + { + "emoji": "🦖", + "usedAt": 0 + }, + { + "emoji": "🫠", + "usedAt": 1 + }, + { + "emoji": "🎉", + "usedAt": 2 + }, + { + "emoji": "💥", + "usedAt": 3 + }, + { + "emoji": "✨", + "usedAt": 4 + }, + { + "emoji": "😭", + "usedAt": 5 + }, + { + "emoji": "🥺", + "usedAt": 6 + }, + { + "emoji": "🙏", + "usedAt": 7 + } + ], + "undrawable": [], + "limit": 6, + "expect": [ + "🙏", + "🥺", + "😭", + "✨", + "💥", + "🎉" + ], + "note": "All used once, so recency orders them and the oldest two drop." + } + ], + "drawability": [ + { + "emoji": "😀", + "drawable": true, + "note": "Emoji 1.0, single code point." + }, + { + "emoji": "👍🏽", + "drawable": true, + "note": "Base plus skin-tone modifier." + }, + { + "emoji": "🧑🏻‍🤝‍🧑🏿", + "drawable": true, + "note": "Mixed-skin-tone ZWJ sequence, Emoji 12.1. The case a glyph-count rule gets wrong on iOS." + }, + { + "emoji": "🐈‍🦄", + "drawable": false, + "note": "Not an RGI sequence, so no font has a ligature; it draws as two emoji." + }, + { + "emoji": "🇦🇦", + "drawable": false, + "note": "Regional indicators that are not a flag draw as two letters." + } + ] +} diff --git a/ui/navigation/src/main/kotlin/com/getcode/navigation/scenes/ModalBottomSheetSceneStrategy.kt b/ui/navigation/src/main/kotlin/com/getcode/navigation/scenes/ModalBottomSheetSceneStrategy.kt index 03665b4042..a8ebd3c7a9 100644 --- a/ui/navigation/src/main/kotlin/com/getcode/navigation/scenes/ModalBottomSheetSceneStrategy.kt +++ b/ui/navigation/src/main/kotlin/com/getcode/navigation/scenes/ModalBottomSheetSceneStrategy.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.key +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -27,6 +28,7 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.platform.LocalView @@ -54,6 +56,7 @@ import com.getcode.navigation.scrim.ScrimOverlay import com.getcode.theme.CodeTheme import com.getcode.ui.core.noRippleClickable import com.getcode.ui.utils.LocalSheetExpansionState +import com.getcode.ui.utils.LocalSheetOverhang import com.getcode.ui.utils.LocalSheetGesturesState import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -244,6 +247,13 @@ internal class ModalBottomSheetScene constructor( val composeScope = rememberCoroutineScope() + // The sheet's own height only changes on relayout, never mid-drag, so measure it once + // and let [LocalSheetOverhang] subtract the live drag offset at placement time. + var sheetHeightPx by remember { mutableFloatStateOf(0f) } + val sheetOverhang = remember(sheetState) { + { (sheetHeightPx - sheetState.offset).coerceAtLeast(0f) } + } + val dismiss = { hide: Boolean -> if (hide && sheetState.currentDetent != SheetDetent.Hidden) { composeScope.launch { @@ -279,6 +289,7 @@ internal class ModalBottomSheetScene constructor( allowDismiss = enabled && !navigator.sheetDragDisabled }, LocalSheetExpansionState provides setContentOverflows, + LocalSheetOverhang provides sheetOverhang, LocalScrimController provides scrim, ) { BackHandler(enabled = effectiveProperties.dismissOnBackPress) { @@ -333,6 +344,7 @@ internal class ModalBottomSheetScene constructor( Sheet( modifier = Modifier .fillMaxWidth() + .onSizeChanged { sheetHeightPx = it.height.toFloat() } // Rounded corners read as a card lifted over the screen behind it. // A fullscreen sheet has no screen behind it to be lifted over, and // the curves would only carve two notches out of the display's own diff --git a/ui/navigation/src/main/kotlin/com/getcode/ui/utils/SheetExpansion.kt b/ui/navigation/src/main/kotlin/com/getcode/ui/utils/SheetExpansion.kt index 1903beca80..29830823bd 100644 --- a/ui/navigation/src/main/kotlin/com/getcode/ui/utils/SheetExpansion.kt +++ b/ui/navigation/src/main/kotlin/com/getcode/ui/utils/SheetExpansion.kt @@ -36,3 +36,14 @@ fun AllowSheetExpansionWhenScrollable(scrollState: LazyListState) { onDispose { setExpandable(false) } } } + +/** + * How far, in px, the host sheet currently runs below the bottom of its container. + * + * A [com.getcode.navigation.HalfSheet] that can expand is laid out at its expanded height and slid + * down, so at the resting detent (and mid-drag) its bottom is off screen. Content that pins + * something to the bottom of the screen lifts it by this much. Read it in a placement or draw + * lambda, not in composition: it tracks the sheet's drag offset, and reading it there moves with + * the sheet in the same frame instead of recomposing every frame. + */ +val LocalSheetOverhang = staticCompositionLocalOf<() -> Float> { { 0f } }