From 88f7a97268c58049ecc209f2ec608e7949387a84 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Wed, 28 Oct 2026 13:58:29 +0000 Subject: [PATCH 1/2] Open the phone's own emoji picker for more reactions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reactions sheet keeps the web's three quick reactions and swaps its fixed grid of sixteen for a More reactions button, as the web has. It opens from NativePHP/mobile-ui: the emoji keyboard on iOS, Jetpack's EmojiPickerView in a sheet on Android. The sheet makes way for it. A picked emoji goes through toggleReaction like a quick one. devnull only takes the emoji in the web's shortcode set, fully qualified and without skin tones, so ReactionEmoji maps what the picker hands over onto that list (a copy of the web's emoji-shortcodes.json): a skin tone comes off and the variation selector follows devnull's. One devnull doesn't know at all, such as ๐Ÿฆโ€๐Ÿ”ฅ, isn't sent: a toast says so and the picker stays up for another. openEmojiPicker() is public so the coming long-press menu can open the picker for any message. From the pinned sheet, picking or closing goes back to the pinned messages. Co-Authored-By: Claude Opus 5.5 --- app/Devnull/ReactionEmoji.php | 60 + app/NativeComponents/Conversation.php | 75 +- resources/emoji-shortcodes.json | 1921 +++++++++++++++++ resources/views/native/conversation.blade.php | 29 +- tests/Feature/Screens/ConversationTest.php | 97 + 5 files changed, 2157 insertions(+), 25 deletions(-) create mode 100644 app/Devnull/ReactionEmoji.php create mode 100644 resources/emoji-shortcodes.json diff --git a/app/Devnull/ReactionEmoji.php b/app/Devnull/ReactionEmoji.php new file mode 100644 index 00000000..7594fb71 --- /dev/null +++ b/app/Devnull/ReactionEmoji.php @@ -0,0 +1,60 @@ +|null + */ + private static ?array $byKey = null; + + /** + * What devnull takes for the emoji: itself, or the same emoji without its skin tone or with devnull's variation + * selector. Null when devnull has nothing like it. + */ + public static function forDevnull(string $emoji): ?string + { + return self::byKey()[self::key($emoji)] ?? null; + } + + /** + * The emoji with no skin tone and no variation selector, which no two of devnull's share. + */ + private static function key(string $emoji): string + { + return (string) preg_replace('/[\x{FE0F}\x{1F3FB}-\x{1F3FF}]/u', '', trim($emoji)); + } + + /** + * @return array + */ + private static function byKey(): array + { + if (self::$byKey !== null) { + return self::$byKey; + } + + /** @var array $shortcodes */ + $shortcodes = json_decode((string) file_get_contents(resource_path('emoji-shortcodes.json')), true); + + $byKey = []; + + foreach ($shortcodes as $emoji) { + $byKey[self::key($emoji)] = $emoji; + } + + return self::$byKey = $byKey; + } +} diff --git a/app/NativeComponents/Conversation.php b/app/NativeComponents/Conversation.php index afe9941f..d6f2a6e5 100644 --- a/app/NativeComponents/Conversation.php +++ b/app/NativeComponents/Conversation.php @@ -13,6 +13,7 @@ use App\Devnull\Exceptions\ApiException; use App\Devnull\MuteDuration; use App\Devnull\Outbox; +use App\Devnull\ReactionEmoji; use App\Devnull\ResponseCache; use App\Devnull\SiteLink; use App\Devnull\Toasts; @@ -58,7 +59,8 @@ * Retry of its own. Dragging a message sideways slides it out of the way * with the finger, its React and Reply growing in where it was, and Pin * for an org's maintainers; let go far enough and it stays open, or it - * springs back. A double tap toggles a heart. + * springs back. A double tap toggles a heart. React offers the web's quick + * reactions, and the phone's own emoji picker for the rest. * * An org's pinned messages show as the web shows them: the latest in a bar * over the top of the thread, which opens them all in a sheet, and a pin @@ -95,11 +97,6 @@ class Conversation extends NativeComponent */ public const array QuickReactions = ['๐Ÿ˜‚', 'โœ…', 'โค๏ธ']; - /** - * The rest the sheet offers, every one of them in devnull's shortcode set. - */ - public const array MoreReactions = ['๐Ÿ‘', '๐Ÿ‘€', '๐Ÿ”ฅ', '๐ŸŽ‰', '๐Ÿš€', '๐Ÿ’ฏ', '๐Ÿ™', '๐Ÿค”', '๐Ÿ‘', '๐Ÿ˜…', '๐Ÿ˜ฎ', '๐Ÿ˜ข', '๐Ÿ™Œ', '๐Ÿ’€', 'โœจ', '๐Ÿค']; - /** * What the sheet shows: React's reactions for a message, or the org's pinned messages. */ @@ -262,6 +259,11 @@ class Conversation extends NativeComponent */ public bool $reactsFromPinned = false; + /** + * The message the phone's emoji picker is up for, if it is. + */ + public ?int $emojiPickerFor = null; + /** * The org's pinned messages, latest pinned first, which everyone in it sees whatever history they were given. * @@ -573,7 +575,65 @@ public function openPinned(): void public function messageSheetClosed(): void { $this->sheetOpen = false; - $this->reactsFromPinned = false; + + // The reactions making way for the emoji picker keep where React came from. + if ($this->emojiPickerFor === null) { + $this->reactsFromPinned = false; + } + } + + /** + * More reactions: the phone's own emoji picker, with its search, recents and skin tones, for any message. The + * reactions sheet makes way for it. + */ + public function openEmojiPicker(int $id): void + { + if ($this->findMessage($id)?->isEvent() !== false) { + return; + } + + $this->actionsForId = null; + $this->sheetOpen = false; + $this->emojiPickerFor = $id; + } + + /** + * Reacts with the emoji picked, as devnull takes it (see ReactionEmoji), and goes back to the pinned messages if + * React came from there. One devnull doesn't know isn't sent: a toast says so and the picker stays up for another. + */ + public function emojiPicked(string $emoji): void + { + $id = $this->emojiPickerFor; + + if ($id === null) { + return; + } + + $reaction = ReactionEmoji::forDevnull($emoji); + + if ($reaction === null) { + Toasts::failed("devnull doesn't take {$emoji} as a reaction yet. Pick another."); + + return; + } + + $this->emojiPickerFor = null; + $this->sheetOpen = $this->reactsFromPinned; + $this->toggleReaction($id, $reaction); + } + + /** + * Closing the picker without a pick goes back to the pinned messages if React came from there. + */ + public function emojiPickerClosed(): void + { + $this->emojiPickerFor = null; + + if ($this->reactsFromPinned) { + $this->sheet = self::PinnedSheet; + $this->sheetOpen = true; + $this->reactsFromPinned = false; + } } public function reply(int $id): void @@ -1382,7 +1442,6 @@ public function render(): View 'replyTo' => $this->findMessage($this->replyToId), 'sheetMessage' => $this->findMessage($this->sheetMessageId), 'quickReactions' => $this->quickReactions(), - 'moreReactions' => self::MoreReactions, 'canPush' => $thread !== null && $thread->access === Thread::Open, 'canPin' => $this->canPin(), 'actionsWidth' => $this->actionsWidth(), diff --git a/resources/emoji-shortcodes.json b/resources/emoji-shortcodes.json new file mode 100644 index 00000000..1699f266 --- /dev/null +++ b/resources/emoji-shortcodes.json @@ -0,0 +1,1921 @@ +{ + "+1": "๐Ÿ‘", + "-1": "๐Ÿ‘Ž", + "100": "๐Ÿ’ฏ", + "1234": "๐Ÿ”ข", + "1st_place_medal": "๐Ÿฅ‡", + "2nd_place_medal": "๐Ÿฅˆ", + "3rd_place_medal": "๐Ÿฅ‰", + "8ball": "๐ŸŽฑ", + "a": "๐Ÿ…ฐ๏ธ", + "ab": "๐Ÿ†Ž", + "abacus": "๐Ÿงฎ", + "abc": "๐Ÿ”ค", + "abcd": "๐Ÿ”ก", + "accept": "๐Ÿ‰‘", + "accordion": "๐Ÿช—", + "adhesive_bandage": "๐Ÿฉน", + "adult": "๐Ÿง‘", + "aerial_tramway": "๐Ÿšก", + "afghanistan": "๐Ÿ‡ฆ๐Ÿ‡ซ", + "airplane": "โœˆ๏ธ", + "aland_islands": "๐Ÿ‡ฆ๐Ÿ‡ฝ", + "alarm_clock": "โฐ", + "albania": "๐Ÿ‡ฆ๐Ÿ‡ฑ", + "alembic": "โš—๏ธ", + "algeria": "๐Ÿ‡ฉ๐Ÿ‡ฟ", + "alien": "๐Ÿ‘ฝ", + "ambulance": "๐Ÿš‘", + "american_samoa": "๐Ÿ‡ฆ๐Ÿ‡ธ", + "amphora": "๐Ÿบ", + "anatomical_heart": "๐Ÿซ€", + "anchor": "โš“", + "andorra": "๐Ÿ‡ฆ๐Ÿ‡ฉ", + "angel": "๐Ÿ‘ผ", + "anger": "๐Ÿ’ข", + "angola": "๐Ÿ‡ฆ๐Ÿ‡ด", + "angry": "๐Ÿ˜ ", + "anguilla": "๐Ÿ‡ฆ๐Ÿ‡ฎ", + "anguished": "๐Ÿ˜ง", + "ant": "๐Ÿœ", + "antarctica": "๐Ÿ‡ฆ๐Ÿ‡ถ", + "antigua_barbuda": "๐Ÿ‡ฆ๐Ÿ‡ฌ", + "apple": "๐ŸŽ", + "aquarius": "โ™’", + "argentina": "๐Ÿ‡ฆ๐Ÿ‡ท", + "aries": "โ™ˆ", + "armenia": "๐Ÿ‡ฆ๐Ÿ‡ฒ", + "arrow_backward": "โ—€๏ธ", + "arrow_double_down": "โฌ", + "arrow_double_up": "โซ", + "arrow_down": "โฌ‡๏ธ", + "arrow_down_small": "๐Ÿ”ฝ", + "arrow_forward": "โ–ถ๏ธ", + "arrow_heading_down": "โคต๏ธ", + "arrow_heading_up": "โคด๏ธ", + "arrow_left": "โฌ…๏ธ", + "arrow_lower_left": "โ†™๏ธ", + "arrow_lower_right": "โ†˜๏ธ", + "arrow_right": "โžก๏ธ", + "arrow_right_hook": "โ†ช๏ธ", + "arrow_up": "โฌ†๏ธ", + "arrow_up_down": "โ†•๏ธ", + "arrow_up_small": "๐Ÿ”ผ", + "arrow_upper_left": "โ†–๏ธ", + "arrow_upper_right": "โ†—๏ธ", + "arrows_clockwise": "๐Ÿ”ƒ", + "arrows_counterclockwise": "๐Ÿ”„", + "art": "๐ŸŽจ", + "articulated_lorry": "๐Ÿš›", + "artificial_satellite": "๐Ÿ›ฐ๏ธ", + "artist": "๐Ÿง‘โ€๐ŸŽจ", + "aruba": "๐Ÿ‡ฆ๐Ÿ‡ผ", + "ascension_island": "๐Ÿ‡ฆ๐Ÿ‡จ", + "asterisk": "*๏ธโƒฃ", + "astonished": "๐Ÿ˜ฒ", + "astronaut": "๐Ÿง‘โ€๐Ÿš€", + "athletic_shoe": "๐Ÿ‘Ÿ", + "atm": "๐Ÿง", + "atom_symbol": "โš›๏ธ", + "australia": "๐Ÿ‡ฆ๐Ÿ‡บ", + "austria": "๐Ÿ‡ฆ๐Ÿ‡น", + "auto_rickshaw": "๐Ÿ›บ", + "avocado": "๐Ÿฅ‘", + "axe": "๐Ÿช“", + "azerbaijan": "๐Ÿ‡ฆ๐Ÿ‡ฟ", + "b": "๐Ÿ…ฑ๏ธ", + "baby": "๐Ÿ‘ถ", + "baby_bottle": "๐Ÿผ", + "baby_chick": "๐Ÿค", + "baby_symbol": "๐Ÿšผ", + "back": "๐Ÿ”™", + "bacon": "๐Ÿฅ“", + "badger": "๐Ÿฆก", + "badminton": "๐Ÿธ", + "bagel": "๐Ÿฅฏ", + "baggage_claim": "๐Ÿ›„", + "baguette_bread": "๐Ÿฅ–", + "bahamas": "๐Ÿ‡ง๐Ÿ‡ธ", + "bahrain": "๐Ÿ‡ง๐Ÿ‡ญ", + "balance_scale": "โš–๏ธ", + "bald_man": "๐Ÿ‘จโ€๐Ÿฆฒ", + "bald_woman": "๐Ÿ‘ฉโ€๐Ÿฆฒ", + "ballet_shoes": "๐Ÿฉฐ", + "balloon": "๐ŸŽˆ", + "ballot_box": "๐Ÿ—ณ๏ธ", + "ballot_box_with_check": "โ˜‘๏ธ", + "bamboo": "๐ŸŽ", + "banana": "๐ŸŒ", + "bangbang": "โ€ผ๏ธ", + "bangladesh": "๐Ÿ‡ง๐Ÿ‡ฉ", + "banjo": "๐Ÿช•", + "bank": "๐Ÿฆ", + "bar_chart": "๐Ÿ“Š", + "barbados": "๐Ÿ‡ง๐Ÿ‡ง", + "barber": "๐Ÿ’ˆ", + "baseball": "โšพ", + "basket": "๐Ÿงบ", + "basketball": "๐Ÿ€", + "basketball_man": "โ›น๏ธโ€โ™‚๏ธ", + "basketball_woman": "โ›น๏ธโ€โ™€๏ธ", + "bat": "๐Ÿฆ‡", + "bath": "๐Ÿ›€", + "bathtub": "๐Ÿ›", + "battery": "๐Ÿ”‹", + "beach_umbrella": "๐Ÿ–๏ธ", + "beans": "๐Ÿซ˜", + "bear": "๐Ÿป", + "bearded_person": "๐Ÿง”", + "beaver": "๐Ÿฆซ", + "bed": "๐Ÿ›๏ธ", + "bee": "๐Ÿ", + "beer": "๐Ÿบ", + "beers": "๐Ÿป", + "beetle": "๐Ÿชฒ", + "beginner": "๐Ÿ”ฐ", + "belarus": "๐Ÿ‡ง๐Ÿ‡พ", + "belgium": "๐Ÿ‡ง๐Ÿ‡ช", + "belize": "๐Ÿ‡ง๐Ÿ‡ฟ", + "bell": "๐Ÿ””", + "bell_pepper": "๐Ÿซ‘", + "bellhop_bell": "๐Ÿ›Ž๏ธ", + "benin": "๐Ÿ‡ง๐Ÿ‡ฏ", + "bento": "๐Ÿฑ", + "bermuda": "๐Ÿ‡ง๐Ÿ‡ฒ", + "beverage_box": "๐Ÿงƒ", + "bhutan": "๐Ÿ‡ง๐Ÿ‡น", + "bicyclist": "๐Ÿšด", + "bike": "๐Ÿšฒ", + "biking_man": "๐Ÿšดโ€โ™‚๏ธ", + "biking_woman": "๐Ÿšดโ€โ™€๏ธ", + "bikini": "๐Ÿ‘™", + "billed_cap": "๐Ÿงข", + "biohazard": "โ˜ฃ๏ธ", + "bird": "๐Ÿฆ", + "birthday": "๐ŸŽ‚", + "bison": "๐Ÿฆฌ", + "biting_lip": "๐Ÿซฆ", + "black_bird": "๐Ÿฆโ€โฌ›", + "black_cat": "๐Ÿˆโ€โฌ›", + "black_circle": "โšซ", + "black_flag": "๐Ÿด", + "black_heart": "๐Ÿ–ค", + "black_joker": "๐Ÿƒ", + "black_large_square": "โฌ›", + "black_medium_small_square": "โ—พ", + "black_medium_square": "โ—ผ๏ธ", + "black_nib": "โœ’๏ธ", + "black_small_square": "โ–ช๏ธ", + "black_square_button": "๐Ÿ”ฒ", + "blond_haired_man": "๐Ÿ‘ฑโ€โ™‚๏ธ", + "blond_haired_person": "๐Ÿ‘ฑ", + "blond_haired_woman": "๐Ÿ‘ฑโ€โ™€๏ธ", + "blonde_woman": "๐Ÿ‘ฑโ€โ™€๏ธ", + "blossom": "๐ŸŒผ", + "blowfish": "๐Ÿก", + "blue_book": "๐Ÿ“˜", + "blue_car": "๐Ÿš™", + "blue_heart": "๐Ÿ’™", + "blue_square": "๐ŸŸฆ", + "blueberries": "๐Ÿซ", + "blush": "๐Ÿ˜Š", + "boar": "๐Ÿ—", + "boat": "โ›ต", + "bolivia": "๐Ÿ‡ง๐Ÿ‡ด", + "bomb": "๐Ÿ’ฃ", + "bone": "๐Ÿฆด", + "book": "๐Ÿ“–", + "bookmark": "๐Ÿ”–", + "bookmark_tabs": "๐Ÿ“‘", + "books": "๐Ÿ“š", + "boom": "๐Ÿ’ฅ", + "boomerang": "๐Ÿชƒ", + "boot": "๐Ÿ‘ข", + "bosnia_herzegovina": "๐Ÿ‡ง๐Ÿ‡ฆ", + "botswana": "๐Ÿ‡ง๐Ÿ‡ผ", + "bouncing_ball_man": "โ›น๏ธโ€โ™‚๏ธ", + "bouncing_ball_person": "โ›น๏ธ", + "bouncing_ball_woman": "โ›น๏ธโ€โ™€๏ธ", + "bouquet": "๐Ÿ’", + "bouvet_island": "๐Ÿ‡ง๐Ÿ‡ป", + "bow": "๐Ÿ™‡", + "bow_and_arrow": "๐Ÿน", + "bowing_man": "๐Ÿ™‡โ€โ™‚๏ธ", + "bowing_woman": "๐Ÿ™‡โ€โ™€๏ธ", + "bowl_with_spoon": "๐Ÿฅฃ", + "bowling": "๐ŸŽณ", + "boxing_glove": "๐ŸฅŠ", + "boy": "๐Ÿ‘ฆ", + "brain": "๐Ÿง ", + "brazil": "๐Ÿ‡ง๐Ÿ‡ท", + "bread": "๐Ÿž", + "breast_feeding": "๐Ÿคฑ", + "bricks": "๐Ÿงฑ", + "bride_with_veil": "๐Ÿ‘ฐโ€โ™€๏ธ", + "bridge_at_night": "๐ŸŒ‰", + "briefcase": "๐Ÿ’ผ", + "british_indian_ocean_territory": "๐Ÿ‡ฎ๐Ÿ‡ด", + "british_virgin_islands": "๐Ÿ‡ป๐Ÿ‡ฌ", + "broccoli": "๐Ÿฅฆ", + "broken_heart": "๐Ÿ’”", + "broom": "๐Ÿงน", + "brown_circle": "๐ŸŸค", + "brown_heart": "๐ŸคŽ", + "brown_square": "๐ŸŸซ", + "brunei": "๐Ÿ‡ง๐Ÿ‡ณ", + "bubble_tea": "๐Ÿง‹", + "bubbles": "๐Ÿซง", + "bucket": "๐Ÿชฃ", + "bug": "๐Ÿ›", + "building_construction": "๐Ÿ—๏ธ", + "bulb": "๐Ÿ’ก", + "bulgaria": "๐Ÿ‡ง๐Ÿ‡ฌ", + "bullettrain_front": "๐Ÿš…", + "bullettrain_side": "๐Ÿš„", + "burkina_faso": "๐Ÿ‡ง๐Ÿ‡ซ", + "burrito": "๐ŸŒฏ", + "burundi": "๐Ÿ‡ง๐Ÿ‡ฎ", + "bus": "๐ŸšŒ", + "business_suit_levitating": "๐Ÿ•ด๏ธ", + "busstop": "๐Ÿš", + "bust_in_silhouette": "๐Ÿ‘ค", + "busts_in_silhouette": "๐Ÿ‘ฅ", + "butter": "๐Ÿงˆ", + "butterfly": "๐Ÿฆ‹", + "cactus": "๐ŸŒต", + "cake": "๐Ÿฐ", + "calendar": "๐Ÿ“†", + "call_me_hand": "๐Ÿค™", + "calling": "๐Ÿ“ฒ", + "cambodia": "๐Ÿ‡ฐ๐Ÿ‡ญ", + "camel": "๐Ÿซ", + "camera": "๐Ÿ“ท", + "camera_flash": "๐Ÿ“ธ", + "cameroon": "๐Ÿ‡จ๐Ÿ‡ฒ", + "camping": "๐Ÿ•๏ธ", + "canada": "๐Ÿ‡จ๐Ÿ‡ฆ", + "canary_islands": "๐Ÿ‡ฎ๐Ÿ‡จ", + "cancer": "โ™‹", + "candle": "๐Ÿ•ฏ๏ธ", + "candy": "๐Ÿฌ", + "canned_food": "๐Ÿฅซ", + "canoe": "๐Ÿ›ถ", + "cape_verde": "๐Ÿ‡จ๐Ÿ‡ป", + "capital_abcd": "๐Ÿ” ", + "capricorn": "โ™‘", + "car": "๐Ÿš—", + "card_file_box": "๐Ÿ—ƒ๏ธ", + "card_index": "๐Ÿ“‡", + "card_index_dividers": "๐Ÿ—‚๏ธ", + "caribbean_netherlands": "๐Ÿ‡ง๐Ÿ‡ถ", + "carousel_horse": "๐ŸŽ ", + "carpentry_saw": "๐Ÿชš", + "carrot": "๐Ÿฅ•", + "cartwheeling": "๐Ÿคธ", + "cat": "๐Ÿฑ", + "cat2": "๐Ÿˆ", + "cayman_islands": "๐Ÿ‡ฐ๐Ÿ‡พ", + "cd": "๐Ÿ’ฟ", + "central_african_republic": "๐Ÿ‡จ๐Ÿ‡ซ", + "ceuta_melilla": "๐Ÿ‡ช๐Ÿ‡ฆ", + "chad": "๐Ÿ‡น๐Ÿ‡ฉ", + "chains": "โ›“๏ธ", + "chair": "๐Ÿช‘", + "champagne": "๐Ÿพ", + "chart": "๐Ÿ’น", + "chart_with_downwards_trend": "๐Ÿ“‰", + "chart_with_upwards_trend": "๐Ÿ“ˆ", + "checkered_flag": "๐Ÿ", + "cheese": "๐Ÿง€", + "cherries": "๐Ÿ’", + "cherry_blossom": "๐ŸŒธ", + "chess_pawn": "โ™Ÿ๏ธ", + "chestnut": "๐ŸŒฐ", + "chicken": "๐Ÿ”", + "child": "๐Ÿง’", + "children_crossing": "๐Ÿšธ", + "chile": "๐Ÿ‡จ๐Ÿ‡ฑ", + "chipmunk": "๐Ÿฟ๏ธ", + "chocolate_bar": "๐Ÿซ", + "chopsticks": "๐Ÿฅข", + "christmas_island": "๐Ÿ‡จ๐Ÿ‡ฝ", + "christmas_tree": "๐ŸŽ„", + "church": "โ›ช", + "cinema": "๐ŸŽฆ", + "circus_tent": "๐ŸŽช", + "city_sunrise": "๐ŸŒ‡", + "city_sunset": "๐ŸŒ†", + "cityscape": "๐Ÿ™๏ธ", + "cl": "๐Ÿ†‘", + "clamp": "๐Ÿ—œ๏ธ", + "clap": "๐Ÿ‘", + "clapper": "๐ŸŽฌ", + "classical_building": "๐Ÿ›๏ธ", + "climbing": "๐Ÿง—", + "climbing_man": "๐Ÿง—โ€โ™‚๏ธ", + "climbing_woman": "๐Ÿง—โ€โ™€๏ธ", + "clinking_glasses": "๐Ÿฅ‚", + "clipboard": "๐Ÿ“‹", + "clipperton_island": "๐Ÿ‡จ๐Ÿ‡ต", + "clock1": "๐Ÿ•", + "clock10": "๐Ÿ•™", + "clock1030": "๐Ÿ•ฅ", + "clock11": "๐Ÿ•š", + "clock1130": "๐Ÿ•ฆ", + "clock12": "๐Ÿ•›", + "clock1230": "๐Ÿ•ง", + "clock130": "๐Ÿ•œ", + "clock2": "๐Ÿ•‘", + "clock230": "๐Ÿ•", + "clock3": "๐Ÿ•’", + "clock330": "๐Ÿ•ž", + "clock4": "๐Ÿ•“", + "clock430": "๐Ÿ•Ÿ", + "clock5": "๐Ÿ•”", + "clock530": "๐Ÿ• ", + "clock6": "๐Ÿ••", + "clock630": "๐Ÿ•ก", + "clock7": "๐Ÿ•–", + "clock730": "๐Ÿ•ข", + "clock8": "๐Ÿ•—", + "clock830": "๐Ÿ•ฃ", + "clock9": "๐Ÿ•˜", + "clock930": "๐Ÿ•ค", + "closed_book": "๐Ÿ“•", + "closed_lock_with_key": "๐Ÿ”", + "closed_umbrella": "๐ŸŒ‚", + "cloud": "โ˜๏ธ", + "cloud_with_lightning": "๐ŸŒฉ๏ธ", + "cloud_with_lightning_and_rain": "โ›ˆ๏ธ", + "cloud_with_rain": "๐ŸŒง๏ธ", + "cloud_with_snow": "๐ŸŒจ๏ธ", + "clown_face": "๐Ÿคก", + "clubs": "โ™ฃ๏ธ", + "cn": "๐Ÿ‡จ๐Ÿ‡ณ", + "coat": "๐Ÿงฅ", + "cockroach": "๐Ÿชณ", + "cocktail": "๐Ÿธ", + "coconut": "๐Ÿฅฅ", + "cocos_islands": "๐Ÿ‡จ๐Ÿ‡จ", + "coffee": "โ˜•", + "coffin": "โšฐ๏ธ", + "coin": "๐Ÿช™", + "cold_face": "๐Ÿฅถ", + "cold_sweat": "๐Ÿ˜ฐ", + "collision": "๐Ÿ’ฅ", + "colombia": "๐Ÿ‡จ๐Ÿ‡ด", + "comet": "โ˜„๏ธ", + "comoros": "๐Ÿ‡ฐ๐Ÿ‡ฒ", + "compass": "๐Ÿงญ", + "computer": "๐Ÿ’ป", + "computer_mouse": "๐Ÿ–ฑ๏ธ", + "confetti_ball": "๐ŸŽŠ", + "confounded": "๐Ÿ˜–", + "confused": "๐Ÿ˜•", + "congo_brazzaville": "๐Ÿ‡จ๐Ÿ‡ฌ", + "congo_kinshasa": "๐Ÿ‡จ๐Ÿ‡ฉ", + "congratulations": "ใŠ—๏ธ", + "construction": "๐Ÿšง", + "construction_worker": "๐Ÿ‘ท", + "construction_worker_man": "๐Ÿ‘ทโ€โ™‚๏ธ", + "construction_worker_woman": "๐Ÿ‘ทโ€โ™€๏ธ", + "control_knobs": "๐ŸŽ›๏ธ", + "convenience_store": "๐Ÿช", + "cook": "๐Ÿง‘โ€๐Ÿณ", + "cook_islands": "๐Ÿ‡จ๐Ÿ‡ฐ", + "cookie": "๐Ÿช", + "cool": "๐Ÿ†’", + "cop": "๐Ÿ‘ฎ", + "copyright": "ยฉ๏ธ", + "coral": "๐Ÿชธ", + "corn": "๐ŸŒฝ", + "costa_rica": "๐Ÿ‡จ๐Ÿ‡ท", + "cote_divoire": "๐Ÿ‡จ๐Ÿ‡ฎ", + "couch_and_lamp": "๐Ÿ›‹๏ธ", + "couple": "๐Ÿ‘ซ", + "couple_with_heart": "๐Ÿ’‘", + "couple_with_heart_man_man": "๐Ÿ‘จโ€โค๏ธโ€๐Ÿ‘จ", + "couple_with_heart_woman_man": "๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ‘จ", + "couple_with_heart_woman_woman": "๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ‘ฉ", + "couplekiss": "๐Ÿ’", + "couplekiss_man_man": "๐Ÿ‘จโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ", + "couplekiss_man_woman": "๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ", + "couplekiss_woman_woman": "๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ", + "cow": "๐Ÿฎ", + "cow2": "๐Ÿ„", + "cowboy_hat_face": "๐Ÿค ", + "crab": "๐Ÿฆ€", + "crayon": "๐Ÿ–๏ธ", + "credit_card": "๐Ÿ’ณ", + "crescent_moon": "๐ŸŒ™", + "cricket": "๐Ÿฆ—", + "cricket_game": "๐Ÿ", + "croatia": "๐Ÿ‡ญ๐Ÿ‡ท", + "crocodile": "๐ŸŠ", + "croissant": "๐Ÿฅ", + "crossed_fingers": "๐Ÿคž", + "crossed_flags": "๐ŸŽŒ", + "crossed_swords": "โš”๏ธ", + "crown": "๐Ÿ‘‘", + "crutch": "๐Ÿฉผ", + "cry": "๐Ÿ˜ข", + "crying_cat_face": "๐Ÿ˜ฟ", + "crystal_ball": "๐Ÿ”ฎ", + "cuba": "๐Ÿ‡จ๐Ÿ‡บ", + "cucumber": "๐Ÿฅ’", + "cup_with_straw": "๐Ÿฅค", + "cupcake": "๐Ÿง", + "cupid": "๐Ÿ’˜", + "curacao": "๐Ÿ‡จ๐Ÿ‡ผ", + "curling_stone": "๐ŸฅŒ", + "curly_haired_man": "๐Ÿ‘จโ€๐Ÿฆฑ", + "curly_haired_woman": "๐Ÿ‘ฉโ€๐Ÿฆฑ", + "curly_loop": "โžฐ", + "currency_exchange": "๐Ÿ’ฑ", + "curry": "๐Ÿ›", + "cursing_face": "๐Ÿคฌ", + "custard": "๐Ÿฎ", + "customs": "๐Ÿ›ƒ", + "cut_of_meat": "๐Ÿฅฉ", + "cyclone": "๐ŸŒ€", + "cyprus": "๐Ÿ‡จ๐Ÿ‡พ", + "czech_republic": "๐Ÿ‡จ๐Ÿ‡ฟ", + "dagger": "๐Ÿ—ก๏ธ", + "dancer": "๐Ÿ’ƒ", + "dancers": "๐Ÿ‘ฏ", + "dancing_men": "๐Ÿ‘ฏโ€โ™‚๏ธ", + "dancing_women": "๐Ÿ‘ฏโ€โ™€๏ธ", + "dango": "๐Ÿก", + "dark_sunglasses": "๐Ÿ•ถ๏ธ", + "dart": "๐ŸŽฏ", + "dash": "๐Ÿ’จ", + "date": "๐Ÿ“…", + "de": "๐Ÿ‡ฉ๐Ÿ‡ช", + "deaf_man": "๐Ÿงโ€โ™‚๏ธ", + "deaf_person": "๐Ÿง", + "deaf_woman": "๐Ÿงโ€โ™€๏ธ", + "deciduous_tree": "๐ŸŒณ", + "deer": "๐ŸฆŒ", + "denmark": "๐Ÿ‡ฉ๐Ÿ‡ฐ", + "department_store": "๐Ÿฌ", + "derelict_house": "๐Ÿš๏ธ", + "desert": "๐Ÿœ๏ธ", + "desert_island": "๐Ÿ๏ธ", + "desktop_computer": "๐Ÿ–ฅ๏ธ", + "detective": "๐Ÿ•ต๏ธ", + "diamond_shape_with_a_dot_inside": "๐Ÿ’ ", + "diamonds": "โ™ฆ๏ธ", + "diego_garcia": "๐Ÿ‡ฉ๐Ÿ‡ฌ", + "disappointed": "๐Ÿ˜ž", + "disappointed_relieved": "๐Ÿ˜ฅ", + "disguised_face": "๐Ÿฅธ", + "dislike": "๐Ÿ‘Ž", + "diving_mask": "๐Ÿคฟ", + "diya_lamp": "๐Ÿช”", + "dizzy": "๐Ÿ’ซ", + "dizzy_face": "๐Ÿ˜ต", + "djibouti": "๐Ÿ‡ฉ๐Ÿ‡ฏ", + "dna": "๐Ÿงฌ", + "do_not_litter": "๐Ÿšฏ", + "dodo": "๐Ÿฆค", + "dog": "๐Ÿถ", + "dog2": "๐Ÿ•", + "dollar": "๐Ÿ’ต", + "dolls": "๐ŸŽŽ", + "dolphin": "๐Ÿฌ", + "dominica": "๐Ÿ‡ฉ๐Ÿ‡ฒ", + "dominican_republic": "๐Ÿ‡ฉ๐Ÿ‡ด", + "donkey": "๐Ÿซ", + "door": "๐Ÿšช", + "dotted_line_face": "๐Ÿซฅ", + "doughnut": "๐Ÿฉ", + "dove": "๐Ÿ•Š๏ธ", + "dragon": "๐Ÿ‰", + "dragon_face": "๐Ÿฒ", + "dress": "๐Ÿ‘—", + "dromedary_camel": "๐Ÿช", + "drooling_face": "๐Ÿคค", + "drop_of_blood": "๐Ÿฉธ", + "droplet": "๐Ÿ’ง", + "drum": "๐Ÿฅ", + "duck": "๐Ÿฆ†", + "dumpling": "๐ŸฅŸ", + "dvd": "๐Ÿ“€", + "e-mail": "๐Ÿ“ง", + "eagle": "๐Ÿฆ…", + "ear": "๐Ÿ‘‚", + "ear_of_rice": "๐ŸŒพ", + "ear_with_hearing_aid": "๐Ÿฆป", + "earth_africa": "๐ŸŒ", + "earth_americas": "๐ŸŒŽ", + "earth_asia": "๐ŸŒ", + "ecuador": "๐Ÿ‡ช๐Ÿ‡จ", + "egg": "๐Ÿฅš", + "eggplant": "๐Ÿ†", + "egypt": "๐Ÿ‡ช๐Ÿ‡ฌ", + "eight": "8๏ธโƒฃ", + "eight_pointed_black_star": "โœด๏ธ", + "eight_spoked_asterisk": "โœณ๏ธ", + "eject_button": "โ๏ธ", + "el_salvador": "๐Ÿ‡ธ๐Ÿ‡ป", + "electric_plug": "๐Ÿ”Œ", + "elephant": "๐Ÿ˜", + "elevator": "๐Ÿ›—", + "elf": "๐Ÿง", + "elf_man": "๐Ÿงโ€โ™‚๏ธ", + "elf_woman": "๐Ÿงโ€โ™€๏ธ", + "email": "๐Ÿ“ง", + "empty_nest": "๐Ÿชน", + "end": "๐Ÿ”š", + "england": "๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ", + "envelope": "โœ‰๏ธ", + "envelope_with_arrow": "๐Ÿ“ฉ", + "equatorial_guinea": "๐Ÿ‡ฌ๐Ÿ‡ถ", + "eritrea": "๐Ÿ‡ช๐Ÿ‡ท", + "es": "๐Ÿ‡ช๐Ÿ‡ธ", + "estonia": "๐Ÿ‡ช๐Ÿ‡ช", + "ethiopia": "๐Ÿ‡ช๐Ÿ‡น", + "eu": "๐Ÿ‡ช๐Ÿ‡บ", + "euro": "๐Ÿ’ถ", + "european_castle": "๐Ÿฐ", + "european_post_office": "๐Ÿค", + "european_union": "๐Ÿ‡ช๐Ÿ‡บ", + "evergreen_tree": "๐ŸŒฒ", + "exclamation": "โ—", + "exploding_head": "๐Ÿคฏ", + "expressionless": "๐Ÿ˜‘", + "eye": "๐Ÿ‘๏ธ", + "eye_speech_bubble": "๐Ÿ‘๏ธโ€๐Ÿ—จ๏ธ", + "eyeglasses": "๐Ÿ‘“", + "eyes": "๐Ÿ‘€", + "face_exhaling": "๐Ÿ˜ฎโ€๐Ÿ’จ", + "face_holding_back_tears": "๐Ÿฅน", + "face_in_clouds": "๐Ÿ˜ถโ€๐ŸŒซ๏ธ", + "face_with_diagonal_mouth": "๐Ÿซค", + "face_with_head_bandage": "๐Ÿค•", + "face_with_open_eyes_and_hand_over_mouth": "๐Ÿซข", + "face_with_peeking_eye": "๐Ÿซฃ", + "face_with_spiral_eyes": "๐Ÿ˜ตโ€๐Ÿ’ซ", + "face_with_thermometer": "๐Ÿค’", + "facepalm": "๐Ÿคฆ", + "facepunch": "๐Ÿ‘Š", + "factory": "๐Ÿญ", + "factory_worker": "๐Ÿง‘โ€๐Ÿญ", + "fairy": "๐Ÿงš", + "fairy_man": "๐Ÿงšโ€โ™‚๏ธ", + "fairy_woman": "๐Ÿงšโ€โ™€๏ธ", + "falafel": "๐Ÿง†", + "falkland_islands": "๐Ÿ‡ซ๐Ÿ‡ฐ", + "fallen_leaf": "๐Ÿ‚", + "family": "๐Ÿ‘ช", + "family_man_boy": "๐Ÿ‘จโ€๐Ÿ‘ฆ", + "family_man_boy_boy": "๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", + "family_man_girl": "๐Ÿ‘จโ€๐Ÿ‘ง", + "family_man_girl_boy": "๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", + "family_man_girl_girl": "๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง", + "family_man_man_boy": "๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆ", + "family_man_man_boy_boy": "๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", + "family_man_man_girl": "๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘ง", + "family_man_man_girl_boy": "๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", + "family_man_man_girl_girl": "๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง", + "family_man_woman_boy": "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ", + "family_man_woman_boy_boy": "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", + "family_man_woman_girl": "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง", + "family_man_woman_girl_boy": "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", + "family_man_woman_girl_girl": "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง", + "family_woman_boy": "๐Ÿ‘ฉโ€๐Ÿ‘ฆ", + "family_woman_boy_boy": "๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", + "family_woman_girl": "๐Ÿ‘ฉโ€๐Ÿ‘ง", + "family_woman_girl_boy": "๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", + "family_woman_girl_girl": "๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง", + "family_woman_woman_boy": "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ", + "family_woman_woman_boy_boy": "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ", + "family_woman_woman_girl": "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ง", + "family_woman_woman_girl_boy": "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", + "family_woman_woman_girl_girl": "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง", + "farmer": "๐Ÿง‘โ€๐ŸŒพ", + "faroe_islands": "๐Ÿ‡ซ๐Ÿ‡ด", + "fast_forward": "โฉ", + "fax": "๐Ÿ“ ", + "fearful": "๐Ÿ˜จ", + "feather": "๐Ÿชถ", + "feet": "๐Ÿพ", + "female_detective": "๐Ÿ•ต๏ธโ€โ™€๏ธ", + "female_sign": "โ™€๏ธ", + "ferris_wheel": "๐ŸŽก", + "ferry": "โ›ด๏ธ", + "field_hockey": "๐Ÿ‘", + "fiji": "๐Ÿ‡ซ๐Ÿ‡ฏ", + "file_cabinet": "๐Ÿ—„๏ธ", + "file_folder": "๐Ÿ“", + "film_projector": "๐Ÿ“ฝ๏ธ", + "film_strip": "๐ŸŽž๏ธ", + "finland": "๐Ÿ‡ซ๐Ÿ‡ฎ", + "fire": "๐Ÿ”ฅ", + "fire_engine": "๐Ÿš’", + "fire_extinguisher": "๐Ÿงฏ", + "firecracker": "๐Ÿงจ", + "firefighter": "๐Ÿง‘โ€๐Ÿš’", + "fireworks": "๐ŸŽ†", + "first_quarter_moon": "๐ŸŒ“", + "first_quarter_moon_with_face": "๐ŸŒ›", + "fish": "๐ŸŸ", + "fish_cake": "๐Ÿฅ", + "fishing_pole_and_fish": "๐ŸŽฃ", + "fist": "โœŠ", + "fist_left": "๐Ÿค›", + "fist_oncoming": "๐Ÿ‘Š", + "fist_raised": "โœŠ", + "fist_right": "๐Ÿคœ", + "five": "5๏ธโƒฃ", + "flags": "๐ŸŽ", + "flamingo": "๐Ÿฆฉ", + "flashlight": "๐Ÿ”ฆ", + "flat_shoe": "๐Ÿฅฟ", + "flatbread": "๐Ÿซ“", + "fleur_de_lis": "โšœ๏ธ", + "flight_arrival": "๐Ÿ›ฌ", + "flight_departure": "๐Ÿ›ซ", + "flipper": "๐Ÿฌ", + "floppy_disk": "๐Ÿ’พ", + "flower_playing_cards": "๐ŸŽด", + "flushed": "๐Ÿ˜ณ", + "flute": "๐Ÿชˆ", + "fly": "๐Ÿชฐ", + "flying_disc": "๐Ÿฅ", + "flying_saucer": "๐Ÿ›ธ", + "fog": "๐ŸŒซ๏ธ", + "foggy": "๐ŸŒ", + "folding_hand_fan": "๐Ÿชญ", + "fondue": "๐Ÿซ•", + "foot": "๐Ÿฆถ", + "football": "๐Ÿˆ", + "footprints": "๐Ÿ‘ฃ", + "fork_and_knife": "๐Ÿด", + "fortune_cookie": "๐Ÿฅ ", + "fountain": "โ›ฒ", + "fountain_pen": "๐Ÿ–‹๏ธ", + "four": "4๏ธโƒฃ", + "four_leaf_clover": "๐Ÿ€", + "fox_face": "๐ŸฆŠ", + "fr": "๐Ÿ‡ซ๐Ÿ‡ท", + "framed_picture": "๐Ÿ–ผ๏ธ", + "free": "๐Ÿ†“", + "french_guiana": "๐Ÿ‡ฌ๐Ÿ‡ซ", + "french_polynesia": "๐Ÿ‡ต๐Ÿ‡ซ", + "french_southern_territories": "๐Ÿ‡น๐Ÿ‡ซ", + "fried_egg": "๐Ÿณ", + "fried_shrimp": "๐Ÿค", + "fries": "๐ŸŸ", + "frog": "๐Ÿธ", + "frowning": "๐Ÿ˜ฆ", + "frowning_face": "โ˜น๏ธ", + "frowning_man": "๐Ÿ™โ€โ™‚๏ธ", + "frowning_person": "๐Ÿ™", + "frowning_woman": "๐Ÿ™โ€โ™€๏ธ", + "fu": "๐Ÿ–•", + "fuelpump": "โ›ฝ", + "full_moon": "๐ŸŒ•", + "full_moon_with_face": "๐ŸŒ", + "funeral_urn": "โšฑ๏ธ", + "gabon": "๐Ÿ‡ฌ๐Ÿ‡ฆ", + "gambia": "๐Ÿ‡ฌ๐Ÿ‡ฒ", + "game_die": "๐ŸŽฒ", + "garlic": "๐Ÿง„", + "gb": "๐Ÿ‡ฌ๐Ÿ‡ง", + "gear": "โš™๏ธ", + "gem": "๐Ÿ’Ž", + "gemini": "โ™Š", + "genie": "๐Ÿงž", + "genie_man": "๐Ÿงžโ€โ™‚๏ธ", + "genie_woman": "๐Ÿงžโ€โ™€๏ธ", + "georgia": "๐Ÿ‡ฌ๐Ÿ‡ช", + "ghana": "๐Ÿ‡ฌ๐Ÿ‡ญ", + "ghost": "๐Ÿ‘ป", + "gibraltar": "๐Ÿ‡ฌ๐Ÿ‡ฎ", + "gift": "๐ŸŽ", + "gift_heart": "๐Ÿ’", + "ginger_root": "๐Ÿซš", + "giraffe": "๐Ÿฆ’", + "girl": "๐Ÿ‘ง", + "globe_with_meridians": "๐ŸŒ", + "gloves": "๐Ÿงค", + "goal_net": "๐Ÿฅ…", + "goat": "๐Ÿ", + "goggles": "๐Ÿฅฝ", + "golf": "โ›ณ", + "golfing": "๐ŸŒ๏ธ", + "golfing_man": "๐ŸŒ๏ธโ€โ™‚๏ธ", + "golfing_woman": "๐ŸŒ๏ธโ€โ™€๏ธ", + "goose": "๐Ÿชฟ", + "gorilla": "๐Ÿฆ", + "grapes": "๐Ÿ‡", + "greece": "๐Ÿ‡ฌ๐Ÿ‡ท", + "green_apple": "๐Ÿ", + "green_book": "๐Ÿ“—", + "green_circle": "๐ŸŸข", + "green_heart": "๐Ÿ’š", + "green_salad": "๐Ÿฅ—", + "green_square": "๐ŸŸฉ", + "greenland": "๐Ÿ‡ฌ๐Ÿ‡ฑ", + "grenada": "๐Ÿ‡ฌ๐Ÿ‡ฉ", + "grey_exclamation": "โ•", + "grey_heart": "๐Ÿฉถ", + "grey_question": "โ”", + "grimacing": "๐Ÿ˜ฌ", + "grin": "๐Ÿ˜", + "grinning": "๐Ÿ˜€", + "guadeloupe": "๐Ÿ‡ฌ๐Ÿ‡ต", + "guam": "๐Ÿ‡ฌ๐Ÿ‡บ", + "guard": "๐Ÿ’‚", + "guardsman": "๐Ÿ’‚โ€โ™‚๏ธ", + "guardswoman": "๐Ÿ’‚โ€โ™€๏ธ", + "guatemala": "๐Ÿ‡ฌ๐Ÿ‡น", + "guernsey": "๐Ÿ‡ฌ๐Ÿ‡ฌ", + "guide_dog": "๐Ÿฆฎ", + "guinea": "๐Ÿ‡ฌ๐Ÿ‡ณ", + "guinea_bissau": "๐Ÿ‡ฌ๐Ÿ‡ผ", + "guitar": "๐ŸŽธ", + "gun": "๐Ÿ”ซ", + "guyana": "๐Ÿ‡ฌ๐Ÿ‡พ", + "hair_pick": "๐Ÿชฎ", + "haircut": "๐Ÿ’‡", + "haircut_man": "๐Ÿ’‡โ€โ™‚๏ธ", + "haircut_woman": "๐Ÿ’‡โ€โ™€๏ธ", + "haiti": "๐Ÿ‡ญ๐Ÿ‡น", + "hamburger": "๐Ÿ”", + "hammer": "๐Ÿ”จ", + "hammer_and_pick": "โš’๏ธ", + "hammer_and_wrench": "๐Ÿ› ๏ธ", + "hamsa": "๐Ÿชฌ", + "hamster": "๐Ÿน", + "hand": "โœ‹", + "hand_over_mouth": "๐Ÿคญ", + "hand_with_index_finger_and_thumb_crossed": "๐Ÿซฐ", + "handbag": "๐Ÿ‘œ", + "handball_person": "๐Ÿคพ", + "handshake": "๐Ÿค", + "hankey": "๐Ÿ’ฉ", + "hash": "#๏ธโƒฃ", + "hatched_chick": "๐Ÿฅ", + "hatching_chick": "๐Ÿฃ", + "headphones": "๐ŸŽง", + "headstone": "๐Ÿชฆ", + "health_worker": "๐Ÿง‘โ€โš•๏ธ", + "hear_no_evil": "๐Ÿ™‰", + "heard_mcdonald_islands": "๐Ÿ‡ญ๐Ÿ‡ฒ", + "heart": "โค๏ธ", + "heart_decoration": "๐Ÿ’Ÿ", + "heart_eyes": "๐Ÿ˜", + "heart_eyes_cat": "๐Ÿ˜ป", + "heart_hands": "๐Ÿซถ", + "heart_on_fire": "โค๏ธโ€๐Ÿ”ฅ", + "heartbeat": "๐Ÿ’“", + "heartpulse": "๐Ÿ’—", + "hearts": "โ™ฅ๏ธ", + "heavy_check_mark": "โœ”๏ธ", + "heavy_division_sign": "โž—", + "heavy_dollar_sign": "๐Ÿ’ฒ", + "heavy_equals_sign": "๐ŸŸฐ", + "heavy_exclamation_mark": "โ—", + "heavy_heart_exclamation": "โฃ๏ธ", + "heavy_minus_sign": "โž–", + "heavy_multiplication_x": "โœ–๏ธ", + "heavy_plus_sign": "โž•", + "hedgehog": "๐Ÿฆ”", + "helicopter": "๐Ÿš", + "herb": "๐ŸŒฟ", + "hibiscus": "๐ŸŒบ", + "high_brightness": "๐Ÿ”†", + "high_heel": "๐Ÿ‘ ", + "hiking_boot": "๐Ÿฅพ", + "hindu_temple": "๐Ÿ›•", + "hippopotamus": "๐Ÿฆ›", + "hocho": "๐Ÿ”ช", + "hole": "๐Ÿ•ณ๏ธ", + "honduras": "๐Ÿ‡ญ๐Ÿ‡ณ", + "honey_pot": "๐Ÿฏ", + "honeybee": "๐Ÿ", + "hong_kong": "๐Ÿ‡ญ๐Ÿ‡ฐ", + "hook": "๐Ÿช", + "horse": "๐Ÿด", + "horse_racing": "๐Ÿ‡", + "hospital": "๐Ÿฅ", + "hot_face": "๐Ÿฅต", + "hot_pepper": "๐ŸŒถ๏ธ", + "hotdog": "๐ŸŒญ", + "hotel": "๐Ÿจ", + "hotsprings": "โ™จ๏ธ", + "hourglass": "โŒ›", + "hourglass_flowing_sand": "โณ", + "house": "๐Ÿ ", + "house_with_garden": "๐Ÿก", + "houses": "๐Ÿ˜๏ธ", + "hugs": "๐Ÿค—", + "hungary": "๐Ÿ‡ญ๐Ÿ‡บ", + "hushed": "๐Ÿ˜ฏ", + "hut": "๐Ÿ›–", + "hyacinth": "๐Ÿชป", + "ice_cream": "๐Ÿจ", + "ice_cube": "๐ŸงŠ", + "ice_hockey": "๐Ÿ’", + "ice_skate": "โ›ธ๏ธ", + "icecream": "๐Ÿฆ", + "iceland": "๐Ÿ‡ฎ๐Ÿ‡ธ", + "id": "๐Ÿ†”", + "identification_card": "๐Ÿชช", + "ideograph_advantage": "๐Ÿ‰", + "imp": "๐Ÿ‘ฟ", + "inbox_tray": "๐Ÿ“ฅ", + "incoming_envelope": "๐Ÿ“จ", + "index_pointing_at_the_viewer": "๐Ÿซต", + "india": "๐Ÿ‡ฎ๐Ÿ‡ณ", + "indonesia": "๐Ÿ‡ฎ๐Ÿ‡ฉ", + "infinity": "โ™พ๏ธ", + "information_desk_person": "๐Ÿ’", + "information_source": "โ„น๏ธ", + "innocent": "๐Ÿ˜‡", + "interrobang": "โ‰๏ธ", + "iphone": "๐Ÿ“ฑ", + "iran": "๐Ÿ‡ฎ๐Ÿ‡ท", + "iraq": "๐Ÿ‡ฎ๐Ÿ‡ถ", + "ireland": "๐Ÿ‡ฎ๐Ÿ‡ช", + "isle_of_man": "๐Ÿ‡ฎ๐Ÿ‡ฒ", + "israel": "๐Ÿ‡ฎ๐Ÿ‡ฑ", + "it": "๐Ÿ‡ฎ๐Ÿ‡น", + "izakaya_lantern": "๐Ÿฎ", + "jack_o_lantern": "๐ŸŽƒ", + "jamaica": "๐Ÿ‡ฏ๐Ÿ‡ฒ", + "japan": "๐Ÿ—พ", + "japanese_castle": "๐Ÿฏ", + "japanese_goblin": "๐Ÿ‘บ", + "japanese_ogre": "๐Ÿ‘น", + "jar": "๐Ÿซ™", + "jeans": "๐Ÿ‘–", + "jellyfish": "๐Ÿชผ", + "jersey": "๐Ÿ‡ฏ๐Ÿ‡ช", + "jigsaw": "๐Ÿงฉ", + "jordan": "๐Ÿ‡ฏ๐Ÿ‡ด", + "joy": "๐Ÿ˜‚", + "joy_cat": "๐Ÿ˜น", + "joystick": "๐Ÿ•น๏ธ", + "jp": "๐Ÿ‡ฏ๐Ÿ‡ต", + "judge": "๐Ÿง‘โ€โš–๏ธ", + "juggling_person": "๐Ÿคน", + "kaaba": "๐Ÿ•‹", + "kangaroo": "๐Ÿฆ˜", + "kazakhstan": "๐Ÿ‡ฐ๐Ÿ‡ฟ", + "kenya": "๐Ÿ‡ฐ๐Ÿ‡ช", + "key": "๐Ÿ”‘", + "keyboard": "โŒจ๏ธ", + "keycap_ten": "๐Ÿ”Ÿ", + "khanda": "๐Ÿชฏ", + "kick_scooter": "๐Ÿ›ด", + "kimono": "๐Ÿ‘˜", + "kiribati": "๐Ÿ‡ฐ๐Ÿ‡ฎ", + "kiss": "๐Ÿ’‹", + "kissing": "๐Ÿ˜—", + "kissing_cat": "๐Ÿ˜ฝ", + "kissing_closed_eyes": "๐Ÿ˜š", + "kissing_heart": "๐Ÿ˜˜", + "kissing_smiling_eyes": "๐Ÿ˜™", + "kite": "๐Ÿช", + "kiwi_fruit": "๐Ÿฅ", + "kneeling_man": "๐ŸงŽโ€โ™‚๏ธ", + "kneeling_person": "๐ŸงŽ", + "kneeling_woman": "๐ŸงŽโ€โ™€๏ธ", + "knife": "๐Ÿ”ช", + "knot": "๐Ÿชข", + "koala": "๐Ÿจ", + "koko": "๐Ÿˆ", + "kosovo": "๐Ÿ‡ฝ๐Ÿ‡ฐ", + "kr": "๐Ÿ‡ฐ๐Ÿ‡ท", + "kuwait": "๐Ÿ‡ฐ๐Ÿ‡ผ", + "kyrgyzstan": "๐Ÿ‡ฐ๐Ÿ‡ฌ", + "lab_coat": "๐Ÿฅผ", + "label": "๐Ÿท๏ธ", + "lacrosse": "๐Ÿฅ", + "ladder": "๐Ÿชœ", + "lady_beetle": "๐Ÿž", + "lantern": "๐Ÿฎ", + "laos": "๐Ÿ‡ฑ๐Ÿ‡ฆ", + "large_blue_circle": "๐Ÿ”ต", + "large_blue_diamond": "๐Ÿ”ท", + "large_orange_diamond": "๐Ÿ”ถ", + "last_quarter_moon": "๐ŸŒ—", + "last_quarter_moon_with_face": "๐ŸŒœ", + "latin_cross": "โœ๏ธ", + "latvia": "๐Ÿ‡ฑ๐Ÿ‡ป", + "laugh": "๐Ÿ˜‚", + "laughing": "๐Ÿ˜†", + "leafy_green": "๐Ÿฅฌ", + "leaves": "๐Ÿƒ", + "lebanon": "๐Ÿ‡ฑ๐Ÿ‡ง", + "ledger": "๐Ÿ“’", + "left_luggage": "๐Ÿ›…", + "left_right_arrow": "โ†”๏ธ", + "left_speech_bubble": "๐Ÿ—จ๏ธ", + "leftwards_arrow_with_hook": "โ†ฉ๏ธ", + "leftwards_hand": "๐Ÿซฒ", + "leftwards_pushing_hand": "๐Ÿซท", + "leg": "๐Ÿฆต", + "lemon": "๐Ÿ‹", + "leo": "โ™Œ", + "leopard": "๐Ÿ†", + "lesotho": "๐Ÿ‡ฑ๐Ÿ‡ธ", + "level_slider": "๐ŸŽš๏ธ", + "liberia": "๐Ÿ‡ฑ๐Ÿ‡ท", + "libra": "โ™Ž", + "libya": "๐Ÿ‡ฑ๐Ÿ‡พ", + "liechtenstein": "๐Ÿ‡ฑ๐Ÿ‡ฎ", + "light_blue_heart": "๐Ÿฉต", + "light_rail": "๐Ÿšˆ", + "like": "๐Ÿ‘", + "link": "๐Ÿ”—", + "lion": "๐Ÿฆ", + "lips": "๐Ÿ‘„", + "lipstick": "๐Ÿ’„", + "lithuania": "๐Ÿ‡ฑ๐Ÿ‡น", + "lizard": "๐ŸฆŽ", + "llama": "๐Ÿฆ™", + "lobster": "๐Ÿฆž", + "lock": "๐Ÿ”’", + "lock_with_ink_pen": "๐Ÿ”", + "lollipop": "๐Ÿญ", + "long_drum": "๐Ÿช˜", + "loop": "โžฟ", + "lotion_bottle": "๐Ÿงด", + "lotus": "๐Ÿชท", + "lotus_position": "๐Ÿง˜", + "lotus_position_man": "๐Ÿง˜โ€โ™‚๏ธ", + "lotus_position_woman": "๐Ÿง˜โ€โ™€๏ธ", + "loud_sound": "๐Ÿ”Š", + "loudspeaker": "๐Ÿ“ข", + "love_hotel": "๐Ÿฉ", + "love_letter": "๐Ÿ’Œ", + "love_you_gesture": "๐ŸคŸ", + "low_battery": "๐Ÿชซ", + "low_brightness": "๐Ÿ”…", + "luggage": "๐Ÿงณ", + "lungs": "๐Ÿซ", + "luxembourg": "๐Ÿ‡ฑ๐Ÿ‡บ", + "lying_face": "๐Ÿคฅ", + "m": "โ“‚๏ธ", + "macau": "๐Ÿ‡ฒ๐Ÿ‡ด", + "macedonia": "๐Ÿ‡ฒ๐Ÿ‡ฐ", + "madagascar": "๐Ÿ‡ฒ๐Ÿ‡ฌ", + "mag": "๐Ÿ”", + "mag_right": "๐Ÿ”Ž", + "mage": "๐Ÿง™", + "mage_man": "๐Ÿง™โ€โ™‚๏ธ", + "mage_woman": "๐Ÿง™โ€โ™€๏ธ", + "magic_wand": "๐Ÿช„", + "magnet": "๐Ÿงฒ", + "mahjong": "๐Ÿ€„", + "mailbox": "๐Ÿ“ซ", + "mailbox_closed": "๐Ÿ“ช", + "mailbox_with_mail": "๐Ÿ“ฌ", + "mailbox_with_no_mail": "๐Ÿ“ญ", + "malawi": "๐Ÿ‡ฒ๐Ÿ‡ผ", + "malaysia": "๐Ÿ‡ฒ๐Ÿ‡พ", + "maldives": "๐Ÿ‡ฒ๐Ÿ‡ป", + "male_detective": "๐Ÿ•ต๏ธโ€โ™‚๏ธ", + "male_sign": "โ™‚๏ธ", + "mali": "๐Ÿ‡ฒ๐Ÿ‡ฑ", + "malta": "๐Ÿ‡ฒ๐Ÿ‡น", + "mammoth": "๐Ÿฆฃ", + "man": "๐Ÿ‘จ", + "man_artist": "๐Ÿ‘จโ€๐ŸŽจ", + "man_astronaut": "๐Ÿ‘จโ€๐Ÿš€", + "man_beard": "๐Ÿง”โ€โ™‚๏ธ", + "man_cartwheeling": "๐Ÿคธโ€โ™‚๏ธ", + "man_cook": "๐Ÿ‘จโ€๐Ÿณ", + "man_dancing": "๐Ÿ•บ", + "man_facepalming": "๐Ÿคฆโ€โ™‚๏ธ", + "man_factory_worker": "๐Ÿ‘จโ€๐Ÿญ", + "man_farmer": "๐Ÿ‘จโ€๐ŸŒพ", + "man_feeding_baby": "๐Ÿ‘จโ€๐Ÿผ", + "man_firefighter": "๐Ÿ‘จโ€๐Ÿš’", + "man_health_worker": "๐Ÿ‘จโ€โš•๏ธ", + "man_in_manual_wheelchair": "๐Ÿ‘จโ€๐Ÿฆฝ", + "man_in_motorized_wheelchair": "๐Ÿ‘จโ€๐Ÿฆผ", + "man_in_tuxedo": "๐Ÿคตโ€โ™‚๏ธ", + "man_judge": "๐Ÿ‘จโ€โš–๏ธ", + "man_juggling": "๐Ÿคนโ€โ™‚๏ธ", + "man_mechanic": "๐Ÿ‘จโ€๐Ÿ”ง", + "man_office_worker": "๐Ÿ‘จโ€๐Ÿ’ผ", + "man_pilot": "๐Ÿ‘จโ€โœˆ๏ธ", + "man_playing_handball": "๐Ÿคพโ€โ™‚๏ธ", + "man_playing_water_polo": "๐Ÿคฝโ€โ™‚๏ธ", + "man_scientist": "๐Ÿ‘จโ€๐Ÿ”ฌ", + "man_shrugging": "๐Ÿคทโ€โ™‚๏ธ", + "man_singer": "๐Ÿ‘จโ€๐ŸŽค", + "man_student": "๐Ÿ‘จโ€๐ŸŽ“", + "man_teacher": "๐Ÿ‘จโ€๐Ÿซ", + "man_technologist": "๐Ÿ‘จโ€๐Ÿ’ป", + "man_with_gua_pi_mao": "๐Ÿ‘ฒ", + "man_with_probing_cane": "๐Ÿ‘จโ€๐Ÿฆฏ", + "man_with_turban": "๐Ÿ‘ณโ€โ™‚๏ธ", + "man_with_veil": "๐Ÿ‘ฐโ€โ™‚๏ธ", + "mandarin": "๐ŸŠ", + "mango": "๐Ÿฅญ", + "mans_shoe": "๐Ÿ‘ž", + "mantelpiece_clock": "๐Ÿ•ฐ๏ธ", + "manual_wheelchair": "๐Ÿฆฝ", + "maple_leaf": "๐Ÿ", + "maracas": "๐Ÿช‡", + "marshall_islands": "๐Ÿ‡ฒ๐Ÿ‡ญ", + "martial_arts_uniform": "๐Ÿฅ‹", + "martinique": "๐Ÿ‡ฒ๐Ÿ‡ถ", + "mask": "๐Ÿ˜ท", + "massage": "๐Ÿ’†", + "massage_man": "๐Ÿ’†โ€โ™‚๏ธ", + "massage_woman": "๐Ÿ’†โ€โ™€๏ธ", + "mate": "๐Ÿง‰", + "mauritania": "๐Ÿ‡ฒ๐Ÿ‡ท", + "mauritius": "๐Ÿ‡ฒ๐Ÿ‡บ", + "mayotte": "๐Ÿ‡พ๐Ÿ‡น", + "meat_on_bone": "๐Ÿ–", + "mechanic": "๐Ÿง‘โ€๐Ÿ”ง", + "mechanical_arm": "๐Ÿฆพ", + "mechanical_leg": "๐Ÿฆฟ", + "medal_military": "๐ŸŽ–๏ธ", + "medal_sports": "๐Ÿ…", + "medical_symbol": "โš•๏ธ", + "mega": "๐Ÿ“ฃ", + "melon": "๐Ÿˆ", + "melting_face": "๐Ÿซ ", + "memo": "๐Ÿ“", + "men_wrestling": "๐Ÿคผโ€โ™‚๏ธ", + "mending_heart": "โค๏ธโ€๐Ÿฉน", + "menorah": "๐Ÿ•Ž", + "mens": "๐Ÿšน", + "mermaid": "๐Ÿงœโ€โ™€๏ธ", + "merman": "๐Ÿงœโ€โ™‚๏ธ", + "merperson": "๐Ÿงœ", + "metal": "๐Ÿค˜", + "metro": "๐Ÿš‡", + "mexico": "๐Ÿ‡ฒ๐Ÿ‡ฝ", + "microbe": "๐Ÿฆ ", + "micronesia": "๐Ÿ‡ซ๐Ÿ‡ฒ", + "microphone": "๐ŸŽค", + "microscope": "๐Ÿ”ฌ", + "middle_finger": "๐Ÿ–•", + "military_helmet": "๐Ÿช–", + "milk_glass": "๐Ÿฅ›", + "milky_way": "๐ŸŒŒ", + "minibus": "๐Ÿš", + "minidisc": "๐Ÿ’ฝ", + "mirror": "๐Ÿชž", + "mirror_ball": "๐Ÿชฉ", + "mobile_phone_off": "๐Ÿ“ด", + "moldova": "๐Ÿ‡ฒ๐Ÿ‡ฉ", + "monaco": "๐Ÿ‡ฒ๐Ÿ‡จ", + "money_mouth_face": "๐Ÿค‘", + "money_with_wings": "๐Ÿ’ธ", + "moneybag": "๐Ÿ’ฐ", + "mongolia": "๐Ÿ‡ฒ๐Ÿ‡ณ", + "monkey": "๐Ÿ’", + "monkey_face": "๐Ÿต", + "monocle_face": "๐Ÿง", + "monorail": "๐Ÿš", + "montenegro": "๐Ÿ‡ฒ๐Ÿ‡ช", + "montserrat": "๐Ÿ‡ฒ๐Ÿ‡ธ", + "moon": "๐ŸŒ”", + "moon_cake": "๐Ÿฅฎ", + "moose": "๐ŸซŽ", + "morocco": "๐Ÿ‡ฒ๐Ÿ‡ฆ", + "mortar_board": "๐ŸŽ“", + "mosque": "๐Ÿ•Œ", + "mosquito": "๐ŸฆŸ", + "motor_boat": "๐Ÿ›ฅ๏ธ", + "motor_scooter": "๐Ÿ›ต", + "motorcycle": "๐Ÿ๏ธ", + "motorized_wheelchair": "๐Ÿฆผ", + "motorway": "๐Ÿ›ฃ๏ธ", + "mount_fuji": "๐Ÿ—ป", + "mountain": "โ›ฐ๏ธ", + "mountain_bicyclist": "๐Ÿšต", + "mountain_biking_man": "๐Ÿšตโ€โ™‚๏ธ", + "mountain_biking_woman": "๐Ÿšตโ€โ™€๏ธ", + "mountain_cableway": "๐Ÿš ", + "mountain_railway": "๐Ÿšž", + "mountain_snow": "๐Ÿ”๏ธ", + "mouse": "๐Ÿญ", + "mouse2": "๐Ÿ", + "mouse_trap": "๐Ÿชค", + "movie_camera": "๐ŸŽฅ", + "moyai": "๐Ÿ—ฟ", + "mozambique": "๐Ÿ‡ฒ๐Ÿ‡ฟ", + "mrs_claus": "๐Ÿคถ", + "muscle": "๐Ÿ’ช", + "mushroom": "๐Ÿ„", + "musical_keyboard": "๐ŸŽน", + "musical_note": "๐ŸŽต", + "musical_score": "๐ŸŽผ", + "mute": "๐Ÿ”‡", + "mx_claus": "๐Ÿง‘โ€๐ŸŽ„", + "myanmar": "๐Ÿ‡ฒ๐Ÿ‡ฒ", + "nail_care": "๐Ÿ’…", + "name_badge": "๐Ÿ“›", + "namibia": "๐Ÿ‡ณ๐Ÿ‡ฆ", + "national_park": "๐Ÿž๏ธ", + "nauru": "๐Ÿ‡ณ๐Ÿ‡ท", + "nauseated_face": "๐Ÿคข", + "nazar_amulet": "๐Ÿงฟ", + "necktie": "๐Ÿ‘”", + "negative_squared_cross_mark": "โŽ", + "nepal": "๐Ÿ‡ณ๐Ÿ‡ต", + "nerd_face": "๐Ÿค“", + "nest_with_eggs": "๐Ÿชบ", + "nesting_dolls": "๐Ÿช†", + "netherlands": "๐Ÿ‡ณ๐Ÿ‡ฑ", + "neutral_face": "๐Ÿ˜", + "new": "๐Ÿ†•", + "new_caledonia": "๐Ÿ‡ณ๐Ÿ‡จ", + "new_moon": "๐ŸŒ‘", + "new_moon_with_face": "๐ŸŒš", + "new_zealand": "๐Ÿ‡ณ๐Ÿ‡ฟ", + "newspaper": "๐Ÿ“ฐ", + "newspaper_roll": "๐Ÿ—ž๏ธ", + "next_track_button": "โญ๏ธ", + "ng": "๐Ÿ†–", + "ng_man": "๐Ÿ™…โ€โ™‚๏ธ", + "ng_woman": "๐Ÿ™…โ€โ™€๏ธ", + "nicaragua": "๐Ÿ‡ณ๐Ÿ‡ฎ", + "niger": "๐Ÿ‡ณ๐Ÿ‡ช", + "nigeria": "๐Ÿ‡ณ๐Ÿ‡ฌ", + "night_with_stars": "๐ŸŒƒ", + "nine": "9๏ธโƒฃ", + "ninja": "๐Ÿฅท", + "niue": "๐Ÿ‡ณ๐Ÿ‡บ", + "no_bell": "๐Ÿ”•", + "no_bicycles": "๐Ÿšณ", + "no_entry": "โ›”", + "no_entry_sign": "๐Ÿšซ", + "no_good": "๐Ÿ™…", + "no_good_man": "๐Ÿ™…โ€โ™‚๏ธ", + "no_good_woman": "๐Ÿ™…โ€โ™€๏ธ", + "no_mobile_phones": "๐Ÿ“ต", + "no_mouth": "๐Ÿ˜ถ", + "no_pedestrians": "๐Ÿšท", + "no_smoking": "๐Ÿšญ", + "non-potable_water": "๐Ÿšฑ", + "norfolk_island": "๐Ÿ‡ณ๐Ÿ‡ซ", + "north_korea": "๐Ÿ‡ฐ๐Ÿ‡ต", + "northern_mariana_islands": "๐Ÿ‡ฒ๐Ÿ‡ต", + "norway": "๐Ÿ‡ณ๐Ÿ‡ด", + "nose": "๐Ÿ‘ƒ", + "notebook": "๐Ÿ““", + "notebook_with_decorative_cover": "๐Ÿ“”", + "notes": "๐ŸŽถ", + "nut_and_bolt": "๐Ÿ”ฉ", + "o": "โญ•", + "o2": "๐Ÿ…พ๏ธ", + "ocean": "๐ŸŒŠ", + "octopus": "๐Ÿ™", + "oden": "๐Ÿข", + "office": "๐Ÿข", + "office_worker": "๐Ÿง‘โ€๐Ÿ’ผ", + "oil_drum": "๐Ÿ›ข๏ธ", + "ok": "๐Ÿ†—", + "ok_hand": "๐Ÿ‘Œ", + "ok_man": "๐Ÿ™†โ€โ™‚๏ธ", + "ok_person": "๐Ÿ™†", + "ok_woman": "๐Ÿ™†โ€โ™€๏ธ", + "old_key": "๐Ÿ—๏ธ", + "older_adult": "๐Ÿง“", + "older_man": "๐Ÿ‘ด", + "older_woman": "๐Ÿ‘ต", + "olive": "๐Ÿซ’", + "om": "๐Ÿ•‰๏ธ", + "oman": "๐Ÿ‡ด๐Ÿ‡ฒ", + "on": "๐Ÿ”›", + "oncoming_automobile": "๐Ÿš˜", + "oncoming_bus": "๐Ÿš", + "oncoming_police_car": "๐Ÿš”", + "oncoming_taxi": "๐Ÿš–", + "one": "1๏ธโƒฃ", + "one_piece_swimsuit": "๐Ÿฉฑ", + "onion": "๐Ÿง…", + "open_book": "๐Ÿ“–", + "open_file_folder": "๐Ÿ“‚", + "open_hands": "๐Ÿ‘", + "open_mouth": "๐Ÿ˜ฎ", + "open_umbrella": "โ˜‚๏ธ", + "ophiuchus": "โ›Ž", + "orange": "๐ŸŠ", + "orange_book": "๐Ÿ“™", + "orange_circle": "๐ŸŸ ", + "orange_heart": "๐Ÿงก", + "orange_square": "๐ŸŸง", + "orangutan": "๐Ÿฆง", + "orthodox_cross": "โ˜ฆ๏ธ", + "otter": "๐Ÿฆฆ", + "outbox_tray": "๐Ÿ“ค", + "owl": "๐Ÿฆ‰", + "ox": "๐Ÿ‚", + "oyster": "๐Ÿฆช", + "package": "๐Ÿ“ฆ", + "page_facing_up": "๐Ÿ“„", + "page_with_curl": "๐Ÿ“ƒ", + "pager": "๐Ÿ“Ÿ", + "paintbrush": "๐Ÿ–Œ๏ธ", + "pakistan": "๐Ÿ‡ต๐Ÿ‡ฐ", + "palau": "๐Ÿ‡ต๐Ÿ‡ผ", + "palestinian_territories": "๐Ÿ‡ต๐Ÿ‡ธ", + "palm_down_hand": "๐Ÿซณ", + "palm_tree": "๐ŸŒด", + "palm_up_hand": "๐Ÿซด", + "palms_up_together": "๐Ÿคฒ", + "panama": "๐Ÿ‡ต๐Ÿ‡ฆ", + "pancakes": "๐Ÿฅž", + "panda_face": "๐Ÿผ", + "paperclip": "๐Ÿ“Ž", + "paperclips": "๐Ÿ–‡๏ธ", + "papua_new_guinea": "๐Ÿ‡ต๐Ÿ‡ฌ", + "parachute": "๐Ÿช‚", + "paraguay": "๐Ÿ‡ต๐Ÿ‡พ", + "parasol_on_ground": "โ›ฑ๏ธ", + "parking": "๐Ÿ…ฟ๏ธ", + "parrot": "๐Ÿฆœ", + "part_alternation_mark": "ใ€ฝ๏ธ", + "partly_sunny": "โ›…", + "party": "๐ŸŽ‰", + "partying_face": "๐Ÿฅณ", + "passenger_ship": "๐Ÿ›ณ๏ธ", + "passport_control": "๐Ÿ›‚", + "pause_button": "โธ๏ธ", + "paw_prints": "๐Ÿพ", + "pea_pod": "๐Ÿซ›", + "peace_symbol": "โ˜ฎ๏ธ", + "peach": "๐Ÿ‘", + "peacock": "๐Ÿฆš", + "peanuts": "๐Ÿฅœ", + "pear": "๐Ÿ", + "pen": "๐Ÿ–Š๏ธ", + "pencil": "๐Ÿ“", + "pencil2": "โœ๏ธ", + "penguin": "๐Ÿง", + "pensive": "๐Ÿ˜”", + "people_holding_hands": "๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘", + "people_hugging": "๐Ÿซ‚", + "performing_arts": "๐ŸŽญ", + "persevere": "๐Ÿ˜ฃ", + "person_bald": "๐Ÿง‘โ€๐Ÿฆฒ", + "person_curly_hair": "๐Ÿง‘โ€๐Ÿฆฑ", + "person_feeding_baby": "๐Ÿง‘โ€๐Ÿผ", + "person_fencing": "๐Ÿคบ", + "person_in_manual_wheelchair": "๐Ÿง‘โ€๐Ÿฆฝ", + "person_in_motorized_wheelchair": "๐Ÿง‘โ€๐Ÿฆผ", + "person_in_tuxedo": "๐Ÿคต", + "person_red_hair": "๐Ÿง‘โ€๐Ÿฆฐ", + "person_white_hair": "๐Ÿง‘โ€๐Ÿฆณ", + "person_with_crown": "๐Ÿซ…", + "person_with_probing_cane": "๐Ÿง‘โ€๐Ÿฆฏ", + "person_with_turban": "๐Ÿ‘ณ", + "person_with_veil": "๐Ÿ‘ฐ", + "peru": "๐Ÿ‡ต๐Ÿ‡ช", + "petri_dish": "๐Ÿงซ", + "philippines": "๐Ÿ‡ต๐Ÿ‡ญ", + "phone": "โ˜Ž๏ธ", + "pick": "โ›๏ธ", + "pickup_truck": "๐Ÿ›ป", + "pie": "๐Ÿฅง", + "pig": "๐Ÿท", + "pig2": "๐Ÿ–", + "pig_nose": "๐Ÿฝ", + "pill": "๐Ÿ’Š", + "pilot": "๐Ÿง‘โ€โœˆ๏ธ", + "pinata": "๐Ÿช…", + "pinched_fingers": "๐ŸคŒ", + "pinching_hand": "๐Ÿค", + "pineapple": "๐Ÿ", + "ping_pong": "๐Ÿ“", + "pink_heart": "๐Ÿฉท", + "pirate_flag": "๐Ÿดโ€โ˜ ๏ธ", + "pisces": "โ™“", + "pitcairn_islands": "๐Ÿ‡ต๐Ÿ‡ณ", + "pizza": "๐Ÿ•", + "placard": "๐Ÿชง", + "place_of_worship": "๐Ÿ›", + "plate_with_cutlery": "๐Ÿฝ๏ธ", + "play_or_pause_button": "โฏ๏ธ", + "playground_slide": "๐Ÿ›", + "pleading_face": "๐Ÿฅบ", + "plunger": "๐Ÿช ", + "point_down": "๐Ÿ‘‡", + "point_left": "๐Ÿ‘ˆ", + "point_right": "๐Ÿ‘‰", + "point_up": "โ˜๏ธ", + "point_up_2": "๐Ÿ‘†", + "poland": "๐Ÿ‡ต๐Ÿ‡ฑ", + "polar_bear": "๐Ÿปโ€โ„๏ธ", + "police_car": "๐Ÿš“", + "police_officer": "๐Ÿ‘ฎ", + "policeman": "๐Ÿ‘ฎโ€โ™‚๏ธ", + "policewoman": "๐Ÿ‘ฎโ€โ™€๏ธ", + "poodle": "๐Ÿฉ", + "poop": "๐Ÿ’ฉ", + "popcorn": "๐Ÿฟ", + "portugal": "๐Ÿ‡ต๐Ÿ‡น", + "post_office": "๐Ÿฃ", + "postal_horn": "๐Ÿ“ฏ", + "postbox": "๐Ÿ“ฎ", + "potable_water": "๐Ÿšฐ", + "potato": "๐Ÿฅ”", + "potted_plant": "๐Ÿชด", + "pouch": "๐Ÿ‘", + "poultry_leg": "๐Ÿ—", + "pound": "๐Ÿ’ท", + "pouring_liquid": "๐Ÿซ—", + "pout": "๐Ÿ˜ก", + "pouting_cat": "๐Ÿ˜พ", + "pouting_face": "๐Ÿ™Ž", + "pouting_man": "๐Ÿ™Žโ€โ™‚๏ธ", + "pouting_woman": "๐Ÿ™Žโ€โ™€๏ธ", + "pray": "๐Ÿ™", + "prayer_beads": "๐Ÿ“ฟ", + "pregnant_man": "๐Ÿซƒ", + "pregnant_person": "๐Ÿซ„", + "pregnant_woman": "๐Ÿคฐ", + "pretzel": "๐Ÿฅจ", + "previous_track_button": "โฎ๏ธ", + "prince": "๐Ÿคด", + "princess": "๐Ÿ‘ธ", + "printer": "๐Ÿ–จ๏ธ", + "probing_cane": "๐Ÿฆฏ", + "puerto_rico": "๐Ÿ‡ต๐Ÿ‡ท", + "puke": "๐Ÿคฎ", + "punch": "๐Ÿ‘Š", + "purple_circle": "๐ŸŸฃ", + "purple_heart": "๐Ÿ’œ", + "purple_square": "๐ŸŸช", + "purse": "๐Ÿ‘›", + "pushpin": "๐Ÿ“Œ", + "put_litter_in_its_place": "๐Ÿšฎ", + "qatar": "๐Ÿ‡ถ๐Ÿ‡ฆ", + "question": "โ“", + "rabbit": "๐Ÿฐ", + "rabbit2": "๐Ÿ‡", + "raccoon": "๐Ÿฆ", + "racehorse": "๐ŸŽ", + "racing_car": "๐ŸŽ๏ธ", + "radio": "๐Ÿ“ป", + "radio_button": "๐Ÿ”˜", + "radioactive": "โ˜ข๏ธ", + "rage": "๐Ÿ˜ก", + "railway_car": "๐Ÿšƒ", + "railway_track": "๐Ÿ›ค๏ธ", + "rainbow": "๐ŸŒˆ", + "rainbow_flag": "๐Ÿณ๏ธโ€๐ŸŒˆ", + "raised_back_of_hand": "๐Ÿคš", + "raised_eyebrow": "๐Ÿคจ", + "raised_hand": "โœ‹", + "raised_hand_with_fingers_splayed": "๐Ÿ–๏ธ", + "raised_hands": "๐Ÿ™Œ", + "raising_hand": "๐Ÿ™‹", + "raising_hand_man": "๐Ÿ™‹โ€โ™‚๏ธ", + "raising_hand_woman": "๐Ÿ™‹โ€โ™€๏ธ", + "ram": "๐Ÿ", + "ramen": "๐Ÿœ", + "rat": "๐Ÿ€", + "razor": "๐Ÿช’", + "receipt": "๐Ÿงพ", + "record_button": "โบ๏ธ", + "recycle": "โ™ป๏ธ", + "red_car": "๐Ÿš—", + "red_circle": "๐Ÿ”ด", + "red_envelope": "๐Ÿงง", + "red_haired_man": "๐Ÿ‘จโ€๐Ÿฆฐ", + "red_haired_woman": "๐Ÿ‘ฉโ€๐Ÿฆฐ", + "red_square": "๐ŸŸฅ", + "registered": "ยฎ๏ธ", + "relaxed": "โ˜บ๏ธ", + "relieved": "๐Ÿ˜Œ", + "reminder_ribbon": "๐ŸŽ—๏ธ", + "repeat": "๐Ÿ”", + "repeat_one": "๐Ÿ”‚", + "rescue_worker_helmet": "โ›‘๏ธ", + "restroom": "๐Ÿšป", + "reunion": "๐Ÿ‡ท๐Ÿ‡ช", + "revolving_hearts": "๐Ÿ’ž", + "rewind": "โช", + "rhinoceros": "๐Ÿฆ", + "ribbon": "๐ŸŽ€", + "rice": "๐Ÿš", + "rice_ball": "๐Ÿ™", + "rice_cracker": "๐Ÿ˜", + "rice_scene": "๐ŸŽ‘", + "right_anger_bubble": "๐Ÿ—ฏ๏ธ", + "rightwards_hand": "๐Ÿซฑ", + "rightwards_pushing_hand": "๐Ÿซธ", + "ring": "๐Ÿ’", + "ring_buoy": "๐Ÿ›Ÿ", + "ringed_planet": "๐Ÿช", + "robot": "๐Ÿค–", + "rock": "๐Ÿชจ", + "rocket": "๐Ÿš€", + "rofl": "๐Ÿคฃ", + "roll_eyes": "๐Ÿ™„", + "roll_of_paper": "๐Ÿงป", + "roller_coaster": "๐ŸŽข", + "roller_skate": "๐Ÿ›ผ", + "romania": "๐Ÿ‡ท๐Ÿ‡ด", + "rooster": "๐Ÿ“", + "rose": "๐ŸŒน", + "rosette": "๐Ÿต๏ธ", + "rotating_light": "๐Ÿšจ", + "round_pushpin": "๐Ÿ“", + "rowboat": "๐Ÿšฃ", + "rowing_man": "๐Ÿšฃโ€โ™‚๏ธ", + "rowing_woman": "๐Ÿšฃโ€โ™€๏ธ", + "ru": "๐Ÿ‡ท๐Ÿ‡บ", + "rugby_football": "๐Ÿ‰", + "runner": "๐Ÿƒ", + "running": "๐Ÿƒ", + "running_man": "๐Ÿƒโ€โ™‚๏ธ", + "running_shirt_with_sash": "๐ŸŽฝ", + "running_woman": "๐Ÿƒโ€โ™€๏ธ", + "rwanda": "๐Ÿ‡ท๐Ÿ‡ผ", + "sa": "๐Ÿˆ‚๏ธ", + "sad": "๐Ÿ˜ข", + "safety_pin": "๐Ÿงท", + "safety_vest": "๐Ÿฆบ", + "sagittarius": "โ™", + "sailboat": "โ›ต", + "sake": "๐Ÿถ", + "salt": "๐Ÿง‚", + "saluting_face": "๐Ÿซก", + "samoa": "๐Ÿ‡ผ๐Ÿ‡ธ", + "san_marino": "๐Ÿ‡ธ๐Ÿ‡ฒ", + "sandal": "๐Ÿ‘ก", + "sandwich": "๐Ÿฅช", + "santa": "๐ŸŽ…", + "sao_tome_principe": "๐Ÿ‡ธ๐Ÿ‡น", + "sari": "๐Ÿฅป", + "sassy_man": "๐Ÿ’โ€โ™‚๏ธ", + "sassy_woman": "๐Ÿ’โ€โ™€๏ธ", + "satellite": "๐Ÿ“ก", + "satisfied": "๐Ÿ˜†", + "saudi_arabia": "๐Ÿ‡ธ๐Ÿ‡ฆ", + "sauna_man": "๐Ÿง–โ€โ™‚๏ธ", + "sauna_person": "๐Ÿง–", + "sauna_woman": "๐Ÿง–โ€โ™€๏ธ", + "sauropod": "๐Ÿฆ•", + "saxophone": "๐ŸŽท", + "scarf": "๐Ÿงฃ", + "school": "๐Ÿซ", + "school_satchel": "๐ŸŽ’", + "scientist": "๐Ÿง‘โ€๐Ÿ”ฌ", + "scissors": "โœ‚๏ธ", + "scorpion": "๐Ÿฆ‚", + "scorpius": "โ™", + "scotland": "๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ", + "scream": "๐Ÿ˜ฑ", + "scream_cat": "๐Ÿ™€", + "screwdriver": "๐Ÿช›", + "scroll": "๐Ÿ“œ", + "seal": "๐Ÿฆญ", + "seat": "๐Ÿ’บ", + "secret": "ใŠ™๏ธ", + "see_no_evil": "๐Ÿ™ˆ", + "seedling": "๐ŸŒฑ", + "selfie": "๐Ÿคณ", + "senegal": "๐Ÿ‡ธ๐Ÿ‡ณ", + "serbia": "๐Ÿ‡ท๐Ÿ‡ธ", + "service_dog": "๐Ÿ•โ€๐Ÿฆบ", + "seven": "7๏ธโƒฃ", + "sewing_needle": "๐Ÿชก", + "seychelles": "๐Ÿ‡ธ๐Ÿ‡จ", + "shaking_face": "๐Ÿซจ", + "shallow_pan_of_food": "๐Ÿฅ˜", + "shamrock": "โ˜˜๏ธ", + "shark": "๐Ÿฆˆ", + "shaved_ice": "๐Ÿง", + "sheep": "๐Ÿ‘", + "shell": "๐Ÿš", + "shield": "๐Ÿ›ก๏ธ", + "shinto_shrine": "โ›ฉ๏ธ", + "ship": "๐Ÿšข", + "shirt": "๐Ÿ‘•", + "shit": "๐Ÿ’ฉ", + "shoe": "๐Ÿ‘ž", + "shopping": "๐Ÿ›๏ธ", + "shopping_cart": "๐Ÿ›’", + "shorts": "๐Ÿฉณ", + "shower": "๐Ÿšฟ", + "shrimp": "๐Ÿฆ", + "shrug": "๐Ÿคท", + "shushing_face": "๐Ÿคซ", + "sierra_leone": "๐Ÿ‡ธ๐Ÿ‡ฑ", + "signal_strength": "๐Ÿ“ถ", + "singapore": "๐Ÿ‡ธ๐Ÿ‡ฌ", + "singer": "๐Ÿง‘โ€๐ŸŽค", + "sint_maarten": "๐Ÿ‡ธ๐Ÿ‡ฝ", + "six": "6๏ธโƒฃ", + "six_pointed_star": "๐Ÿ”ฏ", + "skateboard": "๐Ÿ›น", + "ski": "๐ŸŽฟ", + "skier": "โ›ท๏ธ", + "skull": "๐Ÿ’€", + "skull_and_crossbones": "โ˜ ๏ธ", + "skunk": "๐Ÿฆจ", + "sled": "๐Ÿ›ท", + "sleeping": "๐Ÿ˜ด", + "sleeping_bed": "๐Ÿ›Œ", + "sleepy": "๐Ÿ˜ช", + "slightly_frowning_face": "๐Ÿ™", + "slightly_smiling_face": "๐Ÿ™‚", + "slot_machine": "๐ŸŽฐ", + "sloth": "๐Ÿฆฅ", + "slovakia": "๐Ÿ‡ธ๐Ÿ‡ฐ", + "slovenia": "๐Ÿ‡ธ๐Ÿ‡ฎ", + "small_airplane": "๐Ÿ›ฉ๏ธ", + "small_blue_diamond": "๐Ÿ”น", + "small_orange_diamond": "๐Ÿ”ธ", + "small_red_triangle": "๐Ÿ”บ", + "small_red_triangle_down": "๐Ÿ”ป", + "smile": "๐Ÿ˜„", + "smile_cat": "๐Ÿ˜ธ", + "smiley": "๐Ÿ˜ƒ", + "smiley_cat": "๐Ÿ˜บ", + "smiling_face_with_tear": "๐Ÿฅฒ", + "smiling_face_with_three_hearts": "๐Ÿฅฐ", + "smiling_imp": "๐Ÿ˜ˆ", + "smirk": "๐Ÿ˜", + "smirk_cat": "๐Ÿ˜ผ", + "smoking": "๐Ÿšฌ", + "snail": "๐ŸŒ", + "snake": "๐Ÿ", + "sneezing_face": "๐Ÿคง", + "snowboarder": "๐Ÿ‚", + "snowflake": "โ„๏ธ", + "snowman": "โ›„", + "snowman_with_snow": "โ˜ƒ๏ธ", + "soap": "๐Ÿงผ", + "sob": "๐Ÿ˜ญ", + "soccer": "โšฝ", + "socks": "๐Ÿงฆ", + "softball": "๐ŸฅŽ", + "solomon_islands": "๐Ÿ‡ธ๐Ÿ‡ง", + "somalia": "๐Ÿ‡ธ๐Ÿ‡ด", + "soon": "๐Ÿ”œ", + "sos": "๐Ÿ†˜", + "sound": "๐Ÿ”‰", + "south_africa": "๐Ÿ‡ฟ๐Ÿ‡ฆ", + "south_georgia_south_sandwich_islands": "๐Ÿ‡ฌ๐Ÿ‡ธ", + "south_sudan": "๐Ÿ‡ธ๐Ÿ‡ธ", + "space_invader": "๐Ÿ‘พ", + "spades": "โ™ ๏ธ", + "spaghetti": "๐Ÿ", + "sparkle": "โ‡๏ธ", + "sparkler": "๐ŸŽ‡", + "sparkles": "โœจ", + "sparkling_heart": "๐Ÿ’–", + "speak_no_evil": "๐Ÿ™Š", + "speaker": "๐Ÿ”ˆ", + "speaking_head": "๐Ÿ—ฃ๏ธ", + "speech_balloon": "๐Ÿ’ฌ", + "speedboat": "๐Ÿšค", + "spider": "๐Ÿ•ท๏ธ", + "spider_web": "๐Ÿ•ธ๏ธ", + "spiral_calendar": "๐Ÿ—“๏ธ", + "spiral_notepad": "๐Ÿ—’๏ธ", + "sponge": "๐Ÿงฝ", + "spoon": "๐Ÿฅ„", + "squid": "๐Ÿฆ‘", + "sri_lanka": "๐Ÿ‡ฑ๐Ÿ‡ฐ", + "st_barthelemy": "๐Ÿ‡ง๐Ÿ‡ฑ", + "st_helena": "๐Ÿ‡ธ๐Ÿ‡ญ", + "st_kitts_nevis": "๐Ÿ‡ฐ๐Ÿ‡ณ", + "st_lucia": "๐Ÿ‡ฑ๐Ÿ‡จ", + "st_martin": "๐Ÿ‡ฒ๐Ÿ‡ซ", + "st_pierre_miquelon": "๐Ÿ‡ต๐Ÿ‡ฒ", + "st_vincent_grenadines": "๐Ÿ‡ป๐Ÿ‡จ", + "stadium": "๐ŸŸ๏ธ", + "standing_man": "๐Ÿงโ€โ™‚๏ธ", + "standing_person": "๐Ÿง", + "standing_woman": "๐Ÿงโ€โ™€๏ธ", + "star": "โญ", + "star2": "๐ŸŒŸ", + "star_and_crescent": "โ˜ช๏ธ", + "star_of_david": "โœก๏ธ", + "star_struck": "๐Ÿคฉ", + "stars": "๐ŸŒ ", + "station": "๐Ÿš‰", + "statue_of_liberty": "๐Ÿ—ฝ", + "steam_locomotive": "๐Ÿš‚", + "stethoscope": "๐Ÿฉบ", + "stew": "๐Ÿฒ", + "stop_button": "โน๏ธ", + "stop_sign": "๐Ÿ›‘", + "stopwatch": "โฑ๏ธ", + "straight_ruler": "๐Ÿ“", + "strawberry": "๐Ÿ“", + "stuck_out_tongue": "๐Ÿ˜›", + "stuck_out_tongue_closed_eyes": "๐Ÿ˜", + "stuck_out_tongue_winking_eye": "๐Ÿ˜œ", + "student": "๐Ÿง‘โ€๐ŸŽ“", + "studio_microphone": "๐ŸŽ™๏ธ", + "stuffed_flatbread": "๐Ÿฅ™", + "sudan": "๐Ÿ‡ธ๐Ÿ‡ฉ", + "sun_behind_large_cloud": "๐ŸŒฅ๏ธ", + "sun_behind_rain_cloud": "๐ŸŒฆ๏ธ", + "sun_behind_small_cloud": "๐ŸŒค๏ธ", + "sun_with_face": "๐ŸŒž", + "sunflower": "๐ŸŒป", + "sunglasses": "๐Ÿ˜Ž", + "sunny": "โ˜€๏ธ", + "sunrise": "๐ŸŒ…", + "sunrise_over_mountains": "๐ŸŒ„", + "superhero": "๐Ÿฆธ", + "superhero_man": "๐Ÿฆธโ€โ™‚๏ธ", + "superhero_woman": "๐Ÿฆธโ€โ™€๏ธ", + "supervillain": "๐Ÿฆน", + "supervillain_man": "๐Ÿฆนโ€โ™‚๏ธ", + "supervillain_woman": "๐Ÿฆนโ€โ™€๏ธ", + "surfer": "๐Ÿ„", + "surfing_man": "๐Ÿ„โ€โ™‚๏ธ", + "surfing_woman": "๐Ÿ„โ€โ™€๏ธ", + "suriname": "๐Ÿ‡ธ๐Ÿ‡ท", + "sushi": "๐Ÿฃ", + "suspension_railway": "๐ŸšŸ", + "svalbard_jan_mayen": "๐Ÿ‡ธ๐Ÿ‡ฏ", + "swan": "๐Ÿฆข", + "swaziland": "๐Ÿ‡ธ๐Ÿ‡ฟ", + "sweat": "๐Ÿ˜“", + "sweat_drops": "๐Ÿ’ฆ", + "sweat_smile": "๐Ÿ˜…", + "sweden": "๐Ÿ‡ธ๐Ÿ‡ช", + "sweet_potato": "๐Ÿ ", + "swim_brief": "๐Ÿฉฒ", + "swimmer": "๐ŸŠ", + "swimming_man": "๐ŸŠโ€โ™‚๏ธ", + "swimming_woman": "๐ŸŠโ€โ™€๏ธ", + "switzerland": "๐Ÿ‡จ๐Ÿ‡ญ", + "symbols": "๐Ÿ”ฃ", + "synagogue": "๐Ÿ•", + "syria": "๐Ÿ‡ธ๐Ÿ‡พ", + "syringe": "๐Ÿ’‰", + "t-rex": "๐Ÿฆ–", + "taco": "๐ŸŒฎ", + "tada": "๐ŸŽ‰", + "taiwan": "๐Ÿ‡น๐Ÿ‡ผ", + "tajikistan": "๐Ÿ‡น๐Ÿ‡ฏ", + "takeout_box": "๐Ÿฅก", + "tamale": "๐Ÿซ”", + "tanabata_tree": "๐ŸŽ‹", + "tangerine": "๐ŸŠ", + "tanzania": "๐Ÿ‡น๐Ÿ‡ฟ", + "taurus": "โ™‰", + "taxi": "๐Ÿš•", + "tea": "๐Ÿต", + "teacher": "๐Ÿง‘โ€๐Ÿซ", + "teapot": "๐Ÿซ–", + "technologist": "๐Ÿง‘โ€๐Ÿ’ป", + "teddy_bear": "๐Ÿงธ", + "telephone": "โ˜Ž๏ธ", + "telephone_receiver": "๐Ÿ“ž", + "telescope": "๐Ÿ”ญ", + "tennis": "๐ŸŽพ", + "tent": "โ›บ", + "test_tube": "๐Ÿงช", + "thailand": "๐Ÿ‡น๐Ÿ‡ญ", + "thermometer": "๐ŸŒก๏ธ", + "thinking": "๐Ÿค”", + "thong_sandal": "๐Ÿฉด", + "thought_balloon": "๐Ÿ’ญ", + "thread": "๐Ÿงต", + "three": "3๏ธโƒฃ", + "thumbsdown": "๐Ÿ‘Ž", + "thumbsup": "๐Ÿ‘", + "ticket": "๐ŸŽซ", + "tickets": "๐ŸŽŸ๏ธ", + "tiger": "๐Ÿฏ", + "tiger2": "๐Ÿ…", + "timer_clock": "โฒ๏ธ", + "timor_leste": "๐Ÿ‡น๐Ÿ‡ฑ", + "tipping_hand_man": "๐Ÿ’โ€โ™‚๏ธ", + "tipping_hand_person": "๐Ÿ’", + "tipping_hand_woman": "๐Ÿ’โ€โ™€๏ธ", + "tired_face": "๐Ÿ˜ซ", + "tm": "โ„ข๏ธ", + "togo": "๐Ÿ‡น๐Ÿ‡ฌ", + "toilet": "๐Ÿšฝ", + "tokelau": "๐Ÿ‡น๐Ÿ‡ฐ", + "tokyo_tower": "๐Ÿ—ผ", + "tomato": "๐Ÿ…", + "tonga": "๐Ÿ‡น๐Ÿ‡ด", + "tongue": "๐Ÿ‘…", + "toolbox": "๐Ÿงฐ", + "tooth": "๐Ÿฆท", + "toothbrush": "๐Ÿชฅ", + "top": "๐Ÿ”", + "tophat": "๐ŸŽฉ", + "tornado": "๐ŸŒช๏ธ", + "tr": "๐Ÿ‡น๐Ÿ‡ท", + "trackball": "๐Ÿ–ฒ๏ธ", + "tractor": "๐Ÿšœ", + "traffic_light": "๐Ÿšฅ", + "train": "๐Ÿš‹", + "train2": "๐Ÿš†", + "tram": "๐ŸšŠ", + "transgender_flag": "๐Ÿณ๏ธโ€โšง๏ธ", + "transgender_symbol": "โšง๏ธ", + "triangular_flag_on_post": "๐Ÿšฉ", + "triangular_ruler": "๐Ÿ“", + "trident": "๐Ÿ”ฑ", + "trinidad_tobago": "๐Ÿ‡น๐Ÿ‡น", + "tristan_da_cunha": "๐Ÿ‡น๐Ÿ‡ฆ", + "triumph": "๐Ÿ˜ค", + "troll": "๐ŸงŒ", + "trolleybus": "๐ŸšŽ", + "trophy": "๐Ÿ†", + "tropical_drink": "๐Ÿน", + "tropical_fish": "๐Ÿ ", + "truck": "๐Ÿšš", + "trumpet": "๐ŸŽบ", + "tshirt": "๐Ÿ‘•", + "tulip": "๐ŸŒท", + "tumbler_glass": "๐Ÿฅƒ", + "tunisia": "๐Ÿ‡น๐Ÿ‡ณ", + "turkey": "๐Ÿฆƒ", + "turkmenistan": "๐Ÿ‡น๐Ÿ‡ฒ", + "turks_caicos_islands": "๐Ÿ‡น๐Ÿ‡จ", + "turtle": "๐Ÿข", + "tuvalu": "๐Ÿ‡น๐Ÿ‡ป", + "tv": "๐Ÿ“บ", + "twisted_rightwards_arrows": "๐Ÿ”€", + "two": "2๏ธโƒฃ", + "two_hearts": "๐Ÿ’•", + "two_men_holding_hands": "๐Ÿ‘ฌ", + "two_women_holding_hands": "๐Ÿ‘ญ", + "u5272": "๐Ÿˆน", + "u5408": "๐Ÿˆด", + "u55b6": "๐Ÿˆบ", + "u6307": "๐Ÿˆฏ", + "u6708": "๐Ÿˆท๏ธ", + "u6709": "๐Ÿˆถ", + "u6e80": "๐Ÿˆต", + "u7121": "๐Ÿˆš", + "u7533": "๐Ÿˆธ", + "u7981": "๐Ÿˆฒ", + "u7a7a": "๐Ÿˆณ", + "uganda": "๐Ÿ‡บ๐Ÿ‡ฌ", + "uk": "๐Ÿ‡ฌ๐Ÿ‡ง", + "ukraine": "๐Ÿ‡บ๐Ÿ‡ฆ", + "umbrella": "โ˜”", + "unamused": "๐Ÿ˜’", + "underage": "๐Ÿ”ž", + "unicorn": "๐Ÿฆ„", + "united_arab_emirates": "๐Ÿ‡ฆ๐Ÿ‡ช", + "united_nations": "๐Ÿ‡บ๐Ÿ‡ณ", + "unlock": "๐Ÿ”“", + "up": "๐Ÿ†™", + "upside_down_face": "๐Ÿ™ƒ", + "uruguay": "๐Ÿ‡บ๐Ÿ‡พ", + "us": "๐Ÿ‡บ๐Ÿ‡ธ", + "us_outlying_islands": "๐Ÿ‡บ๐Ÿ‡ฒ", + "us_virgin_islands": "๐Ÿ‡ป๐Ÿ‡ฎ", + "uzbekistan": "๐Ÿ‡บ๐Ÿ‡ฟ", + "v": "โœŒ๏ธ", + "vampire": "๐Ÿง›", + "vampire_man": "๐Ÿง›โ€โ™‚๏ธ", + "vampire_woman": "๐Ÿง›โ€โ™€๏ธ", + "vanuatu": "๐Ÿ‡ป๐Ÿ‡บ", + "vatican_city": "๐Ÿ‡ป๐Ÿ‡ฆ", + "venezuela": "๐Ÿ‡ป๐Ÿ‡ช", + "vertical_traffic_light": "๐Ÿšฆ", + "vhs": "๐Ÿ“ผ", + "vibration_mode": "๐Ÿ“ณ", + "video_camera": "๐Ÿ“น", + "video_game": "๐ŸŽฎ", + "vietnam": "๐Ÿ‡ป๐Ÿ‡ณ", + "violin": "๐ŸŽป", + "virgo": "โ™", + "volcano": "๐ŸŒ‹", + "volleyball": "๐Ÿ", + "vomiting_face": "๐Ÿคฎ", + "vs": "๐Ÿ†š", + "vulcan_salute": "๐Ÿ––", + "waffle": "๐Ÿง‡", + "wales": "๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ", + "walking": "๐Ÿšถ", + "walking_man": "๐Ÿšถโ€โ™‚๏ธ", + "walking_woman": "๐Ÿšถโ€โ™€๏ธ", + "wallis_futuna": "๐Ÿ‡ผ๐Ÿ‡ซ", + "waning_crescent_moon": "๐ŸŒ˜", + "waning_gibbous_moon": "๐ŸŒ–", + "warning": "โš ๏ธ", + "wastebasket": "๐Ÿ—‘๏ธ", + "watch": "โŒš", + "water_buffalo": "๐Ÿƒ", + "water_polo": "๐Ÿคฝ", + "watermelon": "๐Ÿ‰", + "wave": "๐Ÿ‘‹", + "wavy_dash": "ใ€ฐ๏ธ", + "waxing_crescent_moon": "๐ŸŒ’", + "waxing_gibbous_moon": "๐ŸŒ”", + "wc": "๐Ÿšพ", + "weary": "๐Ÿ˜ฉ", + "wedding": "๐Ÿ’’", + "weight_lifting": "๐Ÿ‹๏ธ", + "weight_lifting_man": "๐Ÿ‹๏ธโ€โ™‚๏ธ", + "weight_lifting_woman": "๐Ÿ‹๏ธโ€โ™€๏ธ", + "western_sahara": "๐Ÿ‡ช๐Ÿ‡ญ", + "whale": "๐Ÿณ", + "whale2": "๐Ÿ‹", + "wheel": "๐Ÿ›ž", + "wheel_of_dharma": "โ˜ธ๏ธ", + "wheelchair": "โ™ฟ", + "white_check_mark": "โœ…", + "white_circle": "โšช", + "white_flag": "๐Ÿณ๏ธ", + "white_flower": "๐Ÿ’ฎ", + "white_haired_man": "๐Ÿ‘จโ€๐Ÿฆณ", + "white_haired_woman": "๐Ÿ‘ฉโ€๐Ÿฆณ", + "white_heart": "๐Ÿค", + "white_large_square": "โฌœ", + "white_medium_small_square": "โ—ฝ", + "white_medium_square": "โ—ป๏ธ", + "white_small_square": "โ–ซ๏ธ", + "white_square_button": "๐Ÿ”ณ", + "wilted_flower": "๐Ÿฅ€", + "wind_chime": "๐ŸŽ", + "wind_face": "๐ŸŒฌ๏ธ", + "window": "๐ŸชŸ", + "wine_glass": "๐Ÿท", + "wing": "๐Ÿชฝ", + "wink": "๐Ÿ˜‰", + "wireless": "๐Ÿ›œ", + "wolf": "๐Ÿบ", + "woman": "๐Ÿ‘ฉ", + "woman_artist": "๐Ÿ‘ฉโ€๐ŸŽจ", + "woman_astronaut": "๐Ÿ‘ฉโ€๐Ÿš€", + "woman_beard": "๐Ÿง”โ€โ™€๏ธ", + "woman_cartwheeling": "๐Ÿคธโ€โ™€๏ธ", + "woman_cook": "๐Ÿ‘ฉโ€๐Ÿณ", + "woman_dancing": "๐Ÿ’ƒ", + "woman_facepalming": "๐Ÿคฆโ€โ™€๏ธ", + "woman_factory_worker": "๐Ÿ‘ฉโ€๐Ÿญ", + "woman_farmer": "๐Ÿ‘ฉโ€๐ŸŒพ", + "woman_feeding_baby": "๐Ÿ‘ฉโ€๐Ÿผ", + "woman_firefighter": "๐Ÿ‘ฉโ€๐Ÿš’", + "woman_health_worker": "๐Ÿ‘ฉโ€โš•๏ธ", + "woman_in_manual_wheelchair": "๐Ÿ‘ฉโ€๐Ÿฆฝ", + "woman_in_motorized_wheelchair": "๐Ÿ‘ฉโ€๐Ÿฆผ", + "woman_in_tuxedo": "๐Ÿคตโ€โ™€๏ธ", + "woman_judge": "๐Ÿ‘ฉโ€โš–๏ธ", + "woman_juggling": "๐Ÿคนโ€โ™€๏ธ", + "woman_mechanic": "๐Ÿ‘ฉโ€๐Ÿ”ง", + "woman_office_worker": "๐Ÿ‘ฉโ€๐Ÿ’ผ", + "woman_pilot": "๐Ÿ‘ฉโ€โœˆ๏ธ", + "woman_playing_handball": "๐Ÿคพโ€โ™€๏ธ", + "woman_playing_water_polo": "๐Ÿคฝโ€โ™€๏ธ", + "woman_scientist": "๐Ÿ‘ฉโ€๐Ÿ”ฌ", + "woman_shrugging": "๐Ÿคทโ€โ™€๏ธ", + "woman_singer": "๐Ÿ‘ฉโ€๐ŸŽค", + "woman_student": "๐Ÿ‘ฉโ€๐ŸŽ“", + "woman_teacher": "๐Ÿ‘ฉโ€๐Ÿซ", + "woman_technologist": "๐Ÿ‘ฉโ€๐Ÿ’ป", + "woman_with_headscarf": "๐Ÿง•", + "woman_with_probing_cane": "๐Ÿ‘ฉโ€๐Ÿฆฏ", + "woman_with_turban": "๐Ÿ‘ณโ€โ™€๏ธ", + "woman_with_veil": "๐Ÿ‘ฐโ€โ™€๏ธ", + "womans_clothes": "๐Ÿ‘š", + "womans_hat": "๐Ÿ‘’", + "women_wrestling": "๐Ÿคผโ€โ™€๏ธ", + "womens": "๐Ÿšบ", + "wood": "๐Ÿชต", + "woozy_face": "๐Ÿฅด", + "world_map": "๐Ÿ—บ๏ธ", + "worm": "๐Ÿชฑ", + "worried": "๐Ÿ˜Ÿ", + "wrench": "๐Ÿ”ง", + "wrestling": "๐Ÿคผ", + "writing_hand": "โœ๏ธ", + "x": "โŒ", + "x_ray": "๐Ÿฉป", + "yarn": "๐Ÿงถ", + "yawning_face": "๐Ÿฅฑ", + "yellow_circle": "๐ŸŸก", + "yellow_heart": "๐Ÿ’›", + "yellow_square": "๐ŸŸจ", + "yemen": "๐Ÿ‡พ๐Ÿ‡ช", + "yen": "๐Ÿ’ด", + "yin_yang": "โ˜ฏ๏ธ", + "yo_yo": "๐Ÿช€", + "yum": "๐Ÿ˜‹", + "zambia": "๐Ÿ‡ฟ๐Ÿ‡ฒ", + "zany_face": "๐Ÿคช", + "zap": "โšก", + "zebra": "๐Ÿฆ“", + "zero": "0๏ธโƒฃ", + "zimbabwe": "๐Ÿ‡ฟ๐Ÿ‡ผ", + "zipper_mouth_face": "๐Ÿค", + "zombie": "๐ŸงŸ", + "zombie_man": "๐ŸงŸโ€โ™‚๏ธ", + "zombie_woman": "๐ŸงŸโ€โ™€๏ธ", + "zzz": "๐Ÿ’ค" +} diff --git a/resources/views/native/conversation.blade.php b/resources/views/native/conversation.blade.php index e16c1a0d..97076ffc 100644 --- a/resources/views/native/conversation.blade.php +++ b/resources/views/native/conversation.blade.php @@ -139,14 +139,15 @@ class="w-full py-[4] text-theme-ink" @endif - {{-- React on a message: the reactions the web offers first, then the rest. Or an org's pinned messages. --}} - + {{-- React on a message: the reactions the web offers first, then the phone's emoji picker for the rest. Or an org's pinned messages. --}} + @if ($sheet === \App\NativeComponents\Conversation::PinnedSheet) @if ($pinned !== []) @include('native.partials.pinned-sheet') @endif @elseif ($sheetMessage) - + {{-- iOS sizes the sheet to its detent, so the row sits in the middle of it. Android's sheet is taller. --}} + @foreach ($quickReactions as $emoji) @@ -155,24 +156,18 @@ class="w-full py-[4] text-theme-ink" @endforeach + + + + + - {{-- EDGE's rows don't wrap on iOS, so the rest come in rows of eight. --}} - - @foreach (array_chunk($moreReactions, 8) as $row) - - @foreach ($row as $emoji) - - - {{ $emoji }} - - - @endforeach - - @endforeach - @endif @include('native.partials.mute-sheet') + + {{-- More reactions, from the sheet or anywhere else: the emoji keyboard on iOS, Jetpack's emoji picker on Android. --}} + diff --git a/tests/Feature/Screens/ConversationTest.php b/tests/Feature/Screens/ConversationTest.php index cb4b810e..4151ec44 100644 --- a/tests/Feature/Screens/ConversationTest.php +++ b/tests/Feature/Screens/ConversationTest.php @@ -394,6 +394,82 @@ function showsActions(TestableComponent $screen, string $key): bool expect($screen->instance()->messages[0]->reactions)->toBe([['user_id' => RemotesViewerId, 'emoji' => 'โค๏ธ']]); }); +/** + * Whether the phone's emoji picker is up. + */ +function pickerShows(TestableComponent $screen): bool +{ + return nodeByRef($screen, 'emoji-picker')['props']['visible'] ?? false; +} + +it("opens the phone's emoji picker from More reactions in place of the sheet, and reacts with the emoji picked", function () { + serveRemoteThread(threadJson([remoteMessageJson(['id' => 1])], ['read_through_id' => 1])); + + $screen = finishRefreshes(Native::visit('/remotes/conversations/ada'))->swipe('swipe-1', 'right')->tap('react-1'); + + expect(childRefs($screen, 'quick-reactions'))->toBe(['quick-0', 'quick-1', 'quick-2', 'more-reactions']) + ->and(pickerShows($screen))->toBeFalse(); + + $screen->tap('more-reactions') + ->assertSet('sheetOpen', false) + ->assertSet('emojiPickerFor', 1); + + expect(pickerShows($screen))->toBeTrue(); + + finishRefreshes($screen->select('emoji-picker', '๐ŸŽ‰'))->assertSee('๐ŸŽ‰'); + + expect(sentTo('POST', 'remotes/messages/1/reactions'))->toBe(['{"emoji":"๐ŸŽ‰"}']) + ->and(pickerShows($screen))->toBeFalse(); +}); + +it('takes back a reaction picked again, as the quick reactions do', function () { + serveRemoteThread(threadJson([remoteMessageJson(['id' => 1, 'reactions' => [['user_id' => RemotesViewerId, 'emoji' => '๐ŸŽ‰']]])], ['read_through_id' => 1])); + + finishRefreshes(finishRefreshes(Native::visit('/remotes/conversations/ada'))->call('openEmojiPicker', 1)->select('emoji-picker', '๐ŸŽ‰')); + + expect(sentTo('DELETE', 'remotes/messages/1/reactions'))->toBe(['{"emoji":"๐ŸŽ‰"}']); +}); + +it('reacts with the emoji as devnull has it when the picker adds a skin tone or leaves out the variation selector', function (string $picked, string $sent) { + serveRemoteThread(threadJson([remoteMessageJson(['id' => 1])], ['read_through_id' => 1])); + + finishRefreshes(finishRefreshes(Native::visit('/remotes/conversations/ada'))->call('openEmojiPicker', 1)->select('emoji-picker', $picked)); + + expect(array_map(fn (string $body): string => json_decode($body, true)['emoji'], sentTo('POST', 'remotes/messages/1/reactions')))->toBe([$sent]); +})->with([ + 'a skin tone' => ['๐Ÿ‘๐Ÿฝ', '๐Ÿ‘'], + 'a skin tone on an emoji that needs the variation selector' => ['โœŒ๐Ÿฝ', 'โœŒ๏ธ'], + 'a skin tone inside a sequence' => ['๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ป', '๐Ÿ‘ฉโ€๐Ÿ’ป'], + 'no variation selector' => ['โค', 'โค๏ธ'], + 'as it is' => ['๐Ÿซ ', '๐Ÿซ '], +]); + +it("doesn't send an emoji devnull doesn't know, says so, and keeps the picker up for another", function (string $picked) { + serveRemoteThread(threadJson([remoteMessageJson(['id' => 1])], ['read_through_id' => 1])); + + $screen = finishRefreshes(finishRefreshes(Native::visit('/remotes/conversations/ada'))->call('openEmojiPicker', 1)->select('emoji-picker', $picked)); + + expect(sentTo('POST', 'remotes/messages/1/reactions'))->toBe([]) + ->and(array_column(array_column($this->bridge->callsTo('ToastKit.Show'), 'params'), 'message'))->toBe(["devnull doesn't take {$picked} as a reaction yet. Pick another."]) + ->and(pickerShows($screen))->toBeTrue(); + + finishRefreshes($screen->select('emoji-picker', '๐Ÿš€')); + + expect(sentTo('POST', 'remotes/messages/1/reactions'))->toBe(['{"emoji":"๐Ÿš€"}']); +})->with([ + 'newer than devnull\'s set' => ['๐Ÿฆโ€๐Ÿ”ฅ'], + 'two skin tones in one' => ['๐Ÿซฑ๐Ÿปโ€๐Ÿซฒ๐Ÿฟ'], +]); + +it('sends nothing when the picker is closed without a pick', function () { + serveRemoteThread(threadJson([remoteMessageJson(['id' => 1])], ['read_through_id' => 1])); + + $screen = finishRefreshes(Native::visit('/remotes/conversations/ada'))->call('openEmojiPicker', 1)->dismissSheet('emoji-picker'); + + expect(pickerShows($screen))->toBeFalse() + ->and(sentTo('POST', 'remotes/messages/1/reactions'))->toBe([]); +}); + it("says why a message can't go, in the web's words, and merges or drops a request", function (string $access, string $words) { app(Account::class)->rememberRemotes(new Remotes(requests: true)); serveRemoteThread(threadJson([remoteMessageJson(['id' => 1])], ['read_through_id' => 1, 'access' => $access])); @@ -896,6 +972,27 @@ function serveOrgThread(array $thread, ?array $poll = null, ?Closure $pins = nul expect(json_decode(sentTo('POST', 'remotes/orgs/org-1/messages')[0], true)['reply_to_id'])->toBe(1); }); +it('goes back to the pinned messages from the emoji picker, picked or not', function (?string $picked) { + serveOrgThread(orgThreadJson([orgMessageJson(['id' => 5, 'body' => 'hello org'])], ['pinned' => [ + orgMessageJson(['id' => 1, 'body' => 'standup in 10?']), + ]])); + + $screen = finishRefreshes(Native::visit('/remotes/orgs/org-1'))->tap('pinned-bar')->tap('pin-1-react')->tap('more-reactions') + // iOS tells the sheet it closed once it has made way for the picker. + ->dismissSheet('message-sheet') + ->assertSet('sheetOpen', false); + + finishRefreshes($picked === null ? $screen->dismissSheet('emoji-picker') : $screen->select('emoji-picker', $picked)) + ->assertSet('sheet', Conversation::PinnedSheet) + ->assertSet('sheetOpen', true) + ->assertSet('emojiPickerFor', null); + + expect(sentTo('POST', 'remotes/messages/1/reactions'))->toHaveCount($picked === null ? 0 : 1); +})->with([ + 'picked' => ['๐ŸŽ‰'], + 'closed' => [null], +]); + it('shows three lines of a long pinned message, and the rest with Show more', function () { $long = implode("\n", ['release checklist for today:', '1. tag 2.4', '2. post the changelog', '3. ping the mobile folks']); serveOrgThread(orgThreadJson([orgMessageJson(['id' => 2, 'body' => 'short'])], ['pinned' => [ From 3dfde9bd644f320417cfa2610b5195575ba87b47 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Wed, 28 Oct 2026 18:35:01 +0000 Subject: [PATCH 2/2] Show React's quick reactions in a bubble instead of a sheet Simon didn't want a sheet for a handful of reactions. React now opens a bubble pointing at it, above unless there's no room, with the web's quick reactions and More for the emoji picker. It's from NativePHP/mobile-ui. The message's options stay up while it shows, and reacting puts both away. React in the pinned sheet opens the same bubble over the sheet. The shared bottom sheet now only holds the pinned messages, so $sheet, ReactionsSheet, PinnedSheet and $sheetMessageId are gone. Co-Authored-By: Claude Opus 5.5 --- app/NativeComponents/Conversation.php | 69 ++++++++++--------- resources/views/native/conversation.blade.php | 30 ++------ .../native/partials/message-actions.blade.php | 7 +- .../native/partials/pinned-message.blade.php | 4 ++ .../native/partials/quick-reactions.blade.php | 21 ++++++ tests/Feature/Screens/ConversationTest.php | 59 ++++++++++++---- 6 files changed, 115 insertions(+), 75 deletions(-) create mode 100644 resources/views/native/partials/quick-reactions.blade.php diff --git a/app/NativeComponents/Conversation.php b/app/NativeComponents/Conversation.php index d6f2a6e5..519c0ead 100644 --- a/app/NativeComponents/Conversation.php +++ b/app/NativeComponents/Conversation.php @@ -59,8 +59,9 @@ * Retry of its own. Dragging a message sideways slides it out of the way * with the finger, its React and Reply growing in where it was, and Pin * for an org's maintainers; let go far enough and it stays open, or it - * springs back. A double tap toggles a heart. React offers the web's quick - * reactions, and the phone's own emoji picker for the rest. + * springs back. A double tap toggles a heart. React shows the web's quick + * reactions in a bubble beside it, with More for the phone's own emoji + * picker. * * An org's pinned messages show as the web shows them: the latest in a bar * over the top of the thread, which opens them all in a sheet, and a pin @@ -97,13 +98,6 @@ class Conversation extends NativeComponent */ public const array QuickReactions = ['๐Ÿ˜‚', 'โœ…', 'โค๏ธ']; - /** - * What the sheet shows: React's reactions for a message, or the org's pinned messages. - */ - public const string ReactionsSheet = 'reactions'; - - public const string PinnedSheet = 'pinned'; - /** * The slot beside a bubble for its actions: React and Reply, and in an org room for Pin as well, whether it shows or * not, as the web keeps it, so a pin arriving never rewraps a bubble. Then the gap between the slot and the bubble. @@ -242,20 +236,18 @@ class Conversation extends NativeComponent private array $pans = []; /** - * What the sheet shows, which stays as it was while the sheet slides away. + * Whether the sheet of the org's pinned messages is up. */ - public string $sheet = self::ReactionsSheet; + public bool $sheetOpen = false; /** - * The message React opened the reactions sheet for. + * The message whose React shows the web's quick reactions in a bubble beside it. */ - public ?int $sheetMessageId = null; - - public bool $sheetOpen = false; + public ?int $reactionsFor = null; /** - * React in the pinned sheet shows the reactions in its place, and reacting goes back to the pinned messages, as the - * web keeps its panel open behind the reactions. + * React was pressed in the pinned sheet, so the bubble shows there, and the sheet comes back after the emoji picker + * has made way for it. */ public bool $reactsFromPinned = false; @@ -542,7 +534,8 @@ public function hideActions(): void } /** - * React: the web's reactions for the message, in a sheet. From the pinned sheet, they take its place. + * React: the web's quick reactions and More, in a bubble beside the React that was pressed, above it unless there's + * no room. The message's options stay up underneath. */ public function openReactions(int $id): void { @@ -550,11 +543,17 @@ public function openReactions(int $id): void return; } - $this->reactsFromPinned = $this->sheetOpen && $this->sheet === self::PinnedSheet; - $this->actionsForId = null; - $this->sheet = self::ReactionsSheet; - $this->sheetMessageId = $id; - $this->sheetOpen = true; + $this->reactsFromPinned = $this->sheetOpen; + $this->reactionsFor = $id; + } + + /** + * A tap outside the bubble closes it. + */ + public function reactionsClosed(): void + { + $this->reactionsFor = null; + $this->reactsFromPinned = false; } /** @@ -567,8 +566,8 @@ public function openPinned(): void } $this->actionsForId = null; + $this->reactionsFor = null; $this->reactsFromPinned = false; - $this->sheet = self::PinnedSheet; $this->sheetOpen = true; } @@ -576,15 +575,16 @@ public function messageSheetClosed(): void { $this->sheetOpen = false; - // The reactions making way for the emoji picker keep where React came from. + // The pinned sheet making way for the emoji picker keeps where React came from. if ($this->emojiPickerFor === null) { + $this->reactionsFor = null; $this->reactsFromPinned = false; } } /** * More reactions: the phone's own emoji picker, with its search, recents and skin tones, for any message. The - * reactions sheet makes way for it. + * bubble closes and the pinned sheet makes way for it. */ public function openEmojiPicker(int $id): void { @@ -593,6 +593,7 @@ public function openEmojiPicker(int $id): void } $this->actionsForId = null; + $this->reactionsFor = null; $this->sheetOpen = false; $this->emojiPickerFor = $id; } @@ -619,6 +620,7 @@ public function emojiPicked(string $emoji): void $this->emojiPickerFor = null; $this->sheetOpen = $this->reactsFromPinned; + $this->reactsFromPinned = false; $this->toggleReaction($id, $reaction); } @@ -630,7 +632,6 @@ public function emojiPickerClosed(): void $this->emojiPickerFor = null; if ($this->reactsFromPinned) { - $this->sheet = self::PinnedSheet; $this->sheetOpen = true; $this->reactsFromPinned = false; } @@ -640,6 +641,7 @@ public function reply(int $id): void { $this->replyToId = $id; $this->actionsForId = null; + $this->reactionsFor = null; $this->sheetOpen = false; $this->reactsFromPinned = false; $this->composerKey++; @@ -721,7 +723,7 @@ public function unpinMessage(int $id): void $before = $this->pinned; $this->pinned = array_values(array_filter($this->pinned, fn (Message $pinned): bool => $pinned->id !== $id)); - if ($this->pinned === [] && $this->sheet === self::PinnedSheet) { + if ($this->pinned === []) { $this->sheetOpen = false; } @@ -743,17 +745,17 @@ public function heart(int $id): void /** * Leaves the emoji on the message, or takes it back, at once. If devnull says no, it goes back as it was. Picked in - * the reactions sheet, the sheet closes, or goes back to the pinned messages it was opened from. + * the bubble, the bubble closes and the message's options go away. */ public function toggleReaction(int $id, string $emoji): void { $message = $this->findMessage($id); $me = $this->me(); - if ($this->sheet === self::ReactionsSheet) { - $this->sheet = $this->reactsFromPinned ? self::PinnedSheet : self::ReactionsSheet; - $this->sheetOpen = $this->sheetOpen && $this->reactsFromPinned; + if ($this->reactionsFor !== null) { + $this->reactionsFor = null; $this->reactsFromPinned = false; + $this->actionsForId = null; } if ($message === null || $message->isPending() || $message->isEvent() || $me === null || $this->thread?->access !== Thread::Open) { @@ -984,7 +986,7 @@ private function takePinned(array $pinned, int $version): void }, $pinned); $this->pinnedVersion = $version; - if ($this->pinned === [] && $this->sheet === self::PinnedSheet) { + if ($this->pinned === []) { $this->sheetOpen = false; } } @@ -1440,7 +1442,6 @@ public function render(): View 'items' => $this->items($viewer), 'composerClearance' => self::composerClearance($this->draft, $this->replyToId !== null), 'replyTo' => $this->findMessage($this->replyToId), - 'sheetMessage' => $this->findMessage($this->sheetMessageId), 'quickReactions' => $this->quickReactions(), 'canPush' => $thread !== null && $thread->access === Thread::Open, 'canPin' => $this->canPin(), diff --git a/resources/views/native/conversation.blade.php b/resources/views/native/conversation.blade.php index 97076ffc..480dd6c0 100644 --- a/resources/views/native/conversation.blade.php +++ b/resources/views/native/conversation.blade.php @@ -139,35 +139,15 @@ class="w-full py-[4] text-theme-ink" @endif - {{-- React on a message: the reactions the web offers first, then the phone's emoji picker for the rest. Or an org's pinned messages. --}} - - @if ($sheet === \App\NativeComponents\Conversation::PinnedSheet) - @if ($pinned !== []) - @include('native.partials.pinned-sheet') - @endif - @elseif ($sheetMessage) - {{-- iOS sizes the sheet to its detent, so the row sits in the middle of it. Android's sheet is taller. --}} - - - @foreach ($quickReactions as $emoji) - - - {{ $emoji }} - - - @endforeach - - - - - - - + {{-- An org's pinned messages. --}} + + @if ($pinned !== []) + @include('native.partials.pinned-sheet') @endif @include('native.partials.mute-sheet') - {{-- More reactions, from the sheet or anywhere else: the emoji keyboard on iOS, Jetpack's emoji picker on Android. --}} + {{-- More reactions, from the bubble or anywhere else: the emoji keyboard on iOS, Jetpack's emoji picker on Android. --}} diff --git a/resources/views/native/partials/message-actions.blade.php b/resources/views/native/partials/message-actions.blade.php index 368e37f3..f302067e 100644 --- a/resources/views/native/partials/message-actions.blade.php +++ b/resources/views/native/partials/message-actions.blade.php @@ -3,7 +3,8 @@ nearest it, then Reply, then React, with the web's icons. Every message keeps them, shrunk to nothing, and as the drag slides the message aside each grows in over its share of the way, nearest the bubble first, following the finger there and back. The slot never changes width: in an org it keeps room for Pin whether it shows or not, as the web's - does, so a pin arriving never rewraps a bubble. They only take taps once the message has settled open. + does, so a pin arriving never rewraps a bubble. They only take taps once the message has settled open. React opens + the web's quick reactions in a bubble pointing at it (see quick-reactions). A pinned message's pin shows all the time, nearest the bubble, so the others come in beyond it without moving it. For an org's maintainers it turns into Unpin, in the primary colour, while the others show; for everyone else it @@ -39,6 +40,10 @@ @endif @endforeach + {{-- React's bubble points at React: the last button of theirs, the first of yours. --}} + @if ($reactionsFor === $message->id && ! $reactsFromPinned) + @include('native.partials.quick-reactions', ['anchor' => 'absolute top-0 bottom-0 w-[30] '.($mine ? 'left-0' : 'right-0')]) + @endif @if ($isPinned && $mine) @include('native.partials.message-pin') diff --git a/resources/views/native/partials/pinned-message.blade.php b/resources/views/native/partials/pinned-message.blade.php index 70aff179..0cf9ddc6 100644 --- a/resources/views/native/partials/pinned-message.blade.php +++ b/resources/views/native/partials/pinned-message.blade.php @@ -33,6 +33,10 @@ @endforeach + {{-- React's bubble points at React, the second button. --}} + @if ($reactionsFor === $message->id && $reactsFromPinned) + @include('native.partials.quick-reactions', ['anchor' => 'absolute top-0 bottom-0 left-[32] w-[32]']) + @endif @endif @if ($canPin) diff --git a/resources/views/native/partials/quick-reactions.blade.php b/resources/views/native/partials/quick-reactions.blade.php new file mode 100644 index 00000000..dd7a9b30 --- /dev/null +++ b/resources/views/native/partials/quick-reactions.blade.php @@ -0,0 +1,21 @@ +{{-- + React's bubble: the web's quick reactions for $message, yours tinted in your lane, and More for the phone's emoji + picker. It points at the React button it's laid over ($anchor places it there) and opens above it unless there's no + room. A tap outside closes it. +--}} + + + @foreach ($quickReactions as $emoji) + + + {{ $emoji }} + + + @endforeach + + + + + + + diff --git a/tests/Feature/Screens/ConversationTest.php b/tests/Feature/Screens/ConversationTest.php index 4151ec44..0974949c 100644 --- a/tests/Feature/Screens/ConversationTest.php +++ b/tests/Feature/Screens/ConversationTest.php @@ -311,17 +311,43 @@ function showsActions(TestableComponent $screen, string $key): bool expect(showsActions($screen, '1'))->toBeFalse(); }); -it('opens the reactions from React, without Reply in them', function () { +it("shows the web's quick reactions and More in a bubble pointing at React, and keeps the options up", function () { serveRemoteThread(threadJson([remoteMessageJson(['id' => 1])], ['read_through_id' => 1])); $screen = finishRefreshes(Native::visit('/remotes/conversations/ada')) ->dragEnd('swipe-1', -200) ->tap('react-1') - ->assertSet('sheetOpen', true) + ->assertSet('reactionsFor', 1) + ->assertSet('actionsForId', 1) + ->assertSet('sheetOpen', false); + + $bubble = nodeByRef($screen, 'reactions-bubble'); + + expect($bubble['type'])->toBe('popover') + ->and($bubble['props']['visible'])->toBeTrue() + ->and(childRefs($screen, 'actions-1-buttons'))->toBe(['reply-1', 'react-1', 'reactions-bubble']) + ->and(childRefs($screen, 'quick-reactions'))->toBe(['quick-0', 'quick-1', 'quick-2', 'more-reactions']); + + // A tap outside it closes it, and leaves the options up. + $screen->dismissSheet('reactions-bubble') + ->assertSet('reactionsFor', null) + ->assertSet('actionsForId', 1); + + expect(rendersRef($screen, 'reactions-bubble'))->toBeFalse(); +}); + +it('reacts from the bubble, and puts the bubble and the options away', function () { + serveRemoteThread(threadJson([remoteMessageJson(['id' => 1])], ['read_through_id' => 1])); + + $screen = finishRefreshes(Native::visit('/remotes/conversations/ada'))->dragEnd('swipe-1', -200)->tap('react-1'); + + finishRefreshes($screen->tap('quick-2')) + ->assertSet('reactionsFor', null) ->assertSet('actionsForId', null); - expect(childRefs($screen, 'quick-reactions'))->not->toBeEmpty() - ->and(findNode($screen->tree(), fn (array $node): bool => ($node['ref'] ?? null) === 'reply'))->toBeNull(); + expect(sentTo('POST', 'remotes/messages/1/reactions'))->toBe(['{"emoji":"โค๏ธ"}']) + ->and(rendersRef($screen, 'reactions-bubble'))->toBeFalse() + ->and(rendersRef($screen, 'chip-1-0'))->toBeTrue(); }); it('replies to a message from its Reply button, and raises the keyboard', function () { @@ -402,19 +428,19 @@ function pickerShows(TestableComponent $screen): bool return nodeByRef($screen, 'emoji-picker')['props']['visible'] ?? false; } -it("opens the phone's emoji picker from More reactions in place of the sheet, and reacts with the emoji picked", function () { +it("opens the phone's emoji picker from More in the bubble, and reacts with the emoji picked", function () { serveRemoteThread(threadJson([remoteMessageJson(['id' => 1])], ['read_through_id' => 1])); - $screen = finishRefreshes(Native::visit('/remotes/conversations/ada'))->swipe('swipe-1', 'right')->tap('react-1'); + $screen = finishRefreshes(Native::visit('/remotes/conversations/ada'))->dragEnd('swipe-1', -200)->tap('react-1'); - expect(childRefs($screen, 'quick-reactions'))->toBe(['quick-0', 'quick-1', 'quick-2', 'more-reactions']) - ->and(pickerShows($screen))->toBeFalse(); + expect(pickerShows($screen))->toBeFalse(); $screen->tap('more-reactions') - ->assertSet('sheetOpen', false) + ->assertSet('reactionsFor', null) ->assertSet('emojiPickerFor', 1); - expect(pickerShows($screen))->toBeTrue(); + expect(pickerShows($screen))->toBeTrue() + ->and(rendersRef($screen, 'reactions-bubble'))->toBeFalse(); finishRefreshes($screen->select('emoji-picker', '๐ŸŽ‰'))->assertSee('๐ŸŽ‰'); @@ -937,7 +963,6 @@ function serveOrgThread(array $thread, ?array $poll = null, ?Closure $pins = nul $screen->tap('pinned-bar') ->assertSet('sheetOpen', true) - ->assertSet('sheet', Conversation::PinnedSheet) ->assertSee('Pinned messages') ->assertSee('Yesterday ยท 10:02'); @@ -952,10 +977,15 @@ function serveOrgThread(array $thread, ?array $poll = null, ?Closure $pins = nul $screen = finishRefreshes(Native::visit('/remotes/orgs/org-1'))->tap('pinned-bar'); - // React shows the reactions in the sheet, and reacting goes back to the pinned messages. - $screen->tap('pin-1-react')->assertSet('sheet', Conversation::ReactionsSheet); + // React shows the bubble over the sheet, and reacting leaves the pinned messages up. + $screen->tap('pin-1-react') + ->assertSet('reactionsFor', 1) + ->assertSet('reactsFromPinned', true); + + expect(childRefs($screen, 'pin-1-actions'))->toContain('reactions-bubble'); + finishRefreshes($screen->tap('quick-0')) - ->assertSet('sheet', Conversation::PinnedSheet) + ->assertSet('reactionsFor', null) ->assertSet('sheetOpen', true); expect(sentTo('POST', 'remotes/messages/1/reactions'))->toHaveCount(1) @@ -983,7 +1013,6 @@ function serveOrgThread(array $thread, ?array $poll = null, ?Closure $pins = nul ->assertSet('sheetOpen', false); finishRefreshes($picked === null ? $screen->dismissSheet('emoji-picker') : $screen->select('emoji-picker', $picked)) - ->assertSet('sheet', Conversation::PinnedSheet) ->assertSet('sheetOpen', true) ->assertSet('emojiPickerFor', null);