packages feed

matterhorn 50200.3.1 → 50200.4.0

raw patch · 57 files changed

+1923/−763 lines, 57 filesdep ~mattermost-apidep ~mattermost-api-qc

Dependency ranges changed: mattermost-api, mattermost-api-qc

Files

CHANGELOG.md view
@@ -1,4 +1,53 @@ +50200.4.0+=========++New features:+ * This release includes support for viewing, adding, and removing post+   emoji reactions:+    * A new message select keybinding (default `a`, event+      'react-to-message') opens an emoji search window.+    * Pressing 'Enter' in the emoji search window posts the selected+      reaction to the selected message if you have not already posted+      that reaction, or removes it from the message otherwise.+    * The emoji search window automatically lists reactions that other+      users have posted before other emoji, along with your own posted+      reactions. This allows you to easily add your vote to (or remove)+      reactions.+    * Emoji reactions posted to a message are now displayed in the+      message view window (message select binding `v`).+ * Emoji are now supported in autocompletion by pressing `TAB` after a+   colon (`:`). The autocompletion list also includes custom emoji+   obtained from the server. The standard emoji are sourced from the+   file `emoji.json` located in the same directory as the Matterhorn+   configuration file `config.ini`, or at the path `emoji/emoji.json`+   relative to the location of the `matterhorn` binary (as in releases).+ * The channel list shown by the `/join` command has been redesigned to+   support incremental search similar to the UI for `/msg`.+ * Special `@all` and `@channel` mentions are now highlighted and+   included in autocompletion. (#491)+ * If a channel has new messages that go beyond the top of the displayed+   message area, a `New Messages` line appears at the top of the window+   to indicate that more unread messages precede it. (#131)++Other improvements:+ * The subprocess logger indicates when unexpected stdout output was+   received. (#503)+ * The log manager thread is now started before authentication begins so+   that the authentication code can do logging.+ * Keybinding event names are now included in auto-generated text and+   Markdown keybinding tables.+ * User status updates are automatically batched, resulting in `O(1)`+   user status requests on startup rather than `O(n)`.+ * Command help text now includes `@` and `~` in arguments to more+   clearly indicate user and channel arguments respectively.++Bug fixes:+ * Theme changes now invalidate the rendering cache, fixing color+   artifacts left over in the channel message area.+ * When searching for users, username sigils are always trimed first.+   (#497)+ 50200.3.1 ========= 
README.md view
@@ -79,8 +79,8 @@  * The `/help` command within Matterhorn. * The `F1` key within Matterhorn.-* Running matterhorn with the `-k` argument for text keybindings-* Running matterhorn with the `-K` argument for [Markdown keybindings](keybindings.md)+* Running matterhorn with the `-k` argument to get a text table of available keybindings+* Running matterhorn with the `-K` argument to get a [Markdown table of available keybindings](keybindings.md)      Note: The latter two commands do not start the client but simply     print to stdout and exit.  The bindings shown include any user@@ -111,9 +111,9 @@ To edit your current message in an external editor (`$EDITOR`), use the default binding of `M-k`. -To preview the message you're about to send while you compose it (e.g.-to check on how your Markdown syntax will be rendered), toggle preview-mode with the default binding `M-p`.+To see a live preview of the message you're about to send while you+compose it (e.g. to check on how your Markdown syntax will be rendered),+toggle preview mode with the default binding `M-p`.  To change channels, use `/focus` or one of the default bindings `C-n` (next channel), `C-p` (previous channel), `C-g` (fast channel switch).@@ -167,22 +167,23 @@ * Convenient URL-opening with local browser * Secure password entry via external command (e.g. OSX keychain) * Yank verbatim content from messages into the system clipboard-* Preview message rendering before sending+* Optional live preview during message editing * Optional smart quoting for efficient Markdown entry * Edit messages with `$EDITOR` * Rebindable keys (see `/help keybindings`) * Message editor with kill/yank buffer and readline-style keybindings-* Tab-completion of usernames, channel names, commands, and fence code-  block languages+* Support for adding and removing emoji post reactions+* Tab-completion of usernames, channel names, commands, emoji, and+  fenced code block languages * Support for attachment upload and download * Spell-checking via Aspell * Syntax highlighting of fenced code blocks in messages (works best in   256-color terminals) * Flagging and unflagging of posts, which are then viewable with `M-8`   or `/flags`-* Support for SOCKS 4 and 5 proxies via the `ALL_PROXY`, `HTTP_PROXY`,-  and `HTTPS_PROXY` environment variables. (Plain HTTP proxies are not-  yet supported.)+* Support for SOCKS 4 and 5 proxies via the `ALL_PROXY` and+  `HTTPS_PROXY` environment variables. (Plain HTTP proxies are not yet+  supported.) Also supports `NO_PROXY`. * Multiple color themes with color theme customization support * Custom notifications via notification scripts (see the   `activityNotifyCommand` configuration setting).@@ -210,6 +211,22 @@  * Enter any message input in the message editor in `matterhorn`. After    a short delay after you stop typing, misspelled words will turn red. +# Emoji++Matterhorn loads its emoji list (for auto-completion of `:...:`) at+startup. It looks for an `emoji.json` file in the following locations+and in the following order:++* `~/.config/matterhorn/emoji.json`. This option is to support emoji+  list installation and customization.+* `MHBIN/emoji/emoji.json`, where `MHBIN` is the directory containing+  the `matterhorn` binary. This option is to make it convenient to use+  emoji with `matterhorn` from releases.++The `emoji.json` is included in the Matterhorn releases+and is obtained from [the Mattermost web client source+tree](https://github.com/mattermost/mattermost-webapp/blob/master/utils/emoji.json).+ # Building  To build Matterhorn, you'll need an appropriate `ghc`/`cabal`@@ -281,21 +298,37 @@  # Frequently Asked Questions -* Q: Does matterhorn support Gitlab authentication?-* A: No. But we would be happy to work with contributors who are-  interested in investigating what this would take and/or implementing-  it.  See the Contributing section for details.+## Does matterhorn support Gitlab authentication? -* Q: I enabled italicized text in my theme configuration. Why doesn't it-  work?-* A: Most terminfo files for typical terminal configurations do not-  provide support for italicized text. If your terminal emulator-  supports italics, you must enable it in your terminfo database in-  order to use it in Matterhorn. For more information, see these links:-  * http://www.nerdyweekly.com/posts/enable-italic-text-vim-tmux-gnome-terminal/-  * https://medium.com/@dubistkomisch/how-to-actually-get-italics-and-true-colour-to-work-in-iterm-tmux-vim-9ebe55ebc2be-  * https://github.com/tmux/tmux/blob/2.1/FAQ#L355-L383+No. But we would be happy to work with contributors who are interested+in investigating what this would take and/or implementing it.  See the+Contributing section for details.++## How can I get Matterhorn to render emphasized Markdown text with an italic font?++In `~/.config/matterhorn/theme.ini`,+```+[other]+markdownEmph.style = [italic]+```+and in `~/.config/matterhorn/config.ini`,+```+themeCustomizationFile: theme.ini+```++This is known to work on `gnome-terminal` version `3.32.2`, VTE version `0.56.3`; it may work for you, too. Many terminal emulators do not support italics at all or without various hacks. Let us know what works for you!++## I enabled italicized text in my theme configuration. Why doesn't it work?++Most terminfo files for typical terminal configurations do not+provide support for italicized text. If your terminal emulator+supports italics, you must enable it in your terminfo database in+order to use it in Matterhorn. For more information, see these links:+* http://www.nerdyweekly.com/posts/enable-italic-text-vim-tmux-gnome-terminal/+* https://medium.com/@dubistkomisch/how-to-actually-get-italics-and-true-colour-to-work-in-iterm-tmux-vim-9ebe55ebc2be+* https://github.com/tmux/tmux/blob/2.1/FAQ#L355-L383   -* Q: I am seeing malformed characters when I run matterhorn in my terminal. What could be causing this?-* A: Some terminal emulators cannot handle the extra escaping that occurs when the URL hyperlinking mode-  is enabled. Try setting `hyperlinkUrls = False` in your `config.ini` file.+## I am seeing malformed characters when I run matterhorn in my terminal. What could be causing this?++Some terminal emulators cannot handle the extra escaping that occurs when the URL hyperlinking mode+is enabled. Try setting `hyperlinkUrls = False` in your `config.ini` file.
+ emoji/LICENSE.txt view
@@ -0,0 +1,202 @@++                                 Apache License+                           Version 2.0, January 2004+                        http://www.apache.org/licenses/++   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION++   1. Definitions.++      "License" shall mean the terms and conditions for use, reproduction,+      and distribution as defined by Sections 1 through 9 of this document.++      "Licensor" shall mean the copyright owner or entity authorized by+      the copyright owner that is granting the License.++      "Legal Entity" shall mean the union of the acting entity and all+      other entities that control, are controlled by, or are under common+      control with that entity. For the purposes of this definition,+      "control" means (i) the power, direct or indirect, to cause the+      direction or management of such entity, whether by contract or+      otherwise, or (ii) ownership of fifty percent (50%) or more of the+      outstanding shares, or (iii) beneficial ownership of such entity.++      "You" (or "Your") shall mean an individual or Legal Entity+      exercising permissions granted by this License.++      "Source" form shall mean the preferred form for making modifications,+      including but not limited to software source code, documentation+      source, and configuration files.++      "Object" form shall mean any form resulting from mechanical+      transformation or translation of a Source form, including but+      not limited to compiled object code, generated documentation,+      and conversions to other media types.++      "Work" shall mean the work of authorship, whether in Source or+      Object form, made available under the License, as indicated by a+      copyright notice that is included in or attached to the work+      (an example is provided in the Appendix below).++      "Derivative Works" shall mean any work, whether in Source or Object+      form, that is based on (or derived from) the Work and for which the+      editorial revisions, annotations, elaborations, or other modifications+      represent, as a whole, an original work of authorship. For the purposes+      of this License, Derivative Works shall not include works that remain+      separable from, or merely link (or bind by name) to the interfaces of,+      the Work and Derivative Works thereof.++      "Contribution" shall mean any work of authorship, including+      the original version of the Work and any modifications or additions+      to that Work or Derivative Works thereof, that is intentionally+      submitted to Licensor for inclusion in the Work by the copyright owner+      or by an individual or Legal Entity authorized to submit on behalf of+      the copyright owner. For the purposes of this definition, "submitted"+      means any form of electronic, verbal, or written communication sent+      to the Licensor or its representatives, including but not limited to+      communication on electronic mailing lists, source code control systems,+      and issue tracking systems that are managed by, or on behalf of, the+      Licensor for the purpose of discussing and improving the Work, but+      excluding communication that is conspicuously marked or otherwise+      designated in writing by the copyright owner as "Not a Contribution."++      "Contributor" shall mean Licensor and any individual or Legal Entity+      on behalf of whom a Contribution has been received by Licensor and+      subsequently incorporated within the Work.++   2. Grant of Copyright License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      copyright license to reproduce, prepare Derivative Works of,+      publicly display, publicly perform, sublicense, and distribute the+      Work and such Derivative Works in Source or Object form.++   3. Grant of Patent License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      (except as stated in this section) patent license to make, have made,+      use, offer to sell, sell, import, and otherwise transfer the Work,+      where such license applies only to those patent claims licensable+      by such Contributor that are necessarily infringed by their+      Contribution(s) alone or by combination of their Contribution(s)+      with the Work to which such Contribution(s) was submitted. If You+      institute patent litigation against any entity (including a+      cross-claim or counterclaim in a lawsuit) alleging that the Work+      or a Contribution incorporated within the Work constitutes direct+      or contributory patent infringement, then any patent licenses+      granted to You under this License for that Work shall terminate+      as of the date such litigation is filed.++   4. Redistribution. You may reproduce and distribute copies of the+      Work or Derivative Works thereof in any medium, with or without+      modifications, and in Source or Object form, provided that You+      meet the following conditions:++      (a) You must give any other recipients of the Work or+          Derivative Works a copy of this License; and++      (b) You must cause any modified files to carry prominent notices+          stating that You changed the files; and++      (c) You must retain, in the Source form of any Derivative Works+          that You distribute, all copyright, patent, trademark, and+          attribution notices from the Source form of the Work,+          excluding those notices that do not pertain to any part of+          the Derivative Works; and++      (d) If the Work includes a "NOTICE" text file as part of its+          distribution, then any Derivative Works that You distribute must+          include a readable copy of the attribution notices contained+          within such NOTICE file, excluding those notices that do not+          pertain to any part of the Derivative Works, in at least one+          of the following places: within a NOTICE text file distributed+          as part of the Derivative Works; within the Source form or+          documentation, if provided along with the Derivative Works; or,+          within a display generated by the Derivative Works, if and+          wherever such third-party notices normally appear. The contents+          of the NOTICE file are for informational purposes only and+          do not modify the License. You may add Your own attribution+          notices within Derivative Works that You distribute, alongside+          or as an addendum to the NOTICE text from the Work, provided+          that such additional attribution notices cannot be construed+          as modifying the License.++      You may add Your own copyright statement to Your modifications and+      may provide additional or different license terms and conditions+      for use, reproduction, or distribution of Your modifications, or+      for any such Derivative Works as a whole, provided Your use,+      reproduction, and distribution of the Work otherwise complies with+      the conditions stated in this License.++   5. Submission of Contributions. Unless You explicitly state otherwise,+      any Contribution intentionally submitted for inclusion in the Work+      by You to the Licensor shall be under the terms and conditions of+      this License, without any additional terms or conditions.+      Notwithstanding the above, nothing herein shall supersede or modify+      the terms of any separate license agreement you may have executed+      with Licensor regarding such Contributions.++   6. Trademarks. This License does not grant permission to use the trade+      names, trademarks, service marks, or product names of the Licensor,+      except as required for reasonable and customary use in describing the+      origin of the Work and reproducing the content of the NOTICE file.++   7. Disclaimer of Warranty. Unless required by applicable law or+      agreed to in writing, Licensor provides the Work (and each+      Contributor provides its Contributions) on an "AS IS" BASIS,+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+      implied, including, without limitation, any warranties or conditions+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+      PARTICULAR PURPOSE. You are solely responsible for determining the+      appropriateness of using or redistributing the Work and assume any+      risks associated with Your exercise of permissions under this License.++   8. Limitation of Liability. In no event and under no legal theory,+      whether in tort (including negligence), contract, or otherwise,+      unless required by applicable law (such as deliberate and grossly+      negligent acts) or agreed to in writing, shall any Contributor be+      liable to You for damages, including any direct, indirect, special,+      incidental, or consequential damages of any character arising as a+      result of this License or out of the use or inability to use the+      Work (including but not limited to damages for loss of goodwill,+      work stoppage, computer failure or malfunction, or any and all+      other commercial damages or losses), even if such Contributor+      has been advised of the possibility of such damages.++   9. Accepting Warranty or Additional Liability. While redistributing+      the Work or Derivative Works thereof, You may choose to offer,+      and charge a fee for, acceptance of support, warranty, indemnity,+      or other liability obligations and/or rights consistent with this+      License. However, in accepting such obligations, You may act only+      on Your own behalf and on Your sole responsibility, not on behalf+      of any other Contributor, and only if You agree to indemnify,+      defend, and hold each Contributor harmless for any liability+      incurred by, or claims asserted against, such Contributor by reason+      of your accepting any such warranty or additional liability.++   END OF TERMS AND CONDITIONS++   APPENDIX: How to apply the Apache License to your work.++      To apply the Apache License to your work, attach the following+      boilerplate notice, with the fields enclosed by brackets "{}"+      replaced with your own identifying information. (Don't include+      the brackets!)  The text should be enclosed in the appropriate+      comment syntax for the file format. We also recommend that a+      file or class name and description of purpose be included on the+      same "printed page" as the copyright notice for easier+      identification within third-party archives.++   Copyright {yyyy} {name of copyright owner}++   Licensed under the Apache License, Version 2.0 (the "License");+   you may not use this file except in compliance with the License.+   You may obtain a copy of the License at++       http://www.apache.org/licenses/LICENSE-2.0++   Unless required by applicable law or agreed to in writing, software+   distributed under the License is distributed on an "AS IS" BASIS,+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+   See the License for the specific language governing permissions and+   limitations under the License.
+ emoji/NOTICE.txt view
@@ -0,0 +1,4 @@+The Emoji list in this directory was obtained from the Mattermost+project. Also see the included LICENSE.txt file.++https://github.com/mattermost/mattermost-webapp
+ emoji/emoji.json view
@@ -0,0 +1,1 @@+[{"aliases":["grinning"],"filename":"1f600","category":"people","batch":"1"},{"aliases":["smiley"],"filename":"1f603","category":"people","batch":"1"},{"aliases":["smile"],"filename":"1f604","category":"people","batch":"1"},{"aliases":["grin"],"filename":"1f601","category":"people","batch":"1"},{"aliases":["laughing","satisfied"],"filename":"1f606","category":"people","batch":"1"},{"aliases":["sweat_smile"],"filename":"1f605","category":"people","batch":"1"},{"aliases":["joy"],"filename":"1f602","category":"people","batch":"1"},{"aliases":["rofl"],"filename":"1f923","category":"people","batch":"1"},{"aliases":["relaxed"],"filename":"263a-fe0f","category":"people","batch":"1"},{"aliases":["blush"],"filename":"1f60a","category":"people","batch":"1"},{"aliases":["innocent"],"filename":"1f607","category":"people","batch":"1"},{"aliases":["slightly_smiling_face"],"filename":"1f642","category":"people","batch":"1"},{"aliases":["upside_down_face"],"filename":"1f643","category":"people","batch":"1"},{"aliases":["wink"],"filename":"1f609","category":"people","batch":"1"},{"aliases":["relieved"],"filename":"1f60c","category":"people","batch":"1"},{"aliases":["heart_eyes"],"filename":"1f60d","category":"people","batch":"1"},{"aliases":["kissing_heart"],"filename":"1f618","category":"people","batch":"1"},{"aliases":["kissing"],"filename":"1f617","category":"people","batch":"1"},{"aliases":["kissing_smiling_eyes"],"filename":"1f619","category":"people","batch":"1"},{"aliases":["kissing_closed_eyes"],"filename":"1f61a","category":"people","batch":"1"},{"aliases":["yum"],"filename":"1f60b","category":"people","batch":"1"},{"aliases":["stuck_out_tongue_winking_eye"],"filename":"1f61c","category":"people","batch":"1"},{"aliases":["stuck_out_tongue_closed_eyes"],"filename":"1f61d","category":"people","batch":"1"},{"aliases":["stuck_out_tongue"],"filename":"1f61b","category":"people","batch":"1"},{"aliases":["money_mouth_face"],"filename":"1f911","category":"people","batch":"1"},{"aliases":["hugs"],"filename":"1f917","category":"people","batch":"1"},{"aliases":["nerd_face"],"filename":"1f913","category":"people","batch":"1"},{"aliases":["sunglasses"],"filename":"1f60e","category":"people","batch":"1"},{"aliases":["clown_face"],"filename":"1f921","category":"people","batch":"1"},{"aliases":["cowboy_hat_face"],"filename":"1f920","category":"people","batch":"1"},{"aliases":["smirk"],"filename":"1f60f","category":"people","batch":"1"},{"aliases":["unamused"],"filename":"1f612","category":"people","batch":"1"},{"aliases":["disappointed"],"filename":"1f61e","category":"people","batch":"1"},{"aliases":["pensive"],"filename":"1f614","category":"people","batch":"1"},{"aliases":["worried"],"filename":"1f61f","category":"people","batch":"1"},{"aliases":["confused"],"filename":"1f615","category":"people","batch":"1"},{"aliases":["slightly_frowning_face"],"filename":"1f641","category":"people","batch":"1"},{"aliases":["frowning_face"],"filename":"2639-fe0f","category":"people","batch":"1"},{"aliases":["persevere"],"filename":"1f623","category":"people","batch":"1"},{"aliases":["confounded"],"filename":"1f616","category":"people","batch":"1"},{"aliases":["tired_face"],"filename":"1f62b","category":"people","batch":"1"},{"aliases":["weary"],"filename":"1f629","category":"people","batch":"1"},{"aliases":["triumph"],"filename":"1f624","category":"people","batch":"1"},{"aliases":["angry"],"filename":"1f620","category":"people","batch":"1"},{"aliases":["rage","pout"],"filename":"1f621","category":"people","batch":"1"},{"aliases":["no_mouth"],"filename":"1f636","category":"people","batch":"2"},{"aliases":["neutral_face"],"filename":"1f610","category":"people","batch":"2"},{"aliases":["expressionless"],"filename":"1f611","category":"people","batch":"2"},{"aliases":["hushed"],"filename":"1f62f","category":"people","batch":"2"},{"aliases":["frowning"],"filename":"1f626","category":"people","batch":"2"},{"aliases":["anguished"],"filename":"1f627","category":"people","batch":"2"},{"aliases":["open_mouth"],"filename":"1f62e","category":"people","batch":"2"},{"aliases":["astonished"],"filename":"1f632","category":"people","batch":"2"},{"aliases":["dizzy_face"],"filename":"1f635","category":"people","batch":"2"},{"aliases":["flushed"],"filename":"1f633","category":"people","batch":"2"},{"aliases":["scream"],"filename":"1f631","category":"people","batch":"2"},{"aliases":["fearful"],"filename":"1f628","category":"people","batch":"2"},{"aliases":["cold_sweat"],"filename":"1f630","category":"people","batch":"2"},{"aliases":["cry"],"filename":"1f622","category":"people","batch":"2"},{"aliases":["disappointed_relieved"],"filename":"1f625","category":"people","batch":"2"},{"aliases":["drooling_face"],"filename":"1f924","category":"people","batch":"2"},{"aliases":["sob"],"filename":"1f62d","category":"people","batch":"2"},{"aliases":["sweat"],"filename":"1f613","category":"people","batch":"2"},{"aliases":["sleepy"],"filename":"1f62a","category":"people","batch":"2"},{"aliases":["sleeping"],"filename":"1f634","category":"people","batch":"2"},{"aliases":["roll_eyes"],"filename":"1f644","category":"people","batch":"2"},{"aliases":["thinking"],"filename":"1f914","category":"people","batch":"2"},{"aliases":["lying_face"],"filename":"1f925","category":"people","batch":"2"},{"aliases":["grimacing"],"filename":"1f62c","category":"people","batch":"2"},{"aliases":["zipper_mouth_face"],"filename":"1f910","category":"people","batch":"2"},{"aliases":["nauseated_face"],"filename":"1f922","category":"people","batch":"2"},{"aliases":["sneezing_face"],"filename":"1f927","category":"people","batch":"2"},{"aliases":["mask"],"filename":"1f637","category":"people","batch":"2"},{"aliases":["face_with_thermometer"],"filename":"1f912","category":"people","batch":"2"},{"aliases":["face_with_head_bandage"],"filename":"1f915","category":"people","batch":"2"},{"aliases":["smiling_imp"],"filename":"1f608","category":"people","batch":"2"},{"aliases":["imp"],"filename":"1f47f","category":"people","batch":"2"},{"aliases":["japanese_ogre"],"filename":"1f479","category":"people","batch":"2"},{"aliases":["japanese_goblin"],"filename":"1f47a","category":"people","batch":"2"},{"aliases":["hankey","poop","shit"],"filename":"1f4a9","category":"people","batch":"2"},{"aliases":["ghost"],"filename":"1f47b","category":"people","batch":"2"},{"aliases":["skull"],"filename":"1f480","category":"people","batch":"2"},{"aliases":["skull_and_crossbones"],"filename":"2620-fe0f","category":"people","batch":"2"},{"aliases":["alien"],"filename":"1f47d","category":"people","batch":"2"},{"aliases":["space_invader"],"filename":"1f47e","category":"people","batch":"2"},{"aliases":["robot"],"filename":"1f916","category":"people","batch":"2"},{"aliases":["jack_o_lantern"],"filename":"1f383","category":"people","batch":"2"},{"aliases":["smiley_cat"],"filename":"1f63a","category":"people","batch":"2"},{"aliases":["smile_cat"],"filename":"1f638","category":"people","batch":"2"},{"aliases":["joy_cat"],"filename":"1f639","category":"people","batch":"2"},{"aliases":["heart_eyes_cat"],"filename":"1f63b","category":"people","batch":"3"},{"aliases":["smirk_cat"],"filename":"1f63c","category":"people","batch":"3"},{"aliases":["kissing_cat"],"filename":"1f63d","category":"people","batch":"3"},{"aliases":["scream_cat"],"filename":"1f640","category":"people","batch":"3"},{"aliases":["crying_cat_face"],"filename":"1f63f","category":"people","batch":"3"},{"aliases":["pouting_cat"],"filename":"1f63e","category":"people","batch":"3"},{"aliases":["open_hands"],"filename":"1f450","category":"people","batch":"3"},{"aliases":["raised_hands"],"filename":"1f64c","category":"people","batch":"3"},{"aliases":["clap"],"filename":"1f44f","category":"people","batch":"3"},{"aliases":["pray"],"filename":"1f64f","category":"people","batch":"3"},{"aliases":["handshake"],"filename":"1f91d","category":"people","batch":"3"},{"aliases":["+1","thumbsup"],"filename":"1f44d","category":"people","batch":"3"},{"aliases":["-1","thumbsdown"],"filename":"1f44e","category":"people","batch":"3"},{"aliases":["fist_oncoming","facepunch","punch"],"filename":"1f44a","category":"people","batch":"3"},{"aliases":["fist_raised","fist"],"filename":"270a","category":"people","batch":"3"},{"aliases":["fist_left"],"filename":"1f91b","category":"people","batch":"3"},{"aliases":["fist_right"],"filename":"1f91c","category":"people","batch":"3"},{"aliases":["crossed_fingers"],"filename":"1f91e","category":"people","batch":"3"},{"aliases":["v"],"filename":"270c-fe0f","category":"people","batch":"3"},{"aliases":["metal"],"filename":"1f918","category":"people","batch":"3"},{"aliases":["ok_hand"],"filename":"1f44c","category":"people","batch":"3"},{"aliases":["point_left"],"filename":"1f448","category":"people","batch":"3"},{"aliases":["point_right"],"filename":"1f449","category":"people","batch":"3"},{"aliases":["point_up_2"],"filename":"1f446","category":"people","batch":"3"},{"aliases":["point_down"],"filename":"1f447","category":"people","batch":"3"},{"aliases":["point_up"],"filename":"261d-fe0f","category":"people","batch":"3"},{"aliases":["hand","raised_hand"],"filename":"270b","category":"people","batch":"3"},{"aliases":["raised_back_of_hand"],"filename":"1f91a","category":"people","batch":"3"},{"aliases":["raised_hand_with_fingers_splayed"],"filename":"1f590","category":"people","batch":"3"},{"aliases":["vulcan_salute"],"filename":"1f596","category":"people","batch":"3"},{"aliases":["wave"],"filename":"1f44b","category":"people","batch":"3"},{"aliases":["call_me_hand"],"filename":"1f919","category":"people","batch":"3"},{"aliases":["muscle"],"filename":"1f4aa","category":"people","batch":"3"},{"aliases":["middle_finger","fu"],"filename":"1f595","category":"people","batch":"3"},{"aliases":["writing_hand"],"filename":"270d-fe0f","category":"people","batch":"3"},{"aliases":["selfie"],"filename":"1f933","category":"people","batch":"3"},{"aliases":["nail_care"],"filename":"1f485","category":"people","batch":"3"},{"aliases":["ring"],"filename":"1f48d","category":"people","batch":"3"},{"aliases":["lipstick"],"filename":"1f484","category":"people","batch":"3"},{"aliases":["kiss"],"filename":"1f48b","category":"people","batch":"3"},{"aliases":["lips"],"filename":"1f444","category":"people","batch":"3"},{"aliases":["tongue"],"filename":"1f445","category":"people","batch":"3"},{"aliases":["ear"],"filename":"1f442","category":"people","batch":"3"},{"aliases":["nose"],"filename":"1f443","category":"people","batch":"3"},{"aliases":["footprints"],"filename":"1f463","category":"people","batch":"3"},{"aliases":["eye"],"filename":"1f441","category":"people","batch":"4"},{"aliases":["eyes"],"filename":"1f440","category":"people","batch":"4"},{"aliases":["speaking_head"],"filename":"1f5e3","category":"people","batch":"4"},{"aliases":["bust_in_silhouette"],"filename":"1f464","category":"people","batch":"4"},{"aliases":["busts_in_silhouette"],"filename":"1f465","category":"people","batch":"4"},{"aliases":["baby"],"filename":"1f476","category":"people","batch":"4"},{"aliases":["boy"],"filename":"1f466","category":"people","batch":"4"},{"aliases":["girl"],"filename":"1f467","category":"people","batch":"4"},{"aliases":["man"],"filename":"1f468","category":"people","batch":"4"},{"aliases":["woman"],"filename":"1f469","category":"people","batch":"4"},{"aliases":["blonde_woman"],"filename":"1f471-200d-2640-fe0f","category":"people","batch":"4"},{"aliases":["blonde_man","person_with_blond_hair"],"filename":"1f471","category":"people","batch":"4"},{"aliases":["older_man"],"filename":"1f474","category":"people","batch":"4"},{"aliases":["older_woman"],"filename":"1f475","category":"people","batch":"4"},{"aliases":["man_with_gua_pi_mao"],"filename":"1f472","category":"people","batch":"4"},{"aliases":["woman_with_turban"],"filename":"1f473-200d-2640-fe0f","category":"people","batch":"4"},{"aliases":["man_with_turban"],"filename":"1f473","category":"people","batch":"4"},{"aliases":["policewoman"],"filename":"1f46e-200d-2640-fe0f","category":"people","batch":"4"},{"aliases":["policeman","cop"],"filename":"1f46e","category":"people","batch":"4"},{"aliases":["construction_worker_woman"],"filename":"1f477-200d-2640-fe0f","category":"people","batch":"4"},{"aliases":["construction_worker_man","construction_worker"],"filename":"1f477","category":"people","batch":"4"},{"aliases":["guardswoman"],"filename":"1f482-200d-2640-fe0f","category":"people","batch":"4"},{"aliases":["guardsman"],"filename":"1f482","category":"people","batch":"4"},{"aliases":["female_detective"],"filename":"1f575-fe0f-200d-2640-fe0f","category":"people","batch":"4"},{"aliases":["male_detective","detective"],"filename":"1f575-fe0f","category":"people","batch":"4"},{"aliases":["woman_health_worker"],"filename":"1f469-200d-2695-fe0f","category":"people","batch":"4"},{"aliases":["man_health_worker"],"filename":"1f468-200d-2695-fe0f","category":"people","batch":"4"},{"aliases":["woman_farmer"],"filename":"1f469-200d-1f33e","category":"people","batch":"4"},{"aliases":["man_farmer"],"filename":"1f468-200d-1f33e","category":"people","batch":"4"},{"aliases":["woman_cook"],"filename":"1f469-200d-1f373","category":"people","batch":"4"},{"aliases":["man_cook"],"filename":"1f468-200d-1f373","category":"people","batch":"4"},{"aliases":["woman_student"],"filename":"1f469-200d-1f393","category":"people","batch":"4"},{"aliases":["man_student"],"filename":"1f468-200d-1f393","category":"people","batch":"4"},{"aliases":["woman_singer"],"filename":"1f469-200d-1f3a4","category":"people","batch":"4"},{"aliases":["man_singer"],"filename":"1f468-200d-1f3a4","category":"people","batch":"4"},{"aliases":["woman_teacher"],"filename":"1f469-200d-1f3eb","category":"people","batch":"4"},{"aliases":["man_teacher"],"filename":"1f468-200d-1f3eb","category":"people","batch":"4"},{"aliases":["woman_factory_worker"],"filename":"1f469-200d-1f3ed","category":"people","batch":"4"},{"aliases":["man_factory_worker"],"filename":"1f468-200d-1f3ed","category":"people","batch":"4"},{"aliases":["woman_technologist"],"filename":"1f469-200d-1f4bb","category":"people","batch":"4"},{"aliases":["man_technologist"],"filename":"1f468-200d-1f4bb","category":"people","batch":"4"},{"aliases":["woman_office_worker"],"filename":"1f469-200d-1f4bc","category":"people","batch":"4"},{"aliases":["man_office_worker"],"filename":"1f468-200d-1f4bc","category":"people","batch":"4"},{"aliases":["woman_mechanic"],"filename":"1f469-200d-1f527","category":"people","batch":"4"},{"aliases":["man_mechanic"],"filename":"1f468-200d-1f527","category":"people","batch":"4"},{"aliases":["woman_scientist"],"filename":"1f469-200d-1f52c","category":"people","batch":"5"},{"aliases":["man_scientist"],"filename":"1f468-200d-1f52c","category":"people","batch":"5"},{"aliases":["woman_artist"],"filename":"1f469-200d-1f3a8","category":"people","batch":"5"},{"aliases":["man_artist"],"filename":"1f468-200d-1f3a8","category":"people","batch":"5"},{"aliases":["woman_firefighter"],"filename":"1f469-200d-1f692","category":"people","batch":"5"},{"aliases":["man_firefighter"],"filename":"1f468-200d-1f692","category":"people","batch":"5"},{"aliases":["woman_pilot"],"filename":"1f469-200d-2708-fe0f","category":"people","batch":"5"},{"aliases":["man_pilot"],"filename":"1f468-200d-2708-fe0f","category":"people","batch":"5"},{"aliases":["woman_astronaut"],"filename":"1f469-200d-1f680","category":"people","batch":"5"},{"aliases":["man_astronaut"],"filename":"1f468-200d-1f680","category":"people","batch":"5"},{"aliases":["woman_judge"],"filename":"1f469-200d-2696-fe0f","category":"people","batch":"5"},{"aliases":["man_judge"],"filename":"1f468-200d-2696-fe0f","category":"people","batch":"5"},{"aliases":["mrs_claus"],"filename":"1f936","category":"people","batch":"5"},{"aliases":["santa"],"filename":"1f385","category":"people","batch":"5"},{"aliases":["princess"],"filename":"1f478","category":"people","batch":"5"},{"aliases":["prince"],"filename":"1f934","category":"people","batch":"5"},{"aliases":["bride_with_veil"],"filename":"1f470","category":"people","batch":"5"},{"aliases":["man_in_tuxedo"],"filename":"1f935","category":"people","batch":"5"},{"aliases":["angel"],"filename":"1f47c","category":"people","batch":"5"},{"aliases":["pregnant_woman"],"filename":"1f930","category":"people","batch":"5"},{"aliases":["bowing_woman"],"filename":"1f647-200d-2640-fe0f","category":"people","batch":"5"},{"aliases":["bowing_man","bow"],"filename":"1f647","category":"people","batch":"5"},{"aliases":["tipping_hand_woman","information_desk_person"],"filename":"1f481","category":"people","batch":"5"},{"aliases":["tipping_hand_man"],"filename":"1f481-200d-2642-fe0f","category":"people","batch":"5"},{"aliases":["no_good_woman","no_good","ng_woman"],"filename":"1f645","category":"people","batch":"5"},{"aliases":["no_good_man","ng_man"],"filename":"1f645-200d-2642-fe0f","category":"people","batch":"5"},{"aliases":["ok_woman"],"filename":"1f646","category":"people","batch":"5"},{"aliases":["ok_man"],"filename":"1f646-200d-2642-fe0f","category":"people","batch":"5"},{"aliases":["raising_hand_woman","raising_hand"],"filename":"1f64b","category":"people","batch":"5"},{"aliases":["raising_hand_man"],"filename":"1f64b-200d-2642-fe0f","category":"people","batch":"5"},{"aliases":["woman_facepalming"],"filename":"1f926-200d-2640-fe0f","category":"people","batch":"5"},{"aliases":["man_facepalming"],"filename":"1f926-200d-2642-fe0f","category":"people","batch":"5"},{"aliases":["woman_shrugging"],"filename":"1f937-200d-2640-fe0f","category":"people","batch":"5"},{"aliases":["man_shrugging"],"filename":"1f937-200d-2642-fe0f","category":"people","batch":"5"},{"aliases":["pouting_woman","person_with_pouting_face"],"filename":"1f64e","category":"people","batch":"5"},{"aliases":["pouting_man"],"filename":"1f64e-200d-2642-fe0f","category":"people","batch":"5"},{"aliases":["frowning_woman","person_frowning"],"filename":"1f64d","category":"people","batch":"5"},{"aliases":["frowning_man"],"filename":"1f64d-200d-2642-fe0f","category":"people","batch":"5"},{"aliases":["haircut_woman","haircut"],"filename":"1f487","category":"people","batch":"5"},{"aliases":["haircut_man"],"filename":"1f487-200d-2642-fe0f","category":"people","batch":"5"},{"aliases":["massage_woman","massage"],"filename":"1f486","category":"people","batch":"5"},{"aliases":["massage_man"],"filename":"1f486-200d-2642-fe0f","category":"people","batch":"5"},{"aliases":["business_suit_levitating"],"filename":"1f574","category":"people","batch":"5"},{"aliases":["dancer"],"filename":"1f483","category":"people","batch":"5"},{"aliases":["man_dancing"],"filename":"1f57a","category":"people","batch":"5"},{"aliases":["dancing_women","dancers"],"filename":"1f46f","category":"people","batch":"6"},{"aliases":["dancing_men"],"filename":"1f46f-200d-2642-fe0f","category":"people","batch":"6"},{"aliases":["walking_woman"],"filename":"1f6b6-200d-2640-fe0f","category":"people","batch":"6"},{"aliases":["walking_man","walking"],"filename":"1f6b6","category":"people","batch":"6"},{"aliases":["running_woman"],"filename":"1f3c3-200d-2640-fe0f","category":"people","batch":"6"},{"aliases":["running_man","runner","running"],"filename":"1f3c3","category":"people","batch":"6"},{"aliases":["couple"],"filename":"1f46b","category":"people","batch":"6"},{"aliases":["two_women_holding_hands"],"filename":"1f46d","category":"people","batch":"6"},{"aliases":["two_men_holding_hands"],"filename":"1f46c","category":"people","batch":"6"},{"aliases":["couple_with_heart_woman_man","couple_with_heart"],"filename":"1f491","category":"people","batch":"6"},{"aliases":["couple_with_heart_woman_woman"],"filename":"1f469-200d-2764-fe0f-200d-1f469","category":"people","batch":"6"},{"aliases":["couple_with_heart_man_man"],"filename":"1f468-200d-2764-fe0f-200d-1f468","category":"people","batch":"6"},{"aliases":["couplekiss_man_woman"],"filename":"1f48f","category":"people","batch":"6"},{"aliases":["couplekiss_woman_woman"],"filename":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f469","category":"people","batch":"6"},{"aliases":["couplekiss_man_man"],"filename":"1f468-200d-2764-fe0f-200d-1f48b-200d-1f468","category":"people","batch":"6"},{"aliases":["family_man_woman_boy","family"],"filename":"1f46a","category":"people","batch":"6"},{"aliases":["family_man_woman_girl"],"filename":"1f468-200d-1f469-200d-1f467","category":"people","batch":"6"},{"aliases":["family_man_woman_girl_boy"],"filename":"1f468-200d-1f469-200d-1f467-200d-1f466","category":"people","batch":"6"},{"aliases":["family_man_woman_boy_boy"],"filename":"1f468-200d-1f469-200d-1f466-200d-1f466","category":"people","batch":"6"},{"aliases":["family_man_woman_girl_girl"],"filename":"1f468-200d-1f469-200d-1f467-200d-1f467","category":"people","batch":"6"},{"aliases":["family_woman_woman_boy"],"filename":"1f469-200d-1f469-200d-1f466","category":"people","batch":"6"},{"aliases":["family_woman_woman_girl"],"filename":"1f469-200d-1f469-200d-1f467","category":"people","batch":"6"},{"aliases":["family_woman_woman_girl_boy"],"filename":"1f469-200d-1f469-200d-1f467-200d-1f466","category":"people","batch":"6"},{"aliases":["family_woman_woman_boy_boy"],"filename":"1f469-200d-1f469-200d-1f466-200d-1f466","category":"people","batch":"6"},{"aliases":["family_woman_woman_girl_girl"],"filename":"1f469-200d-1f469-200d-1f467-200d-1f467","category":"people","batch":"6"},{"aliases":["family_man_man_boy"],"filename":"1f468-200d-1f468-200d-1f466","category":"people","batch":"6"},{"aliases":["family_man_man_girl"],"filename":"1f468-200d-1f468-200d-1f467","category":"people","batch":"6"},{"aliases":["family_man_man_girl_boy"],"filename":"1f468-200d-1f468-200d-1f467-200d-1f466","category":"people","batch":"6"},{"aliases":["family_man_man_boy_boy"],"filename":"1f468-200d-1f468-200d-1f466-200d-1f466","category":"people","batch":"6"},{"aliases":["family_man_man_girl_girl"],"filename":"1f468-200d-1f468-200d-1f467-200d-1f467","category":"people","batch":"6"},{"aliases":["family_woman_boy"],"filename":"1f469-200d-1f466","category":"people","batch":"6"},{"aliases":["family_woman_girl"],"filename":"1f469-200d-1f467","category":"people","batch":"6"},{"aliases":["family_woman_girl_boy"],"filename":"1f469-200d-1f467-200d-1f466","category":"people","batch":"6"},{"aliases":["family_woman_boy_boy"],"filename":"1f469-200d-1f466-200d-1f466","category":"people","batch":"6"},{"aliases":["family_woman_girl_girl"],"filename":"1f469-200d-1f467-200d-1f467","category":"people","batch":"6"},{"aliases":["family_man_boy"],"filename":"1f468-200d-1f466","category":"people","batch":"6"},{"aliases":["family_man_girl"],"filename":"1f468-200d-1f467","category":"people","batch":"6"},{"aliases":["family_man_girl_boy"],"filename":"1f468-200d-1f467-200d-1f466","category":"people","batch":"6"},{"aliases":["family_man_boy_boy"],"filename":"1f468-200d-1f466-200d-1f466","category":"people","batch":"6"},{"aliases":["family_man_girl_girl"],"filename":"1f468-200d-1f467-200d-1f467","category":"people","batch":"6"},{"aliases":["womans_clothes"],"filename":"1f45a","category":"people","batch":"6"},{"aliases":["shirt","tshirt"],"filename":"1f455","category":"people","batch":"6"},{"aliases":["jeans"],"filename":"1f456","category":"people","batch":"6"},{"aliases":["necktie"],"filename":"1f454","category":"people","batch":"6"},{"aliases":["dress"],"filename":"1f457","category":"people","batch":"6"},{"aliases":["bikini"],"filename":"1f459","category":"people","batch":"7"},{"aliases":["kimono"],"filename":"1f458","category":"people","batch":"7"},{"aliases":["high_heel"],"filename":"1f460","category":"people","batch":"7"},{"aliases":["sandal"],"filename":"1f461","category":"people","batch":"7"},{"aliases":["boot"],"filename":"1f462","category":"people","batch":"7"},{"aliases":["mans_shoe","shoe"],"filename":"1f45e","category":"people","batch":"7"},{"aliases":["athletic_shoe"],"filename":"1f45f","category":"people","batch":"7"},{"aliases":["womans_hat"],"filename":"1f452","category":"people","batch":"7"},{"aliases":["tophat"],"filename":"1f3a9","category":"people","batch":"7"},{"aliases":["mortar_board"],"filename":"1f393","category":"people","batch":"7"},{"aliases":["crown"],"filename":"1f451","category":"people","batch":"7"},{"aliases":["rescue_worker_helmet"],"filename":"26d1","category":"people","batch":"7"},{"aliases":["school_satchel"],"filename":"1f392","category":"people","batch":"7"},{"aliases":["pouch"],"filename":"1f45d","category":"people","batch":"7"},{"aliases":["purse"],"filename":"1f45b","category":"people","batch":"7"},{"aliases":["handbag"],"filename":"1f45c","category":"people","batch":"7"},{"aliases":["briefcase"],"filename":"1f4bc","category":"people","batch":"7"},{"aliases":["eyeglasses"],"filename":"1f453","category":"people","batch":"7"},{"aliases":["dark_sunglasses"],"filename":"1f576","category":"people","batch":"7"},{"aliases":["closed_umbrella"],"filename":"1f302","category":"people","batch":"7"},{"aliases":["open_umbrella"],"filename":"2602-fe0f","category":"people","batch":"7"},{"aliases":["dog"],"filename":"1f436","category":"nature","batch":"1"},{"aliases":["cat"],"filename":"1f431","category":"nature","batch":"1"},{"aliases":["mouse"],"filename":"1f42d","category":"nature","batch":"1"},{"aliases":["hamster"],"filename":"1f439","category":"nature","batch":"1"},{"aliases":["rabbit"],"filename":"1f430","category":"nature","batch":"1"},{"aliases":["fox_face"],"filename":"1f98a","category":"nature","batch":"1"},{"aliases":["bear"],"filename":"1f43b","category":"nature","batch":"1"},{"aliases":["panda_face"],"filename":"1f43c","category":"nature","batch":"1"},{"aliases":["koala"],"filename":"1f428","category":"nature","batch":"1"},{"aliases":["tiger"],"filename":"1f42f","category":"nature","batch":"1"},{"aliases":["lion"],"filename":"1f981","category":"nature","batch":"1"},{"aliases":["cow"],"filename":"1f42e","category":"nature","batch":"1"},{"aliases":["pig"],"filename":"1f437","category":"nature","batch":"1"},{"aliases":["pig_nose"],"filename":"1f43d","category":"nature","batch":"1"},{"aliases":["frog"],"filename":"1f438","category":"nature","batch":"1"},{"aliases":["monkey_face"],"filename":"1f435","category":"nature","batch":"1"},{"aliases":["see_no_evil"],"filename":"1f648","category":"nature","batch":"1"},{"aliases":["hear_no_evil"],"filename":"1f649","category":"nature","batch":"1"},{"aliases":["speak_no_evil"],"filename":"1f64a","category":"nature","batch":"1"},{"aliases":["monkey"],"filename":"1f412","category":"nature","batch":"1"},{"aliases":["chicken"],"filename":"1f414","category":"nature","batch":"1"},{"aliases":["penguin"],"filename":"1f427","category":"nature","batch":"1"},{"aliases":["bird"],"filename":"1f426","category":"nature","batch":"1"},{"aliases":["baby_chick"],"filename":"1f424","category":"nature","batch":"1"},{"aliases":["hatching_chick"],"filename":"1f423","category":"nature","batch":"1"},{"aliases":["hatched_chick"],"filename":"1f425","category":"nature","batch":"1"},{"aliases":["duck"],"filename":"1f986","category":"nature","batch":"1"},{"aliases":["eagle"],"filename":"1f985","category":"nature","batch":"1"},{"aliases":["owl"],"filename":"1f989","category":"nature","batch":"1"},{"aliases":["bat"],"filename":"1f987","category":"nature","batch":"1"},{"aliases":["wolf"],"filename":"1f43a","category":"nature","batch":"1"},{"aliases":["boar"],"filename":"1f417","category":"nature","batch":"1"},{"aliases":["horse"],"filename":"1f434","category":"nature","batch":"1"},{"aliases":["unicorn"],"filename":"1f984","category":"nature","batch":"1"},{"aliases":["bee","honeybee"],"filename":"1f41d","category":"nature","batch":"1"},{"aliases":["bug"],"filename":"1f41b","category":"nature","batch":"1"},{"aliases":["butterfly"],"filename":"1f98b","category":"nature","batch":"1"},{"aliases":["snail"],"filename":"1f40c","category":"nature","batch":"1"},{"aliases":["shell"],"filename":"1f41a","category":"nature","batch":"1"},{"aliases":["beetle"],"filename":"1f41e","category":"nature","batch":"1"},{"aliases":["ant"],"filename":"1f41c","category":"nature","batch":"1"},{"aliases":["spider"],"filename":"1f577","category":"nature","batch":"1"},{"aliases":["spider_web"],"filename":"1f578","category":"nature","batch":"1"},{"aliases":["turtle"],"filename":"1f422","category":"nature","batch":"1"},{"aliases":["snake"],"filename":"1f40d","category":"nature","batch":"1"},{"aliases":["lizard"],"filename":"1f98e","category":"nature","batch":"2"},{"aliases":["scorpion"],"filename":"1f982","category":"nature","batch":"2"},{"aliases":["crab"],"filename":"1f980","category":"nature","batch":"2"},{"aliases":["squid"],"filename":"1f991","category":"nature","batch":"2"},{"aliases":["octopus"],"filename":"1f419","category":"nature","batch":"2"},{"aliases":["shrimp"],"filename":"1f990","category":"nature","batch":"2"},{"aliases":["tropical_fish"],"filename":"1f420","category":"nature","batch":"2"},{"aliases":["fish"],"filename":"1f41f","category":"nature","batch":"2"},{"aliases":["blowfish"],"filename":"1f421","category":"nature","batch":"2"},{"aliases":["dolphin","flipper"],"filename":"1f42c","category":"nature","batch":"2"},{"aliases":["shark"],"filename":"1f988","category":"nature","batch":"2"},{"aliases":["whale"],"filename":"1f433","category":"nature","batch":"2"},{"aliases":["whale2"],"filename":"1f40b","category":"nature","batch":"2"},{"aliases":["crocodile"],"filename":"1f40a","category":"nature","batch":"2"},{"aliases":["leopard"],"filename":"1f406","category":"nature","batch":"2"},{"aliases":["tiger2"],"filename":"1f405","category":"nature","batch":"2"},{"aliases":["water_buffalo"],"filename":"1f403","category":"nature","batch":"2"},{"aliases":["ox"],"filename":"1f402","category":"nature","batch":"2"},{"aliases":["cow2"],"filename":"1f404","category":"nature","batch":"2"},{"aliases":["deer"],"filename":"1f98c","category":"nature","batch":"2"},{"aliases":["dromedary_camel"],"filename":"1f42a","category":"nature","batch":"2"},{"aliases":["camel"],"filename":"1f42b","category":"nature","batch":"2"},{"aliases":["elephant"],"filename":"1f418","category":"nature","batch":"2"},{"aliases":["rhinoceros"],"filename":"1f98f","category":"nature","batch":"2"},{"aliases":["gorilla"],"filename":"1f98d","category":"nature","batch":"2"},{"aliases":["racehorse"],"filename":"1f40e","category":"nature","batch":"2"},{"aliases":["pig2"],"filename":"1f416","category":"nature","batch":"2"},{"aliases":["goat"],"filename":"1f410","category":"nature","batch":"2"},{"aliases":["ram"],"filename":"1f40f","category":"nature","batch":"2"},{"aliases":["sheep"],"filename":"1f411","category":"nature","batch":"2"},{"aliases":["dog2"],"filename":"1f415","category":"nature","batch":"2"},{"aliases":["poodle"],"filename":"1f429","category":"nature","batch":"2"},{"aliases":["cat2"],"filename":"1f408","category":"nature","batch":"2"},{"aliases":["rooster"],"filename":"1f413","category":"nature","batch":"2"},{"aliases":["turkey"],"filename":"1f983","category":"nature","batch":"2"},{"aliases":["dove"],"filename":"1f54a","category":"nature","batch":"2"},{"aliases":["rabbit2"],"filename":"1f407","category":"nature","batch":"2"},{"aliases":["mouse2"],"filename":"1f401","category":"nature","batch":"2"},{"aliases":["rat"],"filename":"1f400","category":"nature","batch":"2"},{"aliases":["chipmunk"],"filename":"1f43f","category":"nature","batch":"2"},{"aliases":["feet","paw_prints"],"filename":"1f43e","category":"nature","batch":"2"},{"aliases":["dragon"],"filename":"1f409","category":"nature","batch":"2"},{"aliases":["dragon_face"],"filename":"1f432","category":"nature","batch":"2"},{"aliases":["cactus"],"filename":"1f335","category":"nature","batch":"2"},{"aliases":["christmas_tree"],"filename":"1f384","category":"nature","batch":"2"},{"aliases":["evergreen_tree"],"filename":"1f332","category":"nature","batch":"3"},{"aliases":["deciduous_tree"],"filename":"1f333","category":"nature","batch":"3"},{"aliases":["palm_tree"],"filename":"1f334","category":"nature","batch":"3"},{"aliases":["seedling"],"filename":"1f331","category":"nature","batch":"3"},{"aliases":["herb"],"filename":"1f33f","category":"nature","batch":"3"},{"aliases":["shamrock"],"filename":"2618-fe0f","category":"nature","batch":"3"},{"aliases":["four_leaf_clover"],"filename":"1f340","category":"nature","batch":"3"},{"aliases":["bamboo"],"filename":"1f38d","category":"nature","batch":"3"},{"aliases":["tanabata_tree"],"filename":"1f38b","category":"nature","batch":"3"},{"aliases":["leaves"],"filename":"1f343","category":"nature","batch":"3"},{"aliases":["fallen_leaf"],"filename":"1f342","category":"nature","batch":"3"},{"aliases":["maple_leaf"],"filename":"1f341","category":"nature","batch":"3"},{"aliases":["mushroom"],"filename":"1f344","category":"nature","batch":"3"},{"aliases":["ear_of_rice"],"filename":"1f33e","category":"nature","batch":"3"},{"aliases":["bouquet"],"filename":"1f490","category":"nature","batch":"3"},{"aliases":["tulip"],"filename":"1f337","category":"nature","batch":"3"},{"aliases":["rose"],"filename":"1f339","category":"nature","batch":"3"},{"aliases":["wilted_flower"],"filename":"1f940","category":"nature","batch":"3"},{"aliases":["sunflower"],"filename":"1f33b","category":"nature","batch":"3"},{"aliases":["blossom"],"filename":"1f33c","category":"nature","batch":"3"},{"aliases":["cherry_blossom"],"filename":"1f338","category":"nature","batch":"3"},{"aliases":["hibiscus"],"filename":"1f33a","category":"nature","batch":"3"},{"aliases":["earth_americas"],"filename":"1f30e","category":"nature","batch":"3"},{"aliases":["earth_africa"],"filename":"1f30d","category":"nature","batch":"3"},{"aliases":["earth_asia"],"filename":"1f30f","category":"nature","batch":"3"},{"aliases":["full_moon"],"filename":"1f315","category":"nature","batch":"3"},{"aliases":["waning_gibbous_moon"],"filename":"1f316","category":"nature","batch":"3"},{"aliases":["last_quarter_moon"],"filename":"1f317","category":"nature","batch":"3"},{"aliases":["waning_crescent_moon"],"filename":"1f318","category":"nature","batch":"3"},{"aliases":["new_moon"],"filename":"1f311","category":"nature","batch":"3"},{"aliases":["waxing_crescent_moon"],"filename":"1f312","category":"nature","batch":"3"},{"aliases":["first_quarter_moon"],"filename":"1f313","category":"nature","batch":"3"},{"aliases":["moon","waxing_gibbous_moon"],"filename":"1f314","category":"nature","batch":"3"},{"aliases":["new_moon_with_face"],"filename":"1f31a","category":"nature","batch":"3"},{"aliases":["full_moon_with_face"],"filename":"1f31d","category":"nature","batch":"3"},{"aliases":["sun_with_face"],"filename":"1f31e","category":"nature","batch":"3"},{"aliases":["first_quarter_moon_with_face"],"filename":"1f31b","category":"nature","batch":"3"},{"aliases":["last_quarter_moon_with_face"],"filename":"1f31c","category":"nature","batch":"3"},{"aliases":["crescent_moon"],"filename":"1f319","category":"nature","batch":"3"},{"aliases":["dizzy"],"filename":"1f4ab","category":"nature","batch":"3"},{"aliases":["star"],"filename":"2b50-fe0f","category":"nature","batch":"3"},{"aliases":["star2"],"filename":"1f31f","category":"nature","batch":"3"},{"aliases":["sparkles"],"filename":"2728","category":"nature","batch":"3"},{"aliases":["zap"],"filename":"26a1-fe0f","category":"nature","batch":"3"},{"aliases":["fire"],"filename":"1f525","category":"nature","batch":"3"},{"aliases":["boom","collision"],"filename":"1f4a5","category":"nature","batch":"4"},{"aliases":["comet"],"filename":"2604-fe0f","category":"nature","batch":"4"},{"aliases":["sunny"],"filename":"2600-fe0f","category":"nature","batch":"4"},{"aliases":["sun_behind_small_cloud"],"filename":"1f324","category":"nature","batch":"4"},{"aliases":["partly_sunny"],"filename":"26c5-fe0f","category":"nature","batch":"4"},{"aliases":["sun_behind_large_cloud"],"filename":"1f325","category":"nature","batch":"4"},{"aliases":["sun_behind_rain_cloud"],"filename":"1f326","category":"nature","batch":"4"},{"aliases":["rainbow"],"filename":"1f308","category":"nature","batch":"4"},{"aliases":["cloud"],"filename":"2601-fe0f","category":"nature","batch":"4"},{"aliases":["cloud_with_rain"],"filename":"1f327","category":"nature","batch":"4"},{"aliases":["cloud_with_lightning_and_rain"],"filename":"26c8","category":"nature","batch":"4"},{"aliases":["cloud_with_lightning"],"filename":"1f329","category":"nature","batch":"4"},{"aliases":["cloud_with_snow"],"filename":"1f328","category":"nature","batch":"4"},{"aliases":["snowman_with_snow"],"filename":"2603-fe0f","category":"nature","batch":"4"},{"aliases":["snowman"],"filename":"26c4-fe0f","category":"nature","batch":"4"},{"aliases":["snowflake"],"filename":"2744-fe0f","category":"nature","batch":"4"},{"aliases":["wind_face"],"filename":"1f32c","category":"nature","batch":"4"},{"aliases":["dash"],"filename":"1f4a8","category":"nature","batch":"4"},{"aliases":["tornado"],"filename":"1f32a","category":"nature","batch":"4"},{"aliases":["fog"],"filename":"1f32b","category":"nature","batch":"4"},{"aliases":["ocean"],"filename":"1f30a","category":"nature","batch":"4"},{"aliases":["droplet"],"filename":"1f4a7","category":"nature","batch":"4"},{"aliases":["sweat_drops"],"filename":"1f4a6","category":"nature","batch":"4"},{"aliases":["umbrella"],"filename":"2614-fe0f","category":"nature","batch":"4"},{"aliases":["green_apple"],"filename":"1f34f","category":"foods","batch":"1"},{"aliases":["apple"],"filename":"1f34e","category":"foods","batch":"1"},{"aliases":["pear"],"filename":"1f350","category":"foods","batch":"1"},{"aliases":["tangerine","orange","mandarin"],"filename":"1f34a","category":"foods","batch":"1"},{"aliases":["lemon"],"filename":"1f34b","category":"foods","batch":"1"},{"aliases":["banana"],"filename":"1f34c","category":"foods","batch":"1"},{"aliases":["watermelon"],"filename":"1f349","category":"foods","batch":"1"},{"aliases":["grapes"],"filename":"1f347","category":"foods","batch":"1"},{"aliases":["strawberry"],"filename":"1f353","category":"foods","batch":"1"},{"aliases":["melon"],"filename":"1f348","category":"foods","batch":"1"},{"aliases":["cherries"],"filename":"1f352","category":"foods","batch":"1"},{"aliases":["peach"],"filename":"1f351","category":"foods","batch":"1"},{"aliases":["pineapple"],"filename":"1f34d","category":"foods","batch":"1"},{"aliases":["kiwi_fruit"],"filename":"1f95d","category":"foods","batch":"1"},{"aliases":["avocado"],"filename":"1f951","category":"foods","batch":"1"},{"aliases":["tomato"],"filename":"1f345","category":"foods","batch":"1"},{"aliases":["eggplant"],"filename":"1f346","category":"foods","batch":"1"},{"aliases":["cucumber"],"filename":"1f952","category":"foods","batch":"1"},{"aliases":["carrot"],"filename":"1f955","category":"foods","batch":"1"},{"aliases":["corn"],"filename":"1f33d","category":"foods","batch":"1"},{"aliases":["hot_pepper"],"filename":"1f336","category":"foods","batch":"1"},{"aliases":["potato"],"filename":"1f954","category":"foods","batch":"1"},{"aliases":["sweet_potato"],"filename":"1f360","category":"foods","batch":"1"},{"aliases":["chestnut"],"filename":"1f330","category":"foods","batch":"1"},{"aliases":["peanuts"],"filename":"1f95c","category":"foods","batch":"1"},{"aliases":["honey_pot"],"filename":"1f36f","category":"foods","batch":"1"},{"aliases":["croissant"],"filename":"1f950","category":"foods","batch":"1"},{"aliases":["bread"],"filename":"1f35e","category":"foods","batch":"1"},{"aliases":["baguette_bread"],"filename":"1f956","category":"foods","batch":"1"},{"aliases":["cheese"],"filename":"1f9c0","category":"foods","batch":"1"},{"aliases":["egg"],"filename":"1f95a","category":"foods","batch":"1"},{"aliases":["fried_egg"],"filename":"1f373","category":"foods","batch":"1"},{"aliases":["bacon"],"filename":"1f953","category":"foods","batch":"1"},{"aliases":["pancakes"],"filename":"1f95e","category":"foods","batch":"1"},{"aliases":["fried_shrimp"],"filename":"1f364","category":"foods","batch":"1"},{"aliases":["poultry_leg"],"filename":"1f357","category":"foods","batch":"1"},{"aliases":["meat_on_bone"],"filename":"1f356","category":"foods","batch":"1"},{"aliases":["pizza"],"filename":"1f355","category":"foods","batch":"1"},{"aliases":["hotdog"],"filename":"1f32d","category":"foods","batch":"1"},{"aliases":["hamburger"],"filename":"1f354","category":"foods","batch":"1"},{"aliases":["fries"],"filename":"1f35f","category":"foods","batch":"1"},{"aliases":["stuffed_flatbread"],"filename":"1f959","category":"foods","batch":"1"},{"aliases":["taco"],"filename":"1f32e","category":"foods","batch":"1"},{"aliases":["burrito"],"filename":"1f32f","category":"foods","batch":"1"},{"aliases":["green_salad"],"filename":"1f957","category":"foods","batch":"1"},{"aliases":["shallow_pan_of_food"],"filename":"1f958","category":"foods","batch":"2"},{"aliases":["spaghetti"],"filename":"1f35d","category":"foods","batch":"2"},{"aliases":["ramen"],"filename":"1f35c","category":"foods","batch":"2"},{"aliases":["stew"],"filename":"1f372","category":"foods","batch":"2"},{"aliases":["fish_cake"],"filename":"1f365","category":"foods","batch":"2"},{"aliases":["sushi"],"filename":"1f363","category":"foods","batch":"2"},{"aliases":["bento"],"filename":"1f371","category":"foods","batch":"2"},{"aliases":["curry"],"filename":"1f35b","category":"foods","batch":"2"},{"aliases":["rice"],"filename":"1f35a","category":"foods","batch":"2"},{"aliases":["rice_ball"],"filename":"1f359","category":"foods","batch":"2"},{"aliases":["rice_cracker"],"filename":"1f358","category":"foods","batch":"2"},{"aliases":["oden"],"filename":"1f362","category":"foods","batch":"2"},{"aliases":["dango"],"filename":"1f361","category":"foods","batch":"2"},{"aliases":["shaved_ice"],"filename":"1f367","category":"foods","batch":"2"},{"aliases":["ice_cream"],"filename":"1f368","category":"foods","batch":"2"},{"aliases":["icecream"],"filename":"1f366","category":"foods","batch":"2"},{"aliases":["cake"],"filename":"1f370","category":"foods","batch":"2"},{"aliases":["birthday"],"filename":"1f382","category":"foods","batch":"2"},{"aliases":["custard"],"filename":"1f36e","category":"foods","batch":"2"},{"aliases":["lollipop"],"filename":"1f36d","category":"foods","batch":"2"},{"aliases":["candy"],"filename":"1f36c","category":"foods","batch":"2"},{"aliases":["chocolate_bar"],"filename":"1f36b","category":"foods","batch":"2"},{"aliases":["popcorn"],"filename":"1f37f","category":"foods","batch":"2"},{"aliases":["doughnut"],"filename":"1f369","category":"foods","batch":"2"},{"aliases":["cookie"],"filename":"1f36a","category":"foods","batch":"2"},{"aliases":["milk_glass"],"filename":"1f95b","category":"foods","batch":"2"},{"aliases":["baby_bottle"],"filename":"1f37c","category":"foods","batch":"2"},{"aliases":["coffee"],"filename":"2615-fe0f","category":"foods","batch":"2"},{"aliases":["tea"],"filename":"1f375","category":"foods","batch":"2"},{"aliases":["sake"],"filename":"1f376","category":"foods","batch":"2"},{"aliases":["beer"],"filename":"1f37a","category":"foods","batch":"2"},{"aliases":["beers"],"filename":"1f37b","category":"foods","batch":"2"},{"aliases":["clinking_glasses"],"filename":"1f942","category":"foods","batch":"2"},{"aliases":["wine_glass"],"filename":"1f377","category":"foods","batch":"2"},{"aliases":["tumbler_glass"],"filename":"1f943","category":"foods","batch":"2"},{"aliases":["cocktail"],"filename":"1f378","category":"foods","batch":"2"},{"aliases":["tropical_drink"],"filename":"1f379","category":"foods","batch":"2"},{"aliases":["champagne"],"filename":"1f37e","category":"foods","batch":"2"},{"aliases":["spoon"],"filename":"1f944","category":"foods","batch":"2"},{"aliases":["fork_and_knife"],"filename":"1f374","category":"foods","batch":"2"},{"aliases":["plate_with_cutlery"],"filename":"1f37d","category":"foods","batch":"2"},{"aliases":["soccer"],"filename":"26bd-fe0f","category":"activity","batch":"1"},{"aliases":["basketball"],"filename":"1f3c0","category":"activity","batch":"1"},{"aliases":["football"],"filename":"1f3c8","category":"activity","batch":"1"},{"aliases":["baseball"],"filename":"26be-fe0f","category":"activity","batch":"1"},{"aliases":["tennis"],"filename":"1f3be","category":"activity","batch":"1"},{"aliases":["volleyball"],"filename":"1f3d0","category":"activity","batch":"1"},{"aliases":["rugby_football"],"filename":"1f3c9","category":"activity","batch":"1"},{"aliases":["8ball"],"filename":"1f3b1","category":"activity","batch":"1"},{"aliases":["ping_pong"],"filename":"1f3d3","category":"activity","batch":"1"},{"aliases":["badminton"],"filename":"1f3f8","category":"activity","batch":"1"},{"aliases":["goal_net"],"filename":"1f945","category":"activity","batch":"1"},{"aliases":["ice_hockey"],"filename":"1f3d2","category":"activity","batch":"1"},{"aliases":["field_hockey"],"filename":"1f3d1","category":"activity","batch":"1"},{"aliases":["cricket"],"filename":"1f3cf","category":"activity","batch":"1"},{"aliases":["golf"],"filename":"26f3-fe0f","category":"activity","batch":"1"},{"aliases":["bow_and_arrow"],"filename":"1f3f9","category":"activity","batch":"1"},{"aliases":["fishing_pole_and_fish"],"filename":"1f3a3","category":"activity","batch":"1"},{"aliases":["boxing_glove"],"filename":"1f94a","category":"activity","batch":"1"},{"aliases":["martial_arts_uniform"],"filename":"1f94b","category":"activity","batch":"1"},{"aliases":["ice_skate"],"filename":"26f8","category":"activity","batch":"1"},{"aliases":["ski"],"filename":"1f3bf","category":"activity","batch":"1"},{"aliases":["skier"],"filename":"26f7","category":"activity","batch":"1"},{"aliases":["snowboarder"],"filename":"1f3c2","category":"activity","batch":"1"},{"aliases":["weight_lifting_woman"],"filename":"1f3cb-fe0f-200d-2640-fe0f","category":"activity","batch":"1"},{"aliases":["weight_lifting_man"],"filename":"1f3cb-fe0f","category":"activity","batch":"1"},{"aliases":["person_fencing"],"filename":"1f93a","category":"activity","batch":"1"},{"aliases":["women_wrestling"],"filename":"1f93c-200d-2640-fe0f","category":"activity","batch":"1"},{"aliases":["men_wrestling"],"filename":"1f93c-200d-2642-fe0f","category":"activity","batch":"1"},{"aliases":["woman_cartwheeling"],"filename":"1f938-200d-2640-fe0f","category":"activity","batch":"1"},{"aliases":["man_cartwheeling"],"filename":"1f938-200d-2642-fe0f","category":"activity","batch":"1"},{"aliases":["basketball_woman"],"filename":"26f9-fe0f-200d-2640-fe0f","category":"activity","batch":"1"},{"aliases":["basketball_man"],"filename":"26f9-fe0f","category":"activity","batch":"1"},{"aliases":["woman_playing_handball"],"filename":"1f93e-200d-2640-fe0f","category":"activity","batch":"1"},{"aliases":["man_playing_handball"],"filename":"1f93e-200d-2642-fe0f","category":"activity","batch":"1"},{"aliases":["golfing_woman"],"filename":"1f3cc-fe0f-200d-2640-fe0f","category":"activity","batch":"1"},{"aliases":["golfing_man"],"filename":"1f3cc-fe0f","category":"activity","batch":"1"},{"aliases":["surfing_woman"],"filename":"1f3c4-200d-2640-fe0f","category":"activity","batch":"1"},{"aliases":["surfing_man","surfer"],"filename":"1f3c4","category":"activity","batch":"1"},{"aliases":["swimming_woman"],"filename":"1f3ca-200d-2640-fe0f","category":"activity","batch":"1"},{"aliases":["swimming_man","swimmer"],"filename":"1f3ca","category":"activity","batch":"1"},{"aliases":["woman_playing_water_polo"],"filename":"1f93d-200d-2640-fe0f","category":"activity","batch":"1"},{"aliases":["man_playing_water_polo"],"filename":"1f93d-200d-2642-fe0f","category":"activity","batch":"1"},{"aliases":["rowing_woman"],"filename":"1f6a3-200d-2640-fe0f","category":"activity","batch":"1"},{"aliases":["rowing_man","rowboat"],"filename":"1f6a3","category":"activity","batch":"1"},{"aliases":["horse_racing"],"filename":"1f3c7","category":"activity","batch":"1"},{"aliases":["biking_woman"],"filename":"1f6b4-200d-2640-fe0f","category":"activity","batch":"2"},{"aliases":["biking_man","bicyclist"],"filename":"1f6b4","category":"activity","batch":"2"},{"aliases":["mountain_biking_woman"],"filename":"1f6b5-200d-2640-fe0f","category":"activity","batch":"2"},{"aliases":["mountain_biking_man","mountain_bicyclist"],"filename":"1f6b5","category":"activity","batch":"2"},{"aliases":["running_shirt_with_sash"],"filename":"1f3bd","category":"activity","batch":"2"},{"aliases":["medal_sports"],"filename":"1f3c5","category":"activity","batch":"2"},{"aliases":["medal_military"],"filename":"1f396","category":"activity","batch":"2"},{"aliases":["1st_place_medal"],"filename":"1f947","category":"activity","batch":"2"},{"aliases":["2nd_place_medal"],"filename":"1f948","category":"activity","batch":"2"},{"aliases":["3rd_place_medal"],"filename":"1f949","category":"activity","batch":"2"},{"aliases":["trophy"],"filename":"1f3c6","category":"activity","batch":"2"},{"aliases":["rosette"],"filename":"1f3f5","category":"activity","batch":"2"},{"aliases":["reminder_ribbon"],"filename":"1f397","category":"activity","batch":"2"},{"aliases":["ticket"],"filename":"1f3ab","category":"activity","batch":"2"},{"aliases":["tickets"],"filename":"1f39f","category":"activity","batch":"2"},{"aliases":["circus_tent"],"filename":"1f3aa","category":"activity","batch":"2"},{"aliases":["woman_juggling"],"filename":"1f939-200d-2640-fe0f","category":"activity","batch":"2"},{"aliases":["man_juggling"],"filename":"1f939-200d-2642-fe0f","category":"activity","batch":"2"},{"aliases":["performing_arts"],"filename":"1f3ad","category":"activity","batch":"2"},{"aliases":["art"],"filename":"1f3a8","category":"activity","batch":"2"},{"aliases":["clapper"],"filename":"1f3ac","category":"activity","batch":"2"},{"aliases":["microphone"],"filename":"1f3a4","category":"activity","batch":"2"},{"aliases":["headphones"],"filename":"1f3a7","category":"activity","batch":"2"},{"aliases":["musical_score"],"filename":"1f3bc","category":"activity","batch":"2"},{"aliases":["musical_keyboard"],"filename":"1f3b9","category":"activity","batch":"2"},{"aliases":["drum"],"filename":"1f941","category":"activity","batch":"2"},{"aliases":["saxophone"],"filename":"1f3b7","category":"activity","batch":"2"},{"aliases":["trumpet"],"filename":"1f3ba","category":"activity","batch":"2"},{"aliases":["guitar"],"filename":"1f3b8","category":"activity","batch":"2"},{"aliases":["violin"],"filename":"1f3bb","category":"activity","batch":"2"},{"aliases":["game_die"],"filename":"1f3b2","category":"activity","batch":"2"},{"aliases":["dart"],"filename":"1f3af","category":"activity","batch":"2"},{"aliases":["bowling"],"filename":"1f3b3","category":"activity","batch":"2"},{"aliases":["video_game"],"filename":"1f3ae","category":"activity","batch":"2"},{"aliases":["slot_machine"],"filename":"1f3b0","category":"activity","batch":"2"},{"aliases":["car","red_car"],"filename":"1f697","category":"places","batch":"1"},{"aliases":["taxi"],"filename":"1f695","category":"places","batch":"1"},{"aliases":["blue_car"],"filename":"1f699","category":"places","batch":"1"},{"aliases":["bus"],"filename":"1f68c","category":"places","batch":"1"},{"aliases":["trolleybus"],"filename":"1f68e","category":"places","batch":"1"},{"aliases":["racing_car"],"filename":"1f3ce","category":"places","batch":"1"},{"aliases":["police_car"],"filename":"1f693","category":"places","batch":"1"},{"aliases":["ambulance"],"filename":"1f691","category":"places","batch":"1"},{"aliases":["fire_engine"],"filename":"1f692","category":"places","batch":"1"},{"aliases":["minibus"],"filename":"1f690","category":"places","batch":"1"},{"aliases":["truck"],"filename":"1f69a","category":"places","batch":"1"},{"aliases":["articulated_lorry"],"filename":"1f69b","category":"places","batch":"1"},{"aliases":["tractor"],"filename":"1f69c","category":"places","batch":"1"},{"aliases":["kick_scooter"],"filename":"1f6f4","category":"places","batch":"1"},{"aliases":["bike"],"filename":"1f6b2","category":"places","batch":"1"},{"aliases":["motor_scooter"],"filename":"1f6f5","category":"places","batch":"1"},{"aliases":["motorcycle"],"filename":"1f3cd","category":"places","batch":"1"},{"aliases":["rotating_light"],"filename":"1f6a8","category":"places","batch":"1"},{"aliases":["oncoming_police_car"],"filename":"1f694","category":"places","batch":"1"},{"aliases":["oncoming_bus"],"filename":"1f68d","category":"places","batch":"1"},{"aliases":["oncoming_automobile"],"filename":"1f698","category":"places","batch":"1"},{"aliases":["oncoming_taxi"],"filename":"1f696","category":"places","batch":"1"},{"aliases":["aerial_tramway"],"filename":"1f6a1","category":"places","batch":"1"},{"aliases":["mountain_cableway"],"filename":"1f6a0","category":"places","batch":"1"},{"aliases":["suspension_railway"],"filename":"1f69f","category":"places","batch":"1"},{"aliases":["railway_car"],"filename":"1f683","category":"places","batch":"1"},{"aliases":["train"],"filename":"1f68b","category":"places","batch":"1"},{"aliases":["mountain_railway"],"filename":"1f69e","category":"places","batch":"1"},{"aliases":["monorail"],"filename":"1f69d","category":"places","batch":"1"},{"aliases":["bullettrain_side"],"filename":"1f684","category":"places","batch":"1"},{"aliases":["bullettrain_front"],"filename":"1f685","category":"places","batch":"1"},{"aliases":["light_rail"],"filename":"1f688","category":"places","batch":"1"},{"aliases":["steam_locomotive"],"filename":"1f682","category":"places","batch":"1"},{"aliases":["train2"],"filename":"1f686","category":"places","batch":"1"},{"aliases":["metro"],"filename":"1f687","category":"places","batch":"1"},{"aliases":["tram"],"filename":"1f68a","category":"places","batch":"1"},{"aliases":["station"],"filename":"1f689","category":"places","batch":"1"},{"aliases":["helicopter"],"filename":"1f681","category":"places","batch":"1"},{"aliases":["small_airplane"],"filename":"1f6e9","category":"places","batch":"1"},{"aliases":["airplane"],"filename":"2708-fe0f","category":"places","batch":"1"},{"aliases":["flight_departure"],"filename":"1f6eb","category":"places","batch":"1"},{"aliases":["flight_arrival"],"filename":"1f6ec","category":"places","batch":"1"},{"aliases":["rocket"],"filename":"1f680","category":"places","batch":"1"},{"aliases":["artificial_satellite"],"filename":"1f6f0","category":"places","batch":"1"},{"aliases":["seat"],"filename":"1f4ba","category":"places","batch":"1"},{"aliases":["canoe"],"filename":"1f6f6","category":"places","batch":"2"},{"aliases":["boat","sailboat"],"filename":"26f5-fe0f","category":"places","batch":"2"},{"aliases":["motor_boat"],"filename":"1f6e5","category":"places","batch":"2"},{"aliases":["speedboat"],"filename":"1f6a4","category":"places","batch":"2"},{"aliases":["passenger_ship"],"filename":"1f6f3","category":"places","batch":"2"},{"aliases":["ferry"],"filename":"26f4","category":"places","batch":"2"},{"aliases":["ship"],"filename":"1f6a2","category":"places","batch":"2"},{"aliases":["anchor"],"filename":"2693-fe0f","category":"places","batch":"2"},{"aliases":["construction"],"filename":"1f6a7","category":"places","batch":"2"},{"aliases":["fuelpump"],"filename":"26fd-fe0f","category":"places","batch":"2"},{"aliases":["busstop"],"filename":"1f68f","category":"places","batch":"2"},{"aliases":["vertical_traffic_light"],"filename":"1f6a6","category":"places","batch":"2"},{"aliases":["traffic_light"],"filename":"1f6a5","category":"places","batch":"2"},{"aliases":["world_map"],"filename":"1f5fa","category":"places","batch":"2"},{"aliases":["moyai"],"filename":"1f5ff","category":"places","batch":"2"},{"aliases":["statue_of_liberty"],"filename":"1f5fd","category":"places","batch":"2"},{"aliases":["fountain"],"filename":"26f2-fe0f","category":"places","batch":"2"},{"aliases":["tokyo_tower"],"filename":"1f5fc","category":"places","batch":"2"},{"aliases":["european_castle"],"filename":"1f3f0","category":"places","batch":"2"},{"aliases":["japanese_castle"],"filename":"1f3ef","category":"places","batch":"2"},{"aliases":["stadium"],"filename":"1f3df","category":"places","batch":"2"},{"aliases":["ferris_wheel"],"filename":"1f3a1","category":"places","batch":"2"},{"aliases":["roller_coaster"],"filename":"1f3a2","category":"places","batch":"2"},{"aliases":["carousel_horse"],"filename":"1f3a0","category":"places","batch":"2"},{"aliases":["parasol_on_ground"],"filename":"26f1","category":"places","batch":"2"},{"aliases":["beach_umbrella"],"filename":"1f3d6","category":"places","batch":"2"},{"aliases":["desert_island"],"filename":"1f3dd","category":"places","batch":"2"},{"aliases":["mountain"],"filename":"26f0","category":"places","batch":"2"},{"aliases":["mountain_snow"],"filename":"1f3d4","category":"places","batch":"2"},{"aliases":["mount_fuji"],"filename":"1f5fb","category":"places","batch":"2"},{"aliases":["volcano"],"filename":"1f30b","category":"places","batch":"2"},{"aliases":["desert"],"filename":"1f3dc","category":"places","batch":"2"},{"aliases":["camping"],"filename":"1f3d5","category":"places","batch":"2"},{"aliases":["tent"],"filename":"26fa-fe0f","category":"places","batch":"2"},{"aliases":["railway_track"],"filename":"1f6e4","category":"places","batch":"2"},{"aliases":["motorway"],"filename":"1f6e3","category":"places","batch":"2"},{"aliases":["building_construction"],"filename":"1f3d7","category":"places","batch":"2"},{"aliases":["factory"],"filename":"1f3ed","category":"places","batch":"2"},{"aliases":["house"],"filename":"1f3e0","category":"places","batch":"2"},{"aliases":["house_with_garden"],"filename":"1f3e1","category":"places","batch":"2"},{"aliases":["houses"],"filename":"1f3d8","category":"places","batch":"2"},{"aliases":["derelict_house"],"filename":"1f3da","category":"places","batch":"2"},{"aliases":["office"],"filename":"1f3e2","category":"places","batch":"2"},{"aliases":["department_store"],"filename":"1f3ec","category":"places","batch":"2"},{"aliases":["post_office"],"filename":"1f3e3","category":"places","batch":"2"},{"aliases":["european_post_office"],"filename":"1f3e4","category":"places","batch":"3"},{"aliases":["hospital"],"filename":"1f3e5","category":"places","batch":"3"},{"aliases":["bank"],"filename":"1f3e6","category":"places","batch":"3"},{"aliases":["hotel"],"filename":"1f3e8","category":"places","batch":"3"},{"aliases":["convenience_store"],"filename":"1f3ea","category":"places","batch":"3"},{"aliases":["school"],"filename":"1f3eb","category":"places","batch":"3"},{"aliases":["love_hotel"],"filename":"1f3e9","category":"places","batch":"3"},{"aliases":["wedding"],"filename":"1f492","category":"places","batch":"3"},{"aliases":["classical_building"],"filename":"1f3db","category":"places","batch":"3"},{"aliases":["church"],"filename":"26ea-fe0f","category":"places","batch":"3"},{"aliases":["mosque"],"filename":"1f54c","category":"places","batch":"3"},{"aliases":["synagogue"],"filename":"1f54d","category":"places","batch":"3"},{"aliases":["kaaba"],"filename":"1f54b","category":"places","batch":"3"},{"aliases":["shinto_shrine"],"filename":"26e9","category":"places","batch":"3"},{"aliases":["japan"],"filename":"1f5fe","category":"places","batch":"3"},{"aliases":["rice_scene"],"filename":"1f391","category":"places","batch":"3"},{"aliases":["national_park"],"filename":"1f3de","category":"places","batch":"3"},{"aliases":["sunrise"],"filename":"1f305","category":"places","batch":"3"},{"aliases":["sunrise_over_mountains"],"filename":"1f304","category":"places","batch":"3"},{"aliases":["stars"],"filename":"1f320","category":"places","batch":"3"},{"aliases":["sparkler"],"filename":"1f387","category":"places","batch":"3"},{"aliases":["fireworks"],"filename":"1f386","category":"places","batch":"3"},{"aliases":["city_sunrise"],"filename":"1f307","category":"places","batch":"3"},{"aliases":["city_sunset"],"filename":"1f306","category":"places","batch":"3"},{"aliases":["cityscape"],"filename":"1f3d9","category":"places","batch":"3"},{"aliases":["night_with_stars"],"filename":"1f303","category":"places","batch":"3"},{"aliases":["milky_way"],"filename":"1f30c","category":"places","batch":"3"},{"aliases":["bridge_at_night"],"filename":"1f309","category":"places","batch":"3"},{"aliases":["foggy"],"filename":"1f301","category":"places","batch":"3"},{"aliases":["watch"],"filename":"231a-fe0f","category":"objects","batch":"1"},{"aliases":["iphone"],"filename":"1f4f1","category":"objects","batch":"1"},{"aliases":["calling"],"filename":"1f4f2","category":"objects","batch":"1"},{"aliases":["computer"],"filename":"1f4bb","category":"objects","batch":"1"},{"aliases":["keyboard"],"filename":"2328-fe0f","category":"objects","batch":"1"},{"aliases":["desktop_computer"],"filename":"1f5a5","category":"objects","batch":"1"},{"aliases":["printer"],"filename":"1f5a8","category":"objects","batch":"1"},{"aliases":["computer_mouse"],"filename":"1f5b1","category":"objects","batch":"1"},{"aliases":["trackball"],"filename":"1f5b2","category":"objects","batch":"1"},{"aliases":["joystick"],"filename":"1f579","category":"objects","batch":"1"},{"aliases":["clamp"],"filename":"1f5dc","category":"objects","batch":"1"},{"aliases":["minidisc"],"filename":"1f4bd","category":"objects","batch":"1"},{"aliases":["floppy_disk"],"filename":"1f4be","category":"objects","batch":"1"},{"aliases":["cd"],"filename":"1f4bf","category":"objects","batch":"1"},{"aliases":["dvd"],"filename":"1f4c0","category":"objects","batch":"1"},{"aliases":["vhs"],"filename":"1f4fc","category":"objects","batch":"1"},{"aliases":["camera"],"filename":"1f4f7","category":"objects","batch":"1"},{"aliases":["camera_flash"],"filename":"1f4f8","category":"objects","batch":"1"},{"aliases":["video_camera"],"filename":"1f4f9","category":"objects","batch":"1"},{"aliases":["movie_camera"],"filename":"1f3a5","category":"objects","batch":"1"},{"aliases":["film_projector"],"filename":"1f4fd","category":"objects","batch":"1"},{"aliases":["film_strip"],"filename":"1f39e","category":"objects","batch":"1"},{"aliases":["telephone_receiver"],"filename":"1f4de","category":"objects","batch":"1"},{"aliases":["phone","telephone"],"filename":"260e-fe0f","category":"objects","batch":"1"},{"aliases":["pager"],"filename":"1f4df","category":"objects","batch":"1"},{"aliases":["fax"],"filename":"1f4e0","category":"objects","batch":"1"},{"aliases":["tv"],"filename":"1f4fa","category":"objects","batch":"1"},{"aliases":["radio"],"filename":"1f4fb","category":"objects","batch":"1"},{"aliases":["studio_microphone"],"filename":"1f399","category":"objects","batch":"1"},{"aliases":["level_slider"],"filename":"1f39a","category":"objects","batch":"1"},{"aliases":["control_knobs"],"filename":"1f39b","category":"objects","batch":"1"},{"aliases":["stopwatch"],"filename":"23f1","category":"objects","batch":"1"},{"aliases":["timer_clock"],"filename":"23f2","category":"objects","batch":"1"},{"aliases":["alarm_clock"],"filename":"23f0","category":"objects","batch":"1"},{"aliases":["mantelpiece_clock"],"filename":"1f570","category":"objects","batch":"1"},{"aliases":["hourglass"],"filename":"231b-fe0f","category":"objects","batch":"1"},{"aliases":["hourglass_flowing_sand"],"filename":"23f3","category":"objects","batch":"1"},{"aliases":["satellite"],"filename":"1f4e1","category":"objects","batch":"1"},{"aliases":["battery"],"filename":"1f50b","category":"objects","batch":"1"},{"aliases":["electric_plug"],"filename":"1f50c","category":"objects","batch":"1"},{"aliases":["bulb"],"filename":"1f4a1","category":"objects","batch":"1"},{"aliases":["flashlight"],"filename":"1f526","category":"objects","batch":"1"},{"aliases":["candle"],"filename":"1f56f","category":"objects","batch":"1"},{"aliases":["wastebasket"],"filename":"1f5d1","category":"objects","batch":"1"},{"aliases":["oil_drum"],"filename":"1f6e2","category":"objects","batch":"1"},{"aliases":["money_with_wings"],"filename":"1f4b8","category":"objects","batch":"2"},{"aliases":["dollar"],"filename":"1f4b5","category":"objects","batch":"2"},{"aliases":["yen"],"filename":"1f4b4","category":"objects","batch":"2"},{"aliases":["euro"],"filename":"1f4b6","category":"objects","batch":"2"},{"aliases":["pound"],"filename":"1f4b7","category":"objects","batch":"2"},{"aliases":["moneybag"],"filename":"1f4b0","category":"objects","batch":"2"},{"aliases":["credit_card"],"filename":"1f4b3","category":"objects","batch":"2"},{"aliases":["gem"],"filename":"1f48e","category":"objects","batch":"2"},{"aliases":["balance_scale"],"filename":"2696-fe0f","category":"objects","batch":"2"},{"aliases":["wrench"],"filename":"1f527","category":"objects","batch":"2"},{"aliases":["hammer"],"filename":"1f528","category":"objects","batch":"2"},{"aliases":["hammer_and_pick"],"filename":"2692","category":"objects","batch":"2"},{"aliases":["hammer_and_wrench"],"filename":"1f6e0","category":"objects","batch":"2"},{"aliases":["pick"],"filename":"26cf","category":"objects","batch":"2"},{"aliases":["nut_and_bolt"],"filename":"1f529","category":"objects","batch":"2"},{"aliases":["gear"],"filename":"2699-fe0f","category":"objects","batch":"2"},{"aliases":["chains"],"filename":"26d3","category":"objects","batch":"2"},{"aliases":["gun"],"filename":"1f52b","category":"objects","batch":"2"},{"aliases":["bomb"],"filename":"1f4a3","category":"objects","batch":"2"},{"aliases":["hocho","knife"],"filename":"1f52a","category":"objects","batch":"2"},{"aliases":["dagger"],"filename":"1f5e1","category":"objects","batch":"2"},{"aliases":["crossed_swords"],"filename":"2694-fe0f","category":"objects","batch":"2"},{"aliases":["shield"],"filename":"1f6e1","category":"objects","batch":"2"},{"aliases":["smoking"],"filename":"1f6ac","category":"objects","batch":"2"},{"aliases":["coffin"],"filename":"26b0-fe0f","category":"objects","batch":"2"},{"aliases":["funeral_urn"],"filename":"26b1-fe0f","category":"objects","batch":"2"},{"aliases":["amphora"],"filename":"1f3fa","category":"objects","batch":"2"},{"aliases":["crystal_ball"],"filename":"1f52e","category":"objects","batch":"2"},{"aliases":["prayer_beads"],"filename":"1f4ff","category":"objects","batch":"2"},{"aliases":["barber"],"filename":"1f488","category":"objects","batch":"2"},{"aliases":["alembic"],"filename":"2697-fe0f","category":"objects","batch":"2"},{"aliases":["telescope"],"filename":"1f52d","category":"objects","batch":"2"},{"aliases":["microscope"],"filename":"1f52c","category":"objects","batch":"2"},{"aliases":["hole"],"filename":"1f573","category":"objects","batch":"2"},{"aliases":["pill"],"filename":"1f48a","category":"objects","batch":"2"},{"aliases":["syringe"],"filename":"1f489","category":"objects","batch":"2"},{"aliases":["thermometer"],"filename":"1f321","category":"objects","batch":"2"},{"aliases":["toilet"],"filename":"1f6bd","category":"objects","batch":"2"},{"aliases":["potable_water"],"filename":"1f6b0","category":"objects","batch":"2"},{"aliases":["shower"],"filename":"1f6bf","category":"objects","batch":"2"},{"aliases":["bathtub"],"filename":"1f6c1","category":"objects","batch":"2"},{"aliases":["bath"],"filename":"1f6c0","category":"objects","batch":"2"},{"aliases":["bellhop_bell"],"filename":"1f6ce","category":"objects","batch":"2"},{"aliases":["key"],"filename":"1f511","category":"objects","batch":"2"},{"aliases":["old_key"],"filename":"1f5dd","category":"objects","batch":"2"},{"aliases":["door"],"filename":"1f6aa","category":"objects","batch":"3"},{"aliases":["couch_and_lamp"],"filename":"1f6cb","category":"objects","batch":"3"},{"aliases":["bed"],"filename":"1f6cf","category":"objects","batch":"3"},{"aliases":["sleeping_bed"],"filename":"1f6cc","category":"objects","batch":"3"},{"aliases":["framed_picture"],"filename":"1f5bc","category":"objects","batch":"3"},{"aliases":["shopping"],"filename":"1f6cd","category":"objects","batch":"3"},{"aliases":["shopping_cart"],"filename":"1f6d2","category":"objects","batch":"3"},{"aliases":["gift"],"filename":"1f381","category":"objects","batch":"3"},{"aliases":["balloon"],"filename":"1f388","category":"objects","batch":"3"},{"aliases":["flags"],"filename":"1f38f","category":"objects","batch":"3"},{"aliases":["ribbon"],"filename":"1f380","category":"objects","batch":"3"},{"aliases":["confetti_ball"],"filename":"1f38a","category":"objects","batch":"3"},{"aliases":["tada"],"filename":"1f389","category":"objects","batch":"3"},{"aliases":["dolls"],"filename":"1f38e","category":"objects","batch":"3"},{"aliases":["izakaya_lantern","lantern"],"filename":"1f3ee","category":"objects","batch":"3"},{"aliases":["wind_chime"],"filename":"1f390","category":"objects","batch":"3"},{"aliases":["email","envelope"],"filename":"2709-fe0f","category":"objects","batch":"3"},{"aliases":["envelope_with_arrow"],"filename":"1f4e9","category":"objects","batch":"3"},{"aliases":["incoming_envelope"],"filename":"1f4e8","category":"objects","batch":"3"},{"aliases":["e-mail"],"filename":"1f4e7","category":"objects","batch":"3"},{"aliases":["love_letter"],"filename":"1f48c","category":"objects","batch":"3"},{"aliases":["inbox_tray"],"filename":"1f4e5","category":"objects","batch":"3"},{"aliases":["outbox_tray"],"filename":"1f4e4","category":"objects","batch":"3"},{"aliases":["package"],"filename":"1f4e6","category":"objects","batch":"3"},{"aliases":["label"],"filename":"1f3f7","category":"objects","batch":"3"},{"aliases":["mailbox_closed"],"filename":"1f4ea","category":"objects","batch":"3"},{"aliases":["mailbox"],"filename":"1f4eb","category":"objects","batch":"3"},{"aliases":["mailbox_with_mail"],"filename":"1f4ec","category":"objects","batch":"3"},{"aliases":["mailbox_with_no_mail"],"filename":"1f4ed","category":"objects","batch":"3"},{"aliases":["postbox"],"filename":"1f4ee","category":"objects","batch":"3"},{"aliases":["postal_horn"],"filename":"1f4ef","category":"objects","batch":"3"},{"aliases":["scroll"],"filename":"1f4dc","category":"objects","batch":"3"},{"aliases":["page_with_curl"],"filename":"1f4c3","category":"objects","batch":"3"},{"aliases":["page_facing_up"],"filename":"1f4c4","category":"objects","batch":"3"},{"aliases":["bookmark_tabs"],"filename":"1f4d1","category":"objects","batch":"3"},{"aliases":["bar_chart"],"filename":"1f4ca","category":"objects","batch":"3"},{"aliases":["chart_with_upwards_trend"],"filename":"1f4c8","category":"objects","batch":"3"},{"aliases":["chart_with_downwards_trend"],"filename":"1f4c9","category":"objects","batch":"3"},{"aliases":["spiral_notepad"],"filename":"1f5d2","category":"objects","batch":"3"},{"aliases":["spiral_calendar"],"filename":"1f5d3","category":"objects","batch":"3"},{"aliases":["calendar"],"filename":"1f4c6","category":"objects","batch":"3"},{"aliases":["date"],"filename":"1f4c5","category":"objects","batch":"3"},{"aliases":["card_index"],"filename":"1f4c7","category":"objects","batch":"3"},{"aliases":["card_file_box"],"filename":"1f5c3","category":"objects","batch":"3"},{"aliases":["ballot_box"],"filename":"1f5f3","category":"objects","batch":"3"},{"aliases":["file_cabinet"],"filename":"1f5c4","category":"objects","batch":"4"},{"aliases":["clipboard"],"filename":"1f4cb","category":"objects","batch":"4"},{"aliases":["file_folder"],"filename":"1f4c1","category":"objects","batch":"4"},{"aliases":["open_file_folder"],"filename":"1f4c2","category":"objects","batch":"4"},{"aliases":["card_index_dividers"],"filename":"1f5c2","category":"objects","batch":"4"},{"aliases":["newspaper_roll"],"filename":"1f5de","category":"objects","batch":"4"},{"aliases":["newspaper"],"filename":"1f4f0","category":"objects","batch":"4"},{"aliases":["notebook"],"filename":"1f4d3","category":"objects","batch":"4"},{"aliases":["notebook_with_decorative_cover"],"filename":"1f4d4","category":"objects","batch":"4"},{"aliases":["ledger"],"filename":"1f4d2","category":"objects","batch":"4"},{"aliases":["closed_book"],"filename":"1f4d5","category":"objects","batch":"4"},{"aliases":["green_book"],"filename":"1f4d7","category":"objects","batch":"4"},{"aliases":["blue_book"],"filename":"1f4d8","category":"objects","batch":"4"},{"aliases":["orange_book"],"filename":"1f4d9","category":"objects","batch":"4"},{"aliases":["books"],"filename":"1f4da","category":"objects","batch":"4"},{"aliases":["book","open_book"],"filename":"1f4d6","category":"objects","batch":"4"},{"aliases":["bookmark"],"filename":"1f516","category":"objects","batch":"4"},{"aliases":["link"],"filename":"1f517","category":"objects","batch":"4"},{"aliases":["paperclip"],"filename":"1f4ce","category":"objects","batch":"4"},{"aliases":["paperclips"],"filename":"1f587","category":"objects","batch":"4"},{"aliases":["triangular_ruler"],"filename":"1f4d0","category":"objects","batch":"4"},{"aliases":["straight_ruler"],"filename":"1f4cf","category":"objects","batch":"4"},{"aliases":["pushpin"],"filename":"1f4cc","category":"objects","batch":"4"},{"aliases":["round_pushpin"],"filename":"1f4cd","category":"objects","batch":"4"},{"aliases":["scissors"],"filename":"2702-fe0f","category":"objects","batch":"4"},{"aliases":["pen"],"filename":"1f58a","category":"objects","batch":"4"},{"aliases":["fountain_pen"],"filename":"1f58b","category":"objects","batch":"4"},{"aliases":["black_nib"],"filename":"2712-fe0f","category":"objects","batch":"4"},{"aliases":["paintbrush"],"filename":"1f58c","category":"objects","batch":"4"},{"aliases":["crayon"],"filename":"1f58d","category":"objects","batch":"4"},{"aliases":["memo","pencil"],"filename":"1f4dd","category":"objects","batch":"4"},{"aliases":["pencil2"],"filename":"270f-fe0f","category":"objects","batch":"4"},{"aliases":["mag"],"filename":"1f50d","category":"objects","batch":"4"},{"aliases":["mag_right"],"filename":"1f50e","category":"objects","batch":"4"},{"aliases":["lock_with_ink_pen"],"filename":"1f50f","category":"objects","batch":"4"},{"aliases":["closed_lock_with_key"],"filename":"1f510","category":"objects","batch":"4"},{"aliases":["lock"],"filename":"1f512","category":"objects","batch":"4"},{"aliases":["unlock"],"filename":"1f513","category":"objects","batch":"4"},{"aliases":["heart"],"filename":"2764-fe0f","category":"symbols","batch":"1"},{"aliases":["yellow_heart"],"filename":"1f49b","category":"symbols","batch":"1"},{"aliases":["green_heart"],"filename":"1f49a","category":"symbols","batch":"1"},{"aliases":["blue_heart"],"filename":"1f499","category":"symbols","batch":"1"},{"aliases":["purple_heart"],"filename":"1f49c","category":"symbols","batch":"1"},{"aliases":["black_heart"],"filename":"1f5a4","category":"symbols","batch":"1"},{"aliases":["broken_heart"],"filename":"1f494","category":"symbols","batch":"1"},{"aliases":["heavy_heart_exclamation"],"filename":"2763-fe0f","category":"symbols","batch":"1"},{"aliases":["two_hearts"],"filename":"1f495","category":"symbols","batch":"1"},{"aliases":["revolving_hearts"],"filename":"1f49e","category":"symbols","batch":"1"},{"aliases":["heartbeat"],"filename":"1f493","category":"symbols","batch":"1"},{"aliases":["heartpulse"],"filename":"1f497","category":"symbols","batch":"1"},{"aliases":["sparkling_heart"],"filename":"1f496","category":"symbols","batch":"1"},{"aliases":["cupid"],"filename":"1f498","category":"symbols","batch":"1"},{"aliases":["gift_heart"],"filename":"1f49d","category":"symbols","batch":"1"},{"aliases":["heart_decoration"],"filename":"1f49f","category":"symbols","batch":"1"},{"aliases":["peace_symbol"],"filename":"262e-fe0f","category":"symbols","batch":"1"},{"aliases":["latin_cross"],"filename":"271d-fe0f","category":"symbols","batch":"1"},{"aliases":["star_and_crescent"],"filename":"262a-fe0f","category":"symbols","batch":"1"},{"aliases":["om"],"filename":"1f549","category":"symbols","batch":"1"},{"aliases":["wheel_of_dharma"],"filename":"2638-fe0f","category":"symbols","batch":"1"},{"aliases":["star_of_david"],"filename":"2721-fe0f","category":"symbols","batch":"1"},{"aliases":["six_pointed_star"],"filename":"1f52f","category":"symbols","batch":"1"},{"aliases":["menorah"],"filename":"1f54e","category":"symbols","batch":"1"},{"aliases":["yin_yang"],"filename":"262f-fe0f","category":"symbols","batch":"1"},{"aliases":["orthodox_cross"],"filename":"2626-fe0f","category":"symbols","batch":"1"},{"aliases":["place_of_worship"],"filename":"1f6d0","category":"symbols","batch":"1"},{"aliases":["ophiuchus"],"filename":"26ce","category":"symbols","batch":"1"},{"aliases":["aries"],"filename":"2648-fe0f","category":"symbols","batch":"1"},{"aliases":["taurus"],"filename":"2649-fe0f","category":"symbols","batch":"1"},{"aliases":["gemini"],"filename":"264a-fe0f","category":"symbols","batch":"1"},{"aliases":["cancer"],"filename":"264b-fe0f","category":"symbols","batch":"1"},{"aliases":["leo"],"filename":"264c-fe0f","category":"symbols","batch":"1"},{"aliases":["virgo"],"filename":"264d-fe0f","category":"symbols","batch":"1"},{"aliases":["libra"],"filename":"264e-fe0f","category":"symbols","batch":"1"},{"aliases":["scorpius"],"filename":"264f-fe0f","category":"symbols","batch":"1"},{"aliases":["sagittarius"],"filename":"2650-fe0f","category":"symbols","batch":"1"},{"aliases":["capricorn"],"filename":"2651-fe0f","category":"symbols","batch":"1"},{"aliases":["aquarius"],"filename":"2652-fe0f","category":"symbols","batch":"1"},{"aliases":["pisces"],"filename":"2653-fe0f","category":"symbols","batch":"1"},{"aliases":["id"],"filename":"1f194","category":"symbols","batch":"1"},{"aliases":["atom_symbol"],"filename":"269b-fe0f","category":"symbols","batch":"1"},{"aliases":["accept"],"filename":"1f251","category":"symbols","batch":"1"},{"aliases":["radioactive"],"filename":"2622-fe0f","category":"symbols","batch":"1"},{"aliases":["biohazard"],"filename":"2623-fe0f","category":"symbols","batch":"1"},{"aliases":["mobile_phone_off"],"filename":"1f4f4","category":"symbols","batch":"2"},{"aliases":["vibration_mode"],"filename":"1f4f3","category":"symbols","batch":"2"},{"aliases":["u6709"],"filename":"1f236","category":"symbols","batch":"2"},{"aliases":["u7121"],"filename":"1f21a-fe0f","category":"symbols","batch":"2"},{"aliases":["u7533"],"filename":"1f238","category":"symbols","batch":"2"},{"aliases":["u55b6"],"filename":"1f23a","category":"symbols","batch":"2"},{"aliases":["u6708"],"filename":"1f237-fe0f","category":"symbols","batch":"2"},{"aliases":["eight_pointed_black_star"],"filename":"2734-fe0f","category":"symbols","batch":"2"},{"aliases":["vs"],"filename":"1f19a","category":"symbols","batch":"2"},{"aliases":["white_flower"],"filename":"1f4ae","category":"symbols","batch":"2"},{"aliases":["ideograph_advantage"],"filename":"1f250","category":"symbols","batch":"2"},{"aliases":["secret"],"filename":"3299-fe0f","category":"symbols","batch":"2"},{"aliases":["congratulations"],"filename":"3297-fe0f","category":"symbols","batch":"2"},{"aliases":["u5408"],"filename":"1f234","category":"symbols","batch":"2"},{"aliases":["u6e80"],"filename":"1f235","category":"symbols","batch":"2"},{"aliases":["u5272"],"filename":"1f239","category":"symbols","batch":"2"},{"aliases":["u7981"],"filename":"1f232","category":"symbols","batch":"2"},{"aliases":["a"],"filename":"1f170-fe0f","category":"symbols","batch":"2"},{"aliases":["b"],"filename":"1f171-fe0f","category":"symbols","batch":"2"},{"aliases":["ab"],"filename":"1f18e","category":"symbols","batch":"2"},{"aliases":["cl"],"filename":"1f191","category":"symbols","batch":"2"},{"aliases":["o2"],"filename":"1f17e-fe0f","category":"symbols","batch":"2"},{"aliases":["sos"],"filename":"1f198","category":"symbols","batch":"2"},{"aliases":["x"],"filename":"274c","category":"symbols","batch":"2"},{"aliases":["o"],"filename":"2b55-fe0f","category":"symbols","batch":"2"},{"aliases":["stop_sign"],"filename":"1f6d1","category":"symbols","batch":"2"},{"aliases":["no_entry"],"filename":"26d4-fe0f","category":"symbols","batch":"2"},{"aliases":["name_badge"],"filename":"1f4db","category":"symbols","batch":"2"},{"aliases":["no_entry_sign"],"filename":"1f6ab","category":"symbols","batch":"2"},{"aliases":["100"],"filename":"1f4af","category":"symbols","batch":"2"},{"aliases":["anger"],"filename":"1f4a2","category":"symbols","batch":"2"},{"aliases":["hotsprings"],"filename":"2668-fe0f","category":"symbols","batch":"2"},{"aliases":["no_pedestrians"],"filename":"1f6b7","category":"symbols","batch":"2"},{"aliases":["do_not_litter"],"filename":"1f6af","category":"symbols","batch":"2"},{"aliases":["no_bicycles"],"filename":"1f6b3","category":"symbols","batch":"2"},{"aliases":["non-potable_water"],"filename":"1f6b1","category":"symbols","batch":"2"},{"aliases":["underage"],"filename":"1f51e","category":"symbols","batch":"2"},{"aliases":["no_mobile_phones"],"filename":"1f4f5","category":"symbols","batch":"2"},{"aliases":["no_smoking"],"filename":"1f6ad","category":"symbols","batch":"2"},{"aliases":["exclamation","heavy_exclamation_mark"],"filename":"2757-fe0f","category":"symbols","batch":"2"},{"aliases":["grey_exclamation"],"filename":"2755","category":"symbols","batch":"2"},{"aliases":["question"],"filename":"2753","category":"symbols","batch":"2"},{"aliases":["grey_question"],"filename":"2754","category":"symbols","batch":"2"},{"aliases":["bangbang"],"filename":"203c-fe0f","category":"symbols","batch":"2"},{"aliases":["interrobang"],"filename":"2049-fe0f","category":"symbols","batch":"2"},{"aliases":["low_brightness"],"filename":"1f505","category":"symbols","batch":"3"},{"aliases":["high_brightness"],"filename":"1f506","category":"symbols","batch":"3"},{"aliases":["part_alternation_mark"],"filename":"303d-fe0f","category":"symbols","batch":"3"},{"aliases":["warning"],"filename":"26a0-fe0f","category":"symbols","batch":"3"},{"aliases":["children_crossing"],"filename":"1f6b8","category":"symbols","batch":"3"},{"aliases":["trident"],"filename":"1f531","category":"symbols","batch":"3"},{"aliases":["fleur_de_lis"],"filename":"269c-fe0f","category":"symbols","batch":"3"},{"aliases":["beginner"],"filename":"1f530","category":"symbols","batch":"3"},{"aliases":["recycle"],"filename":"267b-fe0f","category":"symbols","batch":"3"},{"aliases":["white_check_mark"],"filename":"2705","category":"symbols","batch":"3"},{"aliases":["u6307"],"filename":"1f22f-fe0f","category":"symbols","batch":"3"},{"aliases":["chart"],"filename":"1f4b9","category":"symbols","batch":"3"},{"aliases":["sparkle"],"filename":"2747-fe0f","category":"symbols","batch":"3"},{"aliases":["eight_spoked_asterisk"],"filename":"2733-fe0f","category":"symbols","batch":"3"},{"aliases":["negative_squared_cross_mark"],"filename":"274e","category":"symbols","batch":"3"},{"aliases":["globe_with_meridians"],"filename":"1f310","category":"symbols","batch":"3"},{"aliases":["diamond_shape_with_a_dot_inside"],"filename":"1f4a0","category":"symbols","batch":"3"},{"aliases":["m"],"filename":"24c2-fe0f","category":"symbols","batch":"3"},{"aliases":["cyclone"],"filename":"1f300","category":"symbols","batch":"3"},{"aliases":["zzz"],"filename":"1f4a4","category":"symbols","batch":"3"},{"aliases":["atm"],"filename":"1f3e7","category":"symbols","batch":"3"},{"aliases":["wc"],"filename":"1f6be","category":"symbols","batch":"3"},{"aliases":["wheelchair"],"filename":"267f-fe0f","category":"symbols","batch":"3"},{"aliases":["parking"],"filename":"1f17f-fe0f","category":"symbols","batch":"3"},{"aliases":["u7a7a"],"filename":"1f233","category":"symbols","batch":"3"},{"aliases":["sa"],"filename":"1f202-fe0f","category":"symbols","batch":"3"},{"aliases":["passport_control"],"filename":"1f6c2","category":"symbols","batch":"3"},{"aliases":["customs"],"filename":"1f6c3","category":"symbols","batch":"3"},{"aliases":["baggage_claim"],"filename":"1f6c4","category":"symbols","batch":"3"},{"aliases":["left_luggage"],"filename":"1f6c5","category":"symbols","batch":"3"},{"aliases":["mens"],"filename":"1f6b9","category":"symbols","batch":"3"},{"aliases":["womens"],"filename":"1f6ba","category":"symbols","batch":"3"},{"aliases":["baby_symbol"],"filename":"1f6bc","category":"symbols","batch":"3"},{"aliases":["restroom"],"filename":"1f6bb","category":"symbols","batch":"3"},{"aliases":["put_litter_in_its_place"],"filename":"1f6ae","category":"symbols","batch":"3"},{"aliases":["cinema"],"filename":"1f3a6","category":"symbols","batch":"3"},{"aliases":["signal_strength"],"filename":"1f4f6","category":"symbols","batch":"3"},{"aliases":["koko"],"filename":"1f201","category":"symbols","batch":"3"},{"aliases":["symbols"],"filename":"1f523","category":"symbols","batch":"3"},{"aliases":["information_source"],"filename":"2139-fe0f","category":"symbols","batch":"3"},{"aliases":["abc"],"filename":"1f524","category":"symbols","batch":"3"},{"aliases":["abcd"],"filename":"1f521","category":"symbols","batch":"3"},{"aliases":["capital_abcd"],"filename":"1f520","category":"symbols","batch":"3"},{"aliases":["ng"],"filename":"1f196","category":"symbols","batch":"3"},{"aliases":["ok"],"filename":"1f197","category":"symbols","batch":"3"},{"aliases":["up"],"filename":"1f199","category":"symbols","batch":"4"},{"aliases":["cool"],"filename":"1f192","category":"symbols","batch":"4"},{"aliases":["new"],"filename":"1f195","category":"symbols","batch":"4"},{"aliases":["free"],"filename":"1f193","category":"symbols","batch":"4"},{"aliases":["zero"],"filename":"0030-fe0f-20e3","category":"symbols","batch":"4"},{"aliases":["one"],"filename":"0031-fe0f-20e3","category":"symbols","batch":"4"},{"aliases":["two"],"filename":"0032-fe0f-20e3","category":"symbols","batch":"4"},{"aliases":["three"],"filename":"0033-fe0f-20e3","category":"symbols","batch":"4"},{"aliases":["four"],"filename":"0034-fe0f-20e3","category":"symbols","batch":"4"},{"aliases":["five"],"filename":"0035-fe0f-20e3","category":"symbols","batch":"4"},{"aliases":["six"],"filename":"0036-fe0f-20e3","category":"symbols","batch":"4"},{"aliases":["seven"],"filename":"0037-fe0f-20e3","category":"symbols","batch":"4"},{"aliases":["eight"],"filename":"0038-fe0f-20e3","category":"symbols","batch":"4"},{"aliases":["nine"],"filename":"0039-fe0f-20e3","category":"symbols","batch":"4"},{"aliases":["keycap_ten"],"filename":"1f51f","category":"symbols","batch":"4"},{"aliases":["1234"],"filename":"1f522","category":"symbols","batch":"4"},{"aliases":["hash"],"filename":"0023-fe0f-20e3","category":"symbols","batch":"4"},{"aliases":["asterisk"],"filename":"002a-fe0f-20e3","category":"symbols","batch":"4"},{"aliases":["arrow_forward"],"filename":"25b6-fe0f","category":"symbols","batch":"4"},{"aliases":["pause_button"],"filename":"23f8","category":"symbols","batch":"4"},{"aliases":["play_or_pause_button"],"filename":"23ef","category":"symbols","batch":"4"},{"aliases":["stop_button"],"filename":"23f9","category":"symbols","batch":"4"},{"aliases":["record_button"],"filename":"23fa","category":"symbols","batch":"4"},{"aliases":["next_track_button"],"filename":"23ed","category":"symbols","batch":"4"},{"aliases":["previous_track_button"],"filename":"23ee","category":"symbols","batch":"4"},{"aliases":["fast_forward"],"filename":"23e9","category":"symbols","batch":"4"},{"aliases":["rewind"],"filename":"23ea","category":"symbols","batch":"4"},{"aliases":["arrow_double_up"],"filename":"23eb","category":"symbols","batch":"4"},{"aliases":["arrow_double_down"],"filename":"23ec","category":"symbols","batch":"4"},{"aliases":["arrow_backward"],"filename":"25c0-fe0f","category":"symbols","batch":"4"},{"aliases":["arrow_up_small"],"filename":"1f53c","category":"symbols","batch":"4"},{"aliases":["arrow_down_small"],"filename":"1f53d","category":"symbols","batch":"4"},{"aliases":["arrow_right"],"filename":"27a1-fe0f","category":"symbols","batch":"4"},{"aliases":["arrow_left"],"filename":"2b05-fe0f","category":"symbols","batch":"4"},{"aliases":["arrow_up"],"filename":"2b06-fe0f","category":"symbols","batch":"4"},{"aliases":["arrow_down"],"filename":"2b07-fe0f","category":"symbols","batch":"4"},{"aliases":["arrow_upper_right"],"filename":"2197-fe0f","category":"symbols","batch":"4"},{"aliases":["arrow_lower_right"],"filename":"2198-fe0f","category":"symbols","batch":"4"},{"aliases":["arrow_lower_left"],"filename":"2199-fe0f","category":"symbols","batch":"4"},{"aliases":["arrow_upper_left"],"filename":"2196-fe0f","category":"symbols","batch":"4"},{"aliases":["arrow_up_down"],"filename":"2195-fe0f","category":"symbols","batch":"4"},{"aliases":["left_right_arrow"],"filename":"2194-fe0f","category":"symbols","batch":"4"},{"aliases":["arrow_right_hook"],"filename":"21aa-fe0f","category":"symbols","batch":"4"},{"aliases":["leftwards_arrow_with_hook"],"filename":"21a9-fe0f","category":"symbols","batch":"4"},{"aliases":["arrow_heading_up"],"filename":"2934-fe0f","category":"symbols","batch":"4"},{"aliases":["arrow_heading_down"],"filename":"2935-fe0f","category":"symbols","batch":"5"},{"aliases":["twisted_rightwards_arrows"],"filename":"1f500","category":"symbols","batch":"5"},{"aliases":["repeat"],"filename":"1f501","category":"symbols","batch":"5"},{"aliases":["repeat_one"],"filename":"1f502","category":"symbols","batch":"5"},{"aliases":["arrows_counterclockwise"],"filename":"1f504","category":"symbols","batch":"5"},{"aliases":["arrows_clockwise"],"filename":"1f503","category":"symbols","batch":"5"},{"aliases":["musical_note"],"filename":"1f3b5","category":"symbols","batch":"5"},{"aliases":["notes"],"filename":"1f3b6","category":"symbols","batch":"5"},{"aliases":["heavy_plus_sign"],"filename":"2795","category":"symbols","batch":"5"},{"aliases":["heavy_minus_sign"],"filename":"2796","category":"symbols","batch":"5"},{"aliases":["heavy_division_sign"],"filename":"2797","category":"symbols","batch":"5"},{"aliases":["heavy_multiplication_x"],"filename":"2716-fe0f","category":"symbols","batch":"5"},{"aliases":["heavy_dollar_sign"],"filename":"1f4b2","category":"symbols","batch":"5"},{"aliases":["currency_exchange"],"filename":"1f4b1","category":"symbols","batch":"5"},{"aliases":["tm"],"filename":"2122-fe0f","category":"symbols","batch":"5"},{"aliases":["copyright"],"filename":"00a9-fe0f","category":"symbols","batch":"5"},{"aliases":["registered"],"filename":"00ae-fe0f","category":"symbols","batch":"5"},{"aliases":["wavy_dash"],"filename":"3030-fe0f","category":"symbols","batch":"5"},{"aliases":["curly_loop"],"filename":"27b0","category":"symbols","batch":"5"},{"aliases":["loop"],"filename":"27bf","category":"symbols","batch":"5"},{"aliases":["end"],"filename":"1f51a","category":"symbols","batch":"5"},{"aliases":["back"],"filename":"1f519","category":"symbols","batch":"5"},{"aliases":["on"],"filename":"1f51b","category":"symbols","batch":"5"},{"aliases":["top"],"filename":"1f51d","category":"symbols","batch":"5"},{"aliases":["soon"],"filename":"1f51c","category":"symbols","batch":"5"},{"aliases":["heavy_check_mark"],"filename":"2714-fe0f","category":"symbols","batch":"5"},{"aliases":["ballot_box_with_check"],"filename":"2611-fe0f","category":"symbols","batch":"5"},{"aliases":["radio_button"],"filename":"1f518","category":"symbols","batch":"5"},{"aliases":["white_circle"],"filename":"26aa-fe0f","category":"symbols","batch":"5"},{"aliases":["black_circle"],"filename":"26ab-fe0f","category":"symbols","batch":"5"},{"aliases":["red_circle"],"filename":"1f534","category":"symbols","batch":"5"},{"aliases":["large_blue_circle"],"filename":"1f535","category":"symbols","batch":"5"},{"aliases":["small_red_triangle"],"filename":"1f53a","category":"symbols","batch":"5"},{"aliases":["small_red_triangle_down"],"filename":"1f53b","category":"symbols","batch":"5"},{"aliases":["small_orange_diamond"],"filename":"1f538","category":"symbols","batch":"5"},{"aliases":["small_blue_diamond"],"filename":"1f539","category":"symbols","batch":"5"},{"aliases":["large_orange_diamond"],"filename":"1f536","category":"symbols","batch":"5"},{"aliases":["large_blue_diamond"],"filename":"1f537","category":"symbols","batch":"5"},{"aliases":["white_square_button"],"filename":"1f533","category":"symbols","batch":"5"},{"aliases":["black_square_button"],"filename":"1f532","category":"symbols","batch":"5"},{"aliases":["black_small_square"],"filename":"25aa-fe0f","category":"symbols","batch":"5"},{"aliases":["white_small_square"],"filename":"25ab-fe0f","category":"symbols","batch":"5"},{"aliases":["black_medium_small_square"],"filename":"25fe-fe0f","category":"symbols","batch":"5"},{"aliases":["white_medium_small_square"],"filename":"25fd-fe0f","category":"symbols","batch":"5"},{"aliases":["black_medium_square"],"filename":"25fc-fe0f","category":"symbols","batch":"5"},{"aliases":["white_medium_square"],"filename":"25fb-fe0f","category":"symbols","batch":"6"},{"aliases":["black_large_square"],"filename":"2b1b-fe0f","category":"symbols","batch":"6"},{"aliases":["white_large_square"],"filename":"2b1c-fe0f","category":"symbols","batch":"6"},{"aliases":["speaker"],"filename":"1f508","category":"symbols","batch":"6"},{"aliases":["mute"],"filename":"1f507","category":"symbols","batch":"6"},{"aliases":["sound"],"filename":"1f509","category":"symbols","batch":"6"},{"aliases":["loud_sound"],"filename":"1f50a","category":"symbols","batch":"6"},{"aliases":["bell"],"filename":"1f514","category":"symbols","batch":"6"},{"aliases":["no_bell"],"filename":"1f515","category":"symbols","batch":"6"},{"aliases":["mega"],"filename":"1f4e3","category":"symbols","batch":"6"},{"aliases":["loudspeaker"],"filename":"1f4e2","category":"symbols","batch":"6"},{"aliases":["eye_speech_bubble"],"filename":"1f441-200d-1f5e8","category":"symbols","batch":"6"},{"aliases":["speech_balloon"],"filename":"1f4ac","category":"symbols","batch":"6"},{"aliases":["thought_balloon"],"filename":"1f4ad","category":"symbols","batch":"6"},{"aliases":["right_anger_bubble"],"filename":"1f5ef","category":"symbols","batch":"6"},{"aliases":["spades"],"filename":"2660-fe0f","category":"symbols","batch":"6"},{"aliases":["clubs"],"filename":"2663-fe0f","category":"symbols","batch":"6"},{"aliases":["hearts"],"filename":"2665-fe0f","category":"symbols","batch":"6"},{"aliases":["diamonds"],"filename":"2666-fe0f","category":"symbols","batch":"6"},{"aliases":["black_joker"],"filename":"1f0cf","category":"symbols","batch":"6"},{"aliases":["flower_playing_cards"],"filename":"1f3b4","category":"symbols","batch":"6"},{"aliases":["mahjong"],"filename":"1f004-fe0f","category":"symbols","batch":"6"},{"aliases":["clock1"],"filename":"1f550","category":"symbols","batch":"6"},{"aliases":["clock2"],"filename":"1f551","category":"symbols","batch":"6"},{"aliases":["clock3"],"filename":"1f552","category":"symbols","batch":"6"},{"aliases":["clock4"],"filename":"1f553","category":"symbols","batch":"6"},{"aliases":["clock5"],"filename":"1f554","category":"symbols","batch":"6"},{"aliases":["clock6"],"filename":"1f555","category":"symbols","batch":"6"},{"aliases":["clock7"],"filename":"1f556","category":"symbols","batch":"6"},{"aliases":["clock8"],"filename":"1f557","category":"symbols","batch":"6"},{"aliases":["clock9"],"filename":"1f558","category":"symbols","batch":"6"},{"aliases":["clock10"],"filename":"1f559","category":"symbols","batch":"6"},{"aliases":["clock11"],"filename":"1f55a","category":"symbols","batch":"6"},{"aliases":["clock12"],"filename":"1f55b","category":"symbols","batch":"6"},{"aliases":["clock130"],"filename":"1f55c","category":"symbols","batch":"6"},{"aliases":["clock230"],"filename":"1f55d","category":"symbols","batch":"6"},{"aliases":["clock330"],"filename":"1f55e","category":"symbols","batch":"6"},{"aliases":["clock430"],"filename":"1f55f","category":"symbols","batch":"6"},{"aliases":["clock530"],"filename":"1f560","category":"symbols","batch":"6"},{"aliases":["clock630"],"filename":"1f561","category":"symbols","batch":"6"},{"aliases":["clock730"],"filename":"1f562","category":"symbols","batch":"6"},{"aliases":["clock830"],"filename":"1f563","category":"symbols","batch":"6"},{"aliases":["clock930"],"filename":"1f564","category":"symbols","batch":"6"},{"aliases":["clock1030"],"filename":"1f565","category":"symbols","batch":"6"},{"aliases":["clock1130"],"filename":"1f566","category":"symbols","batch":"6"},{"aliases":["clock1230"],"filename":"1f567","category":"symbols","batch":"7"},{"aliases":["white_flag"],"filename":"1f3f3-fe0f","category":"flags","batch":"1"},{"aliases":["black_flag"],"filename":"1f3f4","category":"flags","batch":"1"},{"aliases":["checkered_flag"],"filename":"1f3c1","category":"flags","batch":"1"},{"aliases":["triangular_flag_on_post"],"filename":"1f6a9","category":"flags","batch":"1"},{"aliases":["rainbow_flag"],"filename":"1f3f3-fe0f-200d-1f308","category":"flags","batch":"1"},{"aliases":["afghanistan"],"filename":"1f1e6-1f1eb","category":"flags","batch":"1"},{"aliases":["aland_islands"],"filename":"1f1e6-1f1fd","category":"flags","batch":"1"},{"aliases":["albania"],"filename":"1f1e6-1f1f1","category":"flags","batch":"1"},{"aliases":["algeria"],"filename":"1f1e9-1f1ff","category":"flags","batch":"1"},{"aliases":["american_samoa"],"filename":"1f1e6-1f1f8","category":"flags","batch":"1"},{"aliases":["andorra"],"filename":"1f1e6-1f1e9","category":"flags","batch":"1"},{"aliases":["angola"],"filename":"1f1e6-1f1f4","category":"flags","batch":"1"},{"aliases":["anguilla"],"filename":"1f1e6-1f1ee","category":"flags","batch":"1"},{"aliases":["antarctica"],"filename":"1f1e6-1f1f6","category":"flags","batch":"1"},{"aliases":["antigua_barbuda"],"filename":"1f1e6-1f1ec","category":"flags","batch":"1"},{"aliases":["argentina"],"filename":"1f1e6-1f1f7","category":"flags","batch":"1"},{"aliases":["armenia"],"filename":"1f1e6-1f1f2","category":"flags","batch":"1"},{"aliases":["aruba"],"filename":"1f1e6-1f1fc","category":"flags","batch":"1"},{"aliases":["australia"],"filename":"1f1e6-1f1fa","category":"flags","batch":"1"},{"aliases":["austria"],"filename":"1f1e6-1f1f9","category":"flags","batch":"1"},{"aliases":["azerbaijan"],"filename":"1f1e6-1f1ff","category":"flags","batch":"1"},{"aliases":["bahamas"],"filename":"1f1e7-1f1f8","category":"flags","batch":"1"},{"aliases":["bahrain"],"filename":"1f1e7-1f1ed","category":"flags","batch":"1"},{"aliases":["bangladesh"],"filename":"1f1e7-1f1e9","category":"flags","batch":"1"},{"aliases":["barbados"],"filename":"1f1e7-1f1e7","category":"flags","batch":"1"},{"aliases":["belarus"],"filename":"1f1e7-1f1fe","category":"flags","batch":"1"},{"aliases":["belgium"],"filename":"1f1e7-1f1ea","category":"flags","batch":"1"},{"aliases":["belize"],"filename":"1f1e7-1f1ff","category":"flags","batch":"1"},{"aliases":["benin"],"filename":"1f1e7-1f1ef","category":"flags","batch":"1"},{"aliases":["bermuda"],"filename":"1f1e7-1f1f2","category":"flags","batch":"1"},{"aliases":["bhutan"],"filename":"1f1e7-1f1f9","category":"flags","batch":"1"},{"aliases":["bolivia"],"filename":"1f1e7-1f1f4","category":"flags","batch":"1"},{"aliases":["caribbean_netherlands"],"filename":"1f1e7-1f1f6","category":"flags","batch":"1"},{"aliases":["bosnia_herzegovina"],"filename":"1f1e7-1f1e6","category":"flags","batch":"1"},{"aliases":["botswana"],"filename":"1f1e7-1f1fc","category":"flags","batch":"1"},{"aliases":["brazil"],"filename":"1f1e7-1f1f7","category":"flags","batch":"1"},{"aliases":["british_indian_ocean_territory"],"filename":"1f1ee-1f1f4","category":"flags","batch":"1"},{"aliases":["british_virgin_islands"],"filename":"1f1fb-1f1ec","category":"flags","batch":"1"},{"aliases":["brunei"],"filename":"1f1e7-1f1f3","category":"flags","batch":"1"},{"aliases":["bulgaria"],"filename":"1f1e7-1f1ec","category":"flags","batch":"1"},{"aliases":["burkina_faso"],"filename":"1f1e7-1f1eb","category":"flags","batch":"1"},{"aliases":["burundi"],"filename":"1f1e7-1f1ee","category":"flags","batch":"1"},{"aliases":["cape_verde"],"filename":"1f1e8-1f1fb","category":"flags","batch":"1"},{"aliases":["cambodia"],"filename":"1f1f0-1f1ed","category":"flags","batch":"1"},{"aliases":["cameroon"],"filename":"1f1e8-1f1f2","category":"flags","batch":"1"},{"aliases":["canada","ca"],"filename":"1f1e8-1f1e6","category":"flags","batch":"2"},{"aliases":["canary_islands"],"filename":"1f1ee-1f1e8","category":"flags","batch":"2"},{"aliases":["cayman_islands"],"filename":"1f1f0-1f1fe","category":"flags","batch":"2"},{"aliases":["central_african_republic"],"filename":"1f1e8-1f1eb","category":"flags","batch":"2"},{"aliases":["chad"],"filename":"1f1f9-1f1e9","category":"flags","batch":"2"},{"aliases":["chile"],"filename":"1f1e8-1f1f1","category":"flags","batch":"2"},{"aliases":["cn"],"filename":"1f1e8-1f1f3","category":"flags","batch":"2"},{"aliases":["christmas_island"],"filename":"1f1e8-1f1fd","category":"flags","batch":"2"},{"aliases":["cocos_islands"],"filename":"1f1e8-1f1e8","category":"flags","batch":"2"},{"aliases":["colombia"],"filename":"1f1e8-1f1f4","category":"flags","batch":"2"},{"aliases":["comoros"],"filename":"1f1f0-1f1f2","category":"flags","batch":"2"},{"aliases":["congo_brazzaville"],"filename":"1f1e8-1f1ec","category":"flags","batch":"2"},{"aliases":["congo_kinshasa"],"filename":"1f1e8-1f1e9","category":"flags","batch":"2"},{"aliases":["cook_islands"],"filename":"1f1e8-1f1f0","category":"flags","batch":"2"},{"aliases":["costa_rica"],"filename":"1f1e8-1f1f7","category":"flags","batch":"2"},{"aliases":["cote_divoire"],"filename":"1f1e8-1f1ee","category":"flags","batch":"2"},{"aliases":["croatia"],"filename":"1f1ed-1f1f7","category":"flags","batch":"2"},{"aliases":["cuba"],"filename":"1f1e8-1f1fa","category":"flags","batch":"2"},{"aliases":["curacao"],"filename":"1f1e8-1f1fc","category":"flags","batch":"2"},{"aliases":["cyprus"],"filename":"1f1e8-1f1fe","category":"flags","batch":"2"},{"aliases":["czech_republic"],"filename":"1f1e8-1f1ff","category":"flags","batch":"2"},{"aliases":["denmark"],"filename":"1f1e9-1f1f0","category":"flags","batch":"2"},{"aliases":["djibouti"],"filename":"1f1e9-1f1ef","category":"flags","batch":"2"},{"aliases":["dominica"],"filename":"1f1e9-1f1f2","category":"flags","batch":"2"},{"aliases":["dominican_republic"],"filename":"1f1e9-1f1f4","category":"flags","batch":"2"},{"aliases":["ecuador"],"filename":"1f1ea-1f1e8","category":"flags","batch":"2"},{"aliases":["egypt"],"filename":"1f1ea-1f1ec","category":"flags","batch":"2"},{"aliases":["el_salvador"],"filename":"1f1f8-1f1fb","category":"flags","batch":"2"},{"aliases":["equatorial_guinea"],"filename":"1f1ec-1f1f6","category":"flags","batch":"2"},{"aliases":["eritrea"],"filename":"1f1ea-1f1f7","category":"flags","batch":"2"},{"aliases":["estonia"],"filename":"1f1ea-1f1ea","category":"flags","batch":"2"},{"aliases":["ethiopia"],"filename":"1f1ea-1f1f9","category":"flags","batch":"2"},{"aliases":["eu","european_union"],"filename":"1f1ea-1f1fa","category":"flags","batch":"2"},{"aliases":["falkland_islands"],"filename":"1f1eb-1f1f0","category":"flags","batch":"2"},{"aliases":["faroe_islands"],"filename":"1f1eb-1f1f4","category":"flags","batch":"2"},{"aliases":["fiji"],"filename":"1f1eb-1f1ef","category":"flags","batch":"2"},{"aliases":["finland"],"filename":"1f1eb-1f1ee","category":"flags","batch":"2"},{"aliases":["fr"],"filename":"1f1eb-1f1f7","category":"flags","batch":"2"},{"aliases":["french_guiana"],"filename":"1f1ec-1f1eb","category":"flags","batch":"2"},{"aliases":["french_polynesia"],"filename":"1f1f5-1f1eb","category":"flags","batch":"2"},{"aliases":["french_southern_territories"],"filename":"1f1f9-1f1eb","category":"flags","batch":"2"},{"aliases":["gabon"],"filename":"1f1ec-1f1e6","category":"flags","batch":"2"},{"aliases":["gambia"],"filename":"1f1ec-1f1f2","category":"flags","batch":"2"},{"aliases":["georgia"],"filename":"1f1ec-1f1ea","category":"flags","batch":"2"},{"aliases":["de"],"filename":"1f1e9-1f1ea","category":"flags","batch":"2"},{"aliases":["ghana"],"filename":"1f1ec-1f1ed","category":"flags","batch":"3"},{"aliases":["gibraltar"],"filename":"1f1ec-1f1ee","category":"flags","batch":"3"},{"aliases":["greece"],"filename":"1f1ec-1f1f7","category":"flags","batch":"3"},{"aliases":["greenland"],"filename":"1f1ec-1f1f1","category":"flags","batch":"3"},{"aliases":["grenada"],"filename":"1f1ec-1f1e9","category":"flags","batch":"3"},{"aliases":["guadeloupe"],"filename":"1f1ec-1f1f5","category":"flags","batch":"3"},{"aliases":["guam"],"filename":"1f1ec-1f1fa","category":"flags","batch":"3"},{"aliases":["guatemala"],"filename":"1f1ec-1f1f9","category":"flags","batch":"3"},{"aliases":["guernsey"],"filename":"1f1ec-1f1ec","category":"flags","batch":"3"},{"aliases":["guinea"],"filename":"1f1ec-1f1f3","category":"flags","batch":"3"},{"aliases":["guinea_bissau"],"filename":"1f1ec-1f1fc","category":"flags","batch":"3"},{"aliases":["guyana"],"filename":"1f1ec-1f1fe","category":"flags","batch":"3"},{"aliases":["haiti"],"filename":"1f1ed-1f1f9","category":"flags","batch":"3"},{"aliases":["honduras"],"filename":"1f1ed-1f1f3","category":"flags","batch":"3"},{"aliases":["hong_kong"],"filename":"1f1ed-1f1f0","category":"flags","batch":"3"},{"aliases":["hungary"],"filename":"1f1ed-1f1fa","category":"flags","batch":"3"},{"aliases":["iceland"],"filename":"1f1ee-1f1f8","category":"flags","batch":"3"},{"aliases":["india"],"filename":"1f1ee-1f1f3","category":"flags","batch":"3"},{"aliases":["indonesia"],"filename":"1f1ee-1f1e9","category":"flags","batch":"3"},{"aliases":["iran"],"filename":"1f1ee-1f1f7","category":"flags","batch":"3"},{"aliases":["iraq"],"filename":"1f1ee-1f1f6","category":"flags","batch":"3"},{"aliases":["ireland"],"filename":"1f1ee-1f1ea","category":"flags","batch":"3"},{"aliases":["isle_of_man"],"filename":"1f1ee-1f1f2","category":"flags","batch":"3"},{"aliases":["israel"],"filename":"1f1ee-1f1f1","category":"flags","batch":"3"},{"aliases":["it"],"filename":"1f1ee-1f1f9","category":"flags","batch":"3"},{"aliases":["jamaica"],"filename":"1f1ef-1f1f2","category":"flags","batch":"3"},{"aliases":["jp"],"filename":"1f1ef-1f1f5","category":"flags","batch":"3"},{"aliases":["crossed_flags"],"filename":"1f38c","category":"flags","batch":"3"},{"aliases":["jersey"],"filename":"1f1ef-1f1ea","category":"flags","batch":"3"},{"aliases":["jordan"],"filename":"1f1ef-1f1f4","category":"flags","batch":"3"},{"aliases":["kazakhstan"],"filename":"1f1f0-1f1ff","category":"flags","batch":"3"},{"aliases":["kenya"],"filename":"1f1f0-1f1ea","category":"flags","batch":"3"},{"aliases":["kiribati"],"filename":"1f1f0-1f1ee","category":"flags","batch":"3"},{"aliases":["kosovo"],"filename":"1f1fd-1f1f0","category":"flags","batch":"3"},{"aliases":["kuwait"],"filename":"1f1f0-1f1fc","category":"flags","batch":"3"},{"aliases":["kyrgyzstan"],"filename":"1f1f0-1f1ec","category":"flags","batch":"3"},{"aliases":["laos"],"filename":"1f1f1-1f1e6","category":"flags","batch":"3"},{"aliases":["latvia"],"filename":"1f1f1-1f1fb","category":"flags","batch":"3"},{"aliases":["lebanon"],"filename":"1f1f1-1f1e7","category":"flags","batch":"3"},{"aliases":["lesotho"],"filename":"1f1f1-1f1f8","category":"flags","batch":"3"},{"aliases":["liberia"],"filename":"1f1f1-1f1f7","category":"flags","batch":"3"},{"aliases":["libya"],"filename":"1f1f1-1f1fe","category":"flags","batch":"3"},{"aliases":["liechtenstein"],"filename":"1f1f1-1f1ee","category":"flags","batch":"3"},{"aliases":["lithuania"],"filename":"1f1f1-1f1f9","category":"flags","batch":"3"},{"aliases":["luxembourg"],"filename":"1f1f1-1f1fa","category":"flags","batch":"3"},{"aliases":["macau"],"filename":"1f1f2-1f1f4","category":"flags","batch":"4"},{"aliases":["macedonia"],"filename":"1f1f2-1f1f0","category":"flags","batch":"4"},{"aliases":["madagascar"],"filename":"1f1f2-1f1ec","category":"flags","batch":"4"},{"aliases":["malawi"],"filename":"1f1f2-1f1fc","category":"flags","batch":"4"},{"aliases":["malaysia"],"filename":"1f1f2-1f1fe","category":"flags","batch":"4"},{"aliases":["maldives"],"filename":"1f1f2-1f1fb","category":"flags","batch":"4"},{"aliases":["mali"],"filename":"1f1f2-1f1f1","category":"flags","batch":"4"},{"aliases":["malta"],"filename":"1f1f2-1f1f9","category":"flags","batch":"4"},{"aliases":["marshall_islands"],"filename":"1f1f2-1f1ed","category":"flags","batch":"4"},{"aliases":["martinique"],"filename":"1f1f2-1f1f6","category":"flags","batch":"4"},{"aliases":["mauritania"],"filename":"1f1f2-1f1f7","category":"flags","batch":"4"},{"aliases":["mauritius"],"filename":"1f1f2-1f1fa","category":"flags","batch":"4"},{"aliases":["mayotte"],"filename":"1f1fe-1f1f9","category":"flags","batch":"4"},{"aliases":["mexico"],"filename":"1f1f2-1f1fd","category":"flags","batch":"4"},{"aliases":["micronesia"],"filename":"1f1eb-1f1f2","category":"flags","batch":"4"},{"aliases":["moldova"],"filename":"1f1f2-1f1e9","category":"flags","batch":"4"},{"aliases":["monaco"],"filename":"1f1f2-1f1e8","category":"flags","batch":"4"},{"aliases":["mongolia"],"filename":"1f1f2-1f1f3","category":"flags","batch":"4"},{"aliases":["montenegro"],"filename":"1f1f2-1f1ea","category":"flags","batch":"4"},{"aliases":["montserrat"],"filename":"1f1f2-1f1f8","category":"flags","batch":"4"},{"aliases":["morocco"],"filename":"1f1f2-1f1e6","category":"flags","batch":"4"},{"aliases":["mozambique"],"filename":"1f1f2-1f1ff","category":"flags","batch":"4"},{"aliases":["myanmar"],"filename":"1f1f2-1f1f2","category":"flags","batch":"4"},{"aliases":["namibia"],"filename":"1f1f3-1f1e6","category":"flags","batch":"4"},{"aliases":["nauru"],"filename":"1f1f3-1f1f7","category":"flags","batch":"4"},{"aliases":["nepal"],"filename":"1f1f3-1f1f5","category":"flags","batch":"4"},{"aliases":["netherlands"],"filename":"1f1f3-1f1f1","category":"flags","batch":"4"},{"aliases":["new_caledonia"],"filename":"1f1f3-1f1e8","category":"flags","batch":"4"},{"aliases":["new_zealand"],"filename":"1f1f3-1f1ff","category":"flags","batch":"4"},{"aliases":["nicaragua"],"filename":"1f1f3-1f1ee","category":"flags","batch":"4"},{"aliases":["niger"],"filename":"1f1f3-1f1ea","category":"flags","batch":"4"},{"aliases":["nigeria"],"filename":"1f1f3-1f1ec","category":"flags","batch":"4"},{"aliases":["niue"],"filename":"1f1f3-1f1fa","category":"flags","batch":"4"},{"aliases":["norfolk_island"],"filename":"1f1f3-1f1eb","category":"flags","batch":"4"},{"aliases":["northern_mariana_islands"],"filename":"1f1f2-1f1f5","category":"flags","batch":"4"},{"aliases":["north_korea"],"filename":"1f1f0-1f1f5","category":"flags","batch":"4"},{"aliases":["norway"],"filename":"1f1f3-1f1f4","category":"flags","batch":"4"},{"aliases":["oman"],"filename":"1f1f4-1f1f2","category":"flags","batch":"4"},{"aliases":["pakistan","pk"],"filename":"1f1f5-1f1f0","category":"flags","batch":"4"},{"aliases":["palau"],"filename":"1f1f5-1f1fc","category":"flags","batch":"4"},{"aliases":["palestinian_territories"],"filename":"1f1f5-1f1f8","category":"flags","batch":"4"},{"aliases":["panama"],"filename":"1f1f5-1f1e6","category":"flags","batch":"4"},{"aliases":["papua_new_guinea"],"filename":"1f1f5-1f1ec","category":"flags","batch":"4"},{"aliases":["paraguay"],"filename":"1f1f5-1f1fe","category":"flags","batch":"4"},{"aliases":["peru"],"filename":"1f1f5-1f1ea","category":"flags","batch":"4"},{"aliases":["philippines"],"filename":"1f1f5-1f1ed","category":"flags","batch":"5"},{"aliases":["pitcairn_islands"],"filename":"1f1f5-1f1f3","category":"flags","batch":"5"},{"aliases":["poland"],"filename":"1f1f5-1f1f1","category":"flags","batch":"5"},{"aliases":["portugal"],"filename":"1f1f5-1f1f9","category":"flags","batch":"5"},{"aliases":["puerto_rico"],"filename":"1f1f5-1f1f7","category":"flags","batch":"5"},{"aliases":["qatar"],"filename":"1f1f6-1f1e6","category":"flags","batch":"5"},{"aliases":["reunion"],"filename":"1f1f7-1f1ea","category":"flags","batch":"5"},{"aliases":["romania"],"filename":"1f1f7-1f1f4","category":"flags","batch":"5"},{"aliases":["ru"],"filename":"1f1f7-1f1fa","category":"flags","batch":"5"},{"aliases":["rwanda"],"filename":"1f1f7-1f1fc","category":"flags","batch":"5"},{"aliases":["st_barthelemy"],"filename":"1f1e7-1f1f1","category":"flags","batch":"5"},{"aliases":["st_helena"],"filename":"1f1f8-1f1ed","category":"flags","batch":"5"},{"aliases":["st_kitts_nevis"],"filename":"1f1f0-1f1f3","category":"flags","batch":"5"},{"aliases":["st_lucia"],"filename":"1f1f1-1f1e8","category":"flags","batch":"5"},{"aliases":["st_pierre_miquelon"],"filename":"1f1f5-1f1f2","category":"flags","batch":"5"},{"aliases":["st_vincent_grenadines"],"filename":"1f1fb-1f1e8","category":"flags","batch":"5"},{"aliases":["samoa"],"filename":"1f1fc-1f1f8","category":"flags","batch":"5"},{"aliases":["san_marino"],"filename":"1f1f8-1f1f2","category":"flags","batch":"5"},{"aliases":["sao_tome_principe"],"filename":"1f1f8-1f1f9","category":"flags","batch":"5"},{"aliases":["saudi_arabia"],"filename":"1f1f8-1f1e6","category":"flags","batch":"5"},{"aliases":["senegal"],"filename":"1f1f8-1f1f3","category":"flags","batch":"5"},{"aliases":["serbia"],"filename":"1f1f7-1f1f8","category":"flags","batch":"5"},{"aliases":["seychelles"],"filename":"1f1f8-1f1e8","category":"flags","batch":"5"},{"aliases":["sierra_leone"],"filename":"1f1f8-1f1f1","category":"flags","batch":"5"},{"aliases":["singapore"],"filename":"1f1f8-1f1ec","category":"flags","batch":"5"},{"aliases":["sint_maarten"],"filename":"1f1f8-1f1fd","category":"flags","batch":"5"},{"aliases":["slovakia"],"filename":"1f1f8-1f1f0","category":"flags","batch":"5"},{"aliases":["slovenia"],"filename":"1f1f8-1f1ee","category":"flags","batch":"5"},{"aliases":["solomon_islands"],"filename":"1f1f8-1f1e7","category":"flags","batch":"5"},{"aliases":["somalia"],"filename":"1f1f8-1f1f4","category":"flags","batch":"5"},{"aliases":["south_africa","za"],"filename":"1f1ff-1f1e6","category":"flags","batch":"5"},{"aliases":["south_georgia_south_sandwich_islands"],"filename":"1f1ec-1f1f8","category":"flags","batch":"5"},{"aliases":["kr"],"filename":"1f1f0-1f1f7","category":"flags","batch":"5"},{"aliases":["south_sudan"],"filename":"1f1f8-1f1f8","category":"flags","batch":"5"},{"aliases":["es"],"filename":"1f1ea-1f1f8","category":"flags","batch":"5"},{"aliases":["sri_lanka"],"filename":"1f1f1-1f1f0","category":"flags","batch":"5"},{"aliases":["sudan"],"filename":"1f1f8-1f1e9","category":"flags","batch":"5"},{"aliases":["suriname"],"filename":"1f1f8-1f1f7","category":"flags","batch":"5"},{"aliases":["swaziland"],"filename":"1f1f8-1f1ff","category":"flags","batch":"5"},{"aliases":["sweden"],"filename":"1f1f8-1f1ea","category":"flags","batch":"5"},{"aliases":["switzerland"],"filename":"1f1e8-1f1ed","category":"flags","batch":"5"},{"aliases":["syria"],"filename":"1f1f8-1f1fe","category":"flags","batch":"5"},{"aliases":["taiwan"],"filename":"1f1f9-1f1fc","category":"flags","batch":"5"},{"aliases":["tajikistan"],"filename":"1f1f9-1f1ef","category":"flags","batch":"5"},{"aliases":["tanzania"],"filename":"1f1f9-1f1ff","category":"flags","batch":"5"},{"aliases":["thailand"],"filename":"1f1f9-1f1ed","category":"flags","batch":"6"},{"aliases":["timor_leste"],"filename":"1f1f9-1f1f1","category":"flags","batch":"6"},{"aliases":["togo"],"filename":"1f1f9-1f1ec","category":"flags","batch":"6"},{"aliases":["tokelau"],"filename":"1f1f9-1f1f0","category":"flags","batch":"6"},{"aliases":["tonga"],"filename":"1f1f9-1f1f4","category":"flags","batch":"6"},{"aliases":["trinidad_tobago"],"filename":"1f1f9-1f1f9","category":"flags","batch":"6"},{"aliases":["tunisia"],"filename":"1f1f9-1f1f3","category":"flags","batch":"6"},{"aliases":["tr"],"filename":"1f1f9-1f1f7","category":"flags","batch":"6"},{"aliases":["turkmenistan"],"filename":"1f1f9-1f1f2","category":"flags","batch":"6"},{"aliases":["turks_caicos_islands"],"filename":"1f1f9-1f1e8","category":"flags","batch":"6"},{"aliases":["tuvalu"],"filename":"1f1f9-1f1fb","category":"flags","batch":"6"},{"aliases":["uganda"],"filename":"1f1fa-1f1ec","category":"flags","batch":"6"},{"aliases":["ukraine"],"filename":"1f1fa-1f1e6","category":"flags","batch":"6"},{"aliases":["united_arab_emirates"],"filename":"1f1e6-1f1ea","category":"flags","batch":"6"},{"aliases":["gb","uk"],"filename":"1f1ec-1f1e7","category":"flags","batch":"6"},{"aliases":["us"],"filename":"1f1fa-1f1f8","category":"flags","batch":"6"},{"aliases":["us_virgin_islands"],"filename":"1f1fb-1f1ee","category":"flags","batch":"6"},{"aliases":["uruguay"],"filename":"1f1fa-1f1fe","category":"flags","batch":"6"},{"aliases":["uzbekistan"],"filename":"1f1fa-1f1ff","category":"flags","batch":"6"},{"aliases":["vanuatu"],"filename":"1f1fb-1f1fa","category":"flags","batch":"6"},{"aliases":["vatican_city"],"filename":"1f1fb-1f1e6","category":"flags","batch":"6"},{"aliases":["venezuela"],"filename":"1f1fb-1f1ea","category":"flags","batch":"6"},{"aliases":["vietnam"],"filename":"1f1fb-1f1f3","category":"flags","batch":"6"},{"aliases":["wallis_futuna"],"filename":"1f1fc-1f1eb","category":"flags","batch":"6"},{"aliases":["western_sahara"],"filename":"1f1ea-1f1ed","category":"flags","batch":"6"},{"aliases":["yemen"],"filename":"1f1fe-1f1ea","category":"flags","batch":"6"},{"aliases":["zambia"],"filename":"1f1ff-1f1f2","category":"flags","batch":"6"},{"aliases":["zimbabwe"],"filename":"1f1ff-1f1fc","category":"flags","batch":"6"},{"aliases":["bowtie"],"filename":"bowtie","category":"custom","batch":"1"},{"aliases":["feelsgood"],"filename":"feelsgood","category":"custom","batch":"1"},{"aliases":["finnadie"],"filename":"finnadie","category":"custom","batch":"1"},{"aliases":["goberserk"],"filename":"goberserk","category":"custom","batch":"1"},{"aliases":["godmode"],"filename":"godmode","category":"custom","batch":"1"},{"aliases":["hurtrealbad"],"filename":"hurtrealbad","category":"custom","batch":"1"},{"aliases":["neckbeard"],"filename":"neckbeard","category":"custom","batch":"1"},{"aliases":["octocat"],"filename":"octocat","category":"custom","batch":"1"},{"aliases":["rage1"],"filename":"rage1","category":"custom","batch":"1"},{"aliases":["rage2"],"filename":"rage2","category":"custom","batch":"1"},{"aliases":["rage3"],"filename":"rage3","category":"custom","batch":"1"},{"aliases":["rage4"],"filename":"rage4","category":"custom","batch":"1"},{"aliases":["shipit","squirrel"],"filename":"shipit","category":"custom","batch":"1"},{"aliases":["suspect"],"filename":"suspect","category":"custom","batch":"1"},{"aliases":["trollface"],"filename":"trollface","category":"custom","batch":"1"},{"aliases":["mattermost"],"filename":"mattermost","category":"custom","batch":"1"},{"aliases":["open_hands_light_skin_tone"],"filename":"1f450-1f3fb","category":"skintone","batch":"1"},{"aliases":["open_hands_medium_light_skin_tone"],"filename":"1f450-1f3fc","category":"skintone","batch":"1"},{"aliases":["open_hands_medium_skin_tone"],"filename":"1f450-1f3fd","category":"skintone","batch":"1"},{"aliases":["open_hands_medium_dark_skin_tone"],"filename":"1f450-1f3fe","category":"skintone","batch":"1"},{"aliases":["open_hands_dark_skin_tone"],"filename":"1f450-1f3ff","category":"skintone","batch":"1"},{"aliases":["raised_hands_light_skin_tone"],"filename":"1f64c-1f3fb","category":"skintone","batch":"1"},{"aliases":["raised_hands_medium_light_skin_tone"],"filename":"1f64c-1f3fc","category":"skintone","batch":"1"},{"aliases":["raised_hands_medium_skin_tone"],"filename":"1f64c-1f3fd","category":"skintone","batch":"1"},{"aliases":["raised_hands_medium_dark_skin_tone"],"filename":"1f64c-1f3fe","category":"skintone","batch":"1"},{"aliases":["raised_hands_dark_skin_tone"],"filename":"1f64c-1f3ff","category":"skintone","batch":"1"},{"aliases":["clap_light_skin_tone"],"filename":"1f44f-1f3fb","category":"skintone","batch":"1"},{"aliases":["clap_medium_light_skin_tone"],"filename":"1f44f-1f3fc","category":"skintone","batch":"1"},{"aliases":["clap_medium_skin_tone"],"filename":"1f44f-1f3fd","category":"skintone","batch":"1"},{"aliases":["clap_medium_dark_skin_tone"],"filename":"1f44f-1f3fe","category":"skintone","batch":"1"},{"aliases":["clap_dark_skin_tone"],"filename":"1f44f-1f3ff","category":"skintone","batch":"1"},{"aliases":["pray_light_skin_tone"],"filename":"1f64f-1f3fb","category":"skintone","batch":"1"},{"aliases":["pray_medium_light_skin_tone"],"filename":"1f64f-1f3fc","category":"skintone","batch":"1"},{"aliases":["pray_medium_skin_tone"],"filename":"1f64f-1f3fd","category":"skintone","batch":"1"},{"aliases":["pray_medium_dark_skin_tone"],"filename":"1f64f-1f3fe","category":"skintone","batch":"1"},{"aliases":["pray_dark_skin_tone"],"filename":"1f64f-1f3ff","category":"skintone","batch":"1"},{"aliases":["+1_light_skin_tone"],"filename":"1f44d-1f3fb","category":"skintone","batch":"1"},{"aliases":["+1_medium_light_skin_tone"],"filename":"1f44d-1f3fc","category":"skintone","batch":"1"},{"aliases":["+1_medium_skin_tone"],"filename":"1f44d-1f3fd","category":"skintone","batch":"1"},{"aliases":["+1_medium_dark_skin_tone"],"filename":"1f44d-1f3fe","category":"skintone","batch":"1"},{"aliases":["+1_dark_skin_tone"],"filename":"1f44d-1f3ff","category":"skintone","batch":"1"},{"aliases":["-1_light_skin_tone"],"filename":"1f44e-1f3fb","category":"skintone","batch":"1"},{"aliases":["-1_medium_light_skin_tone"],"filename":"1f44e-1f3fc","category":"skintone","batch":"1"},{"aliases":["-1_medium_skin_tone"],"filename":"1f44e-1f3fd","category":"skintone","batch":"1"},{"aliases":["-1_medium_dark_skin_tone"],"filename":"1f44e-1f3fe","category":"skintone","batch":"1"},{"aliases":["-1_dark_skin_tone"],"filename":"1f44e-1f3ff","category":"skintone","batch":"1"},{"aliases":["fist_oncoming_light_skin_tone"],"filename":"1f44a-1f3fb","category":"skintone","batch":"1"},{"aliases":["fist_oncoming_medium_light_skin_tone"],"filename":"1f44a-1f3fc","category":"skintone","batch":"1"},{"aliases":["fist_oncoming_medium_skin_tone"],"filename":"1f44a-1f3fd","category":"skintone","batch":"1"},{"aliases":["fist_oncoming_medium_dark_skin_tone"],"filename":"1f44a-1f3fe","category":"skintone","batch":"1"},{"aliases":["fist_oncoming_dark_skin_tone"],"filename":"1f44a-1f3ff","category":"skintone","batch":"1"},{"aliases":["fist_raised_light_skin_tone"],"filename":"270a-1f3fb","category":"skintone","batch":"1"},{"aliases":["fist_raised_medium_light_skin_tone"],"filename":"270a-1f3fc","category":"skintone","batch":"1"},{"aliases":["fist_raised_medium_skin_tone"],"filename":"270a-1f3fd","category":"skintone","batch":"1"},{"aliases":["fist_raised_medium_dark_skin_tone"],"filename":"270a-1f3fe","category":"skintone","batch":"1"},{"aliases":["fist_raised_dark_skin_tone"],"filename":"270a-1f3ff","category":"skintone","batch":"1"},{"aliases":["fist_left_light_skin_tone"],"filename":"1f91b-1f3fb","category":"skintone","batch":"1"},{"aliases":["fist_left_medium_light_skin_tone"],"filename":"1f91b-1f3fc","category":"skintone","batch":"1"},{"aliases":["fist_left_medium_skin_tone"],"filename":"1f91b-1f3fd","category":"skintone","batch":"1"},{"aliases":["fist_left_medium_dark_skin_tone"],"filename":"1f91b-1f3fe","category":"skintone","batch":"1"},{"aliases":["fist_left_dark_skin_tone"],"filename":"1f91b-1f3ff","category":"skintone","batch":"1"},{"aliases":["fist_right_light_skin_tone"],"filename":"1f91c-1f3fb","category":"skintone","batch":"2"},{"aliases":["fist_right_medium_light_skin_tone"],"filename":"1f91c-1f3fc","category":"skintone","batch":"2"},{"aliases":["fist_right_medium_skin_tone"],"filename":"1f91c-1f3fd","category":"skintone","batch":"2"},{"aliases":["fist_right_medium_dark_skin_tone"],"filename":"1f91c-1f3fe","category":"skintone","batch":"2"},{"aliases":["fist_right_dark_skin_tone"],"filename":"1f91c-1f3ff","category":"skintone","batch":"2"},{"aliases":["crossed_fingers_light_skin_tone"],"filename":"1f91e-1f3fb","category":"skintone","batch":"2"},{"aliases":["crossed_fingers_medium_light_skin_tone"],"filename":"1f91e-1f3fc","category":"skintone","batch":"2"},{"aliases":["crossed_fingers_medium_skin_tone"],"filename":"1f91e-1f3fd","category":"skintone","batch":"2"},{"aliases":["crossed_fingers_medium_dark_skin_tone"],"filename":"1f91e-1f3fe","category":"skintone","batch":"2"},{"aliases":["crossed_fingers_dark_skin_tone"],"filename":"1f91e-1f3ff","category":"skintone","batch":"2"},{"aliases":["v_light_skin_tone"],"filename":"270c-1f3fb","category":"skintone","batch":"2"},{"aliases":["v_medium_light_skin_tone"],"filename":"270c-1f3fc","category":"skintone","batch":"2"},{"aliases":["v_medium_skin_tone"],"filename":"270c-1f3fd","category":"skintone","batch":"2"},{"aliases":["v_medium_dark_skin_tone"],"filename":"270c-1f3fe","category":"skintone","batch":"2"},{"aliases":["v_dark_skin_tone"],"filename":"270c-1f3ff","category":"skintone","batch":"2"},{"aliases":["metal_light_skin_tone"],"filename":"1f918-1f3fb","category":"skintone","batch":"2"},{"aliases":["metal_medium_light_skin_tone"],"filename":"1f918-1f3fc","category":"skintone","batch":"2"},{"aliases":["metal_medium_skin_tone"],"filename":"1f918-1f3fd","category":"skintone","batch":"2"},{"aliases":["metal_medium_dark_skin_tone"],"filename":"1f918-1f3fe","category":"skintone","batch":"2"},{"aliases":["metal_dark_skin_tone"],"filename":"1f918-1f3ff","category":"skintone","batch":"2"},{"aliases":["ok_hand_light_skin_tone"],"filename":"1f44c-1f3fb","category":"skintone","batch":"2"},{"aliases":["ok_hand_medium_light_skin_tone"],"filename":"1f44c-1f3fc","category":"skintone","batch":"2"},{"aliases":["ok_hand_medium_skin_tone"],"filename":"1f44c-1f3fd","category":"skintone","batch":"2"},{"aliases":["ok_hand_medium_dark_skin_tone"],"filename":"1f44c-1f3fe","category":"skintone","batch":"2"},{"aliases":["ok_hand_dark_skin_tone"],"filename":"1f44c-1f3ff","category":"skintone","batch":"2"},{"aliases":["point_left_light_skin_tone"],"filename":"1f448-1f3fb","category":"skintone","batch":"2"},{"aliases":["point_left_medium_light_skin_tone"],"filename":"1f448-1f3fc","category":"skintone","batch":"2"},{"aliases":["point_left_medium_skin_tone"],"filename":"1f448-1f3fd","category":"skintone","batch":"2"},{"aliases":["point_left_medium_dark_skin_tone"],"filename":"1f448-1f3fe","category":"skintone","batch":"2"},{"aliases":["point_left_dark_skin_tone"],"filename":"1f448-1f3ff","category":"skintone","batch":"2"},{"aliases":["point_right_light_skin_tone"],"filename":"1f449-1f3fb","category":"skintone","batch":"2"},{"aliases":["point_right_medium_light_skin_tone"],"filename":"1f449-1f3fc","category":"skintone","batch":"2"},{"aliases":["point_right_medium_skin_tone"],"filename":"1f449-1f3fd","category":"skintone","batch":"2"},{"aliases":["point_right_medium_dark_skin_tone"],"filename":"1f449-1f3fe","category":"skintone","batch":"2"},{"aliases":["point_right_dark_skin_tone"],"filename":"1f449-1f3ff","category":"skintone","batch":"2"},{"aliases":["point_up_2_light_skin_tone"],"filename":"1f446-1f3fb","category":"skintone","batch":"2"},{"aliases":["point_up_2_medium_light_skin_tone"],"filename":"1f446-1f3fc","category":"skintone","batch":"2"},{"aliases":["point_up_2_medium_skin_tone"],"filename":"1f446-1f3fd","category":"skintone","batch":"2"},{"aliases":["point_up_2_medium_dark_skin_tone"],"filename":"1f446-1f3fe","category":"skintone","batch":"2"},{"aliases":["point_up_2_dark_skin_tone"],"filename":"1f446-1f3ff","category":"skintone","batch":"2"},{"aliases":["point_down_light_skin_tone"],"filename":"1f447-1f3fb","category":"skintone","batch":"2"},{"aliases":["point_down_medium_light_skin_tone"],"filename":"1f447-1f3fc","category":"skintone","batch":"2"},{"aliases":["point_down_medium_skin_tone"],"filename":"1f447-1f3fd","category":"skintone","batch":"2"},{"aliases":["point_down_medium_dark_skin_tone"],"filename":"1f447-1f3fe","category":"skintone","batch":"2"},{"aliases":["point_down_dark_skin_tone"],"filename":"1f447-1f3ff","category":"skintone","batch":"2"},{"aliases":["point_up_light_skin_tone"],"filename":"261d-1f3fb","category":"skintone","batch":"3"},{"aliases":["point_up_medium_light_skin_tone"],"filename":"261d-1f3fc","category":"skintone","batch":"3"},{"aliases":["point_up_medium_skin_tone"],"filename":"261d-1f3fd","category":"skintone","batch":"3"},{"aliases":["point_up_medium_dark_skin_tone"],"filename":"261d-1f3fe","category":"skintone","batch":"3"},{"aliases":["point_up_dark_skin_tone"],"filename":"261d-1f3ff","category":"skintone","batch":"3"},{"aliases":["hand_light_skin_tone"],"filename":"270b-1f3fb","category":"skintone","batch":"3"},{"aliases":["hand_medium_light_skin_tone"],"filename":"270b-1f3fc","category":"skintone","batch":"3"},{"aliases":["hand_medium_skin_tone"],"filename":"270b-1f3fd","category":"skintone","batch":"3"},{"aliases":["hand_medium_dark_skin_tone"],"filename":"270b-1f3fe","category":"skintone","batch":"3"},{"aliases":["hand_dark_skin_tone"],"filename":"270b-1f3ff","category":"skintone","batch":"3"},{"aliases":["raised_back_of_hand_light_skin_tone"],"filename":"1f91a-1f3fb","category":"skintone","batch":"3"},{"aliases":["raised_back_of_hand_medium_light_skin_tone"],"filename":"1f91a-1f3fc","category":"skintone","batch":"3"},{"aliases":["raised_back_of_hand_medium_skin_tone"],"filename":"1f91a-1f3fd","category":"skintone","batch":"3"},{"aliases":["raised_back_of_hand_medium_dark_skin_tone"],"filename":"1f91a-1f3fe","category":"skintone","batch":"3"},{"aliases":["raised_back_of_hand_dark_skin_tone"],"filename":"1f91a-1f3ff","category":"skintone","batch":"3"},{"aliases":["raised_hand_with_fingers_splayed_light_skin_tone"],"filename":"1f590-1f3fb","category":"skintone","batch":"3"},{"aliases":["raised_hand_with_fingers_splayed_medium_light_skin_tone"],"filename":"1f590-1f3fc","category":"skintone","batch":"3"},{"aliases":["raised_hand_with_fingers_splayed_medium_skin_tone"],"filename":"1f590-1f3fd","category":"skintone","batch":"3"},{"aliases":["raised_hand_with_fingers_splayed_medium_dark_skin_tone"],"filename":"1f590-1f3fe","category":"skintone","batch":"3"},{"aliases":["raised_hand_with_fingers_splayed_dark_skin_tone"],"filename":"1f590-1f3ff","category":"skintone","batch":"3"},{"aliases":["vulcan_salute_light_skin_tone"],"filename":"1f596-1f3fb","category":"skintone","batch":"3"},{"aliases":["vulcan_salute_medium_light_skin_tone"],"filename":"1f596-1f3fc","category":"skintone","batch":"3"},{"aliases":["vulcan_salute_medium_skin_tone"],"filename":"1f596-1f3fd","category":"skintone","batch":"3"},{"aliases":["vulcan_salute_medium_dark_skin_tone"],"filename":"1f596-1f3fe","category":"skintone","batch":"3"},{"aliases":["vulcan_salute_dark_skin_tone"],"filename":"1f596-1f3ff","category":"skintone","batch":"3"},{"aliases":["wave_light_skin_tone"],"filename":"1f44b-1f3fb","category":"skintone","batch":"3"},{"aliases":["wave_medium_light_skin_tone"],"filename":"1f44b-1f3fc","category":"skintone","batch":"3"},{"aliases":["wave_medium_skin_tone"],"filename":"1f44b-1f3fd","category":"skintone","batch":"3"},{"aliases":["wave_medium_dark_skin_tone"],"filename":"1f44b-1f3fe","category":"skintone","batch":"3"},{"aliases":["wave_dark_skin_tone"],"filename":"1f44b-1f3ff","category":"skintone","batch":"3"},{"aliases":["call_me_hand_light_skin_tone"],"filename":"1f919-1f3fb","category":"skintone","batch":"3"},{"aliases":["call_me_hand_medium_light_skin_tone"],"filename":"1f919-1f3fc","category":"skintone","batch":"3"},{"aliases":["call_me_hand_medium_skin_tone"],"filename":"1f919-1f3fd","category":"skintone","batch":"3"},{"aliases":["call_me_hand_medium_dark_skin_tone"],"filename":"1f919-1f3fe","category":"skintone","batch":"3"},{"aliases":["call_me_hand_dark_skin_tone"],"filename":"1f919-1f3ff","category":"skintone","batch":"3"},{"aliases":["muscle_light_skin_tone"],"filename":"1f4aa-1f3fb","category":"skintone","batch":"3"},{"aliases":["muscle_medium_light_skin_tone"],"filename":"1f4aa-1f3fc","category":"skintone","batch":"3"},{"aliases":["muscle_medium_skin_tone"],"filename":"1f4aa-1f3fd","category":"skintone","batch":"3"},{"aliases":["muscle_medium_dark_skin_tone"],"filename":"1f4aa-1f3fe","category":"skintone","batch":"3"},{"aliases":["muscle_dark_skin_tone"],"filename":"1f4aa-1f3ff","category":"skintone","batch":"3"},{"aliases":["middle_finger_light_skin_tone"],"filename":"1f595-1f3fb","category":"skintone","batch":"3"},{"aliases":["middle_finger_medium_light_skin_tone"],"filename":"1f595-1f3fc","category":"skintone","batch":"3"},{"aliases":["middle_finger_medium_skin_tone"],"filename":"1f595-1f3fd","category":"skintone","batch":"3"},{"aliases":["middle_finger_medium_dark_skin_tone"],"filename":"1f595-1f3fe","category":"skintone","batch":"3"},{"aliases":["middle_finger_dark_skin_tone"],"filename":"1f595-1f3ff","category":"skintone","batch":"3"},{"aliases":["writing_hand_light_skin_tone"],"filename":"270d-1f3fb","category":"skintone","batch":"4"},{"aliases":["writing_hand_medium_light_skin_tone"],"filename":"270d-1f3fc","category":"skintone","batch":"4"},{"aliases":["writing_hand_medium_skin_tone"],"filename":"270d-1f3fd","category":"skintone","batch":"4"},{"aliases":["writing_hand_medium_dark_skin_tone"],"filename":"270d-1f3fe","category":"skintone","batch":"4"},{"aliases":["writing_hand_dark_skin_tone"],"filename":"270d-1f3ff","category":"skintone","batch":"4"},{"aliases":["selfie_light_skin_tone"],"filename":"1f933-1f3fb","category":"skintone","batch":"4"},{"aliases":["selfie_medium_light_skin_tone"],"filename":"1f933-1f3fc","category":"skintone","batch":"4"},{"aliases":["selfie_medium_skin_tone"],"filename":"1f933-1f3fd","category":"skintone","batch":"4"},{"aliases":["selfie_medium_dark_skin_tone"],"filename":"1f933-1f3fe","category":"skintone","batch":"4"},{"aliases":["selfie_dark_skin_tone"],"filename":"1f933-1f3ff","category":"skintone","batch":"4"},{"aliases":["nail_care_light_skin_tone"],"filename":"1f485-1f3fb","category":"skintone","batch":"4"},{"aliases":["nail_care_medium_light_skin_tone"],"filename":"1f485-1f3fc","category":"skintone","batch":"4"},{"aliases":["nail_care_medium_skin_tone"],"filename":"1f485-1f3fd","category":"skintone","batch":"4"},{"aliases":["nail_care_medium_dark_skin_tone"],"filename":"1f485-1f3fe","category":"skintone","batch":"4"},{"aliases":["nail_care_dark_skin_tone"],"filename":"1f485-1f3ff","category":"skintone","batch":"4"},{"aliases":["ear_light_skin_tone"],"filename":"1f442-1f3fb","category":"skintone","batch":"4"},{"aliases":["ear_medium_light_skin_tone"],"filename":"1f442-1f3fc","category":"skintone","batch":"4"},{"aliases":["ear_medium_skin_tone"],"filename":"1f442-1f3fd","category":"skintone","batch":"4"},{"aliases":["ear_medium_dark_skin_tone"],"filename":"1f442-1f3fe","category":"skintone","batch":"4"},{"aliases":["ear_dark_skin_tone"],"filename":"1f442-1f3ff","category":"skintone","batch":"4"},{"aliases":["nose_light_skin_tone"],"filename":"1f443-1f3fb","category":"skintone","batch":"4"},{"aliases":["nose_medium_light_skin_tone"],"filename":"1f443-1f3fc","category":"skintone","batch":"4"},{"aliases":["nose_medium_skin_tone"],"filename":"1f443-1f3fd","category":"skintone","batch":"4"},{"aliases":["nose_medium_dark_skin_tone"],"filename":"1f443-1f3fe","category":"skintone","batch":"4"},{"aliases":["nose_dark_skin_tone"],"filename":"1f443-1f3ff","category":"skintone","batch":"4"},{"aliases":["baby_light_skin_tone"],"filename":"1f476-1f3fb","category":"skintone","batch":"4"},{"aliases":["baby_medium_light_skin_tone"],"filename":"1f476-1f3fc","category":"skintone","batch":"4"},{"aliases":["baby_medium_skin_tone"],"filename":"1f476-1f3fd","category":"skintone","batch":"4"},{"aliases":["baby_medium_dark_skin_tone"],"filename":"1f476-1f3fe","category":"skintone","batch":"4"},{"aliases":["baby_dark_skin_tone"],"filename":"1f476-1f3ff","category":"skintone","batch":"4"},{"aliases":["boy_light_skin_tone"],"filename":"1f466-1f3fb","category":"skintone","batch":"4"},{"aliases":["boy_medium_light_skin_tone"],"filename":"1f466-1f3fc","category":"skintone","batch":"4"},{"aliases":["boy_medium_skin_tone"],"filename":"1f466-1f3fd","category":"skintone","batch":"4"},{"aliases":["boy_medium_dark_skin_tone"],"filename":"1f466-1f3fe","category":"skintone","batch":"4"},{"aliases":["boy_dark_skin_tone"],"filename":"1f466-1f3ff","category":"skintone","batch":"4"},{"aliases":["girl_light_skin_tone"],"filename":"1f467-1f3fb","category":"skintone","batch":"4"},{"aliases":["girl_medium_light_skin_tone"],"filename":"1f467-1f3fc","category":"skintone","batch":"4"},{"aliases":["girl_medium_skin_tone"],"filename":"1f467-1f3fd","category":"skintone","batch":"4"},{"aliases":["girl_medium_dark_skin_tone"],"filename":"1f467-1f3fe","category":"skintone","batch":"4"},{"aliases":["girl_dark_skin_tone"],"filename":"1f467-1f3ff","category":"skintone","batch":"4"},{"aliases":["man_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"4"},{"aliases":["man_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"4"},{"aliases":["man_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"4"},{"aliases":["man_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"4"},{"aliases":["man_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"4"},{"aliases":["woman_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"5"},{"aliases":["woman_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"5"},{"aliases":["woman_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"5"},{"aliases":["woman_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"5"},{"aliases":["woman_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"5"},{"aliases":["blonde_woman_light_skin_tone"],"filename":"1f471-1f3fb","category":"skintone","batch":"5"},{"aliases":["blonde_woman_medium_light_skin_tone"],"filename":"1f471-1f3fc","category":"skintone","batch":"5"},{"aliases":["blonde_woman_medium_skin_tone"],"filename":"1f471-1f3fd","category":"skintone","batch":"5"},{"aliases":["blonde_woman_medium_dark_skin_tone"],"filename":"1f471-1f3fe","category":"skintone","batch":"5"},{"aliases":["blonde_woman_dark_skin_tone"],"filename":"1f471-1f3ff","category":"skintone","batch":"5"},{"aliases":["blonde_woman_light_skin_tone"],"filename":"1f471-1f3fb-200d-2640-fe0f","category":"skintone","batch":"5"},{"aliases":["blonde_woman_medium_light_skin_tone"],"filename":"1f471-1f3fc-200d-2640-fe0f","category":"skintone","batch":"5"},{"aliases":["blonde_woman_medium_skin_tone"],"filename":"1f471-1f3fd-200d-2640-fe0f","category":"skintone","batch":"5"},{"aliases":["blonde_woman_medium_dark_skin_tone"],"filename":"1f471-1f3fe-200d-2640-fe0f","category":"skintone","batch":"5"},{"aliases":["blonde_woman_dark_skin_tone"],"filename":"1f471-1f3ff-200d-2640-fe0f","category":"skintone","batch":"5"},{"aliases":["blonde_man_light_skin_tone"],"filename":"1f471-1f3fb","category":"skintone","batch":"5"},{"aliases":["blonde_man_medium_light_skin_tone"],"filename":"1f471-1f3fc","category":"skintone","batch":"5"},{"aliases":["blonde_man_medium_skin_tone"],"filename":"1f471-1f3fd","category":"skintone","batch":"5"},{"aliases":["blonde_man_medium_dark_skin_tone"],"filename":"1f471-1f3fe","category":"skintone","batch":"5"},{"aliases":["blonde_man_dark_skin_tone"],"filename":"1f471-1f3ff","category":"skintone","batch":"5"},{"aliases":["blonde_man_light_skin_tone"],"filename":"1f471-1f3fb-200d-2640-fe0f","category":"skintone","batch":"5"},{"aliases":["blonde_man_medium_light_skin_tone"],"filename":"1f471-1f3fc-200d-2640-fe0f","category":"skintone","batch":"5"},{"aliases":["blonde_man_medium_skin_tone"],"filename":"1f471-1f3fd-200d-2640-fe0f","category":"skintone","batch":"5"},{"aliases":["blonde_man_medium_dark_skin_tone"],"filename":"1f471-1f3fe-200d-2640-fe0f","category":"skintone","batch":"5"},{"aliases":["blonde_man_dark_skin_tone"],"filename":"1f471-1f3ff-200d-2640-fe0f","category":"skintone","batch":"5"},{"aliases":["older_man_light_skin_tone"],"filename":"1f474-1f3fb","category":"skintone","batch":"5"},{"aliases":["older_man_medium_light_skin_tone"],"filename":"1f474-1f3fc","category":"skintone","batch":"5"},{"aliases":["older_man_medium_skin_tone"],"filename":"1f474-1f3fd","category":"skintone","batch":"5"},{"aliases":["older_man_medium_dark_skin_tone"],"filename":"1f474-1f3fe","category":"skintone","batch":"5"},{"aliases":["older_man_dark_skin_tone"],"filename":"1f474-1f3ff","category":"skintone","batch":"5"},{"aliases":["older_woman_light_skin_tone"],"filename":"1f475-1f3fb","category":"skintone","batch":"5"},{"aliases":["older_woman_medium_light_skin_tone"],"filename":"1f475-1f3fc","category":"skintone","batch":"5"},{"aliases":["older_woman_medium_skin_tone"],"filename":"1f475-1f3fd","category":"skintone","batch":"5"},{"aliases":["older_woman_medium_dark_skin_tone"],"filename":"1f475-1f3fe","category":"skintone","batch":"5"},{"aliases":["older_woman_dark_skin_tone"],"filename":"1f475-1f3ff","category":"skintone","batch":"5"},{"aliases":["man_with_gua_pi_mao_light_skin_tone"],"filename":"1f472-1f3fb","category":"skintone","batch":"5"},{"aliases":["man_with_gua_pi_mao_medium_light_skin_tone"],"filename":"1f472-1f3fc","category":"skintone","batch":"5"},{"aliases":["man_with_gua_pi_mao_medium_skin_tone"],"filename":"1f472-1f3fd","category":"skintone","batch":"5"},{"aliases":["man_with_gua_pi_mao_medium_dark_skin_tone"],"filename":"1f472-1f3fe","category":"skintone","batch":"5"},{"aliases":["man_with_gua_pi_mao_dark_skin_tone"],"filename":"1f472-1f3ff","category":"skintone","batch":"5"},{"aliases":["woman_with_turban_light_skin_tone"],"filename":"1f473-1f3fb","category":"skintone","batch":"5"},{"aliases":["woman_with_turban_medium_light_skin_tone"],"filename":"1f473-1f3fc","category":"skintone","batch":"5"},{"aliases":["woman_with_turban_medium_skin_tone"],"filename":"1f473-1f3fd","category":"skintone","batch":"5"},{"aliases":["woman_with_turban_medium_dark_skin_tone"],"filename":"1f473-1f3fe","category":"skintone","batch":"5"},{"aliases":["woman_with_turban_dark_skin_tone"],"filename":"1f473-1f3ff","category":"skintone","batch":"5"},{"aliases":["woman_with_turban_light_skin_tone"],"filename":"1f473-1f3fb-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["woman_with_turban_medium_light_skin_tone"],"filename":"1f473-1f3fc-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["woman_with_turban_medium_skin_tone"],"filename":"1f473-1f3fd-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["woman_with_turban_medium_dark_skin_tone"],"filename":"1f473-1f3fe-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["woman_with_turban_dark_skin_tone"],"filename":"1f473-1f3ff-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["man_with_turban_light_skin_tone"],"filename":"1f473-1f3fb","category":"skintone","batch":"6"},{"aliases":["man_with_turban_medium_light_skin_tone"],"filename":"1f473-1f3fc","category":"skintone","batch":"6"},{"aliases":["man_with_turban_medium_skin_tone"],"filename":"1f473-1f3fd","category":"skintone","batch":"6"},{"aliases":["man_with_turban_medium_dark_skin_tone"],"filename":"1f473-1f3fe","category":"skintone","batch":"6"},{"aliases":["man_with_turban_dark_skin_tone"],"filename":"1f473-1f3ff","category":"skintone","batch":"6"},{"aliases":["man_with_turban_light_skin_tone"],"filename":"1f473-1f3fb-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["man_with_turban_medium_light_skin_tone"],"filename":"1f473-1f3fc-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["man_with_turban_medium_skin_tone"],"filename":"1f473-1f3fd-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["man_with_turban_medium_dark_skin_tone"],"filename":"1f473-1f3fe-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["man_with_turban_dark_skin_tone"],"filename":"1f473-1f3ff-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["policewoman_light_skin_tone"],"filename":"1f46e-1f3fb","category":"skintone","batch":"6"},{"aliases":["policewoman_medium_light_skin_tone"],"filename":"1f46e-1f3fc","category":"skintone","batch":"6"},{"aliases":["policewoman_medium_skin_tone"],"filename":"1f46e-1f3fd","category":"skintone","batch":"6"},{"aliases":["policewoman_medium_dark_skin_tone"],"filename":"1f46e-1f3fe","category":"skintone","batch":"6"},{"aliases":["policewoman_dark_skin_tone"],"filename":"1f46e-1f3ff","category":"skintone","batch":"6"},{"aliases":["policewoman_light_skin_tone"],"filename":"1f46e-1f3fb-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["policewoman_medium_light_skin_tone"],"filename":"1f46e-1f3fc-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["policewoman_medium_skin_tone"],"filename":"1f46e-1f3fd-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["policewoman_medium_dark_skin_tone"],"filename":"1f46e-1f3fe-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["policewoman_dark_skin_tone"],"filename":"1f46e-1f3ff-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["policeman_light_skin_tone"],"filename":"1f46e-1f3fb","category":"skintone","batch":"6"},{"aliases":["policeman_medium_light_skin_tone"],"filename":"1f46e-1f3fc","category":"skintone","batch":"6"},{"aliases":["policeman_medium_skin_tone"],"filename":"1f46e-1f3fd","category":"skintone","batch":"6"},{"aliases":["policeman_medium_dark_skin_tone"],"filename":"1f46e-1f3fe","category":"skintone","batch":"6"},{"aliases":["policeman_dark_skin_tone"],"filename":"1f46e-1f3ff","category":"skintone","batch":"6"},{"aliases":["policeman_light_skin_tone"],"filename":"1f46e-1f3fb-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["policeman_medium_light_skin_tone"],"filename":"1f46e-1f3fc-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["policeman_medium_skin_tone"],"filename":"1f46e-1f3fd-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["policeman_medium_dark_skin_tone"],"filename":"1f46e-1f3fe-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["policeman_dark_skin_tone"],"filename":"1f46e-1f3ff-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["construction_worker_woman_light_skin_tone"],"filename":"1f477-1f3fb","category":"skintone","batch":"6"},{"aliases":["construction_worker_woman_medium_light_skin_tone"],"filename":"1f477-1f3fc","category":"skintone","batch":"6"},{"aliases":["construction_worker_woman_medium_skin_tone"],"filename":"1f477-1f3fd","category":"skintone","batch":"6"},{"aliases":["construction_worker_woman_medium_dark_skin_tone"],"filename":"1f477-1f3fe","category":"skintone","batch":"6"},{"aliases":["construction_worker_woman_dark_skin_tone"],"filename":"1f477-1f3ff","category":"skintone","batch":"6"},{"aliases":["construction_worker_woman_light_skin_tone"],"filename":"1f477-1f3fb-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["construction_worker_woman_medium_light_skin_tone"],"filename":"1f477-1f3fc-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["construction_worker_woman_medium_skin_tone"],"filename":"1f477-1f3fd-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["construction_worker_woman_medium_dark_skin_tone"],"filename":"1f477-1f3fe-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["construction_worker_woman_dark_skin_tone"],"filename":"1f477-1f3ff-200d-2640-fe0f","category":"skintone","batch":"6"},{"aliases":["construction_worker_man_light_skin_tone"],"filename":"1f477-1f3fb","category":"skintone","batch":"7"},{"aliases":["construction_worker_man_medium_light_skin_tone"],"filename":"1f477-1f3fc","category":"skintone","batch":"7"},{"aliases":["construction_worker_man_medium_skin_tone"],"filename":"1f477-1f3fd","category":"skintone","batch":"7"},{"aliases":["construction_worker_man_medium_dark_skin_tone"],"filename":"1f477-1f3fe","category":"skintone","batch":"7"},{"aliases":["construction_worker_man_dark_skin_tone"],"filename":"1f477-1f3ff","category":"skintone","batch":"7"},{"aliases":["construction_worker_man_light_skin_tone"],"filename":"1f477-1f3fb-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["construction_worker_man_medium_light_skin_tone"],"filename":"1f477-1f3fc-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["construction_worker_man_medium_skin_tone"],"filename":"1f477-1f3fd-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["construction_worker_man_medium_dark_skin_tone"],"filename":"1f477-1f3fe-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["construction_worker_man_dark_skin_tone"],"filename":"1f477-1f3ff-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["guardswoman_light_skin_tone"],"filename":"1f482-1f3fb","category":"skintone","batch":"7"},{"aliases":["guardswoman_medium_light_skin_tone"],"filename":"1f482-1f3fc","category":"skintone","batch":"7"},{"aliases":["guardswoman_medium_skin_tone"],"filename":"1f482-1f3fd","category":"skintone","batch":"7"},{"aliases":["guardswoman_medium_dark_skin_tone"],"filename":"1f482-1f3fe","category":"skintone","batch":"7"},{"aliases":["guardswoman_dark_skin_tone"],"filename":"1f482-1f3ff","category":"skintone","batch":"7"},{"aliases":["guardswoman_light_skin_tone"],"filename":"1f482-1f3fb-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["guardswoman_medium_light_skin_tone"],"filename":"1f482-1f3fc-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["guardswoman_medium_skin_tone"],"filename":"1f482-1f3fd-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["guardswoman_medium_dark_skin_tone"],"filename":"1f482-1f3fe-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["guardswoman_dark_skin_tone"],"filename":"1f482-1f3ff-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["guardsman_light_skin_tone"],"filename":"1f482-1f3fb","category":"skintone","batch":"7"},{"aliases":["guardsman_medium_light_skin_tone"],"filename":"1f482-1f3fc","category":"skintone","batch":"7"},{"aliases":["guardsman_medium_skin_tone"],"filename":"1f482-1f3fd","category":"skintone","batch":"7"},{"aliases":["guardsman_medium_dark_skin_tone"],"filename":"1f482-1f3fe","category":"skintone","batch":"7"},{"aliases":["guardsman_dark_skin_tone"],"filename":"1f482-1f3ff","category":"skintone","batch":"7"},{"aliases":["guardsman_light_skin_tone"],"filename":"1f482-1f3fb-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["guardsman_medium_light_skin_tone"],"filename":"1f482-1f3fc-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["guardsman_medium_skin_tone"],"filename":"1f482-1f3fd-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["guardsman_medium_dark_skin_tone"],"filename":"1f482-1f3fe-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["guardsman_dark_skin_tone"],"filename":"1f482-1f3ff-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["female_detective_light_skin_tone"],"filename":"1f575-1f3fb","category":"skintone","batch":"7"},{"aliases":["female_detective_medium_light_skin_tone"],"filename":"1f575-1f3fc","category":"skintone","batch":"7"},{"aliases":["female_detective_medium_skin_tone"],"filename":"1f575-1f3fd","category":"skintone","batch":"7"},{"aliases":["female_detective_medium_dark_skin_tone"],"filename":"1f575-1f3fe","category":"skintone","batch":"7"},{"aliases":["female_detective_dark_skin_tone"],"filename":"1f575-1f3ff","category":"skintone","batch":"7"},{"aliases":["female_detective_light_skin_tone"],"filename":"1f575-1f3fb-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["female_detective_medium_light_skin_tone"],"filename":"1f575-1f3fc-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["female_detective_medium_skin_tone"],"filename":"1f575-1f3fd-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["female_detective_medium_dark_skin_tone"],"filename":"1f575-1f3fe-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["female_detective_dark_skin_tone"],"filename":"1f575-1f3ff-200d-2640-fe0f","category":"skintone","batch":"7"},{"aliases":["male_detective_light_skin_tone"],"filename":"1f575-1f3fb","category":"skintone","batch":"7"},{"aliases":["male_detective_medium_light_skin_tone"],"filename":"1f575-1f3fc","category":"skintone","batch":"7"},{"aliases":["male_detective_medium_skin_tone"],"filename":"1f575-1f3fd","category":"skintone","batch":"7"},{"aliases":["male_detective_medium_dark_skin_tone"],"filename":"1f575-1f3fe","category":"skintone","batch":"7"},{"aliases":["male_detective_dark_skin_tone"],"filename":"1f575-1f3ff","category":"skintone","batch":"7"},{"aliases":["male_detective_light_skin_tone"],"filename":"1f575-1f3fb-200d-2640-fe0f","category":"skintone","batch":"8"},{"aliases":["male_detective_medium_light_skin_tone"],"filename":"1f575-1f3fc-200d-2640-fe0f","category":"skintone","batch":"8"},{"aliases":["male_detective_medium_skin_tone"],"filename":"1f575-1f3fd-200d-2640-fe0f","category":"skintone","batch":"8"},{"aliases":["male_detective_medium_dark_skin_tone"],"filename":"1f575-1f3fe-200d-2640-fe0f","category":"skintone","batch":"8"},{"aliases":["male_detective_dark_skin_tone"],"filename":"1f575-1f3ff-200d-2640-fe0f","category":"skintone","batch":"8"},{"aliases":["woman_health_worker_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"8"},{"aliases":["woman_health_worker_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"8"},{"aliases":["woman_health_worker_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"8"},{"aliases":["woman_health_worker_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"8"},{"aliases":["woman_health_worker_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"8"},{"aliases":["man_health_worker_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"8"},{"aliases":["man_health_worker_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"8"},{"aliases":["man_health_worker_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"8"},{"aliases":["man_health_worker_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"8"},{"aliases":["man_health_worker_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"8"},{"aliases":["woman_farmer_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"8"},{"aliases":["woman_farmer_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"8"},{"aliases":["woman_farmer_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"8"},{"aliases":["woman_farmer_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"8"},{"aliases":["woman_farmer_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"8"},{"aliases":["man_farmer_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"8"},{"aliases":["man_farmer_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"8"},{"aliases":["man_farmer_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"8"},{"aliases":["man_farmer_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"8"},{"aliases":["man_farmer_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"8"},{"aliases":["woman_cook_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"8"},{"aliases":["woman_cook_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"8"},{"aliases":["woman_cook_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"8"},{"aliases":["woman_cook_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"8"},{"aliases":["woman_cook_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"8"},{"aliases":["man_cook_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"8"},{"aliases":["man_cook_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"8"},{"aliases":["man_cook_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"8"},{"aliases":["man_cook_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"8"},{"aliases":["man_cook_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"8"},{"aliases":["woman_student_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"8"},{"aliases":["woman_student_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"8"},{"aliases":["woman_student_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"8"},{"aliases":["woman_student_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"8"},{"aliases":["woman_student_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"8"},{"aliases":["man_student_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"8"},{"aliases":["man_student_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"8"},{"aliases":["man_student_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"8"},{"aliases":["man_student_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"8"},{"aliases":["man_student_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"8"},{"aliases":["woman_singer_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"9"},{"aliases":["woman_singer_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"9"},{"aliases":["woman_singer_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"9"},{"aliases":["woman_singer_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"9"},{"aliases":["woman_singer_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"9"},{"aliases":["man_singer_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"9"},{"aliases":["man_singer_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"9"},{"aliases":["man_singer_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"9"},{"aliases":["man_singer_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"9"},{"aliases":["man_singer_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"9"},{"aliases":["woman_teacher_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"9"},{"aliases":["woman_teacher_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"9"},{"aliases":["woman_teacher_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"9"},{"aliases":["woman_teacher_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"9"},{"aliases":["woman_teacher_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"9"},{"aliases":["man_teacher_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"9"},{"aliases":["man_teacher_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"9"},{"aliases":["man_teacher_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"9"},{"aliases":["man_teacher_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"9"},{"aliases":["man_teacher_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"9"},{"aliases":["woman_factory_worker_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"9"},{"aliases":["woman_factory_worker_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"9"},{"aliases":["woman_factory_worker_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"9"},{"aliases":["woman_factory_worker_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"9"},{"aliases":["woman_factory_worker_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"9"},{"aliases":["man_factory_worker_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"9"},{"aliases":["man_factory_worker_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"9"},{"aliases":["man_factory_worker_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"9"},{"aliases":["man_factory_worker_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"9"},{"aliases":["man_factory_worker_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"9"},{"aliases":["woman_technologist_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"9"},{"aliases":["woman_technologist_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"9"},{"aliases":["woman_technologist_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"9"},{"aliases":["woman_technologist_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"9"},{"aliases":["woman_technologist_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"9"},{"aliases":["man_technologist_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"9"},{"aliases":["man_technologist_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"9"},{"aliases":["man_technologist_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"9"},{"aliases":["man_technologist_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"9"},{"aliases":["man_technologist_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"9"},{"aliases":["woman_office_worker_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"9"},{"aliases":["woman_office_worker_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"9"},{"aliases":["woman_office_worker_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"9"},{"aliases":["woman_office_worker_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"9"},{"aliases":["woman_office_worker_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"9"},{"aliases":["man_office_worker_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"10"},{"aliases":["man_office_worker_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"10"},{"aliases":["man_office_worker_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"10"},{"aliases":["man_office_worker_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"10"},{"aliases":["man_office_worker_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"10"},{"aliases":["woman_mechanic_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"10"},{"aliases":["woman_mechanic_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"10"},{"aliases":["woman_mechanic_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"10"},{"aliases":["woman_mechanic_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"10"},{"aliases":["woman_mechanic_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"10"},{"aliases":["man_mechanic_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"10"},{"aliases":["man_mechanic_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"10"},{"aliases":["man_mechanic_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"10"},{"aliases":["man_mechanic_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"10"},{"aliases":["man_mechanic_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"10"},{"aliases":["woman_scientist_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"10"},{"aliases":["woman_scientist_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"10"},{"aliases":["woman_scientist_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"10"},{"aliases":["woman_scientist_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"10"},{"aliases":["woman_scientist_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"10"},{"aliases":["man_scientist_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"10"},{"aliases":["man_scientist_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"10"},{"aliases":["man_scientist_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"10"},{"aliases":["man_scientist_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"10"},{"aliases":["man_scientist_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"10"},{"aliases":["woman_artist_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"10"},{"aliases":["woman_artist_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"10"},{"aliases":["woman_artist_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"10"},{"aliases":["woman_artist_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"10"},{"aliases":["woman_artist_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"10"},{"aliases":["man_artist_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"10"},{"aliases":["man_artist_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"10"},{"aliases":["man_artist_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"10"},{"aliases":["man_artist_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"10"},{"aliases":["man_artist_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"10"},{"aliases":["woman_firefighter_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"10"},{"aliases":["woman_firefighter_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"10"},{"aliases":["woman_firefighter_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"10"},{"aliases":["woman_firefighter_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"10"},{"aliases":["woman_firefighter_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"10"},{"aliases":["man_firefighter_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"10"},{"aliases":["man_firefighter_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"10"},{"aliases":["man_firefighter_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"10"},{"aliases":["man_firefighter_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"10"},{"aliases":["man_firefighter_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"10"},{"aliases":["woman_pilot_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"11"},{"aliases":["woman_pilot_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"11"},{"aliases":["woman_pilot_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"11"},{"aliases":["woman_pilot_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"11"},{"aliases":["woman_pilot_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"11"},{"aliases":["man_pilot_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"11"},{"aliases":["man_pilot_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"11"},{"aliases":["man_pilot_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"11"},{"aliases":["man_pilot_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"11"},{"aliases":["man_pilot_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"11"},{"aliases":["woman_astronaut_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"11"},{"aliases":["woman_astronaut_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"11"},{"aliases":["woman_astronaut_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"11"},{"aliases":["woman_astronaut_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"11"},{"aliases":["woman_astronaut_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"11"},{"aliases":["man_astronaut_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"11"},{"aliases":["man_astronaut_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"11"},{"aliases":["man_astronaut_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"11"},{"aliases":["man_astronaut_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"11"},{"aliases":["man_astronaut_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"11"},{"aliases":["woman_judge_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"11"},{"aliases":["woman_judge_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"11"},{"aliases":["woman_judge_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"11"},{"aliases":["woman_judge_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"11"},{"aliases":["woman_judge_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"11"},{"aliases":["man_judge_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"11"},{"aliases":["man_judge_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"11"},{"aliases":["man_judge_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"11"},{"aliases":["man_judge_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"11"},{"aliases":["man_judge_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"11"},{"aliases":["mrs_claus_light_skin_tone"],"filename":"1f936-1f3fb","category":"skintone","batch":"11"},{"aliases":["mrs_claus_medium_light_skin_tone"],"filename":"1f936-1f3fc","category":"skintone","batch":"11"},{"aliases":["mrs_claus_medium_skin_tone"],"filename":"1f936-1f3fd","category":"skintone","batch":"11"},{"aliases":["mrs_claus_medium_dark_skin_tone"],"filename":"1f936-1f3fe","category":"skintone","batch":"11"},{"aliases":["mrs_claus_dark_skin_tone"],"filename":"1f936-1f3ff","category":"skintone","batch":"11"},{"aliases":["santa_light_skin_tone"],"filename":"1f385-1f3fb","category":"skintone","batch":"11"},{"aliases":["santa_medium_light_skin_tone"],"filename":"1f385-1f3fc","category":"skintone","batch":"11"},{"aliases":["santa_medium_skin_tone"],"filename":"1f385-1f3fd","category":"skintone","batch":"11"},{"aliases":["santa_medium_dark_skin_tone"],"filename":"1f385-1f3fe","category":"skintone","batch":"11"},{"aliases":["santa_dark_skin_tone"],"filename":"1f385-1f3ff","category":"skintone","batch":"11"},{"aliases":["princess_light_skin_tone"],"filename":"1f478-1f3fb","category":"skintone","batch":"11"},{"aliases":["princess_medium_light_skin_tone"],"filename":"1f478-1f3fc","category":"skintone","batch":"11"},{"aliases":["princess_medium_skin_tone"],"filename":"1f478-1f3fd","category":"skintone","batch":"11"},{"aliases":["princess_medium_dark_skin_tone"],"filename":"1f478-1f3fe","category":"skintone","batch":"11"},{"aliases":["princess_dark_skin_tone"],"filename":"1f478-1f3ff","category":"skintone","batch":"11"},{"aliases":["prince_light_skin_tone"],"filename":"1f934-1f3fb","category":"skintone","batch":"12"},{"aliases":["prince_medium_light_skin_tone"],"filename":"1f934-1f3fc","category":"skintone","batch":"12"},{"aliases":["prince_medium_skin_tone"],"filename":"1f934-1f3fd","category":"skintone","batch":"12"},{"aliases":["prince_medium_dark_skin_tone"],"filename":"1f934-1f3fe","category":"skintone","batch":"12"},{"aliases":["prince_dark_skin_tone"],"filename":"1f934-1f3ff","category":"skintone","batch":"12"},{"aliases":["bride_with_veil_light_skin_tone"],"filename":"1f470-1f3fb","category":"skintone","batch":"12"},{"aliases":["bride_with_veil_medium_light_skin_tone"],"filename":"1f470-1f3fc","category":"skintone","batch":"12"},{"aliases":["bride_with_veil_medium_skin_tone"],"filename":"1f470-1f3fd","category":"skintone","batch":"12"},{"aliases":["bride_with_veil_medium_dark_skin_tone"],"filename":"1f470-1f3fe","category":"skintone","batch":"12"},{"aliases":["bride_with_veil_dark_skin_tone"],"filename":"1f470-1f3ff","category":"skintone","batch":"12"},{"aliases":["man_in_tuxedo_light_skin_tone"],"filename":"1f935-1f3fb","category":"skintone","batch":"12"},{"aliases":["man_in_tuxedo_medium_light_skin_tone"],"filename":"1f935-1f3fc","category":"skintone","batch":"12"},{"aliases":["man_in_tuxedo_medium_skin_tone"],"filename":"1f935-1f3fd","category":"skintone","batch":"12"},{"aliases":["man_in_tuxedo_medium_dark_skin_tone"],"filename":"1f935-1f3fe","category":"skintone","batch":"12"},{"aliases":["man_in_tuxedo_dark_skin_tone"],"filename":"1f935-1f3ff","category":"skintone","batch":"12"},{"aliases":["angel_light_skin_tone"],"filename":"1f47c-1f3fb","category":"skintone","batch":"12"},{"aliases":["angel_medium_light_skin_tone"],"filename":"1f47c-1f3fc","category":"skintone","batch":"12"},{"aliases":["angel_medium_skin_tone"],"filename":"1f47c-1f3fd","category":"skintone","batch":"12"},{"aliases":["angel_medium_dark_skin_tone"],"filename":"1f47c-1f3fe","category":"skintone","batch":"12"},{"aliases":["angel_dark_skin_tone"],"filename":"1f47c-1f3ff","category":"skintone","batch":"12"},{"aliases":["pregnant_woman_light_skin_tone"],"filename":"1f930-1f3fb","category":"skintone","batch":"12"},{"aliases":["pregnant_woman_medium_light_skin_tone"],"filename":"1f930-1f3fc","category":"skintone","batch":"12"},{"aliases":["pregnant_woman_medium_skin_tone"],"filename":"1f930-1f3fd","category":"skintone","batch":"12"},{"aliases":["pregnant_woman_medium_dark_skin_tone"],"filename":"1f930-1f3fe","category":"skintone","batch":"12"},{"aliases":["pregnant_woman_dark_skin_tone"],"filename":"1f930-1f3ff","category":"skintone","batch":"12"},{"aliases":["bowing_woman_light_skin_tone"],"filename":"1f647-1f3fb","category":"skintone","batch":"12"},{"aliases":["bowing_woman_medium_light_skin_tone"],"filename":"1f647-1f3fc","category":"skintone","batch":"12"},{"aliases":["bowing_woman_medium_skin_tone"],"filename":"1f647-1f3fd","category":"skintone","batch":"12"},{"aliases":["bowing_woman_medium_dark_skin_tone"],"filename":"1f647-1f3fe","category":"skintone","batch":"12"},{"aliases":["bowing_woman_dark_skin_tone"],"filename":"1f647-1f3ff","category":"skintone","batch":"12"},{"aliases":["bowing_woman_light_skin_tone"],"filename":"1f647-1f3fb-200d-2640-fe0f","category":"skintone","batch":"12"},{"aliases":["bowing_woman_medium_light_skin_tone"],"filename":"1f647-1f3fc-200d-2640-fe0f","category":"skintone","batch":"12"},{"aliases":["bowing_woman_medium_skin_tone"],"filename":"1f647-1f3fd-200d-2640-fe0f","category":"skintone","batch":"12"},{"aliases":["bowing_woman_medium_dark_skin_tone"],"filename":"1f647-1f3fe-200d-2640-fe0f","category":"skintone","batch":"12"},{"aliases":["bowing_woman_dark_skin_tone"],"filename":"1f647-1f3ff-200d-2640-fe0f","category":"skintone","batch":"12"},{"aliases":["bowing_man_light_skin_tone"],"filename":"1f647-1f3fb","category":"skintone","batch":"12"},{"aliases":["bowing_man_medium_light_skin_tone"],"filename":"1f647-1f3fc","category":"skintone","batch":"12"},{"aliases":["bowing_man_medium_skin_tone"],"filename":"1f647-1f3fd","category":"skintone","batch":"12"},{"aliases":["bowing_man_medium_dark_skin_tone"],"filename":"1f647-1f3fe","category":"skintone","batch":"12"},{"aliases":["bowing_man_dark_skin_tone"],"filename":"1f647-1f3ff","category":"skintone","batch":"12"},{"aliases":["bowing_man_light_skin_tone"],"filename":"1f647-1f3fb-200d-2640-fe0f","category":"skintone","batch":"12"},{"aliases":["bowing_man_medium_light_skin_tone"],"filename":"1f647-1f3fc-200d-2640-fe0f","category":"skintone","batch":"12"},{"aliases":["bowing_man_medium_skin_tone"],"filename":"1f647-1f3fd-200d-2640-fe0f","category":"skintone","batch":"12"},{"aliases":["bowing_man_medium_dark_skin_tone"],"filename":"1f647-1f3fe-200d-2640-fe0f","category":"skintone","batch":"12"},{"aliases":["bowing_man_dark_skin_tone"],"filename":"1f647-1f3ff-200d-2640-fe0f","category":"skintone","batch":"12"},{"aliases":["tipping_hand_woman_light_skin_tone"],"filename":"1f481-1f3fb","category":"skintone","batch":"13"},{"aliases":["tipping_hand_woman_medium_light_skin_tone"],"filename":"1f481-1f3fc","category":"skintone","batch":"13"},{"aliases":["tipping_hand_woman_medium_skin_tone"],"filename":"1f481-1f3fd","category":"skintone","batch":"13"},{"aliases":["tipping_hand_woman_medium_dark_skin_tone"],"filename":"1f481-1f3fe","category":"skintone","batch":"13"},{"aliases":["tipping_hand_woman_dark_skin_tone"],"filename":"1f481-1f3ff","category":"skintone","batch":"13"},{"aliases":["tipping_hand_woman_light_skin_tone"],"filename":"1f481-1f3fb-200d-2640-fe0f","category":"skintone","batch":"13"},{"aliases":["tipping_hand_woman_medium_light_skin_tone"],"filename":"1f481-1f3fc-200d-2640-fe0f","category":"skintone","batch":"13"},{"aliases":["tipping_hand_woman_medium_skin_tone"],"filename":"1f481-1f3fd-200d-2640-fe0f","category":"skintone","batch":"13"},{"aliases":["tipping_hand_woman_medium_dark_skin_tone"],"filename":"1f481-1f3fe-200d-2640-fe0f","category":"skintone","batch":"13"},{"aliases":["tipping_hand_woman_dark_skin_tone"],"filename":"1f481-1f3ff-200d-2640-fe0f","category":"skintone","batch":"13"},{"aliases":["tipping_hand_man_light_skin_tone"],"filename":"1f481-1f3fb-200d-2642-fe0f","category":"skintone","batch":"13"},{"aliases":["tipping_hand_man_medium_light_skin_tone"],"filename":"1f481-1f3fc-200d-2642-fe0f","category":"skintone","batch":"13"},{"aliases":["tipping_hand_man_medium_skin_tone"],"filename":"1f481-1f3fd-200d-2642-fe0f","category":"skintone","batch":"13"},{"aliases":["tipping_hand_man_medium_dark_skin_tone"],"filename":"1f481-1f3fe-200d-2642-fe0f","category":"skintone","batch":"13"},{"aliases":["tipping_hand_man_dark_skin_tone"],"filename":"1f481-1f3ff-200d-2642-fe0f","category":"skintone","batch":"13"},{"aliases":["no_good_woman_light_skin_tone"],"filename":"1f645-1f3fb","category":"skintone","batch":"13"},{"aliases":["no_good_woman_medium_light_skin_tone"],"filename":"1f645-1f3fc","category":"skintone","batch":"13"},{"aliases":["no_good_woman_medium_skin_tone"],"filename":"1f645-1f3fd","category":"skintone","batch":"13"},{"aliases":["no_good_woman_medium_dark_skin_tone"],"filename":"1f645-1f3fe","category":"skintone","batch":"13"},{"aliases":["no_good_woman_dark_skin_tone"],"filename":"1f645-1f3ff","category":"skintone","batch":"13"},{"aliases":["no_good_woman_light_skin_tone"],"filename":"1f645-1f3fb-200d-2640-fe0f","category":"skintone","batch":"13"},{"aliases":["no_good_woman_medium_light_skin_tone"],"filename":"1f645-1f3fc-200d-2640-fe0f","category":"skintone","batch":"13"},{"aliases":["no_good_woman_medium_skin_tone"],"filename":"1f645-1f3fd-200d-2640-fe0f","category":"skintone","batch":"13"},{"aliases":["no_good_woman_medium_dark_skin_tone"],"filename":"1f645-1f3fe-200d-2640-fe0f","category":"skintone","batch":"13"},{"aliases":["no_good_woman_dark_skin_tone"],"filename":"1f645-1f3ff-200d-2640-fe0f","category":"skintone","batch":"13"},{"aliases":["no_good_man_light_skin_tone"],"filename":"1f645-1f3fb-200d-2642-fe0f","category":"skintone","batch":"13"},{"aliases":["no_good_man_medium_light_skin_tone"],"filename":"1f645-1f3fc-200d-2642-fe0f","category":"skintone","batch":"13"},{"aliases":["no_good_man_medium_skin_tone"],"filename":"1f645-1f3fd-200d-2642-fe0f","category":"skintone","batch":"13"},{"aliases":["no_good_man_medium_dark_skin_tone"],"filename":"1f645-1f3fe-200d-2642-fe0f","category":"skintone","batch":"13"},{"aliases":["no_good_man_dark_skin_tone"],"filename":"1f645-1f3ff-200d-2642-fe0f","category":"skintone","batch":"13"},{"aliases":["ok_woman_light_skin_tone"],"filename":"1f646-1f3fb","category":"skintone","batch":"13"},{"aliases":["ok_woman_medium_light_skin_tone"],"filename":"1f646-1f3fc","category":"skintone","batch":"13"},{"aliases":["ok_woman_medium_skin_tone"],"filename":"1f646-1f3fd","category":"skintone","batch":"13"},{"aliases":["ok_woman_medium_dark_skin_tone"],"filename":"1f646-1f3fe","category":"skintone","batch":"13"},{"aliases":["ok_woman_dark_skin_tone"],"filename":"1f646-1f3ff","category":"skintone","batch":"13"},{"aliases":["ok_woman_light_skin_tone"],"filename":"1f646-1f3fb-200d-2640-fe0f","category":"skintone","batch":"13"},{"aliases":["ok_woman_medium_light_skin_tone"],"filename":"1f646-1f3fc-200d-2640-fe0f","category":"skintone","batch":"13"},{"aliases":["ok_woman_medium_skin_tone"],"filename":"1f646-1f3fd-200d-2640-fe0f","category":"skintone","batch":"13"},{"aliases":["ok_woman_medium_dark_skin_tone"],"filename":"1f646-1f3fe-200d-2640-fe0f","category":"skintone","batch":"13"},{"aliases":["ok_woman_dark_skin_tone"],"filename":"1f646-1f3ff-200d-2640-fe0f","category":"skintone","batch":"13"},{"aliases":["ok_man_light_skin_tone"],"filename":"1f646-1f3fb-200d-2642-fe0f","category":"skintone","batch":"13"},{"aliases":["ok_man_medium_light_skin_tone"],"filename":"1f646-1f3fc-200d-2642-fe0f","category":"skintone","batch":"13"},{"aliases":["ok_man_medium_skin_tone"],"filename":"1f646-1f3fd-200d-2642-fe0f","category":"skintone","batch":"13"},{"aliases":["ok_man_medium_dark_skin_tone"],"filename":"1f646-1f3fe-200d-2642-fe0f","category":"skintone","batch":"13"},{"aliases":["ok_man_dark_skin_tone"],"filename":"1f646-1f3ff-200d-2642-fe0f","category":"skintone","batch":"13"},{"aliases":["raising_hand_woman_light_skin_tone"],"filename":"1f64b-1f3fb","category":"skintone","batch":"14"},{"aliases":["raising_hand_woman_medium_light_skin_tone"],"filename":"1f64b-1f3fc","category":"skintone","batch":"14"},{"aliases":["raising_hand_woman_medium_skin_tone"],"filename":"1f64b-1f3fd","category":"skintone","batch":"14"},{"aliases":["raising_hand_woman_medium_dark_skin_tone"],"filename":"1f64b-1f3fe","category":"skintone","batch":"14"},{"aliases":["raising_hand_woman_dark_skin_tone"],"filename":"1f64b-1f3ff","category":"skintone","batch":"14"},{"aliases":["raising_hand_woman_light_skin_tone"],"filename":"1f64b-1f3fb-200d-2640-fe0f","category":"skintone","batch":"14"},{"aliases":["raising_hand_woman_medium_light_skin_tone"],"filename":"1f64b-1f3fc-200d-2640-fe0f","category":"skintone","batch":"14"},{"aliases":["raising_hand_woman_medium_skin_tone"],"filename":"1f64b-1f3fd-200d-2640-fe0f","category":"skintone","batch":"14"},{"aliases":["raising_hand_woman_medium_dark_skin_tone"],"filename":"1f64b-1f3fe-200d-2640-fe0f","category":"skintone","batch":"14"},{"aliases":["raising_hand_woman_dark_skin_tone"],"filename":"1f64b-1f3ff-200d-2640-fe0f","category":"skintone","batch":"14"},{"aliases":["raising_hand_man_light_skin_tone"],"filename":"1f64b-1f3fb-200d-2642-fe0f","category":"skintone","batch":"14"},{"aliases":["raising_hand_man_medium_light_skin_tone"],"filename":"1f64b-1f3fc-200d-2642-fe0f","category":"skintone","batch":"14"},{"aliases":["raising_hand_man_medium_skin_tone"],"filename":"1f64b-1f3fd-200d-2642-fe0f","category":"skintone","batch":"14"},{"aliases":["raising_hand_man_medium_dark_skin_tone"],"filename":"1f64b-1f3fe-200d-2642-fe0f","category":"skintone","batch":"14"},{"aliases":["raising_hand_man_dark_skin_tone"],"filename":"1f64b-1f3ff-200d-2642-fe0f","category":"skintone","batch":"14"},{"aliases":["woman_facepalming_light_skin_tone"],"filename":"1f926-1f3fb","category":"skintone","batch":"14"},{"aliases":["woman_facepalming_medium_light_skin_tone"],"filename":"1f926-1f3fc","category":"skintone","batch":"14"},{"aliases":["woman_facepalming_medium_skin_tone"],"filename":"1f926-1f3fd","category":"skintone","batch":"14"},{"aliases":["woman_facepalming_medium_dark_skin_tone"],"filename":"1f926-1f3fe","category":"skintone","batch":"14"},{"aliases":["woman_facepalming_dark_skin_tone"],"filename":"1f926-1f3ff","category":"skintone","batch":"14"},{"aliases":["woman_facepalming_light_skin_tone"],"filename":"1f926-1f3fb-200d-2640-fe0f","category":"skintone","batch":"14"},{"aliases":["woman_facepalming_medium_light_skin_tone"],"filename":"1f926-1f3fc-200d-2640-fe0f","category":"skintone","batch":"14"},{"aliases":["woman_facepalming_medium_skin_tone"],"filename":"1f926-1f3fd-200d-2640-fe0f","category":"skintone","batch":"14"},{"aliases":["woman_facepalming_medium_dark_skin_tone"],"filename":"1f926-1f3fe-200d-2640-fe0f","category":"skintone","batch":"14"},{"aliases":["woman_facepalming_dark_skin_tone"],"filename":"1f926-1f3ff-200d-2640-fe0f","category":"skintone","batch":"14"},{"aliases":["man_facepalming_light_skin_tone"],"filename":"1f926-1f3fb-200d-2642-fe0f","category":"skintone","batch":"14"},{"aliases":["man_facepalming_medium_light_skin_tone"],"filename":"1f926-1f3fc-200d-2642-fe0f","category":"skintone","batch":"14"},{"aliases":["man_facepalming_medium_skin_tone"],"filename":"1f926-1f3fd-200d-2642-fe0f","category":"skintone","batch":"14"},{"aliases":["man_facepalming_medium_dark_skin_tone"],"filename":"1f926-1f3fe-200d-2642-fe0f","category":"skintone","batch":"14"},{"aliases":["man_facepalming_dark_skin_tone"],"filename":"1f926-1f3ff-200d-2642-fe0f","category":"skintone","batch":"14"},{"aliases":["woman_shrugging_light_skin_tone"],"filename":"1f937-1f3fb","category":"skintone","batch":"14"},{"aliases":["woman_shrugging_medium_light_skin_tone"],"filename":"1f937-1f3fc","category":"skintone","batch":"14"},{"aliases":["woman_shrugging_medium_skin_tone"],"filename":"1f937-1f3fd","category":"skintone","batch":"14"},{"aliases":["woman_shrugging_medium_dark_skin_tone"],"filename":"1f937-1f3fe","category":"skintone","batch":"14"},{"aliases":["woman_shrugging_dark_skin_tone"],"filename":"1f937-1f3ff","category":"skintone","batch":"14"},{"aliases":["woman_shrugging_light_skin_tone"],"filename":"1f937-1f3fb-200d-2640-fe0f","category":"skintone","batch":"14"},{"aliases":["woman_shrugging_medium_light_skin_tone"],"filename":"1f937-1f3fc-200d-2640-fe0f","category":"skintone","batch":"14"},{"aliases":["woman_shrugging_medium_skin_tone"],"filename":"1f937-1f3fd-200d-2640-fe0f","category":"skintone","batch":"14"},{"aliases":["woman_shrugging_medium_dark_skin_tone"],"filename":"1f937-1f3fe-200d-2640-fe0f","category":"skintone","batch":"14"},{"aliases":["woman_shrugging_dark_skin_tone"],"filename":"1f937-1f3ff-200d-2640-fe0f","category":"skintone","batch":"14"},{"aliases":["man_shrugging_light_skin_tone"],"filename":"1f937-1f3fb-200d-2642-fe0f","category":"skintone","batch":"14"},{"aliases":["man_shrugging_medium_light_skin_tone"],"filename":"1f937-1f3fc-200d-2642-fe0f","category":"skintone","batch":"14"},{"aliases":["man_shrugging_medium_skin_tone"],"filename":"1f937-1f3fd-200d-2642-fe0f","category":"skintone","batch":"14"},{"aliases":["man_shrugging_medium_dark_skin_tone"],"filename":"1f937-1f3fe-200d-2642-fe0f","category":"skintone","batch":"14"},{"aliases":["man_shrugging_dark_skin_tone"],"filename":"1f937-1f3ff-200d-2642-fe0f","category":"skintone","batch":"14"},{"aliases":["pouting_woman_light_skin_tone"],"filename":"1f64e-1f3fb","category":"skintone","batch":"15"},{"aliases":["pouting_woman_medium_light_skin_tone"],"filename":"1f64e-1f3fc","category":"skintone","batch":"15"},{"aliases":["pouting_woman_medium_skin_tone"],"filename":"1f64e-1f3fd","category":"skintone","batch":"15"},{"aliases":["pouting_woman_medium_dark_skin_tone"],"filename":"1f64e-1f3fe","category":"skintone","batch":"15"},{"aliases":["pouting_woman_dark_skin_tone"],"filename":"1f64e-1f3ff","category":"skintone","batch":"15"},{"aliases":["pouting_woman_light_skin_tone"],"filename":"1f64e-1f3fb-200d-2640-fe0f","category":"skintone","batch":"15"},{"aliases":["pouting_woman_medium_light_skin_tone"],"filename":"1f64e-1f3fc-200d-2640-fe0f","category":"skintone","batch":"15"},{"aliases":["pouting_woman_medium_skin_tone"],"filename":"1f64e-1f3fd-200d-2640-fe0f","category":"skintone","batch":"15"},{"aliases":["pouting_woman_medium_dark_skin_tone"],"filename":"1f64e-1f3fe-200d-2640-fe0f","category":"skintone","batch":"15"},{"aliases":["pouting_woman_dark_skin_tone"],"filename":"1f64e-1f3ff-200d-2640-fe0f","category":"skintone","batch":"15"},{"aliases":["pouting_man_light_skin_tone"],"filename":"1f64e-1f3fb-200d-2642-fe0f","category":"skintone","batch":"15"},{"aliases":["pouting_man_medium_light_skin_tone"],"filename":"1f64e-1f3fc-200d-2642-fe0f","category":"skintone","batch":"15"},{"aliases":["pouting_man_medium_skin_tone"],"filename":"1f64e-1f3fd-200d-2642-fe0f","category":"skintone","batch":"15"},{"aliases":["pouting_man_medium_dark_skin_tone"],"filename":"1f64e-1f3fe-200d-2642-fe0f","category":"skintone","batch":"15"},{"aliases":["pouting_man_dark_skin_tone"],"filename":"1f64e-1f3ff-200d-2642-fe0f","category":"skintone","batch":"15"},{"aliases":["frowning_woman_light_skin_tone"],"filename":"1f64d-1f3fb","category":"skintone","batch":"15"},{"aliases":["frowning_woman_medium_light_skin_tone"],"filename":"1f64d-1f3fc","category":"skintone","batch":"15"},{"aliases":["frowning_woman_medium_skin_tone"],"filename":"1f64d-1f3fd","category":"skintone","batch":"15"},{"aliases":["frowning_woman_medium_dark_skin_tone"],"filename":"1f64d-1f3fe","category":"skintone","batch":"15"},{"aliases":["frowning_woman_dark_skin_tone"],"filename":"1f64d-1f3ff","category":"skintone","batch":"15"},{"aliases":["frowning_woman_light_skin_tone"],"filename":"1f64d-1f3fb-200d-2640-fe0f","category":"skintone","batch":"15"},{"aliases":["frowning_woman_medium_light_skin_tone"],"filename":"1f64d-1f3fc-200d-2640-fe0f","category":"skintone","batch":"15"},{"aliases":["frowning_woman_medium_skin_tone"],"filename":"1f64d-1f3fd-200d-2640-fe0f","category":"skintone","batch":"15"},{"aliases":["frowning_woman_medium_dark_skin_tone"],"filename":"1f64d-1f3fe-200d-2640-fe0f","category":"skintone","batch":"15"},{"aliases":["frowning_woman_dark_skin_tone"],"filename":"1f64d-1f3ff-200d-2640-fe0f","category":"skintone","batch":"15"},{"aliases":["frowning_man_light_skin_tone"],"filename":"1f64d-1f3fb-200d-2642-fe0f","category":"skintone","batch":"15"},{"aliases":["frowning_man_medium_light_skin_tone"],"filename":"1f64d-1f3fc-200d-2642-fe0f","category":"skintone","batch":"15"},{"aliases":["frowning_man_medium_skin_tone"],"filename":"1f64d-1f3fd-200d-2642-fe0f","category":"skintone","batch":"15"},{"aliases":["frowning_man_medium_dark_skin_tone"],"filename":"1f64d-1f3fe-200d-2642-fe0f","category":"skintone","batch":"15"},{"aliases":["frowning_man_dark_skin_tone"],"filename":"1f64d-1f3ff-200d-2642-fe0f","category":"skintone","batch":"15"},{"aliases":["haircut_woman_light_skin_tone"],"filename":"1f487-1f3fb","category":"skintone","batch":"15"},{"aliases":["haircut_woman_medium_light_skin_tone"],"filename":"1f487-1f3fc","category":"skintone","batch":"15"},{"aliases":["haircut_woman_medium_skin_tone"],"filename":"1f487-1f3fd","category":"skintone","batch":"15"},{"aliases":["haircut_woman_medium_dark_skin_tone"],"filename":"1f487-1f3fe","category":"skintone","batch":"15"},{"aliases":["haircut_woman_dark_skin_tone"],"filename":"1f487-1f3ff","category":"skintone","batch":"15"},{"aliases":["haircut_woman_light_skin_tone"],"filename":"1f487-1f3fb-200d-2640-fe0f","category":"skintone","batch":"15"},{"aliases":["haircut_woman_medium_light_skin_tone"],"filename":"1f487-1f3fc-200d-2640-fe0f","category":"skintone","batch":"15"},{"aliases":["haircut_woman_medium_skin_tone"],"filename":"1f487-1f3fd-200d-2640-fe0f","category":"skintone","batch":"15"},{"aliases":["haircut_woman_medium_dark_skin_tone"],"filename":"1f487-1f3fe-200d-2640-fe0f","category":"skintone","batch":"15"},{"aliases":["haircut_woman_dark_skin_tone"],"filename":"1f487-1f3ff-200d-2640-fe0f","category":"skintone","batch":"15"},{"aliases":["haircut_man_light_skin_tone"],"filename":"1f487-1f3fb-200d-2642-fe0f","category":"skintone","batch":"15"},{"aliases":["haircut_man_medium_light_skin_tone"],"filename":"1f487-1f3fc-200d-2642-fe0f","category":"skintone","batch":"15"},{"aliases":["haircut_man_medium_skin_tone"],"filename":"1f487-1f3fd-200d-2642-fe0f","category":"skintone","batch":"15"},{"aliases":["haircut_man_medium_dark_skin_tone"],"filename":"1f487-1f3fe-200d-2642-fe0f","category":"skintone","batch":"15"},{"aliases":["haircut_man_dark_skin_tone"],"filename":"1f487-1f3ff-200d-2642-fe0f","category":"skintone","batch":"15"},{"aliases":["massage_woman_light_skin_tone"],"filename":"1f486-1f3fb","category":"skintone","batch":"16"},{"aliases":["massage_woman_medium_light_skin_tone"],"filename":"1f486-1f3fc","category":"skintone","batch":"16"},{"aliases":["massage_woman_medium_skin_tone"],"filename":"1f486-1f3fd","category":"skintone","batch":"16"},{"aliases":["massage_woman_medium_dark_skin_tone"],"filename":"1f486-1f3fe","category":"skintone","batch":"16"},{"aliases":["massage_woman_dark_skin_tone"],"filename":"1f486-1f3ff","category":"skintone","batch":"16"},{"aliases":["massage_woman_light_skin_tone"],"filename":"1f486-1f3fb-200d-2640-fe0f","category":"skintone","batch":"16"},{"aliases":["massage_woman_medium_light_skin_tone"],"filename":"1f486-1f3fc-200d-2640-fe0f","category":"skintone","batch":"16"},{"aliases":["massage_woman_medium_skin_tone"],"filename":"1f486-1f3fd-200d-2640-fe0f","category":"skintone","batch":"16"},{"aliases":["massage_woman_medium_dark_skin_tone"],"filename":"1f486-1f3fe-200d-2640-fe0f","category":"skintone","batch":"16"},{"aliases":["massage_woman_dark_skin_tone"],"filename":"1f486-1f3ff-200d-2640-fe0f","category":"skintone","batch":"16"},{"aliases":["massage_man_light_skin_tone"],"filename":"1f486-1f3fb-200d-2642-fe0f","category":"skintone","batch":"16"},{"aliases":["massage_man_medium_light_skin_tone"],"filename":"1f486-1f3fc-200d-2642-fe0f","category":"skintone","batch":"16"},{"aliases":["massage_man_medium_skin_tone"],"filename":"1f486-1f3fd-200d-2642-fe0f","category":"skintone","batch":"16"},{"aliases":["massage_man_medium_dark_skin_tone"],"filename":"1f486-1f3fe-200d-2642-fe0f","category":"skintone","batch":"16"},{"aliases":["massage_man_dark_skin_tone"],"filename":"1f486-1f3ff-200d-2642-fe0f","category":"skintone","batch":"16"},{"aliases":["business_suit_levitating_light_skin_tone"],"filename":"1f574-1f3fb","category":"skintone","batch":"16"},{"aliases":["business_suit_levitating_medium_light_skin_tone"],"filename":"1f574-1f3fc","category":"skintone","batch":"16"},{"aliases":["business_suit_levitating_medium_skin_tone"],"filename":"1f574-1f3fd","category":"skintone","batch":"16"},{"aliases":["business_suit_levitating_medium_dark_skin_tone"],"filename":"1f574-1f3fe","category":"skintone","batch":"16"},{"aliases":["business_suit_levitating_dark_skin_tone"],"filename":"1f574-1f3ff","category":"skintone","batch":"16"},{"aliases":["dancer_light_skin_tone"],"filename":"1f483-1f3fb","category":"skintone","batch":"16"},{"aliases":["dancer_medium_light_skin_tone"],"filename":"1f483-1f3fc","category":"skintone","batch":"16"},{"aliases":["dancer_medium_skin_tone"],"filename":"1f483-1f3fd","category":"skintone","batch":"16"},{"aliases":["dancer_medium_dark_skin_tone"],"filename":"1f483-1f3fe","category":"skintone","batch":"16"},{"aliases":["dancer_dark_skin_tone"],"filename":"1f483-1f3ff","category":"skintone","batch":"16"},{"aliases":["man_dancing_light_skin_tone"],"filename":"1f57a-1f3fb","category":"skintone","batch":"16"},{"aliases":["man_dancing_medium_light_skin_tone"],"filename":"1f57a-1f3fc","category":"skintone","batch":"16"},{"aliases":["man_dancing_medium_skin_tone"],"filename":"1f57a-1f3fd","category":"skintone","batch":"16"},{"aliases":["man_dancing_medium_dark_skin_tone"],"filename":"1f57a-1f3fe","category":"skintone","batch":"16"},{"aliases":["man_dancing_dark_skin_tone"],"filename":"1f57a-1f3ff","category":"skintone","batch":"16"},{"aliases":["walking_woman_light_skin_tone"],"filename":"1f6b6-1f3fb","category":"skintone","batch":"16"},{"aliases":["walking_woman_medium_light_skin_tone"],"filename":"1f6b6-1f3fc","category":"skintone","batch":"16"},{"aliases":["walking_woman_medium_skin_tone"],"filename":"1f6b6-1f3fd","category":"skintone","batch":"16"},{"aliases":["walking_woman_medium_dark_skin_tone"],"filename":"1f6b6-1f3fe","category":"skintone","batch":"16"},{"aliases":["walking_woman_dark_skin_tone"],"filename":"1f6b6-1f3ff","category":"skintone","batch":"16"},{"aliases":["walking_woman_light_skin_tone"],"filename":"1f6b6-1f3fb-200d-2640-fe0f","category":"skintone","batch":"16"},{"aliases":["walking_woman_medium_light_skin_tone"],"filename":"1f6b6-1f3fc-200d-2640-fe0f","category":"skintone","batch":"16"},{"aliases":["walking_woman_medium_skin_tone"],"filename":"1f6b6-1f3fd-200d-2640-fe0f","category":"skintone","batch":"16"},{"aliases":["walking_woman_medium_dark_skin_tone"],"filename":"1f6b6-1f3fe-200d-2640-fe0f","category":"skintone","batch":"16"},{"aliases":["walking_woman_dark_skin_tone"],"filename":"1f6b6-1f3ff-200d-2640-fe0f","category":"skintone","batch":"16"},{"aliases":["walking_man_light_skin_tone"],"filename":"1f6b6-1f3fb","category":"skintone","batch":"16"},{"aliases":["walking_man_medium_light_skin_tone"],"filename":"1f6b6-1f3fc","category":"skintone","batch":"16"},{"aliases":["walking_man_medium_skin_tone"],"filename":"1f6b6-1f3fd","category":"skintone","batch":"16"},{"aliases":["walking_man_medium_dark_skin_tone"],"filename":"1f6b6-1f3fe","category":"skintone","batch":"16"},{"aliases":["walking_man_dark_skin_tone"],"filename":"1f6b6-1f3ff","category":"skintone","batch":"16"},{"aliases":["walking_man_light_skin_tone"],"filename":"1f6b6-1f3fb-200d-2640-fe0f","category":"skintone","batch":"17"},{"aliases":["walking_man_medium_light_skin_tone"],"filename":"1f6b6-1f3fc-200d-2640-fe0f","category":"skintone","batch":"17"},{"aliases":["walking_man_medium_skin_tone"],"filename":"1f6b6-1f3fd-200d-2640-fe0f","category":"skintone","batch":"17"},{"aliases":["walking_man_medium_dark_skin_tone"],"filename":"1f6b6-1f3fe-200d-2640-fe0f","category":"skintone","batch":"17"},{"aliases":["walking_man_dark_skin_tone"],"filename":"1f6b6-1f3ff-200d-2640-fe0f","category":"skintone","batch":"17"},{"aliases":["running_woman_light_skin_tone"],"filename":"1f3c3-1f3fb","category":"skintone","batch":"17"},{"aliases":["running_woman_medium_light_skin_tone"],"filename":"1f3c3-1f3fc","category":"skintone","batch":"17"},{"aliases":["running_woman_medium_skin_tone"],"filename":"1f3c3-1f3fd","category":"skintone","batch":"17"},{"aliases":["running_woman_medium_dark_skin_tone"],"filename":"1f3c3-1f3fe","category":"skintone","batch":"17"},{"aliases":["running_woman_dark_skin_tone"],"filename":"1f3c3-1f3ff","category":"skintone","batch":"17"},{"aliases":["running_woman_light_skin_tone"],"filename":"1f3c3-1f3fb-200d-2640-fe0f","category":"skintone","batch":"17"},{"aliases":["running_woman_medium_light_skin_tone"],"filename":"1f3c3-1f3fc-200d-2640-fe0f","category":"skintone","batch":"17"},{"aliases":["running_woman_medium_skin_tone"],"filename":"1f3c3-1f3fd-200d-2640-fe0f","category":"skintone","batch":"17"},{"aliases":["running_woman_medium_dark_skin_tone"],"filename":"1f3c3-1f3fe-200d-2640-fe0f","category":"skintone","batch":"17"},{"aliases":["running_woman_dark_skin_tone"],"filename":"1f3c3-1f3ff-200d-2640-fe0f","category":"skintone","batch":"17"},{"aliases":["running_man_light_skin_tone"],"filename":"1f3c3-1f3fb","category":"skintone","batch":"17"},{"aliases":["running_man_medium_light_skin_tone"],"filename":"1f3c3-1f3fc","category":"skintone","batch":"17"},{"aliases":["running_man_medium_skin_tone"],"filename":"1f3c3-1f3fd","category":"skintone","batch":"17"},{"aliases":["running_man_medium_dark_skin_tone"],"filename":"1f3c3-1f3fe","category":"skintone","batch":"17"},{"aliases":["running_man_dark_skin_tone"],"filename":"1f3c3-1f3ff","category":"skintone","batch":"17"},{"aliases":["running_man_light_skin_tone"],"filename":"1f3c3-1f3fb-200d-2640-fe0f","category":"skintone","batch":"17"},{"aliases":["running_man_medium_light_skin_tone"],"filename":"1f3c3-1f3fc-200d-2640-fe0f","category":"skintone","batch":"17"},{"aliases":["running_man_medium_skin_tone"],"filename":"1f3c3-1f3fd-200d-2640-fe0f","category":"skintone","batch":"17"},{"aliases":["running_man_medium_dark_skin_tone"],"filename":"1f3c3-1f3fe-200d-2640-fe0f","category":"skintone","batch":"17"},{"aliases":["running_man_dark_skin_tone"],"filename":"1f3c3-1f3ff-200d-2640-fe0f","category":"skintone","batch":"17"},{"aliases":["couple_with_heart_woman_woman_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"17"},{"aliases":["couple_with_heart_woman_woman_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"17"},{"aliases":["couple_with_heart_woman_woman_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"17"},{"aliases":["couple_with_heart_woman_woman_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"17"},{"aliases":["couple_with_heart_woman_woman_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"17"},{"aliases":["couple_with_heart_man_man_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"17"},{"aliases":["couple_with_heart_man_man_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"17"},{"aliases":["couple_with_heart_man_man_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"17"},{"aliases":["couple_with_heart_man_man_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"17"},{"aliases":["couple_with_heart_man_man_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"17"},{"aliases":["couplekiss_woman_woman_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"17"},{"aliases":["couplekiss_woman_woman_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"17"},{"aliases":["couplekiss_woman_woman_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"17"},{"aliases":["couplekiss_woman_woman_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"17"},{"aliases":["couplekiss_woman_woman_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"17"},{"aliases":["couplekiss_man_man_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"17"},{"aliases":["couplekiss_man_man_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"17"},{"aliases":["couplekiss_man_man_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"17"},{"aliases":["couplekiss_man_man_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"17"},{"aliases":["couplekiss_man_man_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"17"},{"aliases":["family_man_woman_girl_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"18"},{"aliases":["family_man_woman_girl_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"18"},{"aliases":["family_man_woman_girl_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"18"},{"aliases":["family_man_woman_girl_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"18"},{"aliases":["family_man_woman_girl_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"18"},{"aliases":["family_man_woman_girl_boy_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"18"},{"aliases":["family_man_woman_girl_boy_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"18"},{"aliases":["family_man_woman_girl_boy_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"18"},{"aliases":["family_man_woman_girl_boy_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"18"},{"aliases":["family_man_woman_girl_boy_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"18"},{"aliases":["family_man_woman_boy_boy_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"18"},{"aliases":["family_man_woman_boy_boy_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"18"},{"aliases":["family_man_woman_boy_boy_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"18"},{"aliases":["family_man_woman_boy_boy_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"18"},{"aliases":["family_man_woman_boy_boy_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"18"},{"aliases":["family_man_woman_girl_girl_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"18"},{"aliases":["family_man_woman_girl_girl_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"18"},{"aliases":["family_man_woman_girl_girl_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"18"},{"aliases":["family_man_woman_girl_girl_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"18"},{"aliases":["family_man_woman_girl_girl_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_boy_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_boy_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_boy_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_boy_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_boy_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_girl_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_girl_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_girl_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_girl_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_girl_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_girl_boy_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_girl_boy_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_girl_boy_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_girl_boy_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_girl_boy_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_boy_boy_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_boy_boy_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_boy_boy_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_boy_boy_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_boy_boy_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_girl_girl_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_girl_girl_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_girl_girl_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_girl_girl_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"18"},{"aliases":["family_woman_woman_girl_girl_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"18"},{"aliases":["family_man_man_boy_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"19"},{"aliases":["family_man_man_boy_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"19"},{"aliases":["family_man_man_boy_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"19"},{"aliases":["family_man_man_boy_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"19"},{"aliases":["family_man_man_boy_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"19"},{"aliases":["family_man_man_girl_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"19"},{"aliases":["family_man_man_girl_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"19"},{"aliases":["family_man_man_girl_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"19"},{"aliases":["family_man_man_girl_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"19"},{"aliases":["family_man_man_girl_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"19"},{"aliases":["family_man_man_girl_boy_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"19"},{"aliases":["family_man_man_girl_boy_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"19"},{"aliases":["family_man_man_girl_boy_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"19"},{"aliases":["family_man_man_girl_boy_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"19"},{"aliases":["family_man_man_girl_boy_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"19"},{"aliases":["family_man_man_boy_boy_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"19"},{"aliases":["family_man_man_boy_boy_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"19"},{"aliases":["family_man_man_boy_boy_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"19"},{"aliases":["family_man_man_boy_boy_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"19"},{"aliases":["family_man_man_boy_boy_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"19"},{"aliases":["family_man_man_girl_girl_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"19"},{"aliases":["family_man_man_girl_girl_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"19"},{"aliases":["family_man_man_girl_girl_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"19"},{"aliases":["family_man_man_girl_girl_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"19"},{"aliases":["family_man_man_girl_girl_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"19"},{"aliases":["family_woman_boy_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"19"},{"aliases":["family_woman_boy_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"19"},{"aliases":["family_woman_boy_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"19"},{"aliases":["family_woman_boy_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"19"},{"aliases":["family_woman_boy_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"19"},{"aliases":["family_woman_girl_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"19"},{"aliases":["family_woman_girl_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"19"},{"aliases":["family_woman_girl_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"19"},{"aliases":["family_woman_girl_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"19"},{"aliases":["family_woman_girl_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"19"},{"aliases":["family_woman_girl_boy_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"19"},{"aliases":["family_woman_girl_boy_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"19"},{"aliases":["family_woman_girl_boy_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"19"},{"aliases":["family_woman_girl_boy_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"19"},{"aliases":["family_woman_girl_boy_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"19"},{"aliases":["family_woman_boy_boy_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"19"},{"aliases":["family_woman_boy_boy_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"19"},{"aliases":["family_woman_boy_boy_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"19"},{"aliases":["family_woman_boy_boy_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"19"},{"aliases":["family_woman_boy_boy_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"19"},{"aliases":["family_woman_girl_girl_light_skin_tone"],"filename":"1f469-1f3fb","category":"skintone","batch":"20"},{"aliases":["family_woman_girl_girl_medium_light_skin_tone"],"filename":"1f469-1f3fc","category":"skintone","batch":"20"},{"aliases":["family_woman_girl_girl_medium_skin_tone"],"filename":"1f469-1f3fd","category":"skintone","batch":"20"},{"aliases":["family_woman_girl_girl_medium_dark_skin_tone"],"filename":"1f469-1f3fe","category":"skintone","batch":"20"},{"aliases":["family_woman_girl_girl_dark_skin_tone"],"filename":"1f469-1f3ff","category":"skintone","batch":"20"},{"aliases":["family_man_boy_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"20"},{"aliases":["family_man_boy_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"20"},{"aliases":["family_man_boy_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"20"},{"aliases":["family_man_boy_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"20"},{"aliases":["family_man_boy_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"20"},{"aliases":["family_man_girl_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"20"},{"aliases":["family_man_girl_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"20"},{"aliases":["family_man_girl_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"20"},{"aliases":["family_man_girl_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"20"},{"aliases":["family_man_girl_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"20"},{"aliases":["family_man_girl_boy_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"20"},{"aliases":["family_man_girl_boy_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"20"},{"aliases":["family_man_girl_boy_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"20"},{"aliases":["family_man_girl_boy_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"20"},{"aliases":["family_man_girl_boy_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"20"},{"aliases":["family_man_boy_boy_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"20"},{"aliases":["family_man_boy_boy_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"20"},{"aliases":["family_man_boy_boy_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"20"},{"aliases":["family_man_boy_boy_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"20"},{"aliases":["family_man_boy_boy_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"20"},{"aliases":["family_man_girl_girl_light_skin_tone"],"filename":"1f468-1f3fb","category":"skintone","batch":"20"},{"aliases":["family_man_girl_girl_medium_light_skin_tone"],"filename":"1f468-1f3fc","category":"skintone","batch":"20"},{"aliases":["family_man_girl_girl_medium_skin_tone"],"filename":"1f468-1f3fd","category":"skintone","batch":"20"},{"aliases":["family_man_girl_girl_medium_dark_skin_tone"],"filename":"1f468-1f3fe","category":"skintone","batch":"20"},{"aliases":["family_man_girl_girl_dark_skin_tone"],"filename":"1f468-1f3ff","category":"skintone","batch":"20"},{"aliases":["weight_lifting_woman_light_skin_tone"],"filename":"1f3cb-1f3fb","category":"skintone","batch":"20"},{"aliases":["weight_lifting_woman_medium_light_skin_tone"],"filename":"1f3cb-1f3fc","category":"skintone","batch":"20"},{"aliases":["weight_lifting_woman_medium_skin_tone"],"filename":"1f3cb-1f3fd","category":"skintone","batch":"20"},{"aliases":["weight_lifting_woman_medium_dark_skin_tone"],"filename":"1f3cb-1f3fe","category":"skintone","batch":"20"},{"aliases":["weight_lifting_woman_dark_skin_tone"],"filename":"1f3cb-1f3ff","category":"skintone","batch":"20"},{"aliases":["weight_lifting_woman_light_skin_tone"],"filename":"1f3cb-1f3fb-200d-2640-fe0f","category":"skintone","batch":"20"},{"aliases":["weight_lifting_woman_medium_light_skin_tone"],"filename":"1f3cb-1f3fc-200d-2640-fe0f","category":"skintone","batch":"20"},{"aliases":["weight_lifting_woman_medium_skin_tone"],"filename":"1f3cb-1f3fd-200d-2640-fe0f","category":"skintone","batch":"20"},{"aliases":["weight_lifting_woman_medium_dark_skin_tone"],"filename":"1f3cb-1f3fe-200d-2640-fe0f","category":"skintone","batch":"20"},{"aliases":["weight_lifting_woman_dark_skin_tone"],"filename":"1f3cb-1f3ff-200d-2640-fe0f","category":"skintone","batch":"20"},{"aliases":["weight_lifting_man_light_skin_tone"],"filename":"1f3cb-1f3fb","category":"skintone","batch":"20"},{"aliases":["weight_lifting_man_medium_light_skin_tone"],"filename":"1f3cb-1f3fc","category":"skintone","batch":"20"},{"aliases":["weight_lifting_man_medium_skin_tone"],"filename":"1f3cb-1f3fd","category":"skintone","batch":"20"},{"aliases":["weight_lifting_man_medium_dark_skin_tone"],"filename":"1f3cb-1f3fe","category":"skintone","batch":"20"},{"aliases":["weight_lifting_man_dark_skin_tone"],"filename":"1f3cb-1f3ff","category":"skintone","batch":"20"},{"aliases":["weight_lifting_man_light_skin_tone"],"filename":"1f3cb-1f3fb-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["weight_lifting_man_medium_light_skin_tone"],"filename":"1f3cb-1f3fc-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["weight_lifting_man_medium_skin_tone"],"filename":"1f3cb-1f3fd-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["weight_lifting_man_medium_dark_skin_tone"],"filename":"1f3cb-1f3fe-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["weight_lifting_man_dark_skin_tone"],"filename":"1f3cb-1f3ff-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["woman_cartwheeling_light_skin_tone"],"filename":"1f938-1f3fb","category":"skintone","batch":"21"},{"aliases":["woman_cartwheeling_medium_light_skin_tone"],"filename":"1f938-1f3fc","category":"skintone","batch":"21"},{"aliases":["woman_cartwheeling_medium_skin_tone"],"filename":"1f938-1f3fd","category":"skintone","batch":"21"},{"aliases":["woman_cartwheeling_medium_dark_skin_tone"],"filename":"1f938-1f3fe","category":"skintone","batch":"21"},{"aliases":["woman_cartwheeling_dark_skin_tone"],"filename":"1f938-1f3ff","category":"skintone","batch":"21"},{"aliases":["woman_cartwheeling_light_skin_tone"],"filename":"1f938-1f3fb-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["woman_cartwheeling_medium_light_skin_tone"],"filename":"1f938-1f3fc-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["woman_cartwheeling_medium_skin_tone"],"filename":"1f938-1f3fd-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["woman_cartwheeling_medium_dark_skin_tone"],"filename":"1f938-1f3fe-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["woman_cartwheeling_dark_skin_tone"],"filename":"1f938-1f3ff-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["man_cartwheeling_light_skin_tone"],"filename":"1f938-1f3fb-200d-2642-fe0f","category":"skintone","batch":"21"},{"aliases":["man_cartwheeling_medium_light_skin_tone"],"filename":"1f938-1f3fc-200d-2642-fe0f","category":"skintone","batch":"21"},{"aliases":["man_cartwheeling_medium_skin_tone"],"filename":"1f938-1f3fd-200d-2642-fe0f","category":"skintone","batch":"21"},{"aliases":["man_cartwheeling_medium_dark_skin_tone"],"filename":"1f938-1f3fe-200d-2642-fe0f","category":"skintone","batch":"21"},{"aliases":["man_cartwheeling_dark_skin_tone"],"filename":"1f938-1f3ff-200d-2642-fe0f","category":"skintone","batch":"21"},{"aliases":["basketball_woman_light_skin_tone"],"filename":"26f9-1f3fb","category":"skintone","batch":"21"},{"aliases":["basketball_woman_medium_light_skin_tone"],"filename":"26f9-1f3fc","category":"skintone","batch":"21"},{"aliases":["basketball_woman_medium_skin_tone"],"filename":"26f9-1f3fd","category":"skintone","batch":"21"},{"aliases":["basketball_woman_medium_dark_skin_tone"],"filename":"26f9-1f3fe","category":"skintone","batch":"21"},{"aliases":["basketball_woman_dark_skin_tone"],"filename":"26f9-1f3ff","category":"skintone","batch":"21"},{"aliases":["basketball_woman_light_skin_tone"],"filename":"26f9-1f3fb-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["basketball_woman_medium_light_skin_tone"],"filename":"26f9-1f3fc-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["basketball_woman_medium_skin_tone"],"filename":"26f9-1f3fd-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["basketball_woman_medium_dark_skin_tone"],"filename":"26f9-1f3fe-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["basketball_woman_dark_skin_tone"],"filename":"26f9-1f3ff-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["basketball_man_light_skin_tone"],"filename":"26f9-1f3fb","category":"skintone","batch":"21"},{"aliases":["basketball_man_medium_light_skin_tone"],"filename":"26f9-1f3fc","category":"skintone","batch":"21"},{"aliases":["basketball_man_medium_skin_tone"],"filename":"26f9-1f3fd","category":"skintone","batch":"21"},{"aliases":["basketball_man_medium_dark_skin_tone"],"filename":"26f9-1f3fe","category":"skintone","batch":"21"},{"aliases":["basketball_man_dark_skin_tone"],"filename":"26f9-1f3ff","category":"skintone","batch":"21"},{"aliases":["basketball_man_light_skin_tone"],"filename":"26f9-1f3fb-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["basketball_man_medium_light_skin_tone"],"filename":"26f9-1f3fc-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["basketball_man_medium_skin_tone"],"filename":"26f9-1f3fd-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["basketball_man_medium_dark_skin_tone"],"filename":"26f9-1f3fe-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["basketball_man_dark_skin_tone"],"filename":"26f9-1f3ff-200d-2640-fe0f","category":"skintone","batch":"21"},{"aliases":["woman_playing_handball_light_skin_tone"],"filename":"1f93e-1f3fb","category":"skintone","batch":"21"},{"aliases":["woman_playing_handball_medium_light_skin_tone"],"filename":"1f93e-1f3fc","category":"skintone","batch":"21"},{"aliases":["woman_playing_handball_medium_skin_tone"],"filename":"1f93e-1f3fd","category":"skintone","batch":"21"},{"aliases":["woman_playing_handball_medium_dark_skin_tone"],"filename":"1f93e-1f3fe","category":"skintone","batch":"21"},{"aliases":["woman_playing_handball_dark_skin_tone"],"filename":"1f93e-1f3ff","category":"skintone","batch":"21"},{"aliases":["woman_playing_handball_light_skin_tone"],"filename":"1f93e-1f3fb-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["woman_playing_handball_medium_light_skin_tone"],"filename":"1f93e-1f3fc-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["woman_playing_handball_medium_skin_tone"],"filename":"1f93e-1f3fd-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["woman_playing_handball_medium_dark_skin_tone"],"filename":"1f93e-1f3fe-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["woman_playing_handball_dark_skin_tone"],"filename":"1f93e-1f3ff-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["man_playing_handball_light_skin_tone"],"filename":"1f93e-1f3fb-200d-2642-fe0f","category":"skintone","batch":"22"},{"aliases":["man_playing_handball_medium_light_skin_tone"],"filename":"1f93e-1f3fc-200d-2642-fe0f","category":"skintone","batch":"22"},{"aliases":["man_playing_handball_medium_skin_tone"],"filename":"1f93e-1f3fd-200d-2642-fe0f","category":"skintone","batch":"22"},{"aliases":["man_playing_handball_medium_dark_skin_tone"],"filename":"1f93e-1f3fe-200d-2642-fe0f","category":"skintone","batch":"22"},{"aliases":["man_playing_handball_dark_skin_tone"],"filename":"1f93e-1f3ff-200d-2642-fe0f","category":"skintone","batch":"22"},{"aliases":["golfing_woman_light_skin_tone"],"filename":"1f3cc-1f3fb","category":"skintone","batch":"22"},{"aliases":["golfing_woman_medium_light_skin_tone"],"filename":"1f3cc-1f3fc","category":"skintone","batch":"22"},{"aliases":["golfing_woman_medium_skin_tone"],"filename":"1f3cc-1f3fd","category":"skintone","batch":"22"},{"aliases":["golfing_woman_medium_dark_skin_tone"],"filename":"1f3cc-1f3fe","category":"skintone","batch":"22"},{"aliases":["golfing_woman_dark_skin_tone"],"filename":"1f3cc-1f3ff","category":"skintone","batch":"22"},{"aliases":["golfing_woman_light_skin_tone"],"filename":"1f3cc-1f3fb-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["golfing_woman_medium_light_skin_tone"],"filename":"1f3cc-1f3fc-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["golfing_woman_medium_skin_tone"],"filename":"1f3cc-1f3fd-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["golfing_woman_medium_dark_skin_tone"],"filename":"1f3cc-1f3fe-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["golfing_woman_dark_skin_tone"],"filename":"1f3cc-1f3ff-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["golfing_man_light_skin_tone"],"filename":"1f3cc-1f3fb","category":"skintone","batch":"22"},{"aliases":["golfing_man_medium_light_skin_tone"],"filename":"1f3cc-1f3fc","category":"skintone","batch":"22"},{"aliases":["golfing_man_medium_skin_tone"],"filename":"1f3cc-1f3fd","category":"skintone","batch":"22"},{"aliases":["golfing_man_medium_dark_skin_tone"],"filename":"1f3cc-1f3fe","category":"skintone","batch":"22"},{"aliases":["golfing_man_dark_skin_tone"],"filename":"1f3cc-1f3ff","category":"skintone","batch":"22"},{"aliases":["golfing_man_light_skin_tone"],"filename":"1f3cc-1f3fb-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["golfing_man_medium_light_skin_tone"],"filename":"1f3cc-1f3fc-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["golfing_man_medium_skin_tone"],"filename":"1f3cc-1f3fd-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["golfing_man_medium_dark_skin_tone"],"filename":"1f3cc-1f3fe-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["golfing_man_dark_skin_tone"],"filename":"1f3cc-1f3ff-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["surfing_woman_light_skin_tone"],"filename":"1f3c4-1f3fb","category":"skintone","batch":"22"},{"aliases":["surfing_woman_medium_light_skin_tone"],"filename":"1f3c4-1f3fc","category":"skintone","batch":"22"},{"aliases":["surfing_woman_medium_skin_tone"],"filename":"1f3c4-1f3fd","category":"skintone","batch":"22"},{"aliases":["surfing_woman_medium_dark_skin_tone"],"filename":"1f3c4-1f3fe","category":"skintone","batch":"22"},{"aliases":["surfing_woman_dark_skin_tone"],"filename":"1f3c4-1f3ff","category":"skintone","batch":"22"},{"aliases":["surfing_woman_light_skin_tone"],"filename":"1f3c4-1f3fb-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["surfing_woman_medium_light_skin_tone"],"filename":"1f3c4-1f3fc-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["surfing_woman_medium_skin_tone"],"filename":"1f3c4-1f3fd-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["surfing_woman_medium_dark_skin_tone"],"filename":"1f3c4-1f3fe-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["surfing_woman_dark_skin_tone"],"filename":"1f3c4-1f3ff-200d-2640-fe0f","category":"skintone","batch":"22"},{"aliases":["surfing_man_light_skin_tone"],"filename":"1f3c4-1f3fb","category":"skintone","batch":"22"},{"aliases":["surfing_man_medium_light_skin_tone"],"filename":"1f3c4-1f3fc","category":"skintone","batch":"22"},{"aliases":["surfing_man_medium_skin_tone"],"filename":"1f3c4-1f3fd","category":"skintone","batch":"22"},{"aliases":["surfing_man_medium_dark_skin_tone"],"filename":"1f3c4-1f3fe","category":"skintone","batch":"22"},{"aliases":["surfing_man_dark_skin_tone"],"filename":"1f3c4-1f3ff","category":"skintone","batch":"22"},{"aliases":["surfing_man_light_skin_tone"],"filename":"1f3c4-1f3fb-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["surfing_man_medium_light_skin_tone"],"filename":"1f3c4-1f3fc-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["surfing_man_medium_skin_tone"],"filename":"1f3c4-1f3fd-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["surfing_man_medium_dark_skin_tone"],"filename":"1f3c4-1f3fe-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["surfing_man_dark_skin_tone"],"filename":"1f3c4-1f3ff-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["swimming_woman_light_skin_tone"],"filename":"1f3ca-1f3fb","category":"skintone","batch":"23"},{"aliases":["swimming_woman_medium_light_skin_tone"],"filename":"1f3ca-1f3fc","category":"skintone","batch":"23"},{"aliases":["swimming_woman_medium_skin_tone"],"filename":"1f3ca-1f3fd","category":"skintone","batch":"23"},{"aliases":["swimming_woman_medium_dark_skin_tone"],"filename":"1f3ca-1f3fe","category":"skintone","batch":"23"},{"aliases":["swimming_woman_dark_skin_tone"],"filename":"1f3ca-1f3ff","category":"skintone","batch":"23"},{"aliases":["swimming_woman_light_skin_tone"],"filename":"1f3ca-1f3fb-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["swimming_woman_medium_light_skin_tone"],"filename":"1f3ca-1f3fc-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["swimming_woman_medium_skin_tone"],"filename":"1f3ca-1f3fd-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["swimming_woman_medium_dark_skin_tone"],"filename":"1f3ca-1f3fe-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["swimming_woman_dark_skin_tone"],"filename":"1f3ca-1f3ff-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["swimming_man_light_skin_tone"],"filename":"1f3ca-1f3fb","category":"skintone","batch":"23"},{"aliases":["swimming_man_medium_light_skin_tone"],"filename":"1f3ca-1f3fc","category":"skintone","batch":"23"},{"aliases":["swimming_man_medium_skin_tone"],"filename":"1f3ca-1f3fd","category":"skintone","batch":"23"},{"aliases":["swimming_man_medium_dark_skin_tone"],"filename":"1f3ca-1f3fe","category":"skintone","batch":"23"},{"aliases":["swimming_man_dark_skin_tone"],"filename":"1f3ca-1f3ff","category":"skintone","batch":"23"},{"aliases":["swimming_man_light_skin_tone"],"filename":"1f3ca-1f3fb-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["swimming_man_medium_light_skin_tone"],"filename":"1f3ca-1f3fc-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["swimming_man_medium_skin_tone"],"filename":"1f3ca-1f3fd-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["swimming_man_medium_dark_skin_tone"],"filename":"1f3ca-1f3fe-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["swimming_man_dark_skin_tone"],"filename":"1f3ca-1f3ff-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["woman_playing_water_polo_light_skin_tone"],"filename":"1f93d-1f3fb","category":"skintone","batch":"23"},{"aliases":["woman_playing_water_polo_medium_light_skin_tone"],"filename":"1f93d-1f3fc","category":"skintone","batch":"23"},{"aliases":["woman_playing_water_polo_medium_skin_tone"],"filename":"1f93d-1f3fd","category":"skintone","batch":"23"},{"aliases":["woman_playing_water_polo_medium_dark_skin_tone"],"filename":"1f93d-1f3fe","category":"skintone","batch":"23"},{"aliases":["woman_playing_water_polo_dark_skin_tone"],"filename":"1f93d-1f3ff","category":"skintone","batch":"23"},{"aliases":["woman_playing_water_polo_light_skin_tone"],"filename":"1f93d-1f3fb-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["woman_playing_water_polo_medium_light_skin_tone"],"filename":"1f93d-1f3fc-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["woman_playing_water_polo_medium_skin_tone"],"filename":"1f93d-1f3fd-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["woman_playing_water_polo_medium_dark_skin_tone"],"filename":"1f93d-1f3fe-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["woman_playing_water_polo_dark_skin_tone"],"filename":"1f93d-1f3ff-200d-2640-fe0f","category":"skintone","batch":"23"},{"aliases":["man_playing_water_polo_light_skin_tone"],"filename":"1f93d-1f3fb-200d-2642-fe0f","category":"skintone","batch":"23"},{"aliases":["man_playing_water_polo_medium_light_skin_tone"],"filename":"1f93d-1f3fc-200d-2642-fe0f","category":"skintone","batch":"23"},{"aliases":["man_playing_water_polo_medium_skin_tone"],"filename":"1f93d-1f3fd-200d-2642-fe0f","category":"skintone","batch":"23"},{"aliases":["man_playing_water_polo_medium_dark_skin_tone"],"filename":"1f93d-1f3fe-200d-2642-fe0f","category":"skintone","batch":"23"},{"aliases":["man_playing_water_polo_dark_skin_tone"],"filename":"1f93d-1f3ff-200d-2642-fe0f","category":"skintone","batch":"23"},{"aliases":["rowing_woman_light_skin_tone"],"filename":"1f6a3-1f3fb","category":"skintone","batch":"23"},{"aliases":["rowing_woman_medium_light_skin_tone"],"filename":"1f6a3-1f3fc","category":"skintone","batch":"23"},{"aliases":["rowing_woman_medium_skin_tone"],"filename":"1f6a3-1f3fd","category":"skintone","batch":"23"},{"aliases":["rowing_woman_medium_dark_skin_tone"],"filename":"1f6a3-1f3fe","category":"skintone","batch":"23"},{"aliases":["rowing_woman_dark_skin_tone"],"filename":"1f6a3-1f3ff","category":"skintone","batch":"23"},{"aliases":["rowing_woman_light_skin_tone"],"filename":"1f6a3-1f3fb-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["rowing_woman_medium_light_skin_tone"],"filename":"1f6a3-1f3fc-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["rowing_woman_medium_skin_tone"],"filename":"1f6a3-1f3fd-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["rowing_woman_medium_dark_skin_tone"],"filename":"1f6a3-1f3fe-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["rowing_woman_dark_skin_tone"],"filename":"1f6a3-1f3ff-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["rowing_man_light_skin_tone"],"filename":"1f6a3-1f3fb","category":"skintone","batch":"24"},{"aliases":["rowing_man_medium_light_skin_tone"],"filename":"1f6a3-1f3fc","category":"skintone","batch":"24"},{"aliases":["rowing_man_medium_skin_tone"],"filename":"1f6a3-1f3fd","category":"skintone","batch":"24"},{"aliases":["rowing_man_medium_dark_skin_tone"],"filename":"1f6a3-1f3fe","category":"skintone","batch":"24"},{"aliases":["rowing_man_dark_skin_tone"],"filename":"1f6a3-1f3ff","category":"skintone","batch":"24"},{"aliases":["rowing_man_light_skin_tone"],"filename":"1f6a3-1f3fb-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["rowing_man_medium_light_skin_tone"],"filename":"1f6a3-1f3fc-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["rowing_man_medium_skin_tone"],"filename":"1f6a3-1f3fd-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["rowing_man_medium_dark_skin_tone"],"filename":"1f6a3-1f3fe-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["rowing_man_dark_skin_tone"],"filename":"1f6a3-1f3ff-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["horse_racing_light_skin_tone"],"filename":"1f3c7-1f3fb","category":"skintone","batch":"24"},{"aliases":["horse_racing_medium_light_skin_tone"],"filename":"1f3c7-1f3fc","category":"skintone","batch":"24"},{"aliases":["horse_racing_medium_skin_tone"],"filename":"1f3c7-1f3fd","category":"skintone","batch":"24"},{"aliases":["horse_racing_medium_dark_skin_tone"],"filename":"1f3c7-1f3fe","category":"skintone","batch":"24"},{"aliases":["horse_racing_dark_skin_tone"],"filename":"1f3c7-1f3ff","category":"skintone","batch":"24"},{"aliases":["biking_woman_light_skin_tone"],"filename":"1f6b4-1f3fb","category":"skintone","batch":"24"},{"aliases":["biking_woman_medium_light_skin_tone"],"filename":"1f6b4-1f3fc","category":"skintone","batch":"24"},{"aliases":["biking_woman_medium_skin_tone"],"filename":"1f6b4-1f3fd","category":"skintone","batch":"24"},{"aliases":["biking_woman_medium_dark_skin_tone"],"filename":"1f6b4-1f3fe","category":"skintone","batch":"24"},{"aliases":["biking_woman_dark_skin_tone"],"filename":"1f6b4-1f3ff","category":"skintone","batch":"24"},{"aliases":["biking_woman_light_skin_tone"],"filename":"1f6b4-1f3fb-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["biking_woman_medium_light_skin_tone"],"filename":"1f6b4-1f3fc-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["biking_woman_medium_skin_tone"],"filename":"1f6b4-1f3fd-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["biking_woman_medium_dark_skin_tone"],"filename":"1f6b4-1f3fe-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["biking_woman_dark_skin_tone"],"filename":"1f6b4-1f3ff-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["biking_man_light_skin_tone"],"filename":"1f6b4-1f3fb","category":"skintone","batch":"24"},{"aliases":["biking_man_medium_light_skin_tone"],"filename":"1f6b4-1f3fc","category":"skintone","batch":"24"},{"aliases":["biking_man_medium_skin_tone"],"filename":"1f6b4-1f3fd","category":"skintone","batch":"24"},{"aliases":["biking_man_medium_dark_skin_tone"],"filename":"1f6b4-1f3fe","category":"skintone","batch":"24"},{"aliases":["biking_man_dark_skin_tone"],"filename":"1f6b4-1f3ff","category":"skintone","batch":"24"},{"aliases":["biking_man_light_skin_tone"],"filename":"1f6b4-1f3fb-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["biking_man_medium_light_skin_tone"],"filename":"1f6b4-1f3fc-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["biking_man_medium_skin_tone"],"filename":"1f6b4-1f3fd-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["biking_man_medium_dark_skin_tone"],"filename":"1f6b4-1f3fe-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["biking_man_dark_skin_tone"],"filename":"1f6b4-1f3ff-200d-2640-fe0f","category":"skintone","batch":"24"},{"aliases":["mountain_biking_woman_light_skin_tone"],"filename":"1f6b5-1f3fb","category":"skintone","batch":"24"},{"aliases":["mountain_biking_woman_medium_light_skin_tone"],"filename":"1f6b5-1f3fc","category":"skintone","batch":"24"},{"aliases":["mountain_biking_woman_medium_skin_tone"],"filename":"1f6b5-1f3fd","category":"skintone","batch":"24"},{"aliases":["mountain_biking_woman_medium_dark_skin_tone"],"filename":"1f6b5-1f3fe","category":"skintone","batch":"24"},{"aliases":["mountain_biking_woman_dark_skin_tone"],"filename":"1f6b5-1f3ff","category":"skintone","batch":"24"},{"aliases":["mountain_biking_woman_light_skin_tone"],"filename":"1f6b5-1f3fb-200d-2640-fe0f","category":"skintone","batch":"25"},{"aliases":["mountain_biking_woman_medium_light_skin_tone"],"filename":"1f6b5-1f3fc-200d-2640-fe0f","category":"skintone","batch":"25"},{"aliases":["mountain_biking_woman_medium_skin_tone"],"filename":"1f6b5-1f3fd-200d-2640-fe0f","category":"skintone","batch":"25"},{"aliases":["mountain_biking_woman_medium_dark_skin_tone"],"filename":"1f6b5-1f3fe-200d-2640-fe0f","category":"skintone","batch":"25"},{"aliases":["mountain_biking_woman_dark_skin_tone"],"filename":"1f6b5-1f3ff-200d-2640-fe0f","category":"skintone","batch":"25"},{"aliases":["mountain_biking_man_light_skin_tone"],"filename":"1f6b5-1f3fb","category":"skintone","batch":"25"},{"aliases":["mountain_biking_man_medium_light_skin_tone"],"filename":"1f6b5-1f3fc","category":"skintone","batch":"25"},{"aliases":["mountain_biking_man_medium_skin_tone"],"filename":"1f6b5-1f3fd","category":"skintone","batch":"25"},{"aliases":["mountain_biking_man_medium_dark_skin_tone"],"filename":"1f6b5-1f3fe","category":"skintone","batch":"25"},{"aliases":["mountain_biking_man_dark_skin_tone"],"filename":"1f6b5-1f3ff","category":"skintone","batch":"25"},{"aliases":["mountain_biking_man_light_skin_tone"],"filename":"1f6b5-1f3fb-200d-2640-fe0f","category":"skintone","batch":"25"},{"aliases":["mountain_biking_man_medium_light_skin_tone"],"filename":"1f6b5-1f3fc-200d-2640-fe0f","category":"skintone","batch":"25"},{"aliases":["mountain_biking_man_medium_skin_tone"],"filename":"1f6b5-1f3fd-200d-2640-fe0f","category":"skintone","batch":"25"},{"aliases":["mountain_biking_man_medium_dark_skin_tone"],"filename":"1f6b5-1f3fe-200d-2640-fe0f","category":"skintone","batch":"25"},{"aliases":["mountain_biking_man_dark_skin_tone"],"filename":"1f6b5-1f3ff-200d-2640-fe0f","category":"skintone","batch":"25"},{"aliases":["woman_juggling_light_skin_tone"],"filename":"1f939-1f3fb","category":"skintone","batch":"25"},{"aliases":["woman_juggling_medium_light_skin_tone"],"filename":"1f939-1f3fc","category":"skintone","batch":"25"},{"aliases":["woman_juggling_medium_skin_tone"],"filename":"1f939-1f3fd","category":"skintone","batch":"25"},{"aliases":["woman_juggling_medium_dark_skin_tone"],"filename":"1f939-1f3fe","category":"skintone","batch":"25"},{"aliases":["woman_juggling_dark_skin_tone"],"filename":"1f939-1f3ff","category":"skintone","batch":"25"},{"aliases":["woman_juggling_light_skin_tone"],"filename":"1f939-1f3fb-200d-2640-fe0f","category":"skintone","batch":"25"},{"aliases":["woman_juggling_medium_light_skin_tone"],"filename":"1f939-1f3fc-200d-2640-fe0f","category":"skintone","batch":"25"},{"aliases":["woman_juggling_medium_skin_tone"],"filename":"1f939-1f3fd-200d-2640-fe0f","category":"skintone","batch":"25"},{"aliases":["woman_juggling_medium_dark_skin_tone"],"filename":"1f939-1f3fe-200d-2640-fe0f","category":"skintone","batch":"25"},{"aliases":["woman_juggling_dark_skin_tone"],"filename":"1f939-1f3ff-200d-2640-fe0f","category":"skintone","batch":"25"},{"aliases":["man_juggling_light_skin_tone"],"filename":"1f939-1f3fb-200d-2642-fe0f","category":"skintone","batch":"25"},{"aliases":["man_juggling_medium_light_skin_tone"],"filename":"1f939-1f3fc-200d-2642-fe0f","category":"skintone","batch":"25"},{"aliases":["man_juggling_medium_skin_tone"],"filename":"1f939-1f3fd-200d-2642-fe0f","category":"skintone","batch":"25"},{"aliases":["man_juggling_medium_dark_skin_tone"],"filename":"1f939-1f3fe-200d-2642-fe0f","category":"skintone","batch":"25"},{"aliases":["man_juggling_dark_skin_tone"],"filename":"1f939-1f3ff-200d-2642-fe0f","category":"skintone","batch":"25"},{"aliases":["bath_light_skin_tone"],"filename":"1f6c0-1f3fb","category":"skintone","batch":"25"},{"aliases":["bath_medium_light_skin_tone"],"filename":"1f6c0-1f3fc","category":"skintone","batch":"25"},{"aliases":["bath_medium_skin_tone"],"filename":"1f6c0-1f3fd","category":"skintone","batch":"25"},{"aliases":["bath_medium_dark_skin_tone"],"filename":"1f6c0-1f3fe","category":"skintone","batch":"25"},{"aliases":["bath_dark_skin_tone"],"filename":"1f6c0-1f3ff","category":"skintone","batch":"25"}]
matterhorn.cabal view
@@ -1,5 +1,5 @@ name:                matterhorn-version:             50200.3.1+version:             50200.4.0 synopsis:            Terminal client for the Mattermost chat system description:         This is a terminal client for the Mattermost chat                      system. Please see the README for a list of@@ -25,6 +25,9 @@                      scripts/figlet                      scripts/rot13                      notification-scripts/notify+                     emoji/LICENSE.txt+                     emoji/emoji.json+                     emoji/NOTICE.txt  source-repository    head   type: git@@ -52,28 +55,34 @@                        State.Messages                        State.MessageSelect                        State.PostListOverlay+                       State.ReactionEmojiListOverlay                        State.Reactions                        State.Setup                        State.Setup.Threads                        State.Setup.Threads.Logging                        State.Themes                        State.UrlSelect+                       State.ListOverlay                        State.UserListOverlay+                       State.ChannelListOverlay                        State.Users                        Zipper                        Themes                        Draw+                       Emoji                        Draw.ManageAttachments                        Draw.Autocomplete                        Draw.Main                        Draw.Messages                        Draw.ChannelList+                       Draw.ChannelListOverlay+                       Draw.ReactionEmojiListOverlay                        Draw.ShowHelp                        Draw.LeaveChannelConfirm                        Draw.DeleteChannelConfirm                        Draw.PostListOverlay                        Draw.UserListOverlay-                       Draw.JoinChannel+                       Draw.ListOverlay                        Draw.URLList                        Draw.Util                        Draw.ViewMessage@@ -85,11 +94,12 @@                        Events.ShowHelp                        Events.MessageSelect                        Events.Main-                       Events.JoinChannel                        Events.ChannelSelect                        Events.PostListOverlay                        Events.UrlSelect                        Events.UserListOverlay+                       Events.ChannelListOverlay+                       Events.ReactionEmojiListOverlay                        Events.LeaveChannelConfirm                        Events.DeleteChannelConfirm                        Events.ViewMessage@@ -120,7 +130,7 @@                        NoImplicitPrelude   ghc-options:         -Wall -threaded -with-rtsopts=-I0 -Wcompat   build-depends:       base                 >=4.8     && <5-                     , mattermost-api       == 50200.1.4+                     , mattermost-api       == 50200.2.0                      , base-compat          >= 0.9    && < 0.11                      , unordered-containers >= 0.2    && < 0.3                      , containers           >= 0.5.7  && < 0.7@@ -194,8 +204,8 @@                     , filepath             >= 1.4    && < 1.5                     , hashable             >= 1.2    && < 1.3                     , Hclip                >= 3.0    && < 3.1-                    , mattermost-api       == 50200.1.4-                    , mattermost-api-qc    == 50200.1.4+                    , mattermost-api       == 50200.2.0+                    , mattermost-api-qc    == 50200.2.0                     , microlens-platform   >= 0.3    && < 0.4                     , mtl                  >= 2.2    && < 2.3                     , process              >= 1.4    && < 1.7
src/Command.hs view
@@ -32,6 +32,7 @@ import           State.PostListOverlay import           State.Themes import           State.UserListOverlay+import           State.ChannelListOverlay import           State.Users import           Types @@ -52,9 +53,11 @@  printArgSpec :: CmdArgs a -> Text printArgSpec NoArg = ""-printArgSpec (LineArg ts) = "[" <> ts <> "]"-printArgSpec (TokenArg t NoArg) = "[" <> t <> "]"-printArgSpec (TokenArg t rs) = "[" <> t <> "] " <> printArgSpec rs+printArgSpec (LineArg ts) = "<" <> ts <> ">"+printArgSpec (TokenArg t NoArg) = "<" <> t <> ">"+printArgSpec (UserArg rs) = "<" <> userSigil <> "user> " <> printArgSpec rs+printArgSpec (ChannelArg rs) = "<" <> normalChannelSigil <> "channel> " <> printArgSpec rs+printArgSpec (TokenArg t rs) = "<" <> t <> "> " <> printArgSpec rs  matchArgs :: CmdArgs a -> Text -> Either Text a matchArgs NoArg t = case unwordHead t of@@ -63,6 +66,16 @@     | not (T.all Char.isSpace as) -> Left ("Unexpected arguments '" <> t <> "'")     | otherwise -> Left ("Unexpected argument '" <> a <> "'") matchArgs (LineArg _) t = return t+matchArgs spec@(UserArg rs) t = case unwordHead t of+  Nothing -> case rs of+    NoArg -> Left ("Missing argument: " <> printArgSpec spec)+    _     -> Left ("Missing arguments: " <> printArgSpec spec)+  Just (a, as) -> (,) <$> pure a <*> matchArgs rs as+matchArgs spec@(ChannelArg rs) t = case unwordHead t of+  Nothing -> case rs of+    NoArg -> Left ("Missing argument: " <> printArgSpec spec)+    _     -> Left ("Missing arguments: " <> printArgSpec spec)+  Just (a, as) -> (,) <$> pure a <*> matchArgs rs as matchArgs spec@(TokenArg _ rs) t = case unwordHead t of   Nothing -> case rs of     NoArg -> Left ("Missing argument: " <> printArgSpec spec)@@ -102,10 +115,10 @@   , Cmd "leave" "Leave the current channel" NoArg $ \ () ->       startLeaveCurrentChannel -  , Cmd "join" "Browse the list of available channels" NoArg $ \ () ->-      startJoinChannel+  , Cmd "join" "Find a channel to join" NoArg $ \ () ->+      enterChannelListOverlayMode -  , Cmd "join" "Join the specified channel" (TokenArg "channel" NoArg) $ \(n, ()) ->+  , Cmd "join" "Join the specified channel" (ChannelArg NoArg) $ \(n, ()) ->       joinChannelByName n    , Cmd "theme" "List the available themes" NoArg $ \ () ->@@ -128,11 +141,11 @@         enterDMSearchUserList    , Cmd "msg" "Chat with the specified user"-    (TokenArg "user" NoArg) $ \ (name, ()) ->+    (UserArg NoArg) $ \ (name, ()) ->         changeChannelByName name    , Cmd "msg" "Go to a user's channel and send the specified message or command"-    (TokenArg "user" $ LineArg "message or command") $ \ (name, msg) -> do+    (UserArg $ LineArg "message or command") $ \ (name, msg) -> do         withFetchedUserMaybe (UserFetchByUsername name) $ \foundUser -> do             case foundUser of                 Just user -> createOrFocusDMChannel user $ Just $ \cId ->@@ -156,11 +169,11 @@         getLogDestination    , Cmd "add-user" "Add a user to the current channel"-    (TokenArg "username" NoArg) $ \ (uname, ()) ->+    (UserArg NoArg) $ \ (uname, ()) ->         addUserByNameToCurrentChannel uname    , Cmd "remove-user" "Remove a user from the current channel"-    (TokenArg "username" NoArg) $ \ (uname, ()) ->+    (UserArg NoArg) $ \ (uname, ()) ->         removeUserFromCurrentChannel uname    , Cmd "user" "Show users to initiate a private DM chat channel"@@ -176,7 +189,7 @@         toggleChannelListVisibility    , Cmd "focus" "Focus on a channel or user"-    (TokenArg "channel" NoArg) $ \ (name, ()) ->+    (ChannelArg NoArg) $ \ (name, ()) ->         changeChannelByName name    , Cmd "focus" "Select from available channels" NoArg $ \ () ->@@ -195,7 +208,7 @@       listScripts    , Cmd "group-msg" "Create a group chat"-    (LineArg "user1 user2 ...") createGroupChannel+    (LineArg (userSigil <> "user [" <> userSigil <> "user ...]")) createGroupChannel    , Cmd "sh" "Run a prewritten shell script"     (TokenArg "script" (LineArg "message")) $ \ (script, text) ->
src/Draw.hs view
@@ -2,15 +2,19 @@ {-# LANGUAGE PackageImports #-} module Draw (draw) where +import Prelude ()+import Prelude.MH+ import Brick  import Draw.DeleteChannelConfirm-import Draw.JoinChannel import Draw.LeaveChannelConfirm import Draw.Main import Draw.PostListOverlay import Draw.ShowHelp import Draw.UserListOverlay+import Draw.ChannelListOverlay+import Draw.ReactionEmojiListOverlay import Draw.ViewMessage import Draw.ManageAttachments import Types@@ -19,17 +23,18 @@ draw :: ChatState -> [Widget Name] draw st =     case appMode st of-        Main                       -> drawMain st-        UrlSelect                  -> drawMain st+        Main                       -> drawMain True st+        UrlSelect                  -> drawMain True st         ShowHelp topic             -> drawShowHelp topic st-        ChannelSelect              -> drawMain st+        ChannelSelect              -> drawMain True st         LeaveChannelConfirm        -> drawLeaveChannelConfirm st-        JoinChannel                -> drawJoinChannel st-        MessageSelect              -> drawMain st-        MessageSelectDeleteConfirm -> drawMain st+        MessageSelect              -> drawMain True st+        MessageSelectDeleteConfirm -> drawMain True st         DeleteChannelConfirm       -> drawDeleteChannelConfirm st         PostListOverlay contents   -> drawPostListOverlay contents st         UserListOverlay            -> drawUserListOverlay st+        ChannelListOverlay         -> drawChannelListOverlay st+        ReactionEmojiListOverlay   -> drawReactionEmojiListOverlay st         ViewMessage                -> drawViewMessage st         ManageAttachments          -> drawManageAttachments st         ManageAttachmentsBrowseFiles -> drawManageAttachments st
src/Draw/Autocomplete.hs view
@@ -65,6 +65,8 @@                          ]  renderAutocompleteFooterFor :: ClientChannel -> AutocompleteAlternative -> Maybe (Widget Name)+renderAutocompleteFooterFor _ (SpecialMention MentionChannel) = Nothing+renderAutocompleteFooterFor _ (SpecialMention MentionAll) = Nothing renderAutocompleteFooterFor ch (UserCompletion _ False) =     Just $ hBox [ txt $ "("                 , withDefAttr clientEmphAttr (txt userNotInChannelMarker)@@ -82,6 +84,10 @@ renderAutocompleteFooterFor _ _ = Nothing  renderAutocompleteAlternative :: Bool -> AutocompleteAlternative -> Widget Name+renderAutocompleteAlternative sel (EmojiCompletion e) =+    padRight Max $ renderEmojiCompletion sel e+renderAutocompleteAlternative sel (SpecialMention m) =+    padRight Max $ renderSpecialMention m sel renderAutocompleteAlternative sel (UserCompletion u inChan) =     padRight Max $ renderUserCompletion u inChan sel renderAutocompleteAlternative sel (ChannelCompletion inChan c) =@@ -90,6 +96,34 @@     padRight Max $ txt t renderAutocompleteAlternative _ (CommandCompletion n args desc) =     padRight Max $ renderCommandCompletion n args desc++renderSpecialMention :: SpecialMention -> Bool -> Widget Name+renderSpecialMention m sel =+    let usernameWidth = 18+        padTo n a = hLimit n $ vLimit 1 (a <+> fill ' ')+        maybeForce = if sel+                     then forceAttr listSelectedFocusedAttr+                     else id+        t = autocompleteAlternativeReplacement $ SpecialMention m+        desc = case m of+            MentionChannel -> "Notifies all users in this channel"+            MentionAll     -> "Mentions all users in this channel"+    in maybeForce $+       hBox [ txt "  "+            , padTo usernameWidth $ withDefAttr clientEmphAttr $ txt t+            , txt desc+            ]++renderEmojiCompletion :: Bool -> T.Text -> Widget Name+renderEmojiCompletion sel e =+    let maybeForce = if sel+                     then forceAttr listSelectedFocusedAttr+                     else id+    in maybeForce $+       padLeft (Pad 2) $+       withDefAttr emojiAttr $+       txt $+       autocompleteAlternativeReplacement $ EmojiCompletion e  renderUserCompletion :: User -> Bool -> Bool -> Widget Name renderUserCompletion u inChan selected =
+ src/Draw/ChannelListOverlay.hs view
@@ -0,0 +1,51 @@+module Draw.ChannelListOverlay+  ( drawChannelListOverlay+  )+where++import           Prelude ()+import           Prelude.MH++import           Brick+import qualified Data.Text as T+import           Text.Wrap ( defaultWrapSettings, preserveIndentation )++import           Network.Mattermost.Types+import           Network.Mattermost.Lenses++import           Draw.Main+import           Draw.ListOverlay ( drawListOverlay )+import           Types+import           Types.Common ( sanitizeUserText )+import           Themes+++drawChannelListOverlay :: ChatState -> [Widget Name]+drawChannelListOverlay st =+    let overlay = drawListOverlay (st^.csChannelListOverlay) channelSearchScopeHeader+                                  channelSearchScopeNoResults channelSearchScopePrompt+                                  renderChannel+    in joinBorders overlay : drawMain False st++channelSearchScopePrompt :: ChannelSearchScope -> Widget Name+channelSearchScopePrompt scope =+    txt $ case scope of+        AllChannels -> "Search channels:"++channelSearchScopeNoResults :: ChannelSearchScope -> Widget Name+channelSearchScopeNoResults scope =+    txt $ case scope of+        AllChannels -> "No matching channels found."++channelSearchScopeHeader :: ChannelSearchScope -> Widget Name+channelSearchScopeHeader scope =+    withDefAttr clientEmphAttr $ txt $ case scope of+        AllChannels -> "Join a Channel"++renderChannel :: Bool -> Channel -> Widget Name+renderChannel _ chan =+    let baseStr = (sanitizeUserText $ chan^.channelNameL) <> " (" <>+                  (sanitizeUserText $ chan^.channelDisplayNameL) <> ")"+        s = "  " <> (T.strip $ sanitizeUserText $ chan^.channelPurposeL)+    in (vLimit 1 $ padRight Max $ withDefAttr clientEmphAttr $ txt baseStr) <=>+       (vLimit 1 $ txtWrapWith (defaultWrapSettings { preserveIndentation = True }) s)
src/Draw/DeleteChannelConfirm.hs view
@@ -18,7 +18,7 @@  drawDeleteChannelConfirm :: ChatState -> [Widget Name] drawDeleteChannelConfirm st =-    confirmBox st : (forceAttr "invalid" <$> drawMain st)+    confirmBox st : (drawMain False st)  confirmBox :: ChatState -> Widget Name confirmBox st =
− src/Draw/JoinChannel.hs
@@ -1,56 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-module Draw.JoinChannel-    ( drawJoinChannel-    )-where--import           Prelude ()-import           Prelude.MH--import           Brick-import           Brick.Widgets.Border-import           Brick.Widgets.Center-import           Brick.Widgets.List-import qualified Data.Text as T-import qualified Data.Vector as V-import           Text.Wrap ( defaultWrapSettings, preserveIndentation )--import           Network.Mattermost.Lenses ( channelDisplayNameL , channelNameL-                                           , channelPurposeL )-import           Network.Mattermost.Types ( Channel )--import           Draw.Main-import           Themes-import           Types-import           Types.Common---drawJoinChannel :: ChatState -> [Widget Name]-drawJoinChannel st = (joinBorders $ joinChannelBox st) : (forceAttr "invalid" <$> drawMain st)--joinChannelBox :: ChatState -> Widget Name-joinChannelBox st =-    let chList = case st^.csJoinChannelList of-          Nothing -> center $ withDefAttr dialogEmphAttr $ txt "[Loading channel list]"-          Just chanList -> renderList renderJoinListItem True chanList-        highlight = withDefAttr dialogEmphAttr-        cur = maybe 0 ((+1) . fst) (listSelectedElement =<< st^.csJoinChannelList)-        len = maybe 0 (\l -> V.length $ l^.listElementsL) $ st^.csJoinChannelList-    in centerLayer $-       vLimit 20 $-       hLimit 60 $-       withDefAttr dialogAttr $-       borderWithLabel (txt "Join Channel") $-       vBox [ chList-            , hBorderWithLabel (str $ (show cur) <> "/" <> (show len))-            , hCenter $ txt "Use " <+> (highlight $ txt "arrow keys") <+> txt " and " <+>-                        (highlight $ txt "Enter") <+> txt " to select a channel"-            , hCenter $ txt "to join or press " <+> (highlight $ txt "Esc") <+> txt " to cancel."-            ]--renderJoinListItem :: Bool -> Channel -> Widget Name-renderJoinListItem _ chan =-    let baseStr = (sanitizeUserText $ chan^.channelNameL) <> " (" <> (sanitizeUserText $ chan^.channelDisplayNameL) <> ")"-        s = "  " <> (T.strip $ sanitizeUserText $ chan^.channelPurposeL)-    in (vLimit 1 $ padRight Max $ txt baseStr) <=>-       (vLimit 1 $ txtWrapWith (defaultWrapSettings { preserveIndentation = True }) s)
src/Draw/LeaveChannelConfirm.hs view
@@ -18,7 +18,7 @@  drawLeaveChannelConfirm :: ChatState -> [Widget Name] drawLeaveChannelConfirm st =-    confirmBox st : (forceAttr "invalid" <$> drawMain st)+    confirmBox st : (drawMain False st)  confirmBox :: ChatState -> Widget Name confirmBox st =
+ src/Draw/ListOverlay.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Draw.ListOverlay+  ( drawListOverlay+  )+where++import           Prelude ()+import           Prelude.MH++import           Brick+import           Brick.Widgets.Border+import           Brick.Widgets.Center+import           Brick.Widgets.Edit+import qualified Brick.Widgets.List as L+import           Control.Monad.Trans.Reader ( withReaderT )+import qualified Data.Foldable as F+import qualified Data.Text as T+import           Lens.Micro.Platform ( (%~) )++import           Themes+import           Types+++hLimitWithPadding :: Int -> Widget n -> Widget n+hLimitWithPadding pad contents = Widget+  { hSize  = Fixed+  , vSize  = (vSize contents)+  , render =+      withReaderT (& availWidthL  %~ (\ n -> n - (2 * pad))) $ render $ cropToContext contents+  }++-- | Draw a ListOverlayState. This draws a bordered box with the+-- overlay's search input and results list inside the box. The provided+-- functions determine how to render the overlay in various states.+drawListOverlay :: ListOverlayState a b+                -- ^ The overlay state+                -> (b -> Widget Name)+                -- ^ The function to build the window title from the+                -- current search scope+                -> (b -> Widget Name)+                -- ^ The function to generate a message for the search+                -- scope indicating that no results were found+                -> (b -> Widget Name)+                -- ^ The function to generate the editor prompt for the+                -- search scope+                -> (Bool -> a -> Widget Name)+                -- ^ The function to render an item from the overlay's+                -- list+                -> Widget Name+drawListOverlay st scopeHeader scopeNoResults scopePrompt renderItem =+  centerLayer $ hLimitWithPadding 10 $ vLimit 25 $+  borderWithLabel (scopeHeader scope) body+  where+      body = vBox [ (padRight (Pad 1) promptMsg) <+>+                    renderEditor (txt . T.unlines) True (st^.listOverlaySearchInput)+                  , cursorPositionBorder+                  , showResults+                  ]+      plural 1 = ""+      plural _ = "s"+      cursorPositionBorder =+          if st^.listOverlaySearching+          then hBorderWithLabel $ txt "[Searching...]"+          else case st^.listOverlaySearchResults.L.listSelectedL of+              Nothing -> hBorder+              Just _ ->+                  let msg = "Showing " <> show numSearchResults <> " result" <> plural numSearchResults+                  in hBorderWithLabel $ str $ "[" <> msg <> "]"++      scope = st^.listOverlaySearchScope+      promptMsg = scopePrompt scope++      showMessage = center . withDefAttr clientEmphAttr++      showResults+        | numSearchResults == 0 = showMessage $ scopeNoResults scope+        | otherwise = renderedUserList++      renderedUserList = L.renderList renderItem True (st^.listOverlaySearchResults)+      numSearchResults = F.length $ st^.listOverlaySearchResults.L.listElementsL
src/Draw/Main.hs view
@@ -253,6 +253,7 @@                           , mdRenderReplyParent = True                           , mdIndentBlocks      = False                           , mdThreadState       = NoThread+                          , mdShowReactions     = True                           }                         ]             _ -> emptyWidget@@ -399,12 +400,14 @@        (txt "Switch to channel [use ^ and $ to anchor]: ") <+>        (renderEditor (txt . T.concat) True e) -drawMain :: ChatState -> [Widget Name]-drawMain st =-    [ connectionLayer st-    , autocompleteLayer st-    , joinBorders $ mainInterface st-    ]+drawMain :: Bool -> ChatState -> [Widget Name]+drawMain useColor st =+    let maybeColor = if useColor then id else forceAttr "invalid"+    in maybeColor <$>+           [ connectionLayer st+           , autocompleteLayer st+           , joinBorders $ mainInterface st+           ]  connectionLayer :: ChatState -> Widget Name connectionLayer st =@@ -485,6 +488,10 @@                     , ev YankMessageEvent                     , "yank-code"                     )+                  , ( isReactable+                    , ev ReactToMessageEvent+                    , "react"+                    )                   ]         Just postMsg = getSelectedMessage st @@ -545,6 +552,7 @@                                   , mdRenderReplyParent = True                                   , mdIndentBlocks      = True                                   , mdThreadState       = NoThread+                                  , mdShowReactions     = True                                   }                  in (maybePreviewViewport msgPreview) <=>                     hBorderWithLabel (withDefAttr clientEmphAttr $ str "[Preview ↑]")
src/Draw/ManageAttachments.hs view
@@ -21,7 +21,7 @@  drawManageAttachments :: ChatState -> [Widget Name] drawManageAttachments st =-    topLayer : drawMain st+    topLayer : drawMain True st     where         topLayer = case appMode st of             ManageAttachments -> drawAttachmentList st
src/Draw/Messages.hs view
@@ -10,6 +10,7 @@ import           Brick import           Brick.Widgets.Border import           Control.Monad.Trans.Reader ( withReaderT )+import qualified Data.Foldable as F import qualified Graphics.Vty as Vty import           Lens.Micro.Platform ( (.~), to ) import           Network.Mattermost.Types ( ServerTime(..) )@@ -72,6 +73,7 @@               , mdRenderReplyParent = True               , mdIndentBlocks      = True               , mdThreadState       = threadState+              , mdShowReactions     = True               }         fullMsg =           case msg^.mUser of@@ -144,11 +146,34 @@     Widget Greedy Greedy $ do         ctx <- getContext         let targetHeight = ctx^.availHeightL-            renderBuild img (m, threadState) =-                render1HLimit doMsgRender (flip Vty.vertJoin) targetHeight img threadState m             doMsgRender = renderSingleMessage st hs editCutoff -        img <- foldM renderBuild Vty.emptyImage msgs+            newMessagesTransitions = filterMessages (isNewMessagesTransition . fst) msgs+            newMessageTransition = fst <$> (listToMaybe $ F.toList newMessagesTransitions)++            isBelow m transition = m^.mDate > transition^.mDate++            go :: Vty.Image -> DirectionalSeq Retrograde (Message, ThreadState) -> RenderM Name Vty.Image+            go img ms | messagesLength ms == 0 = return img+            go img ms = do+                let Just (m, threadState) = messagesHead ms+                    newMessagesAbove = maybe False (isBelow m) newMessageTransition+                newImg <- render1HLimit doMsgRender (flip Vty.vertJoin) targetHeight img threadState m+                -- If the new message fills the window, check whether+                -- there is still a "New Messages" transition that is+                -- not displayed. If there is, then we need to replace+                -- the top line of the new image with a "New Messages"+                -- indicator.+                if Vty.imageHeight newImg >= targetHeight && newMessagesAbove+                then do+                    transitionResult <- render $ withDefAttr newMessageTransitionAttr $+                                                 hBorderWithLabel (txt "New Messages ↑")+                    let newImg2 = Vty.vertJoin (transitionResult^.imageL)+                                               (Vty.cropTop (targetHeight - 1) newImg)+                    return newImg2+                else go newImg $ messagesDrop 1 ms++        img <- go Vty.emptyImage msgs         return $ emptyResult & imageL .~ (Vty.cropTop targetHeight img)  relaxHeight :: Context -> Context
src/Draw/PostListOverlay.hs view
@@ -33,7 +33,7 @@  drawPostListOverlay :: PostListContents -> ChatState -> [Widget Name] drawPostListOverlay contents st =-  (joinBorders $ drawPostsBox contents st) : (forceAttr "invalid" <$> drawMain st)+  (joinBorders $ drawPostsBox contents st) : (drawMain False st)  -- | Draw a PostListOverlay as a floating overlay on top of whatever -- is rendered beneath it
+ src/Draw/ReactionEmojiListOverlay.hs view
@@ -0,0 +1,37 @@+module Draw.ReactionEmojiListOverlay+  ( drawReactionEmojiListOverlay+  )+where++import           Prelude ()+import           Prelude.MH++import           Brick+import           Brick.Widgets.List ( listSelectedFocusedAttr )+import qualified Data.Text as T++import           Draw.Main+import           Draw.ListOverlay ( drawListOverlay )+import           Types+import           Themes+++drawReactionEmojiListOverlay :: ChatState -> [Widget Name]+drawReactionEmojiListOverlay st =+    let overlay = drawListOverlay (st^.csReactionEmojiListOverlay)+                                  (const $ txt "Search Emoji")+                                  (const $ txt "No matching emoji found.")+                                  (const $ txt "Search emoji:")+                                  renderEmoji+    in joinBorders overlay : drawMain False st++renderEmoji :: Bool -> (Bool, T.Text) -> Widget Name+renderEmoji sel (mine, e) =+    let maybeForce = if sel+                     then forceAttr listSelectedFocusedAttr+                     else id+    in maybeForce $+       padRight Max $+       hBox [ if mine then txt " * " else txt "   "+            , withDefAttr emojiAttr $ txt $ ":" <> e <> ":"+            ]
src/Draw/ShowHelp.hs view
@@ -28,6 +28,8 @@ import           Events.ShowHelp import           Events.UrlSelect import           Events.UserListOverlay+import           Events.ChannelListOverlay+import           Events.ReactionEmojiListOverlay import           Events.ManageAttachments import           Events.ViewMessage import           HelpTopics ( helpTopics )@@ -135,14 +137,15 @@           sectionText = mkKeybindEventSectionHelp keybindEventHelpMarkdown T.unlines mkHeading           mkHeading n =               "\n# " <> n <>-              "\n| Keybinding | Description |" <>-              "\n| ---------- | ----------- |"+              "\n| Keybinding | Event Name | Description |" <>+              "\n| ---------- | ---------- | ----------- |"  keybindingTextTable :: KeyConfig -> Text keybindingTextTable kc = keybindSectionStrings     where keybindSectionStrings = T.concat $ catMaybes $ sectionText <$> keybindSections kc-          sectionText = mkKeybindEventSectionHelp (keybindEventHelpText keybindingWidth) T.unlines mkHeading-          keybindingWidth = 20+          sectionText = mkKeybindEventSectionHelp (keybindEventHelpText keybindingWidth eventNameWidth) T.unlines mkHeading+          keybindingWidth = 15+          eventNameWidth = 30           mkHeading n =               "\n" <> n <>               "\n" <> (T.replicate (T.length n) "=")@@ -375,6 +378,8 @@     , ("Text Editing", editingKeybindings)     , ("Flagged Messages", postListOverlayKeybindings kc)     , ("User Listings", userListOverlayKeybindings kc)+    , ("Channel Search Window", channelListOverlayKeybindings kc)+    , ("Reaction Emoji Search Window", reactionEmojiListOverlayKeybindings kc)     , ("Message Viewer", viewMessageKeybindings kc)     , ("Attachment List", attachmentListKeybindings kc)     ]@@ -424,13 +429,15 @@             ]  keybindEventHelpMarkdown :: (Text, Text, [Text]) -> Text-keybindEventHelpMarkdown (_evName, desc, evs) =+keybindEventHelpMarkdown (evName, desc, evs) =     let quote s = "`" <> s <> "`"-    in "| " <> (T.intercalate ", " $ quote <$> evs) <> " | " <> desc <> " |"+    in "| " <> (T.intercalate ", " $ quote <$> evs) <> " | " <> quote evName <> " | " <> desc <> " |" -keybindEventHelpText :: Int -> (Text, Text, [Text]) -> Text-keybindEventHelpText width (_evName, desc, evs) =-    padTo width (T.intercalate ", " evs) <> " " <> desc+keybindEventHelpText :: Int -> Int -> (Text, Text, [Text]) -> Text+keybindEventHelpText width eventNameWidth (evName, desc, evs) =+    padTo width (T.intercalate ", " evs) <> " " <>+    padTo eventNameWidth evName <> " " <>+    desc  mkKeybindEventHelp :: [Keybinding] -> Maybe (Text, Text, [Text]) mkKeybindEventHelp ks@(KB desc _ _ (Just e):_) =
src/Draw/UserListOverlay.hs view
@@ -1,123 +1,75 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} -module Draw.UserListOverlay where+module Draw.UserListOverlay+  ( drawUserListOverlay+  )+where  import           Prelude () import           Prelude.MH  import           Brick-import           Brick.Widgets.Border-import           Brick.Widgets.Center-import           Brick.Widgets.Edit import qualified Brick.Widgets.List as L-import           Control.Monad.Trans.Reader ( withReaderT )-import qualified Data.Foldable as F import qualified Data.Text as T import qualified Graphics.Vty as V-import           Lens.Micro.Platform ( (%~) )  import           Draw.Main import           Draw.Util ( userSigilFromInfo )+import           Draw.ListOverlay ( drawListOverlay ) import           Themes import           Types  -hLimitWithPadding :: Int -> Widget n -> Widget n-hLimitWithPadding pad contents = Widget-  { hSize  = Fixed-  , vSize  = (vSize contents)-  , render =-      withReaderT (& availWidthL  %~ (\ n -> n - (2 * pad))) $ render $ cropToContext contents-  }- drawUserListOverlay :: ChatState -> [Widget Name] drawUserListOverlay st =-  (joinBorders $ drawUsersBox (st^.csUserListOverlay)) :-  (forceAttr "invalid" <$> drawMain st)---- | Draw a PostListOverlay as a floating overlay on top of whatever--- is rendered beneath it-drawUsersBox :: UserListOverlayState -> Widget Name-drawUsersBox st =-  centerLayer $ hLimitWithPadding 10 $ vLimit 25 $-  borderWithLabel contentHeader body-  where-      body = vBox [ (padRight (Pad 1) $ str promptMsg) <+>-                    renderEditor (txt . T.unlines) True (st^.userListSearchInput)-                  , cursorPositionBorder-                  , userResultList-                  ]-      plural 1 = ""-      plural _ = "s"-      cursorPositionBorder = case st^.userListSearchResults.L.listSelectedL of-          Nothing -> hBorder-          Just _ ->-              let msg = case st^.userListRequestingMore of-                          True -> "Fetching more results..."-                          False -> "Showing " <> show numSearchResults <> " result" <> plural numSearchResults-                          -- NOTE: one day when we resume doing-                          -- pagination, we want to reinstate the-                          -- following logic instead of the False case-                          -- above. Please see State.UserListOverlay for-                          -- details.-                          ---                          -- False -> case st^.userListHasAllResults of-                          --     True -> "Showing all results (" <> show numSearchResults <> ")"-                          --     False -> "Showing first " <>-                          --              show numSearchResults <>-                          --              " result" <> plural numSearchResults-              in hBorderWithLabel $ str $ "[" <> msg <> "]"--      scope = st^.userListSearchScope-      promptMsg = case scope of-          ChannelMembers _ _    -> "Search channel members:"-          ChannelNonMembers _ _ -> "Search users:"-          AllUsers Nothing      -> "Search users:"-          AllUsers (Just _)     -> "Search team members:"--      userResultList =-          if st^.userListSearching-          then showMessage "Searching..."-          else showResults--      showMessage = center . withDefAttr clientEmphAttr . str+    let overlay = drawListOverlay (st^.csUserListOverlay) userSearchScopeHeader+                                  userSearchScopeNoResults userSearchScopePrompt+                                  renderUser+    in joinBorders overlay : drawMain False st -      showResults-        | numSearchResults == 0 =-            showMessage $ case scope of-              ChannelMembers _ _    -> "No users in channel."-              ChannelNonMembers _ _ -> "All users in your team are already in this channel."-              AllUsers _            -> "No users found."-        | otherwise = renderedUserList+userSearchScopePrompt :: UserSearchScope -> Widget Name+userSearchScopePrompt scope =+    txt $ case scope of+        ChannelMembers _ _    -> "Search channel members:"+        ChannelNonMembers _ _ -> "Search users:"+        AllUsers Nothing      -> "Search users:"+        AllUsers (Just _)     -> "Search team members:" -      contentHeader = str $ case scope of-          ChannelMembers _ _    -> "Channel Members"-          ChannelNonMembers _ _ -> "Invite Users to Channel"-          AllUsers Nothing      -> "Users On This Server"-          AllUsers (Just _)     -> "Users In My Team"+userSearchScopeNoResults :: UserSearchScope -> Widget Name+userSearchScopeNoResults scope =+    txt $ case scope of+        ChannelMembers _ _    -> "No users in channel."+        ChannelNonMembers _ _ -> "All users in your team are already in this channel."+        AllUsers _            -> "No users found." -      renderedUserList = L.renderList renderUser True (st^.userListSearchResults)-      numSearchResults = F.length $ st^.userListSearchResults.L.listElementsL+userSearchScopeHeader :: UserSearchScope -> Widget Name+userSearchScopeHeader scope =+    txt $ case scope of+        ChannelMembers {}    -> "Channel Members"+        ChannelNonMembers {} -> "Invite Users to Channel"+        AllUsers Nothing     -> "Users On This Server"+        AllUsers (Just _)    -> "Users In My Team" -      sanitize = T.strip . T.replace "\t" " "-      usernameWidth = 20-      renderUser foc ui =-          (if foc then forceAttr L.listSelectedFocusedAttr else id) $-          vLimit 2 $-          padRight Max $-          hBox $ (padRight (Pad 1) $ colorUsername (ui^.uiName) (T.singleton $ userSigilFromInfo ui))-                 : (hLimit usernameWidth $ padRight Max $ colorUsername (ui^.uiName) (ui^.uiName))-                 : extras-          where-              extras = padRight (Pad 1) <$> catMaybes [mFullname, mNickname, mEmail]-              mFullname = if (not (T.null (ui^.uiFirstName)) || not (T.null (ui^.uiLastName)))-                          then Just $ txt $ (sanitize $ ui^.uiFirstName) <> " " <> (sanitize $ ui^.uiLastName)-                          else Nothing-              mNickname = case ui^.uiNickName of-                            Just n | n /= (ui^.uiName) -> Just $ txt $ "(" <> n <> ")"-                            _ -> Nothing-              mEmail = if (T.null $ ui^.uiEmail)-                       then Nothing-                       else Just $ modifyDefAttr (`V.withURL` ("mailto:" <> ui^.uiEmail)) $-                                   withDefAttr urlAttr (txt ("<" <> ui^.uiEmail <> ">"))+renderUser :: Bool -> UserInfo -> Widget Name+renderUser foc ui =+    (if foc then forceAttr L.listSelectedFocusedAttr else id) $+    vLimit 2 $+    padRight Max $+    hBox $ (padRight (Pad 1) $ colorUsername (ui^.uiName) (T.singleton $ userSigilFromInfo ui))+           : (hLimit usernameWidth $ padRight Max $ colorUsername (ui^.uiName) (ui^.uiName))+           : extras+    where+        sanitize = T.strip . T.replace "\t" " "+        usernameWidth = 20+        extras = padRight (Pad 1) <$> catMaybes [mFullname, mNickname, mEmail]+        mFullname = if (not (T.null (ui^.uiFirstName)) || not (T.null (ui^.uiLastName)))+                    then Just $ txt $ (sanitize $ ui^.uiFirstName) <> " " <> (sanitize $ ui^.uiLastName)+                    else Nothing+        mNickname = case ui^.uiNickName of+                      Just n | n /= (ui^.uiName) -> Just $ txt $ "(" <> n <> ")"+                      _ -> Nothing+        mEmail = if (T.null $ ui^.uiEmail)+                 then Nothing+                 else Just $ modifyDefAttr (`V.withURL` ("mailto:" <> ui^.uiEmail)) $+                             withDefAttr urlAttr (txt ("<" <> ui^.uiEmail <> ">"))
src/Draw/ViewMessage.hs view
@@ -10,6 +10,10 @@ import           Brick.Widgets.Border import           Brick.Widgets.Center import           Lens.Micro.Platform ( to )+import qualified Data.Set as S+import qualified Data.Map as M+import qualified Data.Text as T+import qualified Data.Foldable as F  import           Draw.Main import           Draw.Messages ( nameForUserRef )@@ -19,8 +23,14 @@   drawViewMessage :: ChatState -> [Widget Name]-drawViewMessage st = (viewMessageBox st) : (forceAttr "invalid" <$> drawMain st)+drawViewMessage st = (viewMessageBox st) : (drawMain False st) +maxWidth :: Int+maxWidth = 78++maxHeight :: Int+maxHeight = 25+ viewMessageBox :: ChatState -> Widget Name viewMessageBox st =     let body = case st^.csViewedMessage of@@ -30,29 +40,56 @@                   parent = case msg^.mInReplyToMsg of                        NotAReply -> Nothing                        InReplyTo pId -> getMessageForPostId st pId-              in cached ViewMessageArea $-                 renderMessage $ MessageData { mdEditThreshold     = Nothing-                                             , mdShowOlderEdits    = False-                                             , mdMessage           = msg-                                             , mdIsBot             = isBotMessage msg-                                             , mdUserName          = msg^.mUser.to (nameForUserRef st)-                                             , mdParentMessage     = parent-                                             , mdParentUserName    = parent >>= (^.mUser.to (nameForUserRef st))-                                             , mdRenderReplyParent = True-                                             , mdHighlightSet      = hs-                                             , mdIndentBlocks      = True-                                             , mdThreadState       = NoThread-                                             }+                  reactionsBody = reactionsText st msg+                  msgW = renderMessage $ MessageData { mdEditThreshold     = Nothing+                                                     , mdShowOlderEdits    = False+                                                     , mdMessage           = msg+                                                     , mdIsBot             = isBotMessage msg+                                                     , mdUserName          = msg^.mUser.to (nameForUserRef st)+                                                     , mdParentMessage     = parent+                                                     , mdParentUserName    = parent >>= (^.mUser.to (nameForUserRef st))+                                                     , mdRenderReplyParent = True+                                                     , mdHighlightSet      = hs+                                                     , mdIndentBlocks      = True+                                                     , mdThreadState       = NoThread+                                                     , mdShowReactions     = False+                                                     }+              in cached ViewMessageArea $ msgW <=> reactionsBody      in centerLayer $        Widget Fixed Fixed $ do            ctx <- getContext-           let maxWidth = 80-               maxHeight = 25-               width = min maxWidth (ctx^.availWidthL)+           let width = min (maxWidth + 2) (ctx^.availWidthL)                height = min maxHeight (ctx^.availHeightL)            render $ vLimit height $                     hLimit width $                     borderWithLabel (withDefAttr clientEmphAttr $ str "View Message") $                     viewport ViewMessageArea Both $                     body++reactionsText :: ChatState -> Message -> Widget Name+reactionsText st m =+    hLimit maxWidth body+    where+        body = case null reacList of+            True -> emptyWidget+            False -> padTop (Pad 1) $+                     vBox $+                     (hBorderWithLabel (withDefAttr clientEmphAttr $ txt "Reactions")) : (mkEntry <$> reacList)+        reacList = M.toList (m^.mReactions)+        mkEntry (reactionName, userIdSet) =+            let count = str $ "(" <> show (S.size userIdSet) <> ")"+                name = withDefAttr emojiAttr $ txt $ ":" <> reactionName <> ":"+                usernameList = usernameText userIdSet+            in (name <+> (padLeft (Pad 1) count)) <=>+               (padLeft (Pad 2) usernameList)++        hs = getHighlightSet st++        usernameText uids =+            renderText' hs $+            T.intercalate ", " $+            fmap (userSigil <>) $+            catMaybes (lookupUsername <$> F.toList uids)++        lookupUsername uid = usernameForUserId uid st
+ src/Emoji.hs view
@@ -0,0 +1,103 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Emoji+  ( EmojiCollection+  , loadEmoji+  , emptyEmojiCollection+  , getMatchingEmoji+  , matchesEmoji+  )+where++import           Prelude ()+import           Prelude.MH++import qualified Control.Exception as E+import           Control.Monad.Except+import qualified Data.Aeson as A+import qualified Data.ByteString.Lazy as BSL+import qualified Data.Foldable as F+import qualified Data.Text as T+import qualified Data.Sequence as Seq++import           Network.Mattermost.Types ( Session )+import qualified Network.Mattermost.Endpoints as MM+++newtype EmojiData = EmojiData (Seq.Seq T.Text)++-- | The collection of all emoji names we loaded from a JSON disk file.+-- You might rightly ask: why don't we use a Trie here, for efficient+-- lookups? The answer is that we need infix lookups; prefix matches are+-- not enough. In practice it seems not to matter that much; despite the+-- O(n) search we get good enough performance that we aren't worried+-- about this. If at some point this becomes an issue, other data+-- structures with good infix lookup performance should be identified+-- (full-text search, perhaps?).+newtype EmojiCollection = EmojiCollection [T.Text]++instance A.FromJSON EmojiData where+    parseJSON = A.withArray "EmojiData" $ \v -> do+        aliasVecs <- forM v $ \val ->+            flip (A.withObject "EmojiData Entry") val $ \obj -> do+                as <- obj A..: "aliases"+                forM as $ A.withText "Alias list element" return++        return $ EmojiData $ mconcat $ F.toList aliasVecs++emptyEmojiCollection :: EmojiCollection+emptyEmojiCollection = EmojiCollection mempty++-- | Load an EmojiCollection from a JSON disk file.+loadEmoji :: FilePath -> IO (Either String EmojiCollection)+loadEmoji path = runExceptT $ do+    result <- lift $ E.try $ BSL.readFile path+    case result of+        Left (e::E.SomeException) -> throwError $ show e+        Right bs -> do+            EmojiData es <- ExceptT $ return $ A.eitherDecode bs+            return $ EmojiCollection $ T.toLower <$> F.toList es++-- | Look up matching emoji in the collection using the provided search+-- string. This does a case-insensitive infix match. The search string+-- may be provided with or without leading and trailing colons.+lookupEmoji :: EmojiCollection -> T.Text -> [T.Text]+lookupEmoji (EmojiCollection es) search =+    filter (matchesEmoji search) es++-- | Match a search string against an emoji.+matchesEmoji :: T.Text+             -- ^ The search string (will be converted to lowercase and+             -- colons will be removed)+             -> T.Text+             -- ^ The emoji string (assumed to be lowercase and without+             -- leading/trailing colons)+             -> Bool+matchesEmoji searchString e =+    sanitizeEmojiSearch searchString `T.isInfixOf` e++sanitizeEmojiSearch :: T.Text -> T.Text+sanitizeEmojiSearch = stripColons . T.toLower . T.strip++-- | Perform an emoji search against both the local EmojiCollection as+-- well as the server's custom emoji. Return the results, sorted. If the+-- empty string is specified, all local and all custom emoji will be+-- included in the returned list.+getMatchingEmoji :: Session -> EmojiCollection -> T.Text -> IO [T.Text]+getMatchingEmoji session em rawSearchString = do+    let localAlts = lookupEmoji em rawSearchString+        sanitized = sanitizeEmojiSearch rawSearchString+    custom <- case T.null sanitized of+        True -> MM.mmGetListOfCustomEmoji Nothing Nothing session+        False -> MM.mmSearchCustomEmoji sanitized session+    return $ sort $ (MM.emojiName <$> custom) <> localAlts++stripColons :: T.Text -> T.Text+stripColons t =+    stripHeadColon $ stripTailColon t+    where+        stripHeadColon v = if ":" `T.isPrefixOf` v+                           then T.tail v+                           else v+        stripTailColon v = if ":" `T.isSuffixOf` v+                           then T.init v+                           else v
src/Events.hs view
@@ -32,7 +32,6 @@  import           Events.ChannelSelect import           Events.DeleteChannelConfirm-import           Events.JoinChannel import           Events.LeaveChannelConfirm import           Events.Main import           Events.MessageSelect@@ -40,12 +39,17 @@ import           Events.ShowHelp import           Events.UrlSelect import           Events.UserListOverlay+import           Events.ChannelListOverlay+import           Events.ReactionEmojiListOverlay import           Events.ViewMessage import           Events.ManageAttachments   onEvent :: ChatState -> BrickEvent Name MHEvent -> EventM Name (Next ChatState)-onEvent st ev = runMHEvent st $ onBrickEvent ev+onEvent st ev = runMHEvent st $ do+    onBrickEvent ev+    doPendingUserFetches+    doPendingUserStatusFetches  onBrickEvent :: BrickEvent Name MHEvent -> MH () onBrickEvent (AppEvent e) =@@ -158,12 +162,13 @@         ChannelSelect              -> onEventChannelSelect e         UrlSelect                  -> onEventUrlSelect e         LeaveChannelConfirm        -> onEventLeaveChannelConfirm e-        JoinChannel                -> onEventJoinChannel e         MessageSelect              -> onEventMessageSelect e         MessageSelectDeleteConfirm -> onEventMessageSelectDeleteConfirm e         DeleteChannelConfirm       -> onEventDeleteChannelConfirm e         PostListOverlay _          -> onEventPostListOverlay e         UserListOverlay            -> onEventUserListOverlay e+        ChannelListOverlay         -> onEventChannelListOverlay e+        ReactionEmojiListOverlay   -> onEventReactionEmojiListOverlay e         ViewMessage                -> onEventViewMessage e         ManageAttachments          -> onEventManageAttachments e         ManageAttachmentsBrowseFiles -> onEventManageAttachments e
+ src/Events/ChannelListOverlay.hs view
@@ -0,0 +1,28 @@+module Events.ChannelListOverlay+  ( onEventChannelListOverlay+  , channelListOverlayKeybindings+  )+where++import qualified Graphics.Vty as Vty++import           Events.Keybindings+import           State.ChannelListOverlay+import           State.ListOverlay+import           Types+++onEventChannelListOverlay :: Vty.Event -> MH ()+onEventChannelListOverlay =+    onEventListOverlay csChannelListOverlay channelListOverlayKeybindings++-- | The keybindings we want to use while viewing a channel list overlay+channelListOverlayKeybindings :: KeyConfig -> [Keybinding]+channelListOverlayKeybindings = mkKeybindings+    [ mkKb CancelEvent "Close the channel search list" (exitListOverlay csChannelListOverlay)+    , mkKb SearchSelectUpEvent "Select the previous channel" channelListSelectUp+    , mkKb SearchSelectDownEvent "Select the next channel" channelListSelectDown+    , mkKb PageDownEvent "Page down in the channel list" channelListPageDown+    , mkKb PageUpEvent "Page up in the channel list" channelListPageUp+    , mkKb ActivateListItemEvent "Join the selected channel" (listOverlayActivateCurrent csChannelListOverlay)+    ]
− src/Events/JoinChannel.hs
@@ -1,43 +0,0 @@-module Events.JoinChannel where--import           Prelude ()-import           Prelude.MH--import           Brick.Widgets.List-import qualified Graphics.Vty as Vty-import           Lens.Micro.Platform ( (.=) )--import           Network.Mattermost.Types ( getId )--import           State.Channels ( joinChannel )-import           Types---joinChannelListKeys :: [Vty.Key]-joinChannelListKeys =-    [ Vty.KUp-    , Vty.KDown-    , Vty.KPageUp-    , Vty.KPageDown-    , Vty.KHome-    , Vty.KEnd-    ]--onEventJoinChannel :: Vty.Event -> MH ()-onEventJoinChannel e@(Vty.EvKey k []) | k `elem` joinChannelListKeys = do-    chList <- use csJoinChannelList-    result <- case chList of-        Nothing -> return Nothing-        Just l -> Just <$> mh (handleListEvent e l)-    csJoinChannelList .= result-onEventJoinChannel (Vty.EvKey Vty.KEnter []) = do-    chList <- use csJoinChannelList-    case chList of-        Nothing -> return ()-        Just l -> case listSelectedElement l of-            Nothing -> return ()-            Just (_, chan) -> joinChannel (getId chan)-onEventJoinChannel (Vty.EvKey Vty.KEsc []) = do-    setMode Main-onEventJoinChannel _ = do-    return ()
src/Events/Keybindings.hs view
@@ -146,6 +146,7 @@         DeleteMessageEvent  -> [ key 'd' ]         EditMessageEvent    -> [ key 'e' ]         ReplyMessageEvent   -> [ key 'r' ]+        ReactToMessageEvent -> [ key 'a' ]         OpenMessageURLEvent -> [ key 'o' ]          AttachmentListAddEvent    -> [ key 'a' ]
src/Events/Main.hs view
@@ -5,12 +5,7 @@ import           Prelude.MH  import           Brick.Widgets.Edit-import qualified Brick.Widgets.List as L-import           Data.Char ( isSpace )-import qualified Data.Text.Zipper as Z-import qualified Data.Text.Zipper.Generic.Words as Z import qualified Graphics.Vty as Vty-import           Lens.Micro.Platform ( (%=), to, _Just )  import           Command import           Events.Keybindings@@ -151,38 +146,3 @@     , mkKb EnterFlaggedPostsEvent "View currently flagged posts"          enterFlaggedPostListMode     ]--data Direction = Forwards | Backwards--tabComplete :: Direction -> MH ()-tabComplete dir = do-    let transform list =-            let len = list^.L.listElementsL.to length-            in case dir of-                Forwards ->-                    if (L.listSelected list == Just (len - 1)) ||-                       (L.listSelected list == Nothing && len > 0)-                    then L.listMoveTo 0 list-                    else L.listMoveBy 1 list-                Backwards ->-                    if (L.listSelected list == Just 0) ||-                       (L.listSelected list == Nothing && len > 0)-                    then L.listMoveTo (len - 1) list-                    else L.listMoveBy (-1) list-    csEditState.cedAutocomplete._Just.acCompletionList %= transform--    mac <- use (csEditState.cedAutocomplete)-    case mac of-        Nothing -> return ()-        Just ac -> do-            case ac^.acCompletionList.to L.listSelectedElement of-                Nothing -> return ()-                Just (_, alternative) -> do-                    let replacement = autocompleteAlternativeReplacement alternative-                        maybeEndOfWord z =-                            if maybe True isSpace (Z.currentChar z)-                            then z-                            else Z.moveWordRight z-                    csEditState.cedEditor %=-                        applyEdit (Z.insertMany replacement . Z.deletePrevWord .-                                   maybeEndOfWord)
src/Events/MessageSelect.hs view
@@ -8,6 +8,7 @@  import           Events.Keybindings import           State.MessageSelect+import           State.ReactionEmojiListOverlay import           Types  @@ -71,5 +72,8 @@      , mkKb FillGapEvent "Fetch messages for the selected gap"          fillSelectedGap++    , mkKb ReactToMessageEvent "Post a reaction to the selected message"+         enterReactionEmojiListOverlayMode      ]
+ src/Events/ReactionEmojiListOverlay.hs view
@@ -0,0 +1,28 @@+module Events.ReactionEmojiListOverlay+  ( onEventReactionEmojiListOverlay+  , reactionEmojiListOverlayKeybindings+  )+where++import qualified Graphics.Vty as Vty++import           Events.Keybindings+import           State.ReactionEmojiListOverlay+import           State.ListOverlay+import           Types+++onEventReactionEmojiListOverlay :: Vty.Event -> MH ()+onEventReactionEmojiListOverlay =+    onEventListOverlay csReactionEmojiListOverlay reactionEmojiListOverlayKeybindings++-- | The keybindings we want to use while viewing an emoji list overlay+reactionEmojiListOverlayKeybindings :: KeyConfig -> [Keybinding]+reactionEmojiListOverlayKeybindings = mkKeybindings+    [ mkKb CancelEvent "Close the emoji search window" (exitListOverlay csReactionEmojiListOverlay)+    , mkKb SearchSelectUpEvent "Select the previous emoji" reactionEmojiListSelectUp+    , mkKb SearchSelectDownEvent "Select the next emoji" reactionEmojiListSelectDown+    , mkKb PageDownEvent "Page down in the emoji list" reactionEmojiListPageDown+    , mkKb PageUpEvent "Page up in the emoji list" reactionEmojiListPageUp+    , mkKb ActivateListItemEvent "Post the selected emoji reaction" (listOverlayActivateCurrent csReactionEmojiListOverlay)+    ]
src/Events/UserListOverlay.hs view
@@ -1,37 +1,24 @@ module Events.UserListOverlay where -import           Prelude ()-import           Prelude.MH--import           Brick.Widgets.Edit ( handleEditorEvent ) import qualified Graphics.Vty as Vty  import           Events.Keybindings import           State.UserListOverlay+import           State.ListOverlay import           Types   onEventUserListOverlay :: Vty.Event -> MH () onEventUserListOverlay =-  handleKeyboardEvent userListOverlayKeybindings $ \e -> do-      -- Get the editor content before the event.-      before <- userListSearchString--      -- Handle the editor input event.-      mhHandleEventLensed (csUserListOverlay.userListSearchInput) handleEditorEvent e--      -- Get the editor content after the event. If the string changed,-      -- start a new search.-      after <- userListSearchString-      when (before /= after) resetUserListSearch+    onEventListOverlay csUserListOverlay userListOverlayKeybindings  -- | The keybindings we want to use while viewing a user list overlay userListOverlayKeybindings :: KeyConfig -> [Keybinding] userListOverlayKeybindings = mkKeybindings-  [ mkKb CancelEvent "Close the user search list" exitUserListMode-  , mkKb SearchSelectUpEvent "Select the previous user" userListSelectUp-  , mkKb SearchSelectDownEvent "Select the next user" userListSelectDown-  , mkKb PageDownEvent "Page down in the user list" userListPageDown-  , mkKb PageUpEvent "Page up in the user list" userListPageUp-  , mkKb ActivateListItemEvent "Interact with the selected user" userListActivateCurrent-  ]+    [ mkKb CancelEvent "Close the user search list" (exitListOverlay csUserListOverlay)+    , mkKb SearchSelectUpEvent "Select the previous user" userListSelectUp+    , mkKb SearchSelectDownEvent "Select the next user" userListSelectDown+    , mkKb PageDownEvent "Page down in the user list" userListPageDown+    , mkKb PageUpEvent "Page up in the user list" userListPageUp+    , mkKb ActivateListItemEvent "Interact with the selected user" (listOverlayActivateCurrent csUserListOverlay)+    ]
src/FilePaths.hs view
@@ -12,6 +12,9 @@   , locateConfig   , xdgSyntaxDir   , syntaxDirName+  , userEmojiJsonPath+  , bundledEmojiJsonPath+  , emojiJsonFilename    , Script(..)   , locateScriptPath@@ -29,11 +32,12 @@                         , getPermissions                         , executable                         )+import System.Environment ( getExecutablePath ) import System.Environment.XDG.BaseDir ( getUserConfigFile                                       , getAllConfigFiles                                       , getUserConfigDir                                       )-import System.FilePath ( (</>), takeBaseName )+import System.FilePath ( (</>), takeBaseName, takeDirectory, splitPath, joinPath )   xdgName :: String@@ -59,6 +63,37 @@ -- The path does not necessarily exist. xdgSyntaxDir :: IO FilePath xdgSyntaxDir = (</> syntaxDirName) <$> getUserConfigDir xdgName++-- | Get the XDG path to the user-specific emoji JSON file. The path+-- does not necessarily exist.+userEmojiJsonPath :: IO FilePath+userEmojiJsonPath = (</> emojiJsonFilename) <$> getUserConfigDir xdgName++-- | Get the emoji JSON path relative to the development binary location+-- or the release binary location.+bundledEmojiJsonPath :: IO FilePath+bundledEmojiJsonPath = do+    selfPath <- getExecutablePath+    let distDir = "dist-newstyle/"+        pathBits = splitPath selfPath++    return $ if distDir `elem` pathBits+             then+                 -- We're in development, so use the development+                 -- executable path to locate the emoji path in the+                 -- development tree.+                 (joinPath $ takeWhile (/= distDir) pathBits) </> emojiDirName </> emojiJsonFilename+             else+                 -- In this case we assume the binary is being run from+                 -- a release, in which case the syntax directory is a+                 -- sibling of the executable path.+                 takeDirectory selfPath </> emojiDirName </> emojiJsonFilename++emojiJsonFilename :: FilePath+emojiJsonFilename = "emoji.json"++emojiDirName :: FilePath+emojiDirName = "emoji"  syntaxDirName :: FilePath syntaxDirName = "syntax"
src/Login.hs view
@@ -81,6 +81,7 @@ import           Types ( ConnectionInfo(..)                        , ciPassword, ciUsername, ciHostname                        , ciPort, AuthenticationException(..)+                       , LogManager, LogCategory(..), ioLogWithManager                        )  @@ -164,6 +165,8 @@             -- ^ The function used to set the logger on the             -- ConnectionData that results from a successful login             -- attempt.+            -> LogManager+            -- ^ The log manager used to do logging.             -> Bool             -- ^ Whether the login attempts should use HTTP (True) or             -- not (False, HTTPS only)@@ -172,11 +175,12 @@             -> BChan LoginEvent             -- ^ The channel to which we'll send login attempt results.             -> IO ()-loginWorker setLogger unsafeUseHTTP requestChan respChan = forever $ do+loginWorker setLogger logMgr unsafeUseHTTP requestChan respChan = forever $ do     req <- readBChan requestChan     case req of         DoLogin initial connInfo -> do             writeBChan respChan $ StartConnect initial $ connInfo^.ciHostname+            ioLogWithManager logMgr Nothing LogGeneral $ "Attempting authentication to " <> connInfo^.ciHostname              let connectFunc = if unsafeUseHTTP                               then initConnectionDataInsecure@@ -189,11 +193,17 @@              result <- convertLoginExceptions $ mmLogin cd login             case result of-                Left e ->+                Left e -> do+                    ioLogWithManager logMgr Nothing LogGeneral $+                        "Error authenticating to " <> connInfo^.ciHostname <> ": " <> (T.pack $ show e)                     writeBChan respChan $ LoginResult $ AttemptFailed e-                Right (Left e) ->+                Right (Left e) -> do+                    ioLogWithManager logMgr Nothing LogGeneral $+                        "Error authenticating to " <> connInfo^.ciHostname <> ": " <> (T.pack $ show e)                     writeBChan respChan $ LoginResult $ AttemptFailed $ LoginError e-                Right (Right (sess, user)) ->+                Right (Right (sess, user)) -> do+                    ioLogWithManager logMgr Nothing LogGeneral $+                        "Authenticated successfully to " <> connInfo^.ciHostname <> " as " <> connInfo^.ciUsername                     writeBChan respChan $ LoginResult $ AttemptSucceeded connInfo cd sess user  -- | The amount of time that the startup timer thread will wait before@@ -229,6 +239,8 @@                            -> (ConnectionData -> ConnectionData)                            -- ^ The function to set the logger on                            -- connection handles.+                           -> LogManager+                           -- ^ The log manager used to do logging.                            -> ConnectionInfo                            -- ^ Initial connection info to use to                            -- populate the login form. If the connection@@ -236,11 +248,11 @@                            -- initial connection attempt is made without                            -- first getting the user to hit Enter.                            -> IO (Maybe LoginAttempt, Vty)-interactiveGetLoginSession vty mkVty unsafeUseHTTP setLogger initialConfig = do+interactiveGetLoginSession vty mkVty unsafeUseHTTP setLogger logMgr initialConfig = do     requestChan <- newBChan 10     respChan <- newBChan 10 -    void $ forkIO $ loginWorker setLogger unsafeUseHTTP requestChan respChan+    void $ forkIO $ loginWorker setLogger logMgr unsafeUseHTTP requestChan respChan     void $ forkIO $ startupTimer respChan      let initialState = mkState initialConfig requestChan
src/Markdown.hs view
@@ -103,6 +103,7 @@ data MessageData = MessageData   { mdEditThreshold     :: Maybe ServerTime   , mdShowOlderEdits    :: Bool+  , mdShowReactions     :: Bool   , mdMessage           :: Message   , mdUserName          :: Maybe Text   , mdIsBot             :: Bool@@ -183,12 +184,13 @@                  [ B.txt ("  [attached: `" <> a^.attachmentName <> "`]")                  | a <- toList (msg^.mAttachments)                  ]-        msgReac = if Map.null (msg^.mReactions)+        msgReac = if Map.null (msg^.mReactions) || (not mdShowReactions)           then Nothing-          else let renderR e 1 = " [" <> e <> "]"-                   renderR e n-                     | n > 1     = " [" <> e <> " " <> T.pack (show n) <> "]"-                     | otherwise = ""+          else let renderR e us =+                       let n = Set.size us+                       in if | n == 1    -> " [" <> e <> "]"+                             | n > 1     -> " [" <> e <> " " <> T.pack (show n) <> "]"+                             | otherwise -> ""                    reactionMsg = Map.foldMapWithKey renderR (msg^.mReactions)                in Just $ B.withDefAttr emojiAttr $ B.txt ("   " <> reactionMsg)         withParent p =@@ -474,7 +476,7 @@         gatherStrings s n rs =           let s' = removeCursor s           in case viewl rs of-            _ | (userSigil `T.isPrefixOf` s' && (T.drop 1 s' `Set.member` uSet)) ->+            _ | (userSigil `T.isPrefixOf` s' && (T.toLower (T.drop 1 s') `Set.member` uSet)) ->                 buildString s n <| separate hSet rs             _ | (normalChannelSigil `T.isPrefixOf` s' && (T.drop 1 s' `Set.member` cSet)) ->                 buildString s n <| separate hSet rs@@ -493,7 +495,7 @@                     textWidth s' > 2 ->                       Fragment (TStr s) Emoji                   | Just uname <- userSigil `T.stripPrefix` removeCursor s-                    , removeCursor uname `Set.member` uSet ->+                    , removeCursor (T.toLower uname) `Set.member` uSet ->                       Fragment (TStr s) (User uname)                   | normalChannelSigil `T.isPrefixOf` (removeCursor s) &&                     (T.drop 1 (removeCursor s) `Set.member` cSet) ->
src/State/Autocomplete.hs view
@@ -1,5 +1,6 @@ module State.Autocomplete-  ( checkForAutocompletion+  ( AutocompleteContext(..)+  , checkForAutocompletion   ) where @@ -26,16 +27,36 @@  import {-# SOURCE #-} Command ( commandList, printArgSpec ) import           State.Common+import {-# SOURCE #-} State.Editing ( Direction(..), tabComplete ) import           Types hiding ( newState )+import           Emoji  +data AutocompleteContext =+    AutocompleteContext { autocompleteManual :: Bool+                        -- ^ Whether the autocompletion was manual+                        -- (True) or automatic (False). The automatic+                        -- case is the case where the autocomplete+                        -- lookups and UI are triggered merely by+                        -- entering some initial text (such as "@").+                        -- The manual case is the case where the+                        -- autocomplete lookups and UI are triggered+                        -- explicitly by a user's TAB keypress.+                        , autocompleteFirstMatch :: Bool+                        -- ^ Once the results of the autocomplete lookup+                        -- are available, this flag determines whether+                        -- the user's input is replaced immediately+                        -- with the first available match (True) or not+                        -- (False).+                        }+ -- | Check for whether the currently-edited word in the message editor -- should cause an autocompletion UI to appear. If so, initiate a server -- query or local cache lookup to present the completion alternatives -- for the word at the cursor.-checkForAutocompletion :: MH ()-checkForAutocompletion = do-    result <- getCompleterForInput+checkForAutocompletion :: AutocompleteContext -> MH ()+checkForAutocompletion ctx = do+    result <- getCompleterForInput ctx     case result of         Nothing -> resetAutocomplete         Just (runUpdater, searchString) -> do@@ -44,10 +65,10 @@                                prevResult             when shouldUpdate $ do                 csEditState.cedAutocompletePending .= Just searchString-                runUpdater searchString+                runUpdater ctx searchString -getCompleterForInput :: MH (Maybe (Text -> MH (), Text))-getCompleterForInput = do+getCompleterForInput :: AutocompleteContext -> MH (Maybe (AutocompleteContext -> Text -> MH (), Text))+getCompleterForInput ctx = do     z <- use (csEditState.cedEditor.editContentsL)      let col = snd $ Z.cursorPosition z@@ -59,24 +80,37 @@                 Just (doUserAutoCompletion, T.tail w)             | normalChannelSigil `T.isPrefixOf` w ->                 Just (doChannelAutoCompletion, T.tail w)+            | ":" `T.isPrefixOf` w && autocompleteManual ctx ->+                Just (doEmojiAutoCompletion, T.tail w)             | "```" `T.isPrefixOf` w ->                 Just (doSyntaxAutoCompletion, T.drop 3 w)             | "/" `T.isPrefixOf` w && startCol == 0 ->                 Just (doCommandAutoCompletion, T.tail w)         _ -> Nothing -doSyntaxAutoCompletion :: Text -> MH ()-doSyntaxAutoCompletion searchString = do+doEmojiAutoCompletion :: AutocompleteContext -> Text -> MH ()+doEmojiAutoCompletion ctx searchString = do+    session <- getSession+    em <- use (csResources.crEmoji)+    let label = "Emoji"+    withCachedAutocompleteResults ctx label searchString $+        doAsyncWith Preempt $ do+            results <- getMatchingEmoji session em searchString+            let alts = EmojiCompletion <$> results+            return $ Just $ setCompletionAlternatives ctx searchString alts label++doSyntaxAutoCompletion :: AutocompleteContext -> Text -> MH ()+doSyntaxAutoCompletion ctx searchString = do     mapping <- use (csResources.crSyntaxMap)     let allNames = Sky.sShortname <$> M.elems mapping         (prefixed, notPrefixed) = partition isPrefixed $ filter match allNames         match = (((T.toLower searchString) `T.isInfixOf`) . T.toLower)         isPrefixed = (((T.toLower searchString) `T.isPrefixOf`) . T.toLower)         alts = SyntaxCompletion <$> (sort prefixed <> sort notPrefixed)-    setCompletionAlternatives searchString alts "Languages"+    setCompletionAlternatives ctx searchString alts "Languages" -doCommandAutoCompletion :: Text -> MH ()-doCommandAutoCompletion searchString = do+doCommandAutoCompletion :: AutocompleteContext -> Text -> MH ()+doCommandAutoCompletion ctx searchString = do     let alts = mkAlt <$> sortBy compareCommands (filter matches commandList)         compareCommands a b =             let isAPrefix = searchString `T.isPrefixOf` cmdName a@@ -91,13 +125,15 @@                     lowerSearch `T.isInfixOf` (T.toLower $ cmdDescr c)         mkAlt (Cmd name desc args _) =             CommandCompletion name (printArgSpec args) desc-    setCompletionAlternatives searchString alts "Commands"+    setCompletionAlternatives ctx searchString alts "Commands"  -- | Attempt to re-use a cached autocomplete alternative list for -- a given search string. If the cache contains no such entry (keyed -- on search string), run the specified action, which is assumed to be -- responsible for fetching the completion results from the server.-withCachedAutocompleteResults :: Text+withCachedAutocompleteResults :: AutocompleteContext+                              -- ^ The autocomplete context+                              -> Text                               -- ^ The autocomplete UI label for the                               -- results to be used                               -> Text@@ -106,24 +142,24 @@                               -> MH ()                               -- ^ The action to execute on a cache miss                               -> MH ()-withCachedAutocompleteResults label searchString act = do+withCachedAutocompleteResults ctx label searchString act = do     mCache <- preuse (csEditState.cedAutocomplete._Just.acCachedResponses)      -- Does the cache have results for this search string? If so, use     -- them; otherwise invoke the specified action.     case HM.lookup searchString =<< mCache of-        Just alts -> setCompletionAlternatives searchString alts label+        Just alts -> setCompletionAlternatives ctx searchString alts label         Nothing -> act -doUserAutoCompletion :: Text -> MH ()-doUserAutoCompletion searchString = do+doUserAutoCompletion :: AutocompleteContext -> Text -> MH ()+doUserAutoCompletion ctx searchString = do     session <- getSession     myTid <- gets myTeamId     myUid <- gets myUserId     cId <- use csCurrentChannelId     let label = "Users" -    withCachedAutocompleteResults label searchString $+    withCachedAutocompleteResults ctx label searchString $         doAsyncWith Preempt $ do             ac <- MM.mmAutocompleteUsers (Just myTid) (Just cId) searchString session @@ -133,26 +169,33 @@                        (maybe mempty (fmap (\u -> UserCompletion u False) . active) $                               MM.userAutocompleteOutOfChannel ac) -            return $ Just $ setCompletionAlternatives searchString alts label+                specials = [ MentionAll+                           , MentionChannel+                           ]+                extras = [ SpecialMention m | m <- specials+                         , (T.toLower searchString) `T.isInfixOf` specialMentionName m+                         ] -doChannelAutoCompletion :: Text -> MH ()-doChannelAutoCompletion searchString = do+            return $ Just $ setCompletionAlternatives ctx searchString (extras <> alts) label++doChannelAutoCompletion :: AutocompleteContext -> Text -> MH ()+doChannelAutoCompletion ctx searchString = do     session <- getSession     tId <- gets myTeamId     let label = "Channels"     cs <- use csChannels -    withCachedAutocompleteResults label searchString $ do+    withCachedAutocompleteResults ctx label searchString $ do         doAsyncWith Preempt $ do             results <- MM.mmAutocompleteChannels tId searchString session             let alts = F.toList $ (ChannelCompletion True <$> inChannels) <>                                   (ChannelCompletion False <$> notInChannels)                 (inChannels, notInChannels) = Seq.partition isMember results                 isMember c = isJust $ findChannelById (channelId c) cs-            return $ Just $ setCompletionAlternatives searchString alts label+            return $ Just $ setCompletionAlternatives ctx searchString alts label -setCompletionAlternatives :: Text -> [AutocompleteAlternative] -> Text -> MH ()-setCompletionAlternatives searchString alts ty = do+setCompletionAlternatives :: AutocompleteContext -> Text -> [AutocompleteAlternative] -> Text -> MH ()+setCompletionAlternatives ctx searchString alts ty = do     let list = L.list CompletionList (V.fromList $ F.toList alts) 1         state = AutocompleteState { _acPreviousSearchString = searchString                                   , _acCompletionList =@@ -177,6 +220,9 @@                 in Just newState              mh $ vScrollToBeginning $ viewportScroll CompletionList++            when (autocompleteFirstMatch ctx) $+                tabComplete Forwards         _ ->             -- Do not update the state if this result does not             -- correspond to the search string we used most recently.
+ src/State/ChannelListOverlay.hs view
@@ -0,0 +1,78 @@+module State.ChannelListOverlay+  ( enterChannelListOverlayMode++  , channelListSelectDown+  , channelListSelectUp+  , channelListPageDown+  , channelListPageUp+  )+where++import           Prelude ()+import           Prelude.MH++import qualified Brick.Widgets.List as L+import qualified Data.Vector as Vec+import qualified Data.Sequence as Seq+import           Data.Function ( on )++import           Network.Mattermost.Types+import qualified Network.Mattermost.Endpoints as MM++import           State.ListOverlay+import           State.Channels+import           Types+++enterChannelListOverlayMode :: MH ()+enterChannelListOverlayMode = do+    myTId <- gets myTeamId+    enterListOverlayMode csChannelListOverlay ChannelListOverlay+        AllChannels enterHandler (fetchResults myTId)++enterHandler :: Channel -> MH Bool+enterHandler chan = do+    joinChannel (getId chan)+    return True++fetchResults :: TeamId+             -> ChannelSearchScope+             -- ^ The scope to search+             -> Session+             -- ^ The connection session+             -> Text+             -- ^ The search string+             -> IO (Vec.Vector Channel)+fetchResults myTId AllChannels session searchString = do+    resultChans <- MM.mmSearchChannels myTId searchString session+    -- chans <- Seq.filter (\ c -> not (channelId c `elem` myChannels)) <$> loop mempty 0+    let sortedChans = Vec.fromList $ toList $ Seq.sortBy (compare `on` channelName) resultChans+    return sortedChans++-- | Move the selection up in the channel list overlay by one channel.+channelListSelectUp :: MH ()+channelListSelectUp = channelListMove L.listMoveUp++-- | Move the selection down in the channel list overlay by one channel.+channelListSelectDown :: MH ()+channelListSelectDown = channelListMove L.listMoveDown++-- | Move the selection up in the channel list overlay by a page of channels+-- (channelListPageSize).+channelListPageUp :: MH ()+channelListPageUp = channelListMove (L.listMoveBy (-1 * channelListPageSize))++-- | Move the selection down in the channel list overlay by a page of channels+-- (channelListPageSize).+channelListPageDown :: MH ()+channelListPageDown = channelListMove (L.listMoveBy channelListPageSize)++-- | Transform the channel list results in some way, e.g. by moving the+-- cursor, and then check to see whether the modification warrants a+-- prefetch of more search results.+channelListMove :: (L.List Name Channel -> L.List Name Channel) -> MH ()+channelListMove = listOverlayMove csChannelListOverlay++-- | The number of channels in a "page" for cursor movement purposes.+channelListPageSize :: Int+channelListPageSize = 10
src/State/Channels.hs view
@@ -39,7 +39,6 @@   , startLeaveCurrentChannel   , joinChannel   , joinChannelByName-  , startJoinChannel   , changeChannelByName   , setChannelTopic   , beginCurrentChannelDeleteConfirm@@ -53,20 +52,16 @@  import           Brick.Main ( viewportScroll, vScrollToBeginning, invalidateCache, invalidateCacheEntry ) import           Brick.Widgets.Edit ( applyEdit, getEditContents, editContentsL )-import           Brick.Widgets.List ( list ) import           Control.Concurrent.Async ( runConcurrently, Concurrently(..) )-import qualified Control.Concurrent.STM as STM import           Control.Exception ( SomeException, try ) import           Data.Char ( isAlphaNum ) import qualified Data.HashMap.Strict as HM import qualified Data.Foldable as F-import           Data.Function ( on ) import           Data.List ( nub ) import           Data.Maybe ( fromJust ) import qualified Data.Set as S import qualified Data.Sequence as Seq import qualified Data.Text as T-import qualified Data.Vector as V import           Data.Text.Zipper ( textZipper, clearZipper, insertMany, gotoEOL ) import           Data.Time.Clock ( getCurrentTime ) import           Lens.Micro.Platform@@ -105,11 +100,10 @@     config <- use (csResources.crConfiguration)     csFocus %= Z.updateList (mkChannelZipperList now config cconfig prefs cs us) -    -- Send the new zipper to the status update thread so that we can-    -- fetch the statuses for the users in the new sidebar.+    -- Schedule the current sidebar for user status updates at the end+    -- of this MH action.     newZ <- use csFocus-    statusChan <- use (csResources.crStatusUpdateChan)-    liftIO $ STM.atomically $ STM.writeTChan statusChan newZ+    scheduleUserStatusFetches newZ      -- If the zipper rebuild caused the current channel to change, such     -- as when the previously-focused channel was removed, we need to@@ -907,30 +901,6 @@         return $ Just $ case result of             Left (_::SomeException) -> mhError $ NoSuchChannel rawName             Right chan -> joinChannel $ getId chan--startJoinChannel :: MH ()-startJoinChannel = do-    session <- getSession-    myTId <- gets myTeamId-    myChannels <- use (csChannels.to (filteredChannelIds (const True)))-    doAsyncWith Preempt $ do-        -- We don't get to just request all channels, so we request channels in-        -- chunks of 50.  A better UI might be to request an initial set and-        -- then wait for the user to demand more.-        let fetchCount     = 50-            loop acc start = do-              newChans <- MM.mmGetPublicChannels myTId (Just start) (Just fetchCount) session-              let chans = acc <> newChans-              if length newChans < fetchCount-                then return chans-                else loop chans (start+1)-        chans <- Seq.filter (\ c -> not (channelId c `elem` myChannels)) <$> loop mempty 0-        let sortedChans = V.fromList $ toList $ Seq.sortBy (compare `on` channelName) chans-        return $ Just $ do-            csJoinChannelList .= (Just $ list JoinChannelList sortedChans 2)--    setMode JoinChannel-    csJoinChannelList .= Nothing  -- | If the user is not a member of the specified channel, submit a -- request to join it. Otherwise switch to the channel.
src/State/Common.hs view
@@ -14,8 +14,11 @@   , postErrorMessage'   , addEmoteFormatting   , removeEmoteFormatting-  , fetchUsersByUsername +  , fetchMentionedUsers+  , doPendingUserFetches+  , doPendingUserStatusFetches+   , module State.Async   ) where@@ -62,7 +65,7 @@ -- This also sets the mFlagged field of each message based on whether -- its post ID is a flagged post according to crFlaggedPosts at the time -- of this call.-installMessagesFromPosts :: Posts -> MH (Messages, Set.Set Text)+installMessagesFromPosts :: Posts -> MH Messages installMessagesFromPosts postCollection = do   flags <- use (csResources.crFlaggedPosts) @@ -78,9 +81,10 @@       addNext cp (msgs, us) =           let (msg, mUsernames) = clientPostToMessage cp           in (addMessage (maybeFlag flags msg) msgs, Set.union us mUsernames)-      postsToMessages = foldr addNext (noMessages, mempty)+      (ms, mentions) = foldr addNext (noMessages, mempty) clientPosts -  return $ postsToMessages clientPosts+  fetchMentionedUsers mentions+  return ms     where         maybeFlag flagSet msg           | Just (MessagePostId pId) <- msg^.mMessageId, pId `Set.member` flagSet@@ -308,18 +312,70 @@ addEmoteFormatting :: T.Text -> T.Text addEmoteFormatting t = "*" <> t <> "*" +fetchMentionedUsers :: Set.Set MentionedUser -> MH ()+fetchMentionedUsers ms+    | Set.null ms = return ()+    | otherwise = do+        let convertMention (UsernameMention u) = UserFetchByUsername u+            convertMention (UserIdMention i) = UserFetchById i+        scheduleUserFetches $ convertMention <$> Set.toList ms++doPendingUserStatusFetches :: MH ()+doPendingUserStatusFetches = do+    mz <- getScheduledUserStatusFetches+    case mz of+        Nothing -> return ()+        Just z -> do+            statusChan <- use (csResources.crStatusUpdateChan)+            liftIO $ STM.atomically $ STM.writeTChan statusChan z++doPendingUserFetches :: MH ()+doPendingUserFetches = do+    fs <- getScheduledUserFetches++    let getUsername (UserFetchByUsername u) = Just u+        getUsername _ = Nothing++        getUserId (UserFetchById i) = Just i+        getUserId _ = Nothing++    fetchUsers (catMaybes $ getUsername <$> fs) (catMaybes $ getUserId <$> fs)+ -- | Given a list of usernames, ensure that we have a user record for -- each one in the state, either by confirming that a local record -- exists or by issuing a request for user records.-fetchUsersByUsername :: [Text] -> MH ()-fetchUsersByUsername [] = return ()-fetchUsersByUsername usernames = do+fetchUsers :: [Text] -> [UserId] -> MH ()+fetchUsers rawUsernames uids = do     st <- use id     session <- getSession-    let missing = filter (\n -> (not $ T.null n) && (isNothing $ userByUsername n st)) usernames-    when (not $ null missing) $ do-        mhLog LogGeneral $ T.pack $ "fetchUsersByUsername: getting " <> show usernames+    let usernames = trimUserSigil <$> rawUsernames+        missingUsernames = filter isMissing usernames+        isMissing n = and [ not $ T.null n+                          , not $ isSpecialMention n+                          , isNothing $ userByUsername n st+                          ]+        missingIds = filter (\i -> isNothing $ userById i st) uids++    when (not $ null missingUsernames) $ do+        mhLog LogGeneral $ T.pack $ "fetchUsers: getting " <> show missingUsernames++    when (not $ null missingIds) $ do+        mhLog LogGeneral $ T.pack $ "fetchUsers: getting " <> show missingIds++    when ((not $ null missingUsernames) || (not $ null missingIds)) $ do         doAsyncWith Normal $ do-            results <- mmGetUsersByUsernames (Seq.fromList missing) session-            return $ Just $ do-                forM_ results (\u -> addNewUser $ userInfoFromUser u True)+            act1 <- case null missingUsernames of+                True -> return $ return ()+                False -> do+                    results <- mmGetUsersByUsernames (Seq.fromList missingUsernames) session+                    return $ do+                        forM_ results (\u -> addNewUser $ userInfoFromUser u True)++            act2 <- case null missingIds of+                True -> return $ return ()+                False -> do+                    results <- mmGetUsersByIds (Seq.fromList missingIds) session+                    return $ do+                        forM_ results (\u -> addNewUser $ userInfoFromUser u True)++            return $ Just $ act1 >> act2
src/State/Editing.hs view
@@ -11,13 +11,15 @@   , handleEditingInput   , cancelAutocompleteOrReplyOrEdit   , replyToLatestMessage+  , Direction(..)+  , tabComplete   ) where  import           Prelude () import           Prelude.MH -import           Brick.Main ( invalidateCache )+import           Brick.Main ( invalidateCache, invalidateCacheEntry ) import           Brick.Widgets.Edit ( Editor, applyEdit , handleEditorEvent                                     , getEditContents, editContentsL ) import qualified Brick.Widgets.List as L@@ -25,6 +27,7 @@ import           Control.Arrow import qualified Control.Concurrent.STM as STM import qualified Data.ByteString as BS+import           Data.Char ( isSpace ) import qualified Data.Foldable as F import qualified Data.Set as S import qualified Data.Text as T@@ -33,7 +36,7 @@ import qualified Data.Text.Zipper.Generic.Words as Z import           Data.Time ( getCurrentTime ) import           Graphics.Vty ( Event(..), Key(..), Modifier(..) )-import           Lens.Micro.Platform ( (%=), (.=), (.~), to )+import           Lens.Micro.Platform ( (%=), (.=), (.~), to, _Just ) import qualified System.Environment as Sys import qualified System.Exit as Sys import qualified System.IO as Sys@@ -306,7 +309,10 @@               sendUserTypingAction             | otherwise -> return () -    checkForAutocompletion+    let ctx = AutocompleteContext { autocompleteManual = False+                                  , autocompleteFirstMatch = False+                                  }+    checkForAutocompletion ctx     liftIO $ resetSpellCheckTimer $ st^.csEditState  -- | Send the user_typing action to the server asynchronously, over the@@ -394,6 +400,8 @@  cancelAutocompleteOrReplyOrEdit :: MH () cancelAutocompleteOrReplyOrEdit = do+    cId <- use csCurrentChannelId+    mh $ invalidateCacheEntry $ ChannelMessages cId     ac <- use (csEditState.cedAutocomplete)     case ac of         Just _ -> do@@ -414,5 +422,46 @@     Just msg | isReplyable msg ->         do let Just p = msg^.mOriginalPost            setMode Main+           cId <- use csCurrentChannelId+           mh $ invalidateCacheEntry $ ChannelMessages cId            csEditState.cedEditMode .= Replying msg p     _ -> return ()++data Direction = Forwards | Backwards++tabComplete :: Direction -> MH ()+tabComplete dir = do+    let transform list =+            let len = list^.L.listElementsL.to length+            in case dir of+                Forwards ->+                    if (L.listSelected list == Just (len - 1)) ||+                       (L.listSelected list == Nothing && len > 0)+                    then L.listMoveTo 0 list+                    else L.listMoveBy 1 list+                Backwards ->+                    if (L.listSelected list == Just 0) ||+                       (L.listSelected list == Nothing && len > 0)+                    then L.listMoveTo (len - 1) list+                    else L.listMoveBy (-1) list+    csEditState.cedAutocomplete._Just.acCompletionList %= transform++    mac <- use (csEditState.cedAutocomplete)+    case mac of+        Nothing -> do+            let ctx = AutocompleteContext { autocompleteManual = True+                                          , autocompleteFirstMatch = True+                                          }+            checkForAutocompletion ctx+        Just ac -> do+            case ac^.acCompletionList.to L.listSelectedElement of+                Nothing -> return ()+                Just (_, alternative) -> do+                    let replacement = autocompleteAlternativeReplacement alternative+                        maybeEndOfWord z =+                            if maybe True isSpace (Z.currentChar z)+                            then z+                            else Z.moveWordRight z+                    csEditState.cedEditor %=+                        applyEdit (Z.insertMany replacement . Z.deletePrevWord .+                                   maybeEndOfWord)
+ src/State/Editing.hs-boot view
@@ -0,0 +1,10 @@+module State.Editing+  ( Direction(..)+  , tabComplete+  )+where++import Types ( MH )++data Direction = Forwards | Backwards+tabComplete :: Direction -> MH ()
+ src/State/ListOverlay.hs view
@@ -0,0 +1,138 @@+{-# LANGUAGE RankNTypes #-}+module State.ListOverlay+  ( listOverlayActivateCurrent+  , listOverlaySearchString+  , listOverlayMove+  , exitListOverlay+  , enterListOverlayMode+  , resetListOverlaySearch+  , onEventListOverlay+  )+where++import           Prelude ()+import           Prelude.MH++import qualified Brick.Widgets.List as L+import qualified Brick.Widgets.Edit as E+import qualified Data.Text.Zipper as Z+import qualified Data.Vector as Vec+import           Lens.Micro.Platform ( Lens', (%=), (.=) )+import           Network.Mattermost.Types ( Session )+import qualified Graphics.Vty as Vty++import           Types+import           State.Common+import           Events.Keybindings ( KeyConfig, Keybinding, handleKeyboardEvent )+++-- | Activate the specified list overlay's selected item by invoking the+-- overlay's configured enter keypress handler function.+listOverlayActivateCurrent :: Lens' ChatState (ListOverlayState a b) -> MH ()+listOverlayActivateCurrent which = do+  mItem <- L.listSelectedElement <$> use (which.listOverlaySearchResults)+  case mItem of+      Nothing -> return ()+      Just (_, val) -> do+          handler <- use (which.listOverlayEnterHandler)+          activated <- handler val+          if activated+             then setMode Main+             else return ()++-- | Get the current search string for the specified overlay.+listOverlaySearchString :: Lens' ChatState (ListOverlayState a b) -> MH Text+listOverlaySearchString which =+    (head . E.getEditContents) <$> use (which.listOverlaySearchInput)++-- | Move the list cursor in the specified overlay.+listOverlayMove :: Lens' ChatState (ListOverlayState a b)+                -- ^ Which overlay+                -> (L.List Name a -> L.List Name a)+                -- ^ How to transform the list in the overlay+                -> MH ()+listOverlayMove which how = which.listOverlaySearchResults %= how++-- | Clear the state of the specified list overlay and return to the+-- Main mode.+exitListOverlay :: Lens' ChatState (ListOverlayState a b)+                -- ^ Which overlay to reset+                -> MH ()+exitListOverlay which = do+    newList <- use (which.listOverlayNewList)+    which.listOverlaySearchResults .= newList mempty+    which.listOverlayEnterHandler .= (const $ return False)+    setMode Main++-- | Initialize a list overlay with the specified arguments and switch+-- to the specified mode.+enterListOverlayMode :: (Lens' ChatState (ListOverlayState a b))+                     -- ^ Which overlay to initialize+                     -> Mode+                     -- ^ The mode to change to+                     -> b+                     -- ^ The overlay's initial search scope+                     -> (a -> MH Bool)+                     -- ^ The overlay's enter keypress handler+                     -> (b -> Session -> Text -> IO (Vec.Vector a))+                     -- ^ The overlay's results fetcher function+                     -> MH ()+enterListOverlayMode which mode scope enterHandler fetcher = do+    which.listOverlaySearchScope .= scope+    which.listOverlaySearchInput.E.editContentsL %= Z.clearZipper+    which.listOverlayEnterHandler .= enterHandler+    which.listOverlayFetchResults .= fetcher+    which.listOverlaySearching .= False+    newList <- use (which.listOverlayNewList)+    which.listOverlaySearchResults .= newList mempty+    setMode mode+    resetListOverlaySearch which++-- | Reset the overlay's search by initiating a new search request for+-- the string that is currently in the overlay's editor. This does+-- nothing if a search for this overlay is already in progress.+resetListOverlaySearch :: Lens' ChatState (ListOverlayState a b) -> MH ()+resetListOverlaySearch which = do+    searchPending <- use (which.listOverlaySearching)++    when (not searchPending) $ do+        searchString <- listOverlaySearchString which+        which.listOverlaySearching .= True+        newList <- use (which.listOverlayNewList)+        session <- getSession+        scope <- use (which.listOverlaySearchScope)+        fetcher <- use (which.listOverlayFetchResults)+        doAsyncWith Preempt $ do+            results <- fetcher scope session searchString+            return $ Just $ do+                which.listOverlaySearchResults .= newList results+                which.listOverlaySearching .= False++                -- Now that the results are available, check to see if the+                -- search string changed since this request was submitted.+                -- If so, issue another search.+                afterSearchString <- listOverlaySearchString which+                when (searchString /= afterSearchString) $ resetListOverlaySearch which++-- | Generically handle an event for the list overlay state targeted+-- by the specified lens. Automatically dispatches new searches in the+-- overlay's editor if the editor contents change.+onEventListOverlay :: Lens' ChatState (ListOverlayState a b)+                   -- ^ Which overlay to dispatch to?+                   -> (KeyConfig -> [Keybinding])+                   -- ^ The keybinding builder+                   -> Vty.Event+                   -- ^ The event+                   -> MH ()+onEventListOverlay which keybindings =+    handleKeyboardEvent keybindings $ \e -> do+        -- Get the editor content before the event.+        before <- listOverlaySearchString which++        -- Handle the editor input event.+        mhHandleEventLensed (which.listOverlaySearchInput) E.handleEditorEvent e++        -- Get the editor content after the event. If the string changed,+        -- start a new search.+        after <- listOverlaySearchString which+        when (before /= after) $ resetListOverlaySearch which
src/State/MessageSelect.hs view
@@ -44,9 +44,17 @@ import           Types.Common  +-- | In these modes, we allow access to the selected message state.+messageSelectCompatibleModes :: [Mode]+messageSelectCompatibleModes =+    [ MessageSelect+    , MessageSelectDeleteConfirm+    , ReactionEmojiListOverlay+    ]+ getSelectedMessage :: ChatState -> Maybe Message getSelectedMessage st-    | appMode st /= MessageSelect && appMode st /= MessageSelectDeleteConfirm = Nothing+    | not (appMode st `elem` messageSelectCompatibleModes) = Nothing     | otherwise = do         selMsgId <- selectMessageId $ st^.csMessageSelect         let chanMsgs = st ^. csCurrentChannel . ccContents . cdMessages
src/State/Messages.hs view
@@ -143,7 +143,7 @@     chan . ccContents . cdMessages . traversed . filtered isEditedMessage .= msg     mh $ invalidateCacheEntry (ChannelMessages $ new^.postChannelIdL) -    fetchUsersByUsername $ F.toList mentionedUsers+    fetchMentionedUsers mentionedUsers      when (postUserId new /= Just myId) $         chan %= adjustEditedThreshold new@@ -276,9 +276,7 @@         -- usernames in the text of the messages which we need to use to         -- submit a single batch request for user metadata so we don't         -- submit one request per mention.-        (_, mentionedUsers) <- installMessagesFromPosts posts--        fetchUsersByUsername $ F.toList mentionedUsers+        void $ installMessagesFromPosts posts          -- Add all the new *unique* posts into the existing channel         -- corpus, generating needed fetches of data associated with@@ -419,7 +417,7 @@ -- existing UnknownGap entries and should be called when those are -- irrelevant. ----- The first boolean argument ('fetchMentionedUsers') indicates+-- The first boolean argument ('doFetchMentionedUsers') indicates -- whether this function should schedule a fetch for any mentioned -- users in the message. This is provided so that callers can batch -- this operation if a large collection of messages is being added@@ -438,7 +436,7 @@ -- notifications are generated and if the "New Messages" marker gets -- updated. addMessageToState :: Bool -> Bool -> PostToAdd -> MH PostProcessMessageAdd-addMessageToState fetchMentionedUsers fetchAuthor newPostData = do+addMessageToState doFetchMentionedUsers fetchAuthor newPostData = do     let (new, wasMentioned) = case newPostData of           -- A post from scrollback history has no mention data, and           -- that's okay: we only need to track mentions to tell the@@ -472,7 +470,8 @@                             then applyPreferenceChange pref                             else refreshChannel SidebarUpdateImmediate nc member -                        addMessageToState fetchMentionedUsers fetchAuthor newPostData >>= postProcessMessageAdd+                        addMessageToState doFetchMentionedUsers fetchAuthor newPostData >>=+                            postProcessMessageAdd              return NoAction         Just _ -> do@@ -502,8 +501,8 @@                     let (msg', mentionedUsers) = clientPostToMessage cp                                  & _1.mFlagged .~ ((cp^.cpPostId) `Set.member` flags) -                    when fetchMentionedUsers $-                        fetchUsersByUsername $ F.toList mentionedUsers+                    when doFetchMentionedUsers $+                        fetchMentionedUsers mentionedUsers                      csPostMap.at(postId new) .= Just msg'                     mh $ invalidateCacheEntry (ChannelMessages cId)
src/State/PostListOverlay.hs view
@@ -56,7 +56,7 @@   doAsyncWith Preempt $ do     posts <- fetchOp session     return $ Just $ do-      messages <- fst <$> installMessagesFromPosts posts+      messages <- installMessagesFromPosts posts       -- n.b. do not use addNewPostedMessage because these messages       -- are not new, and so no notifications or channel highlighting       -- or other post-processing should be performed.
+ src/State/ReactionEmojiListOverlay.hs view
@@ -0,0 +1,125 @@+{-# LANGUAGE TupleSections #-}+module State.ReactionEmojiListOverlay+  ( enterReactionEmojiListOverlayMode++  , reactionEmojiListSelectDown+  , reactionEmojiListSelectUp+  , reactionEmojiListPageDown+  , reactionEmojiListPageUp+  )+where++import           Prelude ()+import           Prelude.MH++import qualified Brick.Widgets.List as L+import qualified Data.Vector as Vec+import qualified Data.Text as T+import qualified Data.Map as M+import qualified Data.Set as Set+import           Data.Function ( on )+import           Data.List ( nubBy )+import           Lens.Micro.Platform ( to )++import           Network.Mattermost.Types+import           Network.Mattermost.Endpoints ( mmPostReaction, mmDeleteReaction )++import           Emoji+import           State.ListOverlay+import           State.MessageSelect+import           State.Async+import           Types+++enterReactionEmojiListOverlayMode :: MH ()+enterReactionEmojiListOverlayMode = do+    selectedMessage <- use (to getSelectedMessage)+    case selectedMessage of+        Nothing -> return ()+        Just msg -> do+            em <- use (csResources.crEmoji)+            myId <- gets myUserId+            enterListOverlayMode csReactionEmojiListOverlay ReactionEmojiListOverlay+                () enterHandler (fetchResults myId msg em)++enterHandler :: (Bool, T.Text) -> MH Bool+enterHandler (mine, e) = do+    session <- getSession+    myId <- gets myUserId++    selectedMessage <- use (to getSelectedMessage)+    case selectedMessage of+        Nothing -> return False+        Just m -> do+            case m^.mOriginalPost of+                Nothing -> return False+                Just p -> do+                    case mine of+                        False ->+                            doAsyncWith Preempt $ do+                                mmPostReaction (postId p) myId e session+                                return Nothing+                        True ->+                            doAsyncWith Preempt $ do+                                mmDeleteReaction (postId p) myId e session+                                return Nothing+                    return True++fetchResults :: UserId+             -- ^ My user ID, so we can see which reactions I haven't+             -- posted+             -> Message+             -- ^ The selected message, so we can include its current+             -- reactions in the list+             -> EmojiCollection+             -- ^ The emoji collection+             -> ()+             -- ^ The scope to search+             -> Session+             -- ^ The connection session+             -> Text+             -- ^ The search string+             -> IO (Vec.Vector (Bool, T.Text))+fetchResults myId msg em () session searchString = do+    let currentReactions = [ (myId `Set.member` uIds, k)+                           | (k, uIds) <- M.toList (msg^.mReactions)+                           ]+        matchingCurrentOtherReactions = [ (mine, r) | (mine, r) <- currentReactions+                                        , matchesEmoji searchString r+                                        , not mine+                                        ]+        matchingCurrentMyReactions = [ (mine, r) | (mine, r) <- currentReactions+                                     , matchesEmoji searchString r+                                     , mine+                                     ]+    serverMatches <- getMatchingEmoji session em searchString+    return $ Vec.fromList $ nubBy ((==) `on` snd) $+        matchingCurrentOtherReactions <> matchingCurrentMyReactions <> ((False,) <$> serverMatches)++-- | Move the selection up in the emoji list overlay by one emoji.+reactionEmojiListSelectUp :: MH ()+reactionEmojiListSelectUp = reactionEmojiListMove L.listMoveUp++-- | Move the selection down in the emoji list overlay by one emoji.+reactionEmojiListSelectDown :: MH ()+reactionEmojiListSelectDown = reactionEmojiListMove L.listMoveDown++-- | Move the selection up in the emoji list overlay by a page of emoji+-- (ReactionEmojiListPageSize).+reactionEmojiListPageUp :: MH ()+reactionEmojiListPageUp = reactionEmojiListMove (L.listMoveBy (-1 * reactionEmojiListPageSize))++-- | Move the selection down in the emoji list overlay by a page of emoji+-- (ReactionEmojiListPageSize).+reactionEmojiListPageDown :: MH ()+reactionEmojiListPageDown = reactionEmojiListMove (L.listMoveBy reactionEmojiListPageSize)++-- | Transform the emoji list results in some way, e.g. by moving the+-- cursor, and then check to see whether the modification warrants a+-- prefetch of more search results.+reactionEmojiListMove :: (L.List Name (Bool, T.Text) -> L.List Name (Bool, T.Text)) -> MH ()+reactionEmojiListMove = listOverlayMove csReactionEmojiListOverlay++-- | The number of emoji in a "page" for cursor movement purposes.+reactionEmojiListPageSize :: Int+reactionEmojiListPageSize = 10
src/State/Reactions.hs view
@@ -11,12 +11,14 @@ import           Brick.Main ( invalidateCacheEntry ) import qualified Data.Map.Strict as Map import           Lens.Micro.Platform+import qualified Data.Set as S  import           Network.Mattermost.Endpoints import           Network.Mattermost.Lenses import           Network.Mattermost.Types  import           State.Async+import           State.Common ( fetchMentionedUsers ) import           Types  @@ -31,9 +33,12 @@ addReactions cId rs = do     mh $ invalidateCacheEntry $ ChannelMessages cId     csChannel(cId).ccContents.cdMessages %= fmap upd+    let mentions = S.fromList $ UserIdMention <$> reactionUserId <$> rs+    fetchMentionedUsers mentions   where upd msg = msg & mReactions %~ insertAll (msg^.mMessageId)         insert mId r-          | mId == Just (MessagePostId (r^.reactionPostIdL)) = Map.insertWith (+) (r^.reactionEmojiNameL) 1+          | mId == Just (MessagePostId (r^.reactionPostIdL)) =+              Map.insertWith S.union (r^.reactionEmojiNameL) (S.singleton $ r^.reactionUserIdL)           | otherwise = id         insertAll mId msg = foldr (insert mId) msg rs @@ -42,5 +47,6 @@     mh $ invalidateCacheEntry $ ChannelMessages cId     csChannel(cId).ccContents.cdMessages %= fmap upd   where upd m | m^.mMessageId == Just (MessagePostId $ r^.reactionPostIdL) =-                  m & mReactions %~ (Map.insertWith (+) (r^.reactionEmojiNameL) (-1))+                  m & mReactions %~ (Map.alter delReaction (r^.reactionEmojiNameL))               | otherwise = m+        delReaction mUs = S.delete (r^.reactionUserIdL) <$> mUs
src/State/Setup.hs view
@@ -34,6 +34,8 @@ import           TimeUtils ( lookupLocalTimeZone ) import           Types import           Types.Common+import           Emoji+import           FilePaths ( userEmojiJsonPath, bundledEmojiJsonPath ) import qualified Zipper as Z  @@ -64,10 +66,19 @@   eventChan <- newBChan 25   logMgr <- newLogManager eventChan (configLogMaxBufferSize config) +  -- If we got an initial log location, start logging there.+  case mLogLocation of+      Nothing -> return ()+      Just loc -> startLoggingToFile logMgr loc+   let logApiEvent ev = apiLogEventToLogMessage ev >>= sendLogMessage logMgr       setLogger cd = cd `withLogger` logApiEvent -  (mLastAttempt, loginVty) <- interactiveGetLoginSession initialVty mkVty (configUnsafeUseHTTP config) setLogger (incompleteCredentials config)+  (mLastAttempt, loginVty) <- interactiveGetLoginSession initialVty mkVty+                                                         (configUnsafeUseHTTP config)+                                                         setLogger+                                                         logMgr+                                                         (incompleteCredentials config)    let shutdown vty = do           Vty.shutdown vty@@ -141,6 +152,12 @@    requestChan <- STM.atomically STM.newTChan +  emoji <- either (const emptyEmojiCollection) id <$> do+      result1 <- loadEmoji =<< userEmojiJsonPath+      case result1 of+          Right e -> return $ Right e+          Left _ -> loadEmoji =<< bundledEmojiJsonPath+   let cr = ChatResources { _crSession             = session                          , _crWebsocketThreadId   = Nothing                          , _crConn                = cd@@ -155,14 +172,10 @@                          , _crUserPreferences     = userPrefs                          , _crSyntaxMap           = mempty                          , _crLogManager          = logMgr+                         , _crEmoji               = emoji                          }    st <- initializeState cr myTeam me--  -- If we got an initial log location, start logging there.-  case mLogLocation of-      Nothing -> return ()-      Just loc -> startLoggingToFile logMgr loc    return (st, teamSelVty) 
src/State/Setup/Threads.hs view
@@ -114,11 +114,15 @@                           tmp <- getTemporaryDirectory                           openTempFile tmp "matterhorn-subprocess.log" +                  let unexpectedStdout = ec == ExitSuccess && (not $ emptyOutput out) && not stdoutOkay+                   hPutStrLn logHandle $                       unlines [ "Program: " <> progName                               , "Arguments: " <> show args                               , "Exit code: " <> show ec-                              , "Stdout:"+                              , if unexpectedStdout+                                then "Stdout (unexpected due to successful exit):"+                                else "Stdout:"                               , out                               , "Stderr:"                               , err
src/State/Themes.hs view
@@ -8,6 +8,7 @@ import           Prelude () import           Prelude.MH +import           Brick ( invalidateCache ) import           Brick.Themes ( themeToAttrMap ) import qualified Data.Text as T import           Lens.Micro.Platform ( (.=) )@@ -28,6 +29,6 @@ setTheme name =     case lookupTheme name of         Nothing -> listThemes-        Just it -> csResources.crTheme .=-            (themeToAttrMap $ internalTheme it)-+        Just it -> do+            mh invalidateCache+            csResources.crTheme .= (themeToAttrMap $ internalTheme it)
src/State/UserListOverlay.hs view
@@ -2,37 +2,30 @@   ( enterChannelMembersUserList   , enterChannelInviteUserList   , enterDMSearchUserList-  , resetUserListSearch-  , exitUserListMode -  , userListActivateCurrent   , userListSelectDown   , userListSelectUp   , userListPageDown   , userListPageUp--  , userListSearchString   ) where  import           Prelude () import           Prelude.MH -import qualified Brick.Widgets.Edit as E import qualified Brick.Widgets.List as L import qualified Data.HashMap.Strict as HM import qualified Data.Sequence as Seq import qualified Data.Text as T-import qualified Data.Text.Zipper as Z import qualified Data.Vector as Vec-import           Lens.Micro.Platform ( (.=), (%=), (.~) )+import           Lens.Micro.Platform ( (.~) )  import qualified Network.Mattermost.Endpoints as MM import qualified Network.Mattermost.Types.Config as MM import           Network.Mattermost.Types  import           State.Channels ( createOrFocusDMChannel, addUserToCurrentChannel )-import           State.Common+import           State.ListOverlay import           Types  @@ -40,111 +33,55 @@ -- current channel. enterChannelMembersUserList :: MH () enterChannelMembersUserList = do-  cId <- use csCurrentChannelId-  myId <- gets myUserId-  myTId <- gets myTeamId-  enterUserListMode (ChannelMembers cId myTId)-    (\u -> case u^.uiId /= myId of-      True -> createOrFocusDMChannel u Nothing >> return True-      False -> return False-    )+    cId <- use csCurrentChannelId+    myId <- gets myUserId+    myTId <- gets myTeamId+    enterUserListMode (ChannelMembers cId myTId)+      (\u -> case u^.uiId /= myId of+        True -> createOrFocusDMChannel u Nothing >> return True+        False -> return False+      )  -- | Show the user list overlay for showing users that are not members -- of the current channel for the purpose of adding them to the -- channel. enterChannelInviteUserList :: MH () enterChannelInviteUserList = do-  cId <- use csCurrentChannelId-  myId <- gets myUserId-  myTId <- gets myTeamId-  enterUserListMode (ChannelNonMembers cId myTId)-    (\u -> case u^.uiId /= myId of-      True -> addUserToCurrentChannel u >> return True-      False -> return False-    )+    cId <- use csCurrentChannelId+    myId <- gets myUserId+    myTId <- gets myTeamId+    enterUserListMode (ChannelNonMembers cId myTId)+      (\u -> case u^.uiId /= myId of+        True -> addUserToCurrentChannel u >> return True+        False -> return False+      )  -- | Show the user list overlay for showing all users for the purpose of -- starting a direct message channel with another user. enterDMSearchUserList :: MH () enterDMSearchUserList = do-  myId <- gets myUserId-  myTId <- gets myTeamId-  config <- use csClientConfig-  let restrictTeam = case MM.clientConfigRestrictDirectMessage <$> config of-          Just MM.RestrictTeam -> Just myTId-          _ -> Nothing-  enterUserListMode (AllUsers restrictTeam)-    (\u -> case u^.uiId /= myId of-      True -> createOrFocusDMChannel u Nothing >> return True-      False -> return False-    )---- | Interact with the currently-selected user (depending on how the--- overlay is configured).-userListActivateCurrent :: MH ()-userListActivateCurrent = do-  mItem <- L.listSelectedElement <$> use (csUserListOverlay.userListSearchResults)-  case mItem of-      Nothing -> return ()-      Just (_, user) -> do-          handler <- use (csUserListOverlay.userListEnterHandler)-          activated <- handler user-          if activated-             then setMode Main-             else return ()+    myId <- gets myUserId+    myTId <- gets myTeamId+    config <- use csClientConfig+    let restrictTeam = case MM.clientConfigRestrictDirectMessage <$> config of+            Just MM.RestrictTeam -> Just myTId+            _ -> Nothing+    enterUserListMode (AllUsers restrictTeam)+      (\u -> case u^.uiId /= myId of+        True -> createOrFocusDMChannel u Nothing >> return True+        False -> return False+      )  -- | Show the user list overlay with the given search scope, and issue a -- request to gather the first search results. enterUserListMode :: UserSearchScope -> (UserInfo -> MH Bool) -> MH ()-enterUserListMode scope enterHandler = do-  csUserListOverlay.userListSearchScope .= scope-  csUserListOverlay.userListSearchInput.E.editContentsL %= Z.clearZipper-  csUserListOverlay.userListEnterHandler .= enterHandler-  csUserListOverlay.userListSearching .= False-  csUserListOverlay.userListHasAllResults .= False-  setMode UserListOverlay-  resetUserListSearch--resetUserListSearch :: MH ()-resetUserListSearch = do-  searchPending <- use (csUserListOverlay.userListSearching)--  when (not searchPending) $ do-      searchString <- userListSearchString-      csUserListOverlay.userListSearching .= True-      csUserListOverlay.userListHasAllResults .= False-      csUserListOverlay.userListSearchResults .= listFromUserSearchResults mempty-      session <- getSession-      scope <- use (csUserListOverlay.userListSearchScope)-      doAsyncWith Preempt $ do-          results <- fetchInitialResults scope session searchString-          return $ Just $ do-              let lst = listFromUserSearchResults results-              csUserListOverlay.userListSearchResults .= lst-              -- NOTE: Disabled for now. See the hack note below for-              -- details.-              ---              -- csUserListOverlay.userListHasAllResults .= (length results < searchResultsChunkSize)-              csUserListOverlay.userListSearching .= False--              -- Now that the results are available, check to see if the-              -- search string changed since this request was submitted.-              -- If so, issue another search.-              afterSearchString <- userListSearchString-              when (searchString /= afterSearchString) resetUserListSearch+enterUserListMode scope enterHandler =+    enterListOverlayMode csUserListOverlay UserListOverlay scope enterHandler getUserSearchResults  userInfoFromPair :: User -> Text -> UserInfo userInfoFromPair u status =     userInfoFromUser u True & uiStatus .~ statusFromText status --- | Clear out the state of the user list overlay and return to the Main--- mode.-exitUserListMode :: MH ()-exitUserListMode = do-  csUserListOverlay.userListSearchResults .= listFromUserSearchResults mempty-  csUserListOverlay.userListEnterHandler .= (const $ return False)-  setMode Main- -- | Move the selection up in the user list overlay by one user. userListSelectUp :: MH () userListSelectUp = userListMove L.listMoveUp@@ -167,93 +104,20 @@ -- cursor, and then check to see whether the modification warrants a -- prefetch of more search results. userListMove :: (L.List Name UserInfo -> L.List Name UserInfo) -> MH ()-userListMove f = do-  csUserListOverlay.userListSearchResults %= f-  -- NOTE! Do not enable this. See the docs for maybePrefetchNextChunk.-  -- For now we want to keep the code around in case it can be-  -- reinstated in the future.-  -- maybePrefetchNextChunk---- | We'll attempt to prefetch the next page of results if the cursor--- gets within this many positions of the last result we have.--- selectionPrefetchDelta :: Int--- selectionPrefetchDelta = 10---- Prefetch the next chunk of user list search results if all of the--- following are true:------  * the search string is empty (because we can't paginate searches,---    just fetches for all users), and---  * cursor is within selectionPrefetchDelta positions of the end of---    list, and---  * the length of the current results list is exactly a multiple of---    fetching chunk size (thus indicating a very high probability that---    there are more results to be fetched).------ NOTE: this function should be reinstated and called in 'userListMove'--- if we start using the /users endpoint again in the future. See the--- hack note in the getUserSearchResultsPage below for details. In the--- mean time, no pagination of results is possible so no prefetching--- should be done.--- _maybePrefetchNextChunk :: MH ()--- _maybePrefetchNextChunk = do---   gettingMore <- use (csUserListOverlay.userListRequestingMore)---   hasAll <- use (csUserListOverlay.userListHasAllResults)---   searchString <- userListSearchString---   curIdx <- use (csUserListOverlay.userListSearchResults.L.listSelectedL)---   numResults <- use (csUserListOverlay.userListSearchResults.to F.length)------   let selectionNearEnd = case curIdx of---           Nothing -> False---           Just i -> numResults - (i + 1) < selectionPrefetchDelta------   when (not hasAll && T.null searchString && not gettingMore && selectionNearEnd) $ do---       let pageNum = numResults `div` searchResultsChunkSize------       csUserListOverlay.userListRequestingMore .= True---       session <- getSession---       scope <- use (csUserListOverlay.userListSearchScope)---       myTId <- gets myTeamId---       doAsyncWith Preempt $ do---           newChunk <- getUserSearchResultsPage pageNum myTId scope session searchString---           return $ Just $ do---               -- Because we only ever append, this is safe to do w.r.t.---               -- the selected index of the list. If we ever prepended or---               -- removed, we'd also need to manage the selection index---               -- to ensure it stays in bounds.---               csUserListOverlay.userListSearchResults.L.listElementsL %= (<> newChunk)---               csUserListOverlay.userListRequestingMore .= False------               -- If we got fewer results than we asked for, then we have---               -- them all!---               -----               -- NOTE: disabled for now, see the hack note below.---               -----               -- csUserListOverlay.userListHasAllResults .=---               --     (length newChunk < searchResultsChunkSize)+userListMove = listOverlayMove csUserListOverlay  -- | The number of users in a "page" for cursor movement purposes. userListPageSize :: Int userListPageSize = 10 --- | Perform an initial request for search results in the specified--- scope.-fetchInitialResults :: UserSearchScope -> Session -> Text -> IO (Vec.Vector UserInfo)-fetchInitialResults = getUserSearchResultsPage 0---- searchResultsChunkSize :: Int--- searchResultsChunkSize = 40--getUserSearchResultsPage :: Int-                         -- ^ The page number of results to fetch, starting at zero.-                         -> UserSearchScope-                         -- ^ The scope to search-                         -> Session-                         -- ^ The connection session-                         -> Text-                         -- ^ The search string-                         -> IO (Vec.Vector UserInfo)-getUserSearchResultsPage _pageNum scope s searchString = do+getUserSearchResults :: UserSearchScope+                     -- ^ The scope to search+                     -> Session+                     -- ^ The connection session+                     -> Text+                     -- ^ The search string+                     -> IO (Vec.Vector UserInfo)+getUserSearchResults scope s searchString = do     -- Unfortunately, we don't get pagination control when there is a     -- search string in effect. We'll get at most 100 results from a     -- search.@@ -301,7 +165,3 @@              return $ Vec.fromList usersWithStatus         True -> return mempty--userListSearchString :: MH Text-userListSearchString =-    (head . E.getEditContents) <$> use (csUserListOverlay.userListSearchInput)
src/State/Users.hs view
@@ -2,7 +2,6 @@ module State.Users   ( handleNewUsers   , handleTypingUser-  , UserFetch(..)   , withFetchedUser   , withFetchedUserMaybe   )@@ -11,6 +10,7 @@ import           Prelude () import           Prelude.MH +import qualified Data.Text as T import qualified Data.Foldable as F import qualified Data.Sequence as Seq import           Data.Time ( getCurrentTime )@@ -47,16 +47,6 @@             ts <- liftIO getCurrentTime             csChannels %= modifyChannelById cId (addChannelTypingUser uId ts) --- | A user fetching strategy.-data UserFetch =-    UserFetchById UserId-    -- ^ Fetch the user with the specified ID.-    | UserFetchByUsername Text-    -- ^ Fetch the user with the specified username.-    | UserFetchByNickname Text-    -- ^ Fetch the user with the specified nickname.-    deriving (Eq, Show)- -- | Given a user fetching strategy, locate the user in the state or -- fetch it from the server, and pass the result to the specified -- action. Assumes a single match is the only expected/valid result.@@ -79,30 +69,32 @@      case localMatch of         Just user -> handle $ Just user-        Nothing -> doAsyncWith Normal $ do-            results <- case fetch of-                UserFetchById uId ->-                    MM.mmGetUsersByIds (Seq.singleton uId) session-                UserFetchByUsername uname ->-                    MM.mmGetUsersByUsernames (Seq.singleton uname) session-                UserFetchByNickname nick -> do-                    let req = UserSearch { userSearchTerm = nick-                                         , userSearchAllowInactive = True-                                         , userSearchWithoutTeam = True-                                         , userSearchInChannelId = Nothing-                                         , userSearchNotInTeamId = Nothing-                                         , userSearchNotInChannelId = Nothing-                                         , userSearchTeamId = Nothing-                                         }-                    MM.mmSearchUsers req session+        Nothing -> do+            mhLog LogGeneral $ T.pack $ "withFetchedUserMaybe: getting " <> show fetch+            doAsyncWith Normal $ do+                results <- case fetch of+                    UserFetchById uId ->+                        MM.mmGetUsersByIds (Seq.singleton uId) session+                    UserFetchByUsername uname ->+                        MM.mmGetUsersByUsernames (Seq.singleton $ trimUserSigil uname) session+                    UserFetchByNickname nick -> do+                        let req = UserSearch { userSearchTerm = trimUserSigil nick+                                             , userSearchAllowInactive = True+                                             , userSearchWithoutTeam = True+                                             , userSearchInChannelId = Nothing+                                             , userSearchNotInTeamId = Nothing+                                             , userSearchNotInChannelId = Nothing+                                             , userSearchTeamId = Nothing+                                             }+                        MM.mmSearchUsers req session -            return $ Just $ do-                infos <- forM (F.toList results) $ \u -> do-                    let info = userInfoFromUser u True-                    addNewUser info-                    return info+                return $ Just $ do+                    infos <- forM (F.toList results) $ \u -> do+                        let info = userInfoFromUser u True+                        addNewUser info+                        return info -                case infos of-                    [match] -> handle $ Just match-                    [] -> handle Nothing-                    _ -> postErrorMessage' "Error: ambiguous user information"+                    case infos of+                        [match] -> handle $ Just match+                        [] -> handle Nothing+                        _ -> postErrorMessage' "Error: ambiguous user information"
src/Themes.hs view
@@ -69,7 +69,9 @@ import qualified Skylighting.Styles as Sky import           Skylighting.Types ( TokenType(..) ) +import           Types ( specialUserMentions ) + helpAttr :: AttrName helpAttr = "help" @@ -337,8 +339,12 @@  colorUsername :: Text -> Text -> Widget a colorUsername username display =-  withDefAttr (usernameAttr h) $ txt (display)-    where h = hash username `mod` length usernameColors+    let normalizedUsername = T.toLower username+        aName = if normalizedUsername `elem` specialUserMentions+                then clientEmphAttr+                else usernameAttr h+        h = hash normalizedUsername `mod` length usernameColors+    in withDefAttr aName $ txt (display)  usernameColors :: [Attr] usernameColors =
src/Types.hs view
@@ -43,6 +43,7 @@   , AuthenticationException(..)   , BackgroundInfo(..)   , RequestChan+  , UserFetch(..)    , mkChannelZipperList   , ChannelListGroup(..)@@ -67,9 +68,10 @@   , csRecentChannel   , csPostListOverlay   , csUserListOverlay+  , csChannelListOverlay+  , csReactionEmojiListOverlay   , csMyTeam   , csMessageSelect-  , csJoinChannelList   , csConnectionStatus   , csWorkerIsBusy   , csChannel@@ -107,23 +109,25 @@    , AutocompleteAlternative(..)   , autocompleteAlternativeReplacement+  , SpecialMention(..)+  , specialMentionName+  , isSpecialMention    , PostListOverlayState   , postListSelected   , postListPosts    , UserSearchScope(..)--  , UserListOverlayState-  , userListSearchResults-  , userListSearchInput-  , userListSearchScope-  , userListSearching-  , userListRequestingMore-  , userListHasAllResults-  , userListEnterHandler+  , ChannelSearchScope(..) -  , listFromUserSearchResults+  , ListOverlayState+  , listOverlaySearchResults+  , listOverlaySearchInput+  , listOverlaySearchScope+  , listOverlaySearching+  , listOverlayEnterHandler+  , listOverlayNewList+  , listOverlayFetchResults    , getUsers @@ -141,9 +145,12 @@   , crConfiguration   , crSyntaxMap   , crLogManager+  , crEmoji   , getSession   , getResourceSession +  , specialUserMentions+   , UserPreferences(UserPreferences)   , userPrefShowJoinLeave   , userPrefFlaggedPostList@@ -164,6 +171,10 @@    , MH   , runMHEvent+  , scheduleUserFetches+  , scheduleUserStatusFetches+  , getScheduledUserFetches+  , getScheduledUserStatusFetches   , mh   , generateUUID   , generateUUID_IO@@ -174,6 +185,7 @@    , mhLog   , mhGetIOLogger+  , ioLogWithManager   , LogContext(..)   , withLogContext   , withLogContextChannelId@@ -252,6 +264,7 @@ import           Control.Exception ( SomeException ) import qualified Control.Monad.State as St import qualified Control.Monad.Reader as R+import qualified Data.Set as Set import qualified Data.ByteString as BS import qualified Data.Foldable as F import           Data.Ord ( comparing )@@ -281,6 +294,7 @@ import           Network.Mattermost.WebSocket ( WebsocketEvent )  import           InputHistory+import           Emoji import           Types.Common import           Types.Channels import           Types.DirectionalSeq ( emptyDirSeq )@@ -585,12 +599,16 @@     | UrlList     | MessagePreviewViewport     | UserListSearchInput+    | JoinChannelListSearchInput     | UserListSearchResults     | ViewMessageArea     | ChannelSidebar     | ChannelSelectInput     | AttachmentList     | AttachmentFileBrowser+    | MessageReactionsArea+    | ReactionEmojiList+    | ReactionEmojiListInput     deriving (Eq, Show, Ord)  -- | The sum type of exceptions we expect to encounter on authentication@@ -797,20 +815,50 @@                   , _crUserPreferences     :: UserPreferences                   , _crSyntaxMap           :: SyntaxMap                   , _crLogManager          :: LogManager+                  , _crEmoji               :: EmojiCollection                   } +-- | A "special" mention that does not map to a specific user, but is an+-- alias that the server uses to notify users.+data SpecialMention =+    MentionAll+    -- ^ @all: notify everyone in the channel.+    | MentionChannel+    -- ^ @channel: notify everyone in the channel.+ data AutocompleteAlternative =     UserCompletion User Bool     -- ^ User, plus whether the user is in the channel that triggered     -- the autocomplete+    | SpecialMention SpecialMention+    -- ^ A special mention.     | ChannelCompletion Bool Channel     -- ^ Channel, plus whether the user is a member of the channel     | SyntaxCompletion Text     -- ^ Name of a skylighting syntax definition     | CommandCompletion Text Text Text     -- ^ Name of a slash command, argspec, and description+    | EmojiCompletion Text+    -- ^ The text of an emoji completion +specialMentionName :: SpecialMention -> Text+specialMentionName MentionChannel = "channel"+specialMentionName MentionAll = "all"++isSpecialMention :: T.Text -> Bool+isSpecialMention n = isJust $ lookup (T.toLower $ trimUserSigil n) pairs+    where+        pairs = mkPair <$> mentions+        mentions = [ MentionChannel+                   , MentionAll+                   ]+        mkPair v = (specialMentionName v, v)+ autocompleteAlternativeReplacement :: AutocompleteAlternative -> Text+autocompleteAlternativeReplacement (EmojiCompletion e) =+    ":" <> e <> ":"+autocompleteAlternativeReplacement (SpecialMention m) =+    userSigil <> specialMentionName m autocompleteAlternativeReplacement (UserCompletion u _) =     userSigil <> userUsername u autocompleteAlternativeReplacement (ChannelCompletion _ c) =@@ -940,11 +988,12 @@     | UrlSelect     | LeaveChannelConfirm     | DeleteChannelConfirm-    | JoinChannel     | MessageSelect     | MessageSelectDeleteConfirm     | PostListOverlay PostListContents     | UserListOverlay+    | ReactionEmojiListOverlay+    | ChannelListOverlay     | ViewMessage     | ManageAttachments     | ManageAttachmentsBrowseFiles@@ -1002,15 +1051,16 @@               , _csWorkerIsBusy :: Maybe (Maybe Int)               -- ^ Whether the async worker thread is busy, and its               -- queue length if so.-              , _csJoinChannelList :: Maybe (List Name Channel)-              -- ^ The list of channels presented in the channel join-              -- window.               , _csMessageSelect :: MessageSelectState               -- ^ The state of message selection mode.               , _csPostListOverlay :: PostListOverlayState               -- ^ The state of the post list overlay.-              , _csUserListOverlay :: UserListOverlayState+              , _csUserListOverlay :: ListOverlayState UserInfo UserSearchScope               -- ^ The state of the user list overlay.+              , _csChannelListOverlay :: ListOverlayState Channel ChannelSearchScope+              -- ^ The state of the user list overlay.+              , _csReactionEmojiListOverlay :: ListOverlayState (Bool, T.Text) ()+              -- ^ The state of the reaction emoji list overlay.               , _csClientConfig :: Maybe ClientConfig               -- ^ The Mattermost client configuration, as we understand it.               , _csPendingChannelChange :: Maybe PendingChannelChange@@ -1061,31 +1111,51 @@                      , _csUrlList                     = list UrlList mempty 2                      , _csConnectionStatus            = Connected                      , _csWorkerIsBusy                = Nothing-                     , _csJoinChannelList             = Nothing                      , _csMessageSelect               = MessageSelectState Nothing                      , _csPostListOverlay             = PostListOverlayState emptyDirSeq Nothing                      , _csUserListOverlay             = nullUserListOverlayState+                     , _csChannelListOverlay          = nullChannelListOverlayState+                     , _csReactionEmojiListOverlay    = nullEmojiListOverlayState                      , _csClientConfig                = Nothing                      , _csPendingChannelChange        = Nothing                      , _csViewedMessage               = Nothing                      } -nullUserListOverlayState :: UserListOverlayState+nullChannelListOverlayState :: ListOverlayState Channel ChannelSearchScope+nullChannelListOverlayState =+    let newList rs = list JoinChannelList rs 2+    in ListOverlayState { _listOverlaySearchResults  = newList mempty+                        , _listOverlaySearchInput    = editor JoinChannelListSearchInput (Just 1) ""+                        , _listOverlaySearchScope    = AllChannels+                        , _listOverlaySearching      = False+                        , _listOverlayEnterHandler   = const $ return False+                        , _listOverlayNewList        = newList+                        , _listOverlayFetchResults   = const $ const $ const $ return mempty+                        }++nullUserListOverlayState :: ListOverlayState UserInfo UserSearchScope nullUserListOverlayState =-    UserListOverlayState { _userListSearchResults  = listFromUserSearchResults mempty-                         , _userListSearchInput    = editor UserListSearchInput (Just 1) ""-                         , _userListSearchScope    = AllUsers Nothing-                         , _userListSearching      = False-                         , _userListRequestingMore = False-                         , _userListHasAllResults  = False-                         , _userListEnterHandler   = const $ return False-                         }+    let newList rs = list UserListSearchResults rs 1+    in ListOverlayState { _listOverlaySearchResults  = newList mempty+                        , _listOverlaySearchInput    = editor UserListSearchInput (Just 1) ""+                        , _listOverlaySearchScope    = AllUsers Nothing+                        , _listOverlaySearching      = False+                        , _listOverlayEnterHandler   = const $ return False+                        , _listOverlayNewList        = newList+                        , _listOverlayFetchResults   = const $ const $ const $ return mempty+                        } -listFromUserSearchResults :: Vec.Vector UserInfo -> List Name UserInfo-listFromUserSearchResults rs =-    -- NB: The item height here needs to actually match the UI drawing-    -- in Draw.UserListOverlay.-    list UserListSearchResults rs 1+nullEmojiListOverlayState :: ListOverlayState (Bool, T.Text) ()+nullEmojiListOverlayState =+    let newList rs = list ReactionEmojiList rs 1+    in ListOverlayState { _listOverlaySearchResults  = newList mempty+                        , _listOverlaySearchInput    = editor ReactionEmojiListInput (Just 1) ""+                        , _listOverlaySearchScope    = ()+                        , _listOverlaySearching      = False+                        , _listOverlayEnterHandler   = const $ return False+                        , _listOverlayNewList        = newList+                        , _listOverlayFetchResults   = const $ const $ const $ return mempty+                        }  -- | The state of channel selection mode. data ChannelSelectState =@@ -1110,16 +1180,30 @@                          , _postListSelected :: Maybe PostId                          } --- | The state of the user list overlay.-data UserListOverlayState =-    UserListOverlayState { _userListSearchResults :: List Name UserInfo-                         , _userListSearchInput :: Editor Text Name-                         , _userListSearchScope :: UserSearchScope-                         , _userListSearching :: Bool-                         , _userListRequestingMore :: Bool-                         , _userListHasAllResults :: Bool-                         , _userListEnterHandler :: UserInfo -> MH Bool-                         }+-- | The state of the search result list overlay. Type 'a' is the type+-- of data in the list. Type 'b' is the search scope type.+data ListOverlayState a b =+    ListOverlayState { _listOverlaySearchResults :: List Name a+                     -- ^ The list of search results currently shown in+                     -- the overlay.+                     , _listOverlaySearchInput :: Editor Text Name+                     -- ^ The editor for the overlay's search input.+                     , _listOverlaySearchScope :: b+                     -- ^ The overlay's current search scope.+                     , _listOverlaySearching :: Bool+                     -- ^ Whether a search is in progress (i.e. whether+                     -- we are currently awaiting a response from a+                     -- search query to the server).+                     , _listOverlayEnterHandler :: a -> MH Bool+                     -- ^ The handler to invoke on the selected element+                     -- when the user presses Enter.+                     , _listOverlayNewList :: Vec.Vector a -> List Name a+                     -- ^ The function to build a new brick List from a+                     -- vector of search results.+                     , _listOverlayFetchResults :: b -> Session -> Text -> IO (Vec.Vector a)+                     -- ^ The function to call to issue a search query+                     -- to the server.+                     }  -- | The scope for searching for users in a user list overlay. data UserSearchScope =@@ -1127,6 +1211,10 @@     | ChannelNonMembers ChannelId TeamId     | AllUsers (Maybe TeamId) +-- | The scope for searching for channels to join.+data ChannelSearchScope =+    AllChannels+ -- | Actions that can be sent on the websocket to the server. data WebsocketAction =     UserTyping UTCTime ChannelId (Maybe PostId) -- ^ user typing in the input box@@ -1141,11 +1229,28 @@                }                deriving (Eq, Show) +-- | A user fetching strategy.+data UserFetch =+    UserFetchById UserId+    -- ^ Fetch the user with the specified ID.+    | UserFetchByUsername Text+    -- ^ Fetch the user with the specified username.+    | UserFetchByNickname Text+    -- ^ Fetch the user with the specified nickname.+    deriving (Eq, Show)++data MHState =+    MHState { mhCurrentState :: ChatState+            , mhNextAction :: ChatState -> EventM Name (Next ChatState)+            , mhUsersToFetch :: [UserFetch]+            , mhPendingStatusZipper :: Maybe (Zipper ChannelListGroup ChannelListEntry)+            }+ -- | A value of type 'MH' @a@ represents a computation that can -- manipulate the application state and also request that the -- application quit newtype MH a =-    MH { fromMH :: R.ReaderT (Maybe LogContext) (St.StateT (ChatState, ChatState -> EventM Name (Next ChatState)) (EventM Name)) a }+    MH { fromMH :: R.ReaderT (Maybe LogContext) (St.StateT MHState (EventM Name)) a }  -- | Use a modified logging context for the duration of the specified MH -- action.@@ -1175,22 +1280,44 @@ mhGetIOLogger = do     ctx <- getLogContext     mgr <- use (to (_crLogManager . _csResources))-    return $ \cat msg -> do-        now <- liftIO getCurrentTime-        let lm = LogMessage { logMessageText = msg-                            , logMessageContext = ctx-                            , logMessageCategory = cat-                            , logMessageTimestamp = now-                            }-        liftIO $ sendLogMessage mgr lm+    return $ ioLogWithManager mgr ctx +ioLogWithManager :: LogManager -> Maybe LogContext -> LogCategory -> Text -> IO ()+ioLogWithManager mgr ctx cat msg = do+    now <- getCurrentTime+    let lm = LogMessage { logMessageText = msg+                        , logMessageContext = ctx+                        , logMessageCategory = cat+                        , logMessageTimestamp = now+                        }+    sendLogMessage mgr lm+ -- | Run an 'MM' computation, choosing whether to continue or halt based -- on the resulting runMHEvent :: ChatState -> MH () -> EventM Name (Next ChatState) runMHEvent st (MH mote) = do-  ((), (st', rs)) <- St.runStateT (R.runReaderT mote Nothing) (st, Brick.continue)-  rs st'+  let mhSt = MHState { mhCurrentState = st+                     , mhNextAction = Brick.continue+                     , mhUsersToFetch = []+                     , mhPendingStatusZipper = Nothing+                     }+  ((), st') <- St.runStateT (R.runReaderT mote Nothing) mhSt+  (mhNextAction st') (mhCurrentState st') +scheduleUserFetches :: [UserFetch] -> MH ()+scheduleUserFetches fs = MH $ do+    St.modify $ \s -> s { mhUsersToFetch = fs <> mhUsersToFetch s }++scheduleUserStatusFetches :: Zipper ChannelListGroup ChannelListEntry -> MH ()+scheduleUserStatusFetches z = MH $ do+    St.modify $ \s -> s { mhPendingStatusZipper = Just z }++getScheduledUserFetches :: MH [UserFetch]+getScheduledUserFetches = MH $ St.gets mhUsersToFetch++getScheduledUserStatusFetches :: MH (Maybe (Zipper ChannelListGroup ChannelListEntry))+getScheduledUserStatusFetches = MH $ St.gets mhPendingStatusZipper+ -- | lift a computation in 'EventM' into 'MH' mh :: EventM Name a -> MH a mh = MH . R.lift . St.lift@@ -1203,21 +1330,22 @@  mhHandleEventLensed :: Lens' ChatState b -> (e -> b -> EventM Name b) -> e -> MH () mhHandleEventLensed ln f event = MH $ do-    (st, b) <- St.get+    s <- St.get+    let st = mhCurrentState s     n <- R.lift $ St.lift $ f event (st ^. ln)-    St.put (st & ln .~ n , b)+    St.put (s { mhCurrentState = st & ln .~ n })  mhSuspendAndResume :: (ChatState -> IO ChatState) -> MH () mhSuspendAndResume mote = MH $ do-    (st, _) <- St.get-    St.put (st, \ _ -> Brick.suspendAndResume (mote st))+    s <- St.get+    St.put $ s { mhNextAction = \ _ -> Brick.suspendAndResume (mote $ mhCurrentState s) }  -- | This will request that after this computation finishes the -- application should exit requestQuit :: MH () requestQuit = MH $ do-    (st, _) <- St.get-    St.put (st, Brick.halt)+    s <- St.get+    St.put $ s { mhNextAction = Brick.halt }  instance Functor MH where     fmap f (MH x) = MH (fmap f x)@@ -1233,10 +1361,10 @@ -- We want to pretend that the state is only the ChatState, rather -- than the ChatState and the Brick continuation instance St.MonadState ChatState MH where-    get = fst `fmap` MH St.get+    get = mhCurrentState `fmap` MH St.get     put st = MH $ do-        (_, c) <- St.get-        St.put (st, c)+        s <- St.get+        St.put $ s { mhCurrentState = st }  instance St.MonadIO MH where     liftIO = MH . St.liftIO@@ -1309,7 +1437,7 @@ makeLenses ''ChatEditState makeLenses ''AutocompleteState makeLenses ''PostListOverlayState-makeLenses ''UserListOverlayState+makeLenses ''ListOverlayState makeLenses ''ChannelSelectState makeLenses ''UserPreferences makeLenses ''ConnectionInfo@@ -1481,6 +1609,8 @@ data CmdArgs :: * -> * where     NoArg    :: CmdArgs ()     LineArg  :: Text -> CmdArgs Text+    UserArg  :: CmdArgs rest -> CmdArgs (Text, rest)+    ChannelArg :: CmdArgs rest -> CmdArgs (Text, rest)     TokenArg :: Text -> CmdArgs rest -> CmdArgs (Text, rest)  -- | A 'CmdExec' value represents the implementation of a command when@@ -1560,10 +1690,17 @@  getHighlightSet :: ChatState -> HighlightSet getHighlightSet st =-    HighlightSet { hUserSet = getUsernameSet $ st^.csUsers+    HighlightSet { hUserSet = addSpecialUserMentions $ getUsernameSet $ st^.csUsers                  , hChannelSet = getChannelNameSet $ st^.csChannels                  , hSyntaxMap = st^.csResources.crSyntaxMap                  }++-- From: https://docs.mattermost.com/help/messaging/mentioning-teammates.html+specialUserMentions :: [T.Text]+specialUserMentions = ["all", "channel", "here"]++addSpecialUserMentions :: Set Text -> Set Text+addSpecialUserMentions s = foldr Set.insert s specialUserMentions  getNewMessageCutoff :: ChannelId -> ChatState -> Maybe NewMessageIndicator getNewMessageCutoff cId st = do
src/Types/KeyEvents.hs view
@@ -88,6 +88,7 @@   | DeleteMessageEvent   | EditMessageEvent   | ReplyMessageEvent+  | ReactToMessageEvent    -- Attachments   | AttachmentListAddEvent@@ -148,6 +149,7 @@   , DeleteMessageEvent   , EditMessageEvent   , ReplyMessageEvent+  , ReactToMessageEvent   , AttachmentListAddEvent   , AttachmentListDeleteEvent   , AttachmentOpenEvent@@ -339,6 +341,7 @@   DeleteMessageEvent -> "delete-message"   EditMessageEvent   -> "edit-message"   ReplyMessageEvent  -> "reply-message"+  ReactToMessageEvent -> "react-to-message"    AttachmentListAddEvent    -> "add-to-attachment-list"   AttachmentListDeleteEvent -> "delete-from-attachment-list"
src/Types/Messages.hs view
@@ -41,8 +41,8 @@ module Types.Messages   ( -- * Message and operations on a single Message     Message(..)-  , isDeletable, isReplyable, isEditable, isReplyTo, isGap, isFlaggable-  , isEmote, isJoinLeave, isTransition+  , isDeletable, isReplyable, isReactable, isEditable, isReplyTo, isGap, isFlaggable+  , isEmote, isJoinLeave, isTransition, isNewMessagesTransition   , mText, mUser, mDate, mType, mPending, mDeleted   , mAttachments, mInReplyToMsg, mMessageId, mReactions, mFlagged   , mOriginalPost, mChannelId, mMarkdownSource@@ -50,6 +50,7 @@   , MessageType(..)   , MessageId(..)   , ThreadState(..)+  , MentionedUser(..)   , isPostMessage   , messagePostId   , messageIdPostId@@ -57,6 +58,7 @@   , ReplyState(..)   , clientMessageToMessage   , clientPostToMessage+  , clientPostReactionUserIds   , newMessageOfType     -- * Message Collections   , Messages@@ -64,6 +66,7 @@   , RetrogradeMessages   , MessageOps (..)   , noMessages+  , messagesLength   , filterMessages   , reverseMessages   , unreverseMessages@@ -73,6 +76,8 @@   , retrogradeMsgsWithThreadStates   , findMessage   , getRelMessageId+  , messagesHead+  , messagesDrop   , getNextMessage   , getPrevMessage   , getNextMessageId@@ -160,7 +165,7 @@   , _mAttachments   :: Seq Attachment   , _mInReplyToMsg  :: ReplyState   , _mMessageId     :: Maybe MessageId-  , _mReactions     :: Map.Map Text Int+  , _mReactions     :: Map.Map Text (S.Set UserId)   , _mOriginalPost  :: Maybe Post   , _mFlagged       :: Bool   , _mChannelId     :: Maybe ChannelId@@ -194,6 +199,14 @@       CP Emote -> True       _ -> False +isReactable :: Message -> Bool+isReactable m =+    isJust (messagePostId m) &&+    case _mType m of+      CP NormalPost -> True+      CP Emote -> True+      _ -> False+ isEditable :: Message -> Bool isEditable m =     isJust (messagePostId m) &&@@ -220,6 +233,11 @@                    C NewMessagesTransition -> True                    _ -> False +isNewMessagesTransition :: Message -> Bool+isNewMessagesTransition m = case _mType m of+    C NewMessagesTransition -> True+    _ -> False+ isEmote :: Message -> Bool isEmote m = case _mType m of               CP Emote -> True@@ -294,12 +312,24 @@   }  +data MentionedUser =+    UsernameMention Text+    | UserIdMention UserId+    deriving (Eq, Show, Ord)++clientPostReactionUserIds :: ClientPost -> S.Set UserId+clientPostReactionUserIds cp =+    S.unions $ F.toList $ cp^.cpReactions+ -- | Builds a message from a ClientPost and also returns the set of -- usernames mentioned in the text of the message.-clientPostToMessage :: ClientPost -> (Message, S.Set Text)-clientPostToMessage cp = (m, usernames)+clientPostToMessage :: ClientPost -> (Message, S.Set MentionedUser)+clientPostToMessage cp = (m, mentions)     where-        usernames = findUsernames $ cp^.cpText+        mentions =+            S.fromList $+                (UsernameMention <$> (F.toList $ findUsernames $ cp^.cpText)) <>+                (UserIdMention <$> (F.toList $ clientPostReactionUserIds cp))         m = Message { _mText = cp^.cpText                     , _mMarkdownSource = cp^.cpMarkdownSource                     , _mUser =@@ -363,12 +393,11 @@  -- ** Common operations on Messages -filterMessages ::-  SeqDirection seq =>-  (Message -> Bool) ->-  DirectionalSeq seq Message ->-  DirectionalSeq seq Message-filterMessages p = onDirectedSeq (Seq.filter p)+filterMessages :: SeqDirection seq+               => (a -> Bool)+               -> DirectionalSeq seq a+               -> DirectionalSeq seq a+filterMessages f = onDirectedSeq (Seq.filter f)  class MessageOps a where     -- | addMessage inserts a date in proper chronological order, with@@ -406,6 +435,9 @@ noMessages :: Messages noMessages = DSeq mempty +messagesLength :: DirectionalSeq seq a -> Int+messagesLength (DSeq ms) = Seq.length ms+ -- | Reverse the order of the messages reverseMessages :: Messages -> RetrogradeMessages reverseMessages = DSeq . Seq.reverse . dseq@@ -526,6 +558,12 @@ -- set. getPrevMessage :: Maybe MessageId -> Messages -> Maybe Message getPrevMessage mId = getRelMessageId mId . reverseMessages++messagesHead :: (SeqDirection seq) => DirectionalSeq seq a -> Maybe a+messagesHead = withDirSeqHead id++messagesDrop :: (SeqDirection seq) => Int -> DirectionalSeq seq a -> DirectionalSeq seq a+messagesDrop i = onDirectedSeq (Seq.drop i)  -- | Look forward for the first Message with an ID that follows the -- specified MessageId and return that found Message's ID; if no input
src/Types/Posts.hs view
@@ -54,6 +54,7 @@ import qualified Data.Map.Strict as Map import qualified Data.Sequence as Seq import qualified Data.Text as T+import qualified Data.Set as S import           Data.Time.Clock ( getCurrentTime ) import           Lens.Micro.Platform ( makeLenses ) @@ -123,7 +124,7 @@   , _cpInReplyToPost :: Maybe PostId   , _cpPostId        :: PostId   , _cpChannelId     :: ChannelId-  , _cpReactions     :: Map.Map Text Int+  , _cpReactions     :: Map.Map Text (S.Set UserId)   , _cpOriginalPost  :: Post   , _cpFromWebhook   :: Bool   } deriving (Show)
test/Message_QCA.hs view
@@ -9,6 +9,7 @@ import Data.Maybe (fromMaybe) import Data.UUID (UUID, fromByteString) import qualified Data.ByteString.Lazy as BSL+import qualified Data.Set as S import Network.Mattermost.QuickCheck import Network.Mattermost.Types import Test.Tasty.QuickCheck@@ -18,6 +19,9 @@ genMap :: Ord key => Gen key -> Gen value -> Gen (Map key value) genMap gk gv = let kv = (,) <$> gk <*> gv in fromList <$> listOf kv +genSet :: (Ord v) => Gen v -> Gen (S.Set v)+genSet gv = S.fromList <$> listOf gv+ genUserRef :: Gen UserRef genUserRef = oneof [ return NoUser                    , UserI <$> arbitrary <*> genUserId@@ -36,7 +40,7 @@              <*> genSeq genAttachment              <*> genReplyState              <*> (fmap MessagePostId <$> genMaybe genPostId)-             <*> genMap genText arbitrary+             <*> genMap genText (genSet genUserId)              <*> genMaybe genPost              <*> arbitrary              <*> (Just <$> genChannelId)@@ -63,7 +67,7 @@                               <*> genSeq genAttachment                               <*> genReplyState                               <*> (Just <$> MessagePostId <$> genPostId)  -- must have been Posted if deleted-                              <*> genMap genText arbitrary+                              <*> genMap genText (genSet genUserId)                               <*> genMaybe genPost                               <*> arbitrary                               <*> (Just <$> genChannelId))@@ -84,7 +88,7 @@                          <*> genSeq genAttachment                          <*> genReplyState                          <*> (Just <$> MessagePostId <$> genPostId)-                         <*> genMap genText arbitrary+                         <*> genMap genText (genSet genUserId)                          <*> genMaybe genPost                          <*> arbitrary                          <*> (Just <$> genChannelId))