diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.android.kt new file mode 100644 index 0000000000..0bb9179299 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.android.kt @@ -0,0 +1,7 @@ +package chat.simplex.common.views.chat + +import androidx.compose.runtime.* + +// Android keyboards have their own emoji panel +@Composable +actual fun ComposeEmojiButton(pickerShown: MutableState, enabled: Boolean, onEmoji: (String) -> Unit) {} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.kt new file mode 100644 index 0000000000..ff8482ed7b --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.kt @@ -0,0 +1,14 @@ +package chat.simplex.common.views.chat + +import androidx.compose.runtime.* +import androidx.compose.ui.text.TextRange +import kotlin.math.min + +@Composable +expect fun ComposeEmojiButton(pickerShown: MutableState, enabled: Boolean, onEmoji: (String) -> Unit) + +fun ComposeMessage.insertingEmoji(emoji: String): ComposeMessage { + val start = min(selection.min, text.length) + val end = min(selection.max, text.length) + return ComposeMessage(text.replaceRange(start, end, emoji), TextRange(start + emoji.length)) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 92ccd92f93..357a010d2c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -454,6 +454,7 @@ fun ComposeView( val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) val textStyle = remember(MaterialTheme.colors.isLight) { mutableStateOf(smallFont) } val recState: MutableState = remember { mutableStateOf(RecordingState.NotStarted) } + val emojiPickerShown = rememberSaveable { mutableStateOf(false) } AttachmentSelection(composeState, attachmentOption, composeState::processPickedFile) { uris, text -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(uris, text) } } suspend fun fetchAndUpdateLinkPreview(url: String) { @@ -1278,6 +1279,17 @@ fun ComposeView( val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason((ownerRelayState?.noActiveRelays ?: subscriberRelayState?.noActiveRelays) == true)) val sendMsgEnabled = rememberUpdatedState(userCantSendReason.value == null) val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv) + val emojiEnabled = sendMsgEnabled.value && composeState.value.preview !is ComposePreview.VoicePreview + LaunchedEffect(emojiEnabled) { + if (!emojiEnabled) emojiPickerShown.value = false + } + KeyChangeEffect(emojiPickerShown.value) { + if (!emojiPickerShown.value && emojiEnabled) focusRequester?.requestFocus() + } + + fun onEmoji(emoji: String) { + if (!composeState.value.inProgress) onMessageChange(composeState.value.message.insertingEmoji(emoji)) + } @Composable fun CommandsButton() { @@ -1332,7 +1344,7 @@ fun ComposeView( } @Composable - fun AttachmentAndCommandsButtons() { + fun ComposeActionButtons() { val cInfo = chat.chatInfo Row( Modifier.padding(start = 3.dp, end = 1.dp, bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier), @@ -1346,6 +1358,7 @@ fun ComposeView( if (showAttachment) { AttachmentButton() } + ComposeEmojiButton(emojiPickerShown, emojiEnabled, ::onEmoji) } } @@ -1784,7 +1797,7 @@ fun ComposeView( ContextSendMessageToConnect(generalGetString(MR.strings.compose_send_direct_message_to_connect)) Divider() Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) { - AttachmentAndCommandsButtons() + ComposeActionButtons() SendMsgView_( disableSendButton = disableSendButton, sendToConnect = { withApi { sendMemberContactInvitation() } } @@ -1839,7 +1852,7 @@ fun ComposeView( ) } else { Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) { - AttachmentAndCommandsButtons() + ComposeActionButtons() val broadcastPlaceholder = (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.let { gi -> if (gi.useRelays && gi.membership.memberRole >= GroupMemberRole.Owner && chat.chatInfo.groupChatScope() == null) generalGetString(MR.strings.compose_view_broadcast) else null diff --git a/apps/multiplatform/common/src/commonMain/moko-resources/base/strings.xml b/apps/multiplatform/common/src/commonMain/moko-resources/base/strings.xml index 326be2b05d..9f37a86e52 100644 --- a/apps/multiplatform/common/src/commonMain/moko-resources/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/moko-resources/base/strings.xml @@ -587,6 +587,17 @@ Attach + Emoji + Smileys & emotion + People & body + Animals & nature + Food & drink + Travel & places + Activities + Objects + Symbols + Flags + No emoji found Context icon Cancel image preview Cancel file preview diff --git a/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ComposeEmojiTest.kt b/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ComposeEmojiTest.kt new file mode 100644 index 0000000000..da2fecff60 --- /dev/null +++ b/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ComposeEmojiTest.kt @@ -0,0 +1,42 @@ +package chat.simplex.app + +import androidx.compose.ui.text.TextRange +import chat.simplex.common.views.chat.ComposeMessage +import chat.simplex.common.views.chat.insertingEmoji +import kotlin.test.Test +import kotlin.test.assertEquals + +// use this command for testing: +// ./gradlew desktopTest +class ComposeEmojiTest { + @Test + fun testInsertIntoEmptyMessage() { + assertEquals(ComposeMessage("๐Ÿ˜€", TextRange(2)), ComposeMessage().insertingEmoji("๐Ÿ˜€")) + } + + @Test + fun testInsertAtCursor() { + assertEquals(ComposeMessage("hi๐Ÿ˜€ there", TextRange(4)), ComposeMessage("hi there", TextRange(2)).insertingEmoji("๐Ÿ˜€")) + assertEquals(ComposeMessage("๐Ÿ˜€hi", TextRange(2)), ComposeMessage("hi", TextRange(0)).insertingEmoji("๐Ÿ˜€")) + assertEquals(ComposeMessage("hi๐Ÿ‘", TextRange(4)), ComposeMessage("hi").insertingEmoji("๐Ÿ‘")) + } + + @Test + fun testReplaceSelection() { + assertEquals(ComposeMessage("say โค๏ธ!", TextRange(6)), ComposeMessage("say love!", TextRange(4, 8)).insertingEmoji("โค๏ธ")) + // selection made right to left has start > end + assertEquals(ComposeMessage("say โค๏ธ!", TextRange(6)), ComposeMessage("say love!", TextRange(8, 4)).insertingEmoji("โค๏ธ")) + } + + @Test + fun testInsertAfterEmoji() { + val flag = "๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ" + val withFlag = ComposeMessage(flag) + assertEquals(ComposeMessage(flag + "๐Ÿ‘ฉโ€๐Ÿ’ป", TextRange(flag.length + "๐Ÿ‘ฉโ€๐Ÿ’ป".length)), withFlag.insertingEmoji("๐Ÿ‘ฉโ€๐Ÿ’ป")) + } + + @Test + fun testSelectionBeyondTextIsClamped() { + assertEquals(ComposeMessage("ab๐Ÿ˜€", TextRange(4)), ComposeMessage("ab", TextRange(5)).insertingEmoji("๐Ÿ˜€")) + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.desktop.kt new file mode 100644 index 0000000000..65a3bb8c5a --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.desktop.kt @@ -0,0 +1,248 @@ +package chat.simplex.common.views.chat + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.grid.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.input.key.* +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.* +import androidx.compose.ui.window.* +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF +import chat.simplex.common.ui.theme.EmojiFont +import chat.simplex.common.views.helpers.KeyChangeEffect +import chat.simplex.common.views.helpers.SearchTextField +import chat.simplex.res.* +import dev.icerock.moko.resources.StringResource +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.launch + +private const val EMOJI_DATA_RESOURCE = "/emoji/emoji.txt" +private const val EMOJI_GROUP_PREFIX = "@" +private val EMOJI_POPUP_WIDTH = 360.dp +private val EMOJI_POPUP_HEIGHT = 420.dp +private val EMOJI_CELL_SIZE = 38.dp +private val EMOJI_SIZE = 26.dp +private val EMOJI_TAB_SIZE = 32.dp +private val EMOJI_TAB_ICON_SIZE = 20.dp +// the buttons left of the message field overlap by 8 dp, this keeps the emoji button a little apart +private val EMOJI_BUTTON_START_PADDING = 4.dp + +enum class EmojiCategory(val unicodeGroup: String, val icon: String, val title: StringResource) { + Smileys("Smileys & Emotion", "๐Ÿ˜€", MR.strings.emoji_category_smileys), + People("People & Body", "๐Ÿ‘‹", MR.strings.emoji_category_people), + Animals("Animals & Nature", "๐Ÿป", MR.strings.emoji_category_animals), + Food("Food & Drink", "๐Ÿ”", MR.strings.emoji_category_food), + Travel("Travel & Places", "๐Ÿš—", MR.strings.emoji_category_travel), + Activities("Activities", "โšฝ", MR.strings.emoji_category_activities), + Objects("Objects", "๐Ÿ’ก", MR.strings.emoji_category_objects), + Symbols("Symbols", "๐Ÿ”ฃ", MR.strings.emoji_category_symbols), + Flags("Flags", "๐Ÿ", MR.strings.emoji_category_flags), +} + +data class EmojiData(val emoji: String, val name: String) + +data class EmojiSection(val category: EmojiCategory, val emojis: List) + +fun parseEmojiData(lines: Sequence): List { + val sections = mutableListOf() + var emojis = mutableListOf() + for (line in lines) { + if (line.isEmpty()) continue + if (line.startsWith(EMOJI_GROUP_PREFIX)) { + val group = line.removePrefix(EMOJI_GROUP_PREFIX) + val category = EmojiCategory.entries.firstOrNull { it.unicodeGroup == group } + ?: throw IllegalArgumentException("unknown emoji group: $group") + emojis = mutableListOf() + sections.add(EmojiSection(category, emojis)) + } else { + val (emoji, name) = line.split("\t", limit = 2) + emojis.add(EmojiData(emoji, name)) + } + } + return sections +} + +val emojiSections: List by lazy { + val stream = EmojiCategory::class.java.getResourceAsStream(EMOJI_DATA_RESOURCE) + ?: throw IllegalStateException("missing resource $EMOJI_DATA_RESOURCE") + stream.bufferedReader().useLines(::parseEmojiData) +} + +fun searchEmoji(sections: List, query: String): List { + val q = query.trim() + return sections.flatMap { s -> s.emojis.filter { it.name.contains(q, ignoreCase = true) } } +} + +@Composable +actual fun ComposeEmojiButton(pickerShown: MutableState, enabled: Boolean, onEmoji: (String) -> Unit) { + // the popup opens above the button, so in a short window it must not be taller than the space there + var spaceAbove by remember { mutableStateOf(EMOJI_POPUP_HEIGHT) } + val density = LocalDensity.current + Box(Modifier.padding(start = EMOJI_BUTTON_START_PADDING).onGloballyPositioned { spaceAbove = with(density) { it.positionInWindow().y.toDp() } }) { + EmojiIconButton(painterResource(MR.images.ic_add_reaction), stringResource(MR.strings.icon_descr_emoji), enabled) { + pickerShown.value = !pickerShown.value + } + if (pickerShown.value) { + Popup( + popupPositionProvider = AboveAnchorPositionProvider, + onDismissRequest = { pickerShown.value = false }, + properties = PopupProperties(focusable = true), + // closing on KeyUp, as the window does, so that the same key release does not also close the top modal + onKeyEvent = { + if (it.key == Key.Escape) { + if (it.type == KeyEventType.KeyUp) pickerShown.value = false + true + } else false + } + ) { + EmojiPicker(height = minOf(EMOJI_POPUP_HEIGHT, spaceAbove), onEmoji = onEmoji) + } + } + } +} + +private object AboveAnchorPositionProvider : PopupPositionProvider { + override fun calculatePosition(anchorBounds: IntRect, windowSize: IntSize, layoutDirection: LayoutDirection, popupContentSize: IntSize): IntOffset { + val x = if (layoutDirection == LayoutDirection.Ltr) anchorBounds.left else anchorBounds.right - popupContentSize.width + return IntOffset( + x.coerceIn(0, maxOf(0, windowSize.width - popupContentSize.width)), + (anchorBounds.top - popupContentSize.height).coerceAtLeast(0) + ) + } +} + +@Composable +private fun EmojiPicker(height: Dp, onEmoji: (String) -> Unit) { + val sections = remember { emojiSections } + val query = remember { mutableStateOf("") } + val found = remember { derivedStateOf { if (query.value.isBlank()) null else searchEmoji(sections, query.value) } } + val gridState = rememberLazyGridState() + KeyChangeEffect(found.value) { gridState.scrollToItem(0) } + val headerIndexes = remember(sections) { + var index = 0 + sections.map { s -> index.also { index += s.emojis.size + 1 } } + } + Surface( + Modifier.size(EMOJI_POPUP_WIDTH, height), + shape = RoundedCornerShape(12.dp), + elevation = 8.dp, + border = BorderStroke(1.dp, MaterialTheme.colors.secondary.copy(alpha = 0.2f)) + ) { + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painterResource(MR.images.ic_search), + contentDescription = null, + Modifier.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING_HALF).size(22.dp), + tint = MaterialTheme.colors.secondary + ) + SearchTextField(Modifier.weight(1f), alwaysVisible = false) { query.value = it } + } + if (found.value == null) { + EmojiCategoryTabs(sections, headerIndexes, gridState) + } + Divider() + LazyVerticalGrid( + columns = GridCells.Adaptive(EMOJI_CELL_SIZE), + state = gridState, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(6.dp) + ) { + val foundEmojis = found.value + if (foundEmojis == null) { + sections.forEach { s -> + sectionHeader(s.category.title) + items(s.emojis) { EmojiCell(it.emoji, onEmoji) } + } + } else if (foundEmojis.isEmpty()) { + sectionHeader(MR.strings.no_emoji_found) + } else { + items(foundEmojis) { EmojiCell(it.emoji, onEmoji) } + } + } + } + } +} + +@Composable +private fun EmojiCategoryTabs(sections: List, headerIndexes: List, gridState: LazyGridState) { + val scope = rememberCoroutineScope() + val selected = remember { + derivedStateOf { headerIndexes.indexOfLast { it <= gridState.firstVisibleItemIndex } } + } + Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween) { + sections.forEachIndexed { i, s -> + EmojiTab(selected = selected.value == i, onClick = { scope.launch { gridState.scrollToItem(headerIndexes[i]) } }) { + Text(s.category.icon, fontFamily = EmojiFont, fontSize = with(LocalDensity.current) { EMOJI_TAB_ICON_SIZE.toSp() }) + } + } + } +} + +@Composable +private fun EmojiTab(selected: Boolean, onClick: () -> Unit, content: @Composable () -> Unit) { + Box( + Modifier + .size(EMOJI_TAB_SIZE) + .clip(RoundedCornerShape(8.dp)) + .background(if (selected) MaterialTheme.colors.secondary.copy(alpha = 0.15f) else Color.Transparent) + .focusProperties { canFocus = false } + .clickable(onClick = onClick), + contentAlignment = Alignment.Center + ) { + content() + } +} + +private fun LazyGridScope.sectionHeader(title: StringResource) { + item(span = { GridItemSpan(maxLineSpan) }) { + Text( + stringResource(title), + Modifier.padding(start = 4.dp, top = 8.dp, bottom = 4.dp), + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.secondary + ) + } +} + +@Composable +private fun EmojiCell(emoji: String, onEmoji: (String) -> Unit) { + Box( + Modifier + .size(EMOJI_CELL_SIZE) + .clip(RoundedCornerShape(8.dp)) + // not focusable, so that the search field keeps focus and typing after a click goes to search + .focusProperties { canFocus = false } + .clickable { onEmoji(emoji) }, + contentAlignment = Alignment.Center + ) { + Text(emoji, fontFamily = EmojiFont, fontSize = with(LocalDensity.current) { EMOJI_SIZE.toSp() }, maxLines = 1) + } +} + +@Composable +private fun EmojiIconButton(icon: Painter, contentDescription: String, enabled: Boolean, onClick: () -> Unit) { + IconButton(onClick, enabled = enabled) { + Icon( + icon, + contentDescription = contentDescription, + tint = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + modifier = Modifier + .size(28.dp) + .clip(CircleShape) + ) + } +} diff --git a/apps/multiplatform/common/src/desktopMain/resources/emoji/emoji.txt b/apps/multiplatform/common/src/desktopMain/resources/emoji/emoji.txt new file mode 100644 index 0000000000..2a25c14c9f --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/resources/emoji/emoji.txt @@ -0,0 +1,1879 @@ +@Smileys & Emotion +๐Ÿ˜€ grinning face +๐Ÿ˜ƒ grinning face with big eyes +๐Ÿ˜„ grinning face with smiling eyes +๐Ÿ˜ beaming face with smiling eyes +๐Ÿ˜† grinning squinting face +๐Ÿ˜… grinning face with sweat +๐Ÿคฃ rolling on the floor laughing +๐Ÿ˜‚ face with tears of joy +๐Ÿ™‚ slightly smiling face +๐Ÿ™ƒ upside-down face +๐Ÿซ  melting face +๐Ÿ˜‰ winking face +๐Ÿ˜Š smiling face with smiling eyes +๐Ÿ˜‡ smiling face with halo +๐Ÿฅฐ smiling face with hearts +๐Ÿ˜ smiling face with heart-eyes +๐Ÿคฉ star-struck +๐Ÿ˜˜ face blowing a kiss +๐Ÿ˜— kissing face +โ˜บ๏ธ smiling face +๐Ÿ˜š kissing face with closed eyes +๐Ÿ˜™ kissing face with smiling eyes +๐Ÿฅฒ smiling face with tear +๐Ÿ˜‹ face savoring food +๐Ÿ˜› face with tongue +๐Ÿ˜œ winking face with tongue +๐Ÿคช zany face +๐Ÿ˜ squinting face with tongue +๐Ÿค‘ money-mouth face +๐Ÿค— smiling face with open hands +๐Ÿคญ face with hand over mouth +๐Ÿซข face with open eyes and hand over mouth +๐Ÿซฃ face with peeking eye +๐Ÿคซ shushing face +๐Ÿค” thinking face +๐Ÿซก saluting face +๐Ÿค zipper-mouth face +๐Ÿคจ face with raised eyebrow +๐Ÿ˜ neutral face +๐Ÿ˜‘ expressionless face +๐Ÿ˜ถ face without mouth +๐Ÿซฅ dotted line face +๐Ÿ˜ถโ€๐ŸŒซ๏ธ face in clouds +๐Ÿ˜ smirking face +๐Ÿ˜’ unamused face +๐Ÿ™„ face with rolling eyes +๐Ÿ˜ฌ grimacing face +๐Ÿ˜ฎโ€๐Ÿ’จ face exhaling +๐Ÿคฅ lying face +๐Ÿซจ shaking face +๐Ÿ˜Œ relieved face +๐Ÿ˜” pensive face +๐Ÿ˜ช sleepy face +๐Ÿคค drooling face +๐Ÿ˜ด sleeping face +๐Ÿ˜ท face with medical mask +๐Ÿค’ face with thermometer +๐Ÿค• face with head-bandage +๐Ÿคข nauseated face +๐Ÿคฎ face vomiting +๐Ÿคง sneezing face +๐Ÿฅต hot face +๐Ÿฅถ cold face +๐Ÿฅด woozy face +๐Ÿ˜ต face with crossed-out eyes +๐Ÿ˜ตโ€๐Ÿ’ซ face with spiral eyes +๐Ÿคฏ exploding head +๐Ÿค  cowboy hat face +๐Ÿฅณ partying face +๐Ÿฅธ disguised face +๐Ÿ˜Ž smiling face with sunglasses +๐Ÿค“ nerd face +๐Ÿง face with monocle +๐Ÿ˜• confused face +๐Ÿซค face with diagonal mouth +๐Ÿ˜Ÿ worried face +๐Ÿ™ slightly frowning face +โ˜น๏ธ frowning face +๐Ÿ˜ฎ face with open mouth +๐Ÿ˜ฏ hushed face +๐Ÿ˜ฒ astonished face +๐Ÿ˜ณ flushed face +๐Ÿฅบ pleading face +๐Ÿฅน face holding back tears +๐Ÿ˜ฆ frowning face with open mouth +๐Ÿ˜ง anguished face +๐Ÿ˜จ fearful face +๐Ÿ˜ฐ anxious face with sweat +๐Ÿ˜ฅ sad but relieved face +๐Ÿ˜ข crying face +๐Ÿ˜ญ loudly crying face +๐Ÿ˜ฑ face screaming in fear +๐Ÿ˜– confounded face +๐Ÿ˜ฃ persevering face +๐Ÿ˜ž disappointed face +๐Ÿ˜“ downcast face with sweat +๐Ÿ˜ฉ weary face +๐Ÿ˜ซ tired face +๐Ÿฅฑ yawning face +๐Ÿ˜ค face with steam from nose +๐Ÿ˜ก enraged face +๐Ÿ˜  angry face +๐Ÿคฌ face with symbols on mouth +๐Ÿ˜ˆ smiling face with horns +๐Ÿ‘ฟ angry face with horns +๐Ÿ’€ skull +โ˜ ๏ธ skull and crossbones +๐Ÿ’ฉ pile of poo +๐Ÿคก clown face +๐Ÿ‘น ogre +๐Ÿ‘บ goblin +๐Ÿ‘ป ghost +๐Ÿ‘ฝ alien +๐Ÿ‘พ alien monster +๐Ÿค– robot +๐Ÿ˜บ grinning cat +๐Ÿ˜ธ grinning cat with smiling eyes +๐Ÿ˜น cat with tears of joy +๐Ÿ˜ป smiling cat with heart-eyes +๐Ÿ˜ผ cat with wry smile +๐Ÿ˜ฝ kissing cat +๐Ÿ™€ weary cat +๐Ÿ˜ฟ crying cat +๐Ÿ˜พ pouting cat +๐Ÿ™ˆ see-no-evil monkey +๐Ÿ™‰ hear-no-evil monkey +๐Ÿ™Š speak-no-evil monkey +๐Ÿ’Œ love letter +๐Ÿ’˜ heart with arrow +๐Ÿ’ heart with ribbon +๐Ÿ’– sparkling heart +๐Ÿ’— growing heart +๐Ÿ’“ beating heart +๐Ÿ’ž revolving hearts +๐Ÿ’• two hearts +๐Ÿ’Ÿ heart decoration +โฃ๏ธ heart exclamation +๐Ÿ’” broken heart +โค๏ธโ€๐Ÿ”ฅ heart on fire +โค๏ธโ€๐Ÿฉน mending heart +โค๏ธ red heart +๐Ÿฉท pink heart +๐Ÿงก orange heart +๐Ÿ’› yellow heart +๐Ÿ’š green heart +๐Ÿ’™ blue heart +๐Ÿฉต light blue heart +๐Ÿ’œ purple heart +๐ŸคŽ brown heart +๐Ÿ–ค black heart +๐Ÿฉถ grey heart +๐Ÿค white heart +๐Ÿ’‹ kiss mark +๐Ÿ’ฏ hundred points +๐Ÿ’ข anger symbol +๐Ÿ’ฅ collision +๐Ÿ’ซ dizzy +๐Ÿ’ฆ sweat droplets +๐Ÿ’จ dashing away +๐Ÿ•ณ๏ธ hole +๐Ÿ’ฌ speech balloon +๐Ÿ‘๏ธโ€๐Ÿ—จ๏ธ eye in speech bubble +๐Ÿ—จ๏ธ left speech bubble +๐Ÿ—ฏ๏ธ right anger bubble +๐Ÿ’ญ thought balloon +๐Ÿ’ค ZZZ +@People & Body +๐Ÿ‘‹ waving hand +๐Ÿคš raised back of hand +๐Ÿ–๏ธ hand with fingers splayed +โœ‹ raised hand +๐Ÿ–– vulcan salute +๐Ÿซฑ rightwards hand +๐Ÿซฒ leftwards hand +๐Ÿซณ palm down hand +๐Ÿซด palm up hand +๐Ÿซท leftwards pushing hand +๐Ÿซธ rightwards pushing hand +๐Ÿ‘Œ OK hand +๐ŸคŒ pinched fingers +๐Ÿค pinching hand +โœŒ๏ธ victory hand +๐Ÿคž crossed fingers +๐Ÿซฐ hand with index finger and thumb crossed +๐ŸคŸ love-you gesture +๐Ÿค˜ sign of the horns +๐Ÿค™ call me hand +๐Ÿ‘ˆ backhand index pointing left +๐Ÿ‘‰ backhand index pointing right +๐Ÿ‘† backhand index pointing up +๐Ÿ–• middle finger +๐Ÿ‘‡ backhand index pointing down +โ˜๏ธ index pointing up +๐Ÿซต index pointing at the viewer +๐Ÿ‘ thumbs up +๐Ÿ‘Ž thumbs down +โœŠ raised fist +๐Ÿ‘Š oncoming fist +๐Ÿค› left-facing fist +๐Ÿคœ right-facing fist +๐Ÿ‘ clapping hands +๐Ÿ™Œ raising hands +๐Ÿซถ heart hands +๐Ÿ‘ open hands +๐Ÿคฒ palms up together +๐Ÿค handshake +๐Ÿ™ folded hands +โœ๏ธ writing hand +๐Ÿ’… nail polish +๐Ÿคณ selfie +๐Ÿ’ช flexed biceps +๐Ÿฆพ mechanical arm +๐Ÿฆฟ mechanical leg +๐Ÿฆต leg +๐Ÿฆถ foot +๐Ÿ‘‚ ear +๐Ÿฆป ear with hearing aid +๐Ÿ‘ƒ nose +๐Ÿง  brain +๐Ÿซ€ anatomical heart +๐Ÿซ lungs +๐Ÿฆท tooth +๐Ÿฆด bone +๐Ÿ‘€ eyes +๐Ÿ‘๏ธ eye +๐Ÿ‘… tongue +๐Ÿ‘„ mouth +๐Ÿซฆ biting lip +๐Ÿ‘ถ baby +๐Ÿง’ child +๐Ÿ‘ฆ boy +๐Ÿ‘ง girl +๐Ÿง‘ person +๐Ÿ‘ฑ person: blond hair +๐Ÿ‘จ man +๐Ÿง” person: beard +๐Ÿง”โ€โ™‚๏ธ man: beard +๐Ÿง”โ€โ™€๏ธ woman: beard +๐Ÿ‘จโ€๐Ÿฆฐ man: red hair +๐Ÿ‘จโ€๐Ÿฆฑ man: curly hair +๐Ÿ‘จโ€๐Ÿฆณ man: white hair +๐Ÿ‘จโ€๐Ÿฆฒ man: bald +๐Ÿ‘ฉ woman +๐Ÿ‘ฉโ€๐Ÿฆฐ woman: red hair +๐Ÿง‘โ€๐Ÿฆฐ person: red hair +๐Ÿ‘ฉโ€๐Ÿฆฑ woman: curly hair +๐Ÿง‘โ€๐Ÿฆฑ person: curly hair +๐Ÿ‘ฉโ€๐Ÿฆณ woman: white hair +๐Ÿง‘โ€๐Ÿฆณ person: white hair +๐Ÿ‘ฉโ€๐Ÿฆฒ woman: bald +๐Ÿง‘โ€๐Ÿฆฒ person: bald +๐Ÿ‘ฑโ€โ™€๏ธ woman: blond hair +๐Ÿ‘ฑโ€โ™‚๏ธ man: blond hair +๐Ÿง“ older person +๐Ÿ‘ด old man +๐Ÿ‘ต old woman +๐Ÿ™ person frowning +๐Ÿ™โ€โ™‚๏ธ man frowning +๐Ÿ™โ€โ™€๏ธ woman frowning +๐Ÿ™Ž person pouting +๐Ÿ™Žโ€โ™‚๏ธ man pouting +๐Ÿ™Žโ€โ™€๏ธ woman pouting +๐Ÿ™… person gesturing NO +๐Ÿ™…โ€โ™‚๏ธ man gesturing NO +๐Ÿ™…โ€โ™€๏ธ woman gesturing NO +๐Ÿ™† person gesturing OK +๐Ÿ™†โ€โ™‚๏ธ man gesturing OK +๐Ÿ™†โ€โ™€๏ธ woman gesturing OK +๐Ÿ’ person tipping hand +๐Ÿ’โ€โ™‚๏ธ man tipping hand +๐Ÿ’โ€โ™€๏ธ woman tipping hand +๐Ÿ™‹ person raising hand +๐Ÿ™‹โ€โ™‚๏ธ man raising hand +๐Ÿ™‹โ€โ™€๏ธ woman raising hand +๐Ÿง deaf person +๐Ÿงโ€โ™‚๏ธ deaf man +๐Ÿงโ€โ™€๏ธ deaf woman +๐Ÿ™‡ person bowing +๐Ÿ™‡โ€โ™‚๏ธ man bowing +๐Ÿ™‡โ€โ™€๏ธ woman bowing +๐Ÿคฆ person facepalming +๐Ÿคฆโ€โ™‚๏ธ man facepalming +๐Ÿคฆโ€โ™€๏ธ woman facepalming +๐Ÿคท person shrugging +๐Ÿคทโ€โ™‚๏ธ man shrugging +๐Ÿคทโ€โ™€๏ธ woman shrugging +๐Ÿง‘โ€โš•๏ธ health worker +๐Ÿ‘จโ€โš•๏ธ man health worker +๐Ÿ‘ฉโ€โš•๏ธ woman health worker +๐Ÿง‘โ€๐ŸŽ“ student +๐Ÿ‘จโ€๐ŸŽ“ man student +๐Ÿ‘ฉโ€๐ŸŽ“ woman student +๐Ÿง‘โ€๐Ÿซ teacher +๐Ÿ‘จโ€๐Ÿซ man teacher +๐Ÿ‘ฉโ€๐Ÿซ woman teacher +๐Ÿง‘โ€โš–๏ธ judge +๐Ÿ‘จโ€โš–๏ธ man judge +๐Ÿ‘ฉโ€โš–๏ธ woman judge +๐Ÿง‘โ€๐ŸŒพ farmer +๐Ÿ‘จโ€๐ŸŒพ man farmer +๐Ÿ‘ฉโ€๐ŸŒพ woman farmer +๐Ÿง‘โ€๐Ÿณ cook +๐Ÿ‘จโ€๐Ÿณ man cook +๐Ÿ‘ฉโ€๐Ÿณ woman cook +๐Ÿง‘โ€๐Ÿ”ง mechanic +๐Ÿ‘จโ€๐Ÿ”ง man mechanic +๐Ÿ‘ฉโ€๐Ÿ”ง woman mechanic +๐Ÿง‘โ€๐Ÿญ factory worker +๐Ÿ‘จโ€๐Ÿญ man factory worker +๐Ÿ‘ฉโ€๐Ÿญ woman factory worker +๐Ÿง‘โ€๐Ÿ’ผ office worker +๐Ÿ‘จโ€๐Ÿ’ผ man office worker +๐Ÿ‘ฉโ€๐Ÿ’ผ woman office worker +๐Ÿง‘โ€๐Ÿ”ฌ scientist +๐Ÿ‘จโ€๐Ÿ”ฌ man scientist +๐Ÿ‘ฉโ€๐Ÿ”ฌ woman scientist +๐Ÿง‘โ€๐Ÿ’ป technologist +๐Ÿ‘จโ€๐Ÿ’ป man technologist +๐Ÿ‘ฉโ€๐Ÿ’ป woman technologist +๐Ÿง‘โ€๐ŸŽค singer +๐Ÿ‘จโ€๐ŸŽค man singer +๐Ÿ‘ฉโ€๐ŸŽค woman singer +๐Ÿง‘โ€๐ŸŽจ artist +๐Ÿ‘จโ€๐ŸŽจ man artist +๐Ÿ‘ฉโ€๐ŸŽจ woman artist +๐Ÿง‘โ€โœˆ๏ธ pilot +๐Ÿ‘จโ€โœˆ๏ธ man pilot +๐Ÿ‘ฉโ€โœˆ๏ธ woman pilot +๐Ÿง‘โ€๐Ÿš€ astronaut +๐Ÿ‘จโ€๐Ÿš€ man astronaut +๐Ÿ‘ฉโ€๐Ÿš€ woman astronaut +๐Ÿง‘โ€๐Ÿš’ firefighter +๐Ÿ‘จโ€๐Ÿš’ man firefighter +๐Ÿ‘ฉโ€๐Ÿš’ woman firefighter +๐Ÿ‘ฎ police officer +๐Ÿ‘ฎโ€โ™‚๏ธ man police officer +๐Ÿ‘ฎโ€โ™€๏ธ woman police officer +๐Ÿ•ต๏ธ detective +๐Ÿ•ต๏ธโ€โ™‚๏ธ man detective +๐Ÿ•ต๏ธโ€โ™€๏ธ woman detective +๐Ÿ’‚ guard +๐Ÿ’‚โ€โ™‚๏ธ man guard +๐Ÿ’‚โ€โ™€๏ธ woman guard +๐Ÿฅท ninja +๐Ÿ‘ท construction worker +๐Ÿ‘ทโ€โ™‚๏ธ man construction worker +๐Ÿ‘ทโ€โ™€๏ธ woman construction worker +๐Ÿซ… person with crown +๐Ÿคด prince +๐Ÿ‘ธ princess +๐Ÿ‘ณ person wearing turban +๐Ÿ‘ณโ€โ™‚๏ธ man wearing turban +๐Ÿ‘ณโ€โ™€๏ธ woman wearing turban +๐Ÿ‘ฒ person with skullcap +๐Ÿง• woman with headscarf +๐Ÿคต person in tuxedo +๐Ÿคตโ€โ™‚๏ธ man in tuxedo +๐Ÿคตโ€โ™€๏ธ woman in tuxedo +๐Ÿ‘ฐ person with veil +๐Ÿ‘ฐโ€โ™‚๏ธ man with veil +๐Ÿ‘ฐโ€โ™€๏ธ woman with veil +๐Ÿคฐ pregnant woman +๐Ÿซƒ pregnant man +๐Ÿซ„ pregnant person +๐Ÿคฑ breast-feeding +๐Ÿ‘ฉโ€๐Ÿผ woman feeding baby +๐Ÿ‘จโ€๐Ÿผ man feeding baby +๐Ÿง‘โ€๐Ÿผ person feeding baby +๐Ÿ‘ผ baby angel +๐ŸŽ… Santa Claus +๐Ÿคถ Mrs. Claus +๐Ÿง‘โ€๐ŸŽ„ mx claus +๐Ÿฆธ superhero +๐Ÿฆธโ€โ™‚๏ธ man superhero +๐Ÿฆธโ€โ™€๏ธ woman superhero +๐Ÿฆน supervillain +๐Ÿฆนโ€โ™‚๏ธ man supervillain +๐Ÿฆนโ€โ™€๏ธ woman supervillain +๐Ÿง™ mage +๐Ÿง™โ€โ™‚๏ธ man mage +๐Ÿง™โ€โ™€๏ธ woman mage +๐Ÿงš fairy +๐Ÿงšโ€โ™‚๏ธ man fairy +๐Ÿงšโ€โ™€๏ธ woman fairy +๐Ÿง› vampire +๐Ÿง›โ€โ™‚๏ธ man vampire +๐Ÿง›โ€โ™€๏ธ woman vampire +๐Ÿงœ merperson +๐Ÿงœโ€โ™‚๏ธ merman +๐Ÿงœโ€โ™€๏ธ mermaid +๐Ÿง elf +๐Ÿงโ€โ™‚๏ธ man elf +๐Ÿงโ€โ™€๏ธ woman elf +๐Ÿงž genie +๐Ÿงžโ€โ™‚๏ธ man genie +๐Ÿงžโ€โ™€๏ธ woman genie +๐ŸงŸ zombie +๐ŸงŸโ€โ™‚๏ธ man zombie +๐ŸงŸโ€โ™€๏ธ woman zombie +๐ŸงŒ troll +๐Ÿ’† person getting massage +๐Ÿ’†โ€โ™‚๏ธ man getting massage +๐Ÿ’†โ€โ™€๏ธ woman getting massage +๐Ÿ’‡ person getting haircut +๐Ÿ’‡โ€โ™‚๏ธ man getting haircut +๐Ÿ’‡โ€โ™€๏ธ woman getting haircut +๐Ÿšถ person walking +๐Ÿšถโ€โ™‚๏ธ man walking +๐Ÿšถโ€โ™€๏ธ woman walking +๐Ÿง person standing +๐Ÿงโ€โ™‚๏ธ man standing +๐Ÿงโ€โ™€๏ธ woman standing +๐ŸงŽ person kneeling +๐ŸงŽโ€โ™‚๏ธ man kneeling +๐ŸงŽโ€โ™€๏ธ woman kneeling +๐Ÿง‘โ€๐Ÿฆฏ person with white cane +๐Ÿ‘จโ€๐Ÿฆฏ man with white cane +๐Ÿ‘ฉโ€๐Ÿฆฏ woman with white cane +๐Ÿง‘โ€๐Ÿฆผ person in motorized wheelchair +๐Ÿ‘จโ€๐Ÿฆผ man in motorized wheelchair +๐Ÿ‘ฉโ€๐Ÿฆผ woman in motorized wheelchair +๐Ÿง‘โ€๐Ÿฆฝ person in manual wheelchair +๐Ÿ‘จโ€๐Ÿฆฝ man in manual wheelchair +๐Ÿ‘ฉโ€๐Ÿฆฝ woman in manual wheelchair +๐Ÿƒ person running +๐Ÿƒโ€โ™‚๏ธ man running +๐Ÿƒโ€โ™€๏ธ woman running +๐Ÿ’ƒ woman dancing +๐Ÿ•บ man dancing +๐Ÿ•ด๏ธ person in suit levitating +๐Ÿ‘ฏ people with bunny ears +๐Ÿ‘ฏโ€โ™‚๏ธ men with bunny ears +๐Ÿ‘ฏโ€โ™€๏ธ women with bunny ears +๐Ÿง– person in steamy room +๐Ÿง–โ€โ™‚๏ธ man in steamy room +๐Ÿง–โ€โ™€๏ธ woman in steamy room +๐Ÿง— person climbing +๐Ÿง—โ€โ™‚๏ธ man climbing +๐Ÿง—โ€โ™€๏ธ woman climbing +๐Ÿคบ person fencing +๐Ÿ‡ horse racing +โ›ท๏ธ skier +๐Ÿ‚ snowboarder +๐ŸŒ๏ธ person golfing +๐ŸŒ๏ธโ€โ™‚๏ธ man golfing +๐ŸŒ๏ธโ€โ™€๏ธ woman golfing +๐Ÿ„ person surfing +๐Ÿ„โ€โ™‚๏ธ man surfing +๐Ÿ„โ€โ™€๏ธ woman surfing +๐Ÿšฃ person rowing boat +๐Ÿšฃโ€โ™‚๏ธ man rowing boat +๐Ÿšฃโ€โ™€๏ธ woman rowing boat +๐ŸŠ person swimming +๐ŸŠโ€โ™‚๏ธ man swimming +๐ŸŠโ€โ™€๏ธ woman swimming +โ›น๏ธ person bouncing ball +โ›น๏ธโ€โ™‚๏ธ man bouncing ball +โ›น๏ธโ€โ™€๏ธ woman bouncing ball +๐Ÿ‹๏ธ person lifting weights +๐Ÿ‹๏ธโ€โ™‚๏ธ man lifting weights +๐Ÿ‹๏ธโ€โ™€๏ธ woman lifting weights +๐Ÿšด person biking +๐Ÿšดโ€โ™‚๏ธ man biking +๐Ÿšดโ€โ™€๏ธ woman biking +๐Ÿšต person mountain biking +๐Ÿšตโ€โ™‚๏ธ man mountain biking +๐Ÿšตโ€โ™€๏ธ woman mountain biking +๐Ÿคธ person cartwheeling +๐Ÿคธโ€โ™‚๏ธ man cartwheeling +๐Ÿคธโ€โ™€๏ธ woman cartwheeling +๐Ÿคผ people wrestling +๐Ÿคผโ€โ™‚๏ธ men wrestling +๐Ÿคผโ€โ™€๏ธ women wrestling +๐Ÿคฝ person playing water polo +๐Ÿคฝโ€โ™‚๏ธ man playing water polo +๐Ÿคฝโ€โ™€๏ธ woman playing water polo +๐Ÿคพ person playing handball +๐Ÿคพโ€โ™‚๏ธ man playing handball +๐Ÿคพโ€โ™€๏ธ woman playing handball +๐Ÿคน person juggling +๐Ÿคนโ€โ™‚๏ธ man juggling +๐Ÿคนโ€โ™€๏ธ woman juggling +๐Ÿง˜ person in lotus position +๐Ÿง˜โ€โ™‚๏ธ man in lotus position +๐Ÿง˜โ€โ™€๏ธ woman in lotus position +๐Ÿ›€ person taking bath +๐Ÿ›Œ person in bed +๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ people holding hands +๐Ÿ‘ญ women holding hands +๐Ÿ‘ซ woman and man holding hands +๐Ÿ‘ฌ men holding hands +๐Ÿ’ kiss +๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ kiss: woman, man +๐Ÿ‘จโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ kiss: man, man +๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ kiss: woman, woman +๐Ÿ’‘ couple with heart +๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ‘จ couple with heart: woman, man +๐Ÿ‘จโ€โค๏ธโ€๐Ÿ‘จ couple with heart: man, man +๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ‘ฉ couple with heart: woman, woman +๐Ÿ‘ช family +๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ family: man, woman, boy +๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง family: man, woman, girl +๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ family: man, woman, girl, boy +๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ family: man, woman, boy, boy +๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง family: man, woman, girl, girl +๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆ family: man, man, boy +๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ง family: man, man, girl +๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ family: man, man, girl, boy +๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ family: man, man, boy, boy +๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง family: man, man, girl, girl +๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ family: woman, woman, boy +๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ง family: woman, woman, girl +๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ family: woman, woman, girl, boy +๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ family: woman, woman, boy, boy +๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง family: woman, woman, girl, girl +๐Ÿ‘จโ€๐Ÿ‘ฆ family: man, boy +๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ family: man, boy, boy +๐Ÿ‘จโ€๐Ÿ‘ง family: man, girl +๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ family: man, girl, boy +๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง family: man, girl, girl +๐Ÿ‘ฉโ€๐Ÿ‘ฆ family: woman, boy +๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ family: woman, boy, boy +๐Ÿ‘ฉโ€๐Ÿ‘ง family: woman, girl +๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ family: woman, girl, boy +๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง family: woman, girl, girl +๐Ÿ—ฃ๏ธ speaking head +๐Ÿ‘ค bust in silhouette +๐Ÿ‘ฅ busts in silhouette +๐Ÿซ‚ people hugging +๐Ÿ‘ฃ footprints +@Animals & Nature +๐Ÿต monkey face +๐Ÿ’ monkey +๐Ÿฆ gorilla +๐Ÿฆง orangutan +๐Ÿถ dog face +๐Ÿ• dog +๐Ÿฆฎ guide dog +๐Ÿ•โ€๐Ÿฆบ service dog +๐Ÿฉ poodle +๐Ÿบ wolf +๐ŸฆŠ fox +๐Ÿฆ raccoon +๐Ÿฑ cat face +๐Ÿˆ cat +๐Ÿˆโ€โฌ› black cat +๐Ÿฆ lion +๐Ÿฏ tiger face +๐Ÿ… tiger +๐Ÿ† leopard +๐Ÿด horse face +๐ŸซŽ moose +๐Ÿซ donkey +๐ŸŽ horse +๐Ÿฆ„ unicorn +๐Ÿฆ“ zebra +๐ŸฆŒ deer +๐Ÿฆฌ bison +๐Ÿฎ cow face +๐Ÿ‚ ox +๐Ÿƒ water buffalo +๐Ÿ„ cow +๐Ÿท pig face +๐Ÿ– pig +๐Ÿ— boar +๐Ÿฝ pig nose +๐Ÿ ram +๐Ÿ‘ ewe +๐Ÿ goat +๐Ÿช camel +๐Ÿซ two-hump camel +๐Ÿฆ™ llama +๐Ÿฆ’ giraffe +๐Ÿ˜ elephant +๐Ÿฆฃ mammoth +๐Ÿฆ rhinoceros +๐Ÿฆ› hippopotamus +๐Ÿญ mouse face +๐Ÿ mouse +๐Ÿ€ rat +๐Ÿน hamster +๐Ÿฐ rabbit face +๐Ÿ‡ rabbit +๐Ÿฟ๏ธ chipmunk +๐Ÿฆซ beaver +๐Ÿฆ” hedgehog +๐Ÿฆ‡ bat +๐Ÿป bear +๐Ÿปโ€โ„๏ธ polar bear +๐Ÿจ koala +๐Ÿผ panda +๐Ÿฆฅ sloth +๐Ÿฆฆ otter +๐Ÿฆจ skunk +๐Ÿฆ˜ kangaroo +๐Ÿฆก badger +๐Ÿพ paw prints +๐Ÿฆƒ turkey +๐Ÿ” chicken +๐Ÿ“ rooster +๐Ÿฃ hatching chick +๐Ÿค baby chick +๐Ÿฅ front-facing baby chick +๐Ÿฆ bird +๐Ÿง penguin +๐Ÿ•Š๏ธ dove +๐Ÿฆ… eagle +๐Ÿฆ† duck +๐Ÿฆข swan +๐Ÿฆ‰ owl +๐Ÿฆค dodo +๐Ÿชถ feather +๐Ÿฆฉ flamingo +๐Ÿฆš peacock +๐Ÿฆœ parrot +๐Ÿชฝ wing +๐Ÿฆโ€โฌ› black bird +๐Ÿชฟ goose +๐Ÿธ frog +๐ŸŠ crocodile +๐Ÿข turtle +๐ŸฆŽ lizard +๐Ÿ snake +๐Ÿฒ dragon face +๐Ÿ‰ dragon +๐Ÿฆ• sauropod +๐Ÿฆ– T-Rex +๐Ÿณ spouting whale +๐Ÿ‹ whale +๐Ÿฌ dolphin +๐Ÿฆญ seal +๐ŸŸ fish +๐Ÿ  tropical fish +๐Ÿก blowfish +๐Ÿฆˆ shark +๐Ÿ™ octopus +๐Ÿš spiral shell +๐Ÿชธ coral +๐Ÿชผ jellyfish +๐ŸŒ snail +๐Ÿฆ‹ butterfly +๐Ÿ› bug +๐Ÿœ ant +๐Ÿ honeybee +๐Ÿชฒ beetle +๐Ÿž lady beetle +๐Ÿฆ— cricket +๐Ÿชณ cockroach +๐Ÿ•ท๏ธ spider +๐Ÿ•ธ๏ธ spider web +๐Ÿฆ‚ scorpion +๐ŸฆŸ mosquito +๐Ÿชฐ fly +๐Ÿชฑ worm +๐Ÿฆ  microbe +๐Ÿ’ bouquet +๐ŸŒธ cherry blossom +๐Ÿ’ฎ white flower +๐Ÿชท lotus +๐Ÿต๏ธ rosette +๐ŸŒน rose +๐Ÿฅ€ wilted flower +๐ŸŒบ hibiscus +๐ŸŒป sunflower +๐ŸŒผ blossom +๐ŸŒท tulip +๐Ÿชป hyacinth +๐ŸŒฑ seedling +๐Ÿชด potted plant +๐ŸŒฒ evergreen tree +๐ŸŒณ deciduous tree +๐ŸŒด palm tree +๐ŸŒต cactus +๐ŸŒพ sheaf of rice +๐ŸŒฟ herb +โ˜˜๏ธ shamrock +๐Ÿ€ four leaf clover +๐Ÿ maple leaf +๐Ÿ‚ fallen leaf +๐Ÿƒ leaf fluttering in wind +๐Ÿชน empty nest +๐Ÿชบ nest with eggs +๐Ÿ„ mushroom +@Food & Drink +๐Ÿ‡ grapes +๐Ÿˆ melon +๐Ÿ‰ watermelon +๐ŸŠ tangerine +๐Ÿ‹ lemon +๐ŸŒ banana +๐Ÿ pineapple +๐Ÿฅญ mango +๐ŸŽ red apple +๐Ÿ green apple +๐Ÿ pear +๐Ÿ‘ peach +๐Ÿ’ cherries +๐Ÿ“ strawberry +๐Ÿซ blueberries +๐Ÿฅ kiwi fruit +๐Ÿ… tomato +๐Ÿซ’ olive +๐Ÿฅฅ coconut +๐Ÿฅ‘ avocado +๐Ÿ† eggplant +๐Ÿฅ” potato +๐Ÿฅ• carrot +๐ŸŒฝ ear of corn +๐ŸŒถ๏ธ hot pepper +๐Ÿซ‘ bell pepper +๐Ÿฅ’ cucumber +๐Ÿฅฌ leafy green +๐Ÿฅฆ broccoli +๐Ÿง„ garlic +๐Ÿง… onion +๐Ÿฅœ peanuts +๐Ÿซ˜ beans +๐ŸŒฐ chestnut +๐Ÿซš ginger root +๐Ÿซ› pea pod +๐Ÿž bread +๐Ÿฅ croissant +๐Ÿฅ– baguette bread +๐Ÿซ“ flatbread +๐Ÿฅจ pretzel +๐Ÿฅฏ bagel +๐Ÿฅž pancakes +๐Ÿง‡ waffle +๐Ÿง€ cheese wedge +๐Ÿ– meat on bone +๐Ÿ— poultry leg +๐Ÿฅฉ cut of meat +๐Ÿฅ“ bacon +๐Ÿ” hamburger +๐ŸŸ french fries +๐Ÿ• pizza +๐ŸŒญ hot dog +๐Ÿฅช sandwich +๐ŸŒฎ taco +๐ŸŒฏ burrito +๐Ÿซ” tamale +๐Ÿฅ™ stuffed flatbread +๐Ÿง† falafel +๐Ÿฅš egg +๐Ÿณ cooking +๐Ÿฅ˜ shallow pan of food +๐Ÿฒ pot of food +๐Ÿซ• fondue +๐Ÿฅฃ bowl with spoon +๐Ÿฅ— green salad +๐Ÿฟ popcorn +๐Ÿงˆ butter +๐Ÿง‚ salt +๐Ÿฅซ canned food +๐Ÿฑ bento box +๐Ÿ˜ rice cracker +๐Ÿ™ rice ball +๐Ÿš cooked rice +๐Ÿ› curry rice +๐Ÿœ steaming bowl +๐Ÿ spaghetti +๐Ÿ  roasted sweet potato +๐Ÿข oden +๐Ÿฃ sushi +๐Ÿค fried shrimp +๐Ÿฅ fish cake with swirl +๐Ÿฅฎ moon cake +๐Ÿก dango +๐ŸฅŸ dumpling +๐Ÿฅ  fortune cookie +๐Ÿฅก takeout box +๐Ÿฆ€ crab +๐Ÿฆž lobster +๐Ÿฆ shrimp +๐Ÿฆ‘ squid +๐Ÿฆช oyster +๐Ÿฆ soft ice cream +๐Ÿง shaved ice +๐Ÿจ ice cream +๐Ÿฉ doughnut +๐Ÿช cookie +๐ŸŽ‚ birthday cake +๐Ÿฐ shortcake +๐Ÿง cupcake +๐Ÿฅง pie +๐Ÿซ chocolate bar +๐Ÿฌ candy +๐Ÿญ lollipop +๐Ÿฎ custard +๐Ÿฏ honey pot +๐Ÿผ baby bottle +๐Ÿฅ› glass of milk +โ˜• hot beverage +๐Ÿซ– teapot +๐Ÿต teacup without handle +๐Ÿถ sake +๐Ÿพ bottle with popping cork +๐Ÿท wine glass +๐Ÿธ cocktail glass +๐Ÿน tropical drink +๐Ÿบ beer mug +๐Ÿป clinking beer mugs +๐Ÿฅ‚ clinking glasses +๐Ÿฅƒ tumbler glass +๐Ÿซ— pouring liquid +๐Ÿฅค cup with straw +๐Ÿง‹ bubble tea +๐Ÿงƒ beverage box +๐Ÿง‰ mate +๐ŸงŠ ice +๐Ÿฅข chopsticks +๐Ÿฝ๏ธ fork and knife with plate +๐Ÿด fork and knife +๐Ÿฅ„ spoon +๐Ÿ”ช kitchen knife +๐Ÿซ™ jar +๐Ÿบ amphora +@Travel & Places +๐ŸŒ globe showing Europe-Africa +๐ŸŒŽ globe showing Americas +๐ŸŒ globe showing Asia-Australia +๐ŸŒ globe with meridians +๐Ÿ—บ๏ธ world map +๐Ÿ—พ map of Japan +๐Ÿงญ compass +๐Ÿ”๏ธ snow-capped mountain +โ›ฐ๏ธ mountain +๐ŸŒ‹ volcano +๐Ÿ—ป mount fuji +๐Ÿ•๏ธ camping +๐Ÿ–๏ธ beach with umbrella +๐Ÿœ๏ธ desert +๐Ÿ๏ธ desert island +๐Ÿž๏ธ national park +๐ŸŸ๏ธ stadium +๐Ÿ›๏ธ classical building +๐Ÿ—๏ธ building construction +๐Ÿงฑ brick +๐Ÿชจ rock +๐Ÿชต wood +๐Ÿ›– hut +๐Ÿ˜๏ธ houses +๐Ÿš๏ธ derelict house +๐Ÿ  house +๐Ÿก house with garden +๐Ÿข office building +๐Ÿฃ Japanese post office +๐Ÿค post office +๐Ÿฅ hospital +๐Ÿฆ bank +๐Ÿจ hotel +๐Ÿฉ love hotel +๐Ÿช convenience store +๐Ÿซ school +๐Ÿฌ department store +๐Ÿญ factory +๐Ÿฏ Japanese castle +๐Ÿฐ castle +๐Ÿ’’ wedding +๐Ÿ—ผ Tokyo tower +๐Ÿ—ฝ Statue of Liberty +โ›ช church +๐Ÿ•Œ mosque +๐Ÿ›• hindu temple +๐Ÿ• synagogue +โ›ฉ๏ธ shinto shrine +๐Ÿ•‹ kaaba +โ›ฒ fountain +โ›บ tent +๐ŸŒ foggy +๐ŸŒƒ night with stars +๐Ÿ™๏ธ cityscape +๐ŸŒ„ sunrise over mountains +๐ŸŒ… sunrise +๐ŸŒ† cityscape at dusk +๐ŸŒ‡ sunset +๐ŸŒ‰ bridge at night +โ™จ๏ธ hot springs +๐ŸŽ  carousel horse +๐Ÿ› playground slide +๐ŸŽก ferris wheel +๐ŸŽข roller coaster +๐Ÿ’ˆ barber pole +๐ŸŽช circus tent +๐Ÿš‚ locomotive +๐Ÿšƒ railway car +๐Ÿš„ high-speed train +๐Ÿš… bullet train +๐Ÿš† train +๐Ÿš‡ metro +๐Ÿšˆ light rail +๐Ÿš‰ station +๐ŸšŠ tram +๐Ÿš monorail +๐Ÿšž mountain railway +๐Ÿš‹ tram car +๐ŸšŒ bus +๐Ÿš oncoming bus +๐ŸšŽ trolleybus +๐Ÿš minibus +๐Ÿš‘ ambulance +๐Ÿš’ fire engine +๐Ÿš“ police car +๐Ÿš” oncoming police car +๐Ÿš• taxi +๐Ÿš– oncoming taxi +๐Ÿš— automobile +๐Ÿš˜ oncoming automobile +๐Ÿš™ sport utility vehicle +๐Ÿ›ป pickup truck +๐Ÿšš delivery truck +๐Ÿš› articulated lorry +๐Ÿšœ tractor +๐ŸŽ๏ธ racing car +๐Ÿ๏ธ motorcycle +๐Ÿ›ต motor scooter +๐Ÿฆฝ manual wheelchair +๐Ÿฆผ motorized wheelchair +๐Ÿ›บ auto rickshaw +๐Ÿšฒ bicycle +๐Ÿ›ด kick scooter +๐Ÿ›น skateboard +๐Ÿ›ผ roller skate +๐Ÿš bus stop +๐Ÿ›ฃ๏ธ motorway +๐Ÿ›ค๏ธ railway track +๐Ÿ›ข๏ธ oil drum +โ›ฝ fuel pump +๐Ÿ›ž wheel +๐Ÿšจ police car light +๐Ÿšฅ horizontal traffic light +๐Ÿšฆ vertical traffic light +๐Ÿ›‘ stop sign +๐Ÿšง construction +โš“ anchor +๐Ÿ›Ÿ ring buoy +โ›ต sailboat +๐Ÿ›ถ canoe +๐Ÿšค speedboat +๐Ÿ›ณ๏ธ passenger ship +โ›ด๏ธ ferry +๐Ÿ›ฅ๏ธ motor boat +๐Ÿšข ship +โœˆ๏ธ airplane +๐Ÿ›ฉ๏ธ small airplane +๐Ÿ›ซ airplane departure +๐Ÿ›ฌ airplane arrival +๐Ÿช‚ parachute +๐Ÿ’บ seat +๐Ÿš helicopter +๐ŸšŸ suspension railway +๐Ÿš  mountain cableway +๐Ÿšก aerial tramway +๐Ÿ›ฐ๏ธ satellite +๐Ÿš€ rocket +๐Ÿ›ธ flying saucer +๐Ÿ›Ž๏ธ bellhop bell +๐Ÿงณ luggage +โŒ› hourglass done +โณ hourglass not done +โŒš watch +โฐ alarm clock +โฑ๏ธ stopwatch +โฒ๏ธ timer clock +๐Ÿ•ฐ๏ธ mantelpiece clock +๐Ÿ•› twelve oโ€™clock +๐Ÿ•ง twelve-thirty +๐Ÿ• one oโ€™clock +๐Ÿ•œ one-thirty +๐Ÿ•‘ two oโ€™clock +๐Ÿ• two-thirty +๐Ÿ•’ three oโ€™clock +๐Ÿ•ž three-thirty +๐Ÿ•“ four oโ€™clock +๐Ÿ•Ÿ four-thirty +๐Ÿ•” five oโ€™clock +๐Ÿ•  five-thirty +๐Ÿ•• six oโ€™clock +๐Ÿ•ก six-thirty +๐Ÿ•– seven oโ€™clock +๐Ÿ•ข seven-thirty +๐Ÿ•— eight oโ€™clock +๐Ÿ•ฃ eight-thirty +๐Ÿ•˜ nine oโ€™clock +๐Ÿ•ค nine-thirty +๐Ÿ•™ ten oโ€™clock +๐Ÿ•ฅ ten-thirty +๐Ÿ•š eleven oโ€™clock +๐Ÿ•ฆ eleven-thirty +๐ŸŒ‘ new moon +๐ŸŒ’ waxing crescent moon +๐ŸŒ“ first quarter moon +๐ŸŒ” waxing gibbous moon +๐ŸŒ• full moon +๐ŸŒ– waning gibbous moon +๐ŸŒ— last quarter moon +๐ŸŒ˜ waning crescent moon +๐ŸŒ™ crescent moon +๐ŸŒš new moon face +๐ŸŒ› first quarter moon face +๐ŸŒœ last quarter moon face +๐ŸŒก๏ธ thermometer +โ˜€๏ธ sun +๐ŸŒ full moon face +๐ŸŒž sun with face +๐Ÿช ringed planet +โญ star +๐ŸŒŸ glowing star +๐ŸŒ  shooting star +๐ŸŒŒ milky way +โ˜๏ธ cloud +โ›… sun behind cloud +โ›ˆ๏ธ cloud with lightning and rain +๐ŸŒค๏ธ sun behind small cloud +๐ŸŒฅ๏ธ sun behind large cloud +๐ŸŒฆ๏ธ sun behind rain cloud +๐ŸŒง๏ธ cloud with rain +๐ŸŒจ๏ธ cloud with snow +๐ŸŒฉ๏ธ cloud with lightning +๐ŸŒช๏ธ tornado +๐ŸŒซ๏ธ fog +๐ŸŒฌ๏ธ wind face +๐ŸŒ€ cyclone +๐ŸŒˆ rainbow +๐ŸŒ‚ closed umbrella +โ˜‚๏ธ umbrella +โ˜” umbrella with rain drops +โ›ฑ๏ธ umbrella on ground +โšก high voltage +โ„๏ธ snowflake +โ˜ƒ๏ธ snowman +โ›„ snowman without snow +โ˜„๏ธ comet +๐Ÿ”ฅ fire +๐Ÿ’ง droplet +๐ŸŒŠ water wave +@Activities +๐ŸŽƒ jack-o-lantern +๐ŸŽ„ Christmas tree +๐ŸŽ† fireworks +๐ŸŽ‡ sparkler +๐Ÿงจ firecracker +โœจ sparkles +๐ŸŽˆ balloon +๐ŸŽ‰ party popper +๐ŸŽŠ confetti ball +๐ŸŽ‹ tanabata tree +๐ŸŽ pine decoration +๐ŸŽŽ Japanese dolls +๐ŸŽ carp streamer +๐ŸŽ wind chime +๐ŸŽ‘ moon viewing ceremony +๐Ÿงง red envelope +๐ŸŽ€ ribbon +๐ŸŽ wrapped gift +๐ŸŽ—๏ธ reminder ribbon +๐ŸŽŸ๏ธ admission tickets +๐ŸŽซ ticket +๐ŸŽ–๏ธ military medal +๐Ÿ† trophy +๐Ÿ… sports medal +๐Ÿฅ‡ 1st place medal +๐Ÿฅˆ 2nd place medal +๐Ÿฅ‰ 3rd place medal +โšฝ soccer ball +โšพ baseball +๐ŸฅŽ softball +๐Ÿ€ basketball +๐Ÿ volleyball +๐Ÿˆ american football +๐Ÿ‰ rugby football +๐ŸŽพ tennis +๐Ÿฅ flying disc +๐ŸŽณ bowling +๐Ÿ cricket game +๐Ÿ‘ field hockey +๐Ÿ’ ice hockey +๐Ÿฅ lacrosse +๐Ÿ“ ping pong +๐Ÿธ badminton +๐ŸฅŠ boxing glove +๐Ÿฅ‹ martial arts uniform +๐Ÿฅ… goal net +โ›ณ flag in hole +โ›ธ๏ธ ice skate +๐ŸŽฃ fishing pole +๐Ÿคฟ diving mask +๐ŸŽฝ running shirt +๐ŸŽฟ skis +๐Ÿ›ท sled +๐ŸฅŒ curling stone +๐ŸŽฏ bullseye +๐Ÿช€ yo-yo +๐Ÿช kite +๐Ÿ”ซ water pistol +๐ŸŽฑ pool 8 ball +๐Ÿ”ฎ crystal ball +๐Ÿช„ magic wand +๐ŸŽฎ video game +๐Ÿ•น๏ธ joystick +๐ŸŽฐ slot machine +๐ŸŽฒ game die +๐Ÿงฉ puzzle piece +๐Ÿงธ teddy bear +๐Ÿช… piรฑata +๐Ÿชฉ mirror ball +๐Ÿช† nesting dolls +โ™ ๏ธ spade suit +โ™ฅ๏ธ heart suit +โ™ฆ๏ธ diamond suit +โ™ฃ๏ธ club suit +โ™Ÿ๏ธ chess pawn +๐Ÿƒ joker +๐Ÿ€„ mahjong red dragon +๐ŸŽด flower playing cards +๐ŸŽญ performing arts +๐Ÿ–ผ๏ธ framed picture +๐ŸŽจ artist palette +๐Ÿงต thread +๐Ÿชก sewing needle +๐Ÿงถ yarn +๐Ÿชข knot +@Objects +๐Ÿ‘“ glasses +๐Ÿ•ถ๏ธ sunglasses +๐Ÿฅฝ goggles +๐Ÿฅผ lab coat +๐Ÿฆบ safety vest +๐Ÿ‘” necktie +๐Ÿ‘• t-shirt +๐Ÿ‘– jeans +๐Ÿงฃ scarf +๐Ÿงค gloves +๐Ÿงฅ coat +๐Ÿงฆ socks +๐Ÿ‘— dress +๐Ÿ‘˜ kimono +๐Ÿฅป sari +๐Ÿฉฑ one-piece swimsuit +๐Ÿฉฒ briefs +๐Ÿฉณ shorts +๐Ÿ‘™ bikini +๐Ÿ‘š womanโ€™s clothes +๐Ÿชญ folding hand fan +๐Ÿ‘› purse +๐Ÿ‘œ handbag +๐Ÿ‘ clutch bag +๐Ÿ›๏ธ shopping bags +๐ŸŽ’ backpack +๐Ÿฉด thong sandal +๐Ÿ‘ž manโ€™s shoe +๐Ÿ‘Ÿ running shoe +๐Ÿฅพ hiking boot +๐Ÿฅฟ flat shoe +๐Ÿ‘  high-heeled shoe +๐Ÿ‘ก womanโ€™s sandal +๐Ÿฉฐ ballet shoes +๐Ÿ‘ข womanโ€™s boot +๐Ÿชฎ hair pick +๐Ÿ‘‘ crown +๐Ÿ‘’ womanโ€™s hat +๐ŸŽฉ top hat +๐ŸŽ“ graduation cap +๐Ÿงข billed cap +๐Ÿช– military helmet +โ›‘๏ธ rescue workerโ€™s helmet +๐Ÿ“ฟ prayer beads +๐Ÿ’„ lipstick +๐Ÿ’ ring +๐Ÿ’Ž gem stone +๐Ÿ”‡ muted speaker +๐Ÿ”ˆ speaker low volume +๐Ÿ”‰ speaker medium volume +๐Ÿ”Š speaker high volume +๐Ÿ“ข loudspeaker +๐Ÿ“ฃ megaphone +๐Ÿ“ฏ postal horn +๐Ÿ”” bell +๐Ÿ”• bell with slash +๐ŸŽผ musical score +๐ŸŽต musical note +๐ŸŽถ musical notes +๐ŸŽ™๏ธ studio microphone +๐ŸŽš๏ธ level slider +๐ŸŽ›๏ธ control knobs +๐ŸŽค microphone +๐ŸŽง headphone +๐Ÿ“ป radio +๐ŸŽท saxophone +๐Ÿช— accordion +๐ŸŽธ guitar +๐ŸŽน musical keyboard +๐ŸŽบ trumpet +๐ŸŽป violin +๐Ÿช• banjo +๐Ÿฅ drum +๐Ÿช˜ long drum +๐Ÿช‡ maracas +๐Ÿชˆ flute +๐Ÿ“ฑ mobile phone +๐Ÿ“ฒ mobile phone with arrow +โ˜Ž๏ธ telephone +๐Ÿ“ž telephone receiver +๐Ÿ“Ÿ pager +๐Ÿ“  fax machine +๐Ÿ”‹ battery +๐Ÿชซ low battery +๐Ÿ”Œ electric plug +๐Ÿ’ป laptop +๐Ÿ–ฅ๏ธ desktop computer +๐Ÿ–จ๏ธ printer +โŒจ๏ธ keyboard +๐Ÿ–ฑ๏ธ computer mouse +๐Ÿ–ฒ๏ธ trackball +๐Ÿ’ฝ computer disk +๐Ÿ’พ floppy disk +๐Ÿ’ฟ optical disk +๐Ÿ“€ dvd +๐Ÿงฎ abacus +๐ŸŽฅ movie camera +๐ŸŽž๏ธ film frames +๐Ÿ“ฝ๏ธ film projector +๐ŸŽฌ clapper board +๐Ÿ“บ television +๐Ÿ“ท camera +๐Ÿ“ธ camera with flash +๐Ÿ“น video camera +๐Ÿ“ผ videocassette +๐Ÿ” magnifying glass tilted left +๐Ÿ”Ž magnifying glass tilted right +๐Ÿ•ฏ๏ธ candle +๐Ÿ’ก light bulb +๐Ÿ”ฆ flashlight +๐Ÿฎ red paper lantern +๐Ÿช” diya lamp +๐Ÿ“” notebook with decorative cover +๐Ÿ“• closed book +๐Ÿ“– open book +๐Ÿ“— green book +๐Ÿ“˜ blue book +๐Ÿ“™ orange book +๐Ÿ“š books +๐Ÿ““ notebook +๐Ÿ“’ ledger +๐Ÿ“ƒ page with curl +๐Ÿ“œ scroll +๐Ÿ“„ page facing up +๐Ÿ“ฐ newspaper +๐Ÿ—ž๏ธ rolled-up newspaper +๐Ÿ“‘ bookmark tabs +๐Ÿ”– bookmark +๐Ÿท๏ธ label +๐Ÿ’ฐ money bag +๐Ÿช™ coin +๐Ÿ’ด yen banknote +๐Ÿ’ต dollar banknote +๐Ÿ’ถ euro banknote +๐Ÿ’ท pound banknote +๐Ÿ’ธ money with wings +๐Ÿ’ณ credit card +๐Ÿงพ receipt +๐Ÿ’น chart increasing with yen +โœ‰๏ธ envelope +๐Ÿ“ง e-mail +๐Ÿ“จ incoming envelope +๐Ÿ“ฉ envelope with arrow +๐Ÿ“ค outbox tray +๐Ÿ“ฅ inbox tray +๐Ÿ“ฆ package +๐Ÿ“ซ closed mailbox with raised flag +๐Ÿ“ช closed mailbox with lowered flag +๐Ÿ“ฌ open mailbox with raised flag +๐Ÿ“ญ open mailbox with lowered flag +๐Ÿ“ฎ postbox +๐Ÿ—ณ๏ธ ballot box with ballot +โœ๏ธ pencil +โœ’๏ธ black nib +๐Ÿ–‹๏ธ fountain pen +๐Ÿ–Š๏ธ pen +๐Ÿ–Œ๏ธ paintbrush +๐Ÿ–๏ธ crayon +๐Ÿ“ memo +๐Ÿ’ผ briefcase +๐Ÿ“ file folder +๐Ÿ“‚ open file folder +๐Ÿ—‚๏ธ card index dividers +๐Ÿ“… calendar +๐Ÿ“† tear-off calendar +๐Ÿ—’๏ธ spiral notepad +๐Ÿ—“๏ธ spiral calendar +๐Ÿ“‡ card index +๐Ÿ“ˆ chart increasing +๐Ÿ“‰ chart decreasing +๐Ÿ“Š bar chart +๐Ÿ“‹ clipboard +๐Ÿ“Œ pushpin +๐Ÿ“ round pushpin +๐Ÿ“Ž paperclip +๐Ÿ–‡๏ธ linked paperclips +๐Ÿ“ straight ruler +๐Ÿ“ triangular ruler +โœ‚๏ธ scissors +๐Ÿ—ƒ๏ธ card file box +๐Ÿ—„๏ธ file cabinet +๐Ÿ—‘๏ธ wastebasket +๐Ÿ”’ locked +๐Ÿ”“ unlocked +๐Ÿ” locked with pen +๐Ÿ” locked with key +๐Ÿ”‘ key +๐Ÿ—๏ธ old key +๐Ÿ”จ hammer +๐Ÿช“ axe +โ›๏ธ pick +โš’๏ธ hammer and pick +๐Ÿ› ๏ธ hammer and wrench +๐Ÿ—ก๏ธ dagger +โš”๏ธ crossed swords +๐Ÿ’ฃ bomb +๐Ÿชƒ boomerang +๐Ÿน bow and arrow +๐Ÿ›ก๏ธ shield +๐Ÿชš carpentry saw +๐Ÿ”ง wrench +๐Ÿช› screwdriver +๐Ÿ”ฉ nut and bolt +โš™๏ธ gear +๐Ÿ—œ๏ธ clamp +โš–๏ธ balance scale +๐Ÿฆฏ white cane +๐Ÿ”— link +โ›“๏ธ chains +๐Ÿช hook +๐Ÿงฐ toolbox +๐Ÿงฒ magnet +๐Ÿชœ ladder +โš—๏ธ alembic +๐Ÿงช test tube +๐Ÿงซ petri dish +๐Ÿงฌ dna +๐Ÿ”ฌ microscope +๐Ÿ”ญ telescope +๐Ÿ“ก satellite antenna +๐Ÿ’‰ syringe +๐Ÿฉธ drop of blood +๐Ÿ’Š pill +๐Ÿฉน adhesive bandage +๐Ÿฉผ crutch +๐Ÿฉบ stethoscope +๐Ÿฉป x-ray +๐Ÿšช door +๐Ÿ›— elevator +๐Ÿชž mirror +๐ŸชŸ window +๐Ÿ›๏ธ bed +๐Ÿ›‹๏ธ couch and lamp +๐Ÿช‘ chair +๐Ÿšฝ toilet +๐Ÿช  plunger +๐Ÿšฟ shower +๐Ÿ› bathtub +๐Ÿชค mouse trap +๐Ÿช’ razor +๐Ÿงด lotion bottle +๐Ÿงท safety pin +๐Ÿงน broom +๐Ÿงบ basket +๐Ÿงป roll of paper +๐Ÿชฃ bucket +๐Ÿงผ soap +๐Ÿซง bubbles +๐Ÿชฅ toothbrush +๐Ÿงฝ sponge +๐Ÿงฏ fire extinguisher +๐Ÿ›’ shopping cart +๐Ÿšฌ cigarette +โšฐ๏ธ coffin +๐Ÿชฆ headstone +โšฑ๏ธ funeral urn +๐Ÿงฟ nazar amulet +๐Ÿชฌ hamsa +๐Ÿ—ฟ moai +๐Ÿชง placard +๐Ÿชช identification card +@Symbols +๐Ÿง ATM sign +๐Ÿšฎ litter in bin sign +๐Ÿšฐ potable water +โ™ฟ wheelchair symbol +๐Ÿšน menโ€™s room +๐Ÿšบ womenโ€™s room +๐Ÿšป restroom +๐Ÿšผ baby symbol +๐Ÿšพ water closet +๐Ÿ›‚ passport control +๐Ÿ›ƒ customs +๐Ÿ›„ baggage claim +๐Ÿ›… left luggage +โš ๏ธ warning +๐Ÿšธ children crossing +โ›” no entry +๐Ÿšซ prohibited +๐Ÿšณ no bicycles +๐Ÿšญ no smoking +๐Ÿšฏ no littering +๐Ÿšฑ non-potable water +๐Ÿšท no pedestrians +๐Ÿ“ต no mobile phones +๐Ÿ”ž no one under eighteen +โ˜ข๏ธ radioactive +โ˜ฃ๏ธ biohazard +โฌ†๏ธ up arrow +โ†—๏ธ up-right arrow +โžก๏ธ right arrow +โ†˜๏ธ down-right arrow +โฌ‡๏ธ down arrow +โ†™๏ธ down-left arrow +โฌ…๏ธ left arrow +โ†–๏ธ up-left arrow +โ†•๏ธ up-down arrow +โ†”๏ธ left-right arrow +โ†ฉ๏ธ right arrow curving left +โ†ช๏ธ left arrow curving right +โคด๏ธ right arrow curving up +โคต๏ธ right arrow curving down +๐Ÿ”ƒ clockwise vertical arrows +๐Ÿ”„ counterclockwise arrows button +๐Ÿ”™ BACK arrow +๐Ÿ”š END arrow +๐Ÿ”› ON! arrow +๐Ÿ”œ SOON arrow +๐Ÿ” TOP arrow +๐Ÿ› place of worship +โš›๏ธ atom symbol +๐Ÿ•‰๏ธ om +โœก๏ธ star of David +โ˜ธ๏ธ wheel of dharma +โ˜ฏ๏ธ yin yang +โœ๏ธ latin cross +โ˜ฆ๏ธ orthodox cross +โ˜ช๏ธ star and crescent +โ˜ฎ๏ธ peace symbol +๐Ÿ•Ž menorah +๐Ÿ”ฏ dotted six-pointed star +๐Ÿชฏ khanda +โ™ˆ Aries +โ™‰ Taurus +โ™Š Gemini +โ™‹ Cancer +โ™Œ Leo +โ™ Virgo +โ™Ž Libra +โ™ Scorpio +โ™ Sagittarius +โ™‘ Capricorn +โ™’ Aquarius +โ™“ Pisces +โ›Ž Ophiuchus +๐Ÿ”€ shuffle tracks button +๐Ÿ” repeat button +๐Ÿ”‚ repeat single button +โ–ถ๏ธ play button +โฉ fast-forward button +โญ๏ธ next track button +โฏ๏ธ play or pause button +โ—€๏ธ reverse button +โช fast reverse button +โฎ๏ธ last track button +๐Ÿ”ผ upwards button +โซ fast up button +๐Ÿ”ฝ downwards button +โฌ fast down button +โธ๏ธ pause button +โน๏ธ stop button +โบ๏ธ record button +โ๏ธ eject button +๐ŸŽฆ cinema +๐Ÿ”… dim button +๐Ÿ”† bright button +๐Ÿ“ถ antenna bars +๐Ÿ›œ wireless +๐Ÿ“ณ vibration mode +๐Ÿ“ด mobile phone off +โ™€๏ธ female sign +โ™‚๏ธ male sign +โšง๏ธ transgender symbol +โœ–๏ธ multiply +โž• plus +โž– minus +โž— divide +๐ŸŸฐ heavy equals sign +โ™พ๏ธ infinity +โ€ผ๏ธ double exclamation mark +โ‰๏ธ exclamation question mark +โ“ red question mark +โ” white question mark +โ• white exclamation mark +โ— red exclamation mark +ใ€ฐ๏ธ wavy dash +๐Ÿ’ฑ currency exchange +๐Ÿ’ฒ heavy dollar sign +โš•๏ธ medical symbol +โ™ป๏ธ recycling symbol +โšœ๏ธ fleur-de-lis +๐Ÿ”ฑ trident emblem +๐Ÿ“› name badge +๐Ÿ”ฐ Japanese symbol for beginner +โญ• hollow red circle +โœ… check mark button +โ˜‘๏ธ check box with check +โœ”๏ธ check mark +โŒ cross mark +โŽ cross mark button +โžฐ curly loop +โžฟ double curly loop +ใ€ฝ๏ธ part alternation mark +โœณ๏ธ eight-spoked asterisk +โœด๏ธ eight-pointed star +โ‡๏ธ sparkle +ยฉ๏ธ copyright +ยฎ๏ธ registered +โ„ข๏ธ trade mark +#๏ธโƒฃ keycap: # +*๏ธโƒฃ keycap: * +0๏ธโƒฃ keycap: 0 +1๏ธโƒฃ keycap: 1 +2๏ธโƒฃ keycap: 2 +3๏ธโƒฃ keycap: 3 +4๏ธโƒฃ keycap: 4 +5๏ธโƒฃ keycap: 5 +6๏ธโƒฃ keycap: 6 +7๏ธโƒฃ keycap: 7 +8๏ธโƒฃ keycap: 8 +9๏ธโƒฃ keycap: 9 +๐Ÿ”Ÿ keycap: 10 +๐Ÿ”  input latin uppercase +๐Ÿ”ก input latin lowercase +๐Ÿ”ข input numbers +๐Ÿ”ฃ input symbols +๐Ÿ”ค input latin letters +๐Ÿ…ฐ๏ธ A button (blood type) +๐Ÿ†Ž AB button (blood type) +๐Ÿ…ฑ๏ธ B button (blood type) +๐Ÿ†‘ CL button +๐Ÿ†’ COOL button +๐Ÿ†“ FREE button +โ„น๏ธ information +๐Ÿ†” ID button +โ“‚๏ธ circled M +๐Ÿ†• NEW button +๐Ÿ†– NG button +๐Ÿ…พ๏ธ O button (blood type) +๐Ÿ†— OK button +๐Ÿ…ฟ๏ธ P button +๐Ÿ†˜ SOS button +๐Ÿ†™ UP! button +๐Ÿ†š VS button +๐Ÿˆ Japanese โ€œhereโ€ button +๐Ÿˆ‚๏ธ Japanese โ€œservice chargeโ€ button +๐Ÿˆท๏ธ Japanese โ€œmonthly amountโ€ button +๐Ÿˆถ Japanese โ€œnot free of chargeโ€ button +๐Ÿˆฏ Japanese โ€œreservedโ€ button +๐Ÿ‰ Japanese โ€œbargainโ€ button +๐Ÿˆน Japanese โ€œdiscountโ€ button +๐Ÿˆš Japanese โ€œfree of chargeโ€ button +๐Ÿˆฒ Japanese โ€œprohibitedโ€ button +๐Ÿ‰‘ Japanese โ€œacceptableโ€ button +๐Ÿˆธ Japanese โ€œapplicationโ€ button +๐Ÿˆด Japanese โ€œpassing gradeโ€ button +๐Ÿˆณ Japanese โ€œvacancyโ€ button +ใŠ—๏ธ Japanese โ€œcongratulationsโ€ button +ใŠ™๏ธ Japanese โ€œsecretโ€ button +๐Ÿˆบ Japanese โ€œopen for businessโ€ button +๐Ÿˆต Japanese โ€œno vacancyโ€ button +๐Ÿ”ด red circle +๐ŸŸ  orange circle +๐ŸŸก yellow circle +๐ŸŸข green circle +๐Ÿ”ต blue circle +๐ŸŸฃ purple circle +๐ŸŸค brown circle +โšซ black circle +โšช white circle +๐ŸŸฅ red square +๐ŸŸง orange square +๐ŸŸจ yellow square +๐ŸŸฉ green square +๐ŸŸฆ blue square +๐ŸŸช purple square +๐ŸŸซ brown square +โฌ› black large square +โฌœ white large square +โ—ผ๏ธ black medium square +โ—ป๏ธ white medium square +โ—พ black medium-small square +โ—ฝ white medium-small square +โ–ช๏ธ black small square +โ–ซ๏ธ white small square +๐Ÿ”ถ large orange diamond +๐Ÿ”ท large blue diamond +๐Ÿ”ธ small orange diamond +๐Ÿ”น small blue diamond +๐Ÿ”บ red triangle pointed up +๐Ÿ”ป red triangle pointed down +๐Ÿ’  diamond with a dot +๐Ÿ”˜ radio button +๐Ÿ”ณ white square button +๐Ÿ”ฒ black square button +@Flags +๐Ÿ chequered flag +๐Ÿšฉ triangular flag +๐ŸŽŒ crossed flags +๐Ÿด black flag +๐Ÿณ๏ธ white flag +๐Ÿณ๏ธโ€๐ŸŒˆ rainbow flag +๐Ÿณ๏ธโ€โšง๏ธ transgender flag +๐Ÿดโ€โ˜ ๏ธ pirate flag +๐Ÿ‡ฆ๐Ÿ‡จ flag: Ascension Island +๐Ÿ‡ฆ๐Ÿ‡ฉ flag: Andorra +๐Ÿ‡ฆ๐Ÿ‡ช flag: United Arab Emirates +๐Ÿ‡ฆ๐Ÿ‡ซ flag: Afghanistan +๐Ÿ‡ฆ๐Ÿ‡ฌ flag: Antigua & Barbuda +๐Ÿ‡ฆ๐Ÿ‡ฎ flag: Anguilla +๐Ÿ‡ฆ๐Ÿ‡ฑ flag: Albania +๐Ÿ‡ฆ๐Ÿ‡ฒ flag: Armenia +๐Ÿ‡ฆ๐Ÿ‡ด flag: Angola +๐Ÿ‡ฆ๐Ÿ‡ถ flag: Antarctica +๐Ÿ‡ฆ๐Ÿ‡ท flag: Argentina +๐Ÿ‡ฆ๐Ÿ‡ธ flag: American Samoa +๐Ÿ‡ฆ๐Ÿ‡น flag: Austria +๐Ÿ‡ฆ๐Ÿ‡บ flag: Australia +๐Ÿ‡ฆ๐Ÿ‡ผ flag: Aruba +๐Ÿ‡ฆ๐Ÿ‡ฝ flag: ร…land Islands +๐Ÿ‡ฆ๐Ÿ‡ฟ flag: Azerbaijan +๐Ÿ‡ง๐Ÿ‡ฆ flag: Bosnia & Herzegovina +๐Ÿ‡ง๐Ÿ‡ง flag: Barbados +๐Ÿ‡ง๐Ÿ‡ฉ flag: Bangladesh +๐Ÿ‡ง๐Ÿ‡ช flag: Belgium +๐Ÿ‡ง๐Ÿ‡ซ flag: Burkina Faso +๐Ÿ‡ง๐Ÿ‡ฌ flag: Bulgaria +๐Ÿ‡ง๐Ÿ‡ญ flag: Bahrain +๐Ÿ‡ง๐Ÿ‡ฎ flag: Burundi +๐Ÿ‡ง๐Ÿ‡ฏ flag: Benin +๐Ÿ‡ง๐Ÿ‡ฑ flag: St. Barthรฉlemy +๐Ÿ‡ง๐Ÿ‡ฒ flag: Bermuda +๐Ÿ‡ง๐Ÿ‡ณ flag: Brunei +๐Ÿ‡ง๐Ÿ‡ด flag: Bolivia +๐Ÿ‡ง๐Ÿ‡ถ flag: Caribbean Netherlands +๐Ÿ‡ง๐Ÿ‡ท flag: Brazil +๐Ÿ‡ง๐Ÿ‡ธ flag: Bahamas +๐Ÿ‡ง๐Ÿ‡น flag: Bhutan +๐Ÿ‡ง๐Ÿ‡ป flag: Bouvet Island +๐Ÿ‡ง๐Ÿ‡ผ flag: Botswana +๐Ÿ‡ง๐Ÿ‡พ flag: Belarus +๐Ÿ‡ง๐Ÿ‡ฟ flag: Belize +๐Ÿ‡จ๐Ÿ‡ฆ flag: Canada +๐Ÿ‡จ๐Ÿ‡จ flag: Cocos (Keeling) Islands +๐Ÿ‡จ๐Ÿ‡ฉ flag: Congo - Kinshasa +๐Ÿ‡จ๐Ÿ‡ซ flag: Central African Republic +๐Ÿ‡จ๐Ÿ‡ฌ flag: Congo - Brazzaville +๐Ÿ‡จ๐Ÿ‡ญ flag: Switzerland +๐Ÿ‡จ๐Ÿ‡ฎ flag: Cรดte dโ€™Ivoire +๐Ÿ‡จ๐Ÿ‡ฐ flag: Cook Islands +๐Ÿ‡จ๐Ÿ‡ฑ flag: Chile +๐Ÿ‡จ๐Ÿ‡ฒ flag: Cameroon +๐Ÿ‡จ๐Ÿ‡ณ flag: China +๐Ÿ‡จ๐Ÿ‡ด flag: Colombia +๐Ÿ‡จ๐Ÿ‡ต flag: Clipperton Island +๐Ÿ‡จ๐Ÿ‡ท flag: Costa Rica +๐Ÿ‡จ๐Ÿ‡บ flag: Cuba +๐Ÿ‡จ๐Ÿ‡ป flag: Cape Verde +๐Ÿ‡จ๐Ÿ‡ผ flag: Curaรงao +๐Ÿ‡จ๐Ÿ‡ฝ flag: Christmas Island +๐Ÿ‡จ๐Ÿ‡พ flag: Cyprus +๐Ÿ‡จ๐Ÿ‡ฟ flag: Czechia +๐Ÿ‡ฉ๐Ÿ‡ช flag: Germany +๐Ÿ‡ฉ๐Ÿ‡ฌ flag: Diego Garcia +๐Ÿ‡ฉ๐Ÿ‡ฏ flag: Djibouti +๐Ÿ‡ฉ๐Ÿ‡ฐ flag: Denmark +๐Ÿ‡ฉ๐Ÿ‡ฒ flag: Dominica +๐Ÿ‡ฉ๐Ÿ‡ด flag: Dominican Republic +๐Ÿ‡ฉ๐Ÿ‡ฟ flag: Algeria +๐Ÿ‡ช๐Ÿ‡ฆ flag: Ceuta & Melilla +๐Ÿ‡ช๐Ÿ‡จ flag: Ecuador +๐Ÿ‡ช๐Ÿ‡ช flag: Estonia +๐Ÿ‡ช๐Ÿ‡ฌ flag: Egypt +๐Ÿ‡ช๐Ÿ‡ญ flag: Western Sahara +๐Ÿ‡ช๐Ÿ‡ท flag: Eritrea +๐Ÿ‡ช๐Ÿ‡ธ flag: Spain +๐Ÿ‡ช๐Ÿ‡น flag: Ethiopia +๐Ÿ‡ช๐Ÿ‡บ flag: European Union +๐Ÿ‡ซ๐Ÿ‡ฎ flag: Finland +๐Ÿ‡ซ๐Ÿ‡ฏ flag: Fiji +๐Ÿ‡ซ๐Ÿ‡ฐ flag: Falkland Islands +๐Ÿ‡ซ๐Ÿ‡ฒ flag: Micronesia +๐Ÿ‡ซ๐Ÿ‡ด flag: Faroe Islands +๐Ÿ‡ซ๐Ÿ‡ท flag: France +๐Ÿ‡ฌ๐Ÿ‡ฆ flag: Gabon +๐Ÿ‡ฌ๐Ÿ‡ง flag: United Kingdom +๐Ÿ‡ฌ๐Ÿ‡ฉ flag: Grenada +๐Ÿ‡ฌ๐Ÿ‡ช flag: Georgia +๐Ÿ‡ฌ๐Ÿ‡ซ flag: French Guiana +๐Ÿ‡ฌ๐Ÿ‡ฌ flag: Guernsey +๐Ÿ‡ฌ๐Ÿ‡ญ flag: Ghana +๐Ÿ‡ฌ๐Ÿ‡ฎ flag: Gibraltar +๐Ÿ‡ฌ๐Ÿ‡ฑ flag: Greenland +๐Ÿ‡ฌ๐Ÿ‡ฒ flag: Gambia +๐Ÿ‡ฌ๐Ÿ‡ณ flag: Guinea +๐Ÿ‡ฌ๐Ÿ‡ต flag: Guadeloupe +๐Ÿ‡ฌ๐Ÿ‡ถ flag: Equatorial Guinea +๐Ÿ‡ฌ๐Ÿ‡ท flag: Greece +๐Ÿ‡ฌ๐Ÿ‡ธ flag: South Georgia & South Sandwich Islands +๐Ÿ‡ฌ๐Ÿ‡น flag: Guatemala +๐Ÿ‡ฌ๐Ÿ‡บ flag: Guam +๐Ÿ‡ฌ๐Ÿ‡ผ flag: Guinea-Bissau +๐Ÿ‡ฌ๐Ÿ‡พ flag: Guyana +๐Ÿ‡ญ๐Ÿ‡ฐ flag: Hong Kong SAR China +๐Ÿ‡ญ๐Ÿ‡ฒ flag: Heard & McDonald Islands +๐Ÿ‡ญ๐Ÿ‡ณ flag: Honduras +๐Ÿ‡ญ๐Ÿ‡ท flag: Croatia +๐Ÿ‡ญ๐Ÿ‡น flag: Haiti +๐Ÿ‡ญ๐Ÿ‡บ flag: Hungary +๐Ÿ‡ฎ๐Ÿ‡จ flag: Canary Islands +๐Ÿ‡ฎ๐Ÿ‡ฉ flag: Indonesia +๐Ÿ‡ฎ๐Ÿ‡ช flag: Ireland +๐Ÿ‡ฎ๐Ÿ‡ฑ flag: Israel +๐Ÿ‡ฎ๐Ÿ‡ฒ flag: Isle of Man +๐Ÿ‡ฎ๐Ÿ‡ณ flag: India +๐Ÿ‡ฎ๐Ÿ‡ด flag: British Indian Ocean Territory +๐Ÿ‡ฎ๐Ÿ‡ถ flag: Iraq +๐Ÿ‡ฎ๐Ÿ‡ท flag: Iran +๐Ÿ‡ฎ๐Ÿ‡ธ flag: Iceland +๐Ÿ‡ฎ๐Ÿ‡น flag: Italy +๐Ÿ‡ฏ๐Ÿ‡ช flag: Jersey +๐Ÿ‡ฏ๐Ÿ‡ฒ flag: Jamaica +๐Ÿ‡ฏ๐Ÿ‡ด flag: Jordan +๐Ÿ‡ฏ๐Ÿ‡ต flag: Japan +๐Ÿ‡ฐ๐Ÿ‡ช flag: Kenya +๐Ÿ‡ฐ๐Ÿ‡ฌ flag: Kyrgyzstan +๐Ÿ‡ฐ๐Ÿ‡ญ flag: Cambodia +๐Ÿ‡ฐ๐Ÿ‡ฎ flag: Kiribati +๐Ÿ‡ฐ๐Ÿ‡ฒ flag: Comoros +๐Ÿ‡ฐ๐Ÿ‡ณ flag: St. Kitts & Nevis +๐Ÿ‡ฐ๐Ÿ‡ต flag: North Korea +๐Ÿ‡ฐ๐Ÿ‡ท flag: South Korea +๐Ÿ‡ฐ๐Ÿ‡ผ flag: Kuwait +๐Ÿ‡ฐ๐Ÿ‡พ flag: Cayman Islands +๐Ÿ‡ฐ๐Ÿ‡ฟ flag: Kazakhstan +๐Ÿ‡ฑ๐Ÿ‡ฆ flag: Laos +๐Ÿ‡ฑ๐Ÿ‡ง flag: Lebanon +๐Ÿ‡ฑ๐Ÿ‡จ flag: St. Lucia +๐Ÿ‡ฑ๐Ÿ‡ฎ flag: Liechtenstein +๐Ÿ‡ฑ๐Ÿ‡ฐ flag: Sri Lanka +๐Ÿ‡ฑ๐Ÿ‡ท flag: Liberia +๐Ÿ‡ฑ๐Ÿ‡ธ flag: Lesotho +๐Ÿ‡ฑ๐Ÿ‡น flag: Lithuania +๐Ÿ‡ฑ๐Ÿ‡บ flag: Luxembourg +๐Ÿ‡ฑ๐Ÿ‡ป flag: Latvia +๐Ÿ‡ฑ๐Ÿ‡พ flag: Libya +๐Ÿ‡ฒ๐Ÿ‡ฆ flag: Morocco +๐Ÿ‡ฒ๐Ÿ‡จ flag: Monaco +๐Ÿ‡ฒ๐Ÿ‡ฉ flag: Moldova +๐Ÿ‡ฒ๐Ÿ‡ช flag: Montenegro +๐Ÿ‡ฒ๐Ÿ‡ซ flag: St. Martin +๐Ÿ‡ฒ๐Ÿ‡ฌ flag: Madagascar +๐Ÿ‡ฒ๐Ÿ‡ญ flag: Marshall Islands +๐Ÿ‡ฒ๐Ÿ‡ฐ flag: North Macedonia +๐Ÿ‡ฒ๐Ÿ‡ฑ flag: Mali +๐Ÿ‡ฒ๐Ÿ‡ฒ flag: Myanmar (Burma) +๐Ÿ‡ฒ๐Ÿ‡ณ flag: Mongolia +๐Ÿ‡ฒ๐Ÿ‡ด flag: Macao SAR China +๐Ÿ‡ฒ๐Ÿ‡ต flag: Northern Mariana Islands +๐Ÿ‡ฒ๐Ÿ‡ถ flag: Martinique +๐Ÿ‡ฒ๐Ÿ‡ท flag: Mauritania +๐Ÿ‡ฒ๐Ÿ‡ธ flag: Montserrat +๐Ÿ‡ฒ๐Ÿ‡น flag: Malta +๐Ÿ‡ฒ๐Ÿ‡บ flag: Mauritius +๐Ÿ‡ฒ๐Ÿ‡ป flag: Maldives +๐Ÿ‡ฒ๐Ÿ‡ผ flag: Malawi +๐Ÿ‡ฒ๐Ÿ‡ฝ flag: Mexico +๐Ÿ‡ฒ๐Ÿ‡พ flag: Malaysia +๐Ÿ‡ฒ๐Ÿ‡ฟ flag: Mozambique +๐Ÿ‡ณ๐Ÿ‡ฆ flag: Namibia +๐Ÿ‡ณ๐Ÿ‡จ flag: New Caledonia +๐Ÿ‡ณ๐Ÿ‡ช flag: Niger +๐Ÿ‡ณ๐Ÿ‡ซ flag: Norfolk Island +๐Ÿ‡ณ๐Ÿ‡ฌ flag: Nigeria +๐Ÿ‡ณ๐Ÿ‡ฎ flag: Nicaragua +๐Ÿ‡ณ๐Ÿ‡ฑ flag: Netherlands +๐Ÿ‡ณ๐Ÿ‡ด flag: Norway +๐Ÿ‡ณ๐Ÿ‡ต flag: Nepal +๐Ÿ‡ณ๐Ÿ‡ท flag: Nauru +๐Ÿ‡ณ๐Ÿ‡บ flag: Niue +๐Ÿ‡ณ๐Ÿ‡ฟ flag: New Zealand +๐Ÿ‡ด๐Ÿ‡ฒ flag: Oman +๐Ÿ‡ต๐Ÿ‡ฆ flag: Panama +๐Ÿ‡ต๐Ÿ‡ช flag: Peru +๐Ÿ‡ต๐Ÿ‡ซ flag: French Polynesia +๐Ÿ‡ต๐Ÿ‡ฌ flag: Papua New Guinea +๐Ÿ‡ต๐Ÿ‡ญ flag: Philippines +๐Ÿ‡ต๐Ÿ‡ฐ flag: Pakistan +๐Ÿ‡ต๐Ÿ‡ฑ flag: Poland +๐Ÿ‡ต๐Ÿ‡ฒ flag: St. Pierre & Miquelon +๐Ÿ‡ต๐Ÿ‡ณ flag: Pitcairn Islands +๐Ÿ‡ต๐Ÿ‡ท flag: Puerto Rico +๐Ÿ‡ต๐Ÿ‡ธ flag: Palestinian Territories +๐Ÿ‡ต๐Ÿ‡น flag: Portugal +๐Ÿ‡ต๐Ÿ‡ผ flag: Palau +๐Ÿ‡ต๐Ÿ‡พ flag: Paraguay +๐Ÿ‡ถ๐Ÿ‡ฆ flag: Qatar +๐Ÿ‡ท๐Ÿ‡ช flag: Rรฉunion +๐Ÿ‡ท๐Ÿ‡ด flag: Romania +๐Ÿ‡ท๐Ÿ‡ธ flag: Serbia +๐Ÿ‡ท๐Ÿ‡บ flag: Russia +๐Ÿ‡ท๐Ÿ‡ผ flag: Rwanda +๐Ÿ‡ธ๐Ÿ‡ฆ flag: Saudi Arabia +๐Ÿ‡ธ๐Ÿ‡ง flag: Solomon Islands +๐Ÿ‡ธ๐Ÿ‡จ flag: Seychelles +๐Ÿ‡ธ๐Ÿ‡ฉ flag: Sudan +๐Ÿ‡ธ๐Ÿ‡ช flag: Sweden +๐Ÿ‡ธ๐Ÿ‡ฌ flag: Singapore +๐Ÿ‡ธ๐Ÿ‡ญ flag: St. Helena +๐Ÿ‡ธ๐Ÿ‡ฎ flag: Slovenia +๐Ÿ‡ธ๐Ÿ‡ฏ flag: Svalbard & Jan Mayen +๐Ÿ‡ธ๐Ÿ‡ฐ flag: Slovakia +๐Ÿ‡ธ๐Ÿ‡ฑ flag: Sierra Leone +๐Ÿ‡ธ๐Ÿ‡ฒ flag: San Marino +๐Ÿ‡ธ๐Ÿ‡ณ flag: Senegal +๐Ÿ‡ธ๐Ÿ‡ด flag: Somalia +๐Ÿ‡ธ๐Ÿ‡ท flag: Suriname +๐Ÿ‡ธ๐Ÿ‡ธ flag: South Sudan +๐Ÿ‡ธ๐Ÿ‡น flag: Sรฃo Tomรฉ & Prรญncipe +๐Ÿ‡ธ๐Ÿ‡ป flag: El Salvador +๐Ÿ‡ธ๐Ÿ‡ฝ flag: Sint Maarten +๐Ÿ‡ธ๐Ÿ‡พ flag: Syria +๐Ÿ‡ธ๐Ÿ‡ฟ flag: Eswatini +๐Ÿ‡น๐Ÿ‡ฆ flag: Tristan da Cunha +๐Ÿ‡น๐Ÿ‡จ flag: Turks & Caicos Islands +๐Ÿ‡น๐Ÿ‡ฉ flag: Chad +๐Ÿ‡น๐Ÿ‡ซ flag: French Southern Territories +๐Ÿ‡น๐Ÿ‡ฌ flag: Togo +๐Ÿ‡น๐Ÿ‡ญ flag: Thailand +๐Ÿ‡น๐Ÿ‡ฏ flag: Tajikistan +๐Ÿ‡น๐Ÿ‡ฐ flag: Tokelau +๐Ÿ‡น๐Ÿ‡ฑ flag: Timor-Leste +๐Ÿ‡น๐Ÿ‡ฒ flag: Turkmenistan +๐Ÿ‡น๐Ÿ‡ณ flag: Tunisia +๐Ÿ‡น๐Ÿ‡ด flag: Tonga +๐Ÿ‡น๐Ÿ‡ท flag: Turkey +๐Ÿ‡น๐Ÿ‡น flag: Trinidad & Tobago +๐Ÿ‡น๐Ÿ‡ป flag: Tuvalu +๐Ÿ‡น๐Ÿ‡ผ flag: Taiwan +๐Ÿ‡น๐Ÿ‡ฟ flag: Tanzania +๐Ÿ‡บ๐Ÿ‡ฆ flag: Ukraine +๐Ÿ‡บ๐Ÿ‡ฌ flag: Uganda +๐Ÿ‡บ๐Ÿ‡ฒ flag: U.S. Outlying Islands +๐Ÿ‡บ๐Ÿ‡ณ flag: United Nations +๐Ÿ‡บ๐Ÿ‡ธ flag: United States +๐Ÿ‡บ๐Ÿ‡พ flag: Uruguay +๐Ÿ‡บ๐Ÿ‡ฟ flag: Uzbekistan +๐Ÿ‡ป๐Ÿ‡ฆ flag: Vatican City +๐Ÿ‡ป๐Ÿ‡จ flag: St. Vincent & Grenadines +๐Ÿ‡ป๐Ÿ‡ช flag: Venezuela +๐Ÿ‡ป๐Ÿ‡ฌ flag: British Virgin Islands +๐Ÿ‡ป๐Ÿ‡ฎ flag: U.S. Virgin Islands +๐Ÿ‡ป๐Ÿ‡ณ flag: Vietnam +๐Ÿ‡ป๐Ÿ‡บ flag: Vanuatu +๐Ÿ‡ผ๐Ÿ‡ซ flag: Wallis & Futuna +๐Ÿ‡ผ๐Ÿ‡ธ flag: Samoa +๐Ÿ‡ฝ๐Ÿ‡ฐ flag: Kosovo +๐Ÿ‡พ๐Ÿ‡ช flag: Yemen +๐Ÿ‡พ๐Ÿ‡น flag: Mayotte +๐Ÿ‡ฟ๐Ÿ‡ฆ flag: South Africa +๐Ÿ‡ฟ๐Ÿ‡ฒ flag: Zambia +๐Ÿ‡ฟ๐Ÿ‡ผ flag: Zimbabwe +๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ flag: England +๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ flag: Scotland +๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ flag: Wales diff --git a/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/EmojiPickerDataTest.kt b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/EmojiPickerDataTest.kt new file mode 100644 index 0000000000..c5860ed9de --- /dev/null +++ b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/EmojiPickerDataTest.kt @@ -0,0 +1,55 @@ +package chat.simplex.app + +import chat.simplex.common.views.chat.* +import kotlin.test.* + +// use this command for testing: +// ./gradlew desktopTest +class EmojiPickerDataTest { + private val skinToneModifiers = 0x1F3FB..0x1F3FF + + @Test + fun testBundledDataHasAllCategoriesInOrder() { + assertEquals(EmojiCategory.entries.toList(), emojiSections.map { it.category }) + emojiSections.forEach { assertTrue(it.emojis.isNotEmpty(), "empty category ${it.category}") } + assertEquals(1870, emojiSections.sumOf { it.emojis.size }) + } + + @Test + fun testBundledDataEntries() { + val all = emojiSections.flatMap { it.emojis } + assertEquals(all.size, all.map { it.emoji }.toSet().size, "duplicate emoji") + all.forEach { + assertTrue(it.emoji.isNotEmpty() && it.name.isNotBlank(), "bad entry $it") + assertFalse(it.emoji.codePoints().anyMatch { cp -> cp in skinToneModifiers }, "skin tone variant ${it.name}") + } + assertEquals(EmojiData("๐Ÿ˜€", "grinning face"), emojiSections.first().emojis.first()) + assertEquals(EmojiData("๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ", "flag: Wales"), emojiSections.last().emojis.last()) + } + + @Test + fun testParseEmojiData() { + val sections = parseEmojiData(sequenceOf("@Smileys & Emotion", "๐Ÿ˜€\tgrinning face", "", "@Flags", "๐Ÿ\tchequered flag", "#๏ธโƒฃ\tkeycap: #")) + assertEquals( + listOf( + EmojiSection(EmojiCategory.Smileys, listOf(EmojiData("๐Ÿ˜€", "grinning face"))), + EmojiSection(EmojiCategory.Flags, listOf(EmojiData("๐Ÿ", "chequered flag"), EmojiData("#๏ธโƒฃ", "keycap: #"))), + ), + sections + ) + } + + @Test + fun testParseUnknownGroupFails() { + assertFailsWith { parseEmojiData(sequenceOf("@Component", "๐Ÿป\tlight skin tone")) } + } + + @Test + fun testSearchEmoji() { + assertEquals(listOf("โค๏ธ"), searchEmoji(emojiSections, "red heart").map { it.emoji }) + assertEquals(searchEmoji(emojiSections, "red heart"), searchEmoji(emojiSections, " RED Heart ")) + val flags = searchEmoji(emojiSections, "flag: ") + assertTrue(flags.size > 250, "found ${flags.size} flags") + assertTrue(searchEmoji(emojiSections, "no such emoji name").isEmpty()) + } +} diff --git a/apps/multiplatform/product/concepts.md b/apps/multiplatform/product/concepts.md index d29ac5c2e5..05a637b4bd 100644 --- a/apps/multiplatform/product/concepts.md +++ b/apps/multiplatform/product/concepts.md @@ -22,7 +22,7 @@ This document provides a structured mapping between product-level concepts, thei | PC1 | Chat List | [README.md](README.md) (Navigation Map) | [spec/client/chat-list.md](../spec/client/chat-list.md) | `common/.../views/chatlist/ChatListView.kt`, `ChatListNavLinkView.kt`, `ChatPreviewView.kt`, `GetStakeBanner.kt` | `Controller.hs` (`APIGetChats`) | | PC2 | Direct Chat | [README.md](README.md) (Messaging) | [spec/client/chat-view.md](../spec/client/chat-view.md) | `common/.../views/chat/ChatView.kt`, `ChatInfoView.kt` | `Types.hs` (`Contact`), `Messages.hs` | | PC3 | Group Chat | [README.md](README.md) (Groups) | [spec/client/chat-view.md](../spec/client/chat-view.md) | `common/.../views/chat/ChatView.kt`, `group/GroupChatInfoView.kt` | `Types.hs` (`GroupInfo`, `GroupMember`) | -| PC4 | Message Composition | [README.md](README.md) (Messaging) | [spec/client/compose.md](../spec/client/compose.md) | `common/.../views/chat/ComposeView.kt`, `SendMsgView.kt`, `ComposeVoiceView.kt`, `ComposeImageView.kt`, `ComposeFileView.kt` | `Controller.hs` (`APISendMessages`) | +| PC4 | Message Composition | [README.md](README.md) (Messaging) | [spec/client/compose.md](../spec/client/compose.md) | `common/.../views/chat/ComposeView.kt`, `SendMsgView.kt`, `ComposeVoiceView.kt`, `ComposeImageView.kt`, `ComposeFileView.kt`, `ComposeEmojiPicker.kt` | `Controller.hs` (`APISendMessages`) | | PC5 | Message Reactions | [README.md](README.md) (Messaging) | [spec/api.md](../spec/api.md) | `common/.../views/chat/ChatItemView.kt` (ChatItemReactions composable) | `Controller.hs` (`APIChatItemReaction`) | | PC6 | Message Editing | [README.md](README.md) (Messaging) | [spec/client/compose.md](../spec/client/compose.md) | `common/.../views/chat/ComposeView.kt`, `ChatItemInfoView.kt` | `Controller.hs` (`APIUpdateChatItem`) | | PC7 | Message Deletion | [README.md](README.md) (Messaging) | [spec/api.md](../spec/api.md) | `common/.../views/chat/item/MarkedDeletedItemView.kt`, `DeletedItemView.kt` | `Controller.hs` (`APIDeleteChatItem`) | diff --git a/apps/multiplatform/product/gaps.md b/apps/multiplatform/product/gaps.md index aae24bdca8..1cfff3b220 100644 --- a/apps/multiplatform/product/gaps.md +++ b/apps/multiplatform/product/gaps.md @@ -13,6 +13,7 @@ This document catalogs known gaps in the multiplatform codebase (Android and Des 5. [Documentation: Haskell Store Layer Not Fully Specified](#gap-05-documentation-haskell-store-layer-not-fully-specified) 6. [Desktop: Recording Not Implemented](#gap-06-desktop-recording-not-implemented) 7. [Desktop: Cryptor Not Implemented](#gap-07-desktop-cryptor-not-implemented) +8. [Desktop: Emoji Picker Without Skin Tones and Localized Search](#gap-08-desktop-emoji-picker-without-skin-tones-and-localized-search) --- @@ -289,3 +290,29 @@ This directly undermines RULE-02 (Database Encryption at Rest) and RULE-04 (Self - GAP-03 (Database Passphrase Not Enforced) is compounded by this gap on Desktop. - The `testCrypto()` function referenced in `AppCommon.desktop.kt:39` is commented out with a `// LALAL` marker, suggesting crypto testing was planned but never completed. + +--- + +## GAP-08: Desktop: Emoji Picker Without Skin Tones and Localized Search + +**Severity:** Low +**Category:** Feature / Platform +**Platform:** Desktop only + +### Description + +The desktop compose emoji picker lists emoji without skin tone variants, and its search matches English Unicode emoji names only. + +### Affected Locations + +- `common/src/desktopMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.desktop.kt` +- `scripts/desktop/generate-emoji-data.py` (`../../scripts/desktop/` relative to `apps/multiplatform/`) -- drops skin tone variants + +### Impact + +Desktop users cannot pick a skin tone in the picker, and search in other languages finds nothing. + +### Recommendation + +1. Keep the skin tone variants in the generated data, grouped by base emoji, and add a skin tone choice to the picker. +2. Add localized names (CLDR annotations) to the generated data for search. diff --git a/apps/multiplatform/product/views/chat.md b/apps/multiplatform/product/views/chat.md index 7862574f04..c920e40146 100644 --- a/apps/multiplatform/product/views/chat.md +++ b/apps/multiplatform/product/views/chat.md @@ -95,6 +95,7 @@ Bottom input area for composing messages: |---|---| | Text field | `PlatformTextField` with markdown support, `@mention` autocomplete, file paste support | | Attachment button | Opens `ModalBottomSheetLayout` with options: camera, gallery (image/video), file | +| Emoji button | Desktop only (Android keyboards have their own emoji panel). Opens a popup with search and categories; a picked emoji is inserted at the cursor and the picker stays open. With text selected the emoji goes after it, as the text field drops its selection when the picker takes focus. Picked emoji are not remembered, for privacy. Esc, a click outside or the button closes it | | Send button | Sends message; changes to checkmark for reports; animated size/alpha | | Voice record button | Shown when text is empty and voice allowed; hold to record, release to preview | | Live message button | Start/update live typing message (if `liveMessageAlertShown`) | @@ -132,4 +133,5 @@ When `timedMessageAllowed` is true, compose bar includes a timer icon for settin | `ChatView.kt` | `views/chat/ChatView.kt` | | `ComposeView.kt` | `views/chat/ComposeView.kt` | | `SendMsgView.kt` | `views/chat/SendMsgView.kt` | +| `ComposeEmojiPicker.kt` | `views/chat/ComposeEmojiPicker.kt` (+ `.android.kt`, `.desktop.kt`) | | Chat item views | `views/chat/item/*.kt` | diff --git a/apps/multiplatform/spec/client/compose.md b/apps/multiplatform/spec/client/compose.md index 241dcf667b..e7f879f5b0 100644 --- a/apps/multiplatform/spec/client/compose.md +++ b/apps/multiplatform/spec/client/compose.md @@ -14,6 +14,7 @@ Source: `common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView 6. [Attachment Handling](#6-attachment-handling) 7. [Draft Persistence](#7-draft-persistence) 8. [Source Files](#8-source-files) +9. [Emoji Picker](#9-emoji-picker) --- @@ -41,9 +42,10 @@ ComposeView | |-- ComposeImageView [media thumbnails] | |-- ComposeVoiceView [voice recording waveform] | +-- ComposeFileView [file name display] -|-- AttachmentAndCommandsButtons +|-- ComposeActionButtons | |-- CommandsButton [bot commands "//"] -| +-- AttachmentButton [paperclip icon] +| |-- AttachmentButton [paperclip icon] +| +-- ComposeEmojiButton [desktop emoji picker popup] +-- SendMsgView |-- PlatformTextField [multiline text input] |-- DeleteTextButton [clear text, shown on long text] @@ -348,7 +350,7 @@ When `privacyLinkPreviews` is enabled and the message contains a URL: ## 7. Draft Persistence -**Location:** [`ComposeView.kt#L1230`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt#L1230) (`KeyChangeEffect(chatModel.chatId.value)`) +**Location:** [`ComposeView.kt#L1394`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt#L1394) (`KeyChangeEffect(chatModel.chatId.value)`) Controlled by the `privacySaveLastDraft` preference. @@ -397,3 +399,27 @@ When a draft exists for a chat, `ChatPreviewView` shows a pencil icon with the d | `ComposeContextPendingMemberActionsView.kt` | Pending member compose actions | | `ComposeContextProfilePickerView.kt` | Profile picker in compose context | | `SelectableChatItemToolbars.kt` | Multi-select mode toolbar (delete, forward, moderate) | +| `ComposeEmojiPicker.kt` (+ `.android.kt`, `.desktop.kt`) | Desktop emoji button and picker, emoji insertion | + +--- + + + +## 9. Emoji Picker + +**Source:** [`ComposeEmojiPicker.kt`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.kt), [`ComposeEmojiPicker.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.desktop.kt) + +Desktop only; on Android the [`ComposeEmojiButton()`](../../common/src/androidMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.android.kt) actual is empty, as keyboards have their own emoji panel. + +An emoji button in [`ComposeActionButtons()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt#L1346-L1363) toggles `emojiPickerShown`, owned by `ComposeView`. A picked emoji goes through [`onEmoji()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt#L1290-L1292): [`insertingEmoji()`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.kt#L10-L14) replaces the selection (clamped to the text) and moves the cursor after the emoji, then `onMessageChange` applies markdown parsing, link sanitizing and the large-emoji font. Emoji are ignored while a message is in progress. The picker stays open after picking. The text field collapses its selection to the end when the popup takes focus, so the emoji goes after selected text. + +[`emojiEnabled`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt#L1282-L1288) is false when sending is not allowed or a voice message is previewed; the picker closes then. When the picker closes, the text field gets focus through `focusRequester`. + +| Function | Behavior | +|---|---| +| [`ComposeEmojiButton()`](../../common/src/desktopMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.desktop.kt#L89-L115) | Button 4 dp after the attachment button (`EMOJI_BUTTON_START_PADDING`, against the row's -8 dp overlap). Focusable `Popup` above the button; closes on Esc (on key release, so the window does not also close its top modal), outside click or the button; its height is capped to the space above the button | +| [`EmojiPicker()`](../../common/src/desktopMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.desktop.kt#L127-L178) | Search field, category tabs, `LazyVerticalGrid` with one section per `EmojiCategory`; tabs and cells are not focusable, so typing after a click goes to search. Picked emoji are not stored, so nothing about what the user typed is kept in preferences | + +Data: `desktopMain/resources/emoji/emoji.txt` is generated by `scripts/desktop/generate-emoji-data.py` from Unicode `emoji-test.txt` 15.0 (the version covered by the bundled NotoColorEmoji): fully-qualified emoji without skin tone variants, `@` lines followed by `\t` lines. [`parseEmojiData()`](../../common/src/desktopMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.desktop.kt#L59-L76) maps groups to `EmojiCategory` and fails on an unknown group; [`emojiSections`](../../common/src/desktopMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.desktop.kt#L78-L82) loads it lazily. [`searchEmoji()`](../../common/src/desktopMain/kotlin/chat/simplex/common/views/chat/ComposeEmojiPicker.desktop.kt#L84-L87) matches the English name case-insensitively. + +[GAP] No skin tone selection, and search uses English emoji names only. diff --git a/apps/multiplatform/spec/impact.md b/apps/multiplatform/spec/impact.md index 7e0fb181d8..20053696d9 100644 --- a/apps/multiplatform/spec/impact.md +++ b/apps/multiplatform/spec/impact.md @@ -111,6 +111,7 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/` | `views/chat/ChatView.kt` | PC2, PC3, PC4, PC5, PC6, PC7, PC8, PC9, PC11 | High | Core conversation UI โ€” most messaging features | | `views/chat/ComposeView.kt` | PC4, PC6, PC9, PC10, PC11 | High | Message composition โ€” send path for all messages | | `views/chat/SendMsgView.kt` | PC4, PC9 | Medium | Send button and voice record toggle | +| `views/chat/ComposeEmojiPicker.kt` | PC4 | Low | Emoji button expect declaration, emoji insertion | | `views/chat/ComposeVoiceView.kt` | PC9 | Medium | Voice message recording UI | | `views/chat/ComposeFileView.kt` | PC10 | Low | File attachment preview in compose area | | `views/chat/ComposeImageView.kt` | PC10 | Low | Image attachment preview in compose area | @@ -365,6 +366,7 @@ Path prefix: `common/src/androidMain/kotlin/chat/simplex/common/` | `views/call/CallAudioDeviceManager.kt` | PC17 | Medium | Android audio routing (speaker, earpiece, bluetooth) | | `views/chat/ComposeView.android.kt` | PC4, PC10 | Low | Android compose view extensions | | `views/chat/SendMsgView.android.kt` | PC4 | Low | Android send button extensions | +| `views/chat/ComposeEmojiPicker.android.kt` | PC4 | Low | Empty emoji button actual (no picker on Android) | | `views/chat/item/ChatItemView.android.kt` | PC2, PC3 | Low | Android chat item extensions | | `views/chat/item/CIImageView.android.kt` | PC10 | Low | Android image rendering extensions | | `views/chat/item/CIVideoView.android.kt` | PC10 | Low | Android video rendering extensions | @@ -439,6 +441,7 @@ Path prefix: `common/src/desktopMain/kotlin/chat/simplex/common/` | `views/call/CallView.desktop.kt` | PC17 | Medium | Desktop WebView-based WebRTC call | | `views/chat/ComposeView.desktop.kt` | PC4, PC10 | Low | Desktop compose view (drag-and-drop, paste) | | `views/chat/SendMsgView.desktop.kt` | PC4 | Low | Desktop send shortcut (Enter key handling) | +| `views/chat/ComposeEmojiPicker.desktop.kt` | PC4 | Low | Desktop emoji picker popup (data from `resources/emoji/emoji.txt`, search) | | `views/chat/item/ChatItemView.desktop.kt` | PC2, PC3 | Low | Desktop chat item extensions | | `views/chat/item/CIImageView.desktop.kt` | PC10 | Low | Desktop image rendering | | `views/chat/item/CIVideoView.desktop.kt` | PC10 | Low | Desktop video rendering | diff --git a/plans/2026-10-13-compose-emoji-picker.md b/plans/2026-10-13-compose-emoji-picker.md new file mode 100644 index 0000000000..ded2a0ce47 --- /dev/null +++ b/plans/2026-10-13-compose-emoji-picker.md @@ -0,0 +1,51 @@ +# Desktop: emoji picker in the compose field + +## Problem + +The desktop compose field has no way to choose an emoji: Linux has no system emoji picker, and on Windows and macOS the system pickers are shortcuts most users do not know. Android keyboards have their own emoji panel, so Android is not changed. + +## Solution + +On desktop, an emoji button next to the attachment button opens a popup emoji picker: search field, category tabs, grid of emoji. A picked emoji is inserted at the cursor and the cursor moves after it; the picker stays open, so several emoji can be picked in a row. Esc, a click outside or the button closes it, and focus returns to the text field. The text field collapses its selection to the end when the popup takes focus, so with text selected the emoji goes after it. + +No protocol or core changes: picked emoji are ordinary message text. + +## Technical design + +### Common (`views/chat/ComposeEmojiPicker.kt`) + +- `expect fun ComposeEmojiButton(pickerShown, enabled, onEmoji)`. It sits in `ComposeActionButtons` (renamed from `AttachmentAndCommandsButtons`) after the attachment button. The Android actual is empty. +- `fun ComposeMessage.insertingEmoji(emoji: String): ComposeMessage` replaces `selection` (clamped to the text) with the emoji and puts the cursor after it. `ComposeView.onEmoji` passes the result to `onMessageChange`, so markdown parsing, link sanitizing and the large-emoji font apply as for typed text. Ignored while a message is being sent. + +`ComposeView` owns `emojiPickerShown` (`rememberSaveable`). It is closed when sending becomes disabled or a voice message is recorded (`emojiEnabled`). When it closes, focus returns to the text field through the existing `focusRequester`. + +### Desktop (`ComposeEmojiPicker.desktop.kt`) + +- Button: same look as the attachment button. The row keeps its `spacedBy(-8.dp)` overlap; the emoji button adds `EMOJI_BUTTON_START_PADDING` (4 dp), so its centre is 44 dp from the attachment button. +- Data: `desktopMain/resources/emoji/emoji.txt`, generated by `scripts/desktop/generate-emoji-data.py` from Unicode `emoji-test.txt` 15.0, the version covered by the bundled NotoColorEmoji 2.038 (all 1870 sequences checked against the font cmap). Fully-qualified emoji only, without the Component group and without skin tone variants. Format: `@` lines, then `\t` lines. Parsed once, lazily (`emojiSections`). +- `EmojiCategory` maps Unicode group names to a tab emoji and a localized title. +- Popup: focusable `Popup` above the button (`AboveAnchorPositionProvider`), 360ร—420 dp, or shorter when the window has less space above the button. Search field (auto-focused), category tabs, then a `LazyVerticalGrid` with section headers; tabs and cells are not focusable, so typing after a click still goes to search. Esc (handled on key release, as the window does, so it does not also close the top modal), a click outside, or the button closes it. +- Search: case-insensitive substring of the English Unicode name (`searchEmoji`). +- No recently used list: storing picked emoji in preferences would keep a trace of what the user wrote outside the encrypted database. +- Rendering uses `EmojiFont` (Noto on Windows/Linux, system font on macOS), as emoji-only messages do. + +## Affected docs + +- `product/views/chat.md` โ€” Compose Bar table: emoji button. +- `spec/client/compose.md` โ€” emoji picker functions. +- `spec/impact.md` โ€” new source files. +- `product/concepts.md` โ€” PC4 source list. + +## Not included + +- Android emoji picker: the keyboard's emoji panel covers it. An in-app panel was built and tested in simulation, then dropped; it would also need its own recents disabled for privacy. +- Skin tone selection. +- Emoji shortcode autocomplete (`:smile:`). +- Localized emoji names for search. +- Custom (image) emoji. + +## Verification + +- Unit tests: `insertingEmoji` (empty, cursor, selection, surrogate pairs); desktop data loads, every group maps to a category, no skin tone modifiers, search. +- The real desktop app run under Xvfb with screenshots and a screen recording: button in a group compose bar and its spacing, insert at cursor, several picks, search, no results, category tabs, close by Esc / outside click / button with focus returned, emoji-only message in large font, light and dark themes, editing, live messages, chat switching, small window, large font, right-to-left layout. +- Android: `compileDebugKotlinAndroid` passes; the compose bar is unchanged from before. diff --git a/scripts/desktop/generate-emoji-data.py b/scripts/desktop/generate-emoji-data.py new file mode 100644 index 0000000000..47f18adeb0 --- /dev/null +++ b/scripts/desktop/generate-emoji-data.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# Generates the desktop emoji picker data from Unicode emoji-test.txt. +# Emoji version must not exceed the bundled NotoColorEmoji font (2.038 covers Emoji 15.0): +# curl -O https://unicode.org/Public/emoji/15.0/emoji-test.txt +# python3 scripts/desktop/generate-emoji-data.py emoji-test.txt apps/multiplatform/common/src/desktopMain/resources/emoji/emoji.txt +# Output: "@" lines followed by "\t" lines, in CLDR order. + +import sys + +SKIPPED_GROUPS = {"Component"} +SKIN_TONE_MODIFIERS = {0x1F3FB, 0x1F3FC, 0x1F3FD, 0x1F3FE, 0x1F3FF} + + +def parse(lines): + group = None + for line in lines: + line = line.rstrip("\n") + if line.startswith("# group: "): + group = line[len("# group: "):] + if group not in SKIPPED_GROUPS: + yield "@" + group + elif line and not line.startswith("#") and group not in SKIPPED_GROUPS: + codes, rest = line.split(";", 1) + status, comment = rest.split("#", 1) + if status.strip() != "fully-qualified": + continue + code_points = [int(c, 16) for c in codes.split()] + if SKIN_TONE_MODIFIERS.intersection(code_points): + continue + # comment is " E " + name = comment.strip().split(" ", 2)[2] + yield "".join(map(chr, code_points)) + "\t" + name + + +def main(): + src, dst = sys.argv[1], sys.argv[2] + with open(src, encoding="utf-8") as f: + out = list(parse(f)) + with open(dst, "w", encoding="utf-8", newline="\n") as f: + f.write("\n".join(out) + "\n") + + +if __name__ == "__main__": + main()