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 +49/−0
- README.md +60/−27
- emoji/LICENSE.txt +202/−0
- emoji/NOTICE.txt +4/−0
- emoji/emoji.json +1/−0
- matterhorn.cabal +16/−6
- src/Command.hs +25/−12
- src/Draw.hs +12/−7
- src/Draw/Autocomplete.hs +34/−0
- src/Draw/ChannelListOverlay.hs +51/−0
- src/Draw/DeleteChannelConfirm.hs +1/−1
- src/Draw/JoinChannel.hs +0/−56
- src/Draw/LeaveChannelConfirm.hs +1/−1
- src/Draw/ListOverlay.hs +82/−0
- src/Draw/Main.hs +14/−6
- src/Draw/ManageAttachments.hs +1/−1
- src/Draw/Messages.hs +28/−3
- src/Draw/PostListOverlay.hs +1/−1
- src/Draw/ReactionEmojiListOverlay.hs +37/−0
- src/Draw/ShowHelp.hs +16/−9
- src/Draw/UserListOverlay.hs +51/−99
- src/Draw/ViewMessage.hs +54/−17
- src/Emoji.hs +103/−0
- src/Events.hs +8/−3
- src/Events/ChannelListOverlay.hs +28/−0
- src/Events/JoinChannel.hs +0/−43
- src/Events/Keybindings.hs +1/−0
- src/Events/Main.hs +0/−40
- src/Events/MessageSelect.hs +4/−0
- src/Events/ReactionEmojiListOverlay.hs +28/−0
- src/Events/UserListOverlay.hs +9/−22
- src/FilePaths.hs +36/−1
- src/Login.hs +18/−6
- src/Markdown.hs +9/−7
- src/State/Autocomplete.hs +72/−26
- src/State/ChannelListOverlay.hs +78/−0
- src/State/Channels.hs +3/−33
- src/State/Common.hs +69/−13
- src/State/Editing.hs +52/−3
- src/State/Editing.hs-boot +10/−0
- src/State/ListOverlay.hs +138/−0
- src/State/MessageSelect.hs +9/−1
- src/State/Messages.hs +8/−9
- src/State/PostListOverlay.hs +1/−1
- src/State/ReactionEmojiListOverlay.hs +125/−0
- src/State/Reactions.hs +8/−2
- src/State/Setup.hs +19/−6
- src/State/Setup/Threads.hs +5/−1
- src/State/Themes.hs +4/−3
- src/State/UserListOverlay.hs +40/−180
- src/State/Users.hs +28/−36
- src/Themes.hs +8/−2
- src/Types.hs +200/−63
- src/Types/KeyEvents.hs +3/−0
- src/Types/Messages.hs +50/−12
- src/Types/Posts.hs +2/−1
- test/Message_QCA.hs +7/−3
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))