diff --git a/android/app/src/main/java/com/tsmobile/app/TSBridge.kt b/android/app/src/main/java/com/tsmobile/app/TSBridge.kt index 73154c7..ef565ce 100644 --- a/android/app/src/main/java/com/tsmobile/app/TSBridge.kt +++ b/android/app/src/main/java/com/tsmobile/app/TSBridge.kt @@ -19,8 +19,13 @@ import java.lang.reflect.Proxy object TSBridge { private val connectLock = Any() + // @Volatile:client 被主线程/IO 协程写入(connect/disconnect), + // 被多个 getter 线程读取;保证可见性,避免读到过期的 client 引用。 + @Volatile private var client: TSClient? = null private var connectionGeneration: Long = 0 + // @Volatile:connect() 在持有 connectLock 之前读取它,需保证可见性。 + @Volatile private var currentIdentity: String = "" private fun beginConnection(identity: String): Pair = synchronized(connectLock) { @@ -81,7 +86,7 @@ object TSBridge { defaultChannelPassword: String = "", callbacks: Callbacks, ): String { - val identity = currentIdentity + val identity = currentIdentity() val (tsClient, generation) = beginConnection(identity) val error = doConnect(tsClient, generation, callbacks) { proxy -> tsClient.connect(host, nickname, password, defaultChannel, defaultChannelPassword, proxy) diff --git a/android/app/src/main/java/com/tsmobile/app/data/EmojiMapper.kt b/android/app/src/main/java/com/tsmobile/app/data/EmojiMapper.kt new file mode 100644 index 0000000..7b9b3b6 --- /dev/null +++ b/android/app/src/main/java/com/tsmobile/app/data/EmojiMapper.kt @@ -0,0 +1,1992 @@ +package com.tsmobile.app.data + +/** + * TeamSpeak 聊天文本里的 emoji 短码映射:把 `:smile:` 替换成真正的 Unicode emoji。 + * + * 表由 iamcal/emoji-data 覆盖 gemoji 常见短码,并补充 TeamSpeak 使用的别名。 + * 未识别的短码原样保留。 + */ +object EmojiMapper { + + private val SHORTCODES: Map = mapOf( + "+1" to "\uD83D\uDC4D", + "-1" to "\uD83D\uDC4E", + "100" to "\uD83D\uDCAF", + "1234" to "\uD83D\uDD22", + "8ball" to "\uD83C\uDFB1", + "a" to "\uD83C\uDD70\uFE0F", + "ab" to "\uD83C\uDD8E", + "abacus" to "\uD83E\uDDEE", + "abc" to "\uD83D\uDD24", + "abcd" to "\uD83D\uDD21", + "accept" to "\uD83C\uDE51", + "accordion" to "\uD83E\uDE97", + "adhesive_bandage" to "\uD83E\uDE79", + "admission_tickets" to "\uD83C\uDF9F\uFE0F", + "adult" to "\uD83E\uDDD1", + "aerial_tramway" to "\uD83D\uDEA1", + "airplane" to "\u2708\uFE0F", + "airplane_arriving" to "\uD83D\uDEEC", + "airplane_departure" to "\uD83D\uDEEB", + "alarm_clock" to "\u23F0", + "alembic" to "\u2697\uFE0F", + "alien" to "\uD83D\uDC7D", + "ambulance" to "\uD83D\uDE91", + "amphora" to "\uD83C\uDFFA", + "anatomical_heart" to "\uD83E\uDEC0", + "anchor" to "\u2693", + "angel" to "\uD83D\uDC7C", + "anger" to "\uD83D\uDCA2", + "angry" to "\uD83D\uDE20", + "anguished" to "\uD83D\uDE27", + "ant" to "\uD83D\uDC1C", + "apple" to "\uD83C\uDF4E", + "aquarius" to "\u2652", + "aries" to "\u2648", + "arrow_backward" to "\u25C0\uFE0F", + "arrow_double_down" to "\u23EC", + "arrow_double_up" to "\u23EB", + "arrow_down" to "\u2B07\uFE0F", + "arrow_down_small" to "\uD83D\uDD3D", + "arrow_forward" to "\u25B6\uFE0F", + "arrow_heading_down" to "\u2935\uFE0F", + "arrow_heading_up" to "\u2934\uFE0F", + "arrow_left" to "\u2B05\uFE0F", + "arrow_lower_left" to "\u2199\uFE0F", + "arrow_lower_right" to "\u2198\uFE0F", + "arrow_right" to "\u27A1\uFE0F", + "arrow_right_hook" to "\u21AA\uFE0F", + "arrow_up" to "\u2B06\uFE0F", + "arrow_up_down" to "\u2195\uFE0F", + "arrow_up_small" to "\uD83D\uDD3C", + "arrow_upper_left" to "\u2196\uFE0F", + "arrow_upper_right" to "\u2197\uFE0F", + "arrows_clockwise" to "\uD83D\uDD03", + "arrows_counterclockwise" to "\uD83D\uDD04", + "art" to "\uD83C\uDFA8", + "articulated_lorry" to "\uD83D\uDE9B", + "artist" to "\uD83E\uDDD1\u200D\uD83C\uDFA8", + "astonished" to "\uD83D\uDE32", + "astronaut" to "\uD83E\uDDD1\u200D\uD83D\uDE80", + "athletic_shoe" to "\uD83D\uDC5F", + "atm" to "\uD83C\uDFE7", + "atom_symbol" to "\u269B\uFE0F", + "auto_rickshaw" to "\uD83D\uDEFA", + "avocado" to "\uD83E\uDD51", + "axe" to "\uD83E\uDE93", + "b" to "\uD83C\uDD71\uFE0F", + "baby" to "\uD83D\uDC76", + "baby_bottle" to "\uD83C\uDF7C", + "baby_chick" to "\uD83D\uDC24", + "baby_symbol" to "\uD83D\uDEBC", + "back" to "\uD83D\uDD19", + "bacon" to "\uD83E\uDD53", + "badger" to "\uD83E\uDDA1", + "badminton_racquet_and_shuttlecock" to "\uD83C\uDFF8", + "bagel" to "\uD83E\uDD6F", + "baggage_claim" to "\uD83D\uDEC4", + "baguette_bread" to "\uD83E\uDD56", + "bald_man" to "\uD83D\uDC68\u200D\uD83E\uDDB2", + "bald_person" to "\uD83E\uDDD1\u200D\uD83E\uDDB2", + "bald_woman" to "\uD83D\uDC69\u200D\uD83E\uDDB2", + "ballet_shoes" to "\uD83E\uDE70", + "balloon" to "\uD83C\uDF88", + "ballot_box_with_ballot" to "\uD83D\uDDF3\uFE0F", + "ballot_box_with_check" to "\u2611\uFE0F", + "bamboo" to "\uD83C\uDF8D", + "banana" to "\uD83C\uDF4C", + "bangbang" to "\u203C\uFE0F", + "banjo" to "\uD83E\uDE95", + "bank" to "\uD83C\uDFE6", + "bar_chart" to "\uD83D\uDCCA", + "barber" to "\uD83D\uDC88", + "barely_sunny" to "\uD83C\uDF25\uFE0F", + "baseball" to "\u26BE", + "basket" to "\uD83E\uDDFA", + "basketball" to "\uD83C\uDFC0", + "bat" to "\uD83E\uDD87", + "bath" to "\uD83D\uDEC0", + "bathtub" to "\uD83D\uDEC1", + "battery" to "\uD83D\uDD0B", + "beach_with_umbrella" to "\uD83C\uDFD6\uFE0F", + "beans" to "\uD83E\uDED8", + "bear" to "\uD83D\uDC3B", + "bearded_person" to "\uD83E\uDDD4", + "beaver" to "\uD83E\uDDAB", + "bed" to "\uD83D\uDECF\uFE0F", + "bee" to "\uD83D\uDC1D", + "beer" to "\uD83C\uDF7A", + "beers" to "\uD83C\uDF7B", + "beetle" to "\uD83E\uDEB2", + "beginner" to "\uD83D\uDD30", + "bell" to "\uD83D\uDD14", + "bell_pepper" to "\uD83E\uDED1", + "bellhop_bell" to "\uD83D\uDECE\uFE0F", + "bento" to "\uD83C\uDF71", + "beverage_box" to "\uD83E\uDDC3", + "bicyclist" to "\uD83D\uDEB4", + "bike" to "\uD83D\uDEB2", + "bikini" to "\uD83D\uDC59", + "billed_cap" to "\uD83E\uDDE2", + "biohazard_sign" to "\u2623\uFE0F", + "bird" to "\uD83D\uDC26", + "birthday" to "\uD83C\uDF82", + "bison" to "\uD83E\uDDAC", + "biting_lip" to "\uD83E\uDEE6", + "black_bird" to "\uD83D\uDC26\u200D\u2B1B", + "black_cat" to "\uD83D\uDC08\u200D\u2B1B", + "black_circle" to "\u26AB", + "black_circle_for_record" to "\u23FA\uFE0F", + "black_heart" to "\uD83D\uDDA4", + "black_joker" to "\uD83C\uDCCF", + "black_large_square" to "\u2B1B", + "black_left_pointing_double_triangle_with_vertical_bar" to "\u23EE\uFE0F", + "black_medium_small_square" to "\u25FE", + "black_medium_square" to "\u25FC\uFE0F", + "black_nib" to "\u2712\uFE0F", + "black_right_pointing_double_triangle_with_vertical_bar" to "\u23ED\uFE0F", + "black_right_pointing_triangle_with_double_vertical_bar" to "\u23EF\uFE0F", + "black_small_square" to "\u25AA\uFE0F", + "black_square_button" to "\uD83D\uDD32", + "black_square_for_stop" to "\u23F9\uFE0F", + "blond-haired-man" to "\uD83D\uDC71\u200D\u2642\uFE0F", + "blond-haired-woman" to "\uD83D\uDC71\u200D\u2640\uFE0F", + "blossom" to "\uD83C\uDF3C", + "blowfish" to "\uD83D\uDC21", + "blue_book" to "\uD83D\uDCD8", + "blue_car" to "\uD83D\uDE99", + "blue_heart" to "\uD83D\uDC99", + "blueberries" to "\uD83E\uDED0", + "blush" to "\uD83D\uDE0A", + "boar" to "\uD83D\uDC17", + "boat" to "\u26F5", + "bomb" to "\uD83D\uDCA3", + "bone" to "\uD83E\uDDB4", + "book" to "\uD83D\uDCD6", + "bookmark" to "\uD83D\uDD16", + "bookmark_tabs" to "\uD83D\uDCD1", + "books" to "\uD83D\uDCDA", + "boom" to "\uD83D\uDCA5", + "boomerang" to "\uD83E\uDE83", + "boot" to "\uD83D\uDC62", + "bouquet" to "\uD83D\uDC90", + "bow" to "\uD83D\uDE47", + "bow_and_arrow" to "\uD83C\uDFF9", + "bowl_with_spoon" to "\uD83E\uDD63", + "bowling" to "\uD83C\uDFB3", + "boxing_glove" to "\uD83E\uDD4A", + "boy" to "\uD83D\uDC66", + "brain" to "\uD83E\uDDE0", + "bread" to "\uD83C\uDF5E", + "breast-feeding" to "\uD83E\uDD31", + "bricks" to "\uD83E\uDDF1", + "bride_with_veil" to "\uD83D\uDC70", + "bridge_at_night" to "\uD83C\uDF09", + "briefcase" to "\uD83D\uDCBC", + "briefs" to "\uD83E\uDE72", + "broccoli" to "\uD83E\uDD66", + "broken_chain" to "\u26D3\uFE0F\u200D\uD83D\uDCA5", + "broken_heart" to "\uD83D\uDC94", + "broom" to "\uD83E\uDDF9", + "brown_heart" to "\uD83E\uDD0E", + "brown_mushroom" to "\uD83C\uDF44\u200D\uD83D\uDFEB", + "bubble_tea" to "\uD83E\uDDCB", + "bubbles" to "\uD83E\uDEE7", + "bucket" to "\uD83E\uDEA3", + "bug" to "\uD83D\uDC1B", + "building_construction" to "\uD83C\uDFD7\uFE0F", + "bulb" to "\uD83D\uDCA1", + "bullettrain_front" to "\uD83D\uDE85", + "bullettrain_side" to "\uD83D\uDE84", + "burrito" to "\uD83C\uDF2F", + "bus" to "\uD83D\uDE8C", + "busstop" to "\uD83D\uDE8F", + "bust_in_silhouette" to "\uD83D\uDC64", + "busts_in_silhouette" to "\uD83D\uDC65", + "butter" to "\uD83E\uDDC8", + "butterfly" to "\uD83E\uDD8B", + "cactus" to "\uD83C\uDF35", + "cake" to "\uD83C\uDF70", + "calendar" to "\uD83D\uDCC6", + "call_me_hand" to "\uD83E\uDD19", + "calling" to "\uD83D\uDCF2", + "camel" to "\uD83D\uDC2B", + "camera" to "\uD83D\uDCF7", + "camera_with_flash" to "\uD83D\uDCF8", + "camping" to "\uD83C\uDFD5\uFE0F", + "cancer" to "\u264B", + "candle" to "\uD83D\uDD6F\uFE0F", + "candy" to "\uD83C\uDF6C", + "canned_food" to "\uD83E\uDD6B", + "canoe" to "\uD83D\uDEF6", + "capital_abcd" to "\uD83D\uDD20", + "capricorn" to "\u2651", + "car" to "\uD83D\uDE97", + "card_file_box" to "\uD83D\uDDC3\uFE0F", + "card_index" to "\uD83D\uDCC7", + "card_index_dividers" to "\uD83D\uDDC2\uFE0F", + "carousel_horse" to "\uD83C\uDFA0", + "carpentry_saw" to "\uD83E\uDE9A", + "carrot" to "\uD83E\uDD55", + "cat" to "\uD83D\uDC31", + "cat2" to "\uD83D\uDC08", + "cd" to "\uD83D\uDCBF", + "chains" to "\u26D3\uFE0F", + "chair" to "\uD83E\uDE91", + "champagne" to "\uD83C\uDF7E", + "chart" to "\uD83D\uDCB9", + "chart_with_downwards_trend" to "\uD83D\uDCC9", + "chart_with_upwards_trend" to "\uD83D\uDCC8", + "checkered_flag" to "\uD83C\uDFC1", + "cheese_wedge" to "\uD83E\uDDC0", + "cherries" to "\uD83C\uDF52", + "cherry_blossom" to "\uD83C\uDF38", + "chess_pawn" to "\u265F\uFE0F", + "chestnut" to "\uD83C\uDF30", + "chicken" to "\uD83D\uDC14", + "child" to "\uD83E\uDDD2", + "children_crossing" to "\uD83D\uDEB8", + "chipmunk" to "\uD83D\uDC3F\uFE0F", + "chocolate_bar" to "\uD83C\uDF6B", + "chopsticks" to "\uD83E\uDD62", + "christmas_tree" to "\uD83C\uDF84", + "church" to "\u26EA", + "cinema" to "\uD83C\uDFA6", + "circus_tent" to "\uD83C\uDFAA", + "city_sunrise" to "\uD83C\uDF07", + "city_sunset" to "\uD83C\uDF06", + "cityscape" to "\uD83C\uDFD9\uFE0F", + "cl" to "\uD83C\uDD91", + "clap" to "\uD83D\uDC4F", + "clapper" to "\uD83C\uDFAC", + "classical_building" to "\uD83C\uDFDB\uFE0F", + "clinking_glasses" to "\uD83E\uDD42", + "clipboard" to "\uD83D\uDCCB", + "clock1" to "\uD83D\uDD50", + "clock10" to "\uD83D\uDD59", + "clock1030" to "\uD83D\uDD65", + "clock11" to "\uD83D\uDD5A", + "clock1130" to "\uD83D\uDD66", + "clock12" to "\uD83D\uDD5B", + "clock1230" to "\uD83D\uDD67", + "clock130" to "\uD83D\uDD5C", + "clock2" to "\uD83D\uDD51", + "clock230" to "\uD83D\uDD5D", + "clock3" to "\uD83D\uDD52", + "clock330" to "\uD83D\uDD5E", + "clock4" to "\uD83D\uDD53", + "clock430" to "\uD83D\uDD5F", + "clock5" to "\uD83D\uDD54", + "clock530" to "\uD83D\uDD60", + "clock6" to "\uD83D\uDD55", + "clock630" to "\uD83D\uDD61", + "clock7" to "\uD83D\uDD56", + "clock730" to "\uD83D\uDD62", + "clock8" to "\uD83D\uDD57", + "clock830" to "\uD83D\uDD63", + "clock9" to "\uD83D\uDD58", + "clock930" to "\uD83D\uDD64", + "closed_book" to "\uD83D\uDCD5", + "closed_lock_with_key" to "\uD83D\uDD10", + "closed_umbrella" to "\uD83C\uDF02", + "cloud" to "\u2601\uFE0F", + "clown_face" to "\uD83E\uDD21", + "clubs" to "\u2663\uFE0F", + "cn" to "\uD83C\uDDE8\uD83C\uDDF3", + "coat" to "\uD83E\uDDE5", + "cockroach" to "\uD83E\uDEB3", + "cocktail" to "\uD83C\uDF78", + "coconut" to "\uD83E\uDD65", + "coffee" to "\u2615", + "coffin" to "\u26B0\uFE0F", + "coin" to "\uD83E\uDE99", + "cold_face" to "\uD83E\uDD76", + "cold_sweat" to "\uD83D\uDE30", + "collision" to "\uD83D\uDCA5", + "comet" to "\u2604\uFE0F", + "compass" to "\uD83E\uDDED", + "compression" to "\uD83D\uDDDC\uFE0F", + "computer" to "\uD83D\uDCBB", + "confetti_ball" to "\uD83C\uDF8A", + "confounded" to "\uD83D\uDE16", + "confused" to "\uD83D\uDE15", + "congratulations" to "\u3297\uFE0F", + "construction" to "\uD83D\uDEA7", + "construction_worker" to "\uD83D\uDC77", + "control_knobs" to "\uD83C\uDF9B\uFE0F", + "convenience_store" to "\uD83C\uDFEA", + "cook" to "\uD83E\uDDD1\u200D\uD83C\uDF73", + "cookie" to "\uD83C\uDF6A", + "cooking" to "\uD83C\uDF73", + "cool" to "\uD83C\uDD92", + "cop" to "\uD83D\uDC6E", + "copyright" to "\u00A9\uFE0F", + "coral" to "\uD83E\uDEB8", + "corn" to "\uD83C\uDF3D", + "couch_and_lamp" to "\uD83D\uDECB\uFE0F", + "couple" to "\uD83D\uDC6B", + "couple_with_heart" to "\uD83D\uDC91", + "couplekiss" to "\uD83D\uDC8F", + "cow" to "\uD83D\uDC2E", + "cow2" to "\uD83D\uDC04", + "crab" to "\uD83E\uDD80", + "credit_card" to "\uD83D\uDCB3", + "crescent_moon" to "\uD83C\uDF19", + "cricket" to "\uD83E\uDD97", + "cricket_bat_and_ball" to "\uD83C\uDFCF", + "crocodile" to "\uD83D\uDC0A", + "croissant" to "\uD83E\uDD50", + "crossed_fingers" to "\uD83E\uDD1E", + "crossed_flags" to "\uD83C\uDF8C", + "crossed_swords" to "\u2694\uFE0F", + "crown" to "\uD83D\uDC51", + "crutch" to "\uD83E\uDE7C", + "cry" to "\uD83D\uDE22", + "crying_cat_face" to "\uD83D\uDE3F", + "crystal_ball" to "\uD83D\uDD2E", + "cucumber" to "\uD83E\uDD52", + "cup_with_straw" to "\uD83E\uDD64", + "cupcake" to "\uD83E\uDDC1", + "cupid" to "\uD83D\uDC98", + "curling_stone" to "\uD83E\uDD4C", + "curly_haired_man" to "\uD83D\uDC68\u200D\uD83E\uDDB1", + "curly_haired_person" to "\uD83E\uDDD1\u200D\uD83E\uDDB1", + "curly_haired_woman" to "\uD83D\uDC69\u200D\uD83E\uDDB1", + "curly_loop" to "\u27B0", + "currency_exchange" to "\uD83D\uDCB1", + "curry" to "\uD83C\uDF5B", + "custard" to "\uD83C\uDF6E", + "customs" to "\uD83D\uDEC3", + "cut_of_meat" to "\uD83E\uDD69", + "cyclone" to "\uD83C\uDF00", + "dagger_knife" to "\uD83D\uDDE1\uFE0F", + "dancer" to "\uD83D\uDC83", + "dancers" to "\uD83D\uDC6F", + "dango" to "\uD83C\uDF61", + "dark_sunglasses" to "\uD83D\uDD76\uFE0F", + "dart" to "\uD83C\uDFAF", + "dash" to "\uD83D\uDCA8", + "date" to "\uD83D\uDCC5", + "de" to "\uD83C\uDDE9\uD83C\uDDEA", + "deaf_man" to "\uD83E\uDDCF\u200D\u2642\uFE0F", + "deaf_person" to "\uD83E\uDDCF", + "deaf_woman" to "\uD83E\uDDCF\u200D\u2640\uFE0F", + "deciduous_tree" to "\uD83C\uDF33", + "deer" to "\uD83E\uDD8C", + "department_store" to "\uD83C\uDFEC", + "derelict_house_building" to "\uD83C\uDFDA\uFE0F", + "desert" to "\uD83C\uDFDC\uFE0F", + "desert_island" to "\uD83C\uDFDD\uFE0F", + "desktop_computer" to "\uD83D\uDDA5\uFE0F", + "diamond_shape_with_a_dot_inside" to "\uD83D\uDCA0", + "diamonds" to "\u2666\uFE0F", + "disappointed" to "\uD83D\uDE1E", + "disappointed_relieved" to "\uD83D\uDE25", + "disguised_face" to "\uD83E\uDD78", + "diving_mask" to "\uD83E\uDD3F", + "diya_lamp" to "\uD83E\uDE94", + "dizzy" to "\uD83D\uDCAB", + "dizzy_face" to "\uD83D\uDE35", + "dna" to "\uD83E\uDDEC", + "do_not_litter" to "\uD83D\uDEAF", + "dodo" to "\uD83E\uDDA4", + "dog" to "\uD83D\uDC36", + "dog2" to "\uD83D\uDC15", + "dollar" to "\uD83D\uDCB5", + "dolls" to "\uD83C\uDF8E", + "dolphin" to "\uD83D\uDC2C", + "donkey" to "\uD83E\uDECF", + "door" to "\uD83D\uDEAA", + "dotted_line_face" to "\uD83E\uDEE5", + "double_vertical_bar" to "\u23F8\uFE0F", + "doughnut" to "\uD83C\uDF69", + "dove_of_peace" to "\uD83D\uDD4A\uFE0F", + "dragon" to "\uD83D\uDC09", + "dragon_face" to "\uD83D\uDC32", + "dress" to "\uD83D\uDC57", + "dromedary_camel" to "\uD83D\uDC2A", + "drooling_face" to "\uD83E\uDD24", + "drop_of_blood" to "\uD83E\uDE78", + "droplet" to "\uD83D\uDCA7", + "drum_with_drumsticks" to "\uD83E\uDD41", + "duck" to "\uD83E\uDD86", + "dumpling" to "\uD83E\uDD5F", + "dvd" to "\uD83D\uDCC0", + "e-mail" to "\uD83D\uDCE7", + "eagle" to "\uD83E\uDD85", + "ear" to "\uD83D\uDC42", + "ear_of_rice" to "\uD83C\uDF3E", + "ear_with_hearing_aid" to "\uD83E\uDDBB", + "earth_africa" to "\uD83C\uDF0D", + "earth_americas" to "\uD83C\uDF0E", + "earth_asia" to "\uD83C\uDF0F", + "egg" to "\uD83E\uDD5A", + "eggplant" to "\uD83C\uDF46", + "eight" to "\u0038\uFE0F\u20E3", + "eight_pointed_black_star" to "\u2734\uFE0F", + "eight_spoked_asterisk" to "\u2733\uFE0F", + "eject" to "\u23CF\uFE0F", + "electric_plug" to "\uD83D\uDD0C", + "elephant" to "\uD83D\uDC18", + "elevator" to "\uD83D\uDED7", + "elf" to "\uD83E\uDDDD", + "email" to "\u2709\uFE0F", + "empty_nest" to "\uD83E\uDEB9", + "end" to "\uD83D\uDD1A", + "envelope" to "\u2709\uFE0F", + "envelope_with_arrow" to "\uD83D\uDCE9", + "es" to "\uD83C\uDDEA\uD83C\uDDF8", + "euro" to "\uD83D\uDCB6", + "european_castle" to "\uD83C\uDFF0", + "european_post_office" to "\uD83C\uDFE4", + "evergreen_tree" to "\uD83C\uDF32", + "exclamation" to "\u2757", + "exploding_head" to "\uD83E\uDD2F", + "expressionless" to "\uD83D\uDE11", + "eye" to "\uD83D\uDC41\uFE0F", + "eye-in-speech-bubble" to "\uD83D\uDC41\uFE0F\u200D\uD83D\uDDE8\uFE0F", + "eyeglasses" to "\uD83D\uDC53", + "eyes" to "\uD83D\uDC40", + "face_exhaling" to "\uD83D\uDE2E\u200D\uD83D\uDCA8", + "face_holding_back_tears" to "\uD83E\uDD79", + "face_in_clouds" to "\uD83D\uDE36\u200D\uD83C\uDF2B\uFE0F", + "face_palm" to "\uD83E\uDD26", + "face_vomiting" to "\uD83E\uDD2E", + "face_with_bags_under_eyes" to "\uD83E\uDEE9", + "face_with_cowboy_hat" to "\uD83E\uDD20", + "face_with_diagonal_mouth" to "\uD83E\uDEE4", + "face_with_finger_covering_closed_lips" to "\uD83E\uDD2B", + "face_with_hand_over_mouth" to "\uD83E\uDD2D", + "face_with_head_bandage" to "\uD83E\uDD15", + "face_with_monocle" to "\uD83E\uDDD0", + "face_with_one_eyebrow_raised" to "\uD83E\uDD28", + "face_with_open_eyes_and_hand_over_mouth" to "\uD83E\uDEE2", + "face_with_open_mouth_vomiting" to "\uD83E\uDD2E", + "face_with_peeking_eye" to "\uD83E\uDEE3", + "face_with_raised_eyebrow" to "\uD83E\uDD28", + "face_with_rolling_eyes" to "\uD83D\uDE44", + "face_with_spiral_eyes" to "\uD83D\uDE35\u200D\uD83D\uDCAB", + "face_with_symbols_on_mouth" to "\uD83E\uDD2C", + "face_with_thermometer" to "\uD83E\uDD12", + "facepunch" to "\uD83D\uDC4A", + "factory" to "\uD83C\uDFED", + "factory_worker" to "\uD83E\uDDD1\u200D\uD83C\uDFED", + "fairy" to "\uD83E\uDDDA", + "falafel" to "\uD83E\uDDC6", + "fallen_leaf" to "\uD83C\uDF42", + "family" to "\uD83D\uDC6A", + "family_adult_adult_child" to "\uD83E\uDDD1\u200D\uD83E\uDDD1\u200D\uD83E\uDDD2", + "family_adult_adult_child_child" to "\uD83E\uDDD1\u200D\uD83E\uDDD1\u200D\uD83E\uDDD2\u200D\uD83E\uDDD2", + "family_adult_child" to "\uD83E\uDDD1\u200D\uD83E\uDDD2", + "family_adult_child_child" to "\uD83E\uDDD1\u200D\uD83E\uDDD2\u200D\uD83E\uDDD2", + "farmer" to "\uD83E\uDDD1\u200D\uD83C\uDF3E", + "fast_forward" to "\u23E9", + "fax" to "\uD83D\uDCE0", + "fearful" to "\uD83D\uDE28", + "feather" to "\uD83E\uDEB6", + "feet" to "\uD83D\uDC3E", + "female-artist" to "\uD83D\uDC69\u200D\uD83C\uDFA8", + "female-astronaut" to "\uD83D\uDC69\u200D\uD83D\uDE80", + "female-construction-worker" to "\uD83D\uDC77\u200D\u2640\uFE0F", + "female-cook" to "\uD83D\uDC69\u200D\uD83C\uDF73", + "female-detective" to "\uD83D\uDD75\uFE0F\u200D\u2640\uFE0F", + "female-doctor" to "\uD83D\uDC69\u200D\u2695\uFE0F", + "female-factory-worker" to "\uD83D\uDC69\u200D\uD83C\uDFED", + "female-farmer" to "\uD83D\uDC69\u200D\uD83C\uDF3E", + "female-firefighter" to "\uD83D\uDC69\u200D\uD83D\uDE92", + "female-guard" to "\uD83D\uDC82\u200D\u2640\uFE0F", + "female-judge" to "\uD83D\uDC69\u200D\u2696\uFE0F", + "female-mechanic" to "\uD83D\uDC69\u200D\uD83D\uDD27", + "female-office-worker" to "\uD83D\uDC69\u200D\uD83D\uDCBC", + "female-pilot" to "\uD83D\uDC69\u200D\u2708\uFE0F", + "female-police-officer" to "\uD83D\uDC6E\u200D\u2640\uFE0F", + "female-scientist" to "\uD83D\uDC69\u200D\uD83D\uDD2C", + "female-singer" to "\uD83D\uDC69\u200D\uD83C\uDFA4", + "female-student" to "\uD83D\uDC69\u200D\uD83C\uDF93", + "female-teacher" to "\uD83D\uDC69\u200D\uD83C\uDFEB", + "female-technologist" to "\uD83D\uDC69\u200D\uD83D\uDCBB", + "female_elf" to "\uD83E\uDDDD\u200D\u2640\uFE0F", + "female_fairy" to "\uD83E\uDDDA\u200D\u2640\uFE0F", + "female_genie" to "\uD83E\uDDDE\u200D\u2640\uFE0F", + "female_mage" to "\uD83E\uDDD9\u200D\u2640\uFE0F", + "female_sign" to "\u2640\uFE0F", + "female_superhero" to "\uD83E\uDDB8\u200D\u2640\uFE0F", + "female_supervillain" to "\uD83E\uDDB9\u200D\u2640\uFE0F", + "female_vampire" to "\uD83E\uDDDB\u200D\u2640\uFE0F", + "female_zombie" to "\uD83E\uDDDF\u200D\u2640\uFE0F", + "fencer" to "\uD83E\uDD3A", + "ferris_wheel" to "\uD83C\uDFA1", + "ferry" to "\u26F4\uFE0F", + "field_hockey_stick_and_ball" to "\uD83C\uDFD1", + "file_cabinet" to "\uD83D\uDDC4\uFE0F", + "file_folder" to "\uD83D\uDCC1", + "film_frames" to "\uD83C\uDF9E\uFE0F", + "film_projector" to "\uD83D\uDCFD\uFE0F", + "fingerprint" to "\uD83E\uDEC6", + "fire" to "\uD83D\uDD25", + "fire_engine" to "\uD83D\uDE92", + "fire_extinguisher" to "\uD83E\uDDEF", + "firecracker" to "\uD83E\uDDE8", + "firefighter" to "\uD83E\uDDD1\u200D\uD83D\uDE92", + "fireworks" to "\uD83C\uDF86", + "first_place_medal" to "\uD83E\uDD47", + "first_quarter_moon" to "\uD83C\uDF13", + "first_quarter_moon_with_face" to "\uD83C\uDF1B", + "fish" to "\uD83D\uDC1F", + "fish_cake" to "\uD83C\uDF65", + "fishing_pole_and_fish" to "\uD83C\uDFA3", + "fist" to "\u270A", + "five" to "\u0035\uFE0F\u20E3", + "flag-ac" to "\uD83C\uDDE6\uD83C\uDDE8", + "flag-ad" to "\uD83C\uDDE6\uD83C\uDDE9", + "flag-ae" to "\uD83C\uDDE6\uD83C\uDDEA", + "flag-af" to "\uD83C\uDDE6\uD83C\uDDEB", + "flag-ag" to "\uD83C\uDDE6\uD83C\uDDEC", + "flag-ai" to "\uD83C\uDDE6\uD83C\uDDEE", + "flag-al" to "\uD83C\uDDE6\uD83C\uDDF1", + "flag-am" to "\uD83C\uDDE6\uD83C\uDDF2", + "flag-ao" to "\uD83C\uDDE6\uD83C\uDDF4", + "flag-aq" to "\uD83C\uDDE6\uD83C\uDDF6", + "flag-ar" to "\uD83C\uDDE6\uD83C\uDDF7", + "flag-as" to "\uD83C\uDDE6\uD83C\uDDF8", + "flag-at" to "\uD83C\uDDE6\uD83C\uDDF9", + "flag-au" to "\uD83C\uDDE6\uD83C\uDDFA", + "flag-aw" to "\uD83C\uDDE6\uD83C\uDDFC", + "flag-ax" to "\uD83C\uDDE6\uD83C\uDDFD", + "flag-az" to "\uD83C\uDDE6\uD83C\uDDFF", + "flag-ba" to "\uD83C\uDDE7\uD83C\uDDE6", + "flag-bb" to "\uD83C\uDDE7\uD83C\uDDE7", + "flag-bd" to "\uD83C\uDDE7\uD83C\uDDE9", + "flag-be" to "\uD83C\uDDE7\uD83C\uDDEA", + "flag-bf" to "\uD83C\uDDE7\uD83C\uDDEB", + "flag-bg" to "\uD83C\uDDE7\uD83C\uDDEC", + "flag-bh" to "\uD83C\uDDE7\uD83C\uDDED", + "flag-bi" to "\uD83C\uDDE7\uD83C\uDDEE", + "flag-bj" to "\uD83C\uDDE7\uD83C\uDDEF", + "flag-bl" to "\uD83C\uDDE7\uD83C\uDDF1", + "flag-bm" to "\uD83C\uDDE7\uD83C\uDDF2", + "flag-bn" to "\uD83C\uDDE7\uD83C\uDDF3", + "flag-bo" to "\uD83C\uDDE7\uD83C\uDDF4", + "flag-bq" to "\uD83C\uDDE7\uD83C\uDDF6", + "flag-br" to "\uD83C\uDDE7\uD83C\uDDF7", + "flag-bs" to "\uD83C\uDDE7\uD83C\uDDF8", + "flag-bt" to "\uD83C\uDDE7\uD83C\uDDF9", + "flag-bv" to "\uD83C\uDDE7\uD83C\uDDFB", + "flag-bw" to "\uD83C\uDDE7\uD83C\uDDFC", + "flag-by" to "\uD83C\uDDE7\uD83C\uDDFE", + "flag-bz" to "\uD83C\uDDE7\uD83C\uDDFF", + "flag-ca" to "\uD83C\uDDE8\uD83C\uDDE6", + "flag-cc" to "\uD83C\uDDE8\uD83C\uDDE8", + "flag-cd" to "\uD83C\uDDE8\uD83C\uDDE9", + "flag-cf" to "\uD83C\uDDE8\uD83C\uDDEB", + "flag-cg" to "\uD83C\uDDE8\uD83C\uDDEC", + "flag-ch" to "\uD83C\uDDE8\uD83C\uDDED", + "flag-ci" to "\uD83C\uDDE8\uD83C\uDDEE", + "flag-ck" to "\uD83C\uDDE8\uD83C\uDDF0", + "flag-cl" to "\uD83C\uDDE8\uD83C\uDDF1", + "flag-cm" to "\uD83C\uDDE8\uD83C\uDDF2", + "flag-cn" to "\uD83C\uDDE8\uD83C\uDDF3", + "flag-co" to "\uD83C\uDDE8\uD83C\uDDF4", + "flag-cp" to "\uD83C\uDDE8\uD83C\uDDF5", + "flag-cr" to "\uD83C\uDDE8\uD83C\uDDF7", + "flag-cu" to "\uD83C\uDDE8\uD83C\uDDFA", + "flag-cv" to "\uD83C\uDDE8\uD83C\uDDFB", + "flag-cw" to "\uD83C\uDDE8\uD83C\uDDFC", + "flag-cx" to "\uD83C\uDDE8\uD83C\uDDFD", + "flag-cy" to "\uD83C\uDDE8\uD83C\uDDFE", + "flag-cz" to "\uD83C\uDDE8\uD83C\uDDFF", + "flag-de" to "\uD83C\uDDE9\uD83C\uDDEA", + "flag-dg" to "\uD83C\uDDE9\uD83C\uDDEC", + "flag-dj" to "\uD83C\uDDE9\uD83C\uDDEF", + "flag-dk" to "\uD83C\uDDE9\uD83C\uDDF0", + "flag-dm" to "\uD83C\uDDE9\uD83C\uDDF2", + "flag-do" to "\uD83C\uDDE9\uD83C\uDDF4", + "flag-dz" to "\uD83C\uDDE9\uD83C\uDDFF", + "flag-ea" to "\uD83C\uDDEA\uD83C\uDDE6", + "flag-ec" to "\uD83C\uDDEA\uD83C\uDDE8", + "flag-ee" to "\uD83C\uDDEA\uD83C\uDDEA", + "flag-eg" to "\uD83C\uDDEA\uD83C\uDDEC", + "flag-eh" to "\uD83C\uDDEA\uD83C\uDDED", + "flag-england" to "\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F", + "flag-er" to "\uD83C\uDDEA\uD83C\uDDF7", + "flag-es" to "\uD83C\uDDEA\uD83C\uDDF8", + "flag-et" to "\uD83C\uDDEA\uD83C\uDDF9", + "flag-eu" to "\uD83C\uDDEA\uD83C\uDDFA", + "flag-fi" to "\uD83C\uDDEB\uD83C\uDDEE", + "flag-fj" to "\uD83C\uDDEB\uD83C\uDDEF", + "flag-fk" to "\uD83C\uDDEB\uD83C\uDDF0", + "flag-fm" to "\uD83C\uDDEB\uD83C\uDDF2", + "flag-fo" to "\uD83C\uDDEB\uD83C\uDDF4", + "flag-fr" to "\uD83C\uDDEB\uD83C\uDDF7", + "flag-ga" to "\uD83C\uDDEC\uD83C\uDDE6", + "flag-gb" to "\uD83C\uDDEC\uD83C\uDDE7", + "flag-gd" to "\uD83C\uDDEC\uD83C\uDDE9", + "flag-ge" to "\uD83C\uDDEC\uD83C\uDDEA", + "flag-gf" to "\uD83C\uDDEC\uD83C\uDDEB", + "flag-gg" to "\uD83C\uDDEC\uD83C\uDDEC", + "flag-gh" to "\uD83C\uDDEC\uD83C\uDDED", + "flag-gi" to "\uD83C\uDDEC\uD83C\uDDEE", + "flag-gl" to "\uD83C\uDDEC\uD83C\uDDF1", + "flag-gm" to "\uD83C\uDDEC\uD83C\uDDF2", + "flag-gn" to "\uD83C\uDDEC\uD83C\uDDF3", + "flag-gp" to "\uD83C\uDDEC\uD83C\uDDF5", + "flag-gq" to "\uD83C\uDDEC\uD83C\uDDF6", + "flag-gr" to "\uD83C\uDDEC\uD83C\uDDF7", + "flag-gs" to "\uD83C\uDDEC\uD83C\uDDF8", + "flag-gt" to "\uD83C\uDDEC\uD83C\uDDF9", + "flag-gu" to "\uD83C\uDDEC\uD83C\uDDFA", + "flag-gw" to "\uD83C\uDDEC\uD83C\uDDFC", + "flag-gy" to "\uD83C\uDDEC\uD83C\uDDFE", + "flag-hk" to "\uD83C\uDDED\uD83C\uDDF0", + "flag-hm" to "\uD83C\uDDED\uD83C\uDDF2", + "flag-hn" to "\uD83C\uDDED\uD83C\uDDF3", + "flag-hr" to "\uD83C\uDDED\uD83C\uDDF7", + "flag-ht" to "\uD83C\uDDED\uD83C\uDDF9", + "flag-hu" to "\uD83C\uDDED\uD83C\uDDFA", + "flag-ic" to "\uD83C\uDDEE\uD83C\uDDE8", + "flag-id" to "\uD83C\uDDEE\uD83C\uDDE9", + "flag-ie" to "\uD83C\uDDEE\uD83C\uDDEA", + "flag-il" to "\uD83C\uDDEE\uD83C\uDDF1", + "flag-im" to "\uD83C\uDDEE\uD83C\uDDF2", + "flag-in" to "\uD83C\uDDEE\uD83C\uDDF3", + "flag-io" to "\uD83C\uDDEE\uD83C\uDDF4", + "flag-iq" to "\uD83C\uDDEE\uD83C\uDDF6", + "flag-ir" to "\uD83C\uDDEE\uD83C\uDDF7", + "flag-is" to "\uD83C\uDDEE\uD83C\uDDF8", + "flag-it" to "\uD83C\uDDEE\uD83C\uDDF9", + "flag-je" to "\uD83C\uDDEF\uD83C\uDDEA", + "flag-jm" to "\uD83C\uDDEF\uD83C\uDDF2", + "flag-jo" to "\uD83C\uDDEF\uD83C\uDDF4", + "flag-jp" to "\uD83C\uDDEF\uD83C\uDDF5", + "flag-ke" to "\uD83C\uDDF0\uD83C\uDDEA", + "flag-kg" to "\uD83C\uDDF0\uD83C\uDDEC", + "flag-kh" to "\uD83C\uDDF0\uD83C\uDDED", + "flag-ki" to "\uD83C\uDDF0\uD83C\uDDEE", + "flag-km" to "\uD83C\uDDF0\uD83C\uDDF2", + "flag-kn" to "\uD83C\uDDF0\uD83C\uDDF3", + "flag-kp" to "\uD83C\uDDF0\uD83C\uDDF5", + "flag-kr" to "\uD83C\uDDF0\uD83C\uDDF7", + "flag-kw" to "\uD83C\uDDF0\uD83C\uDDFC", + "flag-ky" to "\uD83C\uDDF0\uD83C\uDDFE", + "flag-kz" to "\uD83C\uDDF0\uD83C\uDDFF", + "flag-la" to "\uD83C\uDDF1\uD83C\uDDE6", + "flag-lb" to "\uD83C\uDDF1\uD83C\uDDE7", + "flag-lc" to "\uD83C\uDDF1\uD83C\uDDE8", + "flag-li" to "\uD83C\uDDF1\uD83C\uDDEE", + "flag-lk" to "\uD83C\uDDF1\uD83C\uDDF0", + "flag-lr" to "\uD83C\uDDF1\uD83C\uDDF7", + "flag-ls" to "\uD83C\uDDF1\uD83C\uDDF8", + "flag-lt" to "\uD83C\uDDF1\uD83C\uDDF9", + "flag-lu" to "\uD83C\uDDF1\uD83C\uDDFA", + "flag-lv" to "\uD83C\uDDF1\uD83C\uDDFB", + "flag-ly" to "\uD83C\uDDF1\uD83C\uDDFE", + "flag-ma" to "\uD83C\uDDF2\uD83C\uDDE6", + "flag-mc" to "\uD83C\uDDF2\uD83C\uDDE8", + "flag-md" to "\uD83C\uDDF2\uD83C\uDDE9", + "flag-me" to "\uD83C\uDDF2\uD83C\uDDEA", + "flag-mf" to "\uD83C\uDDF2\uD83C\uDDEB", + "flag-mg" to "\uD83C\uDDF2\uD83C\uDDEC", + "flag-mh" to "\uD83C\uDDF2\uD83C\uDDED", + "flag-mk" to "\uD83C\uDDF2\uD83C\uDDF0", + "flag-ml" to "\uD83C\uDDF2\uD83C\uDDF1", + "flag-mm" to "\uD83C\uDDF2\uD83C\uDDF2", + "flag-mn" to "\uD83C\uDDF2\uD83C\uDDF3", + "flag-mo" to "\uD83C\uDDF2\uD83C\uDDF4", + "flag-mp" to "\uD83C\uDDF2\uD83C\uDDF5", + "flag-mq" to "\uD83C\uDDF2\uD83C\uDDF6", + "flag-mr" to "\uD83C\uDDF2\uD83C\uDDF7", + "flag-ms" to "\uD83C\uDDF2\uD83C\uDDF8", + "flag-mt" to "\uD83C\uDDF2\uD83C\uDDF9", + "flag-mu" to "\uD83C\uDDF2\uD83C\uDDFA", + "flag-mv" to "\uD83C\uDDF2\uD83C\uDDFB", + "flag-mw" to "\uD83C\uDDF2\uD83C\uDDFC", + "flag-mx" to "\uD83C\uDDF2\uD83C\uDDFD", + "flag-my" to "\uD83C\uDDF2\uD83C\uDDFE", + "flag-mz" to "\uD83C\uDDF2\uD83C\uDDFF", + "flag-na" to "\uD83C\uDDF3\uD83C\uDDE6", + "flag-nc" to "\uD83C\uDDF3\uD83C\uDDE8", + "flag-ne" to "\uD83C\uDDF3\uD83C\uDDEA", + "flag-nf" to "\uD83C\uDDF3\uD83C\uDDEB", + "flag-ng" to "\uD83C\uDDF3\uD83C\uDDEC", + "flag-ni" to "\uD83C\uDDF3\uD83C\uDDEE", + "flag-nl" to "\uD83C\uDDF3\uD83C\uDDF1", + "flag-no" to "\uD83C\uDDF3\uD83C\uDDF4", + "flag-np" to "\uD83C\uDDF3\uD83C\uDDF5", + "flag-nr" to "\uD83C\uDDF3\uD83C\uDDF7", + "flag-nu" to "\uD83C\uDDF3\uD83C\uDDFA", + "flag-nz" to "\uD83C\uDDF3\uD83C\uDDFF", + "flag-om" to "\uD83C\uDDF4\uD83C\uDDF2", + "flag-pa" to "\uD83C\uDDF5\uD83C\uDDE6", + "flag-pe" to "\uD83C\uDDF5\uD83C\uDDEA", + "flag-pf" to "\uD83C\uDDF5\uD83C\uDDEB", + "flag-pg" to "\uD83C\uDDF5\uD83C\uDDEC", + "flag-ph" to "\uD83C\uDDF5\uD83C\uDDED", + "flag-pk" to "\uD83C\uDDF5\uD83C\uDDF0", + "flag-pl" to "\uD83C\uDDF5\uD83C\uDDF1", + "flag-pm" to "\uD83C\uDDF5\uD83C\uDDF2", + "flag-pn" to "\uD83C\uDDF5\uD83C\uDDF3", + "flag-pr" to "\uD83C\uDDF5\uD83C\uDDF7", + "flag-ps" to "\uD83C\uDDF5\uD83C\uDDF8", + "flag-pt" to "\uD83C\uDDF5\uD83C\uDDF9", + "flag-pw" to "\uD83C\uDDF5\uD83C\uDDFC", + "flag-py" to "\uD83C\uDDF5\uD83C\uDDFE", + "flag-qa" to "\uD83C\uDDF6\uD83C\uDDE6", + "flag-re" to "\uD83C\uDDF7\uD83C\uDDEA", + "flag-ro" to "\uD83C\uDDF7\uD83C\uDDF4", + "flag-rs" to "\uD83C\uDDF7\uD83C\uDDF8", + "flag-ru" to "\uD83C\uDDF7\uD83C\uDDFA", + "flag-rw" to "\uD83C\uDDF7\uD83C\uDDFC", + "flag-sa" to "\uD83C\uDDF8\uD83C\uDDE6", + "flag-sark" to "\uD83C\uDDE8\uD83C\uDDF6", + "flag-sb" to "\uD83C\uDDF8\uD83C\uDDE7", + "flag-sc" to "\uD83C\uDDF8\uD83C\uDDE8", + "flag-scotland" to "\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74\uDB40\uDC7F", + "flag-sd" to "\uD83C\uDDF8\uD83C\uDDE9", + "flag-se" to "\uD83C\uDDF8\uD83C\uDDEA", + "flag-sg" to "\uD83C\uDDF8\uD83C\uDDEC", + "flag-sh" to "\uD83C\uDDF8\uD83C\uDDED", + "flag-si" to "\uD83C\uDDF8\uD83C\uDDEE", + "flag-sj" to "\uD83C\uDDF8\uD83C\uDDEF", + "flag-sk" to "\uD83C\uDDF8\uD83C\uDDF0", + "flag-sl" to "\uD83C\uDDF8\uD83C\uDDF1", + "flag-sm" to "\uD83C\uDDF8\uD83C\uDDF2", + "flag-sn" to "\uD83C\uDDF8\uD83C\uDDF3", + "flag-so" to "\uD83C\uDDF8\uD83C\uDDF4", + "flag-sr" to "\uD83C\uDDF8\uD83C\uDDF7", + "flag-ss" to "\uD83C\uDDF8\uD83C\uDDF8", + "flag-st" to "\uD83C\uDDF8\uD83C\uDDF9", + "flag-sv" to "\uD83C\uDDF8\uD83C\uDDFB", + "flag-sx" to "\uD83C\uDDF8\uD83C\uDDFD", + "flag-sy" to "\uD83C\uDDF8\uD83C\uDDFE", + "flag-sz" to "\uD83C\uDDF8\uD83C\uDDFF", + "flag-ta" to "\uD83C\uDDF9\uD83C\uDDE6", + "flag-tc" to "\uD83C\uDDF9\uD83C\uDDE8", + "flag-td" to "\uD83C\uDDF9\uD83C\uDDE9", + "flag-tf" to "\uD83C\uDDF9\uD83C\uDDEB", + "flag-tg" to "\uD83C\uDDF9\uD83C\uDDEC", + "flag-th" to "\uD83C\uDDF9\uD83C\uDDED", + "flag-tj" to "\uD83C\uDDF9\uD83C\uDDEF", + "flag-tk" to "\uD83C\uDDF9\uD83C\uDDF0", + "flag-tl" to "\uD83C\uDDF9\uD83C\uDDF1", + "flag-tm" to "\uD83C\uDDF9\uD83C\uDDF2", + "flag-tn" to "\uD83C\uDDF9\uD83C\uDDF3", + "flag-to" to "\uD83C\uDDF9\uD83C\uDDF4", + "flag-tr" to "\uD83C\uDDF9\uD83C\uDDF7", + "flag-tt" to "\uD83C\uDDF9\uD83C\uDDF9", + "flag-tv" to "\uD83C\uDDF9\uD83C\uDDFB", + "flag-tw" to "\uD83C\uDDF9\uD83C\uDDFC", + "flag-tz" to "\uD83C\uDDF9\uD83C\uDDFF", + "flag-ua" to "\uD83C\uDDFA\uD83C\uDDE6", + "flag-ug" to "\uD83C\uDDFA\uD83C\uDDEC", + "flag-um" to "\uD83C\uDDFA\uD83C\uDDF2", + "flag-un" to "\uD83C\uDDFA\uD83C\uDDF3", + "flag-us" to "\uD83C\uDDFA\uD83C\uDDF8", + "flag-uy" to "\uD83C\uDDFA\uD83C\uDDFE", + "flag-uz" to "\uD83C\uDDFA\uD83C\uDDFF", + "flag-va" to "\uD83C\uDDFB\uD83C\uDDE6", + "flag-vc" to "\uD83C\uDDFB\uD83C\uDDE8", + "flag-ve" to "\uD83C\uDDFB\uD83C\uDDEA", + "flag-vg" to "\uD83C\uDDFB\uD83C\uDDEC", + "flag-vi" to "\uD83C\uDDFB\uD83C\uDDEE", + "flag-vn" to "\uD83C\uDDFB\uD83C\uDDF3", + "flag-vu" to "\uD83C\uDDFB\uD83C\uDDFA", + "flag-wales" to "\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73\uDB40\uDC7F", + "flag-wf" to "\uD83C\uDDFC\uD83C\uDDEB", + "flag-ws" to "\uD83C\uDDFC\uD83C\uDDF8", + "flag-xk" to "\uD83C\uDDFD\uD83C\uDDF0", + "flag-ye" to "\uD83C\uDDFE\uD83C\uDDEA", + "flag-yt" to "\uD83C\uDDFE\uD83C\uDDF9", + "flag-za" to "\uD83C\uDDFF\uD83C\uDDE6", + "flag-zm" to "\uD83C\uDDFF\uD83C\uDDF2", + "flag-zw" to "\uD83C\uDDFF\uD83C\uDDFC", + "flags" to "\uD83C\uDF8F", + "flamingo" to "\uD83E\uDDA9", + "flashlight" to "\uD83D\uDD26", + "flatbread" to "\uD83E\uDED3", + "fleur_de_lis" to "\u269C\uFE0F", + "flipper" to "\uD83D\uDC2C", + "floppy_disk" to "\uD83D\uDCBE", + "flower_playing_cards" to "\uD83C\uDFB4", + "flushed" to "\uD83D\uDE33", + "flute" to "\uD83E\uDE88", + "fly" to "\uD83E\uDEB0", + "flying_disc" to "\uD83E\uDD4F", + "flying_saucer" to "\uD83D\uDEF8", + "fog" to "\uD83C\uDF2B\uFE0F", + "foggy" to "\uD83C\uDF01", + "folding_hand_fan" to "\uD83E\uDEAD", + "fondue" to "\uD83E\uDED5", + "foot" to "\uD83E\uDDB6", + "football" to "\uD83C\uDFC8", + "footprints" to "\uD83D\uDC63", + "fork_and_knife" to "\uD83C\uDF74", + "fortune_cookie" to "\uD83E\uDD60", + "fountain" to "\u26F2", + "four" to "\u0034\uFE0F\u20E3", + "four_leaf_clover" to "\uD83C\uDF40", + "fox_face" to "\uD83E\uDD8A", + "fr" to "\uD83C\uDDEB\uD83C\uDDF7", + "frame_with_picture" to "\uD83D\uDDBC\uFE0F", + "free" to "\uD83C\uDD93", + "fried_egg" to "\uD83C\uDF73", + "fried_shrimp" to "\uD83C\uDF64", + "fries" to "\uD83C\uDF5F", + "frog" to "\uD83D\uDC38", + "frowning" to "\uD83D\uDE26", + "fuelpump" to "\u26FD", + "full_moon" to "\uD83C\uDF15", + "full_moon_with_face" to "\uD83C\uDF1D", + "funeral_urn" to "\u26B1\uFE0F", + "game_die" to "\uD83C\uDFB2", + "garlic" to "\uD83E\uDDC4", + "gb" to "\uD83C\uDDEC\uD83C\uDDE7", + "gear" to "\u2699\uFE0F", + "gem" to "\uD83D\uDC8E", + "gemini" to "\u264A", + "genie" to "\uD83E\uDDDE", + "ghost" to "\uD83D\uDC7B", + "gift" to "\uD83C\uDF81", + "gift_heart" to "\uD83D\uDC9D", + "ginger_root" to "\uD83E\uDEDA", + "giraffe_face" to "\uD83E\uDD92", + "girl" to "\uD83D\uDC67", + "glass_of_milk" to "\uD83E\uDD5B", + "globe_with_meridians" to "\uD83C\uDF10", + "gloves" to "\uD83E\uDDE4", + "goal_net" to "\uD83E\uDD45", + "goat" to "\uD83D\uDC10", + "goggles" to "\uD83E\uDD7D", + "golf" to "\u26F3", + "golfer" to "\uD83C\uDFCC\uFE0F", + "goose" to "\uD83E\uDEBF", + "gorilla" to "\uD83E\uDD8D", + "grapes" to "\uD83C\uDF47", + "green_apple" to "\uD83C\uDF4F", + "green_book" to "\uD83D\uDCD7", + "green_heart" to "\uD83D\uDC9A", + "green_salad" to "\uD83E\uDD57", + "grey_exclamation" to "\u2755", + "grey_heart" to "\uD83E\uDE76", + "grey_question" to "\u2754", + "grimacing" to "\uD83D\uDE2C", + "grin" to "\uD83D\uDE01", + "grinning" to "\uD83D\uDE00", + "grinning_face_with_one_large_and_one_small_eye" to "\uD83E\uDD2A", + "grinning_face_with_star_eyes" to "\uD83E\uDD29", + "guardsman" to "\uD83D\uDC82", + "guide_dog" to "\uD83E\uDDAE", + "guitar" to "\uD83C\uDFB8", + "gun" to "\uD83D\uDD2B", + "hair_pick" to "\uD83E\uDEAE", + "haircut" to "\uD83D\uDC87", + "hamburger" to "\uD83C\uDF54", + "hammer" to "\uD83D\uDD28", + "hammer_and_pick" to "\u2692\uFE0F", + "hammer_and_wrench" to "\uD83D\uDEE0\uFE0F", + "hamsa" to "\uD83E\uDEAC", + "hamster" to "\uD83D\uDC39", + "hand" to "\u270B", + "hand_with_index_and_middle_fingers_crossed" to "\uD83E\uDD1E", + "hand_with_index_finger_and_thumb_crossed" to "\uD83E\uDEF0", + "handbag" to "\uD83D\uDC5C", + "handball" to "\uD83E\uDD3E", + "handshake" to "\uD83E\uDD1D", + "hankey" to "\uD83D\uDCA9", + "harp" to "\uD83E\uDE89", + "hash" to "\u0023\uFE0F\u20E3", + "hatched_chick" to "\uD83D\uDC25", + "hatching_chick" to "\uD83D\uDC23", + "head_shaking_horizontally" to "\uD83D\uDE42\u200D\u2194\uFE0F", + "head_shaking_vertically" to "\uD83D\uDE42\u200D\u2195\uFE0F", + "headphones" to "\uD83C\uDFA7", + "headstone" to "\uD83E\uDEA6", + "health_worker" to "\uD83E\uDDD1\u200D\u2695\uFE0F", + "hear_no_evil" to "\uD83D\uDE49", + "heart" to "\u2764\uFE0F", + "heart_decoration" to "\uD83D\uDC9F", + "heart_eyes" to "\uD83D\uDE0D", + "heart_eyes_cat" to "\uD83D\uDE3B", + "heart_hands" to "\uD83E\uDEF6", + "heart_on_fire" to "\u2764\uFE0F\u200D\uD83D\uDD25", + "heartbeat" to "\uD83D\uDC93", + "heartpulse" to "\uD83D\uDC97", + "hearts" to "\u2665\uFE0F", + "heavy_check_mark" to "\u2714\uFE0F", + "heavy_division_sign" to "\u2797", + "heavy_dollar_sign" to "\uD83D\uDCB2", + "heavy_equals_sign" to "\uD83D\uDFF0", + "heavy_exclamation_mark" to "\u2757", + "heavy_heart_exclamation_mark_ornament" to "\u2763\uFE0F", + "heavy_minus_sign" to "\u2796", + "heavy_multiplication_x" to "\u2716\uFE0F", + "heavy_plus_sign" to "\u2795", + "hedgehog" to "\uD83E\uDD94", + "helicopter" to "\uD83D\uDE81", + "helmet_with_white_cross" to "\u26D1\uFE0F", + "herb" to "\uD83C\uDF3F", + "hibiscus" to "\uD83C\uDF3A", + "high_brightness" to "\uD83D\uDD06", + "high_heel" to "\uD83D\uDC60", + "hiking_boot" to "\uD83E\uDD7E", + "hindu_temple" to "\uD83D\uDED5", + "hippopotamus" to "\uD83E\uDD9B", + "hocho" to "\uD83D\uDD2A", + "hole" to "\uD83D\uDD73\uFE0F", + "honey_pot" to "\uD83C\uDF6F", + "honeybee" to "\uD83D\uDC1D", + "hook" to "\uD83E\uDE9D", + "horse" to "\uD83D\uDC34", + "horse_racing" to "\uD83C\uDFC7", + "hospital" to "\uD83C\uDFE5", + "hot_face" to "\uD83E\uDD75", + "hot_pepper" to "\uD83C\uDF36\uFE0F", + "hotdog" to "\uD83C\uDF2D", + "hotel" to "\uD83C\uDFE8", + "hotsprings" to "\u2668\uFE0F", + "hourglass" to "\u231B", + "hourglass_flowing_sand" to "\u23F3", + "house" to "\uD83C\uDFE0", + "house_buildings" to "\uD83C\uDFD8\uFE0F", + "house_with_garden" to "\uD83C\uDFE1", + "hugging_face" to "\uD83E\uDD17", + "hushed" to "\uD83D\uDE2F", + "hut" to "\uD83D\uDED6", + "hyacinth" to "\uD83E\uDEBB", + "i_love_you_hand_sign" to "\uD83E\uDD1F", + "ice_cream" to "\uD83C\uDF68", + "ice_cube" to "\uD83E\uDDCA", + "ice_hockey_stick_and_puck" to "\uD83C\uDFD2", + "ice_skate" to "\u26F8\uFE0F", + "icecream" to "\uD83C\uDF66", + "id" to "\uD83C\uDD94", + "identification_card" to "\uD83E\uDEAA", + "ideograph_advantage" to "\uD83C\uDE50", + "imp" to "\uD83D\uDC7F", + "inbox_tray" to "\uD83D\uDCE5", + "incoming_envelope" to "\uD83D\uDCE8", + "index_pointing_at_the_viewer" to "\uD83E\uDEF5", + "infinity" to "\u267E\uFE0F", + "information_desk_person" to "\uD83D\uDC81", + "information_source" to "\u2139\uFE0F", + "innocent" to "\uD83D\uDE07", + "interrobang" to "\u2049\uFE0F", + "iphone" to "\uD83D\uDCF1", + "it" to "\uD83C\uDDEE\uD83C\uDDF9", + "izakaya_lantern" to "\uD83C\uDFEE", + "jack_o_lantern" to "\uD83C\uDF83", + "japan" to "\uD83D\uDDFE", + "japanese_castle" to "\uD83C\uDFEF", + "japanese_goblin" to "\uD83D\uDC7A", + "japanese_ogre" to "\uD83D\uDC79", + "jar" to "\uD83E\uDED9", + "jeans" to "\uD83D\uDC56", + "jellyfish" to "\uD83E\uDEBC", + "jigsaw" to "\uD83E\uDDE9", + "joy" to "\uD83D\uDE02", + "joy_cat" to "\uD83D\uDE39", + "joystick" to "\uD83D\uDD79\uFE0F", + "jp" to "\uD83C\uDDEF\uD83C\uDDF5", + "judge" to "\uD83E\uDDD1\u200D\u2696\uFE0F", + "juggling" to "\uD83E\uDD39", + "kaaba" to "\uD83D\uDD4B", + "kangaroo" to "\uD83E\uDD98", + "key" to "\uD83D\uDD11", + "keyboard" to "\u2328\uFE0F", + "keycap_star" to "\u002A\uFE0F\u20E3", + "keycap_ten" to "\uD83D\uDD1F", + "khanda" to "\uD83E\uDEAF", + "kimono" to "\uD83D\uDC58", + "kiss" to "\uD83D\uDC8B", + "kissing" to "\uD83D\uDE17", + "kissing_cat" to "\uD83D\uDE3D", + "kissing_closed_eyes" to "\uD83D\uDE1A", + "kissing_heart" to "\uD83D\uDE18", + "kissing_smiling_eyes" to "\uD83D\uDE19", + "kite" to "\uD83E\uDE81", + "kiwifruit" to "\uD83E\uDD5D", + "kneeling_person" to "\uD83E\uDDCE", + "knife" to "\uD83D\uDD2A", + "knife_fork_plate" to "\uD83C\uDF7D\uFE0F", + "knot" to "\uD83E\uDEA2", + "koala" to "\uD83D\uDC28", + "koko" to "\uD83C\uDE01", + "kr" to "\uD83C\uDDF0\uD83C\uDDF7", + "lab_coat" to "\uD83E\uDD7C", + "label" to "\uD83C\uDFF7\uFE0F", + "lacrosse" to "\uD83E\uDD4D", + "ladder" to "\uD83E\uDE9C", + "lady_beetle" to "\uD83D\uDC1E", + "ladybug" to "\uD83D\uDC1E", + "lantern" to "\uD83C\uDFEE", + "large_blue_circle" to "\uD83D\uDD35", + "large_blue_diamond" to "\uD83D\uDD37", + "large_blue_square" to "\uD83D\uDFE6", + "large_brown_circle" to "\uD83D\uDFE4", + "large_brown_square" to "\uD83D\uDFEB", + "large_green_circle" to "\uD83D\uDFE2", + "large_green_square" to "\uD83D\uDFE9", + "large_orange_circle" to "\uD83D\uDFE0", + "large_orange_diamond" to "\uD83D\uDD36", + "large_orange_square" to "\uD83D\uDFE7", + "large_purple_circle" to "\uD83D\uDFE3", + "large_purple_square" to "\uD83D\uDFEA", + "large_red_square" to "\uD83D\uDFE5", + "large_yellow_circle" to "\uD83D\uDFE1", + "large_yellow_square" to "\uD83D\uDFE8", + "last_quarter_moon" to "\uD83C\uDF17", + "last_quarter_moon_with_face" to "\uD83C\uDF1C", + "latin_cross" to "\u271D\uFE0F", + "laughing" to "\uD83D\uDE06", + "leafless_tree" to "\uD83E\uDEBE", + "leafy_green" to "\uD83E\uDD6C", + "leaves" to "\uD83C\uDF43", + "ledger" to "\uD83D\uDCD2", + "left-facing_fist" to "\uD83E\uDD1B", + "left_luggage" to "\uD83D\uDEC5", + "left_right_arrow" to "\u2194\uFE0F", + "left_speech_bubble" to "\uD83D\uDDE8\uFE0F", + "leftwards_arrow_with_hook" to "\u21A9\uFE0F", + "leftwards_hand" to "\uD83E\uDEF2", + "leftwards_pushing_hand" to "\uD83E\uDEF7", + "leg" to "\uD83E\uDDB5", + "lemon" to "\uD83C\uDF4B", + "leo" to "\u264C", + "leopard" to "\uD83D\uDC06", + "level_slider" to "\uD83C\uDF9A\uFE0F", + "libra" to "\u264E", + "light_blue_heart" to "\uD83E\uDE75", + "light_rail" to "\uD83D\uDE88", + "lightning" to "\uD83C\uDF29\uFE0F", + "lightning_cloud" to "\uD83C\uDF29\uFE0F", + "lime" to "\uD83C\uDF4B\u200D\uD83D\uDFE9", + "link" to "\uD83D\uDD17", + "linked_paperclips" to "\uD83D\uDD87\uFE0F", + "lion_face" to "\uD83E\uDD81", + "lips" to "\uD83D\uDC44", + "lipstick" to "\uD83D\uDC84", + "lizard" to "\uD83E\uDD8E", + "llama" to "\uD83E\uDD99", + "lobster" to "\uD83E\uDD9E", + "lock" to "\uD83D\uDD12", + "lock_with_ink_pen" to "\uD83D\uDD0F", + "lollipop" to "\uD83C\uDF6D", + "long_drum" to "\uD83E\uDE98", + "loop" to "\u27BF", + "lotion_bottle" to "\uD83E\uDDF4", + "lotus" to "\uD83E\uDEB7", + "loud_sound" to "\uD83D\uDD0A", + "loudspeaker" to "\uD83D\uDCE2", + "love_hotel" to "\uD83C\uDFE9", + "love_letter" to "\uD83D\uDC8C", + "low_battery" to "\uD83E\uDEAB", + "low_brightness" to "\uD83D\uDD05", + "lower_left_ballpoint_pen" to "\uD83D\uDD8A\uFE0F", + "lower_left_crayon" to "\uD83D\uDD8D\uFE0F", + "lower_left_fountain_pen" to "\uD83D\uDD8B\uFE0F", + "lower_left_paintbrush" to "\uD83D\uDD8C\uFE0F", + "luggage" to "\uD83E\uDDF3", + "lungs" to "\uD83E\uDEC1", + "lying_face" to "\uD83E\uDD25", + "m" to "\u24C2\uFE0F", + "mag" to "\uD83D\uDD0D", + "mag_right" to "\uD83D\uDD0E", + "mage" to "\uD83E\uDDD9", + "magic_wand" to "\uD83E\uDE84", + "magnet" to "\uD83E\uDDF2", + "mahjong" to "\uD83C\uDC04", + "mailbox" to "\uD83D\uDCEB", + "mailbox_closed" to "\uD83D\uDCEA", + "mailbox_with_mail" to "\uD83D\uDCEC", + "mailbox_with_no_mail" to "\uD83D\uDCED", + "male-artist" to "\uD83D\uDC68\u200D\uD83C\uDFA8", + "male-astronaut" to "\uD83D\uDC68\u200D\uD83D\uDE80", + "male-construction-worker" to "\uD83D\uDC77\u200D\u2642\uFE0F", + "male-cook" to "\uD83D\uDC68\u200D\uD83C\uDF73", + "male-detective" to "\uD83D\uDD75\uFE0F\u200D\u2642\uFE0F", + "male-doctor" to "\uD83D\uDC68\u200D\u2695\uFE0F", + "male-factory-worker" to "\uD83D\uDC68\u200D\uD83C\uDFED", + "male-farmer" to "\uD83D\uDC68\u200D\uD83C\uDF3E", + "male-firefighter" to "\uD83D\uDC68\u200D\uD83D\uDE92", + "male-guard" to "\uD83D\uDC82\u200D\u2642\uFE0F", + "male-judge" to "\uD83D\uDC68\u200D\u2696\uFE0F", + "male-mechanic" to "\uD83D\uDC68\u200D\uD83D\uDD27", + "male-office-worker" to "\uD83D\uDC68\u200D\uD83D\uDCBC", + "male-pilot" to "\uD83D\uDC68\u200D\u2708\uFE0F", + "male-police-officer" to "\uD83D\uDC6E\u200D\u2642\uFE0F", + "male-scientist" to "\uD83D\uDC68\u200D\uD83D\uDD2C", + "male-singer" to "\uD83D\uDC68\u200D\uD83C\uDFA4", + "male-student" to "\uD83D\uDC68\u200D\uD83C\uDF93", + "male-teacher" to "\uD83D\uDC68\u200D\uD83C\uDFEB", + "male-technologist" to "\uD83D\uDC68\u200D\uD83D\uDCBB", + "male_elf" to "\uD83E\uDDDD\u200D\u2642\uFE0F", + "male_fairy" to "\uD83E\uDDDA\u200D\u2642\uFE0F", + "male_genie" to "\uD83E\uDDDE\u200D\u2642\uFE0F", + "male_mage" to "\uD83E\uDDD9\u200D\u2642\uFE0F", + "male_sign" to "\u2642\uFE0F", + "male_superhero" to "\uD83E\uDDB8\u200D\u2642\uFE0F", + "male_supervillain" to "\uD83E\uDDB9\u200D\u2642\uFE0F", + "male_vampire" to "\uD83E\uDDDB\u200D\u2642\uFE0F", + "male_zombie" to "\uD83E\uDDDF\u200D\u2642\uFE0F", + "mammoth" to "\uD83E\uDDA3", + "man" to "\uD83D\uDC68", + "man-biking" to "\uD83D\uDEB4\u200D\u2642\uFE0F", + "man-bouncing-ball" to "\u26F9\uFE0F\u200D\u2642\uFE0F", + "man-bowing" to "\uD83D\uDE47\u200D\u2642\uFE0F", + "man-boy" to "\uD83D\uDC68\u200D\uD83D\uDC66", + "man-boy-boy" to "\uD83D\uDC68\u200D\uD83D\uDC66\u200D\uD83D\uDC66", + "man-cartwheeling" to "\uD83E\uDD38\u200D\u2642\uFE0F", + "man-facepalming" to "\uD83E\uDD26\u200D\u2642\uFE0F", + "man-frowning" to "\uD83D\uDE4D\u200D\u2642\uFE0F", + "man-gesturing-no" to "\uD83D\uDE45\u200D\u2642\uFE0F", + "man-gesturing-ok" to "\uD83D\uDE46\u200D\u2642\uFE0F", + "man-getting-haircut" to "\uD83D\uDC87\u200D\u2642\uFE0F", + "man-getting-massage" to "\uD83D\uDC86\u200D\u2642\uFE0F", + "man-girl" to "\uD83D\uDC68\u200D\uD83D\uDC67", + "man-girl-boy" to "\uD83D\uDC68\u200D\uD83D\uDC67\u200D\uD83D\uDC66", + "man-girl-girl" to "\uD83D\uDC68\u200D\uD83D\uDC67\u200D\uD83D\uDC67", + "man-golfing" to "\uD83C\uDFCC\uFE0F\u200D\u2642\uFE0F", + "man-heart-man" to "\uD83D\uDC68\u200D\u2764\uFE0F\u200D\uD83D\uDC68", + "man-juggling" to "\uD83E\uDD39\u200D\u2642\uFE0F", + "man-kiss-man" to "\uD83D\uDC68\u200D\u2764\uFE0F\u200D\uD83D\uDC8B\u200D\uD83D\uDC68", + "man-lifting-weights" to "\uD83C\uDFCB\uFE0F\u200D\u2642\uFE0F", + "man-man-boy" to "\uD83D\uDC68\u200D\uD83D\uDC68\u200D\uD83D\uDC66", + "man-man-boy-boy" to "\uD83D\uDC68\u200D\uD83D\uDC68\u200D\uD83D\uDC66\u200D\uD83D\uDC66", + "man-man-girl" to "\uD83D\uDC68\u200D\uD83D\uDC68\u200D\uD83D\uDC67", + "man-man-girl-boy" to "\uD83D\uDC68\u200D\uD83D\uDC68\u200D\uD83D\uDC67\u200D\uD83D\uDC66", + "man-man-girl-girl" to "\uD83D\uDC68\u200D\uD83D\uDC68\u200D\uD83D\uDC67\u200D\uD83D\uDC67", + "man-mountain-biking" to "\uD83D\uDEB5\u200D\u2642\uFE0F", + "man-playing-handball" to "\uD83E\uDD3E\u200D\u2642\uFE0F", + "man-playing-water-polo" to "\uD83E\uDD3D\u200D\u2642\uFE0F", + "man-pouting" to "\uD83D\uDE4E\u200D\u2642\uFE0F", + "man-raising-hand" to "\uD83D\uDE4B\u200D\u2642\uFE0F", + "man-rowing-boat" to "\uD83D\uDEA3\u200D\u2642\uFE0F", + "man-running" to "\uD83C\uDFC3\u200D\u2642\uFE0F", + "man-shrugging" to "\uD83E\uDD37\u200D\u2642\uFE0F", + "man-surfing" to "\uD83C\uDFC4\u200D\u2642\uFE0F", + "man-swimming" to "\uD83C\uDFCA\u200D\u2642\uFE0F", + "man-tipping-hand" to "\uD83D\uDC81\u200D\u2642\uFE0F", + "man-walking" to "\uD83D\uDEB6\u200D\u2642\uFE0F", + "man-wearing-turban" to "\uD83D\uDC73\u200D\u2642\uFE0F", + "man-with-bunny-ears-partying" to "\uD83D\uDC6F\u200D\u2642\uFE0F", + "man-woman-boy" to "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC66", + "man-woman-boy-boy" to "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66", + "man-woman-girl" to "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67", + "man-woman-girl-boy" to "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66", + "man-woman-girl-girl" to "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67", + "man-wrestling" to "\uD83E\uDD3C\u200D\u2642\uFE0F", + "man_and_woman_holding_hands" to "\uD83D\uDC6B", + "man_climbing" to "\uD83E\uDDD7\u200D\u2642\uFE0F", + "man_dancing" to "\uD83D\uDD7A", + "man_feeding_baby" to "\uD83D\uDC68\u200D\uD83C\uDF7C", + "man_in_business_suit_levitating" to "\uD83D\uDD74\uFE0F", + "man_in_lotus_position" to "\uD83E\uDDD8\u200D\u2642\uFE0F", + "man_in_manual_wheelchair" to "\uD83D\uDC68\u200D\uD83E\uDDBD", + "man_in_manual_wheelchair_facing_right" to "\uD83D\uDC68\u200D\uD83E\uDDBD\u200D\u27A1\uFE0F", + "man_in_motorized_wheelchair" to "\uD83D\uDC68\u200D\uD83E\uDDBC", + "man_in_motorized_wheelchair_facing_right" to "\uD83D\uDC68\u200D\uD83E\uDDBC\u200D\u27A1\uFE0F", + "man_in_steamy_room" to "\uD83E\uDDD6\u200D\u2642\uFE0F", + "man_in_tuxedo" to "\uD83E\uDD35\u200D\u2642\uFE0F", + "man_kneeling" to "\uD83E\uDDCE\u200D\u2642\uFE0F", + "man_kneeling_facing_right" to "\uD83E\uDDCE\u200D\u2642\uFE0F\u200D\u27A1\uFE0F", + "man_running_facing_right" to "\uD83C\uDFC3\u200D\u2642\uFE0F\u200D\u27A1\uFE0F", + "man_standing" to "\uD83E\uDDCD\u200D\u2642\uFE0F", + "man_walking_facing_right" to "\uD83D\uDEB6\u200D\u2642\uFE0F\u200D\u27A1\uFE0F", + "man_with_beard" to "\uD83E\uDDD4\u200D\u2642\uFE0F", + "man_with_gua_pi_mao" to "\uD83D\uDC72", + "man_with_probing_cane" to "\uD83D\uDC68\u200D\uD83E\uDDAF", + "man_with_turban" to "\uD83D\uDC73", + "man_with_veil" to "\uD83D\uDC70\u200D\u2642\uFE0F", + "man_with_white_cane_facing_right" to "\uD83D\uDC68\u200D\uD83E\uDDAF\u200D\u27A1\uFE0F", + "mango" to "\uD83E\uDD6D", + "mans_shoe" to "\uD83D\uDC5E", + "mantelpiece_clock" to "\uD83D\uDD70\uFE0F", + "manual_wheelchair" to "\uD83E\uDDBD", + "maple_leaf" to "\uD83C\uDF41", + "maracas" to "\uD83E\uDE87", + "martial_arts_uniform" to "\uD83E\uDD4B", + "mask" to "\uD83D\uDE37", + "massage" to "\uD83D\uDC86", + "mate_drink" to "\uD83E\uDDC9", + "meat_on_bone" to "\uD83C\uDF56", + "mechanic" to "\uD83E\uDDD1\u200D\uD83D\uDD27", + "mechanical_arm" to "\uD83E\uDDBE", + "mechanical_leg" to "\uD83E\uDDBF", + "medal" to "\uD83C\uDF96\uFE0F", + "medical_symbol" to "\u2695\uFE0F", + "mega" to "\uD83D\uDCE3", + "melon" to "\uD83C\uDF48", + "melting_face" to "\uD83E\uDEE0", + "memo" to "\uD83D\uDCDD", + "men-with-bunny-ears-partying" to "\uD83D\uDC6F\u200D\u2642\uFE0F", + "men_holding_hands" to "\uD83D\uDC6C", + "mending_heart" to "\u2764\uFE0F\u200D\uD83E\uDE79", + "menorah_with_nine_branches" to "\uD83D\uDD4E", + "mens" to "\uD83D\uDEB9", + "mermaid" to "\uD83E\uDDDC\u200D\u2640\uFE0F", + "merman" to "\uD83E\uDDDC\u200D\u2642\uFE0F", + "merperson" to "\uD83E\uDDDC", + "metro" to "\uD83D\uDE87", + "microbe" to "\uD83E\uDDA0", + "microphone" to "\uD83C\uDFA4", + "microscope" to "\uD83D\uDD2C", + "middle_finger" to "\uD83D\uDD95", + "military_helmet" to "\uD83E\uDE96", + "milky_way" to "\uD83C\uDF0C", + "minibus" to "\uD83D\uDE90", + "minidisc" to "\uD83D\uDCBD", + "mirror" to "\uD83E\uDE9E", + "mirror_ball" to "\uD83E\uDEA9", + "mobile_phone_off" to "\uD83D\uDCF4", + "money_mouth_face" to "\uD83E\uDD11", + "money_with_wings" to "\uD83D\uDCB8", + "moneybag" to "\uD83D\uDCB0", + "monkey" to "\uD83D\uDC12", + "monkey_face" to "\uD83D\uDC35", + "monorail" to "\uD83D\uDE9D", + "moon" to "\uD83C\uDF14", + "moon_cake" to "\uD83E\uDD6E", + "moose" to "\uD83E\uDECE", + "mortar_board" to "\uD83C\uDF93", + "mosque" to "\uD83D\uDD4C", + "mosquito" to "\uD83E\uDD9F", + "mostly_sunny" to "\uD83C\uDF24\uFE0F", + "mother_christmas" to "\uD83E\uDD36", + "motor_boat" to "\uD83D\uDEE5\uFE0F", + "motor_scooter" to "\uD83D\uDEF5", + "motorized_wheelchair" to "\uD83E\uDDBC", + "motorway" to "\uD83D\uDEE3\uFE0F", + "mount_fuji" to "\uD83D\uDDFB", + "mountain" to "\u26F0\uFE0F", + "mountain_bicyclist" to "\uD83D\uDEB5", + "mountain_cableway" to "\uD83D\uDEA0", + "mountain_railway" to "\uD83D\uDE9E", + "mouse" to "\uD83D\uDC2D", + "mouse2" to "\uD83D\uDC01", + "mouse_trap" to "\uD83E\uDEA4", + "movie_camera" to "\uD83C\uDFA5", + "moyai" to "\uD83D\uDDFF", + "mrs_claus" to "\uD83E\uDD36", + "muscle" to "\uD83D\uDCAA", + "mushroom" to "\uD83C\uDF44", + "musical_keyboard" to "\uD83C\uDFB9", + "musical_note" to "\uD83C\uDFB5", + "musical_score" to "\uD83C\uDFBC", + "mute" to "\uD83D\uDD07", + "mx_claus" to "\uD83E\uDDD1\u200D\uD83C\uDF84", + "nail_care" to "\uD83D\uDC85", + "name_badge" to "\uD83D\uDCDB", + "national_park" to "\uD83C\uDFDE\uFE0F", + "nauseated_face" to "\uD83E\uDD22", + "nazar_amulet" to "\uD83E\uDDFF", + "necktie" to "\uD83D\uDC54", + "negative_squared_cross_mark" to "\u274E", + "nerd_face" to "\uD83E\uDD13", + "nest_with_eggs" to "\uD83E\uDEBA", + "nesting_dolls" to "\uD83E\uDE86", + "neutral_face" to "\uD83D\uDE10", + "new" to "\uD83C\uDD95", + "new_moon" to "\uD83C\uDF11", + "new_moon_with_face" to "\uD83C\uDF1A", + "newspaper" to "\uD83D\uDCF0", + "ng" to "\uD83C\uDD96", + "night_with_stars" to "\uD83C\uDF03", + "nine" to "\u0039\uFE0F\u20E3", + "ninja" to "\uD83E\uDD77", + "no_bell" to "\uD83D\uDD15", + "no_bicycles" to "\uD83D\uDEB3", + "no_entry" to "\u26D4", + "no_entry_sign" to "\uD83D\uDEAB", + "no_good" to "\uD83D\uDE45", + "no_mobile_phones" to "\uD83D\uDCF5", + "no_mouth" to "\uD83D\uDE36", + "no_pedestrians" to "\uD83D\uDEB7", + "no_smoking" to "\uD83D\uDEAD", + "non-potable_water" to "\uD83D\uDEB1", + "nose" to "\uD83D\uDC43", + "notebook" to "\uD83D\uDCD3", + "notebook_with_decorative_cover" to "\uD83D\uDCD4", + "notes" to "\uD83C\uDFB6", + "nut_and_bolt" to "\uD83D\uDD29", + "o" to "\u2B55", + "o2" to "\uD83C\uDD7E\uFE0F", + "ocean" to "\uD83C\uDF0A", + "octagonal_sign" to "\uD83D\uDED1", + "octopus" to "\uD83D\uDC19", + "oden" to "\uD83C\uDF62", + "office" to "\uD83C\uDFE2", + "office_worker" to "\uD83E\uDDD1\u200D\uD83D\uDCBC", + "oil_drum" to "\uD83D\uDEE2\uFE0F", + "ok" to "\uD83C\uDD97", + "ok_hand" to "\uD83D\uDC4C", + "ok_woman" to "\uD83D\uDE46", + "old_key" to "\uD83D\uDDDD\uFE0F", + "older_adult" to "\uD83E\uDDD3", + "older_man" to "\uD83D\uDC74", + "older_woman" to "\uD83D\uDC75", + "olive" to "\uD83E\uDED2", + "om_symbol" to "\uD83D\uDD49\uFE0F", + "on" to "\uD83D\uDD1B", + "oncoming_automobile" to "\uD83D\uDE98", + "oncoming_bus" to "\uD83D\uDE8D", + "oncoming_police_car" to "\uD83D\uDE94", + "oncoming_taxi" to "\uD83D\uDE96", + "one" to "\u0031\uFE0F\u20E3", + "one-piece_swimsuit" to "\uD83E\uDE71", + "onion" to "\uD83E\uDDC5", + "open_book" to "\uD83D\uDCD6", + "open_file_folder" to "\uD83D\uDCC2", + "open_hands" to "\uD83D\uDC50", + "open_mouth" to "\uD83D\uDE2E", + "ophiuchus" to "\u26CE", + "orange_book" to "\uD83D\uDCD9", + "orange_heart" to "\uD83E\uDDE1", + "orangutan" to "\uD83E\uDDA7", + "orthodox_cross" to "\u2626\uFE0F", + "otter" to "\uD83E\uDDA6", + "outbox_tray" to "\uD83D\uDCE4", + "owl" to "\uD83E\uDD89", + "ox" to "\uD83D\uDC02", + "oyster" to "\uD83E\uDDAA", + "package" to "\uD83D\uDCE6", + "page_facing_up" to "\uD83D\uDCC4", + "page_with_curl" to "\uD83D\uDCC3", + "pager" to "\uD83D\uDCDF", + "palm_down_hand" to "\uD83E\uDEF3", + "palm_tree" to "\uD83C\uDF34", + "palm_up_hand" to "\uD83E\uDEF4", + "palms_up_together" to "\uD83E\uDD32", + "pancakes" to "\uD83E\uDD5E", + "panda_face" to "\uD83D\uDC3C", + "paperclip" to "\uD83D\uDCCE", + "parachute" to "\uD83E\uDE82", + "parking" to "\uD83C\uDD7F\uFE0F", + "parrot" to "\uD83E\uDD9C", + "part_alternation_mark" to "\u303D\uFE0F", + "partly_sunny" to "\u26C5", + "partly_sunny_rain" to "\uD83C\uDF26\uFE0F", + "partying_face" to "\uD83E\uDD73", + "passenger_ship" to "\uD83D\uDEF3\uFE0F", + "passport_control" to "\uD83D\uDEC2", + "paw_prints" to "\uD83D\uDC3E", + "pea_pod" to "\uD83E\uDEDB", + "peace_symbol" to "\u262E\uFE0F", + "peach" to "\uD83C\uDF51", + "peacock" to "\uD83E\uDD9A", + "peanuts" to "\uD83E\uDD5C", + "pear" to "\uD83C\uDF50", + "pencil" to "\uD83D\uDCDD", + "pencil2" to "\u270F\uFE0F", + "penguin" to "\uD83D\uDC27", + "pensive" to "\uD83D\uDE14", + "people_holding_hands" to "\uD83E\uDDD1\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1", + "people_hugging" to "\uD83E\uDEC2", + "performing_arts" to "\uD83C\uDFAD", + "persevere" to "\uD83D\uDE23", + "person_climbing" to "\uD83E\uDDD7", + "person_doing_cartwheel" to "\uD83E\uDD38", + "person_feeding_baby" to "\uD83E\uDDD1\u200D\uD83C\uDF7C", + "person_frowning" to "\uD83D\uDE4D", + "person_in_lotus_position" to "\uD83E\uDDD8", + "person_in_manual_wheelchair" to "\uD83E\uDDD1\u200D\uD83E\uDDBD", + "person_in_manual_wheelchair_facing_right" to "\uD83E\uDDD1\u200D\uD83E\uDDBD\u200D\u27A1\uFE0F", + "person_in_motorized_wheelchair" to "\uD83E\uDDD1\u200D\uD83E\uDDBC", + "person_in_motorized_wheelchair_facing_right" to "\uD83E\uDDD1\u200D\uD83E\uDDBC\u200D\u27A1\uFE0F", + "person_in_steamy_room" to "\uD83E\uDDD6", + "person_in_tuxedo" to "\uD83E\uDD35", + "person_kneeling_facing_right" to "\uD83E\uDDCE\u200D\u27A1\uFE0F", + "person_running_facing_right" to "\uD83C\uDFC3\u200D\u27A1\uFE0F", + "person_walking_facing_right" to "\uD83D\uDEB6\u200D\u27A1\uFE0F", + "person_with_ball" to "\u26F9\uFE0F", + "person_with_blond_hair" to "\uD83D\uDC71", + "person_with_crown" to "\uD83E\uDEC5", + "person_with_headscarf" to "\uD83E\uDDD5", + "person_with_pouting_face" to "\uD83D\uDE4E", + "person_with_probing_cane" to "\uD83E\uDDD1\u200D\uD83E\uDDAF", + "person_with_white_cane_facing_right" to "\uD83E\uDDD1\u200D\uD83E\uDDAF\u200D\u27A1\uFE0F", + "petri_dish" to "\uD83E\uDDEB", + "phoenix" to "\uD83D\uDC26\u200D\uD83D\uDD25", + "phone" to "\u260E\uFE0F", + "pick" to "\u26CF\uFE0F", + "pickup_truck" to "\uD83D\uDEFB", + "pie" to "\uD83E\uDD67", + "pig" to "\uD83D\uDC37", + "pig2" to "\uD83D\uDC16", + "pig_nose" to "\uD83D\uDC3D", + "pill" to "\uD83D\uDC8A", + "pilot" to "\uD83E\uDDD1\u200D\u2708\uFE0F", + "pinata" to "\uD83E\uDE85", + "pinched_fingers" to "\uD83E\uDD0C", + "pinching_hand" to "\uD83E\uDD0F", + "pineapple" to "\uD83C\uDF4D", + "pink_heart" to "\uD83E\uDE77", + "pirate_flag" to "\uD83C\uDFF4\u200D\u2620\uFE0F", + "pisces" to "\u2653", + "pizza" to "\uD83C\uDF55", + "placard" to "\uD83E\uDEA7", + "place_of_worship" to "\uD83D\uDED0", + "playground_slide" to "\uD83D\uDEDD", + "pleading" to "\uD83E\uDD7A", + "pleading_face" to "\uD83E\uDD7A", + "plunger" to "\uD83E\uDEA0", + "point_down" to "\uD83D\uDC47", + "point_left" to "\uD83D\uDC48", + "point_right" to "\uD83D\uDC49", + "point_up" to "\u261D\uFE0F", + "point_up_2" to "\uD83D\uDC46", + "polar_bear" to "\uD83D\uDC3B\u200D\u2744\uFE0F", + "police_car" to "\uD83D\uDE93", + "poodle" to "\uD83D\uDC29", + "poop" to "\uD83D\uDCA9", + "popcorn" to "\uD83C\uDF7F", + "post_office" to "\uD83C\uDFE3", + "postal_horn" to "\uD83D\uDCEF", + "postbox" to "\uD83D\uDCEE", + "potable_water" to "\uD83D\uDEB0", + "potato" to "\uD83E\uDD54", + "potted_plant" to "\uD83E\uDEB4", + "pouch" to "\uD83D\uDC5D", + "poultry_leg" to "\uD83C\uDF57", + "pound" to "\uD83D\uDCB7", + "pouring_liquid" to "\uD83E\uDED7", + "pouting_cat" to "\uD83D\uDE3E", + "pray" to "\uD83D\uDE4F", + "prayer_beads" to "\uD83D\uDCFF", + "pregnant_man" to "\uD83E\uDEC3", + "pregnant_person" to "\uD83E\uDEC4", + "pregnant_woman" to "\uD83E\uDD30", + "pretzel" to "\uD83E\uDD68", + "prince" to "\uD83E\uDD34", + "princess" to "\uD83D\uDC78", + "printer" to "\uD83D\uDDA8\uFE0F", + "probing_cane" to "\uD83E\uDDAF", + "punch" to "\uD83D\uDC4A", + "purple_heart" to "\uD83D\uDC9C", + "purse" to "\uD83D\uDC5B", + "pushpin" to "\uD83D\uDCCC", + "put_litter_in_its_place" to "\uD83D\uDEAE", + "question" to "\u2753", + "rabbit" to "\uD83D\uDC30", + "rabbit2" to "\uD83D\uDC07", + "raccoon" to "\uD83E\uDD9D", + "racehorse" to "\uD83D\uDC0E", + "racing_car" to "\uD83C\uDFCE\uFE0F", + "racing_motorcycle" to "\uD83C\uDFCD\uFE0F", + "radio" to "\uD83D\uDCFB", + "radio_button" to "\uD83D\uDD18", + "radioactive_sign" to "\u2622\uFE0F", + "rage" to "\uD83D\uDE21", + "railway_car" to "\uD83D\uDE83", + "railway_track" to "\uD83D\uDEE4\uFE0F", + "rain_cloud" to "\uD83C\uDF27\uFE0F", + "rainbow" to "\uD83C\uDF08", + "rainbow-flag" to "\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08", + "raised_back_of_hand" to "\uD83E\uDD1A", + "raised_hand" to "\u270B", + "raised_hand_with_fingers_splayed" to "\uD83D\uDD90\uFE0F", + "raised_hands" to "\uD83D\uDE4C", + "raising_hand" to "\uD83D\uDE4B", + "ram" to "\uD83D\uDC0F", + "ramen" to "\uD83C\uDF5C", + "rat" to "\uD83D\uDC00", + "razor" to "\uD83E\uDE92", + "receipt" to "\uD83E\uDDFE", + "recycle" to "\u267B\uFE0F", + "red_car" to "\uD83D\uDE97", + "red_circle" to "\uD83D\uDD34", + "red_envelope" to "\uD83E\uDDE7", + "red_haired_man" to "\uD83D\uDC68\u200D\uD83E\uDDB0", + "red_haired_person" to "\uD83E\uDDD1\u200D\uD83E\uDDB0", + "red_haired_woman" to "\uD83D\uDC69\u200D\uD83E\uDDB0", + "registered" to "\u00AE\uFE0F", + "relaxed" to "\u263A\uFE0F", + "relieved" to "\uD83D\uDE0C", + "reminder_ribbon" to "\uD83C\uDF97\uFE0F", + "repeat" to "\uD83D\uDD01", + "repeat_one" to "\uD83D\uDD02", + "restroom" to "\uD83D\uDEBB", + "reversed_hand_with_middle_finger_extended" to "\uD83D\uDD95", + "revolving_hearts" to "\uD83D\uDC9E", + "rewind" to "\u23EA", + "rhinoceros" to "\uD83E\uDD8F", + "ribbon" to "\uD83C\uDF80", + "rice" to "\uD83C\uDF5A", + "rice_ball" to "\uD83C\uDF59", + "rice_cracker" to "\uD83C\uDF58", + "rice_scene" to "\uD83C\uDF91", + "right-facing_fist" to "\uD83E\uDD1C", + "right_anger_bubble" to "\uD83D\uDDEF\uFE0F", + "rightwards_hand" to "\uD83E\uDEF1", + "rightwards_pushing_hand" to "\uD83E\uDEF8", + "ring" to "\uD83D\uDC8D", + "ring_buoy" to "\uD83D\uDEDF", + "ringed_planet" to "\uD83E\uDE90", + "robot_face" to "\uD83E\uDD16", + "rock" to "\uD83E\uDEA8", + "rocket" to "\uD83D\uDE80", + "roll_of_paper" to "\uD83E\uDDFB", + "rolled_up_newspaper" to "\uD83D\uDDDE\uFE0F", + "roller_coaster" to "\uD83C\uDFA2", + "roller_skate" to "\uD83D\uDEFC", + "rolling_on_the_floor_laughing" to "\uD83E\uDD23", + "rooster" to "\uD83D\uDC13", + "root_vegetable" to "\uD83E\uDEDC", + "rose" to "\uD83C\uDF39", + "rosette" to "\uD83C\uDFF5\uFE0F", + "rotating_light" to "\uD83D\uDEA8", + "round_pushpin" to "\uD83D\uDCCD", + "rowboat" to "\uD83D\uDEA3", + "ru" to "\uD83C\uDDF7\uD83C\uDDFA", + "rugby_football" to "\uD83C\uDFC9", + "runner" to "\uD83C\uDFC3", + "running" to "\uD83C\uDFC3", + "running_shirt_with_sash" to "\uD83C\uDFBD", + "sa" to "\uD83C\uDE02\uFE0F", + "safety_pin" to "\uD83E\uDDF7", + "safety_vest" to "\uD83E\uDDBA", + "sagittarius" to "\u2650", + "sailboat" to "\u26F5", + "sake" to "\uD83C\uDF76", + "salt" to "\uD83E\uDDC2", + "saluting_face" to "\uD83E\uDEE1", + "sandal" to "\uD83D\uDC61", + "sandwich" to "\uD83E\uDD6A", + "santa" to "\uD83C\uDF85", + "sari" to "\uD83E\uDD7B", + "satellite" to "\uD83D\uDEF0\uFE0F", + "satellite_antenna" to "\uD83D\uDCE1", + "satisfied" to "\uD83D\uDE06", + "sauropod" to "\uD83E\uDD95", + "saxophone" to "\uD83C\uDFB7", + "scales" to "\u2696\uFE0F", + "scarf" to "\uD83E\uDDE3", + "school" to "\uD83C\uDFEB", + "school_satchel" to "\uD83C\uDF92", + "scientist" to "\uD83E\uDDD1\u200D\uD83D\uDD2C", + "scissors" to "\u2702\uFE0F", + "scooter" to "\uD83D\uDEF4", + "scorpion" to "\uD83E\uDD82", + "scorpius" to "\u264F", + "scream" to "\uD83D\uDE31", + "scream_cat" to "\uD83D\uDE40", + "screwdriver" to "\uD83E\uDE9B", + "scroll" to "\uD83D\uDCDC", + "seal" to "\uD83E\uDDAD", + "seat" to "\uD83D\uDCBA", + "second_place_medal" to "\uD83E\uDD48", + "secret" to "\u3299\uFE0F", + "see_no_evil" to "\uD83D\uDE48", + "seedling" to "\uD83C\uDF31", + "selfie" to "\uD83E\uDD33", + "serious_face_with_symbols_covering_mouth" to "\uD83E\uDD2C", + "service_dog" to "\uD83D\uDC15\u200D\uD83E\uDDBA", + "seven" to "\u0037\uFE0F\u20E3", + "sewing_needle" to "\uD83E\uDEA1", + "shaking_face" to "\uD83E\uDEE8", + "shallow_pan_of_food" to "\uD83E\uDD58", + "shamrock" to "\u2618\uFE0F", + "shark" to "\uD83E\uDD88", + "shaved_ice" to "\uD83C\uDF67", + "sheep" to "\uD83D\uDC11", + "shell" to "\uD83D\uDC1A", + "shield" to "\uD83D\uDEE1\uFE0F", + "shinto_shrine" to "\u26E9\uFE0F", + "ship" to "\uD83D\uDEA2", + "shirt" to "\uD83D\uDC55", + "shit" to "\uD83D\uDCA9", + "shocked_face_with_exploding_head" to "\uD83E\uDD2F", + "shoe" to "\uD83D\uDC5E", + "shopping_bags" to "\uD83D\uDECD\uFE0F", + "shopping_trolley" to "\uD83D\uDED2", + "shorts" to "\uD83E\uDE73", + "shovel" to "\uD83E\uDE8F", + "shower" to "\uD83D\uDEBF", + "shrimp" to "\uD83E\uDD90", + "shrug" to "\uD83E\uDD37", + "shushing_face" to "\uD83E\uDD2B", + "sign_of_the_horns" to "\uD83E\uDD18", + "signal_strength" to "\uD83D\uDCF6", + "singer" to "\uD83E\uDDD1\u200D\uD83C\uDFA4", + "six" to "\u0036\uFE0F\u20E3", + "six_pointed_star" to "\uD83D\uDD2F", + "skateboard" to "\uD83D\uDEF9", + "ski" to "\uD83C\uDFBF", + "skier" to "\u26F7\uFE0F", + "skin-tone-2" to "\uD83C\uDFFB", + "skin-tone-3" to "\uD83C\uDFFC", + "skin-tone-4" to "\uD83C\uDFFD", + "skin-tone-5" to "\uD83C\uDFFE", + "skin-tone-6" to "\uD83C\uDFFF", + "skull" to "\uD83D\uDC80", + "skull_and_crossbones" to "\u2620\uFE0F", + "skunk" to "\uD83E\uDDA8", + "sled" to "\uD83D\uDEF7", + "sleeping" to "\uD83D\uDE34", + "sleeping_accommodation" to "\uD83D\uDECC", + "sleepy" to "\uD83D\uDE2A", + "sleuth_or_spy" to "\uD83D\uDD75\uFE0F", + "slightly_frowning_face" to "\uD83D\uDE41", + "slightly_smiling_face" to "\uD83D\uDE42", + "slot_machine" to "\uD83C\uDFB0", + "sloth" to "\uD83E\uDDA5", + "small_airplane" to "\uD83D\uDEE9\uFE0F", + "small_blue_diamond" to "\uD83D\uDD39", + "small_orange_diamond" to "\uD83D\uDD38", + "small_red_triangle" to "\uD83D\uDD3A", + "small_red_triangle_down" to "\uD83D\uDD3B", + "smile" to "\uD83D\uDE04", + "smile_cat" to "\uD83D\uDE38", + "smiley" to "\uD83D\uDE03", + "smiley_cat" to "\uD83D\uDE3A", + "smiling_face_with_3_hearts" to "\uD83E\uDD70", + "smiling_face_with_smiling_eyes_and_hand_covering_mouth" to "\uD83E\uDD2D", + "smiling_face_with_tear" to "\uD83E\uDD72", + "smiling_imp" to "\uD83D\uDE08", + "smirk" to "\uD83D\uDE0F", + "smirk_cat" to "\uD83D\uDE3C", + "smoking" to "\uD83D\uDEAC", + "snail" to "\uD83D\uDC0C", + "snake" to "\uD83D\uDC0D", + "sneezing_face" to "\uD83E\uDD27", + "snow_capped_mountain" to "\uD83C\uDFD4\uFE0F", + "snow_cloud" to "\uD83C\uDF28\uFE0F", + "snowboarder" to "\uD83C\uDFC2", + "snowflake" to "\u2744\uFE0F", + "snowman" to "\u2603\uFE0F", + "snowman_without_snow" to "\u26C4", + "soap" to "\uD83E\uDDFC", + "sob" to "\uD83D\uDE2D", + "soccer" to "\u26BD", + "socks" to "\uD83E\uDDE6", + "softball" to "\uD83E\uDD4E", + "soon" to "\uD83D\uDD1C", + "sos" to "\uD83C\uDD98", + "sound" to "\uD83D\uDD09", + "space_invader" to "\uD83D\uDC7E", + "spades" to "\u2660\uFE0F", + "spaghetti" to "\uD83C\uDF5D", + "sparkle" to "\u2747\uFE0F", + "sparkler" to "\uD83C\uDF87", + "sparkles" to "\u2728", + "sparkling_heart" to "\uD83D\uDC96", + "speak_no_evil" to "\uD83D\uDE4A", + "speaker" to "\uD83D\uDD08", + "speaking_head_in_silhouette" to "\uD83D\uDDE3\uFE0F", + "speech_balloon" to "\uD83D\uDCAC", + "speedboat" to "\uD83D\uDEA4", + "spider" to "\uD83D\uDD77\uFE0F", + "spider_web" to "\uD83D\uDD78\uFE0F", + "spiral_calendar_pad" to "\uD83D\uDDD3\uFE0F", + "spiral_note_pad" to "\uD83D\uDDD2\uFE0F", + "splatter" to "\uD83E\uDEDF", + "spock-hand" to "\uD83D\uDD96", + "sponge" to "\uD83E\uDDFD", + "spoon" to "\uD83E\uDD44", + "sports_medal" to "\uD83C\uDFC5", + "squid" to "\uD83E\uDD91", + "stadium" to "\uD83C\uDFDF\uFE0F", + "staff_of_aesculapius" to "\u2695\uFE0F", + "standing_person" to "\uD83E\uDDCD", + "star" to "\u2B50", + "star-struck" to "\uD83E\uDD29", + "star2" to "\uD83C\uDF1F", + "star_and_crescent" to "\u262A\uFE0F", + "star_of_david" to "\u2721\uFE0F", + "stars" to "\uD83C\uDF20", + "station" to "\uD83D\uDE89", + "statue_of_liberty" to "\uD83D\uDDFD", + "steam_locomotive" to "\uD83D\uDE82", + "stethoscope" to "\uD83E\uDE7A", + "stew" to "\uD83C\uDF72", + "stopwatch" to "\u23F1\uFE0F", + "straight_ruler" to "\uD83D\uDCCF", + "strawberry" to "\uD83C\uDF53", + "stuck_out_tongue" to "\uD83D\uDE1B", + "stuck_out_tongue_closed_eyes" to "\uD83D\uDE1D", + "stuck_out_tongue_winking_eye" to "\uD83D\uDE1C", + "student" to "\uD83E\uDDD1\u200D\uD83C\uDF93", + "studio_microphone" to "\uD83C\uDF99\uFE0F", + "stuffed_flatbread" to "\uD83E\uDD59", + "sun_behind_cloud" to "\uD83C\uDF25\uFE0F", + "sun_behind_rain_cloud" to "\uD83C\uDF26\uFE0F", + "sun_small_cloud" to "\uD83C\uDF24\uFE0F", + "sun_with_face" to "\uD83C\uDF1E", + "sunflower" to "\uD83C\uDF3B", + "sunglasses" to "\uD83D\uDE0E", + "sunny" to "\u2600\uFE0F", + "sunrise" to "\uD83C\uDF05", + "sunrise_over_mountains" to "\uD83C\uDF04", + "superhero" to "\uD83E\uDDB8", + "supervillain" to "\uD83E\uDDB9", + "surfer" to "\uD83C\uDFC4", + "sushi" to "\uD83C\uDF63", + "suspension_railway" to "\uD83D\uDE9F", + "swan" to "\uD83E\uDDA2", + "sweat" to "\uD83D\uDE13", + "sweat_drops" to "\uD83D\uDCA6", + "sweat_smile" to "\uD83D\uDE05", + "sweet_potato" to "\uD83C\uDF60", + "swimmer" to "\uD83C\uDFCA", + "symbols" to "\uD83D\uDD23", + "synagogue" to "\uD83D\uDD4D", + "syringe" to "\uD83D\uDC89", + "t-rex" to "\uD83E\uDD96", + "table_tennis_paddle_and_ball" to "\uD83C\uDFD3", + "taco" to "\uD83C\uDF2E", + "tada" to "\uD83C\uDF89", + "takeout_box" to "\uD83E\uDD61", + "tamale" to "\uD83E\uDED4", + "tanabata_tree" to "\uD83C\uDF8B", + "tangerine" to "\uD83C\uDF4A", + "taurus" to "\u2649", + "taxi" to "\uD83D\uDE95", + "tea" to "\uD83C\uDF75", + "teacher" to "\uD83E\uDDD1\u200D\uD83C\uDFEB", + "teapot" to "\uD83E\uDED6", + "technologist" to "\uD83E\uDDD1\u200D\uD83D\uDCBB", + "teddy_bear" to "\uD83E\uDDF8", + "telephone" to "\u260E\uFE0F", + "telephone_receiver" to "\uD83D\uDCDE", + "telescope" to "\uD83D\uDD2D", + "tennis" to "\uD83C\uDFBE", + "tent" to "\u26FA", + "test_tube" to "\uD83E\uDDEA", + "the_horns" to "\uD83E\uDD18", + "thermometer" to "\uD83C\uDF21\uFE0F", + "thinking_face" to "\uD83E\uDD14", + "third_place_medal" to "\uD83E\uDD49", + "thong_sandal" to "\uD83E\uDE74", + "thought_balloon" to "\uD83D\uDCAD", + "thread" to "\uD83E\uDDF5", + "three" to "\u0033\uFE0F\u20E3", + "three_button_mouse" to "\uD83D\uDDB1\uFE0F", + "thumbsdown" to "\uD83D\uDC4E", + "thumbsup" to "\uD83D\uDC4D", + "thunder_cloud_and_rain" to "\u26C8\uFE0F", + "ticket" to "\uD83C\uDFAB", + "tiger" to "\uD83D\uDC2F", + "tiger2" to "\uD83D\uDC05", + "timer_clock" to "\u23F2\uFE0F", + "tired_face" to "\uD83D\uDE2B", + "tm" to "\u2122\uFE0F", + "toilet" to "\uD83D\uDEBD", + "tokyo_tower" to "\uD83D\uDDFC", + "tomato" to "\uD83C\uDF45", + "tongue" to "\uD83D\uDC45", + "toolbox" to "\uD83E\uDDF0", + "tooth" to "\uD83E\uDDB7", + "toothbrush" to "\uD83E\uDEA5", + "top" to "\uD83D\uDD1D", + "tophat" to "\uD83C\uDFA9", + "tornado" to "\uD83C\uDF2A\uFE0F", + "tornado_cloud" to "\uD83C\uDF2A\uFE0F", + "trackball" to "\uD83D\uDDB2\uFE0F", + "tractor" to "\uD83D\uDE9C", + "traffic_light" to "\uD83D\uDEA5", + "train" to "\uD83D\uDE8B", + "train2" to "\uD83D\uDE86", + "tram" to "\uD83D\uDE8A", + "transgender_flag" to "\uD83C\uDFF3\uFE0F\u200D\u26A7\uFE0F", + "transgender_symbol" to "\u26A7\uFE0F", + "triangular_flag_on_post" to "\uD83D\uDEA9", + "triangular_ruler" to "\uD83D\uDCD0", + "trident" to "\uD83D\uDD31", + "triumph" to "\uD83D\uDE24", + "troll" to "\uD83E\uDDCC", + "trolleybus" to "\uD83D\uDE8E", + "trophy" to "\uD83C\uDFC6", + "tropical_drink" to "\uD83C\uDF79", + "tropical_fish" to "\uD83D\uDC20", + "truck" to "\uD83D\uDE9A", + "trumpet" to "\uD83C\uDFBA", + "tshirt" to "\uD83D\uDC55", + "tulip" to "\uD83C\uDF37", + "tumbler_glass" to "\uD83E\uDD43", + "turkey" to "\uD83E\uDD83", + "turtle" to "\uD83D\uDC22", + "tv" to "\uD83D\uDCFA", + "twisted_rightwards_arrows" to "\uD83D\uDD00", + "two" to "\u0032\uFE0F\u20E3", + "two_hearts" to "\uD83D\uDC95", + "two_men_holding_hands" to "\uD83D\uDC6C", + "two_women_holding_hands" to "\uD83D\uDC6D", + "u5272" to "\uD83C\uDE39", + "u5408" to "\uD83C\uDE34", + "u55b6" to "\uD83C\uDE3A", + "u6307" to "\uD83C\uDE2F", + "u6708" to "\uD83C\uDE37\uFE0F", + "u6709" to "\uD83C\uDE36", + "u6e80" to "\uD83C\uDE35", + "u7121" to "\uD83C\uDE1A", + "u7533" to "\uD83C\uDE38", + "u7981" to "\uD83C\uDE32", + "u7a7a" to "\uD83C\uDE33", + "uk" to "\uD83C\uDDEC\uD83C\uDDE7", + "umbrella" to "\u2602\uFE0F", + "umbrella_on_ground" to "\u26F1\uFE0F", + "umbrella_with_rain_drops" to "\u2614", + "unamused" to "\uD83D\uDE12", + "underage" to "\uD83D\uDD1E", + "unicorn_face" to "\uD83E\uDD84", + "unlock" to "\uD83D\uDD13", + "up" to "\uD83C\uDD99", + "upside_down_face" to "\uD83D\uDE43", + "us" to "\uD83C\uDDFA\uD83C\uDDF8", + "v" to "\u270C\uFE0F", + "vampire" to "\uD83E\uDDDB", + "vertical_traffic_light" to "\uD83D\uDEA6", + "vhs" to "\uD83D\uDCFC", + "vibration_mode" to "\uD83D\uDCF3", + "video_camera" to "\uD83D\uDCF9", + "video_game" to "\uD83C\uDFAE", + "violin" to "\uD83C\uDFBB", + "virgo" to "\u264D", + "volcano" to "\uD83C\uDF0B", + "volleyball" to "\uD83C\uDFD0", + "vs" to "\uD83C\uDD9A", + "waffle" to "\uD83E\uDDC7", + "walking" to "\uD83D\uDEB6", + "waning_crescent_moon" to "\uD83C\uDF18", + "waning_gibbous_moon" to "\uD83C\uDF16", + "warning" to "\u26A0\uFE0F", + "wastebasket" to "\uD83D\uDDD1\uFE0F", + "watch" to "\u231A", + "water_buffalo" to "\uD83D\uDC03", + "water_polo" to "\uD83E\uDD3D", + "watermelon" to "\uD83C\uDF49", + "wave" to "\uD83D\uDC4B", + "waving_black_flag" to "\uD83C\uDFF4", + "waving_white_flag" to "\uD83C\uDFF3\uFE0F", + "wavy_dash" to "\u3030\uFE0F", + "waxing_crescent_moon" to "\uD83C\uDF12", + "waxing_gibbous_moon" to "\uD83C\uDF14", + "wc" to "\uD83D\uDEBE", + "weary" to "\uD83D\uDE29", + "wedding" to "\uD83D\uDC92", + "weight_lifter" to "\uD83C\uDFCB\uFE0F", + "whale" to "\uD83D\uDC33", + "whale2" to "\uD83D\uDC0B", + "wheel" to "\uD83D\uDEDE", + "wheel_of_dharma" to "\u2638\uFE0F", + "wheelchair" to "\u267F", + "white_check_mark" to "\u2705", + "white_circle" to "\u26AA", + "white_flower" to "\uD83D\uDCAE", + "white_frowning_face" to "\u2639\uFE0F", + "white_haired_man" to "\uD83D\uDC68\u200D\uD83E\uDDB3", + "white_haired_person" to "\uD83E\uDDD1\u200D\uD83E\uDDB3", + "white_haired_woman" to "\uD83D\uDC69\u200D\uD83E\uDDB3", + "white_heart" to "\uD83E\uDD0D", + "white_large_square" to "\u2B1C", + "white_medium_small_square" to "\u25FD", + "white_medium_square" to "\u25FB\uFE0F", + "white_small_square" to "\u25AB\uFE0F", + "white_square_button" to "\uD83D\uDD33", + "wilted_flower" to "\uD83E\uDD40", + "wind_blowing_face" to "\uD83C\uDF2C\uFE0F", + "wind_chime" to "\uD83C\uDF90", + "window" to "\uD83E\uDE9F", + "wine_glass" to "\uD83C\uDF77", + "wing" to "\uD83E\uDEBD", + "wink" to "\uD83D\uDE09", + "wireless" to "\uD83D\uDEDC", + "wolf" to "\uD83D\uDC3A", + "woman" to "\uD83D\uDC69", + "woman-biking" to "\uD83D\uDEB4\u200D\u2640\uFE0F", + "woman-bouncing-ball" to "\u26F9\uFE0F\u200D\u2640\uFE0F", + "woman-bowing" to "\uD83D\uDE47\u200D\u2640\uFE0F", + "woman-boy" to "\uD83D\uDC69\u200D\uD83D\uDC66", + "woman-boy-boy" to "\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66", + "woman-cartwheeling" to "\uD83E\uDD38\u200D\u2640\uFE0F", + "woman-facepalming" to "\uD83E\uDD26\u200D\u2640\uFE0F", + "woman-frowning" to "\uD83D\uDE4D\u200D\u2640\uFE0F", + "woman-gesturing-no" to "\uD83D\uDE45\u200D\u2640\uFE0F", + "woman-gesturing-ok" to "\uD83D\uDE46\u200D\u2640\uFE0F", + "woman-getting-haircut" to "\uD83D\uDC87\u200D\u2640\uFE0F", + "woman-getting-massage" to "\uD83D\uDC86\u200D\u2640\uFE0F", + "woman-girl" to "\uD83D\uDC69\u200D\uD83D\uDC67", + "woman-girl-boy" to "\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66", + "woman-girl-girl" to "\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67", + "woman-golfing" to "\uD83C\uDFCC\uFE0F\u200D\u2640\uFE0F", + "woman-heart-man" to "\uD83D\uDC69\u200D\u2764\uFE0F\u200D\uD83D\uDC68", + "woman-heart-woman" to "\uD83D\uDC69\u200D\u2764\uFE0F\u200D\uD83D\uDC69", + "woman-juggling" to "\uD83E\uDD39\u200D\u2640\uFE0F", + "woman-kiss-man" to "\uD83D\uDC69\u200D\u2764\uFE0F\u200D\uD83D\uDC8B\u200D\uD83D\uDC68", + "woman-kiss-woman" to "\uD83D\uDC69\u200D\u2764\uFE0F\u200D\uD83D\uDC8B\u200D\uD83D\uDC69", + "woman-lifting-weights" to "\uD83C\uDFCB\uFE0F\u200D\u2640\uFE0F", + "woman-mountain-biking" to "\uD83D\uDEB5\u200D\u2640\uFE0F", + "woman-playing-handball" to "\uD83E\uDD3E\u200D\u2640\uFE0F", + "woman-playing-water-polo" to "\uD83E\uDD3D\u200D\u2640\uFE0F", + "woman-pouting" to "\uD83D\uDE4E\u200D\u2640\uFE0F", + "woman-raising-hand" to "\uD83D\uDE4B\u200D\u2640\uFE0F", + "woman-rowing-boat" to "\uD83D\uDEA3\u200D\u2640\uFE0F", + "woman-running" to "\uD83C\uDFC3\u200D\u2640\uFE0F", + "woman-shrugging" to "\uD83E\uDD37\u200D\u2640\uFE0F", + "woman-surfing" to "\uD83C\uDFC4\u200D\u2640\uFE0F", + "woman-swimming" to "\uD83C\uDFCA\u200D\u2640\uFE0F", + "woman-tipping-hand" to "\uD83D\uDC81\u200D\u2640\uFE0F", + "woman-walking" to "\uD83D\uDEB6\u200D\u2640\uFE0F", + "woman-wearing-turban" to "\uD83D\uDC73\u200D\u2640\uFE0F", + "woman-with-bunny-ears-partying" to "\uD83D\uDC6F\u200D\u2640\uFE0F", + "woman-woman-boy" to "\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66", + "woman-woman-boy-boy" to "\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66", + "woman-woman-girl" to "\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67", + "woman-woman-girl-boy" to "\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66", + "woman-woman-girl-girl" to "\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67", + "woman-wrestling" to "\uD83E\uDD3C\u200D\u2640\uFE0F", + "woman_and_man_holding_hands" to "\uD83D\uDC6B", + "woman_climbing" to "\uD83E\uDDD7\u200D\u2640\uFE0F", + "woman_feeding_baby" to "\uD83D\uDC69\u200D\uD83C\uDF7C", + "woman_in_lotus_position" to "\uD83E\uDDD8\u200D\u2640\uFE0F", + "woman_in_manual_wheelchair" to "\uD83D\uDC69\u200D\uD83E\uDDBD", + "woman_in_manual_wheelchair_facing_right" to "\uD83D\uDC69\u200D\uD83E\uDDBD\u200D\u27A1\uFE0F", + "woman_in_motorized_wheelchair" to "\uD83D\uDC69\u200D\uD83E\uDDBC", + "woman_in_motorized_wheelchair_facing_right" to "\uD83D\uDC69\u200D\uD83E\uDDBC\u200D\u27A1\uFE0F", + "woman_in_steamy_room" to "\uD83E\uDDD6\u200D\u2640\uFE0F", + "woman_in_tuxedo" to "\uD83E\uDD35\u200D\u2640\uFE0F", + "woman_kneeling" to "\uD83E\uDDCE\u200D\u2640\uFE0F", + "woman_kneeling_facing_right" to "\uD83E\uDDCE\u200D\u2640\uFE0F\u200D\u27A1\uFE0F", + "woman_running_facing_right" to "\uD83C\uDFC3\u200D\u2640\uFE0F\u200D\u27A1\uFE0F", + "woman_standing" to "\uD83E\uDDCD\u200D\u2640\uFE0F", + "woman_walking_facing_right" to "\uD83D\uDEB6\u200D\u2640\uFE0F\u200D\u27A1\uFE0F", + "woman_with_beard" to "\uD83E\uDDD4\u200D\u2640\uFE0F", + "woman_with_probing_cane" to "\uD83D\uDC69\u200D\uD83E\uDDAF", + "woman_with_veil" to "\uD83D\uDC70\u200D\u2640\uFE0F", + "woman_with_white_cane_facing_right" to "\uD83D\uDC69\u200D\uD83E\uDDAF\u200D\u27A1\uFE0F", + "womans_clothes" to "\uD83D\uDC5A", + "womans_flat_shoe" to "\uD83E\uDD7F", + "womans_hat" to "\uD83D\uDC52", + "women-with-bunny-ears-partying" to "\uD83D\uDC6F\u200D\u2640\uFE0F", + "women_holding_hands" to "\uD83D\uDC6D", + "womens" to "\uD83D\uDEBA", + "wood" to "\uD83E\uDEB5", + "woozy_face" to "\uD83E\uDD74", + "world_map" to "\uD83D\uDDFA\uFE0F", + "worm" to "\uD83E\uDEB1", + "worried" to "\uD83D\uDE1F", + "wrench" to "\uD83D\uDD27", + "wrestlers" to "\uD83E\uDD3C", + "writing_hand" to "\u270D\uFE0F", + "x" to "\u274C", + "x-ray" to "\uD83E\uDE7B", + "yarn" to "\uD83E\uDDF6", + "yawning_face" to "\uD83E\uDD71", + "yellow_heart" to "\uD83D\uDC9B", + "yen" to "\uD83D\uDCB4", + "yin_yang" to "\u262F\uFE0F", + "yo-yo" to "\uD83E\uDE80", + "yum" to "\uD83D\uDE0B", + "zany_face" to "\uD83E\uDD2A", + "zap" to "\u26A1", + "zebra_face" to "\uD83E\uDD93", + "zero" to "\u0030\uFE0F\u20E3", + "zipper_mouth_face" to "\uD83E\uDD10", + "zombie" to "\uD83E\uDDDF", + "zzz" to "\uD83D\uDCA4" + ) + + private val pattern = Regex(":([A-Za-z0-9_+\\-]+):") + + fun replace(text: String): String = pattern.replace(text) { m -> + SHORTCODES[m.groupValues[1]] ?: m.value + } +} diff --git a/android/app/src/main/java/com/tsmobile/app/ui/components/AppTopBar.kt b/android/app/src/main/java/com/tsmobile/app/ui/components/AppTopBar.kt index 684a50e..bb4242a 100644 --- a/android/app/src/main/java/com/tsmobile/app/ui/components/AppTopBar.kt +++ b/android/app/src/main/java/com/tsmobile/app/ui/components/AppTopBar.kt @@ -1,5 +1,15 @@ package com.tsmobile.app.ui.components +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -58,23 +68,36 @@ fun AppTopBar( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - textAlign = TextAlign.Center, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - subtitle?.takeIf { it.isNotEmpty() }?.let { + // 标题文字横移切换:旧文字向左滑出,新文字从右滑入。 + AnimatedContent( + targetState = title, + transitionSpec = { headerTextSlide() }, + label = "appTopBarTitle", + ) { t -> Text( - text = it, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + text = t, + style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center, maxLines = 1, overflow = TextOverflow.Ellipsis, ) } + subtitle?.takeIf { it.isNotEmpty() }?.let { s -> + AnimatedContent( + targetState = s, + transitionSpec = { headerTextSlide() }, + label = "appTopBarSubtitle", + ) { subText -> + Text( + text = subText, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } } if (actionIcon != null && onActionClick != null) { @@ -87,3 +110,12 @@ fun AppTopBar( } } } + +/** 标题/副标题的横移过渡:新文字从右侧滑入,旧文字向左侧滑出。 */ +private fun AnimatedContentTransitionScope.headerTextSlide(): ContentTransform = + (slideInHorizontally(animationSpec = tween(280, easing = FastOutSlowInEasing)) { it / 3 } + + fadeIn(animationSpec = tween(240, easing = FastOutSlowInEasing))) + .togetherWith( + slideOutHorizontally(animationSpec = tween(260, easing = FastOutSlowInEasing)) { -it / 3 } + + fadeOut(animationSpec = tween(200, easing = FastOutSlowInEasing)) + ) diff --git a/android/app/src/main/java/com/tsmobile/app/ui/components/Bbcode.kt b/android/app/src/main/java/com/tsmobile/app/ui/components/Bbcode.kt index 2321e9a..832caed 100644 --- a/android/app/src/main/java/com/tsmobile/app/ui/components/Bbcode.kt +++ b/android/app/src/main/java/com/tsmobile/app/ui/components/Bbcode.kt @@ -1,6 +1,7 @@ package com.tsmobile.app.ui.components import android.util.Log +import com.tsmobile.app.data.EmojiMapper import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.LinkAnnotation @@ -49,7 +50,9 @@ object Bbcode { } } - private fun parseInternal(raw: String): AnnotatedString { + private fun parseInternal(rawInput: String): AnnotatedString { + // 先做 emoji 短码替换,再渲染 BBCode。 + val raw = EmojiMapper.replace(rawInput) val builder = AnnotatedString.Builder() val stack = ArrayDeque() var current = RunStyle() diff --git a/android/app/src/main/java/com/tsmobile/app/ui/components/ChatHeader.kt b/android/app/src/main/java/com/tsmobile/app/ui/components/ChatHeader.kt index bdfdb7d..b90ffd9 100644 --- a/android/app/src/main/java/com/tsmobile/app/ui/components/ChatHeader.kt +++ b/android/app/src/main/java/com/tsmobile/app/ui/components/ChatHeader.kt @@ -15,7 +15,7 @@ fun ChatHeader( ) { AppTopBar( title = channelName, - subtitle = "${memberCount}人", + subtitle = "${memberCount} 人在线", navigationIcon = Icons.AutoMirrored.Filled.ArrowBack, navigationContentDescription = "返回", onNavigationClick = onBack, diff --git a/android/app/src/main/java/com/tsmobile/app/ui/components/MessageInputBar.kt b/android/app/src/main/java/com/tsmobile/app/ui/components/MessageInputBar.kt index 3ebc406..4b2c8af 100644 --- a/android/app/src/main/java/com/tsmobile/app/ui/components/MessageInputBar.kt +++ b/android/app/src/main/java/com/tsmobile/app/ui/components/MessageInputBar.kt @@ -5,11 +5,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Send +import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import com.tsmobile.app.data.MessageSendState @@ -29,9 +30,10 @@ fun MessageInputBar( Surface( modifier = modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surface, - tonalElevation = 3.dp, - shadowElevation = 3.dp + // 与下方语音栏同底色,顶部圆角,读成“底部停靠的一组”,而非孤立的浮条。 + shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), + color = MaterialTheme.colorScheme.surfaceContainer, + tonalElevation = 1.dp, ) { Row( modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), @@ -52,7 +54,7 @@ fun MessageInputBar( focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant + unfocusedBorderColor = Color.Transparent, ), maxLines = 4, keyboardOptions = KeyboardOptions( @@ -90,7 +92,7 @@ fun MessageInputBar( ) ) { Icon( - imageVector = Icons.Default.Send, + imageVector = Icons.AutoMirrored.Filled.Send, contentDescription = "发送" ) } diff --git a/android/app/src/main/java/com/tsmobile/app/ui/components/MessageItem.kt b/android/app/src/main/java/com/tsmobile/app/ui/components/MessageItem.kt index eec89d4..deb39a1 100644 --- a/android/app/src/main/java/com/tsmobile/app/ui/components/MessageItem.kt +++ b/android/app/src/main/java/com/tsmobile/app/ui/components/MessageItem.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.tsmobile.app.data.ChatMessage +import com.tsmobile.app.data.EmojiMapper import com.tsmobile.app.data.FileDownloadManager import com.tsmobile.app.data.FileMessageMeta import com.tsmobile.app.data.MessageDeliveryState @@ -58,7 +59,7 @@ fun MessageItem(message: ChatMessage) { verticalAlignment = Alignment.CenterVertically, ) { Text( - text = message.content, + text = EmojiMapper.replace(message.content), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -112,14 +113,6 @@ fun MessageItem(message: ChatMessage) { style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) - if (message.isSelf) { - Spacer(Modifier.width(8.dp)) - Text( - text = message.senderName, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.primary - ) - } } Spacer(Modifier.height(4.dp)) @@ -158,7 +151,7 @@ fun MessageItem(message: ChatMessage) { tonalElevation = 1.dp ) { Text( - text = message.content, + text = EmojiMapper.replace(message.content), style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp) ) diff --git a/android/app/src/main/java/com/tsmobile/app/ui/components/PTTButton.kt b/android/app/src/main/java/com/tsmobile/app/ui/components/PTTButton.kt index f0db9c5..f383d75 100644 --- a/android/app/src/main/java/com/tsmobile/app/ui/components/PTTButton.kt +++ b/android/app/src/main/java/com/tsmobile/app/ui/components/PTTButton.kt @@ -11,8 +11,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription @@ -37,6 +39,7 @@ fun PTTButton( val semanticColors = MaterialTheme.semanticColors val currentOnStartTransmit by rememberUpdatedState(onStartTransmit) val currentOnStopTransmit by rememberUpdatedState(onStopTransmit) + val haptic = LocalHapticFeedback.current Button( onClick = {}, @@ -51,6 +54,8 @@ fun PTTButton( if (!enabled) return@pointerInput awaitEachGesture { awaitFirstDown(requireUnconsumed = false) + // 按下时给予触觉反馈,确认发言动作已开始。 + haptic.performHapticFeedback(HapticFeedbackType.LongPress) currentOnStartTransmit() try { waitForUpOrCancellation() diff --git a/android/app/src/main/java/com/tsmobile/app/ui/navigation/NavGraph.kt b/android/app/src/main/java/com/tsmobile/app/ui/navigation/NavGraph.kt index 391de8d..147bbc5 100644 --- a/android/app/src/main/java/com/tsmobile/app/ui/navigation/NavGraph.kt +++ b/android/app/src/main/java/com/tsmobile/app/ui/navigation/NavGraph.kt @@ -3,6 +3,12 @@ package com.tsmobile.app.ui.navigation import androidx.activity.compose.BackHandler import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable @@ -128,11 +134,46 @@ fun AppNavGraph( NavHost( navController = navController, startDestination = Routes.SERVER_CONFIG, - // 禁用默认的淡入淡出动画,避免页面切换显得迟钝 - enterTransition = { EnterTransition.None }, - exitTransition = { ExitTransition.None }, - popEnterTransition = { EnterTransition.None }, - popExitTransition = { ExitTransition.None }, + // 默认不动画;仅聊天页做“从右滑入/滑出”的过渡,避免整套页面切换显得迟钝。 + enterTransition = { + when (targetState.destination.route) { + // 前进到主界面(首页→频道页、频道页→聊天页):从右侧滑入。 + Routes.CHAT, Routes.CHANNEL_LIST -> + slideInHorizontally(tween(340, easing = FastOutSlowInEasing)) { it } + + fadeIn(tween(280, easing = FastOutSlowInEasing)) + // 断开/被踢后回登录页:从左侧滑入(像“返回主页”)。 + Routes.SERVER_CONFIG -> + slideInHorizontally(tween(340, easing = FastOutSlowInEasing)) { -it } + + fadeIn(tween(280, easing = FastOutSlowInEasing)) + else -> EnterTransition.None + } + }, + exitTransition = { + // 前进/退回到主页面时,底层页面轻微淡出,形成“叠影推进”而不生硬。 + val from = initialState.destination.route + val to = targetState.destination.route + val overlapped = + (from == Routes.SERVER_CONFIG && to == Routes.CHANNEL_LIST) || + (from == Routes.CHANNEL_LIST && to == Routes.CHAT) || + (from == Routes.CHANNEL_LIST && to == Routes.SERVER_CONFIG) + if (overlapped) fadeOut(tween(240, easing = FastOutSlowInEasing)) else ExitTransition.None + }, + popEnterTransition = { + if (targetState.destination.route == Routes.CHANNEL_LIST) { + slideInHorizontally(tween(340, easing = FastOutSlowInEasing)) { -it } + + fadeIn(tween(280, easing = FastOutSlowInEasing)) + } else { + EnterTransition.None + } + }, + popExitTransition = { + if (initialState.destination.route == Routes.CHAT) { + slideOutHorizontally(tween(320, easing = FastOutSlowInEasing)) { it } + + fadeOut(tween(280, easing = FastOutSlowInEasing)) + } else { + ExitTransition.None + } + }, ) { // 服务器配置页 composable(Routes.SERVER_CONFIG) { @@ -240,4 +281,3 @@ fun AppNavGraph( ) } } - diff --git a/android/app/src/main/java/com/tsmobile/app/ui/screens/ChannelListScreen.kt b/android/app/src/main/java/com/tsmobile/app/ui/screens/ChannelListScreen.kt index 0f681a3..e256f49 100644 --- a/android/app/src/main/java/com/tsmobile/app/ui/screens/ChannelListScreen.kt +++ b/android/app/src/main/java/com/tsmobile/app/ui/screens/ChannelListScreen.kt @@ -3,8 +3,17 @@ package com.tsmobile.app.ui.screens import android.content.ClipData import android.content.ClipboardManager import android.content.Context +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* @@ -17,8 +26,10 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @@ -681,49 +692,96 @@ private fun CurrentChannelBar( val currentChannelId by channelViewModel.currentChannelId.collectAsState() val channels by channelViewModel.channels.collectAsState() val channelClients by channelViewModel.channelClients.collectAsState() + val selfClientId by channelViewModel.selfClientId.collectAsState() val currentChannel = channels.find { it.id == currentChannelId } - val clients = channelClients[currentChannelId] ?: emptyList() if (currentChannel != null) { - val selfClientId by channelViewModel.selfClientId.collectAsState() - val previewCandidates = clients.sortedByDescending { it.id == selfClientId } - val semanticColors = MaterialTheme.semanticColors - - Column( + Surface( modifier = Modifier .fillMaxWidth() - .height(68.dp) - .clickable(onClick = onClick) - .background(MaterialTheme.colorScheme.surfaceVariant) - .padding( - start = FOOTER_LEFT_INDENT, - end = FOOTER_RIGHT_INDENT, - top = UiTokens.Spacing.ExtraSmall, - bottom = UiTokens.Spacing.ExtraSmall, - ), - verticalArrangement = Arrangement.spacedBy(UiTokens.Spacing.ExtraSmall), + .clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)) + .clickable(onClick = onClick), + color = MaterialTheme.colorScheme.surfaceContainer, + tonalElevation = UiTokens.Elevation.Subtle, ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - Icons.Default.LocationOn, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(UiTokens.Size.IconMedium), - ) - Spacer(Modifier.width(UiTokens.Spacing.Small)) + // 切换频道时,当前频道栏内容做淡入淡出 + 轻微横向滑入动画。 + AnimatedContent( + targetState = currentChannelId, + transitionSpec = { + (slideInHorizontally(animationSpec = tween(300, easing = FastOutSlowInEasing), initialOffsetX = { it / 4 }) + + fadeIn(animationSpec = tween(260, easing = FastOutSlowInEasing))) + .togetherWith( + slideOutHorizontally(animationSpec = tween(280, easing = FastOutSlowInEasing), targetOffsetX = { -it / 4 }) + + fadeOut(animationSpec = tween(200, easing = FastOutSlowInEasing)) + ) + }, + label = "currentChannelBar", + ) { channelId -> + val channel = channels.find { it.id == channelId } + val members = channelClients[channelId] ?: emptyList() + if (channel != null) { + CurrentChannelBarContent( + channel = channel, + clients = members, + selfClientId = selfClientId, + ) + } + } + } + } +} + +@Composable +private fun CurrentChannelBarContent( + channel: ChannelInfo, + clients: List, + selfClientId: Int, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = UiTokens.Size.AppBarHeight) + .padding( + start = FOOTER_LEFT_INDENT, + top = UiTokens.Spacing.Medium, + end = FOOTER_RIGHT_INDENT, + bottom = UiTokens.Spacing.Medium, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + // 频道图标(tonal 圆) + Box( + modifier = Modifier + .size(UiTokens.Size.ControlHeight) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primaryContainer), + contentAlignment = Alignment.Center, + ) { + Icon( + Icons.Default.LocationOn, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.size(UiTokens.Size.IconMedium), + ) + } + + Spacer(Modifier.width(UiTokens.Spacing.Medium)) + + // 频道名 + 在线人数 + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = currentChannel.name, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium, + text = channel.name, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f), ) - if (currentChannel.isPassword) { + if (channel.isPassword) { Spacer(Modifier.width(UiTokens.Spacing.ExtraSmall)) Icon( Icons.Default.Lock, @@ -732,109 +790,85 @@ private fun CurrentChannelBar( tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } - Spacer(Modifier.width(UiTokens.Spacing.Small)) - Surface( - shape = MaterialTheme.shapes.small, - color = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, - ) { - Text( - text = if (currentChannel.maxClients >= 0) { - "${clients.size}/${currentChannel.maxClients}" - } else { - "${clients.size}" - }, - style = MaterialTheme.typography.labelSmall, - modifier = Modifier.padding(horizontal = UiTokens.Spacing.Small, vertical = UiTokens.Spacing.ExtraSmall), - ) - } - } - - BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { - val maxPreviewCount = when { - maxWidth >= 380.dp -> 5 - maxWidth >= 320.dp -> 4 - else -> 3 - } - val visibleClients = previewCandidates.take(maxPreviewCount) - val hiddenClientCount = clients.size - visibleClients.size - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(UiTokens.Spacing.ExtraSmall), - verticalAlignment = Alignment.CenterVertically, - ) { - visibleClients.forEach { client -> - val isSelf = client.id == selfClientId - val isGrouped = client.serverGroups.isNotEmpty() - MemberPreviewTag( - name = client.nickname, - containerColor = when { - isSelf -> MaterialTheme.colorScheme.primaryContainer - isGrouped -> MaterialTheme.colorScheme.tertiaryContainer - else -> MaterialTheme.colorScheme.surfaceContainerHighest - }, - contentColor = when { - isSelf -> MaterialTheme.colorScheme.onPrimaryContainer - isGrouped -> MaterialTheme.colorScheme.onTertiaryContainer - else -> MaterialTheme.colorScheme.onSurfaceVariant - }, - showPresence = !isSelf && !isGrouped, - presenceColor = semanticColors.success, - modifier = Modifier.weight(1f, fill = false), - ) - } - if (hiddenClientCount > 0) { - Text( - text = "+$hiddenClientCount", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - ) - } - } } + Text( + text = "在线 ${clients.size} 人", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) } + + // 成员头像堆叠 + MemberAvatarStack( + clients = clients, + selfClientId = selfClientId, + maxVisible = 3, + ) + + Spacer(Modifier.width(UiTokens.Spacing.Small)) } } @Composable -private fun MemberPreviewTag( - name: String, - containerColor: Color, - contentColor: Color, - showPresence: Boolean = false, - presenceColor: Color = Color.Unspecified, - modifier: Modifier = Modifier, +private fun MemberAvatarStack( + clients: List, + selfClientId: Int, + maxVisible: Int = 3, ) { - Surface( - modifier = modifier, - shape = MaterialTheme.shapes.small, - color = containerColor, - contentColor = contentColor, + val visible = clients.sortedByDescending { it.id == selfClientId }.take(maxVisible) + val hidden = clients.size - visible.size + val avatarSize = 28.dp + + Row( + horizontalArrangement = Arrangement.spacedBy((-6).dp), + verticalAlignment = Alignment.CenterVertically, ) { - Row( - modifier = Modifier - .widthIn(max = 96.dp) - .height(24.dp) - .padding(horizontal = UiTokens.Spacing.Small), - verticalAlignment = Alignment.CenterVertically, - ) { - if (showPresence) { - Box( - modifier = Modifier - .size(6.dp) - .background(presenceColor, CircleShape), + visible.forEach { client -> + val isSelf = client.id == selfClientId + val isGrouped = client.serverGroups.isNotEmpty() + Box( + modifier = Modifier + .size(avatarSize) + .clip(CircleShape) + .background( + when { + isSelf -> MaterialTheme.colorScheme.primaryContainer + isGrouped -> MaterialTheme.colorScheme.tertiaryContainer + else -> MaterialTheme.colorScheme.surfaceContainerHighest + } + ) + .border( + width = UiTokens.Border.Emphasized, + color = MaterialTheme.colorScheme.surface, + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = client.nickname.take(1).ifBlank { "?" }, + style = MaterialTheme.typography.labelSmall, + color = when { + isSelf -> MaterialTheme.colorScheme.onPrimaryContainer + isGrouped -> MaterialTheme.colorScheme.onTertiaryContainer + else -> MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + } + } + if (hidden > 0) { + Box( + modifier = Modifier + .size(avatarSize) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHighest), + contentAlignment = Alignment.Center, + ) { + Text( + text = "+$hidden", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) - Spacer(Modifier.width(UiTokens.Spacing.ExtraSmall)) } - Text( - text = name, - style = MaterialTheme.typography.labelSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f, fill = false), - ) } } } diff --git a/android/app/src/main/java/com/tsmobile/app/ui/screens/ServerConfigScreen.kt b/android/app/src/main/java/com/tsmobile/app/ui/screens/ServerConfigScreen.kt index 0760557..ba83f0c 100644 --- a/android/app/src/main/java/com/tsmobile/app/ui/screens/ServerConfigScreen.kt +++ b/android/app/src/main/java/com/tsmobile/app/ui/screens/ServerConfigScreen.kt @@ -28,6 +28,7 @@ import com.tsmobile.app.ui.components.ConnectButton import com.tsmobile.app.ui.components.UpdateCheckDialog import com.tsmobile.app.viewmodel.ConnectState import com.tsmobile.app.viewmodel.ServerViewModel +import com.tsmobile.app.viewmodel.ValidationErrors import com.tsmobile.app.viewmodel.formatRelativeTime /** @@ -42,6 +43,8 @@ fun ServerConfigScreen( val state by viewModel.state.collectAsState() val recents by viewModel.recents.collectAsState() val themeMode by viewModel.themeMode.collectAsState() + // 长按删除最近连接的确认目标(非 null 时弹出确认框) + var pendingRemoveRecent by remember { mutableStateOf(null) } Column( modifier = Modifier @@ -66,6 +69,7 @@ fun ServerConfigScreen( onPasswordChange = viewModel::updatePassword, connectState = state.connectState, errorMessage = state.errorMessage, + validationErrors = state.validationErrors, onConnect = { viewModel.connect() }, ) @@ -74,7 +78,7 @@ fun ServerConfigScreen( recents = recents, enabled = state.connectState != ConnectState.CONNECTING, onConnectRecent = { recent -> viewModel.quickConnect(recent) }, - onRemoveRecent = { recent -> viewModel.removeRecent(recent) }, + onRemoveRecent = { recent -> pendingRemoveRecent = recent }, onClearAll = { viewModel.clearRecents() }, ) } @@ -97,6 +101,30 @@ fun ServerConfigScreen( onClose = { viewModel.closeUpdateDialog() }, ) } + + // 删除最近连接确认弹框 + pendingRemoveRecent?.let { target -> + AlertDialog( + onDismissRequest = { pendingRemoveRecent = null }, + title = { Text("删除最近连接") }, + text = { Text("确定删除 ${target.address} 吗?此操作不会影响服务器。") }, + confirmButton = { + TextButton( + onClick = { + viewModel.removeRecent(target) + pendingRemoveRecent = null + }, + ) { + Text("删除", color = MaterialTheme.colorScheme.error) + } + }, + dismissButton = { + TextButton(onClick = { pendingRemoveRecent = null }) { + Text("取消") + } + }, + ) + } } /** @@ -192,8 +220,12 @@ private fun InputSection( onPasswordChange: (String) -> Unit, connectState: ConnectState, errorMessage: String?, + validationErrors: ValidationErrors, onConnect: () -> Unit, ) { + val addressValidation = validationErrors.address + val nicknameValidation = validationErrors.nickname + Column( modifier = Modifier .fillMaxWidth() @@ -206,7 +238,10 @@ private fun InputSection( label = { Text("服务器地址") }, placeholder = { Text("ts.example.com") }, singleLine = true, - isError = connectState == ConnectState.FAILED && address.isBlank(), + isError = addressValidation != null, + supportingText = if (addressValidation != null) { + { Text(addressValidation, color = MaterialTheme.colorScheme.error) } + } else null, modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), ) @@ -220,7 +255,10 @@ private fun InputSection( label = { Text("昵称") }, placeholder = { Text("我的昵称") }, singleLine = true, - isError = connectState == ConnectState.FAILED && nickname.isBlank(), + isError = nicknameValidation != null, + supportingText = if (nicknameValidation != null) { + { Text(nicknameValidation, color = MaterialTheme.colorScheme.error) } + } else null, modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), ) diff --git a/android/app/src/main/java/com/tsmobile/app/viewmodel/ChannelViewModel.kt b/android/app/src/main/java/com/tsmobile/app/viewmodel/ChannelViewModel.kt index 3600d1c..060c59d 100644 --- a/android/app/src/main/java/com/tsmobile/app/viewmodel/ChannelViewModel.kt +++ b/android/app/src/main/java/com/tsmobile/app/viewmodel/ChannelViewModel.kt @@ -67,8 +67,9 @@ class ChannelViewModel(application: Application) : AndroidViewModel(application) private val json = Json { ignoreUnknownKeys = true } - // ChatViewModel 引用(由 ServerViewModel 设置) - var chatViewModel: ChatViewModel? = null + // ChatViewModel 引用(由 ServerViewModel 设置)。 + // 主线程赋值,桥接回调解读,需 @Volatile 保证可见性。 + @Volatile var chatViewModel: ChatViewModel? = null // 频道列表最后刷新时间 private var lastChannelRefreshTime: Long = 0 diff --git a/android/app/src/main/java/com/tsmobile/app/viewmodel/ServerViewModel.kt b/android/app/src/main/java/com/tsmobile/app/viewmodel/ServerViewModel.kt index cf92948..ad662ac 100644 --- a/android/app/src/main/java/com/tsmobile/app/viewmodel/ServerViewModel.kt +++ b/android/app/src/main/java/com/tsmobile/app/viewmodel/ServerViewModel.kt @@ -80,14 +80,13 @@ class ServerViewModel(application: Application) : AndroidViewModel(application) private val recentStore = RecentConnectionsStore(application) private val identityStore = IdentityStore(application) - // ChannelViewModel 引用(由 NavGraph 设置) - var channelViewModel: ChannelViewModel? = null + // ChannelViewModel 引用(由 NavGraph 设置)。 + // NavGraph 在主线程赋值,桥接回调解读,需 @Volatile 保证可见性,避免读到过期 null。 + @Volatile var channelViewModel: ChannelViewModel? = null - // ChatViewModel 引用(由 NavGraph 设置) - var chatViewModel: ChatViewModel? = null + @Volatile var chatViewModel: ChatViewModel? = null - // VoiceViewModel 引用(由 NavGraph 设置) - var voiceViewModel: VoiceViewModel? = null + @Volatile var voiceViewModel: VoiceViewModel? = null // 页面状态 private val _state = MutableStateFlow(ServerScreenState()) @@ -142,12 +141,16 @@ class ServerViewModel(application: Application) : AndroidViewModel(application) val showPokeNotification: StateFlow = _showPokeNotification.asStateFlow() // ── 重连状态 ── - private var reconnectAttempt = 0 - private var reconnectJob: Job? = null - private var lastConnectParams: ConnectParams? = null + // 这些字段被桥接回调线程(onDisconnected / onKicked)与主线程 + // (init 网络收集器、手动操作)并发访问。加 @Volatile 保证跨线程可见性, + // 并用 reconnectLock 串行化重连循环的启动/取消,避免并发启动两条重连。 + private val reconnectLock = Any() + @Volatile private var reconnectAttempt = 0 + @Volatile private var reconnectJob: Job? = null + @Volatile private var lastConnectParams: ConnectParams? = null /** 断联前的频道 ID,用于伪重连后自动回到原频道(空字符串=不自动移动) */ - private var lastChannelId: String = "" + @Volatile private var lastChannelId: String = "" // ── 网络监控 ── private val networkMonitor = NetworkMonitor(application) @@ -393,11 +396,23 @@ class ServerViewModel(application: Application) : AndroidViewModel(application) // --- 输入更新 --- fun updateAddress(value: String) { - _state.update { it.copy(address = value, errorMessage = null) } + _state.update { + it.copy( + address = value, + errorMessage = null, + validationErrors = it.validationErrors.copy(address = null), + ) + } } fun updateNickname(value: String) { - _state.update { it.copy(nickname = value, errorMessage = null) } + _state.update { + it.copy( + nickname = value, + errorMessage = null, + validationErrors = it.validationErrors.copy(nickname = null), + ) + } } fun updatePassword(value: String) { @@ -783,8 +798,10 @@ class ServerViewModel(application: Application) : AndroidViewModel(application) networkMonitor.reset() // 1. 停止重连 - reconnectJob?.let { it.cancel() } - reconnectJob = null + synchronized(reconnectLock) { + reconnectJob?.let { it.cancel() } + reconnectJob = null + } // 2. 设置断开中状态 _connectionState.value = ConnectionState.Disconnecting @@ -809,14 +826,14 @@ class ServerViewModel(application: Application) : AndroidViewModel(application) // 6. 重置状态 _connectionState.value = null _state.update { it.copy(connectState = ConnectState.IDLE) } - lastConnectParams = null + synchronized(reconnectLock) { lastConnectParams = null } } /** * 处理被踢后的重新连接 */ fun reconnectAfterKick() { - val params = lastConnectParams ?: return + val params = synchronized(reconnectLock) { lastConnectParams } ?: return _kickReason.value = "" _connectionState.value = null @@ -836,13 +853,15 @@ class ServerViewModel(application: Application) : AndroidViewModel(application) * 返回主页(被踢后或断开后) */ fun backToHome() { - reconnectJob?.let { it.cancel() } - reconnectJob = null - lastChannelId = "" + synchronized(reconnectLock) { + reconnectJob?.let { it.cancel() } + reconnectJob = null + lastChannelId = "" + lastConnectParams = null + } networkMonitor.reset() _kickReason.value = "" _connectionState.value = null - lastConnectParams = null Repository.clearSession() channelViewModel?.clearChannels() _state.update { it.copy(connectState = ConnectState.IDLE) } @@ -854,56 +873,60 @@ class ServerViewModel(application: Application) : AndroidViewModel(application) * 最多尝试 5 次,间隔递增(2s → 4s → 8s → 16s → 30s) */ private fun startReconnect(reason: String) { - reconnectAttempt = 0 - _connectionState.value = ConnectionState.Reconnecting( - attempt = 0, reason = reason - ) - - reconnectJob = viewModelScope.launch { - while (reconnectAttempt < MAX_RECONNECT_ATTEMPTS && - _connectionState.value is ConnectionState.Reconnecting - ) { - val delayMs = RECONNECT_DELAYS[reconnectAttempt] - android.util.Log.i(TAG, "Reconnect attempt ${reconnectAttempt + 1}/$MAX_RECONNECT_ATTEMPTS in ${delayMs}ms") - delay(delayMs) - - reconnectAttempt++ - _connectionState.value = ConnectionState.Reconnecting( - attempt = reconnectAttempt, reason = reason - ) - - val params = lastConnectParams ?: break - val error = withContext(Dispatchers.IO) { - val identity = identityStore.getOrCreate { Teamspeak.generateIdentity(8) } - if (identity.isBlank()) return@withContext "身份生成失败" - TSBridge.connectWithIdentity( - identity = identity, - host = params.host, - nickname = params.nickname, - password = params.password, - defaultChannel = params.defaultChannel, - defaultChannelPassword = params.defaultChannelPassword, - callbacks = createBridgeCallbacks(), - ) - } - - if (error.isEmpty()) { - android.util.Log.i(TAG, "Reconnect succeeded on attempt $reconnectAttempt") - // 重连成功由 onConnected 回调处理 - return@launch - } - - android.util.Log.w(TAG, "Reconnect attempt $reconnectAttempt failed: $error") - } - - // 重连失败 - android.util.Log.w(TAG, "All reconnect attempts failed") - ConnectionService.stop(getApplication()) // 清除“重连中”通知 - Repository.clearSession() - channelViewModel?.clearChannels() - _connectionState.value = ConnectionState.Disconnected( - reason = reason, wasKicked = false + synchronized(reconnectLock) { + // 取消仍在运行的上一次循环,确保始终只有一条重连流程。 + reconnectJob?.let { it.cancel() } + reconnectAttempt = 0 + _connectionState.value = ConnectionState.Reconnecting( + attempt = 0, reason = reason ) + + reconnectJob = viewModelScope.launch { + while (reconnectAttempt < MAX_RECONNECT_ATTEMPTS && + _connectionState.value is ConnectionState.Reconnecting + ) { + val delayMs = RECONNECT_DELAYS[reconnectAttempt] + android.util.Log.i(TAG, "Reconnect attempt ${reconnectAttempt + 1}/$MAX_RECONNECT_ATTEMPTS in ${delayMs}ms") + delay(delayMs) + + reconnectAttempt++ + _connectionState.value = ConnectionState.Reconnecting( + attempt = reconnectAttempt, reason = reason + ) + + val params = lastConnectParams ?: break + val error = withContext(Dispatchers.IO) { + val identity = identityStore.getOrCreate { Teamspeak.generateIdentity(8) } + if (identity.isBlank()) return@withContext "身份生成失败" + TSBridge.connectWithIdentity( + identity = identity, + host = params.host, + nickname = params.nickname, + password = params.password, + defaultChannel = params.defaultChannel, + defaultChannelPassword = params.defaultChannelPassword, + callbacks = createBridgeCallbacks(), + ) + } + + if (error.isEmpty()) { + android.util.Log.i(TAG, "Reconnect succeeded on attempt $reconnectAttempt") + // 重连成功由 onConnected 回调处理 + return@launch + } + + android.util.Log.w(TAG, "Reconnect attempt $reconnectAttempt failed: $error") + } + + // 重连失败 + android.util.Log.w(TAG, "All reconnect attempts failed") + ConnectionService.stop(getApplication()) // 清除“重连中”通知 + Repository.clearSession() + channelViewModel?.clearChannels() + _connectionState.value = ConnectionState.Disconnected( + reason = reason, wasKicked = false + ) + } } } @@ -911,11 +934,12 @@ class ServerViewModel(application: Application) : AndroidViewModel(application) * 手动重连(用户点击"手动重连"按钮) */ fun manualReconnect() { - reconnectJob?.let { it.cancel() } - reconnectJob = null - reconnectAttempt = 0 - - val params = lastConnectParams ?: return + synchronized(reconnectLock) { + reconnectJob?.let { it.cancel() } + reconnectJob = null + reconnectAttempt = 0 + } + val params = synchronized(reconnectLock) { lastConnectParams } ?: return _connectionState.value = ConnectionState.Reconnecting(attempt = 0, reason = "手动重连") ConnectionService.update(getApplication(), "正在重连...", _state.value.address.trim()) @@ -947,11 +971,13 @@ class ServerViewModel(application: Application) : AndroidViewModel(application) * 放弃重连 */ fun abandonReconnect() { - reconnectJob?.let { it.cancel() } - reconnectJob = null - reconnectAttempt = 0 - - lastChannelId = "" + synchronized(reconnectLock) { + reconnectJob?.let { it.cancel() } + reconnectJob = null + reconnectAttempt = 0 + lastChannelId = "" + lastConnectParams = null + } networkMonitor.reset() ConnectionService.stop(getApplication()) // 清除“重连中”通知 Repository.clearSession() @@ -959,7 +985,6 @@ class ServerViewModel(application: Application) : AndroidViewModel(application) _connectionState.value = ConnectionState.Disconnected( reason = "重连已放弃", wasKicked = false ) - lastConnectParams = null _state.update { it.copy(connectState = ConnectState.IDLE) } } @@ -1023,8 +1048,11 @@ class ServerViewModel(application: Application) : AndroidViewModel(application) // 伪重连:回到断联前的频道(避开僵尸清理窗口) // 注意:此时不设置 Connected —— 遮罩保持显示,防止用户干扰自动操作 - val targetChannel = lastChannelId - lastChannelId = "" // 一次性消费 + val targetChannel = synchronized(reconnectLock) { + val saved = lastChannelId + lastChannelId = "" // 一次性消费 + saved + } if (targetChannel.isNotEmpty() && targetChannel != "0") { // 等待 4 秒:Go 侧 cleanupDuplicateIdentitySessions 在 3 秒后 Kick 僵尸 delay(4_000L) @@ -1103,7 +1131,7 @@ class ServerViewModel(application: Application) : AndroidViewModel(application) // 记录当前频道,供重连后自动回到原频道 val currentChId = Repository.currentChannelId.value if (currentChId != "0" && currentChId.isNotEmpty()) { - lastChannelId = currentChId + synchronized(reconnectLock) { lastChannelId = currentChId } android.util.Log.i(TAG, "Saved lastChannelId=$lastChannelId for pseudo-reconnect") } @@ -1115,7 +1143,7 @@ class ServerViewModel(application: Application) : AndroidViewModel(application) // 服务器主动断线 — 清除通知、清理会话,回到首页 android.util.Log.w(TAG, "Server-initiated disconnect: $message") ConnectionService.stop(getApplication()) - lastChannelId = "" + synchronized(reconnectLock) { lastChannelId = "" } Repository.clearSession() channelViewModel?.clearChannels() _connectionState.value = ConnectionState.Disconnected( @@ -1199,8 +1227,10 @@ class ServerViewModel(application: Application) : AndroidViewModel(application) ConnectionService.stop(getApplication()) // 停止重连 - reconnectJob?.let { it.cancel() } - reconnectJob = null + synchronized(reconnectLock) { + reconnectJob?.let { it.cancel() } + reconnectJob = null + } // 停止语音 voiceViewModel?.stopVoice() diff --git a/android/app/src/test/java/com/tsmobile/app/data/EmojiMapperTest.kt b/android/app/src/test/java/com/tsmobile/app/data/EmojiMapperTest.kt new file mode 100644 index 0000000..27228c7 --- /dev/null +++ b/android/app/src/test/java/com/tsmobile/app/data/EmojiMapperTest.kt @@ -0,0 +1,37 @@ +package com.tsmobile.app.data + +import org.junit.Assert.assertEquals +import org.junit.Test + +class EmojiMapperTest { + + @Test + fun mapsKnownShortcodes() { + assertEquals("😄🔥❤️", EmojiMapper.replace(":smile::fire::heart:")) + } + + @Test + fun mapsTeamSpeakShortcodes() { + assertEquals("🥺🐹👴", EmojiMapper.replace(":pleading::hamster::older_man:")) + } + + @Test + fun mapsMixedText() { + assertEquals("Hello 😄 world", EmojiMapper.replace("Hello :smile: world")) + } + + @Test + fun keepsUnknownShortcodesUnchanged() { + assertEquals(":not_a_real_emoji:", EmojiMapper.replace(":not_a_real_emoji:")) + } + + @Test + fun leavesPlainTextUntouched() { + assertEquals("plain text 123", EmojiMapper.replace("plain text 123")) + } + + @Test + fun handlesAdjacentShortcodesWithoutSpaces() { + assertEquals("😄❤️", EmojiMapper.replace(":smile::heart:")) + } +}