diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2017-03-08 12:45:42 +0000 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2017-03-08 12:45:42 +0000 |
commit | bb11a062eb19f08dd692da7e54ca9ae13fb23f36 (patch) | |
tree | a3c313258a95270d429c5c8a89b6c760e682aa66 | |
parent | 4cbc39bfab32f7c9ac038ba9dcecf14efebb2b9d (diff) | |
parent | 685e261f0a9f2495b29e552489e174987101f1f9 (diff) | |
download | gitlab-ce-bb11a062eb19f08dd692da7e54ca9ae13fb23f36.tar.gz |
Merge branch 'master' into 20450-fix-ujs-actions
* master: (184 commits)
fixed user_access_request_spec
Fixed changelog and a haml condition project.html.haml
Fixed some typos inside the _project.html.haml partial
Added MR number to changelog
Removed the settings gear button inside the Project to a tab
Add changelog for filtered-search-visual-tokens
Fix edit last visual token
Fix filtered search visual token editing dropdown
Code improvements
Add filtered search visual tokens
Fix transient failure in TodoService spec
Returns correct header data for commits endpoint
Fix pagination headers for repository commits api endpoint
Manually set total_count when paginating commits
Fix go-get support for projects in nested groups
Remove unecessary endpoint from repository, add compatibility endpoints for v3 and several improvements
Add configuration instructions for Container Registry Notifications.[ci skip]
Update API endpoints for raw files
Clear AR connections before starting Sidekiq
API V4 is no longer in Beta
...
2277 files changed, 23943 insertions, 12899 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index e075de055e3..42e094bdfc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.17.3 (2017-03-07) + +- Fix the redirect to custom home page URL. !9518 +- Fix broken migration when upgrading straight to 8.17.1. !9613 +- Make projects dropdown only show projects you are a member of. !9614 +- Fix creating a file in an empty repo using the API. !9632 +- Don't copy tooltip when copying GFM. +- Fix cherry-picking or reverting through an MR. + ## 8.17.2 (2017-03-01) - Expire all webpack assets after 8.17.1 included a badly compiled asset. !9602 @@ -18,25 +18,26 @@ gem 'pg', '~> 0.18.2', group: :postgres gem 'rugged', '~> 0.24.0' # Authentication libraries -gem 'devise', '~> 4.2' -gem 'doorkeeper', '~> 4.2.0' -gem 'omniauth', '~> 1.4.2' -gem 'omniauth-auth0', '~> 1.4.1' -gem 'omniauth-azure-oauth2', '~> 0.0.6' -gem 'omniauth-cas3', '~> 1.1.2' -gem 'omniauth-facebook', '~> 4.0.0' -gem 'omniauth-github', '~> 1.1.1' -gem 'omniauth-gitlab', '~> 1.0.2' +gem 'devise', '~> 4.2' +gem 'doorkeeper', '~> 4.2.0' +gem 'doorkeeper-openid_connect', '~> 1.1.0' +gem 'omniauth', '~> 1.4.2' +gem 'omniauth-auth0', '~> 1.4.1' +gem 'omniauth-azure-oauth2', '~> 0.0.6' +gem 'omniauth-cas3', '~> 1.1.2' +gem 'omniauth-facebook', '~> 4.0.0' +gem 'omniauth-github', '~> 1.1.1' +gem 'omniauth-gitlab', '~> 1.0.2' gem 'omniauth-google-oauth2', '~> 0.4.1' -gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos +gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-oauth2-generic', '~> 0.2.2' -gem 'omniauth-saml', '~> 1.7.0' -gem 'omniauth-shibboleth', '~> 1.2.0' -gem 'omniauth-twitter', '~> 1.2.0' -gem 'omniauth_crowd', '~> 2.2.0' -gem 'omniauth-authentiq', '~> 0.3.0' -gem 'rack-oauth2', '~> 1.2.1' -gem 'jwt', '~> 1.5.6' +gem 'omniauth-saml', '~> 1.7.0' +gem 'omniauth-shibboleth', '~> 1.2.0' +gem 'omniauth-twitter', '~> 1.2.0' +gem 'omniauth_crowd', '~> 2.2.0' +gem 'omniauth-authentiq', '~> 0.3.0' +gem 'rack-oauth2', '~> 1.2.1' +gem 'jwt', '~> 1.5.6' # Spam and anti-bot protection gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails' @@ -68,9 +69,9 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false gem 'github-linguist', '~> 4.7.0', require: 'linguist' # API -gem 'grape', '~> 0.19.0' +gem 'grape', '~> 0.19.0' gem 'grape-entity', '~> 0.6.0' -gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' +gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' # Pagination gem 'kaminari', '~> 0.17.0' @@ -102,19 +103,19 @@ gem 'unf', '~> 0.1.4' gem 'seed-fu', '~> 2.3.5' # Markdown and HTML processing -gem 'html-pipeline', '~> 1.11.0' -gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie' -gem 'gitlab-markup', '~> 1.5.1' -gem 'redcarpet', '~> 3.4' -gem 'RedCloth', '~> 4.3.2' -gem 'rdoc', '~> 4.2' -gem 'org-ruby', '~> 0.9.12' -gem 'creole', '~> 0.5.0' -gem 'wikicloth', '0.8.1' -gem 'asciidoctor', '~> 1.5.2' +gem 'html-pipeline', '~> 1.11.0' +gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie' +gem 'gitlab-markup', '~> 1.5.1' +gem 'redcarpet', '~> 3.4' +gem 'RedCloth', '~> 4.3.2' +gem 'rdoc', '~> 4.2' +gem 'org-ruby', '~> 0.9.12' +gem 'creole', '~> 0.5.0' +gem 'wikicloth', '0.8.1' +gem 'asciidoctor', '~> 1.5.2' gem 'asciidoctor-plantuml', '0.0.7' -gem 'rouge', '~> 2.0' -gem 'truncato', '~> 0.7.8' +gem 'rouge', '~> 2.0' +gem 'truncato', '~> 0.7.8' # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM @@ -229,18 +230,18 @@ gem 'sass-rails', '~> 5.0.6' gem 'coffee-rails', '~> 4.1.0' gem 'uglifier', '~> 2.7.2' -gem 'addressable', '~> 2.3.8' -gem 'bootstrap-sass', '~> 3.3.0' -gem 'font-awesome-rails', '~> 4.6.1' -gem 'gemojione', '~> 3.0' -gem 'gon', '~> 6.1.0' +gem 'addressable', '~> 2.3.8' +gem 'bootstrap-sass', '~> 3.3.0' +gem 'font-awesome-rails', '~> 4.7' +gem 'gemojione', '~> 3.0' +gem 'gon', '~> 6.1.0' gem 'jquery-atwho-rails', '~> 1.3.2' -gem 'jquery-rails', '~> 4.1.0' -gem 'request_store', '~> 1.3' -gem 'select2-rails', '~> 3.5.9' -gem 'virtus', '~> 1.0.1' -gem 'net-ssh', '~> 3.0.1' -gem 'base32', '~> 0.3.0' +gem 'jquery-rails', '~> 4.1.0' +gem 'request_store', '~> 1.3' +gem 'select2-rails', '~> 3.5.9' +gem 'virtus', '~> 1.0.1' +gem 'net-ssh', '~> 3.0.1' +gem 'base32', '~> 0.3.0' # Sentry integration gem 'sentry-raven', '~> 2.0.0' @@ -278,13 +279,13 @@ group :development, :test do gem 'awesome_print', '~> 1.2.0', require: false gem 'fuubar', '~> 2.0.0' - gem 'database_cleaner', '~> 1.5.0' + gem 'database_cleaner', '~> 1.5.0' gem 'factory_girl_rails', '~> 4.7.0' - gem 'rspec-rails', '~> 3.5.0' - gem 'rspec-retry', '~> 0.4.5' - gem 'spinach-rails', '~> 0.2.1' + gem 'rspec-rails', '~> 3.5.0' + gem 'rspec-retry', '~> 0.4.5' + gem 'spinach-rails', '~> 0.2.1' gem 'spinach-rerun-reporter', '~> 0.0.2' - gem 'rspec_profiling', '~> 0.0.5' + gem 'rspec_profiling', '~> 0.0.5' # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) gem 'minitest', '~> 5.7.0' @@ -292,13 +293,13 @@ group :development, :test do # Generate Fake data gem 'ffaker', '~> 2.4' - gem 'capybara', '~> 2.6.2' + gem 'capybara', '~> 2.6.2' gem 'capybara-screenshot', '~> 1.0.0' - gem 'poltergeist', '~> 1.9.0' + gem 'poltergeist', '~> 1.9.0' - gem 'spring', '~> 1.7.0' - gem 'spring-commands-rspec', '~> 1.0.4' - gem 'spring-commands-spinach', '~> 1.1.0' + gem 'spring', '~> 1.7.0' + gem 'spring-commands-rspec', '~> 1.0.4' + gem 'spring-commands-spinach', '~> 1.1.0' gem 'rubocop', '~> 0.47.1', require: false gem 'rubocop-rspec', '~> 1.12.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index f59bde27bc4..62388628eaa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,6 +78,7 @@ GEM better_errors (1.0.1) coderay (>= 1.0.0) erubis (>= 2.6.6) + bindata (2.3.5) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) bootstrap-sass (3.3.6) @@ -167,6 +168,9 @@ GEM unf (>= 0.0.5, < 1.0.0) doorkeeper (4.2.0) railties (>= 4.2) + doorkeeper-openid_connect (1.1.2) + doorkeeper (~> 4.0) + json-jwt (~> 1.6) dropzonejs-rails (0.7.2) rails (> 3.1) email_reply_trimmer (0.1.6) @@ -232,7 +236,7 @@ GEM fog-xml (0.1.2) fog-core nokogiri (~> 1.5, >= 1.5.11) - font-awesome-rails (4.6.1.0) + font-awesome-rails (4.7.0.1) railties (>= 3.2, < 5.1) foreman (0.78.0) thor (~> 0.19.1) @@ -376,6 +380,12 @@ GEM railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (1.8.6) + json-jwt (1.7.1) + activesupport + bindata + multi_json (>= 1.3) + securecompare + url_safe_base64 json-schema (2.6.2) addressable (~> 2.3.8) jwt (1.5.6) @@ -684,6 +694,7 @@ GEM scss_lint (0.47.1) rake (>= 0.9, < 11) sass (~> 3.4.15) + securecompare (1.0.0) seed-fu (2.3.6) activerecord (>= 3.1) activesupport (>= 3.1) @@ -789,6 +800,7 @@ GEM get_process_mem (~> 0) unicorn (>= 4, < 6) uniform_notifier (1.10.0) + url_safe_base64 (0.2.2) validates_hostname (1.0.6) activerecord (>= 3.0) activesupport (>= 3.0) @@ -866,6 +878,7 @@ DEPENDENCIES devise-two-factor (~> 3.0.0) diffy (~> 3.1.0) doorkeeper (~> 4.2.0) + doorkeeper-openid_connect (~> 1.1.0) dropzonejs-rails (~> 0.7.1) email_reply_trimmer (~> 0.1) email_spec (~> 1.6.0) @@ -878,7 +891,7 @@ DEPENDENCIES fog-local (~> 0.3) fog-openstack (~> 0.1) fog-rackspace (~> 0.1.1) - font-awesome-rails (~> 4.6.1) + font-awesome-rails (~> 4.7) foreman (~> 0.78.0) fuubar (~> 2.0.0) gemnasium-gitlab-service (~> 0.2) diff --git a/app/assets/images/emoji.png b/app/assets/images/emoji.png Binary files differindex 6f1a34a5591..5dcd9c09b70 100644 --- a/app/assets/images/emoji.png +++ b/app/assets/images/emoji.png diff --git a/app/assets/images/emoji/100.png b/app/assets/images/emoji/100.png Binary files differnew file mode 100644 index 00000000000..6903ff0304a --- /dev/null +++ b/app/assets/images/emoji/100.png diff --git a/app/assets/images/emoji/1234.png b/app/assets/images/emoji/1234.png Binary files differnew file mode 100644 index 00000000000..248dc7e55b6 --- /dev/null +++ b/app/assets/images/emoji/1234.png diff --git a/app/assets/images/emoji/1F627.png b/app/assets/images/emoji/1F627.png Binary files differnew file mode 100644 index 00000000000..f99026a3bc7 --- /dev/null +++ b/app/assets/images/emoji/1F627.png diff --git a/app/assets/images/emoji/8ball.png b/app/assets/images/emoji/8ball.png Binary files differnew file mode 100644 index 00000000000..38ca662eded --- /dev/null +++ b/app/assets/images/emoji/8ball.png diff --git a/app/assets/images/emoji/a.png b/app/assets/images/emoji/a.png Binary files differnew file mode 100644 index 00000000000..8603ff05a17 --- /dev/null +++ b/app/assets/images/emoji/a.png diff --git a/app/assets/images/emoji/ab.png b/app/assets/images/emoji/ab.png Binary files differnew file mode 100644 index 00000000000..d9f2d17dea0 --- /dev/null +++ b/app/assets/images/emoji/ab.png diff --git a/app/assets/images/emoji/abc.png b/app/assets/images/emoji/abc.png Binary files differnew file mode 100644 index 00000000000..7688de692a9 --- /dev/null +++ b/app/assets/images/emoji/abc.png diff --git a/app/assets/images/emoji/abcd.png b/app/assets/images/emoji/abcd.png Binary files differnew file mode 100644 index 00000000000..0996a870570 --- /dev/null +++ b/app/assets/images/emoji/abcd.png diff --git a/app/assets/images/emoji/accept.png b/app/assets/images/emoji/accept.png Binary files differnew file mode 100644 index 00000000000..8afd7ce99cf --- /dev/null +++ b/app/assets/images/emoji/accept.png diff --git a/app/assets/images/emoji/aerial_tramway.png b/app/assets/images/emoji/aerial_tramway.png Binary files differnew file mode 100644 index 00000000000..3eb4b61bf1d --- /dev/null +++ b/app/assets/images/emoji/aerial_tramway.png diff --git a/app/assets/images/emoji/airplane.png b/app/assets/images/emoji/airplane.png Binary files differnew file mode 100644 index 00000000000..268d2ac3c8e --- /dev/null +++ b/app/assets/images/emoji/airplane.png diff --git a/app/assets/images/emoji/airplane_arriving.png b/app/assets/images/emoji/airplane_arriving.png Binary files differnew file mode 100644 index 00000000000..d66841962f2 --- /dev/null +++ b/app/assets/images/emoji/airplane_arriving.png diff --git a/app/assets/images/emoji/airplane_departure.png b/app/assets/images/emoji/airplane_departure.png Binary files differnew file mode 100644 index 00000000000..a5766f9f4ae --- /dev/null +++ b/app/assets/images/emoji/airplane_departure.png diff --git a/app/assets/images/emoji/airplane_small.png b/app/assets/images/emoji/airplane_small.png Binary files differnew file mode 100644 index 00000000000..b731b15e3a8 --- /dev/null +++ b/app/assets/images/emoji/airplane_small.png diff --git a/app/assets/images/emoji/alarm_clock.png b/app/assets/images/emoji/alarm_clock.png Binary files differnew file mode 100644 index 00000000000..cdbc2fbb950 --- /dev/null +++ b/app/assets/images/emoji/alarm_clock.png diff --git a/app/assets/images/emoji/alembic.png b/app/assets/images/emoji/alembic.png Binary files differnew file mode 100644 index 00000000000..307a7324249 --- /dev/null +++ b/app/assets/images/emoji/alembic.png diff --git a/app/assets/images/emoji/alien.png b/app/assets/images/emoji/alien.png Binary files differnew file mode 100644 index 00000000000..3b90e97433b --- /dev/null +++ b/app/assets/images/emoji/alien.png diff --git a/app/assets/images/emoji/ambulance.png b/app/assets/images/emoji/ambulance.png Binary files differnew file mode 100644 index 00000000000..6fb8076d766 --- /dev/null +++ b/app/assets/images/emoji/ambulance.png diff --git a/app/assets/images/emoji/amphora.png b/app/assets/images/emoji/amphora.png Binary files differnew file mode 100644 index 00000000000..96de5056059 --- /dev/null +++ b/app/assets/images/emoji/amphora.png diff --git a/app/assets/images/emoji/anchor.png b/app/assets/images/emoji/anchor.png Binary files differnew file mode 100644 index 00000000000..b036f70a00b --- /dev/null +++ b/app/assets/images/emoji/anchor.png diff --git a/app/assets/images/emoji/angel.png b/app/assets/images/emoji/angel.png Binary files differnew file mode 100644 index 00000000000..66ea97a3b99 --- /dev/null +++ b/app/assets/images/emoji/angel.png diff --git a/app/assets/images/emoji/angel_tone1.png b/app/assets/images/emoji/angel_tone1.png Binary files differnew file mode 100644 index 00000000000..391694dc07e --- /dev/null +++ b/app/assets/images/emoji/angel_tone1.png diff --git a/app/assets/images/emoji/angel_tone2.png b/app/assets/images/emoji/angel_tone2.png Binary files differnew file mode 100644 index 00000000000..700cbe6ed2c --- /dev/null +++ b/app/assets/images/emoji/angel_tone2.png diff --git a/app/assets/images/emoji/angel_tone3.png b/app/assets/images/emoji/angel_tone3.png Binary files differnew file mode 100644 index 00000000000..be597437d25 --- /dev/null +++ b/app/assets/images/emoji/angel_tone3.png diff --git a/app/assets/images/emoji/angel_tone4.png b/app/assets/images/emoji/angel_tone4.png Binary files differnew file mode 100644 index 00000000000..b06d3c853ef --- /dev/null +++ b/app/assets/images/emoji/angel_tone4.png diff --git a/app/assets/images/emoji/angel_tone5.png b/app/assets/images/emoji/angel_tone5.png Binary files differnew file mode 100644 index 00000000000..17bd677e334 --- /dev/null +++ b/app/assets/images/emoji/angel_tone5.png diff --git a/app/assets/images/emoji/anger.png b/app/assets/images/emoji/anger.png Binary files differnew file mode 100644 index 00000000000..d63c2e000e4 --- /dev/null +++ b/app/assets/images/emoji/anger.png diff --git a/app/assets/images/emoji/anger_right.png b/app/assets/images/emoji/anger_right.png Binary files differnew file mode 100644 index 00000000000..f5c97c4d297 --- /dev/null +++ b/app/assets/images/emoji/anger_right.png diff --git a/app/assets/images/emoji/angry.png b/app/assets/images/emoji/angry.png Binary files differnew file mode 100644 index 00000000000..cfc4a6ecde5 --- /dev/null +++ b/app/assets/images/emoji/angry.png diff --git a/app/assets/images/emoji/ant.png b/app/assets/images/emoji/ant.png Binary files differnew file mode 100644 index 00000000000..994127ed6b3 --- /dev/null +++ b/app/assets/images/emoji/ant.png diff --git a/app/assets/images/emoji/apple.png b/app/assets/images/emoji/apple.png Binary files differnew file mode 100644 index 00000000000..da650c60f62 --- /dev/null +++ b/app/assets/images/emoji/apple.png diff --git a/app/assets/images/emoji/aquarius.png b/app/assets/images/emoji/aquarius.png Binary files differnew file mode 100644 index 00000000000..641a4f68889 --- /dev/null +++ b/app/assets/images/emoji/aquarius.png diff --git a/app/assets/images/emoji/aries.png b/app/assets/images/emoji/aries.png Binary files differnew file mode 100644 index 00000000000..21a189d0ede --- /dev/null +++ b/app/assets/images/emoji/aries.png diff --git a/app/assets/images/emoji/arrow_backward.png b/app/assets/images/emoji/arrow_backward.png Binary files differnew file mode 100644 index 00000000000..ee38e3b038e --- /dev/null +++ b/app/assets/images/emoji/arrow_backward.png diff --git a/app/assets/images/emoji/arrow_double_down.png b/app/assets/images/emoji/arrow_double_down.png Binary files differnew file mode 100644 index 00000000000..90193bfcb40 --- /dev/null +++ b/app/assets/images/emoji/arrow_double_down.png diff --git a/app/assets/images/emoji/arrow_double_up.png b/app/assets/images/emoji/arrow_double_up.png Binary files differnew file mode 100644 index 00000000000..13543d5eef2 --- /dev/null +++ b/app/assets/images/emoji/arrow_double_up.png diff --git a/app/assets/images/emoji/arrow_down.png b/app/assets/images/emoji/arrow_down.png Binary files differnew file mode 100644 index 00000000000..b8eefd0b19f --- /dev/null +++ b/app/assets/images/emoji/arrow_down.png diff --git a/app/assets/images/emoji/arrow_down_small.png b/app/assets/images/emoji/arrow_down_small.png Binary files differnew file mode 100644 index 00000000000..5870b9a2241 --- /dev/null +++ b/app/assets/images/emoji/arrow_down_small.png diff --git a/app/assets/images/emoji/arrow_forward.png b/app/assets/images/emoji/arrow_forward.png Binary files differnew file mode 100644 index 00000000000..4e2b682857c --- /dev/null +++ b/app/assets/images/emoji/arrow_forward.png diff --git a/app/assets/images/emoji/arrow_heading_down.png b/app/assets/images/emoji/arrow_heading_down.png Binary files differnew file mode 100644 index 00000000000..2d9d24bca80 --- /dev/null +++ b/app/assets/images/emoji/arrow_heading_down.png diff --git a/app/assets/images/emoji/arrow_heading_up.png b/app/assets/images/emoji/arrow_heading_up.png Binary files differnew file mode 100644 index 00000000000..f29bfcfc0de --- /dev/null +++ b/app/assets/images/emoji/arrow_heading_up.png diff --git a/app/assets/images/emoji/arrow_left.png b/app/assets/images/emoji/arrow_left.png Binary files differnew file mode 100644 index 00000000000..8c685e0a81b --- /dev/null +++ b/app/assets/images/emoji/arrow_left.png diff --git a/app/assets/images/emoji/arrow_lower_left.png b/app/assets/images/emoji/arrow_lower_left.png Binary files differnew file mode 100644 index 00000000000..88b37716078 --- /dev/null +++ b/app/assets/images/emoji/arrow_lower_left.png diff --git a/app/assets/images/emoji/arrow_lower_right.png b/app/assets/images/emoji/arrow_lower_right.png Binary files differnew file mode 100644 index 00000000000..7e807da7392 --- /dev/null +++ b/app/assets/images/emoji/arrow_lower_right.png diff --git a/app/assets/images/emoji/arrow_right.png b/app/assets/images/emoji/arrow_right.png Binary files differnew file mode 100644 index 00000000000..4755670b5cc --- /dev/null +++ b/app/assets/images/emoji/arrow_right.png diff --git a/app/assets/images/emoji/arrow_right_hook.png b/app/assets/images/emoji/arrow_right_hook.png Binary files differnew file mode 100644 index 00000000000..e7258ad3268 --- /dev/null +++ b/app/assets/images/emoji/arrow_right_hook.png diff --git a/app/assets/images/emoji/arrow_up.png b/app/assets/images/emoji/arrow_up.png Binary files differnew file mode 100644 index 00000000000..af8218a87f7 --- /dev/null +++ b/app/assets/images/emoji/arrow_up.png diff --git a/app/assets/images/emoji/arrow_up_down.png b/app/assets/images/emoji/arrow_up_down.png Binary files differnew file mode 100644 index 00000000000..dfa32b97186 --- /dev/null +++ b/app/assets/images/emoji/arrow_up_down.png diff --git a/app/assets/images/emoji/arrow_up_small.png b/app/assets/images/emoji/arrow_up_small.png Binary files differnew file mode 100644 index 00000000000..20a13dcd5cd --- /dev/null +++ b/app/assets/images/emoji/arrow_up_small.png diff --git a/app/assets/images/emoji/arrow_upper_left.png b/app/assets/images/emoji/arrow_upper_left.png Binary files differnew file mode 100644 index 00000000000..f38718fbe34 --- /dev/null +++ b/app/assets/images/emoji/arrow_upper_left.png diff --git a/app/assets/images/emoji/arrow_upper_right.png b/app/assets/images/emoji/arrow_upper_right.png Binary files differnew file mode 100644 index 00000000000..c43e12d0f64 --- /dev/null +++ b/app/assets/images/emoji/arrow_upper_right.png diff --git a/app/assets/images/emoji/arrows_clockwise.png b/app/assets/images/emoji/arrows_clockwise.png Binary files differnew file mode 100644 index 00000000000..26e49c38388 --- /dev/null +++ b/app/assets/images/emoji/arrows_clockwise.png diff --git a/app/assets/images/emoji/arrows_counterclockwise.png b/app/assets/images/emoji/arrows_counterclockwise.png Binary files differnew file mode 100644 index 00000000000..8d06d8e0912 --- /dev/null +++ b/app/assets/images/emoji/arrows_counterclockwise.png diff --git a/app/assets/images/emoji/art.png b/app/assets/images/emoji/art.png Binary files differnew file mode 100644 index 00000000000..bd6afe9ff06 --- /dev/null +++ b/app/assets/images/emoji/art.png diff --git a/app/assets/images/emoji/articulated_lorry.png b/app/assets/images/emoji/articulated_lorry.png Binary files differnew file mode 100644 index 00000000000..c8217317132 --- /dev/null +++ b/app/assets/images/emoji/articulated_lorry.png diff --git a/app/assets/images/emoji/asterisk.png b/app/assets/images/emoji/asterisk.png Binary files differnew file mode 100644 index 00000000000..2f8e5113803 --- /dev/null +++ b/app/assets/images/emoji/asterisk.png diff --git a/app/assets/images/emoji/astonished.png b/app/assets/images/emoji/astonished.png Binary files differnew file mode 100644 index 00000000000..bd0ac55ec8e --- /dev/null +++ b/app/assets/images/emoji/astonished.png diff --git a/app/assets/images/emoji/athletic_shoe.png b/app/assets/images/emoji/athletic_shoe.png Binary files differnew file mode 100644 index 00000000000..423fa07dd5d --- /dev/null +++ b/app/assets/images/emoji/athletic_shoe.png diff --git a/app/assets/images/emoji/atm.png b/app/assets/images/emoji/atm.png Binary files differnew file mode 100644 index 00000000000..4d935307b94 --- /dev/null +++ b/app/assets/images/emoji/atm.png diff --git a/app/assets/images/emoji/atom.png b/app/assets/images/emoji/atom.png Binary files differnew file mode 100644 index 00000000000..5f4567aa093 --- /dev/null +++ b/app/assets/images/emoji/atom.png diff --git a/app/assets/images/emoji/avocado.png b/app/assets/images/emoji/avocado.png Binary files differnew file mode 100644 index 00000000000..06f0d124aed --- /dev/null +++ b/app/assets/images/emoji/avocado.png diff --git a/app/assets/images/emoji/b.png b/app/assets/images/emoji/b.png Binary files differnew file mode 100644 index 00000000000..25875bc6a14 --- /dev/null +++ b/app/assets/images/emoji/b.png diff --git a/app/assets/images/emoji/baby.png b/app/assets/images/emoji/baby.png Binary files differnew file mode 100644 index 00000000000..a4af92c63c7 --- /dev/null +++ b/app/assets/images/emoji/baby.png diff --git a/app/assets/images/emoji/baby_bottle.png b/app/assets/images/emoji/baby_bottle.png Binary files differnew file mode 100644 index 00000000000..2bd10524180 --- /dev/null +++ b/app/assets/images/emoji/baby_bottle.png diff --git a/app/assets/images/emoji/baby_chick.png b/app/assets/images/emoji/baby_chick.png Binary files differnew file mode 100644 index 00000000000..dccd96576ea --- /dev/null +++ b/app/assets/images/emoji/baby_chick.png diff --git a/app/assets/images/emoji/baby_symbol.png b/app/assets/images/emoji/baby_symbol.png Binary files differnew file mode 100644 index 00000000000..64a10b71710 --- /dev/null +++ b/app/assets/images/emoji/baby_symbol.png diff --git a/app/assets/images/emoji/baby_tone1.png b/app/assets/images/emoji/baby_tone1.png Binary files differnew file mode 100644 index 00000000000..d20911d40db --- /dev/null +++ b/app/assets/images/emoji/baby_tone1.png diff --git a/app/assets/images/emoji/baby_tone2.png b/app/assets/images/emoji/baby_tone2.png Binary files differnew file mode 100644 index 00000000000..b0a9b30ed17 --- /dev/null +++ b/app/assets/images/emoji/baby_tone2.png diff --git a/app/assets/images/emoji/baby_tone3.png b/app/assets/images/emoji/baby_tone3.png Binary files differnew file mode 100644 index 00000000000..7de5286fac1 --- /dev/null +++ b/app/assets/images/emoji/baby_tone3.png diff --git a/app/assets/images/emoji/baby_tone4.png b/app/assets/images/emoji/baby_tone4.png Binary files differnew file mode 100644 index 00000000000..9b7a86ac615 --- /dev/null +++ b/app/assets/images/emoji/baby_tone4.png diff --git a/app/assets/images/emoji/baby_tone5.png b/app/assets/images/emoji/baby_tone5.png Binary files differnew file mode 100644 index 00000000000..fe1be34cb88 --- /dev/null +++ b/app/assets/images/emoji/baby_tone5.png diff --git a/app/assets/images/emoji/back.png b/app/assets/images/emoji/back.png Binary files differnew file mode 100644 index 00000000000..d32c5d4f17f --- /dev/null +++ b/app/assets/images/emoji/back.png diff --git a/app/assets/images/emoji/bacon.png b/app/assets/images/emoji/bacon.png Binary files differnew file mode 100644 index 00000000000..f38a485fbe4 --- /dev/null +++ b/app/assets/images/emoji/bacon.png diff --git a/app/assets/images/emoji/badminton.png b/app/assets/images/emoji/badminton.png Binary files differnew file mode 100644 index 00000000000..7ba15708990 --- /dev/null +++ b/app/assets/images/emoji/badminton.png diff --git a/app/assets/images/emoji/baggage_claim.png b/app/assets/images/emoji/baggage_claim.png Binary files differnew file mode 100644 index 00000000000..409b593e78a --- /dev/null +++ b/app/assets/images/emoji/baggage_claim.png diff --git a/app/assets/images/emoji/balloon.png b/app/assets/images/emoji/balloon.png Binary files differnew file mode 100644 index 00000000000..07916fe6df1 --- /dev/null +++ b/app/assets/images/emoji/balloon.png diff --git a/app/assets/images/emoji/ballot_box.png b/app/assets/images/emoji/ballot_box.png Binary files differnew file mode 100644 index 00000000000..9b6767aea9e --- /dev/null +++ b/app/assets/images/emoji/ballot_box.png diff --git a/app/assets/images/emoji/ballot_box_with_check.png b/app/assets/images/emoji/ballot_box_with_check.png Binary files differnew file mode 100644 index 00000000000..284d9573847 --- /dev/null +++ b/app/assets/images/emoji/ballot_box_with_check.png diff --git a/app/assets/images/emoji/bamboo.png b/app/assets/images/emoji/bamboo.png Binary files differnew file mode 100644 index 00000000000..5d5e0e728a0 --- /dev/null +++ b/app/assets/images/emoji/bamboo.png diff --git a/app/assets/images/emoji/banana.png b/app/assets/images/emoji/banana.png Binary files differnew file mode 100644 index 00000000000..f4987279580 --- /dev/null +++ b/app/assets/images/emoji/banana.png diff --git a/app/assets/images/emoji/bangbang.png b/app/assets/images/emoji/bangbang.png Binary files differnew file mode 100644 index 00000000000..58a9c528fca --- /dev/null +++ b/app/assets/images/emoji/bangbang.png diff --git a/app/assets/images/emoji/bank.png b/app/assets/images/emoji/bank.png Binary files differnew file mode 100644 index 00000000000..dffdcef36a1 --- /dev/null +++ b/app/assets/images/emoji/bank.png diff --git a/app/assets/images/emoji/bar_chart.png b/app/assets/images/emoji/bar_chart.png Binary files differnew file mode 100644 index 00000000000..53c89455008 --- /dev/null +++ b/app/assets/images/emoji/bar_chart.png diff --git a/app/assets/images/emoji/barber.png b/app/assets/images/emoji/barber.png Binary files differnew file mode 100644 index 00000000000..896f4d716cf --- /dev/null +++ b/app/assets/images/emoji/barber.png diff --git a/app/assets/images/emoji/baseball.png b/app/assets/images/emoji/baseball.png Binary files differnew file mode 100644 index 00000000000..f8463f1538b --- /dev/null +++ b/app/assets/images/emoji/baseball.png diff --git a/app/assets/images/emoji/basketball.png b/app/assets/images/emoji/basketball.png Binary files differnew file mode 100644 index 00000000000..64c76b79c6d --- /dev/null +++ b/app/assets/images/emoji/basketball.png diff --git a/app/assets/images/emoji/basketball_player.png b/app/assets/images/emoji/basketball_player.png Binary files differnew file mode 100644 index 00000000000..8ce90c5cad6 --- /dev/null +++ b/app/assets/images/emoji/basketball_player.png diff --git a/app/assets/images/emoji/basketball_player_tone1.png b/app/assets/images/emoji/basketball_player_tone1.png Binary files differnew file mode 100644 index 00000000000..cd12c7ab9bf --- /dev/null +++ b/app/assets/images/emoji/basketball_player_tone1.png diff --git a/app/assets/images/emoji/basketball_player_tone2.png b/app/assets/images/emoji/basketball_player_tone2.png Binary files differnew file mode 100644 index 00000000000..f892fd596da --- /dev/null +++ b/app/assets/images/emoji/basketball_player_tone2.png diff --git a/app/assets/images/emoji/basketball_player_tone3.png b/app/assets/images/emoji/basketball_player_tone3.png Binary files differnew file mode 100644 index 00000000000..e109997a91a --- /dev/null +++ b/app/assets/images/emoji/basketball_player_tone3.png diff --git a/app/assets/images/emoji/basketball_player_tone4.png b/app/assets/images/emoji/basketball_player_tone4.png Binary files differnew file mode 100644 index 00000000000..3b90b946af4 --- /dev/null +++ b/app/assets/images/emoji/basketball_player_tone4.png diff --git a/app/assets/images/emoji/basketball_player_tone5.png b/app/assets/images/emoji/basketball_player_tone5.png Binary files differnew file mode 100644 index 00000000000..bafed7828a7 --- /dev/null +++ b/app/assets/images/emoji/basketball_player_tone5.png diff --git a/app/assets/images/emoji/bat.png b/app/assets/images/emoji/bat.png Binary files differnew file mode 100644 index 00000000000..3152c047e00 --- /dev/null +++ b/app/assets/images/emoji/bat.png diff --git a/app/assets/images/emoji/bath.png b/app/assets/images/emoji/bath.png Binary files differnew file mode 100644 index 00000000000..43fba5c8a28 --- /dev/null +++ b/app/assets/images/emoji/bath.png diff --git a/app/assets/images/emoji/bath_tone1.png b/app/assets/images/emoji/bath_tone1.png Binary files differnew file mode 100644 index 00000000000..2152eabf2f5 --- /dev/null +++ b/app/assets/images/emoji/bath_tone1.png diff --git a/app/assets/images/emoji/bath_tone2.png b/app/assets/images/emoji/bath_tone2.png Binary files differnew file mode 100644 index 00000000000..2102e6133e3 --- /dev/null +++ b/app/assets/images/emoji/bath_tone2.png diff --git a/app/assets/images/emoji/bath_tone3.png b/app/assets/images/emoji/bath_tone3.png Binary files differnew file mode 100644 index 00000000000..fae66181e9f --- /dev/null +++ b/app/assets/images/emoji/bath_tone3.png diff --git a/app/assets/images/emoji/bath_tone4.png b/app/assets/images/emoji/bath_tone4.png Binary files differnew file mode 100644 index 00000000000..1f8959d0d99 --- /dev/null +++ b/app/assets/images/emoji/bath_tone4.png diff --git a/app/assets/images/emoji/bath_tone5.png b/app/assets/images/emoji/bath_tone5.png Binary files differnew file mode 100644 index 00000000000..c8a08e84f25 --- /dev/null +++ b/app/assets/images/emoji/bath_tone5.png diff --git a/app/assets/images/emoji/bathtub.png b/app/assets/images/emoji/bathtub.png Binary files differnew file mode 100644 index 00000000000..9a5f09361eb --- /dev/null +++ b/app/assets/images/emoji/bathtub.png diff --git a/app/assets/images/emoji/battery.png b/app/assets/images/emoji/battery.png Binary files differnew file mode 100644 index 00000000000..f593e2bdb65 --- /dev/null +++ b/app/assets/images/emoji/battery.png diff --git a/app/assets/images/emoji/beach.png b/app/assets/images/emoji/beach.png Binary files differnew file mode 100644 index 00000000000..69108c8ea10 --- /dev/null +++ b/app/assets/images/emoji/beach.png diff --git a/app/assets/images/emoji/beach_umbrella.png b/app/assets/images/emoji/beach_umbrella.png Binary files differnew file mode 100644 index 00000000000..220a74f8132 --- /dev/null +++ b/app/assets/images/emoji/beach_umbrella.png diff --git a/app/assets/images/emoji/bear.png b/app/assets/images/emoji/bear.png Binary files differnew file mode 100644 index 00000000000..272d56bbbcc --- /dev/null +++ b/app/assets/images/emoji/bear.png diff --git a/app/assets/images/emoji/bed.png b/app/assets/images/emoji/bed.png Binary files differnew file mode 100644 index 00000000000..86f964e245d --- /dev/null +++ b/app/assets/images/emoji/bed.png diff --git a/app/assets/images/emoji/bee.png b/app/assets/images/emoji/bee.png Binary files differnew file mode 100644 index 00000000000..46156060096 --- /dev/null +++ b/app/assets/images/emoji/bee.png diff --git a/app/assets/images/emoji/beer.png b/app/assets/images/emoji/beer.png Binary files differnew file mode 100644 index 00000000000..b6d73dc0b7a --- /dev/null +++ b/app/assets/images/emoji/beer.png diff --git a/app/assets/images/emoji/beers.png b/app/assets/images/emoji/beers.png Binary files differnew file mode 100644 index 00000000000..b55deb66b41 --- /dev/null +++ b/app/assets/images/emoji/beers.png diff --git a/app/assets/images/emoji/beetle.png b/app/assets/images/emoji/beetle.png Binary files differnew file mode 100644 index 00000000000..3d93174d7fc --- /dev/null +++ b/app/assets/images/emoji/beetle.png diff --git a/app/assets/images/emoji/beginner.png b/app/assets/images/emoji/beginner.png Binary files differnew file mode 100644 index 00000000000..bc434fb7cb5 --- /dev/null +++ b/app/assets/images/emoji/beginner.png diff --git a/app/assets/images/emoji/bell.png b/app/assets/images/emoji/bell.png Binary files differnew file mode 100644 index 00000000000..5b3b0461999 --- /dev/null +++ b/app/assets/images/emoji/bell.png diff --git a/app/assets/images/emoji/bellhop.png b/app/assets/images/emoji/bellhop.png Binary files differnew file mode 100644 index 00000000000..6b3297ceaf7 --- /dev/null +++ b/app/assets/images/emoji/bellhop.png diff --git a/app/assets/images/emoji/bento.png b/app/assets/images/emoji/bento.png Binary files differnew file mode 100644 index 00000000000..83d41ca7eb9 --- /dev/null +++ b/app/assets/images/emoji/bento.png diff --git a/app/assets/images/emoji/bicyclist.png b/app/assets/images/emoji/bicyclist.png Binary files differnew file mode 100644 index 00000000000..9274da11048 --- /dev/null +++ b/app/assets/images/emoji/bicyclist.png diff --git a/app/assets/images/emoji/bicyclist_tone1.png b/app/assets/images/emoji/bicyclist_tone1.png Binary files differnew file mode 100644 index 00000000000..decc2f728fe --- /dev/null +++ b/app/assets/images/emoji/bicyclist_tone1.png diff --git a/app/assets/images/emoji/bicyclist_tone2.png b/app/assets/images/emoji/bicyclist_tone2.png Binary files differnew file mode 100644 index 00000000000..0067717b80a --- /dev/null +++ b/app/assets/images/emoji/bicyclist_tone2.png diff --git a/app/assets/images/emoji/bicyclist_tone3.png b/app/assets/images/emoji/bicyclist_tone3.png Binary files differnew file mode 100644 index 00000000000..a4f7b5e2776 --- /dev/null +++ b/app/assets/images/emoji/bicyclist_tone3.png diff --git a/app/assets/images/emoji/bicyclist_tone4.png b/app/assets/images/emoji/bicyclist_tone4.png Binary files differnew file mode 100644 index 00000000000..a3c8a797db4 --- /dev/null +++ b/app/assets/images/emoji/bicyclist_tone4.png diff --git a/app/assets/images/emoji/bicyclist_tone5.png b/app/assets/images/emoji/bicyclist_tone5.png Binary files differnew file mode 100644 index 00000000000..1606a874051 --- /dev/null +++ b/app/assets/images/emoji/bicyclist_tone5.png diff --git a/app/assets/images/emoji/bike.png b/app/assets/images/emoji/bike.png Binary files differnew file mode 100644 index 00000000000..556ed70f1a7 --- /dev/null +++ b/app/assets/images/emoji/bike.png diff --git a/app/assets/images/emoji/bikini.png b/app/assets/images/emoji/bikini.png Binary files differnew file mode 100644 index 00000000000..77a8a0aae5b --- /dev/null +++ b/app/assets/images/emoji/bikini.png diff --git a/app/assets/images/emoji/biohazard.png b/app/assets/images/emoji/biohazard.png Binary files differnew file mode 100644 index 00000000000..007b4fc2d85 --- /dev/null +++ b/app/assets/images/emoji/biohazard.png diff --git a/app/assets/images/emoji/bird.png b/app/assets/images/emoji/bird.png Binary files differnew file mode 100644 index 00000000000..e201c22be33 --- /dev/null +++ b/app/assets/images/emoji/bird.png diff --git a/app/assets/images/emoji/birthday.png b/app/assets/images/emoji/birthday.png Binary files differnew file mode 100644 index 00000000000..317e9a41949 --- /dev/null +++ b/app/assets/images/emoji/birthday.png diff --git a/app/assets/images/emoji/black_circle.png b/app/assets/images/emoji/black_circle.png Binary files differnew file mode 100644 index 00000000000..b62b87170e8 --- /dev/null +++ b/app/assets/images/emoji/black_circle.png diff --git a/app/assets/images/emoji/black_heart.png b/app/assets/images/emoji/black_heart.png Binary files differnew file mode 100644 index 00000000000..b4068c3e6e8 --- /dev/null +++ b/app/assets/images/emoji/black_heart.png diff --git a/app/assets/images/emoji/black_joker.png b/app/assets/images/emoji/black_joker.png Binary files differnew file mode 100644 index 00000000000..3d0924b68aa --- /dev/null +++ b/app/assets/images/emoji/black_joker.png diff --git a/app/assets/images/emoji/black_large_square.png b/app/assets/images/emoji/black_large_square.png Binary files differnew file mode 100644 index 00000000000..162f2bb4290 --- /dev/null +++ b/app/assets/images/emoji/black_large_square.png diff --git a/app/assets/images/emoji/black_medium_small_square.png b/app/assets/images/emoji/black_medium_small_square.png Binary files differnew file mode 100644 index 00000000000..39765bba610 --- /dev/null +++ b/app/assets/images/emoji/black_medium_small_square.png diff --git a/app/assets/images/emoji/black_medium_square.png b/app/assets/images/emoji/black_medium_square.png Binary files differnew file mode 100644 index 00000000000..05a30a6aa2d --- /dev/null +++ b/app/assets/images/emoji/black_medium_square.png diff --git a/app/assets/images/emoji/black_nib.png b/app/assets/images/emoji/black_nib.png Binary files differnew file mode 100644 index 00000000000..872d0ae1598 --- /dev/null +++ b/app/assets/images/emoji/black_nib.png diff --git a/app/assets/images/emoji/black_small_square.png b/app/assets/images/emoji/black_small_square.png Binary files differnew file mode 100644 index 00000000000..48595d3e1a9 --- /dev/null +++ b/app/assets/images/emoji/black_small_square.png diff --git a/app/assets/images/emoji/black_square_button.png b/app/assets/images/emoji/black_square_button.png Binary files differnew file mode 100644 index 00000000000..a78fc2f6b63 --- /dev/null +++ b/app/assets/images/emoji/black_square_button.png diff --git a/app/assets/images/emoji/blossom.png b/app/assets/images/emoji/blossom.png Binary files differnew file mode 100644 index 00000000000..4083026c157 --- /dev/null +++ b/app/assets/images/emoji/blossom.png diff --git a/app/assets/images/emoji/blowfish.png b/app/assets/images/emoji/blowfish.png Binary files differnew file mode 100644 index 00000000000..a10f4f84e35 --- /dev/null +++ b/app/assets/images/emoji/blowfish.png diff --git a/app/assets/images/emoji/blue_book.png b/app/assets/images/emoji/blue_book.png Binary files differnew file mode 100644 index 00000000000..e1e455401cc --- /dev/null +++ b/app/assets/images/emoji/blue_book.png diff --git a/app/assets/images/emoji/blue_car.png b/app/assets/images/emoji/blue_car.png Binary files differnew file mode 100644 index 00000000000..e8ba817d393 --- /dev/null +++ b/app/assets/images/emoji/blue_car.png diff --git a/app/assets/images/emoji/blue_heart.png b/app/assets/images/emoji/blue_heart.png Binary files differnew file mode 100644 index 00000000000..bdf1287e55e --- /dev/null +++ b/app/assets/images/emoji/blue_heart.png diff --git a/app/assets/images/emoji/blush.png b/app/assets/images/emoji/blush.png Binary files differnew file mode 100644 index 00000000000..aac1a424ad4 --- /dev/null +++ b/app/assets/images/emoji/blush.png diff --git a/app/assets/images/emoji/boar.png b/app/assets/images/emoji/boar.png Binary files differnew file mode 100644 index 00000000000..fead972633c --- /dev/null +++ b/app/assets/images/emoji/boar.png diff --git a/app/assets/images/emoji/bomb.png b/app/assets/images/emoji/bomb.png Binary files differnew file mode 100644 index 00000000000..c7f8f81c939 --- /dev/null +++ b/app/assets/images/emoji/bomb.png diff --git a/app/assets/images/emoji/book.png b/app/assets/images/emoji/book.png Binary files differnew file mode 100644 index 00000000000..0f4447ed396 --- /dev/null +++ b/app/assets/images/emoji/book.png diff --git a/app/assets/images/emoji/bookmark.png b/app/assets/images/emoji/bookmark.png Binary files differnew file mode 100644 index 00000000000..bbb444611f0 --- /dev/null +++ b/app/assets/images/emoji/bookmark.png diff --git a/app/assets/images/emoji/bookmark_tabs.png b/app/assets/images/emoji/bookmark_tabs.png Binary files differnew file mode 100644 index 00000000000..f8d9e01b428 --- /dev/null +++ b/app/assets/images/emoji/bookmark_tabs.png diff --git a/app/assets/images/emoji/books.png b/app/assets/images/emoji/books.png Binary files differnew file mode 100644 index 00000000000..59a8bafeb0d --- /dev/null +++ b/app/assets/images/emoji/books.png diff --git a/app/assets/images/emoji/boom.png b/app/assets/images/emoji/boom.png Binary files differnew file mode 100644 index 00000000000..9b0f027b1a8 --- /dev/null +++ b/app/assets/images/emoji/boom.png diff --git a/app/assets/images/emoji/boot.png b/app/assets/images/emoji/boot.png Binary files differnew file mode 100644 index 00000000000..11f1065ed07 --- /dev/null +++ b/app/assets/images/emoji/boot.png diff --git a/app/assets/images/emoji/bouquet.png b/app/assets/images/emoji/bouquet.png Binary files differnew file mode 100644 index 00000000000..11455af6df4 --- /dev/null +++ b/app/assets/images/emoji/bouquet.png diff --git a/app/assets/images/emoji/bow.png b/app/assets/images/emoji/bow.png Binary files differnew file mode 100644 index 00000000000..d8f793088dc --- /dev/null +++ b/app/assets/images/emoji/bow.png diff --git a/app/assets/images/emoji/bow_and_arrow.png b/app/assets/images/emoji/bow_and_arrow.png Binary files differnew file mode 100644 index 00000000000..6a538bf475f --- /dev/null +++ b/app/assets/images/emoji/bow_and_arrow.png diff --git a/app/assets/images/emoji/bow_tone1.png b/app/assets/images/emoji/bow_tone1.png Binary files differnew file mode 100644 index 00000000000..87afb7b54cf --- /dev/null +++ b/app/assets/images/emoji/bow_tone1.png diff --git a/app/assets/images/emoji/bow_tone2.png b/app/assets/images/emoji/bow_tone2.png Binary files differnew file mode 100644 index 00000000000..3ccf7dc0850 --- /dev/null +++ b/app/assets/images/emoji/bow_tone2.png diff --git a/app/assets/images/emoji/bow_tone3.png b/app/assets/images/emoji/bow_tone3.png Binary files differnew file mode 100644 index 00000000000..8b9eb64f926 --- /dev/null +++ b/app/assets/images/emoji/bow_tone3.png diff --git a/app/assets/images/emoji/bow_tone4.png b/app/assets/images/emoji/bow_tone4.png Binary files differnew file mode 100644 index 00000000000..683795ff40d --- /dev/null +++ b/app/assets/images/emoji/bow_tone4.png diff --git a/app/assets/images/emoji/bow_tone5.png b/app/assets/images/emoji/bow_tone5.png Binary files differnew file mode 100644 index 00000000000..7969d971752 --- /dev/null +++ b/app/assets/images/emoji/bow_tone5.png diff --git a/app/assets/images/emoji/bowling.png b/app/assets/images/emoji/bowling.png Binary files differnew file mode 100644 index 00000000000..63add89e53b --- /dev/null +++ b/app/assets/images/emoji/bowling.png diff --git a/app/assets/images/emoji/boxing_glove.png b/app/assets/images/emoji/boxing_glove.png Binary files differnew file mode 100644 index 00000000000..9838f24e51a --- /dev/null +++ b/app/assets/images/emoji/boxing_glove.png diff --git a/app/assets/images/emoji/boy.png b/app/assets/images/emoji/boy.png Binary files differnew file mode 100644 index 00000000000..8ecfb0a4e92 --- /dev/null +++ b/app/assets/images/emoji/boy.png diff --git a/app/assets/images/emoji/boy_tone1.png b/app/assets/images/emoji/boy_tone1.png Binary files differnew file mode 100644 index 00000000000..2fc436ea512 --- /dev/null +++ b/app/assets/images/emoji/boy_tone1.png diff --git a/app/assets/images/emoji/boy_tone2.png b/app/assets/images/emoji/boy_tone2.png Binary files differnew file mode 100644 index 00000000000..09a5f18d360 --- /dev/null +++ b/app/assets/images/emoji/boy_tone2.png diff --git a/app/assets/images/emoji/boy_tone3.png b/app/assets/images/emoji/boy_tone3.png Binary files differnew file mode 100644 index 00000000000..3cfe675dd3a --- /dev/null +++ b/app/assets/images/emoji/boy_tone3.png diff --git a/app/assets/images/emoji/boy_tone4.png b/app/assets/images/emoji/boy_tone4.png Binary files differnew file mode 100644 index 00000000000..780be0ace36 --- /dev/null +++ b/app/assets/images/emoji/boy_tone4.png diff --git a/app/assets/images/emoji/boy_tone5.png b/app/assets/images/emoji/boy_tone5.png Binary files differnew file mode 100644 index 00000000000..f32fe22e35c --- /dev/null +++ b/app/assets/images/emoji/boy_tone5.png diff --git a/app/assets/images/emoji/bread.png b/app/assets/images/emoji/bread.png Binary files differnew file mode 100644 index 00000000000..6676510aaa5 --- /dev/null +++ b/app/assets/images/emoji/bread.png diff --git a/app/assets/images/emoji/bride_with_veil.png b/app/assets/images/emoji/bride_with_veil.png Binary files differnew file mode 100644 index 00000000000..eaf4bd97890 --- /dev/null +++ b/app/assets/images/emoji/bride_with_veil.png diff --git a/app/assets/images/emoji/bride_with_veil_tone1.png b/app/assets/images/emoji/bride_with_veil_tone1.png Binary files differnew file mode 100644 index 00000000000..c4fb141ae8f --- /dev/null +++ b/app/assets/images/emoji/bride_with_veil_tone1.png diff --git a/app/assets/images/emoji/bride_with_veil_tone2.png b/app/assets/images/emoji/bride_with_veil_tone2.png Binary files differnew file mode 100644 index 00000000000..c248769fc06 --- /dev/null +++ b/app/assets/images/emoji/bride_with_veil_tone2.png diff --git a/app/assets/images/emoji/bride_with_veil_tone3.png b/app/assets/images/emoji/bride_with_veil_tone3.png Binary files differnew file mode 100644 index 00000000000..962c0a6eedb --- /dev/null +++ b/app/assets/images/emoji/bride_with_veil_tone3.png diff --git a/app/assets/images/emoji/bride_with_veil_tone4.png b/app/assets/images/emoji/bride_with_veil_tone4.png Binary files differnew file mode 100644 index 00000000000..740ca208cd4 --- /dev/null +++ b/app/assets/images/emoji/bride_with_veil_tone4.png diff --git a/app/assets/images/emoji/bride_with_veil_tone5.png b/app/assets/images/emoji/bride_with_veil_tone5.png Binary files differnew file mode 100644 index 00000000000..5cc5598587d --- /dev/null +++ b/app/assets/images/emoji/bride_with_veil_tone5.png diff --git a/app/assets/images/emoji/bridge_at_night.png b/app/assets/images/emoji/bridge_at_night.png Binary files differnew file mode 100644 index 00000000000..1d444e0be65 --- /dev/null +++ b/app/assets/images/emoji/bridge_at_night.png diff --git a/app/assets/images/emoji/briefcase.png b/app/assets/images/emoji/briefcase.png Binary files differnew file mode 100644 index 00000000000..b9912ba2148 --- /dev/null +++ b/app/assets/images/emoji/briefcase.png diff --git a/app/assets/images/emoji/broken_heart.png b/app/assets/images/emoji/broken_heart.png Binary files differnew file mode 100644 index 00000000000..718e26ee122 --- /dev/null +++ b/app/assets/images/emoji/broken_heart.png diff --git a/app/assets/images/emoji/bug.png b/app/assets/images/emoji/bug.png Binary files differnew file mode 100644 index 00000000000..e64e72f259a --- /dev/null +++ b/app/assets/images/emoji/bug.png diff --git a/app/assets/images/emoji/bulb.png b/app/assets/images/emoji/bulb.png Binary files differnew file mode 100644 index 00000000000..38e32e02d9f --- /dev/null +++ b/app/assets/images/emoji/bulb.png diff --git a/app/assets/images/emoji/bullettrain_front.png b/app/assets/images/emoji/bullettrain_front.png Binary files differnew file mode 100644 index 00000000000..4f698e056fa --- /dev/null +++ b/app/assets/images/emoji/bullettrain_front.png diff --git a/app/assets/images/emoji/bullettrain_side.png b/app/assets/images/emoji/bullettrain_side.png Binary files differnew file mode 100644 index 00000000000..ed61c67bf07 --- /dev/null +++ b/app/assets/images/emoji/bullettrain_side.png diff --git a/app/assets/images/emoji/burrito.png b/app/assets/images/emoji/burrito.png Binary files differnew file mode 100644 index 00000000000..02bd5601df7 --- /dev/null +++ b/app/assets/images/emoji/burrito.png diff --git a/app/assets/images/emoji/bus.png b/app/assets/images/emoji/bus.png Binary files differnew file mode 100644 index 00000000000..641ddc56ca7 --- /dev/null +++ b/app/assets/images/emoji/bus.png diff --git a/app/assets/images/emoji/busstop.png b/app/assets/images/emoji/busstop.png Binary files differnew file mode 100644 index 00000000000..b2b62208bfd --- /dev/null +++ b/app/assets/images/emoji/busstop.png diff --git a/app/assets/images/emoji/bust_in_silhouette.png b/app/assets/images/emoji/bust_in_silhouette.png Binary files differnew file mode 100644 index 00000000000..123b2cbe1fb --- /dev/null +++ b/app/assets/images/emoji/bust_in_silhouette.png diff --git a/app/assets/images/emoji/busts_in_silhouette.png b/app/assets/images/emoji/busts_in_silhouette.png Binary files differnew file mode 100644 index 00000000000..d7656860a1c --- /dev/null +++ b/app/assets/images/emoji/busts_in_silhouette.png diff --git a/app/assets/images/emoji/butterfly.png b/app/assets/images/emoji/butterfly.png Binary files differnew file mode 100644 index 00000000000..5631fe99226 --- /dev/null +++ b/app/assets/images/emoji/butterfly.png diff --git a/app/assets/images/emoji/cactus.png b/app/assets/images/emoji/cactus.png Binary files differnew file mode 100644 index 00000000000..9b48ccf3d0c --- /dev/null +++ b/app/assets/images/emoji/cactus.png diff --git a/app/assets/images/emoji/cake.png b/app/assets/images/emoji/cake.png Binary files differnew file mode 100644 index 00000000000..4368177be9a --- /dev/null +++ b/app/assets/images/emoji/cake.png diff --git a/app/assets/images/emoji/calendar.png b/app/assets/images/emoji/calendar.png Binary files differnew file mode 100644 index 00000000000..47353b74447 --- /dev/null +++ b/app/assets/images/emoji/calendar.png diff --git a/app/assets/images/emoji/calendar_spiral.png b/app/assets/images/emoji/calendar_spiral.png Binary files differnew file mode 100644 index 00000000000..dec8d49bfa8 --- /dev/null +++ b/app/assets/images/emoji/calendar_spiral.png diff --git a/app/assets/images/emoji/call_me.png b/app/assets/images/emoji/call_me.png Binary files differnew file mode 100644 index 00000000000..a10c59ba711 --- /dev/null +++ b/app/assets/images/emoji/call_me.png diff --git a/app/assets/images/emoji/call_me_tone1.png b/app/assets/images/emoji/call_me_tone1.png Binary files differnew file mode 100644 index 00000000000..2c93201181a --- /dev/null +++ b/app/assets/images/emoji/call_me_tone1.png diff --git a/app/assets/images/emoji/call_me_tone2.png b/app/assets/images/emoji/call_me_tone2.png Binary files differnew file mode 100644 index 00000000000..c39f45a41ed --- /dev/null +++ b/app/assets/images/emoji/call_me_tone2.png diff --git a/app/assets/images/emoji/call_me_tone3.png b/app/assets/images/emoji/call_me_tone3.png Binary files differnew file mode 100644 index 00000000000..83a57f63c29 --- /dev/null +++ b/app/assets/images/emoji/call_me_tone3.png diff --git a/app/assets/images/emoji/call_me_tone4.png b/app/assets/images/emoji/call_me_tone4.png Binary files differnew file mode 100644 index 00000000000..65b3468fe44 --- /dev/null +++ b/app/assets/images/emoji/call_me_tone4.png diff --git a/app/assets/images/emoji/call_me_tone5.png b/app/assets/images/emoji/call_me_tone5.png Binary files differnew file mode 100644 index 00000000000..94ef68ff3b3 --- /dev/null +++ b/app/assets/images/emoji/call_me_tone5.png diff --git a/app/assets/images/emoji/calling.png b/app/assets/images/emoji/calling.png Binary files differnew file mode 100644 index 00000000000..e2f308f8e46 --- /dev/null +++ b/app/assets/images/emoji/calling.png diff --git a/app/assets/images/emoji/camel.png b/app/assets/images/emoji/camel.png Binary files differnew file mode 100644 index 00000000000..b421d07a805 --- /dev/null +++ b/app/assets/images/emoji/camel.png diff --git a/app/assets/images/emoji/camera.png b/app/assets/images/emoji/camera.png Binary files differnew file mode 100644 index 00000000000..0a3429f72ef --- /dev/null +++ b/app/assets/images/emoji/camera.png diff --git a/app/assets/images/emoji/camera_with_flash.png b/app/assets/images/emoji/camera_with_flash.png Binary files differnew file mode 100644 index 00000000000..27471da2029 --- /dev/null +++ b/app/assets/images/emoji/camera_with_flash.png diff --git a/app/assets/images/emoji/camping.png b/app/assets/images/emoji/camping.png Binary files differnew file mode 100644 index 00000000000..d589cc1f44b --- /dev/null +++ b/app/assets/images/emoji/camping.png diff --git a/app/assets/images/emoji/cancer.png b/app/assets/images/emoji/cancer.png Binary files differnew file mode 100644 index 00000000000..a64af07cb5f --- /dev/null +++ b/app/assets/images/emoji/cancer.png diff --git a/app/assets/images/emoji/candle.png b/app/assets/images/emoji/candle.png Binary files differnew file mode 100644 index 00000000000..0b56444e355 --- /dev/null +++ b/app/assets/images/emoji/candle.png diff --git a/app/assets/images/emoji/candy.png b/app/assets/images/emoji/candy.png Binary files differnew file mode 100644 index 00000000000..8c67ace3a35 --- /dev/null +++ b/app/assets/images/emoji/candy.png diff --git a/app/assets/images/emoji/canoe.png b/app/assets/images/emoji/canoe.png Binary files differnew file mode 100644 index 00000000000..e26cdb9da69 --- /dev/null +++ b/app/assets/images/emoji/canoe.png diff --git a/app/assets/images/emoji/capital_abcd.png b/app/assets/images/emoji/capital_abcd.png Binary files differnew file mode 100644 index 00000000000..fe9482d2d8a --- /dev/null +++ b/app/assets/images/emoji/capital_abcd.png diff --git a/app/assets/images/emoji/capricorn.png b/app/assets/images/emoji/capricorn.png Binary files differnew file mode 100644 index 00000000000..6293d31d4b1 --- /dev/null +++ b/app/assets/images/emoji/capricorn.png diff --git a/app/assets/images/emoji/card_box.png b/app/assets/images/emoji/card_box.png Binary files differnew file mode 100644 index 00000000000..f2e764ce59d --- /dev/null +++ b/app/assets/images/emoji/card_box.png diff --git a/app/assets/images/emoji/card_index.png b/app/assets/images/emoji/card_index.png Binary files differnew file mode 100644 index 00000000000..151e11cb3b4 --- /dev/null +++ b/app/assets/images/emoji/card_index.png diff --git a/app/assets/images/emoji/carousel_horse.png b/app/assets/images/emoji/carousel_horse.png Binary files differnew file mode 100644 index 00000000000..a17074edf05 --- /dev/null +++ b/app/assets/images/emoji/carousel_horse.png diff --git a/app/assets/images/emoji/carrot.png b/app/assets/images/emoji/carrot.png Binary files differnew file mode 100644 index 00000000000..c68829b58e7 --- /dev/null +++ b/app/assets/images/emoji/carrot.png diff --git a/app/assets/images/emoji/cartwheel.png b/app/assets/images/emoji/cartwheel.png Binary files differnew file mode 100644 index 00000000000..cbcaa578253 --- /dev/null +++ b/app/assets/images/emoji/cartwheel.png diff --git a/app/assets/images/emoji/cartwheel_tone1.png b/app/assets/images/emoji/cartwheel_tone1.png Binary files differnew file mode 100644 index 00000000000..db6d65895fb --- /dev/null +++ b/app/assets/images/emoji/cartwheel_tone1.png diff --git a/app/assets/images/emoji/cartwheel_tone2.png b/app/assets/images/emoji/cartwheel_tone2.png Binary files differnew file mode 100644 index 00000000000..e00ffbc27a8 --- /dev/null +++ b/app/assets/images/emoji/cartwheel_tone2.png diff --git a/app/assets/images/emoji/cartwheel_tone3.png b/app/assets/images/emoji/cartwheel_tone3.png Binary files differnew file mode 100644 index 00000000000..49321be391f --- /dev/null +++ b/app/assets/images/emoji/cartwheel_tone3.png diff --git a/app/assets/images/emoji/cartwheel_tone4.png b/app/assets/images/emoji/cartwheel_tone4.png Binary files differnew file mode 100644 index 00000000000..d4562b5e3dd --- /dev/null +++ b/app/assets/images/emoji/cartwheel_tone4.png diff --git a/app/assets/images/emoji/cartwheel_tone5.png b/app/assets/images/emoji/cartwheel_tone5.png Binary files differnew file mode 100644 index 00000000000..6e09a870767 --- /dev/null +++ b/app/assets/images/emoji/cartwheel_tone5.png diff --git a/app/assets/images/emoji/cat.png b/app/assets/images/emoji/cat.png Binary files differnew file mode 100644 index 00000000000..efd82c2abf3 --- /dev/null +++ b/app/assets/images/emoji/cat.png diff --git a/app/assets/images/emoji/cat2.png b/app/assets/images/emoji/cat2.png Binary files differnew file mode 100644 index 00000000000..46abe8cbc14 --- /dev/null +++ b/app/assets/images/emoji/cat2.png diff --git a/app/assets/images/emoji/cd.png b/app/assets/images/emoji/cd.png Binary files differnew file mode 100644 index 00000000000..e6b01449cd9 --- /dev/null +++ b/app/assets/images/emoji/cd.png diff --git a/app/assets/images/emoji/chains.png b/app/assets/images/emoji/chains.png Binary files differnew file mode 100644 index 00000000000..57f46139a06 --- /dev/null +++ b/app/assets/images/emoji/chains.png diff --git a/app/assets/images/emoji/champagne.png b/app/assets/images/emoji/champagne.png Binary files differnew file mode 100644 index 00000000000..285a79a93d0 --- /dev/null +++ b/app/assets/images/emoji/champagne.png diff --git a/app/assets/images/emoji/champagne_glass.png b/app/assets/images/emoji/champagne_glass.png Binary files differnew file mode 100644 index 00000000000..31937ae9392 --- /dev/null +++ b/app/assets/images/emoji/champagne_glass.png diff --git a/app/assets/images/emoji/chart.png b/app/assets/images/emoji/chart.png Binary files differnew file mode 100644 index 00000000000..9773f03be22 --- /dev/null +++ b/app/assets/images/emoji/chart.png diff --git a/app/assets/images/emoji/chart_with_downwards_trend.png b/app/assets/images/emoji/chart_with_downwards_trend.png Binary files differnew file mode 100644 index 00000000000..5222ec72d85 --- /dev/null +++ b/app/assets/images/emoji/chart_with_downwards_trend.png diff --git a/app/assets/images/emoji/chart_with_upwards_trend.png b/app/assets/images/emoji/chart_with_upwards_trend.png Binary files differnew file mode 100644 index 00000000000..f13cfcf9956 --- /dev/null +++ b/app/assets/images/emoji/chart_with_upwards_trend.png diff --git a/app/assets/images/emoji/checkered_flag.png b/app/assets/images/emoji/checkered_flag.png Binary files differnew file mode 100644 index 00000000000..5a71eecb89b --- /dev/null +++ b/app/assets/images/emoji/checkered_flag.png diff --git a/app/assets/images/emoji/cheese.png b/app/assets/images/emoji/cheese.png Binary files differnew file mode 100644 index 00000000000..00e99762286 --- /dev/null +++ b/app/assets/images/emoji/cheese.png diff --git a/app/assets/images/emoji/cherries.png b/app/assets/images/emoji/cherries.png Binary files differnew file mode 100644 index 00000000000..9b10cbaac5e --- /dev/null +++ b/app/assets/images/emoji/cherries.png diff --git a/app/assets/images/emoji/cherry_blossom.png b/app/assets/images/emoji/cherry_blossom.png Binary files differnew file mode 100644 index 00000000000..282f3e7bc81 --- /dev/null +++ b/app/assets/images/emoji/cherry_blossom.png diff --git a/app/assets/images/emoji/chestnut.png b/app/assets/images/emoji/chestnut.png Binary files differnew file mode 100644 index 00000000000..e9fb40468ed --- /dev/null +++ b/app/assets/images/emoji/chestnut.png diff --git a/app/assets/images/emoji/chicken.png b/app/assets/images/emoji/chicken.png Binary files differnew file mode 100644 index 00000000000..9a6992e55ba --- /dev/null +++ b/app/assets/images/emoji/chicken.png diff --git a/app/assets/images/emoji/children_crossing.png b/app/assets/images/emoji/children_crossing.png Binary files differnew file mode 100644 index 00000000000..fa4c091c7c3 --- /dev/null +++ b/app/assets/images/emoji/children_crossing.png diff --git a/app/assets/images/emoji/chipmunk.png b/app/assets/images/emoji/chipmunk.png Binary files differnew file mode 100644 index 00000000000..2aac560cb22 --- /dev/null +++ b/app/assets/images/emoji/chipmunk.png diff --git a/app/assets/images/emoji/chocolate_bar.png b/app/assets/images/emoji/chocolate_bar.png Binary files differnew file mode 100644 index 00000000000..318bbd40ef9 --- /dev/null +++ b/app/assets/images/emoji/chocolate_bar.png diff --git a/app/assets/images/emoji/christmas_tree.png b/app/assets/images/emoji/christmas_tree.png Binary files differnew file mode 100644 index 00000000000..4197d37a52b --- /dev/null +++ b/app/assets/images/emoji/christmas_tree.png diff --git a/app/assets/images/emoji/church.png b/app/assets/images/emoji/church.png Binary files differnew file mode 100644 index 00000000000..8242fd272b3 --- /dev/null +++ b/app/assets/images/emoji/church.png diff --git a/app/assets/images/emoji/cinema.png b/app/assets/images/emoji/cinema.png Binary files differnew file mode 100644 index 00000000000..65f27b386f2 --- /dev/null +++ b/app/assets/images/emoji/cinema.png diff --git a/app/assets/images/emoji/circus_tent.png b/app/assets/images/emoji/circus_tent.png Binary files differnew file mode 100644 index 00000000000..b0379775b12 --- /dev/null +++ b/app/assets/images/emoji/circus_tent.png diff --git a/app/assets/images/emoji/city_dusk.png b/app/assets/images/emoji/city_dusk.png Binary files differnew file mode 100644 index 00000000000..80cdff7cf5d --- /dev/null +++ b/app/assets/images/emoji/city_dusk.png diff --git a/app/assets/images/emoji/city_sunset.png b/app/assets/images/emoji/city_sunset.png Binary files differnew file mode 100644 index 00000000000..7cded0ba55b --- /dev/null +++ b/app/assets/images/emoji/city_sunset.png diff --git a/app/assets/images/emoji/cityscape.png b/app/assets/images/emoji/cityscape.png Binary files differnew file mode 100644 index 00000000000..d7b9844a0b4 --- /dev/null +++ b/app/assets/images/emoji/cityscape.png diff --git a/app/assets/images/emoji/cl.png b/app/assets/images/emoji/cl.png Binary files differnew file mode 100644 index 00000000000..8b01b4343e2 --- /dev/null +++ b/app/assets/images/emoji/cl.png diff --git a/app/assets/images/emoji/clap.png b/app/assets/images/emoji/clap.png Binary files differnew file mode 100644 index 00000000000..b0ffe928920 --- /dev/null +++ b/app/assets/images/emoji/clap.png diff --git a/app/assets/images/emoji/clap_tone1.png b/app/assets/images/emoji/clap_tone1.png Binary files differnew file mode 100644 index 00000000000..de4bc837b96 --- /dev/null +++ b/app/assets/images/emoji/clap_tone1.png diff --git a/app/assets/images/emoji/clap_tone2.png b/app/assets/images/emoji/clap_tone2.png Binary files differnew file mode 100644 index 00000000000..1323de775ba --- /dev/null +++ b/app/assets/images/emoji/clap_tone2.png diff --git a/app/assets/images/emoji/clap_tone3.png b/app/assets/images/emoji/clap_tone3.png Binary files differnew file mode 100644 index 00000000000..d448ca19dde --- /dev/null +++ b/app/assets/images/emoji/clap_tone3.png diff --git a/app/assets/images/emoji/clap_tone4.png b/app/assets/images/emoji/clap_tone4.png Binary files differnew file mode 100644 index 00000000000..c49f44ee91d --- /dev/null +++ b/app/assets/images/emoji/clap_tone4.png diff --git a/app/assets/images/emoji/clap_tone5.png b/app/assets/images/emoji/clap_tone5.png Binary files differnew file mode 100644 index 00000000000..29ee9bdf37c --- /dev/null +++ b/app/assets/images/emoji/clap_tone5.png diff --git a/app/assets/images/emoji/clapper.png b/app/assets/images/emoji/clapper.png Binary files differnew file mode 100644 index 00000000000..81390883111 --- /dev/null +++ b/app/assets/images/emoji/clapper.png diff --git a/app/assets/images/emoji/classical_building.png b/app/assets/images/emoji/classical_building.png Binary files differnew file mode 100644 index 00000000000..de7b559daaf --- /dev/null +++ b/app/assets/images/emoji/classical_building.png diff --git a/app/assets/images/emoji/clipboard.png b/app/assets/images/emoji/clipboard.png Binary files differnew file mode 100644 index 00000000000..7edcfc52509 --- /dev/null +++ b/app/assets/images/emoji/clipboard.png diff --git a/app/assets/images/emoji/clock.png b/app/assets/images/emoji/clock.png Binary files differnew file mode 100644 index 00000000000..ffdb451e3a8 --- /dev/null +++ b/app/assets/images/emoji/clock.png diff --git a/app/assets/images/emoji/clock1.png b/app/assets/images/emoji/clock1.png Binary files differnew file mode 100644 index 00000000000..d6e34941f23 --- /dev/null +++ b/app/assets/images/emoji/clock1.png diff --git a/app/assets/images/emoji/clock10.png b/app/assets/images/emoji/clock10.png Binary files differnew file mode 100644 index 00000000000..e62b245cdbe --- /dev/null +++ b/app/assets/images/emoji/clock10.png diff --git a/app/assets/images/emoji/clock1030.png b/app/assets/images/emoji/clock1030.png Binary files differnew file mode 100644 index 00000000000..0802b3c65b9 --- /dev/null +++ b/app/assets/images/emoji/clock1030.png diff --git a/app/assets/images/emoji/clock11.png b/app/assets/images/emoji/clock11.png Binary files differnew file mode 100644 index 00000000000..0983345273b --- /dev/null +++ b/app/assets/images/emoji/clock11.png diff --git a/app/assets/images/emoji/clock1130.png b/app/assets/images/emoji/clock1130.png Binary files differnew file mode 100644 index 00000000000..d970d03b809 --- /dev/null +++ b/app/assets/images/emoji/clock1130.png diff --git a/app/assets/images/emoji/clock12.png b/app/assets/images/emoji/clock12.png Binary files differnew file mode 100644 index 00000000000..e61caa4b3e2 --- /dev/null +++ b/app/assets/images/emoji/clock12.png diff --git a/app/assets/images/emoji/clock1230.png b/app/assets/images/emoji/clock1230.png Binary files differnew file mode 100644 index 00000000000..f2b1d261721 --- /dev/null +++ b/app/assets/images/emoji/clock1230.png diff --git a/app/assets/images/emoji/clock130.png b/app/assets/images/emoji/clock130.png Binary files differnew file mode 100644 index 00000000000..86b7689b84e --- /dev/null +++ b/app/assets/images/emoji/clock130.png diff --git a/app/assets/images/emoji/clock2.png b/app/assets/images/emoji/clock2.png Binary files differnew file mode 100644 index 00000000000..a54253d7d57 --- /dev/null +++ b/app/assets/images/emoji/clock2.png diff --git a/app/assets/images/emoji/clock230.png b/app/assets/images/emoji/clock230.png Binary files differnew file mode 100644 index 00000000000..7a787e018e6 --- /dev/null +++ b/app/assets/images/emoji/clock230.png diff --git a/app/assets/images/emoji/clock3.png b/app/assets/images/emoji/clock3.png Binary files differnew file mode 100644 index 00000000000..27ec4b1f514 --- /dev/null +++ b/app/assets/images/emoji/clock3.png diff --git a/app/assets/images/emoji/clock330.png b/app/assets/images/emoji/clock330.png Binary files differnew file mode 100644 index 00000000000..c6860395cec --- /dev/null +++ b/app/assets/images/emoji/clock330.png diff --git a/app/assets/images/emoji/clock4.png b/app/assets/images/emoji/clock4.png Binary files differnew file mode 100644 index 00000000000..60a1ef4cc13 --- /dev/null +++ b/app/assets/images/emoji/clock4.png diff --git a/app/assets/images/emoji/clock430.png b/app/assets/images/emoji/clock430.png Binary files differnew file mode 100644 index 00000000000..3c05b362122 --- /dev/null +++ b/app/assets/images/emoji/clock430.png diff --git a/app/assets/images/emoji/clock5.png b/app/assets/images/emoji/clock5.png Binary files differnew file mode 100644 index 00000000000..c9382d1e094 --- /dev/null +++ b/app/assets/images/emoji/clock5.png diff --git a/app/assets/images/emoji/clock530.png b/app/assets/images/emoji/clock530.png Binary files differnew file mode 100644 index 00000000000..c21fa926db2 --- /dev/null +++ b/app/assets/images/emoji/clock530.png diff --git a/app/assets/images/emoji/clock6.png b/app/assets/images/emoji/clock6.png Binary files differnew file mode 100644 index 00000000000..8fd5d3f5bd7 --- /dev/null +++ b/app/assets/images/emoji/clock6.png diff --git a/app/assets/images/emoji/clock630.png b/app/assets/images/emoji/clock630.png Binary files differnew file mode 100644 index 00000000000..2aec87fefcf --- /dev/null +++ b/app/assets/images/emoji/clock630.png diff --git a/app/assets/images/emoji/clock7.png b/app/assets/images/emoji/clock7.png Binary files differnew file mode 100644 index 00000000000..8c7084036f2 --- /dev/null +++ b/app/assets/images/emoji/clock7.png diff --git a/app/assets/images/emoji/clock730.png b/app/assets/images/emoji/clock730.png Binary files differnew file mode 100644 index 00000000000..f7a1135e03f --- /dev/null +++ b/app/assets/images/emoji/clock730.png diff --git a/app/assets/images/emoji/clock8.png b/app/assets/images/emoji/clock8.png Binary files differnew file mode 100644 index 00000000000..fcddf722e95 --- /dev/null +++ b/app/assets/images/emoji/clock8.png diff --git a/app/assets/images/emoji/clock830.png b/app/assets/images/emoji/clock830.png Binary files differnew file mode 100644 index 00000000000..799b4aebc08 --- /dev/null +++ b/app/assets/images/emoji/clock830.png diff --git a/app/assets/images/emoji/clock9.png b/app/assets/images/emoji/clock9.png Binary files differnew file mode 100644 index 00000000000..dfbe0117981 --- /dev/null +++ b/app/assets/images/emoji/clock9.png diff --git a/app/assets/images/emoji/clock930.png b/app/assets/images/emoji/clock930.png Binary files differnew file mode 100644 index 00000000000..4a2092ee6f0 --- /dev/null +++ b/app/assets/images/emoji/clock930.png diff --git a/app/assets/images/emoji/closed_book.png b/app/assets/images/emoji/closed_book.png Binary files differnew file mode 100644 index 00000000000..6395cf2151e --- /dev/null +++ b/app/assets/images/emoji/closed_book.png diff --git a/app/assets/images/emoji/closed_lock_with_key.png b/app/assets/images/emoji/closed_lock_with_key.png Binary files differnew file mode 100644 index 00000000000..1c1cd5d0741 --- /dev/null +++ b/app/assets/images/emoji/closed_lock_with_key.png diff --git a/app/assets/images/emoji/closed_umbrella.png b/app/assets/images/emoji/closed_umbrella.png Binary files differnew file mode 100644 index 00000000000..ecefba9e446 --- /dev/null +++ b/app/assets/images/emoji/closed_umbrella.png diff --git a/app/assets/images/emoji/cloud.png b/app/assets/images/emoji/cloud.png Binary files differnew file mode 100644 index 00000000000..5b4f57f77ba --- /dev/null +++ b/app/assets/images/emoji/cloud.png diff --git a/app/assets/images/emoji/cloud_lightning.png b/app/assets/images/emoji/cloud_lightning.png Binary files differnew file mode 100644 index 00000000000..0831e88aa31 --- /dev/null +++ b/app/assets/images/emoji/cloud_lightning.png diff --git a/app/assets/images/emoji/cloud_rain.png b/app/assets/images/emoji/cloud_rain.png Binary files differnew file mode 100644 index 00000000000..385685e0512 --- /dev/null +++ b/app/assets/images/emoji/cloud_rain.png diff --git a/app/assets/images/emoji/cloud_snow.png b/app/assets/images/emoji/cloud_snow.png Binary files differnew file mode 100644 index 00000000000..9720384eb99 --- /dev/null +++ b/app/assets/images/emoji/cloud_snow.png diff --git a/app/assets/images/emoji/cloud_tornado.png b/app/assets/images/emoji/cloud_tornado.png Binary files differnew file mode 100644 index 00000000000..4821c89da1e --- /dev/null +++ b/app/assets/images/emoji/cloud_tornado.png diff --git a/app/assets/images/emoji/clown.png b/app/assets/images/emoji/clown.png Binary files differnew file mode 100644 index 00000000000..02b7ff70049 --- /dev/null +++ b/app/assets/images/emoji/clown.png diff --git a/app/assets/images/emoji/clubs.png b/app/assets/images/emoji/clubs.png Binary files differnew file mode 100644 index 00000000000..4f2abf791ca --- /dev/null +++ b/app/assets/images/emoji/clubs.png diff --git a/app/assets/images/emoji/cocktail.png b/app/assets/images/emoji/cocktail.png Binary files differnew file mode 100644 index 00000000000..2e50c57e98d --- /dev/null +++ b/app/assets/images/emoji/cocktail.png diff --git a/app/assets/images/emoji/coffee.png b/app/assets/images/emoji/coffee.png Binary files differnew file mode 100644 index 00000000000..553061471b1 --- /dev/null +++ b/app/assets/images/emoji/coffee.png diff --git a/app/assets/images/emoji/coffin.png b/app/assets/images/emoji/coffin.png Binary files differnew file mode 100644 index 00000000000..fb2932aa5f6 --- /dev/null +++ b/app/assets/images/emoji/coffin.png diff --git a/app/assets/images/emoji/cold_sweat.png b/app/assets/images/emoji/cold_sweat.png Binary files differnew file mode 100644 index 00000000000..85b2231bbf6 --- /dev/null +++ b/app/assets/images/emoji/cold_sweat.png diff --git a/app/assets/images/emoji/comet.png b/app/assets/images/emoji/comet.png Binary files differnew file mode 100644 index 00000000000..a99751f79be --- /dev/null +++ b/app/assets/images/emoji/comet.png diff --git a/app/assets/images/emoji/compression.png b/app/assets/images/emoji/compression.png Binary files differnew file mode 100644 index 00000000000..d7eda7f362a --- /dev/null +++ b/app/assets/images/emoji/compression.png diff --git a/app/assets/images/emoji/computer.png b/app/assets/images/emoji/computer.png Binary files differnew file mode 100644 index 00000000000..c1fee27e3a9 --- /dev/null +++ b/app/assets/images/emoji/computer.png diff --git a/app/assets/images/emoji/confetti_ball.png b/app/assets/images/emoji/confetti_ball.png Binary files differnew file mode 100644 index 00000000000..ba4fd9b12be --- /dev/null +++ b/app/assets/images/emoji/confetti_ball.png diff --git a/app/assets/images/emoji/confounded.png b/app/assets/images/emoji/confounded.png Binary files differnew file mode 100644 index 00000000000..aa4b29e9375 --- /dev/null +++ b/app/assets/images/emoji/confounded.png diff --git a/app/assets/images/emoji/confused.png b/app/assets/images/emoji/confused.png Binary files differnew file mode 100644 index 00000000000..502b6bf0e0b --- /dev/null +++ b/app/assets/images/emoji/confused.png diff --git a/app/assets/images/emoji/congratulations.png b/app/assets/images/emoji/congratulations.png Binary files differnew file mode 100644 index 00000000000..ba8c89d95ee --- /dev/null +++ b/app/assets/images/emoji/congratulations.png diff --git a/app/assets/images/emoji/construction.png b/app/assets/images/emoji/construction.png Binary files differnew file mode 100644 index 00000000000..ef8db5f471c --- /dev/null +++ b/app/assets/images/emoji/construction.png diff --git a/app/assets/images/emoji/construction_site.png b/app/assets/images/emoji/construction_site.png Binary files differnew file mode 100644 index 00000000000..8206a20f63f --- /dev/null +++ b/app/assets/images/emoji/construction_site.png diff --git a/app/assets/images/emoji/construction_worker.png b/app/assets/images/emoji/construction_worker.png Binary files differnew file mode 100644 index 00000000000..a9970a89005 --- /dev/null +++ b/app/assets/images/emoji/construction_worker.png diff --git a/app/assets/images/emoji/construction_worker_tone1.png b/app/assets/images/emoji/construction_worker_tone1.png Binary files differnew file mode 100644 index 00000000000..2f24a2bab24 --- /dev/null +++ b/app/assets/images/emoji/construction_worker_tone1.png diff --git a/app/assets/images/emoji/construction_worker_tone2.png b/app/assets/images/emoji/construction_worker_tone2.png Binary files differnew file mode 100644 index 00000000000..93c8fec5a75 --- /dev/null +++ b/app/assets/images/emoji/construction_worker_tone2.png diff --git a/app/assets/images/emoji/construction_worker_tone3.png b/app/assets/images/emoji/construction_worker_tone3.png Binary files differnew file mode 100644 index 00000000000..abc1f2af2e0 --- /dev/null +++ b/app/assets/images/emoji/construction_worker_tone3.png diff --git a/app/assets/images/emoji/construction_worker_tone4.png b/app/assets/images/emoji/construction_worker_tone4.png Binary files differnew file mode 100644 index 00000000000..eed83289aeb --- /dev/null +++ b/app/assets/images/emoji/construction_worker_tone4.png diff --git a/app/assets/images/emoji/construction_worker_tone5.png b/app/assets/images/emoji/construction_worker_tone5.png Binary files differnew file mode 100644 index 00000000000..acbb220b8bb --- /dev/null +++ b/app/assets/images/emoji/construction_worker_tone5.png diff --git a/app/assets/images/emoji/control_knobs.png b/app/assets/images/emoji/control_knobs.png Binary files differnew file mode 100644 index 00000000000..6635ac93b50 --- /dev/null +++ b/app/assets/images/emoji/control_knobs.png diff --git a/app/assets/images/emoji/convenience_store.png b/app/assets/images/emoji/convenience_store.png Binary files differnew file mode 100644 index 00000000000..26b53b5669e --- /dev/null +++ b/app/assets/images/emoji/convenience_store.png diff --git a/app/assets/images/emoji/cookie.png b/app/assets/images/emoji/cookie.png Binary files differnew file mode 100644 index 00000000000..1b6bcb1554f --- /dev/null +++ b/app/assets/images/emoji/cookie.png diff --git a/app/assets/images/emoji/cooking.png b/app/assets/images/emoji/cooking.png Binary files differnew file mode 100644 index 00000000000..918c980577a --- /dev/null +++ b/app/assets/images/emoji/cooking.png diff --git a/app/assets/images/emoji/cool.png b/app/assets/images/emoji/cool.png Binary files differnew file mode 100644 index 00000000000..74674978d00 --- /dev/null +++ b/app/assets/images/emoji/cool.png diff --git a/app/assets/images/emoji/cop.png b/app/assets/images/emoji/cop.png Binary files differnew file mode 100644 index 00000000000..0b16d7c17b7 --- /dev/null +++ b/app/assets/images/emoji/cop.png diff --git a/app/assets/images/emoji/cop_tone1.png b/app/assets/images/emoji/cop_tone1.png Binary files differnew file mode 100644 index 00000000000..6ccba3879dc --- /dev/null +++ b/app/assets/images/emoji/cop_tone1.png diff --git a/app/assets/images/emoji/cop_tone2.png b/app/assets/images/emoji/cop_tone2.png Binary files differnew file mode 100644 index 00000000000..7814ea9f52d --- /dev/null +++ b/app/assets/images/emoji/cop_tone2.png diff --git a/app/assets/images/emoji/cop_tone3.png b/app/assets/images/emoji/cop_tone3.png Binary files differnew file mode 100644 index 00000000000..d78e88ec872 --- /dev/null +++ b/app/assets/images/emoji/cop_tone3.png diff --git a/app/assets/images/emoji/cop_tone4.png b/app/assets/images/emoji/cop_tone4.png Binary files differnew file mode 100644 index 00000000000..2e13c508315 --- /dev/null +++ b/app/assets/images/emoji/cop_tone4.png diff --git a/app/assets/images/emoji/cop_tone5.png b/app/assets/images/emoji/cop_tone5.png Binary files differnew file mode 100644 index 00000000000..2980d61cc2e --- /dev/null +++ b/app/assets/images/emoji/cop_tone5.png diff --git a/app/assets/images/emoji/copyright.png b/app/assets/images/emoji/copyright.png Binary files differnew file mode 100644 index 00000000000..6b9a6adbfd2 --- /dev/null +++ b/app/assets/images/emoji/copyright.png diff --git a/app/assets/images/emoji/corn.png b/app/assets/images/emoji/corn.png Binary files differnew file mode 100644 index 00000000000..36e20127931 --- /dev/null +++ b/app/assets/images/emoji/corn.png diff --git a/app/assets/images/emoji/couch.png b/app/assets/images/emoji/couch.png Binary files differnew file mode 100644 index 00000000000..27b19b13bb0 --- /dev/null +++ b/app/assets/images/emoji/couch.png diff --git a/app/assets/images/emoji/couple.png b/app/assets/images/emoji/couple.png Binary files differnew file mode 100644 index 00000000000..960323f3c16 --- /dev/null +++ b/app/assets/images/emoji/couple.png diff --git a/app/assets/images/emoji/couple_mm.png b/app/assets/images/emoji/couple_mm.png Binary files differnew file mode 100644 index 00000000000..8759fa5db87 --- /dev/null +++ b/app/assets/images/emoji/couple_mm.png diff --git a/app/assets/images/emoji/couple_with_heart.png b/app/assets/images/emoji/couple_with_heart.png Binary files differnew file mode 100644 index 00000000000..62111601b36 --- /dev/null +++ b/app/assets/images/emoji/couple_with_heart.png diff --git a/app/assets/images/emoji/couple_ww.png b/app/assets/images/emoji/couple_ww.png Binary files differnew file mode 100644 index 00000000000..08fdabcdc5c --- /dev/null +++ b/app/assets/images/emoji/couple_ww.png diff --git a/app/assets/images/emoji/couplekiss.png b/app/assets/images/emoji/couplekiss.png Binary files differnew file mode 100644 index 00000000000..9aa519da9e8 --- /dev/null +++ b/app/assets/images/emoji/couplekiss.png diff --git a/app/assets/images/emoji/cow.png b/app/assets/images/emoji/cow.png Binary files differnew file mode 100644 index 00000000000..718a3986d64 --- /dev/null +++ b/app/assets/images/emoji/cow.png diff --git a/app/assets/images/emoji/cow2.png b/app/assets/images/emoji/cow2.png Binary files differnew file mode 100644 index 00000000000..4d0ca534ff1 --- /dev/null +++ b/app/assets/images/emoji/cow2.png diff --git a/app/assets/images/emoji/cowboy.png b/app/assets/images/emoji/cowboy.png Binary files differnew file mode 100644 index 00000000000..70dd5d0d9d1 --- /dev/null +++ b/app/assets/images/emoji/cowboy.png diff --git a/app/assets/images/emoji/crab.png b/app/assets/images/emoji/crab.png Binary files differnew file mode 100644 index 00000000000..19f3047ab61 --- /dev/null +++ b/app/assets/images/emoji/crab.png diff --git a/app/assets/images/emoji/crayon.png b/app/assets/images/emoji/crayon.png Binary files differnew file mode 100644 index 00000000000..8d7b427aaa3 --- /dev/null +++ b/app/assets/images/emoji/crayon.png diff --git a/app/assets/images/emoji/credit_card.png b/app/assets/images/emoji/credit_card.png Binary files differnew file mode 100644 index 00000000000..372777d5c61 --- /dev/null +++ b/app/assets/images/emoji/credit_card.png diff --git a/app/assets/images/emoji/crescent_moon.png b/app/assets/images/emoji/crescent_moon.png Binary files differnew file mode 100644 index 00000000000..765420ecec7 --- /dev/null +++ b/app/assets/images/emoji/crescent_moon.png diff --git a/app/assets/images/emoji/cricket.png b/app/assets/images/emoji/cricket.png Binary files differnew file mode 100644 index 00000000000..d602294a2cd --- /dev/null +++ b/app/assets/images/emoji/cricket.png diff --git a/app/assets/images/emoji/crocodile.png b/app/assets/images/emoji/crocodile.png Binary files differnew file mode 100644 index 00000000000..3005c46f176 --- /dev/null +++ b/app/assets/images/emoji/crocodile.png diff --git a/app/assets/images/emoji/croissant.png b/app/assets/images/emoji/croissant.png Binary files differnew file mode 100644 index 00000000000..fb33feb1a38 --- /dev/null +++ b/app/assets/images/emoji/croissant.png diff --git a/app/assets/images/emoji/cross.png b/app/assets/images/emoji/cross.png Binary files differnew file mode 100644 index 00000000000..42b10e82257 --- /dev/null +++ b/app/assets/images/emoji/cross.png diff --git a/app/assets/images/emoji/crossed_flags.png b/app/assets/images/emoji/crossed_flags.png Binary files differnew file mode 100644 index 00000000000..273bd0f0fe5 --- /dev/null +++ b/app/assets/images/emoji/crossed_flags.png diff --git a/app/assets/images/emoji/crossed_swords.png b/app/assets/images/emoji/crossed_swords.png Binary files differnew file mode 100644 index 00000000000..907e9607134 --- /dev/null +++ b/app/assets/images/emoji/crossed_swords.png diff --git a/app/assets/images/emoji/crown.png b/app/assets/images/emoji/crown.png Binary files differnew file mode 100644 index 00000000000..93b82d92f04 --- /dev/null +++ b/app/assets/images/emoji/crown.png diff --git a/app/assets/images/emoji/cruise_ship.png b/app/assets/images/emoji/cruise_ship.png Binary files differnew file mode 100644 index 00000000000..19d4acbe40c --- /dev/null +++ b/app/assets/images/emoji/cruise_ship.png diff --git a/app/assets/images/emoji/cry.png b/app/assets/images/emoji/cry.png Binary files differnew file mode 100644 index 00000000000..b7877f8a173 --- /dev/null +++ b/app/assets/images/emoji/cry.png diff --git a/app/assets/images/emoji/crying_cat_face.png b/app/assets/images/emoji/crying_cat_face.png Binary files differnew file mode 100644 index 00000000000..b4f49715e00 --- /dev/null +++ b/app/assets/images/emoji/crying_cat_face.png diff --git a/app/assets/images/emoji/crystal_ball.png b/app/assets/images/emoji/crystal_ball.png Binary files differnew file mode 100644 index 00000000000..485d5c888f1 --- /dev/null +++ b/app/assets/images/emoji/crystal_ball.png diff --git a/app/assets/images/emoji/cucumber.png b/app/assets/images/emoji/cucumber.png Binary files differnew file mode 100644 index 00000000000..500807059d2 --- /dev/null +++ b/app/assets/images/emoji/cucumber.png diff --git a/app/assets/images/emoji/cupid.png b/app/assets/images/emoji/cupid.png Binary files differnew file mode 100644 index 00000000000..2df0078ddd1 --- /dev/null +++ b/app/assets/images/emoji/cupid.png diff --git a/app/assets/images/emoji/curly_loop.png b/app/assets/images/emoji/curly_loop.png Binary files differnew file mode 100644 index 00000000000..440aa56d50e --- /dev/null +++ b/app/assets/images/emoji/curly_loop.png diff --git a/app/assets/images/emoji/currency_exchange.png b/app/assets/images/emoji/currency_exchange.png Binary files differnew file mode 100644 index 00000000000..4d46c6050e7 --- /dev/null +++ b/app/assets/images/emoji/currency_exchange.png diff --git a/app/assets/images/emoji/curry.png b/app/assets/images/emoji/curry.png Binary files differnew file mode 100644 index 00000000000..69657ca8103 --- /dev/null +++ b/app/assets/images/emoji/curry.png diff --git a/app/assets/images/emoji/custard.png b/app/assets/images/emoji/custard.png Binary files differnew file mode 100644 index 00000000000..fa3df67b8f6 --- /dev/null +++ b/app/assets/images/emoji/custard.png diff --git a/app/assets/images/emoji/customs.png b/app/assets/images/emoji/customs.png Binary files differnew file mode 100644 index 00000000000..21b7ce2c69e --- /dev/null +++ b/app/assets/images/emoji/customs.png diff --git a/app/assets/images/emoji/cyclone.png b/app/assets/images/emoji/cyclone.png Binary files differnew file mode 100644 index 00000000000..ff00b1afe70 --- /dev/null +++ b/app/assets/images/emoji/cyclone.png diff --git a/app/assets/images/emoji/dagger.png b/app/assets/images/emoji/dagger.png Binary files differnew file mode 100644 index 00000000000..66e97b0aa25 --- /dev/null +++ b/app/assets/images/emoji/dagger.png diff --git a/app/assets/images/emoji/dancer.png b/app/assets/images/emoji/dancer.png Binary files differnew file mode 100644 index 00000000000..04b166991cb --- /dev/null +++ b/app/assets/images/emoji/dancer.png diff --git a/app/assets/images/emoji/dancer_tone1.png b/app/assets/images/emoji/dancer_tone1.png Binary files differnew file mode 100644 index 00000000000..2c7b11c3a6e --- /dev/null +++ b/app/assets/images/emoji/dancer_tone1.png diff --git a/app/assets/images/emoji/dancer_tone2.png b/app/assets/images/emoji/dancer_tone2.png Binary files differnew file mode 100644 index 00000000000..cb04b1f907e --- /dev/null +++ b/app/assets/images/emoji/dancer_tone2.png diff --git a/app/assets/images/emoji/dancer_tone3.png b/app/assets/images/emoji/dancer_tone3.png Binary files differnew file mode 100644 index 00000000000..98c5bca7b64 --- /dev/null +++ b/app/assets/images/emoji/dancer_tone3.png diff --git a/app/assets/images/emoji/dancer_tone4.png b/app/assets/images/emoji/dancer_tone4.png Binary files differnew file mode 100644 index 00000000000..fdb1e00cbba --- /dev/null +++ b/app/assets/images/emoji/dancer_tone4.png diff --git a/app/assets/images/emoji/dancer_tone5.png b/app/assets/images/emoji/dancer_tone5.png Binary files differnew file mode 100644 index 00000000000..0e34e0e23f0 --- /dev/null +++ b/app/assets/images/emoji/dancer_tone5.png diff --git a/app/assets/images/emoji/dancers.png b/app/assets/images/emoji/dancers.png Binary files differnew file mode 100644 index 00000000000..67e6ffacb76 --- /dev/null +++ b/app/assets/images/emoji/dancers.png diff --git a/app/assets/images/emoji/dango.png b/app/assets/images/emoji/dango.png Binary files differnew file mode 100644 index 00000000000..f73f37b01c7 --- /dev/null +++ b/app/assets/images/emoji/dango.png diff --git a/app/assets/images/emoji/dark_sunglasses.png b/app/assets/images/emoji/dark_sunglasses.png Binary files differnew file mode 100644 index 00000000000..b1b6db0acff --- /dev/null +++ b/app/assets/images/emoji/dark_sunglasses.png diff --git a/app/assets/images/emoji/dart.png b/app/assets/images/emoji/dart.png Binary files differnew file mode 100644 index 00000000000..f6704aeb8ba --- /dev/null +++ b/app/assets/images/emoji/dart.png diff --git a/app/assets/images/emoji/dash.png b/app/assets/images/emoji/dash.png Binary files differnew file mode 100644 index 00000000000..064b8525c12 --- /dev/null +++ b/app/assets/images/emoji/dash.png diff --git a/app/assets/images/emoji/date.png b/app/assets/images/emoji/date.png Binary files differnew file mode 100644 index 00000000000..f05b3da97b8 --- /dev/null +++ b/app/assets/images/emoji/date.png diff --git a/app/assets/images/emoji/deciduous_tree.png b/app/assets/images/emoji/deciduous_tree.png Binary files differnew file mode 100644 index 00000000000..785fc1c30ea --- /dev/null +++ b/app/assets/images/emoji/deciduous_tree.png diff --git a/app/assets/images/emoji/deer.png b/app/assets/images/emoji/deer.png Binary files differnew file mode 100644 index 00000000000..d8698195ff0 --- /dev/null +++ b/app/assets/images/emoji/deer.png diff --git a/app/assets/images/emoji/department_store.png b/app/assets/images/emoji/department_store.png Binary files differnew file mode 100644 index 00000000000..58867c7a6e1 --- /dev/null +++ b/app/assets/images/emoji/department_store.png diff --git a/app/assets/images/emoji/desert.png b/app/assets/images/emoji/desert.png Binary files differnew file mode 100644 index 00000000000..e9966ff8c65 --- /dev/null +++ b/app/assets/images/emoji/desert.png diff --git a/app/assets/images/emoji/desktop.png b/app/assets/images/emoji/desktop.png Binary files differnew file mode 100644 index 00000000000..909bd42b5e1 --- /dev/null +++ b/app/assets/images/emoji/desktop.png diff --git a/app/assets/images/emoji/diamond_shape_with_a_dot_inside.png b/app/assets/images/emoji/diamond_shape_with_a_dot_inside.png Binary files differnew file mode 100644 index 00000000000..2a22a26d1e2 --- /dev/null +++ b/app/assets/images/emoji/diamond_shape_with_a_dot_inside.png diff --git a/app/assets/images/emoji/diamonds.png b/app/assets/images/emoji/diamonds.png Binary files differnew file mode 100644 index 00000000000..1f25f51f97a --- /dev/null +++ b/app/assets/images/emoji/diamonds.png diff --git a/app/assets/images/emoji/disappointed.png b/app/assets/images/emoji/disappointed.png Binary files differnew file mode 100644 index 00000000000..efe4e67e23c --- /dev/null +++ b/app/assets/images/emoji/disappointed.png diff --git a/app/assets/images/emoji/disappointed_relieved.png b/app/assets/images/emoji/disappointed_relieved.png Binary files differnew file mode 100644 index 00000000000..aef864d2b3d --- /dev/null +++ b/app/assets/images/emoji/disappointed_relieved.png diff --git a/app/assets/images/emoji/dividers.png b/app/assets/images/emoji/dividers.png Binary files differnew file mode 100644 index 00000000000..46a7e403f9d --- /dev/null +++ b/app/assets/images/emoji/dividers.png diff --git a/app/assets/images/emoji/dizzy.png b/app/assets/images/emoji/dizzy.png Binary files differnew file mode 100644 index 00000000000..85f52efad24 --- /dev/null +++ b/app/assets/images/emoji/dizzy.png diff --git a/app/assets/images/emoji/dizzy_face.png b/app/assets/images/emoji/dizzy_face.png Binary files differnew file mode 100644 index 00000000000..3120316ab5e --- /dev/null +++ b/app/assets/images/emoji/dizzy_face.png diff --git a/app/assets/images/emoji/do_not_litter.png b/app/assets/images/emoji/do_not_litter.png Binary files differnew file mode 100644 index 00000000000..341d2575f4f --- /dev/null +++ b/app/assets/images/emoji/do_not_litter.png diff --git a/app/assets/images/emoji/dog.png b/app/assets/images/emoji/dog.png Binary files differnew file mode 100644 index 00000000000..281b81d58bd --- /dev/null +++ b/app/assets/images/emoji/dog.png diff --git a/app/assets/images/emoji/dog2.png b/app/assets/images/emoji/dog2.png Binary files differnew file mode 100644 index 00000000000..976143dbdbe --- /dev/null +++ b/app/assets/images/emoji/dog2.png diff --git a/app/assets/images/emoji/dollar.png b/app/assets/images/emoji/dollar.png Binary files differnew file mode 100644 index 00000000000..a9904c28293 --- /dev/null +++ b/app/assets/images/emoji/dollar.png diff --git a/app/assets/images/emoji/dolls.png b/app/assets/images/emoji/dolls.png Binary files differnew file mode 100644 index 00000000000..10955615110 --- /dev/null +++ b/app/assets/images/emoji/dolls.png diff --git a/app/assets/images/emoji/dolphin.png b/app/assets/images/emoji/dolphin.png Binary files differnew file mode 100644 index 00000000000..81434809003 --- /dev/null +++ b/app/assets/images/emoji/dolphin.png diff --git a/app/assets/images/emoji/door.png b/app/assets/images/emoji/door.png Binary files differnew file mode 100644 index 00000000000..36ae3e27494 --- /dev/null +++ b/app/assets/images/emoji/door.png diff --git a/app/assets/images/emoji/doughnut.png b/app/assets/images/emoji/doughnut.png Binary files differnew file mode 100644 index 00000000000..0ca4cd0bde8 --- /dev/null +++ b/app/assets/images/emoji/doughnut.png diff --git a/app/assets/images/emoji/dove.png b/app/assets/images/emoji/dove.png Binary files differnew file mode 100644 index 00000000000..9580c4917d7 --- /dev/null +++ b/app/assets/images/emoji/dove.png diff --git a/app/assets/images/emoji/dragon.png b/app/assets/images/emoji/dragon.png Binary files differnew file mode 100644 index 00000000000..d6311cf5429 --- /dev/null +++ b/app/assets/images/emoji/dragon.png diff --git a/app/assets/images/emoji/dragon_face.png b/app/assets/images/emoji/dragon_face.png Binary files differnew file mode 100644 index 00000000000..3c2720446c6 --- /dev/null +++ b/app/assets/images/emoji/dragon_face.png diff --git a/app/assets/images/emoji/dress.png b/app/assets/images/emoji/dress.png Binary files differnew file mode 100644 index 00000000000..a697ca5c57d --- /dev/null +++ b/app/assets/images/emoji/dress.png diff --git a/app/assets/images/emoji/dromedary_camel.png b/app/assets/images/emoji/dromedary_camel.png Binary files differnew file mode 100644 index 00000000000..5271637c7c4 --- /dev/null +++ b/app/assets/images/emoji/dromedary_camel.png diff --git a/app/assets/images/emoji/drooling_face.png b/app/assets/images/emoji/drooling_face.png Binary files differnew file mode 100644 index 00000000000..a5460532597 --- /dev/null +++ b/app/assets/images/emoji/drooling_face.png diff --git a/app/assets/images/emoji/droplet.png b/app/assets/images/emoji/droplet.png Binary files differnew file mode 100644 index 00000000000..71241ec3061 --- /dev/null +++ b/app/assets/images/emoji/droplet.png diff --git a/app/assets/images/emoji/drum.png b/app/assets/images/emoji/drum.png Binary files differnew file mode 100644 index 00000000000..b038727cc99 --- /dev/null +++ b/app/assets/images/emoji/drum.png diff --git a/app/assets/images/emoji/duck.png b/app/assets/images/emoji/duck.png Binary files differnew file mode 100644 index 00000000000..74330b77ca3 --- /dev/null +++ b/app/assets/images/emoji/duck.png diff --git a/app/assets/images/emoji/dvd.png b/app/assets/images/emoji/dvd.png Binary files differnew file mode 100644 index 00000000000..045a6f7a08d --- /dev/null +++ b/app/assets/images/emoji/dvd.png diff --git a/app/assets/images/emoji/e-mail.png b/app/assets/images/emoji/e-mail.png Binary files differnew file mode 100644 index 00000000000..d22e654a20b --- /dev/null +++ b/app/assets/images/emoji/e-mail.png diff --git a/app/assets/images/emoji/eagle.png b/app/assets/images/emoji/eagle.png Binary files differnew file mode 100644 index 00000000000..4f277debeef --- /dev/null +++ b/app/assets/images/emoji/eagle.png diff --git a/app/assets/images/emoji/ear.png b/app/assets/images/emoji/ear.png Binary files differnew file mode 100644 index 00000000000..f84f9ff154a --- /dev/null +++ b/app/assets/images/emoji/ear.png diff --git a/app/assets/images/emoji/ear_of_rice.png b/app/assets/images/emoji/ear_of_rice.png Binary files differnew file mode 100644 index 00000000000..3564d9d643a --- /dev/null +++ b/app/assets/images/emoji/ear_of_rice.png diff --git a/app/assets/images/emoji/ear_tone1.png b/app/assets/images/emoji/ear_tone1.png Binary files differnew file mode 100644 index 00000000000..d09e1e41996 --- /dev/null +++ b/app/assets/images/emoji/ear_tone1.png diff --git a/app/assets/images/emoji/ear_tone2.png b/app/assets/images/emoji/ear_tone2.png Binary files differnew file mode 100644 index 00000000000..300d60a9948 --- /dev/null +++ b/app/assets/images/emoji/ear_tone2.png diff --git a/app/assets/images/emoji/ear_tone3.png b/app/assets/images/emoji/ear_tone3.png Binary files differnew file mode 100644 index 00000000000..2a56eebe445 --- /dev/null +++ b/app/assets/images/emoji/ear_tone3.png diff --git a/app/assets/images/emoji/ear_tone4.png b/app/assets/images/emoji/ear_tone4.png Binary files differnew file mode 100644 index 00000000000..bd270f7763e --- /dev/null +++ b/app/assets/images/emoji/ear_tone4.png diff --git a/app/assets/images/emoji/ear_tone5.png b/app/assets/images/emoji/ear_tone5.png Binary files differnew file mode 100644 index 00000000000..b96bb441dff --- /dev/null +++ b/app/assets/images/emoji/ear_tone5.png diff --git a/app/assets/images/emoji/earth_africa.png b/app/assets/images/emoji/earth_africa.png Binary files differnew file mode 100644 index 00000000000..66c3348c23a --- /dev/null +++ b/app/assets/images/emoji/earth_africa.png diff --git a/app/assets/images/emoji/earth_americas.png b/app/assets/images/emoji/earth_americas.png Binary files differnew file mode 100644 index 00000000000..538c3cddd68 --- /dev/null +++ b/app/assets/images/emoji/earth_americas.png diff --git a/app/assets/images/emoji/earth_asia.png b/app/assets/images/emoji/earth_asia.png Binary files differnew file mode 100644 index 00000000000..d8df97fec3c --- /dev/null +++ b/app/assets/images/emoji/earth_asia.png diff --git a/app/assets/images/emoji/egg.png b/app/assets/images/emoji/egg.png Binary files differnew file mode 100644 index 00000000000..c171974d993 --- /dev/null +++ b/app/assets/images/emoji/egg.png diff --git a/app/assets/images/emoji/eggplant.png b/app/assets/images/emoji/eggplant.png Binary files differnew file mode 100644 index 00000000000..fafd7c1a14c --- /dev/null +++ b/app/assets/images/emoji/eggplant.png diff --git a/app/assets/images/emoji/eight.png b/app/assets/images/emoji/eight.png Binary files differnew file mode 100644 index 00000000000..8c95874d4c5 --- /dev/null +++ b/app/assets/images/emoji/eight.png diff --git a/app/assets/images/emoji/eight_pointed_black_star.png b/app/assets/images/emoji/eight_pointed_black_star.png Binary files differnew file mode 100644 index 00000000000..820179bda50 --- /dev/null +++ b/app/assets/images/emoji/eight_pointed_black_star.png diff --git a/app/assets/images/emoji/eight_spoked_asterisk.png b/app/assets/images/emoji/eight_spoked_asterisk.png Binary files differnew file mode 100644 index 00000000000..3307ffa62ee --- /dev/null +++ b/app/assets/images/emoji/eight_spoked_asterisk.png diff --git a/app/assets/images/emoji/eject.png b/app/assets/images/emoji/eject.png Binary files differnew file mode 100644 index 00000000000..ec5cfc48973 --- /dev/null +++ b/app/assets/images/emoji/eject.png diff --git a/app/assets/images/emoji/electric_plug.png b/app/assets/images/emoji/electric_plug.png Binary files differnew file mode 100644 index 00000000000..31d1eb215b4 --- /dev/null +++ b/app/assets/images/emoji/electric_plug.png diff --git a/app/assets/images/emoji/elephant.png b/app/assets/images/emoji/elephant.png Binary files differnew file mode 100644 index 00000000000..b8a6d140595 --- /dev/null +++ b/app/assets/images/emoji/elephant.png diff --git a/app/assets/images/emoji/end.png b/app/assets/images/emoji/end.png Binary files differnew file mode 100644 index 00000000000..ef3ccd5f367 --- /dev/null +++ b/app/assets/images/emoji/end.png diff --git a/app/assets/images/emoji/envelope.png b/app/assets/images/emoji/envelope.png Binary files differnew file mode 100644 index 00000000000..ec77ac375a4 --- /dev/null +++ b/app/assets/images/emoji/envelope.png diff --git a/app/assets/images/emoji/envelope_with_arrow.png b/app/assets/images/emoji/envelope_with_arrow.png Binary files differnew file mode 100644 index 00000000000..7448a6b7673 --- /dev/null +++ b/app/assets/images/emoji/envelope_with_arrow.png diff --git a/app/assets/images/emoji/euro.png b/app/assets/images/emoji/euro.png Binary files differnew file mode 100644 index 00000000000..a49020820e1 --- /dev/null +++ b/app/assets/images/emoji/euro.png diff --git a/app/assets/images/emoji/european_castle.png b/app/assets/images/emoji/european_castle.png Binary files differnew file mode 100644 index 00000000000..888d11332ce --- /dev/null +++ b/app/assets/images/emoji/european_castle.png diff --git a/app/assets/images/emoji/european_post_office.png b/app/assets/images/emoji/european_post_office.png Binary files differnew file mode 100644 index 00000000000..3745aff8dd2 --- /dev/null +++ b/app/assets/images/emoji/european_post_office.png diff --git a/app/assets/images/emoji/evergreen_tree.png b/app/assets/images/emoji/evergreen_tree.png Binary files differnew file mode 100644 index 00000000000..f679d8dd772 --- /dev/null +++ b/app/assets/images/emoji/evergreen_tree.png diff --git a/app/assets/images/emoji/exclamation.png b/app/assets/images/emoji/exclamation.png Binary files differnew file mode 100644 index 00000000000..2c14406422f --- /dev/null +++ b/app/assets/images/emoji/exclamation.png diff --git a/app/assets/images/emoji/expressionless.png b/app/assets/images/emoji/expressionless.png Binary files differnew file mode 100644 index 00000000000..2954017f6c2 --- /dev/null +++ b/app/assets/images/emoji/expressionless.png diff --git a/app/assets/images/emoji/eye.png b/app/assets/images/emoji/eye.png Binary files differnew file mode 100644 index 00000000000..9d989cdd375 --- /dev/null +++ b/app/assets/images/emoji/eye.png diff --git a/app/assets/images/emoji/eye_in_speech_bubble.png b/app/assets/images/emoji/eye_in_speech_bubble.png Binary files differnew file mode 100644 index 00000000000..21bd22bbcce --- /dev/null +++ b/app/assets/images/emoji/eye_in_speech_bubble.png diff --git a/app/assets/images/emoji/eyeglasses.png b/app/assets/images/emoji/eyeglasses.png Binary files differnew file mode 100644 index 00000000000..865d8274acf --- /dev/null +++ b/app/assets/images/emoji/eyeglasses.png diff --git a/app/assets/images/emoji/eyes.png b/app/assets/images/emoji/eyes.png Binary files differnew file mode 100644 index 00000000000..2102ada7e09 --- /dev/null +++ b/app/assets/images/emoji/eyes.png diff --git a/app/assets/images/emoji/face_palm.png b/app/assets/images/emoji/face_palm.png Binary files differnew file mode 100644 index 00000000000..defc796cf16 --- /dev/null +++ b/app/assets/images/emoji/face_palm.png diff --git a/app/assets/images/emoji/face_palm_tone1.png b/app/assets/images/emoji/face_palm_tone1.png Binary files differnew file mode 100644 index 00000000000..2f4b010bb40 --- /dev/null +++ b/app/assets/images/emoji/face_palm_tone1.png diff --git a/app/assets/images/emoji/face_palm_tone2.png b/app/assets/images/emoji/face_palm_tone2.png Binary files differnew file mode 100644 index 00000000000..97fb6831687 --- /dev/null +++ b/app/assets/images/emoji/face_palm_tone2.png diff --git a/app/assets/images/emoji/face_palm_tone3.png b/app/assets/images/emoji/face_palm_tone3.png Binary files differnew file mode 100644 index 00000000000..b5b5c1e5306 --- /dev/null +++ b/app/assets/images/emoji/face_palm_tone3.png diff --git a/app/assets/images/emoji/face_palm_tone4.png b/app/assets/images/emoji/face_palm_tone4.png Binary files differnew file mode 100644 index 00000000000..2840b113483 --- /dev/null +++ b/app/assets/images/emoji/face_palm_tone4.png diff --git a/app/assets/images/emoji/face_palm_tone5.png b/app/assets/images/emoji/face_palm_tone5.png Binary files differnew file mode 100644 index 00000000000..6f070db98be --- /dev/null +++ b/app/assets/images/emoji/face_palm_tone5.png diff --git a/app/assets/images/emoji/factory.png b/app/assets/images/emoji/factory.png Binary files differnew file mode 100644 index 00000000000..e1d2ddf4a27 --- /dev/null +++ b/app/assets/images/emoji/factory.png diff --git a/app/assets/images/emoji/fallen_leaf.png b/app/assets/images/emoji/fallen_leaf.png Binary files differnew file mode 100644 index 00000000000..0d60e7bdf2d --- /dev/null +++ b/app/assets/images/emoji/fallen_leaf.png diff --git a/app/assets/images/emoji/family.png b/app/assets/images/emoji/family.png Binary files differnew file mode 100644 index 00000000000..26421965791 --- /dev/null +++ b/app/assets/images/emoji/family.png diff --git a/app/assets/images/emoji/family_mmb.png b/app/assets/images/emoji/family_mmb.png Binary files differnew file mode 100644 index 00000000000..7a2e4e2c491 --- /dev/null +++ b/app/assets/images/emoji/family_mmb.png diff --git a/app/assets/images/emoji/family_mmbb.png b/app/assets/images/emoji/family_mmbb.png Binary files differnew file mode 100644 index 00000000000..81e6c0fc0ee --- /dev/null +++ b/app/assets/images/emoji/family_mmbb.png diff --git a/app/assets/images/emoji/family_mmg.png b/app/assets/images/emoji/family_mmg.png Binary files differnew file mode 100644 index 00000000000..932a85e1fe5 --- /dev/null +++ b/app/assets/images/emoji/family_mmg.png diff --git a/app/assets/images/emoji/family_mmgb.png b/app/assets/images/emoji/family_mmgb.png Binary files differnew file mode 100644 index 00000000000..41e35166670 --- /dev/null +++ b/app/assets/images/emoji/family_mmgb.png diff --git a/app/assets/images/emoji/family_mmgg.png b/app/assets/images/emoji/family_mmgg.png Binary files differnew file mode 100644 index 00000000000..8e8ccfe6c7f --- /dev/null +++ b/app/assets/images/emoji/family_mmgg.png diff --git a/app/assets/images/emoji/family_mwbb.png b/app/assets/images/emoji/family_mwbb.png Binary files differnew file mode 100644 index 00000000000..b544fbe573f --- /dev/null +++ b/app/assets/images/emoji/family_mwbb.png diff --git a/app/assets/images/emoji/family_mwg.png b/app/assets/images/emoji/family_mwg.png Binary files differnew file mode 100644 index 00000000000..71d2681c32a --- /dev/null +++ b/app/assets/images/emoji/family_mwg.png diff --git a/app/assets/images/emoji/family_mwgb.png b/app/assets/images/emoji/family_mwgb.png Binary files differnew file mode 100644 index 00000000000..40dbf1f7a18 --- /dev/null +++ b/app/assets/images/emoji/family_mwgb.png diff --git a/app/assets/images/emoji/family_mwgg.png b/app/assets/images/emoji/family_mwgg.png Binary files differnew file mode 100644 index 00000000000..bfefa4879cb --- /dev/null +++ b/app/assets/images/emoji/family_mwgg.png diff --git a/app/assets/images/emoji/family_wwb.png b/app/assets/images/emoji/family_wwb.png Binary files differnew file mode 100644 index 00000000000..836feae7c78 --- /dev/null +++ b/app/assets/images/emoji/family_wwb.png diff --git a/app/assets/images/emoji/family_wwbb.png b/app/assets/images/emoji/family_wwbb.png Binary files differnew file mode 100644 index 00000000000..6c6ba45e7bb --- /dev/null +++ b/app/assets/images/emoji/family_wwbb.png diff --git a/app/assets/images/emoji/family_wwg.png b/app/assets/images/emoji/family_wwg.png Binary files differnew file mode 100644 index 00000000000..41225c6fa5a --- /dev/null +++ b/app/assets/images/emoji/family_wwg.png diff --git a/app/assets/images/emoji/family_wwgb.png b/app/assets/images/emoji/family_wwgb.png Binary files differnew file mode 100644 index 00000000000..284d29ab5da --- /dev/null +++ b/app/assets/images/emoji/family_wwgb.png diff --git a/app/assets/images/emoji/family_wwgg.png b/app/assets/images/emoji/family_wwgg.png Binary files differnew file mode 100644 index 00000000000..d8d3f49b85f --- /dev/null +++ b/app/assets/images/emoji/family_wwgg.png diff --git a/app/assets/images/emoji/fast_forward.png b/app/assets/images/emoji/fast_forward.png Binary files differnew file mode 100644 index 00000000000..c406fedfdb1 --- /dev/null +++ b/app/assets/images/emoji/fast_forward.png diff --git a/app/assets/images/emoji/fax.png b/app/assets/images/emoji/fax.png Binary files differnew file mode 100644 index 00000000000..6f929e294c2 --- /dev/null +++ b/app/assets/images/emoji/fax.png diff --git a/app/assets/images/emoji/fearful.png b/app/assets/images/emoji/fearful.png Binary files differnew file mode 100644 index 00000000000..eb8b347cef9 --- /dev/null +++ b/app/assets/images/emoji/fearful.png diff --git a/app/assets/images/emoji/feet.png b/app/assets/images/emoji/feet.png Binary files differnew file mode 100644 index 00000000000..5fe568cee93 --- /dev/null +++ b/app/assets/images/emoji/feet.png diff --git a/app/assets/images/emoji/fencer.png b/app/assets/images/emoji/fencer.png Binary files differnew file mode 100644 index 00000000000..5288c920eb9 --- /dev/null +++ b/app/assets/images/emoji/fencer.png diff --git a/app/assets/images/emoji/ferris_wheel.png b/app/assets/images/emoji/ferris_wheel.png Binary files differnew file mode 100644 index 00000000000..55c8ff0475b --- /dev/null +++ b/app/assets/images/emoji/ferris_wheel.png diff --git a/app/assets/images/emoji/ferry.png b/app/assets/images/emoji/ferry.png Binary files differnew file mode 100644 index 00000000000..41816b3ae34 --- /dev/null +++ b/app/assets/images/emoji/ferry.png diff --git a/app/assets/images/emoji/field_hockey.png b/app/assets/images/emoji/field_hockey.png Binary files differnew file mode 100644 index 00000000000..839637716ee --- /dev/null +++ b/app/assets/images/emoji/field_hockey.png diff --git a/app/assets/images/emoji/file_cabinet.png b/app/assets/images/emoji/file_cabinet.png Binary files differnew file mode 100644 index 00000000000..fddc65dde96 --- /dev/null +++ b/app/assets/images/emoji/file_cabinet.png diff --git a/app/assets/images/emoji/file_folder.png b/app/assets/images/emoji/file_folder.png Binary files differnew file mode 100644 index 00000000000..addedaf0870 --- /dev/null +++ b/app/assets/images/emoji/file_folder.png diff --git a/app/assets/images/emoji/film_frames.png b/app/assets/images/emoji/film_frames.png Binary files differnew file mode 100644 index 00000000000..30143aedbe6 --- /dev/null +++ b/app/assets/images/emoji/film_frames.png diff --git a/app/assets/images/emoji/fingers_crossed.png b/app/assets/images/emoji/fingers_crossed.png Binary files differnew file mode 100644 index 00000000000..4cd18514ea3 --- /dev/null +++ b/app/assets/images/emoji/fingers_crossed.png diff --git a/app/assets/images/emoji/fingers_crossed_tone1.png b/app/assets/images/emoji/fingers_crossed_tone1.png Binary files differnew file mode 100644 index 00000000000..dd2384a6cd5 --- /dev/null +++ b/app/assets/images/emoji/fingers_crossed_tone1.png diff --git a/app/assets/images/emoji/fingers_crossed_tone2.png b/app/assets/images/emoji/fingers_crossed_tone2.png Binary files differnew file mode 100644 index 00000000000..6228401befe --- /dev/null +++ b/app/assets/images/emoji/fingers_crossed_tone2.png diff --git a/app/assets/images/emoji/fingers_crossed_tone3.png b/app/assets/images/emoji/fingers_crossed_tone3.png Binary files differnew file mode 100644 index 00000000000..b1074da15f5 --- /dev/null +++ b/app/assets/images/emoji/fingers_crossed_tone3.png diff --git a/app/assets/images/emoji/fingers_crossed_tone4.png b/app/assets/images/emoji/fingers_crossed_tone4.png Binary files differnew file mode 100644 index 00000000000..75e05e4d332 --- /dev/null +++ b/app/assets/images/emoji/fingers_crossed_tone4.png diff --git a/app/assets/images/emoji/fingers_crossed_tone5.png b/app/assets/images/emoji/fingers_crossed_tone5.png Binary files differnew file mode 100644 index 00000000000..761aebdc30f --- /dev/null +++ b/app/assets/images/emoji/fingers_crossed_tone5.png diff --git a/app/assets/images/emoji/fire.png b/app/assets/images/emoji/fire.png Binary files differnew file mode 100644 index 00000000000..bd3775a460b --- /dev/null +++ b/app/assets/images/emoji/fire.png diff --git a/app/assets/images/emoji/fire_engine.png b/app/assets/images/emoji/fire_engine.png Binary files differnew file mode 100644 index 00000000000..2cd45b7cf7e --- /dev/null +++ b/app/assets/images/emoji/fire_engine.png diff --git a/app/assets/images/emoji/fireworks.png b/app/assets/images/emoji/fireworks.png Binary files differnew file mode 100644 index 00000000000..176c8b58265 --- /dev/null +++ b/app/assets/images/emoji/fireworks.png diff --git a/app/assets/images/emoji/first_place.png b/app/assets/images/emoji/first_place.png Binary files differnew file mode 100644 index 00000000000..15612b66492 --- /dev/null +++ b/app/assets/images/emoji/first_place.png diff --git a/app/assets/images/emoji/first_quarter_moon.png b/app/assets/images/emoji/first_quarter_moon.png Binary files differnew file mode 100644 index 00000000000..5dccaf72a4f --- /dev/null +++ b/app/assets/images/emoji/first_quarter_moon.png diff --git a/app/assets/images/emoji/first_quarter_moon_with_face.png b/app/assets/images/emoji/first_quarter_moon_with_face.png Binary files differnew file mode 100644 index 00000000000..cd8a3d7acd8 --- /dev/null +++ b/app/assets/images/emoji/first_quarter_moon_with_face.png diff --git a/app/assets/images/emoji/fish.png b/app/assets/images/emoji/fish.png Binary files differnew file mode 100644 index 00000000000..c2d2faaacd4 --- /dev/null +++ b/app/assets/images/emoji/fish.png diff --git a/app/assets/images/emoji/fish_cake.png b/app/assets/images/emoji/fish_cake.png Binary files differnew file mode 100644 index 00000000000..157bded65db --- /dev/null +++ b/app/assets/images/emoji/fish_cake.png diff --git a/app/assets/images/emoji/fishing_pole_and_fish.png b/app/assets/images/emoji/fishing_pole_and_fish.png Binary files differnew file mode 100644 index 00000000000..dfcdf07eb50 --- /dev/null +++ b/app/assets/images/emoji/fishing_pole_and_fish.png diff --git a/app/assets/images/emoji/fist.png b/app/assets/images/emoji/fist.png Binary files differnew file mode 100644 index 00000000000..de33592bf98 --- /dev/null +++ b/app/assets/images/emoji/fist.png diff --git a/app/assets/images/emoji/fist_tone1.png b/app/assets/images/emoji/fist_tone1.png Binary files differnew file mode 100644 index 00000000000..02809e2dd68 --- /dev/null +++ b/app/assets/images/emoji/fist_tone1.png diff --git a/app/assets/images/emoji/fist_tone2.png b/app/assets/images/emoji/fist_tone2.png Binary files differnew file mode 100644 index 00000000000..5de34810383 --- /dev/null +++ b/app/assets/images/emoji/fist_tone2.png diff --git a/app/assets/images/emoji/fist_tone3.png b/app/assets/images/emoji/fist_tone3.png Binary files differnew file mode 100644 index 00000000000..0d5240129b1 --- /dev/null +++ b/app/assets/images/emoji/fist_tone3.png diff --git a/app/assets/images/emoji/fist_tone4.png b/app/assets/images/emoji/fist_tone4.png Binary files differnew file mode 100644 index 00000000000..a95c0dd634b --- /dev/null +++ b/app/assets/images/emoji/fist_tone4.png diff --git a/app/assets/images/emoji/fist_tone5.png b/app/assets/images/emoji/fist_tone5.png Binary files differnew file mode 100644 index 00000000000..a2f092fd8c7 --- /dev/null +++ b/app/assets/images/emoji/fist_tone5.png diff --git a/app/assets/images/emoji/five.png b/app/assets/images/emoji/five.png Binary files differnew file mode 100644 index 00000000000..d14371f3f27 --- /dev/null +++ b/app/assets/images/emoji/five.png diff --git a/app/assets/images/emoji/flag_ac.png b/app/assets/images/emoji/flag_ac.png Binary files differnew file mode 100644 index 00000000000..286239920c7 --- /dev/null +++ b/app/assets/images/emoji/flag_ac.png diff --git a/app/assets/images/emoji/flag_ad.png b/app/assets/images/emoji/flag_ad.png Binary files differnew file mode 100644 index 00000000000..20f4b14e8ad --- /dev/null +++ b/app/assets/images/emoji/flag_ad.png diff --git a/app/assets/images/emoji/flag_ae.png b/app/assets/images/emoji/flag_ae.png Binary files differnew file mode 100644 index 00000000000..d16ffe4b862 --- /dev/null +++ b/app/assets/images/emoji/flag_ae.png diff --git a/app/assets/images/emoji/flag_af.png b/app/assets/images/emoji/flag_af.png Binary files differnew file mode 100644 index 00000000000..a51533b554d --- /dev/null +++ b/app/assets/images/emoji/flag_af.png diff --git a/app/assets/images/emoji/flag_ag.png b/app/assets/images/emoji/flag_ag.png Binary files differnew file mode 100644 index 00000000000..07f2ce397d0 --- /dev/null +++ b/app/assets/images/emoji/flag_ag.png diff --git a/app/assets/images/emoji/flag_ai.png b/app/assets/images/emoji/flag_ai.png Binary files differnew file mode 100644 index 00000000000..500b5ab09fb --- /dev/null +++ b/app/assets/images/emoji/flag_ai.png diff --git a/app/assets/images/emoji/flag_al.png b/app/assets/images/emoji/flag_al.png Binary files differnew file mode 100644 index 00000000000..03a20132cc6 --- /dev/null +++ b/app/assets/images/emoji/flag_al.png diff --git a/app/assets/images/emoji/flag_am.png b/app/assets/images/emoji/flag_am.png Binary files differnew file mode 100644 index 00000000000..2ad60a273ec --- /dev/null +++ b/app/assets/images/emoji/flag_am.png diff --git a/app/assets/images/emoji/flag_ao.png b/app/assets/images/emoji/flag_ao.png Binary files differnew file mode 100644 index 00000000000..cb46c31f862 --- /dev/null +++ b/app/assets/images/emoji/flag_ao.png diff --git a/app/assets/images/emoji/flag_aq.png b/app/assets/images/emoji/flag_aq.png Binary files differnew file mode 100644 index 00000000000..b272021d375 --- /dev/null +++ b/app/assets/images/emoji/flag_aq.png diff --git a/app/assets/images/emoji/flag_ar.png b/app/assets/images/emoji/flag_ar.png Binary files differnew file mode 100644 index 00000000000..73136caf3b7 --- /dev/null +++ b/app/assets/images/emoji/flag_ar.png diff --git a/app/assets/images/emoji/flag_as.png b/app/assets/images/emoji/flag_as.png Binary files differnew file mode 100644 index 00000000000..3db45a0d9f3 --- /dev/null +++ b/app/assets/images/emoji/flag_as.png diff --git a/app/assets/images/emoji/flag_at.png b/app/assets/images/emoji/flag_at.png Binary files differnew file mode 100644 index 00000000000..c43769dcb19 --- /dev/null +++ b/app/assets/images/emoji/flag_at.png diff --git a/app/assets/images/emoji/flag_au.png b/app/assets/images/emoji/flag_au.png Binary files differnew file mode 100644 index 00000000000..7794309c78c --- /dev/null +++ b/app/assets/images/emoji/flag_au.png diff --git a/app/assets/images/emoji/flag_aw.png b/app/assets/images/emoji/flag_aw.png Binary files differnew file mode 100644 index 00000000000..02c840d12c9 --- /dev/null +++ b/app/assets/images/emoji/flag_aw.png diff --git a/app/assets/images/emoji/flag_ax.png b/app/assets/images/emoji/flag_ax.png Binary files differnew file mode 100644 index 00000000000..fc5466174bb --- /dev/null +++ b/app/assets/images/emoji/flag_ax.png diff --git a/app/assets/images/emoji/flag_az.png b/app/assets/images/emoji/flag_az.png Binary files differnew file mode 100644 index 00000000000..89d3d15fd9f --- /dev/null +++ b/app/assets/images/emoji/flag_az.png diff --git a/app/assets/images/emoji/flag_ba.png b/app/assets/images/emoji/flag_ba.png Binary files differnew file mode 100644 index 00000000000..25fe407e13c --- /dev/null +++ b/app/assets/images/emoji/flag_ba.png diff --git a/app/assets/images/emoji/flag_bb.png b/app/assets/images/emoji/flag_bb.png Binary files differnew file mode 100644 index 00000000000..bccd8c5c9b0 --- /dev/null +++ b/app/assets/images/emoji/flag_bb.png diff --git a/app/assets/images/emoji/flag_bd.png b/app/assets/images/emoji/flag_bd.png Binary files differnew file mode 100644 index 00000000000..b0597a3149b --- /dev/null +++ b/app/assets/images/emoji/flag_bd.png diff --git a/app/assets/images/emoji/flag_be.png b/app/assets/images/emoji/flag_be.png Binary files differnew file mode 100644 index 00000000000..551f086e3c4 --- /dev/null +++ b/app/assets/images/emoji/flag_be.png diff --git a/app/assets/images/emoji/flag_bf.png b/app/assets/images/emoji/flag_bf.png Binary files differnew file mode 100644 index 00000000000..444d4829f94 --- /dev/null +++ b/app/assets/images/emoji/flag_bf.png diff --git a/app/assets/images/emoji/flag_bg.png b/app/assets/images/emoji/flag_bg.png Binary files differnew file mode 100644 index 00000000000..821eee5e170 --- /dev/null +++ b/app/assets/images/emoji/flag_bg.png diff --git a/app/assets/images/emoji/flag_bh.png b/app/assets/images/emoji/flag_bh.png Binary files differnew file mode 100644 index 00000000000..f33724249f0 --- /dev/null +++ b/app/assets/images/emoji/flag_bh.png diff --git a/app/assets/images/emoji/flag_bi.png b/app/assets/images/emoji/flag_bi.png Binary files differnew file mode 100644 index 00000000000..ea20ac93211 --- /dev/null +++ b/app/assets/images/emoji/flag_bi.png diff --git a/app/assets/images/emoji/flag_bj.png b/app/assets/images/emoji/flag_bj.png Binary files differnew file mode 100644 index 00000000000..7cca4f80457 --- /dev/null +++ b/app/assets/images/emoji/flag_bj.png diff --git a/app/assets/images/emoji/flag_bl.png b/app/assets/images/emoji/flag_bl.png Binary files differnew file mode 100644 index 00000000000..1082e78999f --- /dev/null +++ b/app/assets/images/emoji/flag_bl.png diff --git a/app/assets/images/emoji/flag_black.png b/app/assets/images/emoji/flag_black.png Binary files differnew file mode 100644 index 00000000000..0e28d05d5ac --- /dev/null +++ b/app/assets/images/emoji/flag_black.png diff --git a/app/assets/images/emoji/flag_bm.png b/app/assets/images/emoji/flag_bm.png Binary files differnew file mode 100644 index 00000000000..ab8cafdac63 --- /dev/null +++ b/app/assets/images/emoji/flag_bm.png diff --git a/app/assets/images/emoji/flag_bn.png b/app/assets/images/emoji/flag_bn.png Binary files differnew file mode 100644 index 00000000000..caa9329a896 --- /dev/null +++ b/app/assets/images/emoji/flag_bn.png diff --git a/app/assets/images/emoji/flag_bo.png b/app/assets/images/emoji/flag_bo.png Binary files differnew file mode 100644 index 00000000000..98af62b3da7 --- /dev/null +++ b/app/assets/images/emoji/flag_bo.png diff --git a/app/assets/images/emoji/flag_bq.png b/app/assets/images/emoji/flag_bq.png Binary files differnew file mode 100644 index 00000000000..cb978ef9de9 --- /dev/null +++ b/app/assets/images/emoji/flag_bq.png diff --git a/app/assets/images/emoji/flag_br.png b/app/assets/images/emoji/flag_br.png Binary files differnew file mode 100644 index 00000000000..b139366a42b --- /dev/null +++ b/app/assets/images/emoji/flag_br.png diff --git a/app/assets/images/emoji/flag_bs.png b/app/assets/images/emoji/flag_bs.png Binary files differnew file mode 100644 index 00000000000..d36bcd2fb52 --- /dev/null +++ b/app/assets/images/emoji/flag_bs.png diff --git a/app/assets/images/emoji/flag_bt.png b/app/assets/images/emoji/flag_bt.png Binary files differnew file mode 100644 index 00000000000..ed57aa0360e --- /dev/null +++ b/app/assets/images/emoji/flag_bt.png diff --git a/app/assets/images/emoji/flag_bv.png b/app/assets/images/emoji/flag_bv.png Binary files differnew file mode 100644 index 00000000000..5884e648228 --- /dev/null +++ b/app/assets/images/emoji/flag_bv.png diff --git a/app/assets/images/emoji/flag_bw.png b/app/assets/images/emoji/flag_bw.png Binary files differnew file mode 100644 index 00000000000..cb12f34739d --- /dev/null +++ b/app/assets/images/emoji/flag_bw.png diff --git a/app/assets/images/emoji/flag_by.png b/app/assets/images/emoji/flag_by.png Binary files differnew file mode 100644 index 00000000000..859c05beb13 --- /dev/null +++ b/app/assets/images/emoji/flag_by.png diff --git a/app/assets/images/emoji/flag_bz.png b/app/assets/images/emoji/flag_bz.png Binary files differnew file mode 100644 index 00000000000..34761cd03d8 --- /dev/null +++ b/app/assets/images/emoji/flag_bz.png diff --git a/app/assets/images/emoji/flag_ca.png b/app/assets/images/emoji/flag_ca.png Binary files differnew file mode 100644 index 00000000000..7c5b390e85b --- /dev/null +++ b/app/assets/images/emoji/flag_ca.png diff --git a/app/assets/images/emoji/flag_cc.png b/app/assets/images/emoji/flag_cc.png Binary files differnew file mode 100644 index 00000000000..b6555a23d83 --- /dev/null +++ b/app/assets/images/emoji/flag_cc.png diff --git a/app/assets/images/emoji/flag_cd.png b/app/assets/images/emoji/flag_cd.png Binary files differnew file mode 100644 index 00000000000..fa92009771d --- /dev/null +++ b/app/assets/images/emoji/flag_cd.png diff --git a/app/assets/images/emoji/flag_cf.png b/app/assets/images/emoji/flag_cf.png Binary files differnew file mode 100644 index 00000000000..b969ae29ea9 --- /dev/null +++ b/app/assets/images/emoji/flag_cf.png diff --git a/app/assets/images/emoji/flag_cg.png b/app/assets/images/emoji/flag_cg.png Binary files differnew file mode 100644 index 00000000000..3a38a40a95e --- /dev/null +++ b/app/assets/images/emoji/flag_cg.png diff --git a/app/assets/images/emoji/flag_ch.png b/app/assets/images/emoji/flag_ch.png Binary files differnew file mode 100644 index 00000000000..5ff86b8a3b7 --- /dev/null +++ b/app/assets/images/emoji/flag_ch.png diff --git a/app/assets/images/emoji/flag_ci.png b/app/assets/images/emoji/flag_ci.png Binary files differnew file mode 100644 index 00000000000..e3b4d15c7f1 --- /dev/null +++ b/app/assets/images/emoji/flag_ci.png diff --git a/app/assets/images/emoji/flag_ck.png b/app/assets/images/emoji/flag_ck.png Binary files differnew file mode 100644 index 00000000000..b6b53dbc1c4 --- /dev/null +++ b/app/assets/images/emoji/flag_ck.png diff --git a/app/assets/images/emoji/flag_cl.png b/app/assets/images/emoji/flag_cl.png Binary files differnew file mode 100644 index 00000000000..c9390da5499 --- /dev/null +++ b/app/assets/images/emoji/flag_cl.png diff --git a/app/assets/images/emoji/flag_cm.png b/app/assets/images/emoji/flag_cm.png Binary files differnew file mode 100644 index 00000000000..2d3f6ec4518 --- /dev/null +++ b/app/assets/images/emoji/flag_cm.png diff --git a/app/assets/images/emoji/flag_cn.png b/app/assets/images/emoji/flag_cn.png Binary files differnew file mode 100644 index 00000000000..0a7f350a6d2 --- /dev/null +++ b/app/assets/images/emoji/flag_cn.png diff --git a/app/assets/images/emoji/flag_co.png b/app/assets/images/emoji/flag_co.png Binary files differnew file mode 100644 index 00000000000..7e0f5e0dc3c --- /dev/null +++ b/app/assets/images/emoji/flag_co.png diff --git a/app/assets/images/emoji/flag_cp.png b/app/assets/images/emoji/flag_cp.png Binary files differnew file mode 100644 index 00000000000..70c761036bd --- /dev/null +++ b/app/assets/images/emoji/flag_cp.png diff --git a/app/assets/images/emoji/flag_cr.png b/app/assets/images/emoji/flag_cr.png Binary files differnew file mode 100644 index 00000000000..a5fce126515 --- /dev/null +++ b/app/assets/images/emoji/flag_cr.png diff --git a/app/assets/images/emoji/flag_cu.png b/app/assets/images/emoji/flag_cu.png Binary files differnew file mode 100644 index 00000000000..447328f7dfd --- /dev/null +++ b/app/assets/images/emoji/flag_cu.png diff --git a/app/assets/images/emoji/flag_cv.png b/app/assets/images/emoji/flag_cv.png Binary files differnew file mode 100644 index 00000000000..43faf4d64d5 --- /dev/null +++ b/app/assets/images/emoji/flag_cv.png diff --git a/app/assets/images/emoji/flag_cw.png b/app/assets/images/emoji/flag_cw.png Binary files differnew file mode 100644 index 00000000000..eb39e8d0078 --- /dev/null +++ b/app/assets/images/emoji/flag_cw.png diff --git a/app/assets/images/emoji/flag_cx.png b/app/assets/images/emoji/flag_cx.png Binary files differnew file mode 100644 index 00000000000..09d21359f3a --- /dev/null +++ b/app/assets/images/emoji/flag_cx.png diff --git a/app/assets/images/emoji/flag_cy.png b/app/assets/images/emoji/flag_cy.png Binary files differnew file mode 100644 index 00000000000..154a7aa3176 --- /dev/null +++ b/app/assets/images/emoji/flag_cy.png diff --git a/app/assets/images/emoji/flag_cz.png b/app/assets/images/emoji/flag_cz.png Binary files differnew file mode 100644 index 00000000000..9737ca223c7 --- /dev/null +++ b/app/assets/images/emoji/flag_cz.png diff --git a/app/assets/images/emoji/flag_de.png b/app/assets/images/emoji/flag_de.png Binary files differnew file mode 100644 index 00000000000..98ed76b3bab --- /dev/null +++ b/app/assets/images/emoji/flag_de.png diff --git a/app/assets/images/emoji/flag_dg.png b/app/assets/images/emoji/flag_dg.png Binary files differnew file mode 100644 index 00000000000..aae927d14b8 --- /dev/null +++ b/app/assets/images/emoji/flag_dg.png diff --git a/app/assets/images/emoji/flag_dj.png b/app/assets/images/emoji/flag_dj.png Binary files differnew file mode 100644 index 00000000000..73c2a2acbd9 --- /dev/null +++ b/app/assets/images/emoji/flag_dj.png diff --git a/app/assets/images/emoji/flag_dk.png b/app/assets/images/emoji/flag_dk.png Binary files differnew file mode 100644 index 00000000000..e5a60b06256 --- /dev/null +++ b/app/assets/images/emoji/flag_dk.png diff --git a/app/assets/images/emoji/flag_dm.png b/app/assets/images/emoji/flag_dm.png Binary files differnew file mode 100644 index 00000000000..50f8a53981d --- /dev/null +++ b/app/assets/images/emoji/flag_dm.png diff --git a/app/assets/images/emoji/flag_do.png b/app/assets/images/emoji/flag_do.png Binary files differnew file mode 100644 index 00000000000..037a45d7c26 --- /dev/null +++ b/app/assets/images/emoji/flag_do.png diff --git a/app/assets/images/emoji/flag_dz.png b/app/assets/images/emoji/flag_dz.png Binary files differnew file mode 100644 index 00000000000..24945b10f2d --- /dev/null +++ b/app/assets/images/emoji/flag_dz.png diff --git a/app/assets/images/emoji/flag_ea.png b/app/assets/images/emoji/flag_ea.png Binary files differnew file mode 100644 index 00000000000..356ff347838 --- /dev/null +++ b/app/assets/images/emoji/flag_ea.png diff --git a/app/assets/images/emoji/flag_ec.png b/app/assets/images/emoji/flag_ec.png Binary files differnew file mode 100644 index 00000000000..13814594619 --- /dev/null +++ b/app/assets/images/emoji/flag_ec.png diff --git a/app/assets/images/emoji/flag_ee.png b/app/assets/images/emoji/flag_ee.png Binary files differnew file mode 100644 index 00000000000..84f317e7747 --- /dev/null +++ b/app/assets/images/emoji/flag_ee.png diff --git a/app/assets/images/emoji/flag_eg.png b/app/assets/images/emoji/flag_eg.png Binary files differnew file mode 100644 index 00000000000..57786064a95 --- /dev/null +++ b/app/assets/images/emoji/flag_eg.png diff --git a/app/assets/images/emoji/flag_eh.png b/app/assets/images/emoji/flag_eh.png Binary files differnew file mode 100644 index 00000000000..4d7a76687f6 --- /dev/null +++ b/app/assets/images/emoji/flag_eh.png diff --git a/app/assets/images/emoji/flag_er.png b/app/assets/images/emoji/flag_er.png Binary files differnew file mode 100644 index 00000000000..0c3c724c1fb --- /dev/null +++ b/app/assets/images/emoji/flag_er.png diff --git a/app/assets/images/emoji/flag_es.png b/app/assets/images/emoji/flag_es.png Binary files differnew file mode 100644 index 00000000000..3e73597a225 --- /dev/null +++ b/app/assets/images/emoji/flag_es.png diff --git a/app/assets/images/emoji/flag_et.png b/app/assets/images/emoji/flag_et.png Binary files differnew file mode 100644 index 00000000000..9560a134c97 --- /dev/null +++ b/app/assets/images/emoji/flag_et.png diff --git a/app/assets/images/emoji/flag_eu.png b/app/assets/images/emoji/flag_eu.png Binary files differnew file mode 100644 index 00000000000..0b456cf3330 --- /dev/null +++ b/app/assets/images/emoji/flag_eu.png diff --git a/app/assets/images/emoji/flag_fi.png b/app/assets/images/emoji/flag_fi.png Binary files differnew file mode 100644 index 00000000000..ebcf58abfc5 --- /dev/null +++ b/app/assets/images/emoji/flag_fi.png diff --git a/app/assets/images/emoji/flag_fj.png b/app/assets/images/emoji/flag_fj.png Binary files differnew file mode 100644 index 00000000000..9cc8c37fe37 --- /dev/null +++ b/app/assets/images/emoji/flag_fj.png diff --git a/app/assets/images/emoji/flag_fk.png b/app/assets/images/emoji/flag_fk.png Binary files differnew file mode 100644 index 00000000000..61372fd2549 --- /dev/null +++ b/app/assets/images/emoji/flag_fk.png diff --git a/app/assets/images/emoji/flag_fm.png b/app/assets/images/emoji/flag_fm.png Binary files differnew file mode 100644 index 00000000000..0889825c8e1 --- /dev/null +++ b/app/assets/images/emoji/flag_fm.png diff --git a/app/assets/images/emoji/flag_fo.png b/app/assets/images/emoji/flag_fo.png Binary files differnew file mode 100644 index 00000000000..9a4431b0831 --- /dev/null +++ b/app/assets/images/emoji/flag_fo.png diff --git a/app/assets/images/emoji/flag_fr.png b/app/assets/images/emoji/flag_fr.png Binary files differnew file mode 100644 index 00000000000..62ca19c3fcf --- /dev/null +++ b/app/assets/images/emoji/flag_fr.png diff --git a/app/assets/images/emoji/flag_ga.png b/app/assets/images/emoji/flag_ga.png Binary files differnew file mode 100644 index 00000000000..2e68e527a3e --- /dev/null +++ b/app/assets/images/emoji/flag_ga.png diff --git a/app/assets/images/emoji/flag_gb.png b/app/assets/images/emoji/flag_gb.png Binary files differnew file mode 100644 index 00000000000..3ed10f62347 --- /dev/null +++ b/app/assets/images/emoji/flag_gb.png diff --git a/app/assets/images/emoji/flag_gd.png b/app/assets/images/emoji/flag_gd.png Binary files differnew file mode 100644 index 00000000000..527aad33807 --- /dev/null +++ b/app/assets/images/emoji/flag_gd.png diff --git a/app/assets/images/emoji/flag_ge.png b/app/assets/images/emoji/flag_ge.png Binary files differnew file mode 100644 index 00000000000..a75d142480d --- /dev/null +++ b/app/assets/images/emoji/flag_ge.png diff --git a/app/assets/images/emoji/flag_gf.png b/app/assets/images/emoji/flag_gf.png Binary files differnew file mode 100644 index 00000000000..0cf96f327c0 --- /dev/null +++ b/app/assets/images/emoji/flag_gf.png diff --git a/app/assets/images/emoji/flag_gg.png b/app/assets/images/emoji/flag_gg.png Binary files differnew file mode 100644 index 00000000000..970002c7f76 --- /dev/null +++ b/app/assets/images/emoji/flag_gg.png diff --git a/app/assets/images/emoji/flag_gh.png b/app/assets/images/emoji/flag_gh.png Binary files differnew file mode 100644 index 00000000000..f31b5eb7b45 --- /dev/null +++ b/app/assets/images/emoji/flag_gh.png diff --git a/app/assets/images/emoji/flag_gi.png b/app/assets/images/emoji/flag_gi.png Binary files differnew file mode 100644 index 00000000000..e554a2a1d0c --- /dev/null +++ b/app/assets/images/emoji/flag_gi.png diff --git a/app/assets/images/emoji/flag_gl.png b/app/assets/images/emoji/flag_gl.png Binary files differnew file mode 100644 index 00000000000..2e795dd4e33 --- /dev/null +++ b/app/assets/images/emoji/flag_gl.png diff --git a/app/assets/images/emoji/flag_gm.png b/app/assets/images/emoji/flag_gm.png Binary files differnew file mode 100644 index 00000000000..bb69c0975a3 --- /dev/null +++ b/app/assets/images/emoji/flag_gm.png diff --git a/app/assets/images/emoji/flag_gn.png b/app/assets/images/emoji/flag_gn.png Binary files differnew file mode 100644 index 00000000000..1981f61dbf5 --- /dev/null +++ b/app/assets/images/emoji/flag_gn.png diff --git a/app/assets/images/emoji/flag_gp.png b/app/assets/images/emoji/flag_gp.png Binary files differnew file mode 100644 index 00000000000..10e42e672bd --- /dev/null +++ b/app/assets/images/emoji/flag_gp.png diff --git a/app/assets/images/emoji/flag_gq.png b/app/assets/images/emoji/flag_gq.png Binary files differnew file mode 100644 index 00000000000..11475e61eeb --- /dev/null +++ b/app/assets/images/emoji/flag_gq.png diff --git a/app/assets/images/emoji/flag_gr.png b/app/assets/images/emoji/flag_gr.png Binary files differnew file mode 100644 index 00000000000..0f6bb1b6b94 --- /dev/null +++ b/app/assets/images/emoji/flag_gr.png diff --git a/app/assets/images/emoji/flag_gs.png b/app/assets/images/emoji/flag_gs.png Binary files differnew file mode 100644 index 00000000000..6fc92780453 --- /dev/null +++ b/app/assets/images/emoji/flag_gs.png diff --git a/app/assets/images/emoji/flag_gt.png b/app/assets/images/emoji/flag_gt.png Binary files differnew file mode 100644 index 00000000000..7213d4139ed --- /dev/null +++ b/app/assets/images/emoji/flag_gt.png diff --git a/app/assets/images/emoji/flag_gu.png b/app/assets/images/emoji/flag_gu.png Binary files differnew file mode 100644 index 00000000000..4027549ca3c --- /dev/null +++ b/app/assets/images/emoji/flag_gu.png diff --git a/app/assets/images/emoji/flag_gw.png b/app/assets/images/emoji/flag_gw.png Binary files differnew file mode 100644 index 00000000000..6357f6225f4 --- /dev/null +++ b/app/assets/images/emoji/flag_gw.png diff --git a/app/assets/images/emoji/flag_gy.png b/app/assets/images/emoji/flag_gy.png Binary files differnew file mode 100644 index 00000000000..746e2fb7e44 --- /dev/null +++ b/app/assets/images/emoji/flag_gy.png diff --git a/app/assets/images/emoji/flag_hk.png b/app/assets/images/emoji/flag_hk.png Binary files differnew file mode 100644 index 00000000000..cf0c7151b56 --- /dev/null +++ b/app/assets/images/emoji/flag_hk.png diff --git a/app/assets/images/emoji/flag_hm.png b/app/assets/images/emoji/flag_hm.png Binary files differnew file mode 100644 index 00000000000..b613509e466 --- /dev/null +++ b/app/assets/images/emoji/flag_hm.png diff --git a/app/assets/images/emoji/flag_hn.png b/app/assets/images/emoji/flag_hn.png Binary files differnew file mode 100644 index 00000000000..402cdcefdf8 --- /dev/null +++ b/app/assets/images/emoji/flag_hn.png diff --git a/app/assets/images/emoji/flag_hr.png b/app/assets/images/emoji/flag_hr.png Binary files differnew file mode 100644 index 00000000000..46f4f06b4f2 --- /dev/null +++ b/app/assets/images/emoji/flag_hr.png diff --git a/app/assets/images/emoji/flag_ht.png b/app/assets/images/emoji/flag_ht.png Binary files differnew file mode 100644 index 00000000000..d8d0c888498 --- /dev/null +++ b/app/assets/images/emoji/flag_ht.png diff --git a/app/assets/images/emoji/flag_hu.png b/app/assets/images/emoji/flag_hu.png Binary files differnew file mode 100644 index 00000000000..a898de636a5 --- /dev/null +++ b/app/assets/images/emoji/flag_hu.png diff --git a/app/assets/images/emoji/flag_ic.png b/app/assets/images/emoji/flag_ic.png Binary files differnew file mode 100644 index 00000000000..69fd990aa95 --- /dev/null +++ b/app/assets/images/emoji/flag_ic.png diff --git a/app/assets/images/emoji/flag_id.png b/app/assets/images/emoji/flag_id.png Binary files differnew file mode 100644 index 00000000000..85b4c063a45 --- /dev/null +++ b/app/assets/images/emoji/flag_id.png diff --git a/app/assets/images/emoji/flag_ie.png b/app/assets/images/emoji/flag_ie.png Binary files differnew file mode 100644 index 00000000000..a28295838cc --- /dev/null +++ b/app/assets/images/emoji/flag_ie.png diff --git a/app/assets/images/emoji/flag_il.png b/app/assets/images/emoji/flag_il.png Binary files differnew file mode 100644 index 00000000000..85c410d45fb --- /dev/null +++ b/app/assets/images/emoji/flag_il.png diff --git a/app/assets/images/emoji/flag_im.png b/app/assets/images/emoji/flag_im.png Binary files differnew file mode 100644 index 00000000000..60a2458e38e --- /dev/null +++ b/app/assets/images/emoji/flag_im.png diff --git a/app/assets/images/emoji/flag_in.png b/app/assets/images/emoji/flag_in.png Binary files differnew file mode 100644 index 00000000000..feccc8952ce --- /dev/null +++ b/app/assets/images/emoji/flag_in.png diff --git a/app/assets/images/emoji/flag_io.png b/app/assets/images/emoji/flag_io.png Binary files differnew file mode 100644 index 00000000000..aae927d14b8 --- /dev/null +++ b/app/assets/images/emoji/flag_io.png diff --git a/app/assets/images/emoji/flag_iq.png b/app/assets/images/emoji/flag_iq.png Binary files differnew file mode 100644 index 00000000000..41fd1db6f86 --- /dev/null +++ b/app/assets/images/emoji/flag_iq.png diff --git a/app/assets/images/emoji/flag_ir.png b/app/assets/images/emoji/flag_ir.png Binary files differnew file mode 100644 index 00000000000..ff7aaf62ba6 --- /dev/null +++ b/app/assets/images/emoji/flag_ir.png diff --git a/app/assets/images/emoji/flag_is.png b/app/assets/images/emoji/flag_is.png Binary files differnew file mode 100644 index 00000000000..ad8d4131dd2 --- /dev/null +++ b/app/assets/images/emoji/flag_is.png diff --git a/app/assets/images/emoji/flag_it.png b/app/assets/images/emoji/flag_it.png Binary files differnew file mode 100644 index 00000000000..f21563ec533 --- /dev/null +++ b/app/assets/images/emoji/flag_it.png diff --git a/app/assets/images/emoji/flag_je.png b/app/assets/images/emoji/flag_je.png Binary files differnew file mode 100644 index 00000000000..198a918f6a4 --- /dev/null +++ b/app/assets/images/emoji/flag_je.png diff --git a/app/assets/images/emoji/flag_jm.png b/app/assets/images/emoji/flag_jm.png Binary files differnew file mode 100644 index 00000000000..f84e4f9e8db --- /dev/null +++ b/app/assets/images/emoji/flag_jm.png diff --git a/app/assets/images/emoji/flag_jo.png b/app/assets/images/emoji/flag_jo.png Binary files differnew file mode 100644 index 00000000000..20bfa147e3e --- /dev/null +++ b/app/assets/images/emoji/flag_jo.png diff --git a/app/assets/images/emoji/flag_jp.png b/app/assets/images/emoji/flag_jp.png Binary files differnew file mode 100644 index 00000000000..8d8838e4708 --- /dev/null +++ b/app/assets/images/emoji/flag_jp.png diff --git a/app/assets/images/emoji/flag_ke.png b/app/assets/images/emoji/flag_ke.png Binary files differnew file mode 100644 index 00000000000..9e417ab3009 --- /dev/null +++ b/app/assets/images/emoji/flag_ke.png diff --git a/app/assets/images/emoji/flag_kg.png b/app/assets/images/emoji/flag_kg.png Binary files differnew file mode 100644 index 00000000000..2f2d848fe58 --- /dev/null +++ b/app/assets/images/emoji/flag_kg.png diff --git a/app/assets/images/emoji/flag_kh.png b/app/assets/images/emoji/flag_kh.png Binary files differnew file mode 100644 index 00000000000..9a2877dd620 --- /dev/null +++ b/app/assets/images/emoji/flag_kh.png diff --git a/app/assets/images/emoji/flag_ki.png b/app/assets/images/emoji/flag_ki.png Binary files differnew file mode 100644 index 00000000000..10e507e3245 --- /dev/null +++ b/app/assets/images/emoji/flag_ki.png diff --git a/app/assets/images/emoji/flag_km.png b/app/assets/images/emoji/flag_km.png Binary files differnew file mode 100644 index 00000000000..bd5a0588e03 --- /dev/null +++ b/app/assets/images/emoji/flag_km.png diff --git a/app/assets/images/emoji/flag_kn.png b/app/assets/images/emoji/flag_kn.png Binary files differnew file mode 100644 index 00000000000..776207c9605 --- /dev/null +++ b/app/assets/images/emoji/flag_kn.png diff --git a/app/assets/images/emoji/flag_kp.png b/app/assets/images/emoji/flag_kp.png Binary files differnew file mode 100644 index 00000000000..6b3fd89eaaa --- /dev/null +++ b/app/assets/images/emoji/flag_kp.png diff --git a/app/assets/images/emoji/flag_kr.png b/app/assets/images/emoji/flag_kr.png Binary files differnew file mode 100644 index 00000000000..833a88116e1 --- /dev/null +++ b/app/assets/images/emoji/flag_kr.png diff --git a/app/assets/images/emoji/flag_kw.png b/app/assets/images/emoji/flag_kw.png Binary files differnew file mode 100644 index 00000000000..4d19bfa6ca7 --- /dev/null +++ b/app/assets/images/emoji/flag_kw.png diff --git a/app/assets/images/emoji/flag_ky.png b/app/assets/images/emoji/flag_ky.png Binary files differnew file mode 100644 index 00000000000..40daa4da597 --- /dev/null +++ b/app/assets/images/emoji/flag_ky.png diff --git a/app/assets/images/emoji/flag_kz.png b/app/assets/images/emoji/flag_kz.png Binary files differnew file mode 100644 index 00000000000..2f97a8fd3c6 --- /dev/null +++ b/app/assets/images/emoji/flag_kz.png diff --git a/app/assets/images/emoji/flag_la.png b/app/assets/images/emoji/flag_la.png Binary files differnew file mode 100644 index 00000000000..4d4179f34f6 --- /dev/null +++ b/app/assets/images/emoji/flag_la.png diff --git a/app/assets/images/emoji/flag_lb.png b/app/assets/images/emoji/flag_lb.png Binary files differnew file mode 100644 index 00000000000..3d594467011 --- /dev/null +++ b/app/assets/images/emoji/flag_lb.png diff --git a/app/assets/images/emoji/flag_lc.png b/app/assets/images/emoji/flag_lc.png Binary files differnew file mode 100644 index 00000000000..45547b1e439 --- /dev/null +++ b/app/assets/images/emoji/flag_lc.png diff --git a/app/assets/images/emoji/flag_li.png b/app/assets/images/emoji/flag_li.png Binary files differnew file mode 100644 index 00000000000..0eafa6a2215 --- /dev/null +++ b/app/assets/images/emoji/flag_li.png diff --git a/app/assets/images/emoji/flag_lk.png b/app/assets/images/emoji/flag_lk.png Binary files differnew file mode 100644 index 00000000000..ab4fe10c40c --- /dev/null +++ b/app/assets/images/emoji/flag_lk.png diff --git a/app/assets/images/emoji/flag_lr.png b/app/assets/images/emoji/flag_lr.png Binary files differnew file mode 100644 index 00000000000..f66f267fea2 --- /dev/null +++ b/app/assets/images/emoji/flag_lr.png diff --git a/app/assets/images/emoji/flag_ls.png b/app/assets/images/emoji/flag_ls.png Binary files differnew file mode 100644 index 00000000000..24745631e3c --- /dev/null +++ b/app/assets/images/emoji/flag_ls.png diff --git a/app/assets/images/emoji/flag_lt.png b/app/assets/images/emoji/flag_lt.png Binary files differnew file mode 100644 index 00000000000..d644b56d62a --- /dev/null +++ b/app/assets/images/emoji/flag_lt.png diff --git a/app/assets/images/emoji/flag_lu.png b/app/assets/images/emoji/flag_lu.png Binary files differnew file mode 100644 index 00000000000..a2df9c92994 --- /dev/null +++ b/app/assets/images/emoji/flag_lu.png diff --git a/app/assets/images/emoji/flag_lv.png b/app/assets/images/emoji/flag_lv.png Binary files differnew file mode 100644 index 00000000000..ae680d5f0e3 --- /dev/null +++ b/app/assets/images/emoji/flag_lv.png diff --git a/app/assets/images/emoji/flag_ly.png b/app/assets/images/emoji/flag_ly.png Binary files differnew file mode 100644 index 00000000000..f6e77b0f3ba --- /dev/null +++ b/app/assets/images/emoji/flag_ly.png diff --git a/app/assets/images/emoji/flag_ma.png b/app/assets/images/emoji/flag_ma.png Binary files differnew file mode 100644 index 00000000000..c4a056722cd --- /dev/null +++ b/app/assets/images/emoji/flag_ma.png diff --git a/app/assets/images/emoji/flag_mc.png b/app/assets/images/emoji/flag_mc.png Binary files differnew file mode 100644 index 00000000000..d479eab98cb --- /dev/null +++ b/app/assets/images/emoji/flag_mc.png diff --git a/app/assets/images/emoji/flag_md.png b/app/assets/images/emoji/flag_md.png Binary files differnew file mode 100644 index 00000000000..a7a72539872 --- /dev/null +++ b/app/assets/images/emoji/flag_md.png diff --git a/app/assets/images/emoji/flag_me.png b/app/assets/images/emoji/flag_me.png Binary files differnew file mode 100644 index 00000000000..7c771e7e120 --- /dev/null +++ b/app/assets/images/emoji/flag_me.png diff --git a/app/assets/images/emoji/flag_mf.png b/app/assets/images/emoji/flag_mf.png Binary files differnew file mode 100644 index 00000000000..70c761036bd --- /dev/null +++ b/app/assets/images/emoji/flag_mf.png diff --git a/app/assets/images/emoji/flag_mg.png b/app/assets/images/emoji/flag_mg.png Binary files differnew file mode 100644 index 00000000000..2f3ccdda76f --- /dev/null +++ b/app/assets/images/emoji/flag_mg.png diff --git a/app/assets/images/emoji/flag_mh.png b/app/assets/images/emoji/flag_mh.png Binary files differnew file mode 100644 index 00000000000..598016481c1 --- /dev/null +++ b/app/assets/images/emoji/flag_mh.png diff --git a/app/assets/images/emoji/flag_mk.png b/app/assets/images/emoji/flag_mk.png Binary files differnew file mode 100644 index 00000000000..7ba775ee75c --- /dev/null +++ b/app/assets/images/emoji/flag_mk.png diff --git a/app/assets/images/emoji/flag_ml.png b/app/assets/images/emoji/flag_ml.png Binary files differnew file mode 100644 index 00000000000..68343785468 --- /dev/null +++ b/app/assets/images/emoji/flag_ml.png diff --git a/app/assets/images/emoji/flag_mm.png b/app/assets/images/emoji/flag_mm.png Binary files differnew file mode 100644 index 00000000000..37dc7d71591 --- /dev/null +++ b/app/assets/images/emoji/flag_mm.png diff --git a/app/assets/images/emoji/flag_mn.png b/app/assets/images/emoji/flag_mn.png Binary files differnew file mode 100644 index 00000000000..1f146bbcd1a --- /dev/null +++ b/app/assets/images/emoji/flag_mn.png diff --git a/app/assets/images/emoji/flag_mo.png b/app/assets/images/emoji/flag_mo.png Binary files differnew file mode 100644 index 00000000000..7edde31f64b --- /dev/null +++ b/app/assets/images/emoji/flag_mo.png diff --git a/app/assets/images/emoji/flag_mp.png b/app/assets/images/emoji/flag_mp.png Binary files differnew file mode 100644 index 00000000000..17ec1c441ed --- /dev/null +++ b/app/assets/images/emoji/flag_mp.png diff --git a/app/assets/images/emoji/flag_mq.png b/app/assets/images/emoji/flag_mq.png Binary files differnew file mode 100644 index 00000000000..1e672dc9087 --- /dev/null +++ b/app/assets/images/emoji/flag_mq.png diff --git a/app/assets/images/emoji/flag_mr.png b/app/assets/images/emoji/flag_mr.png Binary files differnew file mode 100644 index 00000000000..f87de46effe --- /dev/null +++ b/app/assets/images/emoji/flag_mr.png diff --git a/app/assets/images/emoji/flag_ms.png b/app/assets/images/emoji/flag_ms.png Binary files differnew file mode 100644 index 00000000000..480b0d4ebda --- /dev/null +++ b/app/assets/images/emoji/flag_ms.png diff --git a/app/assets/images/emoji/flag_mt.png b/app/assets/images/emoji/flag_mt.png Binary files differnew file mode 100644 index 00000000000..c9e1dbdce82 --- /dev/null +++ b/app/assets/images/emoji/flag_mt.png diff --git a/app/assets/images/emoji/flag_mu.png b/app/assets/images/emoji/flag_mu.png Binary files differnew file mode 100644 index 00000000000..55b33cb7c33 --- /dev/null +++ b/app/assets/images/emoji/flag_mu.png diff --git a/app/assets/images/emoji/flag_mv.png b/app/assets/images/emoji/flag_mv.png Binary files differnew file mode 100644 index 00000000000..ce5867126ae --- /dev/null +++ b/app/assets/images/emoji/flag_mv.png diff --git a/app/assets/images/emoji/flag_mw.png b/app/assets/images/emoji/flag_mw.png Binary files differnew file mode 100644 index 00000000000..003d8548401 --- /dev/null +++ b/app/assets/images/emoji/flag_mw.png diff --git a/app/assets/images/emoji/flag_mx.png b/app/assets/images/emoji/flag_mx.png Binary files differnew file mode 100644 index 00000000000..42572bcd0ba --- /dev/null +++ b/app/assets/images/emoji/flag_mx.png diff --git a/app/assets/images/emoji/flag_my.png b/app/assets/images/emoji/flag_my.png Binary files differnew file mode 100644 index 00000000000..17526c26742 --- /dev/null +++ b/app/assets/images/emoji/flag_my.png diff --git a/app/assets/images/emoji/flag_mz.png b/app/assets/images/emoji/flag_mz.png Binary files differnew file mode 100644 index 00000000000..2352a78e786 --- /dev/null +++ b/app/assets/images/emoji/flag_mz.png diff --git a/app/assets/images/emoji/flag_na.png b/app/assets/images/emoji/flag_na.png Binary files differnew file mode 100644 index 00000000000..ed31c3df04d --- /dev/null +++ b/app/assets/images/emoji/flag_na.png diff --git a/app/assets/images/emoji/flag_nc.png b/app/assets/images/emoji/flag_nc.png Binary files differnew file mode 100644 index 00000000000..90b3afebfa3 --- /dev/null +++ b/app/assets/images/emoji/flag_nc.png diff --git a/app/assets/images/emoji/flag_ne.png b/app/assets/images/emoji/flag_ne.png Binary files differnew file mode 100644 index 00000000000..f98a1173c2a --- /dev/null +++ b/app/assets/images/emoji/flag_ne.png diff --git a/app/assets/images/emoji/flag_nf.png b/app/assets/images/emoji/flag_nf.png Binary files differnew file mode 100644 index 00000000000..9099e767420 --- /dev/null +++ b/app/assets/images/emoji/flag_nf.png diff --git a/app/assets/images/emoji/flag_ng.png b/app/assets/images/emoji/flag_ng.png Binary files differnew file mode 100644 index 00000000000..ea0abeff1a1 --- /dev/null +++ b/app/assets/images/emoji/flag_ng.png diff --git a/app/assets/images/emoji/flag_ni.png b/app/assets/images/emoji/flag_ni.png Binary files differnew file mode 100644 index 00000000000..772920dfa10 --- /dev/null +++ b/app/assets/images/emoji/flag_ni.png diff --git a/app/assets/images/emoji/flag_nl.png b/app/assets/images/emoji/flag_nl.png Binary files differnew file mode 100644 index 00000000000..83a0e817e41 --- /dev/null +++ b/app/assets/images/emoji/flag_nl.png diff --git a/app/assets/images/emoji/flag_no.png b/app/assets/images/emoji/flag_no.png Binary files differnew file mode 100644 index 00000000000..99d3142eb7b --- /dev/null +++ b/app/assets/images/emoji/flag_no.png diff --git a/app/assets/images/emoji/flag_np.png b/app/assets/images/emoji/flag_np.png Binary files differnew file mode 100644 index 00000000000..87425a8dfef --- /dev/null +++ b/app/assets/images/emoji/flag_np.png diff --git a/app/assets/images/emoji/flag_nr.png b/app/assets/images/emoji/flag_nr.png Binary files differnew file mode 100644 index 00000000000..b3e3a5d5621 --- /dev/null +++ b/app/assets/images/emoji/flag_nr.png diff --git a/app/assets/images/emoji/flag_nu.png b/app/assets/images/emoji/flag_nu.png Binary files differnew file mode 100644 index 00000000000..f03614443ee --- /dev/null +++ b/app/assets/images/emoji/flag_nu.png diff --git a/app/assets/images/emoji/flag_nz.png b/app/assets/images/emoji/flag_nz.png Binary files differnew file mode 100644 index 00000000000..a4eeeab9cd9 --- /dev/null +++ b/app/assets/images/emoji/flag_nz.png diff --git a/app/assets/images/emoji/flag_om.png b/app/assets/images/emoji/flag_om.png Binary files differnew file mode 100644 index 00000000000..ea824ba31e7 --- /dev/null +++ b/app/assets/images/emoji/flag_om.png diff --git a/app/assets/images/emoji/flag_pa.png b/app/assets/images/emoji/flag_pa.png Binary files differnew file mode 100644 index 00000000000..c3091d89889 --- /dev/null +++ b/app/assets/images/emoji/flag_pa.png diff --git a/app/assets/images/emoji/flag_pe.png b/app/assets/images/emoji/flag_pe.png Binary files differnew file mode 100644 index 00000000000..39223aa9dbb --- /dev/null +++ b/app/assets/images/emoji/flag_pe.png diff --git a/app/assets/images/emoji/flag_pf.png b/app/assets/images/emoji/flag_pf.png Binary files differnew file mode 100644 index 00000000000..113445f8f6e --- /dev/null +++ b/app/assets/images/emoji/flag_pf.png diff --git a/app/assets/images/emoji/flag_pg.png b/app/assets/images/emoji/flag_pg.png Binary files differnew file mode 100644 index 00000000000..825e9dcb762 --- /dev/null +++ b/app/assets/images/emoji/flag_pg.png diff --git a/app/assets/images/emoji/flag_ph.png b/app/assets/images/emoji/flag_ph.png Binary files differnew file mode 100644 index 00000000000..8260e15bd2c --- /dev/null +++ b/app/assets/images/emoji/flag_ph.png diff --git a/app/assets/images/emoji/flag_pk.png b/app/assets/images/emoji/flag_pk.png Binary files differnew file mode 100644 index 00000000000..a7b6a1c5074 --- /dev/null +++ b/app/assets/images/emoji/flag_pk.png diff --git a/app/assets/images/emoji/flag_pl.png b/app/assets/images/emoji/flag_pl.png Binary files differnew file mode 100644 index 00000000000..19de2edec11 --- /dev/null +++ b/app/assets/images/emoji/flag_pl.png diff --git a/app/assets/images/emoji/flag_pm.png b/app/assets/images/emoji/flag_pm.png Binary files differnew file mode 100644 index 00000000000..2ca60554193 --- /dev/null +++ b/app/assets/images/emoji/flag_pm.png diff --git a/app/assets/images/emoji/flag_pn.png b/app/assets/images/emoji/flag_pn.png Binary files differnew file mode 100644 index 00000000000..f2263b154bc --- /dev/null +++ b/app/assets/images/emoji/flag_pn.png diff --git a/app/assets/images/emoji/flag_pr.png b/app/assets/images/emoji/flag_pr.png Binary files differnew file mode 100644 index 00000000000..d0209cddb79 --- /dev/null +++ b/app/assets/images/emoji/flag_pr.png diff --git a/app/assets/images/emoji/flag_ps.png b/app/assets/images/emoji/flag_ps.png Binary files differnew file mode 100644 index 00000000000..7ccab09778b --- /dev/null +++ b/app/assets/images/emoji/flag_ps.png diff --git a/app/assets/images/emoji/flag_pt.png b/app/assets/images/emoji/flag_pt.png Binary files differnew file mode 100644 index 00000000000..cc93f27c64b --- /dev/null +++ b/app/assets/images/emoji/flag_pt.png diff --git a/app/assets/images/emoji/flag_pw.png b/app/assets/images/emoji/flag_pw.png Binary files differnew file mode 100644 index 00000000000..154b2f12d3c --- /dev/null +++ b/app/assets/images/emoji/flag_pw.png diff --git a/app/assets/images/emoji/flag_py.png b/app/assets/images/emoji/flag_py.png Binary files differnew file mode 100644 index 00000000000..662ad2f6ff1 --- /dev/null +++ b/app/assets/images/emoji/flag_py.png diff --git a/app/assets/images/emoji/flag_qa.png b/app/assets/images/emoji/flag_qa.png Binary files differnew file mode 100644 index 00000000000..a01d8b05cc7 --- /dev/null +++ b/app/assets/images/emoji/flag_qa.png diff --git a/app/assets/images/emoji/flag_re.png b/app/assets/images/emoji/flag_re.png Binary files differnew file mode 100644 index 00000000000..57f2bbe9df8 --- /dev/null +++ b/app/assets/images/emoji/flag_re.png diff --git a/app/assets/images/emoji/flag_ro.png b/app/assets/images/emoji/flag_ro.png Binary files differnew file mode 100644 index 00000000000..3e48c447706 --- /dev/null +++ b/app/assets/images/emoji/flag_ro.png diff --git a/app/assets/images/emoji/flag_rs.png b/app/assets/images/emoji/flag_rs.png Binary files differnew file mode 100644 index 00000000000..9df6c9a5235 --- /dev/null +++ b/app/assets/images/emoji/flag_rs.png diff --git a/app/assets/images/emoji/flag_ru.png b/app/assets/images/emoji/flag_ru.png Binary files differnew file mode 100644 index 00000000000..e50c9db90e7 --- /dev/null +++ b/app/assets/images/emoji/flag_ru.png diff --git a/app/assets/images/emoji/flag_rw.png b/app/assets/images/emoji/flag_rw.png Binary files differnew file mode 100644 index 00000000000..c238c874e1d --- /dev/null +++ b/app/assets/images/emoji/flag_rw.png diff --git a/app/assets/images/emoji/flag_sa.png b/app/assets/images/emoji/flag_sa.png Binary files differnew file mode 100644 index 00000000000..4941be7d198 --- /dev/null +++ b/app/assets/images/emoji/flag_sa.png diff --git a/app/assets/images/emoji/flag_sb.png b/app/assets/images/emoji/flag_sb.png Binary files differnew file mode 100644 index 00000000000..7d8f1ac6130 --- /dev/null +++ b/app/assets/images/emoji/flag_sb.png diff --git a/app/assets/images/emoji/flag_sc.png b/app/assets/images/emoji/flag_sc.png Binary files differnew file mode 100644 index 00000000000..6ae4d90765e --- /dev/null +++ b/app/assets/images/emoji/flag_sc.png diff --git a/app/assets/images/emoji/flag_sd.png b/app/assets/images/emoji/flag_sd.png Binary files differnew file mode 100644 index 00000000000..963be1b36fb --- /dev/null +++ b/app/assets/images/emoji/flag_sd.png diff --git a/app/assets/images/emoji/flag_se.png b/app/assets/images/emoji/flag_se.png Binary files differnew file mode 100644 index 00000000000..fc0d0e0ce89 --- /dev/null +++ b/app/assets/images/emoji/flag_se.png diff --git a/app/assets/images/emoji/flag_sg.png b/app/assets/images/emoji/flag_sg.png Binary files differnew file mode 100644 index 00000000000..de3c7737c42 --- /dev/null +++ b/app/assets/images/emoji/flag_sg.png diff --git a/app/assets/images/emoji/flag_sh.png b/app/assets/images/emoji/flag_sh.png Binary files differnew file mode 100644 index 00000000000..40cd9e44e96 --- /dev/null +++ b/app/assets/images/emoji/flag_sh.png diff --git a/app/assets/images/emoji/flag_si.png b/app/assets/images/emoji/flag_si.png Binary files differnew file mode 100644 index 00000000000..e308999dba2 --- /dev/null +++ b/app/assets/images/emoji/flag_si.png diff --git a/app/assets/images/emoji/flag_sj.png b/app/assets/images/emoji/flag_sj.png Binary files differnew file mode 100644 index 00000000000..5884e648228 --- /dev/null +++ b/app/assets/images/emoji/flag_sj.png diff --git a/app/assets/images/emoji/flag_sk.png b/app/assets/images/emoji/flag_sk.png Binary files differnew file mode 100644 index 00000000000..4259d0e1418 --- /dev/null +++ b/app/assets/images/emoji/flag_sk.png diff --git a/app/assets/images/emoji/flag_sl.png b/app/assets/images/emoji/flag_sl.png Binary files differnew file mode 100644 index 00000000000..d2cc68830ab --- /dev/null +++ b/app/assets/images/emoji/flag_sl.png diff --git a/app/assets/images/emoji/flag_sm.png b/app/assets/images/emoji/flag_sm.png Binary files differnew file mode 100644 index 00000000000..03b8708754e --- /dev/null +++ b/app/assets/images/emoji/flag_sm.png diff --git a/app/assets/images/emoji/flag_sn.png b/app/assets/images/emoji/flag_sn.png Binary files differnew file mode 100644 index 00000000000..5368bbe93df --- /dev/null +++ b/app/assets/images/emoji/flag_sn.png diff --git a/app/assets/images/emoji/flag_so.png b/app/assets/images/emoji/flag_so.png Binary files differnew file mode 100644 index 00000000000..68a0597365a --- /dev/null +++ b/app/assets/images/emoji/flag_so.png diff --git a/app/assets/images/emoji/flag_sr.png b/app/assets/images/emoji/flag_sr.png Binary files differnew file mode 100644 index 00000000000..d3251327035 --- /dev/null +++ b/app/assets/images/emoji/flag_sr.png diff --git a/app/assets/images/emoji/flag_ss.png b/app/assets/images/emoji/flag_ss.png Binary files differnew file mode 100644 index 00000000000..122977e798f --- /dev/null +++ b/app/assets/images/emoji/flag_ss.png diff --git a/app/assets/images/emoji/flag_st.png b/app/assets/images/emoji/flag_st.png Binary files differnew file mode 100644 index 00000000000..f83a863d612 --- /dev/null +++ b/app/assets/images/emoji/flag_st.png diff --git a/app/assets/images/emoji/flag_sv.png b/app/assets/images/emoji/flag_sv.png Binary files differnew file mode 100644 index 00000000000..efb83e2f253 --- /dev/null +++ b/app/assets/images/emoji/flag_sv.png diff --git a/app/assets/images/emoji/flag_sx.png b/app/assets/images/emoji/flag_sx.png Binary files differnew file mode 100644 index 00000000000..94b760fbedf --- /dev/null +++ b/app/assets/images/emoji/flag_sx.png diff --git a/app/assets/images/emoji/flag_sy.png b/app/assets/images/emoji/flag_sy.png Binary files differnew file mode 100644 index 00000000000..09a8ee8f78c --- /dev/null +++ b/app/assets/images/emoji/flag_sy.png diff --git a/app/assets/images/emoji/flag_sz.png b/app/assets/images/emoji/flag_sz.png Binary files differnew file mode 100644 index 00000000000..f74e82ea1fd --- /dev/null +++ b/app/assets/images/emoji/flag_sz.png diff --git a/app/assets/images/emoji/flag_ta.png b/app/assets/images/emoji/flag_ta.png Binary files differnew file mode 100644 index 00000000000..b44283e90e2 --- /dev/null +++ b/app/assets/images/emoji/flag_ta.png diff --git a/app/assets/images/emoji/flag_tc.png b/app/assets/images/emoji/flag_tc.png Binary files differnew file mode 100644 index 00000000000..156b33d1ba6 --- /dev/null +++ b/app/assets/images/emoji/flag_tc.png diff --git a/app/assets/images/emoji/flag_td.png b/app/assets/images/emoji/flag_td.png Binary files differnew file mode 100644 index 00000000000..ebe7f592828 --- /dev/null +++ b/app/assets/images/emoji/flag_td.png diff --git a/app/assets/images/emoji/flag_tf.png b/app/assets/images/emoji/flag_tf.png Binary files differnew file mode 100644 index 00000000000..a1a3ad68ee2 --- /dev/null +++ b/app/assets/images/emoji/flag_tf.png diff --git a/app/assets/images/emoji/flag_tg.png b/app/assets/images/emoji/flag_tg.png Binary files differnew file mode 100644 index 00000000000..826b73c9ac5 --- /dev/null +++ b/app/assets/images/emoji/flag_tg.png diff --git a/app/assets/images/emoji/flag_th.png b/app/assets/images/emoji/flag_th.png Binary files differnew file mode 100644 index 00000000000..93ff542c5a6 --- /dev/null +++ b/app/assets/images/emoji/flag_th.png diff --git a/app/assets/images/emoji/flag_tj.png b/app/assets/images/emoji/flag_tj.png Binary files differnew file mode 100644 index 00000000000..7a8a0b6190a --- /dev/null +++ b/app/assets/images/emoji/flag_tj.png diff --git a/app/assets/images/emoji/flag_tk.png b/app/assets/images/emoji/flag_tk.png Binary files differnew file mode 100644 index 00000000000..2fa5a21b1bb --- /dev/null +++ b/app/assets/images/emoji/flag_tk.png diff --git a/app/assets/images/emoji/flag_tl.png b/app/assets/images/emoji/flag_tl.png Binary files differnew file mode 100644 index 00000000000..5b120eccc6f --- /dev/null +++ b/app/assets/images/emoji/flag_tl.png diff --git a/app/assets/images/emoji/flag_tm.png b/app/assets/images/emoji/flag_tm.png Binary files differnew file mode 100644 index 00000000000..c3c4f532302 --- /dev/null +++ b/app/assets/images/emoji/flag_tm.png diff --git a/app/assets/images/emoji/flag_tn.png b/app/assets/images/emoji/flag_tn.png Binary files differnew file mode 100644 index 00000000000..58ef161229f --- /dev/null +++ b/app/assets/images/emoji/flag_tn.png diff --git a/app/assets/images/emoji/flag_to.png b/app/assets/images/emoji/flag_to.png Binary files differnew file mode 100644 index 00000000000..1ffa7bb9d19 --- /dev/null +++ b/app/assets/images/emoji/flag_to.png diff --git a/app/assets/images/emoji/flag_tr.png b/app/assets/images/emoji/flag_tr.png Binary files differnew file mode 100644 index 00000000000..325251fae88 --- /dev/null +++ b/app/assets/images/emoji/flag_tr.png diff --git a/app/assets/images/emoji/flag_tt.png b/app/assets/images/emoji/flag_tt.png Binary files differnew file mode 100644 index 00000000000..ed3bb39a300 --- /dev/null +++ b/app/assets/images/emoji/flag_tt.png diff --git a/app/assets/images/emoji/flag_tv.png b/app/assets/images/emoji/flag_tv.png Binary files differnew file mode 100644 index 00000000000..e82c65c7bb9 --- /dev/null +++ b/app/assets/images/emoji/flag_tv.png diff --git a/app/assets/images/emoji/flag_tw.png b/app/assets/images/emoji/flag_tw.png Binary files differnew file mode 100644 index 00000000000..3a8f00b5928 --- /dev/null +++ b/app/assets/images/emoji/flag_tw.png diff --git a/app/assets/images/emoji/flag_tz.png b/app/assets/images/emoji/flag_tz.png Binary files differnew file mode 100644 index 00000000000..2a020853d4e --- /dev/null +++ b/app/assets/images/emoji/flag_tz.png diff --git a/app/assets/images/emoji/flag_ua.png b/app/assets/images/emoji/flag_ua.png Binary files differnew file mode 100644 index 00000000000..cd84d1bbd36 --- /dev/null +++ b/app/assets/images/emoji/flag_ua.png diff --git a/app/assets/images/emoji/flag_ug.png b/app/assets/images/emoji/flag_ug.png Binary files differnew file mode 100644 index 00000000000..dc97690eb55 --- /dev/null +++ b/app/assets/images/emoji/flag_ug.png diff --git a/app/assets/images/emoji/flag_um.png b/app/assets/images/emoji/flag_um.png Binary files differnew file mode 100644 index 00000000000..4a7ee3cdf13 --- /dev/null +++ b/app/assets/images/emoji/flag_um.png diff --git a/app/assets/images/emoji/flag_us.png b/app/assets/images/emoji/flag_us.png Binary files differnew file mode 100644 index 00000000000..9f730305860 --- /dev/null +++ b/app/assets/images/emoji/flag_us.png diff --git a/app/assets/images/emoji/flag_uy.png b/app/assets/images/emoji/flag_uy.png Binary files differnew file mode 100644 index 00000000000..b8002a697a6 --- /dev/null +++ b/app/assets/images/emoji/flag_uy.png diff --git a/app/assets/images/emoji/flag_uz.png b/app/assets/images/emoji/flag_uz.png Binary files differnew file mode 100644 index 00000000000..d56ca9bc424 --- /dev/null +++ b/app/assets/images/emoji/flag_uz.png diff --git a/app/assets/images/emoji/flag_va.png b/app/assets/images/emoji/flag_va.png Binary files differnew file mode 100644 index 00000000000..ddaf5e3141b --- /dev/null +++ b/app/assets/images/emoji/flag_va.png diff --git a/app/assets/images/emoji/flag_vc.png b/app/assets/images/emoji/flag_vc.png Binary files differnew file mode 100644 index 00000000000..43703c62a71 --- /dev/null +++ b/app/assets/images/emoji/flag_vc.png diff --git a/app/assets/images/emoji/flag_ve.png b/app/assets/images/emoji/flag_ve.png Binary files differnew file mode 100644 index 00000000000..1b62796824e --- /dev/null +++ b/app/assets/images/emoji/flag_ve.png diff --git a/app/assets/images/emoji/flag_vg.png b/app/assets/images/emoji/flag_vg.png Binary files differnew file mode 100644 index 00000000000..536f780f1c0 --- /dev/null +++ b/app/assets/images/emoji/flag_vg.png diff --git a/app/assets/images/emoji/flag_vi.png b/app/assets/images/emoji/flag_vi.png Binary files differnew file mode 100644 index 00000000000..64102012cfe --- /dev/null +++ b/app/assets/images/emoji/flag_vi.png diff --git a/app/assets/images/emoji/flag_vn.png b/app/assets/images/emoji/flag_vn.png Binary files differnew file mode 100644 index 00000000000..427036046b6 --- /dev/null +++ b/app/assets/images/emoji/flag_vn.png diff --git a/app/assets/images/emoji/flag_vu.png b/app/assets/images/emoji/flag_vu.png Binary files differnew file mode 100644 index 00000000000..706eba44070 --- /dev/null +++ b/app/assets/images/emoji/flag_vu.png diff --git a/app/assets/images/emoji/flag_wf.png b/app/assets/images/emoji/flag_wf.png Binary files differnew file mode 100644 index 00000000000..70c761036bd --- /dev/null +++ b/app/assets/images/emoji/flag_wf.png diff --git a/app/assets/images/emoji/flag_white.png b/app/assets/images/emoji/flag_white.png Binary files differnew file mode 100644 index 00000000000..86d6e96d5e9 --- /dev/null +++ b/app/assets/images/emoji/flag_white.png diff --git a/app/assets/images/emoji/flag_ws.png b/app/assets/images/emoji/flag_ws.png Binary files differnew file mode 100644 index 00000000000..a1ea0703141 --- /dev/null +++ b/app/assets/images/emoji/flag_ws.png diff --git a/app/assets/images/emoji/flag_xk.png b/app/assets/images/emoji/flag_xk.png Binary files differnew file mode 100644 index 00000000000..e587a446632 --- /dev/null +++ b/app/assets/images/emoji/flag_xk.png diff --git a/app/assets/images/emoji/flag_ye.png b/app/assets/images/emoji/flag_ye.png Binary files differnew file mode 100644 index 00000000000..eadfebd5f67 --- /dev/null +++ b/app/assets/images/emoji/flag_ye.png diff --git a/app/assets/images/emoji/flag_yt.png b/app/assets/images/emoji/flag_yt.png Binary files differnew file mode 100644 index 00000000000..c81fa6d886e --- /dev/null +++ b/app/assets/images/emoji/flag_yt.png diff --git a/app/assets/images/emoji/flag_za.png b/app/assets/images/emoji/flag_za.png Binary files differnew file mode 100644 index 00000000000..f397ef5072f --- /dev/null +++ b/app/assets/images/emoji/flag_za.png diff --git a/app/assets/images/emoji/flag_zm.png b/app/assets/images/emoji/flag_zm.png Binary files differnew file mode 100644 index 00000000000..2494a31f662 --- /dev/null +++ b/app/assets/images/emoji/flag_zm.png diff --git a/app/assets/images/emoji/flag_zw.png b/app/assets/images/emoji/flag_zw.png Binary files differnew file mode 100644 index 00000000000..e09b9652be6 --- /dev/null +++ b/app/assets/images/emoji/flag_zw.png diff --git a/app/assets/images/emoji/flags.png b/app/assets/images/emoji/flags.png Binary files differnew file mode 100644 index 00000000000..3b451035a3a --- /dev/null +++ b/app/assets/images/emoji/flags.png diff --git a/app/assets/images/emoji/flashlight.png b/app/assets/images/emoji/flashlight.png Binary files differnew file mode 100644 index 00000000000..eee36c25067 --- /dev/null +++ b/app/assets/images/emoji/flashlight.png diff --git a/app/assets/images/emoji/fleur-de-lis.png b/app/assets/images/emoji/fleur-de-lis.png Binary files differnew file mode 100644 index 00000000000..c9250d27fa7 --- /dev/null +++ b/app/assets/images/emoji/fleur-de-lis.png diff --git a/app/assets/images/emoji/floppy_disk.png b/app/assets/images/emoji/floppy_disk.png Binary files differnew file mode 100644 index 00000000000..072a76d3c13 --- /dev/null +++ b/app/assets/images/emoji/floppy_disk.png diff --git a/app/assets/images/emoji/flower_playing_cards.png b/app/assets/images/emoji/flower_playing_cards.png Binary files differnew file mode 100644 index 00000000000..6766b044d95 --- /dev/null +++ b/app/assets/images/emoji/flower_playing_cards.png diff --git a/app/assets/images/emoji/flushed.png b/app/assets/images/emoji/flushed.png Binary files differnew file mode 100644 index 00000000000..829220bc470 --- /dev/null +++ b/app/assets/images/emoji/flushed.png diff --git a/app/assets/images/emoji/fog.png b/app/assets/images/emoji/fog.png Binary files differnew file mode 100644 index 00000000000..4e73c2de272 --- /dev/null +++ b/app/assets/images/emoji/fog.png diff --git a/app/assets/images/emoji/foggy.png b/app/assets/images/emoji/foggy.png Binary files differnew file mode 100644 index 00000000000..57702d8d3ac --- /dev/null +++ b/app/assets/images/emoji/foggy.png diff --git a/app/assets/images/emoji/football.png b/app/assets/images/emoji/football.png Binary files differnew file mode 100644 index 00000000000..10366f41fce --- /dev/null +++ b/app/assets/images/emoji/football.png diff --git a/app/assets/images/emoji/footprints.png b/app/assets/images/emoji/footprints.png Binary files differnew file mode 100644 index 00000000000..b2673c5a1a8 --- /dev/null +++ b/app/assets/images/emoji/footprints.png diff --git a/app/assets/images/emoji/fork_and_knife.png b/app/assets/images/emoji/fork_and_knife.png Binary files differnew file mode 100644 index 00000000000..09f1feaea1c --- /dev/null +++ b/app/assets/images/emoji/fork_and_knife.png diff --git a/app/assets/images/emoji/fork_knife_plate.png b/app/assets/images/emoji/fork_knife_plate.png Binary files differnew file mode 100644 index 00000000000..7411755f708 --- /dev/null +++ b/app/assets/images/emoji/fork_knife_plate.png diff --git a/app/assets/images/emoji/fountain.png b/app/assets/images/emoji/fountain.png Binary files differnew file mode 100644 index 00000000000..293f5d91c0f --- /dev/null +++ b/app/assets/images/emoji/fountain.png diff --git a/app/assets/images/emoji/four.png b/app/assets/images/emoji/four.png Binary files differnew file mode 100644 index 00000000000..b0e914aac45 --- /dev/null +++ b/app/assets/images/emoji/four.png diff --git a/app/assets/images/emoji/four_leaf_clover.png b/app/assets/images/emoji/four_leaf_clover.png Binary files differnew file mode 100644 index 00000000000..fdedfcc2b4e --- /dev/null +++ b/app/assets/images/emoji/four_leaf_clover.png diff --git a/app/assets/images/emoji/fox.png b/app/assets/images/emoji/fox.png Binary files differnew file mode 100644 index 00000000000..1ab339bf054 --- /dev/null +++ b/app/assets/images/emoji/fox.png diff --git a/app/assets/images/emoji/frame_photo.png b/app/assets/images/emoji/frame_photo.png Binary files differnew file mode 100644 index 00000000000..9fe84607bfd --- /dev/null +++ b/app/assets/images/emoji/frame_photo.png diff --git a/app/assets/images/emoji/free.png b/app/assets/images/emoji/free.png Binary files differnew file mode 100644 index 00000000000..b71956eb48a --- /dev/null +++ b/app/assets/images/emoji/free.png diff --git a/app/assets/images/emoji/french_bread.png b/app/assets/images/emoji/french_bread.png Binary files differnew file mode 100644 index 00000000000..4c2c5639822 --- /dev/null +++ b/app/assets/images/emoji/french_bread.png diff --git a/app/assets/images/emoji/fried_shrimp.png b/app/assets/images/emoji/fried_shrimp.png Binary files differnew file mode 100644 index 00000000000..752ba7f1398 --- /dev/null +++ b/app/assets/images/emoji/fried_shrimp.png diff --git a/app/assets/images/emoji/fries.png b/app/assets/images/emoji/fries.png Binary files differnew file mode 100644 index 00000000000..4e2a4caacef --- /dev/null +++ b/app/assets/images/emoji/fries.png diff --git a/app/assets/images/emoji/frog.png b/app/assets/images/emoji/frog.png Binary files differnew file mode 100644 index 00000000000..8825d1ad577 --- /dev/null +++ b/app/assets/images/emoji/frog.png diff --git a/app/assets/images/emoji/frowning.png b/app/assets/images/emoji/frowning.png Binary files differnew file mode 100644 index 00000000000..43ab6b0a1c1 --- /dev/null +++ b/app/assets/images/emoji/frowning.png diff --git a/app/assets/images/emoji/frowning2.png b/app/assets/images/emoji/frowning2.png Binary files differnew file mode 100644 index 00000000000..6ae71f233b9 --- /dev/null +++ b/app/assets/images/emoji/frowning2.png diff --git a/app/assets/images/emoji/fuelpump.png b/app/assets/images/emoji/fuelpump.png Binary files differnew file mode 100644 index 00000000000..05b18794474 --- /dev/null +++ b/app/assets/images/emoji/fuelpump.png diff --git a/app/assets/images/emoji/full_moon.png b/app/assets/images/emoji/full_moon.png Binary files differnew file mode 100644 index 00000000000..c9a2d6aa7c9 --- /dev/null +++ b/app/assets/images/emoji/full_moon.png diff --git a/app/assets/images/emoji/full_moon_with_face.png b/app/assets/images/emoji/full_moon_with_face.png Binary files differnew file mode 100644 index 00000000000..a5c25bbaf64 --- /dev/null +++ b/app/assets/images/emoji/full_moon_with_face.png diff --git a/app/assets/images/emoji/game_die.png b/app/assets/images/emoji/game_die.png Binary files differnew file mode 100644 index 00000000000..ad3626fe5e5 --- /dev/null +++ b/app/assets/images/emoji/game_die.png diff --git a/app/assets/images/emoji/gear.png b/app/assets/images/emoji/gear.png Binary files differnew file mode 100644 index 00000000000..2a1cc2c0ff4 --- /dev/null +++ b/app/assets/images/emoji/gear.png diff --git a/app/assets/images/emoji/gem.png b/app/assets/images/emoji/gem.png Binary files differnew file mode 100644 index 00000000000..db122d26a19 --- /dev/null +++ b/app/assets/images/emoji/gem.png diff --git a/app/assets/images/emoji/gemini.png b/app/assets/images/emoji/gemini.png Binary files differnew file mode 100644 index 00000000000..1a09698cf00 --- /dev/null +++ b/app/assets/images/emoji/gemini.png diff --git a/app/assets/images/emoji/ghost.png b/app/assets/images/emoji/ghost.png Binary files differnew file mode 100644 index 00000000000..5650bc0ed18 --- /dev/null +++ b/app/assets/images/emoji/ghost.png diff --git a/app/assets/images/emoji/gift.png b/app/assets/images/emoji/gift.png Binary files differnew file mode 100644 index 00000000000..844e2164560 --- /dev/null +++ b/app/assets/images/emoji/gift.png diff --git a/app/assets/images/emoji/gift_heart.png b/app/assets/images/emoji/gift_heart.png Binary files differnew file mode 100644 index 00000000000..902ceafe4d1 --- /dev/null +++ b/app/assets/images/emoji/gift_heart.png diff --git a/app/assets/images/emoji/girl.png b/app/assets/images/emoji/girl.png Binary files differnew file mode 100644 index 00000000000..dc1d4d08b39 --- /dev/null +++ b/app/assets/images/emoji/girl.png diff --git a/app/assets/images/emoji/girl_tone1.png b/app/assets/images/emoji/girl_tone1.png Binary files differnew file mode 100644 index 00000000000..bb667e88651 --- /dev/null +++ b/app/assets/images/emoji/girl_tone1.png diff --git a/app/assets/images/emoji/girl_tone2.png b/app/assets/images/emoji/girl_tone2.png Binary files differnew file mode 100644 index 00000000000..a59ed4a3f0d --- /dev/null +++ b/app/assets/images/emoji/girl_tone2.png diff --git a/app/assets/images/emoji/girl_tone3.png b/app/assets/images/emoji/girl_tone3.png Binary files differnew file mode 100644 index 00000000000..517e7f2a7b0 --- /dev/null +++ b/app/assets/images/emoji/girl_tone3.png diff --git a/app/assets/images/emoji/girl_tone4.png b/app/assets/images/emoji/girl_tone4.png Binary files differnew file mode 100644 index 00000000000..542d96c8487 --- /dev/null +++ b/app/assets/images/emoji/girl_tone4.png diff --git a/app/assets/images/emoji/girl_tone5.png b/app/assets/images/emoji/girl_tone5.png Binary files differnew file mode 100644 index 00000000000..66b7c28c2df --- /dev/null +++ b/app/assets/images/emoji/girl_tone5.png diff --git a/app/assets/images/emoji/globe_with_meridians.png b/app/assets/images/emoji/globe_with_meridians.png Binary files differnew file mode 100644 index 00000000000..82450c1a4ba --- /dev/null +++ b/app/assets/images/emoji/globe_with_meridians.png diff --git a/app/assets/images/emoji/goal.png b/app/assets/images/emoji/goal.png Binary files differnew file mode 100644 index 00000000000..df3a53da0fb --- /dev/null +++ b/app/assets/images/emoji/goal.png diff --git a/app/assets/images/emoji/goat.png b/app/assets/images/emoji/goat.png Binary files differnew file mode 100644 index 00000000000..f9d9e38a128 --- /dev/null +++ b/app/assets/images/emoji/goat.png diff --git a/app/assets/images/emoji/golf.png b/app/assets/images/emoji/golf.png Binary files differnew file mode 100644 index 00000000000..f65a21d8a46 --- /dev/null +++ b/app/assets/images/emoji/golf.png diff --git a/app/assets/images/emoji/golfer.png b/app/assets/images/emoji/golfer.png Binary files differnew file mode 100644 index 00000000000..39c552de86d --- /dev/null +++ b/app/assets/images/emoji/golfer.png diff --git a/app/assets/images/emoji/gorilla.png b/app/assets/images/emoji/gorilla.png Binary files differnew file mode 100644 index 00000000000..acc51e13622 --- /dev/null +++ b/app/assets/images/emoji/gorilla.png diff --git a/app/assets/images/emoji/grapes.png b/app/assets/images/emoji/grapes.png Binary files differnew file mode 100644 index 00000000000..30d22218896 --- /dev/null +++ b/app/assets/images/emoji/grapes.png diff --git a/app/assets/images/emoji/green_apple.png b/app/assets/images/emoji/green_apple.png Binary files differnew file mode 100644 index 00000000000..5fd51bd3915 --- /dev/null +++ b/app/assets/images/emoji/green_apple.png diff --git a/app/assets/images/emoji/green_book.png b/app/assets/images/emoji/green_book.png Binary files differnew file mode 100644 index 00000000000..e5e411cf3b5 --- /dev/null +++ b/app/assets/images/emoji/green_book.png diff --git a/app/assets/images/emoji/green_heart.png b/app/assets/images/emoji/green_heart.png Binary files differnew file mode 100644 index 00000000000..c52d60a58be --- /dev/null +++ b/app/assets/images/emoji/green_heart.png diff --git a/app/assets/images/emoji/grey_exclamation.png b/app/assets/images/emoji/grey_exclamation.png Binary files differnew file mode 100644 index 00000000000..9b64da8bf7f --- /dev/null +++ b/app/assets/images/emoji/grey_exclamation.png diff --git a/app/assets/images/emoji/grey_question.png b/app/assets/images/emoji/grey_question.png Binary files differnew file mode 100644 index 00000000000..6e7824c75f6 --- /dev/null +++ b/app/assets/images/emoji/grey_question.png diff --git a/app/assets/images/emoji/grimacing.png b/app/assets/images/emoji/grimacing.png Binary files differnew file mode 100644 index 00000000000..871b2f071c9 --- /dev/null +++ b/app/assets/images/emoji/grimacing.png diff --git a/app/assets/images/emoji/grin.png b/app/assets/images/emoji/grin.png Binary files differnew file mode 100644 index 00000000000..418d94c811b --- /dev/null +++ b/app/assets/images/emoji/grin.png diff --git a/app/assets/images/emoji/grinning.png b/app/assets/images/emoji/grinning.png Binary files differnew file mode 100644 index 00000000000..3e8e0dab78c --- /dev/null +++ b/app/assets/images/emoji/grinning.png diff --git a/app/assets/images/emoji/guardsman.png b/app/assets/images/emoji/guardsman.png Binary files differnew file mode 100644 index 00000000000..8d7ab3c473c --- /dev/null +++ b/app/assets/images/emoji/guardsman.png diff --git a/app/assets/images/emoji/guardsman_tone1.png b/app/assets/images/emoji/guardsman_tone1.png Binary files differnew file mode 100644 index 00000000000..cea9ba27468 --- /dev/null +++ b/app/assets/images/emoji/guardsman_tone1.png diff --git a/app/assets/images/emoji/guardsman_tone2.png b/app/assets/images/emoji/guardsman_tone2.png Binary files differnew file mode 100644 index 00000000000..037464e4028 --- /dev/null +++ b/app/assets/images/emoji/guardsman_tone2.png diff --git a/app/assets/images/emoji/guardsman_tone3.png b/app/assets/images/emoji/guardsman_tone3.png Binary files differnew file mode 100644 index 00000000000..0f6726fbe87 --- /dev/null +++ b/app/assets/images/emoji/guardsman_tone3.png diff --git a/app/assets/images/emoji/guardsman_tone4.png b/app/assets/images/emoji/guardsman_tone4.png Binary files differnew file mode 100644 index 00000000000..85fcf9a3b97 --- /dev/null +++ b/app/assets/images/emoji/guardsman_tone4.png diff --git a/app/assets/images/emoji/guardsman_tone5.png b/app/assets/images/emoji/guardsman_tone5.png Binary files differnew file mode 100644 index 00000000000..e5f9ca7d5a2 --- /dev/null +++ b/app/assets/images/emoji/guardsman_tone5.png diff --git a/app/assets/images/emoji/guitar.png b/app/assets/images/emoji/guitar.png Binary files differnew file mode 100644 index 00000000000..43d752f1e3d --- /dev/null +++ b/app/assets/images/emoji/guitar.png diff --git a/app/assets/images/emoji/gun.png b/app/assets/images/emoji/gun.png Binary files differnew file mode 100644 index 00000000000..89c5c244c7b --- /dev/null +++ b/app/assets/images/emoji/gun.png diff --git a/app/assets/images/emoji/haircut.png b/app/assets/images/emoji/haircut.png Binary files differnew file mode 100644 index 00000000000..91266b12930 --- /dev/null +++ b/app/assets/images/emoji/haircut.png diff --git a/app/assets/images/emoji/haircut_tone1.png b/app/assets/images/emoji/haircut_tone1.png Binary files differnew file mode 100644 index 00000000000..c743b74abeb --- /dev/null +++ b/app/assets/images/emoji/haircut_tone1.png diff --git a/app/assets/images/emoji/haircut_tone2.png b/app/assets/images/emoji/haircut_tone2.png Binary files differnew file mode 100644 index 00000000000..f144f8e55ce --- /dev/null +++ b/app/assets/images/emoji/haircut_tone2.png diff --git a/app/assets/images/emoji/haircut_tone3.png b/app/assets/images/emoji/haircut_tone3.png Binary files differnew file mode 100644 index 00000000000..d5ad19563ac --- /dev/null +++ b/app/assets/images/emoji/haircut_tone3.png diff --git a/app/assets/images/emoji/haircut_tone4.png b/app/assets/images/emoji/haircut_tone4.png Binary files differnew file mode 100644 index 00000000000..244fd3af008 --- /dev/null +++ b/app/assets/images/emoji/haircut_tone4.png diff --git a/app/assets/images/emoji/haircut_tone5.png b/app/assets/images/emoji/haircut_tone5.png Binary files differnew file mode 100644 index 00000000000..20a94a88623 --- /dev/null +++ b/app/assets/images/emoji/haircut_tone5.png diff --git a/app/assets/images/emoji/hamburger.png b/app/assets/images/emoji/hamburger.png Binary files differnew file mode 100644 index 00000000000..3573b28a1fd --- /dev/null +++ b/app/assets/images/emoji/hamburger.png diff --git a/app/assets/images/emoji/hammer.png b/app/assets/images/emoji/hammer.png Binary files differnew file mode 100644 index 00000000000..00736cce47d --- /dev/null +++ b/app/assets/images/emoji/hammer.png diff --git a/app/assets/images/emoji/hammer_pick.png b/app/assets/images/emoji/hammer_pick.png Binary files differnew file mode 100644 index 00000000000..3bee30ec588 --- /dev/null +++ b/app/assets/images/emoji/hammer_pick.png diff --git a/app/assets/images/emoji/hamster.png b/app/assets/images/emoji/hamster.png Binary files differnew file mode 100644 index 00000000000..9a04388e4e7 --- /dev/null +++ b/app/assets/images/emoji/hamster.png diff --git a/app/assets/images/emoji/hand_splayed.png b/app/assets/images/emoji/hand_splayed.png Binary files differnew file mode 100644 index 00000000000..fb5ae8ebb5a --- /dev/null +++ b/app/assets/images/emoji/hand_splayed.png diff --git a/app/assets/images/emoji/hand_splayed_tone1.png b/app/assets/images/emoji/hand_splayed_tone1.png Binary files differnew file mode 100644 index 00000000000..a7888e6bd23 --- /dev/null +++ b/app/assets/images/emoji/hand_splayed_tone1.png diff --git a/app/assets/images/emoji/hand_splayed_tone2.png b/app/assets/images/emoji/hand_splayed_tone2.png Binary files differnew file mode 100644 index 00000000000..cc10fbc272d --- /dev/null +++ b/app/assets/images/emoji/hand_splayed_tone2.png diff --git a/app/assets/images/emoji/hand_splayed_tone3.png b/app/assets/images/emoji/hand_splayed_tone3.png Binary files differnew file mode 100644 index 00000000000..707236ae8a4 --- /dev/null +++ b/app/assets/images/emoji/hand_splayed_tone3.png diff --git a/app/assets/images/emoji/hand_splayed_tone4.png b/app/assets/images/emoji/hand_splayed_tone4.png Binary files differnew file mode 100644 index 00000000000..1430df9c61f --- /dev/null +++ b/app/assets/images/emoji/hand_splayed_tone4.png diff --git a/app/assets/images/emoji/hand_splayed_tone5.png b/app/assets/images/emoji/hand_splayed_tone5.png Binary files differnew file mode 100644 index 00000000000..80bec971b6b --- /dev/null +++ b/app/assets/images/emoji/hand_splayed_tone5.png diff --git a/app/assets/images/emoji/handbag.png b/app/assets/images/emoji/handbag.png Binary files differnew file mode 100644 index 00000000000..cbf75c5d25e --- /dev/null +++ b/app/assets/images/emoji/handbag.png diff --git a/app/assets/images/emoji/handball.png b/app/assets/images/emoji/handball.png Binary files differnew file mode 100644 index 00000000000..1152f1344c7 --- /dev/null +++ b/app/assets/images/emoji/handball.png diff --git a/app/assets/images/emoji/handball_tone1.png b/app/assets/images/emoji/handball_tone1.png Binary files differnew file mode 100644 index 00000000000..c26cac2df98 --- /dev/null +++ b/app/assets/images/emoji/handball_tone1.png diff --git a/app/assets/images/emoji/handball_tone2.png b/app/assets/images/emoji/handball_tone2.png Binary files differnew file mode 100644 index 00000000000..7baaf95a9a2 --- /dev/null +++ b/app/assets/images/emoji/handball_tone2.png diff --git a/app/assets/images/emoji/handball_tone3.png b/app/assets/images/emoji/handball_tone3.png Binary files differnew file mode 100644 index 00000000000..0e3a37c3d40 --- /dev/null +++ b/app/assets/images/emoji/handball_tone3.png diff --git a/app/assets/images/emoji/handball_tone4.png b/app/assets/images/emoji/handball_tone4.png Binary files differnew file mode 100644 index 00000000000..e1233f38266 --- /dev/null +++ b/app/assets/images/emoji/handball_tone4.png diff --git a/app/assets/images/emoji/handball_tone5.png b/app/assets/images/emoji/handball_tone5.png Binary files differnew file mode 100644 index 00000000000..6b1eb9b64b0 --- /dev/null +++ b/app/assets/images/emoji/handball_tone5.png diff --git a/app/assets/images/emoji/handshake.png b/app/assets/images/emoji/handshake.png Binary files differnew file mode 100644 index 00000000000..c5d35fd8138 --- /dev/null +++ b/app/assets/images/emoji/handshake.png diff --git a/app/assets/images/emoji/handshake_tone1.png b/app/assets/images/emoji/handshake_tone1.png Binary files differnew file mode 100644 index 00000000000..8f8fbb9bdca --- /dev/null +++ b/app/assets/images/emoji/handshake_tone1.png diff --git a/app/assets/images/emoji/handshake_tone2.png b/app/assets/images/emoji/handshake_tone2.png Binary files differnew file mode 100644 index 00000000000..336a77a6d78 --- /dev/null +++ b/app/assets/images/emoji/handshake_tone2.png diff --git a/app/assets/images/emoji/handshake_tone3.png b/app/assets/images/emoji/handshake_tone3.png Binary files differnew file mode 100644 index 00000000000..95f62d4fecd --- /dev/null +++ b/app/assets/images/emoji/handshake_tone3.png diff --git a/app/assets/images/emoji/handshake_tone4.png b/app/assets/images/emoji/handshake_tone4.png Binary files differnew file mode 100644 index 00000000000..2b0a6433886 --- /dev/null +++ b/app/assets/images/emoji/handshake_tone4.png diff --git a/app/assets/images/emoji/handshake_tone5.png b/app/assets/images/emoji/handshake_tone5.png Binary files differnew file mode 100644 index 00000000000..40189ee68e4 --- /dev/null +++ b/app/assets/images/emoji/handshake_tone5.png diff --git a/app/assets/images/emoji/hash.png b/app/assets/images/emoji/hash.png Binary files differnew file mode 100644 index 00000000000..6e26f0070b0 --- /dev/null +++ b/app/assets/images/emoji/hash.png diff --git a/app/assets/images/emoji/hatched_chick.png b/app/assets/images/emoji/hatched_chick.png Binary files differnew file mode 100644 index 00000000000..31dfb511e0e --- /dev/null +++ b/app/assets/images/emoji/hatched_chick.png diff --git a/app/assets/images/emoji/hatching_chick.png b/app/assets/images/emoji/hatching_chick.png Binary files differnew file mode 100644 index 00000000000..c5b0e8f3bcc --- /dev/null +++ b/app/assets/images/emoji/hatching_chick.png diff --git a/app/assets/images/emoji/head_bandage.png b/app/assets/images/emoji/head_bandage.png Binary files differnew file mode 100644 index 00000000000..0be723085e0 --- /dev/null +++ b/app/assets/images/emoji/head_bandage.png diff --git a/app/assets/images/emoji/headphones.png b/app/assets/images/emoji/headphones.png Binary files differnew file mode 100644 index 00000000000..e9fd34041d8 --- /dev/null +++ b/app/assets/images/emoji/headphones.png diff --git a/app/assets/images/emoji/hear_no_evil.png b/app/assets/images/emoji/hear_no_evil.png Binary files differnew file mode 100644 index 00000000000..74b6be0c6c5 --- /dev/null +++ b/app/assets/images/emoji/hear_no_evil.png diff --git a/app/assets/images/emoji/heart.png b/app/assets/images/emoji/heart.png Binary files differnew file mode 100644 index 00000000000..638cb72dc4e --- /dev/null +++ b/app/assets/images/emoji/heart.png diff --git a/app/assets/images/emoji/heart_decoration.png b/app/assets/images/emoji/heart_decoration.png Binary files differnew file mode 100644 index 00000000000..5443f60bc63 --- /dev/null +++ b/app/assets/images/emoji/heart_decoration.png diff --git a/app/assets/images/emoji/heart_exclamation.png b/app/assets/images/emoji/heart_exclamation.png Binary files differnew file mode 100644 index 00000000000..91b520be40b --- /dev/null +++ b/app/assets/images/emoji/heart_exclamation.png diff --git a/app/assets/images/emoji/heart_eyes.png b/app/assets/images/emoji/heart_eyes.png Binary files differnew file mode 100644 index 00000000000..73fbee29d4e --- /dev/null +++ b/app/assets/images/emoji/heart_eyes.png diff --git a/app/assets/images/emoji/heart_eyes_cat.png b/app/assets/images/emoji/heart_eyes_cat.png Binary files differnew file mode 100644 index 00000000000..bc5a833f9a1 --- /dev/null +++ b/app/assets/images/emoji/heart_eyes_cat.png diff --git a/app/assets/images/emoji/heartbeat.png b/app/assets/images/emoji/heartbeat.png Binary files differnew file mode 100644 index 00000000000..0bcf2d1d567 --- /dev/null +++ b/app/assets/images/emoji/heartbeat.png diff --git a/app/assets/images/emoji/heartpulse.png b/app/assets/images/emoji/heartpulse.png Binary files differnew file mode 100644 index 00000000000..d6e694e972f --- /dev/null +++ b/app/assets/images/emoji/heartpulse.png diff --git a/app/assets/images/emoji/hearts.png b/app/assets/images/emoji/hearts.png Binary files differnew file mode 100644 index 00000000000..393c3ed5267 --- /dev/null +++ b/app/assets/images/emoji/hearts.png diff --git a/app/assets/images/emoji/heavy_check_mark.png b/app/assets/images/emoji/heavy_check_mark.png Binary files differnew file mode 100644 index 00000000000..03bd695377e --- /dev/null +++ b/app/assets/images/emoji/heavy_check_mark.png diff --git a/app/assets/images/emoji/heavy_division_sign.png b/app/assets/images/emoji/heavy_division_sign.png Binary files differnew file mode 100644 index 00000000000..df32ab21bea --- /dev/null +++ b/app/assets/images/emoji/heavy_division_sign.png diff --git a/app/assets/images/emoji/heavy_dollar_sign.png b/app/assets/images/emoji/heavy_dollar_sign.png Binary files differnew file mode 100644 index 00000000000..ef2c2e20590 --- /dev/null +++ b/app/assets/images/emoji/heavy_dollar_sign.png diff --git a/app/assets/images/emoji/heavy_minus_sign.png b/app/assets/images/emoji/heavy_minus_sign.png Binary files differnew file mode 100644 index 00000000000..054211caf12 --- /dev/null +++ b/app/assets/images/emoji/heavy_minus_sign.png diff --git a/app/assets/images/emoji/heavy_multiplication_x.png b/app/assets/images/emoji/heavy_multiplication_x.png Binary files differnew file mode 100644 index 00000000000..e47cc1b685d --- /dev/null +++ b/app/assets/images/emoji/heavy_multiplication_x.png diff --git a/app/assets/images/emoji/heavy_plus_sign.png b/app/assets/images/emoji/heavy_plus_sign.png Binary files differnew file mode 100644 index 00000000000..40799798aaf --- /dev/null +++ b/app/assets/images/emoji/heavy_plus_sign.png diff --git a/app/assets/images/emoji/helicopter.png b/app/assets/images/emoji/helicopter.png Binary files differnew file mode 100644 index 00000000000..7ec5f39a51a --- /dev/null +++ b/app/assets/images/emoji/helicopter.png diff --git a/app/assets/images/emoji/helmet_with_cross.png b/app/assets/images/emoji/helmet_with_cross.png Binary files differnew file mode 100644 index 00000000000..7140a676038 --- /dev/null +++ b/app/assets/images/emoji/helmet_with_cross.png diff --git a/app/assets/images/emoji/herb.png b/app/assets/images/emoji/herb.png Binary files differnew file mode 100644 index 00000000000..d984d1562bb --- /dev/null +++ b/app/assets/images/emoji/herb.png diff --git a/app/assets/images/emoji/hibiscus.png b/app/assets/images/emoji/hibiscus.png Binary files differnew file mode 100644 index 00000000000..39dd3524233 --- /dev/null +++ b/app/assets/images/emoji/hibiscus.png diff --git a/app/assets/images/emoji/high_brightness.png b/app/assets/images/emoji/high_brightness.png Binary files differnew file mode 100644 index 00000000000..c41f2d5fd50 --- /dev/null +++ b/app/assets/images/emoji/high_brightness.png diff --git a/app/assets/images/emoji/high_heel.png b/app/assets/images/emoji/high_heel.png Binary files differnew file mode 100644 index 00000000000..b331cbccc9d --- /dev/null +++ b/app/assets/images/emoji/high_heel.png diff --git a/app/assets/images/emoji/hockey.png b/app/assets/images/emoji/hockey.png Binary files differnew file mode 100644 index 00000000000..be94e9cbf73 --- /dev/null +++ b/app/assets/images/emoji/hockey.png diff --git a/app/assets/images/emoji/hole.png b/app/assets/images/emoji/hole.png Binary files differnew file mode 100644 index 00000000000..517d2ae0deb --- /dev/null +++ b/app/assets/images/emoji/hole.png diff --git a/app/assets/images/emoji/homes.png b/app/assets/images/emoji/homes.png Binary files differnew file mode 100644 index 00000000000..6ab4a2a2651 --- /dev/null +++ b/app/assets/images/emoji/homes.png diff --git a/app/assets/images/emoji/honey_pot.png b/app/assets/images/emoji/honey_pot.png Binary files differnew file mode 100644 index 00000000000..9d8f592955e --- /dev/null +++ b/app/assets/images/emoji/honey_pot.png diff --git a/app/assets/images/emoji/horse.png b/app/assets/images/emoji/horse.png Binary files differnew file mode 100644 index 00000000000..7cb1172f4e4 --- /dev/null +++ b/app/assets/images/emoji/horse.png diff --git a/app/assets/images/emoji/horse_racing.png b/app/assets/images/emoji/horse_racing.png Binary files differnew file mode 100644 index 00000000000..addf9edac56 --- /dev/null +++ b/app/assets/images/emoji/horse_racing.png diff --git a/app/assets/images/emoji/horse_racing_tone1.png b/app/assets/images/emoji/horse_racing_tone1.png Binary files differnew file mode 100644 index 00000000000..e9bf4092e98 --- /dev/null +++ b/app/assets/images/emoji/horse_racing_tone1.png diff --git a/app/assets/images/emoji/horse_racing_tone2.png b/app/assets/images/emoji/horse_racing_tone2.png Binary files differnew file mode 100644 index 00000000000..031bbc3d867 --- /dev/null +++ b/app/assets/images/emoji/horse_racing_tone2.png diff --git a/app/assets/images/emoji/horse_racing_tone3.png b/app/assets/images/emoji/horse_racing_tone3.png Binary files differnew file mode 100644 index 00000000000..b40ef891f9b --- /dev/null +++ b/app/assets/images/emoji/horse_racing_tone3.png diff --git a/app/assets/images/emoji/horse_racing_tone4.png b/app/assets/images/emoji/horse_racing_tone4.png Binary files differnew file mode 100644 index 00000000000..e286cb85065 --- /dev/null +++ b/app/assets/images/emoji/horse_racing_tone4.png diff --git a/app/assets/images/emoji/horse_racing_tone5.png b/app/assets/images/emoji/horse_racing_tone5.png Binary files differnew file mode 100644 index 00000000000..453c51c6007 --- /dev/null +++ b/app/assets/images/emoji/horse_racing_tone5.png diff --git a/app/assets/images/emoji/hospital.png b/app/assets/images/emoji/hospital.png Binary files differnew file mode 100644 index 00000000000..1cbce4ae767 --- /dev/null +++ b/app/assets/images/emoji/hospital.png diff --git a/app/assets/images/emoji/hot_pepper.png b/app/assets/images/emoji/hot_pepper.png Binary files differnew file mode 100644 index 00000000000..266675bd577 --- /dev/null +++ b/app/assets/images/emoji/hot_pepper.png diff --git a/app/assets/images/emoji/hotdog.png b/app/assets/images/emoji/hotdog.png Binary files differnew file mode 100644 index 00000000000..3c3354d94cb --- /dev/null +++ b/app/assets/images/emoji/hotdog.png diff --git a/app/assets/images/emoji/hotel.png b/app/assets/images/emoji/hotel.png Binary files differnew file mode 100644 index 00000000000..ea8f4c4979a --- /dev/null +++ b/app/assets/images/emoji/hotel.png diff --git a/app/assets/images/emoji/hotsprings.png b/app/assets/images/emoji/hotsprings.png Binary files differnew file mode 100644 index 00000000000..3d9df2d9475 --- /dev/null +++ b/app/assets/images/emoji/hotsprings.png diff --git a/app/assets/images/emoji/hourglass.png b/app/assets/images/emoji/hourglass.png Binary files differnew file mode 100644 index 00000000000..a5db2d1d3f4 --- /dev/null +++ b/app/assets/images/emoji/hourglass.png diff --git a/app/assets/images/emoji/hourglass_flowing_sand.png b/app/assets/images/emoji/hourglass_flowing_sand.png Binary files differnew file mode 100644 index 00000000000..b93b15ed6d8 --- /dev/null +++ b/app/assets/images/emoji/hourglass_flowing_sand.png diff --git a/app/assets/images/emoji/house.png b/app/assets/images/emoji/house.png Binary files differnew file mode 100644 index 00000000000..01c98a0ba92 --- /dev/null +++ b/app/assets/images/emoji/house.png diff --git a/app/assets/images/emoji/house_abandoned.png b/app/assets/images/emoji/house_abandoned.png Binary files differnew file mode 100644 index 00000000000..c55e81de990 --- /dev/null +++ b/app/assets/images/emoji/house_abandoned.png diff --git a/app/assets/images/emoji/house_with_garden.png b/app/assets/images/emoji/house_with_garden.png Binary files differnew file mode 100644 index 00000000000..0aae41598ef --- /dev/null +++ b/app/assets/images/emoji/house_with_garden.png diff --git a/app/assets/images/emoji/hugging.png b/app/assets/images/emoji/hugging.png Binary files differnew file mode 100644 index 00000000000..5bba6dc6d51 --- /dev/null +++ b/app/assets/images/emoji/hugging.png diff --git a/app/assets/images/emoji/hushed.png b/app/assets/images/emoji/hushed.png Binary files differnew file mode 100644 index 00000000000..cad0e23132e --- /dev/null +++ b/app/assets/images/emoji/hushed.png diff --git a/app/assets/images/emoji/ice_cream.png b/app/assets/images/emoji/ice_cream.png Binary files differnew file mode 100644 index 00000000000..94267b9c434 --- /dev/null +++ b/app/assets/images/emoji/ice_cream.png diff --git a/app/assets/images/emoji/ice_skate.png b/app/assets/images/emoji/ice_skate.png Binary files differnew file mode 100644 index 00000000000..8c449b0c039 --- /dev/null +++ b/app/assets/images/emoji/ice_skate.png diff --git a/app/assets/images/emoji/icecream.png b/app/assets/images/emoji/icecream.png Binary files differnew file mode 100644 index 00000000000..8f6546e31a5 --- /dev/null +++ b/app/assets/images/emoji/icecream.png diff --git a/app/assets/images/emoji/id.png b/app/assets/images/emoji/id.png Binary files differnew file mode 100644 index 00000000000..5bf69bf7ba8 --- /dev/null +++ b/app/assets/images/emoji/id.png diff --git a/app/assets/images/emoji/ideograph_advantage.png b/app/assets/images/emoji/ideograph_advantage.png Binary files differnew file mode 100644 index 00000000000..0c0d589caf0 --- /dev/null +++ b/app/assets/images/emoji/ideograph_advantage.png diff --git a/app/assets/images/emoji/imp.png b/app/assets/images/emoji/imp.png Binary files differnew file mode 100644 index 00000000000..9f9a9605539 --- /dev/null +++ b/app/assets/images/emoji/imp.png diff --git a/app/assets/images/emoji/inbox_tray.png b/app/assets/images/emoji/inbox_tray.png Binary files differnew file mode 100644 index 00000000000..41a6be2b0ee --- /dev/null +++ b/app/assets/images/emoji/inbox_tray.png diff --git a/app/assets/images/emoji/incoming_envelope.png b/app/assets/images/emoji/incoming_envelope.png Binary files differnew file mode 100644 index 00000000000..fd22e88182e --- /dev/null +++ b/app/assets/images/emoji/incoming_envelope.png diff --git a/app/assets/images/emoji/information_desk_person.png b/app/assets/images/emoji/information_desk_person.png Binary files differnew file mode 100644 index 00000000000..55fc6294d25 --- /dev/null +++ b/app/assets/images/emoji/information_desk_person.png diff --git a/app/assets/images/emoji/information_desk_person_tone1.png b/app/assets/images/emoji/information_desk_person_tone1.png Binary files differnew file mode 100644 index 00000000000..3d9e2247940 --- /dev/null +++ b/app/assets/images/emoji/information_desk_person_tone1.png diff --git a/app/assets/images/emoji/information_desk_person_tone2.png b/app/assets/images/emoji/information_desk_person_tone2.png Binary files differnew file mode 100644 index 00000000000..879e8b7966d --- /dev/null +++ b/app/assets/images/emoji/information_desk_person_tone2.png diff --git a/app/assets/images/emoji/information_desk_person_tone3.png b/app/assets/images/emoji/information_desk_person_tone3.png Binary files differnew file mode 100644 index 00000000000..307514eab67 --- /dev/null +++ b/app/assets/images/emoji/information_desk_person_tone3.png diff --git a/app/assets/images/emoji/information_desk_person_tone4.png b/app/assets/images/emoji/information_desk_person_tone4.png Binary files differnew file mode 100644 index 00000000000..297395dcb3f --- /dev/null +++ b/app/assets/images/emoji/information_desk_person_tone4.png diff --git a/app/assets/images/emoji/information_desk_person_tone5.png b/app/assets/images/emoji/information_desk_person_tone5.png Binary files differnew file mode 100644 index 00000000000..26f8f22b28b --- /dev/null +++ b/app/assets/images/emoji/information_desk_person_tone5.png diff --git a/app/assets/images/emoji/information_source.png b/app/assets/images/emoji/information_source.png Binary files differnew file mode 100644 index 00000000000..871f2db9314 --- /dev/null +++ b/app/assets/images/emoji/information_source.png diff --git a/app/assets/images/emoji/innocent.png b/app/assets/images/emoji/innocent.png Binary files differnew file mode 100644 index 00000000000..57f5151124f --- /dev/null +++ b/app/assets/images/emoji/innocent.png diff --git a/app/assets/images/emoji/interrobang.png b/app/assets/images/emoji/interrobang.png Binary files differnew file mode 100644 index 00000000000..509813e9bb2 --- /dev/null +++ b/app/assets/images/emoji/interrobang.png diff --git a/app/assets/images/emoji/iphone.png b/app/assets/images/emoji/iphone.png Binary files differnew file mode 100644 index 00000000000..fd377acf872 --- /dev/null +++ b/app/assets/images/emoji/iphone.png diff --git a/app/assets/images/emoji/island.png b/app/assets/images/emoji/island.png Binary files differnew file mode 100644 index 00000000000..7fd834389b7 --- /dev/null +++ b/app/assets/images/emoji/island.png diff --git a/app/assets/images/emoji/izakaya_lantern.png b/app/assets/images/emoji/izakaya_lantern.png Binary files differnew file mode 100644 index 00000000000..dfd933f6f36 --- /dev/null +++ b/app/assets/images/emoji/izakaya_lantern.png diff --git a/app/assets/images/emoji/jack_o_lantern.png b/app/assets/images/emoji/jack_o_lantern.png Binary files differnew file mode 100644 index 00000000000..44c3fc0aec9 --- /dev/null +++ b/app/assets/images/emoji/jack_o_lantern.png diff --git a/app/assets/images/emoji/japan.png b/app/assets/images/emoji/japan.png Binary files differnew file mode 100644 index 00000000000..d86d0a59e12 --- /dev/null +++ b/app/assets/images/emoji/japan.png diff --git a/app/assets/images/emoji/japanese_castle.png b/app/assets/images/emoji/japanese_castle.png Binary files differnew file mode 100644 index 00000000000..64b4e33a1ae --- /dev/null +++ b/app/assets/images/emoji/japanese_castle.png diff --git a/app/assets/images/emoji/japanese_goblin.png b/app/assets/images/emoji/japanese_goblin.png Binary files differnew file mode 100644 index 00000000000..515c6a2250e --- /dev/null +++ b/app/assets/images/emoji/japanese_goblin.png diff --git a/app/assets/images/emoji/japanese_ogre.png b/app/assets/images/emoji/japanese_ogre.png Binary files differnew file mode 100644 index 00000000000..fe8670fdaf1 --- /dev/null +++ b/app/assets/images/emoji/japanese_ogre.png diff --git a/app/assets/images/emoji/jeans.png b/app/assets/images/emoji/jeans.png Binary files differnew file mode 100644 index 00000000000..2a6869d674c --- /dev/null +++ b/app/assets/images/emoji/jeans.png diff --git a/app/assets/images/emoji/joy.png b/app/assets/images/emoji/joy.png Binary files differnew file mode 100644 index 00000000000..0ba3b1859d8 --- /dev/null +++ b/app/assets/images/emoji/joy.png diff --git a/app/assets/images/emoji/joy_cat.png b/app/assets/images/emoji/joy_cat.png Binary files differnew file mode 100644 index 00000000000..aac353179aa --- /dev/null +++ b/app/assets/images/emoji/joy_cat.png diff --git a/app/assets/images/emoji/joystick.png b/app/assets/images/emoji/joystick.png Binary files differnew file mode 100644 index 00000000000..1ee1905434e --- /dev/null +++ b/app/assets/images/emoji/joystick.png diff --git a/app/assets/images/emoji/juggling.png b/app/assets/images/emoji/juggling.png Binary files differnew file mode 100644 index 00000000000..a37f6224a42 --- /dev/null +++ b/app/assets/images/emoji/juggling.png diff --git a/app/assets/images/emoji/juggling_tone1.png b/app/assets/images/emoji/juggling_tone1.png Binary files differnew file mode 100644 index 00000000000..c18eda40031 --- /dev/null +++ b/app/assets/images/emoji/juggling_tone1.png diff --git a/app/assets/images/emoji/juggling_tone2.png b/app/assets/images/emoji/juggling_tone2.png Binary files differnew file mode 100644 index 00000000000..de3b7a555b6 --- /dev/null +++ b/app/assets/images/emoji/juggling_tone2.png diff --git a/app/assets/images/emoji/juggling_tone3.png b/app/assets/images/emoji/juggling_tone3.png Binary files differnew file mode 100644 index 00000000000..74ab6d85458 --- /dev/null +++ b/app/assets/images/emoji/juggling_tone3.png diff --git a/app/assets/images/emoji/juggling_tone4.png b/app/assets/images/emoji/juggling_tone4.png Binary files differnew file mode 100644 index 00000000000..1c57823203f --- /dev/null +++ b/app/assets/images/emoji/juggling_tone4.png diff --git a/app/assets/images/emoji/juggling_tone5.png b/app/assets/images/emoji/juggling_tone5.png Binary files differnew file mode 100644 index 00000000000..c343d6ee98a --- /dev/null +++ b/app/assets/images/emoji/juggling_tone5.png diff --git a/app/assets/images/emoji/kaaba.png b/app/assets/images/emoji/kaaba.png Binary files differnew file mode 100644 index 00000000000..1778c1138e4 --- /dev/null +++ b/app/assets/images/emoji/kaaba.png diff --git a/app/assets/images/emoji/key.png b/app/assets/images/emoji/key.png Binary files differnew file mode 100644 index 00000000000..319cd1b884c --- /dev/null +++ b/app/assets/images/emoji/key.png diff --git a/app/assets/images/emoji/key2.png b/app/assets/images/emoji/key2.png Binary files differnew file mode 100644 index 00000000000..e11d706c6c8 --- /dev/null +++ b/app/assets/images/emoji/key2.png diff --git a/app/assets/images/emoji/keyboard.png b/app/assets/images/emoji/keyboard.png Binary files differnew file mode 100644 index 00000000000..75027cb9af7 --- /dev/null +++ b/app/assets/images/emoji/keyboard.png diff --git a/app/assets/images/emoji/kimono.png b/app/assets/images/emoji/kimono.png Binary files differnew file mode 100644 index 00000000000..abe851115d1 --- /dev/null +++ b/app/assets/images/emoji/kimono.png diff --git a/app/assets/images/emoji/kiss.png b/app/assets/images/emoji/kiss.png Binary files differnew file mode 100644 index 00000000000..85e6dcfc4e8 --- /dev/null +++ b/app/assets/images/emoji/kiss.png diff --git a/app/assets/images/emoji/kiss_mm.png b/app/assets/images/emoji/kiss_mm.png Binary files differnew file mode 100644 index 00000000000..a9a0edae17c --- /dev/null +++ b/app/assets/images/emoji/kiss_mm.png diff --git a/app/assets/images/emoji/kiss_ww.png b/app/assets/images/emoji/kiss_ww.png Binary files differnew file mode 100644 index 00000000000..fdac73cbb1d --- /dev/null +++ b/app/assets/images/emoji/kiss_ww.png diff --git a/app/assets/images/emoji/kissing.png b/app/assets/images/emoji/kissing.png Binary files differnew file mode 100644 index 00000000000..39d325fd8e3 --- /dev/null +++ b/app/assets/images/emoji/kissing.png diff --git a/app/assets/images/emoji/kissing_cat.png b/app/assets/images/emoji/kissing_cat.png Binary files differnew file mode 100644 index 00000000000..6e0bcc77540 --- /dev/null +++ b/app/assets/images/emoji/kissing_cat.png diff --git a/app/assets/images/emoji/kissing_closed_eyes.png b/app/assets/images/emoji/kissing_closed_eyes.png Binary files differnew file mode 100644 index 00000000000..b684d7d4d6c --- /dev/null +++ b/app/assets/images/emoji/kissing_closed_eyes.png diff --git a/app/assets/images/emoji/kissing_heart.png b/app/assets/images/emoji/kissing_heart.png Binary files differnew file mode 100644 index 00000000000..0ff808fd614 --- /dev/null +++ b/app/assets/images/emoji/kissing_heart.png diff --git a/app/assets/images/emoji/kissing_smiling_eyes.png b/app/assets/images/emoji/kissing_smiling_eyes.png Binary files differnew file mode 100644 index 00000000000..e181f17099d --- /dev/null +++ b/app/assets/images/emoji/kissing_smiling_eyes.png diff --git a/app/assets/images/emoji/kiwi.png b/app/assets/images/emoji/kiwi.png Binary files differnew file mode 100644 index 00000000000..dfbd8258074 --- /dev/null +++ b/app/assets/images/emoji/kiwi.png diff --git a/app/assets/images/emoji/knife.png b/app/assets/images/emoji/knife.png Binary files differnew file mode 100644 index 00000000000..1acb9f3077b --- /dev/null +++ b/app/assets/images/emoji/knife.png diff --git a/app/assets/images/emoji/koala.png b/app/assets/images/emoji/koala.png Binary files differnew file mode 100644 index 00000000000..a0aa437a98c --- /dev/null +++ b/app/assets/images/emoji/koala.png diff --git a/app/assets/images/emoji/koko.png b/app/assets/images/emoji/koko.png Binary files differnew file mode 100644 index 00000000000..6450eb44d90 --- /dev/null +++ b/app/assets/images/emoji/koko.png diff --git a/app/assets/images/emoji/label.png b/app/assets/images/emoji/label.png Binary files differnew file mode 100644 index 00000000000..d41c9b4f1e1 --- /dev/null +++ b/app/assets/images/emoji/label.png diff --git a/app/assets/images/emoji/large_blue_circle.png b/app/assets/images/emoji/large_blue_circle.png Binary files differnew file mode 100644 index 00000000000..84078ef3127 --- /dev/null +++ b/app/assets/images/emoji/large_blue_circle.png diff --git a/app/assets/images/emoji/large_blue_diamond.png b/app/assets/images/emoji/large_blue_diamond.png Binary files differnew file mode 100644 index 00000000000..416a58bd5a8 --- /dev/null +++ b/app/assets/images/emoji/large_blue_diamond.png diff --git a/app/assets/images/emoji/large_orange_diamond.png b/app/assets/images/emoji/large_orange_diamond.png Binary files differnew file mode 100644 index 00000000000..73ff0ac36c8 --- /dev/null +++ b/app/assets/images/emoji/large_orange_diamond.png diff --git a/app/assets/images/emoji/last_quarter_moon.png b/app/assets/images/emoji/last_quarter_moon.png Binary files differnew file mode 100644 index 00000000000..0842a0dd408 --- /dev/null +++ b/app/assets/images/emoji/last_quarter_moon.png diff --git a/app/assets/images/emoji/last_quarter_moon_with_face.png b/app/assets/images/emoji/last_quarter_moon_with_face.png Binary files differnew file mode 100644 index 00000000000..94099343c5d --- /dev/null +++ b/app/assets/images/emoji/last_quarter_moon_with_face.png diff --git a/app/assets/images/emoji/laughing.png b/app/assets/images/emoji/laughing.png Binary files differnew file mode 100644 index 00000000000..d94e9505ba1 --- /dev/null +++ b/app/assets/images/emoji/laughing.png diff --git a/app/assets/images/emoji/leaves.png b/app/assets/images/emoji/leaves.png Binary files differnew file mode 100644 index 00000000000..1e43e1af820 --- /dev/null +++ b/app/assets/images/emoji/leaves.png diff --git a/app/assets/images/emoji/ledger.png b/app/assets/images/emoji/ledger.png Binary files differnew file mode 100644 index 00000000000..13e7561a4bd --- /dev/null +++ b/app/assets/images/emoji/ledger.png diff --git a/app/assets/images/emoji/left_facing_fist.png b/app/assets/images/emoji/left_facing_fist.png Binary files differnew file mode 100644 index 00000000000..a9d9fd8d59c --- /dev/null +++ b/app/assets/images/emoji/left_facing_fist.png diff --git a/app/assets/images/emoji/left_facing_fist_tone1.png b/app/assets/images/emoji/left_facing_fist_tone1.png Binary files differnew file mode 100644 index 00000000000..1262a6b4b69 --- /dev/null +++ b/app/assets/images/emoji/left_facing_fist_tone1.png diff --git a/app/assets/images/emoji/left_facing_fist_tone2.png b/app/assets/images/emoji/left_facing_fist_tone2.png Binary files differnew file mode 100644 index 00000000000..40bf70b82b2 --- /dev/null +++ b/app/assets/images/emoji/left_facing_fist_tone2.png diff --git a/app/assets/images/emoji/left_facing_fist_tone3.png b/app/assets/images/emoji/left_facing_fist_tone3.png Binary files differnew file mode 100644 index 00000000000..93f58145111 --- /dev/null +++ b/app/assets/images/emoji/left_facing_fist_tone3.png diff --git a/app/assets/images/emoji/left_facing_fist_tone4.png b/app/assets/images/emoji/left_facing_fist_tone4.png Binary files differnew file mode 100644 index 00000000000..d82b5ec91f0 --- /dev/null +++ b/app/assets/images/emoji/left_facing_fist_tone4.png diff --git a/app/assets/images/emoji/left_facing_fist_tone5.png b/app/assets/images/emoji/left_facing_fist_tone5.png Binary files differnew file mode 100644 index 00000000000..09ae4cd492b --- /dev/null +++ b/app/assets/images/emoji/left_facing_fist_tone5.png diff --git a/app/assets/images/emoji/left_luggage.png b/app/assets/images/emoji/left_luggage.png Binary files differnew file mode 100644 index 00000000000..887b23f3f25 --- /dev/null +++ b/app/assets/images/emoji/left_luggage.png diff --git a/app/assets/images/emoji/left_right_arrow.png b/app/assets/images/emoji/left_right_arrow.png Binary files differnew file mode 100644 index 00000000000..7937f24f2ac --- /dev/null +++ b/app/assets/images/emoji/left_right_arrow.png diff --git a/app/assets/images/emoji/leftwards_arrow_with_hook.png b/app/assets/images/emoji/leftwards_arrow_with_hook.png Binary files differnew file mode 100644 index 00000000000..ba45c2ad9e9 --- /dev/null +++ b/app/assets/images/emoji/leftwards_arrow_with_hook.png diff --git a/app/assets/images/emoji/lemon.png b/app/assets/images/emoji/lemon.png Binary files differnew file mode 100644 index 00000000000..9a7d95ca220 --- /dev/null +++ b/app/assets/images/emoji/lemon.png diff --git a/app/assets/images/emoji/leo.png b/app/assets/images/emoji/leo.png Binary files differnew file mode 100644 index 00000000000..30158d34de9 --- /dev/null +++ b/app/assets/images/emoji/leo.png diff --git a/app/assets/images/emoji/leopard.png b/app/assets/images/emoji/leopard.png Binary files differnew file mode 100644 index 00000000000..8aac3d49448 --- /dev/null +++ b/app/assets/images/emoji/leopard.png diff --git a/app/assets/images/emoji/level_slider.png b/app/assets/images/emoji/level_slider.png Binary files differnew file mode 100644 index 00000000000..720a3b34119 --- /dev/null +++ b/app/assets/images/emoji/level_slider.png diff --git a/app/assets/images/emoji/levitate.png b/app/assets/images/emoji/levitate.png Binary files differnew file mode 100644 index 00000000000..3dc315a3d91 --- /dev/null +++ b/app/assets/images/emoji/levitate.png diff --git a/app/assets/images/emoji/libra.png b/app/assets/images/emoji/libra.png Binary files differnew file mode 100644 index 00000000000..8fd133a357c --- /dev/null +++ b/app/assets/images/emoji/libra.png diff --git a/app/assets/images/emoji/lifter.png b/app/assets/images/emoji/lifter.png Binary files differnew file mode 100644 index 00000000000..afdeaa476af --- /dev/null +++ b/app/assets/images/emoji/lifter.png diff --git a/app/assets/images/emoji/lifter_tone1.png b/app/assets/images/emoji/lifter_tone1.png Binary files differnew file mode 100644 index 00000000000..febaad123ec --- /dev/null +++ b/app/assets/images/emoji/lifter_tone1.png diff --git a/app/assets/images/emoji/lifter_tone2.png b/app/assets/images/emoji/lifter_tone2.png Binary files differnew file mode 100644 index 00000000000..27ae794a18e --- /dev/null +++ b/app/assets/images/emoji/lifter_tone2.png diff --git a/app/assets/images/emoji/lifter_tone3.png b/app/assets/images/emoji/lifter_tone3.png Binary files differnew file mode 100644 index 00000000000..45c4c22c709 --- /dev/null +++ b/app/assets/images/emoji/lifter_tone3.png diff --git a/app/assets/images/emoji/lifter_tone4.png b/app/assets/images/emoji/lifter_tone4.png Binary files differnew file mode 100644 index 00000000000..67dd21d2464 --- /dev/null +++ b/app/assets/images/emoji/lifter_tone4.png diff --git a/app/assets/images/emoji/lifter_tone5.png b/app/assets/images/emoji/lifter_tone5.png Binary files differnew file mode 100644 index 00000000000..fa0152038b6 --- /dev/null +++ b/app/assets/images/emoji/lifter_tone5.png diff --git a/app/assets/images/emoji/light_rail.png b/app/assets/images/emoji/light_rail.png Binary files differnew file mode 100644 index 00000000000..a64829f5078 --- /dev/null +++ b/app/assets/images/emoji/light_rail.png diff --git a/app/assets/images/emoji/link.png b/app/assets/images/emoji/link.png Binary files differnew file mode 100644 index 00000000000..ae20f0f8eec --- /dev/null +++ b/app/assets/images/emoji/link.png diff --git a/app/assets/images/emoji/lion_face.png b/app/assets/images/emoji/lion_face.png Binary files differnew file mode 100644 index 00000000000..5062ab47ecf --- /dev/null +++ b/app/assets/images/emoji/lion_face.png diff --git a/app/assets/images/emoji/lips.png b/app/assets/images/emoji/lips.png Binary files differnew file mode 100644 index 00000000000..35f3cc2006f --- /dev/null +++ b/app/assets/images/emoji/lips.png diff --git a/app/assets/images/emoji/lipstick.png b/app/assets/images/emoji/lipstick.png Binary files differnew file mode 100644 index 00000000000..61a0c084c99 --- /dev/null +++ b/app/assets/images/emoji/lipstick.png diff --git a/app/assets/images/emoji/lizard.png b/app/assets/images/emoji/lizard.png Binary files differnew file mode 100644 index 00000000000..8363876050e --- /dev/null +++ b/app/assets/images/emoji/lizard.png diff --git a/app/assets/images/emoji/lock.png b/app/assets/images/emoji/lock.png Binary files differnew file mode 100644 index 00000000000..5a739c46644 --- /dev/null +++ b/app/assets/images/emoji/lock.png diff --git a/app/assets/images/emoji/lock_with_ink_pen.png b/app/assets/images/emoji/lock_with_ink_pen.png Binary files differnew file mode 100644 index 00000000000..19a07d162fb --- /dev/null +++ b/app/assets/images/emoji/lock_with_ink_pen.png diff --git a/app/assets/images/emoji/lollipop.png b/app/assets/images/emoji/lollipop.png Binary files differnew file mode 100644 index 00000000000..ad76d7bf916 --- /dev/null +++ b/app/assets/images/emoji/lollipop.png diff --git a/app/assets/images/emoji/loop.png b/app/assets/images/emoji/loop.png Binary files differnew file mode 100644 index 00000000000..0b82c8fe315 --- /dev/null +++ b/app/assets/images/emoji/loop.png diff --git a/app/assets/images/emoji/loud_sound.png b/app/assets/images/emoji/loud_sound.png Binary files differnew file mode 100644 index 00000000000..8370033a539 --- /dev/null +++ b/app/assets/images/emoji/loud_sound.png diff --git a/app/assets/images/emoji/loudspeaker.png b/app/assets/images/emoji/loudspeaker.png Binary files differnew file mode 100644 index 00000000000..5fd76a95b82 --- /dev/null +++ b/app/assets/images/emoji/loudspeaker.png diff --git a/app/assets/images/emoji/love_hotel.png b/app/assets/images/emoji/love_hotel.png Binary files differnew file mode 100644 index 00000000000..5e136be6f8b --- /dev/null +++ b/app/assets/images/emoji/love_hotel.png diff --git a/app/assets/images/emoji/love_letter.png b/app/assets/images/emoji/love_letter.png Binary files differnew file mode 100644 index 00000000000..3c3c767e784 --- /dev/null +++ b/app/assets/images/emoji/love_letter.png diff --git a/app/assets/images/emoji/low_brightness.png b/app/assets/images/emoji/low_brightness.png Binary files differnew file mode 100644 index 00000000000..543011d3961 --- /dev/null +++ b/app/assets/images/emoji/low_brightness.png diff --git a/app/assets/images/emoji/lying_face.png b/app/assets/images/emoji/lying_face.png Binary files differnew file mode 100644 index 00000000000..02827e2628b --- /dev/null +++ b/app/assets/images/emoji/lying_face.png diff --git a/app/assets/images/emoji/m.png b/app/assets/images/emoji/m.png Binary files differnew file mode 100644 index 00000000000..8a3506fc1d7 --- /dev/null +++ b/app/assets/images/emoji/m.png diff --git a/app/assets/images/emoji/mag.png b/app/assets/images/emoji/mag.png Binary files differnew file mode 100644 index 00000000000..55487156ac6 --- /dev/null +++ b/app/assets/images/emoji/mag.png diff --git a/app/assets/images/emoji/mag_right.png b/app/assets/images/emoji/mag_right.png Binary files differnew file mode 100644 index 00000000000..0f4b1bca876 --- /dev/null +++ b/app/assets/images/emoji/mag_right.png diff --git a/app/assets/images/emoji/mahjong.png b/app/assets/images/emoji/mahjong.png Binary files differnew file mode 100644 index 00000000000..66fd32025b2 --- /dev/null +++ b/app/assets/images/emoji/mahjong.png diff --git a/app/assets/images/emoji/mailbox.png b/app/assets/images/emoji/mailbox.png Binary files differnew file mode 100644 index 00000000000..ef5174e40dd --- /dev/null +++ b/app/assets/images/emoji/mailbox.png diff --git a/app/assets/images/emoji/mailbox_closed.png b/app/assets/images/emoji/mailbox_closed.png Binary files differnew file mode 100644 index 00000000000..ddc705db0d8 --- /dev/null +++ b/app/assets/images/emoji/mailbox_closed.png diff --git a/app/assets/images/emoji/mailbox_with_mail.png b/app/assets/images/emoji/mailbox_with_mail.png Binary files differnew file mode 100644 index 00000000000..5460616a5b1 --- /dev/null +++ b/app/assets/images/emoji/mailbox_with_mail.png diff --git a/app/assets/images/emoji/mailbox_with_no_mail.png b/app/assets/images/emoji/mailbox_with_no_mail.png Binary files differnew file mode 100644 index 00000000000..f9aeee6b15a --- /dev/null +++ b/app/assets/images/emoji/mailbox_with_no_mail.png diff --git a/app/assets/images/emoji/man.png b/app/assets/images/emoji/man.png Binary files differnew file mode 100644 index 00000000000..857a02e5146 --- /dev/null +++ b/app/assets/images/emoji/man.png diff --git a/app/assets/images/emoji/man_dancing.png b/app/assets/images/emoji/man_dancing.png Binary files differnew file mode 100644 index 00000000000..ccff3bede5a --- /dev/null +++ b/app/assets/images/emoji/man_dancing.png diff --git a/app/assets/images/emoji/man_dancing_tone1.png b/app/assets/images/emoji/man_dancing_tone1.png Binary files differnew file mode 100644 index 00000000000..e0b9f82d905 --- /dev/null +++ b/app/assets/images/emoji/man_dancing_tone1.png diff --git a/app/assets/images/emoji/man_dancing_tone2.png b/app/assets/images/emoji/man_dancing_tone2.png Binary files differnew file mode 100644 index 00000000000..a5beed56e2e --- /dev/null +++ b/app/assets/images/emoji/man_dancing_tone2.png diff --git a/app/assets/images/emoji/man_dancing_tone3.png b/app/assets/images/emoji/man_dancing_tone3.png Binary files differnew file mode 100644 index 00000000000..2fa20180a6e --- /dev/null +++ b/app/assets/images/emoji/man_dancing_tone3.png diff --git a/app/assets/images/emoji/man_dancing_tone4.png b/app/assets/images/emoji/man_dancing_tone4.png Binary files differnew file mode 100644 index 00000000000..bd3528c83ba --- /dev/null +++ b/app/assets/images/emoji/man_dancing_tone4.png diff --git a/app/assets/images/emoji/man_dancing_tone5.png b/app/assets/images/emoji/man_dancing_tone5.png Binary files differnew file mode 100644 index 00000000000..41fd4f880c9 --- /dev/null +++ b/app/assets/images/emoji/man_dancing_tone5.png diff --git a/app/assets/images/emoji/man_in_tuxedo.png b/app/assets/images/emoji/man_in_tuxedo.png Binary files differnew file mode 100644 index 00000000000..5f7e9303f89 --- /dev/null +++ b/app/assets/images/emoji/man_in_tuxedo.png diff --git a/app/assets/images/emoji/man_in_tuxedo_tone1.png b/app/assets/images/emoji/man_in_tuxedo_tone1.png Binary files differnew file mode 100644 index 00000000000..7b6b3acd99b --- /dev/null +++ b/app/assets/images/emoji/man_in_tuxedo_tone1.png diff --git a/app/assets/images/emoji/man_in_tuxedo_tone2.png b/app/assets/images/emoji/man_in_tuxedo_tone2.png Binary files differnew file mode 100644 index 00000000000..7975191b360 --- /dev/null +++ b/app/assets/images/emoji/man_in_tuxedo_tone2.png diff --git a/app/assets/images/emoji/man_in_tuxedo_tone3.png b/app/assets/images/emoji/man_in_tuxedo_tone3.png Binary files differnew file mode 100644 index 00000000000..a2816f600ae --- /dev/null +++ b/app/assets/images/emoji/man_in_tuxedo_tone3.png diff --git a/app/assets/images/emoji/man_in_tuxedo_tone4.png b/app/assets/images/emoji/man_in_tuxedo_tone4.png Binary files differnew file mode 100644 index 00000000000..ea8291760f9 --- /dev/null +++ b/app/assets/images/emoji/man_in_tuxedo_tone4.png diff --git a/app/assets/images/emoji/man_in_tuxedo_tone5.png b/app/assets/images/emoji/man_in_tuxedo_tone5.png Binary files differnew file mode 100644 index 00000000000..c743e05fc5e --- /dev/null +++ b/app/assets/images/emoji/man_in_tuxedo_tone5.png diff --git a/app/assets/images/emoji/man_tone1.png b/app/assets/images/emoji/man_tone1.png Binary files differnew file mode 100644 index 00000000000..bb86e963a80 --- /dev/null +++ b/app/assets/images/emoji/man_tone1.png diff --git a/app/assets/images/emoji/man_tone2.png b/app/assets/images/emoji/man_tone2.png Binary files differnew file mode 100644 index 00000000000..fdeeaff46f5 --- /dev/null +++ b/app/assets/images/emoji/man_tone2.png diff --git a/app/assets/images/emoji/man_tone3.png b/app/assets/images/emoji/man_tone3.png Binary files differnew file mode 100644 index 00000000000..7ae0b5df9cf --- /dev/null +++ b/app/assets/images/emoji/man_tone3.png diff --git a/app/assets/images/emoji/man_tone4.png b/app/assets/images/emoji/man_tone4.png Binary files differnew file mode 100644 index 00000000000..db14cde99b8 --- /dev/null +++ b/app/assets/images/emoji/man_tone4.png diff --git a/app/assets/images/emoji/man_tone5.png b/app/assets/images/emoji/man_tone5.png Binary files differnew file mode 100644 index 00000000000..7c67a70529c --- /dev/null +++ b/app/assets/images/emoji/man_tone5.png diff --git a/app/assets/images/emoji/man_with_gua_pi_mao.png b/app/assets/images/emoji/man_with_gua_pi_mao.png Binary files differnew file mode 100644 index 00000000000..7841e13608d --- /dev/null +++ b/app/assets/images/emoji/man_with_gua_pi_mao.png diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone1.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone1.png Binary files differnew file mode 100644 index 00000000000..5b7b3def19c --- /dev/null +++ b/app/assets/images/emoji/man_with_gua_pi_mao_tone1.png diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone2.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone2.png Binary files differnew file mode 100644 index 00000000000..c8b9cf87f4b --- /dev/null +++ b/app/assets/images/emoji/man_with_gua_pi_mao_tone2.png diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone3.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone3.png Binary files differnew file mode 100644 index 00000000000..effdd0c4c84 --- /dev/null +++ b/app/assets/images/emoji/man_with_gua_pi_mao_tone3.png diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone4.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone4.png Binary files differnew file mode 100644 index 00000000000..f885ff46fa1 --- /dev/null +++ b/app/assets/images/emoji/man_with_gua_pi_mao_tone4.png diff --git a/app/assets/images/emoji/man_with_gua_pi_mao_tone5.png b/app/assets/images/emoji/man_with_gua_pi_mao_tone5.png Binary files differnew file mode 100644 index 00000000000..a6d55ca1380 --- /dev/null +++ b/app/assets/images/emoji/man_with_gua_pi_mao_tone5.png diff --git a/app/assets/images/emoji/man_with_turban.png b/app/assets/images/emoji/man_with_turban.png Binary files differnew file mode 100644 index 00000000000..51cf047f966 --- /dev/null +++ b/app/assets/images/emoji/man_with_turban.png diff --git a/app/assets/images/emoji/man_with_turban_tone1.png b/app/assets/images/emoji/man_with_turban_tone1.png Binary files differnew file mode 100644 index 00000000000..1e12ee4b231 --- /dev/null +++ b/app/assets/images/emoji/man_with_turban_tone1.png diff --git a/app/assets/images/emoji/man_with_turban_tone2.png b/app/assets/images/emoji/man_with_turban_tone2.png Binary files differnew file mode 100644 index 00000000000..37de4cceb23 --- /dev/null +++ b/app/assets/images/emoji/man_with_turban_tone2.png diff --git a/app/assets/images/emoji/man_with_turban_tone3.png b/app/assets/images/emoji/man_with_turban_tone3.png Binary files differnew file mode 100644 index 00000000000..f607afd3450 --- /dev/null +++ b/app/assets/images/emoji/man_with_turban_tone3.png diff --git a/app/assets/images/emoji/man_with_turban_tone4.png b/app/assets/images/emoji/man_with_turban_tone4.png Binary files differnew file mode 100644 index 00000000000..c05695888af --- /dev/null +++ b/app/assets/images/emoji/man_with_turban_tone4.png diff --git a/app/assets/images/emoji/man_with_turban_tone5.png b/app/assets/images/emoji/man_with_turban_tone5.png Binary files differnew file mode 100644 index 00000000000..4b4ff64720b --- /dev/null +++ b/app/assets/images/emoji/man_with_turban_tone5.png diff --git a/app/assets/images/emoji/mans_shoe.png b/app/assets/images/emoji/mans_shoe.png Binary files differnew file mode 100644 index 00000000000..4bf7541032c --- /dev/null +++ b/app/assets/images/emoji/mans_shoe.png diff --git a/app/assets/images/emoji/map.png b/app/assets/images/emoji/map.png Binary files differnew file mode 100644 index 00000000000..15efe32c798 --- /dev/null +++ b/app/assets/images/emoji/map.png diff --git a/app/assets/images/emoji/maple_leaf.png b/app/assets/images/emoji/maple_leaf.png Binary files differnew file mode 100644 index 00000000000..c49acea67f7 --- /dev/null +++ b/app/assets/images/emoji/maple_leaf.png diff --git a/app/assets/images/emoji/martial_arts_uniform.png b/app/assets/images/emoji/martial_arts_uniform.png Binary files differnew file mode 100644 index 00000000000..8d6114761f6 --- /dev/null +++ b/app/assets/images/emoji/martial_arts_uniform.png diff --git a/app/assets/images/emoji/mask.png b/app/assets/images/emoji/mask.png Binary files differnew file mode 100644 index 00000000000..1e800acd1c0 --- /dev/null +++ b/app/assets/images/emoji/mask.png diff --git a/app/assets/images/emoji/massage.png b/app/assets/images/emoji/massage.png Binary files differnew file mode 100644 index 00000000000..b91d845e374 --- /dev/null +++ b/app/assets/images/emoji/massage.png diff --git a/app/assets/images/emoji/massage_tone1.png b/app/assets/images/emoji/massage_tone1.png Binary files differnew file mode 100644 index 00000000000..e0f415d3186 --- /dev/null +++ b/app/assets/images/emoji/massage_tone1.png diff --git a/app/assets/images/emoji/massage_tone2.png b/app/assets/images/emoji/massage_tone2.png Binary files differnew file mode 100644 index 00000000000..0bb244a270b --- /dev/null +++ b/app/assets/images/emoji/massage_tone2.png diff --git a/app/assets/images/emoji/massage_tone3.png b/app/assets/images/emoji/massage_tone3.png Binary files differnew file mode 100644 index 00000000000..a117ee81a22 --- /dev/null +++ b/app/assets/images/emoji/massage_tone3.png diff --git a/app/assets/images/emoji/massage_tone4.png b/app/assets/images/emoji/massage_tone4.png Binary files differnew file mode 100644 index 00000000000..6f42ab017f4 --- /dev/null +++ b/app/assets/images/emoji/massage_tone4.png diff --git a/app/assets/images/emoji/massage_tone5.png b/app/assets/images/emoji/massage_tone5.png Binary files differnew file mode 100644 index 00000000000..6a388c0d0b5 --- /dev/null +++ b/app/assets/images/emoji/massage_tone5.png diff --git a/app/assets/images/emoji/meat_on_bone.png b/app/assets/images/emoji/meat_on_bone.png Binary files differnew file mode 100644 index 00000000000..b20a59d1690 --- /dev/null +++ b/app/assets/images/emoji/meat_on_bone.png diff --git a/app/assets/images/emoji/medal.png b/app/assets/images/emoji/medal.png Binary files differnew file mode 100644 index 00000000000..b85896b14da --- /dev/null +++ b/app/assets/images/emoji/medal.png diff --git a/app/assets/images/emoji/mega.png b/app/assets/images/emoji/mega.png Binary files differnew file mode 100644 index 00000000000..4e6735188e3 --- /dev/null +++ b/app/assets/images/emoji/mega.png diff --git a/app/assets/images/emoji/melon.png b/app/assets/images/emoji/melon.png Binary files differnew file mode 100644 index 00000000000..c01232d419d --- /dev/null +++ b/app/assets/images/emoji/melon.png diff --git a/app/assets/images/emoji/menorah.png b/app/assets/images/emoji/menorah.png Binary files differnew file mode 100644 index 00000000000..b4297362869 --- /dev/null +++ b/app/assets/images/emoji/menorah.png diff --git a/app/assets/images/emoji/mens.png b/app/assets/images/emoji/mens.png Binary files differnew file mode 100644 index 00000000000..f5a1e1ba0cd --- /dev/null +++ b/app/assets/images/emoji/mens.png diff --git a/app/assets/images/emoji/metal.png b/app/assets/images/emoji/metal.png Binary files differnew file mode 100644 index 00000000000..4aa6e7e0a44 --- /dev/null +++ b/app/assets/images/emoji/metal.png diff --git a/app/assets/images/emoji/metal_tone1.png b/app/assets/images/emoji/metal_tone1.png Binary files differnew file mode 100644 index 00000000000..c080d2addbd --- /dev/null +++ b/app/assets/images/emoji/metal_tone1.png diff --git a/app/assets/images/emoji/metal_tone2.png b/app/assets/images/emoji/metal_tone2.png Binary files differnew file mode 100644 index 00000000000..12313529bcf --- /dev/null +++ b/app/assets/images/emoji/metal_tone2.png diff --git a/app/assets/images/emoji/metal_tone3.png b/app/assets/images/emoji/metal_tone3.png Binary files differnew file mode 100644 index 00000000000..ca9be6ae67b --- /dev/null +++ b/app/assets/images/emoji/metal_tone3.png diff --git a/app/assets/images/emoji/metal_tone4.png b/app/assets/images/emoji/metal_tone4.png Binary files differnew file mode 100644 index 00000000000..abe28cbf890 --- /dev/null +++ b/app/assets/images/emoji/metal_tone4.png diff --git a/app/assets/images/emoji/metal_tone5.png b/app/assets/images/emoji/metal_tone5.png Binary files differnew file mode 100644 index 00000000000..0c6b5dd34ed --- /dev/null +++ b/app/assets/images/emoji/metal_tone5.png diff --git a/app/assets/images/emoji/metro.png b/app/assets/images/emoji/metro.png Binary files differnew file mode 100644 index 00000000000..1de8f0551f3 --- /dev/null +++ b/app/assets/images/emoji/metro.png diff --git a/app/assets/images/emoji/microphone.png b/app/assets/images/emoji/microphone.png Binary files differnew file mode 100644 index 00000000000..d4e6b0def25 --- /dev/null +++ b/app/assets/images/emoji/microphone.png diff --git a/app/assets/images/emoji/microphone2.png b/app/assets/images/emoji/microphone2.png Binary files differnew file mode 100644 index 00000000000..cd9167654ff --- /dev/null +++ b/app/assets/images/emoji/microphone2.png diff --git a/app/assets/images/emoji/microscope.png b/app/assets/images/emoji/microscope.png Binary files differnew file mode 100644 index 00000000000..90f5acf6a78 --- /dev/null +++ b/app/assets/images/emoji/microscope.png diff --git a/app/assets/images/emoji/middle_finger.png b/app/assets/images/emoji/middle_finger.png Binary files differnew file mode 100644 index 00000000000..697f7a25eb2 --- /dev/null +++ b/app/assets/images/emoji/middle_finger.png diff --git a/app/assets/images/emoji/middle_finger_tone1.png b/app/assets/images/emoji/middle_finger_tone1.png Binary files differnew file mode 100644 index 00000000000..61ef12a1548 --- /dev/null +++ b/app/assets/images/emoji/middle_finger_tone1.png diff --git a/app/assets/images/emoji/middle_finger_tone2.png b/app/assets/images/emoji/middle_finger_tone2.png Binary files differnew file mode 100644 index 00000000000..c31a69be9af --- /dev/null +++ b/app/assets/images/emoji/middle_finger_tone2.png diff --git a/app/assets/images/emoji/middle_finger_tone3.png b/app/assets/images/emoji/middle_finger_tone3.png Binary files differnew file mode 100644 index 00000000000..73ac216ce63 --- /dev/null +++ b/app/assets/images/emoji/middle_finger_tone3.png diff --git a/app/assets/images/emoji/middle_finger_tone4.png b/app/assets/images/emoji/middle_finger_tone4.png Binary files differnew file mode 100644 index 00000000000..80b8ab7706d --- /dev/null +++ b/app/assets/images/emoji/middle_finger_tone4.png diff --git a/app/assets/images/emoji/middle_finger_tone5.png b/app/assets/images/emoji/middle_finger_tone5.png Binary files differnew file mode 100644 index 00000000000..a8826b196e8 --- /dev/null +++ b/app/assets/images/emoji/middle_finger_tone5.png diff --git a/app/assets/images/emoji/military_medal.png b/app/assets/images/emoji/military_medal.png Binary files differnew file mode 100644 index 00000000000..ecd3fb03584 --- /dev/null +++ b/app/assets/images/emoji/military_medal.png diff --git a/app/assets/images/emoji/milk.png b/app/assets/images/emoji/milk.png Binary files differnew file mode 100644 index 00000000000..e4fcf2e64f3 --- /dev/null +++ b/app/assets/images/emoji/milk.png diff --git a/app/assets/images/emoji/milky_way.png b/app/assets/images/emoji/milky_way.png Binary files differnew file mode 100644 index 00000000000..b2b8ac59c5e --- /dev/null +++ b/app/assets/images/emoji/milky_way.png diff --git a/app/assets/images/emoji/minibus.png b/app/assets/images/emoji/minibus.png Binary files differnew file mode 100644 index 00000000000..c60dd8f47ab --- /dev/null +++ b/app/assets/images/emoji/minibus.png diff --git a/app/assets/images/emoji/minidisc.png b/app/assets/images/emoji/minidisc.png Binary files differnew file mode 100644 index 00000000000..9fa94cfbe74 --- /dev/null +++ b/app/assets/images/emoji/minidisc.png diff --git a/app/assets/images/emoji/mobile_phone_off.png b/app/assets/images/emoji/mobile_phone_off.png Binary files differnew file mode 100644 index 00000000000..8b661ec1c94 --- /dev/null +++ b/app/assets/images/emoji/mobile_phone_off.png diff --git a/app/assets/images/emoji/money_mouth.png b/app/assets/images/emoji/money_mouth.png Binary files differnew file mode 100644 index 00000000000..75fd1e90cb0 --- /dev/null +++ b/app/assets/images/emoji/money_mouth.png diff --git a/app/assets/images/emoji/money_with_wings.png b/app/assets/images/emoji/money_with_wings.png Binary files differnew file mode 100644 index 00000000000..f022b04b3c2 --- /dev/null +++ b/app/assets/images/emoji/money_with_wings.png diff --git a/app/assets/images/emoji/moneybag.png b/app/assets/images/emoji/moneybag.png Binary files differnew file mode 100644 index 00000000000..b9296be0902 --- /dev/null +++ b/app/assets/images/emoji/moneybag.png diff --git a/app/assets/images/emoji/monkey.png b/app/assets/images/emoji/monkey.png Binary files differnew file mode 100644 index 00000000000..9fae29448e3 --- /dev/null +++ b/app/assets/images/emoji/monkey.png diff --git a/app/assets/images/emoji/monkey_face.png b/app/assets/images/emoji/monkey_face.png Binary files differnew file mode 100644 index 00000000000..7cab9b91a82 --- /dev/null +++ b/app/assets/images/emoji/monkey_face.png diff --git a/app/assets/images/emoji/monorail.png b/app/assets/images/emoji/monorail.png Binary files differnew file mode 100644 index 00000000000..11eb1f574bf --- /dev/null +++ b/app/assets/images/emoji/monorail.png diff --git a/app/assets/images/emoji/mortar_board.png b/app/assets/images/emoji/mortar_board.png Binary files differnew file mode 100644 index 00000000000..8b17ddd9d00 --- /dev/null +++ b/app/assets/images/emoji/mortar_board.png diff --git a/app/assets/images/emoji/mosque.png b/app/assets/images/emoji/mosque.png Binary files differnew file mode 100644 index 00000000000..ef770b26d96 --- /dev/null +++ b/app/assets/images/emoji/mosque.png diff --git a/app/assets/images/emoji/motor_scooter.png b/app/assets/images/emoji/motor_scooter.png Binary files differnew file mode 100644 index 00000000000..c5afa72d807 --- /dev/null +++ b/app/assets/images/emoji/motor_scooter.png diff --git a/app/assets/images/emoji/motorboat.png b/app/assets/images/emoji/motorboat.png Binary files differnew file mode 100644 index 00000000000..0506db1a40f --- /dev/null +++ b/app/assets/images/emoji/motorboat.png diff --git a/app/assets/images/emoji/motorcycle.png b/app/assets/images/emoji/motorcycle.png Binary files differnew file mode 100644 index 00000000000..3d1d567e8ec --- /dev/null +++ b/app/assets/images/emoji/motorcycle.png diff --git a/app/assets/images/emoji/motorway.png b/app/assets/images/emoji/motorway.png Binary files differnew file mode 100644 index 00000000000..8c3d3d03e3f --- /dev/null +++ b/app/assets/images/emoji/motorway.png diff --git a/app/assets/images/emoji/mount_fuji.png b/app/assets/images/emoji/mount_fuji.png Binary files differnew file mode 100644 index 00000000000..88a54752458 --- /dev/null +++ b/app/assets/images/emoji/mount_fuji.png diff --git a/app/assets/images/emoji/mountain.png b/app/assets/images/emoji/mountain.png Binary files differnew file mode 100644 index 00000000000..6722ebdd294 --- /dev/null +++ b/app/assets/images/emoji/mountain.png diff --git a/app/assets/images/emoji/mountain_bicyclist.png b/app/assets/images/emoji/mountain_bicyclist.png Binary files differnew file mode 100644 index 00000000000..41d3dc3ac6f --- /dev/null +++ b/app/assets/images/emoji/mountain_bicyclist.png diff --git a/app/assets/images/emoji/mountain_bicyclist_tone1.png b/app/assets/images/emoji/mountain_bicyclist_tone1.png Binary files differnew file mode 100644 index 00000000000..e9f1daf5e40 --- /dev/null +++ b/app/assets/images/emoji/mountain_bicyclist_tone1.png diff --git a/app/assets/images/emoji/mountain_bicyclist_tone2.png b/app/assets/images/emoji/mountain_bicyclist_tone2.png Binary files differnew file mode 100644 index 00000000000..555b9e29d4d --- /dev/null +++ b/app/assets/images/emoji/mountain_bicyclist_tone2.png diff --git a/app/assets/images/emoji/mountain_bicyclist_tone3.png b/app/assets/images/emoji/mountain_bicyclist_tone3.png Binary files differnew file mode 100644 index 00000000000..7df5508ec8c --- /dev/null +++ b/app/assets/images/emoji/mountain_bicyclist_tone3.png diff --git a/app/assets/images/emoji/mountain_bicyclist_tone4.png b/app/assets/images/emoji/mountain_bicyclist_tone4.png Binary files differnew file mode 100644 index 00000000000..f94b3450697 --- /dev/null +++ b/app/assets/images/emoji/mountain_bicyclist_tone4.png diff --git a/app/assets/images/emoji/mountain_bicyclist_tone5.png b/app/assets/images/emoji/mountain_bicyclist_tone5.png Binary files differnew file mode 100644 index 00000000000..16a45861e1f --- /dev/null +++ b/app/assets/images/emoji/mountain_bicyclist_tone5.png diff --git a/app/assets/images/emoji/mountain_cableway.png b/app/assets/images/emoji/mountain_cableway.png Binary files differnew file mode 100644 index 00000000000..1dea73ca53b --- /dev/null +++ b/app/assets/images/emoji/mountain_cableway.png diff --git a/app/assets/images/emoji/mountain_railway.png b/app/assets/images/emoji/mountain_railway.png Binary files differnew file mode 100644 index 00000000000..ade2218e469 --- /dev/null +++ b/app/assets/images/emoji/mountain_railway.png diff --git a/app/assets/images/emoji/mountain_snow.png b/app/assets/images/emoji/mountain_snow.png Binary files differnew file mode 100644 index 00000000000..76e1cfd8313 --- /dev/null +++ b/app/assets/images/emoji/mountain_snow.png diff --git a/app/assets/images/emoji/mouse.png b/app/assets/images/emoji/mouse.png Binary files differnew file mode 100644 index 00000000000..50afcd3262e --- /dev/null +++ b/app/assets/images/emoji/mouse.png diff --git a/app/assets/images/emoji/mouse2.png b/app/assets/images/emoji/mouse2.png Binary files differnew file mode 100644 index 00000000000..20fb041f09f --- /dev/null +++ b/app/assets/images/emoji/mouse2.png diff --git a/app/assets/images/emoji/mouse_three_button.png b/app/assets/images/emoji/mouse_three_button.png Binary files differnew file mode 100644 index 00000000000..e84e96ff6e8 --- /dev/null +++ b/app/assets/images/emoji/mouse_three_button.png diff --git a/app/assets/images/emoji/movie_camera.png b/app/assets/images/emoji/movie_camera.png Binary files differnew file mode 100644 index 00000000000..4e73b130155 --- /dev/null +++ b/app/assets/images/emoji/movie_camera.png diff --git a/app/assets/images/emoji/moyai.png b/app/assets/images/emoji/moyai.png Binary files differnew file mode 100644 index 00000000000..e6a7779c45b --- /dev/null +++ b/app/assets/images/emoji/moyai.png diff --git a/app/assets/images/emoji/mrs_claus.png b/app/assets/images/emoji/mrs_claus.png Binary files differnew file mode 100644 index 00000000000..078f0657f95 --- /dev/null +++ b/app/assets/images/emoji/mrs_claus.png diff --git a/app/assets/images/emoji/mrs_claus_tone1.png b/app/assets/images/emoji/mrs_claus_tone1.png Binary files differnew file mode 100644 index 00000000000..d8a695d7035 --- /dev/null +++ b/app/assets/images/emoji/mrs_claus_tone1.png diff --git a/app/assets/images/emoji/mrs_claus_tone2.png b/app/assets/images/emoji/mrs_claus_tone2.png Binary files differnew file mode 100644 index 00000000000..0e17e8c51f3 --- /dev/null +++ b/app/assets/images/emoji/mrs_claus_tone2.png diff --git a/app/assets/images/emoji/mrs_claus_tone3.png b/app/assets/images/emoji/mrs_claus_tone3.png Binary files differnew file mode 100644 index 00000000000..c3ee4d1dfae --- /dev/null +++ b/app/assets/images/emoji/mrs_claus_tone3.png diff --git a/app/assets/images/emoji/mrs_claus_tone4.png b/app/assets/images/emoji/mrs_claus_tone4.png Binary files differnew file mode 100644 index 00000000000..68a556da2fe --- /dev/null +++ b/app/assets/images/emoji/mrs_claus_tone4.png diff --git a/app/assets/images/emoji/mrs_claus_tone5.png b/app/assets/images/emoji/mrs_claus_tone5.png Binary files differnew file mode 100644 index 00000000000..ccab3c40ff2 --- /dev/null +++ b/app/assets/images/emoji/mrs_claus_tone5.png diff --git a/app/assets/images/emoji/muscle.png b/app/assets/images/emoji/muscle.png Binary files differnew file mode 100644 index 00000000000..7e67c1880f7 --- /dev/null +++ b/app/assets/images/emoji/muscle.png diff --git a/app/assets/images/emoji/muscle_tone1.png b/app/assets/images/emoji/muscle_tone1.png Binary files differnew file mode 100644 index 00000000000..1522942ce51 --- /dev/null +++ b/app/assets/images/emoji/muscle_tone1.png diff --git a/app/assets/images/emoji/muscle_tone2.png b/app/assets/images/emoji/muscle_tone2.png Binary files differnew file mode 100644 index 00000000000..569c6e832ca --- /dev/null +++ b/app/assets/images/emoji/muscle_tone2.png diff --git a/app/assets/images/emoji/muscle_tone3.png b/app/assets/images/emoji/muscle_tone3.png Binary files differnew file mode 100644 index 00000000000..0a76b00fa89 --- /dev/null +++ b/app/assets/images/emoji/muscle_tone3.png diff --git a/app/assets/images/emoji/muscle_tone4.png b/app/assets/images/emoji/muscle_tone4.png Binary files differnew file mode 100644 index 00000000000..f0cf31328e0 --- /dev/null +++ b/app/assets/images/emoji/muscle_tone4.png diff --git a/app/assets/images/emoji/muscle_tone5.png b/app/assets/images/emoji/muscle_tone5.png Binary files differnew file mode 100644 index 00000000000..4fda92460e8 --- /dev/null +++ b/app/assets/images/emoji/muscle_tone5.png diff --git a/app/assets/images/emoji/mushroom.png b/app/assets/images/emoji/mushroom.png Binary files differnew file mode 100644 index 00000000000..dd85742ba2c --- /dev/null +++ b/app/assets/images/emoji/mushroom.png diff --git a/app/assets/images/emoji/musical_keyboard.png b/app/assets/images/emoji/musical_keyboard.png Binary files differnew file mode 100644 index 00000000000..442b7456842 --- /dev/null +++ b/app/assets/images/emoji/musical_keyboard.png diff --git a/app/assets/images/emoji/musical_note.png b/app/assets/images/emoji/musical_note.png Binary files differnew file mode 100644 index 00000000000..06691ef61bb --- /dev/null +++ b/app/assets/images/emoji/musical_note.png diff --git a/app/assets/images/emoji/musical_score.png b/app/assets/images/emoji/musical_score.png Binary files differnew file mode 100644 index 00000000000..47dc05a8ef5 --- /dev/null +++ b/app/assets/images/emoji/musical_score.png diff --git a/app/assets/images/emoji/mute.png b/app/assets/images/emoji/mute.png Binary files differnew file mode 100644 index 00000000000..7c1788e5075 --- /dev/null +++ b/app/assets/images/emoji/mute.png diff --git a/app/assets/images/emoji/nail_care.png b/app/assets/images/emoji/nail_care.png Binary files differnew file mode 100644 index 00000000000..aa52af7050d --- /dev/null +++ b/app/assets/images/emoji/nail_care.png diff --git a/app/assets/images/emoji/nail_care_tone1.png b/app/assets/images/emoji/nail_care_tone1.png Binary files differnew file mode 100644 index 00000000000..26e883dd244 --- /dev/null +++ b/app/assets/images/emoji/nail_care_tone1.png diff --git a/app/assets/images/emoji/nail_care_tone2.png b/app/assets/images/emoji/nail_care_tone2.png Binary files differnew file mode 100644 index 00000000000..61257b47ea3 --- /dev/null +++ b/app/assets/images/emoji/nail_care_tone2.png diff --git a/app/assets/images/emoji/nail_care_tone3.png b/app/assets/images/emoji/nail_care_tone3.png Binary files differnew file mode 100644 index 00000000000..29871b05f62 --- /dev/null +++ b/app/assets/images/emoji/nail_care_tone3.png diff --git a/app/assets/images/emoji/nail_care_tone4.png b/app/assets/images/emoji/nail_care_tone4.png Binary files differnew file mode 100644 index 00000000000..2881de0b17d --- /dev/null +++ b/app/assets/images/emoji/nail_care_tone4.png diff --git a/app/assets/images/emoji/nail_care_tone5.png b/app/assets/images/emoji/nail_care_tone5.png Binary files differnew file mode 100644 index 00000000000..a0b7c0a45a6 --- /dev/null +++ b/app/assets/images/emoji/nail_care_tone5.png diff --git a/app/assets/images/emoji/name_badge.png b/app/assets/images/emoji/name_badge.png Binary files differnew file mode 100644 index 00000000000..ec5ee213e20 --- /dev/null +++ b/app/assets/images/emoji/name_badge.png diff --git a/app/assets/images/emoji/nauseated_face.png b/app/assets/images/emoji/nauseated_face.png Binary files differnew file mode 100644 index 00000000000..a566c109c28 --- /dev/null +++ b/app/assets/images/emoji/nauseated_face.png diff --git a/app/assets/images/emoji/necktie.png b/app/assets/images/emoji/necktie.png Binary files differnew file mode 100644 index 00000000000..1804e7f3ff3 --- /dev/null +++ b/app/assets/images/emoji/necktie.png diff --git a/app/assets/images/emoji/negative_squared_cross_mark.png b/app/assets/images/emoji/negative_squared_cross_mark.png Binary files differnew file mode 100644 index 00000000000..dae487f1f98 --- /dev/null +++ b/app/assets/images/emoji/negative_squared_cross_mark.png diff --git a/app/assets/images/emoji/nerd.png b/app/assets/images/emoji/nerd.png Binary files differnew file mode 100644 index 00000000000..7820bd581dc --- /dev/null +++ b/app/assets/images/emoji/nerd.png diff --git a/app/assets/images/emoji/neutral_face.png b/app/assets/images/emoji/neutral_face.png Binary files differnew file mode 100644 index 00000000000..065d193afe4 --- /dev/null +++ b/app/assets/images/emoji/neutral_face.png diff --git a/app/assets/images/emoji/new.png b/app/assets/images/emoji/new.png Binary files differnew file mode 100644 index 00000000000..b4f85488d1a --- /dev/null +++ b/app/assets/images/emoji/new.png diff --git a/app/assets/images/emoji/new_moon.png b/app/assets/images/emoji/new_moon.png Binary files differnew file mode 100644 index 00000000000..ecff72caa42 --- /dev/null +++ b/app/assets/images/emoji/new_moon.png diff --git a/app/assets/images/emoji/new_moon_with_face.png b/app/assets/images/emoji/new_moon_with_face.png Binary files differnew file mode 100644 index 00000000000..150dd12400c --- /dev/null +++ b/app/assets/images/emoji/new_moon_with_face.png diff --git a/app/assets/images/emoji/newspaper.png b/app/assets/images/emoji/newspaper.png Binary files differnew file mode 100644 index 00000000000..2aa8f060bde --- /dev/null +++ b/app/assets/images/emoji/newspaper.png diff --git a/app/assets/images/emoji/newspaper2.png b/app/assets/images/emoji/newspaper2.png Binary files differnew file mode 100644 index 00000000000..f64748df2b2 --- /dev/null +++ b/app/assets/images/emoji/newspaper2.png diff --git a/app/assets/images/emoji/ng.png b/app/assets/images/emoji/ng.png Binary files differnew file mode 100644 index 00000000000..ee8d20f5ebc --- /dev/null +++ b/app/assets/images/emoji/ng.png diff --git a/app/assets/images/emoji/night_with_stars.png b/app/assets/images/emoji/night_with_stars.png Binary files differnew file mode 100644 index 00000000000..ca2018f456d --- /dev/null +++ b/app/assets/images/emoji/night_with_stars.png diff --git a/app/assets/images/emoji/nine.png b/app/assets/images/emoji/nine.png Binary files differnew file mode 100644 index 00000000000..9fce3d1eca9 --- /dev/null +++ b/app/assets/images/emoji/nine.png diff --git a/app/assets/images/emoji/no_bell.png b/app/assets/images/emoji/no_bell.png Binary files differnew file mode 100644 index 00000000000..15cb38dd1e7 --- /dev/null +++ b/app/assets/images/emoji/no_bell.png diff --git a/app/assets/images/emoji/no_bicycles.png b/app/assets/images/emoji/no_bicycles.png Binary files differnew file mode 100644 index 00000000000..19c85421ce9 --- /dev/null +++ b/app/assets/images/emoji/no_bicycles.png diff --git a/app/assets/images/emoji/no_entry.png b/app/assets/images/emoji/no_entry.png Binary files differnew file mode 100644 index 00000000000..476800fc5c6 --- /dev/null +++ b/app/assets/images/emoji/no_entry.png diff --git a/app/assets/images/emoji/no_entry_sign.png b/app/assets/images/emoji/no_entry_sign.png Binary files differnew file mode 100644 index 00000000000..d2efd65e74b --- /dev/null +++ b/app/assets/images/emoji/no_entry_sign.png diff --git a/app/assets/images/emoji/no_good.png b/app/assets/images/emoji/no_good.png Binary files differnew file mode 100644 index 00000000000..ed577100322 --- /dev/null +++ b/app/assets/images/emoji/no_good.png diff --git a/app/assets/images/emoji/no_good_tone1.png b/app/assets/images/emoji/no_good_tone1.png Binary files differnew file mode 100644 index 00000000000..5c1a3cbb884 --- /dev/null +++ b/app/assets/images/emoji/no_good_tone1.png diff --git a/app/assets/images/emoji/no_good_tone2.png b/app/assets/images/emoji/no_good_tone2.png Binary files differnew file mode 100644 index 00000000000..80d8021f8fe --- /dev/null +++ b/app/assets/images/emoji/no_good_tone2.png diff --git a/app/assets/images/emoji/no_good_tone3.png b/app/assets/images/emoji/no_good_tone3.png Binary files differnew file mode 100644 index 00000000000..635e6a00815 --- /dev/null +++ b/app/assets/images/emoji/no_good_tone3.png diff --git a/app/assets/images/emoji/no_good_tone4.png b/app/assets/images/emoji/no_good_tone4.png Binary files differnew file mode 100644 index 00000000000..b96e412a374 --- /dev/null +++ b/app/assets/images/emoji/no_good_tone4.png diff --git a/app/assets/images/emoji/no_good_tone5.png b/app/assets/images/emoji/no_good_tone5.png Binary files differnew file mode 100644 index 00000000000..9a7084afa0a --- /dev/null +++ b/app/assets/images/emoji/no_good_tone5.png diff --git a/app/assets/images/emoji/no_mobile_phones.png b/app/assets/images/emoji/no_mobile_phones.png Binary files differnew file mode 100644 index 00000000000..7b1ae6ea579 --- /dev/null +++ b/app/assets/images/emoji/no_mobile_phones.png diff --git a/app/assets/images/emoji/no_mouth.png b/app/assets/images/emoji/no_mouth.png Binary files differnew file mode 100644 index 00000000000..b642f6c1172 --- /dev/null +++ b/app/assets/images/emoji/no_mouth.png diff --git a/app/assets/images/emoji/no_pedestrians.png b/app/assets/images/emoji/no_pedestrians.png Binary files differnew file mode 100644 index 00000000000..286aa577a23 --- /dev/null +++ b/app/assets/images/emoji/no_pedestrians.png diff --git a/app/assets/images/emoji/no_smoking.png b/app/assets/images/emoji/no_smoking.png Binary files differnew file mode 100644 index 00000000000..586b8d29d05 --- /dev/null +++ b/app/assets/images/emoji/no_smoking.png diff --git a/app/assets/images/emoji/non-potable_water.png b/app/assets/images/emoji/non-potable_water.png Binary files differnew file mode 100644 index 00000000000..827d4193f4e --- /dev/null +++ b/app/assets/images/emoji/non-potable_water.png diff --git a/app/assets/images/emoji/nose.png b/app/assets/images/emoji/nose.png Binary files differnew file mode 100644 index 00000000000..2f04ac5f98f --- /dev/null +++ b/app/assets/images/emoji/nose.png diff --git a/app/assets/images/emoji/nose_tone1.png b/app/assets/images/emoji/nose_tone1.png Binary files differnew file mode 100644 index 00000000000..8008d17506e --- /dev/null +++ b/app/assets/images/emoji/nose_tone1.png diff --git a/app/assets/images/emoji/nose_tone2.png b/app/assets/images/emoji/nose_tone2.png Binary files differnew file mode 100644 index 00000000000..ac17f26e827 --- /dev/null +++ b/app/assets/images/emoji/nose_tone2.png diff --git a/app/assets/images/emoji/nose_tone3.png b/app/assets/images/emoji/nose_tone3.png Binary files differnew file mode 100644 index 00000000000..d8b6cbe0f8e --- /dev/null +++ b/app/assets/images/emoji/nose_tone3.png diff --git a/app/assets/images/emoji/nose_tone4.png b/app/assets/images/emoji/nose_tone4.png Binary files differnew file mode 100644 index 00000000000..004b2631e2e --- /dev/null +++ b/app/assets/images/emoji/nose_tone4.png diff --git a/app/assets/images/emoji/nose_tone5.png b/app/assets/images/emoji/nose_tone5.png Binary files differnew file mode 100644 index 00000000000..7b33821f6c9 --- /dev/null +++ b/app/assets/images/emoji/nose_tone5.png diff --git a/app/assets/images/emoji/notebook.png b/app/assets/images/emoji/notebook.png Binary files differnew file mode 100644 index 00000000000..f6c28b4915d --- /dev/null +++ b/app/assets/images/emoji/notebook.png diff --git a/app/assets/images/emoji/notebook_with_decorative_cover.png b/app/assets/images/emoji/notebook_with_decorative_cover.png Binary files differnew file mode 100644 index 00000000000..03f566b6d2c --- /dev/null +++ b/app/assets/images/emoji/notebook_with_decorative_cover.png diff --git a/app/assets/images/emoji/notepad_spiral.png b/app/assets/images/emoji/notepad_spiral.png Binary files differnew file mode 100644 index 00000000000..85faa10d8ea --- /dev/null +++ b/app/assets/images/emoji/notepad_spiral.png diff --git a/app/assets/images/emoji/notes.png b/app/assets/images/emoji/notes.png Binary files differnew file mode 100644 index 00000000000..57d499aa181 --- /dev/null +++ b/app/assets/images/emoji/notes.png diff --git a/app/assets/images/emoji/nut_and_bolt.png b/app/assets/images/emoji/nut_and_bolt.png Binary files differnew file mode 100644 index 00000000000..4b9ae155319 --- /dev/null +++ b/app/assets/images/emoji/nut_and_bolt.png diff --git a/app/assets/images/emoji/o.png b/app/assets/images/emoji/o.png Binary files differnew file mode 100644 index 00000000000..3fe75ce4675 --- /dev/null +++ b/app/assets/images/emoji/o.png diff --git a/app/assets/images/emoji/o2.png b/app/assets/images/emoji/o2.png Binary files differnew file mode 100644 index 00000000000..73278ba194a --- /dev/null +++ b/app/assets/images/emoji/o2.png diff --git a/app/assets/images/emoji/ocean.png b/app/assets/images/emoji/ocean.png Binary files differnew file mode 100644 index 00000000000..45ff1e87703 --- /dev/null +++ b/app/assets/images/emoji/ocean.png diff --git a/app/assets/images/emoji/octagonal_sign.png b/app/assets/images/emoji/octagonal_sign.png Binary files differnew file mode 100644 index 00000000000..5ed61004045 --- /dev/null +++ b/app/assets/images/emoji/octagonal_sign.png diff --git a/app/assets/images/emoji/octopus.png b/app/assets/images/emoji/octopus.png Binary files differnew file mode 100644 index 00000000000..72c84074aac --- /dev/null +++ b/app/assets/images/emoji/octopus.png diff --git a/app/assets/images/emoji/oden.png b/app/assets/images/emoji/oden.png Binary files differnew file mode 100644 index 00000000000..d38a849fece --- /dev/null +++ b/app/assets/images/emoji/oden.png diff --git a/app/assets/images/emoji/office.png b/app/assets/images/emoji/office.png Binary files differnew file mode 100644 index 00000000000..7eee927d1b0 --- /dev/null +++ b/app/assets/images/emoji/office.png diff --git a/app/assets/images/emoji/oil.png b/app/assets/images/emoji/oil.png Binary files differnew file mode 100644 index 00000000000..c4c4d42da8b --- /dev/null +++ b/app/assets/images/emoji/oil.png diff --git a/app/assets/images/emoji/ok.png b/app/assets/images/emoji/ok.png Binary files differnew file mode 100644 index 00000000000..d0d775532ff --- /dev/null +++ b/app/assets/images/emoji/ok.png diff --git a/app/assets/images/emoji/ok_hand.png b/app/assets/images/emoji/ok_hand.png Binary files differnew file mode 100644 index 00000000000..028d69b0de3 --- /dev/null +++ b/app/assets/images/emoji/ok_hand.png diff --git a/app/assets/images/emoji/ok_hand_tone1.png b/app/assets/images/emoji/ok_hand_tone1.png Binary files differnew file mode 100644 index 00000000000..cecf7b2ab5a --- /dev/null +++ b/app/assets/images/emoji/ok_hand_tone1.png diff --git a/app/assets/images/emoji/ok_hand_tone2.png b/app/assets/images/emoji/ok_hand_tone2.png Binary files differnew file mode 100644 index 00000000000..c19239bcd3d --- /dev/null +++ b/app/assets/images/emoji/ok_hand_tone2.png diff --git a/app/assets/images/emoji/ok_hand_tone3.png b/app/assets/images/emoji/ok_hand_tone3.png Binary files differnew file mode 100644 index 00000000000..94b65b03ecd --- /dev/null +++ b/app/assets/images/emoji/ok_hand_tone3.png diff --git a/app/assets/images/emoji/ok_hand_tone4.png b/app/assets/images/emoji/ok_hand_tone4.png Binary files differnew file mode 100644 index 00000000000..03d26f08e6a --- /dev/null +++ b/app/assets/images/emoji/ok_hand_tone4.png diff --git a/app/assets/images/emoji/ok_hand_tone5.png b/app/assets/images/emoji/ok_hand_tone5.png Binary files differnew file mode 100644 index 00000000000..d4b24086364 --- /dev/null +++ b/app/assets/images/emoji/ok_hand_tone5.png diff --git a/app/assets/images/emoji/ok_woman.png b/app/assets/images/emoji/ok_woman.png Binary files differnew file mode 100644 index 00000000000..90a2c7469c4 --- /dev/null +++ b/app/assets/images/emoji/ok_woman.png diff --git a/app/assets/images/emoji/ok_woman_tone1.png b/app/assets/images/emoji/ok_woman_tone1.png Binary files differnew file mode 100644 index 00000000000..c99543e785b --- /dev/null +++ b/app/assets/images/emoji/ok_woman_tone1.png diff --git a/app/assets/images/emoji/ok_woman_tone2.png b/app/assets/images/emoji/ok_woman_tone2.png Binary files differnew file mode 100644 index 00000000000..ad5fae813db --- /dev/null +++ b/app/assets/images/emoji/ok_woman_tone2.png diff --git a/app/assets/images/emoji/ok_woman_tone3.png b/app/assets/images/emoji/ok_woman_tone3.png Binary files differnew file mode 100644 index 00000000000..51bf4fab406 --- /dev/null +++ b/app/assets/images/emoji/ok_woman_tone3.png diff --git a/app/assets/images/emoji/ok_woman_tone4.png b/app/assets/images/emoji/ok_woman_tone4.png Binary files differnew file mode 100644 index 00000000000..ee3f9dc640a --- /dev/null +++ b/app/assets/images/emoji/ok_woman_tone4.png diff --git a/app/assets/images/emoji/ok_woman_tone5.png b/app/assets/images/emoji/ok_woman_tone5.png Binary files differnew file mode 100644 index 00000000000..62a9d9237f7 --- /dev/null +++ b/app/assets/images/emoji/ok_woman_tone5.png diff --git a/app/assets/images/emoji/older_man.png b/app/assets/images/emoji/older_man.png Binary files differnew file mode 100644 index 00000000000..4ace4e6f308 --- /dev/null +++ b/app/assets/images/emoji/older_man.png diff --git a/app/assets/images/emoji/older_man_tone1.png b/app/assets/images/emoji/older_man_tone1.png Binary files differnew file mode 100644 index 00000000000..ab459baace8 --- /dev/null +++ b/app/assets/images/emoji/older_man_tone1.png diff --git a/app/assets/images/emoji/older_man_tone2.png b/app/assets/images/emoji/older_man_tone2.png Binary files differnew file mode 100644 index 00000000000..f4dfc7694ea --- /dev/null +++ b/app/assets/images/emoji/older_man_tone2.png diff --git a/app/assets/images/emoji/older_man_tone3.png b/app/assets/images/emoji/older_man_tone3.png Binary files differnew file mode 100644 index 00000000000..5ffd11792f4 --- /dev/null +++ b/app/assets/images/emoji/older_man_tone3.png diff --git a/app/assets/images/emoji/older_man_tone4.png b/app/assets/images/emoji/older_man_tone4.png Binary files differnew file mode 100644 index 00000000000..b350a764bfd --- /dev/null +++ b/app/assets/images/emoji/older_man_tone4.png diff --git a/app/assets/images/emoji/older_man_tone5.png b/app/assets/images/emoji/older_man_tone5.png Binary files differnew file mode 100644 index 00000000000..05fe24a1708 --- /dev/null +++ b/app/assets/images/emoji/older_man_tone5.png diff --git a/app/assets/images/emoji/older_woman.png b/app/assets/images/emoji/older_woman.png Binary files differnew file mode 100644 index 00000000000..52dc4987143 --- /dev/null +++ b/app/assets/images/emoji/older_woman.png diff --git a/app/assets/images/emoji/older_woman_tone1.png b/app/assets/images/emoji/older_woman_tone1.png Binary files differnew file mode 100644 index 00000000000..b49e821402c --- /dev/null +++ b/app/assets/images/emoji/older_woman_tone1.png diff --git a/app/assets/images/emoji/older_woman_tone2.png b/app/assets/images/emoji/older_woman_tone2.png Binary files differnew file mode 100644 index 00000000000..e86bf5ab3b7 --- /dev/null +++ b/app/assets/images/emoji/older_woman_tone2.png diff --git a/app/assets/images/emoji/older_woman_tone3.png b/app/assets/images/emoji/older_woman_tone3.png Binary files differnew file mode 100644 index 00000000000..83fc14b0874 --- /dev/null +++ b/app/assets/images/emoji/older_woman_tone3.png diff --git a/app/assets/images/emoji/older_woman_tone4.png b/app/assets/images/emoji/older_woman_tone4.png Binary files differnew file mode 100644 index 00000000000..e4aa8a424d4 --- /dev/null +++ b/app/assets/images/emoji/older_woman_tone4.png diff --git a/app/assets/images/emoji/older_woman_tone5.png b/app/assets/images/emoji/older_woman_tone5.png Binary files differnew file mode 100644 index 00000000000..4009012bb0a --- /dev/null +++ b/app/assets/images/emoji/older_woman_tone5.png diff --git a/app/assets/images/emoji/om_symbol.png b/app/assets/images/emoji/om_symbol.png Binary files differnew file mode 100644 index 00000000000..a35c63c459c --- /dev/null +++ b/app/assets/images/emoji/om_symbol.png diff --git a/app/assets/images/emoji/on.png b/app/assets/images/emoji/on.png Binary files differnew file mode 100644 index 00000000000..a0c371ae21e --- /dev/null +++ b/app/assets/images/emoji/on.png diff --git a/app/assets/images/emoji/oncoming_automobile.png b/app/assets/images/emoji/oncoming_automobile.png Binary files differnew file mode 100644 index 00000000000..3c7e1d52e63 --- /dev/null +++ b/app/assets/images/emoji/oncoming_automobile.png diff --git a/app/assets/images/emoji/oncoming_bus.png b/app/assets/images/emoji/oncoming_bus.png Binary files differnew file mode 100644 index 00000000000..ad91e256c7f --- /dev/null +++ b/app/assets/images/emoji/oncoming_bus.png diff --git a/app/assets/images/emoji/oncoming_police_car.png b/app/assets/images/emoji/oncoming_police_car.png Binary files differnew file mode 100644 index 00000000000..c9109c85b5d --- /dev/null +++ b/app/assets/images/emoji/oncoming_police_car.png diff --git a/app/assets/images/emoji/oncoming_taxi.png b/app/assets/images/emoji/oncoming_taxi.png Binary files differnew file mode 100644 index 00000000000..fea14e45846 --- /dev/null +++ b/app/assets/images/emoji/oncoming_taxi.png diff --git a/app/assets/images/emoji/one.png b/app/assets/images/emoji/one.png Binary files differnew file mode 100644 index 00000000000..e6d84b80128 --- /dev/null +++ b/app/assets/images/emoji/one.png diff --git a/app/assets/images/emoji/open_file_folder.png b/app/assets/images/emoji/open_file_folder.png Binary files differnew file mode 100644 index 00000000000..3993b09222f --- /dev/null +++ b/app/assets/images/emoji/open_file_folder.png diff --git a/app/assets/images/emoji/open_hands.png b/app/assets/images/emoji/open_hands.png Binary files differnew file mode 100644 index 00000000000..1cf75c9101e --- /dev/null +++ b/app/assets/images/emoji/open_hands.png diff --git a/app/assets/images/emoji/open_hands_tone1.png b/app/assets/images/emoji/open_hands_tone1.png Binary files differnew file mode 100644 index 00000000000..352d2614f11 --- /dev/null +++ b/app/assets/images/emoji/open_hands_tone1.png diff --git a/app/assets/images/emoji/open_hands_tone2.png b/app/assets/images/emoji/open_hands_tone2.png Binary files differnew file mode 100644 index 00000000000..70824a50c73 --- /dev/null +++ b/app/assets/images/emoji/open_hands_tone2.png diff --git a/app/assets/images/emoji/open_hands_tone3.png b/app/assets/images/emoji/open_hands_tone3.png Binary files differnew file mode 100644 index 00000000000..d7d136bd3db --- /dev/null +++ b/app/assets/images/emoji/open_hands_tone3.png diff --git a/app/assets/images/emoji/open_hands_tone4.png b/app/assets/images/emoji/open_hands_tone4.png Binary files differnew file mode 100644 index 00000000000..df4eaa711e7 --- /dev/null +++ b/app/assets/images/emoji/open_hands_tone4.png diff --git a/app/assets/images/emoji/open_hands_tone5.png b/app/assets/images/emoji/open_hands_tone5.png Binary files differnew file mode 100644 index 00000000000..7dc04eaebd8 --- /dev/null +++ b/app/assets/images/emoji/open_hands_tone5.png diff --git a/app/assets/images/emoji/open_mouth.png b/app/assets/images/emoji/open_mouth.png Binary files differnew file mode 100644 index 00000000000..a62cd27e148 --- /dev/null +++ b/app/assets/images/emoji/open_mouth.png diff --git a/app/assets/images/emoji/ophiuchus.png b/app/assets/images/emoji/ophiuchus.png Binary files differnew file mode 100644 index 00000000000..0a780a700da --- /dev/null +++ b/app/assets/images/emoji/ophiuchus.png diff --git a/app/assets/images/emoji/orange_book.png b/app/assets/images/emoji/orange_book.png Binary files differnew file mode 100644 index 00000000000..ab40e6ae6a2 --- /dev/null +++ b/app/assets/images/emoji/orange_book.png diff --git a/app/assets/images/emoji/orthodox_cross.png b/app/assets/images/emoji/orthodox_cross.png Binary files differnew file mode 100644 index 00000000000..0530e33a4d4 --- /dev/null +++ b/app/assets/images/emoji/orthodox_cross.png diff --git a/app/assets/images/emoji/outbox_tray.png b/app/assets/images/emoji/outbox_tray.png Binary files differnew file mode 100644 index 00000000000..46493ed5b2c --- /dev/null +++ b/app/assets/images/emoji/outbox_tray.png diff --git a/app/assets/images/emoji/owl.png b/app/assets/images/emoji/owl.png Binary files differnew file mode 100644 index 00000000000..fa6815480c3 --- /dev/null +++ b/app/assets/images/emoji/owl.png diff --git a/app/assets/images/emoji/ox.png b/app/assets/images/emoji/ox.png Binary files differnew file mode 100644 index 00000000000..badf5708f2f --- /dev/null +++ b/app/assets/images/emoji/ox.png diff --git a/app/assets/images/emoji/package.png b/app/assets/images/emoji/package.png Binary files differnew file mode 100644 index 00000000000..85431756ad8 --- /dev/null +++ b/app/assets/images/emoji/package.png diff --git a/app/assets/images/emoji/page_facing_up.png b/app/assets/images/emoji/page_facing_up.png Binary files differnew file mode 100644 index 00000000000..ba4ed757e01 --- /dev/null +++ b/app/assets/images/emoji/page_facing_up.png diff --git a/app/assets/images/emoji/page_with_curl.png b/app/assets/images/emoji/page_with_curl.png Binary files differnew file mode 100644 index 00000000000..06355319c74 --- /dev/null +++ b/app/assets/images/emoji/page_with_curl.png diff --git a/app/assets/images/emoji/pager.png b/app/assets/images/emoji/pager.png Binary files differnew file mode 100644 index 00000000000..b24b99306a2 --- /dev/null +++ b/app/assets/images/emoji/pager.png diff --git a/app/assets/images/emoji/paintbrush.png b/app/assets/images/emoji/paintbrush.png Binary files differnew file mode 100644 index 00000000000..28bffbaa3c9 --- /dev/null +++ b/app/assets/images/emoji/paintbrush.png diff --git a/app/assets/images/emoji/palm_tree.png b/app/assets/images/emoji/palm_tree.png Binary files differnew file mode 100644 index 00000000000..4bbb10f4f19 --- /dev/null +++ b/app/assets/images/emoji/palm_tree.png diff --git a/app/assets/images/emoji/pancakes.png b/app/assets/images/emoji/pancakes.png Binary files differnew file mode 100644 index 00000000000..6223d1a28e9 --- /dev/null +++ b/app/assets/images/emoji/pancakes.png diff --git a/app/assets/images/emoji/panda_face.png b/app/assets/images/emoji/panda_face.png Binary files differnew file mode 100644 index 00000000000..978382775ce --- /dev/null +++ b/app/assets/images/emoji/panda_face.png diff --git a/app/assets/images/emoji/paperclip.png b/app/assets/images/emoji/paperclip.png Binary files differnew file mode 100644 index 00000000000..8cd8d4f8750 --- /dev/null +++ b/app/assets/images/emoji/paperclip.png diff --git a/app/assets/images/emoji/paperclips.png b/app/assets/images/emoji/paperclips.png Binary files differnew file mode 100644 index 00000000000..76021e8c705 --- /dev/null +++ b/app/assets/images/emoji/paperclips.png diff --git a/app/assets/images/emoji/park.png b/app/assets/images/emoji/park.png Binary files differnew file mode 100644 index 00000000000..63ec7016301 --- /dev/null +++ b/app/assets/images/emoji/park.png diff --git a/app/assets/images/emoji/parking.png b/app/assets/images/emoji/parking.png Binary files differnew file mode 100644 index 00000000000..7be7dac27e8 --- /dev/null +++ b/app/assets/images/emoji/parking.png diff --git a/app/assets/images/emoji/part_alternation_mark.png b/app/assets/images/emoji/part_alternation_mark.png Binary files differnew file mode 100644 index 00000000000..70453d41528 --- /dev/null +++ b/app/assets/images/emoji/part_alternation_mark.png diff --git a/app/assets/images/emoji/partly_sunny.png b/app/assets/images/emoji/partly_sunny.png Binary files differnew file mode 100644 index 00000000000..a55e59c344c --- /dev/null +++ b/app/assets/images/emoji/partly_sunny.png diff --git a/app/assets/images/emoji/passport_control.png b/app/assets/images/emoji/passport_control.png Binary files differnew file mode 100644 index 00000000000..079e34ee4d4 --- /dev/null +++ b/app/assets/images/emoji/passport_control.png diff --git a/app/assets/images/emoji/pause_button.png b/app/assets/images/emoji/pause_button.png Binary files differnew file mode 100644 index 00000000000..4f07e7ebfd7 --- /dev/null +++ b/app/assets/images/emoji/pause_button.png diff --git a/app/assets/images/emoji/peace.png b/app/assets/images/emoji/peace.png Binary files differnew file mode 100644 index 00000000000..86033faf477 --- /dev/null +++ b/app/assets/images/emoji/peace.png diff --git a/app/assets/images/emoji/peach.png b/app/assets/images/emoji/peach.png Binary files differnew file mode 100644 index 00000000000..9ab57cbb758 --- /dev/null +++ b/app/assets/images/emoji/peach.png diff --git a/app/assets/images/emoji/peanuts.png b/app/assets/images/emoji/peanuts.png Binary files differnew file mode 100644 index 00000000000..b64fadad010 --- /dev/null +++ b/app/assets/images/emoji/peanuts.png diff --git a/app/assets/images/emoji/pear.png b/app/assets/images/emoji/pear.png Binary files differnew file mode 100644 index 00000000000..3869f718bcf --- /dev/null +++ b/app/assets/images/emoji/pear.png diff --git a/app/assets/images/emoji/pen_ballpoint.png b/app/assets/images/emoji/pen_ballpoint.png Binary files differnew file mode 100644 index 00000000000..6ef7a342433 --- /dev/null +++ b/app/assets/images/emoji/pen_ballpoint.png diff --git a/app/assets/images/emoji/pen_fountain.png b/app/assets/images/emoji/pen_fountain.png Binary files differnew file mode 100644 index 00000000000..3ca4bd2c231 --- /dev/null +++ b/app/assets/images/emoji/pen_fountain.png diff --git a/app/assets/images/emoji/pencil.png b/app/assets/images/emoji/pencil.png Binary files differnew file mode 100644 index 00000000000..edc6155e168 --- /dev/null +++ b/app/assets/images/emoji/pencil.png diff --git a/app/assets/images/emoji/pencil2.png b/app/assets/images/emoji/pencil2.png Binary files differnew file mode 100644 index 00000000000..3833d590fa2 --- /dev/null +++ b/app/assets/images/emoji/pencil2.png diff --git a/app/assets/images/emoji/penguin.png b/app/assets/images/emoji/penguin.png Binary files differnew file mode 100644 index 00000000000..c0064fb9734 --- /dev/null +++ b/app/assets/images/emoji/penguin.png diff --git a/app/assets/images/emoji/pensive.png b/app/assets/images/emoji/pensive.png Binary files differnew file mode 100644 index 00000000000..490fb566954 --- /dev/null +++ b/app/assets/images/emoji/pensive.png diff --git a/app/assets/images/emoji/performing_arts.png b/app/assets/images/emoji/performing_arts.png Binary files differnew file mode 100644 index 00000000000..685441fdaa1 --- /dev/null +++ b/app/assets/images/emoji/performing_arts.png diff --git a/app/assets/images/emoji/persevere.png b/app/assets/images/emoji/persevere.png Binary files differnew file mode 100644 index 00000000000..646a05fe908 --- /dev/null +++ b/app/assets/images/emoji/persevere.png diff --git a/app/assets/images/emoji/person_frowning.png b/app/assets/images/emoji/person_frowning.png Binary files differnew file mode 100644 index 00000000000..579324959a1 --- /dev/null +++ b/app/assets/images/emoji/person_frowning.png diff --git a/app/assets/images/emoji/person_frowning_tone1.png b/app/assets/images/emoji/person_frowning_tone1.png Binary files differnew file mode 100644 index 00000000000..21d3bb43923 --- /dev/null +++ b/app/assets/images/emoji/person_frowning_tone1.png diff --git a/app/assets/images/emoji/person_frowning_tone2.png b/app/assets/images/emoji/person_frowning_tone2.png Binary files differnew file mode 100644 index 00000000000..973f5fc8382 --- /dev/null +++ b/app/assets/images/emoji/person_frowning_tone2.png diff --git a/app/assets/images/emoji/person_frowning_tone3.png b/app/assets/images/emoji/person_frowning_tone3.png Binary files differnew file mode 100644 index 00000000000..41fbcc78816 --- /dev/null +++ b/app/assets/images/emoji/person_frowning_tone3.png diff --git a/app/assets/images/emoji/person_frowning_tone4.png b/app/assets/images/emoji/person_frowning_tone4.png Binary files differnew file mode 100644 index 00000000000..5a37c741030 --- /dev/null +++ b/app/assets/images/emoji/person_frowning_tone4.png diff --git a/app/assets/images/emoji/person_frowning_tone5.png b/app/assets/images/emoji/person_frowning_tone5.png Binary files differnew file mode 100644 index 00000000000..e08141f3efe --- /dev/null +++ b/app/assets/images/emoji/person_frowning_tone5.png diff --git a/app/assets/images/emoji/person_with_blond_hair.png b/app/assets/images/emoji/person_with_blond_hair.png Binary files differnew file mode 100644 index 00000000000..ad6f01a7dda --- /dev/null +++ b/app/assets/images/emoji/person_with_blond_hair.png diff --git a/app/assets/images/emoji/person_with_blond_hair_tone1.png b/app/assets/images/emoji/person_with_blond_hair_tone1.png Binary files differnew file mode 100644 index 00000000000..7d18ef24445 --- /dev/null +++ b/app/assets/images/emoji/person_with_blond_hair_tone1.png diff --git a/app/assets/images/emoji/person_with_blond_hair_tone2.png b/app/assets/images/emoji/person_with_blond_hair_tone2.png Binary files differnew file mode 100644 index 00000000000..dae1307315c --- /dev/null +++ b/app/assets/images/emoji/person_with_blond_hair_tone2.png diff --git a/app/assets/images/emoji/person_with_blond_hair_tone3.png b/app/assets/images/emoji/person_with_blond_hair_tone3.png Binary files differnew file mode 100644 index 00000000000..684677e8e5a --- /dev/null +++ b/app/assets/images/emoji/person_with_blond_hair_tone3.png diff --git a/app/assets/images/emoji/person_with_blond_hair_tone4.png b/app/assets/images/emoji/person_with_blond_hair_tone4.png Binary files differnew file mode 100644 index 00000000000..012be0b51f8 --- /dev/null +++ b/app/assets/images/emoji/person_with_blond_hair_tone4.png diff --git a/app/assets/images/emoji/person_with_blond_hair_tone5.png b/app/assets/images/emoji/person_with_blond_hair_tone5.png Binary files differnew file mode 100644 index 00000000000..d4ecc4cf44b --- /dev/null +++ b/app/assets/images/emoji/person_with_blond_hair_tone5.png diff --git a/app/assets/images/emoji/person_with_pouting_face.png b/app/assets/images/emoji/person_with_pouting_face.png Binary files differnew file mode 100644 index 00000000000..10eb0571078 --- /dev/null +++ b/app/assets/images/emoji/person_with_pouting_face.png diff --git a/app/assets/images/emoji/person_with_pouting_face_tone1.png b/app/assets/images/emoji/person_with_pouting_face_tone1.png Binary files differnew file mode 100644 index 00000000000..57e826b75a4 --- /dev/null +++ b/app/assets/images/emoji/person_with_pouting_face_tone1.png diff --git a/app/assets/images/emoji/person_with_pouting_face_tone2.png b/app/assets/images/emoji/person_with_pouting_face_tone2.png Binary files differnew file mode 100644 index 00000000000..3f317c0c25f --- /dev/null +++ b/app/assets/images/emoji/person_with_pouting_face_tone2.png diff --git a/app/assets/images/emoji/person_with_pouting_face_tone3.png b/app/assets/images/emoji/person_with_pouting_face_tone3.png Binary files differnew file mode 100644 index 00000000000..d2fbb6c20bf --- /dev/null +++ b/app/assets/images/emoji/person_with_pouting_face_tone3.png diff --git a/app/assets/images/emoji/person_with_pouting_face_tone4.png b/app/assets/images/emoji/person_with_pouting_face_tone4.png Binary files differnew file mode 100644 index 00000000000..643ceb4a5c5 --- /dev/null +++ b/app/assets/images/emoji/person_with_pouting_face_tone4.png diff --git a/app/assets/images/emoji/person_with_pouting_face_tone5.png b/app/assets/images/emoji/person_with_pouting_face_tone5.png Binary files differnew file mode 100644 index 00000000000..b2eb6859c32 --- /dev/null +++ b/app/assets/images/emoji/person_with_pouting_face_tone5.png diff --git a/app/assets/images/emoji/pick.png b/app/assets/images/emoji/pick.png Binary files differnew file mode 100644 index 00000000000..6370fe6d791 --- /dev/null +++ b/app/assets/images/emoji/pick.png diff --git a/app/assets/images/emoji/pig.png b/app/assets/images/emoji/pig.png Binary files differnew file mode 100644 index 00000000000..afe05ca1676 --- /dev/null +++ b/app/assets/images/emoji/pig.png diff --git a/app/assets/images/emoji/pig2.png b/app/assets/images/emoji/pig2.png Binary files differnew file mode 100644 index 00000000000..5f31c1a2d75 --- /dev/null +++ b/app/assets/images/emoji/pig2.png diff --git a/app/assets/images/emoji/pig_nose.png b/app/assets/images/emoji/pig_nose.png Binary files differnew file mode 100644 index 00000000000..3610ae4a910 --- /dev/null +++ b/app/assets/images/emoji/pig_nose.png diff --git a/app/assets/images/emoji/pill.png b/app/assets/images/emoji/pill.png Binary files differnew file mode 100644 index 00000000000..1d4530e77a3 --- /dev/null +++ b/app/assets/images/emoji/pill.png diff --git a/app/assets/images/emoji/pineapple.png b/app/assets/images/emoji/pineapple.png Binary files differnew file mode 100644 index 00000000000..c89a1606462 --- /dev/null +++ b/app/assets/images/emoji/pineapple.png diff --git a/app/assets/images/emoji/ping_pong.png b/app/assets/images/emoji/ping_pong.png Binary files differnew file mode 100644 index 00000000000..ff3c51727d1 --- /dev/null +++ b/app/assets/images/emoji/ping_pong.png diff --git a/app/assets/images/emoji/pisces.png b/app/assets/images/emoji/pisces.png Binary files differnew file mode 100644 index 00000000000..7f6f646a95c --- /dev/null +++ b/app/assets/images/emoji/pisces.png diff --git a/app/assets/images/emoji/pizza.png b/app/assets/images/emoji/pizza.png Binary files differnew file mode 100644 index 00000000000..e07365cb398 --- /dev/null +++ b/app/assets/images/emoji/pizza.png diff --git a/app/assets/images/emoji/place_of_worship.png b/app/assets/images/emoji/place_of_worship.png Binary files differnew file mode 100644 index 00000000000..207d59cce85 --- /dev/null +++ b/app/assets/images/emoji/place_of_worship.png diff --git a/app/assets/images/emoji/play_pause.png b/app/assets/images/emoji/play_pause.png Binary files differnew file mode 100644 index 00000000000..a9f857139ac --- /dev/null +++ b/app/assets/images/emoji/play_pause.png diff --git a/app/assets/images/emoji/point_down.png b/app/assets/images/emoji/point_down.png Binary files differnew file mode 100644 index 00000000000..00d3d13ab5c --- /dev/null +++ b/app/assets/images/emoji/point_down.png diff --git a/app/assets/images/emoji/point_down_tone1.png b/app/assets/images/emoji/point_down_tone1.png Binary files differnew file mode 100644 index 00000000000..140f157d8c7 --- /dev/null +++ b/app/assets/images/emoji/point_down_tone1.png diff --git a/app/assets/images/emoji/point_down_tone2.png b/app/assets/images/emoji/point_down_tone2.png Binary files differnew file mode 100644 index 00000000000..d518544f7fa --- /dev/null +++ b/app/assets/images/emoji/point_down_tone2.png diff --git a/app/assets/images/emoji/point_down_tone3.png b/app/assets/images/emoji/point_down_tone3.png Binary files differnew file mode 100644 index 00000000000..018b688b8b7 --- /dev/null +++ b/app/assets/images/emoji/point_down_tone3.png diff --git a/app/assets/images/emoji/point_down_tone4.png b/app/assets/images/emoji/point_down_tone4.png Binary files differnew file mode 100644 index 00000000000..98845bf6f72 --- /dev/null +++ b/app/assets/images/emoji/point_down_tone4.png diff --git a/app/assets/images/emoji/point_down_tone5.png b/app/assets/images/emoji/point_down_tone5.png Binary files differnew file mode 100644 index 00000000000..9a9b039a9fc --- /dev/null +++ b/app/assets/images/emoji/point_down_tone5.png diff --git a/app/assets/images/emoji/point_left.png b/app/assets/images/emoji/point_left.png Binary files differnew file mode 100644 index 00000000000..599fa2e3cf1 --- /dev/null +++ b/app/assets/images/emoji/point_left.png diff --git a/app/assets/images/emoji/point_left_tone1.png b/app/assets/images/emoji/point_left_tone1.png Binary files differnew file mode 100644 index 00000000000..88e2c306076 --- /dev/null +++ b/app/assets/images/emoji/point_left_tone1.png diff --git a/app/assets/images/emoji/point_left_tone2.png b/app/assets/images/emoji/point_left_tone2.png Binary files differnew file mode 100644 index 00000000000..d3c89d87c5f --- /dev/null +++ b/app/assets/images/emoji/point_left_tone2.png diff --git a/app/assets/images/emoji/point_left_tone3.png b/app/assets/images/emoji/point_left_tone3.png Binary files differnew file mode 100644 index 00000000000..b23b9167358 --- /dev/null +++ b/app/assets/images/emoji/point_left_tone3.png diff --git a/app/assets/images/emoji/point_left_tone4.png b/app/assets/images/emoji/point_left_tone4.png Binary files differnew file mode 100644 index 00000000000..3093f325c27 --- /dev/null +++ b/app/assets/images/emoji/point_left_tone4.png diff --git a/app/assets/images/emoji/point_left_tone5.png b/app/assets/images/emoji/point_left_tone5.png Binary files differnew file mode 100644 index 00000000000..2b4cbfa120c --- /dev/null +++ b/app/assets/images/emoji/point_left_tone5.png diff --git a/app/assets/images/emoji/point_right.png b/app/assets/images/emoji/point_right.png Binary files differnew file mode 100644 index 00000000000..93a3cd34aa5 --- /dev/null +++ b/app/assets/images/emoji/point_right.png diff --git a/app/assets/images/emoji/point_right_tone1.png b/app/assets/images/emoji/point_right_tone1.png Binary files differnew file mode 100644 index 00000000000..4a28c6bbc89 --- /dev/null +++ b/app/assets/images/emoji/point_right_tone1.png diff --git a/app/assets/images/emoji/point_right_tone2.png b/app/assets/images/emoji/point_right_tone2.png Binary files differnew file mode 100644 index 00000000000..7cb13231733 --- /dev/null +++ b/app/assets/images/emoji/point_right_tone2.png diff --git a/app/assets/images/emoji/point_right_tone3.png b/app/assets/images/emoji/point_right_tone3.png Binary files differnew file mode 100644 index 00000000000..5514807d71a --- /dev/null +++ b/app/assets/images/emoji/point_right_tone3.png diff --git a/app/assets/images/emoji/point_right_tone4.png b/app/assets/images/emoji/point_right_tone4.png Binary files differnew file mode 100644 index 00000000000..b8541d6440d --- /dev/null +++ b/app/assets/images/emoji/point_right_tone4.png diff --git a/app/assets/images/emoji/point_right_tone5.png b/app/assets/images/emoji/point_right_tone5.png Binary files differnew file mode 100644 index 00000000000..1b7aab07bb1 --- /dev/null +++ b/app/assets/images/emoji/point_right_tone5.png diff --git a/app/assets/images/emoji/point_up.png b/app/assets/images/emoji/point_up.png Binary files differnew file mode 100644 index 00000000000..f4978ff0f00 --- /dev/null +++ b/app/assets/images/emoji/point_up.png diff --git a/app/assets/images/emoji/point_up_2.png b/app/assets/images/emoji/point_up_2.png Binary files differnew file mode 100644 index 00000000000..bc496dfeae4 --- /dev/null +++ b/app/assets/images/emoji/point_up_2.png diff --git a/app/assets/images/emoji/point_up_2_tone1.png b/app/assets/images/emoji/point_up_2_tone1.png Binary files differnew file mode 100644 index 00000000000..a12a7e78430 --- /dev/null +++ b/app/assets/images/emoji/point_up_2_tone1.png diff --git a/app/assets/images/emoji/point_up_2_tone2.png b/app/assets/images/emoji/point_up_2_tone2.png Binary files differnew file mode 100644 index 00000000000..cdff40ceab0 --- /dev/null +++ b/app/assets/images/emoji/point_up_2_tone2.png diff --git a/app/assets/images/emoji/point_up_2_tone3.png b/app/assets/images/emoji/point_up_2_tone3.png Binary files differnew file mode 100644 index 00000000000..a07ce9e5ae8 --- /dev/null +++ b/app/assets/images/emoji/point_up_2_tone3.png diff --git a/app/assets/images/emoji/point_up_2_tone4.png b/app/assets/images/emoji/point_up_2_tone4.png Binary files differnew file mode 100644 index 00000000000..4f86c88ba42 --- /dev/null +++ b/app/assets/images/emoji/point_up_2_tone4.png diff --git a/app/assets/images/emoji/point_up_2_tone5.png b/app/assets/images/emoji/point_up_2_tone5.png Binary files differnew file mode 100644 index 00000000000..ed1b26c35d3 --- /dev/null +++ b/app/assets/images/emoji/point_up_2_tone5.png diff --git a/app/assets/images/emoji/point_up_tone1.png b/app/assets/images/emoji/point_up_tone1.png Binary files differnew file mode 100644 index 00000000000..6a9db21d64c --- /dev/null +++ b/app/assets/images/emoji/point_up_tone1.png diff --git a/app/assets/images/emoji/point_up_tone2.png b/app/assets/images/emoji/point_up_tone2.png Binary files differnew file mode 100644 index 00000000000..15aa9ea0e05 --- /dev/null +++ b/app/assets/images/emoji/point_up_tone2.png diff --git a/app/assets/images/emoji/point_up_tone3.png b/app/assets/images/emoji/point_up_tone3.png Binary files differnew file mode 100644 index 00000000000..652b73a9c5d --- /dev/null +++ b/app/assets/images/emoji/point_up_tone3.png diff --git a/app/assets/images/emoji/point_up_tone4.png b/app/assets/images/emoji/point_up_tone4.png Binary files differnew file mode 100644 index 00000000000..692bad926e9 --- /dev/null +++ b/app/assets/images/emoji/point_up_tone4.png diff --git a/app/assets/images/emoji/point_up_tone5.png b/app/assets/images/emoji/point_up_tone5.png Binary files differnew file mode 100644 index 00000000000..1e1b10fb71c --- /dev/null +++ b/app/assets/images/emoji/point_up_tone5.png diff --git a/app/assets/images/emoji/police_car.png b/app/assets/images/emoji/police_car.png Binary files differnew file mode 100644 index 00000000000..3da4253de7e --- /dev/null +++ b/app/assets/images/emoji/police_car.png diff --git a/app/assets/images/emoji/poodle.png b/app/assets/images/emoji/poodle.png Binary files differnew file mode 100644 index 00000000000..8ec39e396af --- /dev/null +++ b/app/assets/images/emoji/poodle.png diff --git a/app/assets/images/emoji/poop.png b/app/assets/images/emoji/poop.png Binary files differnew file mode 100644 index 00000000000..10b15e72d56 --- /dev/null +++ b/app/assets/images/emoji/poop.png diff --git a/app/assets/images/emoji/popcorn.png b/app/assets/images/emoji/popcorn.png Binary files differnew file mode 100644 index 00000000000..36853e381d4 --- /dev/null +++ b/app/assets/images/emoji/popcorn.png diff --git a/app/assets/images/emoji/post_office.png b/app/assets/images/emoji/post_office.png Binary files differnew file mode 100644 index 00000000000..a23848f9aa0 --- /dev/null +++ b/app/assets/images/emoji/post_office.png diff --git a/app/assets/images/emoji/postal_horn.png b/app/assets/images/emoji/postal_horn.png Binary files differnew file mode 100644 index 00000000000..c173b8dbd67 --- /dev/null +++ b/app/assets/images/emoji/postal_horn.png diff --git a/app/assets/images/emoji/postbox.png b/app/assets/images/emoji/postbox.png Binary files differnew file mode 100644 index 00000000000..07c9c4ab3d6 --- /dev/null +++ b/app/assets/images/emoji/postbox.png diff --git a/app/assets/images/emoji/potable_water.png b/app/assets/images/emoji/potable_water.png Binary files differnew file mode 100644 index 00000000000..2c610049459 --- /dev/null +++ b/app/assets/images/emoji/potable_water.png diff --git a/app/assets/images/emoji/potato.png b/app/assets/images/emoji/potato.png Binary files differnew file mode 100644 index 00000000000..70350ca2c0a --- /dev/null +++ b/app/assets/images/emoji/potato.png diff --git a/app/assets/images/emoji/pouch.png b/app/assets/images/emoji/pouch.png Binary files differnew file mode 100644 index 00000000000..8795c6c66ff --- /dev/null +++ b/app/assets/images/emoji/pouch.png diff --git a/app/assets/images/emoji/poultry_leg.png b/app/assets/images/emoji/poultry_leg.png Binary files differnew file mode 100644 index 00000000000..eea4a53a2f9 --- /dev/null +++ b/app/assets/images/emoji/poultry_leg.png diff --git a/app/assets/images/emoji/pound.png b/app/assets/images/emoji/pound.png Binary files differnew file mode 100644 index 00000000000..a0d4c4099e9 --- /dev/null +++ b/app/assets/images/emoji/pound.png diff --git a/app/assets/images/emoji/pouting_cat.png b/app/assets/images/emoji/pouting_cat.png Binary files differnew file mode 100644 index 00000000000..41ddfeab42b --- /dev/null +++ b/app/assets/images/emoji/pouting_cat.png diff --git a/app/assets/images/emoji/pray.png b/app/assets/images/emoji/pray.png Binary files differnew file mode 100644 index 00000000000..8347f2435be --- /dev/null +++ b/app/assets/images/emoji/pray.png diff --git a/app/assets/images/emoji/pray_tone1.png b/app/assets/images/emoji/pray_tone1.png Binary files differnew file mode 100644 index 00000000000..060ef257172 --- /dev/null +++ b/app/assets/images/emoji/pray_tone1.png diff --git a/app/assets/images/emoji/pray_tone2.png b/app/assets/images/emoji/pray_tone2.png Binary files differnew file mode 100644 index 00000000000..56dc607c07a --- /dev/null +++ b/app/assets/images/emoji/pray_tone2.png diff --git a/app/assets/images/emoji/pray_tone3.png b/app/assets/images/emoji/pray_tone3.png Binary files differnew file mode 100644 index 00000000000..0f33b862008 --- /dev/null +++ b/app/assets/images/emoji/pray_tone3.png diff --git a/app/assets/images/emoji/pray_tone4.png b/app/assets/images/emoji/pray_tone4.png Binary files differnew file mode 100644 index 00000000000..2ea8dc11657 --- /dev/null +++ b/app/assets/images/emoji/pray_tone4.png diff --git a/app/assets/images/emoji/pray_tone5.png b/app/assets/images/emoji/pray_tone5.png Binary files differnew file mode 100644 index 00000000000..2128a6c4703 --- /dev/null +++ b/app/assets/images/emoji/pray_tone5.png diff --git a/app/assets/images/emoji/prayer_beads.png b/app/assets/images/emoji/prayer_beads.png Binary files differnew file mode 100644 index 00000000000..a4b6dfcc62e --- /dev/null +++ b/app/assets/images/emoji/prayer_beads.png diff --git a/app/assets/images/emoji/pregnant_woman.png b/app/assets/images/emoji/pregnant_woman.png Binary files differnew file mode 100644 index 00000000000..084e83a414a --- /dev/null +++ b/app/assets/images/emoji/pregnant_woman.png diff --git a/app/assets/images/emoji/pregnant_woman_tone1.png b/app/assets/images/emoji/pregnant_woman_tone1.png Binary files differnew file mode 100644 index 00000000000..a78703b33aa --- /dev/null +++ b/app/assets/images/emoji/pregnant_woman_tone1.png diff --git a/app/assets/images/emoji/pregnant_woman_tone2.png b/app/assets/images/emoji/pregnant_woman_tone2.png Binary files differnew file mode 100644 index 00000000000..0068c6c4a77 --- /dev/null +++ b/app/assets/images/emoji/pregnant_woman_tone2.png diff --git a/app/assets/images/emoji/pregnant_woman_tone3.png b/app/assets/images/emoji/pregnant_woman_tone3.png Binary files differnew file mode 100644 index 00000000000..3206296b684 --- /dev/null +++ b/app/assets/images/emoji/pregnant_woman_tone3.png diff --git a/app/assets/images/emoji/pregnant_woman_tone4.png b/app/assets/images/emoji/pregnant_woman_tone4.png Binary files differnew file mode 100644 index 00000000000..120fda5cd8c --- /dev/null +++ b/app/assets/images/emoji/pregnant_woman_tone4.png diff --git a/app/assets/images/emoji/pregnant_woman_tone5.png b/app/assets/images/emoji/pregnant_woman_tone5.png Binary files differnew file mode 100644 index 00000000000..569bfdf05ce --- /dev/null +++ b/app/assets/images/emoji/pregnant_woman_tone5.png diff --git a/app/assets/images/emoji/prince.png b/app/assets/images/emoji/prince.png Binary files differnew file mode 100644 index 00000000000..38d69344c84 --- /dev/null +++ b/app/assets/images/emoji/prince.png diff --git a/app/assets/images/emoji/prince_tone1.png b/app/assets/images/emoji/prince_tone1.png Binary files differnew file mode 100644 index 00000000000..849930c8887 --- /dev/null +++ b/app/assets/images/emoji/prince_tone1.png diff --git a/app/assets/images/emoji/prince_tone2.png b/app/assets/images/emoji/prince_tone2.png Binary files differnew file mode 100644 index 00000000000..23d8b3b1285 --- /dev/null +++ b/app/assets/images/emoji/prince_tone2.png diff --git a/app/assets/images/emoji/prince_tone3.png b/app/assets/images/emoji/prince_tone3.png Binary files differnew file mode 100644 index 00000000000..db6dfff0647 --- /dev/null +++ b/app/assets/images/emoji/prince_tone3.png diff --git a/app/assets/images/emoji/prince_tone4.png b/app/assets/images/emoji/prince_tone4.png Binary files differnew file mode 100644 index 00000000000..8e10f8be6a8 --- /dev/null +++ b/app/assets/images/emoji/prince_tone4.png diff --git a/app/assets/images/emoji/prince_tone5.png b/app/assets/images/emoji/prince_tone5.png Binary files differnew file mode 100644 index 00000000000..138d4ea7048 --- /dev/null +++ b/app/assets/images/emoji/prince_tone5.png diff --git a/app/assets/images/emoji/princess.png b/app/assets/images/emoji/princess.png Binary files differnew file mode 100644 index 00000000000..879e9fa8c5d --- /dev/null +++ b/app/assets/images/emoji/princess.png diff --git a/app/assets/images/emoji/princess_tone1.png b/app/assets/images/emoji/princess_tone1.png Binary files differnew file mode 100644 index 00000000000..c28078cdc36 --- /dev/null +++ b/app/assets/images/emoji/princess_tone1.png diff --git a/app/assets/images/emoji/princess_tone2.png b/app/assets/images/emoji/princess_tone2.png Binary files differnew file mode 100644 index 00000000000..dcd20e6ecd4 --- /dev/null +++ b/app/assets/images/emoji/princess_tone2.png diff --git a/app/assets/images/emoji/princess_tone3.png b/app/assets/images/emoji/princess_tone3.png Binary files differnew file mode 100644 index 00000000000..cde6f315c56 --- /dev/null +++ b/app/assets/images/emoji/princess_tone3.png diff --git a/app/assets/images/emoji/princess_tone4.png b/app/assets/images/emoji/princess_tone4.png Binary files differnew file mode 100644 index 00000000000..c71e69caaef --- /dev/null +++ b/app/assets/images/emoji/princess_tone4.png diff --git a/app/assets/images/emoji/princess_tone5.png b/app/assets/images/emoji/princess_tone5.png Binary files differnew file mode 100644 index 00000000000..063e2645910 --- /dev/null +++ b/app/assets/images/emoji/princess_tone5.png diff --git a/app/assets/images/emoji/printer.png b/app/assets/images/emoji/printer.png Binary files differnew file mode 100644 index 00000000000..027c830f0fe --- /dev/null +++ b/app/assets/images/emoji/printer.png diff --git a/app/assets/images/emoji/projector.png b/app/assets/images/emoji/projector.png Binary files differnew file mode 100644 index 00000000000..ce9ab0daa28 --- /dev/null +++ b/app/assets/images/emoji/projector.png diff --git a/app/assets/images/emoji/punch.png b/app/assets/images/emoji/punch.png Binary files differnew file mode 100644 index 00000000000..b14ca5f5211 --- /dev/null +++ b/app/assets/images/emoji/punch.png diff --git a/app/assets/images/emoji/punch_tone1.png b/app/assets/images/emoji/punch_tone1.png Binary files differnew file mode 100644 index 00000000000..93c7d17fb47 --- /dev/null +++ b/app/assets/images/emoji/punch_tone1.png diff --git a/app/assets/images/emoji/punch_tone2.png b/app/assets/images/emoji/punch_tone2.png Binary files differnew file mode 100644 index 00000000000..c0a1af6e10a --- /dev/null +++ b/app/assets/images/emoji/punch_tone2.png diff --git a/app/assets/images/emoji/punch_tone3.png b/app/assets/images/emoji/punch_tone3.png Binary files differnew file mode 100644 index 00000000000..1458b021201 --- /dev/null +++ b/app/assets/images/emoji/punch_tone3.png diff --git a/app/assets/images/emoji/punch_tone4.png b/app/assets/images/emoji/punch_tone4.png Binary files differnew file mode 100644 index 00000000000..c1466bfcdef --- /dev/null +++ b/app/assets/images/emoji/punch_tone4.png diff --git a/app/assets/images/emoji/punch_tone5.png b/app/assets/images/emoji/punch_tone5.png Binary files differnew file mode 100644 index 00000000000..00b4ddb8953 --- /dev/null +++ b/app/assets/images/emoji/punch_tone5.png diff --git a/app/assets/images/emoji/purple_heart.png b/app/assets/images/emoji/purple_heart.png Binary files differnew file mode 100644 index 00000000000..95c53a9ade6 --- /dev/null +++ b/app/assets/images/emoji/purple_heart.png diff --git a/app/assets/images/emoji/purse.png b/app/assets/images/emoji/purse.png Binary files differnew file mode 100644 index 00000000000..981346193c5 --- /dev/null +++ b/app/assets/images/emoji/purse.png diff --git a/app/assets/images/emoji/pushpin.png b/app/assets/images/emoji/pushpin.png Binary files differnew file mode 100644 index 00000000000..57e07d7f4cc --- /dev/null +++ b/app/assets/images/emoji/pushpin.png diff --git a/app/assets/images/emoji/put_litter_in_its_place.png b/app/assets/images/emoji/put_litter_in_its_place.png Binary files differnew file mode 100644 index 00000000000..82a84f9a375 --- /dev/null +++ b/app/assets/images/emoji/put_litter_in_its_place.png diff --git a/app/assets/images/emoji/question.png b/app/assets/images/emoji/question.png Binary files differnew file mode 100644 index 00000000000..5a58f3458aa --- /dev/null +++ b/app/assets/images/emoji/question.png diff --git a/app/assets/images/emoji/rabbit.png b/app/assets/images/emoji/rabbit.png Binary files differnew file mode 100644 index 00000000000..ea75ab0426e --- /dev/null +++ b/app/assets/images/emoji/rabbit.png diff --git a/app/assets/images/emoji/rabbit2.png b/app/assets/images/emoji/rabbit2.png Binary files differnew file mode 100644 index 00000000000..2c8a29c642f --- /dev/null +++ b/app/assets/images/emoji/rabbit2.png diff --git a/app/assets/images/emoji/race_car.png b/app/assets/images/emoji/race_car.png Binary files differnew file mode 100644 index 00000000000..fe3f045f446 --- /dev/null +++ b/app/assets/images/emoji/race_car.png diff --git a/app/assets/images/emoji/racehorse.png b/app/assets/images/emoji/racehorse.png Binary files differnew file mode 100644 index 00000000000..b3e73cc8903 --- /dev/null +++ b/app/assets/images/emoji/racehorse.png diff --git a/app/assets/images/emoji/radio.png b/app/assets/images/emoji/radio.png Binary files differnew file mode 100644 index 00000000000..dec381fa242 --- /dev/null +++ b/app/assets/images/emoji/radio.png diff --git a/app/assets/images/emoji/radio_button.png b/app/assets/images/emoji/radio_button.png Binary files differnew file mode 100644 index 00000000000..3a23449d917 --- /dev/null +++ b/app/assets/images/emoji/radio_button.png diff --git a/app/assets/images/emoji/radioactive.png b/app/assets/images/emoji/radioactive.png Binary files differnew file mode 100644 index 00000000000..3b46199fe37 --- /dev/null +++ b/app/assets/images/emoji/radioactive.png diff --git a/app/assets/images/emoji/rage.png b/app/assets/images/emoji/rage.png Binary files differnew file mode 100644 index 00000000000..9d739bd40ad --- /dev/null +++ b/app/assets/images/emoji/rage.png diff --git a/app/assets/images/emoji/railway_car.png b/app/assets/images/emoji/railway_car.png Binary files differnew file mode 100644 index 00000000000..a9acbf13008 --- /dev/null +++ b/app/assets/images/emoji/railway_car.png diff --git a/app/assets/images/emoji/railway_track.png b/app/assets/images/emoji/railway_track.png Binary files differnew file mode 100644 index 00000000000..e1a7a0d1430 --- /dev/null +++ b/app/assets/images/emoji/railway_track.png diff --git a/app/assets/images/emoji/rainbow.png b/app/assets/images/emoji/rainbow.png Binary files differnew file mode 100644 index 00000000000..154735d7147 --- /dev/null +++ b/app/assets/images/emoji/rainbow.png diff --git a/app/assets/images/emoji/raised_back_of_hand.png b/app/assets/images/emoji/raised_back_of_hand.png Binary files differnew file mode 100644 index 00000000000..479234294b4 --- /dev/null +++ b/app/assets/images/emoji/raised_back_of_hand.png diff --git a/app/assets/images/emoji/raised_back_of_hand_tone1.png b/app/assets/images/emoji/raised_back_of_hand_tone1.png Binary files differnew file mode 100644 index 00000000000..813d28499b5 --- /dev/null +++ b/app/assets/images/emoji/raised_back_of_hand_tone1.png diff --git a/app/assets/images/emoji/raised_back_of_hand_tone2.png b/app/assets/images/emoji/raised_back_of_hand_tone2.png Binary files differnew file mode 100644 index 00000000000..192ff795e37 --- /dev/null +++ b/app/assets/images/emoji/raised_back_of_hand_tone2.png diff --git a/app/assets/images/emoji/raised_back_of_hand_tone3.png b/app/assets/images/emoji/raised_back_of_hand_tone3.png Binary files differnew file mode 100644 index 00000000000..61a727abe6b --- /dev/null +++ b/app/assets/images/emoji/raised_back_of_hand_tone3.png diff --git a/app/assets/images/emoji/raised_back_of_hand_tone4.png b/app/assets/images/emoji/raised_back_of_hand_tone4.png Binary files differnew file mode 100644 index 00000000000..2e83da511f5 --- /dev/null +++ b/app/assets/images/emoji/raised_back_of_hand_tone4.png diff --git a/app/assets/images/emoji/raised_back_of_hand_tone5.png b/app/assets/images/emoji/raised_back_of_hand_tone5.png Binary files differnew file mode 100644 index 00000000000..d7a5b95a02c --- /dev/null +++ b/app/assets/images/emoji/raised_back_of_hand_tone5.png diff --git a/app/assets/images/emoji/raised_hand.png b/app/assets/images/emoji/raised_hand.png Binary files differnew file mode 100644 index 00000000000..6b2954315d1 --- /dev/null +++ b/app/assets/images/emoji/raised_hand.png diff --git a/app/assets/images/emoji/raised_hand_tone1.png b/app/assets/images/emoji/raised_hand_tone1.png Binary files differnew file mode 100644 index 00000000000..3b752902c07 --- /dev/null +++ b/app/assets/images/emoji/raised_hand_tone1.png diff --git a/app/assets/images/emoji/raised_hand_tone2.png b/app/assets/images/emoji/raised_hand_tone2.png Binary files differnew file mode 100644 index 00000000000..44e2a514c60 --- /dev/null +++ b/app/assets/images/emoji/raised_hand_tone2.png diff --git a/app/assets/images/emoji/raised_hand_tone3.png b/app/assets/images/emoji/raised_hand_tone3.png Binary files differnew file mode 100644 index 00000000000..5bb62a7528a --- /dev/null +++ b/app/assets/images/emoji/raised_hand_tone3.png diff --git a/app/assets/images/emoji/raised_hand_tone4.png b/app/assets/images/emoji/raised_hand_tone4.png Binary files differnew file mode 100644 index 00000000000..c7f8c9ec270 --- /dev/null +++ b/app/assets/images/emoji/raised_hand_tone4.png diff --git a/app/assets/images/emoji/raised_hand_tone5.png b/app/assets/images/emoji/raised_hand_tone5.png Binary files differnew file mode 100644 index 00000000000..c601b58a73e --- /dev/null +++ b/app/assets/images/emoji/raised_hand_tone5.png diff --git a/app/assets/images/emoji/raised_hands.png b/app/assets/images/emoji/raised_hands.png Binary files differnew file mode 100644 index 00000000000..c0155f728e7 --- /dev/null +++ b/app/assets/images/emoji/raised_hands.png diff --git a/app/assets/images/emoji/raised_hands_tone1.png b/app/assets/images/emoji/raised_hands_tone1.png Binary files differnew file mode 100644 index 00000000000..1168b8236b6 --- /dev/null +++ b/app/assets/images/emoji/raised_hands_tone1.png diff --git a/app/assets/images/emoji/raised_hands_tone2.png b/app/assets/images/emoji/raised_hands_tone2.png Binary files differnew file mode 100644 index 00000000000..322de622903 --- /dev/null +++ b/app/assets/images/emoji/raised_hands_tone2.png diff --git a/app/assets/images/emoji/raised_hands_tone3.png b/app/assets/images/emoji/raised_hands_tone3.png Binary files differnew file mode 100644 index 00000000000..2aa24e05ae1 --- /dev/null +++ b/app/assets/images/emoji/raised_hands_tone3.png diff --git a/app/assets/images/emoji/raised_hands_tone4.png b/app/assets/images/emoji/raised_hands_tone4.png Binary files differnew file mode 100644 index 00000000000..f31bf0db992 --- /dev/null +++ b/app/assets/images/emoji/raised_hands_tone4.png diff --git a/app/assets/images/emoji/raised_hands_tone5.png b/app/assets/images/emoji/raised_hands_tone5.png Binary files differnew file mode 100644 index 00000000000..5e95067f98b --- /dev/null +++ b/app/assets/images/emoji/raised_hands_tone5.png diff --git a/app/assets/images/emoji/raising_hand.png b/app/assets/images/emoji/raising_hand.png Binary files differnew file mode 100644 index 00000000000..2880708c0cc --- /dev/null +++ b/app/assets/images/emoji/raising_hand.png diff --git a/app/assets/images/emoji/raising_hand_tone1.png b/app/assets/images/emoji/raising_hand_tone1.png Binary files differnew file mode 100644 index 00000000000..1c90e3e2689 --- /dev/null +++ b/app/assets/images/emoji/raising_hand_tone1.png diff --git a/app/assets/images/emoji/raising_hand_tone2.png b/app/assets/images/emoji/raising_hand_tone2.png Binary files differnew file mode 100644 index 00000000000..82c3ef2bfc5 --- /dev/null +++ b/app/assets/images/emoji/raising_hand_tone2.png diff --git a/app/assets/images/emoji/raising_hand_tone3.png b/app/assets/images/emoji/raising_hand_tone3.png Binary files differnew file mode 100644 index 00000000000..1b1da2aa0ca --- /dev/null +++ b/app/assets/images/emoji/raising_hand_tone3.png diff --git a/app/assets/images/emoji/raising_hand_tone4.png b/app/assets/images/emoji/raising_hand_tone4.png Binary files differnew file mode 100644 index 00000000000..e453855c01f --- /dev/null +++ b/app/assets/images/emoji/raising_hand_tone4.png diff --git a/app/assets/images/emoji/raising_hand_tone5.png b/app/assets/images/emoji/raising_hand_tone5.png Binary files differnew file mode 100644 index 00000000000..b86200fd844 --- /dev/null +++ b/app/assets/images/emoji/raising_hand_tone5.png diff --git a/app/assets/images/emoji/ram.png b/app/assets/images/emoji/ram.png Binary files differnew file mode 100644 index 00000000000..52a44464c9b --- /dev/null +++ b/app/assets/images/emoji/ram.png diff --git a/app/assets/images/emoji/ramen.png b/app/assets/images/emoji/ramen.png Binary files differnew file mode 100644 index 00000000000..c1cb7cd7384 --- /dev/null +++ b/app/assets/images/emoji/ramen.png diff --git a/app/assets/images/emoji/rat.png b/app/assets/images/emoji/rat.png Binary files differnew file mode 100644 index 00000000000..86219144f10 --- /dev/null +++ b/app/assets/images/emoji/rat.png diff --git a/app/assets/images/emoji/record_button.png b/app/assets/images/emoji/record_button.png Binary files differnew file mode 100644 index 00000000000..ada52830fce --- /dev/null +++ b/app/assets/images/emoji/record_button.png diff --git a/app/assets/images/emoji/recycle.png b/app/assets/images/emoji/recycle.png Binary files differnew file mode 100644 index 00000000000..9221f095c37 --- /dev/null +++ b/app/assets/images/emoji/recycle.png diff --git a/app/assets/images/emoji/red_car.png b/app/assets/images/emoji/red_car.png Binary files differnew file mode 100644 index 00000000000..b3e6a774dea --- /dev/null +++ b/app/assets/images/emoji/red_car.png diff --git a/app/assets/images/emoji/red_circle.png b/app/assets/images/emoji/red_circle.png Binary files differnew file mode 100644 index 00000000000..4bef930d92f --- /dev/null +++ b/app/assets/images/emoji/red_circle.png diff --git a/app/assets/images/emoji/registered.png b/app/assets/images/emoji/registered.png Binary files differnew file mode 100644 index 00000000000..53ef9f2d4e6 --- /dev/null +++ b/app/assets/images/emoji/registered.png diff --git a/app/assets/images/emoji/relaxed.png b/app/assets/images/emoji/relaxed.png Binary files differnew file mode 100644 index 00000000000..e9e53c03d45 --- /dev/null +++ b/app/assets/images/emoji/relaxed.png diff --git a/app/assets/images/emoji/relieved.png b/app/assets/images/emoji/relieved.png Binary files differnew file mode 100644 index 00000000000..715ad0bf53f --- /dev/null +++ b/app/assets/images/emoji/relieved.png diff --git a/app/assets/images/emoji/reminder_ribbon.png b/app/assets/images/emoji/reminder_ribbon.png Binary files differnew file mode 100644 index 00000000000..3988bbd094c --- /dev/null +++ b/app/assets/images/emoji/reminder_ribbon.png diff --git a/app/assets/images/emoji/repeat.png b/app/assets/images/emoji/repeat.png Binary files differnew file mode 100644 index 00000000000..540ce4e0fba --- /dev/null +++ b/app/assets/images/emoji/repeat.png diff --git a/app/assets/images/emoji/repeat_one.png b/app/assets/images/emoji/repeat_one.png Binary files differnew file mode 100644 index 00000000000..9567e83337f --- /dev/null +++ b/app/assets/images/emoji/repeat_one.png diff --git a/app/assets/images/emoji/restroom.png b/app/assets/images/emoji/restroom.png Binary files differnew file mode 100644 index 00000000000..9588e0f0ef7 --- /dev/null +++ b/app/assets/images/emoji/restroom.png diff --git a/app/assets/images/emoji/revolving_hearts.png b/app/assets/images/emoji/revolving_hearts.png Binary files differnew file mode 100644 index 00000000000..7b9d1948f73 --- /dev/null +++ b/app/assets/images/emoji/revolving_hearts.png diff --git a/app/assets/images/emoji/rewind.png b/app/assets/images/emoji/rewind.png Binary files differnew file mode 100644 index 00000000000..e22e2bd3da5 --- /dev/null +++ b/app/assets/images/emoji/rewind.png diff --git a/app/assets/images/emoji/rhino.png b/app/assets/images/emoji/rhino.png Binary files differnew file mode 100644 index 00000000000..12f4e0d9d9b --- /dev/null +++ b/app/assets/images/emoji/rhino.png diff --git a/app/assets/images/emoji/ribbon.png b/app/assets/images/emoji/ribbon.png Binary files differnew file mode 100644 index 00000000000..0f253c3d8c8 --- /dev/null +++ b/app/assets/images/emoji/ribbon.png diff --git a/app/assets/images/emoji/rice.png b/app/assets/images/emoji/rice.png Binary files differnew file mode 100644 index 00000000000..6e3ac7956b1 --- /dev/null +++ b/app/assets/images/emoji/rice.png diff --git a/app/assets/images/emoji/rice_ball.png b/app/assets/images/emoji/rice_ball.png Binary files differnew file mode 100644 index 00000000000..d3d8ee25cb8 --- /dev/null +++ b/app/assets/images/emoji/rice_ball.png diff --git a/app/assets/images/emoji/rice_cracker.png b/app/assets/images/emoji/rice_cracker.png Binary files differnew file mode 100644 index 00000000000..7fbd08e4ff9 --- /dev/null +++ b/app/assets/images/emoji/rice_cracker.png diff --git a/app/assets/images/emoji/rice_scene.png b/app/assets/images/emoji/rice_scene.png Binary files differnew file mode 100644 index 00000000000..1a28426592a --- /dev/null +++ b/app/assets/images/emoji/rice_scene.png diff --git a/app/assets/images/emoji/right_facing_fist.png b/app/assets/images/emoji/right_facing_fist.png Binary files differnew file mode 100644 index 00000000000..754ed066d2c --- /dev/null +++ b/app/assets/images/emoji/right_facing_fist.png diff --git a/app/assets/images/emoji/right_facing_fist_tone1.png b/app/assets/images/emoji/right_facing_fist_tone1.png Binary files differnew file mode 100644 index 00000000000..33ded2f61a6 --- /dev/null +++ b/app/assets/images/emoji/right_facing_fist_tone1.png diff --git a/app/assets/images/emoji/right_facing_fist_tone2.png b/app/assets/images/emoji/right_facing_fist_tone2.png Binary files differnew file mode 100644 index 00000000000..88054e335c7 --- /dev/null +++ b/app/assets/images/emoji/right_facing_fist_tone2.png diff --git a/app/assets/images/emoji/right_facing_fist_tone3.png b/app/assets/images/emoji/right_facing_fist_tone3.png Binary files differnew file mode 100644 index 00000000000..84b9f5da7f7 --- /dev/null +++ b/app/assets/images/emoji/right_facing_fist_tone3.png diff --git a/app/assets/images/emoji/right_facing_fist_tone4.png b/app/assets/images/emoji/right_facing_fist_tone4.png Binary files differnew file mode 100644 index 00000000000..e741cfea68b --- /dev/null +++ b/app/assets/images/emoji/right_facing_fist_tone4.png diff --git a/app/assets/images/emoji/right_facing_fist_tone5.png b/app/assets/images/emoji/right_facing_fist_tone5.png Binary files differnew file mode 100644 index 00000000000..cf66d760c1f --- /dev/null +++ b/app/assets/images/emoji/right_facing_fist_tone5.png diff --git a/app/assets/images/emoji/ring.png b/app/assets/images/emoji/ring.png Binary files differnew file mode 100644 index 00000000000..87d227adb74 --- /dev/null +++ b/app/assets/images/emoji/ring.png diff --git a/app/assets/images/emoji/robot.png b/app/assets/images/emoji/robot.png Binary files differnew file mode 100644 index 00000000000..7cc62612c6a --- /dev/null +++ b/app/assets/images/emoji/robot.png diff --git a/app/assets/images/emoji/rocket.png b/app/assets/images/emoji/rocket.png Binary files differnew file mode 100644 index 00000000000..0d8da089a37 --- /dev/null +++ b/app/assets/images/emoji/rocket.png diff --git a/app/assets/images/emoji/rofl.png b/app/assets/images/emoji/rofl.png Binary files differnew file mode 100644 index 00000000000..b1736fedfeb --- /dev/null +++ b/app/assets/images/emoji/rofl.png diff --git a/app/assets/images/emoji/roller_coaster.png b/app/assets/images/emoji/roller_coaster.png Binary files differnew file mode 100644 index 00000000000..5b849e071e8 --- /dev/null +++ b/app/assets/images/emoji/roller_coaster.png diff --git a/app/assets/images/emoji/rolling_eyes.png b/app/assets/images/emoji/rolling_eyes.png Binary files differnew file mode 100644 index 00000000000..2f77b9fc3b9 --- /dev/null +++ b/app/assets/images/emoji/rolling_eyes.png diff --git a/app/assets/images/emoji/rooster.png b/app/assets/images/emoji/rooster.png Binary files differnew file mode 100644 index 00000000000..bbf2bbff97a --- /dev/null +++ b/app/assets/images/emoji/rooster.png diff --git a/app/assets/images/emoji/rose.png b/app/assets/images/emoji/rose.png Binary files differnew file mode 100644 index 00000000000..52c286d31ce --- /dev/null +++ b/app/assets/images/emoji/rose.png diff --git a/app/assets/images/emoji/rosette.png b/app/assets/images/emoji/rosette.png Binary files differnew file mode 100644 index 00000000000..8030e494bcf --- /dev/null +++ b/app/assets/images/emoji/rosette.png diff --git a/app/assets/images/emoji/rotating_light.png b/app/assets/images/emoji/rotating_light.png Binary files differnew file mode 100644 index 00000000000..cad66b0afef --- /dev/null +++ b/app/assets/images/emoji/rotating_light.png diff --git a/app/assets/images/emoji/round_pushpin.png b/app/assets/images/emoji/round_pushpin.png Binary files differnew file mode 100644 index 00000000000..28b9d72866e --- /dev/null +++ b/app/assets/images/emoji/round_pushpin.png diff --git a/app/assets/images/emoji/rowboat.png b/app/assets/images/emoji/rowboat.png Binary files differnew file mode 100644 index 00000000000..dd4dfc095d9 --- /dev/null +++ b/app/assets/images/emoji/rowboat.png diff --git a/app/assets/images/emoji/rowboat_tone1.png b/app/assets/images/emoji/rowboat_tone1.png Binary files differnew file mode 100644 index 00000000000..5e5d18548cb --- /dev/null +++ b/app/assets/images/emoji/rowboat_tone1.png diff --git a/app/assets/images/emoji/rowboat_tone2.png b/app/assets/images/emoji/rowboat_tone2.png Binary files differnew file mode 100644 index 00000000000..9b123ef8871 --- /dev/null +++ b/app/assets/images/emoji/rowboat_tone2.png diff --git a/app/assets/images/emoji/rowboat_tone3.png b/app/assets/images/emoji/rowboat_tone3.png Binary files differnew file mode 100644 index 00000000000..8ebd89a55f5 --- /dev/null +++ b/app/assets/images/emoji/rowboat_tone3.png diff --git a/app/assets/images/emoji/rowboat_tone4.png b/app/assets/images/emoji/rowboat_tone4.png Binary files differnew file mode 100644 index 00000000000..2b0d04f8725 --- /dev/null +++ b/app/assets/images/emoji/rowboat_tone4.png diff --git a/app/assets/images/emoji/rowboat_tone5.png b/app/assets/images/emoji/rowboat_tone5.png Binary files differnew file mode 100644 index 00000000000..b346f2dfc84 --- /dev/null +++ b/app/assets/images/emoji/rowboat_tone5.png diff --git a/app/assets/images/emoji/rugby_football.png b/app/assets/images/emoji/rugby_football.png Binary files differnew file mode 100644 index 00000000000..b1872273436 --- /dev/null +++ b/app/assets/images/emoji/rugby_football.png diff --git a/app/assets/images/emoji/runner.png b/app/assets/images/emoji/runner.png Binary files differnew file mode 100644 index 00000000000..e914915976a --- /dev/null +++ b/app/assets/images/emoji/runner.png diff --git a/app/assets/images/emoji/runner_tone1.png b/app/assets/images/emoji/runner_tone1.png Binary files differnew file mode 100644 index 00000000000..9355239a52d --- /dev/null +++ b/app/assets/images/emoji/runner_tone1.png diff --git a/app/assets/images/emoji/runner_tone2.png b/app/assets/images/emoji/runner_tone2.png Binary files differnew file mode 100644 index 00000000000..6112fd5c376 --- /dev/null +++ b/app/assets/images/emoji/runner_tone2.png diff --git a/app/assets/images/emoji/runner_tone3.png b/app/assets/images/emoji/runner_tone3.png Binary files differnew file mode 100644 index 00000000000..625ec708f48 --- /dev/null +++ b/app/assets/images/emoji/runner_tone3.png diff --git a/app/assets/images/emoji/runner_tone4.png b/app/assets/images/emoji/runner_tone4.png Binary files differnew file mode 100644 index 00000000000..242f1b56337 --- /dev/null +++ b/app/assets/images/emoji/runner_tone4.png diff --git a/app/assets/images/emoji/runner_tone5.png b/app/assets/images/emoji/runner_tone5.png Binary files differnew file mode 100644 index 00000000000..2976c6f019f --- /dev/null +++ b/app/assets/images/emoji/runner_tone5.png diff --git a/app/assets/images/emoji/running_shirt_with_sash.png b/app/assets/images/emoji/running_shirt_with_sash.png Binary files differnew file mode 100644 index 00000000000..6d83c06b803 --- /dev/null +++ b/app/assets/images/emoji/running_shirt_with_sash.png diff --git a/app/assets/images/emoji/sa.png b/app/assets/images/emoji/sa.png Binary files differnew file mode 100644 index 00000000000..900f9633247 --- /dev/null +++ b/app/assets/images/emoji/sa.png diff --git a/app/assets/images/emoji/sagittarius.png b/app/assets/images/emoji/sagittarius.png Binary files differnew file mode 100644 index 00000000000..f8d94ff2923 --- /dev/null +++ b/app/assets/images/emoji/sagittarius.png diff --git a/app/assets/images/emoji/sailboat.png b/app/assets/images/emoji/sailboat.png Binary files differnew file mode 100644 index 00000000000..772ef11da5d --- /dev/null +++ b/app/assets/images/emoji/sailboat.png diff --git a/app/assets/images/emoji/sake.png b/app/assets/images/emoji/sake.png Binary files differnew file mode 100644 index 00000000000..2933f5672c4 --- /dev/null +++ b/app/assets/images/emoji/sake.png diff --git a/app/assets/images/emoji/salad.png b/app/assets/images/emoji/salad.png Binary files differnew file mode 100644 index 00000000000..c89f9341158 --- /dev/null +++ b/app/assets/images/emoji/salad.png diff --git a/app/assets/images/emoji/sandal.png b/app/assets/images/emoji/sandal.png Binary files differnew file mode 100644 index 00000000000..9d9f5122b7a --- /dev/null +++ b/app/assets/images/emoji/sandal.png diff --git a/app/assets/images/emoji/santa.png b/app/assets/images/emoji/santa.png Binary files differnew file mode 100644 index 00000000000..bc83ab80d52 --- /dev/null +++ b/app/assets/images/emoji/santa.png diff --git a/app/assets/images/emoji/santa_tone1.png b/app/assets/images/emoji/santa_tone1.png Binary files differnew file mode 100644 index 00000000000..5233ffb7174 --- /dev/null +++ b/app/assets/images/emoji/santa_tone1.png diff --git a/app/assets/images/emoji/santa_tone2.png b/app/assets/images/emoji/santa_tone2.png Binary files differnew file mode 100644 index 00000000000..4e845438197 --- /dev/null +++ b/app/assets/images/emoji/santa_tone2.png diff --git a/app/assets/images/emoji/santa_tone3.png b/app/assets/images/emoji/santa_tone3.png Binary files differnew file mode 100644 index 00000000000..7fc4f33b60f --- /dev/null +++ b/app/assets/images/emoji/santa_tone3.png diff --git a/app/assets/images/emoji/santa_tone4.png b/app/assets/images/emoji/santa_tone4.png Binary files differnew file mode 100644 index 00000000000..d1d5a15132d --- /dev/null +++ b/app/assets/images/emoji/santa_tone4.png diff --git a/app/assets/images/emoji/santa_tone5.png b/app/assets/images/emoji/santa_tone5.png Binary files differnew file mode 100644 index 00000000000..4d697a01f24 --- /dev/null +++ b/app/assets/images/emoji/santa_tone5.png diff --git a/app/assets/images/emoji/satellite.png b/app/assets/images/emoji/satellite.png Binary files differnew file mode 100644 index 00000000000..db0372795f4 --- /dev/null +++ b/app/assets/images/emoji/satellite.png diff --git a/app/assets/images/emoji/satellite_orbital.png b/app/assets/images/emoji/satellite_orbital.png Binary files differnew file mode 100644 index 00000000000..4ba55d6e297 --- /dev/null +++ b/app/assets/images/emoji/satellite_orbital.png diff --git a/app/assets/images/emoji/saxophone.png b/app/assets/images/emoji/saxophone.png Binary files differnew file mode 100644 index 00000000000..a392faec291 --- /dev/null +++ b/app/assets/images/emoji/saxophone.png diff --git a/app/assets/images/emoji/scales.png b/app/assets/images/emoji/scales.png Binary files differnew file mode 100644 index 00000000000..0757eda1684 --- /dev/null +++ b/app/assets/images/emoji/scales.png diff --git a/app/assets/images/emoji/school.png b/app/assets/images/emoji/school.png Binary files differnew file mode 100644 index 00000000000..269759534f0 --- /dev/null +++ b/app/assets/images/emoji/school.png diff --git a/app/assets/images/emoji/school_satchel.png b/app/assets/images/emoji/school_satchel.png Binary files differnew file mode 100644 index 00000000000..9997c86e7dc --- /dev/null +++ b/app/assets/images/emoji/school_satchel.png diff --git a/app/assets/images/emoji/scissors.png b/app/assets/images/emoji/scissors.png Binary files differnew file mode 100644 index 00000000000..270571c8cdd --- /dev/null +++ b/app/assets/images/emoji/scissors.png diff --git a/app/assets/images/emoji/scooter.png b/app/assets/images/emoji/scooter.png Binary files differnew file mode 100644 index 00000000000..4ab7ef59cd2 --- /dev/null +++ b/app/assets/images/emoji/scooter.png diff --git a/app/assets/images/emoji/scorpion.png b/app/assets/images/emoji/scorpion.png Binary files differnew file mode 100644 index 00000000000..449a6b281c9 --- /dev/null +++ b/app/assets/images/emoji/scorpion.png diff --git a/app/assets/images/emoji/scorpius.png b/app/assets/images/emoji/scorpius.png Binary files differnew file mode 100644 index 00000000000..c31a9920455 --- /dev/null +++ b/app/assets/images/emoji/scorpius.png diff --git a/app/assets/images/emoji/scream.png b/app/assets/images/emoji/scream.png Binary files differnew file mode 100644 index 00000000000..c3bea9f2510 --- /dev/null +++ b/app/assets/images/emoji/scream.png diff --git a/app/assets/images/emoji/scream_cat.png b/app/assets/images/emoji/scream_cat.png Binary files differnew file mode 100644 index 00000000000..15803ad8e6e --- /dev/null +++ b/app/assets/images/emoji/scream_cat.png diff --git a/app/assets/images/emoji/scroll.png b/app/assets/images/emoji/scroll.png Binary files differnew file mode 100644 index 00000000000..50ee5dcd4b9 --- /dev/null +++ b/app/assets/images/emoji/scroll.png diff --git a/app/assets/images/emoji/seat.png b/app/assets/images/emoji/seat.png Binary files differnew file mode 100644 index 00000000000..a6d72d95adb --- /dev/null +++ b/app/assets/images/emoji/seat.png diff --git a/app/assets/images/emoji/second_place.png b/app/assets/images/emoji/second_place.png Binary files differnew file mode 100644 index 00000000000..17b011268b6 --- /dev/null +++ b/app/assets/images/emoji/second_place.png diff --git a/app/assets/images/emoji/secret.png b/app/assets/images/emoji/secret.png Binary files differnew file mode 100644 index 00000000000..5fd72608e60 --- /dev/null +++ b/app/assets/images/emoji/secret.png diff --git a/app/assets/images/emoji/see_no_evil.png b/app/assets/images/emoji/see_no_evil.png Binary files differnew file mode 100644 index 00000000000..5187e474531 --- /dev/null +++ b/app/assets/images/emoji/see_no_evil.png diff --git a/app/assets/images/emoji/seedling.png b/app/assets/images/emoji/seedling.png Binary files differnew file mode 100644 index 00000000000..ae0948bcfd6 --- /dev/null +++ b/app/assets/images/emoji/seedling.png diff --git a/app/assets/images/emoji/selfie.png b/app/assets/images/emoji/selfie.png Binary files differnew file mode 100644 index 00000000000..6a1ba75c7e3 --- /dev/null +++ b/app/assets/images/emoji/selfie.png diff --git a/app/assets/images/emoji/selfie_tone1.png b/app/assets/images/emoji/selfie_tone1.png Binary files differnew file mode 100644 index 00000000000..290e075b56f --- /dev/null +++ b/app/assets/images/emoji/selfie_tone1.png diff --git a/app/assets/images/emoji/selfie_tone2.png b/app/assets/images/emoji/selfie_tone2.png Binary files differnew file mode 100644 index 00000000000..fcd9595b643 --- /dev/null +++ b/app/assets/images/emoji/selfie_tone2.png diff --git a/app/assets/images/emoji/selfie_tone3.png b/app/assets/images/emoji/selfie_tone3.png Binary files differnew file mode 100644 index 00000000000..f3a22fdf435 --- /dev/null +++ b/app/assets/images/emoji/selfie_tone3.png diff --git a/app/assets/images/emoji/selfie_tone4.png b/app/assets/images/emoji/selfie_tone4.png Binary files differnew file mode 100644 index 00000000000..cdecf6d9f4e --- /dev/null +++ b/app/assets/images/emoji/selfie_tone4.png diff --git a/app/assets/images/emoji/selfie_tone5.png b/app/assets/images/emoji/selfie_tone5.png Binary files differnew file mode 100644 index 00000000000..86acbb6c202 --- /dev/null +++ b/app/assets/images/emoji/selfie_tone5.png diff --git a/app/assets/images/emoji/seven.png b/app/assets/images/emoji/seven.png Binary files differnew file mode 100644 index 00000000000..9b3476ae7c7 --- /dev/null +++ b/app/assets/images/emoji/seven.png diff --git a/app/assets/images/emoji/shallow_pan_of_food.png b/app/assets/images/emoji/shallow_pan_of_food.png Binary files differnew file mode 100644 index 00000000000..663a1006acd --- /dev/null +++ b/app/assets/images/emoji/shallow_pan_of_food.png diff --git a/app/assets/images/emoji/shamrock.png b/app/assets/images/emoji/shamrock.png Binary files differnew file mode 100644 index 00000000000..f202aecfe6f --- /dev/null +++ b/app/assets/images/emoji/shamrock.png diff --git a/app/assets/images/emoji/shark.png b/app/assets/images/emoji/shark.png Binary files differnew file mode 100644 index 00000000000..c75076d57d8 --- /dev/null +++ b/app/assets/images/emoji/shark.png diff --git a/app/assets/images/emoji/shaved_ice.png b/app/assets/images/emoji/shaved_ice.png Binary files differnew file mode 100644 index 00000000000..36dfb53ca93 --- /dev/null +++ b/app/assets/images/emoji/shaved_ice.png diff --git a/app/assets/images/emoji/sheep.png b/app/assets/images/emoji/sheep.png Binary files differnew file mode 100644 index 00000000000..102b8a52b28 --- /dev/null +++ b/app/assets/images/emoji/sheep.png diff --git a/app/assets/images/emoji/shell.png b/app/assets/images/emoji/shell.png Binary files differnew file mode 100644 index 00000000000..55721629f62 --- /dev/null +++ b/app/assets/images/emoji/shell.png diff --git a/app/assets/images/emoji/shield.png b/app/assets/images/emoji/shield.png Binary files differnew file mode 100644 index 00000000000..610bf033ce0 --- /dev/null +++ b/app/assets/images/emoji/shield.png diff --git a/app/assets/images/emoji/shinto_shrine.png b/app/assets/images/emoji/shinto_shrine.png Binary files differnew file mode 100644 index 00000000000..5a344975bf3 --- /dev/null +++ b/app/assets/images/emoji/shinto_shrine.png diff --git a/app/assets/images/emoji/ship.png b/app/assets/images/emoji/ship.png Binary files differnew file mode 100644 index 00000000000..62d54f7d6c9 --- /dev/null +++ b/app/assets/images/emoji/ship.png diff --git a/app/assets/images/emoji/shirt.png b/app/assets/images/emoji/shirt.png Binary files differnew file mode 100644 index 00000000000..af08dec8b59 --- /dev/null +++ b/app/assets/images/emoji/shirt.png diff --git a/app/assets/images/emoji/shopping_bags.png b/app/assets/images/emoji/shopping_bags.png Binary files differnew file mode 100644 index 00000000000..99f2a2b13ac --- /dev/null +++ b/app/assets/images/emoji/shopping_bags.png diff --git a/app/assets/images/emoji/shopping_cart.png b/app/assets/images/emoji/shopping_cart.png Binary files differnew file mode 100644 index 00000000000..1086fe6e456 --- /dev/null +++ b/app/assets/images/emoji/shopping_cart.png diff --git a/app/assets/images/emoji/shower.png b/app/assets/images/emoji/shower.png Binary files differnew file mode 100644 index 00000000000..156776a2e52 --- /dev/null +++ b/app/assets/images/emoji/shower.png diff --git a/app/assets/images/emoji/shrimp.png b/app/assets/images/emoji/shrimp.png Binary files differnew file mode 100644 index 00000000000..49eff28a71e --- /dev/null +++ b/app/assets/images/emoji/shrimp.png diff --git a/app/assets/images/emoji/shrug.png b/app/assets/images/emoji/shrug.png Binary files differnew file mode 100644 index 00000000000..76e63bfac77 --- /dev/null +++ b/app/assets/images/emoji/shrug.png diff --git a/app/assets/images/emoji/shrug_tone1.png b/app/assets/images/emoji/shrug_tone1.png Binary files differnew file mode 100644 index 00000000000..1c895e64468 --- /dev/null +++ b/app/assets/images/emoji/shrug_tone1.png diff --git a/app/assets/images/emoji/shrug_tone2.png b/app/assets/images/emoji/shrug_tone2.png Binary files differnew file mode 100644 index 00000000000..4e3ca8f8bac --- /dev/null +++ b/app/assets/images/emoji/shrug_tone2.png diff --git a/app/assets/images/emoji/shrug_tone3.png b/app/assets/images/emoji/shrug_tone3.png Binary files differnew file mode 100644 index 00000000000..d1b16a19bb5 --- /dev/null +++ b/app/assets/images/emoji/shrug_tone3.png diff --git a/app/assets/images/emoji/shrug_tone4.png b/app/assets/images/emoji/shrug_tone4.png Binary files differnew file mode 100644 index 00000000000..5fbef3f2255 --- /dev/null +++ b/app/assets/images/emoji/shrug_tone4.png diff --git a/app/assets/images/emoji/shrug_tone5.png b/app/assets/images/emoji/shrug_tone5.png Binary files differnew file mode 100644 index 00000000000..4af2e28bc5c --- /dev/null +++ b/app/assets/images/emoji/shrug_tone5.png diff --git a/app/assets/images/emoji/signal_strength.png b/app/assets/images/emoji/signal_strength.png Binary files differnew file mode 100644 index 00000000000..ee2b5a4b519 --- /dev/null +++ b/app/assets/images/emoji/signal_strength.png diff --git a/app/assets/images/emoji/six.png b/app/assets/images/emoji/six.png Binary files differnew file mode 100644 index 00000000000..371b3acef2c --- /dev/null +++ b/app/assets/images/emoji/six.png diff --git a/app/assets/images/emoji/six_pointed_star.png b/app/assets/images/emoji/six_pointed_star.png Binary files differnew file mode 100644 index 00000000000..2eb1707458b --- /dev/null +++ b/app/assets/images/emoji/six_pointed_star.png diff --git a/app/assets/images/emoji/ski.png b/app/assets/images/emoji/ski.png Binary files differnew file mode 100644 index 00000000000..4a2d2c12306 --- /dev/null +++ b/app/assets/images/emoji/ski.png diff --git a/app/assets/images/emoji/skier.png b/app/assets/images/emoji/skier.png Binary files differnew file mode 100644 index 00000000000..2eb3bdce2af --- /dev/null +++ b/app/assets/images/emoji/skier.png diff --git a/app/assets/images/emoji/skull.png b/app/assets/images/emoji/skull.png Binary files differnew file mode 100644 index 00000000000..26abb17296a --- /dev/null +++ b/app/assets/images/emoji/skull.png diff --git a/app/assets/images/emoji/skull_crossbones.png b/app/assets/images/emoji/skull_crossbones.png Binary files differnew file mode 100644 index 00000000000..b459df9227a --- /dev/null +++ b/app/assets/images/emoji/skull_crossbones.png diff --git a/app/assets/images/emoji/sleeping.png b/app/assets/images/emoji/sleeping.png Binary files differnew file mode 100644 index 00000000000..9ecf600d6d8 --- /dev/null +++ b/app/assets/images/emoji/sleeping.png diff --git a/app/assets/images/emoji/sleeping_accommodation.png b/app/assets/images/emoji/sleeping_accommodation.png Binary files differnew file mode 100644 index 00000000000..c739e7fb69b --- /dev/null +++ b/app/assets/images/emoji/sleeping_accommodation.png diff --git a/app/assets/images/emoji/sleepy.png b/app/assets/images/emoji/sleepy.png Binary files differnew file mode 100644 index 00000000000..836b4107717 --- /dev/null +++ b/app/assets/images/emoji/sleepy.png diff --git a/app/assets/images/emoji/slight_frown.png b/app/assets/images/emoji/slight_frown.png Binary files differnew file mode 100644 index 00000000000..b2f1d983d36 --- /dev/null +++ b/app/assets/images/emoji/slight_frown.png diff --git a/app/assets/images/emoji/slight_smile.png b/app/assets/images/emoji/slight_smile.png Binary files differnew file mode 100644 index 00000000000..ddd7d65dd3d --- /dev/null +++ b/app/assets/images/emoji/slight_smile.png diff --git a/app/assets/images/emoji/slot_machine.png b/app/assets/images/emoji/slot_machine.png Binary files differnew file mode 100644 index 00000000000..ee71b6c268c --- /dev/null +++ b/app/assets/images/emoji/slot_machine.png diff --git a/app/assets/images/emoji/small_blue_diamond.png b/app/assets/images/emoji/small_blue_diamond.png Binary files differnew file mode 100644 index 00000000000..b86b5bc4db3 --- /dev/null +++ b/app/assets/images/emoji/small_blue_diamond.png diff --git a/app/assets/images/emoji/small_orange_diamond.png b/app/assets/images/emoji/small_orange_diamond.png Binary files differnew file mode 100644 index 00000000000..e1c6ed9b2f8 --- /dev/null +++ b/app/assets/images/emoji/small_orange_diamond.png diff --git a/app/assets/images/emoji/small_red_triangle.png b/app/assets/images/emoji/small_red_triangle.png Binary files differnew file mode 100644 index 00000000000..785887c195a --- /dev/null +++ b/app/assets/images/emoji/small_red_triangle.png diff --git a/app/assets/images/emoji/small_red_triangle_down.png b/app/assets/images/emoji/small_red_triangle_down.png Binary files differnew file mode 100644 index 00000000000..a83beff1914 --- /dev/null +++ b/app/assets/images/emoji/small_red_triangle_down.png diff --git a/app/assets/images/emoji/smile.png b/app/assets/images/emoji/smile.png Binary files differnew file mode 100644 index 00000000000..aa47ffe978c --- /dev/null +++ b/app/assets/images/emoji/smile.png diff --git a/app/assets/images/emoji/smile_cat.png b/app/assets/images/emoji/smile_cat.png Binary files differnew file mode 100644 index 00000000000..6f25f11dd3a --- /dev/null +++ b/app/assets/images/emoji/smile_cat.png diff --git a/app/assets/images/emoji/smiley.png b/app/assets/images/emoji/smiley.png Binary files differnew file mode 100644 index 00000000000..30957a65968 --- /dev/null +++ b/app/assets/images/emoji/smiley.png diff --git a/app/assets/images/emoji/smiley_cat.png b/app/assets/images/emoji/smiley_cat.png Binary files differnew file mode 100644 index 00000000000..163b57a3427 --- /dev/null +++ b/app/assets/images/emoji/smiley_cat.png diff --git a/app/assets/images/emoji/smiling_imp.png b/app/assets/images/emoji/smiling_imp.png Binary files differnew file mode 100644 index 00000000000..cc2c5f1ec72 --- /dev/null +++ b/app/assets/images/emoji/smiling_imp.png diff --git a/app/assets/images/emoji/smirk.png b/app/assets/images/emoji/smirk.png Binary files differnew file mode 100644 index 00000000000..87852109988 --- /dev/null +++ b/app/assets/images/emoji/smirk.png diff --git a/app/assets/images/emoji/smirk_cat.png b/app/assets/images/emoji/smirk_cat.png Binary files differnew file mode 100644 index 00000000000..9ac5954c199 --- /dev/null +++ b/app/assets/images/emoji/smirk_cat.png diff --git a/app/assets/images/emoji/smoking.png b/app/assets/images/emoji/smoking.png Binary files differnew file mode 100644 index 00000000000..910f648c8f9 --- /dev/null +++ b/app/assets/images/emoji/smoking.png diff --git a/app/assets/images/emoji/snail.png b/app/assets/images/emoji/snail.png Binary files differnew file mode 100644 index 00000000000..f4ea071e2d3 --- /dev/null +++ b/app/assets/images/emoji/snail.png diff --git a/app/assets/images/emoji/snake.png b/app/assets/images/emoji/snake.png Binary files differnew file mode 100644 index 00000000000..d0278a28d8c --- /dev/null +++ b/app/assets/images/emoji/snake.png diff --git a/app/assets/images/emoji/sneezing_face.png b/app/assets/images/emoji/sneezing_face.png Binary files differnew file mode 100644 index 00000000000..ccf07d4b64d --- /dev/null +++ b/app/assets/images/emoji/sneezing_face.png diff --git a/app/assets/images/emoji/snowboarder.png b/app/assets/images/emoji/snowboarder.png Binary files differnew file mode 100644 index 00000000000..6361c0f2c9d --- /dev/null +++ b/app/assets/images/emoji/snowboarder.png diff --git a/app/assets/images/emoji/snowflake.png b/app/assets/images/emoji/snowflake.png Binary files differnew file mode 100644 index 00000000000..db319a77ec6 --- /dev/null +++ b/app/assets/images/emoji/snowflake.png diff --git a/app/assets/images/emoji/snowman.png b/app/assets/images/emoji/snowman.png Binary files differnew file mode 100644 index 00000000000..20c177c2aff --- /dev/null +++ b/app/assets/images/emoji/snowman.png diff --git a/app/assets/images/emoji/snowman2.png b/app/assets/images/emoji/snowman2.png Binary files differnew file mode 100644 index 00000000000..896f28502af --- /dev/null +++ b/app/assets/images/emoji/snowman2.png diff --git a/app/assets/images/emoji/sob.png b/app/assets/images/emoji/sob.png Binary files differnew file mode 100644 index 00000000000..52e3517a1ee --- /dev/null +++ b/app/assets/images/emoji/sob.png diff --git a/app/assets/images/emoji/soccer.png b/app/assets/images/emoji/soccer.png Binary files differnew file mode 100644 index 00000000000..28cfa218d6d --- /dev/null +++ b/app/assets/images/emoji/soccer.png diff --git a/app/assets/images/emoji/soon.png b/app/assets/images/emoji/soon.png Binary files differnew file mode 100644 index 00000000000..8cdfd86690d --- /dev/null +++ b/app/assets/images/emoji/soon.png diff --git a/app/assets/images/emoji/sos.png b/app/assets/images/emoji/sos.png Binary files differnew file mode 100644 index 00000000000..d7d8c9953e4 --- /dev/null +++ b/app/assets/images/emoji/sos.png diff --git a/app/assets/images/emoji/sound.png b/app/assets/images/emoji/sound.png Binary files differnew file mode 100644 index 00000000000..e75ddca53ba --- /dev/null +++ b/app/assets/images/emoji/sound.png diff --git a/app/assets/images/emoji/space_invader.png b/app/assets/images/emoji/space_invader.png Binary files differnew file mode 100644 index 00000000000..2e73f5f32e5 --- /dev/null +++ b/app/assets/images/emoji/space_invader.png diff --git a/app/assets/images/emoji/spades.png b/app/assets/images/emoji/spades.png Binary files differnew file mode 100644 index 00000000000..f822f184cb0 --- /dev/null +++ b/app/assets/images/emoji/spades.png diff --git a/app/assets/images/emoji/spaghetti.png b/app/assets/images/emoji/spaghetti.png Binary files differnew file mode 100644 index 00000000000..89c24a321f1 --- /dev/null +++ b/app/assets/images/emoji/spaghetti.png diff --git a/app/assets/images/emoji/sparkle.png b/app/assets/images/emoji/sparkle.png Binary files differnew file mode 100644 index 00000000000..6aa7b6ec9cf --- /dev/null +++ b/app/assets/images/emoji/sparkle.png diff --git a/app/assets/images/emoji/sparkler.png b/app/assets/images/emoji/sparkler.png Binary files differnew file mode 100644 index 00000000000..30339cd6e09 --- /dev/null +++ b/app/assets/images/emoji/sparkler.png diff --git a/app/assets/images/emoji/sparkles.png b/app/assets/images/emoji/sparkles.png Binary files differnew file mode 100644 index 00000000000..169bc10b023 --- /dev/null +++ b/app/assets/images/emoji/sparkles.png diff --git a/app/assets/images/emoji/sparkling_heart.png b/app/assets/images/emoji/sparkling_heart.png Binary files differnew file mode 100644 index 00000000000..6709269454e --- /dev/null +++ b/app/assets/images/emoji/sparkling_heart.png diff --git a/app/assets/images/emoji/speak_no_evil.png b/app/assets/images/emoji/speak_no_evil.png Binary files differnew file mode 100644 index 00000000000..9d9e07c974b --- /dev/null +++ b/app/assets/images/emoji/speak_no_evil.png diff --git a/app/assets/images/emoji/speaker.png b/app/assets/images/emoji/speaker.png Binary files differnew file mode 100644 index 00000000000..7bcffb8fc43 --- /dev/null +++ b/app/assets/images/emoji/speaker.png diff --git a/app/assets/images/emoji/speaking_head.png b/app/assets/images/emoji/speaking_head.png Binary files differnew file mode 100644 index 00000000000..2df93aaae09 --- /dev/null +++ b/app/assets/images/emoji/speaking_head.png diff --git a/app/assets/images/emoji/speech_balloon.png b/app/assets/images/emoji/speech_balloon.png Binary files differnew file mode 100644 index 00000000000..a34ef741733 --- /dev/null +++ b/app/assets/images/emoji/speech_balloon.png diff --git a/app/assets/images/emoji/speedboat.png b/app/assets/images/emoji/speedboat.png Binary files differnew file mode 100644 index 00000000000..74059d12de1 --- /dev/null +++ b/app/assets/images/emoji/speedboat.png diff --git a/app/assets/images/emoji/spider.png b/app/assets/images/emoji/spider.png Binary files differnew file mode 100644 index 00000000000..3849fa90b94 --- /dev/null +++ b/app/assets/images/emoji/spider.png diff --git a/app/assets/images/emoji/spider_web.png b/app/assets/images/emoji/spider_web.png Binary files differnew file mode 100644 index 00000000000..ba448ee7fba --- /dev/null +++ b/app/assets/images/emoji/spider_web.png diff --git a/app/assets/images/emoji/spoon.png b/app/assets/images/emoji/spoon.png Binary files differnew file mode 100644 index 00000000000..3c4da766aee --- /dev/null +++ b/app/assets/images/emoji/spoon.png diff --git a/app/assets/images/emoji/spy.png b/app/assets/images/emoji/spy.png Binary files differnew file mode 100644 index 00000000000..a729e9584d6 --- /dev/null +++ b/app/assets/images/emoji/spy.png diff --git a/app/assets/images/emoji/spy_tone1.png b/app/assets/images/emoji/spy_tone1.png Binary files differnew file mode 100644 index 00000000000..2d1c022caee --- /dev/null +++ b/app/assets/images/emoji/spy_tone1.png diff --git a/app/assets/images/emoji/spy_tone2.png b/app/assets/images/emoji/spy_tone2.png Binary files differnew file mode 100644 index 00000000000..548b9c26f5d --- /dev/null +++ b/app/assets/images/emoji/spy_tone2.png diff --git a/app/assets/images/emoji/spy_tone3.png b/app/assets/images/emoji/spy_tone3.png Binary files differnew file mode 100644 index 00000000000..b023f4b18e1 --- /dev/null +++ b/app/assets/images/emoji/spy_tone3.png diff --git a/app/assets/images/emoji/spy_tone4.png b/app/assets/images/emoji/spy_tone4.png Binary files differnew file mode 100644 index 00000000000..d8300af492d --- /dev/null +++ b/app/assets/images/emoji/spy_tone4.png diff --git a/app/assets/images/emoji/spy_tone5.png b/app/assets/images/emoji/spy_tone5.png Binary files differnew file mode 100644 index 00000000000..ca1462595fa --- /dev/null +++ b/app/assets/images/emoji/spy_tone5.png diff --git a/app/assets/images/emoji/squid.png b/app/assets/images/emoji/squid.png Binary files differnew file mode 100644 index 00000000000..d2af223f0cb --- /dev/null +++ b/app/assets/images/emoji/squid.png diff --git a/app/assets/images/emoji/stadium.png b/app/assets/images/emoji/stadium.png Binary files differnew file mode 100644 index 00000000000..00cd6db5e29 --- /dev/null +++ b/app/assets/images/emoji/stadium.png diff --git a/app/assets/images/emoji/star.png b/app/assets/images/emoji/star.png Binary files differnew file mode 100644 index 00000000000..c930947076e --- /dev/null +++ b/app/assets/images/emoji/star.png diff --git a/app/assets/images/emoji/star2.png b/app/assets/images/emoji/star2.png Binary files differnew file mode 100644 index 00000000000..2f5cba592db --- /dev/null +++ b/app/assets/images/emoji/star2.png diff --git a/app/assets/images/emoji/star_and_crescent.png b/app/assets/images/emoji/star_and_crescent.png Binary files differnew file mode 100644 index 00000000000..e182636457d --- /dev/null +++ b/app/assets/images/emoji/star_and_crescent.png diff --git a/app/assets/images/emoji/star_of_david.png b/app/assets/images/emoji/star_of_david.png Binary files differnew file mode 100644 index 00000000000..fc59d0dde24 --- /dev/null +++ b/app/assets/images/emoji/star_of_david.png diff --git a/app/assets/images/emoji/stars.png b/app/assets/images/emoji/stars.png Binary files differnew file mode 100644 index 00000000000..aa45384d1c6 --- /dev/null +++ b/app/assets/images/emoji/stars.png diff --git a/app/assets/images/emoji/station.png b/app/assets/images/emoji/station.png Binary files differnew file mode 100644 index 00000000000..5c26fee529c --- /dev/null +++ b/app/assets/images/emoji/station.png diff --git a/app/assets/images/emoji/statue_of_liberty.png b/app/assets/images/emoji/statue_of_liberty.png Binary files differnew file mode 100644 index 00000000000..05df8289b59 --- /dev/null +++ b/app/assets/images/emoji/statue_of_liberty.png diff --git a/app/assets/images/emoji/steam_locomotive.png b/app/assets/images/emoji/steam_locomotive.png Binary files differnew file mode 100644 index 00000000000..9ac0d999c4c --- /dev/null +++ b/app/assets/images/emoji/steam_locomotive.png diff --git a/app/assets/images/emoji/stew.png b/app/assets/images/emoji/stew.png Binary files differnew file mode 100644 index 00000000000..6b3f010c17a --- /dev/null +++ b/app/assets/images/emoji/stew.png diff --git a/app/assets/images/emoji/stop_button.png b/app/assets/images/emoji/stop_button.png Binary files differnew file mode 100644 index 00000000000..cfa99988ac2 --- /dev/null +++ b/app/assets/images/emoji/stop_button.png diff --git a/app/assets/images/emoji/stopwatch.png b/app/assets/images/emoji/stopwatch.png Binary files differnew file mode 100644 index 00000000000..8fae1c9a898 --- /dev/null +++ b/app/assets/images/emoji/stopwatch.png diff --git a/app/assets/images/emoji/straight_ruler.png b/app/assets/images/emoji/straight_ruler.png Binary files differnew file mode 100644 index 00000000000..1017b7433a1 --- /dev/null +++ b/app/assets/images/emoji/straight_ruler.png diff --git a/app/assets/images/emoji/strawberry.png b/app/assets/images/emoji/strawberry.png Binary files differnew file mode 100644 index 00000000000..7bb86f0b29c --- /dev/null +++ b/app/assets/images/emoji/strawberry.png diff --git a/app/assets/images/emoji/stuck_out_tongue.png b/app/assets/images/emoji/stuck_out_tongue.png Binary files differnew file mode 100644 index 00000000000..25757341f96 --- /dev/null +++ b/app/assets/images/emoji/stuck_out_tongue.png diff --git a/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png b/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png Binary files differnew file mode 100644 index 00000000000..5c0401e9b1d --- /dev/null +++ b/app/assets/images/emoji/stuck_out_tongue_closed_eyes.png diff --git a/app/assets/images/emoji/stuck_out_tongue_winking_eye.png b/app/assets/images/emoji/stuck_out_tongue_winking_eye.png Binary files differnew file mode 100644 index 00000000000..4817eaa3dc6 --- /dev/null +++ b/app/assets/images/emoji/stuck_out_tongue_winking_eye.png diff --git a/app/assets/images/emoji/stuffed_flatbread.png b/app/assets/images/emoji/stuffed_flatbread.png Binary files differnew file mode 100644 index 00000000000..a2e10df40a5 --- /dev/null +++ b/app/assets/images/emoji/stuffed_flatbread.png diff --git a/app/assets/images/emoji/sun_with_face.png b/app/assets/images/emoji/sun_with_face.png Binary files differnew file mode 100644 index 00000000000..14a4ea971db --- /dev/null +++ b/app/assets/images/emoji/sun_with_face.png diff --git a/app/assets/images/emoji/sunflower.png b/app/assets/images/emoji/sunflower.png Binary files differnew file mode 100644 index 00000000000..08cc07761ea --- /dev/null +++ b/app/assets/images/emoji/sunflower.png diff --git a/app/assets/images/emoji/sunglasses.png b/app/assets/images/emoji/sunglasses.png Binary files differnew file mode 100644 index 00000000000..20011735110 --- /dev/null +++ b/app/assets/images/emoji/sunglasses.png diff --git a/app/assets/images/emoji/sunny.png b/app/assets/images/emoji/sunny.png Binary files differnew file mode 100644 index 00000000000..fd521ae31a7 --- /dev/null +++ b/app/assets/images/emoji/sunny.png diff --git a/app/assets/images/emoji/sunrise.png b/app/assets/images/emoji/sunrise.png Binary files differnew file mode 100644 index 00000000000..4ad36003c20 --- /dev/null +++ b/app/assets/images/emoji/sunrise.png diff --git a/app/assets/images/emoji/sunrise_over_mountains.png b/app/assets/images/emoji/sunrise_over_mountains.png Binary files differnew file mode 100644 index 00000000000..2b99307344d --- /dev/null +++ b/app/assets/images/emoji/sunrise_over_mountains.png diff --git a/app/assets/images/emoji/surfer.png b/app/assets/images/emoji/surfer.png Binary files differnew file mode 100644 index 00000000000..3ab017adf4b --- /dev/null +++ b/app/assets/images/emoji/surfer.png diff --git a/app/assets/images/emoji/surfer_tone1.png b/app/assets/images/emoji/surfer_tone1.png Binary files differnew file mode 100644 index 00000000000..b5faaa524cc --- /dev/null +++ b/app/assets/images/emoji/surfer_tone1.png diff --git a/app/assets/images/emoji/surfer_tone2.png b/app/assets/images/emoji/surfer_tone2.png Binary files differnew file mode 100644 index 00000000000..6d92e412ff1 --- /dev/null +++ b/app/assets/images/emoji/surfer_tone2.png diff --git a/app/assets/images/emoji/surfer_tone3.png b/app/assets/images/emoji/surfer_tone3.png Binary files differnew file mode 100644 index 00000000000..f05ef59496e --- /dev/null +++ b/app/assets/images/emoji/surfer_tone3.png diff --git a/app/assets/images/emoji/surfer_tone4.png b/app/assets/images/emoji/surfer_tone4.png Binary files differnew file mode 100644 index 00000000000..35e143d19dc --- /dev/null +++ b/app/assets/images/emoji/surfer_tone4.png diff --git a/app/assets/images/emoji/surfer_tone5.png b/app/assets/images/emoji/surfer_tone5.png Binary files differnew file mode 100644 index 00000000000..38917658eac --- /dev/null +++ b/app/assets/images/emoji/surfer_tone5.png diff --git a/app/assets/images/emoji/sushi.png b/app/assets/images/emoji/sushi.png Binary files differnew file mode 100644 index 00000000000..f171fd2f7a1 --- /dev/null +++ b/app/assets/images/emoji/sushi.png diff --git a/app/assets/images/emoji/suspension_railway.png b/app/assets/images/emoji/suspension_railway.png Binary files differnew file mode 100644 index 00000000000..a59d5f48c24 --- /dev/null +++ b/app/assets/images/emoji/suspension_railway.png diff --git a/app/assets/images/emoji/sweat.png b/app/assets/images/emoji/sweat.png Binary files differnew file mode 100644 index 00000000000..f0dae7b7893 --- /dev/null +++ b/app/assets/images/emoji/sweat.png diff --git a/app/assets/images/emoji/sweat_drops.png b/app/assets/images/emoji/sweat_drops.png Binary files differnew file mode 100644 index 00000000000..4106117ebc8 --- /dev/null +++ b/app/assets/images/emoji/sweat_drops.png diff --git a/app/assets/images/emoji/sweat_smile.png b/app/assets/images/emoji/sweat_smile.png Binary files differnew file mode 100644 index 00000000000..cb18d9c899b --- /dev/null +++ b/app/assets/images/emoji/sweat_smile.png diff --git a/app/assets/images/emoji/sweet_potato.png b/app/assets/images/emoji/sweet_potato.png Binary files differnew file mode 100644 index 00000000000..92a425f2e20 --- /dev/null +++ b/app/assets/images/emoji/sweet_potato.png diff --git a/app/assets/images/emoji/swimmer.png b/app/assets/images/emoji/swimmer.png Binary files differnew file mode 100644 index 00000000000..55b4d72f9a7 --- /dev/null +++ b/app/assets/images/emoji/swimmer.png diff --git a/app/assets/images/emoji/swimmer_tone1.png b/app/assets/images/emoji/swimmer_tone1.png Binary files differnew file mode 100644 index 00000000000..38441c9ca9a --- /dev/null +++ b/app/assets/images/emoji/swimmer_tone1.png diff --git a/app/assets/images/emoji/swimmer_tone2.png b/app/assets/images/emoji/swimmer_tone2.png Binary files differnew file mode 100644 index 00000000000..b0d43112444 --- /dev/null +++ b/app/assets/images/emoji/swimmer_tone2.png diff --git a/app/assets/images/emoji/swimmer_tone3.png b/app/assets/images/emoji/swimmer_tone3.png Binary files differnew file mode 100644 index 00000000000..211e77e2aa0 --- /dev/null +++ b/app/assets/images/emoji/swimmer_tone3.png diff --git a/app/assets/images/emoji/swimmer_tone4.png b/app/assets/images/emoji/swimmer_tone4.png Binary files differnew file mode 100644 index 00000000000..f34c34db9d2 --- /dev/null +++ b/app/assets/images/emoji/swimmer_tone4.png diff --git a/app/assets/images/emoji/swimmer_tone5.png b/app/assets/images/emoji/swimmer_tone5.png Binary files differnew file mode 100644 index 00000000000..3e9231ff868 --- /dev/null +++ b/app/assets/images/emoji/swimmer_tone5.png diff --git a/app/assets/images/emoji/symbols.png b/app/assets/images/emoji/symbols.png Binary files differnew file mode 100644 index 00000000000..ac2fc1f358f --- /dev/null +++ b/app/assets/images/emoji/symbols.png diff --git a/app/assets/images/emoji/synagogue.png b/app/assets/images/emoji/synagogue.png Binary files differnew file mode 100644 index 00000000000..ee347904c80 --- /dev/null +++ b/app/assets/images/emoji/synagogue.png diff --git a/app/assets/images/emoji/syringe.png b/app/assets/images/emoji/syringe.png Binary files differnew file mode 100644 index 00000000000..71c1a9528d5 --- /dev/null +++ b/app/assets/images/emoji/syringe.png diff --git a/app/assets/images/emoji/taco.png b/app/assets/images/emoji/taco.png Binary files differnew file mode 100644 index 00000000000..10e847a4619 --- /dev/null +++ b/app/assets/images/emoji/taco.png diff --git a/app/assets/images/emoji/tada.png b/app/assets/images/emoji/tada.png Binary files differnew file mode 100644 index 00000000000..0244d60f269 --- /dev/null +++ b/app/assets/images/emoji/tada.png diff --git a/app/assets/images/emoji/tanabata_tree.png b/app/assets/images/emoji/tanabata_tree.png Binary files differnew file mode 100644 index 00000000000..46fcb3a1aac --- /dev/null +++ b/app/assets/images/emoji/tanabata_tree.png diff --git a/app/assets/images/emoji/tangerine.png b/app/assets/images/emoji/tangerine.png Binary files differnew file mode 100644 index 00000000000..ab14e5378db --- /dev/null +++ b/app/assets/images/emoji/tangerine.png diff --git a/app/assets/images/emoji/taurus.png b/app/assets/images/emoji/taurus.png Binary files differnew file mode 100644 index 00000000000..b2a370df42b --- /dev/null +++ b/app/assets/images/emoji/taurus.png diff --git a/app/assets/images/emoji/taxi.png b/app/assets/images/emoji/taxi.png Binary files differnew file mode 100644 index 00000000000..55f4cc84797 --- /dev/null +++ b/app/assets/images/emoji/taxi.png diff --git a/app/assets/images/emoji/tea.png b/app/assets/images/emoji/tea.png Binary files differnew file mode 100644 index 00000000000..b53b98f0c45 --- /dev/null +++ b/app/assets/images/emoji/tea.png diff --git a/app/assets/images/emoji/telephone.png b/app/assets/images/emoji/telephone.png Binary files differnew file mode 100644 index 00000000000..a1e69f566bc --- /dev/null +++ b/app/assets/images/emoji/telephone.png diff --git a/app/assets/images/emoji/telephone_receiver.png b/app/assets/images/emoji/telephone_receiver.png Binary files differnew file mode 100644 index 00000000000..69388316c35 --- /dev/null +++ b/app/assets/images/emoji/telephone_receiver.png diff --git a/app/assets/images/emoji/telescope.png b/app/assets/images/emoji/telescope.png Binary files differnew file mode 100644 index 00000000000..d63154614b5 --- /dev/null +++ b/app/assets/images/emoji/telescope.png diff --git a/app/assets/images/emoji/ten.png b/app/assets/images/emoji/ten.png Binary files differnew file mode 100644 index 00000000000..782d4004962 --- /dev/null +++ b/app/assets/images/emoji/ten.png diff --git a/app/assets/images/emoji/tennis.png b/app/assets/images/emoji/tennis.png Binary files differnew file mode 100644 index 00000000000..7e68ba8f301 --- /dev/null +++ b/app/assets/images/emoji/tennis.png diff --git a/app/assets/images/emoji/tent.png b/app/assets/images/emoji/tent.png Binary files differnew file mode 100644 index 00000000000..3fddcfc56eb --- /dev/null +++ b/app/assets/images/emoji/tent.png diff --git a/app/assets/images/emoji/thermometer.png b/app/assets/images/emoji/thermometer.png Binary files differnew file mode 100644 index 00000000000..b1147392426 --- /dev/null +++ b/app/assets/images/emoji/thermometer.png diff --git a/app/assets/images/emoji/thermometer_face.png b/app/assets/images/emoji/thermometer_face.png Binary files differnew file mode 100644 index 00000000000..8fc57387563 --- /dev/null +++ b/app/assets/images/emoji/thermometer_face.png diff --git a/app/assets/images/emoji/thinking.png b/app/assets/images/emoji/thinking.png Binary files differnew file mode 100644 index 00000000000..c18f6fd14ad --- /dev/null +++ b/app/assets/images/emoji/thinking.png diff --git a/app/assets/images/emoji/third_place.png b/app/assets/images/emoji/third_place.png Binary files differnew file mode 100644 index 00000000000..636e04a5950 --- /dev/null +++ b/app/assets/images/emoji/third_place.png diff --git a/app/assets/images/emoji/thought_balloon.png b/app/assets/images/emoji/thought_balloon.png Binary files differnew file mode 100644 index 00000000000..72fe8fa7022 --- /dev/null +++ b/app/assets/images/emoji/thought_balloon.png diff --git a/app/assets/images/emoji/three.png b/app/assets/images/emoji/three.png Binary files differnew file mode 100644 index 00000000000..dbaa6183e72 --- /dev/null +++ b/app/assets/images/emoji/three.png diff --git a/app/assets/images/emoji/thumbsdown.png b/app/assets/images/emoji/thumbsdown.png Binary files differnew file mode 100644 index 00000000000..b63da2f20a8 --- /dev/null +++ b/app/assets/images/emoji/thumbsdown.png diff --git a/app/assets/images/emoji/thumbsdown_tone1.png b/app/assets/images/emoji/thumbsdown_tone1.png Binary files differnew file mode 100644 index 00000000000..a1631af8e92 --- /dev/null +++ b/app/assets/images/emoji/thumbsdown_tone1.png diff --git a/app/assets/images/emoji/thumbsdown_tone2.png b/app/assets/images/emoji/thumbsdown_tone2.png Binary files differnew file mode 100644 index 00000000000..85fff82d595 --- /dev/null +++ b/app/assets/images/emoji/thumbsdown_tone2.png diff --git a/app/assets/images/emoji/thumbsdown_tone3.png b/app/assets/images/emoji/thumbsdown_tone3.png Binary files differnew file mode 100644 index 00000000000..eeba3be80fd --- /dev/null +++ b/app/assets/images/emoji/thumbsdown_tone3.png diff --git a/app/assets/images/emoji/thumbsdown_tone4.png b/app/assets/images/emoji/thumbsdown_tone4.png Binary files differnew file mode 100644 index 00000000000..1addafdaed0 --- /dev/null +++ b/app/assets/images/emoji/thumbsdown_tone4.png diff --git a/app/assets/images/emoji/thumbsdown_tone5.png b/app/assets/images/emoji/thumbsdown_tone5.png Binary files differnew file mode 100644 index 00000000000..37ec07b5721 --- /dev/null +++ b/app/assets/images/emoji/thumbsdown_tone5.png diff --git a/app/assets/images/emoji/thumbsup.png b/app/assets/images/emoji/thumbsup.png Binary files differnew file mode 100644 index 00000000000..f9e6f13a34f --- /dev/null +++ b/app/assets/images/emoji/thumbsup.png diff --git a/app/assets/images/emoji/thumbsup_tone1.png b/app/assets/images/emoji/thumbsup_tone1.png Binary files differnew file mode 100644 index 00000000000..39684cd5cc7 --- /dev/null +++ b/app/assets/images/emoji/thumbsup_tone1.png diff --git a/app/assets/images/emoji/thumbsup_tone2.png b/app/assets/images/emoji/thumbsup_tone2.png Binary files differnew file mode 100644 index 00000000000..a9b59723573 --- /dev/null +++ b/app/assets/images/emoji/thumbsup_tone2.png diff --git a/app/assets/images/emoji/thumbsup_tone3.png b/app/assets/images/emoji/thumbsup_tone3.png Binary files differnew file mode 100644 index 00000000000..c5e29167015 --- /dev/null +++ b/app/assets/images/emoji/thumbsup_tone3.png diff --git a/app/assets/images/emoji/thumbsup_tone4.png b/app/assets/images/emoji/thumbsup_tone4.png Binary files differnew file mode 100644 index 00000000000..5bf4857a884 --- /dev/null +++ b/app/assets/images/emoji/thumbsup_tone4.png diff --git a/app/assets/images/emoji/thumbsup_tone5.png b/app/assets/images/emoji/thumbsup_tone5.png Binary files differnew file mode 100644 index 00000000000..d829f787c61 --- /dev/null +++ b/app/assets/images/emoji/thumbsup_tone5.png diff --git a/app/assets/images/emoji/thunder_cloud_rain.png b/app/assets/images/emoji/thunder_cloud_rain.png Binary files differnew file mode 100644 index 00000000000..31a26a1b6ee --- /dev/null +++ b/app/assets/images/emoji/thunder_cloud_rain.png diff --git a/app/assets/images/emoji/ticket.png b/app/assets/images/emoji/ticket.png Binary files differnew file mode 100644 index 00000000000..605936bb6b3 --- /dev/null +++ b/app/assets/images/emoji/ticket.png diff --git a/app/assets/images/emoji/tickets.png b/app/assets/images/emoji/tickets.png Binary files differnew file mode 100644 index 00000000000..e510f4a7a50 --- /dev/null +++ b/app/assets/images/emoji/tickets.png diff --git a/app/assets/images/emoji/tiger.png b/app/assets/images/emoji/tiger.png Binary files differnew file mode 100644 index 00000000000..a4d3ef086d4 --- /dev/null +++ b/app/assets/images/emoji/tiger.png diff --git a/app/assets/images/emoji/tiger2.png b/app/assets/images/emoji/tiger2.png Binary files differnew file mode 100644 index 00000000000..871a8b74d56 --- /dev/null +++ b/app/assets/images/emoji/tiger2.png diff --git a/app/assets/images/emoji/timer.png b/app/assets/images/emoji/timer.png Binary files differnew file mode 100644 index 00000000000..8a3be574c24 --- /dev/null +++ b/app/assets/images/emoji/timer.png diff --git a/app/assets/images/emoji/tired_face.png b/app/assets/images/emoji/tired_face.png Binary files differnew file mode 100644 index 00000000000..4e01eff5b23 --- /dev/null +++ b/app/assets/images/emoji/tired_face.png diff --git a/app/assets/images/emoji/tm.png b/app/assets/images/emoji/tm.png Binary files differnew file mode 100644 index 00000000000..7a0c44a2c2b --- /dev/null +++ b/app/assets/images/emoji/tm.png diff --git a/app/assets/images/emoji/toilet.png b/app/assets/images/emoji/toilet.png Binary files differnew file mode 100644 index 00000000000..1392f761835 --- /dev/null +++ b/app/assets/images/emoji/toilet.png diff --git a/app/assets/images/emoji/tokyo_tower.png b/app/assets/images/emoji/tokyo_tower.png Binary files differnew file mode 100644 index 00000000000..37df7fc65b1 --- /dev/null +++ b/app/assets/images/emoji/tokyo_tower.png diff --git a/app/assets/images/emoji/tomato.png b/app/assets/images/emoji/tomato.png Binary files differnew file mode 100644 index 00000000000..497da8f6b22 --- /dev/null +++ b/app/assets/images/emoji/tomato.png diff --git a/app/assets/images/emoji/tone1.png b/app/assets/images/emoji/tone1.png Binary files differnew file mode 100644 index 00000000000..c395f3d0d68 --- /dev/null +++ b/app/assets/images/emoji/tone1.png diff --git a/app/assets/images/emoji/tone2.png b/app/assets/images/emoji/tone2.png Binary files differnew file mode 100644 index 00000000000..080847431c1 --- /dev/null +++ b/app/assets/images/emoji/tone2.png diff --git a/app/assets/images/emoji/tone3.png b/app/assets/images/emoji/tone3.png Binary files differnew file mode 100644 index 00000000000..482dd403475 --- /dev/null +++ b/app/assets/images/emoji/tone3.png diff --git a/app/assets/images/emoji/tone4.png b/app/assets/images/emoji/tone4.png Binary files differnew file mode 100644 index 00000000000..5cae8bb20b0 --- /dev/null +++ b/app/assets/images/emoji/tone4.png diff --git a/app/assets/images/emoji/tone5.png b/app/assets/images/emoji/tone5.png Binary files differnew file mode 100644 index 00000000000..49d1a8c3a64 --- /dev/null +++ b/app/assets/images/emoji/tone5.png diff --git a/app/assets/images/emoji/tongue.png b/app/assets/images/emoji/tongue.png Binary files differnew file mode 100644 index 00000000000..70ce9c1225f --- /dev/null +++ b/app/assets/images/emoji/tongue.png diff --git a/app/assets/images/emoji/tools.png b/app/assets/images/emoji/tools.png Binary files differnew file mode 100644 index 00000000000..3c6049273a9 --- /dev/null +++ b/app/assets/images/emoji/tools.png diff --git a/app/assets/images/emoji/top.png b/app/assets/images/emoji/top.png Binary files differnew file mode 100644 index 00000000000..49dea8c08b5 --- /dev/null +++ b/app/assets/images/emoji/top.png diff --git a/app/assets/images/emoji/tophat.png b/app/assets/images/emoji/tophat.png Binary files differnew file mode 100644 index 00000000000..131b657b109 --- /dev/null +++ b/app/assets/images/emoji/tophat.png diff --git a/app/assets/images/emoji/track_next.png b/app/assets/images/emoji/track_next.png Binary files differnew file mode 100644 index 00000000000..f8880d33bab --- /dev/null +++ b/app/assets/images/emoji/track_next.png diff --git a/app/assets/images/emoji/track_previous.png b/app/assets/images/emoji/track_previous.png Binary files differnew file mode 100644 index 00000000000..1ffd0566cfc --- /dev/null +++ b/app/assets/images/emoji/track_previous.png diff --git a/app/assets/images/emoji/trackball.png b/app/assets/images/emoji/trackball.png Binary files differnew file mode 100644 index 00000000000..3bea84ad7ce --- /dev/null +++ b/app/assets/images/emoji/trackball.png diff --git a/app/assets/images/emoji/tractor.png b/app/assets/images/emoji/tractor.png Binary files differnew file mode 100644 index 00000000000..c1bf8cae44f --- /dev/null +++ b/app/assets/images/emoji/tractor.png diff --git a/app/assets/images/emoji/traffic_light.png b/app/assets/images/emoji/traffic_light.png Binary files differnew file mode 100644 index 00000000000..6b312285b00 --- /dev/null +++ b/app/assets/images/emoji/traffic_light.png diff --git a/app/assets/images/emoji/train.png b/app/assets/images/emoji/train.png Binary files differnew file mode 100644 index 00000000000..3c80321f7e8 --- /dev/null +++ b/app/assets/images/emoji/train.png diff --git a/app/assets/images/emoji/train2.png b/app/assets/images/emoji/train2.png Binary files differnew file mode 100644 index 00000000000..367c7bc5d39 --- /dev/null +++ b/app/assets/images/emoji/train2.png diff --git a/app/assets/images/emoji/tram.png b/app/assets/images/emoji/tram.png Binary files differnew file mode 100644 index 00000000000..b6f0e69038f --- /dev/null +++ b/app/assets/images/emoji/tram.png diff --git a/app/assets/images/emoji/triangular_flag_on_post.png b/app/assets/images/emoji/triangular_flag_on_post.png Binary files differnew file mode 100644 index 00000000000..c12d8b06886 --- /dev/null +++ b/app/assets/images/emoji/triangular_flag_on_post.png diff --git a/app/assets/images/emoji/triangular_ruler.png b/app/assets/images/emoji/triangular_ruler.png Binary files differnew file mode 100644 index 00000000000..77dee9ee843 --- /dev/null +++ b/app/assets/images/emoji/triangular_ruler.png diff --git a/app/assets/images/emoji/trident.png b/app/assets/images/emoji/trident.png Binary files differnew file mode 100644 index 00000000000..777a1dad121 --- /dev/null +++ b/app/assets/images/emoji/trident.png diff --git a/app/assets/images/emoji/triumph.png b/app/assets/images/emoji/triumph.png Binary files differnew file mode 100644 index 00000000000..0be7a501969 --- /dev/null +++ b/app/assets/images/emoji/triumph.png diff --git a/app/assets/images/emoji/trolleybus.png b/app/assets/images/emoji/trolleybus.png Binary files differnew file mode 100644 index 00000000000..139a9931b52 --- /dev/null +++ b/app/assets/images/emoji/trolleybus.png diff --git a/app/assets/images/emoji/trophy.png b/app/assets/images/emoji/trophy.png Binary files differnew file mode 100644 index 00000000000..ac2895c1896 --- /dev/null +++ b/app/assets/images/emoji/trophy.png diff --git a/app/assets/images/emoji/tropical_drink.png b/app/assets/images/emoji/tropical_drink.png Binary files differnew file mode 100644 index 00000000000..cd714f81b36 --- /dev/null +++ b/app/assets/images/emoji/tropical_drink.png diff --git a/app/assets/images/emoji/tropical_fish.png b/app/assets/images/emoji/tropical_fish.png Binary files differnew file mode 100644 index 00000000000..252105235a6 --- /dev/null +++ b/app/assets/images/emoji/tropical_fish.png diff --git a/app/assets/images/emoji/truck.png b/app/assets/images/emoji/truck.png Binary files differnew file mode 100644 index 00000000000..130de047f8b --- /dev/null +++ b/app/assets/images/emoji/truck.png diff --git a/app/assets/images/emoji/trumpet.png b/app/assets/images/emoji/trumpet.png Binary files differnew file mode 100644 index 00000000000..864ccbcd04a --- /dev/null +++ b/app/assets/images/emoji/trumpet.png diff --git a/app/assets/images/emoji/tulip.png b/app/assets/images/emoji/tulip.png Binary files differnew file mode 100644 index 00000000000..f799d75c182 --- /dev/null +++ b/app/assets/images/emoji/tulip.png diff --git a/app/assets/images/emoji/tumbler_glass.png b/app/assets/images/emoji/tumbler_glass.png Binary files differnew file mode 100644 index 00000000000..7bf09229879 --- /dev/null +++ b/app/assets/images/emoji/tumbler_glass.png diff --git a/app/assets/images/emoji/turkey.png b/app/assets/images/emoji/turkey.png Binary files differnew file mode 100644 index 00000000000..344af94c9ec --- /dev/null +++ b/app/assets/images/emoji/turkey.png diff --git a/app/assets/images/emoji/turtle.png b/app/assets/images/emoji/turtle.png Binary files differnew file mode 100644 index 00000000000..c22f7519fe8 --- /dev/null +++ b/app/assets/images/emoji/turtle.png diff --git a/app/assets/images/emoji/tv.png b/app/assets/images/emoji/tv.png Binary files differnew file mode 100644 index 00000000000..999f1fb5c6d --- /dev/null +++ b/app/assets/images/emoji/tv.png diff --git a/app/assets/images/emoji/twisted_rightwards_arrows.png b/app/assets/images/emoji/twisted_rightwards_arrows.png Binary files differnew file mode 100644 index 00000000000..5904badde65 --- /dev/null +++ b/app/assets/images/emoji/twisted_rightwards_arrows.png diff --git a/app/assets/images/emoji/two.png b/app/assets/images/emoji/two.png Binary files differnew file mode 100644 index 00000000000..927339c9bff --- /dev/null +++ b/app/assets/images/emoji/two.png diff --git a/app/assets/images/emoji/two_hearts.png b/app/assets/images/emoji/two_hearts.png Binary files differnew file mode 100644 index 00000000000..4d8c3386042 --- /dev/null +++ b/app/assets/images/emoji/two_hearts.png diff --git a/app/assets/images/emoji/two_men_holding_hands.png b/app/assets/images/emoji/two_men_holding_hands.png Binary files differnew file mode 100644 index 00000000000..a511fda822a --- /dev/null +++ b/app/assets/images/emoji/two_men_holding_hands.png diff --git a/app/assets/images/emoji/two_women_holding_hands.png b/app/assets/images/emoji/two_women_holding_hands.png Binary files differnew file mode 100644 index 00000000000..b077cd3e40f --- /dev/null +++ b/app/assets/images/emoji/two_women_holding_hands.png diff --git a/app/assets/images/emoji/u5272.png b/app/assets/images/emoji/u5272.png Binary files differnew file mode 100644 index 00000000000..c4f837fe684 --- /dev/null +++ b/app/assets/images/emoji/u5272.png diff --git a/app/assets/images/emoji/u5408.png b/app/assets/images/emoji/u5408.png Binary files differnew file mode 100644 index 00000000000..8375ad9d9af --- /dev/null +++ b/app/assets/images/emoji/u5408.png diff --git a/app/assets/images/emoji/u55b6.png b/app/assets/images/emoji/u55b6.png Binary files differnew file mode 100644 index 00000000000..d21cb30eaf3 --- /dev/null +++ b/app/assets/images/emoji/u55b6.png diff --git a/app/assets/images/emoji/u6307.png b/app/assets/images/emoji/u6307.png Binary files differnew file mode 100644 index 00000000000..078e23e4ff3 --- /dev/null +++ b/app/assets/images/emoji/u6307.png diff --git a/app/assets/images/emoji/u6708.png b/app/assets/images/emoji/u6708.png Binary files differnew file mode 100644 index 00000000000..c41bd36a26a --- /dev/null +++ b/app/assets/images/emoji/u6708.png diff --git a/app/assets/images/emoji/u6709.png b/app/assets/images/emoji/u6709.png Binary files differnew file mode 100644 index 00000000000..a4510de41c0 --- /dev/null +++ b/app/assets/images/emoji/u6709.png diff --git a/app/assets/images/emoji/u6e80.png b/app/assets/images/emoji/u6e80.png Binary files differnew file mode 100644 index 00000000000..f9dea8b8833 --- /dev/null +++ b/app/assets/images/emoji/u6e80.png diff --git a/app/assets/images/emoji/u7121.png b/app/assets/images/emoji/u7121.png Binary files differnew file mode 100644 index 00000000000..d3a19b420de --- /dev/null +++ b/app/assets/images/emoji/u7121.png diff --git a/app/assets/images/emoji/u7533.png b/app/assets/images/emoji/u7533.png Binary files differnew file mode 100644 index 00000000000..6b7af0ee222 --- /dev/null +++ b/app/assets/images/emoji/u7533.png diff --git a/app/assets/images/emoji/u7981.png b/app/assets/images/emoji/u7981.png Binary files differnew file mode 100644 index 00000000000..4c704e03433 --- /dev/null +++ b/app/assets/images/emoji/u7981.png diff --git a/app/assets/images/emoji/u7a7a.png b/app/assets/images/emoji/u7a7a.png Binary files differnew file mode 100644 index 00000000000..47966c1ea93 --- /dev/null +++ b/app/assets/images/emoji/u7a7a.png diff --git a/app/assets/images/emoji/umbrella.png b/app/assets/images/emoji/umbrella.png Binary files differnew file mode 100644 index 00000000000..5b35b7ff6a4 --- /dev/null +++ b/app/assets/images/emoji/umbrella.png diff --git a/app/assets/images/emoji/umbrella2.png b/app/assets/images/emoji/umbrella2.png Binary files differnew file mode 100644 index 00000000000..97fe859e74f --- /dev/null +++ b/app/assets/images/emoji/umbrella2.png diff --git a/app/assets/images/emoji/unamused.png b/app/assets/images/emoji/unamused.png Binary files differnew file mode 100644 index 00000000000..25e3677f2eb --- /dev/null +++ b/app/assets/images/emoji/unamused.png diff --git a/app/assets/images/emoji/underage.png b/app/assets/images/emoji/underage.png Binary files differnew file mode 100644 index 00000000000..6dfe6da51e2 --- /dev/null +++ b/app/assets/images/emoji/underage.png diff --git a/app/assets/images/emoji/unicorn.png b/app/assets/images/emoji/unicorn.png Binary files differnew file mode 100644 index 00000000000..05a97969f7e --- /dev/null +++ b/app/assets/images/emoji/unicorn.png diff --git a/app/assets/images/emoji/unlock.png b/app/assets/images/emoji/unlock.png Binary files differnew file mode 100644 index 00000000000..4a74a693911 --- /dev/null +++ b/app/assets/images/emoji/unlock.png diff --git a/app/assets/images/emoji/up.png b/app/assets/images/emoji/up.png Binary files differnew file mode 100644 index 00000000000..0d42142ba04 --- /dev/null +++ b/app/assets/images/emoji/up.png diff --git a/app/assets/images/emoji/upside_down.png b/app/assets/images/emoji/upside_down.png Binary files differnew file mode 100644 index 00000000000..128f31c9828 --- /dev/null +++ b/app/assets/images/emoji/upside_down.png diff --git a/app/assets/images/emoji/urn.png b/app/assets/images/emoji/urn.png Binary files differnew file mode 100644 index 00000000000..6b5b3503438 --- /dev/null +++ b/app/assets/images/emoji/urn.png diff --git a/app/assets/images/emoji/v.png b/app/assets/images/emoji/v.png Binary files differnew file mode 100644 index 00000000000..70c5516ffee --- /dev/null +++ b/app/assets/images/emoji/v.png diff --git a/app/assets/images/emoji/v_tone1.png b/app/assets/images/emoji/v_tone1.png Binary files differnew file mode 100644 index 00000000000..6ac54a745f4 --- /dev/null +++ b/app/assets/images/emoji/v_tone1.png diff --git a/app/assets/images/emoji/v_tone2.png b/app/assets/images/emoji/v_tone2.png Binary files differnew file mode 100644 index 00000000000..6dd9669866d --- /dev/null +++ b/app/assets/images/emoji/v_tone2.png diff --git a/app/assets/images/emoji/v_tone3.png b/app/assets/images/emoji/v_tone3.png Binary files differnew file mode 100644 index 00000000000..a615e53f02f --- /dev/null +++ b/app/assets/images/emoji/v_tone3.png diff --git a/app/assets/images/emoji/v_tone4.png b/app/assets/images/emoji/v_tone4.png Binary files differnew file mode 100644 index 00000000000..33a34bd5a78 --- /dev/null +++ b/app/assets/images/emoji/v_tone4.png diff --git a/app/assets/images/emoji/v_tone5.png b/app/assets/images/emoji/v_tone5.png Binary files differnew file mode 100644 index 00000000000..45ad14b6c9c --- /dev/null +++ b/app/assets/images/emoji/v_tone5.png diff --git a/app/assets/images/emoji/vertical_traffic_light.png b/app/assets/images/emoji/vertical_traffic_light.png Binary files differnew file mode 100644 index 00000000000..8085973eecf --- /dev/null +++ b/app/assets/images/emoji/vertical_traffic_light.png diff --git a/app/assets/images/emoji/vhs.png b/app/assets/images/emoji/vhs.png Binary files differnew file mode 100644 index 00000000000..b9eb78ecd92 --- /dev/null +++ b/app/assets/images/emoji/vhs.png diff --git a/app/assets/images/emoji/vibration_mode.png b/app/assets/images/emoji/vibration_mode.png Binary files differnew file mode 100644 index 00000000000..cc46510e48e --- /dev/null +++ b/app/assets/images/emoji/vibration_mode.png diff --git a/app/assets/images/emoji/video_camera.png b/app/assets/images/emoji/video_camera.png Binary files differnew file mode 100644 index 00000000000..85b300d425c --- /dev/null +++ b/app/assets/images/emoji/video_camera.png diff --git a/app/assets/images/emoji/video_game.png b/app/assets/images/emoji/video_game.png Binary files differnew file mode 100644 index 00000000000..316a9106a55 --- /dev/null +++ b/app/assets/images/emoji/video_game.png diff --git a/app/assets/images/emoji/violin.png b/app/assets/images/emoji/violin.png Binary files differnew file mode 100644 index 00000000000..e1e76cce242 --- /dev/null +++ b/app/assets/images/emoji/violin.png diff --git a/app/assets/images/emoji/virgo.png b/app/assets/images/emoji/virgo.png Binary files differnew file mode 100644 index 00000000000..a6b56c2cb5e --- /dev/null +++ b/app/assets/images/emoji/virgo.png diff --git a/app/assets/images/emoji/volcano.png b/app/assets/images/emoji/volcano.png Binary files differnew file mode 100644 index 00000000000..931d569294c --- /dev/null +++ b/app/assets/images/emoji/volcano.png diff --git a/app/assets/images/emoji/volleyball.png b/app/assets/images/emoji/volleyball.png Binary files differnew file mode 100644 index 00000000000..7a0e49d4b07 --- /dev/null +++ b/app/assets/images/emoji/volleyball.png diff --git a/app/assets/images/emoji/vs.png b/app/assets/images/emoji/vs.png Binary files differnew file mode 100644 index 00000000000..e1180f4a464 --- /dev/null +++ b/app/assets/images/emoji/vs.png diff --git a/app/assets/images/emoji/vulcan.png b/app/assets/images/emoji/vulcan.png Binary files differnew file mode 100644 index 00000000000..54728bcaf5c --- /dev/null +++ b/app/assets/images/emoji/vulcan.png diff --git a/app/assets/images/emoji/vulcan_tone1.png b/app/assets/images/emoji/vulcan_tone1.png Binary files differnew file mode 100644 index 00000000000..8aff5d8fa16 --- /dev/null +++ b/app/assets/images/emoji/vulcan_tone1.png diff --git a/app/assets/images/emoji/vulcan_tone2.png b/app/assets/images/emoji/vulcan_tone2.png Binary files differnew file mode 100644 index 00000000000..82b7ad519b4 --- /dev/null +++ b/app/assets/images/emoji/vulcan_tone2.png diff --git a/app/assets/images/emoji/vulcan_tone3.png b/app/assets/images/emoji/vulcan_tone3.png Binary files differnew file mode 100644 index 00000000000..d1400e1dd28 --- /dev/null +++ b/app/assets/images/emoji/vulcan_tone3.png diff --git a/app/assets/images/emoji/vulcan_tone4.png b/app/assets/images/emoji/vulcan_tone4.png Binary files differnew file mode 100644 index 00000000000..47e2b280148 --- /dev/null +++ b/app/assets/images/emoji/vulcan_tone4.png diff --git a/app/assets/images/emoji/vulcan_tone5.png b/app/assets/images/emoji/vulcan_tone5.png Binary files differnew file mode 100644 index 00000000000..60b5c6077be --- /dev/null +++ b/app/assets/images/emoji/vulcan_tone5.png diff --git a/app/assets/images/emoji/walking.png b/app/assets/images/emoji/walking.png Binary files differnew file mode 100644 index 00000000000..06dc169a3fd --- /dev/null +++ b/app/assets/images/emoji/walking.png diff --git a/app/assets/images/emoji/walking_tone1.png b/app/assets/images/emoji/walking_tone1.png Binary files differnew file mode 100644 index 00000000000..4e391b45a0b --- /dev/null +++ b/app/assets/images/emoji/walking_tone1.png diff --git a/app/assets/images/emoji/walking_tone2.png b/app/assets/images/emoji/walking_tone2.png Binary files differnew file mode 100644 index 00000000000..31f94a1bce1 --- /dev/null +++ b/app/assets/images/emoji/walking_tone2.png diff --git a/app/assets/images/emoji/walking_tone3.png b/app/assets/images/emoji/walking_tone3.png Binary files differnew file mode 100644 index 00000000000..f7ed8e39c2e --- /dev/null +++ b/app/assets/images/emoji/walking_tone3.png diff --git a/app/assets/images/emoji/walking_tone4.png b/app/assets/images/emoji/walking_tone4.png Binary files differnew file mode 100644 index 00000000000..e58dc04c7b2 --- /dev/null +++ b/app/assets/images/emoji/walking_tone4.png diff --git a/app/assets/images/emoji/walking_tone5.png b/app/assets/images/emoji/walking_tone5.png Binary files differnew file mode 100644 index 00000000000..ba4e1b58fcb --- /dev/null +++ b/app/assets/images/emoji/walking_tone5.png diff --git a/app/assets/images/emoji/waning_crescent_moon.png b/app/assets/images/emoji/waning_crescent_moon.png Binary files differnew file mode 100644 index 00000000000..cf68706b871 --- /dev/null +++ b/app/assets/images/emoji/waning_crescent_moon.png diff --git a/app/assets/images/emoji/waning_gibbous_moon.png b/app/assets/images/emoji/waning_gibbous_moon.png Binary files differnew file mode 100644 index 00000000000..24e16266119 --- /dev/null +++ b/app/assets/images/emoji/waning_gibbous_moon.png diff --git a/app/assets/images/emoji/warning.png b/app/assets/images/emoji/warning.png Binary files differnew file mode 100644 index 00000000000..35691c2ed97 --- /dev/null +++ b/app/assets/images/emoji/warning.png diff --git a/app/assets/images/emoji/wastebasket.png b/app/assets/images/emoji/wastebasket.png Binary files differnew file mode 100644 index 00000000000..2b3c484b498 --- /dev/null +++ b/app/assets/images/emoji/wastebasket.png diff --git a/app/assets/images/emoji/watch.png b/app/assets/images/emoji/watch.png Binary files differnew file mode 100644 index 00000000000..64819bc6e21 --- /dev/null +++ b/app/assets/images/emoji/watch.png diff --git a/app/assets/images/emoji/water_buffalo.png b/app/assets/images/emoji/water_buffalo.png Binary files differnew file mode 100644 index 00000000000..80446615caf --- /dev/null +++ b/app/assets/images/emoji/water_buffalo.png diff --git a/app/assets/images/emoji/water_polo.png b/app/assets/images/emoji/water_polo.png Binary files differnew file mode 100644 index 00000000000..cb44576780d --- /dev/null +++ b/app/assets/images/emoji/water_polo.png diff --git a/app/assets/images/emoji/water_polo_tone1.png b/app/assets/images/emoji/water_polo_tone1.png Binary files differnew file mode 100644 index 00000000000..bed1a908d6a --- /dev/null +++ b/app/assets/images/emoji/water_polo_tone1.png diff --git a/app/assets/images/emoji/water_polo_tone2.png b/app/assets/images/emoji/water_polo_tone2.png Binary files differnew file mode 100644 index 00000000000..ec5a43b4d4a --- /dev/null +++ b/app/assets/images/emoji/water_polo_tone2.png diff --git a/app/assets/images/emoji/water_polo_tone3.png b/app/assets/images/emoji/water_polo_tone3.png Binary files differnew file mode 100644 index 00000000000..b081a4a5a96 --- /dev/null +++ b/app/assets/images/emoji/water_polo_tone3.png diff --git a/app/assets/images/emoji/water_polo_tone4.png b/app/assets/images/emoji/water_polo_tone4.png Binary files differnew file mode 100644 index 00000000000..82cfbc3b0c7 --- /dev/null +++ b/app/assets/images/emoji/water_polo_tone4.png diff --git a/app/assets/images/emoji/water_polo_tone5.png b/app/assets/images/emoji/water_polo_tone5.png Binary files differnew file mode 100644 index 00000000000..bd3366eb06c --- /dev/null +++ b/app/assets/images/emoji/water_polo_tone5.png diff --git a/app/assets/images/emoji/watermelon.png b/app/assets/images/emoji/watermelon.png Binary files differnew file mode 100644 index 00000000000..0761488b4c9 --- /dev/null +++ b/app/assets/images/emoji/watermelon.png diff --git a/app/assets/images/emoji/wave.png b/app/assets/images/emoji/wave.png Binary files differnew file mode 100644 index 00000000000..e0cd79b45f5 --- /dev/null +++ b/app/assets/images/emoji/wave.png diff --git a/app/assets/images/emoji/wave_tone1.png b/app/assets/images/emoji/wave_tone1.png Binary files differnew file mode 100644 index 00000000000..6b2b34b106e --- /dev/null +++ b/app/assets/images/emoji/wave_tone1.png diff --git a/app/assets/images/emoji/wave_tone2.png b/app/assets/images/emoji/wave_tone2.png Binary files differnew file mode 100644 index 00000000000..b857119732e --- /dev/null +++ b/app/assets/images/emoji/wave_tone2.png diff --git a/app/assets/images/emoji/wave_tone3.png b/app/assets/images/emoji/wave_tone3.png Binary files differnew file mode 100644 index 00000000000..6283b670f43 --- /dev/null +++ b/app/assets/images/emoji/wave_tone3.png diff --git a/app/assets/images/emoji/wave_tone4.png b/app/assets/images/emoji/wave_tone4.png Binary files differnew file mode 100644 index 00000000000..fe6b2baa747 --- /dev/null +++ b/app/assets/images/emoji/wave_tone4.png diff --git a/app/assets/images/emoji/wave_tone5.png b/app/assets/images/emoji/wave_tone5.png Binary files differnew file mode 100644 index 00000000000..4bd168ebb78 --- /dev/null +++ b/app/assets/images/emoji/wave_tone5.png diff --git a/app/assets/images/emoji/wavy_dash.png b/app/assets/images/emoji/wavy_dash.png Binary files differnew file mode 100644 index 00000000000..001c8d6e47d --- /dev/null +++ b/app/assets/images/emoji/wavy_dash.png diff --git a/app/assets/images/emoji/waxing_crescent_moon.png b/app/assets/images/emoji/waxing_crescent_moon.png Binary files differnew file mode 100644 index 00000000000..687125173d9 --- /dev/null +++ b/app/assets/images/emoji/waxing_crescent_moon.png diff --git a/app/assets/images/emoji/waxing_gibbous_moon.png b/app/assets/images/emoji/waxing_gibbous_moon.png Binary files differnew file mode 100644 index 00000000000..3a808156318 --- /dev/null +++ b/app/assets/images/emoji/waxing_gibbous_moon.png diff --git a/app/assets/images/emoji/wc.png b/app/assets/images/emoji/wc.png Binary files differnew file mode 100644 index 00000000000..aa433e84ba6 --- /dev/null +++ b/app/assets/images/emoji/wc.png diff --git a/app/assets/images/emoji/weary.png b/app/assets/images/emoji/weary.png Binary files differnew file mode 100644 index 00000000000..98bfbd24a16 --- /dev/null +++ b/app/assets/images/emoji/weary.png diff --git a/app/assets/images/emoji/wedding.png b/app/assets/images/emoji/wedding.png Binary files differnew file mode 100644 index 00000000000..d0d8aa0bfae --- /dev/null +++ b/app/assets/images/emoji/wedding.png diff --git a/app/assets/images/emoji/whale.png b/app/assets/images/emoji/whale.png Binary files differnew file mode 100644 index 00000000000..9f19b44257c --- /dev/null +++ b/app/assets/images/emoji/whale.png diff --git a/app/assets/images/emoji/whale2.png b/app/assets/images/emoji/whale2.png Binary files differnew file mode 100644 index 00000000000..0df9d3c73a4 --- /dev/null +++ b/app/assets/images/emoji/whale2.png diff --git a/app/assets/images/emoji/wheel_of_dharma.png b/app/assets/images/emoji/wheel_of_dharma.png Binary files differnew file mode 100644 index 00000000000..3666db0016b --- /dev/null +++ b/app/assets/images/emoji/wheel_of_dharma.png diff --git a/app/assets/images/emoji/wheelchair.png b/app/assets/images/emoji/wheelchair.png Binary files differnew file mode 100644 index 00000000000..4e5b2698eac --- /dev/null +++ b/app/assets/images/emoji/wheelchair.png diff --git a/app/assets/images/emoji/white_check_mark.png b/app/assets/images/emoji/white_check_mark.png Binary files differnew file mode 100644 index 00000000000..e55f087e544 --- /dev/null +++ b/app/assets/images/emoji/white_check_mark.png diff --git a/app/assets/images/emoji/white_circle.png b/app/assets/images/emoji/white_circle.png Binary files differnew file mode 100644 index 00000000000..c19e15684dd --- /dev/null +++ b/app/assets/images/emoji/white_circle.png diff --git a/app/assets/images/emoji/white_flower.png b/app/assets/images/emoji/white_flower.png Binary files differnew file mode 100644 index 00000000000..d6af8b60077 --- /dev/null +++ b/app/assets/images/emoji/white_flower.png diff --git a/app/assets/images/emoji/white_large_square.png b/app/assets/images/emoji/white_large_square.png Binary files differnew file mode 100644 index 00000000000..6f06c1c79de --- /dev/null +++ b/app/assets/images/emoji/white_large_square.png diff --git a/app/assets/images/emoji/white_medium_small_square.png b/app/assets/images/emoji/white_medium_small_square.png Binary files differnew file mode 100644 index 00000000000..ae874126750 --- /dev/null +++ b/app/assets/images/emoji/white_medium_small_square.png diff --git a/app/assets/images/emoji/white_medium_square.png b/app/assets/images/emoji/white_medium_square.png Binary files differnew file mode 100644 index 00000000000..8daacf57059 --- /dev/null +++ b/app/assets/images/emoji/white_medium_square.png diff --git a/app/assets/images/emoji/white_small_square.png b/app/assets/images/emoji/white_small_square.png Binary files differnew file mode 100644 index 00000000000..d7ebdb0c0ed --- /dev/null +++ b/app/assets/images/emoji/white_small_square.png diff --git a/app/assets/images/emoji/white_square_button.png b/app/assets/images/emoji/white_square_button.png Binary files differnew file mode 100644 index 00000000000..934b1cedfd2 --- /dev/null +++ b/app/assets/images/emoji/white_square_button.png diff --git a/app/assets/images/emoji/white_sun_cloud.png b/app/assets/images/emoji/white_sun_cloud.png Binary files differnew file mode 100644 index 00000000000..0a4cc100269 --- /dev/null +++ b/app/assets/images/emoji/white_sun_cloud.png diff --git a/app/assets/images/emoji/white_sun_rain_cloud.png b/app/assets/images/emoji/white_sun_rain_cloud.png Binary files differnew file mode 100644 index 00000000000..491f9ca4839 --- /dev/null +++ b/app/assets/images/emoji/white_sun_rain_cloud.png diff --git a/app/assets/images/emoji/white_sun_small_cloud.png b/app/assets/images/emoji/white_sun_small_cloud.png Binary files differnew file mode 100644 index 00000000000..cead0bfa521 --- /dev/null +++ b/app/assets/images/emoji/white_sun_small_cloud.png diff --git a/app/assets/images/emoji/wilted_rose.png b/app/assets/images/emoji/wilted_rose.png Binary files differnew file mode 100644 index 00000000000..62412b143ae --- /dev/null +++ b/app/assets/images/emoji/wilted_rose.png diff --git a/app/assets/images/emoji/wind_blowing_face.png b/app/assets/images/emoji/wind_blowing_face.png Binary files differnew file mode 100644 index 00000000000..df81b652eb6 --- /dev/null +++ b/app/assets/images/emoji/wind_blowing_face.png diff --git a/app/assets/images/emoji/wind_chime.png b/app/assets/images/emoji/wind_chime.png Binary files differnew file mode 100644 index 00000000000..3c9ef3a95f6 --- /dev/null +++ b/app/assets/images/emoji/wind_chime.png diff --git a/app/assets/images/emoji/wine_glass.png b/app/assets/images/emoji/wine_glass.png Binary files differnew file mode 100644 index 00000000000..3cc98689192 --- /dev/null +++ b/app/assets/images/emoji/wine_glass.png diff --git a/app/assets/images/emoji/wink.png b/app/assets/images/emoji/wink.png Binary files differnew file mode 100644 index 00000000000..7ea7810a37d --- /dev/null +++ b/app/assets/images/emoji/wink.png diff --git a/app/assets/images/emoji/wolf.png b/app/assets/images/emoji/wolf.png Binary files differnew file mode 100644 index 00000000000..ba7220f2de9 --- /dev/null +++ b/app/assets/images/emoji/wolf.png diff --git a/app/assets/images/emoji/woman.png b/app/assets/images/emoji/woman.png Binary files differnew file mode 100644 index 00000000000..ece440e7a61 --- /dev/null +++ b/app/assets/images/emoji/woman.png diff --git a/app/assets/images/emoji/woman_tone1.png b/app/assets/images/emoji/woman_tone1.png Binary files differnew file mode 100644 index 00000000000..ff089b8889b --- /dev/null +++ b/app/assets/images/emoji/woman_tone1.png diff --git a/app/assets/images/emoji/woman_tone2.png b/app/assets/images/emoji/woman_tone2.png Binary files differnew file mode 100644 index 00000000000..0719c378016 --- /dev/null +++ b/app/assets/images/emoji/woman_tone2.png diff --git a/app/assets/images/emoji/woman_tone3.png b/app/assets/images/emoji/woman_tone3.png Binary files differnew file mode 100644 index 00000000000..5672e2fd52d --- /dev/null +++ b/app/assets/images/emoji/woman_tone3.png diff --git a/app/assets/images/emoji/woman_tone4.png b/app/assets/images/emoji/woman_tone4.png Binary files differnew file mode 100644 index 00000000000..5754aab558b --- /dev/null +++ b/app/assets/images/emoji/woman_tone4.png diff --git a/app/assets/images/emoji/woman_tone5.png b/app/assets/images/emoji/woman_tone5.png Binary files differnew file mode 100644 index 00000000000..fc252af3a39 --- /dev/null +++ b/app/assets/images/emoji/woman_tone5.png diff --git a/app/assets/images/emoji/womans_clothes.png b/app/assets/images/emoji/womans_clothes.png Binary files differnew file mode 100644 index 00000000000..01410dc8107 --- /dev/null +++ b/app/assets/images/emoji/womans_clothes.png diff --git a/app/assets/images/emoji/womans_hat.png b/app/assets/images/emoji/womans_hat.png Binary files differnew file mode 100644 index 00000000000..b837b6a2e47 --- /dev/null +++ b/app/assets/images/emoji/womans_hat.png diff --git a/app/assets/images/emoji/womens.png b/app/assets/images/emoji/womens.png Binary files differnew file mode 100644 index 00000000000..d4ecc22e7b3 --- /dev/null +++ b/app/assets/images/emoji/womens.png diff --git a/app/assets/images/emoji/worried.png b/app/assets/images/emoji/worried.png Binary files differnew file mode 100644 index 00000000000..7074afcf5b7 --- /dev/null +++ b/app/assets/images/emoji/worried.png diff --git a/app/assets/images/emoji/wrench.png b/app/assets/images/emoji/wrench.png Binary files differnew file mode 100644 index 00000000000..c16b7439697 --- /dev/null +++ b/app/assets/images/emoji/wrench.png diff --git a/app/assets/images/emoji/wrestlers.png b/app/assets/images/emoji/wrestlers.png Binary files differnew file mode 100644 index 00000000000..71e67cfad85 --- /dev/null +++ b/app/assets/images/emoji/wrestlers.png diff --git a/app/assets/images/emoji/wrestlers_tone1.png b/app/assets/images/emoji/wrestlers_tone1.png Binary files differnew file mode 100644 index 00000000000..379070fd03b --- /dev/null +++ b/app/assets/images/emoji/wrestlers_tone1.png diff --git a/app/assets/images/emoji/wrestlers_tone2.png b/app/assets/images/emoji/wrestlers_tone2.png Binary files differnew file mode 100644 index 00000000000..6863ea9209d --- /dev/null +++ b/app/assets/images/emoji/wrestlers_tone2.png diff --git a/app/assets/images/emoji/wrestlers_tone3.png b/app/assets/images/emoji/wrestlers_tone3.png Binary files differnew file mode 100644 index 00000000000..b7e62910127 --- /dev/null +++ b/app/assets/images/emoji/wrestlers_tone3.png diff --git a/app/assets/images/emoji/wrestlers_tone4.png b/app/assets/images/emoji/wrestlers_tone4.png Binary files differnew file mode 100644 index 00000000000..750f9589233 --- /dev/null +++ b/app/assets/images/emoji/wrestlers_tone4.png diff --git a/app/assets/images/emoji/wrestlers_tone5.png b/app/assets/images/emoji/wrestlers_tone5.png Binary files differnew file mode 100644 index 00000000000..36ab9bb3f42 --- /dev/null +++ b/app/assets/images/emoji/wrestlers_tone5.png diff --git a/app/assets/images/emoji/writing_hand.png b/app/assets/images/emoji/writing_hand.png Binary files differnew file mode 100644 index 00000000000..85639f8ac40 --- /dev/null +++ b/app/assets/images/emoji/writing_hand.png diff --git a/app/assets/images/emoji/writing_hand_tone1.png b/app/assets/images/emoji/writing_hand_tone1.png Binary files differnew file mode 100644 index 00000000000..7923d8ebb17 --- /dev/null +++ b/app/assets/images/emoji/writing_hand_tone1.png diff --git a/app/assets/images/emoji/writing_hand_tone2.png b/app/assets/images/emoji/writing_hand_tone2.png Binary files differnew file mode 100644 index 00000000000..bcb304e15d2 --- /dev/null +++ b/app/assets/images/emoji/writing_hand_tone2.png diff --git a/app/assets/images/emoji/writing_hand_tone3.png b/app/assets/images/emoji/writing_hand_tone3.png Binary files differnew file mode 100644 index 00000000000..fd885fd2d90 --- /dev/null +++ b/app/assets/images/emoji/writing_hand_tone3.png diff --git a/app/assets/images/emoji/writing_hand_tone4.png b/app/assets/images/emoji/writing_hand_tone4.png Binary files differnew file mode 100644 index 00000000000..d065b8c64ab --- /dev/null +++ b/app/assets/images/emoji/writing_hand_tone4.png diff --git a/app/assets/images/emoji/writing_hand_tone5.png b/app/assets/images/emoji/writing_hand_tone5.png Binary files differnew file mode 100644 index 00000000000..a44b3dd757c --- /dev/null +++ b/app/assets/images/emoji/writing_hand_tone5.png diff --git a/app/assets/images/emoji/x.png b/app/assets/images/emoji/x.png Binary files differnew file mode 100644 index 00000000000..9f9ed0f7ad2 --- /dev/null +++ b/app/assets/images/emoji/x.png diff --git a/app/assets/images/emoji/yellow_heart.png b/app/assets/images/emoji/yellow_heart.png Binary files differnew file mode 100644 index 00000000000..7901a9d0103 --- /dev/null +++ b/app/assets/images/emoji/yellow_heart.png diff --git a/app/assets/images/emoji/yen.png b/app/assets/images/emoji/yen.png Binary files differnew file mode 100644 index 00000000000..63ee4799d66 --- /dev/null +++ b/app/assets/images/emoji/yen.png diff --git a/app/assets/images/emoji/yin_yang.png b/app/assets/images/emoji/yin_yang.png Binary files differnew file mode 100644 index 00000000000..f2900f6338f --- /dev/null +++ b/app/assets/images/emoji/yin_yang.png diff --git a/app/assets/images/emoji/yum.png b/app/assets/images/emoji/yum.png Binary files differnew file mode 100644 index 00000000000..2df15753ca1 --- /dev/null +++ b/app/assets/images/emoji/yum.png diff --git a/app/assets/images/emoji/zap.png b/app/assets/images/emoji/zap.png Binary files differnew file mode 100644 index 00000000000..47e68e48e49 --- /dev/null +++ b/app/assets/images/emoji/zap.png diff --git a/app/assets/images/emoji/zero.png b/app/assets/images/emoji/zero.png Binary files differnew file mode 100644 index 00000000000..13aca83e018 --- /dev/null +++ b/app/assets/images/emoji/zero.png diff --git a/app/assets/images/emoji/zipper_mouth.png b/app/assets/images/emoji/zipper_mouth.png Binary files differnew file mode 100644 index 00000000000..f8ced2502a7 --- /dev/null +++ b/app/assets/images/emoji/zipper_mouth.png diff --git a/app/assets/images/emoji/zzz.png b/app/assets/images/emoji/zzz.png Binary files differnew file mode 100644 index 00000000000..9bc72b4469f --- /dev/null +++ b/app/assets/images/emoji/zzz.png diff --git a/app/assets/images/emoji@2x.png b/app/assets/images/emoji@2x.png Binary files differindex dc9cae1d44c..b0fa9e1139e 100644 --- a/app/assets/images/emoji@2x.png +++ b/app/assets/images/emoji@2x.png diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index a4ccb30e447..4667980a960 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,380 +1,518 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, brace-style, no-underscore-dangle, no-return-assign, camelcase */ /* global Cookies */ -var emojiAliases = require('emoji-aliases'); - -(function() { - this.AwardsHandler = (function() { - var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence - function AwardsHandler() { - this.aliases = emojiAliases; - $(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) { - return function(e) { - e.stopPropagation(); - e.preventDefault(); - return _this.showEmojiMenu($(e.currentTarget)); - }; - })(this)); - $('html').on('click', function(e) { - var $target; - $target = $(e.target); - if (!$target.closest('.emoji-menu-content').length) { - $('.js-awards-block.current').removeClass('current'); - } - if (!$target.closest('.emoji-menu').length) { - if ($('.emoji-menu').is(':visible')) { - $('.js-add-award.is-active').removeClass('is-active'); - return $('.emoji-menu').removeClass('is-visible'); - } - } - }); - $(document).off('click', '.js-emoji-btn').on('click', '.js-emoji-btn', (function(_this) { - return function(e) { - var $target, emoji; - e.preventDefault(); - $target = $(e.currentTarget); - emoji = $target.find('.icon').data('emoji'); - $target.closest('.js-awards-block').addClass('current'); - return _this.addAward(_this.getVotesBlock(), _this.getAwardUrl(), emoji); - }; - })(this)); +const emojiMap = require('emoji-map'); +const emojiAliases = require('emoji-aliases'); +const glEmoji = require('./behaviors/gl_emoji'); + +const glEmojiTag = glEmoji.glEmojiTag; + +const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; +const requestAnimationFrame = window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.setTimeout; + +const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence + +let categoryMap = null; + +const categoryLabelMap = { + activity: 'Activity', + people: 'People', + nature: 'Nature', + food: 'Food', + travel: 'Travel', + objects: 'Objects', + symbols: 'Symbols', + flags: 'Flags', +}; + +function buildCategoryMap() { + return Object.keys(emojiMap).reduce((currentCategoryMap, emojiNameKey) => { + const emojiInfo = emojiMap[emojiNameKey]; + if (currentCategoryMap[emojiInfo.category]) { + currentCategoryMap[emojiInfo.category].push(emojiNameKey); } - AwardsHandler.prototype.showEmojiMenu = function($addBtn) { - var $holder, $menu, url; - $menu = $('.emoji-menu'); - if ($addBtn.hasClass('js-note-emoji')) { - $addBtn.closest('.note').find('.js-awards-block').addClass('current'); - } else { - $addBtn.closest('.js-awards-block').addClass('current'); - } - if ($menu.length) { - $holder = $addBtn.closest('.js-award-holder'); - if ($menu.is('.is-visible')) { - $addBtn.removeClass('is-active'); - $menu.removeClass('is-visible'); - return $('#emoji_search').blur(); - } else { - $addBtn.addClass('is-active'); - this.positionMenu($menu, $addBtn); - $menu.addClass('is-visible'); - return $('#emoji_search').focus(); - } - } else { - $addBtn.addClass('is-loading is-active'); - url = this.getAwardMenuUrl(); - return this.createEmojiMenu(url, (function(_this) { - return function() { - $addBtn.removeClass('is-loading'); - $menu = $('.emoji-menu'); - _this.positionMenu($menu, $addBtn); - if (!_this.frequentEmojiBlockRendered) { - _this.renderFrequentlyUsedBlock(); - } - return setTimeout(function() { - $menu.addClass('is-visible'); - $('#emoji_search').focus(); - return _this.setupSearch(); - }, 200); - }; - })(this)); - } - }; - - AwardsHandler.prototype.createEmojiMenu = function(awardMenuUrl, callback) { - return $.get(awardMenuUrl, function(response) { - $('body').append(response); - return callback(); + return currentCategoryMap; + }, { + activity: [], + people: [], + nature: [], + food: [], + travel: [], + objects: [], + symbols: [], + flags: [], + }); +} + +function renderCategory(name, emojiList) { + return ` + <h5 class="emoji-menu-title"> + ${name} + </h5> + <ul class="clearfix emoji-menu-list"> + ${emojiList.map(emojiName => ` + <li class="emoji-menu-list-item"> + <button class="emoji-menu-btn text-center js-emoji-btn" type="button"> + ${glEmojiTag(emojiName, { + sprite: true, + })} + </button> + </li> + `).join('\n')} + </ul> + `; +} + +function AwardsHandler() { + this.eventListeners = []; + this.aliases = emojiAliases; + // If the user shows intent let's pre-build the menu + this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => { + const $menu = $('.emoji-menu'); + if ($menu.length === 0) { + requestAnimationFrame(() => { + this.createEmojiMenu(); }); - }; - - AwardsHandler.prototype.positionMenu = function($menu, $addBtn) { - var css, position; - position = $addBtn.data('position'); - // The menu could potentially be off-screen or in a hidden overflow element - // So we position the element absolute in the body - css = { - top: ($addBtn.offset().top + $addBtn.outerHeight()) + "px" - }; - if (position === 'right') { - css.left = (($addBtn.offset().left - $menu.outerWidth()) + 20) + "px"; - $menu.addClass('is-aligned-right'); - } else { - css.left = ($addBtn.offset().left) + "px"; - $menu.removeClass('is-aligned-right'); - } - return $menu.css(css); - }; - - AwardsHandler.prototype.addAward = function(votesBlock, awardUrl, emoji, checkMutuality, callback) { - if (checkMutuality == null) { - checkMutuality = true; - } - emoji = this.normilizeEmojiName(emoji); - this.postEmoji(awardUrl, emoji, (function(_this) { - return function() { - _this.addAwardToEmojiBar(votesBlock, emoji, checkMutuality); - return typeof callback === "function" ? callback() : void 0; - }; - })(this)); - return $('.emoji-menu').removeClass('is-visible'); - }; - - AwardsHandler.prototype.addAwardToEmojiBar = function(votesBlock, emoji, checkForMutuality) { - var $emojiButton, counter; - if (checkForMutuality == null) { - checkForMutuality = true; - } - if (checkForMutuality) { - this.checkMutuality(votesBlock, emoji); - } - this.addEmojiToFrequentlyUsedList(emoji); - emoji = this.normilizeEmojiName(emoji); - $emojiButton = this.findEmojiIcon(votesBlock, emoji).parent(); - if ($emojiButton.length > 0) { - if (this.isActive($emojiButton)) { - return this.decrementCounter($emojiButton, emoji); - } else { - counter = $emojiButton.find('.js-counter'); - counter.text(parseInt(counter.text(), 10) + 1); - $emojiButton.addClass('active'); - this.addYouToUserList(votesBlock, emoji); - return this.animateEmoji($emojiButton); - } - } else { - votesBlock.removeClass('hidden'); - return this.createEmoji(votesBlock, emoji); - } - }; - - AwardsHandler.prototype.getVotesBlock = function() { - var currentBlock; - currentBlock = $('.js-awards-block.current'); - if (currentBlock.length) { - return currentBlock; - } else { - return $('.js-awards-block').eq(0); - } - }; - - AwardsHandler.prototype.getAwardUrl = function() { - return this.getVotesBlock().data('award-url'); - }; - - AwardsHandler.prototype.checkMutuality = function(votesBlock, emoji) { - var $emojiButton, awardUrl, isAlreadyVoted, mutualVote; - awardUrl = this.getAwardUrl(); - if (emoji === 'thumbsup' || emoji === 'thumbsdown') { - mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup'; - $emojiButton = votesBlock.find("[data-emoji=" + mutualVote + "]").parent(); - isAlreadyVoted = $emojiButton.hasClass('active'); - if (isAlreadyVoted) { - this.addAward(votesBlock, awardUrl, mutualVote, false); - } - } - }; - - AwardsHandler.prototype.isActive = function($emojiButton) { - return $emojiButton.hasClass('active'); - }; - - AwardsHandler.prototype.decrementCounter = function($emojiButton, emoji) { - var counter, counterNumber; - counter = $('.js-counter', $emojiButton); - counterNumber = parseInt(counter.text(), 10); - if (counterNumber > 1) { - counter.text(counterNumber - 1); - this.removeYouFromUserList($emojiButton, emoji); - } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') { - $emojiButton.tooltip('destroy'); - counter.text('0'); - this.removeYouFromUserList($emojiButton, emoji); - if ($emojiButton.parents('.note').length) { - this.removeEmoji($emojiButton); - } - } else { - this.removeEmoji($emojiButton); - } - return $emojiButton.removeClass('active'); - }; - - AwardsHandler.prototype.removeEmoji = function($emojiButton) { - var $votesBlock; - $emojiButton.tooltip('destroy'); - $emojiButton.remove(); - $votesBlock = this.getVotesBlock(); - if ($votesBlock.find('.js-emoji-btn').length === 0) { - return $votesBlock.addClass('hidden'); - } - }; - - AwardsHandler.prototype.getAwardTooltip = function($awardBlock) { - return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || ''; - }; - - AwardsHandler.prototype.toSentence = function(list) { - if (list.length <= 2) { - return list.join(' and '); + } + // Prebuild the categoryMap + categoryMap = categoryMap || buildCategoryMap(); + }); + this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => { + e.stopPropagation(); + e.preventDefault(); + this.showEmojiMenu($(e.currentTarget)); + }); + + this.registerEventListener('on', $('html'), 'click', (e) => { + const $target = $(e.target); + if (!$target.closest('.emoji-menu-content').length) { + $('.js-awards-block.current').removeClass('current'); + } + if (!$target.closest('.emoji-menu').length) { + if ($('.emoji-menu').is(':visible')) { + $('.js-add-award.is-active').removeClass('is-active'); + $('.emoji-menu').removeClass('is-visible'); } - else { - return list.slice(0, -1).join(', ') + ', and ' + list[list.length - 1]; + } + }); + this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', (e) => { + e.preventDefault(); + const $target = $(e.currentTarget); + const $glEmojiElement = $target.find('gl-emoji'); + const $spriteIconElement = $target.find('.icon'); + const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name'); + $target.closest('.js-awards-block').addClass('current'); + return this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji); + }); +} + +AwardsHandler.prototype.registerEventListener = function registerEventListener(method = 'on', element, ...args) { + element[method].call(element, ...args); + this.eventListeners.push({ + element, + args, + }); +}; + +AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) { + if ($addBtn.hasClass('js-note-emoji')) { + $addBtn.closest('.note').find('.js-awards-block').addClass('current'); + } else { + $addBtn.closest('.js-awards-block').addClass('current'); + } + + const $menu = $('.emoji-menu'); + if ($menu.length) { + if ($menu.is('.is-visible')) { + $addBtn.removeClass('is-active'); + $menu.removeClass('is-visible'); + $('#emoji_search').blur(); + } else { + $addBtn.addClass('is-active'); + this.positionMenu($menu, $addBtn); + $menu.addClass('is-visible'); + $('#emoji_search').focus(); + } + } else { + $addBtn.addClass('is-loading is-active'); + this.createEmojiMenu(() => { + const $createdMenu = $('.emoji-menu'); + $addBtn.removeClass('is-loading'); + this.positionMenu($createdMenu, $addBtn); + if (!this.frequentEmojiBlockRendered) { + this.renderFrequentlyUsedBlock(); } - }; - - AwardsHandler.prototype.removeYouFromUserList = function($emojiButton, emoji) { - var authors, awardBlock, newAuthors, originalTitle; - awardBlock = $emojiButton; - originalTitle = this.getAwardTooltip(awardBlock); - authors = originalTitle.split(FROM_SENTENCE_REGEX); - authors.splice(authors.indexOf('You'), 1); - return awardBlock - .closest('.js-emoji-btn') - .removeData('title') - .removeAttr('data-title') - .removeAttr('data-original-title') - .attr('title', this.toSentence(authors)) - .tooltip('fixTitle'); - }; - - AwardsHandler.prototype.addYouToUserList = function(votesBlock, emoji) { - var awardBlock, origTitle, users; - awardBlock = this.findEmojiIcon(votesBlock, emoji).parent(); - origTitle = this.getAwardTooltip(awardBlock); - users = []; - if (origTitle) { - users = origTitle.trim().split(FROM_SENTENCE_REGEX); + return setTimeout(() => { + $createdMenu.addClass('is-visible'); + $('#emoji_search').focus(); + }, 200); + }); + } +}; + +// Create the emoji menu with the first category of emojis. +// Then render the remaining categories of emojis one by one to avoid jank. +AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) { + if (this.isCreatingEmojiMenu) { + return; + } + this.isCreatingEmojiMenu = true; + + // Render the first category + categoryMap = categoryMap || buildCategoryMap(); + const categoryNameKey = Object.keys(categoryMap)[0]; + const emojisInCategory = categoryMap[categoryNameKey]; + const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); + + const emojiMenuMarkup = ` + <div class="emoji-menu"> + <input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" placeholder="Search emoji" /> + + <div class="emoji-menu-content"> + ${firstCategory} + </div> + </div> + `; + + document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup); + + this.addRemainingEmojiMenuCategories(); + this.setupSearch(); + if (callback) { + callback(); + } +}; + +AwardsHandler + .prototype + .addRemainingEmojiMenuCategories = function addRemainingEmojiMenuCategories() { + if (this.isAddingRemainingEmojiMenuCategories) { + return; + } + this.isAddingRemainingEmojiMenuCategories = true; + + categoryMap = categoryMap || buildCategoryMap(); + + // Avoid the jank and render the remaining categories separately + // This will take more time, but makes UI more responsive + const menu = document.querySelector('.emoji-menu'); + const emojiContentElement = menu.querySelector('.emoji-menu-content'); + const remainingCategories = Object.keys(categoryMap).slice(1); + const allCategoriesAddedPromise = remainingCategories.reduce( + (promiseChain, categoryNameKey) => + promiseChain.then(() => + new Promise((resolve) => { + const emojisInCategory = categoryMap[categoryNameKey]; + const categoryMarkup = renderCategory( + categoryLabelMap[categoryNameKey], + emojisInCategory, + ); + requestAnimationFrame(() => { + emojiContentElement.insertAdjacentHTML('beforeend', categoryMarkup); + resolve(); + }); + }), + ), + Promise.resolve(), + ); + + allCategoriesAddedPromise.then(() => { + // Used for tests + // We check for the menu in case it was destroyed in the meantime + if (menu) { + menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish')); } - users.unshift('You'); - return awardBlock - .attr('title', this.toSentence(users)) - .tooltip('fixTitle'); - }; - - AwardsHandler.prototype.createEmoji_ = function(votesBlock, emoji) { - var $emojiButton, buttonHtml, emojiCssClass; - emojiCssClass = this.resolveNameToCssClass(emoji); - buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='You' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>"; - $emojiButton = $(buttonHtml); - $emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('emoji', emoji); + }); + }; + +AwardsHandler.prototype.positionMenu = function positionMenu($menu, $addBtn) { + const position = $addBtn.data('position'); + // The menu could potentially be off-screen or in a hidden overflow element + // So we position the element absolute in the body + const css = { + top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`, + }; + if (position === 'right') { + css.left = `${($addBtn.offset().left - $menu.outerWidth()) + 20}px`; + $menu.addClass('is-aligned-right'); + } else { + css.left = `${$addBtn.offset().left}px`; + $menu.removeClass('is-aligned-right'); + } + return $menu.css(css); +}; + +AwardsHandler.prototype.addAward = function addAward( + votesBlock, + awardUrl, + emoji, + checkMutuality, + callback, +) { + const normalizedEmoji = this.normalizeEmojiName(emoji); + this.postEmoji(awardUrl, normalizedEmoji, () => { + this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); + return typeof callback === 'function' ? callback() : undefined; + }); + return $('.emoji-menu').removeClass('is-visible'); +}; + +AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar( + votesBlock, + emoji, + checkForMutuality, +) { + if (checkForMutuality || checkForMutuality === null) { + this.checkMutuality(votesBlock, emoji); + } + this.addEmojiToFrequentlyUsedList(emoji); + const normalizedEmoji = this.normalizeEmojiName(emoji); + const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); + if ($emojiButton.length > 0) { + if (this.isActive($emojiButton)) { + this.decrementCounter($emojiButton, normalizedEmoji); + } else { + const counter = $emojiButton.find('.js-counter'); + counter.text(parseInt(counter.text(), 10) + 1); + $emojiButton.addClass('active'); + this.addYouToUserList(votesBlock, normalizedEmoji); this.animateEmoji($emojiButton); - $('.award-control').tooltip(); - return votesBlock.removeClass('current'); - }; - - AwardsHandler.prototype.animateEmoji = function($emoji) { - var className = 'pulse animated once short'; - $emoji.addClass(className); + } + } else { + votesBlock.removeClass('hidden'); + this.createEmoji(votesBlock, normalizedEmoji); + } +}; + +AwardsHandler.prototype.getVotesBlock = function getVotesBlock() { + const currentBlock = $('.js-awards-block.current'); + let resultantVotesBlock = currentBlock; + if (currentBlock.length === 0) { + resultantVotesBlock = $('.js-awards-block').eq(0); + } + + return resultantVotesBlock; +}; + +AwardsHandler.prototype.getAwardUrl = function getAwardUrl() { + return this.getVotesBlock().data('award-url'); +}; + +AwardsHandler.prototype.checkMutuality = function checkMutuality(votesBlock, emoji) { + const awardUrl = this.getAwardUrl(); + if (emoji === 'thumbsup' || emoji === 'thumbsdown') { + const mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup'; + const $emojiButton = votesBlock.find(`[data-name="${mutualVote}"]`).parent(); + const isAlreadyVoted = $emojiButton.hasClass('active'); + if (isAlreadyVoted) { + this.addAward(votesBlock, awardUrl, mutualVote, false); + } + } +}; + +AwardsHandler.prototype.isActive = function isActive($emojiButton) { + return $emojiButton.hasClass('active'); +}; + +AwardsHandler.prototype.decrementCounter = function decrementCounter($emojiButton, emoji) { + const counter = $('.js-counter', $emojiButton); + const counterNumber = parseInt(counter.text(), 10); + if (counterNumber > 1) { + counter.text(counterNumber - 1); + this.removeYouFromUserList($emojiButton); + } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') { + $emojiButton.tooltip('destroy'); + counter.text('0'); + this.removeYouFromUserList($emojiButton); + if ($emojiButton.parents('.note').length) { + this.removeEmoji($emojiButton); + } + } else { + this.removeEmoji($emojiButton); + } + return $emojiButton.removeClass('active'); +}; + +AwardsHandler.prototype.removeEmoji = function removeEmoji($emojiButton) { + $emojiButton.tooltip('destroy'); + $emojiButton.remove(); + const $votesBlock = this.getVotesBlock(); + if ($votesBlock.find('.js-emoji-btn').length === 0) { + $votesBlock.addClass('hidden'); + } +}; + +AwardsHandler.prototype.getAwardTooltip = function getAwardTooltip($awardBlock) { + return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || ''; +}; + +AwardsHandler.prototype.toSentence = function toSentence(list) { + let sentence; + if (list.length <= 2) { + sentence = list.join(' and '); + } else { + sentence = `${list.slice(0, -1).join(', ')}, and ${list[list.length - 1]}`; + } + + return sentence; +}; + +AwardsHandler.prototype.removeYouFromUserList = function removeYouFromUserList($emojiButton) { + const awardBlock = $emojiButton; + const originalTitle = this.getAwardTooltip(awardBlock); + const authors = originalTitle.split(FROM_SENTENCE_REGEX); + authors.splice(authors.indexOf('You'), 1); + return awardBlock + .closest('.js-emoji-btn') + .removeData('title') + .removeAttr('data-title') + .removeAttr('data-original-title') + .attr('title', this.toSentence(authors)) + .tooltip('fixTitle'); +}; + +AwardsHandler.prototype.addYouToUserList = function addYouToUserList(votesBlock, emoji) { + const awardBlock = this.findEmojiIcon(votesBlock, emoji).parent(); + const origTitle = this.getAwardTooltip(awardBlock); + let users = []; + if (origTitle) { + users = origTitle.trim().split(FROM_SENTENCE_REGEX); + } + users.unshift('You'); + return awardBlock + .attr('title', this.toSentence(users)) + .tooltip('fixTitle'); +}; + +AwardsHandler + .prototype + .createAwardButtonForVotesBlock = function createAwardButtonForVotesBlock(votesBlock, emojiName) { + const buttonHtml = ` + <button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom"> + ${glEmojiTag(emojiName)} + <span class="award-control-text js-counter">1</span> + </button> + `; + const $emojiButton = $(buttonHtml); + $emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('name', emojiName); + this.animateEmoji($emojiButton); + $('.award-control').tooltip(); + votesBlock.removeClass('current'); + }; + +AwardsHandler.prototype.animateEmoji = function animateEmoji($emoji) { + const className = 'pulse animated once short'; + $emoji.addClass(className); + + this.registerEventListener('on', $emoji, animationEndEventString, (e) => { + $(e.currentTarget).removeClass(className); + }); +}; + +AwardsHandler.prototype.createEmoji = function createEmoji(votesBlock, emoji) { + if ($('.emoji-menu').length) { + this.createAwardButtonForVotesBlock(votesBlock, emoji); + } + this.createEmojiMenu(() => { + this.createAwardButtonForVotesBlock(votesBlock, emoji); + }); +}; + +AwardsHandler.prototype.postEmoji = function postEmoji(awardUrl, emoji, callback) { + return $.post(awardUrl, { + name: emoji, + }, (data) => { + if (data.ok) { + callback(); + } + }); +}; + +AwardsHandler.prototype.findEmojiIcon = function findEmojiIcon(votesBlock, emoji) { + return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`); +}; + +AwardsHandler.prototype.scrollToAwards = function scrollToAwards() { + const options = { + scrollTop: $('.awards').offset().top - 110, + }; + return $('body, html').animate(options, 200); +}; + +AwardsHandler.prototype.normalizeEmojiName = function normalizeEmojiName(emoji) { + return Object.prototype.hasOwnProperty.call(this.aliases, emoji) ? this.aliases[emoji] : emoji; +}; + +AwardsHandler + .prototype + .addEmojiToFrequentlyUsedList = function addEmojiToFrequentlyUsedList(emoji) { + const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); + frequentlyUsedEmojis.push(emoji); + Cookies.set('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }); + }; + +AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmojis() { + const frequentlyUsedEmojis = (Cookies.get('frequently_used_emojis') || '').split(','); + return _.compact(_.uniq(frequentlyUsedEmojis)); +}; + +AwardsHandler.prototype.renderFrequentlyUsedBlock = function renderFrequentlyUsedBlock() { + if (Cookies.get('frequently_used_emojis')) { + const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); + const ul = $('<ul class="clearfix emoji-menu-list frequent-emojis">'); + for (let i = 0, len = frequentlyUsedEmojis.length; i < len; i += 1) { + const emoji = frequentlyUsedEmojis[i]; + $(`.emoji-menu-content [data-name="${emoji}"]`).closest('li').clone().appendTo(ul); + } + $('.emoji-menu-content').prepend(ul).prepend($('<h5>').text('Frequently used')); + } + this.frequentEmojiBlockRendered = true; +}; + +AwardsHandler.prototype.setupSearch = function setupSearch() { + this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => { + const term = $(e.target).val().trim(); + // Clean previous search results + $('ul.emoji-menu-search, h5.emoji-search').remove(); + if (term.length > 0) { + // Generate a search result block + const h5 = $('<h5 class="emoji-search" />').text('Search results'); + const foundEmojis = this.searchEmojis(term).show(); + const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis); + $('.emoji-menu-content ul, .emoji-menu-content h5').hide(); + $('.emoji-menu-content').append(h5).append(ul); + } else { + $('.emoji-menu-content').children().show(); + } + }); +}; - $emoji.on('webkitAnimationEnd animationEnd', function() { - $(this).removeClass(className); - }); - }; +AwardsHandler.prototype.searchEmojis = function searchEmojis(term) { + const safeTerm = term.toLowerCase(); - AwardsHandler.prototype.createEmoji = function(votesBlock, emoji) { - if ($('.emoji-menu').length) { - return this.createEmoji_(votesBlock, emoji); - } - return this.createEmojiMenu(this.getAwardMenuUrl(), (function(_this) { - return function() { - return _this.createEmoji_(votesBlock, emoji); - }; - })(this)); - }; - - AwardsHandler.prototype.getAwardMenuUrl = function() { - return gon.award_menu_url; - }; - - AwardsHandler.prototype.resolveNameToCssClass = function(emoji) { - var emojiIcon, unicodeName; - emojiIcon = $(".emoji-menu-content [data-emoji='" + emoji + "']"); - if (emojiIcon.length > 0) { - unicodeName = emojiIcon.data('unicode-name'); - } else { - // Find by alias - unicodeName = $(".emoji-menu-content [data-aliases*=':" + emoji + ":']").data('unicode-name'); - } - return "emoji-" + unicodeName; - }; - - AwardsHandler.prototype.postEmoji = function(awardUrl, emoji, callback) { - return $.post(awardUrl, { - name: emoji - }, function(data) { - if (data.ok) { - return callback(); - } - }); - }; - - AwardsHandler.prototype.findEmojiIcon = function(votesBlock, emoji) { - return votesBlock.find(".js-emoji-btn [data-emoji='" + emoji + "']"); - }; - - AwardsHandler.prototype.scrollToAwards = function() { - var options; - options = { - scrollTop: $('.awards').offset().top - 110 - }; - return $('body, html').animate(options, 200); - }; - - AwardsHandler.prototype.normilizeEmojiName = function(emoji) { - return this.aliases[emoji] || emoji; - }; - - AwardsHandler.prototype.addEmojiToFrequentlyUsedList = function(emoji) { - var frequentlyUsedEmojis; - frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); - frequentlyUsedEmojis.push(emoji); - Cookies.set('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }); - }; - - AwardsHandler.prototype.getFrequentlyUsedEmojis = function() { - var frequentlyUsedEmojis; - frequentlyUsedEmojis = (Cookies.get('frequently_used_emojis') || '').split(','); - return _.compact(_.uniq(frequentlyUsedEmojis)); - }; - - AwardsHandler.prototype.renderFrequentlyUsedBlock = function() { - var emoji, frequentlyUsedEmojis, i, len, ul; - if (Cookies.get('frequently_used_emojis')) { - frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); - ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>"); - for (i = 0, len = frequentlyUsedEmojis.length; i < len; i += 1) { - emoji = frequentlyUsedEmojis[i]; - $(".emoji-menu-content [data-emoji='" + emoji + "']").closest('li').clone().appendTo(ul); - } - $('.emoji-menu-content').prepend(ul).prepend($('<h5>').text('Frequently used')); - } - return this.frequentEmojiBlockRendered = true; - }; - - AwardsHandler.prototype.setupSearch = function() { - return $('input.emoji-search').on('keyup', (function(_this) { - return function(ev) { - var found_emojis, h5, term, ul; - term = $(ev.target).val(); - // Clean previous search results - $('ul.emoji-menu-search, h5.emoji-search').remove(); - if (term) { - // Generate a search result block - h5 = $('<h5 class="emoji-search" />').text('Search results'); - found_emojis = _this.searchEmojis(term).show(); - ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis); - $('.emoji-menu-content ul, .emoji-menu-content h5').hide(); - return $('.emoji-menu-content').append(h5).append(ul); - } else { - return $('.emoji-menu-content').children().show(); - } - }; - })(this)); - }; - - AwardsHandler.prototype.searchEmojis = function(term) { - return $(".emoji-menu-list:not(.frequent-emojis) [data-emoji*='" + term + "']").closest('li').clone(); - }; - - return AwardsHandler; - })(); -}).call(window); + const namesMatchingAlias = []; + Object.keys(emojiAliases).forEach((alias) => { + if (alias.indexOf(safeTerm) >= 0) { + namesMatchingAlias.push(emojiAliases[alias]); + } + }); + const $matchingElements = namesMatchingAlias.concat(safeTerm) + .reduce( + ($result, searchTerm) => + $result.add($(`.emoji-menu-list:not(.frequent-emojis) [data-name*="${searchTerm}"]`)), + $([]), + ); + return $matchingElements.closest('li').clone(); +}; + +AwardsHandler.prototype.destroy = function destroy() { + this.eventListeners.forEach((entry) => { + entry.element.off.call(entry.element, ...entry.args); + }); + $('.emoji-menu').remove(); +}; + +module.exports = AwardsHandler; diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js new file mode 100644 index 00000000000..d1d98c3919f --- /dev/null +++ b/app/assets/javascripts/behaviors/gl_emoji.js @@ -0,0 +1,217 @@ +const installCustomElements = require('document-register-element'); +const emojiMap = require('emoji-map'); +const emojiAliases = require('emoji-aliases'); +const generatedUnicodeSupportMap = require('./gl_emoji/unicode_support_map'); +const spreadString = require('./gl_emoji/spread_string'); + +installCustomElements(window); + +function emojiImageTag(name, src) { + return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`; +} + +function assembleFallbackImageSrc(inputName) { + const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ? + emojiAliases[inputName] : inputName; + const emojiInfo = emojiMap[name]; + const fallbackImageSrc = `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${emojiInfo.digest}.png`; + + return fallbackImageSrc; +} +const glEmojiTagDefaults = { + sprite: false, + forceFallback: false, +}; +function glEmojiTag(inputName, options) { + const opts = Object.assign({}, glEmojiTagDefaults, options); + const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ? + emojiAliases[inputName] : inputName; + const emojiInfo = emojiMap[name]; + const fallbackImageSrc = assembleFallbackImageSrc(name); + const fallbackSpriteClass = `emoji-${name}`; + + const classList = []; + if (opts.forceFallback && opts.sprite) { + classList.push('emoji-icon'); + classList.push(fallbackSpriteClass); + } + const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : ''; + const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : ''; + let contents = emojiInfo.moji; + if (opts.forceFallback && !opts.sprite) { + contents = emojiImageTag(name, fallbackImageSrc); + } + + return ` + <gl-emoji + ${classAttribute} + data-name="${name}" + data-fallback-src="${fallbackImageSrc}" + ${fallbackSpriteAttribute} + data-unicode-version="${emojiInfo.unicodeVersion}" + > + ${contents} + </gl-emoji> + `; +} + +// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/ +const flagACodePoint = 127462; // parseInt('1F1E6', 16) +const flagZCodePoint = 127487; // parseInt('1F1FF', 16) +function isFlagEmoji(emojiUnicode) { + const cp = emojiUnicode.codePointAt(0); + // Length 4 because flags are made of 2 characters which are surrogate pairs + return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint; +} + +// Chrome <57 renders keycaps oddly +// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294 +// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png +function isKeycapEmoji(emojiUnicode) { + return emojiUnicode.length === 3 && emojiUnicode[2] === '\u20E3'; +} + +// Check for a skin tone variation emoji which aren't always supported +const tone1 = 127995;// parseInt('1F3FB', 16) +const tone5 = 127999;// parseInt('1F3FF', 16) +function isSkinToneComboEmoji(emojiUnicode) { + return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => { + const cp = char.codePointAt(0); + return cp >= tone1 && cp <= tone5; + }); +} + +// macOS supports most skin tone emoji's but +// doesn't support the skin tone versions of horse racing +const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16) +function isHorceRacingSkinToneComboEmoji(emojiUnicode) { + return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint && + isSkinToneComboEmoji(emojiUnicode); +} + +// Check for `family_*`, `kiss_*`, `couple_*` +// For ex. Windows 8.1 Firefox 51.0.1, doesn't support these +const zwj = 8205; // parseInt('200D', 16) +const personStartCodePoint = 128102; // parseInt('1F466', 16) +const personEndCodePoint = 128105; // parseInt('1F469', 16) +function isPersonZwjEmoji(emojiUnicode) { + let hasPersonEmoji = false; + let hasZwj = false; + spreadString(emojiUnicode).forEach((character) => { + const cp = character.codePointAt(0); + if (cp === zwj) { + hasZwj = true; + } else if (cp >= personStartCodePoint && cp <= personEndCodePoint) { + hasPersonEmoji = true; + } + }); + + return hasPersonEmoji && hasZwj; +} + +// Helper so we don't have to run `isFlagEmoji` twice +// in `isEmojiUnicodeSupported` logic +function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) { + const isFlagResult = isFlagEmoji(emojiUnicode); + return ( + (unicodeSupportMap.flag && isFlagResult) || + !isFlagResult + ); +} + +// Helper so we don't have to run `isSkinToneComboEmoji` twice +// in `isEmojiUnicodeSupported` logic +function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) { + const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode); + return ( + (unicodeSupportMap.skinToneModifier && isSkinToneResult) || + !isSkinToneResult + ); +} + +// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice +// in `isEmojiUnicodeSupported` logic +function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) { + const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode); + return ( + (unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) || + !isHorseRacingSkinToneResult + ); +} + +// Helper so we don't have to run `isPersonZwjEmoji` twice +// in `isEmojiUnicodeSupported` logic +function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) { + const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode); + return ( + (unicodeSupportMap.personZwj && isPersonZwjResult) || + !isPersonZwjResult + ); +} + +// Takes in a support map and determines whether +// the given unicode emoji is supported on the platform. +// +// Combines all the edge case tests into a one-stop shop method +function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) { + const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome && + unicodeSupportMap.meta.chromeVersion < 57; + + // For comments about each scenario, see the comments above each individual respective function + return unicodeSupportMap[unicodeVersion] && + !(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) && + checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) && + checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) && + checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) && + checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode); +} + +const GlEmojiElementProto = Object.create(HTMLElement.prototype); +GlEmojiElementProto.createdCallback = function createdCallback() { + const emojiUnicode = this.textContent.trim(); + const { + name, + unicodeVersion, + fallbackSrc, + fallbackSpriteClass, + } = this.dataset; + + const isEmojiUnicode = this.childNodes && Array.prototype.every.call( + this.childNodes, + childNode => childNode.nodeType === 3, + ); + const hasImageFallback = fallbackSrc && fallbackSrc.length > 0; + const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0; + + if ( + isEmojiUnicode && + !isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion) + ) { + // CSS sprite fallback takes precedence over image fallback + if (hasCssSpriteFalback) { + // IE 11 doesn't like adding multiple at once :( + this.classList.add('emoji-icon'); + this.classList.add(fallbackSpriteClass); + } else if (hasImageFallback) { + this.innerHTML = emojiImageTag(name, fallbackSrc); + } else { + const src = assembleFallbackImageSrc(name); + this.innerHTML = emojiImageTag(name, src); + } + } +}; + +document.registerElement('gl-emoji', { + prototype: GlEmojiElementProto, +}); + +module.exports = { + emojiImageTag, + glEmojiTag, + isEmojiUnicodeSupported, + isFlagEmoji, + isKeycapEmoji, + isSkinToneComboEmoji, + isHorceRacingSkinToneComboEmoji, + isPersonZwjEmoji, +}; diff --git a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js new file mode 100644 index 00000000000..2380349c4fa --- /dev/null +++ b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js @@ -0,0 +1,50 @@ +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt#Fixing_charCodeAt()_to_handle_non-Basic-Multilingual-Plane_characters_if_their_presence_earlier_in_the_string_is_known +function knownCharCodeAt(givenString, index) { + const str = `${givenString}`; + const end = str.length; + + const surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; + let idx = index; + while ((surrogatePairs.exec(str)) != null) { + const li = surrogatePairs.lastIndex; + if (li - 2 < idx) { + idx += 1; + } else { + break; + } + } + + if (idx >= end || idx < 0) { + return NaN; + } + + const code = str.charCodeAt(idx); + + let high; + let low; + if (code >= 0xD800 && code <= 0xDBFF) { + high = code; + low = str.charCodeAt(idx + 1); + // Go one further, since one of the "characters" is part of a surrogate pair + return ((high - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; + } + return code; +} + +// See http://stackoverflow.com/a/38901550/796832 +// ES5/PhantomJS compatible version of spreading a string +// +// [...'foo'] -> ['f', 'o', 'o'] +// [...'🖐🏿'] -> ['🖐', '🏿'] +function spreadString(str) { + const arr = []; + let i = 0; + while (!isNaN(knownCharCodeAt(str, i))) { + const codePoint = knownCharCodeAt(str, i); + arr.push(String.fromCodePoint(codePoint)); + i += 1; + } + return arr; +} + +module.exports = spreadString; diff --git a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js new file mode 100644 index 00000000000..f31716d4c07 --- /dev/null +++ b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js @@ -0,0 +1,154 @@ +const unicodeSupportTestMap = { + // man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/ + // occupationZwj: '\u{1F468}\u{200D}\u{1F393}', + // woman, biking (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/ + // sexZwj: '\u{1F6B4}\u{200D}\u{2640}', + // family_mwgb + // Windows 8.1, Firefox 51.0.1 does not support `family_`, `kiss_`, `couple_` + personZwj: '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}', + // horse_racing_tone5 + // Special case that is not supported on macOS 10.12 even though `skinToneModifier` succeeds + horseRacing: '\u{1F3C7}\u{1F3FF}', + // US flag, http://emojipedia.org/flags/ + flag: '\u{1F1FA}\u{1F1F8}', + // http://emojipedia.org/modifiers/ + skinToneModifier: [ + // spy_tone5 + '\u{1F575}\u{1F3FF}', + // person_with_ball_tone5 + '\u{26F9}\u{1F3FF}', + // angel_tone5 + '\u{1F47C}\u{1F3FF}', + ], + // rofl, http://emojipedia.org/unicode-9.0/ + '9.0': '\u{1F923}', + // metal, http://emojipedia.org/unicode-8.0/ + '8.0': '\u{1F918}', + // spy, http://emojipedia.org/unicode-7.0/ + '7.0': '\u{1F575}', + // expressionless, http://emojipedia.org/unicode-6.1/ + 6.1: '\u{1F611}', + // japanese_goblin, http://emojipedia.org/unicode-6.0/ + '6.0': '\u{1F47A}', + // sailboat, http://emojipedia.org/unicode-5.2/ + 5.2: '\u{26F5}', + // mahjong, http://emojipedia.org/unicode-5.1/ + 5.1: '\u{1F004}', + // gear, http://emojipedia.org/unicode-4.1/ + 4.1: '\u{2699}', + // zap, http://emojipedia.org/unicode-4.0/ + '4.0': '\u{26A1}', + // recycle, http://emojipedia.org/unicode-3.2/ + 3.2: '\u{267B}', + // information_source, http://emojipedia.org/unicode-3.0/ + '3.0': '\u{2139}', + // heart, http://emojipedia.org/unicode-1.1/ + 1.1: '\u{2764}', +}; + +function checkPixelInImageDataArray(pixelOffset, imageDataArray) { + // `4 *` because RGBA + const indexOffset = 4 * pixelOffset; + const hasColor = imageDataArray[indexOffset + 0] || + imageDataArray[indexOffset + 1] || + imageDataArray[indexOffset + 2]; + const isVisible = imageDataArray[indexOffset + 3]; + // Check for some sort of color other than black + if (hasColor && isVisible) { + return true; + } + return false; +} + +const chromeMatches = navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\./); +const isChrome = chromeMatches && chromeMatches.length > 0; +const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatches[1], 10); + +// We use 16px because mobile Safari (iOS 9.3) doesn't properly scale emojis :/ +// See 32px, https://i.imgur.com/htY6Zym.png +// See 16px, https://i.imgur.com/FPPsIF8.png +const fontSize = 16; +function testUnicodeSupportMap(testMap) { + const testMapKeys = Object.keys(testMap); + const numTestEntries = testMapKeys + .reduce((list, testKey) => list.concat(testMap[testKey]), []).length; + + const canvas = document.createElement('canvas'); + (window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas; + const ctx = canvas.getContext('2d'); + canvas.width = (2 * fontSize); + canvas.height = (numTestEntries * fontSize); + ctx.fillStyle = '#000000'; + ctx.textBaseline = 'middle'; + ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`; + // Write each emoji to the canvas vertically + let writeIndex = 0; + testMapKeys.forEach((testKey) => { + const testEntry = testMap[testKey]; + [].concat(testEntry).forEach((emojiUnicode) => { + ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2)); + writeIndex += 1; + }); + }); + + // Read from the canvas + const resultMap = {}; + let readIndex = 0; + testMapKeys.forEach((testKey) => { + const testEntry = testMap[testKey]; + // This needs to be a `reduce` instead of `every` because we need to + // keep the `readIndex` in sync from the writes by running all entries + const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => { + // Sample along the vertical-middle for a couple of characters + const imageData = ctx.getImageData( + 0, + (readIndex * fontSize) + (fontSize / 2), + 2 * fontSize, + 1, + ).data; + + let isValidEmoji = false; + for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) { + const isLookingAtFirstChar = currentPixel < fontSize; + const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2)); + // Check for the emoji somewhere along the row + if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) { + isValidEmoji = true; + + // Check to see that nothing is rendered next to the first character + // to ensure that the ZWJ sequence rendered as one piece + } else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) { + isValidEmoji = false; + break; + } + } + + readIndex += 1; + return isSatisfied && isValidEmoji; + }, true); + + resultMap[testKey] = isTestSatisfied; + }); + + resultMap.meta = { + isChrome, + chromeVersion, + }; + + return resultMap; +} + +let unicodeSupportMap; +const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent'); +try { + unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map')); +} catch (err) { + // swallow +} +if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) { + unicodeSupportMap = testUnicodeSupportMap(unicodeSupportTestMap); + window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent); + window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap)); +} + +module.exports = unicodeSupportMap; diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js index 2d52e96e7fb..1330d4ae840 100644 --- a/app/assets/javascripts/boards/components/board_list.js +++ b/app/assets/javascripts/boards/components/board_list.js @@ -56,11 +56,6 @@ import boardCard from './board_card'; }); } }, - computed: { - orderedIssues () { - return _.sortBy(this.issues, 'priority'); - }, - }, methods: { listHeight () { return this.$refs.list.getBoundingClientRect().height; @@ -92,9 +87,9 @@ import boardCard from './board_card'; const options = gl.issueBoards.getBoardSortableDefaultOptions({ scroll: document.querySelectorAll('.boards-list')[0], group: 'issues', - sort: false, disabled: this.disabled, filter: '.board-list-count, .is-disabled', + dataIdAttr: 'data-issue-id', onStart: (e) => { const card = this.$refs.issue[e.oldIndex]; @@ -111,6 +106,13 @@ import boardCard from './board_card'; e.item.remove(); }); }, + onUpdate: (e) => { + const sortedArray = this.sortable.toArray().filter(id => id !== '-1'); + gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray); + }, + onMove(e) { + return !e.related.classList.contains('board-list-count'); + } }); this.sortable = Sortable.create(this.$refs.list, options); diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index 2d0a295ae4d..ca5e6fa7e9d 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -15,6 +15,7 @@ class ListIssue { this.labels = []; this.selected = false; this.assignee = false; + this.position = obj.relative_position || Infinity; if (obj.assignee) { this.assignee = new ListUser(obj.assignee); @@ -27,10 +28,6 @@ class ListIssue { obj.labels.forEach((label) => { this.labels.push(new ListLabel(label)); }); - - this.priority = this.labels.reduce((max, label) => { - return (label.priority < max) ? label.priority : max; - }, Infinity); } addLabel (label) { diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 8158ed4ec2c..f237567208c 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -110,9 +110,20 @@ class List { } addIssue (issue, listFrom, newIndex) { + let moveBeforeIid = null; + let moveAfterIid = null; + if (!this.findIssue(issue.id)) { if (newIndex !== undefined) { this.issues.splice(newIndex, 0, issue); + + if (this.issues[newIndex - 1]) { + moveBeforeIid = this.issues[newIndex - 1].id; + } + + if (this.issues[newIndex + 1]) { + moveAfterIid = this.issues[newIndex + 1].id; + } } else { this.issues.push(issue); } @@ -123,13 +134,21 @@ class List { if (listFrom) { this.issuesSize += 1; - this.updateIssueLabel(issue, listFrom); + + this.updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid); } } } - updateIssueLabel(issue, listFrom) { - gl.boardService.moveIssue(issue.id, listFrom.id, this.id) + moveIssue (issue, oldIndex, newIndex, moveBeforeIid, moveAfterIid) { + this.issues.splice(oldIndex, 1); + this.issues.splice(newIndex, 0, issue); + + gl.boardService.moveIssue(issue.id, null, null, moveBeforeIid, moveAfterIid); + } + + updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) { + gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid) .then(() => { listFrom.getIssues(false); }); diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index 065e90518df..e54102814d6 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -64,10 +64,12 @@ class BoardService { return this.issues.get(data); } - moveIssue (id, from_list_id, to_list_id) { + moveIssue (id, from_list_id = null, to_list_id = null, move_before_iid = null, move_after_iid = null) { return this.issue.update({ id }, { from_list_id, - to_list_id + to_list_id, + move_before_iid, + move_after_iid, }); } diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 56436c8fdc7..3866c6bbfc6 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -109,6 +109,12 @@ listFrom.removeIssue(issue); } }, + moveIssueInList (list, issue, oldIndex, newIndex, idArray) { + const beforeId = parseInt(idArray[newIndex - 1], 10) || null; + const afterId = parseInt(idArray[newIndex + 1], 10) || null; + + list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId); + }, findList (key, val, type = 'label') { return this.state.lists.filter((list) => { const byType = type ? list['type'] === type : true; diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js index 16bdb4db5af..8883c339335 100644 --- a/app/assets/javascripts/copy_as_gfm.js +++ b/app/assets/javascripts/copy_as_gfm.js @@ -49,6 +49,9 @@ require('./lib/utils/common_utils'); 'img.emoji'(el, text) { return el.getAttribute('alt'); }, + 'gl-emoji'(el, text) { + return `:${el.getAttribute('data-name')}:`; + }, }, ImageLinkFilter: { 'a.no-attachment-icon'(el, text) { diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 31f10f89245..546bdc9c8d7 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,3 +1,4 @@ +import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make this a bundle /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ /* global UsernameValidator */ /* global ActiveTabMemoizer */ @@ -286,7 +287,7 @@ const UserCallout = require('./user_callout'); case 'search:show': new Search(); break; - case 'projects:protected_branches:index': + case 'projects:repository:show': new gl.ProtectedBranchCreate(); new gl.ProtectedBranchEditList(); break; @@ -297,6 +298,8 @@ const UserCallout = require('./user_callout'); case 'ci:lints:show': new gl.CILintEditor(); break; + case 'projects:environments:metrics': + new PrometheusGraph(); case 'users:show': new UserCallout(); break; diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index 5cdf11c6a2c..f61be741b4a 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -37,11 +37,14 @@ require('../window')(function(w){ } } - self.hook.list[config.method].call(self.hook.list, data); + if (!self.destroyed) { + self.hook.list[config.method].call(self.hook.list, data); + } }, init: function init(hook) { var self = this; + self.destroyed = false; self.cache = self.cache || {}; var config = hook.config.droplabAjax; this.hook = hook; @@ -79,6 +82,7 @@ require('../window')(function(w){ destroy: function() { var dynamicList = this.hook.list.list.querySelector('[data-dynamic]'); + this.destroyed = true; if (this.listTemplate && dynamicList) { dynamicList.outerHTML = this.listTemplate; } diff --git a/app/assets/javascripts/extensions/string.js b/app/assets/javascripts/extensions/string.js new file mode 100644 index 00000000000..fe23be0bbc1 --- /dev/null +++ b/app/assets/javascripts/extensions/string.js @@ -0,0 +1,2 @@ +require('string.prototype.codepointat'); +require('string.fromcodepoint'); diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 9e92d544bef..38ff3fb7158 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -28,6 +28,23 @@ require('./filtered_search_dropdown'); const tag = selected.querySelector('.js-filter-tag').innerText.trim(); if (tag.length) { + // Get previous input values in the input field and convert them into visual tokens + const previousInputValues = this.input.value.split(' '); + const searchTerms = []; + + previousInputValues.forEach((value, index) => { + searchTerms.push(value); + + if (index === previousInputValues.length - 1 + && token.indexOf(value.toLowerCase()) !== -1) { + searchTerms.pop(); + } + }); + + if (searchTerms.length > 0) { + gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' ')); + } + gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', '')); } this.dismissDropdown(); @@ -39,7 +56,7 @@ require('./filtered_search_dropdown'); renderContent() { const dropdownData = []; - [].forEach.call(this.input.parentElement.querySelectorAll('.dropdown-menu'), (dropdownMenu) => { + [].forEach.call(this.input.closest('.filtered-search-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => { const { icon, hint, tag } = dropdownMenu.dataset; if (icon && hint && tag) { dropdownData.push({ diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index 7e9c6f74aa5..04e2afad02f 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -39,7 +39,12 @@ require('./filtered_search_dropdown'); getSearchInput() { const query = gl.DropdownUtils.getSearchInput(this.input); const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); - let value = lastToken.value || ''; + + let value = lastToken || ''; + + if (value[0] === '@') { + value = value.slice(1); + } // Removes the first character if it is a quotation so that we can search // with multiple words diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index de3fa116717..b52081df646 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -22,38 +22,40 @@ static filterWithSymbol(filterSymbol, input, item) { const updatedItem = item; - const query = gl.DropdownUtils.getSearchInput(input); - const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query); + const searchInput = gl.DropdownUtils.getSearchInput(input); - if (lastToken !== searchToken) { - const title = updatedItem.title.toLowerCase(); - let value = lastToken.value.toLowerCase(); + const title = updatedItem.title.toLowerCase(); + let value = searchInput.toLowerCase(); + let symbol = ''; - // Removes the first character if it is a quotation so that we can search - // with multiple words - if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { - value = value.slice(1); - } - - // Eg. filterSymbol = ~ for labels - const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1; - const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1; + // Remove the symbol for filter + if (value[0] === filterSymbol) { + symbol = value[0]; + value = value.slice(1); + } - updatedItem.droplab_hidden = !match && !matchWithoutSymbol; - } else { - updatedItem.droplab_hidden = false; + // Removes the first character if it is a quotation so that we can search + // with multiple words + if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { + value = value.slice(1); } + // Eg. filterSymbol = ~ for labels + const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1; + const match = title.indexOf(`${symbol}${value}`) !== -1; + + updatedItem.droplab_hidden = !match && !matchWithoutSymbol; + return updatedItem; } static filterHint(input, item) { const updatedItem = item; - const query = gl.DropdownUtils.getSearchInput(input); - let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + const searchInput = gl.DropdownUtils.getSearchInput(input); + let { lastToken } = gl.FilteredSearchTokenizer.processTokens(searchInput); lastToken = lastToken.key || lastToken || ''; - if (!lastToken || query.split('').last() === ' ') { + if (!lastToken || searchInput.split('').last() === ' ') { updatedItem.droplab_hidden = false; } else if (lastToken) { const split = lastToken.split(':'); @@ -70,13 +72,41 @@ const dataValue = selected.getAttribute('data-value'); if (dataValue) { - gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue); + gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true); } // Return boolean based on whether it was set return dataValue !== null; } + // Determines the full search query (visual tokens + input) + static getSearchQuery() { + const tokensContainer = document.querySelector('.tokens-container'); + const values = []; + + [].forEach.call(tokensContainer.querySelectorAll('.js-visual-token'), (token) => { + const name = token.querySelector('.name'); + const value = token.querySelector('.value'); + const symbol = value && value.dataset.symbol ? value.dataset.symbol : ''; + let valueText = ''; + + if (value && value.innerText) { + valueText = value.innerText; + } + + if (token.className.indexOf('filtered-search-token') !== -1) { + values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`); + } else { + values.push(name.innerText); + } + }); + + const input = document.querySelector('.filtered-search'); + values.push(input && input.value); + + return values.join(' '); + } + static getSearchInput(filteredSearchInput) { const inputValue = filteredSearchInput.value; const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput); diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js index faaba994f46..856eb6590ee 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js +++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js @@ -7,3 +7,4 @@ require('./filtered_search_dropdown'); require('./filtered_search_manager'); require('./filtered_search_token_keys'); require('./filtered_search_tokenizer'); +require('./filtered_search_visual_tokens'); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js index dd565da507e..134bdc6ad80 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js @@ -35,7 +35,7 @@ if (!dataValueSet) { const value = getValueFunction(selected); - gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value); + gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true); } this.dismissDropdown(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index cecd3518ce3..608c65c78a4 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -58,35 +58,15 @@ }; } - static addWordToInput(tokenName, tokenValue = '') { + static addWordToInput(tokenName, tokenValue = '', clicked = false) { const input = document.querySelector('.filtered-search'); - const inputValue = input.value; - const word = `${tokenName}:${tokenValue}`; - // Get the string to replace - let newCaretPosition = input.selectionStart; - const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input); + gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue); + input.value = ''; - input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`; - - // If we have added a tokenValue at the end of the input, - // add a space and set selection to the end - if (right >= inputValue.length && tokenValue !== '') { - input.value += ' '; - newCaretPosition = input.value.length; + if (clicked) { + gl.FilteredSearchVisualTokens.moveInputToTheRight(); } - - gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input); - } - - static updateInputCaretPosition(selectionStart, input) { - // Reset the position - // Sometimes can end up at end of input - input.setSelectionRange(selectionStart, selectionStart); - - const { right } = gl.DropdownUtils.getInputSelectionPosition(input); - - input.setSelectionRange(right, right); } updateCurrentDropdownOffset() { @@ -94,19 +74,14 @@ } updateDropdownOffset(key) { - if (!this.font) { - this.font = window.getComputedStyle(this.filteredSearchInput).font; - } - - const input = this.filteredSearchInput; - const inputText = input.value.slice(0, input.selectionStart); - const filterIconPadding = 27; - let offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding; + // Always align dropdown with the input field + let offset = this.filteredSearchInput.getBoundingClientRect().left - document.querySelector('.scroll-container').getBoundingClientRect().left; - const currentDropdownWidth = this.mapping[key].element.clientWidth === 0 ? 200 : - this.mapping[key].element.clientWidth; - const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth; + const maxInputWidth = 240; + const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth; + // Make sure offset never exceeds the input container + const offsetMaxWidth = document.querySelector('.scroll-container').clientWidth - currentDropdownWidth; if (offsetMaxWidth < offset) { offset = offsetMaxWidth; } @@ -164,8 +139,8 @@ } setDropdown() { - const { lastToken, searchToken } = this.tokenizer - .processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput)); + const query = gl.DropdownUtils.getSearchQuery(); + const { lastToken, searchToken } = this.tokenizer.processTokens(query); if (this.currentDropdown) { this.updateCurrentDropdownOffset(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index bbafead0305..58a984048de 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -3,6 +3,7 @@ constructor(page) { this.filteredSearchInput = document.querySelector('.filtered-search'); this.clearSearchButton = document.querySelector('.clear-search'); + this.tokensContainer = document.querySelector('.tokens-container'); this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; if (this.filteredSearchInput) { @@ -27,36 +28,62 @@ this.handleFormSubmit = this.handleFormSubmit.bind(this); this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this); + this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this); + this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this); this.checkForEnterWrapper = this.checkForEnter.bind(this); this.clearSearchWrapper = this.clearSearch.bind(this); this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); + this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this); + this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this); + this.editTokenWrapper = this.editToken.bind(this); this.tokenChange = this.tokenChange.bind(this); this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); + this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper); + this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper); this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper); this.filteredSearchInput.addEventListener('click', this.tokenChange); this.filteredSearchInput.addEventListener('keyup', this.tokenChange); + this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken); + this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper); this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); + document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); + document.addEventListener('click', this.unselectEditTokensWrapper); + document.addEventListener('keydown', this.removeSelectedTokenWrapper); } unbindEvents() { this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit); this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); + this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper); + this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper); this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper); this.filteredSearchInput.removeEventListener('click', this.tokenChange); this.filteredSearchInput.removeEventListener('keyup', this.tokenChange); + this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken); + this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper); this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); + document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); + document.removeEventListener('click', this.unselectEditTokensWrapper); + document.removeEventListener('keydown', this.removeSelectedTokenWrapper); } checkForBackspace(e) { // 8 = Backspace Key // 46 = Delete Key if (e.keyCode === 8 || e.keyCode === 46) { + const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + if (this.filteredSearchInput.value === '' && lastVisualToken) { + this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); + gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + } + // Reposition dropdown so that it is aligned with cursor this.dropdownManager.updateCurrentDropdownOffset(); } @@ -86,11 +113,68 @@ } } - toggleClearSearchButton(e) { - if (e.target.value) { - this.clearSearchButton.classList.remove('hidden'); - } else { - this.clearSearchButton.classList.add('hidden'); + static selectToken(e) { + const button = e.target.closest('.selectable'); + + if (button) { + e.preventDefault(); + e.stopPropagation(); + gl.FilteredSearchVisualTokens.selectToken(button); + } + } + + unselectEditTokens(e) { + const inputContainer = document.querySelector('.filtered-search-input-container'); + const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); + const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null; + const isElementTokensContainer = e.target.classList.contains('tokens-container'); + + if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) { + gl.FilteredSearchVisualTokens.moveInputToTheRight(); + this.dropdownManager.resetDropdowns(); + } + } + + editToken(e) { + const token = e.target.closest('.js-visual-token'); + + if (token) { + gl.FilteredSearchVisualTokens.editToken(token); + this.tokenChange(); + } + } + + toggleClearSearchButton() { + const query = gl.DropdownUtils.getSearchQuery(); + const hidden = 'hidden'; + const hasHidden = this.clearSearchButton.classList.contains(hidden); + + if (query.length === 0 && !hasHidden) { + this.clearSearchButton.classList.add(hidden); + } else if (query.length && hasHidden) { + this.clearSearchButton.classList.remove(hidden); + } + } + + handleInputPlaceholder() { + const query = gl.DropdownUtils.getSearchQuery(); + const placeholder = 'Search or filter results...'; + const currentPlaceholder = this.filteredSearchInput.placeholder; + + if (query.length === 0 && currentPlaceholder !== placeholder) { + this.filteredSearchInput.placeholder = placeholder; + } else if (query.length > 0 && currentPlaceholder !== '') { + this.filteredSearchInput.placeholder = ''; + } + } + + removeSelectedToken(e) { + // 8 = Backspace Key + // 46 = Delete Key + if (e.keyCode === 8 || e.keyCode === 46) { + gl.FilteredSearchVisualTokens.removeSelectedToken(); + this.handleInputPlaceholder(); + this.toggleClearSearchButton(); } } @@ -98,11 +182,67 @@ e.preventDefault(); this.filteredSearchInput.value = ''; + + const removeElements = []; + + [].forEach.call(this.tokensContainer.children, (t) => { + if (t.classList.contains('js-visual-token')) { + removeElements.push(t); + } + }); + + removeElements.forEach((el) => { + el.parentElement.removeChild(el); + }); + this.clearSearchButton.classList.add('hidden'); + this.handleInputPlaceholder(); this.dropdownManager.resetDropdowns(); } + handleInputVisualToken() { + const input = this.filteredSearchInput; + const { tokens, searchToken } + = gl.FilteredSearchTokenizer.processTokens(input.value); + const { isLastVisualTokenValid } + = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + if (isLastVisualTokenValid) { + tokens.forEach((t) => { + input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, ''); + gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`); + }); + + const fragments = searchToken.split(':'); + if (fragments.length > 1) { + const inputValues = fragments[0].split(' '); + const tokenKey = inputValues.last(); + + if (inputValues.length > 1) { + inputValues.pop(); + const searchTerms = inputValues.join(' '); + + input.value = input.value.replace(searchTerms, ''); + gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms); + } + + gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey); + input.value = input.value.replace(`${tokenKey}:`, ''); + } + } else { + // Keep listening to token until we determine that the user is done typing the token value + const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g; + + if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') { + gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken); + + // Trim the last space as seen in the if statement above + input.value = input.value.replace(searchToken, '').trim(); + } + } + } + handleFormSubmit(e) { e.preventDefault(); this.search(); @@ -111,7 +251,7 @@ loadSearchParamsFromURL() { const params = gl.utils.getUrlParamsArray(); const usernameParams = this.getUsernameParams(); - const inputValues = []; + let hasFilteredSearch = false; params.forEach((p) => { const split = p.split('='); @@ -122,7 +262,8 @@ const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p); if (condition) { - inputValues.push(`${condition.tokenKey}:${condition.value}`); + hasFilteredSearch = true; + gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value); } else { // Sanitize value since URL converts spaces into + // Replace before decode so that we know what was originally + versus the encoded + @@ -140,34 +281,37 @@ quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; } - inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); + hasFilteredSearch = true; + gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); } else if (!match && keyParam === 'assignee_id') { const id = parseInt(value, 10); if (usernameParams[id]) { - inputValues.push(`assignee:@${usernameParams[id]}`); + hasFilteredSearch = true; + gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`); } } else if (!match && keyParam === 'author_id') { const id = parseInt(value, 10); if (usernameParams[id]) { - inputValues.push(`author:@${usernameParams[id]}`); + hasFilteredSearch = true; + gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`); } } else if (!match && keyParam === 'search') { - inputValues.push(sanitizedValue); + hasFilteredSearch = true; + this.filteredSearchInput.value = sanitizedValue; } } }); - // Trim the last space value - this.filteredSearchInput.value = inputValues.join(' '); - - if (inputValues.length > 0) { + if (hasFilteredSearch) { this.clearSearchButton.classList.remove('hidden'); + this.handleInputPlaceholder(); } } search() { const paths = []; - const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); + const { tokens, searchToken } + = this.tokenizer.processTokens(gl.DropdownUtils.getSearchQuery()); const currentState = gl.utils.getParameterByName('state') || 'opened'; paths.push(`state=${currentState}`); diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js new file mode 100644 index 00000000000..320afa26130 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -0,0 +1,200 @@ +class FilteredSearchVisualTokens { + static getLastVisualTokenBeforeInput() { + const inputLi = document.querySelector('.input-token'); + const lastVisualToken = inputLi && inputLi.previousElementSibling; + + return { + lastVisualToken, + isLastVisualTokenValid: lastVisualToken === null || lastVisualToken.className.indexOf('filtered-search-term') !== -1 || (lastVisualToken && lastVisualToken.querySelector('.value') !== null), + }; + } + + static unselectTokens() { + const otherTokens = document.querySelectorAll('.js-visual-token .selectable.selected'); + [].forEach.call(otherTokens, t => t.classList.remove('selected')); + } + + static selectToken(tokenButton) { + const selected = tokenButton.classList.contains('selected'); + FilteredSearchVisualTokens.unselectTokens(); + + if (!selected) { + tokenButton.classList.add('selected'); + } + } + + static removeSelectedToken() { + const selected = document.querySelector('.js-visual-token .selected'); + + if (selected) { + const li = selected.closest('.js-visual-token'); + li.parentElement.removeChild(li); + } + } + + static createVisualTokenElementHTML() { + return ` + <div class="selectable" role="button"> + <div class="name"></div> + <div class="value"></div> + </div> + `; + } + + static addVisualTokenElement(name, value, isSearchTerm) { + const li = document.createElement('li'); + li.classList.add('js-visual-token'); + li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token'); + + if (value) { + li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML(); + li.querySelector('.value').innerText = value; + } else { + li.innerHTML = '<div class="name"></div>'; + } + li.querySelector('.name').innerText = name; + + const tokensContainer = document.querySelector('.tokens-container'); + const input = document.querySelector('.filtered-search'); + tokensContainer.insertBefore(li, input.parentElement); + } + + static addValueToPreviousVisualTokenElement(value) { + const { lastVisualToken, isLastVisualTokenValid } = + FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + if (!isLastVisualTokenValid && lastVisualToken.classList.contains('filtered-search-token')) { + const name = FilteredSearchVisualTokens.getLastTokenPartial(); + lastVisualToken.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML(); + lastVisualToken.querySelector('.name').innerText = name; + lastVisualToken.querySelector('.value').innerText = value; + } + } + + static addFilterVisualToken(tokenName, tokenValue) { + const { lastVisualToken, isLastVisualTokenValid } + = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement; + + if (isLastVisualTokenValid) { + addVisualTokenElement(tokenName, tokenValue); + } else { + const previousTokenName = lastVisualToken.querySelector('.name').innerText; + const tokensContainer = document.querySelector('.tokens-container'); + tokensContainer.removeChild(lastVisualToken); + + const value = tokenValue || tokenName; + addVisualTokenElement(previousTokenName, value); + } + } + + static addSearchVisualToken(searchTerm) { + const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + if (lastVisualToken && lastVisualToken.classList.contains('filtered-search-term')) { + lastVisualToken.querySelector('.name').innerText += ` ${searchTerm}`; + } else { + FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, true); + } + } + + static getLastTokenPartial() { + const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + if (!lastVisualToken) return ''; + + const value = lastVisualToken.querySelector('.value'); + const name = lastVisualToken.querySelector('.name'); + + const valueText = value ? value.innerText : ''; + const nameText = name ? name.innerText : ''; + + return valueText || nameText; + } + + static removeLastTokenPartial() { + const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + if (lastVisualToken) { + const value = lastVisualToken.querySelector('.value'); + + if (value) { + const button = lastVisualToken.querySelector('.selectable'); + button.removeChild(value); + lastVisualToken.innerHTML = button.innerHTML; + } else { + lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken); + } + } + } + + static tokenizeInput() { + const input = document.querySelector('.filtered-search'); + const { isLastVisualTokenValid } = + gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + if (input.value) { + if (isLastVisualTokenValid) { + gl.FilteredSearchVisualTokens.addSearchVisualToken(input.value); + } else { + FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement(input.value); + } + + input.value = ''; + } + } + + static editToken(token) { + const input = document.querySelector('.filtered-search'); + + FilteredSearchVisualTokens.tokenizeInput(); + + // Replace token with input field + const tokenContainer = token.parentElement; + const inputLi = input.parentElement; + tokenContainer.replaceChild(inputLi, token); + + const name = token.querySelector('.name'); + const value = token.querySelector('.value'); + + if (token.classList.contains('filtered-search-token')) { + FilteredSearchVisualTokens.addFilterVisualToken(name.innerText); + input.value = value.innerText; + } else { + // token is a search term + input.value = name.innerText; + } + + // Opens dropdown + const inputEvent = new Event('input'); + input.dispatchEvent(inputEvent); + + // Adds cursor to input + input.focus(); + } + + static moveInputToTheRight() { + const input = document.querySelector('.filtered-search'); + const inputLi = input.parentElement; + const tokenContainer = document.querySelector('.tokens-container'); + + FilteredSearchVisualTokens.tokenizeInput(); + + if (!tokenContainer.lastElementChild.isEqualNode(inputLi)) { + const { isLastVisualTokenValid } = + gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + if (!isLastVisualTokenValid) { + const lastPartial = gl.FilteredSearchVisualTokens.getLastTokenPartial(); + gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + gl.FilteredSearchVisualTokens.addSearchVisualToken(lastPartial); + } + + tokenContainer.removeChild(inputLi); + tokenContainer.appendChild(inputLi); + } + } +} + +window.gl = window.gl || {}; +gl.FilteredSearchVisualTokens = FilteredSearchVisualTokens; diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 60d6658dc16..1bc04a5ad96 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -1,5 +1,11 @@ /* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */ +const emojiMap = require('emoji-map'); +const emojiAliases = require('emoji-aliases'); +const glEmoji = require('./behaviors/gl_emoji'); + +const glEmojiTag = glEmoji.glEmojiTag; + // Creates the variables for setting up GFM auto-completion (function() { if (window.gl == null) { @@ -26,7 +32,12 @@ }, // Emoji Emoji: { - template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>' + templateFunction: function(name) { + return `<li> + ${name} ${glEmojiTag(name)} + </li> + `; + } }, // Team Members Members: { @@ -113,7 +124,7 @@ $input.atwho({ at: ':', displayTpl: function(value) { - return value.path != null ? this.Emoji.template : this.Loading.template; + return value && value.name ? this.Emoji.templateFunction(value.name) : this.Loading.template; }.bind(this), insertTpl: ':${name}:', skipSpecialCharacterTest: true, @@ -355,6 +366,8 @@ this.isLoadingData[at] = true; if (this.cachedData[at]) { this.loadData($input, at, this.cachedData[at]); + } else if (this.atTypeMap[at] === 'emojis') { + this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases))); } else { $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => { this.loadData($input, at, data); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 6ac659fd290..ae4dd64424c 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -3,7 +3,6 @@ /* global Cookies */ /* global Flash */ /* global ConfirmDangerModal */ -/* global AwardsHandler */ /* global Aside */ import jQuery from 'jquery'; @@ -19,6 +18,15 @@ require('mousetrap/plugins/pause/mousetrap-pause'); require('vendor/fuzzaldrin-plus'); require('es6-promise').polyfill(); +// extensions +require('./extensions/string'); +require('./extensions/array'); +require('./extensions/custom_event'); +require('./extensions/element'); +require('./extensions/jquery'); +require('./extensions/object'); +require('es6-promise').polyfill(); + // expose common libraries as globals (TODO: remove these) window.jQuery = jQuery; window.$ = jQuery; @@ -62,13 +70,6 @@ require('./templates/issuable_template_selectors'); require('./commit/file.js'); require('./commit/image_file.js'); -// extensions -require('./extensions/array'); -require('./extensions/custom_event'); -require('./extensions/element'); -require('./extensions/jquery'); -require('./extensions/object'); - // lib/utils require('./lib/utils/animate'); require('./lib/utils/bootstrap_linked_tabs'); @@ -100,7 +101,7 @@ require('./ajax_loading_spinner'); require('./api'); require('./aside'); require('./autosave'); -require('./awards_handler'); +const AwardsHandler = require('./awards_handler'); require('./breakpoints'); require('./broadcast_message'); require('./build'); diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js new file mode 100644 index 00000000000..9384fe3f276 --- /dev/null +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -0,0 +1,333 @@ +/* eslint-disable no-new*/ +import d3 from 'd3'; +import _ from 'underscore'; +import statusCodes from '~/lib/utils/http_status'; +import '~/lib/utils/common_utils'; +import Flash from '~/flash'; + +const prometheusGraphsContainer = '.prometheus-graph'; +const metricsEndpoint = 'metrics.json'; +const timeFormat = d3.time.format('%H:%M'); +const dayFormat = d3.time.format('%b %e, %a'); +const bisectDate = d3.bisector(d => d.time).left; +const extraAddedWidthParent = 100; + +class PrometheusGraph { + + constructor() { + this.margin = { top: 80, right: 180, bottom: 80, left: 100 }; + this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 }; + const parentContainerWidth = $(prometheusGraphsContainer).parent().width() + + extraAddedWidthParent; + this.originalWidth = parentContainerWidth; + this.originalHeight = 400; + this.width = parentContainerWidth - this.margin.left - this.margin.right; + this.height = 400 - this.margin.top - this.margin.bottom; + this.backOffRequestCounter = 0; + this.configureGraph(); + this.init(); + } + + createGraph() { + const self = this; + _.each(this.data, (value, key) => { + if (value.length > 0 && (key === 'cpu_values' || key === 'memory_values')) { + self.plotValues(value, key); + } + }); + } + + init() { + const self = this; + this.getData().then((metricsResponse) => { + if (metricsResponse === {}) { + new Flash('Empty metrics', 'alert'); + } else { + self.transformData(metricsResponse); + self.createGraph(); + } + }); + } + + plotValues(valuesToPlot, key) { + const x = d3.time.scale() + .range([0, this.width]); + + const y = d3.scale.linear() + .range([this.height, 0]); + + const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`; + + const graphSpecifics = this.graphSpecificProperties[key]; + + const chart = d3.select(prometheusGraphContainer) + .attr('width', this.width + this.margin.left + this.margin.right) + .attr('height', this.height + this.margin.bottom + this.margin.top) + .append('g') + .attr('transform', `translate(${this.margin.left},${this.margin.top})`); + + const axisLabelContainer = d3.select(prometheusGraphContainer) + .attr('width', this.originalWidth + this.marginLabelContainer.left + this.marginLabelContainer.right) + .attr('height', this.originalHeight + this.marginLabelContainer.bottom + this.marginLabelContainer.top) + .append('g') + .attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`); + + x.domain(d3.extent(valuesToPlot, d => d.time)); + y.domain([0, d3.max(valuesToPlot.map(metricValue => metricValue.value))]); + + const xAxis = d3.svg.axis() + .scale(x) + .ticks(this.commonGraphProperties.axis_no_ticks) + .orient('bottom'); + + const yAxis = d3.svg.axis() + .scale(y) + .ticks(this.commonGraphProperties.axis_no_ticks) + .tickSize(-this.width) + .orient('left'); + + this.createAxisLabelContainers(axisLabelContainer, key); + + chart.append('g') + .attr('class', 'x-axis') + .attr('transform', `translate(0,${this.height})`) + .call(xAxis); + + chart.append('g') + .attr('class', 'y-axis') + .call(yAxis); + + const area = d3.svg.area() + .x(d => x(d.time)) + .y0(this.height) + .y1(d => y(d.value)) + .interpolate('linear'); + + const line = d3.svg.line() + .x(d => x(d.time)) + .y(d => y(d.value)); + + chart.append('path') + .datum(valuesToPlot) + .attr('d', area) + .attr('class', 'metric-area') + .attr('fill', graphSpecifics.area_fill_color); + + chart.append('path') + .datum(valuesToPlot) + .attr('class', 'metric-line') + .attr('stroke', graphSpecifics.line_color) + .attr('fill', 'none') + .attr('stroke-width', this.commonGraphProperties.area_stroke_width) + .attr('d', line); + + // Overlay area for the mouseover events + chart.append('rect') + .attr('class', 'prometheus-graph-overlay') + .attr('width', this.width) + .attr('height', this.height) + .on('mousemove', this.handleMouseOverGraph.bind(this, x, y, valuesToPlot, chart, prometheusGraphContainer, key)); + } + + // The legends from the metric + createAxisLabelContainers(axisLabelContainer, key) { + const graphSpecifics = this.graphSpecificProperties[key]; + + axisLabelContainer.append('line') + .attr('class', 'label-x-axis-line') + .attr('stroke', '#000000') + .attr('stroke-width', '1') + .attr({ + x1: 0, + y1: this.originalHeight - this.marginLabelContainer.top, + x2: this.originalWidth - this.margin.right, + y2: this.originalHeight - this.marginLabelContainer.top, + }); + + axisLabelContainer.append('line') + .attr('class', 'label-y-axis-line') + .attr('stroke', '#000000') + .attr('stroke-width', '1') + .attr({ + x1: 0, + y1: 0, + x2: 0, + y2: this.originalHeight - this.marginLabelContainer.top, + }); + + axisLabelContainer.append('text') + .attr('class', 'label-axis-text') + .attr('text-anchor', 'middle') + .attr('transform', `translate(15, ${(this.originalHeight - this.marginLabelContainer.top) / 2}) rotate(-90)`) + .text(graphSpecifics.graph_legend_title); + + axisLabelContainer.append('rect') + .attr('class', 'rect-axis-text') + .attr('x', (this.originalWidth / 2) - this.margin.right) + .attr('y', this.originalHeight - this.marginLabelContainer.top - 20) + .attr('width', 30) + .attr('height', 80); + + axisLabelContainer.append('text') + .attr('class', 'label-axis-text') + .attr('x', (this.originalWidth / 2) - this.margin.right) + .attr('y', this.originalHeight - this.marginLabelContainer.top) + .attr('dy', '.35em') + .text('Time'); + + // Legends + + // Metric Usage + axisLabelContainer.append('rect') + .attr('x', this.originalWidth - 170) + .attr('y', (this.originalHeight / 2) - 80) + .style('fill', graphSpecifics.area_fill_color) + .attr('width', 20) + .attr('height', 35); + + axisLabelContainer.append('text') + .attr('class', 'label-axis-text') + .attr('x', this.originalWidth - 140) + .attr('y', (this.originalHeight / 2) - 65) + .text(graphSpecifics.graph_legend_title); + + axisLabelContainer.append('text') + .attr('class', 'text-metric-usage') + .attr('x', this.originalWidth - 140) + .attr('y', (this.originalHeight / 2) - 50); + } + + handleMouseOverGraph(x, y, valuesToPlot, chart, prometheusGraphContainer, key) { + const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`); + const timeValueFromOverlay = x.invert(d3.mouse(rectOverlay)[0]); + const timeValueIndex = bisectDate(valuesToPlot, timeValueFromOverlay, 1); + const d0 = valuesToPlot[timeValueIndex - 1]; + const d1 = valuesToPlot[timeValueIndex]; + const currentData = timeValueFromOverlay - d0.time > d1.time - timeValueFromOverlay ? d1 : d0; + const maxValueMetric = y(d3.max(valuesToPlot.map(metricValue => metricValue.value))); + const currentTimeCoordinate = x(currentData.time); + const graphSpecifics = this.graphSpecificProperties[key]; + // Remove the current selectors + d3.selectAll(`${prometheusGraphContainer} .selected-metric-line`).remove(); + d3.selectAll(`${prometheusGraphContainer} .circle-metric`).remove(); + d3.selectAll(`${prometheusGraphContainer} .rect-text-metric`).remove(); + d3.selectAll(`${prometheusGraphContainer} .text-metric`).remove(); + + chart.append('line') + .attr('class', 'selected-metric-line') + .attr({ + x1: currentTimeCoordinate, + y1: y(0), + x2: currentTimeCoordinate, + y2: maxValueMetric, + }); + + chart.append('circle') + .attr('class', 'circle-metric') + .attr('fill', graphSpecifics.line_color) + .attr('cx', currentTimeCoordinate) + .attr('cy', y(currentData.value)) + .attr('r', this.commonGraphProperties.circle_radius_metric); + + // The little box with text + const rectTextMetric = chart.append('g') + .attr('class', 'rect-text-metric') + .attr('translate', `(${currentTimeCoordinate}, ${y(currentData.value)})`); + + rectTextMetric.append('rect') + .attr('class', 'rect-metric') + .attr('x', currentTimeCoordinate + 10) + .attr('y', maxValueMetric) + .attr('width', this.commonGraphProperties.rect_text_width) + .attr('height', this.commonGraphProperties.rect_text_height); + + rectTextMetric.append('text') + .attr('class', 'text-metric') + .attr('x', currentTimeCoordinate + 35) + .attr('y', maxValueMetric + 35) + .text(timeFormat(currentData.time)); + + rectTextMetric.append('text') + .attr('class', 'text-metric-date') + .attr('x', currentTimeCoordinate + 15) + .attr('y', maxValueMetric + 15) + .text(dayFormat(currentData.time)); + + // Update the text + d3.select(`${prometheusGraphContainer} .text-metric-usage`) + .text(currentData.value.substring(0, 8)); + } + + configureGraph() { + this.graphSpecificProperties = { + cpu_values: { + area_fill_color: '#edf3fc', + line_color: '#5b99f7', + graph_legend_title: 'CPU Usage (Cores)', + }, + memory_values: { + area_fill_color: '#fca326', + line_color: '#fc6d26', + graph_legend_title: 'Memory Usage (MB)', + }, + }; + + this.commonGraphProperties = { + area_stroke_width: 2, + median_total_characters: 8, + circle_radius_metric: 5, + rect_text_width: 90, + rect_text_height: 40, + axis_no_ticks: 3, + }; + } + + getData() { + const maxNumberOfRequests = 3; + return gl.utils.backOff((next, stop) => { + $.ajax({ + url: metricsEndpoint, + dataType: 'json', + }) + .done((data, statusText, resp) => { + if (resp.status === statusCodes.NO_CONTENT) { + this.backOffRequestCounter = this.backOffRequestCounter += 1; + if (this.backOffRequestCounter < maxNumberOfRequests) { + next(); + } else { + stop({ + status: resp.status, + metrics: data, + }); + } + } else { + stop({ + status: resp.status, + metrics: data, + }); + } + }).fail(stop); + }) + .then((resp) => { + if (resp.status === statusCodes.NO_CONTENT) { + return {}; + } + return resp.metrics; + }) + .catch(() => new Flash('An error occurred while fetching metrics.', 'alert')); + } + + transformData(metricsResponse) { + const metricTypes = {}; + _.each(metricsResponse.metrics, (value, key) => { + const metricValues = value[0].values; + metricTypes[key] = _.map(metricValues, metric => ({ + time: new Date(metric[0] * 1000), + value: metric[1], + })); + }); + this.data = metricTypes; + } +} + +export default PrometheusGraph; diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index 73db8c10b99..09a58cad2b2 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -16,6 +16,9 @@ require('./shortcuts'); Mousetrap.bind('g p', function() { return ShortcutsNavigation.findAndFollowLink('.shortcuts-project'); }); + Mousetrap.bind('g e', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-project-activity'); + }); Mousetrap.bind('g f', function() { return ShortcutsNavigation.findAndFollowLink('.shortcuts-tree'); }); @@ -28,6 +31,9 @@ require('./shortcuts'); Mousetrap.bind('g n', function() { return ShortcutsNavigation.findAndFollowLink('.shortcuts-network'); }); + Mousetrap.bind('g g', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-repository-charts'); + }); Mousetrap.bind('g i', function() { return ShortcutsNavigation.findAndFollowLink('.shortcuts-issues'); }); diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js index 7dba5840c8a..d48f2404fa5 100644 --- a/app/assets/javascripts/test_utils/simulate_drag.js +++ b/app/assets/javascripts/test_utils/simulate_drag.js @@ -43,7 +43,14 @@ return event; } - function getTraget(target) { + function isLast(target) { + var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; + var children = el.children; + + return children.length - 1 === target.index; + } + + function getTarget(target) { var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; var children = el.children; @@ -75,12 +82,22 @@ function simulateDrag(options, callback) { options.to.el = options.to.el || options.from.el; - var fromEl = getTraget(options.from); - var toEl = getTraget(options.to); + var fromEl = getTarget(options.from); + var toEl = getTarget(options.to); + var firstEl = getTarget({ + el: options.to.el, + index: 'first' + }); + var lastEl = getTarget({ + el: options.to.el, + index: 'last' + }); var scrollable = options.scrollable; var fromRect = getRect(fromEl); var toRect = getRect(toEl); + var firstRect = getRect(firstEl); + var lastRect = getRect(lastEl); var startTime = new Date().getTime(); var duration = options.duration || 1000; @@ -88,6 +105,12 @@ options.ontap && options.ontap(); window.SIMULATE_DRAG_ACTIVE = 1; + if (options.to.index === 0) { + toRect.cy = firstRect.y; + } else if (isLast(options.to)) { + toRect.cy = lastRect.y + lastRect.h + 50; + } + var dragInterval = setInterval(function loop() { var progress = (new Date().getTime() - startTime) / duration; var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft; diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index de33a31b411..27af859f7d8 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -60,6 +60,15 @@ }); }; + $('.assign-to-me-link').on('click', (e) => { + e.preventDefault(); + $(e.currentTarget).hide(); + const $input = $(`input[name="${$dropdown.data('field-name')}"]`); + $input.val(gon.current_user_id); + selectedId = $input.val(); + $dropdown.find('.dropdown-toggle-text').text(gon.current_user_fullname).removeClass('is-default'); + }); + $block.on('click', '.js-assign-yourself', function(e) { e.preventDefault(); @@ -199,6 +208,11 @@ if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { e.preventDefault(); selectedId = user.id; + if (selectedId === gon.current_user_id) { + $('.assign-to-me-link').hide(); + } else { + $('.assign-to-me-link').show(); + } return; } if ($el.closest('.add-issues-modal').length) { @@ -234,11 +248,16 @@ id: function (user) { return user.id; }, + opened: function(e) { + const $el = $(e.currentTarget); + $el.find('.is-active').removeClass('is-active'); + $el.find(`li[data-user-id="${selectedId}"] .dropdown-menu-user-link`).addClass('is-active'); + }, renderRow: function(user) { var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username; username = user.username ? "@" + user.username : ""; avatar = user.avatar_url ? user.avatar_url : false; - selected = user.id === selectedId ? "is-active" : ""; + selected = user.id === parseInt(selectedId, 10) ? "is-active" : ""; img = ""; if (user.beforeDivider != null) { "<li> <a href='#' class='" + selected + "'> " + user.name + " </a> </li>"; @@ -248,7 +267,7 @@ } } // split into three parts so we can remove the username section if nessesary - listWithName = "<li> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>"; + listWithName = "<li data-user-id=" + user.id + "> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>"; listWithUserName = "<span class='dropdown-menu-user-username'> " + username + " </span>"; listClosingTags = "</a> </li>"; if (username === '') { diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 39cf3b5f8ae..5bb7e8caec1 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -44,5 +44,6 @@ @import "framework/images.scss"; @import "framework/broadcast-messages"; @import "framework/emojis.scss"; +@import "framework/emoji-sprites.scss"; @import "framework/icons.scss"; @import "framework/snippets.scss"; diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index 49907417e26..f363affa46c 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -7,6 +7,7 @@ .emoji-menu { position: absolute; + top: 0; margin-top: 3px; padding: $gl-padding; z-index: 9; @@ -20,7 +21,7 @@ opacity: 0; transform: scale(.2); transform-origin: 0 -45px; - transition: .3s cubic-bezier(.87,-.41,.19,1.44); + transition: .3s cubic-bezier(.67,.06,.19,1.44); transition-property: transform, opacity; &.is-aligned-right { @@ -47,12 +48,13 @@ } .emoji-menu-list { - list-style: none; - padding-left: 0; margin-bottom: 0; + padding-left: 0; + list-style: none; } .emoji-menu-list-item { + float: left; padding: 3px; margin-left: 1px; margin-right: 1px; @@ -97,6 +99,8 @@ padding: 5px 6px; outline: 0; + line-height: 1; + &.disabled { cursor: default; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 2a403589a53..887ab481de4 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -221,6 +221,7 @@ color: $gl-text-color-secondary; font-size: 13px; line-height: 22px; + text-transform: capitalize; padding: 0 10px; } diff --git a/app/assets/stylesheets/framework/emoji-sprites.scss b/app/assets/stylesheets/framework/emoji-sprites.scss new file mode 100644 index 00000000000..925415f84b1 --- /dev/null +++ b/app/assets/stylesheets/framework/emoji-sprites.scss @@ -0,0 +1,1811 @@ +.emoji-zzz { background-position: 0 0; } +.emoji-1234 { background-position: -20px 0; } +.emoji-1F627 { background-position: 0 -20px; } +.emoji-8ball { background-position: -20px -20px; } +.emoji-a { background-position: -40px 0; } +.emoji-ab { background-position: -40px -20px; } +.emoji-abc { background-position: 0 -40px; } +.emoji-abcd { background-position: -20px -40px; } +.emoji-accept { background-position: -40px -40px; } +.emoji-aerial_tramway { background-position: -60px 0; } +.emoji-airplane { background-position: -60px -20px; } +.emoji-airplane_arriving { background-position: -60px -40px; } +.emoji-airplane_departure { background-position: 0 -60px; } +.emoji-airplane_small { background-position: -20px -60px; } +.emoji-alarm_clock { background-position: -40px -60px; } +.emoji-alembic { background-position: -60px -60px; } +.emoji-alien { background-position: -80px 0; } +.emoji-ambulance { background-position: -80px -20px; } +.emoji-amphora { background-position: -80px -40px; } +.emoji-anchor { background-position: -80px -60px; } +.emoji-angel { background-position: 0 -80px; } +.emoji-angel_tone1 { background-position: -20px -80px; } +.emoji-angel_tone2 { background-position: -40px -80px; } +.emoji-angel_tone3 { background-position: -60px -80px; } +.emoji-angel_tone4 { background-position: -80px -80px; } +.emoji-angel_tone5 { background-position: -100px 0; } +.emoji-anger { background-position: -100px -20px; } +.emoji-anger_right { background-position: -100px -40px; } +.emoji-angry { background-position: -100px -60px; } +.emoji-ant { background-position: -100px -80px; } +.emoji-apple { background-position: 0 -100px; } +.emoji-aquarius { background-position: -20px -100px; } +.emoji-aries { background-position: -40px -100px; } +.emoji-arrow_backward { background-position: -60px -100px; } +.emoji-arrow_double_down { background-position: -80px -100px; } +.emoji-arrow_double_up { background-position: -100px -100px; } +.emoji-arrow_down { background-position: -120px 0; } +.emoji-arrow_down_small { background-position: -120px -20px; } +.emoji-arrow_forward { background-position: -120px -40px; } +.emoji-arrow_heading_down { background-position: -120px -60px; } +.emoji-arrow_heading_up { background-position: -120px -80px; } +.emoji-arrow_left { background-position: -120px -100px; } +.emoji-arrow_lower_left { background-position: 0 -120px; } +.emoji-arrow_lower_right { background-position: -20px -120px; } +.emoji-arrow_right { background-position: -40px -120px; } +.emoji-arrow_right_hook { background-position: -60px -120px; } +.emoji-arrow_up { background-position: -80px -120px; } +.emoji-arrow_up_down { background-position: -100px -120px; } +.emoji-arrow_up_small { background-position: -120px -120px; } +.emoji-arrow_upper_left { background-position: -140px 0; } +.emoji-arrow_upper_right { background-position: -140px -20px; } +.emoji-arrows_clockwise { background-position: -140px -40px; } +.emoji-arrows_counterclockwise { background-position: -140px -60px; } +.emoji-art { background-position: -140px -80px; } +.emoji-articulated_lorry { background-position: -140px -100px; } +.emoji-asterisk { background-position: -140px -120px; } +.emoji-astonished { background-position: 0 -140px; } +.emoji-athletic_shoe { background-position: -20px -140px; } +.emoji-atm { background-position: -40px -140px; } +.emoji-atom { background-position: -60px -140px; } +.emoji-avocado { background-position: -80px -140px; } +.emoji-b { background-position: -100px -140px; } +.emoji-baby { background-position: -120px -140px; } +.emoji-baby_bottle { background-position: -140px -140px; } +.emoji-baby_chick { background-position: -160px 0; } +.emoji-baby_symbol { background-position: -160px -20px; } +.emoji-baby_tone1 { background-position: -160px -40px; } +.emoji-baby_tone2 { background-position: -160px -60px; } +.emoji-baby_tone3 { background-position: -160px -80px; } +.emoji-baby_tone4 { background-position: -160px -100px; } +.emoji-baby_tone5 { background-position: -160px -120px; } +.emoji-back { background-position: -160px -140px; } +.emoji-bacon { background-position: 0 -160px; } +.emoji-badminton { background-position: -20px -160px; } +.emoji-baggage_claim { background-position: -40px -160px; } +.emoji-balloon { background-position: -60px -160px; } +.emoji-ballot_box { background-position: -80px -160px; } +.emoji-ballot_box_with_check { background-position: -100px -160px; } +.emoji-bamboo { background-position: -120px -160px; } +.emoji-banana { background-position: -140px -160px; } +.emoji-bangbang { background-position: -160px -160px; } +.emoji-bank { background-position: -180px 0; } +.emoji-bar_chart { background-position: -180px -20px; } +.emoji-barber { background-position: -180px -40px; } +.emoji-baseball { background-position: -180px -60px; } +.emoji-basketball { background-position: -180px -80px; } +.emoji-basketball_player { background-position: -180px -100px; } +.emoji-basketball_player_tone1 { background-position: -180px -120px; } +.emoji-basketball_player_tone2 { background-position: -180px -140px; } +.emoji-basketball_player_tone3 { background-position: -180px -160px; } +.emoji-basketball_player_tone4 { background-position: 0 -180px; } +.emoji-basketball_player_tone5 { background-position: -20px -180px; } +.emoji-bat { background-position: -40px -180px; } +.emoji-bath { background-position: -60px -180px; } +.emoji-bath_tone1 { background-position: -80px -180px; } +.emoji-bath_tone2 { background-position: -100px -180px; } +.emoji-bath_tone3 { background-position: -120px -180px; } +.emoji-bath_tone4 { background-position: -140px -180px; } +.emoji-bath_tone5 { background-position: -160px -180px; } +.emoji-bathtub { background-position: -180px -180px; } +.emoji-battery { background-position: -200px 0; } +.emoji-beach { background-position: -200px -20px; } +.emoji-beach_umbrella { background-position: -200px -40px; } +.emoji-bear { background-position: -200px -60px; } +.emoji-bed { background-position: -200px -80px; } +.emoji-bee { background-position: -200px -100px; } +.emoji-beer { background-position: -200px -120px; } +.emoji-beers { background-position: -200px -140px; } +.emoji-beetle { background-position: -200px -160px; } +.emoji-beginner { background-position: -200px -180px; } +.emoji-bell { background-position: 0 -200px; } +.emoji-bellhop { background-position: -20px -200px; } +.emoji-bento { background-position: -40px -200px; } +.emoji-bicyclist { background-position: -60px -200px; } +.emoji-bicyclist_tone1 { background-position: -80px -200px; } +.emoji-bicyclist_tone2 { background-position: -100px -200px; } +.emoji-bicyclist_tone3 { background-position: -120px -200px; } +.emoji-bicyclist_tone4 { background-position: -140px -200px; } +.emoji-bicyclist_tone5 { background-position: -160px -200px; } +.emoji-bike { background-position: -180px -200px; } +.emoji-bikini { background-position: -200px -200px; } +.emoji-biohazard { background-position: -220px 0; } +.emoji-bird { background-position: -220px -20px; } +.emoji-birthday { background-position: -220px -40px; } +.emoji-black_circle { background-position: -220px -60px; } +.emoji-black_heart { background-position: -220px -80px; } +.emoji-black_joker { background-position: -220px -100px; } +.emoji-black_large_square { background-position: -220px -120px; } +.emoji-black_medium_small_square { background-position: -220px -140px; } +.emoji-black_medium_square { background-position: -220px -160px; } +.emoji-black_nib { background-position: -220px -180px; } +.emoji-black_small_square { background-position: -220px -200px; } +.emoji-black_square_button { background-position: 0 -220px; } +.emoji-blossom { background-position: -20px -220px; } +.emoji-blowfish { background-position: -40px -220px; } +.emoji-blue_book { background-position: -60px -220px; } +.emoji-blue_car { background-position: -80px -220px; } +.emoji-blue_heart { background-position: -100px -220px; } +.emoji-blush { background-position: -120px -220px; } +.emoji-boar { background-position: -140px -220px; } +.emoji-bomb { background-position: -160px -220px; } +.emoji-book { background-position: -180px -220px; } +.emoji-bookmark { background-position: -200px -220px; } +.emoji-bookmark_tabs { background-position: -220px -220px; } +.emoji-books { background-position: -240px 0; } +.emoji-boom { background-position: -240px -20px; } +.emoji-boot { background-position: -240px -40px; } +.emoji-bouquet { background-position: -240px -60px; } +.emoji-bow { background-position: -240px -80px; } +.emoji-bow_and_arrow { background-position: -240px -100px; } +.emoji-bow_tone1 { background-position: -240px -120px; } +.emoji-bow_tone2 { background-position: -240px -140px; } +.emoji-bow_tone3 { background-position: -240px -160px; } +.emoji-bow_tone4 { background-position: -240px -180px; } +.emoji-bow_tone5 { background-position: -240px -200px; } +.emoji-bowling { background-position: -240px -220px; } +.emoji-boxing_glove { background-position: 0 -240px; } +.emoji-boy { background-position: -20px -240px; } +.emoji-boy_tone1 { background-position: -40px -240px; } +.emoji-boy_tone2 { background-position: -60px -240px; } +.emoji-boy_tone3 { background-position: -80px -240px; } +.emoji-boy_tone4 { background-position: -100px -240px; } +.emoji-boy_tone5 { background-position: -120px -240px; } +.emoji-bread { background-position: -140px -240px; } +.emoji-bride_with_veil { background-position: -160px -240px; } +.emoji-bride_with_veil_tone1 { background-position: -180px -240px; } +.emoji-bride_with_veil_tone2 { background-position: -200px -240px; } +.emoji-bride_with_veil_tone3 { background-position: -220px -240px; } +.emoji-bride_with_veil_tone4 { background-position: -240px -240px; } +.emoji-bride_with_veil_tone5 { background-position: -260px 0; } +.emoji-bridge_at_night { background-position: -260px -20px; } +.emoji-briefcase { background-position: -260px -40px; } +.emoji-broken_heart { background-position: -260px -60px; } +.emoji-bug { background-position: -260px -80px; } +.emoji-bulb { background-position: -260px -100px; } +.emoji-bullettrain_front { background-position: -260px -120px; } +.emoji-bullettrain_side { background-position: -260px -140px; } +.emoji-burrito { background-position: -260px -160px; } +.emoji-bus { background-position: -260px -180px; } +.emoji-busstop { background-position: -260px -200px; } +.emoji-bust_in_silhouette { background-position: -260px -220px; } +.emoji-busts_in_silhouette { background-position: -260px -240px; } +.emoji-butterfly { background-position: 0 -260px; } +.emoji-cactus { background-position: -20px -260px; } +.emoji-cake { background-position: -40px -260px; } +.emoji-calendar { background-position: -60px -260px; } +.emoji-calendar_spiral { background-position: -80px -260px; } +.emoji-call_me { background-position: -100px -260px; } +.emoji-call_me_tone1 { background-position: -120px -260px; } +.emoji-call_me_tone2 { background-position: -140px -260px; } +.emoji-call_me_tone3 { background-position: -160px -260px; } +.emoji-call_me_tone4 { background-position: -180px -260px; } +.emoji-call_me_tone5 { background-position: -200px -260px; } +.emoji-calling { background-position: -220px -260px; } +.emoji-camel { background-position: -240px -260px; } +.emoji-camera { background-position: -260px -260px; } +.emoji-camera_with_flash { background-position: -280px 0; } +.emoji-camping { background-position: -280px -20px; } +.emoji-cancer { background-position: -280px -40px; } +.emoji-candle { background-position: -280px -60px; } +.emoji-candy { background-position: -280px -80px; } +.emoji-canoe { background-position: -280px -100px; } +.emoji-capital_abcd { background-position: -280px -120px; } +.emoji-capricorn { background-position: -280px -140px; } +.emoji-card_box { background-position: -280px -160px; } +.emoji-card_index { background-position: -280px -180px; } +.emoji-carousel_horse { background-position: -280px -200px; } +.emoji-carrot { background-position: -280px -220px; } +.emoji-cartwheel { background-position: -280px -240px; } +.emoji-cartwheel_tone1 { background-position: -280px -260px; } +.emoji-cartwheel_tone2 { background-position: 0 -280px; } +.emoji-cartwheel_tone3 { background-position: -20px -280px; } +.emoji-cartwheel_tone4 { background-position: -40px -280px; } +.emoji-cartwheel_tone5 { background-position: -60px -280px; } +.emoji-cat { background-position: -80px -280px; } +.emoji-cat2 { background-position: -100px -280px; } +.emoji-cd { background-position: -120px -280px; } +.emoji-chains { background-position: -140px -280px; } +.emoji-champagne { background-position: -160px -280px; } +.emoji-champagne_glass { background-position: -180px -280px; } +.emoji-chart { background-position: -200px -280px; } +.emoji-chart_with_downwards_trend { background-position: -220px -280px; } +.emoji-chart_with_upwards_trend { background-position: -240px -280px; } +.emoji-checkered_flag { background-position: -260px -280px; } +.emoji-cheese { background-position: -280px -280px; } +.emoji-cherries { background-position: -300px 0; } +.emoji-cherry_blossom { background-position: -300px -20px; } +.emoji-chestnut { background-position: -300px -40px; } +.emoji-chicken { background-position: -300px -60px; } +.emoji-children_crossing { background-position: -300px -80px; } +.emoji-chipmunk { background-position: -300px -100px; } +.emoji-chocolate_bar { background-position: -300px -120px; } +.emoji-christmas_tree { background-position: -300px -140px; } +.emoji-church { background-position: -300px -160px; } +.emoji-cinema { background-position: -300px -180px; } +.emoji-circus_tent { background-position: -300px -200px; } +.emoji-city_dusk { background-position: -300px -220px; } +.emoji-city_sunset { background-position: -300px -240px; } +.emoji-cityscape { background-position: -300px -260px; } +.emoji-cl { background-position: -300px -280px; } +.emoji-clap { background-position: 0 -300px; } +.emoji-clap_tone1 { background-position: -20px -300px; } +.emoji-clap_tone2 { background-position: -40px -300px; } +.emoji-clap_tone3 { background-position: -60px -300px; } +.emoji-clap_tone4 { background-position: -80px -300px; } +.emoji-clap_tone5 { background-position: -100px -300px; } +.emoji-clapper { background-position: -120px -300px; } +.emoji-classical_building { background-position: -140px -300px; } +.emoji-clipboard { background-position: -160px -300px; } +.emoji-clock { background-position: -180px -300px; } +.emoji-clock1 { background-position: -200px -300px; } +.emoji-clock10 { background-position: -220px -300px; } +.emoji-clock1030 { background-position: -240px -300px; } +.emoji-clock11 { background-position: -260px -300px; } +.emoji-clock1130 { background-position: -280px -300px; } +.emoji-clock12 { background-position: -300px -300px; } +.emoji-clock1230 { background-position: -320px 0; } +.emoji-clock130 { background-position: -320px -20px; } +.emoji-clock2 { background-position: -320px -40px; } +.emoji-clock230 { background-position: -320px -60px; } +.emoji-clock3 { background-position: -320px -80px; } +.emoji-clock330 { background-position: -320px -100px; } +.emoji-clock4 { background-position: -320px -120px; } +.emoji-clock430 { background-position: -320px -140px; } +.emoji-clock5 { background-position: -320px -160px; } +.emoji-clock530 { background-position: -320px -180px; } +.emoji-clock6 { background-position: -320px -200px; } +.emoji-clock630 { background-position: -320px -220px; } +.emoji-clock7 { background-position: -320px -240px; } +.emoji-clock730 { background-position: -320px -260px; } +.emoji-clock8 { background-position: -320px -280px; } +.emoji-clock830 { background-position: -320px -300px; } +.emoji-clock9 { background-position: 0 -320px; } +.emoji-clock930 { background-position: -20px -320px; } +.emoji-closed_book { background-position: -40px -320px; } +.emoji-closed_lock_with_key { background-position: -60px -320px; } +.emoji-closed_umbrella { background-position: -80px -320px; } +.emoji-cloud { background-position: -100px -320px; } +.emoji-cloud_lightning { background-position: -120px -320px; } +.emoji-cloud_rain { background-position: -140px -320px; } +.emoji-cloud_snow { background-position: -160px -320px; } +.emoji-cloud_tornado { background-position: -180px -320px; } +.emoji-clown { background-position: -200px -320px; } +.emoji-clubs { background-position: -220px -320px; } +.emoji-cocktail { background-position: -240px -320px; } +.emoji-coffee { background-position: -260px -320px; } +.emoji-coffin { background-position: -280px -320px; } +.emoji-cold_sweat { background-position: -300px -320px; } +.emoji-comet { background-position: -320px -320px; } +.emoji-compression { background-position: -340px 0; } +.emoji-computer { background-position: -340px -20px; } +.emoji-confetti_ball { background-position: -340px -40px; } +.emoji-confounded { background-position: -340px -60px; } +.emoji-confused { background-position: -340px -80px; } +.emoji-congratulations { background-position: -340px -100px; } +.emoji-construction { background-position: -340px -120px; } +.emoji-construction_site { background-position: -340px -140px; } +.emoji-construction_worker { background-position: -340px -160px; } +.emoji-construction_worker_tone1 { background-position: -340px -180px; } +.emoji-construction_worker_tone2 { background-position: -340px -200px; } +.emoji-construction_worker_tone3 { background-position: -340px -220px; } +.emoji-construction_worker_tone4 { background-position: -340px -240px; } +.emoji-construction_worker_tone5 { background-position: -340px -260px; } +.emoji-control_knobs { background-position: -340px -280px; } +.emoji-convenience_store { background-position: -340px -300px; } +.emoji-cookie { background-position: -340px -320px; } +.emoji-cooking { background-position: 0 -340px; } +.emoji-cool { background-position: -20px -340px; } +.emoji-cop { background-position: -40px -340px; } +.emoji-cop_tone1 { background-position: -60px -340px; } +.emoji-cop_tone2 { background-position: -80px -340px; } +.emoji-cop_tone3 { background-position: -100px -340px; } +.emoji-cop_tone4 { background-position: -120px -340px; } +.emoji-cop_tone5 { background-position: -140px -340px; } +.emoji-copyright { background-position: -160px -340px; } +.emoji-corn { background-position: -180px -340px; } +.emoji-couch { background-position: -200px -340px; } +.emoji-couple { background-position: -220px -340px; } +.emoji-couple_mm { background-position: -240px -340px; } +.emoji-couple_with_heart { background-position: -260px -340px; } +.emoji-couple_ww { background-position: -280px -340px; } +.emoji-couplekiss { background-position: -300px -340px; } +.emoji-cow { background-position: -320px -340px; } +.emoji-cow2 { background-position: -340px -340px; } +.emoji-cowboy { background-position: -360px 0; } +.emoji-crab { background-position: -360px -20px; } +.emoji-crayon { background-position: -360px -40px; } +.emoji-credit_card { background-position: -360px -60px; } +.emoji-crescent_moon { background-position: -360px -80px; } +.emoji-cricket { background-position: -360px -100px; } +.emoji-crocodile { background-position: -360px -120px; } +.emoji-croissant { background-position: -360px -140px; } +.emoji-cross { background-position: -360px -160px; } +.emoji-crossed_flags { background-position: -360px -180px; } +.emoji-crossed_swords { background-position: -360px -200px; } +.emoji-crown { background-position: -360px -220px; } +.emoji-cruise_ship { background-position: -360px -240px; } +.emoji-cry { background-position: -360px -260px; } +.emoji-crying_cat_face { background-position: -360px -280px; } +.emoji-crystal_ball { background-position: -360px -300px; } +.emoji-cucumber { background-position: -360px -320px; } +.emoji-cupid { background-position: -360px -340px; } +.emoji-curly_loop { background-position: 0 -360px; } +.emoji-currency_exchange { background-position: -20px -360px; } +.emoji-curry { background-position: -40px -360px; } +.emoji-custard { background-position: -60px -360px; } +.emoji-customs { background-position: -80px -360px; } +.emoji-cyclone { background-position: -100px -360px; } +.emoji-dagger { background-position: -120px -360px; } +.emoji-dancer { background-position: -140px -360px; } +.emoji-dancer_tone1 { background-position: -160px -360px; } +.emoji-dancer_tone2 { background-position: -180px -360px; } +.emoji-dancer_tone3 { background-position: -200px -360px; } +.emoji-dancer_tone4 { background-position: -220px -360px; } +.emoji-dancer_tone5 { background-position: -240px -360px; } +.emoji-dancers { background-position: -260px -360px; } +.emoji-dango { background-position: -280px -360px; } +.emoji-dark_sunglasses { background-position: -300px -360px; } +.emoji-dart { background-position: -320px -360px; } +.emoji-dash { background-position: -340px -360px; } +.emoji-date { background-position: -360px -360px; } +.emoji-deciduous_tree { background-position: -380px 0; } +.emoji-deer { background-position: -380px -20px; } +.emoji-department_store { background-position: -380px -40px; } +.emoji-desert { background-position: -380px -60px; } +.emoji-desktop { background-position: -380px -80px; } +.emoji-diamond_shape_with_a_dot_inside { background-position: -380px -100px; } +.emoji-diamonds { background-position: -380px -120px; } +.emoji-disappointed { background-position: -380px -140px; } +.emoji-disappointed_relieved { background-position: -380px -160px; } +.emoji-dividers { background-position: -380px -180px; } +.emoji-dizzy { background-position: -380px -200px; } +.emoji-dizzy_face { background-position: -380px -220px; } +.emoji-do_not_litter { background-position: -380px -240px; } +.emoji-dog { background-position: -380px -260px; } +.emoji-dog2 { background-position: -380px -280px; } +.emoji-dollar { background-position: -380px -300px; } +.emoji-dolls { background-position: -380px -320px; } +.emoji-dolphin { background-position: -380px -340px; } +.emoji-door { background-position: -380px -360px; } +.emoji-doughnut { background-position: 0 -380px; } +.emoji-dove { background-position: -20px -380px; } +.emoji-dragon { background-position: -40px -380px; } +.emoji-dragon_face { background-position: -60px -380px; } +.emoji-dress { background-position: -80px -380px; } +.emoji-dromedary_camel { background-position: -100px -380px; } +.emoji-drooling_face { background-position: -120px -380px; } +.emoji-droplet { background-position: -140px -380px; } +.emoji-drum { background-position: -160px -380px; } +.emoji-duck { background-position: -180px -380px; } +.emoji-dvd { background-position: -200px -380px; } +.emoji-e-mail { background-position: -220px -380px; } +.emoji-eagle { background-position: -240px -380px; } +.emoji-ear { background-position: -260px -380px; } +.emoji-ear_of_rice { background-position: -280px -380px; } +.emoji-ear_tone1 { background-position: -300px -380px; } +.emoji-ear_tone2 { background-position: -320px -380px; } +.emoji-ear_tone3 { background-position: -340px -380px; } +.emoji-ear_tone4 { background-position: -360px -380px; } +.emoji-ear_tone5 { background-position: -380px -380px; } +.emoji-earth_africa { background-position: -400px 0; } +.emoji-earth_americas { background-position: -400px -20px; } +.emoji-earth_asia { background-position: -400px -40px; } +.emoji-egg { background-position: -400px -60px; } +.emoji-eggplant { background-position: -400px -80px; } +.emoji-eight { background-position: -400px -100px; } +.emoji-eight_pointed_black_star { background-position: -400px -120px; } +.emoji-eight_spoked_asterisk { background-position: -400px -140px; } +.emoji-eject { background-position: -400px -160px; } +.emoji-electric_plug { background-position: -400px -180px; } +.emoji-elephant { background-position: -400px -200px; } +.emoji-end { background-position: -400px -220px; } +.emoji-envelope { background-position: -400px -240px; } +.emoji-envelope_with_arrow { background-position: -400px -260px; } +.emoji-euro { background-position: -400px -280px; } +.emoji-european_castle { background-position: -400px -300px; } +.emoji-european_post_office { background-position: -400px -320px; } +.emoji-evergreen_tree { background-position: -400px -340px; } +.emoji-exclamation { background-position: -400px -360px; } +.emoji-expressionless { background-position: -400px -380px; } +.emoji-eye { background-position: 0 -400px; } +.emoji-eye_in_speech_bubble { background-position: -20px -400px; } +.emoji-eyeglasses { background-position: -40px -400px; } +.emoji-eyes { background-position: -60px -400px; } +.emoji-face_palm { background-position: -80px -400px; } +.emoji-face_palm_tone1 { background-position: -100px -400px; } +.emoji-face_palm_tone2 { background-position: -120px -400px; } +.emoji-face_palm_tone3 { background-position: -140px -400px; } +.emoji-face_palm_tone4 { background-position: -160px -400px; } +.emoji-face_palm_tone5 { background-position: -180px -400px; } +.emoji-factory { background-position: -200px -400px; } +.emoji-fallen_leaf { background-position: -220px -400px; } +.emoji-family { background-position: -240px -400px; } +.emoji-family_mmb { background-position: -260px -400px; } +.emoji-family_mmbb { background-position: -280px -400px; } +.emoji-family_mmg { background-position: -300px -400px; } +.emoji-family_mmgb { background-position: -320px -400px; } +.emoji-family_mmgg { background-position: -340px -400px; } +.emoji-family_mwbb { background-position: -360px -400px; } +.emoji-family_mwg { background-position: -380px -400px; } +.emoji-family_mwgb { background-position: -400px -400px; } +.emoji-family_mwgg { background-position: -420px 0; } +.emoji-family_wwb { background-position: -420px -20px; } +.emoji-family_wwbb { background-position: -420px -40px; } +.emoji-family_wwg { background-position: -420px -60px; } +.emoji-family_wwgb { background-position: -420px -80px; } +.emoji-family_wwgg { background-position: -420px -100px; } +.emoji-fast_forward { background-position: -420px -120px; } +.emoji-fax { background-position: -420px -140px; } +.emoji-fearful { background-position: -420px -160px; } +.emoji-feet { background-position: -420px -180px; } +.emoji-fencer { background-position: -420px -200px; } +.emoji-ferris_wheel { background-position: -420px -220px; } +.emoji-ferry { background-position: -420px -240px; } +.emoji-field_hockey { background-position: -420px -260px; } +.emoji-file_cabinet { background-position: -420px -280px; } +.emoji-file_folder { background-position: -420px -300px; } +.emoji-film_frames { background-position: -420px -320px; } +.emoji-fingers_crossed { background-position: -420px -340px; } +.emoji-fingers_crossed_tone1 { background-position: -420px -360px; } +.emoji-fingers_crossed_tone2 { background-position: -420px -380px; } +.emoji-fingers_crossed_tone3 { background-position: -420px -400px; } +.emoji-fingers_crossed_tone4 { background-position: 0 -420px; } +.emoji-fingers_crossed_tone5 { background-position: -20px -420px; } +.emoji-fire { background-position: -40px -420px; } +.emoji-fire_engine { background-position: -60px -420px; } +.emoji-fireworks { background-position: -80px -420px; } +.emoji-first_place { background-position: -100px -420px; } +.emoji-first_quarter_moon { background-position: -120px -420px; } +.emoji-first_quarter_moon_with_face { background-position: -140px -420px; } +.emoji-fish { background-position: -160px -420px; } +.emoji-fish_cake { background-position: -180px -420px; } +.emoji-fishing_pole_and_fish { background-position: -200px -420px; } +.emoji-fist { background-position: -220px -420px; } +.emoji-fist_tone1 { background-position: -240px -420px; } +.emoji-fist_tone2 { background-position: -260px -420px; } +.emoji-fist_tone3 { background-position: -280px -420px; } +.emoji-fist_tone4 { background-position: -300px -420px; } +.emoji-fist_tone5 { background-position: -320px -420px; } +.emoji-five { background-position: -340px -420px; } +.emoji-flag_ac { background-position: -360px -420px; } +.emoji-flag_ad { background-position: -380px -420px; } +.emoji-flag_ae { background-position: -400px -420px; } +.emoji-flag_af { background-position: -420px -420px; } +.emoji-flag_ag { background-position: -440px 0; } +.emoji-flag_ai { background-position: -440px -20px; } +.emoji-flag_al { background-position: -440px -40px; } +.emoji-flag_am { background-position: -440px -60px; } +.emoji-flag_ao { background-position: -440px -80px; } +.emoji-flag_aq { background-position: -440px -100px; } +.emoji-flag_ar { background-position: -440px -120px; } +.emoji-flag_as { background-position: -440px -140px; } +.emoji-flag_at { background-position: -440px -160px; } +.emoji-flag_au { background-position: -440px -180px; } +.emoji-flag_aw { background-position: -440px -200px; } +.emoji-flag_ax { background-position: -440px -220px; } +.emoji-flag_az { background-position: -440px -240px; } +.emoji-flag_ba { background-position: -440px -260px; } +.emoji-flag_bb { background-position: -440px -280px; } +.emoji-flag_bd { background-position: -440px -300px; } +.emoji-flag_be { background-position: -440px -320px; } +.emoji-flag_bf { background-position: -440px -340px; } +.emoji-flag_bg { background-position: -440px -360px; } +.emoji-flag_bh { background-position: -440px -380px; } +.emoji-flag_bi { background-position: -440px -400px; } +.emoji-flag_bj { background-position: -440px -420px; } +.emoji-flag_bl { background-position: 0 -440px; } +.emoji-flag_black { background-position: -20px -440px; } +.emoji-flag_bm { background-position: -40px -440px; } +.emoji-flag_bn { background-position: -60px -440px; } +.emoji-flag_bo { background-position: -80px -440px; } +.emoji-flag_bq { background-position: -100px -440px; } +.emoji-flag_br { background-position: -120px -440px; } +.emoji-flag_bs { background-position: -140px -440px; } +.emoji-flag_bt { background-position: -160px -440px; } +.emoji-flag_bv { background-position: -180px -440px; } +.emoji-flag_bw { background-position: -200px -440px; } +.emoji-flag_by { background-position: -220px -440px; } +.emoji-flag_bz { background-position: -240px -440px; } +.emoji-flag_ca { background-position: -260px -440px; } +.emoji-flag_cc { background-position: -280px -440px; } +.emoji-flag_cd { background-position: -300px -440px; } +.emoji-flag_cf { background-position: -320px -440px; } +.emoji-flag_cg { background-position: -340px -440px; } +.emoji-flag_ch { background-position: -360px -440px; } +.emoji-flag_ci { background-position: -380px -440px; } +.emoji-flag_ck { background-position: -400px -440px; } +.emoji-flag_cl { background-position: -420px -440px; } +.emoji-flag_cm { background-position: -440px -440px; } +.emoji-flag_cn { background-position: -460px 0; } +.emoji-flag_co { background-position: -460px -20px; } +.emoji-flag_cp { background-position: -460px -40px; } +.emoji-flag_cr { background-position: -460px -60px; } +.emoji-flag_cu { background-position: -460px -80px; } +.emoji-flag_cv { background-position: -460px -100px; } +.emoji-flag_cw { background-position: -460px -120px; } +.emoji-flag_cx { background-position: -460px -140px; } +.emoji-flag_cy { background-position: -460px -160px; } +.emoji-flag_cz { background-position: -460px -180px; } +.emoji-flag_de { background-position: -460px -200px; } +.emoji-flag_dg { background-position: -460px -220px; } +.emoji-flag_dj { background-position: -460px -240px; } +.emoji-flag_dk { background-position: -460px -260px; } +.emoji-flag_dm { background-position: -460px -280px; } +.emoji-flag_do { background-position: -460px -300px; } +.emoji-flag_dz { background-position: -460px -320px; } +.emoji-flag_ea { background-position: -460px -340px; } +.emoji-flag_ec { background-position: -460px -360px; } +.emoji-flag_ee { background-position: -460px -380px; } +.emoji-flag_eg { background-position: -460px -400px; } +.emoji-flag_eh { background-position: -460px -420px; } +.emoji-flag_er { background-position: -460px -440px; } +.emoji-flag_es { background-position: 0 -460px; } +.emoji-flag_et { background-position: -20px -460px; } +.emoji-flag_eu { background-position: -40px -460px; } +.emoji-flag_fi { background-position: -60px -460px; } +.emoji-flag_fj { background-position: -80px -460px; } +.emoji-flag_fk { background-position: -100px -460px; } +.emoji-flag_fm { background-position: -120px -460px; } +.emoji-flag_fo { background-position: -140px -460px; } +.emoji-flag_fr { background-position: -160px -460px; } +.emoji-flag_ga { background-position: -180px -460px; } +.emoji-flag_gb { background-position: -200px -460px; } +.emoji-flag_gd { background-position: -220px -460px; } +.emoji-flag_ge { background-position: -240px -460px; } +.emoji-flag_gf { background-position: -260px -460px; } +.emoji-flag_gg { background-position: -280px -460px; } +.emoji-flag_gh { background-position: -300px -460px; } +.emoji-flag_gi { background-position: -320px -460px; } +.emoji-flag_gl { background-position: -340px -460px; } +.emoji-flag_gm { background-position: -360px -460px; } +.emoji-flag_gn { background-position: -380px -460px; } +.emoji-flag_gp { background-position: -400px -460px; } +.emoji-flag_gq { background-position: -420px -460px; } +.emoji-flag_gr { background-position: -440px -460px; } +.emoji-flag_gs { background-position: -460px -460px; } +.emoji-flag_gt { background-position: -480px 0; } +.emoji-flag_gu { background-position: -480px -20px; } +.emoji-flag_gw { background-position: -480px -40px; } +.emoji-flag_gy { background-position: -480px -60px; } +.emoji-flag_hk { background-position: -480px -80px; } +.emoji-flag_hm { background-position: -480px -100px; } +.emoji-flag_hn { background-position: -480px -120px; } +.emoji-flag_hr { background-position: -480px -140px; } +.emoji-flag_ht { background-position: -480px -160px; } +.emoji-flag_hu { background-position: -480px -180px; } +.emoji-flag_ic { background-position: -480px -200px; } +.emoji-flag_id { background-position: -480px -220px; } +.emoji-flag_ie { background-position: -480px -240px; } +.emoji-flag_il { background-position: -480px -260px; } +.emoji-flag_im { background-position: -480px -280px; } +.emoji-flag_in { background-position: -480px -300px; } +.emoji-flag_io { background-position: -480px -320px; } +.emoji-flag_iq { background-position: -480px -340px; } +.emoji-flag_ir { background-position: -480px -360px; } +.emoji-flag_is { background-position: -480px -380px; } +.emoji-flag_it { background-position: -480px -400px; } +.emoji-flag_je { background-position: -480px -420px; } +.emoji-flag_jm { background-position: -480px -440px; } +.emoji-flag_jo { background-position: -480px -460px; } +.emoji-flag_jp { background-position: 0 -480px; } +.emoji-flag_ke { background-position: -20px -480px; } +.emoji-flag_kg { background-position: -40px -480px; } +.emoji-flag_kh { background-position: -60px -480px; } +.emoji-flag_ki { background-position: -80px -480px; } +.emoji-flag_km { background-position: -100px -480px; } +.emoji-flag_kn { background-position: -120px -480px; } +.emoji-flag_kp { background-position: -140px -480px; } +.emoji-flag_kr { background-position: -160px -480px; } +.emoji-flag_kw { background-position: -180px -480px; } +.emoji-flag_ky { background-position: -200px -480px; } +.emoji-flag_kz { background-position: -220px -480px; } +.emoji-flag_la { background-position: -240px -480px; } +.emoji-flag_lb { background-position: -260px -480px; } +.emoji-flag_lc { background-position: -280px -480px; } +.emoji-flag_li { background-position: -300px -480px; } +.emoji-flag_lk { background-position: -320px -480px; } +.emoji-flag_lr { background-position: -340px -480px; } +.emoji-flag_ls { background-position: -360px -480px; } +.emoji-flag_lt { background-position: -380px -480px; } +.emoji-flag_lu { background-position: -400px -480px; } +.emoji-flag_lv { background-position: -420px -480px; } +.emoji-flag_ly { background-position: -440px -480px; } +.emoji-flag_ma { background-position: -460px -480px; } +.emoji-flag_mc { background-position: -480px -480px; } +.emoji-flag_md { background-position: -500px 0; } +.emoji-flag_me { background-position: -500px -20px; } +.emoji-flag_mf { background-position: -500px -40px; } +.emoji-flag_mg { background-position: -500px -60px; } +.emoji-flag_mh { background-position: -500px -80px; } +.emoji-flag_mk { background-position: -500px -100px; } +.emoji-flag_ml { background-position: -500px -120px; } +.emoji-flag_mm { background-position: -500px -140px; } +.emoji-flag_mn { background-position: -500px -160px; } +.emoji-flag_mo { background-position: -500px -180px; } +.emoji-flag_mp { background-position: -500px -200px; } +.emoji-flag_mq { background-position: -500px -220px; } +.emoji-flag_mr { background-position: -500px -240px; } +.emoji-flag_ms { background-position: -500px -260px; } +.emoji-flag_mt { background-position: -500px -280px; } +.emoji-flag_mu { background-position: -500px -300px; } +.emoji-flag_mv { background-position: -500px -320px; } +.emoji-flag_mw { background-position: -500px -340px; } +.emoji-flag_mx { background-position: -500px -360px; } +.emoji-flag_my { background-position: -500px -380px; } +.emoji-flag_mz { background-position: -500px -400px; } +.emoji-flag_na { background-position: -500px -420px; } +.emoji-flag_nc { background-position: -500px -440px; } +.emoji-flag_ne { background-position: -500px -460px; } +.emoji-flag_nf { background-position: -500px -480px; } +.emoji-flag_ng { background-position: 0 -500px; } +.emoji-flag_ni { background-position: -20px -500px; } +.emoji-flag_nl { background-position: -40px -500px; } +.emoji-flag_no { background-position: -60px -500px; } +.emoji-flag_np { background-position: -80px -500px; } +.emoji-flag_nr { background-position: -100px -500px; } +.emoji-flag_nu { background-position: -120px -500px; } +.emoji-flag_nz { background-position: -140px -500px; } +.emoji-flag_om { background-position: -160px -500px; } +.emoji-flag_pa { background-position: -180px -500px; } +.emoji-flag_pe { background-position: -200px -500px; } +.emoji-flag_pf { background-position: -220px -500px; } +.emoji-flag_pg { background-position: -240px -500px; } +.emoji-flag_ph { background-position: -260px -500px; } +.emoji-flag_pk { background-position: -280px -500px; } +.emoji-flag_pl { background-position: -300px -500px; } +.emoji-flag_pm { background-position: -320px -500px; } +.emoji-flag_pn { background-position: -340px -500px; } +.emoji-flag_pr { background-position: -360px -500px; } +.emoji-flag_ps { background-position: -380px -500px; } +.emoji-flag_pt { background-position: -400px -500px; } +.emoji-flag_pw { background-position: -420px -500px; } +.emoji-flag_py { background-position: -440px -500px; } +.emoji-flag_qa { background-position: -460px -500px; } +.emoji-flag_re { background-position: -480px -500px; } +.emoji-flag_ro { background-position: -500px -500px; } +.emoji-flag_rs { background-position: -520px 0; } +.emoji-flag_ru { background-position: -520px -20px; } +.emoji-flag_rw { background-position: -520px -40px; } +.emoji-flag_sa { background-position: -520px -60px; } +.emoji-flag_sb { background-position: -520px -80px; } +.emoji-flag_sc { background-position: -520px -100px; } +.emoji-flag_sd { background-position: -520px -120px; } +.emoji-flag_se { background-position: -520px -140px; } +.emoji-flag_sg { background-position: -520px -160px; } +.emoji-flag_sh { background-position: -520px -180px; } +.emoji-flag_si { background-position: -520px -200px; } +.emoji-flag_sj { background-position: -520px -220px; } +.emoji-flag_sk { background-position: -520px -240px; } +.emoji-flag_sl { background-position: -520px -260px; } +.emoji-flag_sm { background-position: -520px -280px; } +.emoji-flag_sn { background-position: -520px -300px; } +.emoji-flag_so { background-position: -520px -320px; } +.emoji-flag_sr { background-position: -520px -340px; } +.emoji-flag_ss { background-position: -520px -360px; } +.emoji-flag_st { background-position: -520px -380px; } +.emoji-flag_sv { background-position: -520px -400px; } +.emoji-flag_sx { background-position: -520px -420px; } +.emoji-flag_sy { background-position: -520px -440px; } +.emoji-flag_sz { background-position: -520px -460px; } +.emoji-flag_ta { background-position: -520px -480px; } +.emoji-flag_tc { background-position: -520px -500px; } +.emoji-flag_td { background-position: 0 -520px; } +.emoji-flag_tf { background-position: -20px -520px; } +.emoji-flag_tg { background-position: -40px -520px; } +.emoji-flag_th { background-position: -60px -520px; } +.emoji-flag_tj { background-position: -80px -520px; } +.emoji-flag_tk { background-position: -100px -520px; } +.emoji-flag_tl { background-position: -120px -520px; } +.emoji-flag_tm { background-position: -140px -520px; } +.emoji-flag_tn { background-position: -160px -520px; } +.emoji-flag_to { background-position: -180px -520px; } +.emoji-flag_tr { background-position: -200px -520px; } +.emoji-flag_tt { background-position: -220px -520px; } +.emoji-flag_tv { background-position: -240px -520px; } +.emoji-flag_tw { background-position: -260px -520px; } +.emoji-flag_tz { background-position: -280px -520px; } +.emoji-flag_ua { background-position: -300px -520px; } +.emoji-flag_ug { background-position: -320px -520px; } +.emoji-flag_um { background-position: -340px -520px; } +.emoji-flag_us { background-position: -360px -520px; } +.emoji-flag_uy { background-position: -380px -520px; } +.emoji-flag_uz { background-position: -400px -520px; } +.emoji-flag_va { background-position: -420px -520px; } +.emoji-flag_vc { background-position: -440px -520px; } +.emoji-flag_ve { background-position: -460px -520px; } +.emoji-flag_vg { background-position: -480px -520px; } +.emoji-flag_vi { background-position: -500px -520px; } +.emoji-flag_vn { background-position: -520px -520px; } +.emoji-flag_vu { background-position: -540px 0; } +.emoji-flag_wf { background-position: -540px -20px; } +.emoji-flag_white { background-position: -540px -40px; } +.emoji-flag_ws { background-position: -540px -60px; } +.emoji-flag_xk { background-position: -540px -80px; } +.emoji-flag_ye { background-position: -540px -100px; } +.emoji-flag_yt { background-position: -540px -120px; } +.emoji-flag_za { background-position: -540px -140px; } +.emoji-flag_zm { background-position: -540px -160px; } +.emoji-flag_zw { background-position: -540px -180px; } +.emoji-flags { background-position: -540px -200px; } +.emoji-flashlight { background-position: -540px -220px; } +.emoji-fleur-de-lis { background-position: -540px -240px; } +.emoji-floppy_disk { background-position: -540px -260px; } +.emoji-flower_playing_cards { background-position: -540px -280px; } +.emoji-flushed { background-position: -540px -300px; } +.emoji-fog { background-position: -540px -320px; } +.emoji-foggy { background-position: -540px -340px; } +.emoji-football { background-position: -540px -360px; } +.emoji-footprints { background-position: -540px -380px; } +.emoji-fork_and_knife { background-position: -540px -400px; } +.emoji-fork_knife_plate { background-position: -540px -420px; } +.emoji-fountain { background-position: -540px -440px; } +.emoji-four { background-position: -540px -460px; } +.emoji-four_leaf_clover { background-position: -540px -480px; } +.emoji-fox { background-position: -540px -500px; } +.emoji-frame_photo { background-position: -540px -520px; } +.emoji-free { background-position: 0 -540px; } +.emoji-french_bread { background-position: -20px -540px; } +.emoji-fried_shrimp { background-position: -40px -540px; } +.emoji-fries { background-position: -60px -540px; } +.emoji-frog { background-position: -80px -540px; } +.emoji-frowning { background-position: -100px -540px; } +.emoji-frowning2 { background-position: -120px -540px; } +.emoji-fuelpump { background-position: -140px -540px; } +.emoji-full_moon { background-position: -160px -540px; } +.emoji-full_moon_with_face { background-position: -180px -540px; } +.emoji-game_die { background-position: -200px -540px; } +.emoji-gear { background-position: -220px -540px; } +.emoji-gem { background-position: -240px -540px; } +.emoji-gemini { background-position: -260px -540px; } +.emoji-ghost { background-position: -280px -540px; } +.emoji-gift { background-position: -300px -540px; } +.emoji-gift_heart { background-position: -320px -540px; } +.emoji-girl { background-position: -340px -540px; } +.emoji-girl_tone1 { background-position: -360px -540px; } +.emoji-girl_tone2 { background-position: -380px -540px; } +.emoji-girl_tone3 { background-position: -400px -540px; } +.emoji-girl_tone4 { background-position: -420px -540px; } +.emoji-girl_tone5 { background-position: -440px -540px; } +.emoji-globe_with_meridians { background-position: -460px -540px; } +.emoji-goal { background-position: -480px -540px; } +.emoji-goat { background-position: -500px -540px; } +.emoji-golf { background-position: -520px -540px; } +.emoji-golfer { background-position: -540px -540px; } +.emoji-gorilla { background-position: -560px 0; } +.emoji-grapes { background-position: -560px -20px; } +.emoji-green_apple { background-position: -560px -40px; } +.emoji-green_book { background-position: -560px -60px; } +.emoji-green_heart { background-position: -560px -80px; } +.emoji-grey_exclamation { background-position: -560px -100px; } +.emoji-grey_question { background-position: -560px -120px; } +.emoji-grimacing { background-position: -560px -140px; } +.emoji-grin { background-position: -560px -160px; } +.emoji-grinning { background-position: -560px -180px; } +.emoji-guardsman { background-position: -560px -200px; } +.emoji-guardsman_tone1 { background-position: -560px -220px; } +.emoji-guardsman_tone2 { background-position: -560px -240px; } +.emoji-guardsman_tone3 { background-position: -560px -260px; } +.emoji-guardsman_tone4 { background-position: -560px -280px; } +.emoji-guardsman_tone5 { background-position: -560px -300px; } +.emoji-guitar { background-position: -560px -320px; } +.emoji-gun { background-position: -560px -340px; } +.emoji-haircut { background-position: -560px -360px; } +.emoji-haircut_tone1 { background-position: -560px -380px; } +.emoji-haircut_tone2 { background-position: -560px -400px; } +.emoji-haircut_tone3 { background-position: -560px -420px; } +.emoji-haircut_tone4 { background-position: -560px -440px; } +.emoji-haircut_tone5 { background-position: -560px -460px; } +.emoji-hamburger { background-position: -560px -480px; } +.emoji-hammer { background-position: -560px -500px; } +.emoji-hammer_pick { background-position: -560px -520px; } +.emoji-hamster { background-position: -560px -540px; } +.emoji-hand_splayed { background-position: 0 -560px; } +.emoji-hand_splayed_tone1 { background-position: -20px -560px; } +.emoji-hand_splayed_tone2 { background-position: -40px -560px; } +.emoji-hand_splayed_tone3 { background-position: -60px -560px; } +.emoji-hand_splayed_tone4 { background-position: -80px -560px; } +.emoji-hand_splayed_tone5 { background-position: -100px -560px; } +.emoji-handbag { background-position: -120px -560px; } +.emoji-handball { background-position: -140px -560px; } +.emoji-handball_tone1 { background-position: -160px -560px; } +.emoji-handball_tone2 { background-position: -180px -560px; } +.emoji-handball_tone3 { background-position: -200px -560px; } +.emoji-handball_tone4 { background-position: -220px -560px; } +.emoji-handball_tone5 { background-position: -240px -560px; } +.emoji-handshake { background-position: -260px -560px; } +.emoji-handshake_tone1 { background-position: -280px -560px; } +.emoji-handshake_tone2 { background-position: -300px -560px; } +.emoji-handshake_tone3 { background-position: -320px -560px; } +.emoji-handshake_tone4 { background-position: -340px -560px; } +.emoji-handshake_tone5 { background-position: -360px -560px; } +.emoji-hash { background-position: -380px -560px; } +.emoji-hatched_chick { background-position: -400px -560px; } +.emoji-hatching_chick { background-position: -420px -560px; } +.emoji-head_bandage { background-position: -440px -560px; } +.emoji-headphones { background-position: -460px -560px; } +.emoji-hear_no_evil { background-position: -480px -560px; } +.emoji-heart { background-position: -500px -560px; } +.emoji-heart_decoration { background-position: -520px -560px; } +.emoji-heart_exclamation { background-position: -540px -560px; } +.emoji-heart_eyes { background-position: -560px -560px; } +.emoji-heart_eyes_cat { background-position: -580px 0; } +.emoji-heartbeat { background-position: -580px -20px; } +.emoji-heartpulse { background-position: -580px -40px; } +.emoji-hearts { background-position: -580px -60px; } +.emoji-heavy_check_mark { background-position: -580px -80px; } +.emoji-heavy_division_sign { background-position: -580px -100px; } +.emoji-heavy_dollar_sign { background-position: -580px -120px; } +.emoji-heavy_minus_sign { background-position: -580px -140px; } +.emoji-heavy_multiplication_x { background-position: -580px -160px; } +.emoji-heavy_plus_sign { background-position: -580px -180px; } +.emoji-helicopter { background-position: -580px -200px; } +.emoji-helmet_with_cross { background-position: -580px -220px; } +.emoji-herb { background-position: -580px -240px; } +.emoji-hibiscus { background-position: -580px -260px; } +.emoji-high_brightness { background-position: -580px -280px; } +.emoji-high_heel { background-position: -580px -300px; } +.emoji-hockey { background-position: -580px -320px; } +.emoji-hole { background-position: -580px -340px; } +.emoji-homes { background-position: -580px -360px; } +.emoji-honey_pot { background-position: -580px -380px; } +.emoji-horse { background-position: -580px -400px; } +.emoji-horse_racing { background-position: -580px -420px; } +.emoji-horse_racing_tone1 { background-position: -580px -440px; } +.emoji-horse_racing_tone2 { background-position: -580px -460px; } +.emoji-horse_racing_tone3 { background-position: -580px -480px; } +.emoji-horse_racing_tone4 { background-position: -580px -500px; } +.emoji-horse_racing_tone5 { background-position: -580px -520px; } +.emoji-hospital { background-position: -580px -540px; } +.emoji-hot_pepper { background-position: -580px -560px; } +.emoji-hotdog { background-position: 0 -580px; } +.emoji-hotel { background-position: -20px -580px; } +.emoji-hotsprings { background-position: -40px -580px; } +.emoji-hourglass { background-position: -60px -580px; } +.emoji-hourglass_flowing_sand { background-position: -80px -580px; } +.emoji-house { background-position: -100px -580px; } +.emoji-house_abandoned { background-position: -120px -580px; } +.emoji-house_with_garden { background-position: -140px -580px; } +.emoji-hugging { background-position: -160px -580px; } +.emoji-hushed { background-position: -180px -580px; } +.emoji-ice_cream { background-position: -200px -580px; } +.emoji-ice_skate { background-position: -220px -580px; } +.emoji-icecream { background-position: -240px -580px; } +.emoji-id { background-position: -260px -580px; } +.emoji-ideograph_advantage { background-position: -280px -580px; } +.emoji-imp { background-position: -300px -580px; } +.emoji-inbox_tray { background-position: -320px -580px; } +.emoji-incoming_envelope { background-position: -340px -580px; } +.emoji-information_desk_person { background-position: -360px -580px; } +.emoji-information_desk_person_tone1 { background-position: -380px -580px; } +.emoji-information_desk_person_tone2 { background-position: -400px -580px; } +.emoji-information_desk_person_tone3 { background-position: -420px -580px; } +.emoji-information_desk_person_tone4 { background-position: -440px -580px; } +.emoji-information_desk_person_tone5 { background-position: -460px -580px; } +.emoji-information_source { background-position: -480px -580px; } +.emoji-innocent { background-position: -500px -580px; } +.emoji-interrobang { background-position: -520px -580px; } +.emoji-iphone { background-position: -540px -580px; } +.emoji-island { background-position: -560px -580px; } +.emoji-izakaya_lantern { background-position: -580px -580px; } +.emoji-jack_o_lantern { background-position: -600px 0; } +.emoji-japan { background-position: -600px -20px; } +.emoji-japanese_castle { background-position: -600px -40px; } +.emoji-japanese_goblin { background-position: -600px -60px; } +.emoji-japanese_ogre { background-position: -600px -80px; } +.emoji-jeans { background-position: -600px -100px; } +.emoji-joy { background-position: -600px -120px; } +.emoji-joy_cat { background-position: -600px -140px; } +.emoji-joystick { background-position: -600px -160px; } +.emoji-juggling { background-position: -600px -180px; } +.emoji-juggling_tone1 { background-position: -600px -200px; } +.emoji-juggling_tone2 { background-position: -600px -220px; } +.emoji-juggling_tone3 { background-position: -600px -240px; } +.emoji-juggling_tone4 { background-position: -600px -260px; } +.emoji-juggling_tone5 { background-position: -600px -280px; } +.emoji-kaaba { background-position: -600px -300px; } +.emoji-key { background-position: -600px -320px; } +.emoji-key2 { background-position: -600px -340px; } +.emoji-keyboard { background-position: -600px -360px; } +.emoji-kimono { background-position: -600px -380px; } +.emoji-kiss { background-position: -600px -400px; } +.emoji-kiss_mm { background-position: -600px -420px; } +.emoji-kiss_ww { background-position: -600px -440px; } +.emoji-kissing { background-position: -600px -460px; } +.emoji-kissing_cat { background-position: -600px -480px; } +.emoji-kissing_closed_eyes { background-position: -600px -500px; } +.emoji-kissing_heart { background-position: -600px -520px; } +.emoji-kissing_smiling_eyes { background-position: -600px -540px; } +.emoji-kiwi { background-position: -600px -560px; } +.emoji-knife { background-position: -600px -580px; } +.emoji-koala { background-position: 0 -600px; } +.emoji-koko { background-position: -20px -600px; } +.emoji-label { background-position: -40px -600px; } +.emoji-large_blue_circle { background-position: -60px -600px; } +.emoji-large_blue_diamond { background-position: -80px -600px; } +.emoji-large_orange_diamond { background-position: -100px -600px; } +.emoji-last_quarter_moon { background-position: -120px -600px; } +.emoji-last_quarter_moon_with_face { background-position: -140px -600px; } +.emoji-laughing { background-position: -160px -600px; } +.emoji-leaves { background-position: -180px -600px; } +.emoji-ledger { background-position: -200px -600px; } +.emoji-left_facing_fist { background-position: -220px -600px; } +.emoji-left_facing_fist_tone1 { background-position: -240px -600px; } +.emoji-left_facing_fist_tone2 { background-position: -260px -600px; } +.emoji-left_facing_fist_tone3 { background-position: -280px -600px; } +.emoji-left_facing_fist_tone4 { background-position: -300px -600px; } +.emoji-left_facing_fist_tone5 { background-position: -320px -600px; } +.emoji-left_luggage { background-position: -340px -600px; } +.emoji-left_right_arrow { background-position: -360px -600px; } +.emoji-leftwards_arrow_with_hook { background-position: -380px -600px; } +.emoji-lemon { background-position: -400px -600px; } +.emoji-leo { background-position: -420px -600px; } +.emoji-leopard { background-position: -440px -600px; } +.emoji-level_slider { background-position: -460px -600px; } +.emoji-levitate { background-position: -480px -600px; } +.emoji-libra { background-position: -500px -600px; } +.emoji-lifter { background-position: -520px -600px; } +.emoji-lifter_tone1 { background-position: -540px -600px; } +.emoji-lifter_tone2 { background-position: -560px -600px; } +.emoji-lifter_tone3 { background-position: -580px -600px; } +.emoji-lifter_tone4 { background-position: -600px -600px; } +.emoji-lifter_tone5 { background-position: -620px 0; } +.emoji-light_rail { background-position: -620px -20px; } +.emoji-link { background-position: -620px -40px; } +.emoji-lion_face { background-position: -620px -60px; } +.emoji-lips { background-position: -620px -80px; } +.emoji-lipstick { background-position: -620px -100px; } +.emoji-lizard { background-position: -620px -120px; } +.emoji-lock { background-position: -620px -140px; } +.emoji-lock_with_ink_pen { background-position: -620px -160px; } +.emoji-lollipop { background-position: -620px -180px; } +.emoji-loop { background-position: -620px -200px; } +.emoji-loud_sound { background-position: -620px -220px; } +.emoji-loudspeaker { background-position: -620px -240px; } +.emoji-love_hotel { background-position: -620px -260px; } +.emoji-love_letter { background-position: -620px -280px; } +.emoji-low_brightness { background-position: -620px -300px; } +.emoji-lying_face { background-position: -620px -320px; } +.emoji-m { background-position: -620px -340px; } +.emoji-mag { background-position: -620px -360px; } +.emoji-mag_right { background-position: -620px -380px; } +.emoji-mahjong { background-position: -620px -400px; } +.emoji-mailbox { background-position: -620px -420px; } +.emoji-mailbox_closed { background-position: -620px -440px; } +.emoji-mailbox_with_mail { background-position: -620px -460px; } +.emoji-mailbox_with_no_mail { background-position: -620px -480px; } +.emoji-man { background-position: -620px -500px; } +.emoji-man_dancing { background-position: -620px -520px; } +.emoji-man_dancing_tone1 { background-position: -620px -540px; } +.emoji-man_dancing_tone2 { background-position: -620px -560px; } +.emoji-man_dancing_tone3 { background-position: -620px -580px; } +.emoji-man_dancing_tone4 { background-position: -620px -600px; } +.emoji-man_dancing_tone5 { background-position: 0 -620px; } +.emoji-man_in_tuxedo { background-position: -20px -620px; } +.emoji-man_in_tuxedo_tone1 { background-position: -40px -620px; } +.emoji-man_in_tuxedo_tone2 { background-position: -60px -620px; } +.emoji-man_in_tuxedo_tone3 { background-position: -80px -620px; } +.emoji-man_in_tuxedo_tone4 { background-position: -100px -620px; } +.emoji-man_in_tuxedo_tone5 { background-position: -120px -620px; } +.emoji-man_tone1 { background-position: -140px -620px; } +.emoji-man_tone2 { background-position: -160px -620px; } +.emoji-man_tone3 { background-position: -180px -620px; } +.emoji-man_tone4 { background-position: -200px -620px; } +.emoji-man_tone5 { background-position: -220px -620px; } +.emoji-man_with_gua_pi_mao { background-position: -240px -620px; } +.emoji-man_with_gua_pi_mao_tone1 { background-position: -260px -620px; } +.emoji-man_with_gua_pi_mao_tone2 { background-position: -280px -620px; } +.emoji-man_with_gua_pi_mao_tone3 { background-position: -300px -620px; } +.emoji-man_with_gua_pi_mao_tone4 { background-position: -320px -620px; } +.emoji-man_with_gua_pi_mao_tone5 { background-position: -340px -620px; } +.emoji-man_with_turban { background-position: -360px -620px; } +.emoji-man_with_turban_tone1 { background-position: -380px -620px; } +.emoji-man_with_turban_tone2 { background-position: -400px -620px; } +.emoji-man_with_turban_tone3 { background-position: -420px -620px; } +.emoji-man_with_turban_tone4 { background-position: -440px -620px; } +.emoji-man_with_turban_tone5 { background-position: -460px -620px; } +.emoji-mans_shoe { background-position: -480px -620px; } +.emoji-map { background-position: -500px -620px; } +.emoji-maple_leaf { background-position: -520px -620px; } +.emoji-martial_arts_uniform { background-position: -540px -620px; } +.emoji-mask { background-position: -560px -620px; } +.emoji-massage { background-position: -580px -620px; } +.emoji-massage_tone1 { background-position: -600px -620px; } +.emoji-massage_tone2 { background-position: -620px -620px; } +.emoji-massage_tone3 { background-position: -640px 0; } +.emoji-massage_tone4 { background-position: -640px -20px; } +.emoji-massage_tone5 { background-position: -640px -40px; } +.emoji-meat_on_bone { background-position: -640px -60px; } +.emoji-medal { background-position: -640px -80px; } +.emoji-mega { background-position: -640px -100px; } +.emoji-melon { background-position: -640px -120px; } +.emoji-menorah { background-position: -640px -140px; } +.emoji-mens { background-position: -640px -160px; } +.emoji-metal { background-position: -640px -180px; } +.emoji-metal_tone1 { background-position: -640px -200px; } +.emoji-metal_tone2 { background-position: -640px -220px; } +.emoji-metal_tone3 { background-position: -640px -240px; } +.emoji-metal_tone4 { background-position: -640px -260px; } +.emoji-metal_tone5 { background-position: -640px -280px; } +.emoji-metro { background-position: -640px -300px; } +.emoji-microphone { background-position: -640px -320px; } +.emoji-microphone2 { background-position: -640px -340px; } +.emoji-microscope { background-position: -640px -360px; } +.emoji-middle_finger { background-position: -640px -380px; } +.emoji-middle_finger_tone1 { background-position: -640px -400px; } +.emoji-middle_finger_tone2 { background-position: -640px -420px; } +.emoji-middle_finger_tone3 { background-position: -640px -440px; } +.emoji-middle_finger_tone4 { background-position: -640px -460px; } +.emoji-middle_finger_tone5 { background-position: -640px -480px; } +.emoji-military_medal { background-position: -640px -500px; } +.emoji-milk { background-position: -640px -520px; } +.emoji-milky_way { background-position: -640px -540px; } +.emoji-minibus { background-position: -640px -560px; } +.emoji-minidisc { background-position: -640px -580px; } +.emoji-mobile_phone_off { background-position: -640px -600px; } +.emoji-money_mouth { background-position: -640px -620px; } +.emoji-money_with_wings { background-position: 0 -640px; } +.emoji-moneybag { background-position: -20px -640px; } +.emoji-monkey { background-position: -40px -640px; } +.emoji-monkey_face { background-position: -60px -640px; } +.emoji-monorail { background-position: -80px -640px; } +.emoji-mortar_board { background-position: -100px -640px; } +.emoji-mosque { background-position: -120px -640px; } +.emoji-motor_scooter { background-position: -140px -640px; } +.emoji-motorboat { background-position: -160px -640px; } +.emoji-motorcycle { background-position: -180px -640px; } +.emoji-motorway { background-position: -200px -640px; } +.emoji-mount_fuji { background-position: -220px -640px; } +.emoji-mountain { background-position: -240px -640px; } +.emoji-mountain_bicyclist { background-position: -260px -640px; } +.emoji-mountain_bicyclist_tone1 { background-position: -280px -640px; } +.emoji-mountain_bicyclist_tone2 { background-position: -300px -640px; } +.emoji-mountain_bicyclist_tone3 { background-position: -320px -640px; } +.emoji-mountain_bicyclist_tone4 { background-position: -340px -640px; } +.emoji-mountain_bicyclist_tone5 { background-position: -360px -640px; } +.emoji-mountain_cableway { background-position: -380px -640px; } +.emoji-mountain_railway { background-position: -400px -640px; } +.emoji-mountain_snow { background-position: -420px -640px; } +.emoji-mouse { background-position: -440px -640px; } +.emoji-mouse2 { background-position: -460px -640px; } +.emoji-mouse_three_button { background-position: -480px -640px; } +.emoji-movie_camera { background-position: -500px -640px; } +.emoji-moyai { background-position: -520px -640px; } +.emoji-mrs_claus { background-position: -540px -640px; } +.emoji-mrs_claus_tone1 { background-position: -560px -640px; } +.emoji-mrs_claus_tone2 { background-position: -580px -640px; } +.emoji-mrs_claus_tone3 { background-position: -600px -640px; } +.emoji-mrs_claus_tone4 { background-position: -620px -640px; } +.emoji-mrs_claus_tone5 { background-position: -640px -640px; } +.emoji-muscle { background-position: -660px 0; } +.emoji-muscle_tone1 { background-position: -660px -20px; } +.emoji-muscle_tone2 { background-position: -660px -40px; } +.emoji-muscle_tone3 { background-position: -660px -60px; } +.emoji-muscle_tone4 { background-position: -660px -80px; } +.emoji-muscle_tone5 { background-position: -660px -100px; } +.emoji-mushroom { background-position: -660px -120px; } +.emoji-musical_keyboard { background-position: -660px -140px; } +.emoji-musical_note { background-position: -660px -160px; } +.emoji-musical_score { background-position: -660px -180px; } +.emoji-mute { background-position: -660px -200px; } +.emoji-nail_care { background-position: -660px -220px; } +.emoji-nail_care_tone1 { background-position: -660px -240px; } +.emoji-nail_care_tone2 { background-position: -660px -260px; } +.emoji-nail_care_tone3 { background-position: -660px -280px; } +.emoji-nail_care_tone4 { background-position: -660px -300px; } +.emoji-nail_care_tone5 { background-position: -660px -320px; } +.emoji-name_badge { background-position: -660px -340px; } +.emoji-nauseated_face { background-position: -660px -360px; } +.emoji-necktie { background-position: -660px -380px; } +.emoji-negative_squared_cross_mark { background-position: -660px -400px; } +.emoji-nerd { background-position: -660px -420px; } +.emoji-neutral_face { background-position: -660px -440px; } +.emoji-new { background-position: -660px -460px; } +.emoji-new_moon { background-position: -660px -480px; } +.emoji-new_moon_with_face { background-position: -660px -500px; } +.emoji-newspaper { background-position: -660px -520px; } +.emoji-newspaper2 { background-position: -660px -540px; } +.emoji-ng { background-position: -660px -560px; } +.emoji-night_with_stars { background-position: -660px -580px; } +.emoji-nine { background-position: -660px -600px; } +.emoji-no_bell { background-position: -660px -620px; } +.emoji-no_bicycles { background-position: -660px -640px; } +.emoji-no_entry { background-position: 0 -660px; } +.emoji-no_entry_sign { background-position: -20px -660px; } +.emoji-no_good { background-position: -40px -660px; } +.emoji-no_good_tone1 { background-position: -60px -660px; } +.emoji-no_good_tone2 { background-position: -80px -660px; } +.emoji-no_good_tone3 { background-position: -100px -660px; } +.emoji-no_good_tone4 { background-position: -120px -660px; } +.emoji-no_good_tone5 { background-position: -140px -660px; } +.emoji-no_mobile_phones { background-position: -160px -660px; } +.emoji-no_mouth { background-position: -180px -660px; } +.emoji-no_pedestrians { background-position: -200px -660px; } +.emoji-no_smoking { background-position: -220px -660px; } +.emoji-non-potable_water { background-position: -240px -660px; } +.emoji-nose { background-position: -260px -660px; } +.emoji-nose_tone1 { background-position: -280px -660px; } +.emoji-nose_tone2 { background-position: -300px -660px; } +.emoji-nose_tone3 { background-position: -320px -660px; } +.emoji-nose_tone4 { background-position: -340px -660px; } +.emoji-nose_tone5 { background-position: -360px -660px; } +.emoji-notebook { background-position: -380px -660px; } +.emoji-notebook_with_decorative_cover { background-position: -400px -660px; } +.emoji-notepad_spiral { background-position: -420px -660px; } +.emoji-notes { background-position: -440px -660px; } +.emoji-nut_and_bolt { background-position: -460px -660px; } +.emoji-o { background-position: -480px -660px; } +.emoji-o2 { background-position: -500px -660px; } +.emoji-ocean { background-position: -520px -660px; } +.emoji-octagonal_sign { background-position: -540px -660px; } +.emoji-octopus { background-position: -560px -660px; } +.emoji-oden { background-position: -580px -660px; } +.emoji-office { background-position: -600px -660px; } +.emoji-oil { background-position: -620px -660px; } +.emoji-ok { background-position: -640px -660px; } +.emoji-ok_hand { background-position: -660px -660px; } +.emoji-ok_hand_tone1 { background-position: -680px 0; } +.emoji-ok_hand_tone2 { background-position: -680px -20px; } +.emoji-ok_hand_tone3 { background-position: -680px -40px; } +.emoji-ok_hand_tone4 { background-position: -680px -60px; } +.emoji-ok_hand_tone5 { background-position: -680px -80px; } +.emoji-ok_woman { background-position: -680px -100px; } +.emoji-ok_woman_tone1 { background-position: -680px -120px; } +.emoji-ok_woman_tone2 { background-position: -680px -140px; } +.emoji-ok_woman_tone3 { background-position: -680px -160px; } +.emoji-ok_woman_tone4 { background-position: -680px -180px; } +.emoji-ok_woman_tone5 { background-position: -680px -200px; } +.emoji-older_man { background-position: -680px -220px; } +.emoji-older_man_tone1 { background-position: -680px -240px; } +.emoji-older_man_tone2 { background-position: -680px -260px; } +.emoji-older_man_tone3 { background-position: -680px -280px; } +.emoji-older_man_tone4 { background-position: -680px -300px; } +.emoji-older_man_tone5 { background-position: -680px -320px; } +.emoji-older_woman { background-position: -680px -340px; } +.emoji-older_woman_tone1 { background-position: -680px -360px; } +.emoji-older_woman_tone2 { background-position: -680px -380px; } +.emoji-older_woman_tone3 { background-position: -680px -400px; } +.emoji-older_woman_tone4 { background-position: -680px -420px; } +.emoji-older_woman_tone5 { background-position: -680px -440px; } +.emoji-om_symbol { background-position: -680px -460px; } +.emoji-on { background-position: -680px -480px; } +.emoji-oncoming_automobile { background-position: -680px -500px; } +.emoji-oncoming_bus { background-position: -680px -520px; } +.emoji-oncoming_police_car { background-position: -680px -540px; } +.emoji-oncoming_taxi { background-position: -680px -560px; } +.emoji-one { background-position: -680px -580px; } +.emoji-open_file_folder { background-position: -680px -600px; } +.emoji-open_hands { background-position: -680px -620px; } +.emoji-open_hands_tone1 { background-position: -680px -640px; } +.emoji-open_hands_tone2 { background-position: -680px -660px; } +.emoji-open_hands_tone3 { background-position: 0 -680px; } +.emoji-open_hands_tone4 { background-position: -20px -680px; } +.emoji-open_hands_tone5 { background-position: -40px -680px; } +.emoji-open_mouth { background-position: -60px -680px; } +.emoji-ophiuchus { background-position: -80px -680px; } +.emoji-orange_book { background-position: -100px -680px; } +.emoji-orthodox_cross { background-position: -120px -680px; } +.emoji-outbox_tray { background-position: -140px -680px; } +.emoji-owl { background-position: -160px -680px; } +.emoji-ox { background-position: -180px -680px; } +.emoji-package { background-position: -200px -680px; } +.emoji-page_facing_up { background-position: -220px -680px; } +.emoji-page_with_curl { background-position: -240px -680px; } +.emoji-pager { background-position: -260px -680px; } +.emoji-paintbrush { background-position: -280px -680px; } +.emoji-palm_tree { background-position: -300px -680px; } +.emoji-pancakes { background-position: -320px -680px; } +.emoji-panda_face { background-position: -340px -680px; } +.emoji-paperclip { background-position: -360px -680px; } +.emoji-paperclips { background-position: -380px -680px; } +.emoji-park { background-position: -400px -680px; } +.emoji-parking { background-position: -420px -680px; } +.emoji-part_alternation_mark { background-position: -440px -680px; } +.emoji-partly_sunny { background-position: -460px -680px; } +.emoji-passport_control { background-position: -480px -680px; } +.emoji-pause_button { background-position: -500px -680px; } +.emoji-peace { background-position: -520px -680px; } +.emoji-peach { background-position: -540px -680px; } +.emoji-peanuts { background-position: -560px -680px; } +.emoji-pear { background-position: -580px -680px; } +.emoji-pen_ballpoint { background-position: -600px -680px; } +.emoji-pen_fountain { background-position: -620px -680px; } +.emoji-pencil { background-position: -640px -680px; } +.emoji-pencil2 { background-position: -660px -680px; } +.emoji-penguin { background-position: -680px -680px; } +.emoji-pensive { background-position: -700px 0; } +.emoji-performing_arts { background-position: -700px -20px; } +.emoji-persevere { background-position: -700px -40px; } +.emoji-person_frowning { background-position: -700px -60px; } +.emoji-person_frowning_tone1 { background-position: -700px -80px; } +.emoji-person_frowning_tone2 { background-position: -700px -100px; } +.emoji-person_frowning_tone3 { background-position: -700px -120px; } +.emoji-person_frowning_tone4 { background-position: -700px -140px; } +.emoji-person_frowning_tone5 { background-position: -700px -160px; } +.emoji-person_with_blond_hair { background-position: -700px -180px; } +.emoji-person_with_blond_hair_tone1 { background-position: -700px -200px; } +.emoji-person_with_blond_hair_tone2 { background-position: -700px -220px; } +.emoji-person_with_blond_hair_tone3 { background-position: -700px -240px; } +.emoji-person_with_blond_hair_tone4 { background-position: -700px -260px; } +.emoji-person_with_blond_hair_tone5 { background-position: -700px -280px; } +.emoji-person_with_pouting_face { background-position: -700px -300px; } +.emoji-person_with_pouting_face_tone1 { background-position: -700px -320px; } +.emoji-person_with_pouting_face_tone2 { background-position: -700px -340px; } +.emoji-person_with_pouting_face_tone3 { background-position: -700px -360px; } +.emoji-person_with_pouting_face_tone4 { background-position: -700px -380px; } +.emoji-person_with_pouting_face_tone5 { background-position: -700px -400px; } +.emoji-pick { background-position: -700px -420px; } +.emoji-pig { background-position: -700px -440px; } +.emoji-pig2 { background-position: -700px -460px; } +.emoji-pig_nose { background-position: -700px -480px; } +.emoji-pill { background-position: -700px -500px; } +.emoji-pineapple { background-position: -700px -520px; } +.emoji-ping_pong { background-position: -700px -540px; } +.emoji-pisces { background-position: -700px -560px; } +.emoji-pizza { background-position: -700px -580px; } +.emoji-place_of_worship { background-position: -700px -600px; } +.emoji-play_pause { background-position: -700px -620px; } +.emoji-point_down { background-position: -700px -640px; } +.emoji-point_down_tone1 { background-position: -700px -660px; } +.emoji-point_down_tone2 { background-position: -700px -680px; } +.emoji-point_down_tone3 { background-position: 0 -700px; } +.emoji-point_down_tone4 { background-position: -20px -700px; } +.emoji-point_down_tone5 { background-position: -40px -700px; } +.emoji-point_left { background-position: -60px -700px; } +.emoji-point_left_tone1 { background-position: -80px -700px; } +.emoji-point_left_tone2 { background-position: -100px -700px; } +.emoji-point_left_tone3 { background-position: -120px -700px; } +.emoji-point_left_tone4 { background-position: -140px -700px; } +.emoji-point_left_tone5 { background-position: -160px -700px; } +.emoji-point_right { background-position: -180px -700px; } +.emoji-point_right_tone1 { background-position: -200px -700px; } +.emoji-point_right_tone2 { background-position: -220px -700px; } +.emoji-point_right_tone3 { background-position: -240px -700px; } +.emoji-point_right_tone4 { background-position: -260px -700px; } +.emoji-point_right_tone5 { background-position: -280px -700px; } +.emoji-point_up { background-position: -300px -700px; } +.emoji-point_up_2 { background-position: -320px -700px; } +.emoji-point_up_2_tone1 { background-position: -340px -700px; } +.emoji-point_up_2_tone2 { background-position: -360px -700px; } +.emoji-point_up_2_tone3 { background-position: -380px -700px; } +.emoji-point_up_2_tone4 { background-position: -400px -700px; } +.emoji-point_up_2_tone5 { background-position: -420px -700px; } +.emoji-point_up_tone1 { background-position: -440px -700px; } +.emoji-point_up_tone2 { background-position: -460px -700px; } +.emoji-point_up_tone3 { background-position: -480px -700px; } +.emoji-point_up_tone4 { background-position: -500px -700px; } +.emoji-point_up_tone5 { background-position: -520px -700px; } +.emoji-police_car { background-position: -540px -700px; } +.emoji-poodle { background-position: -560px -700px; } +.emoji-poop { background-position: -580px -700px; } +.emoji-popcorn { background-position: -600px -700px; } +.emoji-post_office { background-position: -620px -700px; } +.emoji-postal_horn { background-position: -640px -700px; } +.emoji-postbox { background-position: -660px -700px; } +.emoji-potable_water { background-position: -680px -700px; } +.emoji-potato { background-position: -700px -700px; } +.emoji-pouch { background-position: -720px 0; } +.emoji-poultry_leg { background-position: -720px -20px; } +.emoji-pound { background-position: -720px -40px; } +.emoji-pouting_cat { background-position: -720px -60px; } +.emoji-pray { background-position: -720px -80px; } +.emoji-pray_tone1 { background-position: -720px -100px; } +.emoji-pray_tone2 { background-position: -720px -120px; } +.emoji-pray_tone3 { background-position: -720px -140px; } +.emoji-pray_tone4 { background-position: -720px -160px; } +.emoji-pray_tone5 { background-position: -720px -180px; } +.emoji-prayer_beads { background-position: -720px -200px; } +.emoji-pregnant_woman { background-position: -720px -220px; } +.emoji-pregnant_woman_tone1 { background-position: -720px -240px; } +.emoji-pregnant_woman_tone2 { background-position: -720px -260px; } +.emoji-pregnant_woman_tone3 { background-position: -720px -280px; } +.emoji-pregnant_woman_tone4 { background-position: -720px -300px; } +.emoji-pregnant_woman_tone5 { background-position: -720px -320px; } +.emoji-prince { background-position: -720px -340px; } +.emoji-prince_tone1 { background-position: -720px -360px; } +.emoji-prince_tone2 { background-position: -720px -380px; } +.emoji-prince_tone3 { background-position: -720px -400px; } +.emoji-prince_tone4 { background-position: -720px -420px; } +.emoji-prince_tone5 { background-position: -720px -440px; } +.emoji-princess { background-position: -720px -460px; } +.emoji-princess_tone1 { background-position: -720px -480px; } +.emoji-princess_tone2 { background-position: -720px -500px; } +.emoji-princess_tone3 { background-position: -720px -520px; } +.emoji-princess_tone4 { background-position: -720px -540px; } +.emoji-princess_tone5 { background-position: -720px -560px; } +.emoji-printer { background-position: -720px -580px; } +.emoji-projector { background-position: -720px -600px; } +.emoji-punch { background-position: -720px -620px; } +.emoji-punch_tone1 { background-position: -720px -640px; } +.emoji-punch_tone2 { background-position: -720px -660px; } +.emoji-punch_tone3 { background-position: -720px -680px; } +.emoji-punch_tone4 { background-position: -720px -700px; } +.emoji-punch_tone5 { background-position: 0 -720px; } +.emoji-purple_heart { background-position: -20px -720px; } +.emoji-purse { background-position: -40px -720px; } +.emoji-pushpin { background-position: -60px -720px; } +.emoji-put_litter_in_its_place { background-position: -80px -720px; } +.emoji-question { background-position: -100px -720px; } +.emoji-rabbit { background-position: -120px -720px; } +.emoji-rabbit2 { background-position: -140px -720px; } +.emoji-race_car { background-position: -160px -720px; } +.emoji-racehorse { background-position: -180px -720px; } +.emoji-radio { background-position: -200px -720px; } +.emoji-radio_button { background-position: -220px -720px; } +.emoji-radioactive { background-position: -240px -720px; } +.emoji-rage { background-position: -260px -720px; } +.emoji-railway_car { background-position: -280px -720px; } +.emoji-railway_track { background-position: -300px -720px; } +.emoji-rainbow { background-position: -320px -720px; } +.emoji-raised_back_of_hand { background-position: -340px -720px; } +.emoji-raised_back_of_hand_tone1 { background-position: -360px -720px; } +.emoji-raised_back_of_hand_tone2 { background-position: -380px -720px; } +.emoji-raised_back_of_hand_tone3 { background-position: -400px -720px; } +.emoji-raised_back_of_hand_tone4 { background-position: -420px -720px; } +.emoji-raised_back_of_hand_tone5 { background-position: -440px -720px; } +.emoji-raised_hand { background-position: -460px -720px; } +.emoji-raised_hand_tone1 { background-position: -480px -720px; } +.emoji-raised_hand_tone2 { background-position: -500px -720px; } +.emoji-raised_hand_tone3 { background-position: -520px -720px; } +.emoji-raised_hand_tone4 { background-position: -540px -720px; } +.emoji-raised_hand_tone5 { background-position: -560px -720px; } +.emoji-raised_hands { background-position: -580px -720px; } +.emoji-raised_hands_tone1 { background-position: -600px -720px; } +.emoji-raised_hands_tone2 { background-position: -620px -720px; } +.emoji-raised_hands_tone3 { background-position: -640px -720px; } +.emoji-raised_hands_tone4 { background-position: -660px -720px; } +.emoji-raised_hands_tone5 { background-position: -680px -720px; } +.emoji-raising_hand { background-position: -700px -720px; } +.emoji-raising_hand_tone1 { background-position: -720px -720px; } +.emoji-raising_hand_tone2 { background-position: -740px 0; } +.emoji-raising_hand_tone3 { background-position: -740px -20px; } +.emoji-raising_hand_tone4 { background-position: -740px -40px; } +.emoji-raising_hand_tone5 { background-position: -740px -60px; } +.emoji-ram { background-position: -740px -80px; } +.emoji-ramen { background-position: -740px -100px; } +.emoji-rat { background-position: -740px -120px; } +.emoji-record_button { background-position: -740px -140px; } +.emoji-recycle { background-position: -740px -160px; } +.emoji-red_car { background-position: -740px -180px; } +.emoji-red_circle { background-position: -740px -200px; } +.emoji-registered { background-position: -740px -220px; } +.emoji-relaxed { background-position: -740px -240px; } +.emoji-relieved { background-position: -740px -260px; } +.emoji-reminder_ribbon { background-position: -740px -280px; } +.emoji-repeat { background-position: -740px -300px; } +.emoji-repeat_one { background-position: -740px -320px; } +.emoji-restroom { background-position: -740px -340px; } +.emoji-revolving_hearts { background-position: -740px -360px; } +.emoji-rewind { background-position: -740px -380px; } +.emoji-rhino { background-position: -740px -400px; } +.emoji-ribbon { background-position: -740px -420px; } +.emoji-rice { background-position: -740px -440px; } +.emoji-rice_ball { background-position: -740px -460px; } +.emoji-rice_cracker { background-position: -740px -480px; } +.emoji-rice_scene { background-position: -740px -500px; } +.emoji-right_facing_fist { background-position: -740px -520px; } +.emoji-right_facing_fist_tone1 { background-position: -740px -540px; } +.emoji-right_facing_fist_tone2 { background-position: -740px -560px; } +.emoji-right_facing_fist_tone3 { background-position: -740px -580px; } +.emoji-right_facing_fist_tone4 { background-position: -740px -600px; } +.emoji-right_facing_fist_tone5 { background-position: -740px -620px; } +.emoji-ring { background-position: -740px -640px; } +.emoji-robot { background-position: -740px -660px; } +.emoji-rocket { background-position: -740px -680px; } +.emoji-rofl { background-position: -740px -700px; } +.emoji-roller_coaster { background-position: -740px -720px; } +.emoji-rolling_eyes { background-position: 0 -740px; } +.emoji-rooster { background-position: -20px -740px; } +.emoji-rose { background-position: -40px -740px; } +.emoji-rosette { background-position: -60px -740px; } +.emoji-rotating_light { background-position: -80px -740px; } +.emoji-round_pushpin { background-position: -100px -740px; } +.emoji-rowboat { background-position: -120px -740px; } +.emoji-rowboat_tone1 { background-position: -140px -740px; } +.emoji-rowboat_tone2 { background-position: -160px -740px; } +.emoji-rowboat_tone3 { background-position: -180px -740px; } +.emoji-rowboat_tone4 { background-position: -200px -740px; } +.emoji-rowboat_tone5 { background-position: -220px -740px; } +.emoji-rugby_football { background-position: -240px -740px; } +.emoji-runner { background-position: -260px -740px; } +.emoji-runner_tone1 { background-position: -280px -740px; } +.emoji-runner_tone2 { background-position: -300px -740px; } +.emoji-runner_tone3 { background-position: -320px -740px; } +.emoji-runner_tone4 { background-position: -340px -740px; } +.emoji-runner_tone5 { background-position: -360px -740px; } +.emoji-running_shirt_with_sash { background-position: -380px -740px; } +.emoji-sa { background-position: -400px -740px; } +.emoji-sagittarius { background-position: -420px -740px; } +.emoji-sailboat { background-position: -440px -740px; } +.emoji-sake { background-position: -460px -740px; } +.emoji-salad { background-position: -480px -740px; } +.emoji-sandal { background-position: -500px -740px; } +.emoji-santa { background-position: -520px -740px; } +.emoji-santa_tone1 { background-position: -540px -740px; } +.emoji-santa_tone2 { background-position: -560px -740px; } +.emoji-santa_tone3 { background-position: -580px -740px; } +.emoji-santa_tone4 { background-position: -600px -740px; } +.emoji-santa_tone5 { background-position: -620px -740px; } +.emoji-satellite { background-position: -640px -740px; } +.emoji-satellite_orbital { background-position: -660px -740px; } +.emoji-saxophone { background-position: -680px -740px; } +.emoji-scales { background-position: -700px -740px; } +.emoji-school { background-position: -720px -740px; } +.emoji-school_satchel { background-position: -740px -740px; } +.emoji-scissors { background-position: -760px 0; } +.emoji-scooter { background-position: -760px -20px; } +.emoji-scorpion { background-position: -760px -40px; } +.emoji-scorpius { background-position: -760px -60px; } +.emoji-scream { background-position: -760px -80px; } +.emoji-scream_cat { background-position: -760px -100px; } +.emoji-scroll { background-position: -760px -120px; } +.emoji-seat { background-position: -760px -140px; } +.emoji-second_place { background-position: -760px -160px; } +.emoji-secret { background-position: -760px -180px; } +.emoji-see_no_evil { background-position: -760px -200px; } +.emoji-seedling { background-position: -760px -220px; } +.emoji-selfie { background-position: -760px -240px; } +.emoji-selfie_tone1 { background-position: -760px -260px; } +.emoji-selfie_tone2 { background-position: -760px -280px; } +.emoji-selfie_tone3 { background-position: -760px -300px; } +.emoji-selfie_tone4 { background-position: -760px -320px; } +.emoji-selfie_tone5 { background-position: -760px -340px; } +.emoji-seven { background-position: -760px -360px; } +.emoji-shallow_pan_of_food { background-position: -760px -380px; } +.emoji-shamrock { background-position: -760px -400px; } +.emoji-shark { background-position: -760px -420px; } +.emoji-shaved_ice { background-position: -760px -440px; } +.emoji-sheep { background-position: -760px -460px; } +.emoji-shell { background-position: -760px -480px; } +.emoji-shield { background-position: -760px -500px; } +.emoji-shinto_shrine { background-position: -760px -520px; } +.emoji-ship { background-position: -760px -540px; } +.emoji-shirt { background-position: -760px -560px; } +.emoji-shopping_bags { background-position: -760px -580px; } +.emoji-shopping_cart { background-position: -760px -600px; } +.emoji-shower { background-position: -760px -620px; } +.emoji-shrimp { background-position: -760px -640px; } +.emoji-shrug { background-position: -760px -660px; } +.emoji-shrug_tone1 { background-position: -760px -680px; } +.emoji-shrug_tone2 { background-position: -760px -700px; } +.emoji-shrug_tone3 { background-position: -760px -720px; } +.emoji-shrug_tone4 { background-position: -760px -740px; } +.emoji-shrug_tone5 { background-position: 0 -760px; } +.emoji-signal_strength { background-position: -20px -760px; } +.emoji-six { background-position: -40px -760px; } +.emoji-six_pointed_star { background-position: -60px -760px; } +.emoji-ski { background-position: -80px -760px; } +.emoji-skier { background-position: -100px -760px; } +.emoji-skull { background-position: -120px -760px; } +.emoji-skull_crossbones { background-position: -140px -760px; } +.emoji-sleeping { background-position: -160px -760px; } +.emoji-sleeping_accommodation { background-position: -180px -760px; } +.emoji-sleepy { background-position: -200px -760px; } +.emoji-slight_frown { background-position: -220px -760px; } +.emoji-slight_smile { background-position: -240px -760px; } +.emoji-slot_machine { background-position: -260px -760px; } +.emoji-small_blue_diamond { background-position: -280px -760px; } +.emoji-small_orange_diamond { background-position: -300px -760px; } +.emoji-small_red_triangle { background-position: -320px -760px; } +.emoji-small_red_triangle_down { background-position: -340px -760px; } +.emoji-smile { background-position: -360px -760px; } +.emoji-smile_cat { background-position: -380px -760px; } +.emoji-smiley { background-position: -400px -760px; } +.emoji-smiley_cat { background-position: -420px -760px; } +.emoji-smiling_imp { background-position: -440px -760px; } +.emoji-smirk { background-position: -460px -760px; } +.emoji-smirk_cat { background-position: -480px -760px; } +.emoji-smoking { background-position: -500px -760px; } +.emoji-snail { background-position: -520px -760px; } +.emoji-snake { background-position: -540px -760px; } +.emoji-sneezing_face { background-position: -560px -760px; } +.emoji-snowboarder { background-position: -580px -760px; } +.emoji-snowflake { background-position: -600px -760px; } +.emoji-snowman { background-position: -620px -760px; } +.emoji-snowman2 { background-position: -640px -760px; } +.emoji-sob { background-position: -660px -760px; } +.emoji-soccer { background-position: -680px -760px; } +.emoji-soon { background-position: -700px -760px; } +.emoji-sos { background-position: -720px -760px; } +.emoji-sound { background-position: -740px -760px; } +.emoji-space_invader { background-position: -760px -760px; } +.emoji-spades { background-position: -780px 0; } +.emoji-spaghetti { background-position: -780px -20px; } +.emoji-sparkle { background-position: -780px -40px; } +.emoji-sparkler { background-position: -780px -60px; } +.emoji-sparkles { background-position: -780px -80px; } +.emoji-sparkling_heart { background-position: -780px -100px; } +.emoji-speak_no_evil { background-position: -780px -120px; } +.emoji-speaker { background-position: -780px -140px; } +.emoji-speaking_head { background-position: -780px -160px; } +.emoji-speech_balloon { background-position: -780px -180px; } +.emoji-speedboat { background-position: -780px -200px; } +.emoji-spider { background-position: -780px -220px; } +.emoji-spider_web { background-position: -780px -240px; } +.emoji-spoon { background-position: -780px -260px; } +.emoji-spy { background-position: -780px -280px; } +.emoji-spy_tone1 { background-position: -780px -300px; } +.emoji-spy_tone2 { background-position: -780px -320px; } +.emoji-spy_tone3 { background-position: -780px -340px; } +.emoji-spy_tone4 { background-position: -780px -360px; } +.emoji-spy_tone5 { background-position: -780px -380px; } +.emoji-squid { background-position: -780px -400px; } +.emoji-stadium { background-position: -780px -420px; } +.emoji-star { background-position: -780px -440px; } +.emoji-star2 { background-position: -780px -460px; } +.emoji-star_and_crescent { background-position: -780px -480px; } +.emoji-star_of_david { background-position: -780px -500px; } +.emoji-stars { background-position: -780px -520px; } +.emoji-station { background-position: -780px -540px; } +.emoji-statue_of_liberty { background-position: -780px -560px; } +.emoji-steam_locomotive { background-position: -780px -580px; } +.emoji-stew { background-position: -780px -600px; } +.emoji-stop_button { background-position: -780px -620px; } +.emoji-stopwatch { background-position: -780px -640px; } +.emoji-straight_ruler { background-position: -780px -660px; } +.emoji-strawberry { background-position: -780px -680px; } +.emoji-stuck_out_tongue { background-position: -780px -700px; } +.emoji-stuck_out_tongue_closed_eyes { background-position: -780px -720px; } +.emoji-stuck_out_tongue_winking_eye { background-position: -780px -740px; } +.emoji-stuffed_flatbread { background-position: -780px -760px; } +.emoji-sun_with_face { background-position: 0 -780px; } +.emoji-sunflower { background-position: -20px -780px; } +.emoji-sunglasses { background-position: -40px -780px; } +.emoji-sunny { background-position: -60px -780px; } +.emoji-sunrise { background-position: -80px -780px; } +.emoji-sunrise_over_mountains { background-position: -100px -780px; } +.emoji-surfer { background-position: -120px -780px; } +.emoji-surfer_tone1 { background-position: -140px -780px; } +.emoji-surfer_tone2 { background-position: -160px -780px; } +.emoji-surfer_tone3 { background-position: -180px -780px; } +.emoji-surfer_tone4 { background-position: -200px -780px; } +.emoji-surfer_tone5 { background-position: -220px -780px; } +.emoji-sushi { background-position: -240px -780px; } +.emoji-suspension_railway { background-position: -260px -780px; } +.emoji-sweat { background-position: -280px -780px; } +.emoji-sweat_drops { background-position: -300px -780px; } +.emoji-sweat_smile { background-position: -320px -780px; } +.emoji-sweet_potato { background-position: -340px -780px; } +.emoji-swimmer { background-position: -360px -780px; } +.emoji-swimmer_tone1 { background-position: -380px -780px; } +.emoji-swimmer_tone2 { background-position: -400px -780px; } +.emoji-swimmer_tone3 { background-position: -420px -780px; } +.emoji-swimmer_tone4 { background-position: -440px -780px; } +.emoji-swimmer_tone5 { background-position: -460px -780px; } +.emoji-symbols { background-position: -480px -780px; } +.emoji-synagogue { background-position: -500px -780px; } +.emoji-syringe { background-position: -520px -780px; } +.emoji-taco { background-position: -540px -780px; } +.emoji-tada { background-position: -560px -780px; } +.emoji-tanabata_tree { background-position: -580px -780px; } +.emoji-tangerine { background-position: -600px -780px; } +.emoji-taurus { background-position: -620px -780px; } +.emoji-taxi { background-position: -640px -780px; } +.emoji-tea { background-position: -660px -780px; } +.emoji-telephone { background-position: -680px -780px; } +.emoji-telephone_receiver { background-position: -700px -780px; } +.emoji-telescope { background-position: -720px -780px; } +.emoji-ten { background-position: -740px -780px; } +.emoji-tennis { background-position: -760px -780px; } +.emoji-tent { background-position: -780px -780px; } +.emoji-thermometer { background-position: -800px 0; } +.emoji-thermometer_face { background-position: -800px -20px; } +.emoji-thinking { background-position: -800px -40px; } +.emoji-third_place { background-position: -800px -60px; } +.emoji-thought_balloon { background-position: -800px -80px; } +.emoji-three { background-position: -800px -100px; } +.emoji-thumbsdown { background-position: -800px -120px; } +.emoji-thumbsdown_tone1 { background-position: -800px -140px; } +.emoji-thumbsdown_tone2 { background-position: -800px -160px; } +.emoji-thumbsdown_tone3 { background-position: -800px -180px; } +.emoji-thumbsdown_tone4 { background-position: -800px -200px; } +.emoji-thumbsdown_tone5 { background-position: -800px -220px; } +.emoji-thumbsup { background-position: -800px -240px; } +.emoji-thumbsup_tone1 { background-position: -800px -260px; } +.emoji-thumbsup_tone2 { background-position: -800px -280px; } +.emoji-thumbsup_tone3 { background-position: -800px -300px; } +.emoji-thumbsup_tone4 { background-position: -800px -320px; } +.emoji-thumbsup_tone5 { background-position: -800px -340px; } +.emoji-thunder_cloud_rain { background-position: -800px -360px; } +.emoji-ticket { background-position: -800px -380px; } +.emoji-tickets { background-position: -800px -400px; } +.emoji-tiger { background-position: -800px -420px; } +.emoji-tiger2 { background-position: -800px -440px; } +.emoji-timer { background-position: -800px -460px; } +.emoji-tired_face { background-position: -800px -480px; } +.emoji-tm { background-position: -800px -500px; } +.emoji-toilet { background-position: -800px -520px; } +.emoji-tokyo_tower { background-position: -800px -540px; } +.emoji-tomato { background-position: -800px -560px; } +.emoji-tone1 { background-position: -800px -580px; } +.emoji-tone2 { background-position: -800px -600px; } +.emoji-tone3 { background-position: -800px -620px; } +.emoji-tone4 { background-position: -800px -640px; } +.emoji-tone5 { background-position: -800px -660px; } +.emoji-tongue { background-position: -800px -680px; } +.emoji-tools { background-position: -800px -700px; } +.emoji-top { background-position: -800px -720px; } +.emoji-tophat { background-position: -800px -740px; } +.emoji-track_next { background-position: -800px -760px; } +.emoji-track_previous { background-position: -800px -780px; } +.emoji-trackball { background-position: 0 -800px; } +.emoji-tractor { background-position: -20px -800px; } +.emoji-traffic_light { background-position: -40px -800px; } +.emoji-train { background-position: -60px -800px; } +.emoji-train2 { background-position: -80px -800px; } +.emoji-tram { background-position: -100px -800px; } +.emoji-triangular_flag_on_post { background-position: -120px -800px; } +.emoji-triangular_ruler { background-position: -140px -800px; } +.emoji-trident { background-position: -160px -800px; } +.emoji-triumph { background-position: -180px -800px; } +.emoji-trolleybus { background-position: -200px -800px; } +.emoji-trophy { background-position: -220px -800px; } +.emoji-tropical_drink { background-position: -240px -800px; } +.emoji-tropical_fish { background-position: -260px -800px; } +.emoji-truck { background-position: -280px -800px; } +.emoji-trumpet { background-position: -300px -800px; } +.emoji-tulip { background-position: -320px -800px; } +.emoji-tumbler_glass { background-position: -340px -800px; } +.emoji-turkey { background-position: -360px -800px; } +.emoji-turtle { background-position: -380px -800px; } +.emoji-tv { background-position: -400px -800px; } +.emoji-twisted_rightwards_arrows { background-position: -420px -800px; } +.emoji-two { background-position: -440px -800px; } +.emoji-two_hearts { background-position: -460px -800px; } +.emoji-two_men_holding_hands { background-position: -480px -800px; } +.emoji-two_women_holding_hands { background-position: -500px -800px; } +.emoji-u5272 { background-position: -520px -800px; } +.emoji-u5408 { background-position: -540px -800px; } +.emoji-u55b6 { background-position: -560px -800px; } +.emoji-u6307 { background-position: -580px -800px; } +.emoji-u6708 { background-position: -600px -800px; } +.emoji-u6709 { background-position: -620px -800px; } +.emoji-u6e80 { background-position: -640px -800px; } +.emoji-u7121 { background-position: -660px -800px; } +.emoji-u7533 { background-position: -680px -800px; } +.emoji-u7981 { background-position: -700px -800px; } +.emoji-u7a7a { background-position: -720px -800px; } +.emoji-umbrella { background-position: -740px -800px; } +.emoji-umbrella2 { background-position: -760px -800px; } +.emoji-unamused { background-position: -780px -800px; } +.emoji-underage { background-position: -800px -800px; } +.emoji-unicorn { background-position: -820px 0; } +.emoji-unlock { background-position: -820px -20px; } +.emoji-up { background-position: -820px -40px; } +.emoji-upside_down { background-position: -820px -60px; } +.emoji-urn { background-position: -820px -80px; } +.emoji-v { background-position: -820px -100px; } +.emoji-v_tone1 { background-position: -820px -120px; } +.emoji-v_tone2 { background-position: -820px -140px; } +.emoji-v_tone3 { background-position: -820px -160px; } +.emoji-v_tone4 { background-position: -820px -180px; } +.emoji-v_tone5 { background-position: -820px -200px; } +.emoji-vertical_traffic_light { background-position: -820px -220px; } +.emoji-vhs { background-position: -820px -240px; } +.emoji-vibration_mode { background-position: -820px -260px; } +.emoji-video_camera { background-position: -820px -280px; } +.emoji-video_game { background-position: -820px -300px; } +.emoji-violin { background-position: -820px -320px; } +.emoji-virgo { background-position: -820px -340px; } +.emoji-volcano { background-position: -820px -360px; } +.emoji-volleyball { background-position: -820px -380px; } +.emoji-vs { background-position: -820px -400px; } +.emoji-vulcan { background-position: -820px -420px; } +.emoji-vulcan_tone1 { background-position: -820px -440px; } +.emoji-vulcan_tone2 { background-position: -820px -460px; } +.emoji-vulcan_tone3 { background-position: -820px -480px; } +.emoji-vulcan_tone4 { background-position: -820px -500px; } +.emoji-vulcan_tone5 { background-position: -820px -520px; } +.emoji-walking { background-position: -820px -540px; } +.emoji-walking_tone1 { background-position: -820px -560px; } +.emoji-walking_tone2 { background-position: -820px -580px; } +.emoji-walking_tone3 { background-position: -820px -600px; } +.emoji-walking_tone4 { background-position: -820px -620px; } +.emoji-walking_tone5 { background-position: -820px -640px; } +.emoji-waning_crescent_moon { background-position: -820px -660px; } +.emoji-waning_gibbous_moon { background-position: -820px -680px; } +.emoji-warning { background-position: -820px -700px; } +.emoji-wastebasket { background-position: -820px -720px; } +.emoji-watch { background-position: -820px -740px; } +.emoji-water_buffalo { background-position: -820px -760px; } +.emoji-water_polo { background-position: -820px -780px; } +.emoji-water_polo_tone1 { background-position: -820px -800px; } +.emoji-water_polo_tone2 { background-position: 0 -820px; } +.emoji-water_polo_tone3 { background-position: -20px -820px; } +.emoji-water_polo_tone4 { background-position: -40px -820px; } +.emoji-water_polo_tone5 { background-position: -60px -820px; } +.emoji-watermelon { background-position: -80px -820px; } +.emoji-wave { background-position: -100px -820px; } +.emoji-wave_tone1 { background-position: -120px -820px; } +.emoji-wave_tone2 { background-position: -140px -820px; } +.emoji-wave_tone3 { background-position: -160px -820px; } +.emoji-wave_tone4 { background-position: -180px -820px; } +.emoji-wave_tone5 { background-position: -200px -820px; } +.emoji-wavy_dash { background-position: -220px -820px; } +.emoji-waxing_crescent_moon { background-position: -240px -820px; } +.emoji-waxing_gibbous_moon { background-position: -260px -820px; } +.emoji-wc { background-position: -280px -820px; } +.emoji-weary { background-position: -300px -820px; } +.emoji-wedding { background-position: -320px -820px; } +.emoji-whale { background-position: -340px -820px; } +.emoji-whale2 { background-position: -360px -820px; } +.emoji-wheel_of_dharma { background-position: -380px -820px; } +.emoji-wheelchair { background-position: -400px -820px; } +.emoji-white_check_mark { background-position: -420px -820px; } +.emoji-white_circle { background-position: -440px -820px; } +.emoji-white_flower { background-position: -460px -820px; } +.emoji-white_large_square { background-position: -480px -820px; } +.emoji-white_medium_small_square { background-position: -500px -820px; } +.emoji-white_medium_square { background-position: -520px -820px; } +.emoji-white_small_square { background-position: -540px -820px; } +.emoji-white_square_button { background-position: -560px -820px; } +.emoji-white_sun_cloud { background-position: -580px -820px; } +.emoji-white_sun_rain_cloud { background-position: -600px -820px; } +.emoji-white_sun_small_cloud { background-position: -620px -820px; } +.emoji-wilted_rose { background-position: -640px -820px; } +.emoji-wind_blowing_face { background-position: -660px -820px; } +.emoji-wind_chime { background-position: -680px -820px; } +.emoji-wine_glass { background-position: -700px -820px; } +.emoji-wink { background-position: -720px -820px; } +.emoji-wolf { background-position: -740px -820px; } +.emoji-woman { background-position: -760px -820px; } +.emoji-woman_tone1 { background-position: -780px -820px; } +.emoji-woman_tone2 { background-position: -800px -820px; } +.emoji-woman_tone3 { background-position: -820px -820px; } +.emoji-woman_tone4 { background-position: -840px 0; } +.emoji-woman_tone5 { background-position: -840px -20px; } +.emoji-womans_clothes { background-position: -840px -40px; } +.emoji-womans_hat { background-position: -840px -60px; } +.emoji-womens { background-position: -840px -80px; } +.emoji-worried { background-position: -840px -100px; } +.emoji-wrench { background-position: -840px -120px; } +.emoji-wrestlers { background-position: -840px -140px; } +.emoji-wrestlers_tone1 { background-position: -840px -160px; } +.emoji-wrestlers_tone2 { background-position: -840px -180px; } +.emoji-wrestlers_tone3 { background-position: -840px -200px; } +.emoji-wrestlers_tone4 { background-position: -840px -220px; } +.emoji-wrestlers_tone5 { background-position: -840px -240px; } +.emoji-writing_hand { background-position: -840px -260px; } +.emoji-writing_hand_tone1 { background-position: -840px -280px; } +.emoji-writing_hand_tone2 { background-position: -840px -300px; } +.emoji-writing_hand_tone3 { background-position: -840px -320px; } +.emoji-writing_hand_tone4 { background-position: -840px -340px; } +.emoji-writing_hand_tone5 { background-position: -840px -360px; } +.emoji-x { background-position: -840px -380px; } +.emoji-yellow_heart { background-position: -840px -400px; } +.emoji-yen { background-position: -840px -420px; } +.emoji-yin_yang { background-position: -840px -440px; } +.emoji-yum { background-position: -840px -460px; } +.emoji-zap { background-position: -840px -480px; } +.emoji-zero { background-position: -840px -500px; } +.emoji-zipper_mouth { background-position: -840px -520px; } +.emoji-100 { background-position: -840px -540px; } + +.emoji-icon { + background-image: image-url('emoji.png'); + background-repeat: no-repeat; + color: transparent; + text-indent: -99em; + height: 20px; + width: 20px; + + @media only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and (min--moz-device-pixel-ratio: 2), + only screen and (-o-min-device-pixel-ratio: 2/1), + only screen and (min-device-pixel-ratio: 2), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 2dppx) { + background-image: image-url('emoji@2x.png'); + background-size: 860px 840px; + } +} diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss index 7158de65143..0a8bc95590e 100644 --- a/app/assets/stylesheets/framework/emojis.scss +++ b/app/assets/stylesheets/framework/emojis.scss @@ -1,1809 +1,6 @@ -.emoji-0023-20E3 { background-position: 0 0; } -.emoji-002A-20E3 { background-position: -20px 0; } -.emoji-0030-20E3 { background-position: 0 -20px; } -.emoji-0031-20E3 { background-position: -20px -20px; } -.emoji-0032-20E3 { background-position: -40px 0; } -.emoji-0033-20E3 { background-position: -40px -20px; } -.emoji-0034-20E3 { background-position: 0 -40px; } -.emoji-0035-20E3 { background-position: -20px -40px; } -.emoji-0036-20E3 { background-position: -40px -40px; } -.emoji-0037-20E3 { background-position: -60px 0; } -.emoji-0038-20E3 { background-position: -60px -20px; } -.emoji-0039-20E3 { background-position: -60px -40px; } -.emoji-00A9 { background-position: 0 -60px; } -.emoji-00AE { background-position: -20px -60px; } -.emoji-1F004 { background-position: -40px -60px; } -.emoji-1F0CF { background-position: -60px -60px; } -.emoji-1F170 { background-position: -80px 0; } -.emoji-1F171 { background-position: -80px -20px; } -.emoji-1F17E { background-position: -80px -40px; } -.emoji-1F17F { background-position: -80px -60px; } -.emoji-1F18E { background-position: 0 -80px; } -.emoji-1F191 { background-position: -20px -80px; } -.emoji-1F192 { background-position: -40px -80px; } -.emoji-1F193 { background-position: -60px -80px; } -.emoji-1F194 { background-position: -80px -80px; } -.emoji-1F195 { background-position: -100px 0; } -.emoji-1F196 { background-position: -100px -20px; } -.emoji-1F197 { background-position: -100px -40px; } -.emoji-1F198 { background-position: -100px -60px; } -.emoji-1F199 { background-position: -100px -80px; } -.emoji-1F19A { background-position: 0 -100px; } -.emoji-1F1E6-1F1E8 { background-position: -20px -100px; } -.emoji-1F1E6-1F1E9 { background-position: -40px -100px; } -.emoji-1F1E6-1F1EA { background-position: -60px -100px; } -.emoji-1F1E6-1F1EB { background-position: -80px -100px; } -.emoji-1F1E6-1F1EC { background-position: -100px -100px; } -.emoji-1F1E6-1F1EE { background-position: -120px 0; } -.emoji-1F1E6-1F1F1 { background-position: -120px -20px; } -.emoji-1F1E6-1F1F2 { background-position: -120px -40px; } -.emoji-1F1E6-1F1F4 { background-position: -120px -60px; } -.emoji-1F1E6-1F1F6 { background-position: -120px -80px; } -.emoji-1F1E6-1F1F7 { background-position: -120px -100px; } -.emoji-1F1E6-1F1F8 { background-position: 0 -120px; } -.emoji-1F1E6-1F1F9 { background-position: -20px -120px; } -.emoji-1F1E6-1F1FA { background-position: -40px -120px; } -.emoji-1F1E6-1F1FC { background-position: -60px -120px; } -.emoji-1F1E6-1F1FD { background-position: -80px -120px; } -.emoji-1F1E6-1F1FF { background-position: -100px -120px; } -.emoji-1F1E7-1F1E6 { background-position: -120px -120px; } -.emoji-1F1E7-1F1E7 { background-position: -140px 0; } -.emoji-1F1E7-1F1E9 { background-position: -140px -20px; } -.emoji-1F1E7-1F1EA { background-position: -140px -40px; } -.emoji-1F1E7-1F1EB { background-position: -140px -60px; } -.emoji-1F1E7-1F1EC { background-position: -140px -80px; } -.emoji-1F1E7-1F1ED { background-position: -140px -100px; } -.emoji-1F1E7-1F1EE { background-position: -140px -120px; } -.emoji-1F1E7-1F1EF { background-position: 0 -140px; } -.emoji-1F1E7-1F1F1 { background-position: -20px -140px; } -.emoji-1F1E7-1F1F2 { background-position: -40px -140px; } -.emoji-1F1E7-1F1F3 { background-position: -60px -140px; } -.emoji-1F1E7-1F1F4 { background-position: -80px -140px; } -.emoji-1F1E7-1F1F6 { background-position: -100px -140px; } -.emoji-1F1E7-1F1F7 { background-position: -120px -140px; } -.emoji-1F1E7-1F1F8 { background-position: -140px -140px; } -.emoji-1F1E7-1F1F9 { background-position: -160px 0; } -.emoji-1F1E7-1F1FB { background-position: -160px -20px; } -.emoji-1F1E7-1F1FC { background-position: -160px -40px; } -.emoji-1F1E7-1F1FE { background-position: -160px -60px; } -.emoji-1F1E7-1F1FF { background-position: -160px -80px; } -.emoji-1F1E8-1F1E6 { background-position: -160px -100px; } -.emoji-1F1E8-1F1E8 { background-position: -160px -120px; } -.emoji-1F1E8-1F1E9 { background-position: -160px -140px; } -.emoji-1F1E8-1F1EB { background-position: 0 -160px; } -.emoji-1F1E8-1F1EC { background-position: -20px -160px; } -.emoji-1F1E8-1F1ED { background-position: -40px -160px; } -.emoji-1F1E8-1F1EE { background-position: -60px -160px; } -.emoji-1F1E8-1F1F0 { background-position: -80px -160px; } -.emoji-1F1E8-1F1F1 { background-position: -100px -160px; } -.emoji-1F1E8-1F1F2 { background-position: -120px -160px; } -.emoji-1F1E8-1F1F3 { background-position: -140px -160px; } -.emoji-1F1E8-1F1F4 { background-position: -160px -160px; } -.emoji-1F1E8-1F1F5 { background-position: -180px 0; } -.emoji-1F1E8-1F1F7 { background-position: -180px -20px; } -.emoji-1F1E8-1F1FA { background-position: -180px -40px; } -.emoji-1F1E8-1F1FB { background-position: -180px -60px; } -.emoji-1F1E8-1F1FC { background-position: -180px -80px; } -.emoji-1F1E8-1F1FD { background-position: -180px -100px; } -.emoji-1F1E8-1F1FE { background-position: -180px -120px; } -.emoji-1F1E8-1F1FF { background-position: -180px -140px; } -.emoji-1F1E9-1F1EA { background-position: -180px -160px; } -.emoji-1F1E9-1F1EC { background-position: 0 -180px; } -.emoji-1F1E9-1F1EF { background-position: -20px -180px; } -.emoji-1F1E9-1F1F0 { background-position: -40px -180px; } -.emoji-1F1E9-1F1F2 { background-position: -60px -180px; } -.emoji-1F1E9-1F1F4 { background-position: -80px -180px; } -.emoji-1F1E9-1F1FF { background-position: -100px -180px; } -.emoji-1F1EA-1F1E6 { background-position: -120px -180px; } -.emoji-1F1EA-1F1E8 { background-position: -140px -180px; } -.emoji-1F1EA-1F1EA { background-position: -160px -180px; } -.emoji-1F1EA-1F1EC { background-position: -180px -180px; } -.emoji-1F1EA-1F1ED { background-position: -200px 0; } -.emoji-1F1EA-1F1F7 { background-position: -200px -20px; } -.emoji-1F1EA-1F1F8 { background-position: -200px -40px; } -.emoji-1F1EA-1F1F9 { background-position: -200px -60px; } -.emoji-1F1EA-1F1FA { background-position: -200px -80px; } -.emoji-1F1EB-1F1EE { background-position: -200px -100px; } -.emoji-1F1EB-1F1EF { background-position: -200px -120px; } -.emoji-1F1EB-1F1F0 { background-position: -200px -140px; } -.emoji-1F1EB-1F1F2 { background-position: -200px -160px; } -.emoji-1F1EB-1F1F4 { background-position: -200px -180px; } -.emoji-1F1EB-1F1F7 { background-position: 0 -200px; } -.emoji-1F1EC-1F1E6 { background-position: -20px -200px; } -.emoji-1F1EC-1F1E7 { background-position: -40px -200px; } -.emoji-1F1EC-1F1E9 { background-position: -60px -200px; } -.emoji-1F1EC-1F1EA { background-position: -80px -200px; } -.emoji-1F1EC-1F1EB { background-position: -100px -200px; } -.emoji-1F1EC-1F1EC { background-position: -120px -200px; } -.emoji-1F1EC-1F1ED { background-position: -140px -200px; } -.emoji-1F1EC-1F1EE { background-position: -160px -200px; } -.emoji-1F1EC-1F1F1 { background-position: -180px -200px; } -.emoji-1F1EC-1F1F2 { background-position: -200px -200px; } -.emoji-1F1EC-1F1F3 { background-position: -220px 0; } -.emoji-1F1EC-1F1F5 { background-position: -220px -20px; } -.emoji-1F1EC-1F1F6 { background-position: -220px -40px; } -.emoji-1F1EC-1F1F7 { background-position: -220px -60px; } -.emoji-1F1EC-1F1F8 { background-position: -220px -80px; } -.emoji-1F1EC-1F1F9 { background-position: -220px -100px; } -.emoji-1F1EC-1F1FA { background-position: -220px -120px; } -.emoji-1F1EC-1F1FC { background-position: -220px -140px; } -.emoji-1F1EC-1F1FE { background-position: -220px -160px; } -.emoji-1F1ED-1F1F0 { background-position: -220px -180px; } -.emoji-1F1ED-1F1F2 { background-position: -220px -200px; } -.emoji-1F1ED-1F1F3 { background-position: 0 -220px; } -.emoji-1F1ED-1F1F7 { background-position: -20px -220px; } -.emoji-1F1ED-1F1F9 { background-position: -40px -220px; } -.emoji-1F1ED-1F1FA { background-position: -60px -220px; } -.emoji-1F1EE-1F1E8 { background-position: -80px -220px; } -.emoji-1F1EE-1F1E9 { background-position: -100px -220px; } -.emoji-1F1EE-1F1EA { background-position: -120px -220px; } -.emoji-1F1EE-1F1F1 { background-position: -140px -220px; } -.emoji-1F1EE-1F1F2 { background-position: -160px -220px; } -.emoji-1F1EE-1F1F3 { background-position: -180px -220px; } -.emoji-1F1EE-1F1F4 { background-position: -200px -220px; } -.emoji-1F1EE-1F1F6 { background-position: -220px -220px; } -.emoji-1F1EE-1F1F7 { background-position: -240px 0; } -.emoji-1F1EE-1F1F8 { background-position: -240px -20px; } -.emoji-1F1EE-1F1F9 { background-position: -240px -40px; } -.emoji-1F1EF-1F1EA { background-position: -240px -60px; } -.emoji-1F1EF-1F1F2 { background-position: -240px -80px; } -.emoji-1F1EF-1F1F4 { background-position: -240px -100px; } -.emoji-1F1EF-1F1F5 { background-position: -240px -120px; } -.emoji-1F1F0-1F1EA { background-position: -240px -140px; } -.emoji-1F1F0-1F1EC { background-position: -240px -160px; } -.emoji-1F1F0-1F1ED { background-position: -240px -180px; } -.emoji-1F1F0-1F1EE { background-position: -240px -200px; } -.emoji-1F1F0-1F1F2 { background-position: -240px -220px; } -.emoji-1F1F0-1F1F3 { background-position: 0 -240px; } -.emoji-1F1F0-1F1F5 { background-position: -20px -240px; } -.emoji-1F1F0-1F1F7 { background-position: -40px -240px; } -.emoji-1F1F0-1F1FC { background-position: -60px -240px; } -.emoji-1F1F0-1F1FE { background-position: -80px -240px; } -.emoji-1F1F0-1F1FF { background-position: -100px -240px; } -.emoji-1F1F1-1F1E6 { background-position: -120px -240px; } -.emoji-1F1F1-1F1E7 { background-position: -140px -240px; } -.emoji-1F1F1-1F1E8 { background-position: -160px -240px; } -.emoji-1F1F1-1F1EE { background-position: -180px -240px; } -.emoji-1F1F1-1F1F0 { background-position: -200px -240px; } -.emoji-1F1F1-1F1F7 { background-position: -220px -240px; } -.emoji-1F1F1-1F1F8 { background-position: -240px -240px; } -.emoji-1F1F1-1F1F9 { background-position: -260px 0; } -.emoji-1F1F1-1F1FA { background-position: -260px -20px; } -.emoji-1F1F1-1F1FB { background-position: -260px -40px; } -.emoji-1F1F1-1F1FE { background-position: -260px -60px; } -.emoji-1F1F2-1F1E6 { background-position: -260px -80px; } -.emoji-1F1F2-1F1E8 { background-position: -260px -100px; } -.emoji-1F1F2-1F1E9 { background-position: -260px -120px; } -.emoji-1F1F2-1F1EA { background-position: -260px -140px; } -.emoji-1F1F2-1F1EB { background-position: -260px -160px; } -.emoji-1F1F2-1F1EC { background-position: -260px -180px; } -.emoji-1F1F2-1F1ED { background-position: -260px -200px; } -.emoji-1F1F2-1F1F0 { background-position: -260px -220px; } -.emoji-1F1F2-1F1F1 { background-position: -260px -240px; } -.emoji-1F1F2-1F1F2 { background-position: 0 -260px; } -.emoji-1F1F2-1F1F3 { background-position: -20px -260px; } -.emoji-1F1F2-1F1F4 { background-position: -40px -260px; } -.emoji-1F1F2-1F1F5 { background-position: -60px -260px; } -.emoji-1F1F2-1F1F6 { background-position: -80px -260px; } -.emoji-1F1F2-1F1F7 { background-position: -100px -260px; } -.emoji-1F1F2-1F1F8 { background-position: -120px -260px; } -.emoji-1F1F2-1F1F9 { background-position: -140px -260px; } -.emoji-1F1F2-1F1FA { background-position: -160px -260px; } -.emoji-1F1F2-1F1FB { background-position: -180px -260px; } -.emoji-1F1F2-1F1FC { background-position: -200px -260px; } -.emoji-1F1F2-1F1FD { background-position: -220px -260px; } -.emoji-1F1F2-1F1FE { background-position: -240px -260px; } -.emoji-1F1F2-1F1FF { background-position: -260px -260px; } -.emoji-1F1F3-1F1E6 { background-position: -280px 0; } -.emoji-1F1F3-1F1E8 { background-position: -280px -20px; } -.emoji-1F1F3-1F1EA { background-position: -280px -40px; } -.emoji-1F1F3-1F1EB { background-position: -280px -60px; } -.emoji-1F1F3-1F1EC { background-position: -280px -80px; } -.emoji-1F1F3-1F1EE { background-position: -280px -100px; } -.emoji-1F1F3-1F1F1 { background-position: -280px -120px; } -.emoji-1F1F3-1F1F4 { background-position: -280px -140px; } -.emoji-1F1F3-1F1F5 { background-position: -280px -160px; } -.emoji-1F1F3-1F1F7 { background-position: -280px -180px; } -.emoji-1F1F3-1F1FA { background-position: -280px -200px; } -.emoji-1F1F3-1F1FF { background-position: -280px -220px; } -.emoji-1F1F4-1F1F2 { background-position: -280px -240px; } -.emoji-1F1F5-1F1E6 { background-position: -280px -260px; } -.emoji-1F1F5-1F1EA { background-position: 0 -280px; } -.emoji-1F1F5-1F1EB { background-position: -20px -280px; } -.emoji-1F1F5-1F1EC { background-position: -40px -280px; } -.emoji-1F1F5-1F1ED { background-position: -60px -280px; } -.emoji-1F1F5-1F1F0 { background-position: -80px -280px; } -.emoji-1F1F5-1F1F1 { background-position: -100px -280px; } -.emoji-1F1F5-1F1F2 { background-position: -120px -280px; } -.emoji-1F1F5-1F1F3 { background-position: -140px -280px; } -.emoji-1F1F5-1F1F7 { background-position: -160px -280px; } -.emoji-1F1F5-1F1F8 { background-position: -180px -280px; } -.emoji-1F1F5-1F1F9 { background-position: -200px -280px; } -.emoji-1F1F5-1F1FC { background-position: -220px -280px; } -.emoji-1F1F5-1F1FE { background-position: -240px -280px; } -.emoji-1F1F6-1F1E6 { background-position: -260px -280px; } -.emoji-1F1F7-1F1EA { background-position: -280px -280px; } -.emoji-1F1F7-1F1F4 { background-position: -300px 0; } -.emoji-1F1F7-1F1F8 { background-position: -300px -20px; } -.emoji-1F1F7-1F1FA { background-position: -300px -40px; } -.emoji-1F1F7-1F1FC { background-position: -300px -60px; } -.emoji-1F1F8-1F1E6 { background-position: -300px -80px; } -.emoji-1F1F8-1F1E7 { background-position: -300px -100px; } -.emoji-1F1F8-1F1E8 { background-position: -300px -120px; } -.emoji-1F1F8-1F1E9 { background-position: -300px -140px; } -.emoji-1F1F8-1F1EA { background-position: -300px -160px; } -.emoji-1F1F8-1F1EC { background-position: -300px -180px; } -.emoji-1F1F8-1F1ED { background-position: -300px -200px; } -.emoji-1F1F8-1F1EE { background-position: -300px -220px; } -.emoji-1F1F8-1F1EF { background-position: -300px -240px; } -.emoji-1F1F8-1F1F0 { background-position: -300px -260px; } -.emoji-1F1F8-1F1F1 { background-position: -300px -280px; } -.emoji-1F1F8-1F1F2 { background-position: 0 -300px; } -.emoji-1F1F8-1F1F3 { background-position: -20px -300px; } -.emoji-1F1F8-1F1F4 { background-position: -40px -300px; } -.emoji-1F1F8-1F1F7 { background-position: -60px -300px; } -.emoji-1F1F8-1F1F8 { background-position: -80px -300px; } -.emoji-1F1F8-1F1F9 { background-position: -100px -300px; } -.emoji-1F1F8-1F1FB { background-position: -120px -300px; } -.emoji-1F1F8-1F1FD { background-position: -140px -300px; } -.emoji-1F1F8-1F1FE { background-position: -160px -300px; } -.emoji-1F1F8-1F1FF { background-position: -180px -300px; } -.emoji-1F1F9-1F1E6 { background-position: -200px -300px; } -.emoji-1F1F9-1F1E8 { background-position: -220px -300px; } -.emoji-1F1F9-1F1E9 { background-position: -240px -300px; } -.emoji-1F1F9-1F1EB { background-position: -260px -300px; } -.emoji-1F1F9-1F1EC { background-position: -280px -300px; } -.emoji-1F1F9-1F1ED { background-position: -300px -300px; } -.emoji-1F1F9-1F1EF { background-position: -320px 0; } -.emoji-1F1F9-1F1F0 { background-position: -320px -20px; } -.emoji-1F1F9-1F1F1 { background-position: -320px -40px; } -.emoji-1F1F9-1F1F2 { background-position: -320px -60px; } -.emoji-1F1F9-1F1F3 { background-position: -320px -80px; } -.emoji-1F1F9-1F1F4 { background-position: -320px -100px; } -.emoji-1F1F9-1F1F7 { background-position: -320px -120px; } -.emoji-1F1F9-1F1F9 { background-position: -320px -140px; } -.emoji-1F1F9-1F1FB { background-position: -320px -160px; } -.emoji-1F1F9-1F1FC { background-position: -320px -180px; } -.emoji-1F1F9-1F1FF { background-position: -320px -200px; } -.emoji-1F1FA-1F1E6 { background-position: -320px -220px; } -.emoji-1F1FA-1F1EC { background-position: -320px -240px; } -.emoji-1F1FA-1F1F2 { background-position: -320px -260px; } -.emoji-1F1FA-1F1F8 { background-position: -320px -280px; } -.emoji-1F1FA-1F1FE { background-position: -320px -300px; } -.emoji-1F1FA-1F1FF { background-position: 0 -320px; } -.emoji-1F1FB-1F1E6 { background-position: -20px -320px; } -.emoji-1F1FB-1F1E8 { background-position: -40px -320px; } -.emoji-1F1FB-1F1EA { background-position: -60px -320px; } -.emoji-1F1FB-1F1EC { background-position: -80px -320px; } -.emoji-1F1FB-1F1EE { background-position: -100px -320px; } -.emoji-1F1FB-1F1F3 { background-position: -120px -320px; } -.emoji-1F1FB-1F1FA { background-position: -140px -320px; } -.emoji-1F1FC-1F1EB { background-position: -160px -320px; } -.emoji-1F1FC-1F1F8 { background-position: -180px -320px; } -.emoji-1F1FD-1F1F0 { background-position: -200px -320px; } -.emoji-1F1FE-1F1EA { background-position: -220px -320px; } -.emoji-1F1FE-1F1F9 { background-position: -240px -320px; } -.emoji-1F1FF-1F1E6 { background-position: -260px -320px; } -.emoji-1F1FF-1F1F2 { background-position: -280px -320px; } -.emoji-1F1FF-1F1FC { background-position: -300px -320px; } -.emoji-1F201 { background-position: -320px -320px; } -.emoji-1F202 { background-position: -340px 0; } -.emoji-1F21A { background-position: -340px -20px; } -.emoji-1F22F { background-position: -340px -40px; } -.emoji-1F232 { background-position: -340px -60px; } -.emoji-1F233 { background-position: -340px -80px; } -.emoji-1F234 { background-position: -340px -100px; } -.emoji-1F235 { background-position: -340px -120px; } -.emoji-1F236 { background-position: -340px -140px; } -.emoji-1F237 { background-position: -340px -160px; } -.emoji-1F238 { background-position: -340px -180px; } -.emoji-1F239 { background-position: -340px -200px; } -.emoji-1F23A { background-position: -340px -220px; } -.emoji-1F250 { background-position: -340px -240px; } -.emoji-1F251 { background-position: -340px -260px; } -.emoji-1F300 { background-position: -340px -280px; } -.emoji-1F301 { background-position: -340px -300px; } -.emoji-1F302 { background-position: -340px -320px; } -.emoji-1F303 { background-position: 0 -340px; } -.emoji-1F304 { background-position: -20px -340px; } -.emoji-1F305 { background-position: -40px -340px; } -.emoji-1F306 { background-position: -60px -340px; } -.emoji-1F307 { background-position: -80px -340px; } -.emoji-1F308 { background-position: -100px -340px; } -.emoji-1F309 { background-position: -120px -340px; } -.emoji-1F30A { background-position: -140px -340px; } -.emoji-1F30B { background-position: -160px -340px; } -.emoji-1F30C { background-position: -180px -340px; } -.emoji-1F30D { background-position: -200px -340px; } -.emoji-1F30E { background-position: -220px -340px; } -.emoji-1F30F { background-position: -240px -340px; } -.emoji-1F310 { background-position: -260px -340px; } -.emoji-1F311 { background-position: -280px -340px; } -.emoji-1F312 { background-position: -300px -340px; } -.emoji-1F313 { background-position: -320px -340px; } -.emoji-1F314 { background-position: -340px -340px; } -.emoji-1F315 { background-position: -360px 0; } -.emoji-1F316 { background-position: -360px -20px; } -.emoji-1F317 { background-position: -360px -40px; } -.emoji-1F318 { background-position: -360px -60px; } -.emoji-1F319 { background-position: -360px -80px; } -.emoji-1F31A { background-position: -360px -100px; } -.emoji-1F31B { background-position: -360px -120px; } -.emoji-1F31C { background-position: -360px -140px; } -.emoji-1F31D { background-position: -360px -160px; } -.emoji-1F31E { background-position: -360px -180px; } -.emoji-1F31F { background-position: -360px -200px; } -.emoji-1F320 { background-position: -360px -220px; } -.emoji-1F321 { background-position: -360px -240px; } -.emoji-1F324 { background-position: -360px -260px; } -.emoji-1F325 { background-position: -360px -280px; } -.emoji-1F326 { background-position: -360px -300px; } -.emoji-1F327 { background-position: -360px -320px; } -.emoji-1F328 { background-position: -360px -340px; } -.emoji-1F329 { background-position: 0 -360px; } -.emoji-1F32A { background-position: -20px -360px; } -.emoji-1F32B { background-position: -40px -360px; } -.emoji-1F32C { background-position: -60px -360px; } -.emoji-1F32D { background-position: -80px -360px; } -.emoji-1F32E { background-position: -100px -360px; } -.emoji-1F32F { background-position: -120px -360px; } -.emoji-1F330 { background-position: -140px -360px; } -.emoji-1F331 { background-position: -160px -360px; } -.emoji-1F332 { background-position: -180px -360px; } -.emoji-1F333 { background-position: -200px -360px; } -.emoji-1F334 { background-position: -220px -360px; } -.emoji-1F335 { background-position: -240px -360px; } -.emoji-1F336 { background-position: -260px -360px; } -.emoji-1F337 { background-position: -280px -360px; } -.emoji-1F338 { background-position: -300px -360px; } -.emoji-1F339 { background-position: -320px -360px; } -.emoji-1F33A { background-position: -340px -360px; } -.emoji-1F33B { background-position: -360px -360px; } -.emoji-1F33C { background-position: -380px 0; } -.emoji-1F33D { background-position: -380px -20px; } -.emoji-1F33E { background-position: -380px -40px; } -.emoji-1F33F { background-position: -380px -60px; } -.emoji-1F340 { background-position: -380px -80px; } -.emoji-1F341 { background-position: -380px -100px; } -.emoji-1F342 { background-position: -380px -120px; } -.emoji-1F343 { background-position: -380px -140px; } -.emoji-1F344 { background-position: -380px -160px; } -.emoji-1F345 { background-position: -380px -180px; } -.emoji-1F346 { background-position: -380px -200px; } -.emoji-1F347 { background-position: -380px -220px; } -.emoji-1F348 { background-position: -380px -240px; } -.emoji-1F349 { background-position: -380px -260px; } -.emoji-1F34A { background-position: -380px -280px; } -.emoji-1F34B { background-position: -380px -300px; } -.emoji-1F34C { background-position: -380px -320px; } -.emoji-1F34D { background-position: -380px -340px; } -.emoji-1F34E { background-position: -380px -360px; } -.emoji-1F34F { background-position: 0 -380px; } -.emoji-1F350 { background-position: -20px -380px; } -.emoji-1F351 { background-position: -40px -380px; } -.emoji-1F352 { background-position: -60px -380px; } -.emoji-1F353 { background-position: -80px -380px; } -.emoji-1F354 { background-position: -100px -380px; } -.emoji-1F355 { background-position: -120px -380px; } -.emoji-1F356 { background-position: -140px -380px; } -.emoji-1F357 { background-position: -160px -380px; } -.emoji-1F358 { background-position: -180px -380px; } -.emoji-1F359 { background-position: -200px -380px; } -.emoji-1F35A { background-position: -220px -380px; } -.emoji-1F35B { background-position: -240px -380px; } -.emoji-1F35C { background-position: -260px -380px; } -.emoji-1F35D { background-position: -280px -380px; } -.emoji-1F35E { background-position: -300px -380px; } -.emoji-1F35F { background-position: -320px -380px; } -.emoji-1F360 { background-position: -340px -380px; } -.emoji-1F361 { background-position: -360px -380px; } -.emoji-1F362 { background-position: -380px -380px; } -.emoji-1F363 { background-position: -400px 0; } -.emoji-1F364 { background-position: -400px -20px; } -.emoji-1F365 { background-position: -400px -40px; } -.emoji-1F366 { background-position: -400px -60px; } -.emoji-1F367 { background-position: -400px -80px; } -.emoji-1F368 { background-position: -400px -100px; } -.emoji-1F369 { background-position: -400px -120px; } -.emoji-1F36A { background-position: -400px -140px; } -.emoji-1F36B { background-position: -400px -160px; } -.emoji-1F36C { background-position: -400px -180px; } -.emoji-1F36D { background-position: -400px -200px; } -.emoji-1F36E { background-position: -400px -220px; } -.emoji-1F36F { background-position: -400px -240px; } -.emoji-1F370 { background-position: -400px -260px; } -.emoji-1F371 { background-position: -400px -280px; } -.emoji-1F372 { background-position: -400px -300px; } -.emoji-1F373 { background-position: -400px -320px; } -.emoji-1F374 { background-position: -400px -340px; } -.emoji-1F375 { background-position: -400px -360px; } -.emoji-1F376 { background-position: -400px -380px; } -.emoji-1F377 { background-position: 0 -400px; } -.emoji-1F378 { background-position: -20px -400px; } -.emoji-1F379 { background-position: -40px -400px; } -.emoji-1F37A { background-position: -60px -400px; } -.emoji-1F37B { background-position: -80px -400px; } -.emoji-1F37C { background-position: -100px -400px; } -.emoji-1F37D { background-position: -120px -400px; } -.emoji-1F37E { background-position: -140px -400px; } -.emoji-1F37F { background-position: -160px -400px; } -.emoji-1F380 { background-position: -180px -400px; } -.emoji-1F381 { background-position: -200px -400px; } -.emoji-1F382 { background-position: -220px -400px; } -.emoji-1F383 { background-position: -240px -400px; } -.emoji-1F384 { background-position: -260px -400px; } -.emoji-1F385 { background-position: -280px -400px; } -.emoji-1F385-1F3FB { background-position: -300px -400px; } -.emoji-1F385-1F3FC { background-position: -320px -400px; } -.emoji-1F385-1F3FD { background-position: -340px -400px; } -.emoji-1F385-1F3FE { background-position: -360px -400px; } -.emoji-1F385-1F3FF { background-position: -380px -400px; } -.emoji-1F386 { background-position: -400px -400px; } -.emoji-1F387 { background-position: -420px 0; } -.emoji-1F388 { background-position: -420px -20px; } -.emoji-1F389 { background-position: -420px -40px; } -.emoji-1F38A { background-position: -420px -60px; } -.emoji-1F38B { background-position: -420px -80px; } -.emoji-1F38C { background-position: -420px -100px; } -.emoji-1F38D { background-position: -420px -120px; } -.emoji-1F38E { background-position: -420px -140px; } -.emoji-1F38F { background-position: -420px -160px; } -.emoji-1F390 { background-position: -420px -180px; } -.emoji-1F391 { background-position: -420px -200px; } -.emoji-1F392 { background-position: -420px -220px; } -.emoji-1F393 { background-position: -420px -240px; } -.emoji-1F396 { background-position: -420px -260px; } -.emoji-1F397 { background-position: -420px -280px; } -.emoji-1F399 { background-position: -420px -300px; } -.emoji-1F39A { background-position: -420px -320px; } -.emoji-1F39B { background-position: -420px -340px; } -.emoji-1F39E { background-position: -420px -360px; } -.emoji-1F39F { background-position: -420px -380px; } -.emoji-1F3A0 { background-position: -420px -400px; } -.emoji-1F3A1 { background-position: 0 -420px; } -.emoji-1F3A2 { background-position: -20px -420px; } -.emoji-1F3A3 { background-position: -40px -420px; } -.emoji-1F3A4 { background-position: -60px -420px; } -.emoji-1F3A5 { background-position: -80px -420px; } -.emoji-1F3A6 { background-position: -100px -420px; } -.emoji-1F3A7 { background-position: -120px -420px; } -.emoji-1F3A8 { background-position: -140px -420px; } -.emoji-1F3A9 { background-position: -160px -420px; } -.emoji-1F3AA { background-position: -180px -420px; } -.emoji-1F3AB { background-position: -200px -420px; } -.emoji-1F3AC { background-position: -220px -420px; } -.emoji-1F3AD { background-position: -240px -420px; } -.emoji-1F3AE { background-position: -260px -420px; } -.emoji-1F3AF { background-position: -280px -420px; } -.emoji-1F3B0 { background-position: -300px -420px; } -.emoji-1F3B1 { background-position: -320px -420px; } -.emoji-1F3B2 { background-position: -340px -420px; } -.emoji-1F3B3 { background-position: -360px -420px; } -.emoji-1F3B4 { background-position: -380px -420px; } -.emoji-1F3B5 { background-position: -400px -420px; } -.emoji-1F3B6 { background-position: -420px -420px; } -.emoji-1F3B7 { background-position: -440px 0; } -.emoji-1F3B8 { background-position: -440px -20px; } -.emoji-1F3B9 { background-position: -440px -40px; } -.emoji-1F3BA { background-position: -440px -60px; } -.emoji-1F3BB { background-position: -440px -80px; } -.emoji-1F3BC { background-position: -440px -100px; } -.emoji-1F3BD { background-position: -440px -120px; } -.emoji-1F3BE { background-position: -440px -140px; } -.emoji-1F3BF { background-position: -440px -160px; } -.emoji-1F3C0 { background-position: -440px -180px; } -.emoji-1F3C1 { background-position: -440px -200px; } -.emoji-1F3C2 { background-position: -440px -220px; } -.emoji-1F3C3 { background-position: -440px -240px; } -.emoji-1F3C3-1F3FB { background-position: -440px -260px; } -.emoji-1F3C3-1F3FC { background-position: -440px -280px; } -.emoji-1F3C3-1F3FD { background-position: -440px -300px; } -.emoji-1F3C3-1F3FE { background-position: -440px -320px; } -.emoji-1F3C3-1F3FF { background-position: -440px -340px; } -.emoji-1F3C4 { background-position: -440px -360px; } -.emoji-1F3C4-1F3FB { background-position: -440px -380px; } -.emoji-1F3C4-1F3FC { background-position: -440px -400px; } -.emoji-1F3C4-1F3FD { background-position: -440px -420px; } -.emoji-1F3C4-1F3FE { background-position: 0 -440px; } -.emoji-1F3C4-1F3FF { background-position: -20px -440px; } -.emoji-1F3C5 { background-position: -40px -440px; } -.emoji-1F3C6 { background-position: -60px -440px; } -.emoji-1F3C7 { background-position: -80px -440px; } -.emoji-1F3C7-1F3FB { background-position: -100px -440px; } -.emoji-1F3C7-1F3FC { background-position: -120px -440px; } -.emoji-1F3C7-1F3FD { background-position: -140px -440px; } -.emoji-1F3C7-1F3FE { background-position: -160px -440px; } -.emoji-1F3C7-1F3FF { background-position: -180px -440px; } -.emoji-1F3C8 { background-position: -200px -440px; } -.emoji-1F3C9 { background-position: -220px -440px; } -.emoji-1F3CA { background-position: -240px -440px; } -.emoji-1F3CA-1F3FB { background-position: -260px -440px; } -.emoji-1F3CA-1F3FC { background-position: -280px -440px; } -.emoji-1F3CA-1F3FD { background-position: -300px -440px; } -.emoji-1F3CA-1F3FE { background-position: -320px -440px; } -.emoji-1F3CA-1F3FF { background-position: -340px -440px; } -.emoji-1F3CB { background-position: -360px -440px; } -.emoji-1F3CB-1F3FB { background-position: -380px -440px; } -.emoji-1F3CB-1F3FC { background-position: -400px -440px; } -.emoji-1F3CB-1F3FD { background-position: -420px -440px; } -.emoji-1F3CB-1F3FE { background-position: -440px -440px; } -.emoji-1F3CB-1F3FF { background-position: -460px 0; } -.emoji-1F3CC { background-position: -460px -20px; } -.emoji-1F3CD { background-position: -460px -40px; } -.emoji-1F3CE { background-position: -460px -60px; } -.emoji-1F3CF { background-position: -460px -80px; } -.emoji-1F3D0 { background-position: -460px -100px; } -.emoji-1F3D1 { background-position: -460px -120px; } -.emoji-1F3D2 { background-position: -460px -140px; } -.emoji-1F3D3 { background-position: -460px -160px; } -.emoji-1F3D4 { background-position: -460px -180px; } -.emoji-1F3D5 { background-position: -460px -200px; } -.emoji-1F3D6 { background-position: -460px -220px; } -.emoji-1F3D7 { background-position: -460px -240px; } -.emoji-1F3D8 { background-position: -460px -260px; } -.emoji-1F3D9 { background-position: -460px -280px; } -.emoji-1F3DA { background-position: -460px -300px; } -.emoji-1F3DB { background-position: -460px -320px; } -.emoji-1F3DC { background-position: -460px -340px; } -.emoji-1F3DD { background-position: -460px -360px; } -.emoji-1F3DE { background-position: -460px -380px; } -.emoji-1F3DF { background-position: -460px -400px; } -.emoji-1F3E0 { background-position: -460px -420px; } -.emoji-1F3E1 { background-position: -460px -440px; } -.emoji-1F3E2 { background-position: 0 -460px; } -.emoji-1F3E3 { background-position: -20px -460px; } -.emoji-1F3E4 { background-position: -40px -460px; } -.emoji-1F3E5 { background-position: -60px -460px; } -.emoji-1F3E6 { background-position: -80px -460px; } -.emoji-1F3E7 { background-position: -100px -460px; } -.emoji-1F3E8 { background-position: -120px -460px; } -.emoji-1F3E9 { background-position: -140px -460px; } -.emoji-1F3EA { background-position: -160px -460px; } -.emoji-1F3EB { background-position: -180px -460px; } -.emoji-1F3EC { background-position: -200px -460px; } -.emoji-1F3ED { background-position: -220px -460px; } -.emoji-1F3EE { background-position: -240px -460px; } -.emoji-1F3EF { background-position: -260px -460px; } -.emoji-1F3F0 { background-position: -280px -460px; } -.emoji-1F3F3 { background-position: -300px -460px; } -.emoji-1F3F4 { background-position: -320px -460px; } -.emoji-1F3F5 { background-position: -340px -460px; } -.emoji-1F3F7 { background-position: -360px -460px; } -.emoji-1F3F8 { background-position: -380px -460px; } -.emoji-1F3F9 { background-position: -400px -460px; } -.emoji-1F3FA { background-position: -420px -460px; } -.emoji-1F3FB { background-position: -440px -460px; } -.emoji-1F3FC { background-position: -460px -460px; } -.emoji-1F3FD { background-position: -480px 0; } -.emoji-1F3FE { background-position: -480px -20px; } -.emoji-1F3FF { background-position: -480px -40px; } -.emoji-1F400 { background-position: -480px -60px; } -.emoji-1F401 { background-position: -480px -80px; } -.emoji-1F402 { background-position: -480px -100px; } -.emoji-1F403 { background-position: -480px -120px; } -.emoji-1F404 { background-position: -480px -140px; } -.emoji-1F405 { background-position: -480px -160px; } -.emoji-1F406 { background-position: -480px -180px; } -.emoji-1F407 { background-position: -480px -200px; } -.emoji-1F408 { background-position: -480px -220px; } -.emoji-1F409 { background-position: -480px -240px; } -.emoji-1F40A { background-position: -480px -260px; } -.emoji-1F40B { background-position: -480px -280px; } -.emoji-1F40C { background-position: -480px -300px; } -.emoji-1F40D { background-position: -480px -320px; } -.emoji-1F40E { background-position: -480px -340px; } -.emoji-1F40F { background-position: -480px -360px; } -.emoji-1F410 { background-position: -480px -380px; } -.emoji-1F411 { background-position: -480px -400px; } -.emoji-1F412 { background-position: -480px -420px; } -.emoji-1F413 { background-position: -480px -440px; } -.emoji-1F414 { background-position: -480px -460px; } -.emoji-1F415 { background-position: 0 -480px; } -.emoji-1F416 { background-position: -20px -480px; } -.emoji-1F417 { background-position: -40px -480px; } -.emoji-1F418 { background-position: -60px -480px; } -.emoji-1F419 { background-position: -80px -480px; } -.emoji-1F41A { background-position: -100px -480px; } -.emoji-1F41B { background-position: -120px -480px; } -.emoji-1F41C { background-position: -140px -480px; } -.emoji-1F41D { background-position: -160px -480px; } -.emoji-1F41E { background-position: -180px -480px; } -.emoji-1F41F { background-position: -200px -480px; } -.emoji-1F420 { background-position: -220px -480px; } -.emoji-1F421 { background-position: -240px -480px; } -.emoji-1F422 { background-position: -260px -480px; } -.emoji-1F423 { background-position: -280px -480px; } -.emoji-1F424 { background-position: -300px -480px; } -.emoji-1F425 { background-position: -320px -480px; } -.emoji-1F426 { background-position: -340px -480px; } -.emoji-1F427 { background-position: -360px -480px; } -.emoji-1F428 { background-position: -380px -480px; } -.emoji-1F429 { background-position: -400px -480px; } -.emoji-1F42A { background-position: -420px -480px; } -.emoji-1F42B { background-position: -440px -480px; } -.emoji-1F42C { background-position: -460px -480px; } -.emoji-1F42D { background-position: -480px -480px; } -.emoji-1F42E { background-position: -500px 0; } -.emoji-1F42F { background-position: -500px -20px; } -.emoji-1F430 { background-position: -500px -40px; } -.emoji-1F431 { background-position: -500px -60px; } -.emoji-1F432 { background-position: -500px -80px; } -.emoji-1F433 { background-position: -500px -100px; } -.emoji-1F434 { background-position: -500px -120px; } -.emoji-1F435 { background-position: -500px -140px; } -.emoji-1F436 { background-position: -500px -160px; } -.emoji-1F437 { background-position: -500px -180px; } -.emoji-1F438 { background-position: -500px -200px; } -.emoji-1F439 { background-position: -500px -220px; } -.emoji-1F43A { background-position: -500px -240px; } -.emoji-1F43B { background-position: -500px -260px; } -.emoji-1F43C { background-position: -500px -280px; } -.emoji-1F43D { background-position: -500px -300px; } -.emoji-1F43E { background-position: -500px -320px; } -.emoji-1F43F { background-position: -500px -340px; } -.emoji-1F440 { background-position: -500px -360px; } -.emoji-1F441 { background-position: -500px -380px; } -.emoji-1F441-1F5E8 { background-position: -500px -400px; } -.emoji-1F442 { background-position: -500px -420px; } -.emoji-1F442-1F3FB { background-position: -500px -440px; } -.emoji-1F442-1F3FC { background-position: -500px -460px; } -.emoji-1F442-1F3FD { background-position: -500px -480px; } -.emoji-1F442-1F3FE { background-position: 0 -500px; } -.emoji-1F442-1F3FF { background-position: -20px -500px; } -.emoji-1F443 { background-position: -40px -500px; } -.emoji-1F443-1F3FB { background-position: -60px -500px; } -.emoji-1F443-1F3FC { background-position: -80px -500px; } -.emoji-1F443-1F3FD { background-position: -100px -500px; } -.emoji-1F443-1F3FE { background-position: -120px -500px; } -.emoji-1F443-1F3FF { background-position: -140px -500px; } -.emoji-1F444 { background-position: -160px -500px; } -.emoji-1F445 { background-position: -180px -500px; } -.emoji-1F446 { background-position: -200px -500px; } -.emoji-1F446-1F3FB { background-position: -220px -500px; } -.emoji-1F446-1F3FC { background-position: -240px -500px; } -.emoji-1F446-1F3FD { background-position: -260px -500px; } -.emoji-1F446-1F3FE { background-position: -280px -500px; } -.emoji-1F446-1F3FF { background-position: -300px -500px; } -.emoji-1F447 { background-position: -320px -500px; } -.emoji-1F447-1F3FB { background-position: -340px -500px; } -.emoji-1F447-1F3FC { background-position: -360px -500px; } -.emoji-1F447-1F3FD { background-position: -380px -500px; } -.emoji-1F447-1F3FE { background-position: -400px -500px; } -.emoji-1F447-1F3FF { background-position: -420px -500px; } -.emoji-1F448 { background-position: -440px -500px; } -.emoji-1F448-1F3FB { background-position: -460px -500px; } -.emoji-1F448-1F3FC { background-position: -480px -500px; } -.emoji-1F448-1F3FD { background-position: -500px -500px; } -.emoji-1F448-1F3FE { background-position: -520px 0; } -.emoji-1F448-1F3FF { background-position: -520px -20px; } -.emoji-1F449 { background-position: -520px -40px; } -.emoji-1F449-1F3FB { background-position: -520px -60px; } -.emoji-1F449-1F3FC { background-position: -520px -80px; } -.emoji-1F449-1F3FD { background-position: -520px -100px; } -.emoji-1F449-1F3FE { background-position: -520px -120px; } -.emoji-1F449-1F3FF { background-position: -520px -140px; } -.emoji-1F44A { background-position: -520px -160px; } -.emoji-1F44A-1F3FB { background-position: -520px -180px; } -.emoji-1F44A-1F3FC { background-position: -520px -200px; } -.emoji-1F44A-1F3FD { background-position: -520px -220px; } -.emoji-1F44A-1F3FE { background-position: -520px -240px; } -.emoji-1F44A-1F3FF { background-position: -520px -260px; } -.emoji-1F44B { background-position: -520px -280px; } -.emoji-1F44B-1F3FB { background-position: -520px -300px; } -.emoji-1F44B-1F3FC { background-position: -520px -320px; } -.emoji-1F44B-1F3FD { background-position: -520px -340px; } -.emoji-1F44B-1F3FE { background-position: -520px -360px; } -.emoji-1F44B-1F3FF { background-position: -520px -380px; } -.emoji-1F44C { background-position: -520px -400px; } -.emoji-1F44C-1F3FB { background-position: -520px -420px; } -.emoji-1F44C-1F3FC { background-position: -520px -440px; } -.emoji-1F44C-1F3FD { background-position: -520px -460px; } -.emoji-1F44C-1F3FE { background-position: -520px -480px; } -.emoji-1F44C-1F3FF { background-position: -520px -500px; } -.emoji-1F44D { background-position: 0 -520px; } -.emoji-1F44D-1F3FB { background-position: -20px -520px; } -.emoji-1F44D-1F3FC { background-position: -40px -520px; } -.emoji-1F44D-1F3FD { background-position: -60px -520px; } -.emoji-1F44D-1F3FE { background-position: -80px -520px; } -.emoji-1F44D-1F3FF { background-position: -100px -520px; } -.emoji-1F44E { background-position: -120px -520px; } -.emoji-1F44E-1F3FB { background-position: -140px -520px; } -.emoji-1F44E-1F3FC { background-position: -160px -520px; } -.emoji-1F44E-1F3FD { background-position: -180px -520px; } -.emoji-1F44E-1F3FE { background-position: -200px -520px; } -.emoji-1F44E-1F3FF { background-position: -220px -520px; } -.emoji-1F44F { background-position: -240px -520px; } -.emoji-1F44F-1F3FB { background-position: -260px -520px; } -.emoji-1F44F-1F3FC { background-position: -280px -520px; } -.emoji-1F44F-1F3FD { background-position: -300px -520px; } -.emoji-1F44F-1F3FE { background-position: -320px -520px; } -.emoji-1F44F-1F3FF { background-position: -340px -520px; } -.emoji-1F450 { background-position: -360px -520px; } -.emoji-1F450-1F3FB { background-position: -380px -520px; } -.emoji-1F450-1F3FC { background-position: -400px -520px; } -.emoji-1F450-1F3FD { background-position: -420px -520px; } -.emoji-1F450-1F3FE { background-position: -440px -520px; } -.emoji-1F450-1F3FF { background-position: -460px -520px; } -.emoji-1F451 { background-position: -480px -520px; } -.emoji-1F452 { background-position: -500px -520px; } -.emoji-1F453 { background-position: -520px -520px; } -.emoji-1F454 { background-position: -540px 0; } -.emoji-1F455 { background-position: -540px -20px; } -.emoji-1F456 { background-position: -540px -40px; } -.emoji-1F457 { background-position: -540px -60px; } -.emoji-1F458 { background-position: -540px -80px; } -.emoji-1F459 { background-position: -540px -100px; } -.emoji-1F45A { background-position: -540px -120px; } -.emoji-1F45B { background-position: -540px -140px; } -.emoji-1F45C { background-position: -540px -160px; } -.emoji-1F45D { background-position: -540px -180px; } -.emoji-1F45E { background-position: -540px -200px; } -.emoji-1F45F { background-position: -540px -220px; } -.emoji-1F460 { background-position: -540px -240px; } -.emoji-1F461 { background-position: -540px -260px; } -.emoji-1F462 { background-position: -540px -280px; } -.emoji-1F463 { background-position: -540px -300px; } -.emoji-1F464 { background-position: -540px -320px; } -.emoji-1F465 { background-position: -540px -340px; } -.emoji-1F466 { background-position: -540px -360px; } -.emoji-1F466-1F3FB { background-position: -540px -380px; } -.emoji-1F466-1F3FC { background-position: -540px -400px; } -.emoji-1F466-1F3FD { background-position: -540px -420px; } -.emoji-1F466-1F3FE { background-position: -540px -440px; } -.emoji-1F466-1F3FF { background-position: -540px -460px; } -.emoji-1F467 { background-position: -540px -480px; } -.emoji-1F467-1F3FB { background-position: -540px -500px; } -.emoji-1F467-1F3FC { background-position: -540px -520px; } -.emoji-1F467-1F3FD { background-position: 0 -540px; } -.emoji-1F467-1F3FE { background-position: -20px -540px; } -.emoji-1F467-1F3FF { background-position: -40px -540px; } -.emoji-1F468 { background-position: -60px -540px; } -.emoji-1F468-1F3FB { background-position: -80px -540px; } -.emoji-1F468-1F3FC { background-position: -100px -540px; } -.emoji-1F468-1F3FD { background-position: -120px -540px; } -.emoji-1F468-1F3FE { background-position: -140px -540px; } -.emoji-1F468-1F3FF { background-position: -160px -540px; } -.emoji-1F468-1F468-1F466 { background-position: -180px -540px; } -.emoji-1F468-1F468-1F466-1F466 { background-position: -200px -540px; } -.emoji-1F468-1F468-1F467 { background-position: -220px -540px; } -.emoji-1F468-1F468-1F467-1F466 { background-position: -240px -540px; } -.emoji-1F468-1F468-1F467-1F467 { background-position: -260px -540px; } -.emoji-1F468-1F469-1F466-1F466 { background-position: -280px -540px; } -.emoji-1F468-1F469-1F467 { background-position: -300px -540px; } -.emoji-1F468-1F469-1F467-1F466 { background-position: -320px -540px; } -.emoji-1F468-1F469-1F467-1F467 { background-position: -340px -540px; } -.emoji-1F468-2764-1F468 { background-position: -360px -540px; } -.emoji-1F468-2764-1F48B-1F468 { background-position: -380px -540px; } -.emoji-1F469 { background-position: -400px -540px; } -.emoji-1F469-1F3FB { background-position: -420px -540px; } -.emoji-1F469-1F3FC { background-position: -440px -540px; } -.emoji-1F469-1F3FD { background-position: -460px -540px; } -.emoji-1F469-1F3FE { background-position: -480px -540px; } -.emoji-1F469-1F3FF { background-position: -500px -540px; } -.emoji-1F469-1F469-1F466 { background-position: -520px -540px; } -.emoji-1F469-1F469-1F466-1F466 { background-position: -540px -540px; } -.emoji-1F469-1F469-1F467 { background-position: -560px 0; } -.emoji-1F469-1F469-1F467-1F466 { background-position: -560px -20px; } -.emoji-1F469-1F469-1F467-1F467 { background-position: -560px -40px; } -.emoji-1F469-2764-1F469 { background-position: -560px -60px; } -.emoji-1F469-2764-1F48B-1F469 { background-position: -560px -80px; } -.emoji-1F46A { background-position: -560px -100px; } -.emoji-1F46B { background-position: -560px -120px; } -.emoji-1F46C { background-position: -560px -140px; } -.emoji-1F46D { background-position: -560px -160px; } -.emoji-1F46E { background-position: -560px -180px; } -.emoji-1F46E-1F3FB { background-position: -560px -200px; } -.emoji-1F46E-1F3FC { background-position: -560px -220px; } -.emoji-1F46E-1F3FD { background-position: -560px -240px; } -.emoji-1F46E-1F3FE { background-position: -560px -260px; } -.emoji-1F46E-1F3FF { background-position: -560px -280px; } -.emoji-1F46F { background-position: -560px -300px; } -.emoji-1F470 { background-position: -560px -320px; } -.emoji-1F470-1F3FB { background-position: -560px -340px; } -.emoji-1F470-1F3FC { background-position: -560px -360px; } -.emoji-1F470-1F3FD { background-position: -560px -380px; } -.emoji-1F470-1F3FE { background-position: -560px -400px; } -.emoji-1F470-1F3FF { background-position: -560px -420px; } -.emoji-1F471 { background-position: -560px -440px; } -.emoji-1F471-1F3FB { background-position: -560px -460px; } -.emoji-1F471-1F3FC { background-position: -560px -480px; } -.emoji-1F471-1F3FD { background-position: -560px -500px; } -.emoji-1F471-1F3FE { background-position: -560px -520px; } -.emoji-1F471-1F3FF { background-position: -560px -540px; } -.emoji-1F472 { background-position: 0 -560px; } -.emoji-1F472-1F3FB { background-position: -20px -560px; } -.emoji-1F472-1F3FC { background-position: -40px -560px; } -.emoji-1F472-1F3FD { background-position: -60px -560px; } -.emoji-1F472-1F3FE { background-position: -80px -560px; } -.emoji-1F472-1F3FF { background-position: -100px -560px; } -.emoji-1F473 { background-position: -120px -560px; } -.emoji-1F473-1F3FB { background-position: -140px -560px; } -.emoji-1F473-1F3FC { background-position: -160px -560px; } -.emoji-1F473-1F3FD { background-position: -180px -560px; } -.emoji-1F473-1F3FE { background-position: -200px -560px; } -.emoji-1F473-1F3FF { background-position: -220px -560px; } -.emoji-1F474 { background-position: -240px -560px; } -.emoji-1F474-1F3FB { background-position: -260px -560px; } -.emoji-1F474-1F3FC { background-position: -280px -560px; } -.emoji-1F474-1F3FD { background-position: -300px -560px; } -.emoji-1F474-1F3FE { background-position: -320px -560px; } -.emoji-1F474-1F3FF { background-position: -340px -560px; } -.emoji-1F475 { background-position: -360px -560px; } -.emoji-1F475-1F3FB { background-position: -380px -560px; } -.emoji-1F475-1F3FC { background-position: -400px -560px; } -.emoji-1F475-1F3FD { background-position: -420px -560px; } -.emoji-1F475-1F3FE { background-position: -440px -560px; } -.emoji-1F475-1F3FF { background-position: -460px -560px; } -.emoji-1F476 { background-position: -480px -560px; } -.emoji-1F476-1F3FB { background-position: -500px -560px; } -.emoji-1F476-1F3FC { background-position: -520px -560px; } -.emoji-1F476-1F3FD { background-position: -540px -560px; } -.emoji-1F476-1F3FE { background-position: -560px -560px; } -.emoji-1F476-1F3FF { background-position: -580px 0; } -.emoji-1F477 { background-position: -580px -20px; } -.emoji-1F477-1F3FB { background-position: -580px -40px; } -.emoji-1F477-1F3FC { background-position: -580px -60px; } -.emoji-1F477-1F3FD { background-position: -580px -80px; } -.emoji-1F477-1F3FE { background-position: -580px -100px; } -.emoji-1F477-1F3FF { background-position: -580px -120px; } -.emoji-1F478 { background-position: -580px -140px; } -.emoji-1F478-1F3FB { background-position: -580px -160px; } -.emoji-1F478-1F3FC { background-position: -580px -180px; } -.emoji-1F478-1F3FD { background-position: -580px -200px; } -.emoji-1F478-1F3FE { background-position: -580px -220px; } -.emoji-1F478-1F3FF { background-position: -580px -240px; } -.emoji-1F479 { background-position: -580px -260px; } -.emoji-1F47A { background-position: -580px -280px; } -.emoji-1F47B { background-position: -580px -300px; } -.emoji-1F47C { background-position: -580px -320px; } -.emoji-1F47C-1F3FB { background-position: -580px -340px; } -.emoji-1F47C-1F3FC { background-position: -580px -360px; } -.emoji-1F47C-1F3FD { background-position: -580px -380px; } -.emoji-1F47C-1F3FE { background-position: -580px -400px; } -.emoji-1F47C-1F3FF { background-position: -580px -420px; } -.emoji-1F47D { background-position: -580px -440px; } -.emoji-1F47E { background-position: -580px -460px; } -.emoji-1F47F { background-position: -580px -480px; } -.emoji-1F480 { background-position: -580px -500px; } -.emoji-1F481 { background-position: -580px -520px; } -.emoji-1F481-1F3FB { background-position: -580px -540px; } -.emoji-1F481-1F3FC { background-position: -580px -560px; } -.emoji-1F481-1F3FD { background-position: 0 -580px; } -.emoji-1F481-1F3FE { background-position: -20px -580px; } -.emoji-1F481-1F3FF { background-position: -40px -580px; } -.emoji-1F482 { background-position: -60px -580px; } -.emoji-1F482-1F3FB { background-position: -80px -580px; } -.emoji-1F482-1F3FC { background-position: -100px -580px; } -.emoji-1F482-1F3FD { background-position: -120px -580px; } -.emoji-1F482-1F3FE { background-position: -140px -580px; } -.emoji-1F482-1F3FF { background-position: -160px -580px; } -.emoji-1F483 { background-position: -180px -580px; } -.emoji-1F483-1F3FB { background-position: -200px -580px; } -.emoji-1F483-1F3FC { background-position: -220px -580px; } -.emoji-1F483-1F3FD { background-position: -240px -580px; } -.emoji-1F483-1F3FE { background-position: -260px -580px; } -.emoji-1F483-1F3FF { background-position: -280px -580px; } -.emoji-1F484 { background-position: -300px -580px; } -.emoji-1F485 { background-position: -320px -580px; } -.emoji-1F485-1F3FB { background-position: -340px -580px; } -.emoji-1F485-1F3FC { background-position: -360px -580px; } -.emoji-1F485-1F3FD { background-position: -380px -580px; } -.emoji-1F485-1F3FE { background-position: -400px -580px; } -.emoji-1F485-1F3FF { background-position: -420px -580px; } -.emoji-1F486 { background-position: -440px -580px; } -.emoji-1F486-1F3FB { background-position: -460px -580px; } -.emoji-1F486-1F3FC { background-position: -480px -580px; } -.emoji-1F486-1F3FD { background-position: -500px -580px; } -.emoji-1F486-1F3FE { background-position: -520px -580px; } -.emoji-1F486-1F3FF { background-position: -540px -580px; } -.emoji-1F487 { background-position: -560px -580px; } -.emoji-1F487-1F3FB { background-position: -580px -580px; } -.emoji-1F487-1F3FC { background-position: -600px 0; } -.emoji-1F487-1F3FD { background-position: -600px -20px; } -.emoji-1F487-1F3FE { background-position: -600px -40px; } -.emoji-1F487-1F3FF { background-position: -600px -60px; } -.emoji-1F488 { background-position: -600px -80px; } -.emoji-1F489 { background-position: -600px -100px; } -.emoji-1F48A { background-position: -600px -120px; } -.emoji-1F48B { background-position: -600px -140px; } -.emoji-1F48C { background-position: -600px -160px; } -.emoji-1F48D { background-position: -600px -180px; } -.emoji-1F48E { background-position: -600px -200px; } -.emoji-1F48F { background-position: -600px -220px; } -.emoji-1F490 { background-position: -600px -240px; } -.emoji-1F491 { background-position: -600px -260px; } -.emoji-1F492 { background-position: -600px -280px; } -.emoji-1F493 { background-position: -600px -300px; } -.emoji-1F494 { background-position: -600px -320px; } -.emoji-1F495 { background-position: -600px -340px; } -.emoji-1F496 { background-position: -600px -360px; } -.emoji-1F497 { background-position: -600px -380px; } -.emoji-1F498 { background-position: -600px -400px; } -.emoji-1F499 { background-position: -600px -420px; } -.emoji-1F49A { background-position: -600px -440px; } -.emoji-1F49B { background-position: -600px -460px; } -.emoji-1F49C { background-position: -600px -480px; } -.emoji-1F49D { background-position: -600px -500px; } -.emoji-1F49E { background-position: -600px -520px; } -.emoji-1F49F { background-position: -600px -540px; } -.emoji-1F4A0 { background-position: -600px -560px; } -.emoji-1F4A1 { background-position: -600px -580px; } -.emoji-1F4A2 { background-position: 0 -600px; } -.emoji-1F4A3 { background-position: -20px -600px; } -.emoji-1F4A4 { background-position: -40px -600px; } -.emoji-1F4A5 { background-position: -60px -600px; } -.emoji-1F4A6 { background-position: -80px -600px; } -.emoji-1F4A7 { background-position: -100px -600px; } -.emoji-1F4A8 { background-position: -120px -600px; } -.emoji-1F4A9 { background-position: -140px -600px; } -.emoji-1F4AA { background-position: -160px -600px; } -.emoji-1F4AA-1F3FB { background-position: -180px -600px; } -.emoji-1F4AA-1F3FC { background-position: -200px -600px; } -.emoji-1F4AA-1F3FD { background-position: -220px -600px; } -.emoji-1F4AA-1F3FE { background-position: -240px -600px; } -.emoji-1F4AA-1F3FF { background-position: -260px -600px; } -.emoji-1F4AB { background-position: -280px -600px; } -.emoji-1F4AC { background-position: -300px -600px; } -.emoji-1F4AD { background-position: -320px -600px; } -.emoji-1F4AE { background-position: -340px -600px; } -.emoji-1F4AF { background-position: -360px -600px; } -.emoji-1F4B0 { background-position: -380px -600px; } -.emoji-1F4B1 { background-position: -400px -600px; } -.emoji-1F4B2 { background-position: -420px -600px; } -.emoji-1F4B3 { background-position: -440px -600px; } -.emoji-1F4B4 { background-position: -460px -600px; } -.emoji-1F4B5 { background-position: -480px -600px; } -.emoji-1F4B6 { background-position: -500px -600px; } -.emoji-1F4B7 { background-position: -520px -600px; } -.emoji-1F4B8 { background-position: -540px -600px; } -.emoji-1F4B9 { background-position: -560px -600px; } -.emoji-1F4BA { background-position: -580px -600px; } -.emoji-1F4BB { background-position: -600px -600px; } -.emoji-1F4BC { background-position: -620px 0; } -.emoji-1F4BD { background-position: -620px -20px; } -.emoji-1F4BE { background-position: -620px -40px; } -.emoji-1F4BF { background-position: -620px -60px; } -.emoji-1F4C0 { background-position: -620px -80px; } -.emoji-1F4C1 { background-position: -620px -100px; } -.emoji-1F4C2 { background-position: -620px -120px; } -.emoji-1F4C3 { background-position: -620px -140px; } -.emoji-1F4C4 { background-position: -620px -160px; } -.emoji-1F4C5 { background-position: -620px -180px; } -.emoji-1F4C6 { background-position: -620px -200px; } -.emoji-1F4C7 { background-position: -620px -220px; } -.emoji-1F4C8 { background-position: -620px -240px; } -.emoji-1F4C9 { background-position: -620px -260px; } -.emoji-1F4CA { background-position: -620px -280px; } -.emoji-1F4CB { background-position: -620px -300px; } -.emoji-1F4CC { background-position: -620px -320px; } -.emoji-1F4CD { background-position: -620px -340px; } -.emoji-1F4CE { background-position: -620px -360px; } -.emoji-1F4CF { background-position: -620px -380px; } -.emoji-1F4D0 { background-position: -620px -400px; } -.emoji-1F4D1 { background-position: -620px -420px; } -.emoji-1F4D2 { background-position: -620px -440px; } -.emoji-1F4D3 { background-position: -620px -460px; } -.emoji-1F4D4 { background-position: -620px -480px; } -.emoji-1F4D5 { background-position: -620px -500px; } -.emoji-1F4D6 { background-position: -620px -520px; } -.emoji-1F4D7 { background-position: -620px -540px; } -.emoji-1F4D8 { background-position: -620px -560px; } -.emoji-1F4D9 { background-position: -620px -580px; } -.emoji-1F4DA { background-position: -620px -600px; } -.emoji-1F4DB { background-position: 0 -620px; } -.emoji-1F4DC { background-position: -20px -620px; } -.emoji-1F4DD { background-position: -40px -620px; } -.emoji-1F4DE { background-position: -60px -620px; } -.emoji-1F4DF { background-position: -80px -620px; } -.emoji-1F4E0 { background-position: -100px -620px; } -.emoji-1F4E1 { background-position: -120px -620px; } -.emoji-1F4E2 { background-position: -140px -620px; } -.emoji-1F4E3 { background-position: -160px -620px; } -.emoji-1F4E4 { background-position: -180px -620px; } -.emoji-1F4E5 { background-position: -200px -620px; } -.emoji-1F4E6 { background-position: -220px -620px; } -.emoji-1F4E7 { background-position: -240px -620px; } -.emoji-1F4E8 { background-position: -260px -620px; } -.emoji-1F4E9 { background-position: -280px -620px; } -.emoji-1F4EA { background-position: -300px -620px; } -.emoji-1F4EB { background-position: -320px -620px; } -.emoji-1F4EC { background-position: -340px -620px; } -.emoji-1F4ED { background-position: -360px -620px; } -.emoji-1F4EE { background-position: -380px -620px; } -.emoji-1F4EF { background-position: -400px -620px; } -.emoji-1F4F0 { background-position: -420px -620px; } -.emoji-1F4F1 { background-position: -440px -620px; } -.emoji-1F4F2 { background-position: -460px -620px; } -.emoji-1F4F3 { background-position: -480px -620px; } -.emoji-1F4F4 { background-position: -500px -620px; } -.emoji-1F4F5 { background-position: -520px -620px; } -.emoji-1F4F6 { background-position: -540px -620px; } -.emoji-1F4F7 { background-position: -560px -620px; } -.emoji-1F4F8 { background-position: -580px -620px; } -.emoji-1F4F9 { background-position: -600px -620px; } -.emoji-1F4FA { background-position: -620px -620px; } -.emoji-1F4FB { background-position: -640px 0; } -.emoji-1F4FC { background-position: -640px -20px; } -.emoji-1F4FD { background-position: -640px -40px; } -.emoji-1F4FF { background-position: -640px -60px; } -.emoji-1F500 { background-position: -640px -80px; } -.emoji-1F501 { background-position: -640px -100px; } -.emoji-1F502 { background-position: -640px -120px; } -.emoji-1F503 { background-position: -640px -140px; } -.emoji-1F504 { background-position: -640px -160px; } -.emoji-1F505 { background-position: -640px -180px; } -.emoji-1F506 { background-position: -640px -200px; } -.emoji-1F507 { background-position: -640px -220px; } -.emoji-1F508 { background-position: -640px -240px; } -.emoji-1F509 { background-position: -640px -260px; } -.emoji-1F50A { background-position: -640px -280px; } -.emoji-1F50B { background-position: -640px -300px; } -.emoji-1F50C { background-position: -640px -320px; } -.emoji-1F50D { background-position: -640px -340px; } -.emoji-1F50E { background-position: -640px -360px; } -.emoji-1F50F { background-position: -640px -380px; } -.emoji-1F510 { background-position: -640px -400px; } -.emoji-1F511 { background-position: -640px -420px; } -.emoji-1F512 { background-position: -640px -440px; } -.emoji-1F513 { background-position: -640px -460px; } -.emoji-1F514 { background-position: -640px -480px; } -.emoji-1F515 { background-position: -640px -500px; } -.emoji-1F516 { background-position: -640px -520px; } -.emoji-1F517 { background-position: -640px -540px; } -.emoji-1F518 { background-position: -640px -560px; } -.emoji-1F519 { background-position: -640px -580px; } -.emoji-1F51A { background-position: -640px -600px; } -.emoji-1F51B { background-position: -640px -620px; } -.emoji-1F51C { background-position: 0 -640px; } -.emoji-1F51D { background-position: -20px -640px; } -.emoji-1F51E { background-position: -40px -640px; } -.emoji-1F51F { background-position: -60px -640px; } -.emoji-1F520 { background-position: -80px -640px; } -.emoji-1F521 { background-position: -100px -640px; } -.emoji-1F522 { background-position: -120px -640px; } -.emoji-1F523 { background-position: -140px -640px; } -.emoji-1F524 { background-position: -160px -640px; } -.emoji-1F525 { background-position: -180px -640px; } -.emoji-1F526 { background-position: -200px -640px; } -.emoji-1F527 { background-position: -220px -640px; } -.emoji-1F528 { background-position: -240px -640px; } -.emoji-1F529 { background-position: -260px -640px; } -.emoji-1F52A { background-position: -280px -640px; } -.emoji-1F52B { background-position: -300px -640px; } -.emoji-1F52C { background-position: -320px -640px; } -.emoji-1F52D { background-position: -340px -640px; } -.emoji-1F52E { background-position: -360px -640px; } -.emoji-1F52F { background-position: -380px -640px; } -.emoji-1F530 { background-position: -400px -640px; } -.emoji-1F531 { background-position: -420px -640px; } -.emoji-1F532 { background-position: -440px -640px; } -.emoji-1F533 { background-position: -460px -640px; } -.emoji-1F534 { background-position: -480px -640px; } -.emoji-1F535 { background-position: -500px -640px; } -.emoji-1F536 { background-position: -520px -640px; } -.emoji-1F537 { background-position: -540px -640px; } -.emoji-1F538 { background-position: -560px -640px; } -.emoji-1F539 { background-position: -580px -640px; } -.emoji-1F53A { background-position: -600px -640px; } -.emoji-1F53B { background-position: -620px -640px; } -.emoji-1F53C { background-position: -640px -640px; } -.emoji-1F53D { background-position: -660px 0; } -.emoji-1F549 { background-position: -660px -20px; } -.emoji-1F54A { background-position: -660px -40px; } -.emoji-1F54B { background-position: -660px -60px; } -.emoji-1F54C { background-position: -660px -80px; } -.emoji-1F54D { background-position: -660px -100px; } -.emoji-1F54E { background-position: -660px -120px; } -.emoji-1F550 { background-position: -660px -140px; } -.emoji-1F551 { background-position: -660px -160px; } -.emoji-1F552 { background-position: -660px -180px; } -.emoji-1F553 { background-position: -660px -200px; } -.emoji-1F554 { background-position: -660px -220px; } -.emoji-1F555 { background-position: -660px -240px; } -.emoji-1F556 { background-position: -660px -260px; } -.emoji-1F557 { background-position: -660px -280px; } -.emoji-1F558 { background-position: -660px -300px; } -.emoji-1F559 { background-position: -660px -320px; } -.emoji-1F55A { background-position: -660px -340px; } -.emoji-1F55B { background-position: -660px -360px; } -.emoji-1F55C { background-position: -660px -380px; } -.emoji-1F55D { background-position: -660px -400px; } -.emoji-1F55E { background-position: -660px -420px; } -.emoji-1F55F { background-position: -660px -440px; } -.emoji-1F560 { background-position: -660px -460px; } -.emoji-1F561 { background-position: -660px -480px; } -.emoji-1F562 { background-position: -660px -500px; } -.emoji-1F563 { background-position: -660px -520px; } -.emoji-1F564 { background-position: -660px -540px; } -.emoji-1F565 { background-position: -660px -560px; } -.emoji-1F566 { background-position: -660px -580px; } -.emoji-1F567 { background-position: -660px -600px; } -.emoji-1F56F { background-position: -660px -620px; } -.emoji-1F570 { background-position: -660px -640px; } -.emoji-1F573 { background-position: 0 -660px; } -.emoji-1F574 { background-position: -20px -660px; } -.emoji-1F575 { background-position: -40px -660px; } -.emoji-1F575-1F3FB { background-position: -60px -660px; } -.emoji-1F575-1F3FC { background-position: -80px -660px; } -.emoji-1F575-1F3FD { background-position: -100px -660px; } -.emoji-1F575-1F3FE { background-position: -120px -660px; } -.emoji-1F575-1F3FF { background-position: -140px -660px; } -.emoji-1F576 { background-position: -160px -660px; } -.emoji-1F577 { background-position: -180px -660px; } -.emoji-1F578 { background-position: -200px -660px; } -.emoji-1F579 { background-position: -220px -660px; } -.emoji-1F57A { background-position: -240px -660px; } -.emoji-1F57A-1F3FB { background-position: -260px -660px; } -.emoji-1F57A-1F3FC { background-position: -280px -660px; } -.emoji-1F57A-1F3FD { background-position: -300px -660px; } -.emoji-1F57A-1F3FE { background-position: -320px -660px; } -.emoji-1F57A-1F3FF { background-position: -340px -660px; } -.emoji-1F587 { background-position: -360px -660px; } -.emoji-1F58A { background-position: -380px -660px; } -.emoji-1F58B { background-position: -400px -660px; } -.emoji-1F58C { background-position: -420px -660px; } -.emoji-1F58D { background-position: -440px -660px; } -.emoji-1F590 { background-position: -460px -660px; } -.emoji-1F590-1F3FB { background-position: -480px -660px; } -.emoji-1F590-1F3FC { background-position: -500px -660px; } -.emoji-1F590-1F3FD { background-position: -520px -660px; } -.emoji-1F590-1F3FE { background-position: -540px -660px; } -.emoji-1F590-1F3FF { background-position: -560px -660px; } -.emoji-1F595 { background-position: -580px -660px; } -.emoji-1F595-1F3FB { background-position: -600px -660px; } -.emoji-1F595-1F3FC { background-position: -620px -660px; } -.emoji-1F595-1F3FD { background-position: -640px -660px; } -.emoji-1F595-1F3FE { background-position: -660px -660px; } -.emoji-1F595-1F3FF { background-position: -680px 0; } -.emoji-1F596 { background-position: -680px -20px; } -.emoji-1F596-1F3FB { background-position: -680px -40px; } -.emoji-1F596-1F3FC { background-position: -680px -60px; } -.emoji-1F596-1F3FD { background-position: -680px -80px; } -.emoji-1F596-1F3FE { background-position: -680px -100px; } -.emoji-1F596-1F3FF { background-position: -680px -120px; } -.emoji-1F5A4 { background-position: -680px -140px; } -.emoji-1F5A5 { background-position: -680px -160px; } -.emoji-1F5A8 { background-position: -680px -180px; } -.emoji-1F5B1 { background-position: -680px -200px; } -.emoji-1F5B2 { background-position: -680px -220px; } -.emoji-1F5BC { background-position: -680px -240px; } -.emoji-1F5C2 { background-position: -680px -260px; } -.emoji-1F5C3 { background-position: -680px -280px; } -.emoji-1F5C4 { background-position: -680px -300px; } -.emoji-1F5D1 { background-position: -680px -320px; } -.emoji-1F5D2 { background-position: -680px -340px; } -.emoji-1F5D3 { background-position: -680px -360px; } -.emoji-1F5DC { background-position: -680px -380px; } -.emoji-1F5DD { background-position: -680px -400px; } -.emoji-1F5DE { background-position: -680px -420px; } -.emoji-1F5E1 { background-position: -680px -440px; } -.emoji-1F5E3 { background-position: -680px -460px; } -.emoji-1F5EF { background-position: -680px -480px; } -.emoji-1F5F3 { background-position: -680px -500px; } -.emoji-1F5FA { background-position: -680px -520px; } -.emoji-1F5FB { background-position: -680px -540px; } -.emoji-1F5FC { background-position: -680px -560px; } -.emoji-1F5FD { background-position: -680px -580px; } -.emoji-1F5FE { background-position: -680px -600px; } -.emoji-1F5FF { background-position: -680px -620px; } -.emoji-1F600 { background-position: -680px -640px; } -.emoji-1F601 { background-position: -680px -660px; } -.emoji-1F602 { background-position: 0 -680px; } -.emoji-1F603 { background-position: -20px -680px; } -.emoji-1F604 { background-position: -40px -680px; } -.emoji-1F605 { background-position: -60px -680px; } -.emoji-1F606 { background-position: -80px -680px; } -.emoji-1F607 { background-position: -100px -680px; } -.emoji-1F608 { background-position: -120px -680px; } -.emoji-1F609 { background-position: -140px -680px; } -.emoji-1F60A { background-position: -160px -680px; } -.emoji-1F60B { background-position: -180px -680px; } -.emoji-1F60C { background-position: -200px -680px; } -.emoji-1F60D { background-position: -220px -680px; } -.emoji-1F60E { background-position: -240px -680px; } -.emoji-1F60F { background-position: -260px -680px; } -.emoji-1F610 { background-position: -280px -680px; } -.emoji-1F611 { background-position: -300px -680px; } -.emoji-1F612 { background-position: -320px -680px; } -.emoji-1F613 { background-position: -340px -680px; } -.emoji-1F614 { background-position: -360px -680px; } -.emoji-1F615 { background-position: -380px -680px; } -.emoji-1F616 { background-position: -400px -680px; } -.emoji-1F617 { background-position: -420px -680px; } -.emoji-1F618 { background-position: -440px -680px; } -.emoji-1F619 { background-position: -460px -680px; } -.emoji-1F61A { background-position: -480px -680px; } -.emoji-1F61B { background-position: -500px -680px; } -.emoji-1F61C { background-position: -520px -680px; } -.emoji-1F61D { background-position: -540px -680px; } -.emoji-1F61E { background-position: -560px -680px; } -.emoji-1F61F { background-position: -580px -680px; } -.emoji-1F620 { background-position: -600px -680px; } -.emoji-1F621 { background-position: -620px -680px; } -.emoji-1F622 { background-position: -640px -680px; } -.emoji-1F623 { background-position: -660px -680px; } -.emoji-1F624 { background-position: -680px -680px; } -.emoji-1F625 { background-position: -700px 0; } -.emoji-1F626 { background-position: -700px -20px; } -.emoji-1F627 { background-position: -700px -40px; } -.emoji-1F628 { background-position: -700px -60px; } -.emoji-1F629 { background-position: -700px -80px; } -.emoji-1F62A { background-position: -700px -100px; } -.emoji-1F62B { background-position: -700px -120px; } -.emoji-1F62C { background-position: -700px -140px; } -.emoji-1F62D { background-position: -700px -160px; } -.emoji-1F62E { background-position: -700px -180px; } -.emoji-1F62F { background-position: -700px -200px; } -.emoji-1F630 { background-position: -700px -220px; } -.emoji-1F631 { background-position: -700px -240px; } -.emoji-1F632 { background-position: -700px -260px; } -.emoji-1F633 { background-position: -700px -280px; } -.emoji-1F634 { background-position: -700px -300px; } -.emoji-1F635 { background-position: -700px -320px; } -.emoji-1F636 { background-position: -700px -340px; } -.emoji-1F637 { background-position: -700px -360px; } -.emoji-1F638 { background-position: -700px -380px; } -.emoji-1F639 { background-position: -700px -400px; } -.emoji-1F63A { background-position: -700px -420px; } -.emoji-1F63B { background-position: -700px -440px; } -.emoji-1F63C { background-position: -700px -460px; } -.emoji-1F63D { background-position: -700px -480px; } -.emoji-1F63E { background-position: -700px -500px; } -.emoji-1F63F { background-position: -700px -520px; } -.emoji-1F640 { background-position: -700px -540px; } -.emoji-1F641 { background-position: -700px -560px; } -.emoji-1F642 { background-position: -700px -580px; } -.emoji-1F643 { background-position: -700px -600px; } -.emoji-1F644 { background-position: -700px -620px; } -.emoji-1F645 { background-position: -700px -640px; } -.emoji-1F645-1F3FB { background-position: -700px -660px; } -.emoji-1F645-1F3FC { background-position: -700px -680px; } -.emoji-1F645-1F3FD { background-position: 0 -700px; } -.emoji-1F645-1F3FE { background-position: -20px -700px; } -.emoji-1F645-1F3FF { background-position: -40px -700px; } -.emoji-1F646 { background-position: -60px -700px; } -.emoji-1F646-1F3FB { background-position: -80px -700px; } -.emoji-1F646-1F3FC { background-position: -100px -700px; } -.emoji-1F646-1F3FD { background-position: -120px -700px; } -.emoji-1F646-1F3FE { background-position: -140px -700px; } -.emoji-1F646-1F3FF { background-position: -160px -700px; } -.emoji-1F647 { background-position: -180px -700px; } -.emoji-1F647-1F3FB { background-position: -200px -700px; } -.emoji-1F647-1F3FC { background-position: -220px -700px; } -.emoji-1F647-1F3FD { background-position: -240px -700px; } -.emoji-1F647-1F3FE { background-position: -260px -700px; } -.emoji-1F647-1F3FF { background-position: -280px -700px; } -.emoji-1F648 { background-position: -300px -700px; } -.emoji-1F649 { background-position: -320px -700px; } -.emoji-1F64A { background-position: -340px -700px; } -.emoji-1F64B { background-position: -360px -700px; } -.emoji-1F64B-1F3FB { background-position: -380px -700px; } -.emoji-1F64B-1F3FC { background-position: -400px -700px; } -.emoji-1F64B-1F3FD { background-position: -420px -700px; } -.emoji-1F64B-1F3FE { background-position: -440px -700px; } -.emoji-1F64B-1F3FF { background-position: -460px -700px; } -.emoji-1F64C { background-position: -480px -700px; } -.emoji-1F64C-1F3FB { background-position: -500px -700px; } -.emoji-1F64C-1F3FC { background-position: -520px -700px; } -.emoji-1F64C-1F3FD { background-position: -540px -700px; } -.emoji-1F64C-1F3FE { background-position: -560px -700px; } -.emoji-1F64C-1F3FF { background-position: -580px -700px; } -.emoji-1F64D { background-position: -600px -700px; } -.emoji-1F64D-1F3FB { background-position: -620px -700px; } -.emoji-1F64D-1F3FC { background-position: -640px -700px; } -.emoji-1F64D-1F3FD { background-position: -660px -700px; } -.emoji-1F64D-1F3FE { background-position: -680px -700px; } -.emoji-1F64D-1F3FF { background-position: -700px -700px; } -.emoji-1F64E { background-position: -720px 0; } -.emoji-1F64E-1F3FB { background-position: -720px -20px; } -.emoji-1F64E-1F3FC { background-position: -720px -40px; } -.emoji-1F64E-1F3FD { background-position: -720px -60px; } -.emoji-1F64E-1F3FE { background-position: -720px -80px; } -.emoji-1F64E-1F3FF { background-position: -720px -100px; } -.emoji-1F64F { background-position: -720px -120px; } -.emoji-1F64F-1F3FB { background-position: -720px -140px; } -.emoji-1F64F-1F3FC { background-position: -720px -160px; } -.emoji-1F64F-1F3FD { background-position: -720px -180px; } -.emoji-1F64F-1F3FE { background-position: -720px -200px; } -.emoji-1F64F-1F3FF { background-position: -720px -220px; } -.emoji-1F680 { background-position: -720px -240px; } -.emoji-1F681 { background-position: -720px -260px; } -.emoji-1F682 { background-position: -720px -280px; } -.emoji-1F683 { background-position: -720px -300px; } -.emoji-1F684 { background-position: -720px -320px; } -.emoji-1F685 { background-position: -720px -340px; } -.emoji-1F686 { background-position: -720px -360px; } -.emoji-1F687 { background-position: -720px -380px; } -.emoji-1F688 { background-position: -720px -400px; } -.emoji-1F689 { background-position: -720px -420px; } -.emoji-1F68A { background-position: -720px -440px; } -.emoji-1F68B { background-position: -720px -460px; } -.emoji-1F68C { background-position: -720px -480px; } -.emoji-1F68D { background-position: -720px -500px; } -.emoji-1F68E { background-position: -720px -520px; } -.emoji-1F68F { background-position: -720px -540px; } -.emoji-1F690 { background-position: -720px -560px; } -.emoji-1F691 { background-position: -720px -580px; } -.emoji-1F692 { background-position: -720px -600px; } -.emoji-1F693 { background-position: -720px -620px; } -.emoji-1F694 { background-position: -720px -640px; } -.emoji-1F695 { background-position: -720px -660px; } -.emoji-1F696 { background-position: -720px -680px; } -.emoji-1F697 { background-position: -720px -700px; } -.emoji-1F698 { background-position: 0 -720px; } -.emoji-1F699 { background-position: -20px -720px; } -.emoji-1F69A { background-position: -40px -720px; } -.emoji-1F69B { background-position: -60px -720px; } -.emoji-1F69C { background-position: -80px -720px; } -.emoji-1F69D { background-position: -100px -720px; } -.emoji-1F69E { background-position: -120px -720px; } -.emoji-1F69F { background-position: -140px -720px; } -.emoji-1F6A0 { background-position: -160px -720px; } -.emoji-1F6A1 { background-position: -180px -720px; } -.emoji-1F6A2 { background-position: -200px -720px; } -.emoji-1F6A3 { background-position: -220px -720px; } -.emoji-1F6A3-1F3FB { background-position: -240px -720px; } -.emoji-1F6A3-1F3FC { background-position: -260px -720px; } -.emoji-1F6A3-1F3FD { background-position: -280px -720px; } -.emoji-1F6A3-1F3FE { background-position: -300px -720px; } -.emoji-1F6A3-1F3FF { background-position: -320px -720px; } -.emoji-1F6A4 { background-position: -340px -720px; } -.emoji-1F6A5 { background-position: -360px -720px; } -.emoji-1F6A6 { background-position: -380px -720px; } -.emoji-1F6A7 { background-position: -400px -720px; } -.emoji-1F6A8 { background-position: -420px -720px; } -.emoji-1F6A9 { background-position: -440px -720px; } -.emoji-1F6AA { background-position: -460px -720px; } -.emoji-1F6AB { background-position: -480px -720px; } -.emoji-1F6AC { background-position: -500px -720px; } -.emoji-1F6AD { background-position: -520px -720px; } -.emoji-1F6AE { background-position: -540px -720px; } -.emoji-1F6AF { background-position: -560px -720px; } -.emoji-1F6B0 { background-position: -580px -720px; } -.emoji-1F6B1 { background-position: -600px -720px; } -.emoji-1F6B2 { background-position: -620px -720px; } -.emoji-1F6B3 { background-position: -640px -720px; } -.emoji-1F6B4 { background-position: -660px -720px; } -.emoji-1F6B4-1F3FB { background-position: -680px -720px; } -.emoji-1F6B4-1F3FC { background-position: -700px -720px; } -.emoji-1F6B4-1F3FD { background-position: -720px -720px; } -.emoji-1F6B4-1F3FE { background-position: -740px 0; } -.emoji-1F6B4-1F3FF { background-position: -740px -20px; } -.emoji-1F6B5 { background-position: -740px -40px; } -.emoji-1F6B5-1F3FB { background-position: -740px -60px; } -.emoji-1F6B5-1F3FC { background-position: -740px -80px; } -.emoji-1F6B5-1F3FD { background-position: -740px -100px; } -.emoji-1F6B5-1F3FE { background-position: -740px -120px; } -.emoji-1F6B5-1F3FF { background-position: -740px -140px; } -.emoji-1F6B6 { background-position: -740px -160px; } -.emoji-1F6B6-1F3FB { background-position: -740px -180px; } -.emoji-1F6B6-1F3FC { background-position: -740px -200px; } -.emoji-1F6B6-1F3FD { background-position: -740px -220px; } -.emoji-1F6B6-1F3FE { background-position: -740px -240px; } -.emoji-1F6B6-1F3FF { background-position: -740px -260px; } -.emoji-1F6B7 { background-position: -740px -280px; } -.emoji-1F6B8 { background-position: -740px -300px; } -.emoji-1F6B9 { background-position: -740px -320px; } -.emoji-1F6BA { background-position: -740px -340px; } -.emoji-1F6BB { background-position: -740px -360px; } -.emoji-1F6BC { background-position: -740px -380px; } -.emoji-1F6BD { background-position: -740px -400px; } -.emoji-1F6BE { background-position: -740px -420px; } -.emoji-1F6BF { background-position: -740px -440px; } -.emoji-1F6C0 { background-position: -740px -460px; } -.emoji-1F6C0-1F3FB { background-position: -740px -480px; } -.emoji-1F6C0-1F3FC { background-position: -740px -500px; } -.emoji-1F6C0-1F3FD { background-position: -740px -520px; } -.emoji-1F6C0-1F3FE { background-position: -740px -540px; } -.emoji-1F6C0-1F3FF { background-position: -740px -560px; } -.emoji-1F6C1 { background-position: -740px -580px; } -.emoji-1F6C2 { background-position: -740px -600px; } -.emoji-1F6C3 { background-position: -740px -620px; } -.emoji-1F6C4 { background-position: -740px -640px; } -.emoji-1F6C5 { background-position: -740px -660px; } -.emoji-1F6CB { background-position: -740px -680px; } -.emoji-1F6CC { background-position: -740px -700px; } -.emoji-1F6CD { background-position: -740px -720px; } -.emoji-1F6CE { background-position: 0 -740px; } -.emoji-1F6CF { background-position: -20px -740px; } -.emoji-1F6D0 { background-position: -40px -740px; } -.emoji-1F6D1 { background-position: -60px -740px; } -.emoji-1F6D2 { background-position: -80px -740px; } -.emoji-1F6E0 { background-position: -100px -740px; } -.emoji-1F6E1 { background-position: -120px -740px; } -.emoji-1F6E2 { background-position: -140px -740px; } -.emoji-1F6E3 { background-position: -160px -740px; } -.emoji-1F6E4 { background-position: -180px -740px; } -.emoji-1F6E5 { background-position: -200px -740px; } -.emoji-1F6E9 { background-position: -220px -740px; } -.emoji-1F6EB { background-position: -240px -740px; } -.emoji-1F6EC { background-position: -260px -740px; } -.emoji-1F6F0 { background-position: -280px -740px; } -.emoji-1F6F3 { background-position: -300px -740px; } -.emoji-1F6F4 { background-position: -320px -740px; } -.emoji-1F6F5 { background-position: -340px -740px; } -.emoji-1F6F6 { background-position: -360px -740px; } -.emoji-1F910 { background-position: -380px -740px; } -.emoji-1F911 { background-position: -400px -740px; } -.emoji-1F912 { background-position: -420px -740px; } -.emoji-1F913 { background-position: -440px -740px; } -.emoji-1F914 { background-position: -460px -740px; } -.emoji-1F915 { background-position: -480px -740px; } -.emoji-1F916 { background-position: -500px -740px; } -.emoji-1F917 { background-position: -520px -740px; } -.emoji-1F918 { background-position: -540px -740px; } -.emoji-1F918-1F3FB { background-position: -560px -740px; } -.emoji-1F918-1F3FC { background-position: -580px -740px; } -.emoji-1F918-1F3FD { background-position: -600px -740px; } -.emoji-1F918-1F3FE { background-position: -620px -740px; } -.emoji-1F918-1F3FF { background-position: -640px -740px; } -.emoji-1F919 { background-position: -660px -740px; } -.emoji-1F919-1F3FB { background-position: -680px -740px; } -.emoji-1F919-1F3FC { background-position: -700px -740px; } -.emoji-1F919-1F3FD { background-position: -720px -740px; } -.emoji-1F919-1F3FE { background-position: -740px -740px; } -.emoji-1F919-1F3FF { background-position: -760px 0; } -.emoji-1F91A { background-position: -760px -20px; } -.emoji-1F91A-1F3FB { background-position: -760px -40px; } -.emoji-1F91A-1F3FC { background-position: -760px -60px; } -.emoji-1F91A-1F3FD { background-position: -760px -80px; } -.emoji-1F91A-1F3FE { background-position: -760px -100px; } -.emoji-1F91A-1F3FF { background-position: -760px -120px; } -.emoji-1F91B { background-position: -760px -140px; } -.emoji-1F91B-1F3FB { background-position: -760px -160px; } -.emoji-1F91B-1F3FC { background-position: -760px -180px; } -.emoji-1F91B-1F3FD { background-position: -760px -200px; } -.emoji-1F91B-1F3FE { background-position: -760px -220px; } -.emoji-1F91B-1F3FF { background-position: -760px -240px; } -.emoji-1F91C { background-position: -760px -260px; } -.emoji-1F91C-1F3FB { background-position: -760px -280px; } -.emoji-1F91C-1F3FC { background-position: -760px -300px; } -.emoji-1F91C-1F3FD { background-position: -760px -320px; } -.emoji-1F91C-1F3FE { background-position: -760px -340px; } -.emoji-1F91C-1F3FF { background-position: -760px -360px; } -.emoji-1F91D { background-position: -760px -380px; } -.emoji-1F91D-1F3FB { background-position: -760px -400px; } -.emoji-1F91D-1F3FC { background-position: -760px -420px; } -.emoji-1F91D-1F3FD { background-position: -760px -440px; } -.emoji-1F91D-1F3FE { background-position: -760px -460px; } -.emoji-1F91D-1F3FF { background-position: -760px -480px; } -.emoji-1F91E { background-position: -760px -500px; } -.emoji-1F91E-1F3FB { background-position: -760px -520px; } -.emoji-1F91E-1F3FC { background-position: -760px -540px; } -.emoji-1F91E-1F3FD { background-position: -760px -560px; } -.emoji-1F91E-1F3FE { background-position: -760px -580px; } -.emoji-1F91E-1F3FF { background-position: -760px -600px; } -.emoji-1F920 { background-position: -760px -620px; } -.emoji-1F921 { background-position: -760px -640px; } -.emoji-1F922 { background-position: -760px -660px; } -.emoji-1F923 { background-position: -760px -680px; } -.emoji-1F924 { background-position: -760px -700px; } -.emoji-1F925 { background-position: -760px -720px; } -.emoji-1F926 { background-position: -760px -740px; } -.emoji-1F926-1F3FB { background-position: 0 -760px; } -.emoji-1F926-1F3FC { background-position: -20px -760px; } -.emoji-1F926-1F3FD { background-position: -40px -760px; } -.emoji-1F926-1F3FE { background-position: -60px -760px; } -.emoji-1F926-1F3FF { background-position: -80px -760px; } -.emoji-1F927 { background-position: -100px -760px; } -.emoji-1F930 { background-position: -120px -760px; } -.emoji-1F930-1F3FB { background-position: -140px -760px; } -.emoji-1F930-1F3FC { background-position: -160px -760px; } -.emoji-1F930-1F3FD { background-position: -180px -760px; } -.emoji-1F930-1F3FE { background-position: -200px -760px; } -.emoji-1F930-1F3FF { background-position: -220px -760px; } -.emoji-1F933 { background-position: -240px -760px; } -.emoji-1F933-1F3FB { background-position: -260px -760px; } -.emoji-1F933-1F3FC { background-position: -280px -760px; } -.emoji-1F933-1F3FD { background-position: -300px -760px; } -.emoji-1F933-1F3FE { background-position: -320px -760px; } -.emoji-1F933-1F3FF { background-position: -340px -760px; } -.emoji-1F934 { background-position: -360px -760px; } -.emoji-1F934-1F3FB { background-position: -380px -760px; } -.emoji-1F934-1F3FC { background-position: -400px -760px; } -.emoji-1F934-1F3FD { background-position: -420px -760px; } -.emoji-1F934-1F3FE { background-position: -440px -760px; } -.emoji-1F934-1F3FF { background-position: -460px -760px; } -.emoji-1F935 { background-position: -480px -760px; } -.emoji-1F935-1F3FB { background-position: -500px -760px; } -.emoji-1F935-1F3FC { background-position: -520px -760px; } -.emoji-1F935-1F3FD { background-position: -540px -760px; } -.emoji-1F935-1F3FE { background-position: -560px -760px; } -.emoji-1F935-1F3FF { background-position: -580px -760px; } -.emoji-1F936 { background-position: -600px -760px; } -.emoji-1F936-1F3FB { background-position: -620px -760px; } -.emoji-1F936-1F3FC { background-position: -640px -760px; } -.emoji-1F936-1F3FD { background-position: -660px -760px; } -.emoji-1F936-1F3FE { background-position: -680px -760px; } -.emoji-1F936-1F3FF { background-position: -700px -760px; } -.emoji-1F937 { background-position: -720px -760px; } -.emoji-1F937-1F3FB { background-position: -740px -760px; } -.emoji-1F937-1F3FC { background-position: -760px -760px; } -.emoji-1F937-1F3FD { background-position: -780px 0; } -.emoji-1F937-1F3FE { background-position: -780px -20px; } -.emoji-1F937-1F3FF { background-position: -780px -40px; } -.emoji-1F938 { background-position: -780px -60px; } -.emoji-1F938-1F3FB { background-position: -780px -80px; } -.emoji-1F938-1F3FC { background-position: -780px -100px; } -.emoji-1F938-1F3FD { background-position: -780px -120px; } -.emoji-1F938-1F3FE { background-position: -780px -140px; } -.emoji-1F938-1F3FF { background-position: -780px -160px; } -.emoji-1F939 { background-position: -780px -180px; } -.emoji-1F939-1F3FB { background-position: -780px -200px; } -.emoji-1F939-1F3FC { background-position: -780px -220px; } -.emoji-1F939-1F3FD { background-position: -780px -240px; } -.emoji-1F939-1F3FE { background-position: -780px -260px; } -.emoji-1F939-1F3FF { background-position: -780px -280px; } -.emoji-1F93A { background-position: -780px -300px; } -.emoji-1F93C { background-position: -780px -320px; } -.emoji-1F93C-1F3FB { background-position: -780px -340px; } -.emoji-1F93C-1F3FC { background-position: -780px -360px; } -.emoji-1F93C-1F3FD { background-position: -780px -380px; } -.emoji-1F93C-1F3FE { background-position: -780px -400px; } -.emoji-1F93C-1F3FF { background-position: -780px -420px; } -.emoji-1F93D { background-position: -780px -440px; } -.emoji-1F93D-1F3FB { background-position: -780px -460px; } -.emoji-1F93D-1F3FC { background-position: -780px -480px; } -.emoji-1F93D-1F3FD { background-position: -780px -500px; } -.emoji-1F93D-1F3FE { background-position: -780px -520px; } -.emoji-1F93D-1F3FF { background-position: -780px -540px; } -.emoji-1F93E { background-position: -780px -560px; } -.emoji-1F93E-1F3FB { background-position: -780px -580px; } -.emoji-1F93E-1F3FC { background-position: -780px -600px; } -.emoji-1F93E-1F3FD { background-position: -780px -620px; } -.emoji-1F93E-1F3FE { background-position: -780px -640px; } -.emoji-1F93E-1F3FF { background-position: -780px -660px; } -.emoji-1F940 { background-position: -780px -680px; } -.emoji-1F941 { background-position: -780px -700px; } -.emoji-1F942 { background-position: -780px -720px; } -.emoji-1F943 { background-position: -780px -740px; } -.emoji-1F944 { background-position: -780px -760px; } -.emoji-1F945 { background-position: 0 -780px; } -.emoji-1F947 { background-position: -20px -780px; } -.emoji-1F948 { background-position: -40px -780px; } -.emoji-1F949 { background-position: -60px -780px; } -.emoji-1F94A { background-position: -80px -780px; } -.emoji-1F94B { background-position: -100px -780px; } -.emoji-1F950 { background-position: -120px -780px; } -.emoji-1F951 { background-position: -140px -780px; } -.emoji-1F952 { background-position: -160px -780px; } -.emoji-1F953 { background-position: -180px -780px; } -.emoji-1F954 { background-position: -200px -780px; } -.emoji-1F955 { background-position: -220px -780px; } -.emoji-1F956 { background-position: -240px -780px; } -.emoji-1F957 { background-position: -260px -780px; } -.emoji-1F958 { background-position: -280px -780px; } -.emoji-1F959 { background-position: -300px -780px; } -.emoji-1F95A { background-position: -320px -780px; } -.emoji-1F95B { background-position: -340px -780px; } -.emoji-1F95C { background-position: -360px -780px; } -.emoji-1F95D { background-position: -380px -780px; } -.emoji-1F95E { background-position: -400px -780px; } -.emoji-1F980 { background-position: -420px -780px; } -.emoji-1F981 { background-position: -440px -780px; } -.emoji-1F982 { background-position: -460px -780px; } -.emoji-1F983 { background-position: -480px -780px; } -.emoji-1F984 { background-position: -500px -780px; } -.emoji-1F985 { background-position: -520px -780px; } -.emoji-1F986 { background-position: -540px -780px; } -.emoji-1F987 { background-position: -560px -780px; } -.emoji-1F988 { background-position: -580px -780px; } -.emoji-1F989 { background-position: -600px -780px; } -.emoji-1F98A { background-position: -620px -780px; } -.emoji-1F98B { background-position: -640px -780px; } -.emoji-1F98C { background-position: -660px -780px; } -.emoji-1F98D { background-position: -680px -780px; } -.emoji-1F98E { background-position: -700px -780px; } -.emoji-1F98F { background-position: -720px -780px; } -.emoji-1F990 { background-position: -740px -780px; } -.emoji-1F991 { background-position: -760px -780px; } -.emoji-1F9C0 { background-position: -780px -780px; } -.emoji-203C { background-position: -800px 0; } -.emoji-2049 { background-position: -800px -20px; } -.emoji-2122 { background-position: -800px -40px; } -.emoji-2139 { background-position: -800px -60px; } -.emoji-2194 { background-position: -800px -80px; } -.emoji-2195 { background-position: -800px -100px; } -.emoji-2196 { background-position: -800px -120px; } -.emoji-2197 { background-position: -800px -140px; } -.emoji-2198 { background-position: -800px -160px; } -.emoji-2199 { background-position: -800px -180px; } -.emoji-21A9 { background-position: -800px -200px; } -.emoji-21AA { background-position: -800px -220px; } -.emoji-231A { background-position: -800px -240px; } -.emoji-231B { background-position: -800px -260px; } -.emoji-2328 { background-position: -800px -280px; } -.emoji-23CF { background-position: -800px -300px; } -.emoji-23E9 { background-position: -800px -320px; } -.emoji-23EA { background-position: -800px -340px; } -.emoji-23EB { background-position: -800px -360px; } -.emoji-23EC { background-position: -800px -380px; } -.emoji-23ED { background-position: -800px -400px; } -.emoji-23EE { background-position: -800px -420px; } -.emoji-23EF { background-position: -800px -440px; } -.emoji-23F0 { background-position: -800px -460px; } -.emoji-23F1 { background-position: -800px -480px; } -.emoji-23F2 { background-position: -800px -500px; } -.emoji-23F3 { background-position: -800px -520px; } -.emoji-23F8 { background-position: -800px -540px; } -.emoji-23F9 { background-position: -800px -560px; } -.emoji-23FA { background-position: -800px -580px; } -.emoji-24C2 { background-position: -800px -600px; } -.emoji-25AA { background-position: -800px -620px; } -.emoji-25AB { background-position: -800px -640px; } -.emoji-25B6 { background-position: -800px -660px; } -.emoji-25C0 { background-position: -800px -680px; } -.emoji-25FB { background-position: -800px -700px; } -.emoji-25FC { background-position: -800px -720px; } -.emoji-25FD { background-position: -800px -740px; } -.emoji-25FE { background-position: -800px -760px; } -.emoji-2600 { background-position: -800px -780px; } -.emoji-2601 { background-position: 0 -800px; } -.emoji-2602 { background-position: -20px -800px; } -.emoji-2603 { background-position: -40px -800px; } -.emoji-2604 { background-position: -60px -800px; } -.emoji-260E { background-position: -80px -800px; } -.emoji-2611 { background-position: -100px -800px; } -.emoji-2614 { background-position: -120px -800px; } -.emoji-2615 { background-position: -140px -800px; } -.emoji-2618 { background-position: -160px -800px; } -.emoji-261D { background-position: -180px -800px; } -.emoji-261D-1F3FB { background-position: -200px -800px; } -.emoji-261D-1F3FC { background-position: -220px -800px; } -.emoji-261D-1F3FD { background-position: -240px -800px; } -.emoji-261D-1F3FE { background-position: -260px -800px; } -.emoji-261D-1F3FF { background-position: -280px -800px; } -.emoji-2620 { background-position: -300px -800px; } -.emoji-2622 { background-position: -320px -800px; } -.emoji-2623 { background-position: -340px -800px; } -.emoji-2626 { background-position: -360px -800px; } -.emoji-262A { background-position: -380px -800px; } -.emoji-262E { background-position: -400px -800px; } -.emoji-262F { background-position: -420px -800px; } -.emoji-2638 { background-position: -440px -800px; } -.emoji-2639 { background-position: -460px -800px; } -.emoji-263A { background-position: -480px -800px; } -.emoji-2648 { background-position: -500px -800px; } -.emoji-2649 { background-position: -520px -800px; } -.emoji-264A { background-position: -540px -800px; } -.emoji-264B { background-position: -560px -800px; } -.emoji-264C { background-position: -580px -800px; } -.emoji-264D { background-position: -600px -800px; } -.emoji-264E { background-position: -620px -800px; } -.emoji-264F { background-position: -640px -800px; } -.emoji-2650 { background-position: -660px -800px; } -.emoji-2651 { background-position: -680px -800px; } -.emoji-2652 { background-position: -700px -800px; } -.emoji-2653 { background-position: -720px -800px; } -.emoji-2660 { background-position: -740px -800px; } -.emoji-2663 { background-position: -760px -800px; } -.emoji-2665 { background-position: -780px -800px; } -.emoji-2666 { background-position: -800px -800px; } -.emoji-2668 { background-position: -820px 0; } -.emoji-267B { background-position: -820px -20px; } -.emoji-267F { background-position: -820px -40px; } -.emoji-2692 { background-position: -820px -60px; } -.emoji-2693 { background-position: -820px -80px; } -.emoji-2694 { background-position: -820px -100px; } -.emoji-2696 { background-position: -820px -120px; } -.emoji-2697 { background-position: -820px -140px; } -.emoji-2699 { background-position: -820px -160px; } -.emoji-269B { background-position: -820px -180px; } -.emoji-269C { background-position: -820px -200px; } -.emoji-26A0 { background-position: -820px -220px; } -.emoji-26A1 { background-position: -820px -240px; } -.emoji-26AA { background-position: -820px -260px; } -.emoji-26AB { background-position: -820px -280px; } -.emoji-26B0 { background-position: -820px -300px; } -.emoji-26B1 { background-position: -820px -320px; } -.emoji-26BD { background-position: -820px -340px; } -.emoji-26BE { background-position: -820px -360px; } -.emoji-26C4 { background-position: -820px -380px; } -.emoji-26C5 { background-position: -820px -400px; } -.emoji-26C8 { background-position: -820px -420px; } -.emoji-26CE { background-position: -820px -440px; } -.emoji-26CF { background-position: -820px -460px; } -.emoji-26D1 { background-position: -820px -480px; } -.emoji-26D3 { background-position: -820px -500px; } -.emoji-26D4 { background-position: -820px -520px; } -.emoji-26E9 { background-position: -820px -540px; } -.emoji-26EA { background-position: -820px -560px; } -.emoji-26F0 { background-position: -820px -580px; } -.emoji-26F1 { background-position: -820px -600px; } -.emoji-26F2 { background-position: -820px -620px; } -.emoji-26F3 { background-position: -820px -640px; } -.emoji-26F4 { background-position: -820px -660px; } -.emoji-26F5 { background-position: -820px -680px; } -.emoji-26F7 { background-position: -820px -700px; } -.emoji-26F8 { background-position: -820px -720px; } -.emoji-26F9 { background-position: -820px -740px; } -.emoji-26F9-1F3FB { background-position: -820px -760px; } -.emoji-26F9-1F3FC { background-position: -820px -780px; } -.emoji-26F9-1F3FD { background-position: -820px -800px; } -.emoji-26F9-1F3FE { background-position: 0 -820px; } -.emoji-26F9-1F3FF { background-position: -20px -820px; } -.emoji-26FA { background-position: -40px -820px; } -.emoji-26FD { background-position: -60px -820px; } -.emoji-2702 { background-position: -80px -820px; } -.emoji-2705 { background-position: -100px -820px; } -.emoji-2708 { background-position: -120px -820px; } -.emoji-2709 { background-position: -140px -820px; } -.emoji-270A { background-position: -160px -820px; } -.emoji-270A-1F3FB { background-position: -180px -820px; } -.emoji-270A-1F3FC { background-position: -200px -820px; } -.emoji-270A-1F3FD { background-position: -220px -820px; } -.emoji-270A-1F3FE { background-position: -240px -820px; } -.emoji-270A-1F3FF { background-position: -260px -820px; } -.emoji-270B { background-position: -280px -820px; } -.emoji-270B-1F3FB { background-position: -300px -820px; } -.emoji-270B-1F3FC { background-position: -320px -820px; } -.emoji-270B-1F3FD { background-position: -340px -820px; } -.emoji-270B-1F3FE { background-position: -360px -820px; } -.emoji-270B-1F3FF { background-position: -380px -820px; } -.emoji-270C { background-position: -400px -820px; } -.emoji-270C-1F3FB { background-position: -420px -820px; } -.emoji-270C-1F3FC { background-position: -440px -820px; } -.emoji-270C-1F3FD { background-position: -460px -820px; } -.emoji-270C-1F3FE { background-position: -480px -820px; } -.emoji-270C-1F3FF { background-position: -500px -820px; } -.emoji-270D { background-position: -520px -820px; } -.emoji-270D-1F3FB { background-position: -540px -820px; } -.emoji-270D-1F3FC { background-position: -560px -820px; } -.emoji-270D-1F3FD { background-position: -580px -820px; } -.emoji-270D-1F3FE { background-position: -600px -820px; } -.emoji-270D-1F3FF { background-position: -620px -820px; } -.emoji-270F { background-position: -640px -820px; } -.emoji-2712 { background-position: -660px -820px; } -.emoji-2714 { background-position: -680px -820px; } -.emoji-2716 { background-position: -700px -820px; } -.emoji-271D { background-position: -720px -820px; } -.emoji-2721 { background-position: -740px -820px; } -.emoji-2728 { background-position: -760px -820px; } -.emoji-2733 { background-position: -780px -820px; } -.emoji-2734 { background-position: -800px -820px; } -.emoji-2744 { background-position: -820px -820px; } -.emoji-2747 { background-position: -840px 0; } -.emoji-274C { background-position: -840px -20px; } -.emoji-274E { background-position: -840px -40px; } -.emoji-2753 { background-position: -840px -60px; } -.emoji-2754 { background-position: -840px -80px; } -.emoji-2755 { background-position: -840px -100px; } -.emoji-2757 { background-position: -840px -120px; } -.emoji-2763 { background-position: -840px -140px; } -.emoji-2764 { background-position: -840px -160px; } -.emoji-2795 { background-position: -840px -180px; } -.emoji-2796 { background-position: -840px -200px; } -.emoji-2797 { background-position: -840px -220px; } -.emoji-27A1 { background-position: -840px -240px; } -.emoji-27B0 { background-position: -840px -260px; } -.emoji-27BF { background-position: -840px -280px; } -.emoji-2934 { background-position: -840px -300px; } -.emoji-2935 { background-position: -840px -320px; } -.emoji-2B05 { background-position: -840px -340px; } -.emoji-2B06 { background-position: -840px -360px; } -.emoji-2B07 { background-position: -840px -380px; } -.emoji-2B1B { background-position: -840px -400px; } -.emoji-2B1C { background-position: -840px -420px; } -.emoji-2B50 { background-position: -840px -440px; } -.emoji-2B55 { background-position: -840px -460px; } -.emoji-3030 { background-position: -840px -480px; } -.emoji-303D { background-position: -840px -500px; } -.emoji-3297 { background-position: -840px -520px; } -.emoji-3299 { background-position: -840px -540px; } - -.emoji-icon { - background-image: image-url('emoji.png'); - background-repeat: no-repeat; - height: 20px; - width: 20px; - - @media only screen and (-webkit-min-device-pixel-ratio: 2), - only screen and (min--moz-device-pixel-ratio: 2), - only screen and (-o-min-device-pixel-ratio: 2/1), - only screen and (min-device-pixel-ratio: 2), - only screen and (min-resolution: 192dpi), - only screen and (min-resolution: 2dppx) { - background-image: image-url('emoji@2x.png'); - background-size: 860px 840px; - } +gl-emoji { + display: inline-block; + display: inline-flex; + vertical-align: middle; + font-size: 1.5em; } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 0ba00cea8b5..8f2150066c7 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -4,6 +4,21 @@ &.reset-filters { padding: 7px; } + + &.update-issues-btn { + float: right; + margin-right: 0; + + @media (max-width: $screen-xs-max) { + float: none; + } + } +} + +.filters-section { + @media (max-width: $screen-xs-max) { + display: inline-block; + } } @media (min-width: $screen-sm-min) { @@ -34,6 +49,11 @@ display: block; margin: 0 0 10px; } + + .dropdown-menu-toggle, + .update-issues-btn .btn { + width: 100%; + } } .filtered-search-container { @@ -44,6 +64,89 @@ -webkit-flex-direction: column; flex-direction: column; } + + .tokens-container { + display: -webkit-flex; + display: flex; + flex: 1; + -webkit-flex: 1; + padding-left: 30px; + position: relative; + margin-bottom: 0; + } + + .input-token { + flex: 1; + -webkit-flex: 1; + } + + .filtered-search-token + .input-token:not(:last-child) { + max-width: 200px; + } +} + +.filtered-search-token, +.filtered-search-term { + display: -webkit-flex; + display: flex; + margin-top: 5px; + margin-bottom: 5px; + + .selectable { + display: -webkit-flex; + display: flex; + } + + .name, + .value { + display: inline-block; + padding: 2px 7px; + } + + .name { + background-color: $filter-name-resting-color; + color: $filter-name-text-color; + border-radius: 2px 0 0 2px; + margin-right: 1px; + text-transform: capitalize; + } + + .value { + background-color: $white-normal; + color: $filter-value-text-color; + border-radius: 0 2px 2px 0; + margin-right: 5px; + } + + .selected { + .name { + background-color: $filter-name-selected-color; + } + + .value { + background-color: $filter-value-selected-color; + } + } +} + +.filtered-search-term { + .name { + background-color: inherit; + color: $black; + text-transform: none; + } + + .selectable { + cursor: text; + } +} + +.scroll-container { + display: -webkit-flex; + display: flex; + overflow-x: scroll; + white-space: nowrap; + width: 100%; } .filtered-search-input-container { @@ -51,6 +154,9 @@ display: flex; position: relative; width: 100%; + border: 1px solid $border-color; + background-color: $white-light; + max-width: 87%; @media (max-width: $screen-xs-min) { -webkit-flex: 1 1 100%; @@ -67,12 +173,22 @@ } .form-control { - padding-left: 25px; + position: relative; + min-width: 200px; + padding-left: 0; padding-right: 25px; + border-color: transparent; &:focus ~ .fa-filter { color: $common-gray-dark; } + + &:focus, + &:hover { + outline: none; + border-color: transparent; + box-shadow: none; + } } .fa-filter { @@ -89,12 +205,13 @@ .clear-search { width: 35px; - background-color: transparent; + background-color: $white-light; border: none; position: absolute; right: 0; height: 100%; outline: none; + z-index: 1; &:hover .fa-times { color: $common-gray-dark; @@ -111,7 +228,15 @@ overflow: auto; } -@media (max-width: $screen-xs-min) { +@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + .issues-details-filters { + .dropdown-menu-toggle { + width: 100px; + } + } +} + +@media (max-width: $screen-xs-max) { .issues-details-filters { padding: 0 0 10px; background-color: $white-light; @@ -205,4 +330,4 @@ .filter-dropdown-loading { padding: 8px 16px; -}
\ No newline at end of file +} diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 55ed4b7b06c..7adbb0a4188 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -229,44 +229,6 @@ ul.content-list { } } -// Table list -.table-list { - display: table; - width: 100%; - - .table-list-row { - display: table-row; - } - - .table-list-cell { - display: table-cell; - vertical-align: top; - padding: 10px 16px; - border-bottom: 1px solid $gray-darker; - - &.avatar-cell { - width: 36px; - padding-right: 0; - - img { - margin-right: 0; - } - } - } - - &.table-wide { - .table-list-cell { - &:last-of-type { - padding-right: 0; - } - - &:first-of-type { - padding-left: 0; - } - } - } -} - .panel > .content-list > li { padding: $gl-padding-top $gl-padding; } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index d4758d90352..a668a6c4c39 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -147,6 +147,9 @@ } .atwho-view { + overflow-y: auto; + overflow-x: hidden; + small.description { float: right; padding: 3px 5px; @@ -162,4 +165,8 @@ @include disableAllAnimation; } } + + ul > li { + white-space: nowrap; + } } diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 8e2c56a8488..eb73f7cc794 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -100,8 +100,7 @@ @media (max-width: $screen-sm-max) { .issues-filters { - .milestone-filter, - .labels-filter { + .milestone-filter { display: none; } } diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index efe93724013..9d8d08dff88 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -48,11 +48,3 @@ line-height: inherit; } } - -.panel-default { - .table-list-row:last-child { - .table-list-cell { - border-bottom: 0; - } - } -} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index ba0af072716..6841adb637e 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -248,7 +248,7 @@ $diff-view-modes-border: #c1c1c1; * Fonts */ $monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; -$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; +$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; /* * Dropdowns @@ -540,3 +540,12 @@ Pipeline Graph $stage-hover-bg: #eaf3fc; $stage-hover-border: #d1e7fc; $action-icon-color: #d6d6d6; + +/* +Filtered Search +*/ +$filter-name-resting-color: #f8f8f8; +$filter-name-text-color: rgba(0, 0, 0, 0.55); +$filter-value-text-color: rgba(0, 0, 0, 0.85); +$filter-name-selected-color: #ebebeb; +$filter-value-selected-color: #d7d7d7; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index c3d45d708c1..2029b6893ef 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -78,6 +78,7 @@ padding: 5px 10px; background-color: $gray-light; border-bottom: 1px solid $gray-darker; + border-top: 1px solid $gray-darker; font-size: 14px; &:first-child { @@ -117,10 +118,37 @@ } } +.commit.flex-list { + display: flex; +} + +.avatar-cell { + width: 46px; + padding-left: 10px; + + img { + margin-right: 0; + } +} + +.commit-detail { + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-grow: 1; + padding-left: 10px; + + .merge-request-branches & { + flex-direction: column; + } +} + +.commit-content { + padding-right: 10px; +} + .commit-actions { @media (min-width: $screen-sm-min) { - width: 300px; - text-align: right; font-size: 0; } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 9ae2e962d14..73a5da715f2 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -151,3 +151,71 @@ } } } + +.prometheus-graph { + text { + fill: $stat-graph-axis-fill; + } +} + +.x-axis path, +.y-axis path, +.label-x-axis-line, +.label-y-axis-line { + fill: none; + stroke-width: 1; + shape-rendering: crispEdges; +} + +.x-axis path, +.y-axis path { + stroke: $stat-graph-axis-fill; +} + +.label-x-axis-line, +.label-y-axis-line { + stroke: $border-color; +} + +.y-axis { + line { + stroke: $stat-graph-axis-fill; + stroke-width: 1; + } +} + +.metric-area { + opacity: 0.8; +} + +.prometheus-graph-overlay { + fill: none; + opacity: 0.0; + pointer-events: all; +} + +.rect-text-metric { + fill: $white-light; + stroke-width: 1; + stroke: $black; +} + +.rect-axis-text { + fill: $white-light; +} + +.text-metric, +.text-median-metric, +.text-metric-usage, +.text-metric-date { + fill: $black; +} + +.text-metric-date { + font-weight: 200; +} + +.selected-metric-line { + stroke: $black; + stroke-width: 1; +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index a629a5333d7..d3496e19dde 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -3,7 +3,6 @@ * */ .mr-state-widget { - background: $gray-light; color: $gl-text-color; border: 1px solid $border-color; border-radius: 2px; @@ -109,12 +108,17 @@ @media (max-width: $screen-xs-max) { flex-wrap: wrap; } + + .ci-status-icon > .icon-link > svg { + width: 22px; + height: 22px; + } } .mr-widget-body, .ci_widget, .mr-widget-footer { - padding: $gl-padding; + padding: 16px; } .mr-widget-pipeline-graph { @@ -174,10 +178,6 @@ } } - p:last-child { - margin-bottom: 0; - } - .btn-grouped { margin-left: 0; margin-right: 7px; @@ -240,8 +240,7 @@ .commit { margin: 0; - padding-top: 2px; - padding-bottom: 2px; + padding: 10px 0; list-style: none; &:hover { @@ -340,8 +339,61 @@ } } +.remove-message-pipes { + ul { + margin: 10px 0 0 12px; + padding: 0; + list-style: none; + border-left: 2px solid $border-color; + display: inline-block; + } + + li { + position: relative; + margin: 0; + padding: 0; + display: block; + + span { + margin-left: 15px; + max-height: 20px; + } + } + + li::before { + content: ''; + position: absolute; + border-top: 2px solid $border-color; + height: 1px; + top: 8px; + width: 8px; + } + + li:last-child { + &::before { + top: 18px; + } + + span { + display: block; + position: relative; + top: 5px; + margin-top: 5px; + } + } +} + .mr-source-target { + background-color: $gray-light; line-height: 31px; + border-style: solid; + border-width: 1px; + border-color: $border-color; + border-top-right-radius: 3px; + border-top-left-radius: 3px; + border-bottom: none; + padding: 16px; + margin-bottom: -1px; } .panel-new-merge-request { @@ -356,7 +408,7 @@ } .panel-footer { - padding: 5px 10px; + padding: 0; .btn { min-width: auto; @@ -426,6 +478,11 @@ } } +.assign-to-me-link { + padding-left: 12px; + white-space: nowrap; +} + .table-holder { .ci-table { @@ -437,6 +494,8 @@ } .merged-buttons { + margin-top: 20px; + .btn { float: left; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 00f5f2645b3..dc79de19d48 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -331,6 +331,10 @@ ul.notes { &:hover { color: $gl-link-color; + } + + &:focus, + &:hover { text-decoration: none; } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 69eea1b2217..20eabc83142 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -115,7 +115,7 @@ .table.ci-table { - &.builds-page tr { + &.builds-page tbody tr { height: 71px; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 07b93430442..4914933430f 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -494,11 +494,11 @@ a.deploy-project-label { .project-stats { font-size: 0; text-align: center; - border-bottom: 1px solid $border-color; .nav { padding-top: 12px; padding-bottom: 12px; + border-bottom: 1px solid $border-color; } .nav > li { @@ -645,30 +645,15 @@ pre.light-well { } .project-last-commit { + background-color: $gray-light; + border: 1px solid $border-color; + border-radius: $border-radius-base; + padding: 12px; + @media (min-width: $screen-sm-min) { margin-top: $gl-padding; } - &.container-fluid { - padding-top: 12px; - padding-bottom: 12px; - background-color: $gray-light; - border: 1px solid $border-color; - border-right-width: 0; - border-left-width: 0; - - @media (min-width: $screen-sm-min) { - border-right-width: 1px; - border-left-width: 1px; - } - } - - &.container-limited { - @media (min-width: 1281px) { - border-radius: $border-radius-base; - } - } - .ci-status { margin-right: $gl-padding; } @@ -761,6 +746,8 @@ pre.light-well { } .protected-branches-list { + margin-bottom: 30px; + a { color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index a28a87ed4f8..3889deee21a 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -24,3 +24,14 @@ .service-settings .control-label { padding-top: 0; } + +.token-token-container { + #impersonation-token-token { + width: 80%; + display: inline; + } + + .btn-clipboard { + margin-left: 5px; + } +} diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss new file mode 100644 index 00000000000..b97a29cd1a0 --- /dev/null +++ b/app/assets/stylesheets/pages/settings_ci_cd.scss @@ -0,0 +1,12 @@ +.triggers-container { + .label-container { + display: inline-block; + margin-left: 10px; + } +} + +.trigger-actions { + .btn { + margin-left: 10px; + } +} diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 8d1063fc26f..fc4da4c495f 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -139,18 +139,10 @@ .blob-commit-info { list-style: none; background: $gray-light; - padding: 6px 0; + padding: 16px 16px 16px 6px; border: 1px solid $border-color; border-bottom: none; margin: 0; - - .table-list-cell { - border-bottom: none; - } - - .commit-actions { - width: 260px; - } } #modal-remove-blob > .modal-dialog { width: 850px; } diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb index 62f62e99a97..9c9f420c1e0 100644 --- a/app/controllers/admin/applications_controller.rb +++ b/app/controllers/admin/applications_controller.rb @@ -2,7 +2,7 @@ class Admin::ApplicationsController < Admin::ApplicationController include OauthApplications before_action :set_application, only: [:show, :edit, :update, :destroy] - before_action :load_scopes, only: [:new, :edit] + before_action :load_scopes, only: [:new, :create, :edit, :update] def index @applications = Doorkeeper::Application.where("owner_id IS NULL") diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb new file mode 100644 index 00000000000..07c8bf714fc --- /dev/null +++ b/app/controllers/admin/impersonation_tokens_controller.rb @@ -0,0 +1,53 @@ +class Admin::ImpersonationTokensController < Admin::ApplicationController + before_action :user + + def index + set_index_vars + end + + def create + @impersonation_token = finder.build(impersonation_token_params) + + if @impersonation_token.save + flash[:impersonation_token] = @impersonation_token.token + redirect_to admin_user_impersonation_tokens_path, notice: "A new impersonation token has been created." + else + set_index_vars + render :index + end + end + + def revoke + @impersonation_token = finder.find(params[:id]) + + if @impersonation_token.revoke! + flash[:notice] = "Revoked impersonation token #{@impersonation_token.name}!" + else + flash[:alert] = "Could not revoke impersonation token #{@impersonation_token.name}." + end + + redirect_to admin_user_impersonation_tokens_path + end + + private + + def user + @user ||= User.find_by!(username: params[:user_id]) + end + + def finder(options = {}) + PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options)) + end + + def impersonation_token_params + params.require(:personal_access_token).permit(:name, :expires_at, :impersonation, scopes: []) + end + + def set_index_vars + @scopes = Gitlab::Auth::API_SCOPES + + @impersonation_token ||= finder.build + @inactive_impersonation_tokens = finder(state: 'inactive').execute + @active_impersonation_tokens = finder(state: 'active').execute.order(:expires_at) + end +end diff --git a/app/controllers/concerns/repository_settings_redirect.rb b/app/controllers/concerns/repository_settings_redirect.rb new file mode 100644 index 00000000000..0854c73a02f --- /dev/null +++ b/app/controllers/concerns/repository_settings_redirect.rb @@ -0,0 +1,7 @@ +module RepositorySettingsRedirect + extend ActiveSupport::Concern + + def redirect_to_repository_settings(project) + redirect_to namespace_project_settings_repository_path(project.namespace, project) + end +end diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb deleted file mode 100644 index 1bec5a7d27f..00000000000 --- a/app/controllers/emojis_controller.rb +++ /dev/null @@ -1,6 +0,0 @@ -class EmojisController < ApplicationController - layout false - - def index - end -end diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index c721dca58d9..05190103767 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -1,8 +1,8 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController - before_action :authenticate_resource_owner! - layout 'profile' + # Overriden from Doorkeeper::AuthorizationsController to + # include the call to session.delete def new if pre_auth.authorizable? if skip_authorization? || matching_token? @@ -16,44 +16,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController render "doorkeeper/authorizations/error" end end - - # TODO: Handle raise invalid authorization - def create - redirect_or_render authorization.authorize - end - - def destroy - redirect_or_render authorization.deny - end - - private - - def matching_token? - Doorkeeper::AccessToken.matching_token_for(pre_auth.client, - current_resource_owner.id, - pre_auth.scopes) - end - - def redirect_or_render(auth) - if auth.redirectable? - redirect_to auth.redirect_uri - else - render json: auth.body, status: auth.status - end - end - - def pre_auth - @pre_auth ||= - Doorkeeper::OAuth::PreAuthorization.new(Doorkeeper.configuration, - server.client_via_uid, - params) - end - - def authorization - @authorization ||= strategy.request - end - - def strategy - @strategy ||= server.authorization_request(pre_auth.response_type) - end end diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index 6e007f17913..0abe7ea3c9b 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -4,7 +4,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController end def create - @personal_access_token = current_user.personal_access_tokens.generate(personal_access_token_params) + @personal_access_token = finder.build(personal_access_token_params) if @personal_access_token.save flash[:personal_access_token] = @personal_access_token.token @@ -16,7 +16,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController end def revoke - @personal_access_token = current_user.personal_access_tokens.find(params[:id]) + @personal_access_token = finder.find(params[:id]) if @personal_access_token.revoke! flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!" @@ -29,14 +29,19 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController private + def finder(options = {}) + PersonalAccessTokensFinder.new({ user: current_user, impersonation: false }.merge(options)) + end + def personal_access_token_params params.require(:personal_access_token).permit(:name, :expires_at, scopes: []) end def set_index_vars - @personal_access_token ||= current_user.personal_access_tokens.build - @scopes = Gitlab::Auth::SCOPES - @active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at) - @inactive_personal_access_tokens = current_user.personal_access_tokens.inactive + @scopes = Gitlab::Auth::API_SCOPES + + @personal_access_token = finder.build + @inactive_personal_access_tokens = finder(state: 'inactive').execute + @active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at) end end diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb index d9dfa534669..ffb54390965 100644 --- a/app/controllers/projects/autocomplete_sources_controller.rb +++ b/app/controllers/projects/autocomplete_sources_controller.rb @@ -1,9 +1,5 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController - before_action :load_autocomplete_service, except: [:emojis, :members] - - def emojis - render json: Gitlab::AwardEmoji.urls - end + before_action :load_autocomplete_service, except: [:members] def members render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable) diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index 61fef4dc133..28c9646910d 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -8,6 +8,7 @@ module Projects def index issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute issues = issues.page(params[:page]).per(params[:per] || 20) + make_sure_position_is_set(issues) render json: { issues: serialize_as_json(issues), @@ -38,6 +39,12 @@ module Projects private + def make_sure_position_is_set(issues) + issues.each do |issue| + issue.move_to_end && issue.save unless issue.relative_position + end + end + def issue @issue ||= IssuesFinder.new(current_user, project_id: project.id) @@ -63,7 +70,7 @@ module Projects end def move_params - params.permit(:board_id, :id, :from_list_id, :to_list_id) + params.permit(:board_id, :id, :from_list_id, :to_list_id, :move_before_iid, :move_after_iid) end def issue_params @@ -73,7 +80,7 @@ module Projects def serialize_as_json(resource) resource.as_json( labels: true, - only: [:id, :iid, :title, :confidential, :due_date], + only: [:id, :iid, :title, :confidential, :due_date, :relative_position], include: { assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, milestone: { only: [:id, :title] } diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index b094491e006..1502b734f37 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -1,4 +1,5 @@ class Projects::DeployKeysController < Projects::ApplicationController + include RepositorySettingsRedirect respond_to :html # Authorize @@ -7,51 +8,36 @@ class Projects::DeployKeysController < Projects::ApplicationController layout "project_settings" def index - @key = DeployKey.new - set_index_vars + redirect_to_repository_settings(@project) end def new - redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) + redirect_to_repository_settings(@project) end def create @key = DeployKey.new(deploy_key_params.merge(user: current_user)) - set_index_vars - if @key.valid? && @project.deploy_keys << @key - redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) - else - render "index" + unless @key.valid? && @project.deploy_keys << @key + flash[:alert] = @key.errors.full_messages.join(', ').html_safe end + redirect_to_repository_settings(@project) end def enable Projects::EnableDeployKeyService.new(@project, current_user, params).execute - redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) + redirect_to_repository_settings(@project) end def disable @project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy - redirect_back_or_default(default: { action: 'index' }) + redirect_to_repository_settings(@project) end protected - def set_index_vars - @enabled_keys ||= @project.deploy_keys - - @available_keys ||= current_user.accessible_deploy_keys - @enabled_keys - @available_project_keys ||= current_user.project_deploy_keys - @enabled_keys - @available_public_keys ||= DeployKey.are_public - @enabled_keys - - # Public keys that are already used by another accessible project are already - # in @available_project_keys. - @available_public_keys -= @available_project_keys - end - def deploy_key_params params.require(:deploy_key).permit(:key, :title, :can_push) end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index fed75396d6e..fa37963dfd4 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -5,7 +5,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController before_action :authorize_create_deployment!, only: [:stop] before_action :authorize_update_environment!, only: [:edit, :update] before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize] - before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize] + before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics] before_action :verify_api_request!, only: :terminal_websocket_authorize def index @@ -109,6 +109,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController end end + def metrics + # Currently, this acts as a hint to load the metrics details into the cache + # if they aren't there already + @metrics = environment.metrics || {} + + respond_to do |format| + format.html + format.json do + render json: @metrics, status: @metrics.any? ? :ok : :no_content + end + end + end + private def verify_api_request! diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index 2f422d352ed..a8cb07eb67a 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -1,26 +1,22 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController + include RepositorySettingsRedirect # Authorize before_action :require_non_empty_project before_action :authorize_admin_project! before_action :load_protected_branch, only: [:show, :update, :destroy] - before_action :load_protected_branches, only: [:index] layout "project_settings" def index - @protected_branch = @project.protected_branches.new - load_gon_index + redirect_to_repository_settings(@project) end def create @protected_branch = ::ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute - if @protected_branch.persisted? - redirect_to namespace_project_protected_branches_path(@project.namespace, @project) - else - load_protected_branches - load_gon_index - render :index + unless @protected_branch.persisted? + flash[:alert] = @protected_branches.errors.full_messages.join(', ').html_safe end + redirect_to_repository_settings(@project) end def show @@ -45,7 +41,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController @protected_branch.destroy respond_to do |format| - format.html { redirect_to namespace_project_protected_branches_path } + format.html { redirect_to_repository_settings(@project) } format.js { head :ok } end end @@ -61,24 +57,4 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController merge_access_levels_attributes: [:access_level, :id], push_access_levels_attributes: [:access_level, :id]) end - - def load_protected_branches - @protected_branches = @project.protected_branches.order(:name).page(params[:page]) - end - - def access_levels_options - { - push_access_levels: { - "Roles" => ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }, - }, - merge_access_levels: { - "Roles" => ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } } - } - } - end - - def load_gon_index - params = { open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } } - gon.push(params.merge(access_levels_options)) - end end diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb new file mode 100644 index 00000000000..b6ce4abca45 --- /dev/null +++ b/app/controllers/projects/settings/repository_controller.rb @@ -0,0 +1,50 @@ +module Projects + module Settings + class RepositoryController < Projects::ApplicationController + before_action :authorize_admin_project! + + def show + @deploy_keys = DeployKeysPresenter + .new(@project, current_user: current_user) + + define_protected_branches + end + + private + + def define_protected_branches + load_protected_branches + @protected_branch = @project.protected_branches.new + load_gon_index + end + + def load_protected_branches + @protected_branches = @project.protected_branches.order(:name).page(params[:page]) + end + + def access_levels_options + { + push_access_levels: { + roles: ProtectedBranch::PushAccessLevel.human_access_levels.map do |id, text| + { id: id, text: text, before_divider: true } + end + }, + merge_access_levels: { + roles: ProtectedBranch::MergeAccessLevel.human_access_levels.map do |id, text| + { id: id, text: text, before_divider: true } + end + } + } + end + + def open_branches + branches = @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } + { open_branches: branches } + end + + def load_gon_index + gon.push(open_branches.merge(access_levels_options)) + end + end + end +end diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb index b2c11ea4156..c47198c5eb6 100644 --- a/app/controllers/projects/triggers_controller.rb +++ b/app/controllers/projects/triggers_controller.rb @@ -1,5 +1,8 @@ class Projects::TriggersController < Projects::ApplicationController before_action :authorize_admin_build! + before_action :authorize_manage_trigger!, except: [:index, :create] + before_action :authorize_admin_trigger!, only: [:edit, :update] + before_action :trigger, only: [:take_ownership, :edit, :update, :destroy] layout 'project_settings' @@ -8,27 +11,67 @@ class Projects::TriggersController < Projects::ApplicationController end def create - @trigger = project.triggers.new - @trigger.save + @trigger = project.triggers.create(create_params.merge(owner: current_user)) if @trigger.valid? - redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Trigger was created successfully.' + flash[:notice] = 'Trigger was created successfully.' else - @triggers = project.triggers.select(&:persisted?) - render action: "show" + flash[:alert] = 'You could not create a new trigger.' + end + + redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project) + end + + def take_ownership + if trigger.update(owner: current_user) + flash[:notice] = 'Trigger was re-assigned.' + else + flash[:alert] = 'You could not take ownership of trigger.' + end + + redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project) + end + + def edit + end + + def update + if trigger.update(update_params) + redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project), notice: 'Trigger was successfully updated.' + else + render action: "edit" end end def destroy - trigger.destroy - flash[:alert] = "Trigger removed" + if trigger.destroy + flash[:notice] = "Trigger removed." + else + flash[:alert] = "Could not remove the trigger." + end redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project) end private + def authorize_manage_trigger! + access_denied! unless can?(current_user, :manage_trigger, trigger) + end + + def authorize_admin_trigger! + access_denied! unless can?(current_user, :admin_trigger, trigger) + end + def trigger - @trigger ||= project.triggers.find(params[:id]) + @trigger ||= project.triggers.find(params[:id]) || render_404 + end + + def create_params + params.require(:trigger).permit(:description) + end + + def update_params + params.require(:trigger).permit(:description) end end diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 509f4f412ca..f1bfd574f04 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -14,6 +14,8 @@ class UploadsController < ApplicationController end disposition = uploader.image? ? 'inline' : 'attachment' + + expires_in 0.seconds, must_revalidate: true, private: true send_file uploader.file.path, disposition: disposition end diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb new file mode 100644 index 00000000000..760166b453f --- /dev/null +++ b/app/finders/personal_access_tokens_finder.rb @@ -0,0 +1,45 @@ +class PersonalAccessTokensFinder + attr_accessor :params + + delegate :build, :find, :find_by, to: :execute + + def initialize(params = {}) + @params = params + end + + def execute + tokens = PersonalAccessToken.all + tokens = by_user(tokens) + tokens = by_impersonation(tokens) + by_state(tokens) + end + + private + + def by_user(tokens) + return tokens unless @params[:user] + tokens.where(user: @params[:user]) + end + + def by_impersonation(tokens) + case @params[:impersonation] + when true + tokens.with_impersonation + when false + tokens.without_impersonation + else + tokens + end + end + + def by_state(tokens) + case @params[:state] + when 'active' + tokens.active + when 'inactive' + tokens.inactive + else + tokens + end + end +end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 4b025669f69..ca326dd0627 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -81,8 +81,8 @@ module ApplicationSettingsHelper end def repository_storages_options_for_select - options = Gitlab.config.repositories.storages.map do |name, path| - ["#{name} - #{path}", name] + options = Gitlab.config.repositories.storages.map do |name, storage| + ["#{name} - #{storage['path']}", name] end options_for_select(options, @application_setting.repository_storages) diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index 5ac3e66bb1f..2fcb7a59fc3 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -12,7 +12,7 @@ module BuildsHelper build_url: namespace_project_build_url(@project.namespace, @project, @build, :json), build_status: @build.status, build_stage: @build.stage, - log_state: @build.trace_with_state[:state].to_s + log_state: '' } end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 94f3b480178..2c2c408b035 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -48,6 +48,8 @@ module CiStatusHelper 'icon_status_created' when 'skipped' 'icon_status_skipped' + when 'manual' + 'icon_status_manual' else 'icon_status_canceled' end diff --git a/app/helpers/emoji_helper.rb b/app/helpers/emoji_helper.rb new file mode 100644 index 00000000000..482f68f412b --- /dev/null +++ b/app/helpers/emoji_helper.rb @@ -0,0 +1,5 @@ +module EmojiHelper + def emoji_icon(*args) + raw Gitlab::Emoji.gl_emoji_tag(*args) + end +end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 362046c0270..5605393c0c3 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -162,7 +162,12 @@ module EventsHelper def event_note(text, options = {}) text = first_line_in_markdown(text, 150, options) - sanitize(text, tags: %w(a img b pre code p span)) + + sanitize( + text, + tags: %w(a img b pre code p span), + attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style'] + ) end def event_commit_title(message) diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index f16a63e2178..e9b7cbbad6a 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -74,6 +74,10 @@ module GitlabRoutingHelper namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args) end + def environment_metrics_path(environment, *args) + metrics_namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args) + end + def issue_path(entity, *args) namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args) end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index a2d21b67a77..4bdf07fe1ad 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -87,34 +87,6 @@ module IssuesHelper icon('eye-slash') if issue.confidential? end - def emoji_icon(name, unicode = nil, aliases = [], sprite: true) - unicode ||= Gitlab::Emoji.emoji_filename(name) rescue "" - - data = { - aliases: aliases.join(" "), - emoji: name, - unicode_name: unicode - } - - if sprite - # Emoji icons for the emoji menu, these use a spritesheet. - content_tag :div, "", - class: "icon emoji-icon emoji-#{unicode}", - title: name, - data: data - else - # Emoji icons displayed separately, used for the awards already given - # to an issue or merge request. - content_tag :img, "", - class: "icon emoji", - title: name, - height: "20px", - width: "20px", - src: url_to_image("#{unicode}.png"), - data: data - end - end - def award_user_list(awards, current_user, limit: 10) names = awards.map do |award| award.user == current_user ? 'You' : award.user.name diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 74cccb23956..710218082f2 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -35,9 +35,8 @@ module PreferencesHelper def project_view_choices [ - ['Readme', :readme], - ['Activity view', :activity], - ['Files and Readme (default)', :files] + ['Files and Readme (default)', :files], + ['Activity view', :activity] ] end diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 8ad3851fb9a..18734f1411f 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -50,7 +50,7 @@ module SortingHelper end def sort_title_priority - 'Priority' + 'Label priority' end def sort_title_oldest_updated diff --git a/app/models/chat_team.rb b/app/models/chat_team.rb index 7952141a0d6..c52b6f15913 100644 --- a/app/models/chat_team.rb +++ b/app/models/chat_team.rb @@ -1,5 +1,6 @@ class ChatTeam < ActiveRecord::Base validates :team_id, presence: true + validates :namespace, uniqueness: true belongs_to :namespace end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index d69643967a1..3722047251d 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -517,6 +517,27 @@ module Ci ] end + def steps + [Gitlab::Ci::Build::Step.from_commands(self), + Gitlab::Ci::Build::Step.from_after_script(self)].compact + end + + def image + Gitlab::Ci::Build::Image.from_image(self) + end + + def services + Gitlab::Ci::Build::Image.from_services(self) + end + + def artifacts + [options[:artifacts]] + end + + def cache + [options[:cache]] + end + def credentials Gitlab::Ci::Build::Credentials::Factory.new(self).create! end @@ -543,10 +564,35 @@ module Ci @unscoped_project ||= Project.unscoped.find_by(id: gl_project_id) end + CI_REGISTRY_USER = 'gitlab-ci-token'.freeze + def predefined_variables variables = [ { key: 'CI', value: 'true', public: true }, { key: 'GITLAB_CI', value: 'true', public: true }, + { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, + { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, + { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, + { key: 'CI_JOB_ID', value: id.to_s, public: true }, + { key: 'CI_JOB_NAME', value: name, public: true }, + { key: 'CI_JOB_STAGE', value: stage, public: true }, + { key: 'CI_JOB_TOKEN', value: token, public: false }, + { key: 'CI_COMMIT_SHA', value: sha, public: true }, + { key: 'CI_COMMIT_REF_NAME', value: ref, public: true }, + { key: 'CI_COMMIT_REF_SLUG', value: ref_slug, public: true }, + { key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER, public: true }, + { key: 'CI_REGISTRY_PASSWORD', value: token, public: false }, + { key: 'CI_REPOSITORY_URL', value: repo_url, public: false } + ] + + variables << { key: "CI_COMMIT_TAG", value: ref, public: true } if tag? + variables << { key: "CI_PIPELINE_TRIGGERED", value: 'true', public: true } if trigger_request + variables << { key: "CI_JOB_MANUAL", value: 'true', public: true } if action? + variables.concat(legacy_variables) + end + + def legacy_variables + variables = [ { key: 'CI_BUILD_ID', value: id.to_s, public: true }, { key: 'CI_BUILD_TOKEN', value: token, public: false }, { key: 'CI_BUILD_REF', value: sha, public: true }, @@ -554,14 +600,12 @@ module Ci { key: 'CI_BUILD_REF_NAME', value: ref, public: true }, { key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true }, { key: 'CI_BUILD_NAME', value: name, public: true }, - { key: 'CI_BUILD_STAGE', value: stage, public: true }, - { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, - { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, - { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true } + { key: 'CI_BUILD_STAGE', value: stage, public: true } ] - variables << { key: 'CI_BUILD_TAG', value: ref, public: true } if tag? - variables << { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } if trigger_request - variables << { key: 'CI_BUILD_MANUAL', value: 'true', public: true } if action? + + variables << { key: "CI_BUILD_TAG", value: ref, public: true } if tag? + variables << { key: "CI_BUILD_TRIGGERED", value: 'true', public: true } if trigger_request + variables << { key: "CI_BUILD_MANUAL", value: 'true', public: true } if action? variables end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 4863c34a6a6..edd21f984c8 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -127,18 +127,15 @@ module Ci def tick_runner_queue SecureRandom.hex.tap do |new_update| - Gitlab::Redis.with do |redis| - redis.set(runner_queue_key, new_update, ex: RUNNER_QUEUE_EXPIRY_TIME) - end + ::Gitlab::Workhorse.set_key_and_notify(runner_queue_key, new_update, + expire: RUNNER_QUEUE_EXPIRY_TIME, overwrite: true) end end def ensure_runner_queue_value - Gitlab::Redis.with do |redis| - value = SecureRandom.hex - redis.set(runner_queue_key, value, ex: RUNNER_QUEUE_EXPIRY_TIME, nx: true) - redis.get(runner_queue_key) - end + new_value = SecureRandom.hex + ::Gitlab::Workhorse.set_key_and_notify(runner_queue_key, new_value, + expire: RUNNER_QUEUE_EXPIRY_TIME, overwrite: false) end def is_runner_queue_value_latest?(value) diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index 8aa45b2f02e..90473d41c04 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -29,8 +29,12 @@ module Ci token[0...4] end - def can_show_token?(user) - owner.blank? || owner == user + def legacy? + self.owner_id.blank? + end + + def can_access_project? + self.owner_id.blank? || Ability.allowed?(self.owner, :create_build, project) end end end diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 073ac4c1b65..a7fd0a15f0f 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -101,6 +101,6 @@ module Awardable private def normalize_name(name) - Gitlab::AwardEmoji.normalize_emoji_name(name) + Gitlab::Emoji.normalize_emoji_name(name) end end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index b819947c9e6..5101cc7e687 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -7,7 +7,7 @@ module HasStatus STARTED_STATUSES = %w[running success failed skipped manual].freeze ACTIVE_STATUSES = %w[pending running].freeze COMPLETED_STATUSES = %w[success failed canceled skipped].freeze - ORDERED_STATUSES = %w[manual failed pending running canceled success skipped].freeze + ORDERED_STATUSES = %w[failed pending running manual canceled success skipped created].freeze class_methods do def status_sql diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb new file mode 100644 index 00000000000..603f2dd7e5d --- /dev/null +++ b/app/models/concerns/relative_positioning.rb @@ -0,0 +1,101 @@ +module RelativePositioning + extend ActiveSupport::Concern + + MIN_POSITION = 0 + MAX_POSITION = Gitlab::Database::MAX_INT_VALUE + + included do + after_save :save_positionable_neighbours + end + + def min_relative_position + self.class.in_projects(project.id).minimum(:relative_position) + end + + def max_relative_position + self.class.in_projects(project.id).maximum(:relative_position) + end + + def prev_relative_position + prev_pos = nil + + if self.relative_position + prev_pos = self.class. + in_projects(project.id). + where('relative_position < ?', self.relative_position). + maximum(:relative_position) + end + + prev_pos || MIN_POSITION + end + + def next_relative_position + next_pos = nil + + if self.relative_position + next_pos = self.class. + in_projects(project.id). + where('relative_position > ?', self.relative_position). + minimum(:relative_position) + end + + next_pos || MAX_POSITION + end + + def move_between(before, after) + return move_after(before) unless after + return move_before(after) unless before + + pos_before = before.relative_position + pos_after = after.relative_position + + if pos_after && (pos_before == pos_after) + self.relative_position = pos_before + before.move_before(self) + after.move_after(self) + + @positionable_neighbours = [before, after] + else + self.relative_position = position_between(pos_before, pos_after) + end + end + + def move_before(after) + self.relative_position = position_between(after.prev_relative_position, after.relative_position) + end + + def move_after(before) + self.relative_position = position_between(before.relative_position, before.next_relative_position) + end + + def move_to_end + self.relative_position = position_between(max_relative_position, MAX_POSITION) + end + + private + + # This method takes two integer values (positions) and + # calculates some random position between them. The range is huge as + # the maximum integer value is 2147483647. Ideally, the calculated value would be + # exactly between those terminating values, but this will introduce possibility of a race condition + # so two or more issues can get the same value, we want to avoid that and we also want to avoid + # using a lock here. If we have two issues with distance more than one thousand, we are OK. + # Given the huge range of possible values that integer can fit we shoud never face a problem. + def position_between(pos_before, pos_after) + pos_before ||= MIN_POSITION + pos_after ||= MAX_POSITION + + pos_before, pos_after = [pos_before, pos_after].sort + + rand(pos_before.next..pos_after.pred) + end + + def save_positionable_neighbours + return unless @positionable_neighbours + + status = @positionable_neighbours.all?(&:save) + @positionable_neighbours = nil + + status + end +end diff --git a/app/models/environment.rb b/app/models/environment.rb index 1a21b5e52b5..bf33010fd21 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -145,6 +145,14 @@ class Environment < ActiveRecord::Base project.deployment_service.terminals(self) if has_terminals? end + def has_metrics? + project.monitoring_service.present? && available? && last_deployment.present? + end + + def metrics + project.monitoring_service.metrics(self) if has_metrics? + end + # An environment name is not necessarily suitable for use in URLs, DNS # or other third-party contexts, so provide a slugified version. A slug has # the following properties: diff --git a/app/models/issue.rb b/app/models/issue.rb index de90f19f854..0f7a26ee3e1 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -7,6 +7,7 @@ class Issue < ActiveRecord::Base include Sortable include Spammable include FasterCacheKeys + include RelativePositioning DueDateStruct = Struct.new(:title, :name).freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze diff --git a/app/models/oauth_access_grant.rb b/app/models/oauth_access_grant.rb new file mode 100644 index 00000000000..3a997406565 --- /dev/null +++ b/app/models/oauth_access_grant.rb @@ -0,0 +1,4 @@ +class OauthAccessGrant < Doorkeeper::AccessGrant + belongs_to :resource_owner, class_name: 'User' + belongs_to :application, class_name: 'Doorkeeper::Application' +end diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb index 116fb71ac08..b85f5dbaf2e 100644 --- a/app/models/oauth_access_token.rb +++ b/app/models/oauth_access_token.rb @@ -1,4 +1,4 @@ -class OauthAccessToken < ActiveRecord::Base +class OauthAccessToken < Doorkeeper::AccessToken belongs_to :resource_owner, class_name: 'User' belongs_to :application, class_name: 'Doorkeeper::Application' end diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 10a34c42fd8..e8b000ddad6 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -1,4 +1,5 @@ class PersonalAccessToken < ActiveRecord::Base + include Expirable include TokenAuthenticatable add_authentication_token_field :token @@ -6,17 +7,30 @@ class PersonalAccessToken < ActiveRecord::Base belongs_to :user - scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") } + before_save :ensure_token + + scope :active, -> { where("revoked = false AND (expires_at >= NOW() OR expires_at IS NULL)") } scope :inactive, -> { where("revoked = true OR expires_at < NOW()") } + scope :with_impersonation, -> { where(impersonation: true) } + scope :without_impersonation, -> { where(impersonation: false) } - def self.generate(params) - personal_access_token = self.new(params) - personal_access_token.ensure_token - personal_access_token - end + validates :scopes, presence: true + validate :validate_api_scopes def revoke! self.revoked = true self.save end + + def active? + !revoked? && !expired? + end + + protected + + def validate_api_scopes + unless scopes.all? { |scope| Gitlab::Auth::API_SCOPES.include?(scope.to_sym) } + errors.add :scopes, "can only contain API scopes" + end + end end diff --git a/app/models/project.rb b/app/models/project.rb index 7d211784c3c..8c2dadf4659 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -113,6 +113,7 @@ class Project < ActiveRecord::Base has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project has_one :external_wiki_service, dependent: :destroy has_one :kubernetes_service, dependent: :destroy, inverse_of: :project + has_one :prometheus_service, dependent: :destroy, inverse_of: :project has_one :mock_ci_service, dependent: :destroy has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" @@ -392,7 +393,7 @@ class Project < ActiveRecord::Base end def repository_storage_path - Gitlab.config.repositories.storages[repository_storage] + Gitlab.config.repositories.storages[repository_storage]['path'] end def team @@ -771,6 +772,14 @@ class Project < ActiveRecord::Base @deployment_service ||= deployment_services.reorder(nil).find_by(active: true) end + def monitoring_services + services.where(category: :monitoring) + end + + def monitoring_service + @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true) + end + def jira_tracker? issues_tracker.to_param == 'jira' end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index f2e1c906dac..02fbd5497fa 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -36,7 +36,7 @@ class KubernetesService < DeploymentService def initialize_properties if properties.nil? self.properties = {} - self.namespace = project.path if project.present? + self.namespace = "#{project.path}-#{project.id}" if project.present? end end diff --git a/app/models/project_services/monitoring_service.rb b/app/models/project_services/monitoring_service.rb new file mode 100644 index 00000000000..ea585721e8f --- /dev/null +++ b/app/models/project_services/monitoring_service.rb @@ -0,0 +1,16 @@ +# Base class for monitoring services +# +# These services integrate with a deployment solution like Prometheus +# to provide additional features for environments. +class MonitoringService < Service + default_value_for :category, 'monitoring' + + def self.supported_events + %w() + end + + # Environments have a number of metrics + def metrics(environment) + raise NotImplementedError + end +end diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb new file mode 100644 index 00000000000..375966b9efc --- /dev/null +++ b/app/models/project_services/prometheus_service.rb @@ -0,0 +1,93 @@ +class PrometheusService < MonitoringService + include ReactiveCaching + + self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] } + self.reactive_cache_lease_timeout = 30.seconds + self.reactive_cache_refresh_interval = 30.seconds + self.reactive_cache_lifetime = 1.minute + + # Access to prometheus is directly through the API + prop_accessor :api_url + + with_options presence: true, if: :activated? do + validates :api_url, url: true + end + + after_save :clear_reactive_cache! + + def initialize_properties + if properties.nil? + self.properties = {} + end + end + + def title + 'Prometheus' + end + + def description + 'Prometheus monitoring' + end + + def help + 'Retrieves `container_cpu_usage_seconds_total` and `container_memory_usage_bytes` from the configured Prometheus server. An `environment` label is required on each metric to identify the Environment.' + end + + def self.to_param + 'prometheus' + end + + def fields + [ + { + type: 'text', + name: 'api_url', + title: 'API URL', + placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/' + } + ] + end + + # Check we can connect to the Prometheus API + def test(*args) + client.ping + + { success: true, result: 'Checked API endpoint' } + rescue Gitlab::PrometheusError => err + { success: false, result: err } + end + + def metrics(environment) + with_reactive_cache(environment.slug) do |data| + data + end + end + + # Cache metrics for specific environment + def calculate_reactive_cache(environment_slug) + return unless active? && project && !project.pending_delete? + + memory_query = %{sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})/1024/1024} + cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m]))} + + { + success: true, + metrics: { + # Memory used in MB + memory_values: client.query_range(memory_query, start: 8.hours.ago), + memory_current: client.query(memory_query), + # CPU Usage rate in cores. + cpu_values: client.query_range(cpu_query, start: 8.hours.ago), + cpu_current: client.query(cpu_query) + }, + last_update: Time.now.utc + } + + rescue Gitlab::PrometheusError => err + { success: false, result: err.message } + end + + def client + @prometheus ||= Gitlab::Prometheus.new(api_url: api_url) + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb index e7cc8d6e083..6ab04440ca8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -50,10 +50,6 @@ class Repository end end - def self.storages - Gitlab.config.repositories.storages - end - def initialize(path_with_namespace, project) @path_with_namespace = path_with_namespace @project = project @@ -316,11 +312,13 @@ class Repository if !branch_name || branch_name == root_ref branches.each do |branch| cache.expire(:"diverging_commit_counts_#{branch.name}") + cache.expire(:"commit_count_#{branch.name}") end # In case a commit is pushed to a non-root branch we only have to flush the # cache for said branch. else cache.expire(:"diverging_commit_counts_#{branch_name}") + cache.expire(:"commit_count_#{branch_name}") end end @@ -500,6 +498,16 @@ class Repository end cache_method :commit_count, fallback: 0 + def commit_count_for_ref(ref) + return 0 unless exists? + + begin + cache.fetch(:"commit_count_#{ref}") { raw_repository.commit_count(ref) } + rescue Rugged::ReferenceError + 0 + end + end + def branch_names branches.map(&:name) end diff --git a/app/models/service.rb b/app/models/service.rb index 3ef4cbead10..2f75a2e4e7f 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -232,6 +232,7 @@ class Service < ActiveRecord::Base mattermost pipelines_email pivotaltracker + prometheus pushover redmine slack_slash_commands diff --git a/app/models/user.rb b/app/models/user.rb index bd57904a2cd..76fb4cd470e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -325,8 +325,7 @@ class User < ActiveRecord::Base end def find_by_personal_access_token(token_string) - personal_access_token = PersonalAccessToken.active.find_by_token(token_string) if token_string - personal_access_token&.user + PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user end # Returns a user for the given SSH key. diff --git a/app/policies/ci/trigger_policy.rb b/app/policies/ci/trigger_policy.rb new file mode 100644 index 00000000000..c90c9ac0583 --- /dev/null +++ b/app/policies/ci/trigger_policy.rb @@ -0,0 +1,13 @@ +module Ci + class TriggerPolicy < BasePolicy + def rules + delegate! @subject.project + + if can?(:admin_build) + can! :admin_trigger if @subject.owner.blank? || + @subject.owner == @user + can! :manage_trigger + end + end + end +end diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb new file mode 100644 index 00000000000..86ac513b3c0 --- /dev/null +++ b/app/presenters/projects/settings/deploy_keys_presenter.rb @@ -0,0 +1,60 @@ +module Projects + module Settings + class DeployKeysPresenter < Gitlab::View::Presenter::Simple + presents :project + delegate :size, to: :enabled_keys, prefix: true + delegate :size, to: :available_project_keys, prefix: true + delegate :size, to: :available_public_keys, prefix: true + + def new_key + @key ||= DeployKey.new + end + + def enabled_keys + @enabled_keys ||= project.deploy_keys + end + + def any_keys_enabled? + enabled_keys.any? + end + + def available_keys + @available_keys ||= current_user.accessible_deploy_keys - enabled_keys + end + + def available_project_keys + @available_project_keys ||= current_user.project_deploy_keys - enabled_keys + end + + def any_available_project_keys_enabled? + available_project_keys.any? + end + + def key_available?(deploy_key) + available_keys.include?(deploy_key) + end + + def available_public_keys + return @available_public_keys if defined?(@available_public_keys) + + @available_public_keys ||= DeployKey.are_public - enabled_keys + + # Public keys that are already used by another accessible project are already + # in @available_project_keys. + @available_public_keys -= available_project_keys + end + + def any_available_public_keys_enabled? + available_public_keys.any? + end + + def to_partial_path + 'projects/deploy_keys/index' + end + + def form_partial_path + 'projects/deploy_keys/form' + end + end + end +end diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 8a94c54b6ab..185838764c1 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -5,7 +5,7 @@ module Boards issues = IssuesFinder.new(current_user, filter_params).execute issues = without_board_labels(issues) unless movable_list? issues = with_list_label(issues) if movable_list? - issues + issues.reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC')) end private @@ -26,7 +26,6 @@ module Boards def filter_params set_default_scope - set_default_sort set_project set_state @@ -37,10 +36,6 @@ module Boards params[:scope] = 'all' end - def set_default_sort - params[:sort] = 'priority' - end - def set_project params[:project_id] = project.id end diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 96554a92a02..2a9981ab884 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -3,7 +3,7 @@ module Boards class MoveService < BaseService def execute(issue) return false unless can?(current_user, :update_issue, issue) - return false unless valid_move? + return false if issue_params.empty? update_service.execute(issue) end @@ -14,7 +14,7 @@ module Boards @board ||= project.boards.find(params[:board_id]) end - def valid_move? + def move_between_lists? moving_from_list.present? && moving_to_list.present? && moving_from_list != moving_to_list end @@ -32,11 +32,19 @@ module Boards end def issue_params - { - add_label_ids: add_label_ids, - remove_label_ids: remove_label_ids, - state_event: issue_state - } + attrs = {} + + if move_between_lists? + attrs.merge!( + add_label_ids: add_label_ids, + remove_label_ids: remove_label_ids, + state_event: issue_state, + ) + end + + attrs[:move_between_iids] = move_between_iids if move_between_iids + + attrs end def issue_state @@ -58,6 +66,12 @@ module Boards Array(label_ids).compact end + + def move_between_iids + return unless params[:move_after_iid] || params[:move_before_iid] + + [params[:move_after_iid], params[:move_before_iid]] + end end end end diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_job_service.rb index 5b52a0425de..0ab9042bf24 100644 --- a/app/services/ci/register_build_service.rb +++ b/app/services/ci/register_job_service.rb @@ -1,7 +1,7 @@ module Ci # This class responsible for assigning # proper pending build to runner on runner API request - class RegisterBuildService + class RegisterJobService include Gitlab::CurrentSettings attr_reader :runner diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index dbe2fda27b5..bc7431c89a8 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -99,6 +99,8 @@ class GitPushService < BaseService UpdateMergeRequestsWorker .perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref]) + SystemHookPushWorker.perform_async(build_push_data.dup, :push_hooks) + EventCreateService.new.push(@project, current_user, build_push_data) @project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_services(build_push_data.dup, :push_hooks) diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index b618c3e038e..b071a398481 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -211,7 +211,7 @@ class IssuableBaseService < BaseService label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids) params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids) - if params.present? + if issuable.changed? || params.present? issuable.assign_attributes(params.merge(updated_by: current_user)) before_update(issuable) diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index 366b3572738..85b6eb3fe3d 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -13,6 +13,7 @@ module Issues def before_create(issue) spam_check(issue, current_user) + issue.move_to_end end def after_create(issuable) diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 22e32b13259..a444c78b609 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -3,8 +3,8 @@ module Issues include SpamCheckService def execute(issue) + handle_move_between_iids(issue) filter_spam_check_params - update(issue) end @@ -37,11 +37,13 @@ module Issues end added_labels = issue.labels - old_labels + if added_labels.present? notification_service.relabeled_issue(issue, added_labels, current_user) end added_mentions = issue.mentioned_users - old_mentioned_users + if added_mentions.present? notification_service.new_mentions_in_issue(issue, added_mentions, current_user) end @@ -55,8 +57,24 @@ module Issues Issues::CloseService end + def handle_move_between_iids(issue) + return unless params[:move_between_iids] + + after_iid, before_iid = params.delete(:move_between_iids) + + issue_before = get_issue_if_allowed(issue.project, before_iid) if before_iid + issue_after = get_issue_if_allowed(issue.project, after_iid) if after_iid + + issue.move_between(issue_before, issue_after) + end + private + def get_issue_if_allowed(project, iid) + issue = project.issues.find_by(iid: iid) + issue if can?(current_user, :update_issue, issue) + end + def create_confidentiality_note(issue) SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user) end diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb index eb3ed31b65b..03921db6947 100644 --- a/app/validators/namespace_validator.rb +++ b/app/validators/namespace_validator.rb @@ -35,12 +35,21 @@ class NamespaceValidator < ActiveModel::EachValidator users ].freeze + WILDCARD_ROUTES = %w[tree commits wikis new edit create update logs_tree + preview blob blame raw files create_dir find_file].freeze + + STRICT_RESERVED = (RESERVED + WILDCARD_ROUTES).freeze + def self.valid?(value) !reserved?(value) && follow_format?(value) end - def self.reserved?(value) - RESERVED.include?(value) + def self.reserved?(value, strict: false) + if strict + STRICT_RESERVED.include?(value) + else + RESERVED.include?(value) + end end def self.follow_format?(value) @@ -54,7 +63,9 @@ class NamespaceValidator < ActiveModel::EachValidator record.errors.add(attribute, Gitlab::Regex.namespace_regex_message) end - if reserved?(value) + strict = record.is_a?(Group) && record.parent_id + + if reserved?(value, strict: strict) record.errors.add(attribute, "#{value} is a reserved name") end end diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb index 36279daa743..ee2ae65be7b 100644 --- a/app/validators/project_path_validator.rb +++ b/app/validators/project_path_validator.rb @@ -14,10 +14,8 @@ class ProjectPathValidator < ActiveModel::EachValidator # without tree as reserved name routing can match 'group/project' as group name, # 'tree' as project name and 'deploy_keys' as route. # - RESERVED = (NamespaceValidator::RESERVED - - %w[dashboard help ci admin search notes services assets profile public] + - %w[tree commits wikis new edit create update logs_tree - preview blob blame raw files create_dir find_file]).freeze + RESERVED = (NamespaceValidator::STRICT_RESERVED - + %w[dashboard help ci admin search notes services assets profile public]).freeze def self.valid?(value) !reserved?(value) diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml index 6c48328da4f..6a5e170ddd8 100644 --- a/app/views/admin/abuse_reports/index.html.haml +++ b/app/views/admin/abuse_reports/index.html.haml @@ -16,4 +16,4 @@ - else .empty-state .text-center - %h4 There are no abuse reports! #{emoji_icon 'tada'} + %h4 There are no abuse reports! #{emoji_icon('tada')} diff --git a/app/views/admin/impersonation_tokens/index.html.haml b/app/views/admin/impersonation_tokens/index.html.haml new file mode 100644 index 00000000000..1378dde52ab --- /dev/null +++ b/app/views/admin/impersonation_tokens/index.html.haml @@ -0,0 +1,8 @@ +- page_title "Impersonation Tokens", @user.name, "Users" += render 'admin/users/head' + +.row.prepend-top-default + .col-lg-12 + = render "shared/personal_access_tokens_form", path: admin_user_impersonation_tokens_path, impersonation: true, token: @impersonation_token, scopes: @scopes + + = render "shared/personal_access_tokens_table", impersonation: true, active_tokens: @active_impersonation_tokens, inactive_tokens: @inactive_impersonation_tokens diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index 9984e733956..d20be373564 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -21,4 +21,6 @@ = link_to "SSH keys", keys_admin_user_path(@user) = nav_link(controller: :identities) do = link_to "Identities", admin_user_identities_path(@user) + = nav_link(controller: :impersonation_tokens) do + = link_to "Impersonation Tokens", admin_user_impersonation_tokens_path(@user) .append-bottom-default diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index e3305e21e96..a1ef34dc588 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -4,7 +4,7 @@ %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_state_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } } - = emoji_icon(emoji, sprite: false) + = emoji_icon(emoji) %span.award-control-text.js-counter = awards.count diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index a196561f381..82aa51f9778 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -27,6 +27,7 @@ = hidden_field_tag :state, @pre_auth.state = hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :scope, @pre_auth.scope + = hidden_field_tag :nonce, @pre_auth.nonce = submit_tag "Authorize", class: "btn btn-success wide pull-left" = form_tag oauth_authorization_path, method: :delete do = hidden_field_tag :client_id, @pre_auth.client.uid @@ -34,4 +35,5 @@ = hidden_field_tag :state, @pre_auth.state = hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :scope, @pre_auth.scope + = hidden_field_tag :nonce, @pre_auth.nonce = submit_tag "Deny", class: "btn btn-danger prepend-left-10" diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml deleted file mode 100644 index 49bd9acd2db..00000000000 --- a/app/views/emojis/index.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -.emoji-menu - = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control", placeholder: "Search emoji" - .emoji-menu-content - - Gitlab::AwardEmoji.emoji_by_category.each do |category, emojis| - %h5.emoji-menu-title - = Gitlab::AwardEmoji::CATEGORIES[category] - %ul.clearfix.emoji-menu-list - - emojis.each do |emoji| - %li.pull-left.text-center.emoji-menu-list-item - %button.emoji-menu-btn.text-center.js-emoji-btn{ type: "button" } - = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"]) diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 5d1369c2010..2684f16c373 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -131,6 +131,12 @@ %tr %td.shortcut .key g + .key e + %td + Go to the project's activity feed + %tr + %td.shortcut + .key g .key f %td Go to files @@ -155,6 +161,12 @@ %tr %td.shortcut .key g + .key g + %td + Go to repository charts + %tr + %td.shortcut + .key g .key i %td Go to issues diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml index 3daa1e90a8c..769f6fb0151 100644 --- a/app/views/layouts/_init_auto_complete.html.haml +++ b/app/views/layouts/_init_auto_complete.html.haml @@ -4,7 +4,6 @@ - if project :javascript gl.GfmAutoComplete.dataSources = { - emojis: "#{emojis_namespace_project_autocomplete_sources_path(project.namespace, project)}", members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}", issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}", mergeRequests: "#{merge_requests_namespace_project_autocomplete_sources_path(project.namespace, project)}", diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 2335d467389..299dace3406 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,20 +1,4 @@ -- if current_user - .controls - .dropdown.project-settings-dropdown - %a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', 'data-toggle' => 'dropdown' } - = icon('cog') - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - - can_edit = can?(current_user, :admin_project, @project) - - = render 'layouts/nav/project_settings', can_edit: can_edit - - - if can_edit - %li.divider - %li - = link_to edit_project_path(@project) do - Edit Project - +- can_edit = can?(current_user, :admin_project, @project) .scrolling-tabs-container{ class: nav_control_class } .fade-left = icon('angle-left') @@ -71,18 +55,41 @@ %span Snippets - -# Global shortcut to network page for compatibility + - if project_nav_tab? :settings + = nav_link(path: %w[projects#edit members#show integrations#show repository#show ci_cd#show pages#show]) do + = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do + %span + Settings + - else + = nav_link(path: %w[members#show]) do + = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Settings', class: 'shortcuts-tree' do + %span + Settings + + -# Shortcut to Project > Activity + %li.hidden + = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do + %span + Activity + + -# Shortcut to Repository > Graph (formerly, Network) - if project_nav_tab? :network %li.hidden = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do - Network + Graph + + -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs") + - unless @project.empty_repo? + %li.hidden + = link_to charts_namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do + Charts - -# Shortcut to create a new issue + -# Shortcut to Issues > New Issue %li.hidden = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do Create a new issue - -# Shortcut to builds page + -# Shortcut to Pipelines > Jobs - if project_nav_tab? :builds %li.hidden = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml deleted file mode 100644 index 665725f6862..00000000000 --- a/app/views/layouts/nav/_project_settings.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -- if project_nav_tab? :team - = nav_link(controller: [:members, :teams]) do - = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do - %span - Members -- if can_edit - = nav_link(controller: :deploy_keys) do - = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do - %span - Deploy Keys - = nav_link(controller: :integrations) do - = link_to namespace_project_settings_integrations_path(@project.namespace, @project), title: 'Integrations' do - %span - Integrations - = nav_link(controller: :protected_branches) do - = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do - %span - Protected Branches - - - if @project.feature_available?(:builds, current_user) - = nav_link(controller: :ci_cd) do - = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do - %span - CI/CD Pipelines - = nav_link(controller: :pages) do - = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages', data: {placement: 'right'} do - %span - Pages diff --git a/app/views/profiles/personal_access_tokens/_form.html.haml b/app/views/profiles/personal_access_tokens/_form.html.haml deleted file mode 100644 index 3f6efa33953..00000000000 --- a/app/views/profiles/personal_access_tokens/_form.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -- personal_access_token = local_assigns.fetch(:personal_access_token) -- scopes = local_assigns.fetch(:scopes) - -= form_for [:profile, personal_access_token], method: :post, html: { class: 'js-requires-input' } do |f| - - = form_errors(personal_access_token) - - .form-group - = f.label :name, class: 'label-light' - = f.text_field :name, class: "form-control", required: true - - .form-group - = f.label :expires_at, class: 'label-light' - = f.text_field :expires_at, class: "datepicker form-control" - - .form-group - = f.label :scopes, class: 'label-light' - = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: personal_access_token, scopes: scopes - - .prepend-top-default - = f.submit 'Create Personal Access Token', class: "btn btn-create" diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 903b957c26b..0645ecad496 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -24,80 +24,11 @@ %hr - %h5.prepend-top-0 - Add a Personal Access Token - %p.profile-settings-content - Pick a name for the application, and we'll give you a unique token. - - = render "form", personal_access_token: @personal_access_token, scopes: @scopes - - %hr - - %h5 Active Personal Access Tokens (#{@active_personal_access_tokens.length}) - - - if @active_personal_access_tokens.present? - .table-responsive - %table.table.active-personal-access-tokens - %thead - %tr - %th Name - %th Created - %th Expires - %th Scopes - %th - %tbody - - @active_personal_access_tokens.each do |token| - %tr - %td= token.name - %td= token.created_at.to_date.to_s(:medium) - %td - - if token.expires_at.present? - = token.expires_at.to_date.to_s(:medium) - - else - %span.personal-access-tokens-never-expires-label Never - %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>" - %td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." } - - - else - .settings-message.text-center - You don't have any active tokens yet. - - %hr - - %h5 Inactive Personal Access Tokens (#{@inactive_personal_access_tokens.length}) - - - if @inactive_personal_access_tokens.present? - .table-responsive - %table.table.inactive-personal-access-tokens - %thead - %tr - %th Name - %th Created - %tbody - - @inactive_personal_access_tokens.each do |token| - %tr - %td= token.name - %td= token.created_at.to_date.to_s(:medium) - - - else - .settings-message.text-center - There are no inactive tokens. + = render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes + = render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens :javascript - var $dateField = $('#personal_access_token_expires_at'); - var date = $dateField.val(); - - new Pikaday({ - field: $dateField.get(0), - theme: 'gitlab-theme', - format: 'yyyy-mm-dd', - minDate: new Date(), - onSelect: function(dateText) { - $dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); - } - }); - $("#created-personal-access-token").click(function() { this.select(); }); diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 41a7191302d..24ff74ecb3b 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -18,7 +18,7 @@ - else = link_to title, '#' -%ul.blob-commit-info.table-list.hidden-xs +%ul.blob-commit-info.hidden-xs - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) = render blob_commit, project: @project, ref: @ref diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml index 0993e880da9..4a4dd84d5d2 100644 --- a/app/views/projects/boards/components/_board_list.html.haml +++ b/app/views/projects/boards/components/_board_list.html.haml @@ -8,7 +8,7 @@ "v-show" => "!loading", ":data-board" => "list.id", ":class" => '{ "is-smaller": showIssueForm }' } - %board-card{ "v-for" => "(issue, index) in orderedIssues", + %board-card{ "v-for" => "(issue, index) in issues", "ref" => "issue", ":index" => "index", ":list" => "list", @@ -17,7 +17,8 @@ ":root-path" => "rootPath", ":disabled" => "disabled", ":key" => "issue.id" } - %li.board-list-count.text-center{ "v-if" => "showCount" } + %li.board-list-count.text-center{ "v-if" => "showCount", + "data-issue-id" => "-1" } = icon("spinner spin", "v-show" => "list.loadingMore" ) %span{ "v-if" => "list.issues.length === list.issuesSize" } Showing all issues diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 228dad528ab..307010edb58 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,6 +1,5 @@ - @no_container = true - page_title "#{@build.name} (##{@build.id})", "Jobs" -- trace_with_state = @build.trace_with_state = render "projects/pipelines/head", build_subnav: true %div{ class: container_class } diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 4d0b7a5ca85..d001e01609a 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -34,8 +34,9 @@ = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) %li.clearfix = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) - %li.clearfix - = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit) + - if can_collaborate_with_project? + %li.clearfix + = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit) %li.divider %li.dropdown-header Download diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 002e3d345dc..6ab9a80e083 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -9,33 +9,34 @@ - cache_key.push(commit.status(ref)) if commit.status(ref) = cache(cache_key, expires_in: 1.day) do - %li.commit.table-list-row.js-toggle-container{ id: "commit-#{commit.short_id}" } + %li.commit.flex-list.js-toggle-container{ id: "commit-#{commit.short_id}" } - .table-list-cell.avatar-cell.hidden-xs + .avatar-cell.hidden-xs = author_avatar(commit, size: 36) - .table-list-cell.commit-content - = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title" - %span.commit-row-message.visible-xs-inline - · - = commit.short_id - - if commit.status(ref) - .visible-xs-inline - = render_commit_status(commit, ref: ref) - - if commit.description? - %a.text-expander.hidden-xs.js-toggle-button ... + .commit-detail + .commit-content + = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title" + %span.commit-row-message.visible-xs-inline + · + = commit.short_id + - if commit.status(ref) + .visible-xs-inline + = render_commit_status(commit, ref: ref) + - if commit.description? + %a.text-expander.hidden-xs.js-toggle-button ... - - if commit.description? - %pre.commit-row-description.js-toggle-content - = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author)) - .commiter - = commit_author_link(commit, avatar: false, size: 24) - committed - #{time_ago_with_tooltip(commit.committed_date)} + - if commit.description? + %pre.commit-row-description.js-toggle-content + = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author)) + .commiter + = commit_author_link(commit, avatar: false, size: 24) + committed + #{time_ago_with_tooltip(commit.committed_date)} - .table-list-cell.commit-actions.hidden-xs - - if commit.status(ref) - = render_commit_status(commit, ref: ref) - = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard") - = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" - = link_to_browse_code(project, commit) + .commit-actions.flex-row.hidden-xs + - if commit.status(ref) + = render_commit_status(commit, ref: ref) + = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard") + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" + = link_to_browse_code(project, commit) diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml index 64d93e4141c..6f5835cb9be 100644 --- a/app/views/projects/commits/_commit_list.html.haml +++ b/app/views/projects/commits/_commit_list.html.haml @@ -11,4 +11,4 @@ %li.warning-row.unstyled #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. - else - %ul.content-list.table-list= render commits, project: @project, ref: @ref + %ul.content-list= render commits, project: @project, ref: @ref diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 904cdb5767f..88c7d7bc44b 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -4,7 +4,7 @@ - commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| %li.commit-header #{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')} %li.commits-row - %ul.content-list.commit-list.table-list.table-wide + %ul.content-list.commit-list = render commits, project: project, ref: ref - if hidden > 0 diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml index d1e3cb14022..ec8fc4c9ee8 100644 --- a/app/views/projects/deploy_keys/_deploy_key.html.haml +++ b/app/views/projects/deploy_keys/_deploy_key.html.haml @@ -18,7 +18,7 @@ %span.key-created-at created #{time_ago_with_tooltip(deploy_key.created_at)} .visible-xs-block.visible-sm-block - - if @available_keys.include?(deploy_key) + - if @deploy_keys.key_available?(deploy_key) = link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: "btn btn-sm prepend-left-10", method: :put do Enable - else diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index c91bb9c255a..1421da72418 100644 --- a/app/views/projects/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -1,5 +1,5 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input" } do |f| - = form_errors(@key) += form_for [@project.namespace.becomes(Namespace), @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input" } do |f| + = form_errors(@deploy_keys.new_key) .form-group = f.label :title, class: "label-light" = f.text_field :title, class: 'form-control', autofocus: true, required: true diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml new file mode 100644 index 00000000000..0cbe9b3275a --- /dev/null +++ b/app/views/projects/deploy_keys/_index.html.haml @@ -0,0 +1,34 @@ +.row.prepend-top-default + .col-lg-3.profile-settings-sidebar + %h4.prepend-top-0 + Deploy Keys + %p + Deploy keys allow read-only access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. + .col-lg-9 + %h5.prepend-top-0 + Create a new deploy key for this project + = render @deploy_keys.form_partial_path + .col-lg-9.col-lg-offset-3 + %hr + .col-lg-9.col-lg-offset-3.append-bottom-default.deploy-keys + %h5.prepend-top-0 + Enabled deploy keys for this project (#{@deploy_keys.enabled_keys_size}) + - if @deploy_keys.any_keys_enabled? + %ul.well-list + = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.enabled_keys, as: :deploy_key + - else + .settings-message.text-center + No deploy keys found. Create one with the form above. + %h5.prepend-top-default + Deploy keys from projects you have access to (#{@deploy_keys.available_project_keys_size}) + - if @deploy_keys.any_available_project_keys_enabled? + %ul.well-list + = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.available_project_keys, as: :deploy_key + - else + .settings-message.text-center + No deploy keys from your projects could be found. Create one with the form above or add existing one below. + - if @deploy_keys.any_available_public_keys_enabled? + %h5.prepend-top-default + Public deploy keys available to any project (#{@deploy_keys.available_public_keys_size}) + %ul.well-list + = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.available_public_keys, as: :deploy_key diff --git a/app/views/projects/deploy_keys/index.html.haml b/app/views/projects/deploy_keys/index.html.haml deleted file mode 100644 index 04fbb37d93f..00000000000 --- a/app/views/projects/deploy_keys/index.html.haml +++ /dev/null @@ -1,36 +0,0 @@ -- page_title "Deploy Keys" - -.row.prepend-top-default - .col-lg-3.profile-settings-sidebar - %h4.prepend-top-0 - = page_title - %p - Deploy keys allow read-only access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. - .col-lg-9 - %h5.prepend-top-0 - Create a new deploy key for this project - = render "form" - .col-lg-9.col-lg-offset-3 - %hr - .col-lg-9.col-lg-offset-3.append-bottom-default.deploy-keys - %h5.prepend-top-0 - Enabled deploy keys for this project (#{@enabled_keys.size}) - - if @enabled_keys.any? - %ul.well-list - = render @enabled_keys - - else - .settings-message.text-center - No deploy keys found. Create one with the form above or add existing one below. - %h5.prepend-top-default - Deploy keys from projects you have access to (#{@available_project_keys.size}) - - if @available_project_keys.any? - %ul.well-list - = render @available_project_keys - - else - .settings-message.text-center - No deploy keys from your projects could be found. Create one with the form above or add existing one below. - - if @available_public_keys.any? - %h5.prepend-top-default - Public deploy keys available to any project (#{@available_public_keys.size}) - %ul.well-list - = render @available_public_keys diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 83ae9fd10ec..2802a4eca7b 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -1,3 +1,4 @@ += render "projects/settings/head" .project-edit-container .row.prepend-top-default .col-lg-3.profile-settings-sidebar diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml new file mode 100644 index 00000000000..acbac1869fd --- /dev/null +++ b/app/views/projects/environments/_metrics_button.html.haml @@ -0,0 +1,6 @@ +- environment = local_assigns.fetch(:environment) + +- return unless environment.has_metrics? && can?(current_user, :read_environment, environment) + += link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do + = icon('area-chart') diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml new file mode 100644 index 00000000000..f8e94ca98ae --- /dev/null +++ b/app/views/projects/environments/metrics.html.haml @@ -0,0 +1,21 @@ +- @no_container = true +- page_title "Metrics for environment", @environment.name += render "projects/pipelines/head" + +%div{ class: container_class } + .top-area + .row + .col-sm-6 + %h3.page-title + Environment: + = @environment.name + + .col-sm-6 + .nav-controls + = render 'projects/deployments/actions', deployment: @environment.last_deployment + .row + .col-sm-12 + %svg.prometheus-graph{ 'graph-type' => 'cpu_values' } + .row + .col-sm-12 + %svg.prometheus-graph{ 'graph-type' => 'memory_values' } diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 7036325fff8..29a98f23b88 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -8,6 +8,7 @@ %h3.page-title= @environment.name .col-md-3 .nav-controls + = render 'projects/environments/metrics_button', environment: @environment = render 'projects/environments/terminal_button', environment: @environment = render 'projects/environments/external_url', environment: @environment - if can?(current_user, :update_environment, @environment) diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 7076f5db015..8b011af78eb 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -1,8 +1,2 @@ = form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form common-note-form js-quick-submit js-requires-input' } do |f| = render 'shared/issuable/form', f: f, issuable: @issue - -:javascript - $('.assign-to-me-link').on('click', function(e){ - $('#issue_assignee_id').val("#{current_user.id}").trigger("change"); - e.preventDefault(); - }); diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 88525f4036a..9607a7b5d06 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -1,8 +1,2 @@ = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f| = render 'shared/issuable/form', f: f, issuable: @merge_request - -:javascript - $('.assign-to-me-link').on('click', function(e){ - $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change"); - e.preventDefault(); - }); diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index 466ec1475d8..ad14b4e583e 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -21,7 +21,7 @@ selected: f.object.source_project_id .merge-request-select.dropdown = f.hidden_field :source_branch - = dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" } + = dropdown_toggle local_assigns.fetch(f.object.source_branch, "Select source branch"), { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" } .dropdown-menu.dropdown-menu-selectable.dropdown-source-branch = dropdown_title("Select source branch") = dropdown_filter("Search branches") @@ -30,7 +30,7 @@ branches: @merge_request.source_branches, selected: f.object.source_branch .panel-footer - = icon('spinner spin', class: 'js-source-loading') + .text-center= icon('spinner spin', class: 'js-source-loading') %ul.list-unstyled.mr_source_commit .col-md-6 @@ -60,7 +60,7 @@ branches: @merge_request.target_branches, selected: f.object.target_branch .panel-footer - = icon('spinner spin', class: "js-target-loading") + .text-center= icon('spinner spin', class: "js-target-loading") %ul.list-unstyled.mr_target_commit - if @merge_request.errors.any? diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index bd72310c16b..e7fcac4c477 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -52,11 +52,6 @@ = spinner :javascript - $('.assign-to-me-link').on('click', function(e){ - $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change"); - e.preventDefault(); - }); -:javascript var merge_request = new MergeRequest({ action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}" }); diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 03d618327d4..17be0490a86 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -29,9 +29,9 @@ %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) .normal - %span Request to merge + %span <b>Request to merge</b> %span.label-branch= source_branch_with_namespace(@merge_request) - %span into + %span <b>into</b> %span.label-branch = link_to_if @merge_request.target_branch_exists?, @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) - if @merge_request.open? && @merge_request.diverged_from_target_branch? diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index c676953f729..1298376ac25 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,8 +1,8 @@ - if @pipeline .mr-widget-heading - - %w[success success_with_warnings skipped canceled failed running pending].each do |status| + - %w[success success_with_warnings skipped manual canceled failed running pending].each do |status| .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) } - %div{ class: "ci-status-icon-#{status}" } + %div{ class: "ci-status-icon ci-status-icon-#{status}" } = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do = ci_icon_for_status(status) %span diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml index 7794d6d7df2..adc3bbc37f3 100644 --- a/app/views/projects/merge_requests/widget/_merged.html.haml +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -7,28 +7,46 @@ by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)} #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} - if !@merge_request.source_branch_exists? || params[:deleted_source_branch] - %p - The changes were merged into - #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. - The source branch has been removed. + .remove-message-pipes + %ul + %li + %span + The changes were merged into + #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. + %li + %span + The source branch has been removed. = render 'projects/merge_requests/widget/merged_buttons' - elsif @merge_request.can_remove_source_branch?(current_user) - .remove_source_branch_widget - %p - The changes were merged into - #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. - You can remove the source branch now. + .remove_source_branch_widget.remove-message-pipes + %ul + %li + %span + The changes were merged into + #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. + %li + %span + You can remove the source branch now. = render 'projects/merge_requests/widget/merged_buttons', source_branch_exists: true - .remove_source_branch_widget.failed.hide - %p - Failed to remove source branch '#{@merge_request.source_branch}'. - - .remove_source_branch_in_progress.hide - %p - = icon('spinner spin') - Removing source branch '#{@merge_request.source_branch}'. Please wait, this page will be automatically reloaded. + .remove_source_branch_widget.failed.remove-message-pipes.hide + %ul + %li + %span + Failed to remove source branch '#{@merge_request.source_branch}'. + .remove_source_branch_in_progress.remove-message-pipes.hide + %ul + %li + %span + = icon('spinner spin') + Removing source branch '#{@merge_request.source_branch}'. + %li + %span + Please wait, this page will be automatically reloaded. - else - %p - The changes were merged into - #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. - = render 'projects/merge_requests/widget/merged_buttons' + .remove-message-pipes + %ul + %li + %span + The changes were merged into + #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. + = render 'projects/merge_requests/widget/merged_buttons' diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml index 9eef011b591..caf3bf54eef 100644 --- a/app/views/projects/merge_requests/widget/_merged_buttons.haml +++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml @@ -9,6 +9,6 @@ = icon('trash-o') Remove Source Branch - if mr_can_be_reverted - = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "warning") + = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "close") - if mr_can_be_cherry_picked = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "default") diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index f0ccc4e00fd..bc426f1dc0c 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -27,6 +27,8 @@ = render 'projects/merge_requests/widget/open/build_failed' - elsif !@merge_request.mergeable_discussions_state? = render 'projects/merge_requests/widget/open/unresolved_discussions' + - elsif @pipeline&.blocked? + = render 'projects/merge_requests/widget/open/manual' - elsif @merge_request.can_be_merged? || resolved_conflicts = render 'projects/merge_requests/widget/open/accept' diff --git a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml index c98b2c42597..621ee313026 100644 --- a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml +++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml @@ -3,20 +3,24 @@ - can_merge = @merge_request.can_be_merged_via_command_line_by?(current_user) %h4.has-conflicts - = icon("exclamation-triangle") - This merge request contains merge conflicts + %p + = icon("exclamation-triangle") + This merge request contains merge conflicts -%p - To merge this request, resolve these conflicts - - if can_resolve && !can_resolve_in_ui - locally - or - - unless can_merge - ask someone with write access to this repository to - merge it locally. +.remove-message-pipes + %ul + %li + %span + To merge this request, resolve these conflicts + - if can_resolve && !can_resolve_in_ui + locally + or + - unless can_merge + ask someone with write access to this repository to + merge it locally. - if (can_resolve && can_resolve_in_ui) || can_merge - .btn-group + .merged-buttons.clearfix - if can_resolve && can_resolve_in_ui = link_to "Resolve conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn" - if can_merge diff --git a/app/views/projects/merge_requests/widget/open/_manual.html.haml b/app/views/projects/merge_requests/widget/open/_manual.html.haml new file mode 100644 index 00000000000..9078b7e21dd --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_manual.html.haml @@ -0,0 +1,4 @@ +%h4 + Pipeline blocked +%p + The pipeline for this merge request requires a manual action to proceed. diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml index 40a683d3fbd..5f347acce4d 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_pipeline_succeeds.html.haml @@ -4,15 +4,20 @@ %h4 Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} to be merged automatically when the pipeline succeeds. -%div - %p - = succeed '.' do - The changes will be merged into - %span.label-branch= @merge_request.target_branch - - if @merge_request.remove_source_branch? - The source branch will be removed. - - else - The source branch will not be removed. +.remove-message-pipes + %ul + %li + %span + = succeed '.' do + The changes will be merged into #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"} + - if @merge_request.remove_source_branch? + %li + %span + The source branch will be removed. + - else + %li + %span + The source branch will not be removed. - remove_source_branch_button = !@merge_request.remove_source_branch? && @merge_request.can_remove_source_branch?(current_user) && @merge_request.merge_user == current_user - user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user) diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml index b6595269b06..259d5bd63d6 100644 --- a/app/views/projects/pages/show.html.haml +++ b/app/views/projects/pages/show.html.haml @@ -1,4 +1,6 @@ - page_title 'Pages' += render "projects/settings/head" + %h3.page_title Pages diff --git a/app/views/projects/pipelines/_stage.html.haml b/app/views/projects/pipelines/_stage.html.haml index a0b14a7274a..3feb99cfcd7 100644 --- a/app/views/projects/pipelines/_stage.html.haml +++ b/app/views/projects/pipelines/_stage.html.haml @@ -1,3 +1,5 @@ -- @stage.statuses.latest.each do |status| - %li - = render 'ci/status/dropdown_graph_badge', subject: status +- grouped_statuses = @stage.statuses.latest_ordered.group_by(&:status) +- HasStatus::ORDERED_STATUSES.each do |ordered_status| + - grouped_statuses.fetch(ordered_status, []).each do |status| + %li + = render 'ci/status/dropdown_graph_badge', subject: status diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index 04b19a8c5a7..cf0db943865 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -23,6 +23,6 @@ - if can_admin_project %th %tbody - = render partial: @protected_branches, locals: { can_admin_project: can_admin_project } + = render partial: 'projects/protected_branches/protected_branch', collection: @protected_branches, locals: { can_admin_project: can_admin_project} = paginate @protected_branches, theme: 'gitlab' diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml index e95a3b1b4c3..b8e885b4d9a 100644 --- a/app/views/projects/protected_branches/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml @@ -10,7 +10,7 @@ = f.label :name, class: 'col-md-2 text-right' do Branch: .col-md-10 - = render partial: "dropdown", locals: { f: f } + = render partial: "projects/protected_branches/dropdown", locals: { f: f } .help-block = link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches') such as diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/_index.html.haml index b3b419bd92d..2d8c519c025 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/_index.html.haml @@ -1,11 +1,10 @@ -- page_title "Protected branches" - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('protected_branches') .row.prepend-top-default.append-bottom-default .col-lg-3 %h4.prepend-top-0 - = page_title + Protected Branches %p Keep stable branches secure and force developers to use merge requests. %p.prepend-top-20 By default, protected branches are designed to: @@ -17,6 +16,6 @@ %p.append-bottom-0 Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}. .col-lg-9 - if can? current_user, :admin_project, @project - = render 'create_protected_branch' + = render 'projects/protected_branches/create_protected_branch' - = render "branches_list" + = render "projects/protected_branches/branches_list" diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml index 0193800dedf..b2a6b8469a3 100644 --- a/app/views/projects/protected_branches/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -14,7 +14,7 @@ - else (branch was removed from repository) - = render partial: 'update_protected_branch', locals: { protected_branch: protected_branch } + = render partial: 'projects/protected_branches/update_protected_branch', locals: { protected_branch: protected_branch } - if can_admin_project %td diff --git a/app/views/projects/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml new file mode 100644 index 00000000000..88bcb541dac --- /dev/null +++ b/app/views/projects/settings/_head.html.haml @@ -0,0 +1,33 @@ += content_for :sub_nav do + .scrolling-tabs-container.sub-nav-scroll + = render 'shared/nav_scroll' + .nav-links.sub-nav.scrolling-tabs + %ul{ class: container_class } + - can_edit = can?(current_user, :admin_project, @project) + - if can_edit + = nav_link(controller: :projects) do + = link_to edit_project_path(@project), title: 'General' do + %span + General + = nav_link(controller: :members) do + = link_to project_settings_members_path(@project), title: 'Members' do + %span + Members + - if can_edit + = nav_link(controller: :integrations) do + = link_to project_settings_integrations_path(@project), title: 'Integrations' do + %span + Integrations + = nav_link(controller: :repository) do + = link_to namespace_project_settings_repository_path(@project.namespace, @project), title: 'Repository' do + %span + Repository + - if @project.feature_available?(:builds, current_user) + = nav_link(controller: :ci_cd) do + = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do + %span + CI/CD Pipelines + = nav_link(controller: :pages) do + = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages' do + %span + Pages diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 52f5f7b81e2..e2603096014 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -1,4 +1,5 @@ - page_title "CI/CD Pipelines" += render "projects/settings/head" = render 'projects/runners/index' = render 'projects/variables/index' diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml index aa38a889cdd..f69992566b5 100644 --- a/app/views/projects/settings/integrations/show.html.haml +++ b/app/views/projects/settings/integrations/show.html.haml @@ -1,3 +1,4 @@ - page_title 'Integrations' += render "projects/settings/head" = render 'projects/hooks/index' = render 'projects/services/index' diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml index d81ed7bb609..20e1ad68244 100644 --- a/app/views/projects/settings/members/show.html.haml +++ b/app/views/projects/settings/members/show.html.haml @@ -1,4 +1,5 @@ - page_title "Members" += render "projects/settings/head" = render "projects/project_members/index" - if can?(current_user, :admin_project, @project) diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml new file mode 100644 index 00000000000..4c02302e161 --- /dev/null +++ b/app/views/projects/settings/repository/show.html.haml @@ -0,0 +1,5 @@ +- page_title "Repository" += render "projects/settings/head" + += render @deploy_keys += render "projects/protected_branches/index" diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 30d185e6556..de1229d58aa 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -74,8 +74,9 @@ Set up auto deploy - if @repository.commit - .project-last-commit{ class: container_class } - = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project + %div{ class: container_class } + .project-last-commit + = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project %div{ class: container_class } - if @project.archived? diff --git a/app/views/projects/triggers/_content.html.haml b/app/views/projects/triggers/_content.html.haml new file mode 100644 index 00000000000..ea32eac2ae2 --- /dev/null +++ b/app/views/projects/triggers/_content.html.haml @@ -0,0 +1,14 @@ +%h4.prepend-top-0 + Triggers +%p.prepend-top-20 + Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will + impersonate their associated user including their access to projects and their project + permissions. +%p.prepend-top-20 + Triggers with the + %span.label.label-primary legacy + label do not have an associated user and only have access to the current project. +%p.append-bottom-0 + = succeed '.' do + Learn more in the + = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank' diff --git a/app/views/projects/triggers/_form.html.haml b/app/views/projects/triggers/_form.html.haml new file mode 100644 index 00000000000..5f708b3a2ed --- /dev/null +++ b/app/views/projects/triggers/_form.html.haml @@ -0,0 +1,11 @@ += form_for [@project.namespace.becomes(Namespace), @project, @trigger], html: { class: 'gl-show-field-errors' } do |f| + = form_errors(@trigger) + + - if @trigger.token + .form-group + %label.label-light Token + %p.form-control-static= @trigger.token + .form-group + = f.label :key, "Description", class: "label-light" + = f.text_field :description, class: "form-control", required: true, title: 'Trigger description is required.', placeholder: "Trigger description" + = f.submit btn_text, class: "btn btn-save" diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml index 33883facf9b..cc74e50a5e3 100644 --- a/app/views/projects/triggers/_index.html.haml +++ b/app/views/projects/triggers/_index.html.haml @@ -1,35 +1,31 @@ -.row.prepend-top-default.append-bottom-default +.row.prepend-top-default.append-bottom-default.triggers-container .col-lg-3 - %h4.prepend-top-0 - Triggers - %p.prepend-top-20 - Triggers can force a specific branch or tag to get rebuilt with an API call. - %p.append-bottom-0 - = succeed '.' do - Learn more in the - = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank' + = render "projects/triggers/content" .col-lg-9 .panel.panel-default .panel-heading %h4.panel-title Manage your project's triggers .panel-body + = render "projects/triggers/form", btn_text: "Add trigger" + %hr - if @triggers.any? - .table-responsive + .table-responsive.triggers-list %table.table %thead %th %strong Token %th + %strong Description + %th + %strong Owner + %th %strong Last used %th = render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger - else %p.settings-message.text-center.append-bottom-default - No triggers have been created yet. Add one using the button below. - - = form_for @trigger, url: url_for(controller: '/projects/triggers', action: 'create') do |f| - = f.submit "Add trigger", class: 'btn btn-success' + No triggers have been created yet. Add one using the form above. .panel-footer diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml index 112b51712ef..ed68e0ed56d 100644 --- a/app/views/projects/triggers/_trigger.html.haml +++ b/app/views/projects/triggers/_trigger.html.haml @@ -1,12 +1,42 @@ %tr %td - %span.monospace= trigger.token + - if can?(current_user, :admin_trigger, trigger) + %span= trigger.token + = clipboard_button(clipboard_text: trigger.token, title: "Copy trigger token to clipboard") + - else + %span= trigger.short_token + + .label-container + - if trigger.legacy? + %span.label.label-primary.has-tooltip{ title: "Trigger makes use of deprecated functionality" } legacy + - if !trigger.can_access_project? + %span.label.label-danger.has-tooltip{ title: "Trigger user has insufficient permissions to project" } invalid + + %td + - if trigger.description? && trigger.description.length > 15 + %span.has-tooltip{ title: trigger.description }= truncate(trigger.description, length: 15) + - else + = trigger.description + + %td + - if trigger.owner + .trigger-owner.sr-only= trigger.owner.name + = user_avatar(user: trigger.owner, size: 20) %td - - if trigger.last_trigger_request - #{time_ago_in_words(trigger.last_trigger_request.created_at)} ago + - if trigger.last_used + #{time_ago_in_words(trigger.last_used)} ago - else Never - %td.text-right - = link_to 'Revoke', namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-warning btn-sm" + %td.text-right.trigger-actions + - take_ownership_confirmation = "By taking ownership you will bind this trigger to your user account. With this the trigger will have access to all your projects as if it was you. Are you sure?" + - revoke_trigger_confirmation = "By revoking a trigger you will break any processes making use of it. Are you sure?" + - if trigger.owner != current_user && can?(current_user, :manage_trigger, trigger) + = link_to 'Take ownership', take_ownership_namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: take_ownership_confirmation }, method: :post, class: "btn btn-default btn-sm btn-trigger-take-ownership" + - if can?(current_user, :admin_trigger, trigger) + = link_to edit_namespace_project_trigger_path(@project.namespace, @project, trigger), method: :get, title: "Edit", class: "btn btn-default btn-sm" do + %i.fa.fa-pencil + - if can?(current_user, :manage_trigger, trigger) + = link_to namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: revoke_trigger_confirmation }, method: :delete, title: "Revoke", class: "btn btn-default btn-warning btn-sm btn-trigger-revoke" do + %i.fa.fa-trash diff --git a/app/views/projects/triggers/edit.html.haml b/app/views/projects/triggers/edit.html.haml new file mode 100644 index 00000000000..c35df322b9d --- /dev/null +++ b/app/views/projects/triggers/edit.html.haml @@ -0,0 +1,9 @@ +- page_title "Trigger" + +.row.prepend-top-default.append-bottom-default + .col-lg-3 + = render "content" + .col-lg-9 + %h4.prepend-top-0 + Update trigger + = render "form", btn_text: "Save trigger" diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 22004ecacbc..02133d09cdf 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -11,7 +11,7 @@ .results.prepend-top-10 - if @scope == 'commits' - %ul.content-list.commit-list.table-list.table-wide + %ul.content-list.commit-list = render partial: "search/results/commit", collection: @search_objects - else .search-results diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml new file mode 100644 index 00000000000..af4cc90f4a7 --- /dev/null +++ b/app/views/shared/_personal_access_tokens_form.html.haml @@ -0,0 +1,39 @@ +- type = impersonation ? "Impersonation" : "Personal Access" + +%h5.prepend-top-0 + Add a #{type} Token +%p.profile-settings-content + Pick a name for the application, and we'll give you a unique #{type} Token. + += form_for token, url: path, method: :post, html: { class: 'js-requires-input' } do |f| + + = form_errors(token) + + .form-group + = f.label :name, class: 'label-light' + = f.text_field :name, class: "form-control", required: true + + .form-group + = f.label :expires_at, class: 'label-light' + = f.text_field :expires_at, class: "datepicker form-control" + + .form-group + = f.label :scopes, class: 'label-light' + = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: token, scopes: scopes + + .prepend-top-default + = f.submit "Create #{type} Token", class: "btn btn-create" + +:javascript + var $dateField = $('.datepicker'); + var date = $dateField.val(); + + new Pikaday({ + field: $dateField.get(0), + theme: 'gitlab-theme', + format: 'yyyy-mm-dd', + minDate: new Date(), + onSelect: function(dateText) { + $dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); + } + }); diff --git a/app/views/shared/_personal_access_tokens_table.html.haml b/app/views/shared/_personal_access_tokens_table.html.haml new file mode 100644 index 00000000000..67a49815478 --- /dev/null +++ b/app/views/shared/_personal_access_tokens_table.html.haml @@ -0,0 +1,60 @@ +- type = impersonation ? "Impersonation" : "Personal Access" +%hr + +%h5 Active #{type} Tokens (#{active_tokens.length}) +- if impersonation + %p.profile-settings-content + To see all the user's personal access tokens you must impersonate them first. + +- if active_tokens.present? + .table-responsive + %table.table.active-tokens + %thead + %tr + %th Name + %th Created + %th Expires + %th Scopes + - if impersonation + %th Token + %th + %tbody + - active_tokens.each do |token| + %tr + %td= token.name + %td= token.created_at.to_date.to_s(:medium) + %td + - if token.expires? + %span{ class: ('text-warning' if token.expires_soon?) } + In #{distance_of_time_in_words_to_now(token.expires_at)} + - else + %span.token-never-expires-label Never + %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>" + - if impersonation + %td.token-token-container + = text_field_tag 'impersonation-token-token', token.token, readonly: true, class: "form-control" + = clipboard_button(clipboard_text: token.token) + - path = impersonation ? revoke_admin_user_impersonation_token_path(token.user, token) : revoke_profile_personal_access_token_path(token) + %td= link_to "Revoke", path, method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." } +- else + .settings-message.text-center + This user has no active #{type} Tokens. + +%hr + +%h5 Inactive #{type} Tokens (#{inactive_tokens.length}) +- if inactive_tokens.present? + .table-responsive + %table.table.inactive-tokens + %thead + %tr + %th Name + %th Created + %tbody + - inactive_tokens.each do |token| + %tr + %td= token.name + %td= token.created_at.to_date.to_s(:medium) +- else + .settings-message.text-center + This user has no inactive #{type} Tokens. diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 62f09cc2dc1..32128f3b3dc 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -11,10 +11,13 @@ class: "check_all_issues left" .issues-other-filters.filtered-search-container .filtered-search-input-container - %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]), 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) } - = icon('filter') - %button.clear-search.hidden{ type: 'button' } - = icon('times') + .scroll-container + %ul.tokens-container.list-unstyled + %li.input-token + %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]), 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) } + = icon('filter') + %button.clear-search.hidden{ type: 'button' } + = icon('times') #js-dropdown-hint.dropdown-menu.hint-dropdown %ul{ 'data-dropdown' => true } %li.filter-dropdown-item{ 'data-action' => 'submit' } @@ -112,7 +115,7 @@ = hidden_field_tag 'update[issuable_ids]', [] = hidden_field_tag :state_event, params[:state_event] - .filter-item.inline + .filter-item.inline.update-issues-btn = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save" :javascript diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml index a47085230b8..7a21f19ded4 100644 --- a/app/views/shared/issuable/form/_metadata.html.haml +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -13,10 +13,10 @@ = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder - - if issuable.assignee_id - = form.hidden_field :assignee_id + = form.hidden_field :assignee_id = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) + = link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignee_id == current_user.id}" .form-group.issue-milestone = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 2fff6b0105d..2cd87895c55 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -3,8 +3,8 @@ class PostReceive include DedicatedSidekiqQueue def perform(repo_path, identifier, changes) - if path = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1].to_s) } - repo_path.gsub!(path[1].to_s, "") + if repository_storage = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1]['path'].to_s) } + repo_path.gsub!(repository_storage[1]['path'].to_s, "") else log("Check gitlab.yml config for correct repositories.storages values. No repository storage path matches \"#{repo_path}\"") end diff --git a/app/workers/system_hook_push_worker.rb b/app/workers/system_hook_push_worker.rb new file mode 100644 index 00000000000..e43bbe35de9 --- /dev/null +++ b/app/workers/system_hook_push_worker.rb @@ -0,0 +1,8 @@ +class SystemHookPushWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def perform(push_data, hook_id) + SystemHooksService.new.execute_hooks(push_data, hook_id) + end +end diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb index acc4d858136..89ae17cef37 100644 --- a/app/workers/update_merge_requests_worker.rb +++ b/app/workers/update_merge_requests_worker.rb @@ -10,8 +10,5 @@ class UpdateMergeRequestsWorker return unless user MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref) - - push_data = Gitlab::DataBuilder::Push.build(project, user, oldrev, newrev, ref, []) - SystemHooksService.new.execute_hooks(push_data, :push_hooks) end end diff --git a/changelogs/unreleased/1381-present-commits-pagination-headers-correctly.yml b/changelogs/unreleased/1381-present-commits-pagination-headers-correctly.yml new file mode 100644 index 00000000000..1b7e294bd67 --- /dev/null +++ b/changelogs/unreleased/1381-present-commits-pagination-headers-correctly.yml @@ -0,0 +1,4 @@ +--- +title: "GET 'projects/:id/repository/commits' endpoint improvements" +merge_request: 9679 +author: George Andrinopoulos, Jordan Ryan Reuter diff --git a/changelogs/unreleased/18962-update-issues-button-jumps.yml b/changelogs/unreleased/18962-update-issues-button-jumps.yml new file mode 100644 index 00000000000..7be136ac4ff --- /dev/null +++ b/changelogs/unreleased/18962-update-issues-button-jumps.yml @@ -0,0 +1,4 @@ +--- +title: Align bulk update issues button to the right +merge_request: +author: diff --git a/changelogs/unreleased/23948-assign-to-me.yml b/changelogs/unreleased/23948-assign-to-me.yml new file mode 100644 index 00000000000..d73aa92b0e9 --- /dev/null +++ b/changelogs/unreleased/23948-assign-to-me.yml @@ -0,0 +1,4 @@ +--- +title: Re-add Assign to me link to Merge Request and Issues +merge_request: +author: diff --git a/changelogs/unreleased/25367-add-impersonation-token.yml b/changelogs/unreleased/25367-add-impersonation-token.yml new file mode 100644 index 00000000000..4a30f960036 --- /dev/null +++ b/changelogs/unreleased/25367-add-impersonation-token.yml @@ -0,0 +1,4 @@ +--- +title: Manage user personal access tokens through api and add impersonation tokens +merge_request: 9099 +author: Simon Vocella diff --git a/changelogs/unreleased/26188-tag-creation-404-for-guests.yml b/changelogs/unreleased/26188-tag-creation-404-for-guests.yml new file mode 100644 index 00000000000..fb00d46ea1f --- /dev/null +++ b/changelogs/unreleased/26188-tag-creation-404-for-guests.yml @@ -0,0 +1,4 @@ +--- +title: Don't show links to tag a commit for users that are not permitted +merge_request: 8407 +author: diff --git a/changelogs/unreleased/26371-native-emojis-v3-code.yml b/changelogs/unreleased/26371-native-emojis-v3-code.yml new file mode 100644 index 00000000000..88346711490 --- /dev/null +++ b/changelogs/unreleased/26371-native-emojis-v3-code.yml @@ -0,0 +1,4 @@ +--- +title: Use native unicode emojis +merge_request: +author: diff --git a/changelogs/unreleased/26732-combine-deploy-keys-and-push-rules-and-mirror-repository-and-protect-branches-settings-pages.yml b/changelogs/unreleased/26732-combine-deploy-keys-and-push-rules-and-mirror-repository-and-protect-branches-settings-pages.yml new file mode 100644 index 00000000000..6fc4615dab8 --- /dev/null +++ b/changelogs/unreleased/26732-combine-deploy-keys-and-push-rules-and-mirror-repository-and-protect-branches-settings-pages.yml @@ -0,0 +1,5 @@ +--- +title: Combined deploy keys, push rules, protect branches and mirror repository settings options into a single one called + Repository +merge_request: +author: diff --git a/changelogs/unreleased/26790-label-color-todos.yml b/changelogs/unreleased/26790-label-color-todos.yml new file mode 100644 index 00000000000..74084473d81 --- /dev/null +++ b/changelogs/unreleased/26790-label-color-todos.yml @@ -0,0 +1,4 @@ +--- +title: fix background color for labels mention in todo +merge_request: 9155 +author: mhasbini diff --git a/changelogs/unreleased/27568-refactor-very-slow-dropdown-asignee-spec.yml b/changelogs/unreleased/27568-refactor-very-slow-dropdown-asignee-spec.yml new file mode 100644 index 00000000000..5c738af7704 --- /dev/null +++ b/changelogs/unreleased/27568-refactor-very-slow-dropdown-asignee-spec.yml @@ -0,0 +1,4 @@ +--- +title: Refactor dropdown_assignee_spec +merge_request: 9711 +author: George Andrinopoulos diff --git a/changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml b/changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml new file mode 100644 index 00000000000..adc129d8dca --- /dev/null +++ b/changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml @@ -0,0 +1,4 @@ +--- +title: Uploaded files which content can change now require revalidation on each page load +merge_request: 9453 +author: diff --git a/changelogs/unreleased/28019-make-builds-show-faster.yml b/changelogs/unreleased/28019-make-builds-show-faster.yml new file mode 100644 index 00000000000..bbfea0e4c88 --- /dev/null +++ b/changelogs/unreleased/28019-make-builds-show-faster.yml @@ -0,0 +1,4 @@ +--- +title: Avoid calling Build#trace_with_state for performance +merge_request: 9149 +author: Takuya Noguchi diff --git a/changelogs/unreleased/28447-hybrid-repository-storages.yml b/changelogs/unreleased/28447-hybrid-repository-storages.yml new file mode 100644 index 00000000000..00dfc5781b9 --- /dev/null +++ b/changelogs/unreleased/28447-hybrid-repository-storages.yml @@ -0,0 +1,4 @@ +--- +title: Update storage settings to allow extra values per repository storage +merge_request: 9597 +author: diff --git a/changelogs/unreleased/28516-default-kubernetes-namespace.yml b/changelogs/unreleased/28516-default-kubernetes-namespace.yml new file mode 100644 index 00000000000..9fa5c681a53 --- /dev/null +++ b/changelogs/unreleased/28516-default-kubernetes-namespace.yml @@ -0,0 +1,4 @@ +--- +title: Make a default namespace of Kubernetes service to contain project ID +merge_request: +author: diff --git a/changelogs/unreleased/28538-restore-nav-shortcuts.yml b/changelogs/unreleased/28538-restore-nav-shortcuts.yml new file mode 100644 index 00000000000..07b39cd50d1 --- /dev/null +++ b/changelogs/unreleased/28538-restore-nav-shortcuts.yml @@ -0,0 +1,4 @@ +--- +title: Restore keyboard shortcuts for "Activity" and "Charts" +merge_request: 9680 +author: diff --git a/changelogs/unreleased/28598-narrow-environment-payload-by-using-basic-project.yml b/changelogs/unreleased/28598-narrow-environment-payload-by-using-basic-project.yml new file mode 100644 index 00000000000..ada726c9048 --- /dev/null +++ b/changelogs/unreleased/28598-narrow-environment-payload-by-using-basic-project.yml @@ -0,0 +1,4 @@ +--- +title: Narrow environment payload by using basic project details resource +merge_request: +author: diff --git a/changelogs/unreleased/28609-fix-redirect-to-home-page-url.yml b/changelogs/unreleased/28609-fix-redirect-to-home-page-url.yml deleted file mode 100644 index baf832d4495..00000000000 --- a/changelogs/unreleased/28609-fix-redirect-to-home-page-url.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix the redirect to custom home page URL -merge_request: 9518 -author: diff --git a/changelogs/unreleased/28835-jobs-head.yml b/changelogs/unreleased/28835-jobs-head.yml new file mode 100644 index 00000000000..1580cfb19ba --- /dev/null +++ b/changelogs/unreleased/28835-jobs-head.yml @@ -0,0 +1,4 @@ +--- +title: Fix jobs table header height +merge_request: +author: diff --git a/changelogs/unreleased/28850-fix-broken-migration.yml b/changelogs/unreleased/28850-fix-broken-migration.yml deleted file mode 100644 index 7f59a7708bc..00000000000 --- a/changelogs/unreleased/28850-fix-broken-migration.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix broken migration when upgrading straight to 8.17.1 -merge_request: 9613 -author: diff --git a/changelogs/unreleased/add-changelog-filtered-search-visual-tokens.yml b/changelogs/unreleased/add-changelog-filtered-search-visual-tokens.yml new file mode 100644 index 00000000000..d10e4cb7c87 --- /dev/null +++ b/changelogs/unreleased/add-changelog-filtered-search-visual-tokens.yml @@ -0,0 +1,4 @@ +--- +title: Add filtered search visual tokens +merge_request: 8969 +author: diff --git a/changelogs/unreleased/add-git-version-to-system-info.yml b/changelogs/unreleased/add-git-version-to-system-info.yml new file mode 100644 index 00000000000..2827fcec28d --- /dev/null +++ b/changelogs/unreleased/add-git-version-to-system-info.yml @@ -0,0 +1,4 @@ +--- +title: Add git version to gitlab:env:info +merge_request: 9128 +author: Semyon Pupkov diff --git a/changelogs/unreleased/add-pipeline-triggers.yml b/changelogs/unreleased/add-pipeline-triggers.yml new file mode 100644 index 00000000000..81b11da0bb2 --- /dev/null +++ b/changelogs/unreleased/add-pipeline-triggers.yml @@ -0,0 +1,4 @@ +--- +title: Add pipeline trigger API with user permissions +merge_request: 9277 +author: diff --git a/changelogs/unreleased/clear-connections-before-starting-sidekiq.yml b/changelogs/unreleased/clear-connections-before-starting-sidekiq.yml new file mode 100644 index 00000000000..8778fac6e9d --- /dev/null +++ b/changelogs/unreleased/clear-connections-before-starting-sidekiq.yml @@ -0,0 +1,4 @@ +--- +title: Clear ActiveRecord connections before starting Sidekiq +merge_request: +author: diff --git a/changelogs/unreleased/dm-dont-copy-toolip.yml b/changelogs/unreleased/dm-dont-copy-toolip.yml deleted file mode 100644 index 2b134da66ab..00000000000 --- a/changelogs/unreleased/dm-dont-copy-toolip.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't copy tooltip when copying GFM -merge_request: -author: diff --git a/changelogs/unreleased/dm-fix-api-create-file-on-empty-repo.yml b/changelogs/unreleased/dm-fix-api-create-file-on-empty-repo.yml deleted file mode 100644 index 7ac25c0a83e..00000000000 --- a/changelogs/unreleased/dm-fix-api-create-file-on-empty-repo.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix creating a file in an empty repo using the API -merge_request: 9632 -author: diff --git a/changelogs/unreleased/dm-fix-cherry-pick.yml b/changelogs/unreleased/dm-fix-cherry-pick.yml deleted file mode 100644 index e924b821d7e..00000000000 --- a/changelogs/unreleased/dm-fix-cherry-pick.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix cherry-picking or reverting through an MR -merge_request: -author: diff --git a/changelogs/unreleased/dz-nested-groups-restrictions.yml b/changelogs/unreleased/dz-nested-groups-restrictions.yml new file mode 100644 index 00000000000..2ffb6032525 --- /dev/null +++ b/changelogs/unreleased/dz-nested-groups-restrictions.yml @@ -0,0 +1,4 @@ +--- +title: Restrict nested group names to prevent ambiguous routes +merge_request: 9738 +author: diff --git a/changelogs/unreleased/feature-openid-connect.yml b/changelogs/unreleased/feature-openid-connect.yml new file mode 100644 index 00000000000..e84eb7aff86 --- /dev/null +++ b/changelogs/unreleased/feature-openid-connect.yml @@ -0,0 +1,4 @@ +--- +title: Implement OpenID Connect identity provider +merge_request: 8018 +author: Markus Koller diff --git a/changelogs/unreleased/feature-runner-jobs-v4-api.yml b/changelogs/unreleased/feature-runner-jobs-v4-api.yml new file mode 100644 index 00000000000..b24ea65266d --- /dev/null +++ b/changelogs/unreleased/feature-runner-jobs-v4-api.yml @@ -0,0 +1,4 @@ +--- +title: Add Runner's jobs v4 API +merge_request: 9273 +author: diff --git a/changelogs/unreleased/feature-syshook_commits.yml b/changelogs/unreleased/feature-syshook_commits.yml new file mode 100644 index 00000000000..1305f5cd414 --- /dev/null +++ b/changelogs/unreleased/feature-syshook_commits.yml @@ -0,0 +1,4 @@ +--- +title: Added commit array to Syshook json +merge_request: 9685 +author: Gabriele Pongelli diff --git a/changelogs/unreleased/fix-gb-deprecate-ci-config-types.yml b/changelogs/unreleased/fix-gb-deprecate-ci-config-types.yml new file mode 100644 index 00000000000..605b5f01d0e --- /dev/null +++ b/changelogs/unreleased/fix-gb-deprecate-ci-config-types.yml @@ -0,0 +1,4 @@ +--- +title: Deprecate usage of `types` configuration entry to describe CI/CD stages +merge_request: 9766 +author: diff --git a/changelogs/unreleased/issue_16834.yml b/changelogs/unreleased/issue_16834.yml new file mode 100644 index 00000000000..06175579ac3 --- /dev/null +++ b/changelogs/unreleased/issue_16834.yml @@ -0,0 +1,4 @@ +--- +title: Update API endpoints for raw files +merge_request: +author: diff --git a/changelogs/unreleased/priority-to-label-priority.yml b/changelogs/unreleased/priority-to-label-priority.yml new file mode 100644 index 00000000000..2d9c58bfd9b --- /dev/null +++ b/changelogs/unreleased/priority-to-label-priority.yml @@ -0,0 +1,4 @@ +--- +title: Rename priority sorting option to label priority +merge_request: +author: diff --git a/changelogs/unreleased/remove-readme-option.yml b/changelogs/unreleased/remove-readme-option.yml new file mode 100644 index 00000000000..1d4c862c00e --- /dev/null +++ b/changelogs/unreleased/remove-readme-option.yml @@ -0,0 +1,4 @@ +--- +title: Remove readme-only project view preference +merge_request: +author: diff --git a/changelogs/unreleased/rfr-20170307-change-default-project-number-limit.yml b/changelogs/unreleased/rfr-20170307-change-default-project-number-limit.yml new file mode 100644 index 00000000000..e799dd3b48d --- /dev/null +++ b/changelogs/unreleased/rfr-20170307-change-default-project-number-limit.yml @@ -0,0 +1,4 @@ +--- +title: Change project count limit from 10 to 100000 +merge_request: +author: diff --git a/changelogs/unreleased/set-default-cache-key-for-jobs.yml b/changelogs/unreleased/set-default-cache-key-for-jobs.yml new file mode 100644 index 00000000000..b69348d2ece --- /dev/null +++ b/changelogs/unreleased/set-default-cache-key-for-jobs.yml @@ -0,0 +1,4 @@ +--- +title: Set default cache key to "default" for jobs +merge_request: 9666 +author: diff --git a/changelogs/unreleased/settings-tab.yml b/changelogs/unreleased/settings-tab.yml new file mode 100644 index 00000000000..69990c9a917 --- /dev/null +++ b/changelogs/unreleased/settings-tab.yml @@ -0,0 +1,4 @@ +--- +title: Moved project settings from the gear drop-down menu to a tab +merge_request: 9786 +author: diff --git a/changelogs/unreleased/sort-builds-in-stage-dropdown.yml b/changelogs/unreleased/sort-builds-in-stage-dropdown.yml new file mode 100644 index 00000000000..646f25125b1 --- /dev/null +++ b/changelogs/unreleased/sort-builds-in-stage-dropdown.yml @@ -0,0 +1,4 @@ +--- +title: Sort builds in stage dropdown +merge_request: +author: diff --git a/changelogs/unreleased/tc-api-pipeline-jobs.yml b/changelogs/unreleased/tc-api-pipeline-jobs.yml new file mode 100644 index 00000000000..993c1b6526a --- /dev/null +++ b/changelogs/unreleased/tc-api-pipeline-jobs.yml @@ -0,0 +1,4 @@ +--- +title: Add GET /projects/:id/pipelines/:pipeline_id/jobs endpoint +merge_request: 9727 +author: diff --git a/changelogs/unreleased/use-redis-channel-to-post-runner-notifcations.yml b/changelogs/unreleased/use-redis-channel-to-post-runner-notifcations.yml new file mode 100644 index 00000000000..ff5a58f6232 --- /dev/null +++ b/changelogs/unreleased/use-redis-channel-to-post-runner-notifcations.yml @@ -0,0 +1,4 @@ +--- +title: Use redis channel to post notifications +merge_request: +author: diff --git a/changelogs/unreleased/use-v3-api-on-frontend.yml b/changelogs/unreleased/use-v3-api-on-frontend.yml deleted file mode 100644 index 467ad3c8276..00000000000 --- a/changelogs/unreleased/use-v3-api-on-frontend.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make projects dropdown only show projects you are a member of -merge_request: 9614 -author: diff --git a/changelogs/unreleased/zj-variables-build-job.yml b/changelogs/unreleased/zj-variables-build-job.yml new file mode 100644 index 00000000000..1cb0919f824 --- /dev/null +++ b/changelogs/unreleased/zj-variables-build-job.yml @@ -0,0 +1,4 @@ +--- +title: Rename job environment variables to new terminology +merge_request: 9756 +author: diff --git a/config/application.rb b/config/application.rb index fce3d5bcd22..cdb93e50e66 100644 --- a/config/application.rb +++ b/config/application.rb @@ -91,7 +91,6 @@ module Gitlab # Enable the asset pipeline config.assets.enabled = true - config.assets.paths << Gemojione.images_path config.assets.paths << "vendor/assets/fonts" config.assets.precompile << "*.png" config.assets.precompile << "print.css" diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index be34a4000fa..720df0cac2d 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -461,7 +461,8 @@ production: &base # gitlab-shell invokes Dir.pwd inside the repository path and that results # real path not the symlink. storages: # You must have at least a `default` storage path. - default: /home/git/repositories/ + default: + path: /home/git/repositories/ ## Backup settings backup: @@ -574,7 +575,8 @@ test: path: tmp/tests/gitlab-satellites/ repositories: storages: - default: tmp/tests/repositories/ + default: + path: tmp/tests/repositories/ backup: path: tmp/tests/backups gitlab_shell: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 933844e4ea6..b45d0e23080 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -83,7 +83,7 @@ class Settings < Settingslogic def base_url(config) custom_port = on_standard_port?(config) ? nil : ":#{config.port}" - + [ config.protocol, "://", @@ -186,7 +186,7 @@ Settings['issues_tracker'] ||= {} # GitLab # Settings['gitlab'] ||= Settingslogic.new({}) -Settings.gitlab['default_projects_limit'] ||= 10 +Settings.gitlab['default_projects_limit'] ||= 100000 Settings.gitlab['default_branch_protection'] ||= 2 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil? Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost' @@ -366,8 +366,13 @@ Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.send(:build_gitlab_shell_s # Settings['repositories'] ||= Settingslogic.new({}) Settings.repositories['storages'] ||= {} -# Setting gitlab_shell.repos_path is DEPRECATED and WILL BE REMOVED in version 9.0 -Settings.repositories.storages['default'] ||= Settings.gitlab_shell['repos_path'] || Settings.gitlab['user_home'] + '/repositories/' +unless Settings.repositories.storages['default'] + Settings.repositories.storages['default'] ||= {} + # We set the path only if the default storage doesn't exist, in case it exists + # but follows the pre-9.0 configuration structure. `6_validations.rb` initializer + # will validate all storages and throw a relevant error to the user if necessary. + Settings.repositories.storages['default']['path'] ||= Settings.gitlab['user_home'] + '/repositories/' +end # # The repository_downloads_path is used to remove outdated repository @@ -376,11 +381,11 @@ Settings.repositories.storages['default'] ||= Settings.gitlab_shell['repos_path' # data-integrity issue. In this case, we sets it to the default # repository_downloads_path value. # -repositories_storages_path = Settings.repositories.storages.values +repositories_storages = Settings.repositories.storages.values repository_downloads_path = Settings.gitlab['repository_downloads_path'].to_s.gsub(/\/$/, '') repository_downloads_full_path = File.expand_path(repository_downloads_path, Settings.gitlab['user_home']) -if repository_downloads_path.blank? || repositories_storages_path.any? { |path| [repository_downloads_path, repository_downloads_full_path].include?(path.gsub(/\/$/, '')) } +if repository_downloads_path.blank? || repositories_storages.any? { |rs| [repository_downloads_path, repository_downloads_full_path].include?(rs['path'].gsub(/\/$/, '')) } Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') end diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb index d92f64e1647..abe570f430c 100644 --- a/config/initializers/6_validations.rb +++ b/config/initializers/6_validations.rb @@ -4,8 +4,8 @@ end def find_parent_path(name, path) parent = Pathname.new(path).realpath.parent - Gitlab.config.repositories.storages.detect do |n, p| - name != n && Pathname.new(p).realpath == parent + Gitlab.config.repositories.storages.detect do |n, rs| + name != n && Pathname.new(rs['path']).realpath == parent end end @@ -16,10 +16,22 @@ end def validate_storages storage_validation_error('No repository storage path defined') if Gitlab.config.repositories.storages.empty? - Gitlab.config.repositories.storages.each do |name, path| + Gitlab.config.repositories.storages.each do |name, repository_storage| storage_validation_error("\"#{name}\" is not a valid storage name") unless storage_name_valid?(name) - parent_name, _parent_path = find_parent_path(name, path) + if repository_storage.is_a?(String) + error = "#{name} is not a valid storage, because it has no `path` key. " \ + "It may be configured as:\n\n#{name}:\n path: #{repository_storage}\n\n" \ + "Refer to gitlab.yml.example for an updated example" + + storage_validation_error(error) + end + + if !repository_storage.is_a?(Hash) || repository_storage['path'].nil? + storage_validation_error("#{name} is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example") + end + + parent_name, _parent_path = find_parent_path(name, repository_storage['path']) if parent_name storage_validation_error("#{name} is a nested path of #{parent_name}. Nested paths are not supported for repository storages") end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 88cd0f5f652..a5636765774 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -6,9 +6,14 @@ Doorkeeper.configure do # This block will be called to check whether the resource owner is authenticated or not. resource_owner_authenticator do # Put your resource owner authentication logic here. - # Ensure user is redirected to redirect_uri after login - session[:user_return_to] = request.fullpath - current_user || redirect_to(new_user_session_url) + if current_user + current_user + else + # Ensure user is redirected to redirect_uri after login + session[:user_return_to] = request.fullpath + redirect_to(new_user_session_url) + nil + end end resource_owner_from_credentials do |routes| diff --git a/config/initializers/doorkeeper_openid_connect.rb b/config/initializers/doorkeeper_openid_connect.rb new file mode 100644 index 00000000000..700ca25b884 --- /dev/null +++ b/config/initializers/doorkeeper_openid_connect.rb @@ -0,0 +1,36 @@ +Doorkeeper::OpenidConnect.configure do + issuer Gitlab.config.gitlab.url + + jws_private_key Rails.application.secrets.jws_private_key + + resource_owner_from_access_token do |access_token| + User.active.find_by(id: access_token.resource_owner_id) + end + + auth_time_from_resource_owner do |user| + user.current_sign_in_at + end + + reauthenticate_resource_owner do |user, return_to| + store_location_for user, return_to + sign_out user + redirect_to new_user_session_url + end + + subject do |user| + # hash the user's ID with the Rails secret_key_base to avoid revealing it + Digest::SHA256.hexdigest "#{user.id}-#{Rails.application.secrets.secret_key_base}" + end + + claims do + with_options scope: :openid do |o| + o.claim(:name) { |user| user.name } + o.claim(:nickname) { |user| user.username } + o.claim(:email) { |user| user.public_email } + o.claim(:email_verified) { |user| true if user.public_email? } + o.claim(:website) { |user| user.full_website_url if user.website_url? } + o.claim(:profile) { |user| Rails.application.routes.url_helpers.user_url user } + o.claim(:picture) { |user| user.avatar_url } + end + end +end diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb index 0ef9f51e5cf..ac353d14499 100644 --- a/config/initializers/rspec_profiling.rb +++ b/config/initializers/rspec_profiling.rb @@ -1,22 +1,41 @@ -module RspecProfilingConnection - def establish_connection - ::RspecProfiling::Collectors::PSQL::Result.establish_connection(ENV['RSPEC_PROFILING_POSTGRES_URL']) +module RspecProfilingExt + module PSQL + def establish_connection + ::RspecProfiling::Collectors::PSQL::Result.establish_connection(ENV['RSPEC_PROFILING_POSTGRES_URL']) + end end -end -module RspecProfilingGitBranchCi - def branch - ENV['CI_BUILD_REF_NAME'] || super + module Git + def branch + ENV['CI_BUILD_REF_NAME'] || super + end + end + + module Run + def example_finished(*args) + super + rescue => err + return if @already_logged_example_finished_error + + $stderr.puts "rspec_profiling couldn't collect an example: #{err}. Further warnings suppressed." + @already_logged_example_finished_error = true + end + + alias_method :example_passed, :example_finished + alias_method :example_failed, :example_finished end end if Rails.env.test? RspecProfiling.configure do |config| if ENV['RSPEC_PROFILING_POSTGRES_URL'] - RspecProfiling::Collectors::PSQL.prepend(RspecProfilingConnection) + RspecProfiling::Collectors::PSQL.prepend(RspecProfilingExt::PSQL) config.collector = RspecProfiling::Collectors::PSQL end end - RspecProfiling::VCS::Git.prepend(RspecProfilingGitBranchCi) if ENV.has_key?('CI') + if ENV.has_key?('CI') + RspecProfiling::VCS::Git.prepend(RspecProfilingExt::Git) + RspecProfiling::Run.prepend(RspecProfilingExt::Run) + end end diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index 291fa6c0abc..f9c1d2165d3 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -24,7 +24,8 @@ def create_tokens defaults = { secret_key_base: file_secret_key || generate_new_secure_token, otp_key_base: env_secret_key || file_secret_key || generate_new_secure_token, - db_key_base: generate_new_secure_token + db_key_base: generate_new_secure_token, + jws_private_key: generate_new_rsa_private_key } missing_secrets = set_missing_keys(defaults) @@ -41,6 +42,10 @@ def generate_new_secure_token SecureRandom.hex(64) end +def generate_new_rsa_private_key + OpenSSL::PKey::RSA.new(2048).to_pem +end + def warn_missing_secret(secret) warn "Missing Rails.application.secrets.#{secret} for #{Rails.env} environment. The secret will be generated and stored in config/secrets.yml." end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 0c4516b70f0..2b018c68703 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -19,6 +19,12 @@ Sidekiq.configure_server do |config| chain.add Gitlab::SidekiqStatus::ClientMiddleware end + config.on :startup do + # Clear any connections that might have been obtained before starting + # Sidekiq (e.g. in an initializer). + ActiveRecord::Base.clear_all_connections! + end + # Sidekiq-cron: load recurring jobs from gitlab.yml # UGLY Hack to get nested hash from settingslogic cron_jobs = JSON.parse(Gitlab.config.cron_jobs.to_json) diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 1d728282d90..14d49885fb3 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -60,6 +60,7 @@ en: scopes: api: Access your API read_user: Read user information + openid: Authenticate using OpenID Connect flash: applications: diff --git a/config/routes.rb b/config/routes.rb index 06d565df469..1a851da6203 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,14 +22,13 @@ Rails.application.routes.draw do authorizations: 'oauth/authorizations' end + use_doorkeeper_openid_connect + # Autocomplete get '/autocomplete/users' => 'autocomplete#users' get '/autocomplete/users/:id' => 'autocomplete#user' get '/autocomplete/projects' => 'autocomplete#projects' - # Emojis - resources :emojis, only: :index - # Search get 'search' => 'search#show' get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 8e99239f350..486ce3c5c87 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -2,6 +2,11 @@ namespace :admin do resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do resources :keys, only: [:show, :destroy] resources :identities, except: [:show] + resources :impersonation_tokens, only: [:index, :create] do + member do + put :revoke + end + end member do get :projects diff --git a/config/routes/project.rb b/config/routes/project.rb index 7dc7963ab88..44b8ae7aedd 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -13,7 +13,6 @@ constraints(ProjectUrlConstrainer.new) do resources :autocomplete_sources, only: [] do collection do - get 'emojis' get 'members' get 'issues' get 'merge_requests' @@ -136,7 +135,11 @@ constraints(ProjectUrlConstrainer.new) do resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } resources :variables, only: [:index, :show, :update, :create, :destroy] - resources :triggers, only: [:index, :create, :destroy] + resources :triggers, only: [:index, :create, :edit, :update, :destroy] do + member do + post :take_ownership + end + end resources :pipelines, only: [:index, :new, :create, :show] do collection do @@ -156,6 +159,7 @@ constraints(ProjectUrlConstrainer.new) do member do post :stop get :terminal + get :metrics get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil } end @@ -325,6 +329,7 @@ constraints(ProjectUrlConstrainer.new) do resource :members, only: [:show] resource :ci_cd, only: [:show], controller: 'ci_cd' resource :integrations, only: [:show] + resource :repository, only: [:show], controller: :repository end # Since both wiki and repository routing contains wildcard characters diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 824f99e687e..9d2066a6490 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -52,3 +52,4 @@ - [cronjob, 1] - [default, 1] - [pages, 1] + - [system_hook_push, 1] diff --git a/config/webpack.config.js b/config/webpack.config.js index d9fa70c29fb..7298e7109c6 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -132,7 +132,9 @@ var config = { extensions: ['.js', '.es6', '.js.es6'], alias: { '~': path.join(ROOT_PATH, 'app/assets/javascripts'), + 'emoji-map$': path.join(ROOT_PATH, 'fixtures/emojis/digests.json'), 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'), + 'empty_states': path.join(ROOT_PATH, 'app/views/shared/empty_states'), 'icons': path.join(ROOT_PATH, 'app/views/shared/icons'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), 'vue$': 'vue/dist/vue.common.js', diff --git a/db/fixtures/development/15_award_emoji.rb b/db/fixtures/development/15_award_emoji.rb index ea343c26b69..137a036edaf 100644 --- a/db/fixtures/development/15_award_emoji.rb +++ b/db/fixtures/development/15_award_emoji.rb @@ -1,7 +1,7 @@ require './spec/support/sidekiq' Gitlab::Seeder.quiet do - emoji = Gitlab::AwardEmoji.emojis.keys + emoji = Gitlab::Emoji.emojis.keys Issue.order(Gitlab::Database.random).limit(Issue.count / 2).each do |issue| project = issue.project diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb index e8de7ccf3db..66203486d53 100644 --- a/db/migrate/20140502125220_migrate_repo_size.rb +++ b/db/migrate/20140502125220_migrate_repo_size.rb @@ -8,7 +8,7 @@ class MigrateRepoSize < ActiveRecord::Migration project_data.each do |project| id = project['id'] namespace_path = project['namespace_path'] || '' - repos_path = Gitlab.config.gitlab_shell['repos_path'] || Gitlab.config.repositories.storages.default + repos_path = Gitlab.config.gitlab_shell['repos_path'] || Gitlab.config.repositories.storages.default['path'] path = File.join(repos_path, namespace_path, project['project_path'] + '.git') begin diff --git a/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb b/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb index 63f7392e54f..7a8ed99c68f 100644 --- a/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb +++ b/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb @@ -1,9 +1,15 @@ class AddIndexOnRequestedAtToMembers < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers + DOWNTIME = false + disable_ddl_transaction! - def change + def up add_concurrent_index :members, :requested_at end + + def down + remove_index :members, :requested_at if index_exists? :members, :requested_at + end end diff --git a/db/migrate/20160620115026_add_index_on_runners_locked.rb b/db/migrate/20160620115026_add_index_on_runners_locked.rb index dfa5110dea4..6ca486c63d1 100644 --- a/db/migrate/20160620115026_add_index_on_runners_locked.rb +++ b/db/migrate/20160620115026_add_index_on_runners_locked.rb @@ -4,9 +4,15 @@ class AddIndexOnRunnersLocked < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers + DOWNTIME = false + disable_ddl_transaction! - def change + def up add_concurrent_index :ci_runners, :locked end + + def down + remove_index :ci_runners, :locked if index_exists? :ci_runners, :locked + end end diff --git a/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb b/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb index 7c991c6d998..a05a4c679e3 100644 --- a/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb +++ b/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb @@ -1,9 +1,15 @@ class AddIndexForPipelineUserId < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers + DOWNTIME = false + disable_ddl_transaction! - def change + def up add_concurrent_index :ci_commits, :user_id end + + def down + remove_index :ci_commits, :user_id if index_exists? :ci_commits, :user_id + end end diff --git a/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb index a853de3abfb..3f074723b4a 100644 --- a/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb +++ b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb @@ -5,8 +5,15 @@ class AddDeletedAtToNamespaces < ActiveRecord::Migration disable_ddl_transaction! - def change + def up add_column :namespaces, :deleted_at, :datetime + add_concurrent_index :namespaces, :deleted_at end + + def down + remove_index :namespaces, :deleted_at if index_exists? :namespaces, :deleted_at + + remove_column :namespaces, :deleted_at + end end diff --git a/db/migrate/20160808085602_add_index_for_build_token.rb b/db/migrate/20160808085602_add_index_for_build_token.rb index 10ef42afce1..6c5d7268e72 100644 --- a/db/migrate/20160808085602_add_index_for_build_token.rb +++ b/db/migrate/20160808085602_add_index_for_build_token.rb @@ -6,7 +6,11 @@ class AddIndexForBuildToken < ActiveRecord::Migration disable_ddl_transaction! - def change + def up add_concurrent_index :ci_builds, :token, unique: true end + + def down + remove_index :ci_builds, :token, unique: true if index_exists? :ci_builds, :token, unique: true + end end diff --git a/db/migrate/20160819221631_add_index_to_note_discussion_id.rb b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb index b6e8bb18e7b..8f693e97a58 100644 --- a/db/migrate/20160819221631_add_index_to_note_discussion_id.rb +++ b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb @@ -8,7 +8,11 @@ class AddIndexToNoteDiscussionId < ActiveRecord::Migration disable_ddl_transaction! - def change + def up add_concurrent_index :notes, :discussion_id end + + def down + remove_index :notes, :discussion_id if index_exists? :notes, :discussion_id + end end diff --git a/db/migrate/20160819232256_add_incoming_email_token_to_users.rb b/db/migrate/20160819232256_add_incoming_email_token_to_users.rb index f2cf956adc9..bcad3416d04 100644 --- a/db/migrate/20160819232256_add_incoming_email_token_to_users.rb +++ b/db/migrate/20160819232256_add_incoming_email_token_to_users.rb @@ -9,8 +9,15 @@ class AddIncomingEmailTokenToUsers < ActiveRecord::Migration disable_ddl_transaction! - def change + def up add_column :users, :incoming_email_token, :string + add_concurrent_index :users, :incoming_email_token end + + def down + remove_index :users, :incoming_email_token if index_exists? :users, :incoming_email_token + + remove_column :users, :incoming_email_token + end end diff --git a/db/migrate/20160919145149_add_group_id_to_labels.rb b/db/migrate/20160919145149_add_group_id_to_labels.rb index d10f3a6d104..828b6afddb1 100644 --- a/db/migrate/20160919145149_add_group_id_to_labels.rb +++ b/db/migrate/20160919145149_add_group_id_to_labels.rb @@ -5,9 +5,15 @@ class AddGroupIdToLabels < ActiveRecord::Migration disable_ddl_transaction! - def change + def up add_column :labels, :group_id, :integer add_foreign_key :labels, :namespaces, column: :group_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey add_concurrent_index :labels, :group_id end + + def down + remove_index :labels, :group_id if index_exists? :labels, :group_id + remove_foreign_key :labels, :namespaces, column: :group_id + remove_column :labels, :group_id + end end diff --git a/db/migrate/20160920160832_add_index_to_labels_title.rb b/db/migrate/20160920160832_add_index_to_labels_title.rb index b5de552b98c..19f7b1076a7 100644 --- a/db/migrate/20160920160832_add_index_to_labels_title.rb +++ b/db/migrate/20160920160832_add_index_to_labels_title.rb @@ -5,7 +5,11 @@ class AddIndexToLabelsTitle < ActiveRecord::Migration disable_ddl_transaction! - def change + def up add_concurrent_index :labels, :title end + + def down + remove_index :labels, :title if index_exists? :labels, :title + end end diff --git a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb index 2abfe47b776..ad3eb4a26f9 100644 --- a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb +++ b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb @@ -25,9 +25,15 @@ class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration # comments: # disable_ddl_transaction! - def change + def up add_column :merge_request_metrics, :pipeline_id, :integer - add_concurrent_index :merge_request_metrics, :pipeline_id add_foreign_key :merge_request_metrics, :ci_commits, column: :pipeline_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey + add_concurrent_index :merge_request_metrics, :pipeline_id + end + + def down + remove_index :merge_request_metrics, :pipeline_id if index_exists? :merge_request_metrics, :pipeline_id + remove_foreign_key :merge_request_metrics, :ci_commits, column: :pipeline_id + remove_column :merge_request_metrics, :pipeline_id end end diff --git a/db/migrate/20161106185620_add_project_import_data_project_index.rb b/db/migrate/20161106185620_add_project_import_data_project_index.rb index 750a6a8c51e..94b8ddd46f5 100644 --- a/db/migrate/20161106185620_add_project_import_data_project_index.rb +++ b/db/migrate/20161106185620_add_project_import_data_project_index.rb @@ -6,7 +6,11 @@ class AddProjectImportDataProjectIndex < ActiveRecord::Migration disable_ddl_transaction! - def change + def up add_concurrent_index :project_import_data, :project_id end + + def down + remove_index :project_import_data, :project_id if index_exists? :project_import_data, :project_id + end end diff --git a/db/migrate/20161124111395_add_index_to_parent_id.rb b/db/migrate/20161124111395_add_index_to_parent_id.rb index eab74c01dfd..73f9d92bb22 100644 --- a/db/migrate/20161124111395_add_index_to_parent_id.rb +++ b/db/migrate/20161124111395_add_index_to_parent_id.rb @@ -8,7 +8,11 @@ class AddIndexToParentId < ActiveRecord::Migration disable_ddl_transaction! - def change + def up add_concurrent_index(:namespaces, [:parent_id, :id], unique: true) end + + def down + remove_index :namespaces, [:parent_id, :id] if index_exists? :namespaces, [:parent_id, :id] + end end diff --git a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb index 3e1f6b1627d..e5292cfba07 100644 --- a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb +++ b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb @@ -12,7 +12,7 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration end def repository_storage_path - Gitlab.config.repositories.storages[repository_storage] + Gitlab.config.repositories.storages[repository_storage]['path'] end def repository_path diff --git a/db/migrate/20161202152035_add_index_to_routes.rb b/db/migrate/20161202152035_add_index_to_routes.rb index 4a51337bda6..6d6c8906204 100644 --- a/db/migrate/20161202152035_add_index_to_routes.rb +++ b/db/migrate/20161202152035_add_index_to_routes.rb @@ -9,8 +9,13 @@ class AddIndexToRoutes < ActiveRecord::Migration disable_ddl_transaction! - def change + def up add_concurrent_index(:routes, :path, unique: true) add_concurrent_index(:routes, [:source_type, :source_id], unique: true) end + + def down + remove_index(:routes, :path) if index_exists? :routes, :path + remove_index(:routes, [:source_type, :source_id]) if index_exists? :routes, [:source_type, :source_id] + end end diff --git a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb index e9fcef1cd45..d7ef1aa83d9 100644 --- a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb +++ b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb @@ -9,7 +9,11 @@ class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration disable_ddl_transaction! - def change + def up add_concurrent_index :environments, [:project_id, :slug], unique: true end + + def down + remove_index :environments, [:project_id, :slug], unique: true if index_exists? :environments, [:project_id, :slug] + end end diff --git a/db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb b/db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb new file mode 100644 index 00000000000..e63d5927f86 --- /dev/null +++ b/db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb @@ -0,0 +1,37 @@ +class CreateDoorkeeperOpenidConnectTables < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + create_table :oauth_openid_requests do |t| + t.integer :access_grant_id, null: false + t.string :nonce, null: false + end + + if Gitlab::Database.postgresql? + # add foreign key without validation to avoid downtime on PostgreSQL, + # also see db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb + execute %q{ + ALTER TABLE "oauth_openid_requests" + ADD CONSTRAINT "fk_oauth_openid_requests_oauth_access_grants_access_grant_id" + FOREIGN KEY ("access_grant_id") + REFERENCES "oauth_access_grants" ("id") + NOT VALID; + } + else + execute %q{ + ALTER TABLE oauth_openid_requests + ADD CONSTRAINT fk_oauth_openid_requests_oauth_access_grants_access_grant_id + FOREIGN KEY (access_grant_id) + REFERENCES oauth_access_grants (id); + } + end + end + + def down + drop_table :oauth_openid_requests + end +end diff --git a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb index 241afc6b097..8fb1f9d5e73 100644 --- a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb +++ b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb @@ -60,7 +60,7 @@ class RemoveDotGitFromGroupNames < ActiveRecord::Migration def move_namespace(group_id, path_was, path) repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row| - Gitlab.config.repositories.storages[row['repository_storage']] + Gitlab.config.repositories.storages[row['repository_storage']]['path'] end.compact # Move the namespace directory in all storages paths used by member projects diff --git a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb index a0ce927161f..61dcc8c54f5 100644 --- a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb +++ b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb @@ -71,7 +71,7 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration route_exists = route_exists?(path) Gitlab.config.repositories.storages.each_value do |storage| - if route_exists || path_exists?(path, storage) + if route_exists || path_exists?(path, storage['path']) counter += 1 path = "#{base}#{counter}" @@ -84,7 +84,7 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration def move_namespace(namespace_id, path_was, path) repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{namespace_id}").map do |row| - Gitlab.config.repositories.storages[row['repository_storage']] + Gitlab.config.repositories.storages[row['repository_storage']]['path'] end.compact # Move the namespace directory in all storages paths used by member projects diff --git a/db/migrate/20161228124936_change_expires_at_to_date_in_personal_access_tokens.rb b/db/migrate/20161228124936_change_expires_at_to_date_in_personal_access_tokens.rb new file mode 100644 index 00000000000..af1bac897cc --- /dev/null +++ b/db/migrate/20161228124936_change_expires_at_to_date_in_personal_access_tokens.rb @@ -0,0 +1,18 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class ChangeExpiresAtToDateInPersonalAccessTokens < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = true + DOWNTIME_REASON = 'This migration requires downtime because it alters expires_at column from datetime to date' + + def up + change_column :personal_access_tokens, :expires_at, :date + end + + def down + change_column :personal_access_tokens, :expires_at, :datetime + end +end diff --git a/db/migrate/20161228135550_add_impersonation_to_personal_access_tokens.rb b/db/migrate/20161228135550_add_impersonation_to_personal_access_tokens.rb new file mode 100644 index 00000000000..ea9caceaa2c --- /dev/null +++ b/db/migrate/20161228135550_add_impersonation_to_personal_access_tokens.rb @@ -0,0 +1,18 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddImpersonationToPersonalAccessTokens < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def up + add_column_with_default :personal_access_tokens, :impersonation, :boolean, default: false, allow_null: false + end + + def down + remove_column :personal_access_tokens, :impersonation + end +end diff --git a/db/migrate/20170131221752_add_relative_position_to_issues.rb b/db/migrate/20170131221752_add_relative_position_to_issues.rb new file mode 100644 index 00000000000..1baad0893e3 --- /dev/null +++ b/db/migrate/20170131221752_add_relative_position_to_issues.rb @@ -0,0 +1,37 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddRelativePositionToIssues < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + disable_ddl_transaction! + + def up + add_column :issues, :relative_position, :integer + + add_concurrent_index :issues, :relative_position + end + + def down + remove_column :issues, :relative_position + + remove_index :issues, :relative_position if index_exists? :issues, :relative_position + end +end diff --git a/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb index 8f944930807..31ef458c44f 100644 --- a/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb +++ b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb @@ -5,7 +5,11 @@ class AddIndexToLabelsForTypeAndProject < ActiveRecord::Migration disable_ddl_transaction! - def change + def up add_concurrent_index :labels, [:type, :project_id] end + + def down + remove_index :labels, [:type, :project_id] if index_exists? :labels, [:type, :project_id] + end end diff --git a/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb b/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb index f922ed209aa..70fb0ef12f9 100644 --- a/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb +++ b/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb @@ -5,8 +5,13 @@ class AddIndexToLabelsForTitleAndProject < ActiveRecord::Migration disable_ddl_transaction! - def change + def up add_concurrent_index :labels, :title add_concurrent_index :labels, :project_id end + + def down + remove_index :labels, :title if index_exists? :labels, :title + remove_index :labels, :project_id if index_exists? :labels, :project_id + end end diff --git a/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb index 61e49c14fc0..07d4f8af27f 100644 --- a/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb +++ b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb @@ -5,7 +5,11 @@ class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration disable_ddl_transaction! - def change + def up add_concurrent_index :ci_trigger_requests, :commit_id end + + def down + remove_index :ci_trigger_requests, :commit_id if index_exists? :ci_trigger_requests, :commit_id + end end diff --git a/db/migrate/20170210103609_add_index_to_user_agent_detail.rb b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb index c01753cfbd2..2d8329b7862 100644 --- a/db/migrate/20170210103609_add_index_to_user_agent_detail.rb +++ b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb @@ -8,7 +8,11 @@ class AddIndexToUserAgentDetail < ActiveRecord::Migration disable_ddl_transaction! - def change - add_concurrent_index(:user_agent_details, [:subject_id, :subject_type]) + def up + add_concurrent_index :user_agent_details, [:subject_id, :subject_type] + end + + def down + remove_index :user_agent_details, [:subject_id, :subject_type] if index_exists? :user_agent_details, [:subject_id, :subject_type] end end diff --git a/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb index 7b1e687977b..65adc90c2c1 100644 --- a/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb +++ b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb @@ -4,7 +4,11 @@ class AddIndexForLatestSuccessfulPipeline < ActiveRecord::Migration disable_ddl_transaction! - def change - add_concurrent_index(:ci_commits, [:gl_project_id, :ref, :status]) + def up + add_concurrent_index :ci_commits, [:gl_project_id, :ref, :status] + end + + def down + remove_index :ci_commits, [:gl_project_id, :ref, :status] if index_exists? :ci_commits, [:gl_project_id, :ref, :status] end end diff --git a/db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb b/db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb new file mode 100644 index 00000000000..e206f9af636 --- /dev/null +++ b/db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb @@ -0,0 +1,20 @@ +class ValidateForeignKeysOnOauthOpenidRequests < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + if Gitlab::Database.postgresql? + execute %q{ + ALTER TABLE "oauth_openid_requests" + VALIDATE CONSTRAINT "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"; + } + end + end + + def down + # noop + end +end diff --git a/db/schema.rb b/db/schema.rb index 624cf9432d0..3ec5461f600 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -61,6 +61,7 @@ ActiveRecord::Schema.define(version: 20170306170512) do t.boolean "shared_runners_enabled", default: true, null: false t.integer "max_artifacts_size", default: 100, null: false t.string "runners_registration_token" + t.integer "max_pages_size", default: 100, null: false t.boolean "require_two_factor_authentication", default: false t.integer "two_factor_grace_period", default: 48 t.boolean "metrics_enabled", default: false @@ -109,7 +110,6 @@ ActiveRecord::Schema.define(version: 20170306170512) do t.boolean "html_emails_enabled", default: true t.string "plantuml_url" t.boolean "plantuml_enabled" - t.integer "max_pages_size", default: 100, null: false t.integer "terminal_max_session_time", default: 0, null: false t.string "default_artifacts_expire_in", default: "0", null: false t.integer "unique_ips_limit_per_user" @@ -530,6 +530,7 @@ ActiveRecord::Schema.define(version: 20170306170512) do t.text "title_html" t.text "description_html" t.integer "time_estimate" + t.integer "relative_position" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -541,6 +542,7 @@ ActiveRecord::Schema.define(version: 20170306170512) do add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree + add_index "issues", ["relative_position"], name: "index_issues_on_relative_position", using: :btree add_index "issues", ["state"], name: "index_issues_on_state", using: :btree add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} @@ -771,8 +773,8 @@ ActiveRecord::Schema.define(version: 20170306170512) do t.integer "visibility_level", default: 20, null: false t.boolean "request_access_enabled", default: false, null: false t.datetime "deleted_at" - t.boolean "lfs_enabled" t.text "description_html" + t.boolean "lfs_enabled" t.integer "parent_id" end @@ -878,6 +880,11 @@ ActiveRecord::Schema.define(version: 20170306170512) do add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree + create_table "oauth_openid_requests", force: :cascade do |t| + t.integer "access_grant_id", null: false + t.string "nonce", null: false + end + create_table "pages_domains", force: :cascade do |t| t.integer "project_id" t.text "certificate" @@ -894,10 +901,11 @@ ActiveRecord::Schema.define(version: 20170306170512) do t.string "token", null: false t.string "name", null: false t.boolean "revoked", default: false - t.datetime "expires_at" + t.date "expires_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "scopes", default: "--- []\n", null: false + t.boolean "impersonation", default: false, null: false end add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree @@ -1374,6 +1382,7 @@ ActiveRecord::Schema.define(version: 20170306170512) do add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade + add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id" add_foreign_key "personal_access_tokens", "users" add_foreign_key "project_authorizations", "projects", on_delete: :cascade add_foreign_key "project_authorizations", "users", on_delete: :cascade diff --git a/doc/administration/auth/crowd.md b/doc/administration/auth/crowd.md new file mode 100644 index 00000000000..2c289c67a6d --- /dev/null +++ b/doc/administration/auth/crowd.md @@ -0,0 +1,68 @@ +# Atlassian Crowd OmniAuth Provider + +## Configure a new Crowd application + +1. Choose 'Applications' in the top menu, then 'Add application'. +1. Go through the 'Add application' steps, entering the appropriate details. + The screenshot below shows an example configuration. + +  + +## Configure GitLab + +1. On your GitLab server, open the configuration file. + + **Omnibus:** + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + **Source:** + + ```sh + cd /home/git/gitlab + + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration) + for initial settings. + +1. Add the provider configuration: + + **Omnibus:** + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "crowd", + "args" => { + "crowd_server_url" => "CROWD_SERVER_URL", + "application_name" => "YOUR_APP_NAME", + "application_password" => "YOUR_APP_PASSWORD" + } + } + ] + ``` + + **Source:** + + ``` + - { name: 'crowd', + args: { + crowd_server_url: 'CROWD_SERVER_URL', + application_name: 'YOUR_APP_NAME', + application_password: 'YOUR_APP_PASSWORD' } } + ``` +1. Change `CROWD_SERVER_URL` to the URL of your Crowd server. +1. Change `YOUR_APP_NAME` to the application name from Crowd applications page. +1. Change `YOUR_APP_PASSWORD` to the application password you've set. +1. Save the configuration file. +1. [Reconfigure][] or [restart][] for the changes to take effect if you + installed GitLab via Omnibus or from source respectively. + +On the sign in page there should now be a Crowd tab in the sign in form. + +[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure +[restart]: ../restart_gitlab.md#installations-from-source diff --git a/doc/administration/auth/img/crowd_application.png b/doc/administration/auth/img/crowd_application.png Binary files differnew file mode 100644 index 00000000000..7deea9dac8e --- /dev/null +++ b/doc/administration/auth/img/crowd_application.png diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index 28e413ef447..f707039827b 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -512,6 +512,62 @@ Currently, there is no storage limitation, which means a user can upload an infinite amount of Docker images with arbitrary sizes. This setting will be configurable in future releases. +## Configure Container Registry notifications + +You can configure the Container Registry to send webhook notifications in +response to events happening within the registry. + +Read more about the Container Registry notifications config options in the +[Docker Registry notifications documentation][notifications-config]. + +>**Note:** +Multiple endpoints can be configured for the Container Registry. + + +**Omnibus GitLab installations** + +To configure a notification endpoint in Omnibus: + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + registry['notifications'] = [ + { + 'name' => 'test_endpoint', + 'url' => 'https://gitlab.example.com/notify', + 'timeout' => '500ms', + 'threshold' => 5, + 'backoff' => '1s', + 'headers' => { + "Authorization" => ["AUTHORIZATION_EXAMPLE_TOKEN"] + } + } + ] + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +--- + +**Installations from source** + +Configuring the notification endpoint is done in your registry config YML file created +when you [deployed your docker registry][registry-deploy]. + +Example: + +``` +notifications: + endpoints: + - name: alistener + disabled: false + url: https://my.listener.com/event + headers: <http.Header> + timeout: 500 + threshold: 5 + backoff: 1000 +``` + ## Changelog **GitLab 8.8 ([source docs][8-8-docs])** @@ -532,3 +588,5 @@ configurable in future releases. [registry-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/registry-ssl [existing-domain]: #configure-container-registry-under-an-existing-gitlab-domain [new-domain]: #configure-container-registry-under-its-own-domain +[notifications-config]: https://docs.docker.com/registry/notifications/ +[registry-notifications-config]: https://docs.docker.com/registry/configuration/#notifications
\ No newline at end of file diff --git a/doc/administration/repository_storage_paths.md b/doc/administration/repository_storage_paths.md index d6aa6101026..55a45119525 100644 --- a/doc/administration/repository_storage_paths.md +++ b/doc/administration/repository_storage_paths.md @@ -52,9 +52,12 @@ respectively. # Paths where repositories can be stored. Give the canonicalized absolute pathname. # NOTE: REPOS PATHS MUST NOT CONTAIN ANY SYMLINK!!! storages: # You must have at least a 'default' storage path. - default: /home/git/repositories - nfs: /mnt/nfs/repositories - cephfs: /mnt/cephfs/repositories + default: + path: /home/git/repositories + nfs: + path: /mnt/nfs/repositories + cephfs: + path: /mnt/cephfs/repositories ``` 1. [Restart GitLab] for the changes to take effect. @@ -75,9 +78,9 @@ working, you can remove the `repos_path` line. ```ruby git_data_dirs({ - "default" => "/var/opt/gitlab/git-data", - "nfs" => "/mnt/nfs/git-data", - "cephfs" => "/mnt/cephfs/git-data" + "default" => { "path" => "/var/opt/gitlab/git-data" }, + "nfs" => { "path" => "/mnt/nfs/git-data" }, + "cephfs" => { "path" => "/mnt/cephfs/git-data" } }) ``` diff --git a/doc/api/README.md b/doc/api/README.md index 285cd2435ac..58d090b8f5e 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -11,7 +11,6 @@ following locations: - [Award Emoji](award_emoji.md) - [Branches](branches.md) - [Broadcast Messages](broadcast_messages.md) -- [Builds](builds.md) - [Build Variables](build_variables.md) - [Commits](commits.md) - [Deployments](deployments.md) @@ -23,6 +22,7 @@ following locations: - [Group Members](members.md) - [Issues](issues.md) - [Issue Boards](boards.md) +- [Jobs](jobs.md) - [Keys](keys.md) - [Labels](labels.md) - [Merge Requests](merge_requests.md) @@ -221,6 +221,14 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23 curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v4/projects" ``` +## Impersonation Tokens + +Impersonation Tokens are a type of Personal Access Token that can only be created by an admin for a specific user. These can be used by automated tools +to authenticate with the API as a specific user, as a better alternative to using the user's password or private token directly, which may change over time, +and to using the [Sudo](#sudo) feature, which requires the tool to know an admin's password or private token, which can change over time as well and are extremely powerful. + +For more information about the usage please refer to the [Users](users.md) page + ## Pagination Sometimes the returned result will span across many pages. When listing diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md index 3470f8ce497..f57928d3c93 100644 --- a/doc/api/award_emoji.md +++ b/doc/api/award_emoji.md @@ -14,17 +14,17 @@ requests, snippets, and notes/comments. Issues, merge requests, snippets, and no Gets a list of all award emoji ``` -GET /projects/:id/issues/:issue_id/award_emoji -GET /projects/:id/merge_requests/:merge_request_id/award_emoji +GET /projects/:id/issues/:issue_iid/award_emoji +GET /projects/:id/merge_requests/:merge_request_iid/award_emoji GET /projects/:id/snippets/:snippet_id/award_emoji ``` Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `awardable_id` | integer | yes | The ID of an awardable | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji @@ -74,18 +74,18 @@ Example Response: Gets a single award emoji from an issue, snippet, or merge request. ``` -GET /projects/:id/issues/:issue_id/award_emoji/:award_id -GET /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id +GET /projects/:id/issues/:issue_iid/award_emoji/:award_id +GET /projects/:id/merge_requests/:merge_request_iid/award_emoji/:award_id GET /projects/:id/snippets/:snippet_id/award_emoji/:award_id ``` Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `awardable_id` | integer | yes | The ID of an awardable | -| `award_id` | integer | yes | The ID of the award emoji | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable | +| `award_id` | integer | yes | The ID of the award emoji | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/1 @@ -117,18 +117,18 @@ Example Response: This end point creates an award emoji on the specified resource ``` -POST /projects/:id/issues/:issue_id/award_emoji -POST /projects/:id/merge_requests/:merge_request_id/award_emoji +POST /projects/:id/issues/:issue_iid/award_emoji +POST /projects/:id/merge_requests/:merge_request_iid/award_emoji POST /projects/:id/snippets/:snippet_id/award_emoji ``` Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `awardable_id` | integer | yes | The ID of an awardable | -| `name` | string | yes | The name of the emoji, without colons | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable | +| `name` | string | yes | The name of the emoji, without colons | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji?name=blowfish @@ -161,18 +161,18 @@ Sometimes its just not meant to be, and you'll have to remove your award. Only a admins or the author of the award. ``` -DELETE /projects/:id/issues/:issue_id/award_emoji/:award_id -DELETE /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id +DELETE /projects/:id/issues/:issue_iid/award_emoji/:award_id +DELETE /projects/:id/merge_requests/:merge_request_iid/award_emoji/:award_id DELETE /projects/:id/snippets/:snippet_id/award_emoji/:award_id ``` Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of an issue | -| `award_id` | integer | yes | The ID of a award_emoji | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_iid` | integer | yes | The internal ID of an issue | +| `award_id` | integer | yes | The ID of a award_emoji | ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/344 @@ -188,16 +188,16 @@ easily adapted for notes on a Merge Request. ### List a note's award emoji ``` -GET /projects/:id/issues/:issue_id/notes/:note_id/award_emoji +GET /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji ``` Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of an issue | -| `note_id` | integer | yes | The ID of an note | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_iid` | integer | yes | The internal ID of an issue | +| `note_id` | integer | yes | The ID of an note | ```bash @@ -230,17 +230,17 @@ Example Response: ### Get single note's award emoji ``` -GET /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id +GET /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji/:award_id ``` Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of an issue | -| `note_id` | integer | yes | The ID of a note | -| `award_id` | integer | yes | The ID of the award emoji | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_iid` | integer | yes | The internal ID of an issue | +| `note_id` | integer | yes | The ID of a note | +| `award_id` | integer | yes | The ID of the award emoji | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji/2 @@ -270,17 +270,17 @@ Example Response: ### Award a new emoji on a note ``` -POST /projects/:id/issues/:issue_id/notes/:note_id/award_emoji +POST /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji ``` Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of an issue | -| `note_id` | integer | yes | The ID of a note | -| `name` | string | yes | The name of the emoji, without colons | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_iid` | integer | yes | The internal ID of an issue | +| `note_id` | integer | yes | The ID of a note | +| `name` | string | yes | The name of the emoji, without colons | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji?name=rocket @@ -313,17 +313,17 @@ Sometimes its just not meant to be, and you'll have to remove your award. Only a admins or the author of the award. ``` -DELETE /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id +DELETE /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji/:award_id ``` Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of an issue | -| `note_id` | integer | yes | The ID of a note | -| `award_id` | integer | yes | The ID of a award_emoji | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_iid` | integer | yes | The internal ID of an issue | +| `note_id` | integer | yes | The ID of a note | +| `award_id` | integer | yes | The ID of a award_emoji | ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/345 diff --git a/doc/api/builds.md b/doc/api/builds.md new file mode 100644 index 00000000000..a6edda68bc4 --- /dev/null +++ b/doc/api/builds.md @@ -0,0 +1 @@ +This document was moved to [another location](jobs.md). diff --git a/doc/api/ci/builds.md b/doc/api/ci/builds.md index b6d79706a84..c8374d94716 100644 --- a/doc/api/ci/builds.md +++ b/doc/api/ci/builds.md @@ -5,7 +5,7 @@ API used by runners to receive and update builds. >**Note:** This API is intended to be used only by Runners as their own communication channel. For the consumer API see the -[Builds API](../builds.md). +[Jobs API](../jobs.md). ## Authentication diff --git a/doc/api/issues.md b/doc/api/issues.md index 4047ff14af2..e25841926f8 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -261,13 +261,13 @@ Example response: Get a single project issue. ``` -GET /projects/:id/issues/:issue_id +GET /projects/:id/issues/:issue_iid ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `issue_id`| integer | yes | The ID of a project's issue | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_iid` | integer | yes | The internal ID of a project's issue | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/41 @@ -385,22 +385,22 @@ Updates an existing project issue. This call is also used to mark an issue as closed. ``` -PUT /projects/:id/issues/:issue_id +PUT /projects/:id/issues/:issue_iid ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of a project's issue | -| `title` | string | no | The title of an issue | -| `description` | string | no | The description of an issue | -| `confidential` | boolean | no | Updates an issue to be confidential | -| `assignee_id` | integer | no | The ID of a user to assign the issue to | -| `milestone_id` | integer | no | The ID of a milestone to assign the issue to | -| `labels` | string | no | Comma-separated label names for an issue | -| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it | -| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) | -| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_iid` | integer | yes | The internal ID of a project's issue | +| `title` | string | no | The title of an issue | +| `description` | string | no | The description of an issue | +| `confidential` | boolean | no | Updates an issue to be confidential | +| `assignee_id` | integer | no | The ID of a user to assign the issue to | +| `milestone_id` | integer | no | The ID of a milestone to assign the issue to | +| `labels` | string | no | Comma-separated label names for an issue | +| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it | +| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) | +| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | ```bash curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85?state_event=close @@ -444,13 +444,13 @@ Example response: Only for admins and project owners. Soft deletes the issue in question. ``` -DELETE /projects/:id/issues/:issue_id +DELETE /projects/:id/issues/:issue_iid ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of a project's issue | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_iid` | integer | yes | The internal ID of a project's issue | ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85 @@ -466,14 +466,14 @@ If a given label and/or milestone with the same name also exists in the target project, it will then be assigned to the issue that is being moved. ``` -POST /projects/:id/issues/:issue_id/move +POST /projects/:id/issues/:issue_iid/move ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of a project's issue | -| `to_project_id` | integer | yes | The ID of the new project | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_iid` | integer | yes | The internal ID of a project's issue | +| `to_project_id` | integer | yes | The ID of the new project | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85/move @@ -522,13 +522,13 @@ If the user is already subscribed to the issue, the status code `304` is returned. ``` -POST /projects/:id/issues/:issue_id/subscribe +POST /projects/:id/issues/:issue_iid/subscribe ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of a project's issue | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_iid` | integer | yes | The internal ID of a project's issue | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/subscribe @@ -577,13 +577,13 @@ from it. If the user is not subscribed to the issue, the status code `304` is returned. ``` -POST /projects/:id/issues/:issue_id/unsubscribe +POST /projects/:id/issues/:issue_iid/unsubscribe ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of a project's issue | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_iid` | integer | yes | The internal ID of a project's issue | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/unsubscribe @@ -596,13 +596,13 @@ there already exists a todo for the user on that issue, status code `304` is returned. ``` -POST /projects/:id/issues/:issue_id/todo +POST /projects/:id/issues/:issue_iid/todo ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of a project's issue | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_iid` | integer | yes | The internal ID of a project's issue | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/todo @@ -687,14 +687,14 @@ Example response: Sets an estimated time of work for this issue. ``` -POST /projects/:id/issues/:issue_id/time_estimate +POST /projects/:id/issues/:issue_iid/time_estimate ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of a project's issue | -| `duration` | string | yes | The duration in human format. e.g: 3h30m | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_iid` | integer | yes | The internal ID of a project's issue | +| `duration` | string | yes | The duration in human format. e.g: 3h30m | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/time_estimate?duration=3h30m @@ -716,13 +716,13 @@ Example response: Resets the estimated time for this issue to 0 seconds. ``` -POST /projects/:id/issues/:issue_id/reset_time_estimate +POST /projects/:id/issues/:issue_iid/reset_time_estimate ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of a project's issue | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_iid` | integer | yes | The internal ID of a project's issue | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/reset_time_estimate @@ -744,14 +744,14 @@ Example response: Adds spent time for this issue ``` -POST /projects/:id/issues/:issue_id/add_spent_time +POST /projects/:id/issues/:issue_iid/add_spent_time ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of a project's issue | -| `duration` | string | yes | The duration in human format. e.g: 3h30m | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_iid` | integer | yes | The internal ID of a project's issue | +| `duration` | string | yes | The duration in human format. e.g: 3h30m | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/add_spent_time?duration=1h @@ -773,13 +773,13 @@ Example response: Resets the total spent time for this issue to 0 seconds. ``` -POST /projects/:id/issues/:issue_id/reset_spent_time +POST /projects/:id/issues/:issue_iid/reset_spent_time ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of a project's issue | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_iid` | integer | yes | The internal ID of a project's issue | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/reset_spent_time @@ -799,13 +799,13 @@ Example response: ## Get time tracking stats ``` -GET /projects/:id/issues/:issue_id/time_stats +GET /projects/:id/issues/:issue_iid/time_stats ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of a project's issue | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_iid` | integer | yes | The internal ID of a project's issue | ```bash curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/time_stats diff --git a/doc/api/jobs.md b/doc/api/jobs.md index 296f1d025dd..7340123e09d 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -1,6 +1,6 @@ # Jobs API -## List project jobs +## List project jobs Get a list of jobs in a project. @@ -14,7 +14,123 @@ GET /projects/:id/jobs | `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all jobs if none provided | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/jobs?scope%5B0%5D=pending&scope%5B1%5D=running' +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/jobs?scope[]=pending&scope[]=running' +``` + +Example of response + +```json +[ + { + "commit": { + "author_email": "admin@example.com", + "author_name": "Administrator", + "created_at": "2015-12-24T16:51:14.000+01:00", + "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "message": "Test the CI integration.", + "short_id": "0ff3ae19", + "title": "Test the CI integration." + }, + "coverage": null, + "created_at": "2015-12-24T15:51:21.802Z", + "artifacts_file": { + "filename": "artifacts.zip", + "size": 1000 + }, + "finished_at": "2015-12-24T17:54:27.895Z", + "id": 7, + "name": "teaspoon", + "pipeline": { + "id": 6, + "ref": "master", + "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "status": "pending" + }, + "ref": "master", + "runner": null, + "stage": "test", + "started_at": "2015-12-24T17:54:27.722Z", + "status": "failed", + "tag": false, + "user": { + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "bio": null, + "created_at": "2015-12-21T13:14:24.077Z", + "id": 1, + "is_admin": true, + "linkedin": "", + "name": "Administrator", + "skype": "", + "state": "active", + "twitter": "", + "username": "root", + "web_url": "http://gitlab.dev/root", + "website_url": "" + } + }, + { + "commit": { + "author_email": "admin@example.com", + "author_name": "Administrator", + "created_at": "2015-12-24T16:51:14.000+01:00", + "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "message": "Test the CI integration.", + "short_id": "0ff3ae19", + "title": "Test the CI integration." + }, + "coverage": null, + "created_at": "2015-12-24T15:51:21.727Z", + "artifacts_file": null, + "finished_at": "2015-12-24T17:54:24.921Z", + "id": 6, + "name": "spinach:other", + "pipeline": { + "id": 6, + "ref": "master", + "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "status": "pending" + }, + "ref": "master", + "runner": null, + "stage": "test", + "started_at": "2015-12-24T17:54:24.729Z", + "status": "failed", + "tag": false, + "user": { + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "bio": null, + "created_at": "2015-12-21T13:14:24.077Z", + "id": 1, + "is_admin": true, + "linkedin": "", + "name": "Administrator", + "skype": "", + "state": "active", + "twitter": "", + "username": "root", + "web_url": "http://gitlab.dev/root", + "website_url": "" + } + } +] +``` + +## List pipeline jobs + +Get a list of jobs for a pipeline. + +``` +GET /projects/:id/pipeline/:pipeline_id/jobs +``` + +| Attribute | Type | Required | Description | +|---------------|--------------------------------|----------|----------------------| +| `id` | integer | yes | The ID of a project | +| `pipeline_id` | integer | yes | The ID of a pipeline | +| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all jobs if none provided | + +``` +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/pipelines/6/jobs?scope[]=pending&scope[]=running' ``` Example of response diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 09d23cd2ff6..2e0545da1c4 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -82,13 +82,13 @@ Parameters: Shows information about a single merge request. ``` -GET /projects/:id/merge_requests/:merge_request_id +GET /projects/:id/merge_requests/:merge_request_iid ``` Parameters: - `id` (required) - The ID of a project -- `merge_request_id` (required) - The ID of MR +- `merge_request_iid` (required) - The internal ID of the merge request ```json { @@ -150,13 +150,13 @@ Parameters: Get a list of merge request commits. ``` -GET /projects/:id/merge_requests/:merge_request_id/commits +GET /projects/:id/merge_requests/:merge_request_iid/commits ``` Parameters: - `id` (required) - The ID of a project -- `merge_request_id` (required) - The ID of MR +- `merge_request_iid` (required) - The internal ID of the merge request ```json @@ -187,13 +187,13 @@ Parameters: Shows information about the merge request including its files and changes. ``` -GET /projects/:id/merge_requests/:merge_request_id/changes +GET /projects/:id/merge_requests/:merge_request_iid/changes ``` Parameters: - `id` (required) - The ID of a project -- `merge_request_id` (required) - The ID of MR +- `merge_request_iid` (required) - The internal ID of the merge request ```json { @@ -269,18 +269,18 @@ Creates a new merge request. POST /projects/:id/merge_requests ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | string | yes | The ID of a project | -| `source_branch` | string | yes | The source branch | -| `target_branch` | string | yes | The target branch | -| `title` | string | yes | Title of MR | -| `assignee_id` | integer | no | Assignee user ID | -| `description` | string | no | Description of MR | -| `target_project_id` | integer | no | The target project (numeric id) | -| `labels` | string | no | Labels for MR as a comma-separated list | -| `milestone_id` | integer | no | The ID of a milestone | -| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | string | yes | The ID of a project | +| `source_branch` | string | yes | The source branch | +| `target_branch` | string | yes | The target branch | +| `title` | string | yes | Title of MR | +| `assignee_id` | integer | no | Assignee user ID | +| `description` | string | no | Description of MR | +| `target_project_id` | integer | no | The target project (numeric id) | +| `labels` | string | no | Labels for MR as a comma-separated list | +| `milestone_id` | integer | no | The ID of a milestone | +| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | ```json { @@ -342,21 +342,21 @@ POST /projects/:id/merge_requests Updates an existing merge request. You can change the target branch, title, or even close the MR. ``` -PUT /projects/:id/merge_requests/:merge_request_id +PUT /projects/:id/merge_requests/:merge_request_iid ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | string | yes | The ID of a project | -| `merge_request_id` | integer | yes | The ID of a merge request | -| `target_branch` | string | no | The target branch | -| `title` | string | no | Title of MR | -| `assignee_id` | integer | no | Assignee user ID | -| `description` | string | no | Description of MR | -| `state_event` | string | no | New state (close/reopen) | -| `labels` | string | no | Labels for MR as a comma-separated list | -| `milestone_id` | integer | no | The ID of a milestone | -| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | string | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The ID of a merge request | +| `target_branch` | string | no | The target branch | +| `title` | string | no | Title of MR | +| `assignee_id` | integer | no | Assignee user ID | +| `description` | string | no | Description of MR | +| `state_event` | string | no | New state (close/reopen) | +| `labels` | string | no | Labels for MR as a comma-separated list | +| `milestone_id` | integer | no | The ID of a milestone | +| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | Must include at least one non-required attribute from above. @@ -419,13 +419,13 @@ Must include at least one non-required attribute from above. Only for admins and project owners. Soft deletes the merge request in question. ``` -DELETE /projects/:id/merge_requests/:merge_request_id +DELETE /projects/:id/merge_requests/:merge_request_iid ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `merge_request_id` | integer | yes | The ID of a project's merge request | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The internal ID of the merge request | ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/merge_requests/85 @@ -445,13 +445,13 @@ If the `sha` parameter is passed and does not match the HEAD of the source - you If you don't have permissions to accept this merge request - you'll get a `401` ``` -PUT /projects/:id/merge_requests/:merge_request_id/merge +PUT /projects/:id/merge_requests/:merge_request_iid/merge ``` Parameters: - `id` (required) - The ID of a project -- `merge_request_id` (required) - ID of MR +- `merge_request_iid` (required) - Internal ID of MR - `merge_commit_message` (optional) - Custom merge commit message - `should_remove_source_branch` (optional) - if `true` removes the source branch - `merge_when_pipeline_succeeds` (optional) - if `true` the MR is merged when the pipeline succeeds @@ -520,12 +520,12 @@ If the merge request is already merged or closed - you get `405` and error messa In case the merge request is not set to be merged when the pipeline succeeds, you'll also get a `406` error. ``` -PUT /projects/:id/merge_requests/:merge_request_id/cancel_merge_when_pipeline_succeeds +PUT /projects/:id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds ``` Parameters: - `id` (required) - The ID of a project -- `merge_request_id` (required) - ID of MR +- `merge_request_iid` (required) - Internal ID of MR ```json { @@ -591,13 +591,13 @@ Comments are done via the [notes](notes.md) resource. Get all the issues that would be closed by merging the provided merge request. ``` -GET /projects/:id/merge_requests/:merge_request_id/closes_issues +GET /projects/:id/merge_requests/:merge_request_iid/closes_issues ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `merge_request_id` | integer | yes | The ID of the merge request | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The internal ID of the merge request | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/76/merge_requests/1/closes_issues @@ -666,13 +666,13 @@ Subscribes the authenticated user to a merge request to receive notification. If status code `304` is returned. ``` -POST /projects/:id/merge_requests/:merge_request_id/subscribe +POST /projects/:id/merge_requests/:merge_request_iid/subscribe ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `merge_request_id` | integer | yes | The ID of the merge request | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The internal ID of the merge request | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/17/subscribe @@ -740,13 +740,13 @@ notifications from that merge request. If the user is not subscribed to the merge request, the status code `304` is returned. ``` -POST /projects/:id/merge_requests/:merge_request_id/unsubscribe +POST /projects/:id/merge_requests/:merge_request_iid/unsubscribe ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `merge_request_id` | integer | yes | The ID of the merge request | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The internal ID of the merge request | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/17/unsubscribe @@ -814,13 +814,13 @@ If there already exists a todo for the user on that merge request, status code `304` is returned. ``` -POST /projects/:id/merge_requests/:merge_request_id/todo +POST /projects/:id/merge_requests/:merge_request_iid/todo ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `merge_request_id` | integer | yes | The ID of the merge request | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The internal ID of the merge request | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/27/todo @@ -914,13 +914,13 @@ Example response: Get a list of merge request diff versions. ``` -GET /projects/:id/merge_requests/:merge_request_id/versions +GET /projects/:id/merge_requests/:merge_request_iid/versions ``` -| Attribute | Type | Required | Description | -| --------- | ------- | -------- | --------------------- | -| `id` | String | yes | The ID of the project | -| `merge_request_id` | integer | yes | The ID of the merge request | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | String | yes | The ID of the project | +| `merge_request_iid` | integer | yes | The internal ID of the merge request | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/merge_requests/1/versions @@ -955,14 +955,14 @@ Example response: Get a single merge request diff version. ``` -GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id +GET /projects/:id/merge_requests/:merge_request_iid/versions/:version_id ``` -| Attribute | Type | Required | Description | -| --------- | ------- | -------- | --------------------- | -| `id` | String | yes | The ID of the project | -| `merge_request_id` | integer | yes | The ID of the merge request | -| `version_id` | integer | yes | The ID of the merge request diff version | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `id` | String | yes | The ID of the project | +| `merge_request_iid` | integer | yes | The internal ID of the merge request | +| `version_id` | integer | yes | The ID of the merge request diff version | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/merge_requests/1/versions/1 @@ -1022,14 +1022,14 @@ Example response: Sets an estimated time of work for this merge request. ``` -POST /projects/:id/merge_requests/:merge_request_id/time_estimate +POST /projects/:id/merge_requests/:merge_request_iid/time_estimate ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `merge_request_id` | integer | yes | The ID of a project's merge request | -| `duration` | string | yes | The duration in human format. e.g: 3h30m | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The internal ID of the merge request | +| `duration` | string | yes | The duration in human format. e.g: 3h30m | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/time_estimate?duration=3h30m @@ -1051,13 +1051,13 @@ Example response: Resets the estimated time for this merge request to 0 seconds. ``` -POST /projects/:id/merge_requests/:merge_request_id/reset_time_estimate +POST /projects/:id/merge_requests/:merge_request_iid/reset_time_estimate ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `merge_request_id` | integer | yes | The ID of a project's merge_request | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The internal ID of a project's merge_request | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/reset_time_estimate @@ -1079,14 +1079,14 @@ Example response: Adds spent time for this merge request ``` -POST /projects/:id/merge_requests/:merge_request_id/add_spent_time +POST /projects/:id/merge_requests/:merge_request_iid/add_spent_time ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `merge_request_id` | integer | yes | The ID of a project's merge request | -| `duration` | string | yes | The duration in human format. e.g: 3h30m | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The internal ID of the merge request | +| `duration` | string | yes | The duration in human format. e.g: 3h30m | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/add_spent_time?duration=1h @@ -1108,13 +1108,13 @@ Example response: Resets the total spent time for this merge request to 0 seconds. ``` -POST /projects/:id/merge_requests/:merge_request_id/reset_spent_time +POST /projects/:id/merge_requests/:merge_request_iid/reset_spent_time ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `merge_request_id` | integer | yes | The ID of a project's merge_request | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The internal ID of a project's merge_request | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/reset_spent_time @@ -1134,13 +1134,13 @@ Example response: ## Get time tracking stats ``` -GET /projects/:id/merge_requests/:merge_request_id/time_stats +GET /projects/:id/merge_requests/:merge_request_iid/time_stats ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of a project | -| `merge_request_id` | integer | yes | The ID of a project's merge request | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_iid` | integer | yes | The internal ID of the merge request | ```bash curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/93/time_stats diff --git a/doc/api/projects.md b/doc/api/projects.md index 28e4bfe39dc..686f3dba35d 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -20,7 +20,7 @@ Constants for project visibility levels are next: ## List projects -Get a list of projects for which the authenticated user is a member. +Get a list of visible projects for authenticated user. When being accessed without authentication, all public projects are returned. ``` GET /projects diff --git a/doc/api/repositories.md b/doc/api/repositories.md index ddd11bb2a14..b1bf9ca07cc 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -15,7 +15,7 @@ Parameters: - `id` (required) - The ID of a project - `path` (optional) - The path inside repository. Used to get contend of subdirectories -- `ref_name` (optional) - The name of a repository branch or tag or if not given the default branch +- `ref` (optional) - The name of a repository branch or tag or if not given the default branch - `recursive` (optional) - Boolean value used to get a recursive tree (false by default) ```json @@ -72,10 +72,11 @@ Parameters: ] ``` -## Raw file content +## Get a blob from repository -Get the raw file contents for a file by commit SHA and path. This endpoint can -be accessed without authentication if the repository is publicly accessible. +Allows you to receive information about blob in repository like size and +content. Note that blob content is Base64 encoded. This endpoint can be accessed +without authentication if the repository is publicly accessible. ``` GET /projects/:id/repository/blobs/:sha @@ -85,7 +86,6 @@ Parameters: - `id` (required) - The ID of a project - `sha` (required) - The commit or branch name -- `filepath` (required) - The path the file ## Raw blob content @@ -93,7 +93,7 @@ Get the raw file contents for a blob by blob SHA. This endpoint can be accessed without authentication if the repository is publicly accessible. ``` -GET /projects/:id/repository/raw_blobs/:sha +GET /projects/:id/repository/blobs/:sha/raw ``` Parameters: diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index ec56d0efa1c..aec91abd390 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -11,11 +11,11 @@ content. Note that file content is Base64 encoded. This endpoint can be accessed without authentication if the repository is publicly accessible. ``` -GET /projects/:id/repository/files +GET /projects/:id/repository/files/:file_path ``` ```bash -curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files?file_path=app/models/key.rb&ref=master' +curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fmodels%2Fkey%2Erb?ref=master' ``` Example response: @@ -36,17 +36,32 @@ Example response: Parameters: -- `file_path` (required) - Full path to new file. Ex. lib/class.rb +- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb +- `ref` (required) - The name of branch, tag or commit + +## Get raw file from repository + +``` +GET /projects/:id/repository/files/:file_path/raw +``` + +```bash +curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fmodels%2Fkey%2Erb/raw?ref=master' +``` + +Parameters: + +- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb - `ref` (required) - The name of branch, tag or commit ## Create new file in repository ``` -POST /projects/:id/repository/files +POST /projects/:id/repository/files/:file_path ``` ```bash -curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file' +curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fprojectrb%2E?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file' ``` Example response: @@ -60,7 +75,7 @@ Example response: Parameters: -- `file_path` (required) - Full path to new file. Ex. lib/class.rb +- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb - `branch` (required) - The name of branch - `encoding` (optional) - Change encoding to 'base64'. Default is text. - `author_email` (optional) - Specify the commit author's email address @@ -71,11 +86,11 @@ Parameters: ## Update existing file in repository ``` -PUT /projects/:id/repository/files +PUT /projects/:id/repository/files/:file_path ``` ```bash -curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file' +curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file' ``` Example response: @@ -89,7 +104,7 @@ Example response: Parameters: -- `file_path` (required) - Full path to file. Ex. lib/class.rb +- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb - `branch` (required) - The name of branch - `encoding` (optional) - Change encoding to 'base64'. Default is text. - `author_email` (optional) - Specify the commit author's email address @@ -109,11 +124,11 @@ Currently gitlab-shell has a boolean return code, preventing GitLab from specify ## Delete existing file in repository ``` -DELETE /projects/:id/repository/files +DELETE /projects/:id/repository/files/:file_path ``` ```bash -curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' +curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' ``` Example response: @@ -127,7 +142,7 @@ Example response: Parameters: -- `file_path` (required) - Full path to file. Ex. lib/class.rb +- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb - `branch` (required) - The name of branch - `author_email` (optional) - Specify the commit author's email address - `author_name` (optional) - Specify the commit author's name diff --git a/doc/api/settings.md b/doc/api/settings.md index 38a37cd920c..ad975e2e325 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -20,7 +20,7 @@ Example response: ```json { - "default_projects_limit" : 10, + "default_projects_limit" : 100000, "signup_enabled" : true, "id" : 1, "default_branch_protection" : 2, @@ -60,7 +60,7 @@ PUT /application/settings | Attribute | Type | Required | Description | | --------- | ---- | :------: | ----------- | -| `default_projects_limit` | integer | no | Project limit per user. Default is `10` | +| `default_projects_limit` | integer | no | Project limit per user. Default is `100000` | | `signup_enabled` | boolean | no | Enable registration. Default is `true`. | | `signin_enabled` | boolean | no | Enable login via a GitLab account. Default is `true`. | | `gravatar_enabled` | boolean | no | Enable Gravatar | @@ -98,7 +98,7 @@ Example response: ```json { "id": 1, - "default_projects_limit": 10, + "default_projects_limit": 100000, "signup_enabled": true, "signin_enabled": true, "gravatar_enabled": true, diff --git a/doc/api/users.md b/doc/api/users.md index 95f6bcfccb6..14b5c6c713e 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -827,3 +827,99 @@ Example response: } ] ``` + +## Retrieve user impersonation tokens + +It retrieves every impersonation token of the user. Note that only administrators can do this. +This function takes pagination parameters `page` and `per_page` to restrict the list of impersonation tokens. + +``` +GET /users/:user_id/impersonation_tokens +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `user_id` | integer | yes | The ID of the user | +| `state` | string | no | filter tokens based on state (all, active, inactive) | + +Example response: +```json +[ + { + "id": 1, + "name": "mytoken", + "revoked": false, + "expires_at": "2017-01-04", + "scopes": ['api'], + "active": true, + "impersonation": true, + "token": "9koXpg98eAheJpvBs5tK" + } +] +``` + +## Show a user's impersonation token + +It shows a user's impersonation token. Note that only administrators can do this. + +``` +GET /users/:user_id/impersonation_tokens/:impersonation_token_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `user_id` | integer | yes | The ID of the user | +| `impersonation_token_id` | integer | yes | The ID of the impersonation token | + +## Create a impersonation token + +It creates a new impersonation token. Note that only administrators can do this. +You are only able to create impersonation tokens to impersonate the user and perform +both API calls and Git reads and writes. The user will not see these tokens in his profile +settings page. + +``` +POST /users/:user_id/impersonation_tokens +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `user_id` | integer | yes | The ID of the user | +| `name` | string | yes | The name of the impersonation token | +| `expires_at` | date | no | The expiration date of the impersonation token | +| `scopes` | array | no | The array of scopes of the impersonation token (api, read_user) | + +Example response: +```json +{ + "id": 1, + "name": "mytoken", + "revoked": false, + "expires_at": "2017-01-04", + "scopes": ['api'], + "active": true, + "impersonation": true, + "token": "9koXpg98eAheJpvBs5tK" +} +``` + +## Revoke an impersonation token + +It revokes an impersonation token. Note that only administrators can revoke impersonation tokens. + +``` +DELETE /users/:user_id/impersonation_tokens/:impersonation_token_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `user_id` | integer | yes | The ID of the user | +| `impersonation_token_id` | integer | yes | The ID of the impersonation token | diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index e5ef64fa8dc..0794156bc39 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -1,8 +1,10 @@ # V3 to V4 version -Our V4 API version is currently available as *Beta*! It means that V3 -will still be supported and remain unchanged for now, but be aware that the following -changes are in V4: +Since GitLab 9.0, API V4 is the preferred version to be used. + +V3 will remain working until at least GitLab 9.3. The V3 API documentation is still [available](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/doc/api/README.md). + +Below are the changes made between V3 and V4. ### 8.17 @@ -68,3 +70,13 @@ changes are in V4: - Rename Build Triggers to be Pipeline Triggers API [!9713](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9713) - `POST /projects/:id/trigger/builds` to `POST /projects/:id/trigger/pipeline` - Require description when creating a new trigger `POST /projects/:id/triggers` +- Simplify project payload exposed on Environment endpoints [!9675](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9675) +- API uses merge request `IID`s (internal ID, as in the web UI) rather than `ID`s. This affects the merge requests, award emoji, todos, and time tracking APIs. [!9530](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9530) +- API uses issue `IID`s (internal ID, as in the web UI) rather than `ID`s. This affects the issues, award emoji, todos, and time tracking APIs. [!9530](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9530) +- Change initial page from `0` to `1` on `GET projects/:id/repository/commits` (like on the rest of the API) [!9679] (https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9679) +- Return correct `Link` header data for `GET projects/:id/repository/commits` [!9679] (https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9679) +- Update endpoints for repository files [!9637](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9637) + - Moved `/projects/:id/repository/files?file_path=:file_path` to `/projects/:id/repository/files/:file_path` (`:file_path` should be URL-encoded) + - `/projects/:id/repository/blobs/:sha` now returns JSON attributes for the blob identified by `:sha`, instead of finding the commit identified by `:sha` and returning the raw content of the blob in that commit identified by the required `?filepath=:filepath` + - Moved `/projects/:id/repository/commits/:sha/blob?file_path=:file_path` and `/projects/:id/repository/blobs/:sha?file_path=:file_path` to `/projects/:id/repository/files/:file_path/raw?ref=:sha` + - `/projects/:id/repository/tree` parameter `ref_name` has been renamed to `ref` for consistency diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index a9e25187b88..4c3e7c4e86e 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -35,17 +35,28 @@ version of Runner required. | **CI_SERVER_NAME** | all | all | The name of CI server that is used to coordinate jobs | | **CI_SERVER_VERSION** | all | all | GitLab version that is used to schedule jobs | | **CI_SERVER_REVISION** | all | all | GitLab revision that is used to schedule jobs | -| **CI_BUILD_ID** | all | all | The unique id of the current job that GitLab CI uses internally | -| **CI_BUILD_REF** | all | all | The commit revision for which project is built | -| **CI_BUILD_TAG** | all | 0.5 | The commit tag name. Present only when building tags. | -| **CI_BUILD_NAME** | all | 0.5 | The name of the job as defined in `.gitlab-ci.yml` | -| **CI_BUILD_STAGE** | all | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` | -| **CI_BUILD_REF_NAME** | all | all | The branch or tag name for which project is built | -| **CI_BUILD_REF_SLUG** | 8.15 | all | `$CI_BUILD_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. | -| **CI_BUILD_REPO** | all | all | The URL to clone the Git repository | -| **CI_BUILD_TRIGGERED** | all | 0.5 | The flag to indicate that job was [triggered] | -| **CI_BUILD_MANUAL** | 8.12 | all | The flag to indicate that job was manually started | -| **CI_BUILD_TOKEN** | all | 1.2 | Token used for authenticating with the GitLab Container Registry | +| **CI_BUILD_ID** | all | all | The unique id of the current job that GitLab CI uses internally. Deprecated, use CI_JOB_ID | +| **CI_JOB_ID** | 9.0 | all | The unique id of the current job that GitLab CI uses internally | +| **CI_BUILD_REF** | all | all | The commit revision for which project is built. Deprecated, use CI_COMMIT_REF | +| **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built | +| **CI_BUILD_TAG** | all | 0.5 | The commit tag name. Present only when building tags. Deprecated, use CI_COMMIT_TAG | +| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. | +| **CI_BUILD_NAME** | all | 0.5 | The name of the job as defined in `.gitlab-ci.yml`. Deprecated, use CI_JOB_NAME | +| **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` | +| **CI_BUILD_STAGE** | all | 0.5 | The name of the stage as defined in `.gitlab-ci.yml`. Deprecated, use CI_JOB_STAGE | +| **CI_JOB_STAGE** | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` | +| **CI_BUILD_REF_NAME** | all | all | The branch or tag name for which project is built. Deprecated, use CI_COMMIT_REF_NAME | +| **CI_COMMIT_REF_NAME** | 9.0 | all | The branch or tag name for which project is built | +| **CI_BUILD_REF_SLUG** | 8.15 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. Deprecated, use CI_COMMIT_REF_SLUG | +| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. | +| **CI_BUILD_REPO** | all | all | The URL to clone the Git repository. Deprecated, use CI_REPOSITORY | +| **CI_REPOSITORY_URL** | 9.0 | all | The URL to clone the Git repository | +| **CI_BUILD_TRIGGERED** | all | 0.5 | The flag to indicate that job was [triggered]. Deprecated, use CI_PIPELINE_TRIGGERED | +| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] | +| **CI_BUILD_MANUAL** | 8.12 | all | The flag to indicate that job was manually started. Deprecated, use CI_JOB_MANUAL | +| **CI_JOB_MANUAL** | 8.12 | all | The flag to indicate that job was manually started | +| **CI_BUILD_TOKEN** | all | 1.2 | Token used for authenticating with the GitLab Container Registry. Deprecated, use CI_JOB_TOKEN | +| **CI_JOB_TOKEN** | 9.0 | 1.2 | Token used for authenticating with the GitLab Container Registry | | **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally | | **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally | | **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built | @@ -66,21 +77,22 @@ version of Runner required. | **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a job | | **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the job | | **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the job | - +| **CI_REGISTRY_USER** | 9.0 | all | The username to use to push containers to the GitLab Container Registry | +| **CI_REGISTRY_PASSWORD** | 9.0 | all | The password to use to push containers to the GitLab Container Registry | Example values: ```bash -export CI_BUILD_ID="50" -export CI_BUILD_REF="1ecfd275763eff1d6b4844ea3168962458c9f27a" -export CI_BUILD_REF_NAME="master" -export CI_BUILD_REPO="https://gitab-ci-token:abcde-1234ABCD5678ef@example.com/gitlab-org/gitlab-ce.git" -export CI_BUILD_TAG="1.0.0" -export CI_BUILD_NAME="spec:other" -export CI_BUILD_STAGE="test" -export CI_BUILD_MANUAL="true" -export CI_BUILD_TRIGGERED="true" -export CI_BUILD_TOKEN="abcde-1234ABCD5678ef" +export CI_JOB_ID="50" +export CI_COMMIT_SHA="1ecfd275763eff1d6b4844ea3168962458c9f27a" +export CI_COMMIT_REF_NAME="master" +export CI_REPOSITORY="https://gitab-ci-token:abcde-1234ABCD5678ef@example.com/gitlab-org/gitlab-ce.git" +export CI_COMMIT_TAG="1.0.0" +export CI_JOB_NAME="spec:other" +export CI_JOB_STAGE="test" +export CI_JOB_MANUAL="true" +export CI_JOB_TRIGGERED="true" +export CI_JOB_TOKEN="abcde-1234ABCD5678ef" export CI_PIPELINE_ID="1000" export CI_PROJECT_ID="34" export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce" @@ -99,8 +111,30 @@ export CI_SERVER_REVISION="70606bf" export CI_SERVER_VERSION="8.9.0" export GITLAB_USER_ID="42" export GITLAB_USER_EMAIL="user@example.com" +export CI_REGISTRY_USER="gitlab-ci-token" +export CI_REGISTRY_PASSWORD="longalfanumstring" ``` +## 9.0 Renaming + +To follow conventions of naming across GitLab, and to futher move away from the +`build` term and toward `job` CI variables have been renamed for the 9.0 +release. + +| 8.X name | 9.0 name | +|----------|----------| +| CI_BUILD_ID | CI_JOB_ID | +| CI_BUILD_REF | CI_COMMIT_SHA | +| CI_BUILD_TAG | CI_COMMIT_TAG | +| CI_BUILD_REF_NAME | CI_COMMIT_REF_NAME | +| CI_BUILD_REF_SLUG | CI_COMMIT_REF_SLUG | +| CI_BUILD_NAME | CI_JOB_NAME | +| CI_BUILD_STAGE | CI_JOB_STAGE | +| CI_BUILD_REPO | CI_REPOSITORY | +| CI_BUILD_TRIGGERED | CI_PIPELINE_TRIGGERED | +| CI_BUILD_MANUAL | CI_JOB_MANUAL | +| CI_BUILD_TOKEN | CI_JOB_TOKEN | + ## `.gitlab-ci.yaml` defined variables >**Note:** diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index fd1171eff7e..49fa8761e5e 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -70,7 +70,7 @@ There are a few reserved `keywords` that **cannot** be used as job names: | image | no | Use docker image, covered in [Use Docker](../docker/README.md) | | services | no | Use docker services, covered in [Use Docker](../docker/README.md) | | stages | no | Define build stages | -| types | no | Alias for `stages` | +| types | no | Alias for `stages` (deprecated) | | before_script | no | Define commands that run before each job's script | | after_script | no | Define commands that run after each job's script | | variables | no | Define build variables | @@ -130,6 +130,8 @@ There are also two edge cases worth mentioning: ### types +> Deprecated, and will be removed in 10.0. Use [stages](#stages) instead. + Alias for [stages](#stages). ### variables @@ -166,10 +168,11 @@ which can be set in GitLab's UI. cached between jobs. You can only use paths that are within the project workspace. -**By default the caching is enabled per-job and per-branch.** +**By default caching is enabled and shared between pipelines and jobs, +starting from GitLab 9.0** -If `cache` is defined outside the scope of the jobs, it means it is set -globally and all jobs will use its definition. +If `cache` is defined outside the scope of jobs, it means it is set +globally and all jobs will use that definition. Cache all files in `binaries` and `.config`: @@ -202,7 +205,7 @@ rspec: - binaries/ ``` -Locally defined cache overwrites globally defined options. The following `rspec` +Locally defined cache overrides globally defined options. The following `rspec` job will cache only `binaries/`: ```yaml @@ -213,10 +216,15 @@ cache: rspec: script: test cache: + key: rspec paths: - binaries/ ``` +Note that since cache is shared between jobs, if you're using different +paths for different jobs, you should also set a different **cache:key** +otherwise cache content can be overwritten. + The cache is provided on a best-effort basis, so don't expect that the cache will be always present. For implementation details, please check GitLab Runner. @@ -233,6 +241,9 @@ different jobs or even different branches. The `cache:key` variable can use any of the [predefined variables](../variables/README.md). +The default key is **default** across the project, therefore everything is +shared between each pipelines and jobs by default, starting from GitLab 9.0. + --- **Example configurations** diff --git a/doc/integration/README.md b/doc/integration/README.md index 22bdf33443d..e56e58498a6 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -12,6 +12,7 @@ See the documentation below for details on how to configure these services. - [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider - [CAS](cas.md) Configure GitLab to sign in using CAS - [OAuth2 provider](oauth_provider.md) OAuth2 application creation +- [OpenID Connect](openid_connect_provider.md) Use GitLab as an identity provider - [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages - [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users - [Akismet](akismet.md) Configure Akismet to stop spam diff --git a/doc/integration/crowd.md b/doc/integration/crowd.md index f8370cd349e..2bc526dc3db 100644 --- a/doc/integration/crowd.md +++ b/doc/integration/crowd.md @@ -1,63 +1 @@ -# Crowd OmniAuth Provider - -To enable the Crowd OmniAuth provider you must register your application with Crowd. To configure Crowd integration you need an application name and password. - -1. On your GitLab server, open the configuration file. - - For omnibus package: - - ```sh - sudo editor /etc/gitlab/gitlab.rb - ``` - - For installations from source: - - ```sh - cd /home/git/gitlab - - sudo -u git -H editor config/gitlab.yml - ``` - -1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. - -1. Add the provider configuration: - - For omnibus package: - - ```ruby - gitlab_rails['omniauth_providers'] = [ - { - "name" => "crowd", - "args" => { - "crowd_server_url" => "CROWD", - "application_name" => "YOUR_APP_NAME", - "application_password" => "YOUR_APP_PASSWORD" - } - } - ] - ``` - - For installations from source: - - ``` - - { name: 'crowd', - args: { - crowd_server_url: 'CROWD SERVER URL', - application_name: 'YOUR_APP_NAME', - application_password: 'YOUR_APP_PASSWORD' } } - ``` - -1. Change 'YOUR_APP_NAME' to the application name from Crowd applications page. - -1. Change 'YOUR_APP_PASSWORD' to the application password you've set. - -1. Save the configuration file. - -1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you - installed GitLab via Omnibus or from source respectively. - -On the sign in page there should now be a Crowd tab in the sign in form. - -[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure -[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source - +This document was moved to [`administration/auth/crowd`](../administration/auth/crowd.md). diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 47e20d7566a..6c11f46a70a 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -27,7 +27,7 @@ contains some settings that are common for all providers. - [Twitter](twitter.md) - [Shibboleth](shibboleth.md) - [SAML](saml.md) -- [Crowd](crowd.md) +- [Crowd](../administration/auth/crowd.md) - [Azure](azure.md) - [Auth0](auth0.md) - [Authentiq](../administration/auth/authentiq.md) diff --git a/doc/integration/openid_connect_provider.md b/doc/integration/openid_connect_provider.md new file mode 100644 index 00000000000..56f367d841e --- /dev/null +++ b/doc/integration/openid_connect_provider.md @@ -0,0 +1,47 @@ +# GitLab as OpenID Connect identity provider + +This document is about using GitLab as an OpenID Connect identity provider +to sign in to other services. + +## Introduction to OpenID Connect + +[OpenID Connect] \(OIC) is a simple identity layer on top of the +OAuth 2.0 protocol. It allows clients to verify the identity of the end-user +based on the authentication performed by GitLab, as well as to obtain +basic profile information about the end-user in an interoperable and +REST-like manner. OIC performs many of the same tasks as OpenID 2.0, +but does so in a way that is API-friendly, and usable by native and +mobile applications. + +On the client side, you can use [omniauth-openid-connect] for Rails +applications, or any of the other available [client implementations]. + +GitLab's implementation uses the [doorkeeper-openid_connect] gem, refer +to its README for more details about which parts of the specifications +are supported. + +## Enabling OpenID Connect for OAuth applications + +Refer to the [OAuth guide] for basic information on how to set up OAuth +applications in GitLab. To enable OIC for an application, all you have to do +is select the `openid` scope in the application settings. + +Currently the following user information is shared with clients: + +| Claim | Type | Description | +|:-----------------|:----------|:------------| +| `sub` | `string` | An opaque token that uniquely identifies the user +| `auth_time` | `integer` | The timestamp for the user's last authentication +| `name` | `string` | The user's full name +| `nickname` | `string` | The user's GitLab username +| `email` | `string` | The user's public email address +| `email_verified` | `boolean` | Whether the user's public email address was verified +| `website` | `string` | URL for the user's website +| `profile` | `string` | URL for the user's GitLab profile +| `picture` | `string` | URL for the user's GitLab avatar + +[OpenID Connect]: http://openid.net/connect/ "OpenID Connect website" +[doorkeeper-openid_connect]: https://github.com/doorkeeper-gem/doorkeeper-openid_connect "Doorkeeper::OpenidConnect website" +[OAuth guide]: oauth_provider.md "GitLab as OAuth2 authentication service provider" +[omniauth-openid-connect]: https://github.com/jjbohn/omniauth-openid-connect/ "OmniAuth::OpenIDConnect website" +[client implementations]: http://openid.net/developers/libraries#connect "List of available client implementations" diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md index 7b934ecd87a..4cc8be752c4 100644 --- a/doc/update/8.17-to-9.0.md +++ b/doc/update/8.17-to-9.0.md @@ -1,3 +1,66 @@ +#### Configuration changes for repository storages + +This version introduces a new configuration structure for repository storages. +Update your current configuration as follows, replacing with your storages names and paths: + +**For installations from source** + +1. Update your `gitlab.yml`, from + + ```yaml + repositories: + storages: # You must have at least a 'default' storage path. + default: /home/git/repositories + nfs: /mnt/nfs/repositories + cephfs: /mnt/cephfs/repositories + ``` + + to + + ```yaml + repositories: + storages: # You must have at least a 'default' storage path. + default: + path: /home/git/repositories + nfs: + path: /mnt/nfs/repositories + cephfs: + path: /mnt/cephfs/repositories + ``` + +**For Omnibus installations** + +1. Upate your `/etc/gitlab/gitlab.rb`, from + + ```ruby + git_data_dirs({ + "default" => "/var/opt/gitlab/git-data", + "nfs" => "/mnt/nfs/git-data", + "cephfs" => "/mnt/cephfs/git-data" + }) + ``` + + to + + ```ruby + git_data_dirs({ + "default" => { "path" => "/var/opt/gitlab/git-data" }, + "nfs" => { "path" => "/mnt/nfs/git-data" }, + "cephfs" => { "path" => "/mnt/cephfs/git-data" } + }) + ``` + +#### Git configuration + +Configure Git to generate packfile bitmaps (introduced in Git 2.0) on +the GitLab server during `git gc`. + +```sh +cd /home/git/gitlab + +sudo -u git -H git config --global repack.writeBitmaps true +``` + #### Nginx configuration Ensure you're still up-to-date with the latest NGINX configuration changes: @@ -12,7 +75,7 @@ git diff origin/8-17-stable:lib/support/nginx/gitlab-ssl origin/9-0-stable:lib/s git diff origin/8-17-stable:lib/support/nginx/gitlab origin/9-0-stable:lib/support/nginx/gitlab ``` -If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx +If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx configuration as GitLab application no longer handles setting it. If you are using Apache instead of NGINX please see the updated [Apache templates]. diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md index 65e67aa1512..7aa9b46081a 100644 --- a/doc/workflow/shortcuts.md +++ b/doc/workflow/shortcuts.md @@ -42,10 +42,12 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?' | Keyboard Shortcut | Description | | ----------------- | ----------- | | <kbd>g</kbd> + <kbd>p</kbd> | Go to the project's home page | +| <kbd>g</kbd> + <kbd>e</kbd> | Go to the project's activity feed | | <kbd>g</kbd> + <kbd>f</kbd> | Go to files | | <kbd>g</kbd> + <kbd>c</kbd> | Go to commits | | <kbd>g</kbd> + <kbd>b</kbd> | Go to jobs | | <kbd>g</kbd> + <kbd>n</kbd> | Go to network graph | +| <kbd>g</kbd> + <kbd>g</kbd> | Go to repository charts | | <kbd>g</kbd> + <kbd>i</kbd> | Go to issues | | <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests | | <kbd>g</kbd> + <kbd>s</kbd> | Go to snippets | diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index 1dd2bdd9b36..0d6f7350181 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -7,8 +7,9 @@ Feature: Project Active Tab Scenario: On Project Home Given I visit my project's home page - Then the active main tab should be Home - And no other main tabs should be active + Then the active sub tab should be Home + And no other sub tabs should be active + And the active main tab should be Project Scenario: On Project Repository Given I visit my project's files page @@ -34,36 +35,45 @@ Feature: Project Active Tab Scenario: On Project Home/Show Given I visit my project's home page - Then the active main tab should be Home + Then the active sub tab should be Home + And no other sub tabs should be active + And the active main tab should be Project And no other main tabs should be active + Scenario: On Project Home/Activity + Given I visit my project's home page + And I click the "Activity" tab + Then the active sub tab should be Activity + And no other sub tabs should be active + And the active main tab should be Project + # Sub Tabs: Settings Scenario: On Project Settings/Integrations Given I visit my project's settings page And I click the "Integrations" tab - Then the active sub nav should be Integrations - And no other sub navs should be active + Then the active sub tab should be Integrations + And no other sub tabs should be active And the active main tab should be Settings - Scenario: On Project Settings/Deploy Keys + Scenario: On Project Settings/Repository Given I visit my project's settings page - And I click the "Deploy Keys" tab - Then the active sub nav should be Deploy Keys - And no other sub navs should be active + And I click the "Repository" tab + Then the active sub tab should be Repository + And no other sub tabs should be active And the active main tab should be Settings Scenario: On Project Settings/Pages Given I visit my project's settings page And I click the "Pages" tab - Then the active sub nav should be Pages - And no other sub navs should be active + Then the active sub tab should be Pages + And no other sub tabs should be active And the active main tab should be Settings Scenario: On Project Members Given I visit my project's members page - Then the active sub nav should be Members - And no other sub navs should be active + Then the active sub tab should be Members + And no other sub tabs should be active And the active main tab should be Settings # Sub Tabs: Repository @@ -93,6 +103,13 @@ Feature: Project Active Tab And no other sub tabs should be active And the active main tab should be Repository + Scenario: On Project Repository/Charts + Given I visit my project's commits page + And I click the "Charts" tab + Then the active sub tab should be Charts + And no other sub tabs should be active + And the active main tab should be Repository + Scenario: On Project Repository/Branches Given I visit my project's commits page And I click the "Branches" tab diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature index 95de63ba21a..b47fca31ef2 100644 --- a/features/project/shortcuts.feature +++ b/features/project/shortcuts.feature @@ -25,6 +25,12 @@ Feature: Project Shortcuts And the active main tab should be Repository @javascript + Scenario: Navigate to repository charts tab + Given I press "g" and "g" + Then the active sub tab should be Charts + And the active main tab should be Repository + + @javascript Scenario: Navigate to issues tab Given I press "g" and "i" Then the active main tab should be Issues @@ -47,4 +53,11 @@ Feature: Project Shortcuts @javascript Scenario: Navigate to project home Given I press "g" and "p" - Then the active main tab should be Home + Then the active sub tab should be Home + And the active main tab should be Project + + @javascript + Scenario: Navigate to project feed + Given I press "g" and "e" + Then the active sub tab should be Activity + And the active main tab should be Project diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index e842d7bec2b..4befd49ac81 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -22,37 +22,53 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps end step 'I click the "Edit Project"' do - page.within '.layout-nav .controls' do + page.within '.sub-nav' do click_link('Edit Project') end end step 'I click the "Integrations" tab' do - click_link('Integrations') + page.within '.sub-nav' do + click_link('Integrations') + end end - step 'I click the "Deploy Keys" tab' do - click_link('Deploy Keys') + step 'I click the "Repository" tab' do + page.within '.sub-nav' do + click_link('Repository') + end end step 'I click the "Pages" tab' do - click_link('Pages') + page.within '.sub-nav' do + click_link('Pages') + end end - step 'the active sub nav should be Members' do - ensure_active_sub_nav('Members') + step 'I click the "Activity" tab' do + page.within '.sub-nav' do + click_link('Activity') + end end - step 'the active sub nav should be Integrations' do - ensure_active_sub_nav('Integrations') + step 'the active sub tab should be Members' do + ensure_active_sub_tab('Members') end - step 'the active sub nav should be Deploy Keys' do - ensure_active_sub_nav('Deploy Keys') + step 'the active sub tab should be Integrations' do + ensure_active_sub_tab('Integrations') end - step 'the active sub nav should be Pages' do - ensure_active_sub_nav('Pages') + step 'the active sub tab should be Repository' do + ensure_active_sub_tab('Repository') + end + + step 'the active sub tab should be Pages' do + ensure_active_sub_tab('Pages') + end + + step 'the active sub tab should be Activity' do + ensure_active_sub_tab('Activity') end # Sub Tabs: Commits @@ -71,6 +87,12 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps click_link('Tags') end + step 'I click the "Charts" tab' do + page.within '.sub-nav' do + click_link('Charts') + end + end + step 'the active sub tab should be Compare' do ensure_active_sub_tab('Compare') end diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb index edf78f62f9a..580a19494c2 100644 --- a/features/steps/project/deploy_keys.rb +++ b/features/steps/project/deploy_keys.rb @@ -36,7 +36,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps end step 'I should be on deploy keys page' do - expect(current_path).to eq namespace_project_deploy_keys_path(@project.namespace, @project) + expect(current_path).to eq namespace_project_settings_repository_path(@project.namespace, @project) end step 'I should see newly created deploy key' do diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb index dd7a58b454a..1762d5bdf95 100644 --- a/features/steps/project/issues/award_emoji.rb +++ b/features/steps/project/issues/award_emoji.rb @@ -90,7 +90,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps step 'I see search result for "hand"' do page.within '.emoji-menu-content' do - expect(page).to have_selector '[data-emoji="raised_hand"]' + expect(page).to have_selector '[data-name="raised_hand"]' end end diff --git a/features/steps/project/project_shortcuts.rb b/features/steps/project/project_shortcuts.rb index 02c08b784bc..8143b01ca40 100644 --- a/features/steps/project/project_shortcuts.rb +++ b/features/steps/project/project_shortcuts.rb @@ -34,4 +34,9 @@ class Spinach::Features::ProjectShortcuts < Spinach::FeatureSteps find('body').native.send_key('g') find('body').native.send_key('w') end + + step 'I press "g" and "e"' do + find('body').native.send_key('g') + find('body').native.send_key('e') + end end diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index 83446afe424..0cb9229dbae 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -4,7 +4,7 @@ module SharedProjectTab include Spinach::DSL include SharedActiveTab - step 'the active main tab should be Home' do + step 'the active main tab should be Project' do ensure_active_main_tab('Project') end @@ -16,8 +16,8 @@ module SharedProjectTab ensure_active_main_tab('Issues') end - step 'the active main tab should be Members' do - ensure_active_main_tab('Members') + step 'the active sub tab should be Members' do + ensure_active_sub_tab('Members') end step 'the active main tab should be Merge Requests' do @@ -33,7 +33,7 @@ module SharedProjectTab end step 'the active main tab should be Settings' do - expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 0) + ensure_active_main_tab('Settings') end step 'the active sub tab should be Graph' do @@ -47,4 +47,16 @@ module SharedProjectTab step 'the active sub tab should be Commits' do ensure_active_sub_tab('Commits') end + + step 'the active sub tab should be Home' do + ensure_active_sub_tab('Home') + end + + step 'the active sub tab should be Activity' do + ensure_active_sub_tab('Activity') + end + + step 'the active sub tab should be Charts' do + ensure_active_sub_tab('Charts') + end end diff --git a/fixtures/emojis/digests.json b/fixtures/emojis/digests.json index 078d3413f33..3cbc4702dac 100644 --- a/fixtures/emojis/digests.json +++ b/fixtures/emojis/digests.json @@ -1,11622 +1,10748 @@ -[ - { - "name": "100", - "unicode": "1F4AF", +{ + "100": { + "category": "symbols", + "moji": "💯", + "unicodeVersion": "6.0", "digest": "add3bd7d06b6dd445788b277f8c9e5dcf42a54d3ec8b7fb9e7a39695dd95d094" }, - { - "name": "1234", - "unicode": "1F522", + "1234": { + "category": "symbols", + "moji": "🔢", + "unicodeVersion": "6.0", "digest": "c5ac5c8147f5bfd644fad6b470432bba86ffc7bcee04a0e0d277cd1ca485207f" }, - { - "name": "8ball", - "unicode": "1F3B1", + "8ball": { + "category": "activity", + "moji": "🎱", + "unicodeVersion": "6.0", "digest": "a6e6855775b66c505adee65926a264103ebddf2e2d963db7c009b4fec3a24178" }, - { - "name": "a", - "unicode": "1F170", + "a": { + "category": "symbols", + "moji": "🅰", + "unicodeVersion": "6.0", "digest": "bddbb39e8a1d35d42b7c08e7d47f63988cb4d8614b79f74e70b9c67c221896cc" }, - { - "name": "ab", - "unicode": "1F18E", + "ab": { + "category": "symbols", + "moji": "🆎", + "unicodeVersion": "6.0", "digest": "67430fe5fce981160e2ea9052962e49f264322d3abfc2828fbc311b6cdf67ae8" }, - { - "name": "abc", - "unicode": "1F524", + "abc": { + "category": "symbols", + "moji": "🔤", + "unicodeVersion": "6.0", "digest": "282c817ee3414d77a74b815962c33dd9fe71fabaea8c7a9cec466100fbe32187" }, - { - "name": "abcd", - "unicode": "1F521", + "abcd": { + "category": "symbols", + "moji": "🔡", + "unicodeVersion": "6.0", "digest": "686728c759f4683c64762ee4eda0a91bf2041f0ae4f358aacf6c09bf51892eff" }, - { - "name": "accept", - "unicode": "1F251", + "accept": { + "category": "symbols", + "moji": "🉑", + "unicodeVersion": "6.0", "digest": "7208d34c761f10a7fd28f98e25535eba13ff91a64442fc282a98bb77722614f1" }, - { - "name": "aerial_tramway", - "unicode": "1F6A1", + "aerial_tramway": { + "category": "travel", + "moji": "🚡", + "unicodeVersion": "6.0", "digest": "98df666f34370fc34ce280d84bba5a7e617f733fbbfe66caa424b2afa6ab6777" }, - { - "name": "airplane", - "unicode": "2708", + "airplane": { + "category": "travel", + "moji": "✈", + "unicodeVersion": "1.1", "digest": "cc12cf259ef88e57717620cd2bd5aa6a02a8631ee532a3bde24bee78edc5de33" }, - { - "name": "airplane_arriving", - "unicode": "1F6EC", + "airplane_arriving": { + "category": "travel", + "moji": "🛬", + "unicodeVersion": "7.0", "digest": "80d5b4675f91c4cff06d146d795a065b0ce2a74557df4d9e3314e3d3b5c4ae82" }, - { - "name": "airplane_departure", - "unicode": "1F6EB", + "airplane_departure": { + "category": "travel", + "moji": "🛫", + "unicodeVersion": "7.0", "digest": "5544eace06b8e1b6ea91940e893e013d33d6b166e14e6d128a87f2cd2de88332" }, - { - "name": "airplane_small", - "unicode": "1F6E9", + "airplane_small": { + "category": "travel", + "moji": "🛩", + "unicodeVersion": "7.0", "digest": "1a2e07abbbe90d05cee7ff8dd52f443d595ccb38959f3089fe016b77e5d6de7d" }, - { - "name": "small_airplane", - "unicode": "1F6E9", - "digest": "1a2e07abbbe90d05cee7ff8dd52f443d595ccb38959f3089fe016b77e5d6de7d" - }, - { - "name": "alarm_clock", - "unicode": "23F0", + "alarm_clock": { + "category": "objects", + "moji": "⏰", + "unicodeVersion": "6.0", "digest": "fef05a3cd1cddbeca4de8091b94bddb93790b03fa213da86c0eec420f8c49599" }, - { - "name": "alembic", - "unicode": "2697", + "alembic": { + "category": "objects", + "moji": "⚗", + "unicodeVersion": "4.1", "digest": "c94b2a4bf24ccf4db27a22c9725cfe900f4a99ec49ef2411d67952bcb2ca1bfb" }, - { - "name": "alien", - "unicode": "1F47D", + "alien": { + "category": "people", + "moji": "👽", + "unicodeVersion": "6.0", "digest": "856ba98202b244c13a5ee3014a6f7ad592d8c119a30d79e4fc790b74b0e321f7" }, - { - "name": "ambulance", - "unicode": "1F691", + "ambulance": { + "category": "travel", + "moji": "🚑", + "unicodeVersion": "6.0", "digest": "d9b3c1873de496a4554e715342c72290fb69a9c6766d7885f38bfe9491d052da" }, - { - "name": "amphora", - "unicode": "1F3FA", + "amphora": { + "category": "objects", + "moji": "🏺", + "unicodeVersion": "8.0", "digest": "4015f907b649b5e348502cc0e3685ed184e180dca5cc81c43ec516e14df127bf" }, - { - "name": "anchor", - "unicode": "2693", + "anchor": { + "category": "travel", + "moji": "⚓", + "unicodeVersion": "4.1", "digest": "2b29b34ef896ebab70016301e3d1880209bbc3c5a5b8d832e43afff9b17ad792" }, - { - "name": "angel", - "unicode": "1F47C", + "angel": { + "category": "people", + "moji": "👼", + "unicodeVersion": "6.0", "digest": "db75c2460aaf9cd07cb41fe22c8a6079f3667ffe612a71611358720e2b5512a4" }, - { - "name": "angel_tone1", - "unicode": "1F47C-1F3FB", + "angel_tone1": { + "category": "people", + "moji": "👼🏻", + "unicodeVersion": "8.0", "digest": "5871a622469b96296365adaf77d83167759692124c20e5a6e062a525af33472a" }, - { - "name": "angel_tone2", - "unicode": "1F47C-1F3FC", + "angel_tone2": { + "category": "people", + "moji": "👼🏼", + "unicodeVersion": "8.0", "digest": "f5993198a5d9daf39e761c783461f07bca237f4e9b739ac300bb8ca001a69a1a" }, - { - "name": "angel_tone3", - "unicode": "1F47C-1F3FD", + "angel_tone3": { + "category": "people", + "moji": "👼🏽", + "unicodeVersion": "8.0", "digest": "f0c97a7c4354626267d6ab0f388e4297ad255ab9b061f9c68fbcaa0abfc52783" }, - { - "name": "angel_tone4", - "unicode": "1F47C-1F3FE", + "angel_tone4": { + "category": "people", + "moji": "👼🏾", + "unicodeVersion": "8.0", "digest": "6e5dc724c1939d1b0d1a91343662b5bd61ced7709c97802977145ffab6a1f7ac" }, - { - "name": "angel_tone5", - "unicode": "1F47C-1F3FF", + "angel_tone5": { + "category": "people", + "moji": "👼🏿", + "unicodeVersion": "8.0", "digest": "52186e1de350c27d25d6010edf44f64a30338b65912ca178429fbcfbd88113c2" }, - { - "name": "anger", - "unicode": "1F4A2", + "anger": { + "category": "symbols", + "moji": "💢", + "unicodeVersion": "6.0", "digest": "332493913891aa0eda2743b4bb16c4682400f249998bf34eb292246c9009e17f" }, - { - "name": "anger_right", - "unicode": "1F5EF", + "anger_right": { + "category": "symbols", + "moji": "🗯", + "unicodeVersion": "7.0", "digest": "8b049511ef3b1b28325841e2f87c60773eaf2f65cabba58d8b0ec3de9b10c0ae" }, - { - "name": "right_anger_bubble", - "unicode": "1F5EF", - "digest": "8b049511ef3b1b28325841e2f87c60773eaf2f65cabba58d8b0ec3de9b10c0ae" - }, - { - "name": "angry", - "unicode": "1F620", + "angry": { + "category": "people", + "moji": "😠", + "unicodeVersion": "6.0", "digest": "7e09e7e821f511606341fb5ce4011a8ed9809766ab86b7983ffa6ea352b39ec1" }, - { - "name": "anguished", - "unicode": "1F627", - "digest": "a2b6f052996969a17150249d9ef5db742da3d6585bd38ca61eb14c4c13cda54f" - }, - { - "name": "ant", - "unicode": "1F41C", + "ant": { + "category": "nature", + "moji": "🐜", + "unicodeVersion": "6.0", "digest": "929abeaff7ba21ab71cd1ab798af7a6b611e3b3ce1af80cede09a116b223e442" }, - { - "name": "apple", - "unicode": "1F34E", + "apple": { + "category": "food", + "moji": "🍎", + "unicodeVersion": "6.0", "digest": "2a1b85ce57e3d236ae7777dcf332ec37d03bfd7b19806521a353bc532083224d" }, - { - "name": "aquarius", - "unicode": "2652", + "aquarius": { + "category": "symbols", + "moji": "♒", + "unicodeVersion": "1.1", "digest": "fdc42cd41b0dace5eae6baba3143f1e40295d48a29e7103a5bba1d84a056c39d" }, - { - "name": "aries", - "unicode": "2648", + "aries": { + "category": "symbols", + "moji": "♈", + "unicodeVersion": "1.1", "digest": "deb135debcde0a98f40361a84ab64d57c18b5b445cd2f4199e8936f052899737" }, - { - "name": "arrow_backward", - "unicode": "25C0", + "arrow_backward": { + "category": "symbols", + "moji": "◀", + "unicodeVersion": "1.1", "digest": "e162ac82e90d1e925d479fa5c45b9340e0a53287be04e43cbbb2a89c7e7e45e4" }, - { - "name": "arrow_double_down", - "unicode": "23EC", + "arrow_double_down": { + "category": "symbols", + "moji": "⏬", + "unicodeVersion": "6.0", "digest": "03ca890b05338d40972c7a056d672df620a203c6ca52ff3ff530f1a710905507" }, - { - "name": "arrow_double_up", - "unicode": "23EB", + "arrow_double_up": { + "category": "symbols", + "moji": "⏫", + "unicodeVersion": "6.0", "digest": "e753f05bce993d62d5dc79e33c441ced059381b6ce21fa3ea4200f1b3236e59d" }, - { - "name": "arrow_down", - "unicode": "2B07", + "arrow_down": { + "category": "symbols", + "moji": "⬇", + "unicodeVersion": "4.0", "digest": "9bf1bd2ea652ca9321087de58c7a112ea04c35676a6ee0766154183f8b95af6c" }, - { - "name": "arrow_down_small", - "unicode": "1F53D", + "arrow_down_small": { + "category": "symbols", + "moji": "🔽", + "unicodeVersion": "6.0", "digest": "7766198bc60cf59d6cdaeeaa700c2282bfff2f0fdeb22cf4581ca284b87a3bb7" }, - { - "name": "arrow_forward", - "unicode": "25B6", + "arrow_forward": { + "category": "symbols", + "moji": "▶", + "unicodeVersion": "1.1", "digest": "db77d9accd1e02224f5d612f79cd691e6befdf22063475204836be6572510fb7" }, - { - "name": "arrow_heading_down", - "unicode": "2935", + "arrow_heading_down": { + "category": "symbols", + "moji": "⤵", + "unicodeVersion": "3.2", "digest": "f5396069c8f63c13e6c3e0ecd34267c932451309ade9c1171d410563153bf909" }, - { - "name": "arrow_heading_up", - "unicode": "2934", + "arrow_heading_up": { + "category": "symbols", + "moji": "⤴", + "unicodeVersion": "3.2", "digest": "1cad71923fa3df24cf543cae4ce775b0f74936f2edd685fd86a7525c41a14568" }, - { - "name": "arrow_left", - "unicode": "2B05", + "arrow_left": { + "category": "symbols", + "moji": "⬅", + "unicodeVersion": "4.0", "digest": "b629bb3dbe161ef89cfcfced0c7968a68e44a019ad509132987e4973bdc874e7" }, - { - "name": "arrow_lower_left", - "unicode": "2199", + "arrow_lower_left": { + "category": "symbols", + "moji": "↙", + "unicodeVersion": "1.1", "digest": "879136ba0e24e6bf3be70118abcb716d71bd74f7b62347bc052b6533c0ea534d" }, - { - "name": "arrow_lower_right", - "unicode": "2198", + "arrow_lower_right": { + "category": "symbols", + "moji": "↘", + "unicodeVersion": "1.1", "digest": "86d52ac9b961991e3aaa6a9f9b5ace4db6ffd1b5c171c09c23b516473b55066d" }, - { - "name": "arrow_right", - "unicode": "27A1", + "arrow_right": { + "category": "symbols", + "moji": "➡", + "unicodeVersion": "1.1", "digest": "45f26a1cbb0f00ed3609b39da52e9d9e896a77e361c4c8036b1bf8038171bd49" }, - { - "name": "arrow_right_hook", - "unicode": "21AA", + "arrow_right_hook": { + "category": "symbols", + "moji": "↪", + "unicodeVersion": "1.1", "digest": "4f452679c71bcea4fc4a701c55156fef3ddc1ebbc70570bedfc9d3a029637ab1" }, - { - "name": "arrow_up", - "unicode": "2B06", + "arrow_up": { + "category": "symbols", + "moji": "⬆", + "unicodeVersion": "4.0", "digest": "982b988ef6651d8a71867ba7c87f640f62dd0eeb0b7c358f5a5c37e8fe507b8b" }, - { - "name": "arrow_up_down", - "unicode": "2195", + "arrow_up_down": { + "category": "symbols", + "moji": "↕", + "unicodeVersion": "1.1", "digest": "645ed8fb6646f49bfd95af1752336deacdadbe5cba13904023a704288f3b0e2c" }, - { - "name": "arrow_up_small", - "unicode": "1F53C", + "arrow_up_small": { + "category": "symbols", + "moji": "🔼", + "unicodeVersion": "6.0", "digest": "4a8c5789c13a852517e639e7a62c2d331464e6fb0358985aa97c1515e97b5e8b" }, - { - "name": "arrow_upper_left", - "unicode": "2196", + "arrow_upper_left": { + "category": "symbols", + "moji": "↖", + "unicodeVersion": "1.1", "digest": "79026f828d6ceb7c55a9542770962ba6dcd08203995f6ceeb70333a12307d376" }, - { - "name": "arrow_upper_right", - "unicode": "2197", + "arrow_upper_right": { + "category": "symbols", + "moji": "↗", + "unicodeVersion": "1.1", "digest": "7e0f33dfbe65628991c170130d366a3e2cedaf8862ddfcaf3960f395d3da1926" }, - { - "name": "arrows_clockwise", - "unicode": "1F503", + "arrows_clockwise": { + "category": "symbols", + "moji": "🔃", + "unicodeVersion": "6.0", "digest": "88669679977f7157f0acaa9d6a1b77ccf84d25eb78c5bc8afcde38d3635e7144" }, - { - "name": "arrows_counterclockwise", - "unicode": "1F504", + "arrows_counterclockwise": { + "category": "symbols", + "moji": "🔄", + "unicodeVersion": "6.0", "digest": "a2c6a6d3643c128aee3304cd03bb3d7cfe4d35d3ba825bc9c1142d7832b4426e" }, - { - "name": "art", - "unicode": "1F3A8", + "art": { + "category": "activity", + "moji": "🎨", + "unicodeVersion": "6.0", "digest": "b6bc6c4bfb594aadcbb641d006031867678504764bbe0ab84e7b08567a9498da" }, - { - "name": "articulated_lorry", - "unicode": "1F69B", + "articulated_lorry": { + "category": "travel", + "moji": "🚛", + "unicodeVersion": "6.0", "digest": "c115e6613ebd718268aa31d265e017138b9fb58bbb8201eb3f40de2380e460aa" }, - { - "name": "asterisk", - "unicode": "002A-20E3", + "asterisk": { + "category": "symbols", + "moji": "*⃣", + "unicodeVersion": "3.0", "digest": "33d92093f2914448d5a939cf62e8ee3e32931923abdef5f0210e8a8150fa312d" }, - { - "name": "keycap_asterisk", - "unicode": "002A-20E3", - "digest": "33d92093f2914448d5a939cf62e8ee3e32931923abdef5f0210e8a8150fa312d" - }, - { - "name": "astonished", - "unicode": "1F632", + "astonished": { + "category": "people", + "moji": "😲", + "unicodeVersion": "6.0", "digest": "f8531bdda5070d10492709085f4ff652b8be9be6458758940358b9fc594a1f14" }, - { - "name": "athletic_shoe", - "unicode": "1F45F", + "athletic_shoe": { + "category": "people", + "moji": "👟", + "unicodeVersion": "6.0", "digest": "1f90dc390e0dea679085465b7f9e786dfd7dd56a3b219987144ed37ab1e9bf95" }, - { - "name": "atm", - "unicode": "1F3E7", + "atm": { + "category": "symbols", + "moji": "🏧", + "unicodeVersion": "6.0", "digest": "7d3ce6a6afb4951546883404b8e36904179f88f1aa533706cf7bf0bbe0d6fd3c" }, - { - "name": "atom", - "unicode": "269B", - "digest": "6b6bb83b00707a314e46ff8eefbda40978a291ec7881caba1b1ee273f49c1368" - }, - { - "name": "atom_symbol", - "unicode": "269B", + "atom": { + "category": "symbols", + "moji": "⚛", + "unicodeVersion": "4.1", "digest": "6b6bb83b00707a314e46ff8eefbda40978a291ec7881caba1b1ee273f49c1368" }, - { - "name": "avocado", - "unicode": "1F951", + "avocado": { + "category": "food", + "moji": "🥑", + "unicodeVersion": "9.0", "digest": "bc1fb203d63b18985598400925de24050bb192afda1cbf0813f85cb139869eff" }, - { - "name": "b", - "unicode": "1F171", + "b": { + "category": "symbols", + "moji": "🅱", + "unicodeVersion": "6.0", "digest": "722f9db9442e7c0fc0d0ac0f5291fbf47c6a0ac4d8abd42e97957da705fb82bf" }, - { - "name": "baby", - "unicode": "1F476", + "baby": { + "category": "people", + "moji": "👶", + "unicodeVersion": "6.0", "digest": "219ae5a571aaf90c060956cd1c56dcc27708c827cecdca3ba1122058a3c4847b" }, - { - "name": "baby_bottle", - "unicode": "1F37C", + "baby_bottle": { + "category": "food", + "moji": "🍼", + "unicodeVersion": "6.0", "digest": "4fb71689e9d634e8d1699cf454a71e43f2b5b1a5dbab0bf186626934fdf5b782" }, - { - "name": "baby_chick", - "unicode": "1F424", + "baby_chick": { + "category": "nature", + "moji": "🐤", + "unicodeVersion": "6.0", "digest": "14119874e9b5548028dfb9cc593a541efc1d075ac839a565b92e0c3253cffe7e" }, - { - "name": "baby_symbol", - "unicode": "1F6BC", + "baby_symbol": { + "category": "symbols", + "moji": "🚼", + "unicodeVersion": "6.0", "digest": "fb4db66868cda45ea3879ffc2ff4f763c56d2d889ae0ab17fe171129ede02f98" }, - { - "name": "baby_tone1", - "unicode": "1F476-1F3FB", + "baby_tone1": { + "category": "people", + "moji": "👶🏻", + "unicodeVersion": "8.0", "digest": "cd3faf223a298c34e05d469d9d0db08438d97df7fd82c0973f8a9e07d553f5b1" }, - { - "name": "baby_tone2", - "unicode": "1F476-1F3FC", + "baby_tone2": { + "category": "people", + "moji": "👶🏼", + "unicodeVersion": "8.0", "digest": "5b4539e22e0dd726c27eb8af2357f9240a52aed3f710f3234571cff029cc6198" }, - { - "name": "baby_tone3", - "unicode": "1F476-1F3FD", + "baby_tone3": { + "category": "people", + "moji": "👶🏽", + "unicodeVersion": "8.0", "digest": "720e740e1ac63c6372269132b1fb6e07a6b91f5c808cc3adef59f0b4500e5e72" }, - { - "name": "baby_tone4", - "unicode": "1F476-1F3FE", + "baby_tone4": { + "category": "people", + "moji": "👶🏾", + "unicodeVersion": "8.0", "digest": "5e43b69c509bd526ad6f081764578c30b6f3285fb7442222e05ccf62e53bfb64" }, - { - "name": "baby_tone5", - "unicode": "1F476-1F3FF", + "baby_tone5": { + "category": "people", + "moji": "👶🏿", + "unicodeVersion": "8.0", "digest": "85bba6e0940ccfb99999fe124e815f9dd340d00a5568e13967b02245a62dbf54" }, - { - "name": "back", - "unicode": "1F519", + "back": { + "category": "symbols", + "moji": "🔙", + "unicodeVersion": "6.0", "digest": "083e4e48b51092c28efb4532e840e1091b5d4b685c6e0f221aa0228f061cd91e" }, - { - "name": "bacon", - "unicode": "1F953", + "bacon": { + "category": "food", + "moji": "🥓", + "unicodeVersion": "9.0", "digest": "18ad3817f1f88a69706db5727a58e763dde6c21a2d4f184c3d728c32dc5fa05a" }, - { - "name": "badminton", - "unicode": "1F3F8", + "badminton": { + "category": "activity", + "moji": "🏸", + "unicodeVersion": "8.0", "digest": "353eb7ee93decd9fe0072e4d78a5618d5e2d9e77a6e4de9fe171870d75e02a66" }, - { - "name": "baggage_claim", - "unicode": "1F6C4", + "baggage_claim": { + "category": "symbols", + "moji": "🛄", + "unicodeVersion": "6.0", "digest": "7d6bceca92c266da6d2b91dfcf244546fc11022e039e7da8e6888c1696bb2186" }, - { - "name": "balloon", - "unicode": "1F388", + "balloon": { + "category": "objects", + "moji": "🎈", + "unicodeVersion": "6.0", "digest": "65760aedc1503b426927cff78c24449d563843a274961d962718fa9638375d54" }, - { - "name": "ballot_box", - "unicode": "1F5F3", + "ballot_box": { + "category": "objects", + "moji": "🗳", + "unicodeVersion": "7.0", "digest": "4175a56eca5c6458574a681e109b1403fbb143cf27f69ae6c1917650f3e08892" }, - { - "name": "ballot_box_with_ballot", - "unicode": "1F5F3", - "digest": "4175a56eca5c6458574a681e109b1403fbb143cf27f69ae6c1917650f3e08892" - }, - { - "name": "ballot_box_with_check", - "unicode": "2611", + "ballot_box_with_check": { + "category": "symbols", + "moji": "☑", + "unicodeVersion": "1.1", "digest": "c98d6f3588dd87e2f318bbfe6c646399a905450edfd814edae4e5b1bddef2134" }, - { - "name": "bamboo", - "unicode": "1F38D", + "bamboo": { + "category": "nature", + "moji": "🎍", + "unicodeVersion": "6.0", "digest": "e4ee65088df43d7081b1ce6fd996f66f3e0accd88840855c47a98a22997823dd" }, - { - "name": "banana", - "unicode": "1F34C", + "banana": { + "category": "food", + "moji": "🍌", + "unicodeVersion": "6.0", "digest": "f9e8ff910c282c20a8907ff64926b5de4ee250529a1ed718fb33302e6fff8dd9" }, - { - "name": "bangbang", - "unicode": "203C", + "bangbang": { + "category": "symbols", + "moji": "‼", + "unicodeVersion": "1.1", "digest": "76536fee63fe964a3f3839d309b1f45028fb0c43f4d1eeee495f17e1532b4def" }, - { - "name": "bank", - "unicode": "1F3E6", + "bank": { + "category": "travel", + "moji": "🏦", + "unicodeVersion": "6.0", "digest": "f5d2976bf6d521638ccacc74be06bd4abfeab06c5d898a9d245edad45a5b6306" }, - { - "name": "bar_chart", - "unicode": "1F4CA", + "bar_chart": { + "category": "objects", + "moji": "📊", + "unicodeVersion": "6.0", "digest": "65a328a1b2d7a5332dd4d93f4dbca13d976f0a505b00835c3fc458e394804240" }, - { - "name": "barber", - "unicode": "1F488", + "barber": { + "category": "objects", + "moji": "💈", + "unicodeVersion": "6.0", "digest": "5e8053d3bb3765a8632fd1cbfe21163f74ed79f6be377eb9603eaaf883d8dc46" }, - { - "name": "baseball", - "unicode": "26BE", + "baseball": { + "category": "activity", + "moji": "⚾", + "unicodeVersion": "5.2", "digest": "46ac16f8b5455b942f6dbff9483a6fd277721e6719d2731573baabd21c44b34f" }, - { - "name": "basketball", - "unicode": "1F3C0", + "basketball": { + "category": "activity", + "moji": "🏀", + "unicodeVersion": "6.0", "digest": "cc83e2aea8fcd2e9a5789e1932ee3766c40843c142fd3565c4e77dafb21ec7d7" }, - { - "name": "basketball_player", - "unicode": "26F9", - "digest": "793ba53c95e8def769383b612037bc9b9bceecaf1e0430c50a4cc128ad18d9b9" - }, - { - "name": "person_with_ball", - "unicode": "26F9", + "basketball_player": { + "category": "activity", + "moji": "⛹", + "unicodeVersion": "5.2", "digest": "793ba53c95e8def769383b612037bc9b9bceecaf1e0430c50a4cc128ad18d9b9" }, - { - "name": "basketball_player_tone1", - "unicode": "26F9-1F3FB", + "basketball_player_tone1": { + "category": "activity", + "moji": "⛹🏻", + "unicodeVersion": "8.0", "digest": "2a06522b971e68ee5b8777a58253009b548f4da2fb723c638acb3d7b04edba8f" }, - { - "name": "person_with_ball_tone1", - "unicode": "26F9-1F3FB", - "digest": "2a06522b971e68ee5b8777a58253009b548f4da2fb723c638acb3d7b04edba8f" - }, - { - "name": "basketball_player_tone2", - "unicode": "26F9-1F3FC", - "digest": "ecc0e44ab9bc478ba45a055fd69a3a38377b917aac5047963fe80ff8ae5fd8e3" - }, - { - "name": "person_with_ball_tone2", - "unicode": "26F9-1F3FC", + "basketball_player_tone2": { + "category": "activity", + "moji": "⛹🏼", + "unicodeVersion": "8.0", "digest": "ecc0e44ab9bc478ba45a055fd69a3a38377b917aac5047963fe80ff8ae5fd8e3" }, - { - "name": "basketball_player_tone3", - "unicode": "26F9-1F3FD", + "basketball_player_tone3": { + "category": "activity", + "moji": "⛹🏽", + "unicodeVersion": "8.0", "digest": "2d38f1851c685d29532c042461d7b5b996e5f04f0ed54857c66073c62a99ceac" }, - { - "name": "person_with_ball_tone3", - "unicode": "26F9-1F3FD", - "digest": "2d38f1851c685d29532c042461d7b5b996e5f04f0ed54857c66073c62a99ceac" - }, - { - "name": "basketball_player_tone4", - "unicode": "26F9-1F3FE", + "basketball_player_tone4": { + "category": "activity", + "moji": "⛹🏾", + "unicodeVersion": "8.0", "digest": "09e957c6e9ffc196415f28073aa261feba8efba0bdc694dc08f8f7cd1f88f720" }, - { - "name": "person_with_ball_tone4", - "unicode": "26F9-1F3FE", - "digest": "09e957c6e9ffc196415f28073aa261feba8efba0bdc694dc08f8f7cd1f88f720" - }, - { - "name": "basketball_player_tone5", - "unicode": "26F9-1F3FF", + "basketball_player_tone5": { + "category": "activity", + "moji": "⛹🏿", + "unicodeVersion": "8.0", "digest": "c631cefc5d2a0a31bdb9f0a0d97ea68b1c6928e565468998403034644572a0b0" }, - { - "name": "person_with_ball_tone5", - "unicode": "26F9-1F3FF", - "digest": "c631cefc5d2a0a31bdb9f0a0d97ea68b1c6928e565468998403034644572a0b0" - }, - { - "name": "bat", - "unicode": "1F987", + "bat": { + "category": "nature", + "moji": "🦇", + "unicodeVersion": "9.0", "digest": "8fc19e0d7d6f80906bdbc06d616a810de66180d96cf28070a53fa61b88904535" }, - { - "name": "bath", - "unicode": "1F6C0", + "bath": { + "category": "activity", + "moji": "🛀", + "unicodeVersion": "6.0", "digest": "33b371832f90aad50baf5296f3ad4cc081c319b279f989c74409903d8568e917" }, - { - "name": "bath_tone1", - "unicode": "1F6C0-1F3FB", + "bath_tone1": { + "category": "activity", + "moji": "🛀🏻", + "unicodeVersion": "8.0", "digest": "7ae2989e47788ba71359d52da68feec95aaff68a77d5a6556957df1617af8536" }, - { - "name": "bath_tone2", - "unicode": "1F6C0-1F3FC", + "bath_tone2": { + "category": "activity", + "moji": "🛀🏼", + "unicodeVersion": "8.0", "digest": "2e86f8edad54d15a7094cd52160cbe51d10aa1750cfb0b3b58e93533f070e327" }, - { - "name": "bath_tone3", - "unicode": "1F6C0-1F3FD", + "bath_tone3": { + "category": "activity", + "moji": "🛀🏽", + "unicodeVersion": "8.0", "digest": "654c0cd083a67ff330a38d07352876d265390e5399e5352598d64a6c7e5eeba7" }, - { - "name": "bath_tone4", - "unicode": "1F6C0-1F3FE", + "bath_tone4": { + "category": "activity", + "moji": "🛀🏾", + "unicodeVersion": "8.0", "digest": "adad88c6830f31c4b5be194d1987d6aadf4adf45e4cb7f2e4657f0d20c0d663a" }, - { - "name": "bath_tone5", - "unicode": "1F6C0-1F3FF", + "bath_tone5": { + "category": "activity", + "moji": "🛀🏿", + "unicodeVersion": "8.0", "digest": "952c4c9bf24e001e23a33ebf97bd92969cd9143e28ce93f9aafc708a8f966903" }, - { - "name": "bathtub", - "unicode": "1F6C1", + "bathtub": { + "category": "objects", + "moji": "🛁", + "unicodeVersion": "6.0", "digest": "844dffb87ef872594195069b0d0df27c3fe51f3967ccbc8b2df811a086dd483a" }, - { - "name": "battery", - "unicode": "1F50B", + "battery": { + "category": "objects", + "moji": "🔋", + "unicodeVersion": "6.0", "digest": "949ae06648667fb13d9121a6dfdd03bf8692794b28c36e9a8e8ac4515664449a" }, - { - "name": "beach", - "unicode": "1F3D6", - "digest": "37fa2158977d470186caaa1aa06669b6dc5026ba49a0c44c5255541f8e974e26" - }, - { - "name": "beach_with_umbrella", - "unicode": "1F3D6", + "beach": { + "category": "travel", + "moji": "🏖", + "unicodeVersion": "7.0", "digest": "37fa2158977d470186caaa1aa06669b6dc5026ba49a0c44c5255541f8e974e26" }, - { - "name": "beach_umbrella", - "unicode": "26F1", + "beach_umbrella": { + "category": "objects", + "moji": "⛱", + "unicodeVersion": "5.2", "digest": "d045f1de10038b9fb1eaa2529b2f80b7e3be1cff503efcc2d680663d1fbbc18f" }, - { - "name": "umbrella_on_ground", - "unicode": "26F1", - "digest": "d045f1de10038b9fb1eaa2529b2f80b7e3be1cff503efcc2d680663d1fbbc18f" - }, - { - "name": "bear", - "unicode": "1F43B", + "bear": { + "category": "nature", + "moji": "🐻", + "unicodeVersion": "6.0", "digest": "a4b9066eaa5681e6af06e596a96a5217037460ffc3b013e8db4d34d762413246" }, - { - "name": "bed", - "unicode": "1F6CF", + "bed": { + "category": "objects", + "moji": "🛏", + "unicodeVersion": "7.0", "digest": "08f6e20db51b1fb650b390a0a3074938646772f3fcee8c295d47742e44fe1e30" }, - { - "name": "bee", - "unicode": "1F41D", + "bee": { + "category": "nature", + "moji": "🐝", + "unicodeVersion": "6.0", "digest": "5beb9a1650681b4adf69999d4808231c38f41a3ec693480b807cda86f964c570" }, - { - "name": "beer", - "unicode": "1F37A", + "beer": { + "category": "food", + "moji": "🍺", + "unicodeVersion": "6.0", "digest": "69e227104976548ee0f37375fe1526fd65ef0a328d2d92db2feb1edfd7032bd4" }, - { - "name": "beers", - "unicode": "1F37B", + "beers": { + "category": "food", + "moji": "🍻", + "unicodeVersion": "6.0", "digest": "db8b32d93bf6d161a3b027e55651d8f51231b13928b3610987ef62bb634d7501" }, - { - "name": "beetle", - "unicode": "1F41E", + "beetle": { + "category": "nature", + "moji": "🐞", + "unicodeVersion": "6.0", "digest": "5aaa428e3f63f7cd1696839ab05be03fa0cd0cbed30a05c36cb270da330c3849" }, - { - "name": "beginner", - "unicode": "1F530", + "beginner": { + "category": "symbols", + "moji": "🔰", + "unicodeVersion": "6.0", "digest": "2de4fdf92f182c42b12b7527034eaf767d996848b61f31ee69167728411ca0b1" }, - { - "name": "bell", - "unicode": "1F514", + "bell": { + "category": "symbols", + "moji": "🔔", + "unicodeVersion": "6.0", "digest": "18d419417746ead408072b78fe2edb6314cdb49492873966fa9f9f06be09899b" }, - { - "name": "bellhop", - "unicode": "1F6CE", - "digest": "b8187bc4059f6a0924a47fe3f6c07f656bed0334bbcbfa1e89f800fe6594ff08" - }, - { - "name": "bellhop_bell", - "unicode": "1F6CE", + "bellhop": { + "category": "objects", + "moji": "🛎", + "unicodeVersion": "7.0", "digest": "b8187bc4059f6a0924a47fe3f6c07f656bed0334bbcbfa1e89f800fe6594ff08" }, - { - "name": "bento", - "unicode": "1F371", + "bento": { + "category": "food", + "moji": "🍱", + "unicodeVersion": "6.0", "digest": "d46d4f681c5da7f7678b51be3445454a8ed18d917e132ae79077f05310e485f1" }, - { - "name": "bicyclist", - "unicode": "1F6B4", + "bicyclist": { + "category": "activity", + "moji": "🚴", + "unicodeVersion": "6.0", "digest": "3302147b6b47c16adb97d78b7b761a1ca80e6d0b41d0b60f4da338d2f55f968b" }, - { - "name": "bicyclist_tone1", - "unicode": "1F6B4-1F3FB", + "bicyclist_tone1": { + "category": "activity", + "moji": "🚴🏻", + "unicodeVersion": "8.0", "digest": "27eaae0eb61f5e7b3cd9faf02c042d6643a368051a7c9d7da4e0fb9802d39242" }, - { - "name": "bicyclist_tone2", - "unicode": "1F6B4-1F3FC", + "bicyclist_tone2": { + "category": "activity", + "moji": "🚴🏼", + "unicodeVersion": "8.0", "digest": "39ee9e1071700da7079ad0146bf5711c3a222991eeca8b29b72a65677604444d" }, - { - "name": "bicyclist_tone3", - "unicode": "1F6B4-1F3FD", + "bicyclist_tone3": { + "category": "activity", + "moji": "🚴🏽", + "unicodeVersion": "8.0", "digest": "03e1d2c4232c896147a9d4bf43becd61edbb5c84fc7193ecea474c0f9fb36817" }, - { - "name": "bicyclist_tone4", - "unicode": "1F6B4-1F3FE", + "bicyclist_tone4": { + "category": "activity", + "moji": "🚴🏾", + "unicodeVersion": "8.0", "digest": "61393d9c4805be0379d86dd5bec9a1b02314433ab36cfd85bb48dfd073746617" }, - { - "name": "bicyclist_tone5", - "unicode": "1F6B4-1F3FF", + "bicyclist_tone5": { + "category": "activity", + "moji": "🚴🏿", + "unicodeVersion": "8.0", "digest": "2b46d5f8303e5710dbf5db3a4edc9d88a032fe123fe79158024c9f51df5458c6" }, - { - "name": "bike", - "unicode": "1F6B2", + "bike": { + "category": "travel", + "moji": "🚲", + "unicodeVersion": "6.0", "digest": "b41daa7c549d483e2336186a28baaa8ecb11986f490c0c54c793c44900c8f652" }, - { - "name": "bikini", - "unicode": "1F459", + "bikini": { + "category": "people", + "moji": "👙", + "unicodeVersion": "6.0", "digest": "07fe156f64673818d69ce3bf03950ca59e3b5d346e45ca541da4078ab791f5ae" }, - { - "name": "biohazard", - "unicode": "2623", - "digest": "96163e31f0b8dc5a59772133ede9cc2f40f94330d0b15e3d044b28747e2be788" - }, - { - "name": "biohazard_sign", - "unicode": "2623", + "biohazard": { + "category": "symbols", + "moji": "☣", + "unicodeVersion": "1.1", "digest": "96163e31f0b8dc5a59772133ede9cc2f40f94330d0b15e3d044b28747e2be788" }, - { - "name": "bird", - "unicode": "1F426", + "bird": { + "category": "nature", + "moji": "🐦", + "unicodeVersion": "6.0", "digest": "f916eaf8f271b3767ade9eabb69594c0479f45472d471cabaf59f6e965c161e0" }, - { - "name": "birthday", - "unicode": "1F382", + "birthday": { + "category": "food", + "moji": "🎂", + "unicodeVersion": "6.0", "digest": "89e7c4c598ebee8ec8ab11ebe4ccc6defb7c4d2987ee2379a19b3b59827dd98a" }, - { - "name": "black_circle", - "unicode": "26AB", + "black_circle": { + "category": "symbols", + "moji": "⚫", + "unicodeVersion": "4.1", "digest": "c2ba672994ad0f99d7fdc449f3fee45a2dca68a58f9fe95825b38465a30ef44e" }, - { - "name": "black_heart", - "unicode": "1F5A4", + "black_heart": { + "category": "symbols", + "moji": "🖤", + "unicodeVersion": "9.0", "digest": "f334679168d6dd7328c28e9ae3cb2b1fca0e9c2777938d586bfe623db2a688b9" }, - { - "name": "black_joker", - "unicode": "1F0CF", + "black_joker": { + "category": "symbols", + "moji": "🃏", + "unicodeVersion": "6.0", "digest": "d004b25f186494d5b2c65204caa9daecd749c840a0bea5718735e18109e5394d" }, - { - "name": "black_large_square", - "unicode": "2B1B", + "black_large_square": { + "category": "symbols", + "moji": "⬛", + "unicodeVersion": "5.1", "digest": "cbd90dcbc2f674eafa53820548b5263c18c9845ab39937f085e85aca0aebb479" }, - { - "name": "black_medium_small_square", - "unicode": "25FE", + "black_medium_small_square": { + "category": "symbols", + "moji": "◾", + "unicodeVersion": "3.2", "digest": "ab38363c2e862b8f67c719397a09a18e1ef996eec190691fdf769f5cfb209660" }, - { - "name": "black_medium_square", - "unicode": "25FC", + "black_medium_square": { + "category": "symbols", + "moji": "◼", + "unicodeVersion": "3.2", "digest": "c9ffa87c37e8ee65fadcf755176949901aec7367e02abb85e63cad60cd922116" }, - { - "name": "black_nib", - "unicode": "2712", + "black_nib": { + "category": "objects", + "moji": "✒", + "unicodeVersion": "1.1", "digest": "58fb23b1155102970eaa23765e7d529a21e8e545e076ec1158bf11b4de5f51a8" }, - { - "name": "black_small_square", - "unicode": "25AA", + "black_small_square": { + "category": "symbols", + "moji": "▪", + "unicodeVersion": "1.1", "digest": "f69be6de578fffce5a3e60eda690104b2ef6a855c630040104fb760a02ff1aef" }, - { - "name": "black_square_button", - "unicode": "1F532", + "black_square_button": { + "category": "symbols", + "moji": "🔲", + "unicodeVersion": "6.0", "digest": "9d818fcd08ed38cd0bbbcfd83e665aa29b3761c0d8b9806d8954d36785e267a8" }, - { - "name": "blossom", - "unicode": "1F33C", + "blossom": { + "category": "nature", + "moji": "🌼", + "unicodeVersion": "6.0", "digest": "e8cf369d4e4cdb4eccc2ebcbb35439b0344221115701daae642e58dff8544922" }, - { - "name": "blowfish", - "unicode": "1F421", + "blowfish": { + "category": "nature", + "moji": "🐡", + "unicodeVersion": "6.0", "digest": "e706849ed00f08a82312381c76f6f9ba6cc261fbf87a839c85e7dd54138f9dc3" }, - { - "name": "blue_book", - "unicode": "1F4D8", + "blue_book": { + "category": "objects", + "moji": "📘", + "unicodeVersion": "6.0", "digest": "4c845748fe890516b32981b0b62bf3e8e9d906840c2060179f4f844100780615" }, - { - "name": "blue_car", - "unicode": "1F699", + "blue_car": { + "category": "travel", + "moji": "🚙", + "unicodeVersion": "6.0", "digest": "eca91934eb5481726cfd897b1ed5eac306e14d02499fbe49316aaec6c72b6707" }, - { - "name": "blue_heart", - "unicode": "1F499", + "blue_heart": { + "category": "symbols", + "moji": "💙", + "unicodeVersion": "6.0", "digest": "2caa0c8d18538cc871c6fe328a52f71e1df8aabf4d1cc2f5324b261d1b8cb99a" }, - { - "name": "blush", - "unicode": "1F60A", + "blush": { + "category": "people", + "moji": "😊", + "unicodeVersion": "6.0", "digest": "3bfe8d603cfa39999c164779f666d39bbc507f124ba80233ee72da7b3b0c0457" }, - { - "name": "boar", - "unicode": "1F417", + "boar": { + "category": "nature", + "moji": "🐗", + "unicodeVersion": "6.0", "digest": "c9d67479cace427ac3c30460fcffa1bf9a8e5262c0390962405dbbe6bf830fa6" }, - { - "name": "bomb", - "unicode": "1F4A3", + "bomb": { + "category": "objects", + "moji": "💣", + "unicodeVersion": "6.0", "digest": "0155559abc4084f80e9b0b2a2091b8710ddd6369993b7fdd0685f4f8c2fd7e6c" }, - { - "name": "book", - "unicode": "1F4D6", + "book": { + "category": "objects", + "moji": "📖", + "unicodeVersion": "6.0", "digest": "9d912a9d1bb10dc7f2645b345ed09e90461e83df0de275acb806f1f75cef1fcf" }, - { - "name": "bookmark", - "unicode": "1F516", + "bookmark": { + "category": "objects", + "moji": "🔖", + "unicodeVersion": "6.0", "digest": "5705e3108259d6900649157843c50e22d0086c3630b291d3f942da1a736e3e3d" }, - { - "name": "bookmark_tabs", - "unicode": "1F4D1", + "bookmark_tabs": { + "category": "objects", + "moji": "📑", + "unicodeVersion": "6.0", "digest": "c8fc7c9f3f82e1ccc97fc591345fdd88b09eec0fca428d8d4632a121cf1bc39a" }, - { - "name": "books", - "unicode": "1F4DA", + "books": { + "category": "objects", + "moji": "📚", + "unicodeVersion": "6.0", "digest": "cbcf55d39dd05d26ef7350bc51e0e2f064f78bb8f59d407b516d63f68558f8e4" }, - { - "name": "boom", - "unicode": "1F4A5", + "boom": { + "category": "nature", + "moji": "💥", + "unicodeVersion": "6.0", "digest": "f5400e9583f7f997cd2385f21379f6229424a9b221445bc8f36c0bb64bdb3168" }, - { - "name": "boot", - "unicode": "1F462", + "boot": { + "category": "people", + "moji": "👢", + "unicodeVersion": "6.0", "digest": "b4706ff35909a6fb759a3b8a797e90cb67ffc60e4853386a7d89ace9693a9364" }, - { - "name": "bouquet", - "unicode": "1F490", + "bouquet": { + "category": "nature", + "moji": "💐", + "unicodeVersion": "6.0", "digest": "b93751a27b40f6185a22b3e8b413f0fe09b6010d1057c672e1a23088e0b8286f" }, - { - "name": "bow", - "unicode": "1F647", + "bow": { + "category": "people", + "moji": "🙇", + "unicodeVersion": "6.0", "digest": "33cd6da4d408f18d98bebc6a277dea8b914150e32ee472586ce3f1eb814462bd" }, - { - "name": "bow_and_arrow", - "unicode": "1F3F9", - "digest": "051b4d50ab21a68b8583a6313ec183e3e1e96f493b0f4541fbb888f0b95fdd4d" - }, - { - "name": "archery", - "unicode": "1F3F9", + "bow_and_arrow": { + "category": "activity", + "moji": "🏹", + "unicodeVersion": "8.0", "digest": "051b4d50ab21a68b8583a6313ec183e3e1e96f493b0f4541fbb888f0b95fdd4d" }, - { - "name": "bow_tone1", - "unicode": "1F647-1F3FB", + "bow_tone1": { + "category": "people", + "moji": "🙇🏻", + "unicodeVersion": "8.0", "digest": "995c8400ad60d5adc66c9ae5e3c0ecf56c48b478ad79418d45b6289933d25bdd" }, - { - "name": "bow_tone2", - "unicode": "1F647-1F3FC", + "bow_tone2": { + "category": "people", + "moji": "🙇🏼", + "unicodeVersion": "8.0", "digest": "af89eec2fccda99d9bdd373b2345595882fee1c0a15d29af9028089e20255325" }, - { - "name": "bow_tone3", - "unicode": "1F647-1F3FD", + "bow_tone3": { + "category": "people", + "moji": "🙇🏽", + "unicodeVersion": "8.0", "digest": "015d8122abdf2d0caa03815545f50fb7a71e05dacd46aaa133cc9ace5192f266" }, - { - "name": "bow_tone4", - "unicode": "1F647-1F3FE", + "bow_tone4": { + "category": "people", + "moji": "🙇🏾", + "unicodeVersion": "8.0", "digest": "e8409096a795b775def654d36aeccb8eb91e83d7d1b32145cd73fd0b7b9e885c" }, - { - "name": "bow_tone5", - "unicode": "1F647-1F3FF", + "bow_tone5": { + "category": "people", + "moji": "🙇🏿", + "unicodeVersion": "8.0", "digest": "d87042cde8dbad9fb1a91a2ec60116e27b4a76388b5779d771a0bbae12a2814d" }, - { - "name": "bowling", - "unicode": "1F3B3", + "bowling": { + "category": "activity", + "moji": "🎳", + "unicodeVersion": "6.0", "digest": "737f2cdfa4ac964baade585a39771b18080bd5e9b55c8661d3518f468f344662" }, - { - "name": "boxing_glove", - "unicode": "1F94A", + "boxing_glove": { + "category": "activity", + "moji": "🥊", + "unicodeVersion": "9.0", "digest": "c914b2ce45f20afad66ad6f0d1b0750c4469e4f48b686dfc4aad1ec8d289c563" }, - { - "name": "boxing_gloves", - "unicode": "1F94A", - "digest": "c914b2ce45f20afad66ad6f0d1b0750c4469e4f48b686dfc4aad1ec8d289c563" - }, - { - "name": "boy", - "unicode": "1F466", + "boy": { + "category": "people", + "moji": "👦", + "unicodeVersion": "6.0", "digest": "7bc0173d8c88f3f12d41f213f7a3a9f5ebf65efad610fd5a2a31935128a6a6c1" }, - { - "name": "boy_tone1", - "unicode": "1F466-1F3FB", + "boy_tone1": { + "category": "people", + "moji": "👦🏻", + "unicodeVersion": "8.0", "digest": "c0e2f0483715b239fe145b0056566f7a3a722319d9a87c1e66733dff1916a19f" }, - { - "name": "boy_tone2", - "unicode": "1F466-1F3FC", + "boy_tone2": { + "category": "people", + "moji": "👦🏼", + "unicodeVersion": "8.0", "digest": "0001d0bd1ff4dbd898604ba965b4039d09667d955bc0349301b992f9ab6dd7fd" }, - { - "name": "boy_tone3", - "unicode": "1F466-1F3FD", + "boy_tone3": { + "category": "people", + "moji": "👦🏽", + "unicodeVersion": "8.0", "digest": "e0f08755955fd2e0bd1c5d5e84429b2a234b24a744bb50bb9f1148495b2b29f9" }, - { - "name": "boy_tone4", - "unicode": "1F466-1F3FE", + "boy_tone4": { + "category": "people", + "moji": "👦🏾", + "unicodeVersion": "8.0", "digest": "04b6bfee58a26b1ce2e5b403504a7033aaf395f03f5cd23e824f32c90c395fe6" }, - { - "name": "boy_tone5", - "unicode": "1F466-1F3FF", + "boy_tone5": { + "category": "people", + "moji": "👦🏿", + "unicodeVersion": "8.0", "digest": "0f76e97237203950da36c737dcc6f56dcd6c123401a8c817a0636376c7f38ef5" }, - { - "name": "bread", - "unicode": "1F35E", + "bread": { + "category": "food", + "moji": "🍞", + "unicodeVersion": "6.0", "digest": "81739830f16f33e6a1dd7cc17c25df207846062bb5167bb8abed7fdd49268b86" }, - { - "name": "bride_with_veil", - "unicode": "1F470", + "bride_with_veil": { + "category": "people", + "moji": "👰", + "unicodeVersion": "6.0", "digest": "8e24bd91c3f564cf6148f2b3b4a7d692c11dd059e76a13331fdfb04ae060ea70" }, - { - "name": "bride_with_veil_tone1", - "unicode": "1F470-1F3FB", + "bride_with_veil_tone1": { + "category": "people", + "moji": "👰🏻", + "unicodeVersion": "8.0", "digest": "0bd2f16f72586f50e768b14b9b353f2e98ccbb2581a568c33b06be56e70ca063" }, - { - "name": "bride_with_veil_tone2", - "unicode": "1F470-1F3FC", + "bride_with_veil_tone2": { + "category": "people", + "moji": "👰🏼", + "unicodeVersion": "8.0", "digest": "e5463f811b2075754f0718b891757cd2e81071edf7af2215581227e1aad1d068" }, - { - "name": "bride_with_veil_tone3", - "unicode": "1F470-1F3FD", + "bride_with_veil_tone3": { + "category": "people", + "moji": "👰🏽", + "unicodeVersion": "8.0", "digest": "e5a053a26f7ccebae7eb12f638be5ed80f77b744708d783eab2eb8aa091cf516" }, - { - "name": "bride_with_veil_tone4", - "unicode": "1F470-1F3FE", + "bride_with_veil_tone4": { + "category": "people", + "moji": "👰🏾", + "unicodeVersion": "8.0", "digest": "410e23825e4401460946dc67a618bd3ace6e1a7c07dd88580a2349423685261f" }, - { - "name": "bride_with_veil_tone5", - "unicode": "1F470-1F3FF", + "bride_with_veil_tone5": { + "category": "people", + "moji": "👰🏿", + "unicodeVersion": "8.0", "digest": "454e87e5a74e13e5b4993541231516fbbe6dbe9f990e1a6f3f4a744d7d4c1615" }, - { - "name": "bridge_at_night", - "unicode": "1F309", + "bridge_at_night": { + "category": "travel", + "moji": "🌉", + "unicodeVersion": "6.0", "digest": "9d3cda5a59e27e3c90939f1ddbe7e998b3ea4fcacfa1467dea0edf39613c2d7f" }, - { - "name": "briefcase", - "unicode": "1F4BC", + "briefcase": { + "category": "people", + "moji": "💼", + "unicodeVersion": "6.0", "digest": "9d00d6a92632aaadc71b017f448c883b27eb31a7554ebb51f7e3a9841f0f7f2b" }, - { - "name": "broken_heart", - "unicode": "1F494", + "broken_heart": { + "category": "symbols", + "moji": "💔", + "unicodeVersion": "6.0", "digest": "c7ca53f444d72e596af46b61ffbc9e7c18a645020c22691e44f967db98dbf853" }, - { - "name": "bug", - "unicode": "1F41B", + "bug": { + "category": "nature", + "moji": "🐛", + "unicodeVersion": "6.0", "digest": "0dccb1d5eb91769377b4c5b310f007b60f54a5c48ba9e467b3a06898a4831b90" }, - { - "name": "bulb", - "unicode": "1F4A1", + "bulb": { + "category": "objects", + "moji": "💡", + "unicodeVersion": "6.0", "digest": "ccdaa2dfde5a88a347035a94b9d4d86cfc335ce0a73292423f5788a4bd21a5a8" }, - { - "name": "bullettrain_front", - "unicode": "1F685", + "bullettrain_front": { + "category": "travel", + "moji": "🚅", + "unicodeVersion": "6.0", "digest": "5195a6a6d23f28e1aa5ebac6ede0f6c6a8b7ff33a9edf034814f227fe976177a" }, - { - "name": "bullettrain_side", - "unicode": "1F684", + "bullettrain_side": { + "category": "travel", + "moji": "🚄", + "unicodeVersion": "6.0", "digest": "96e74842e919716b7bbbab57339bfd70f099a9bcb4710dffd7c80cf38a7bbff7" }, - { - "name": "burrito", - "unicode": "1F32F", + "burrito": { + "category": "food", + "moji": "🌯", + "unicodeVersion": "8.0", "digest": "b2cf81f1efdf87e674461f73f67cd4b58a5f695e65598d0dd3899f2597da43cf" }, - { - "name": "bus", - "unicode": "1F68C", + "bus": { + "category": "travel", + "moji": "🚌", + "unicodeVersion": "6.0", "digest": "192850b762edad21ac8770df38b9cae6d2bc1697a838462f3e36066bfb4eee50" }, - { - "name": "busstop", - "unicode": "1F68F", + "busstop": { + "category": "travel", + "moji": "🚏", + "unicodeVersion": "6.0", "digest": "adabb1ec36402b33feb636eae3656e5a8b51ff1071bcb14125d8ab80d6d12d2a" }, - { - "name": "bust_in_silhouette", - "unicode": "1F464", + "bust_in_silhouette": { + "category": "people", + "moji": "👤", + "unicodeVersion": "6.0", "digest": "277ae43301f1e49e0be03c8e52f0dc7b70c67f9d146bca0a14172e0098f115e6" }, - { - "name": "busts_in_silhouette", - "unicode": "1F465", + "busts_in_silhouette": { + "category": "people", + "moji": "👥", + "unicodeVersion": "6.0", "digest": "7fee96f1b68bb2c6002e47f2ed13c06baa6a3168441b9aca572db7ec45612f7b" }, - { - "name": "butterfly", - "unicode": "1F98B", + "butterfly": { + "category": "nature", + "moji": "🦋", + "unicodeVersion": "9.0", "digest": "a91b6598c17b44a8dc8935a1d99e25f4483ea41470cdd2da343039a9eec29ef1" }, - { - "name": "cactus", - "unicode": "1F335", + "cactus": { + "category": "nature", + "moji": "🌵", + "unicodeVersion": "6.0", "digest": "2c5c4c35f26c7046fdc002b337e0d939729b33a26980e675950f9934c91e40fd" }, - { - "name": "cake", - "unicode": "1F370", + "cake": { + "category": "food", + "moji": "🍰", + "unicodeVersion": "6.0", "digest": "b928902df8084210d51c1da36f9119164a325393c391b28cd8ea914e0b95c17b" }, - { - "name": "calendar", - "unicode": "1F4C6", + "calendar": { + "category": "objects", + "moji": "📆", + "unicodeVersion": "6.0", "digest": "9d990be27778daab041a3583edbd8f83fc8957e42a3aec729c0e2e224a8d05e3" }, - { - "name": "calendar_spiral", - "unicode": "1F5D3", - "digest": "441a0750eade7ce33e28e58bec76958990c412b68409fcdde59ebad1f25361bb" - }, - { - "name": "spiral_calendar_pad", - "unicode": "1F5D3", + "calendar_spiral": { + "category": "objects", + "moji": "🗓", + "unicodeVersion": "7.0", "digest": "441a0750eade7ce33e28e58bec76958990c412b68409fcdde59ebad1f25361bb" }, - { - "name": "call_me", - "unicode": "1F919", - "digest": "83d2ed96dcb8b4adf4f4d030ffd07e25ca16351e1a4fbefdf9f46f5ca496a55f" - }, - { - "name": "call_me_hand", - "unicode": "1F919", + "call_me": { + "category": "people", + "moji": "🤙", + "unicodeVersion": "9.0", "digest": "83d2ed96dcb8b4adf4f4d030ffd07e25ca16351e1a4fbefdf9f46f5ca496a55f" }, - { - "name": "call_me_tone1", - "unicode": "1F919-1F3FB", + "call_me_tone1": { + "category": "people", + "moji": "🤙🏻", + "unicodeVersion": "9.0", "digest": "4a5748efa83e7294e8338b8795d4d315ff1cd31ead6759004d0eb330e50de8cd" }, - { - "name": "call_me_hand_tone1", - "unicode": "1F919-1F3FB", - "digest": "4a5748efa83e7294e8338b8795d4d315ff1cd31ead6759004d0eb330e50de8cd" - }, - { - "name": "call_me_tone2", - "unicode": "1F919-1F3FC", - "digest": "54feaa6e3c5789ae6e15622127f0e0213234b4b886e1588ce95814348b1f1519" - }, - { - "name": "call_me_hand_tone2", - "unicode": "1F919-1F3FC", + "call_me_tone2": { + "category": "people", + "moji": "🤙🏼", + "unicodeVersion": "9.0", "digest": "54feaa6e3c5789ae6e15622127f0e0213234b4b886e1588ce95814348b1f1519" }, - { - "name": "call_me_tone3", - "unicode": "1F919-1F3FD", + "call_me_tone3": { + "category": "people", + "moji": "🤙🏽", + "unicodeVersion": "9.0", "digest": "57e949b951e14843b712dab5a828f915ee255f5bb973db33946aab4057427419" }, - { - "name": "call_me_hand_tone3", - "unicode": "1F919-1F3FD", - "digest": "57e949b951e14843b712dab5a828f915ee255f5bb973db33946aab4057427419" - }, - { - "name": "call_me_tone4", - "unicode": "1F919-1F3FE", - "digest": "f7787e933978a09c7b8ab8d3b1e1ab395aaae998c455e93bb3db24a4c8a60fe0" - }, - { - "name": "call_me_hand_tone4", - "unicode": "1F919-1F3FE", + "call_me_tone4": { + "category": "people", + "moji": "🤙🏾", + "unicodeVersion": "9.0", "digest": "f7787e933978a09c7b8ab8d3b1e1ab395aaae998c455e93bb3db24a4c8a60fe0" }, - { - "name": "call_me_tone5", - "unicode": "1F919-1F3FF", + "call_me_tone5": { + "category": "people", + "moji": "🤙🏿", + "unicodeVersion": "9.0", "digest": "1fdb7d833d000b117d20d48142d3026a61cc9c8b712ebb498fa66bf75c74d7a5" }, - { - "name": "call_me_hand_tone5", - "unicode": "1F919-1F3FF", - "digest": "1fdb7d833d000b117d20d48142d3026a61cc9c8b712ebb498fa66bf75c74d7a5" - }, - { - "name": "calling", - "unicode": "1F4F2", + "calling": { + "category": "objects", + "moji": "📲", + "unicodeVersion": "6.0", "digest": "acf668c75c11c36686005788266524a972fa1c5bcf666ff3403d909edc5cee91" }, - { - "name": "camel", - "unicode": "1F42B", + "camel": { + "category": "nature", + "moji": "🐫", + "unicodeVersion": "6.0", "digest": "5f927927a7ab1277d0dc8b8211436957968b1e11365a8bf535e9bb94f92c5631" }, - { - "name": "camera", - "unicode": "1F4F7", + "camera": { + "category": "objects", + "moji": "📷", + "unicodeVersion": "6.0", "digest": "fde03e396822a36cd6ae756ede885b945a074395264162731ca5db47a3b39d80" }, - { - "name": "camera_with_flash", - "unicode": "1F4F8", + "camera_with_flash": { + "category": "objects", + "moji": "📸", + "unicodeVersion": "7.0", "digest": "9afd380208187780f00244c45d4db6c5ea1ea088d4a1bd8fc92a8f3877149750" }, - { - "name": "camping", - "unicode": "1F3D5", + "camping": { + "category": "travel", + "moji": "🏕", + "unicodeVersion": "7.0", "digest": "a42a4ff9521affa72db7b0f01da169b4cb6afb9db1c5dfad47dd4c507bfc30d9" }, - { - "name": "cancer", - "unicode": "264B", + "cancer": { + "category": "symbols", + "moji": "♋", + "unicodeVersion": "1.1", "digest": "528c6f21df99a756b553d93a7f395b0f662b30a323affd05f0cedee8ff7b41d6" }, - { - "name": "candle", - "unicode": "1F56F", + "candle": { + "category": "objects", + "moji": "🕯", + "unicodeVersion": "7.0", "digest": "211c04dc3a91b071c284d4180ed09f9d3320e3fd6ba8a9fddd0677bc97fd12cb" }, - { - "name": "candy", - "unicode": "1F36C", + "candy": { + "category": "food", + "moji": "🍬", + "unicodeVersion": "6.0", "digest": "9cff4538918f60f770fceb96e964f5dc3ce31fd08ddd2ab3bfdf2981bfa74100" }, - { - "name": "canoe", - "unicode": "1F6F6", - "digest": "56ca308cc2ad4827468cf58c4ccf6ef6b3382835a91e935540a2b973e01d2572" - }, - { - "name": "kayak", - "unicode": "1F6F6", + "canoe": { + "category": "travel", + "moji": "🛶", + "unicodeVersion": "9.0", "digest": "56ca308cc2ad4827468cf58c4ccf6ef6b3382835a91e935540a2b973e01d2572" }, - { - "name": "capital_abcd", - "unicode": "1F520", + "capital_abcd": { + "category": "symbols", + "moji": "🔠", + "unicodeVersion": "6.0", "digest": "a416d0b3f564037b680f801fb773b6eaf67225e2cbbfd2cb8a5db0de044321fa" }, - { - "name": "capricorn", - "unicode": "2651", + "capricorn": { + "category": "symbols", + "moji": "♑", + "unicodeVersion": "1.1", "digest": "f11abad102603737b55486fe2ea4d01f28b203394bcd84f19a7948156e6c4b96" }, - { - "name": "card_box", - "unicode": "1F5C3", + "card_box": { + "category": "objects", + "moji": "🗃", + "unicodeVersion": "7.0", "digest": "7a6199d562f30e02ed31094de6aebeb99eae8ac156f6910463dfed73256f4c9a" }, - { - "name": "card_file_box", - "unicode": "1F5C3", - "digest": "7a6199d562f30e02ed31094de6aebeb99eae8ac156f6910463dfed73256f4c9a" - }, - { - "name": "card_index", - "unicode": "1F4C7", + "card_index": { + "category": "objects", + "moji": "📇", + "unicodeVersion": "6.0", "digest": "86e187e0a72ca5d00207d6ef34d66ce15046848a831c2b5184fb840c5332a2a8" }, - { - "name": "carousel_horse", - "unicode": "1F3A0", + "carousel_horse": { + "category": "travel", + "moji": "🎠", + "unicodeVersion": "6.0", "digest": "c0e7059efc39a64233f774c02ddb1ab51888fff180f906ce13a6e4f9509672fe" }, - { - "name": "carrot", - "unicode": "1F955", + "carrot": { + "category": "food", + "moji": "🥕", + "unicodeVersion": "9.0", "digest": "3a6fd98b63ee73d982a9cdacb08cf7b4014368cde8ffce6056b7df25a5a472b1" }, - { - "name": "cartwheel", - "unicode": "1F938", - "digest": "d78de3435e0b04a9b1a1048ae12e63e3248f9ace3a0db4d3bda584f22af18863" - }, - { - "name": "person_doing_cartwheel", - "unicode": "1F938", + "cartwheel": { + "category": "activity", + "moji": "🤸", + "unicodeVersion": "9.0", "digest": "d78de3435e0b04a9b1a1048ae12e63e3248f9ace3a0db4d3bda584f22af18863" }, - { - "name": "cartwheel_tone1", - "unicode": "1F938-1F3FB", + "cartwheel_tone1": { + "category": "activity", + "moji": "🤸🏻", + "unicodeVersion": "9.0", "digest": "39a49781a269bb40d8efc8fd73c973b00fb2e192850ea6073062b5dea0cd5b74" }, - { - "name": "person_doing_cartwheel_tone1", - "unicode": "1F938-1F3FB", - "digest": "39a49781a269bb40d8efc8fd73c973b00fb2e192850ea6073062b5dea0cd5b74" - }, - { - "name": "cartwheel_tone2", - "unicode": "1F938-1F3FC", - "digest": "6231eb35be45457fd648f8f4b79983f03705c9d983a18067f7e6d9ae47bc1958" - }, - { - "name": "person_doing_cartwheel_tone2", - "unicode": "1F938-1F3FC", + "cartwheel_tone2": { + "category": "activity", + "moji": "🤸🏼", + "unicodeVersion": "9.0", "digest": "6231eb35be45457fd648f8f4b79983f03705c9d983a18067f7e6d9ae47bc1958" }, - { - "name": "cartwheel_tone3", - "unicode": "1F938-1F3FD", + "cartwheel_tone3": { + "category": "activity", + "moji": "🤸🏽", + "unicodeVersion": "9.0", "digest": "ca483c78cc823811a8c279c501d9b283e4c990dafc5995ad40e68ecb0af554df" }, - { - "name": "person_doing_cartwheel_tone3", - "unicode": "1F938-1F3FD", - "digest": "ca483c78cc823811a8c279c501d9b283e4c990dafc5995ad40e68ecb0af554df" - }, - { - "name": "cartwheel_tone4", - "unicode": "1F938-1F3FE", - "digest": "8253afb672431c84e498014c30babb00b9284bec773009e79f7f06aa7108643e" - }, - { - "name": "person_doing_cartwheel_tone4", - "unicode": "1F938-1F3FE", + "cartwheel_tone4": { + "category": "activity", + "moji": "🤸🏾,", + "unicodeVersion": "9.0", "digest": "8253afb672431c84e498014c30babb00b9284bec773009e79f7f06aa7108643e" }, - { - "name": "cartwheel_tone5", - "unicode": "1F938-1F3FF", + "cartwheel_tone5": { + "category": "activity", + "moji": "🤸🏿", + "unicodeVersion": "9.0", "digest": "6fd92baff57c38b3adb6753d9e7e547e762971a8872fd3f1e71c6aaf0b1d3ab9" }, - { - "name": "person_doing_cartwheel_tone5", - "unicode": "1F938-1F3FF", - "digest": "6fd92baff57c38b3adb6753d9e7e547e762971a8872fd3f1e71c6aaf0b1d3ab9" - }, - { - "name": "cat", - "unicode": "1F431", + "cat": { + "category": "nature", + "moji": "🐱", + "unicodeVersion": "6.0", "digest": "e52d0d3a205a0ba99094717e171a7f572b713a0e21b276ffa4a826596fe5cafc" }, - { - "name": "cat2", - "unicode": "1F408", + "cat2": { + "category": "nature", + "moji": "🐈", + "unicodeVersion": "6.0", "digest": "46aa67a99f782935932c77b8de93287142297abe52928c173191cf55bb8f4339" }, - { - "name": "cd", - "unicode": "1F4BF", + "cd": { + "category": "objects", + "moji": "💿", + "unicodeVersion": "6.0", "digest": "16363d8a34b873c12df6354b99f575cae3d80e0d27100ed7eea70f0310953c7b" }, - { - "name": "chains", - "unicode": "26D3", + "chains": { + "category": "objects", + "moji": "⛓", + "unicodeVersion": "5.2", "digest": "3884cdbc6f2b433062af06f942552e563231c24727a2f10fa280b3bb7aa614e2" }, - { - "name": "champagne", - "unicode": "1F37E", - "digest": "9e6e8987f30a37ae0f3d7dab2f5eeb50aa32b4f31402b29315eb2994afc72457" - }, - { - "name": "bottle_with_popping_cork", - "unicode": "1F37E", + "champagne": { + "category": "food", + "moji": "🍾", + "unicodeVersion": "8.0", "digest": "9e6e8987f30a37ae0f3d7dab2f5eeb50aa32b4f31402b29315eb2994afc72457" }, - { - "name": "champagne_glass", - "unicode": "1F942", + "champagne_glass": { + "category": "food", + "moji": "🥂", + "unicodeVersion": "9.0", "digest": "5a2e4773f7eb126a00122cbfa4dc535da51ce00e0bf0d8d6ff8bab8b3365f8d2" }, - { - "name": "clinking_glass", - "unicode": "1F942", - "digest": "5a2e4773f7eb126a00122cbfa4dc535da51ce00e0bf0d8d6ff8bab8b3365f8d2" - }, - { - "name": "chart", - "unicode": "1F4B9", + "chart": { + "category": "symbols", + "moji": "💹", + "unicodeVersion": "6.0", "digest": "a092dbc08f925b028286b2b495a5f59033b8537a586a694f46f4c1e7c3a1e27f" }, - { - "name": "chart_with_downwards_trend", - "unicode": "1F4C9", + "chart_with_downwards_trend": { + "category": "objects", + "moji": "📉", + "unicodeVersion": "6.0", "digest": "5db7ccbc37665736a9c0b2f50247dcc09e404ec37f39db45b7b8b9464172a18c" }, - { - "name": "chart_with_upwards_trend", - "unicode": "1F4C8", + "chart_with_upwards_trend": { + "category": "objects", + "moji": "📈", + "unicodeVersion": "6.0", "digest": "bc4ea250b102fe5c09847e471478aff065ad3df755d9717896d38d887d9c6733" }, - { - "name": "checkered_flag", - "unicode": "1F3C1", + "checkered_flag": { + "category": "travel", + "moji": "🏁", + "unicodeVersion": "6.0", "digest": "0e77180e0cf9fc87e755a5a42cf23aec6bf30931db41331311e97ba0be178b78" }, - { - "name": "cheese", - "unicode": "1F9C0", + "cheese": { + "category": "food", + "moji": "🧀", + "unicodeVersion": "8.0", "digest": "50a6cb906c2120e2bbc0e22105924262007cfe1554d7b02b8cc84b6adedc6a0b" }, - { - "name": "cheese_wedge", - "unicode": "1F9C0", - "digest": "50a6cb906c2120e2bbc0e22105924262007cfe1554d7b02b8cc84b6adedc6a0b" - }, - { - "name": "cherries", - "unicode": "1F352", + "cherries": { + "category": "food", + "moji": "🍒", + "unicodeVersion": "6.0", "digest": "13b8db9e7e6eec8509aa80c762966e1bf3538fcb1ac3d6eab18ee4da1528cf84" }, - { - "name": "cherry_blossom", - "unicode": "1F338", + "cherry_blossom": { + "category": "nature", + "moji": "🌸", + "unicodeVersion": "6.0", "digest": "af3083f5f8dd94936113f2e16caba5aec7a774d5589aa08bf5de82a2d278cc66" }, - { - "name": "chestnut", - "unicode": "1F330", + "chestnut": { + "category": "nature", + "moji": "🌰", + "unicodeVersion": "6.0", "digest": "9f85b79b207a69ab81ab88dcef04954000965b039b4cf57de5f1b381745ab98b" }, - { - "name": "chicken", - "unicode": "1F414", + "chicken": { + "category": "nature", + "moji": "🐔", + "unicodeVersion": "6.0", "digest": "57ceb4459d183740009caac6ebed089d2f1e12f67c138e1be1d0f992313c0ac4" }, - { - "name": "children_crossing", - "unicode": "1F6B8", + "children_crossing": { + "category": "symbols", + "moji": "🚸", + "unicodeVersion": "6.0", "digest": "0ded7d9aca0161e8ef8e2858c3c198e70e4badc7105ac3a6886e06975de19106" }, - { - "name": "chipmunk", - "unicode": "1F43F", + "chipmunk": { + "category": "nature", + "moji": "🐿", + "unicodeVersion": "7.0", "digest": "5b0dc1a859163097727ba2ba5ffca38b0a54d925eebb089977d28d0b4d917a3f" }, - { - "name": "chocolate_bar", - "unicode": "1F36B", + "chocolate_bar": { + "category": "food", + "moji": "🍫", + "unicodeVersion": "6.0", "digest": "dd273e5050488acaf885f8a18b6e2b3901f69c5b39fa6465fb60621783d4109a" }, - { - "name": "christmas_tree", - "unicode": "1F384", + "christmas_tree": { + "category": "nature", + "moji": "🎄", + "unicodeVersion": "6.0", "digest": "ce60cbe2ebbe8057be8edea2392455fedd2bcda64a0a831f6a1942028af7e747" }, - { - "name": "church", - "unicode": "26EA", + "church": { + "category": "travel", + "moji": "⛪", + "unicodeVersion": "5.2", "digest": "2c328456528f7336e59443e20ec3ab22fe71f1fccb1dd50d0ad68eb206937557" }, - { - "name": "cinema", - "unicode": "1F3A6", + "cinema": { + "category": "symbols", + "moji": "🎦", + "unicodeVersion": "6.0", "digest": "4c26dcdc76f93dbc2a1dc49ed4e132b8e8f2b7cdc1acf5e09b3dfd99430d97cd" }, - { - "name": "circus_tent", - "unicode": "1F3AA", + "circus_tent": { + "category": "activity", + "moji": "🎪", + "unicodeVersion": "6.0", "digest": "fec5f2a06222be8be549178b29720343cc00145177ec387ca4e6f3432481fe77" }, - { - "name": "city_dusk", - "unicode": "1F306", + "city_dusk": { + "category": "travel", + "moji": "🌆", + "unicodeVersion": "6.0", "digest": "bba345e949dcc51f5f018220f000223797970c82ead2ab9c822f9dc0847aa155" }, - { - "name": "city_sunset", - "unicode": "1F307", + "city_sunset": { + "category": "travel", + "moji": "🌇", + "unicodeVersion": "6.0", "digest": "a846df1a4c7c778f8e1729804aece86eb29d2fcb95dc39eaaf2aae1897f3dcc7" }, - { - "name": "city_sunrise", - "unicode": "1F307", - "digest": "a846df1a4c7c778f8e1729804aece86eb29d2fcb95dc39eaaf2aae1897f3dcc7" - }, - { - "name": "cityscape", - "unicode": "1F3D9", + "cityscape": { + "category": "travel", + "moji": "🏙", + "unicodeVersion": "7.0", "digest": "ee360be7514c4bfb0d539dd28f3b2031ebcef04e850723ec0685fb54bd8e6d5f" }, - { - "name": "cl", - "unicode": "1F191", + "cl": { + "category": "symbols", + "moji": "🆑", + "unicodeVersion": "6.0", "digest": "fcec2855dbad9fda11d6e2802bc0dcaabab0b5be233508f5e439f156f07602c1" }, - { - "name": "clap", - "unicode": "1F44F", + "clap": { + "category": "people", + "moji": "👏", + "unicodeVersion": "6.0", "digest": "a1860ce7812a9f6fb55e45761e1b79a2f8f0620eb04f80748a38420889d58a2a" }, - { - "name": "clap_tone1", - "unicode": "1F44F-1F3FB", + "clap_tone1": { + "category": "people", + "moji": "👏🏻", + "unicodeVersion": "8.0", "digest": "18a7022e08223fb2109af5a9b9a5b4f47dc870ce4453f4987d2d0b729ef54586" }, - { - "name": "clap_tone2", - "unicode": "1F44F-1F3FC", + "clap_tone2": { + "category": "people", + "moji": "👏🏼", + "unicodeVersion": "8.0", "digest": "5954c8658b15e755d2018d8674df84d38e22ffededc4d726c6a33b709f71426a" }, - { - "name": "clap_tone3", - "unicode": "1F44F-1F3FD", + "clap_tone3": { + "category": "people", + "moji": "👏🏽", + "unicodeVersion": "8.0", "digest": "22639b6bd3c53784a2f855d6db7bdf31621519f19dfc29a6bc310eee6421f742" }, - { - "name": "clap_tone4", - "unicode": "1F44F-1F3FE", + "clap_tone4": { + "category": "people", + "moji": "👏🏾", + "unicodeVersion": "8.0", "digest": "e55248dc163d1bbd118b50cd8767750ead86d082151febbc0a75b32d63abceec" }, - { - "name": "clap_tone5", - "unicode": "1F44F-1F3FF", + "clap_tone5": { + "category": "people", + "moji": "👏🏿", + "unicodeVersion": "8.0", "digest": "76046b8157dabbe048a07fc318122456020c9c980fc1b8ab76802330e07b3b53" }, - { - "name": "clapper", - "unicode": "1F3AC", + "clapper": { + "category": "activity", + "moji": "🎬", + "unicodeVersion": "6.0", "digest": "8149752a0e3e8abede2d433d1afab6d217877d0c76adb1e2845a0142c0cdcbaa" }, - { - "name": "classical_building", - "unicode": "1F3DB", + "classical_building": { + "category": "travel", + "moji": "🏛", + "unicodeVersion": "7.0", "digest": "9ee0d00c43d6e22b6a3ddea67619737270cc7e9294797a19c7c60d5f92aa44fa" }, - { - "name": "clipboard", - "unicode": "1F4CB", + "clipboard": { + "category": "objects", + "moji": "📋", + "unicodeVersion": "6.0", "digest": "bdd7f7d973c714e59d2903d401a876e6018794c7987c9ca57108c137c5edc25f" }, - { - "name": "clock", - "unicode": "1F570", - "digest": "302835eab2637db799acf69b3d795571ef3432251267050db0704f2954e8b190" - }, - { - "name": "mantlepiece_clock", - "unicode": "1F570", + "clock": { + "category": "objects", + "moji": "🕰", + "unicodeVersion": "7.0", "digest": "302835eab2637db799acf69b3d795571ef3432251267050db0704f2954e8b190" }, - { - "name": "clock1", - "unicode": "1F550", + "clock1": { + "category": "symbols", + "moji": "🕐", + "unicodeVersion": "6.0", "digest": "1778eec07ce061c9393e5abee5ca83b24e1ce61d8a75fa2e39efcb31aa160395" }, - { - "name": "clock10", - "unicode": "1F559", + "clock10": { + "category": "symbols", + "moji": "🕙", + "unicodeVersion": "6.0", "digest": "601fc12ea5280a54c2e69dbb685f454e4165fe771756ed6f89016e29e683a24f" }, - { - "name": "clock1030", - "unicode": "1F565", + "clock1030": { + "category": "symbols", + "moji": "🕥", + "unicodeVersion": "6.0", "digest": "4fd155f08f797542d52cff4b0aa3ca9f080f37a41c301b82f90ff6d4693c890e" }, - { - "name": "clock11", - "unicode": "1F55A", + "clock11": { + "category": "symbols", + "moji": "🕚", + "unicodeVersion": "6.0", "digest": "5c79dc812e812e8a01993ea633b323d654ce3a7ea258692781a4896e4ad2017e" }, - { - "name": "clock1130", - "unicode": "1F566", + "clock1130": { + "category": "symbols", + "moji": "🕦", + "unicodeVersion": "6.0", "digest": "41497ee2020ee5ac9aa5f9b07560f7afca7c422b04214449cfc5cea9f020f52e" }, - { - "name": "clock12", - "unicode": "1F55B", + "clock12": { + "category": "symbols", + "moji": "🕛", + "unicodeVersion": "6.0", "digest": "046bb7ffa5f5d27c2e3411ba543484d9dabb8ebf6d6e7a7e9bfb088c1813500c" }, - { - "name": "clock1230", - "unicode": "1F567", + "clock1230": { + "category": "symbols", + "moji": "🕧", + "unicodeVersion": "6.0", "digest": "bbfe9db5a2043aaba19a7a2a0185c7efcebf1e8c9263b8233f75b53c4825f0f4" }, - { - "name": "clock130", - "unicode": "1F55C", + "clock130": { + "category": "symbols", + "moji": "🕜", + "unicodeVersion": "6.0", "digest": "8662cb395ee680c2781123305c4c8ce8c0df9565c2c942668940be540cc0c094" }, - { - "name": "clock2", - "unicode": "1F551", + "clock2": { + "category": "symbols", + "moji": "🕑", + "unicodeVersion": "6.0", "digest": "42f7429748b612dce7de77221cbbc710655811f7bb23e2a986c36e6d662f0ec4" }, - { - "name": "clock230", - "unicode": "1F55D", + "clock230": { + "category": "symbols", + "moji": "🕝", + "unicodeVersion": "6.0", "digest": "e710b6ef14227cd240ea3e2a867c8ef45b5c060adf3cb30ba9077c2351fe6677" }, - { - "name": "clock3", - "unicode": "1F552", + "clock3": { + "category": "symbols", + "moji": "🕒", + "unicodeVersion": "6.0", "digest": "7340d465b398a378211dff9ec806db579d061206fd6fc238623d070cfe0a55ce" }, - { - "name": "clock330", - "unicode": "1F55E", + "clock330": { + "category": "symbols", + "moji": "🕞", + "unicodeVersion": "6.0", "digest": "7aa4a15cc8de04ed3bdeb0f8a54a7915065f2809a07054e002d89926c9766831" }, - { - "name": "clock4", - "unicode": "1F553", + "clock4": { + "category": "symbols", + "moji": "🕓", + "unicodeVersion": "6.0", "digest": "36fd88e81ad488b0ec49a911a838693281573fa14736ae4a6dd1c40a4ff69bb1" }, - { - "name": "clock430", - "unicode": "1F55F", + "clock430": { + "category": "symbols", + "moji": "🕟", + "unicodeVersion": "6.0", "digest": "7bd5dd71e89d95dcf18b9e8c1fe2a353a7da3b69aadb8dda80ee9bafb05da58d" }, - { - "name": "clock5", - "unicode": "1F554", + "clock5": { + "category": "symbols", + "moji": "🕔", + "unicodeVersion": "6.0", "digest": "aa406409e56a0bfd8c850e44efe45fd190ffd7bf7061e934ed7928dfbdfc9eba" }, - { - "name": "clock530", - "unicode": "1F560", + "clock530": { + "category": "symbols", + "moji": "🕠", + "unicodeVersion": "6.0", "digest": "25dd3bcc53ddd98eeea498d7dbd4c306ef39dd033f15909063388a0800febf41" }, - { - "name": "clock6", - "unicode": "1F555", + "clock6": { + "category": "symbols", + "moji": "🕕", + "unicodeVersion": "6.0", "digest": "0a321eaf1bc5db8436bbadac66c45ba257fc98ad4c7569ce3fc6602c824b6d7c" }, - { - "name": "clock630", - "unicode": "1F561", + "clock630": { + "category": "symbols", + "moji": "🕡", + "unicodeVersion": "6.0", "digest": "55a4c5a665fdd38a724e9357a93c55401fcd5f1b13078c25754bd70c3fc4ccec" }, - { - "name": "clock7", - "unicode": "1F556", + "clock7": { + "category": "symbols", + "moji": "🕖", + "unicodeVersion": "6.0", "digest": "6154306545716e865da0ec537ee4f22bfe6c7294502a64a2dcf425c587d0e2a2" }, - { - "name": "clock730", - "unicode": "1F562", + "clock730": { + "category": "symbols", + "moji": "🕢", + "unicodeVersion": "6.0", "digest": "6925654de642e50f84661f94364a96c87757d73fffe766aacbf4bbd70130547b" }, - { - "name": "clock8", - "unicode": "1F557", + "clock8": { + "category": "symbols", + "moji": "🕗", + "unicodeVersion": "6.0", "digest": "9be2d189c7ea56d39fd259f84853d753c1cf33e64f8ed57f86f822d9ae23a1ee" }, - { - "name": "clock830", - "unicode": "1F563", + "clock830": { + "category": "symbols", + "moji": "🕣", + "unicodeVersion": "6.0", "digest": "16878613c0000d2f558c88d080551f424a8bd9df1358e0f931dd25c3da68f2d9" }, - { - "name": "clock9", - "unicode": "1F558", + "clock9": { + "category": "symbols", + "moji": "🕘", + "unicodeVersion": "6.0", "digest": "1d1e7e3c9d085ffa5b7c0f3d9fd394b734f16ae3b60df09af50fe6c8d4f3c8bb" }, - { - "name": "clock930", - "unicode": "1F564", + "clock930": { + "category": "symbols", + "moji": "🕤", + "unicodeVersion": "6.0", "digest": "9fdef6a4939315c017b165e1dbac7710fb335df8c309be3fe2a011ef7fc28d74" }, - { - "name": "closed_book", - "unicode": "1F4D5", + "closed_book": { + "category": "objects", + "moji": "📕", + "unicodeVersion": "6.0", "digest": "b18288629d201bfdfc5d66ec47df89809d00642b15732757e6a04789f36a7d9f" }, - { - "name": "closed_lock_with_key", - "unicode": "1F510", + "closed_lock_with_key": { + "category": "objects", + "moji": "🔐", + "unicodeVersion": "6.0", "digest": "e39adfe9b30973bca16472c2b7e6462b064a93b9d452aa48edd74c727641a83d" }, - { - "name": "closed_umbrella", - "unicode": "1F302", + "closed_umbrella": { + "category": "people", + "moji": "🌂", + "unicodeVersion": "6.0", "digest": "2cc0592c74601f7439e88c3c1ec4f05e3459608ef1ea6558c5824ed7c3889727" }, - { - "name": "cloud", - "unicode": "2601", + "cloud": { + "category": "nature", + "moji": "☁", + "unicodeVersion": "1.1", "digest": "5b3a19718dfa8a381929665afdc2284464d24020c8dd0caff4dad465a1f536ba" }, - { - "name": "cloud_lightning", - "unicode": "1F329", + "cloud_lightning": { + "category": "nature", + "moji": "🌩", + "unicodeVersion": "7.0", "digest": "2b32f6d87726df2935ad81870879ccec30ce9b4fd5861d1a6317f9eca2f013d9" }, - { - "name": "cloud_with_lightning", - "unicode": "1F329", - "digest": "2b32f6d87726df2935ad81870879ccec30ce9b4fd5861d1a6317f9eca2f013d9" - }, - { - "name": "cloud_rain", - "unicode": "1F327", - "digest": "1e1e8bc59e168e1d2e72bf11f2d43cb578cbf0a5f1daf383bba5c56fb750ee71" - }, - { - "name": "cloud_with_rain", - "unicode": "1F327", + "cloud_rain": { + "category": "nature", + "moji": "🌧", + "unicodeVersion": "7.0", "digest": "1e1e8bc59e168e1d2e72bf11f2d43cb578cbf0a5f1daf383bba5c56fb750ee71" }, - { - "name": "cloud_snow", - "unicode": "1F328", - "digest": "2d364f859b83e684213e8eece1640208d80a8de0a49d0fc8e0e24c5a8493a3b1" - }, - { - "name": "cloud_with_snow", - "unicode": "1F328", + "cloud_snow": { + "category": "nature", + "moji": "🌨", + "unicodeVersion": "7.0", "digest": "2d364f859b83e684213e8eece1640208d80a8de0a49d0fc8e0e24c5a8493a3b1" }, - { - "name": "cloud_tornado", - "unicode": "1F32A", - "digest": "7cbed2343c280ba3996082b3d0fb9d8cd57d6e62fe6c9ecb159f46b4a2e49151" - }, - { - "name": "cloud_with_tornado", - "unicode": "1F32A", + "cloud_tornado": { + "category": "nature", + "moji": "🌪", + "unicodeVersion": "7.0", "digest": "7cbed2343c280ba3996082b3d0fb9d8cd57d6e62fe6c9ecb159f46b4a2e49151" }, - { - "name": "clown", - "unicode": "1F921", - "digest": "eea95687caabc9e808514c2450ba599e5e24ef47923dbec86f5297a64438e2e5" - }, - { - "name": "clown_face", - "unicode": "1F921", + "clown": { + "category": "people", + "moji": "🤡", + "unicodeVersion": "9.0", "digest": "eea95687caabc9e808514c2450ba599e5e24ef47923dbec86f5297a64438e2e5" }, - { - "name": "clubs", - "unicode": "2663", + "clubs": { + "category": "symbols", + "moji": "♣", + "unicodeVersion": "1.1", "digest": "b8cf72ecd8568ced077b475d94788fb282bdb06d25031b5d54dd63e25effb138" }, - { - "name": "cocktail", - "unicode": "1F378", + "cocktail": { + "category": "food", + "moji": "🍸", + "unicodeVersion": "6.0", "digest": "3792def2cde885cf32167f04904d3b0b788388e8af410c63e4cd31550feba775" }, - { - "name": "coffee", - "unicode": "2615", + "coffee": { + "category": "food", + "moji": "☕", + "unicodeVersion": "4.0", "digest": "0d29615a7a67d3aafa257b909bb915dc74fa8f854acb0d9a2c29e94eedf80326" }, - { - "name": "coffin", - "unicode": "26B0", + "coffin": { + "category": "objects", + "moji": "⚰", + "unicodeVersion": "4.1", "digest": "78eccc1aad2a822649fba8503d4d30354bef367c4271193c40ddb692308f9db8" }, - { - "name": "cold_sweat", - "unicode": "1F630", + "cold_sweat": { + "category": "people", + "moji": "😰", + "unicodeVersion": "6.0", "digest": "f53aab523ed3fa2224a16881d263fb5e039f163380f92feb2c63c20f9b14dcd2" }, - { - "name": "comet", - "unicode": "2604", + "comet": { + "category": "nature", + "moji": "☄", + "unicodeVersion": "1.1", "digest": "40ce93e55c6e57a88d80670b37171190bd5ffc87b7078891d8de5b15795385c5" }, - { - "name": "compression", - "unicode": "1F5DC", + "compression": { + "category": "objects", + "moji": "🗜", + "unicodeVersion": "7.0", "digest": "c8841f7afb5345f1c31da116a7fb41d07232ea58d3f7f1a75c5890aa1a80bfd6" }, - { - "name": "computer", - "unicode": "1F4BB", + "computer": { + "category": "objects", + "moji": "💻", + "unicodeVersion": "6.0", "digest": "c970ce76b5607434895b0407bdaa93140f887930781a17dd7dcf16f711451d93" }, - { - "name": "confetti_ball", - "unicode": "1F38A", + "confetti_ball": { + "category": "objects", + "moji": "🎊", + "unicodeVersion": "6.0", "digest": "a638b16f1acdbcf69edf760161b1bd7ff1fd5426c5b1203ad9d294dcc0701f10" }, - { - "name": "confounded", - "unicode": "1F616", + "confounded": { + "category": "people", + "moji": "😖", + "unicodeVersion": "6.0", "digest": "e2ff3b4df65d00c1ca9ae0cb379f959ea2cecefb3d676d4f8c2c5f2c103da4f6" }, - { - "name": "confused", - "unicode": "1F615", + "confused": { + "category": "people", + "moji": "😕", + "unicodeVersion": "6.1", "digest": "118d7f830ec08a3ac4b798eebb77a989b8c142f2588727181be4a2548e3c4f06" }, - { - "name": "congratulations", - "unicode": "3297", + "congratulations": { + "category": "symbols", + "moji": "㊗", + "unicodeVersion": "1.1", "digest": "02fd1338c54fe5f9a0fd861f23c56edc1d39bcd3140b68f0f626f9e2494d2d1c" }, - { - "name": "construction", - "unicode": "1F6A7", + "construction": { + "category": "travel", + "moji": "🚧", + "unicodeVersion": "6.0", "digest": "c3a0401331111b9eda1206bee5f322db80b0870547d307b10dcac1314e4078c8" }, - { - "name": "construction_site", - "unicode": "1F3D7", - "digest": "c611f0a5de10f000a0756935f226845c7292f19ff5581d1f7a7554316338bbcb" - }, - { - "name": "building_construction", - "unicode": "1F3D7", + "construction_site": { + "category": "travel", + "moji": "🏗", + "unicodeVersion": "7.0", "digest": "c611f0a5de10f000a0756935f226845c7292f19ff5581d1f7a7554316338bbcb" }, - { - "name": "construction_worker", - "unicode": "1F477", + "construction_worker": { + "category": "people", + "moji": "👷", + "unicodeVersion": "6.0", "digest": "8c094733987e7c4da8d3aa4588b530ae07042bd70cf337b1fd412a70ee8f0ed6" }, - { - "name": "construction_worker_tone1", - "unicode": "1F477-1F3FB", + "construction_worker_tone1": { + "category": "people", + "moji": "👷🏻", + "unicodeVersion": "8.0", "digest": "fcd927405fef4486105cd3aff62155467d21cebbc013924d4b52b717b566602b" }, - { - "name": "construction_worker_tone2", - "unicode": "1F477-1F3FC", + "construction_worker_tone2": { + "category": "people", + "moji": "👷🏼", + "unicodeVersion": "8.0", "digest": "d1ec773828936c703dd6e334e696dc3cf7c34c0a8ec691564a384b735cdeaaba" }, - { - "name": "construction_worker_tone3", - "unicode": "1F477-1F3FD", + "construction_worker_tone3": { + "category": "people", + "moji": "👷🏽", + "unicodeVersion": "8.0", "digest": "37c114d6879b9b32b800b0d4cf770dcbe04d1455698130ecd709a0cb9dea880b" }, - { - "name": "construction_worker_tone4", - "unicode": "1F477-1F3FE", + "construction_worker_tone4": { + "category": "people", + "moji": "👷🏾", + "unicodeVersion": "8.0", "digest": "5264996c1bedb6061a0dfdddce233d863bf308d27127ad152b63bfd983162cf7" }, - { - "name": "construction_worker_tone5", - "unicode": "1F477-1F3FF", + "construction_worker_tone5": { + "category": "people", + "moji": "👷🏿", + "unicodeVersion": "8.0", "digest": "87051aec81fd5dfd4dc44ff0411a528ee08253e9494d37efa550694e28dde6d3" }, - { - "name": "control_knobs", - "unicode": "1F39B", + "control_knobs": { + "category": "objects", + "moji": "🎛", + "unicodeVersion": "7.0", "digest": "0d7f33ff7acc1cc3a81e6a786ff007df20da145e3070f338505dfed5100e9fcb" }, - { - "name": "convenience_store", - "unicode": "1F3EA", + "convenience_store": { + "category": "travel", + "moji": "🏪", + "unicodeVersion": "6.0", "digest": "975dcf9b8e9e3fb1e29574b41300b9d96fd64703b3c18ff52f9f1875d1cf1b52" }, - { - "name": "cookie", - "unicode": "1F36A", + "cookie": { + "category": "food", + "moji": "🍪", + "unicodeVersion": "6.0", "digest": "4bed3522bd50091ac5b68ca760661eb484d7f1b9c9d564d2097bd812b7f28ae4" }, - { - "name": "cooking", - "unicode": "1F373", + "cooking": { + "category": "food", + "moji": "🍳", + "unicodeVersion": "6.0", "digest": "563ffd6cae381ce1e318cdacc54e70040d6a01a50d0db8aeb50edbbe413eac58" }, - { - "name": "cool", - "unicode": "1F192", + "cool": { + "category": "symbols", + "moji": "🆒", + "unicodeVersion": "6.0", "digest": "5739a37341c782a4736adfce804e12776ae33081098a3d052d8ae9a64b4d22d1" }, - { - "name": "cop", - "unicode": "1F46E", + "cop": { + "category": "people", + "moji": "👮", + "unicodeVersion": "6.0", "digest": "78996521bbe231d03ebea355226d8a1515f47cde7b2fbeca1037e7b7e5133466" }, - { - "name": "cop_tone1", - "unicode": "1F46E-1F3FB", + "cop_tone1": { + "category": "people", + "moji": "👮🏻", + "unicodeVersion": "8.0", "digest": "8a38cd107f5f4c0b821ac43f32df5dc57facaf39fbafb98483ec00fd7df41baf" }, - { - "name": "cop_tone2", - "unicode": "1F46E-1F3FC", + "cop_tone2": { + "category": "people", + "moji": "👮🏼", + "unicodeVersion": "8.0", "digest": "8ab8ab086f3ff82aa4bf4760c3c822846ec2696c41d21dffdac12d5afbe398b7" }, - { - "name": "cop_tone3", - "unicode": "1F46E-1F3FD", + "cop_tone3": { + "category": "people", + "moji": "👮🏽", + "unicodeVersion": "8.0", "digest": "fce710a99fd44a7c8af3ea01b2007e46d3ff38d7a0dff1ef26d6f893ede7e6d2" }, - { - "name": "cop_tone4", - "unicode": "1F46E-1F3FE", + "cop_tone4": { + "category": "people", + "moji": "👮🏾", + "unicodeVersion": "8.0", "digest": "3017dd73ef475379911c5e6c79bd0f9f533dbbc5057bce6a11244faa12996ba0" }, - { - "name": "cop_tone5", - "unicode": "1F46E-1F3FF", + "cop_tone5": { + "category": "people", + "moji": "👮🏿", + "unicodeVersion": "8.0", "digest": "a3b8807b3f2a8d6ee9bcec0339355bda486e8c930f727139f5447a4b046a6307" }, - { - "name": "copyright", - "unicode": "00A9", + "copyright": { + "category": "symbols", + "moji": "©", + "unicodeVersion": "1.1", "digest": "cc28663cdd3f8333d9bb57b511348cde4e51bda19cf0629dccb05c8fc425e079" }, - { - "name": "corn", - "unicode": "1F33D", + "corn": { + "category": "food", + "moji": "🌽", + "unicodeVersion": "6.0", "digest": "a099a0b291fa758690e6ee6c762b9ade9a0e3350a707c52d968dfffbcc467de5" }, - { - "name": "couch", - "unicode": "1F6CB", - "digest": "84cd734dbaa7f9f519438036d687e7a53217130779bc3de30258f163521b9474" - }, - { - "name": "couch_and_lamp", - "unicode": "1F6CB", + "couch": { + "category": "objects", + "moji": "🛋", + "unicodeVersion": "7.0", "digest": "84cd734dbaa7f9f519438036d687e7a53217130779bc3de30258f163521b9474" }, - { - "name": "couple", - "unicode": "1F46B", + "couple": { + "category": "people", + "moji": "👫", + "unicodeVersion": "6.0", "digest": "c897ba76e24e2f43a4aa261c2754800a8473f43c7ce53f9909a6af2c4897732a" }, - { - "name": "couple_mm", - "unicode": "1F468-2764-1F468", + "couple_mm": { + "category": "people", + "moji": "👨❤️👨", + "unicodeVersion": "6.0", "digest": "c812471d35d46e12270653039a907d1dfa2dea0defd65596283e5b8e03cea803" }, - { - "name": "couple_with_heart_mm", - "unicode": "1F468-2764-1F468", - "digest": "c812471d35d46e12270653039a907d1dfa2dea0defd65596283e5b8e03cea803" - }, - { - "name": "couple_with_heart", - "unicode": "1F491", + "couple_with_heart": { + "category": "people", + "moji": "💑", + "unicodeVersion": "6.0", "digest": "420bfa81bad10365550c77a98e1c07eb00d03663fe7b610fab1aca8a0a9d201b" }, - { - "name": "couple_ww", - "unicode": "1F469-2764-1F469", - "digest": "7ac49153a612d63302299eee996308b7dcafa0a152473dab679215036fe6567e" - }, - { - "name": "couple_with_heart_ww", - "unicode": "1F469-2764-1F469", + "couple_ww": { + "category": "people", + "moji": "👩❤️👩", + "unicodeVersion": "6.0", "digest": "7ac49153a612d63302299eee996308b7dcafa0a152473dab679215036fe6567e" }, - { - "name": "couplekiss", - "unicode": "1F48F", + "couplekiss": { + "category": "people", + "moji": "💏", + "unicodeVersion": "6.0", "digest": "1acfef9d375c4c1deb235babd856b0f90ad4f3194751694cb6abb44f00f29e42" }, - { - "name": "cow", - "unicode": "1F42E", + "cow": { + "category": "nature", + "moji": "🐮", + "unicodeVersion": "6.0", "digest": "d71c854ff8b343ee24b8c2b9d56c7cb3fc6fa1a6dc0d7a137841b9f646e6d71b" }, - { - "name": "cow2", - "unicode": "1F404", + "cow2": { + "category": "nature", + "moji": "🐄", + "unicodeVersion": "6.0", "digest": "e7a5131d7dee0f3356814b0ac1ea8ff280b12a7b580181e20ddb0b7eeb7e7339" }, - { - "name": "cowboy", - "unicode": "1F920", + "cowboy": { + "category": "people", + "moji": "🤠", + "unicodeVersion": "9.0", "digest": "1aabf23f6b95a9b772fdb8eb45b8ec93584a5357f9131c6eabc9d1b83fe67e89" }, - { - "name": "face_with_cowboy_hat", - "unicode": "1F920", - "digest": "1aabf23f6b95a9b772fdb8eb45b8ec93584a5357f9131c6eabc9d1b83fe67e89" - }, - { - "name": "crab", - "unicode": "1F980", + "crab": { + "category": "nature", + "moji": "🦀", + "unicodeVersion": "8.0", "digest": "e6be16699fdb5d87f42f28f6cc141a44b7ffd834ecdd536813c4b5b86d3fc4a5" }, - { - "name": "crayon", - "unicode": "1F58D", - "digest": "b180d6afa4777861222a4228164ce284230fe90c589f52ffa9351bac777e901a" - }, - { - "name": "lower_left_crayon", - "unicode": "1F58D", + "crayon": { + "category": "objects", + "moji": "🖍", + "unicodeVersion": "7.0", "digest": "b180d6afa4777861222a4228164ce284230fe90c589f52ffa9351bac777e901a" }, - { - "name": "credit_card", - "unicode": "1F4B3", + "credit_card": { + "category": "objects", + "moji": "💳", + "unicodeVersion": "6.0", "digest": "808cd120fd3738eb2be1f6c6c029d98387b0e03fca7d1451e8fbf9c5ab3f643f" }, - { - "name": "crescent_moon", - "unicode": "1F319", + "crescent_moon": { + "category": "nature", + "moji": "🌙", + "unicodeVersion": "6.0", "digest": "042e7e01e6e88b97a763b7cc41e2a2b3fe68a649bacf4a090cd28fc653baf640" }, - { - "name": "cricket", - "unicode": "1F3CF", + "cricket": { + "category": "activity", + "moji": "🏏", + "unicodeVersion": "8.0", "digest": "4c4559d0b4efe24cc248fa57f413541307992e519d0cb9fb8828637ac2f4cc16" }, - { - "name": "cricket_bat_ball", - "unicode": "1F3CF", - "digest": "4c4559d0b4efe24cc248fa57f413541307992e519d0cb9fb8828637ac2f4cc16" - }, - { - "name": "crocodile", - "unicode": "1F40A", + "crocodile": { + "category": "nature", + "moji": "🐊", + "unicodeVersion": "6.0", "digest": "59cb4164c50b6bc9ae311ce6f7610467c1aaafa848b5fff7614f064715f91992" }, - { - "name": "croissant", - "unicode": "1F950", + "croissant": { + "category": "food", + "moji": "🥐", + "unicodeVersion": "9.0", "digest": "b751e287157a1e276617a841a5b5f7f1208ca226cfd8fa947f144390b65a5e16" }, - { - "name": "cross", - "unicode": "271D", - "digest": "a6b07c838fb75ef2ebefa2df6005e8d784753239ec03c37695a13e3b1954d653" - }, - { - "name": "latin_cross", - "unicode": "271D", + "cross": { + "category": "symbols", + "moji": "✝", + "unicodeVersion": "1.1", "digest": "a6b07c838fb75ef2ebefa2df6005e8d784753239ec03c37695a13e3b1954d653" }, - { - "name": "crossed_flags", - "unicode": "1F38C", + "crossed_flags": { + "category": "objects", + "moji": "🎌", + "unicodeVersion": "6.0", "digest": "2841c671075e6f1a79c61c2d716423159fb0bc0786e3fb0049697766533bf262" }, - { - "name": "crossed_swords", - "unicode": "2694", + "crossed_swords": { + "category": "objects", + "moji": "⚔", + "unicodeVersion": "4.1", "digest": "3771a5b26b514236521ce44e15f7730fa9148c6a782b9b600ab870a1f7de6f9f" }, - { - "name": "crown", - "unicode": "1F451", + "crown": { + "category": "people", + "moji": "👑", + "unicodeVersion": "6.0", "digest": "6741e58d8f823194e0a3484ac1563e20d9e0b44c1bc46d82444dfffa092cdfc7" }, - { - "name": "cruise_ship", - "unicode": "1F6F3", + "cruise_ship": { + "category": "travel", + "moji": "🛳", + "unicodeVersion": "7.0", "digest": "2b7b62db5d118a632673564099e3405ea6d61ea9b8e123b5a2aaf011bb2a54a4" }, - { - "name": "passenger_ship", - "unicode": "1F6F3", - "digest": "2b7b62db5d118a632673564099e3405ea6d61ea9b8e123b5a2aaf011bb2a54a4" - }, - { - "name": "cry", - "unicode": "1F622", + "cry": { + "category": "people", + "moji": "😢", + "unicodeVersion": "6.0", "digest": "fc3307ec4fe75539770c1123a0e8e721d9e021009a502655132f68d7cc453816" }, - { - "name": "crying_cat_face", - "unicode": "1F63F", + "crying_cat_face": { + "category": "people", + "moji": "😿", + "unicodeVersion": "6.0", "digest": "4942c24935c22babdcb8af41d2c0a7588356b6b674bc238902e2f10ad03e2c5b" }, - { - "name": "crystal_ball", - "unicode": "1F52E", + "crystal_ball": { + "category": "objects", + "moji": "🔮", + "unicodeVersion": "6.0", "digest": "05f73b30b1e5b0fc66fb5dc6caddd2d547ee7b9d2f97513dc908ba1a2e352e30" }, - { - "name": "cucumber", - "unicode": "1F952", + "cucumber": { + "category": "food", + "moji": "🥒", + "unicodeVersion": "9.0", "digest": "d1196e23f2f155ef5c1330f8497f40957a7357cb177127f457c5c471f0a23727" }, - { - "name": "cupid", - "unicode": "1F498", + "cupid": { + "category": "symbols", + "moji": "💘", + "unicodeVersion": "6.0", "digest": "246e71f44c6ebc2e4f887e25438e4f894e8cc92e06069e711b893ff391abb658" }, - { - "name": "curly_loop", - "unicode": "27B0", + "curly_loop": { + "category": "symbols", + "moji": "➰", + "unicodeVersion": "6.0", "digest": "9e4eb98d6597888f91208080c6a79824adb432ea34f46c85da26cb630bd1cc73" }, - { - "name": "currency_exchange", - "unicode": "1F4B1", + "currency_exchange": { + "category": "symbols", + "moji": "💱", + "unicodeVersion": "6.0", "digest": "b85377265b9876888969aa42b65bba0be523a370175baf226f20131e535af554" }, - { - "name": "curry", - "unicode": "1F35B", + "curry": { + "category": "food", + "moji": "🍛", + "unicodeVersion": "6.0", "digest": "a01c0a713662817720b485f7739f57e61afc025f5c43792f4de961c94f92f31e" }, - { - "name": "custard", - "unicode": "1F36E", + "custard": { + "category": "food", + "moji": "🍮", + "unicodeVersion": "6.0", "digest": "85c2b9ac904134a6c3587eb0a0806f2ab4282c5ed5c79d41734f3203998f757e" }, - { - "name": "customs", - "unicode": "1F6C3", + "customs": { + "category": "symbols", + "moji": "🛃", + "unicodeVersion": "6.0", "digest": "eb2546e1e617d4c1a1f614318af5e5dacf3e8d9479ffa08108977defa83ded32" }, - { - "name": "cyclone", - "unicode": "1F300", + "cyclone": { + "category": "symbols", + "moji": "🌀", + "unicodeVersion": "6.0", "digest": "7a0f8564d76adf2d0ed272f56dc0d01fb7b557852e0ca797e73f5472b8630bf3" }, - { - "name": "dagger", - "unicode": "1F5E1", + "dagger": { + "category": "objects", + "moji": "🗡", + "unicodeVersion": "7.0", "digest": "35a179168198d03295e626cc27d3b92d30a732c55a2ca75d7a11a0fbed414772" }, - { - "name": "dagger_knife", - "unicode": "1F5E1", - "digest": "35a179168198d03295e626cc27d3b92d30a732c55a2ca75d7a11a0fbed414772" - }, - { - "name": "dancer", - "unicode": "1F483", + "dancer": { + "category": "people", + "moji": "💃", + "unicodeVersion": "6.0", "digest": "66ffa86827e85acae4aa870c0859fe3a9dad03d21ff4bc800b61c95c902a8a90" }, - { - "name": "dancer_tone1", - "unicode": "1F483-1F3FB", + "dancer_tone1": { + "category": "people", + "moji": "💃🏻", + "unicodeVersion": "8.0", "digest": "bdbee740addc890e369d3469a3585eb0d1e4fbc7e04dd6f6aca762d8aeee6a8c" }, - { - "name": "dancer_tone2", - "unicode": "1F483-1F3FC", + "dancer_tone2": { + "category": "people", + "moji": "💃🏼", + "unicodeVersion": "8.0", "digest": "9f7b4c627241eaa2def9717a5286a423f0b9c1b044dd9ea4442a76f1858d14a4" }, - { - "name": "dancer_tone3", - "unicode": "1F483-1F3FD", + "dancer_tone3": { + "category": "people", + "moji": "💃🏽", + "unicodeVersion": "8.0", "digest": "a6bd49a377ce6c2004bf126b6f66d0b21d8c14103c2add7b10f12ed9e1c2d302" }, - { - "name": "dancer_tone4", - "unicode": "1F483-1F3FE", + "dancer_tone4": { + "category": "people", + "moji": "💃🏾", + "unicodeVersion": "8.0", "digest": "4ec2a7629c01b0e9006b5cda4deae3bf297ce3b71d18063f93eeb5c14be19a1a" }, - { - "name": "dancer_tone5", - "unicode": "1F483-1F3FF", + "dancer_tone5": { + "category": "people", + "moji": "💃🏿", + "unicodeVersion": "8.0", "digest": "2b48e3a6b366c6f55f73b816e6fb03c39e9890f586f7e9c9043cf0c013d9cdd5" }, - { - "name": "dancers", - "unicode": "1F46F", + "dancers": { + "category": "people", + "moji": "👯", + "unicodeVersion": "6.0", "digest": "12be66ed19d232bb387270f40bece68bd0cb2342b318f6c9bb8b49c64ff7d0ad" }, - { - "name": "dango", - "unicode": "1F361", + "dango": { + "category": "food", + "moji": "🍡", + "unicodeVersion": "6.0", "digest": "34e8cd153c50f2d725abe8934c35c96a3ab533f0cc5fbb1e1474eafad1dc1fc2" }, - { - "name": "dark_sunglasses", - "unicode": "1F576", + "dark_sunglasses": { + "category": "people", + "moji": "🕶", + "unicodeVersion": "7.0", "digest": "d0a735ad5bf0ece00af2a21abf950b89292ebd8ca6e28b1dbb1368252fb44afe" }, - { - "name": "dart", - "unicode": "1F3AF", + "dart": { + "category": "activity", + "moji": "🎯", + "unicodeVersion": "6.0", "digest": "998642f06a875905e0a6bf30963c025baff1cf55b8e76884b9119f2d71188b0c" }, - { - "name": "dash", - "unicode": "1F4A8", + "dash": { + "category": "nature", + "moji": "💨", + "unicodeVersion": "6.0", "digest": "f7aae7d3887c67d76f3329c2dc9e6807dc580a4b07ab35599c7805e41823a345" }, - { - "name": "date", - "unicode": "1F4C5", + "date": { + "category": "objects", + "moji": "📅", + "unicodeVersion": "6.0", "digest": "d0b695e4a7cfbbe71b4fbebf345b66ca98f0cf1c751362928e54c23ca78d4c7b" }, - { - "name": "deciduous_tree", - "unicode": "1F333", + "deciduous_tree": { + "category": "nature", + "moji": "🌳", + "unicodeVersion": "6.0", "digest": "3c70f1a77f2754f41c830e88d43b7d53c14311d64626ded164aa9ac7d2695790" }, - { - "name": "deer", - "unicode": "1F98C", + "deer": { + "category": "nature", + "moji": "🦌", + "unicodeVersion": "9.0", "digest": "7f4302ca68fd121ee73be48d0a0a0fb9e7e2741071a491ad2b7b0eab9f11ad25" }, - { - "name": "department_store", - "unicode": "1F3EC", + "department_store": { + "category": "travel", + "moji": "🏬", + "unicodeVersion": "6.0", "digest": "4be910d2efe74d8ce2c1f41d7753c8873579faca83fcf779a4887d8ab9e5923b" }, - { - "name": "desert", - "unicode": "1F3DC", + "desert": { + "category": "travel", + "moji": "🏜", + "unicodeVersion": "7.0", "digest": "d4b1a11c5130debe042df6cc2b3389f15c68a5cb32dc1b3a82b78f733d0c9e4e" }, - { - "name": "desktop", - "unicode": "1F5A5", + "desktop": { + "category": "objects", + "moji": "🖥", + "unicodeVersion": "7.0", "digest": "cde5bfb6c71bb7d663808a3561b24cb5b5560f95f510b40f81250cac1b21933e" }, - { - "name": "desktop_computer", - "unicode": "1F5A5", - "digest": "cde5bfb6c71bb7d663808a3561b24cb5b5560f95f510b40f81250cac1b21933e" - }, - { - "name": "diamond_shape_with_a_dot_inside", - "unicode": "1F4A0", + "diamond_shape_with_a_dot_inside": { + "category": "symbols", + "moji": "💠", + "unicodeVersion": "6.0", "digest": "e91323577ab89e95b0fa0b9272ea0c797b76908f24d36992630e9325273a4ce3" }, - { - "name": "diamonds", - "unicode": "2666", + "diamonds": { + "category": "symbols", + "moji": "♦", + "unicodeVersion": "1.1", "digest": "bf3d9a020afe8aa226db73590bc193a9c2c3e6e642edd2445c5960c3e67cf153" }, - { - "name": "disappointed", - "unicode": "1F61E", + "disappointed": { + "category": "people", + "moji": "😞", + "unicodeVersion": "6.0", "digest": "c0f406c6beea0fd1328adefc097d04aa16b72f7a5afa0867967d8ea25d72db17" }, - { - "name": "disappointed_relieved", - "unicode": "1F625", + "disappointed_relieved": { + "category": "people", + "moji": "😥", + "unicodeVersion": "6.0", "digest": "c826f5dd4f2f7e5289d720851d4826ab8284d915606c1b152ab229b7fadbba14" }, - { - "name": "dividers", - "unicode": "1F5C2", - "digest": "4b2c653b18cf0fa31f1f0ac94a6fbd214ea0d1b0a90a450ab6e169906fc5764f" - }, - { - "name": "card_index_dividers", - "unicode": "1F5C2", + "dividers": { + "category": "objects", + "moji": "🗂", + "unicodeVersion": "7.0", "digest": "4b2c653b18cf0fa31f1f0ac94a6fbd214ea0d1b0a90a450ab6e169906fc5764f" }, - { - "name": "dizzy", - "unicode": "1F4AB", + "dizzy": { + "category": "nature", + "moji": "💫", + "unicodeVersion": "6.0", "digest": "d577545c2de42389695447c6ebbfef895f30f0fda84eef45684f9bf4a9c27ff1" }, - { - "name": "dizzy_face", - "unicode": "1F635", + "dizzy_face": { + "category": "people", + "moji": "😵", + "unicodeVersion": "6.0", "digest": "7b3aeaffb4e15ccf633b91dda4a44847a1eb28d78ce58b4d171b20a771bde414" }, - { - "name": "do_not_litter", - "unicode": "1F6AF", + "do_not_litter": { + "category": "symbols", + "moji": "🚯", + "unicodeVersion": "6.0", "digest": "98b07fbbcdb438d1b8a755869fa2de8e180a77fce359ec830eb46d38ec3e67cb" }, - { - "name": "dog", - "unicode": "1F436", + "dog": { + "category": "nature", + "moji": "🐶", + "unicodeVersion": "6.0", "digest": "3b31ce067b13e463284ce85536512cb1f8cd8b52fe73659f69971d0d6c1dfc11" }, - { - "name": "dog2", - "unicode": "1F415", + "dog2": { + "category": "nature", + "moji": "🐕", + "unicodeVersion": "6.0", "digest": "0a8901bce5ed994533ff84299b2a1364de28d872c9f9510d3426a83e8a9d2e34" }, - { - "name": "dollar", - "unicode": "1F4B5", + "dollar": { + "category": "objects", + "moji": "💵", + "unicodeVersion": "6.0", "digest": "52438e38867aedc021740bb41f9ba336e75a50faa148419412a01d75d8c93155" }, - { - "name": "dolls", - "unicode": "1F38E", + "dolls": { + "category": "objects", + "moji": "🎎", + "unicodeVersion": "6.0", "digest": "a687184e9a0915deef44bb3cacfb19d3f3f19cf2c110f1da90191dd567333c57" }, - { - "name": "dolphin", - "unicode": "1F42C", + "dolphin": { + "category": "nature", + "moji": "🐬", + "unicodeVersion": "6.0", "digest": "0b7ee08f4236232ca533ed3a3023d28020d36f178efaec5ce8b0e13a84778512" }, - { - "name": "door", - "unicode": "1F6AA", + "door": { + "category": "objects", + "moji": "🚪", + "unicodeVersion": "6.0", "digest": "984a9ca88852ebdb539e0c385d9c6ffe5010e9189bc372a3d00f5c8d44c8e6f5" }, - { - "name": "doughnut", - "unicode": "1F369", + "doughnut": { + "category": "food", + "moji": "🍩", + "unicodeVersion": "6.0", "digest": "27634587e6a53807baa32157bb06b0e115c8ad8aefebba7ebb0b65a084170e3a" }, - { - "name": "dove", - "unicode": "1F54A", + "dove": { + "category": "nature", + "moji": "🕊", + "unicodeVersion": "7.0", "digest": "7c665f8594ffa53e72b01647e9d27360fb87d52d02fe9f20fc5fda08f9797dc3" }, - { - "name": "dove_of_peace", - "unicode": "1F54A", - "digest": "7c665f8594ffa53e72b01647e9d27360fb87d52d02fe9f20fc5fda08f9797dc3" - }, - { - "name": "dragon", - "unicode": "1F409", + "dragon": { + "category": "nature", + "moji": "🐉", + "unicodeVersion": "6.0", "digest": "2abcb3d945d848e34ffc76203b29ef26df7458856166fffd155611f7bbe72652" }, - { - "name": "dragon_face", - "unicode": "1F432", + "dragon_face": { + "category": "nature", + "moji": "🐲", + "unicodeVersion": "6.0", "digest": "0030548931b931e3b51f26cf660394aee36499e688ba83ce9cfccb635dcd4d54" }, - { - "name": "dress", - "unicode": "1F457", + "dress": { + "category": "people", + "moji": "👗", + "unicodeVersion": "6.0", "digest": "96ceba928fb356f7c0ae99bf22552321f08a65d5f1c0340ab89641219ad366ad" }, - { - "name": "dromedary_camel", - "unicode": "1F42A", + "dromedary_camel": { + "category": "nature", + "moji": "🐪", + "unicodeVersion": "6.0", "digest": "e06ef69c29f0fb12481727c0b4124e700572d3d7955e173279320f43f286518d" }, - { - "name": "drooling_face", - "unicode": "1F924", - "digest": "5203cb05cd266d7a7c929ab40364ad68571d380d9c7ff93a8d6d55261abaa1ba" - }, - { - "name": "drool", - "unicode": "1F924", + "drooling_face": { + "category": "people", + "moji": "🤤", + "unicodeVersion": "9.0", "digest": "5203cb05cd266d7a7c929ab40364ad68571d380d9c7ff93a8d6d55261abaa1ba" }, - { - "name": "droplet", - "unicode": "1F4A7", + "droplet": { + "category": "nature", + "moji": "💧", + "unicodeVersion": "6.0", "digest": "6475b4a4460a672c436a68f282ac97fb31e2934db4b80620063ee816159aa7c3" }, - { - "name": "drum", - "unicode": "1F941", - "digest": "0d0639980b1a5dcbf1c3e7ef47263fb6543b871242c58452a8c2f642525d9dd8" - }, - { - "name": "drum_with_drumsticks", - "unicode": "1F941", + "drum": { + "category": "activity", + "moji": "🥁", + "unicodeVersion": "9.0", "digest": "0d0639980b1a5dcbf1c3e7ef47263fb6543b871242c58452a8c2f642525d9dd8" }, - { - "name": "duck", - "unicode": "1F986", + "duck": { + "category": "nature", + "moji": "🦆", + "unicodeVersion": "9.0", "digest": "8f8373798a7727368b32328e7a9a349727a949e7391ddd243b6456141a4f7e94" }, - { - "name": "dvd", - "unicode": "1F4C0", + "dvd": { + "category": "objects", + "moji": "📀", + "unicodeVersion": "6.0", "digest": "3b7903285d91277181c26fdc9df857761bbac509d352e320c2519ea3b132704f" }, - { - "name": "e-mail", - "unicode": "1F4E7", - "digest": "39b5a57a2376e4a1137e381be02a1775bd580e0371438f5297a401ea634f1830" - }, - { - "name": "email", - "unicode": "1F4E7", + "e-mail": { + "category": "objects", + "moji": "📧", + "unicodeVersion": "6.0", "digest": "39b5a57a2376e4a1137e381be02a1775bd580e0371438f5297a401ea634f1830" }, - { - "name": "eagle", - "unicode": "1F985", + "eagle": { + "category": "nature", + "moji": "🦅", + "unicodeVersion": "9.0", "digest": "b44fd4f61b83c5114358a272343ac9b0eabbc70847f739bbdbf8aae3ade5bc1d" }, - { - "name": "ear", - "unicode": "1F442", + "ear": { + "category": "people", + "moji": "👂", + "unicodeVersion": "6.0", "digest": "4fdeb5a46e69311ecfd09c5b45c9018c24b625e28475cca8fa516b086ef952f8" }, - { - "name": "ear_of_rice", - "unicode": "1F33E", + "ear_of_rice": { + "category": "nature", + "moji": "🌾", + "unicodeVersion": "6.0", "digest": "2997c340c2b333d6ba9b73f94ff1a1881735fe0cc4f0c72d7719b305499fc425" }, - { - "name": "ear_tone1", - "unicode": "1F442-1F3FB", + "ear_tone1": { + "category": "people", + "moji": "👂🏻", + "unicodeVersion": "8.0", "digest": "5ca759b8569a377a4e63e30d94b585b9f76d15348a8a0c1ba19fdc522790615e" }, - { - "name": "ear_tone2", - "unicode": "1F442-1F3FC", + "ear_tone2": { + "category": "people", + "moji": "👂🏼", + "unicodeVersion": "8.0", "digest": "12aafb3ef2cfcdc892b2877c2e24920620f0f77f850e12afbfe55eadce9e37df" }, - { - "name": "ear_tone3", - "unicode": "1F442-1F3FD", + "ear_tone3": { + "category": "people", + "moji": "👂🏽", + "unicodeVersion": "8.0", "digest": "f4d28d9f72cf116ac92d80061eb84c918d6523bf53b2ad526f5457aba487d527" }, - { - "name": "ear_tone4", - "unicode": "1F442-1F3FE", + "ear_tone4": { + "category": "people", + "moji": "👂🏾", + "unicodeVersion": "8.0", "digest": "eaa9453670f7e3adc6ec6934ee70efc9bf60fe6c99c5804b7ba9e3804aec65de" }, - { - "name": "ear_tone5", - "unicode": "1F442-1F3FF", + "ear_tone5": { + "category": "people", + "moji": "👂🏿", + "unicodeVersion": "8.0", "digest": "54bd0782419489556b80e9e0d15b05df74757aa4e04ba565f45c20d3dd60e3f1" }, - { - "name": "earth_africa", - "unicode": "1F30D", + "earth_africa": { + "category": "nature", + "moji": "🌍", + "unicodeVersion": "6.0", "digest": "c691a6f591f5a07b268fd64efe113e81cec8d5963ad83ced2537422343ff7ecf" }, - { - "name": "earth_americas", - "unicode": "1F30E", + "earth_americas": { + "category": "nature", + "moji": "🌎", + "unicodeVersion": "6.0", "digest": "a9c60cf8341ff59a9cc1a715b7144af734fcd28915a8e003a31ebf2abf9aedb1" }, - { - "name": "earth_asia", - "unicode": "1F30F", + "earth_asia": { + "category": "nature", + "moji": "🌏", + "unicodeVersion": "6.0", "digest": "ee2beb61fb8c87279161c5a8c4ad17bb71ce790123f8fa33522941d027e060a5" }, - { - "name": "egg", - "unicode": "1F95A", + "egg": { + "category": "food", + "moji": "🥚", + "unicodeVersion": "9.0", "digest": "72b9c841af784e7cbccbbe48ba833df5cecdd284397c199cab079872e879d92f" }, - { - "name": "eggplant", - "unicode": "1F346", + "eggplant": { + "category": "food", + "moji": "🍆", + "unicodeVersion": "6.0", "digest": "ec0a460e0cf0e615f51279677594a899672e1b4ecd9396e17a8cfa2a3efe5238" }, - { - "name": "eight", - "unicode": "0038-20E3", + "eight": { + "category": "symbols", + "moji": "8️⃣", + "unicodeVersion": "3.0", "digest": "57ff905033a32747690adba6486d12b09eb4d45de556f4e1ab6fb04e1fb861a8" }, - { - "name": "eight_pointed_black_star", - "unicode": "2734", + "eight_pointed_black_star": { + "category": "symbols", + "moji": "✴", + "unicodeVersion": "1.1", "digest": "7bf11f6e28591e3d0625296aaabf4ecb75c982e425abf3049339e93494acc17e" }, - { - "name": "eight_spoked_asterisk", - "unicode": "2733", + "eight_spoked_asterisk": { + "category": "symbols", + "moji": "✳", + "unicodeVersion": "1.1", "digest": "bb0758e7cc0e357285937671a91489bd32ce9d248eecdcc9c275a53a66325b26" }, - { - "name": "eject", - "unicode": "23CF", + "eject": { + "category": "symbols", + "moji": "⏏", + "unicodeVersion": "4.0", "digest": "eeb0cd23ead0c965e307de517a6805265f0c780c3e454e64bc4c1425dfe7548e" }, - { - "name": "eject_symbol", - "unicode": "23CF", - "digest": "eeb0cd23ead0c965e307de517a6805265f0c780c3e454e64bc4c1425dfe7548e" - }, - { - "name": "electric_plug", - "unicode": "1F50C", + "electric_plug": { + "category": "objects", + "moji": "🔌", + "unicodeVersion": "6.0", "digest": "b10ce87af86fa4f4022572ceb5ecd73bea867347a86832a7ea248364b0aad8d0" }, - { - "name": "elephant", - "unicode": "1F418", + "elephant": { + "category": "nature", + "moji": "🐘", + "unicodeVersion": "6.0", "digest": "b7750f4b013fbd28ac5330e1694ef4d3b4a9c6fc7b807879db0c24b035a16c29" }, - { - "name": "end", - "unicode": "1F51A", + "end": { + "category": "symbols", + "moji": "🔚", + "unicodeVersion": "6.0", "digest": "dd93aee6986eb637a8b58f234da47568b88525599f73246e322af030351997a2" }, - { - "name": "envelope", - "unicode": "2709", + "envelope": { + "category": "objects", + "moji": "✉", + "unicodeVersion": "1.1", "digest": "f5a512022a2f5280f372ff39c22cbda815f698710ca66f8f8c4d08418f98ca78" }, - { - "name": "envelope_with_arrow", - "unicode": "1F4E9", + "envelope_with_arrow": { + "category": "objects", + "moji": "📩", + "unicodeVersion": "6.0", "digest": "f8643212e6a94f58ccf2bcedc54c5fda8ebeab274f4a8803f253de5f50ddb1d6" }, - { - "name": "euro", - "unicode": "1F4B6", + "euro": { + "category": "objects", + "moji": "💶", + "unicodeVersion": "6.0", "digest": "3af3e223e8f26468a94f6f5c17198432656e8d20b3bab31566c2b5a86e717df4" }, - { - "name": "european_castle", - "unicode": "1F3F0", + "european_castle": { + "category": "travel", + "moji": "🏰", + "unicodeVersion": "6.0", "digest": "21082d0be7e3b2794e59ff0170da0cfe42a9b734cf02704603e3b52ff48202ba" }, - { - "name": "european_post_office", - "unicode": "1F3E4", + "european_post_office": { + "category": "travel", + "moji": "🏤", + "unicodeVersion": "6.0", "digest": "02b4c7602939f0cb9cb2b4e05996bcdb6bd93cf8025c2ea02db8cbe13ca397d0" }, - { - "name": "evergreen_tree", - "unicode": "1F332", + "evergreen_tree": { + "category": "nature", + "moji": "🌲", + "unicodeVersion": "6.0", "digest": "74b226098e66c0a94a92e0f22b9d631736e12dca72c34182c9d0ba56aa593172" }, - { - "name": "exclamation", - "unicode": "2757", + "exclamation": { + "category": "symbols", + "moji": "❗", + "unicodeVersion": "5.2", "digest": "45b87ae4593656d7da49ff5645fb6a2a18d582553295358da9f09f1ae8272445" }, - { - "name": "expressionless", - "unicode": "1F611", + "expressionless": { + "category": "people", + "moji": "😑", + "unicodeVersion": "6.1", "digest": "34e2a1c8121f4f0bc4ce33d226d8cc1a4ebf5260746df2b23e29eef24ee9372e" }, - { - "name": "eye", - "unicode": "1F441", + "eye": { + "category": "people", + "moji": "👁", + "unicodeVersion": "7.0", "digest": "79ecff79c2edee630e72725b54e67ee2e96d24ca03fef2954a56a09c0a2227f8" }, - { - "name": "eye_in_speech_bubble", - "unicode": "1F441-1F5E8", + "eye_in_speech_bubble": { + "category": "symbols", + "moji": "👁🗨", + "unicodeVersion": "7.0", "digest": "c0050c026c2a3060723cab2df2603c1c7da7ed81faedb9ebe16cd89721928a55" }, - { - "name": "eyeglasses", - "unicode": "1F453", + "eyeglasses": { + "category": "people", + "moji": "👓", + "unicodeVersion": "6.0", "digest": "d4a9585d6c43ef514a97c45c64607162e775a45544821f1470c6f8f25b93ab81" }, - { - "name": "eyes", - "unicode": "1F440", + "eyes": { + "category": "people", + "moji": "👀", + "unicodeVersion": "6.0", "digest": "1d5cae0b9b2e51e1de54295685d7f0c72ee794e2e6335a95b1d056c7e77260e8" }, - { - "name": "face_palm", - "unicode": "1F926", + "face_palm": { + "category": "people", + "moji": "🤦", + "unicodeVersion": "9.0", "digest": "4ec873048b34b1bb34430724cf28e4bee6c0a9eee88ce39b9d1565047dc92420" }, - { - "name": "face_palm_tone1", - "unicode": "1F926-1F3FB", + "face_palm_tone1": { + "category": "people", + "moji": "🤦🏻", + "unicodeVersion": "9.0", "digest": "e93ef92b4c01dbea6c400e708e23dd36da92ccfbf5eb4f177b3b20c3a46bdc19" }, - { - "name": "face_palm_tone2", - "unicode": "1F926-1F3FC", + "face_palm_tone2": { + "category": "people", + "moji": "🤦🏼", + "unicodeVersion": "9.0", "digest": "22c8bf9fd9fa2ed9dca7a6397ed00ba6cfe9aeef2b0fb7b516ee4dda0df050ea" }, - { - "name": "face_palm_tone3", - "unicode": "1F926-1F3FD", + "face_palm_tone3": { + "category": "people", + "moji": "🤦🏽", + "unicodeVersion": "9.0", "digest": "c0b8bb9d2423e6787b6bdf1ca5a13f52853e4f48a9a1af0f2d4af1364fff022e" }, - { - "name": "face_palm_tone4", - "unicode": "1F926-1F3FE", + "face_palm_tone4": { + "category": "people", + "moji": "🤦🏾", + "unicodeVersion": "9.0", "digest": "f522ab186adcbb4549ea2c03500cdd7a86add548e43ebf7a54d58cc24deea072" }, - { - "name": "face_palm_tone5", - "unicode": "1F926-1F3FF", + "face_palm_tone5": { + "category": "people", + "moji": "🤦🏿", + "unicodeVersion": "9.0", "digest": "363507ae7178b5ec583635f47bcab10c897346f48b85d8759b1004c32cd8ad65" }, - { - "name": "factory", - "unicode": "1F3ED", + "factory": { + "category": "travel", + "moji": "🏭", + "unicodeVersion": "6.0", "digest": "c7aeb61ed8b0ac5c91d5197c73f1e2bb801921c22a76bb82c7659d990680dcb0" }, - { - "name": "fallen_leaf", - "unicode": "1F342", + "fallen_leaf": { + "category": "nature", + "moji": "🍂", + "unicodeVersion": "6.0", "digest": "81fce04231d48db0e55f3697f930e9a7e3306bed5e35f1234e98c40a24ac5626" }, - { - "name": "family", - "unicode": "1F46A", + "family": { + "category": "people", + "moji": "👪", + "unicodeVersion": "6.0", "digest": "06f2ce63768ffe43b3d9b2a9660b34d043f37b3c91610dd62343ba21df8ecbe5" }, - { - "name": "family_mmb", - "unicode": "1F468-1F468-1F466", + "family_mmb": { + "category": "people", + "moji": "👨👨👦", + "unicodeVersion": "6.0", "digest": "41a18405be796699a7eb7c36ab6f7d898e322749997f45387377acf5bb16a50f" }, - { - "name": "family_mmbb", - "unicode": "1F468-1F468-1F466-1F466", + "family_mmbb": { + "category": "people", + "moji": "👨👨👦👦", + "unicodeVersion": "6.0", "digest": "87255d1d18c6971c8c083c818e598424c1bd717eed892478b7e9516639dbfb45" }, - { - "name": "family_mmg", - "unicode": "1F468-1F468-1F467", + "family_mmg": { + "category": "people", + "moji": "👨👨👧", + "unicodeVersion": "6.0", "digest": "a132b1b8f10b318d8e23aee15dab4caa14528aeb3c89966d4bcc25fb54af72ad" }, - { - "name": "family_mmgb", - "unicode": "1F468-1F468-1F467-1F466", + "family_mmgb": { + "category": "people", + "moji": "👨👨👧👦", + "unicodeVersion": "6.0", "digest": "eb2bc1966df406aaf38ce5a58db9324162799cdacf31f74f40e6384807a8efc2" }, - { - "name": "family_mmgg", - "unicode": "1F468-1F468-1F467-1F467", + "family_mmgg": { + "category": "people", + "moji": "👨👨👧👧", + "unicodeVersion": "6.0", "digest": "24f3d60f98fbd6b687f7cacfb629390b90509a754036e5439ae5294759c0606b" }, - { - "name": "family_mwbb", - "unicode": "1F468-1F469-1F466-1F466", + "family_mwbb": { + "category": "people", + "moji": "👨👩👦👦", + "unicodeVersion": "6.0", "digest": "2f77692bcb9275c4df501b64a18401dcaf8c68b21f26fbdad59b1feab0c98fd1" }, - { - "name": "family_mwg", - "unicode": "1F468-1F469-1F467", + "family_mwg": { + "category": "people", + "moji": "👨👩👧", + "unicodeVersion": "6.0", "digest": "1a976d13127665d9386cebfdb24e5572dc499bda484c0ee05585886edc616130" }, - { - "name": "family_mwgb", - "unicode": "1F468-1F469-1F467-1F466", + "family_mwgb": { + "category": "people", + "moji": "👨👩👧👦", + "unicodeVersion": "6.0", "digest": "960ec2cbac13ef208e73644cd36711b83e6c070c36950f834f3669812839b7f8" }, - { - "name": "family_mwgg", - "unicode": "1F468-1F469-1F467-1F467", + "family_mwgg": { + "category": "people", + "moji": "👨👩👧👧", + "unicodeVersion": "6.0", "digest": "8353b03dfa5c24aba75a0abdfdac01603f593819d54b4c7f2f88aafb31da0c6a" }, - { - "name": "family_wwb", - "unicode": "1F469-1F469-1F466", + "family_wwb": { + "category": "people", + "moji": "👩👩👦", + "unicodeVersion": "6.0", "digest": "07a5dd397718c553573689f6512f386729c13a12d5dc78be47c06405769cd98a" }, - { - "name": "family_wwbb", - "unicode": "1F469-1F469-1F466-1F466", + "family_wwbb": { + "category": "people", + "moji": "👩👩👦👦", + "unicodeVersion": "6.0", "digest": "b627f460f1da0d47b0b662402940b2b77c9538d380d05436dfca4b456c50c939" }, - { - "name": "family_wwg", - "unicode": "1F469-1F469-1F467", + "family_wwg": { + "category": "people", + "moji": "👩👩👧", + "unicodeVersion": "6.0", "digest": "2d6f373bed53f1028f0fbe9caf036465a351f37b9e00fca7d722cc5a1984f251" }, - { - "name": "family_wwgb", - "unicode": "1F469-1F469-1F467-1F466", + "family_wwgb": { + "category": "people", + "moji": "👩👩👧👦", + "unicodeVersion": "6.0", "digest": "72be5c85e1621f73d6794edd6e428febdb366b9e4c816f7829897fd1ab34642b" }, - { - "name": "family_wwgg", - "unicode": "1F469-1F469-1F467-1F467", + "family_wwgg": { + "category": "people", + "moji": "👩👩👧👧", + "unicodeVersion": "6.0", "digest": "c39e0916069460d2d9741bddf58e76f5d6a09254cba0eeb262345adf8630bc32" }, - { - "name": "fast_forward", - "unicode": "23E9", + "fast_forward": { + "category": "symbols", + "moji": "⏩", + "unicodeVersion": "6.0", "digest": "e7d2d8085cfd406c2b096e8dd147dd3722290a5727b1f7df185989526a2335ec" }, - { - "name": "fax", - "unicode": "1F4E0", + "fax": { + "category": "objects", + "moji": "📠", + "unicodeVersion": "6.0", "digest": "ff85ffa440c5379c9b138ebe2d7912d6098da3b37a051b80442d5557b7f993b0" }, - { - "name": "fearful", - "unicode": "1F628", + "fearful": { + "category": "people", + "moji": "😨", + "unicodeVersion": "6.0", "digest": "b72bdf7d075d5c4e38bbd8512fb45fda2e85c9c8732a47e67575ae9f2ed4c5df" }, - { - "name": "feet", - "unicode": "1F43E", + "feet": { + "category": "nature", + "moji": "🐾", + "unicodeVersion": "6.0", "digest": "45aca538d3a9831a0c7de491e5656c17705c07b8f4ac8e85254656b608976016" }, - { - "name": "fencer", - "unicode": "1F93A", - "digest": "5db00fa456af9f6c7cb88d300579dd63e426bcb97ad25486b664aff25c688e21" - }, - { - "name": "fencing", - "unicode": "1F93A", + "fencer": { + "category": "activity", + "moji": "🤺", + "unicodeVersion": "9.0", "digest": "5db00fa456af9f6c7cb88d300579dd63e426bcb97ad25486b664aff25c688e21" }, - { - "name": "ferris_wheel", - "unicode": "1F3A1", + "ferris_wheel": { + "category": "travel", + "moji": "🎡", + "unicodeVersion": "6.0", "digest": "24b4551b7b79a2a5fd73de61542f2b444f896a52030c5f29791c8fcfcc28b95c" }, - { - "name": "ferry", - "unicode": "26F4", + "ferry": { + "category": "travel", + "moji": "⛴", + "unicodeVersion": "5.2", "digest": "5002a72af2e3c4cef9a36ad5987aeed7d99f96bfd13e56f78957315ec7e749a3" }, - { - "name": "field_hockey", - "unicode": "1F3D1", + "field_hockey": { + "category": "activity", + "moji": "🏑", + "unicodeVersion": "8.0", "digest": "4ee091d96161ba719ab8fd6f2b03f96d902a6f22cffe0563b930618bb8ac2b67" }, - { - "name": "file_cabinet", - "unicode": "1F5C4", + "file_cabinet": { + "category": "objects", + "moji": "🗄", + "unicodeVersion": "7.0", "digest": "92914147bf93e6d64271ff99d217a18a9850a367d08a5f9f458ecf9311a5bbe9" }, - { - "name": "file_folder", - "unicode": "1F4C1", + "file_folder": { + "category": "objects", + "moji": "📁", + "unicodeVersion": "6.0", "digest": "62a42a929267cfbfdb795ead381c9657c343458bc5fca95ea8a0ab892c61d4f6" }, - { - "name": "film_frames", - "unicode": "1F39E", + "film_frames": { + "category": "objects", + "moji": "🎞", + "unicodeVersion": "7.0", "digest": "4da212148cadb9c4ea91e60d2d8316e38cea99ef4f14afc023711dd7c54ade5a" }, - { - "name": "fingers_crossed", - "unicode": "1F91E", - "digest": "a5c797ead191b9712e185083266b455cdf09f6a34c10f8c51aa145e6073427e1" - }, - { - "name": "hand_with_index_and_middle_finger_crossed", - "unicode": "1F91E", + "fingers_crossed": { + "category": "people", + "moji": "🤞", + "unicodeVersion": "9.0", "digest": "a5c797ead191b9712e185083266b455cdf09f6a34c10f8c51aa145e6073427e1" }, - { - "name": "fingers_crossed_tone1", - "unicode": "1F91E-1F3FB", - "digest": "db56d47bf887f2d8459a3aaba23f15c0087234ae5a54125052e7046e034a4988" - }, - { - "name": "hand_with_index_and_middle_fingers_crossed_tone1", - "unicode": "1F91E-1F3FB", + "fingers_crossed_tone1": { + "category": "people", + "moji": "🤞🏻", + "unicodeVersion": "9.0", "digest": "db56d47bf887f2d8459a3aaba23f15c0087234ae5a54125052e7046e034a4988" }, - { - "name": "fingers_crossed_tone2", - "unicode": "1F91E-1F3FC", - "digest": "19f1bcca3991db7ed2037278c0baab6cd7f12aeaf2e0074de402c4d9e45c1899" - }, - { - "name": "hand_with_index_and_middle_fingers_crossed_tone2", - "unicode": "1F91E-1F3FC", + "fingers_crossed_tone2": { + "category": "people", + "moji": "🤞🏼", + "unicodeVersion": "9.0", "digest": "19f1bcca3991db7ed2037278c0baab6cd7f12aeaf2e0074de402c4d9e45c1899" }, - { - "name": "fingers_crossed_tone3", - "unicode": "1F91E-1F3FD", - "digest": "895a3314f6a310f31f7e728bcca20ff834fbfac62ce00e27e3ea5ad0dfc1ba35" - }, - { - "name": "hand_with_index_and_middle_fingers_crossed_tone3", - "unicode": "1F91E-1F3FD", + "fingers_crossed_tone3": { + "category": "people", + "moji": "🤞🏽", + "unicodeVersion": "9.0", "digest": "895a3314f6a310f31f7e728bcca20ff834fbfac62ce00e27e3ea5ad0dfc1ba35" }, - { - "name": "fingers_crossed_tone4", - "unicode": "1F91E-1F3FE", - "digest": "fcb5c4de2001d23a5df1b8702624d134b7f94e93e2dcc8adf6c1033c77722b0e" - }, - { - "name": "hand_with_index_and_middle_fingers_crossed_tone4", - "unicode": "1F91E-1F3FE", + "fingers_crossed_tone4": { + "category": "people", + "moji": "🤞🏾", + "unicodeVersion": "9.0", "digest": "fcb5c4de2001d23a5df1b8702624d134b7f94e93e2dcc8adf6c1033c77722b0e" }, - { - "name": "fingers_crossed_tone5", - "unicode": "1F91E-1F3FF", - "digest": "50132c78d530b048c21be4e788b446872a79b3b3a91009db12f4021c44c8469d" - }, - { - "name": "hand_with_index_and_middle_fingers_crossed_tone5", - "unicode": "1F91E-1F3FF", + "fingers_crossed_tone5": { + "category": "people", + "moji": "🤞🏿", + "unicodeVersion": "9.0", "digest": "50132c78d530b048c21be4e788b446872a79b3b3a91009db12f4021c44c8469d" }, - { - "name": "fire", - "unicode": "1F525", + "fire": { + "category": "nature", + "moji": "🔥", + "unicodeVersion": "6.0", "digest": "b3e67c913903d900f5e50e7e7e4d7e9370bb6ceedfbee548be39e4c9e4b69416" }, - { - "name": "flame", - "unicode": "1F525", - "digest": "b3e67c913903d900f5e50e7e7e4d7e9370bb6ceedfbee548be39e4c9e4b69416" - }, - { - "name": "fire_engine", - "unicode": "1F692", + "fire_engine": { + "category": "travel", + "moji": "🚒", + "unicodeVersion": "6.0", "digest": "c3a518f27d625e3b62dffa227eb82764bf0a147f10ec0e7f4f43f3f96751af20" }, - { - "name": "fireworks", - "unicode": "1F386", + "fireworks": { + "category": "travel", + "moji": "🎆", + "unicodeVersion": "6.0", "digest": "b62ae08a00c0cc6eba8f9666c8fd9946ce57c3cfc01fe99542a8690a4a566a65" }, - { - "name": "first_place", - "unicode": "1F947", - "digest": "e3de5d9f14f05544dbee5965cc2baa20e7b417a488c8a18598979038860fd901" - }, - { - "name": "first_place_medal", - "unicode": "1F947", + "first_place": { + "category": "activity", + "moji": "🥇", + "unicodeVersion": "9.0", "digest": "e3de5d9f14f05544dbee5965cc2baa20e7b417a488c8a18598979038860fd901" }, - { - "name": "first_quarter_moon", - "unicode": "1F313", + "first_quarter_moon": { + "category": "nature", + "moji": "🌓", + "unicodeVersion": "6.0", "digest": "a207ce93084448622a4a5c49c85c566a9fda6be7337c86a013eeb713fe47fd29" }, - { - "name": "first_quarter_moon_with_face", - "unicode": "1F31B", + "first_quarter_moon_with_face": { + "category": "nature", + "moji": "🌛", + "unicodeVersion": "6.0", "digest": "1d1f54a5075f2311bcc017c44898b9d8c58edc13b298d58c238fff9ab8ee2ef3" }, - { - "name": "fish", - "unicode": "1F41F", + "fish": { + "category": "nature", + "moji": "🐟", + "unicodeVersion": "6.0", "digest": "8f62f08fbeaf39694c19816b5c7d4f292017fe5bf9f8dd7e40f1630f5f83b28b" }, - { - "name": "fish_cake", - "unicode": "1F365", + "fish_cake": { + "category": "food", + "moji": "🍥", + "unicodeVersion": "6.0", "digest": "5a6ca2100c8830927b22afa6f1d2fc821f5692cd23507fe5a776f6e085cbbfb2" }, - { - "name": "fishing_pole_and_fish", - "unicode": "1F3A3", + "fishing_pole_and_fish": { + "category": "activity", + "moji": "🎣", + "unicodeVersion": "6.0", "digest": "f8fb84eccceec88321b0a2a46f732ecfc378f787c19c27ac1327735f1ca9a48b" }, - { - "name": "fist", - "unicode": "270A", + "fist": { + "category": "people", + "moji": "✊", + "unicodeVersion": "6.0", "digest": "557f96d85615b8d78436bc67266115bfc8556c97c14f7909dfda1cf134e8344f" }, - { - "name": "fist_tone1", - "unicode": "270A-1F3FB", + "fist_tone1": { + "category": "people", + "moji": "✊🏻", + "unicodeVersion": "8.0", "digest": "6c1b946f9e01abc39b5085e24e8b6077fc0e34188e8daa30c6a3adddd387413e" }, - { - "name": "fist_tone2", - "unicode": "270A-1F3FC", + "fist_tone2": { + "category": "people", + "moji": "✊🏼", + "unicodeVersion": "8.0", "digest": "e9b9e1ec638dca4d5e1519bca7338f58cce2f2a282ee4c3581e8643166fc415f" }, - { - "name": "fist_tone3", - "unicode": "270A-1F3FD", + "fist_tone3": { + "category": "people", + "moji": "✊🏽", + "unicodeVersion": "8.0", "digest": "8c14d24055c143960b3d2a27fe23c55d2d3ac5f84f87e4e876616235e8698c7f" }, - { - "name": "fist_tone4", - "unicode": "270A-1F3FE", + "fist_tone4": { + "category": "people", + "moji": "✊🏾", + "unicodeVersion": "8.0", "digest": "923f034f481e952e6e5d1664588f99f79bd5416d4197b0ade6621f2669ce5765" }, - { - "name": "fist_tone5", - "unicode": "270A-1F3FF", + "fist_tone5": { + "category": "people", + "moji": "✊🏿", + "unicodeVersion": "8.0", "digest": "d691d2902216080916a29047e07d7a5bf2aed07e062067ca9d01cbf6fdf48c8d" }, - { - "name": "five", - "unicode": "0035-20E3", + "five": { + "category": "symbols", + "moji": "5️⃣", + "unicodeVersion": "3.0", "digest": "8f03f62fdbf744ae49c8a60fbf715ebfccbd6b62d91148e0923907006f3c2726" }, - { - "name": "flag_ac", - "unicode": "1F1E6-1F1E8", - "digest": "2e5c08535dc8ea96422d56a36b4fffc0b3bd2a13f2ab0d8dbd0e3a29bf3fc40c" - }, - { - "name": "ac", - "unicode": "1F1E6-1F1E8", + "flag_ac": { + "category": "flags", + "moji": "🇦🇨", + "unicodeVersion": "6.0", "digest": "2e5c08535dc8ea96422d56a36b4fffc0b3bd2a13f2ab0d8dbd0e3a29bf3fc40c" }, - { - "name": "flag_ad", - "unicode": "1F1E6-1F1E9", + "flag_ad": { + "category": "flags", + "moji": "🇦🇩", + "unicodeVersion": "6.0", "digest": "184fdcf790b8e2fd851b2b2b32f8636c595dd289734d12dc01ae4aa177e2043a" }, - { - "name": "ad", - "unicode": "1F1E6-1F1E9", - "digest": "184fdcf790b8e2fd851b2b2b32f8636c595dd289734d12dc01ae4aa177e2043a" - }, - { - "name": "flag_ae", - "unicode": "1F1E6-1F1EA", - "digest": "4a3257a9ce118e97567e76280f24d60fb555f1bada2eb26a2442a47f9398d21e" - }, - { - "name": "ae", - "unicode": "1F1E6-1F1EA", + "flag_ae": { + "category": "flags", + "moji": "🇦🇪", + "unicodeVersion": "6.0", "digest": "4a3257a9ce118e97567e76280f24d60fb555f1bada2eb26a2442a47f9398d21e" }, - { - "name": "flag_af", - "unicode": "1F1E6-1F1EB", + "flag_af": { + "category": "flags", + "moji": "🇦🇫", + "unicodeVersion": "6.0", "digest": "0f6c719cac7ab3140694f6b580787ecdbf503e38f16de7ec5803f7d06a088ec3" }, - { - "name": "af", - "unicode": "1F1E6-1F1EB", - "digest": "0f6c719cac7ab3140694f6b580787ecdbf503e38f16de7ec5803f7d06a088ec3" - }, - { - "name": "flag_ag", - "unicode": "1F1E6-1F1EC", - "digest": "92bf5a0e74564739862e9ba79331ffa656b7bae2ace0fc8dfd288984e4d510d4" - }, - { - "name": "ag", - "unicode": "1F1E6-1F1EC", + "flag_ag": { + "category": "flags", + "moji": "🇦🇬", + "unicodeVersion": "6.0", "digest": "92bf5a0e74564739862e9ba79331ffa656b7bae2ace0fc8dfd288984e4d510d4" }, - { - "name": "flag_ai", - "unicode": "1F1E6-1F1EE", + "flag_ai": { + "category": "flags", + "moji": "🇦🇮", + "unicodeVersion": "6.0", "digest": "aeaadc7ffafd8a1e01fdabc69d35f725d5f737b4c284a36191d96729f4e66e8f" }, - { - "name": "ai", - "unicode": "1F1E6-1F1EE", - "digest": "aeaadc7ffafd8a1e01fdabc69d35f725d5f737b4c284a36191d96729f4e66e8f" - }, - { - "name": "flag_al", - "unicode": "1F1E6-1F1F1", - "digest": "5ce7866d214d18c5f3438d480d14e77d104c4de679f0fdfca8cf0a44ce48eeea" - }, - { - "name": "al", - "unicode": "1F1E6-1F1F1", + "flag_al": { + "category": "flags", + "moji": "🇦🇱", + "unicodeVersion": "6.0", "digest": "5ce7866d214d18c5f3438d480d14e77d104c4de679f0fdfca8cf0a44ce48eeea" }, - { - "name": "flag_am", - "unicode": "1F1E6-1F1F2", + "flag_am": { + "category": "flags", + "moji": "🇦🇲", + "unicodeVersion": "6.0", "digest": "b40f5705f0cf9ef0fa7ffff0b371c4099319001ce79f894c317912f4dc5de4c8" }, - { - "name": "am", - "unicode": "1F1E6-1F1F2", - "digest": "b40f5705f0cf9ef0fa7ffff0b371c4099319001ce79f894c317912f4dc5de4c8" - }, - { - "name": "flag_ao", - "unicode": "1F1E6-1F1F4", + "flag_ao": { + "category": "flags", + "moji": "🇦🇴", + "unicodeVersion": "6.0", "digest": "eab6fbc1824d6e3cd152e8ec1d82e1beaebe02b53b35c6f7a883b8548af02f3a" }, - { - "name": "ao", - "unicode": "1F1E6-1F1F4", - "digest": "eab6fbc1824d6e3cd152e8ec1d82e1beaebe02b53b35c6f7a883b8548af02f3a" - }, - { - "name": "flag_aq", - "unicode": "1F1E6-1F1F6", + "flag_aq": { + "category": "flags", + "moji": "🇦🇶", + "unicodeVersion": "6.0", "digest": "367f6677a683a5f0e7248ab3a8f46d06ba146a0fd75004c70bac0e913147cdaa" }, - { - "name": "aq", - "unicode": "1F1E6-1F1F6", - "digest": "367f6677a683a5f0e7248ab3a8f46d06ba146a0fd75004c70bac0e913147cdaa" - }, - { - "name": "flag_ar", - "unicode": "1F1E6-1F1F7", - "digest": "f0dc466b3216957f2679d7208c2d7cf288448b0739b9270a7c5fa717577bdf25" - }, - { - "name": "ar", - "unicode": "1F1E6-1F1F7", + "flag_ar": { + "category": "flags", + "moji": "🇦🇷", + "unicodeVersion": "6.0", "digest": "f0dc466b3216957f2679d7208c2d7cf288448b0739b9270a7c5fa717577bdf25" }, - { - "name": "flag_as", - "unicode": "1F1E6-1F1F8", + "flag_as": { + "category": "flags", + "moji": "🇦🇸", + "unicodeVersion": "6.0", "digest": "fcb7a865c7763c63b23485cc27207b99a3a8492e83d5b5ee2df259a9f68f77d6" }, - { - "name": "as", - "unicode": "1F1E6-1F1F8", - "digest": "fcb7a865c7763c63b23485cc27207b99a3a8492e83d5b5ee2df259a9f68f77d6" - }, - { - "name": "flag_at", - "unicode": "1F1E6-1F1F9", - "digest": "1d3d58e9abc034f9a093a94716eddf9811d54dfaf27969fd322b3809fac70217" - }, - { - "name": "at", - "unicode": "1F1E6-1F1F9", + "flag_at": { + "category": "flags", + "moji": "🇦🇹", + "unicodeVersion": "6.0", "digest": "1d3d58e9abc034f9a093a94716eddf9811d54dfaf27969fd322b3809fac70217" }, - { - "name": "flag_au", - "unicode": "1F1E6-1F1FA", - "digest": "789563b64c71a5ad49078d335dc166ef614edb56d1e401885d32fb191c198fbd" - }, - { - "name": "au", - "unicode": "1F1E6-1F1FA", + "flag_au": { + "category": "flags", + "moji": "🇦🇺", + "unicodeVersion": "6.0", "digest": "789563b64c71a5ad49078d335dc166ef614edb56d1e401885d32fb191c198fbd" }, - { - "name": "flag_aw", - "unicode": "1F1E6-1F1FC", - "digest": "1504dc3fd8457b44fdf75c15e136dc46a13e8342d1f98949728cdc1238843e0c" - }, - { - "name": "aw", - "unicode": "1F1E6-1F1FC", + "flag_aw": { + "category": "flags", + "moji": "🇦🇼", + "unicodeVersion": "6.0", "digest": "1504dc3fd8457b44fdf75c15e136dc46a13e8342d1f98949728cdc1238843e0c" }, - { - "name": "flag_ax", - "unicode": "1F1E6-1F1FD", + "flag_ax": { + "category": "flags", + "moji": "🇦🇽", + "unicodeVersion": "6.0", "digest": "e96fa3525f3be25016a4cf8428261735f3ed5fc9fe5b827b461746a3f08877bf" }, - { - "name": "ax", - "unicode": "1F1E6-1F1FD", - "digest": "e96fa3525f3be25016a4cf8428261735f3ed5fc9fe5b827b461746a3f08877bf" - }, - { - "name": "flag_az", - "unicode": "1F1E6-1F1FF", - "digest": "12c366ac2c38b91314fb29056e09fa6e7417766cebde3045859cdb127549f4a2" - }, - { - "name": "az", - "unicode": "1F1E6-1F1FF", + "flag_az": { + "category": "flags", + "moji": "🇦🇿", + "unicodeVersion": "6.0", "digest": "12c366ac2c38b91314fb29056e09fa6e7417766cebde3045859cdb127549f4a2" }, - { - "name": "flag_ba", - "unicode": "1F1E7-1F1E6", - "digest": "0819ea3901510ac20c7f10e67e5f6c818210f17a362c1d12e299c41feb07f828" - }, - { - "name": "ba", - "unicode": "1F1E7-1F1E6", + "flag_ba": { + "category": "flags", + "moji": "🇧🇦", + "unicodeVersion": "6.0", "digest": "0819ea3901510ac20c7f10e67e5f6c818210f17a362c1d12e299c41feb07f828" }, - { - "name": "flag_bb", - "unicode": "1F1E7-1F1E7", + "flag_bb": { + "category": "flags", + "moji": "🇧🇧", + "unicodeVersion": "6.0", "digest": "cf32778a272ed6cbc8e783b59befd9b204009c69c61a425e148d867808b7fab9" }, - { - "name": "bb", - "unicode": "1F1E7-1F1E7", - "digest": "cf32778a272ed6cbc8e783b59befd9b204009c69c61a425e148d867808b7fab9" - }, - { - "name": "flag_bd", - "unicode": "1F1E7-1F1E9", - "digest": "e6ed186644a874588e879513aec92f8107220dcdd14c766dee61f266ce045665" - }, - { - "name": "bd", - "unicode": "1F1E7-1F1E9", + "flag_bd": { + "category": "flags", + "moji": "🇧🇩", + "unicodeVersion": "6.0", "digest": "e6ed186644a874588e879513aec92f8107220dcdd14c766dee61f266ce045665" }, - { - "name": "flag_be", - "unicode": "1F1E7-1F1EA", + "flag_be": { + "category": "flags", + "moji": "🇧🇪", + "unicodeVersion": "6.0", "digest": "4d941011d15d9f6e755d6f7694884758baf17ac0691bf5d63700f8d6dbcdb948" }, - { - "name": "be", - "unicode": "1F1E7-1F1EA", - "digest": "4d941011d15d9f6e755d6f7694884758baf17ac0691bf5d63700f8d6dbcdb948" - }, - { - "name": "flag_bf", - "unicode": "1F1E7-1F1EB", - "digest": "fcc57dbda9a86f725f558b6c6309484c97e65f1644aae4f9fb5e642681f6c2e0" - }, - { - "name": "bf", - "unicode": "1F1E7-1F1EB", + "flag_bf": { + "category": "flags", + "moji": "🇧🇫", + "unicodeVersion": "6.0", "digest": "fcc57dbda9a86f725f558b6c6309484c97e65f1644aae4f9fb5e642681f6c2e0" }, - { - "name": "flag_bg", - "unicode": "1F1E7-1F1EC", + "flag_bg": { + "category": "flags", + "moji": "🇧🇬", + "unicodeVersion": "6.0", "digest": "816c47ed96c36c90723da150645902ea8ba18b44757fdd776c7b3542cfecfb18" }, - { - "name": "bg", - "unicode": "1F1E7-1F1EC", - "digest": "816c47ed96c36c90723da150645902ea8ba18b44757fdd776c7b3542cfecfb18" - }, - { - "name": "flag_bh", - "unicode": "1F1E7-1F1ED", - "digest": "2cd5c21775a6e73f59d08c9ee0cedf4e8241e562eab939573501d47681987737" - }, - { - "name": "bh", - "unicode": "1F1E7-1F1ED", + "flag_bh": { + "category": "flags", + "moji": "🇧🇭", + "unicodeVersion": "6.0", "digest": "2cd5c21775a6e73f59d08c9ee0cedf4e8241e562eab939573501d47681987737" }, - { - "name": "flag_bi", - "unicode": "1F1E7-1F1EE", + "flag_bi": { + "category": "flags", + "moji": "🇧🇮", + "unicodeVersion": "6.0", "digest": "2da82acbec5518360633c1b0b56d55a79b67237f67d92af5e5cd75a2f3bd550e" }, - { - "name": "bi", - "unicode": "1F1E7-1F1EE", - "digest": "2da82acbec5518360633c1b0b56d55a79b67237f67d92af5e5cd75a2f3bd550e" - }, - { - "name": "flag_bj", - "unicode": "1F1E7-1F1EF", - "digest": "8fe8c34651eb4e28ab395261a5b72b6f37579535ed676d15de131914e19c0436" - }, - { - "name": "bj", - "unicode": "1F1E7-1F1EF", + "flag_bj": { + "category": "flags", + "moji": "🇧🇯", + "unicodeVersion": "6.0", "digest": "8fe8c34651eb4e28ab395261a5b72b6f37579535ed676d15de131914e19c0436" }, - { - "name": "flag_bl", - "unicode": "1F1E7-1F1F1", + "flag_bl": { + "category": "flags", + "moji": "🇧🇱", + "unicodeVersion": "6.0", "digest": "d37f2a215ee7ef5b5ab62d2a0c87e90553b17c6ee310f803a71e9fd72db880e7" }, - { - "name": "bl", - "unicode": "1F1E7-1F1F1", - "digest": "d37f2a215ee7ef5b5ab62d2a0c87e90553b17c6ee310f803a71e9fd72db880e7" - }, - { - "name": "flag_black", - "unicode": "1F3F4", - "digest": "3740bfc9bcb3b46b697b8b7c47ab2c3e95eca9dbcba12f2bf98a01302704f203" - }, - { - "name": "waving_black_flag", - "unicode": "1F3F4", + "flag_black": { + "category": "objects", + "moji": "🏴", + "unicodeVersion": "6.0", "digest": "3740bfc9bcb3b46b697b8b7c47ab2c3e95eca9dbcba12f2bf98a01302704f203" }, - { - "name": "flag_bm", - "unicode": "1F1E7-1F1F2", + "flag_bm": { + "category": "flags", + "moji": "🇧🇲", + "unicodeVersion": "6.0", "digest": "ccd21655573f3c955d616c5c7b1eac2be1d4772ff611648d6713ba55d9e4aa9b" }, - { - "name": "bm", - "unicode": "1F1E7-1F1F2", - "digest": "ccd21655573f3c955d616c5c7b1eac2be1d4772ff611648d6713ba55d9e4aa9b" - }, - { - "name": "flag_bn", - "unicode": "1F1E7-1F1F3", - "digest": "54330c3d7a37392e69098c213fd8c78f3faab4e7e5909c039188110422514228" - }, - { - "name": "bn", - "unicode": "1F1E7-1F1F3", + "flag_bn": { + "category": "flags", + "moji": "🇧🇳", + "unicodeVersion": "6.0", "digest": "54330c3d7a37392e69098c213fd8c78f3faab4e7e5909c039188110422514228" }, - { - "name": "flag_bo", - "unicode": "1F1E7-1F1F4", + "flag_bo": { + "category": "flags", + "moji": "🇧🇴", + "unicodeVersion": "6.0", "digest": "32aff973b26f4f91ca19dddd7861b564da43cfbee87603d8c004f1111342366c" }, - { - "name": "bo", - "unicode": "1F1E7-1F1F4", - "digest": "32aff973b26f4f91ca19dddd7861b564da43cfbee87603d8c004f1111342366c" - }, - { - "name": "flag_bq", - "unicode": "1F1E7-1F1F6", - "digest": "b1ebc959c43f706ca430d8633d9efaa9c60133871506b5f030b730cfb4c19e6f" - }, - { - "name": "bq", - "unicode": "1F1E7-1F1F6", + "flag_bq": { + "category": "flags", + "moji": "🇧🇶", + "unicodeVersion": "6.0", "digest": "b1ebc959c43f706ca430d8633d9efaa9c60133871506b5f030b730cfb4c19e6f" }, - { - "name": "flag_br", - "unicode": "1F1E7-1F1F7", + "flag_br": { + "category": "flags", + "moji": "🇧🇷", + "unicodeVersion": "6.0", "digest": "64fb154d71fa34ff4838bc405f3e58a4102cf0cb49ca4b06fc3c7a6bf39671f0" }, - { - "name": "br", - "unicode": "1F1E7-1F1F7", - "digest": "64fb154d71fa34ff4838bc405f3e58a4102cf0cb49ca4b06fc3c7a6bf39671f0" - }, - { - "name": "flag_bs", - "unicode": "1F1E7-1F1F8", + "flag_bs": { + "category": "flags", + "moji": "🇧🇸", + "unicodeVersion": "6.0", "digest": "c4b07e5f652ab06ece95d3774ce8b1399a935f8a28d440cb13cc8bd0b9728ed5" }, - { - "name": "bs", - "unicode": "1F1E7-1F1F8", - "digest": "c4b07e5f652ab06ece95d3774ce8b1399a935f8a28d440cb13cc8bd0b9728ed5" - }, - { - "name": "flag_bt", - "unicode": "1F1E7-1F1F9", + "flag_bt": { + "category": "flags", + "moji": "🇧🇹", + "unicodeVersion": "6.0", "digest": "901ddbd999dd89a87c1e1208b1470cb4e604a9bc023d0cbcdee64e1bc54079ba" }, - { - "name": "bt", - "unicode": "1F1E7-1F1F9", - "digest": "901ddbd999dd89a87c1e1208b1470cb4e604a9bc023d0cbcdee64e1bc54079ba" - }, - { - "name": "flag_bv", - "unicode": "1F1E7-1F1FB", - "digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6" - }, - { - "name": "bv", - "unicode": "1F1E7-1F1FB", + "flag_bv": { + "category": "flags", + "moji": "🇧🇻", + "unicodeVersion": "6.0", "digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6" }, - { - "name": "flag_bw", - "unicode": "1F1E7-1F1FC", + "flag_bw": { + "category": "flags", + "moji": "🇧🇼", + "unicodeVersion": "6.0", "digest": "05aa351bc04dc0fe2669441ab500e000d48b1f0d7ad9e885c7abfb898aa0eb3f" }, - { - "name": "bw", - "unicode": "1F1E7-1F1FC", - "digest": "05aa351bc04dc0fe2669441ab500e000d48b1f0d7ad9e885c7abfb898aa0eb3f" - }, - { - "name": "flag_by", - "unicode": "1F1E7-1F1FE", - "digest": "6eda3b87336ecf0aae4963986d86b916a055d8268c70520303288f235a93b0d9" - }, - { - "name": "by", - "unicode": "1F1E7-1F1FE", + "flag_by": { + "category": "flags", + "moji": "🇧🇾", + "unicodeVersion": "6.0", "digest": "6eda3b87336ecf0aae4963986d86b916a055d8268c70520303288f235a93b0d9" }, - { - "name": "flag_bz", - "unicode": "1F1E7-1F1FF", - "digest": "d76ed945b1408558a30a99b8eed6712de968fc49fba1721b5660b8f48087e45a" - }, - { - "name": "bz", - "unicode": "1F1E7-1F1FF", + "flag_bz": { + "category": "flags", + "moji": "🇧🇿", + "unicodeVersion": "6.0", "digest": "d76ed945b1408558a30a99b8eed6712de968fc49fba1721b5660b8f48087e45a" }, - { - "name": "flag_ca", - "unicode": "1F1E8-1F1E6", - "digest": "2fd036047d89751c05de5577909b58347883bc89c3b7d90bec28ad4770a98ecd" - }, - { - "name": "ca", - "unicode": "1F1E8-1F1E6", + "flag_ca": { + "category": "flags", + "moji": "🇨🇦", + "unicodeVersion": "6.0", "digest": "2fd036047d89751c05de5577909b58347883bc89c3b7d90bec28ad4770a98ecd" }, - { - "name": "flag_cc", - "unicode": "1F1E8-1F1E8", + "flag_cc": { + "category": "flags", + "moji": "🇨🇨", + "unicodeVersion": "6.0", "digest": "837ba181a01c71f05d438d205efaaee99f93b2370c97b13e6132f99860323e36" }, - { - "name": "cc", - "unicode": "1F1E8-1F1E8", - "digest": "837ba181a01c71f05d438d205efaaee99f93b2370c97b13e6132f99860323e36" - }, - { - "name": "flag_cd", - "unicode": "1F1E8-1F1E9", - "digest": "318689274b4b3b58aed7fc1654127499a9da69bff1b83e592e86e69d167ce16f" - }, - { - "name": "congo", - "unicode": "1F1E8-1F1E9", + "flag_cd": { + "category": "flags", + "moji": "🇨🇩", + "unicodeVersion": "6.0", "digest": "318689274b4b3b58aed7fc1654127499a9da69bff1b83e592e86e69d167ce16f" }, - { - "name": "flag_cf", - "unicode": "1F1E8-1F1EB", - "digest": "06d6042849d3b7b217c2b18ba787aae449e8c7d2537e2e5974744ec196062228" - }, - { - "name": "cf", - "unicode": "1F1E8-1F1EB", + "flag_cf": { + "category": "flags", + "moji": "🇨🇫", + "unicodeVersion": "6.0", "digest": "06d6042849d3b7b217c2b18ba787aae449e8c7d2537e2e5974744ec196062228" }, - { - "name": "flag_cg", - "unicode": "1F1E8-1F1EC", - "digest": "09f45d2dcb5a24d8349ef86e7405cc29ef3d65a908c0bff3221c3b4546547813" - }, - { - "name": "cg", - "unicode": "1F1E8-1F1EC", + "flag_cg": { + "category": "flags", + "moji": "🇨🇬", + "unicodeVersion": "6.0", "digest": "09f45d2dcb5a24d8349ef86e7405cc29ef3d65a908c0bff3221c3b4546547813" }, - { - "name": "flag_ch", - "unicode": "1F1E8-1F1ED", - "digest": "53d6d35aeeebb0b4b1ad858dc3691e649ac73d30b3be76f96d5fe9605fa99386" - }, - { - "name": "ch", - "unicode": "1F1E8-1F1ED", + "flag_ch": { + "category": "flags", + "moji": "🇨🇭", + "unicodeVersion": "6.0", "digest": "53d6d35aeeebb0b4b1ad858dc3691e649ac73d30b3be76f96d5fe9605fa99386" }, - { - "name": "flag_ci", - "unicode": "1F1E8-1F1EE", - "digest": "7d85a0c314b7397c9397a54ce2f3a4dc5f40d0234e586dbd8a541a8666f0f51e" - }, - { - "name": "ci", - "unicode": "1F1E8-1F1EE", + "flag_ci": { + "category": "flags", + "moji": "🇨🇮", + "unicodeVersion": "6.0", "digest": "7d85a0c314b7397c9397a54ce2f3a4dc5f40d0234e586dbd8a541a8666f0f51e" }, - { - "name": "flag_ck", - "unicode": "1F1E8-1F1F0", - "digest": "c1aa105fe106ed09ed59a596859a0ce4e65a415c59f63df51961491cb947b136" - }, - { - "name": "ck", - "unicode": "1F1E8-1F1F0", + "flag_ck": { + "category": "flags", + "moji": "🇨🇰", + "unicodeVersion": "6.0", "digest": "c1aa105fe106ed09ed59a596859a0ce4e65a415c59f63df51961491cb947b136" }, - { - "name": "flag_cl", - "unicode": "1F1E8-1F1F1", - "digest": "0fffdad0d892f5c08aaa332af1ed2c228583d89a43190e979a3c3cb020d5a723" - }, - { - "name": "chile", - "unicode": "1F1E8-1F1F1", + "flag_cl": { + "category": "flags", + "moji": "🇨🇱", + "unicodeVersion": "6.0", "digest": "0fffdad0d892f5c08aaa332af1ed2c228583d89a43190e979a3c3cb020d5a723" }, - { - "name": "flag_cm", - "unicode": "1F1E8-1F1F2", - "digest": "e9f55e41a1fd2735a82ad7a7ac39326a944cb20423ffba3608ac53a46036caad" - }, - { - "name": "cm", - "unicode": "1F1E8-1F1F2", + "flag_cm": { + "category": "flags", + "moji": "🇨🇲", + "unicodeVersion": "6.0", "digest": "e9f55e41a1fd2735a82ad7a7ac39326a944cb20423ffba3608ac53a46036caad" }, - { - "name": "flag_cn", - "unicode": "1F1E8-1F1F3", - "digest": "e2c8fee7e3bd51b13d6083d5bf344abe6b9b642e3cbb099d38b4ce341c99d890" - }, - { - "name": "cn", - "unicode": "1F1E8-1F1F3", + "flag_cn": { + "category": "flags", + "moji": "🇨🇳", + "unicodeVersion": "6.0", "digest": "e2c8fee7e3bd51b13d6083d5bf344abe6b9b642e3cbb099d38b4ce341c99d890" }, - { - "name": "flag_co", - "unicode": "1F1E8-1F1F4", - "digest": "51c60d0979bf8342eaff7cda9faf4b0dfab38efaf5ddf3717eb8f0e2a595b15f" - }, - { - "name": "co", - "unicode": "1F1E8-1F1F4", + "flag_co": { + "category": "flags", + "moji": "🇨🇴", + "unicodeVersion": "6.0", "digest": "51c60d0979bf8342eaff7cda9faf4b0dfab38efaf5ddf3717eb8f0e2a595b15f" }, - { - "name": "flag_cp", - "unicode": "1F1E8-1F1F5", + "flag_cp": { + "category": "flags", + "moji": "🇨🇵", + "unicodeVersion": "6.0", "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9" }, - { - "name": "cp", - "unicode": "1F1E8-1F1F5", - "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9" - }, - { - "name": "flag_cr", - "unicode": "1F1E8-1F1F7", - "digest": "907905971b219e617a34eef4839b0bd08d98f3480e2631bce523120dcef95196" - }, - { - "name": "cr", - "unicode": "1F1E8-1F1F7", + "flag_cr": { + "category": "flags", + "moji": "🇨🇷", + "unicodeVersion": "6.0", "digest": "907905971b219e617a34eef4839b0bd08d98f3480e2631bce523120dcef95196" }, - { - "name": "flag_cu", - "unicode": "1F1E8-1F1FA", + "flag_cu": { + "category": "flags", + "moji": "🇨🇺", + "unicodeVersion": "6.0", "digest": "d88cea729dc9dbbbcadac0409ec561995f061b2280577c01c6c6b37de347f150" }, - { - "name": "cu", - "unicode": "1F1E8-1F1FA", - "digest": "d88cea729dc9dbbbcadac0409ec561995f061b2280577c01c6c6b37de347f150" - }, - { - "name": "flag_cv", - "unicode": "1F1E8-1F1FB", - "digest": "5ce97944adfce09e96387e6f872256482ac99ccbc60017c4d58ddd15b6fb67a7" - }, - { - "name": "cv", - "unicode": "1F1E8-1F1FB", + "flag_cv": { + "category": "flags", + "moji": "🇨🇻", + "unicodeVersion": "6.0", "digest": "5ce97944adfce09e96387e6f872256482ac99ccbc60017c4d58ddd15b6fb67a7" }, - { - "name": "flag_cw", - "unicode": "1F1E8-1F1FC", + "flag_cw": { + "category": "flags", + "moji": "🇨🇼", + "unicodeVersion": "6.0", "digest": "a6fc31bd66ddc2ee8e7bde3aeabfe1c4ad00c9688abae234a541cc1236d68c1b" }, - { - "name": "cw", - "unicode": "1F1E8-1F1FC", - "digest": "a6fc31bd66ddc2ee8e7bde3aeabfe1c4ad00c9688abae234a541cc1236d68c1b" - }, - { - "name": "flag_cx", - "unicode": "1F1E8-1F1FD", - "digest": "1261b32bfa22fa1441f5390ff499ac6b921d7ac59cc8acda3deb3a2beb4fb345" - }, - { - "name": "cx", - "unicode": "1F1E8-1F1FD", + "flag_cx": { + "category": "flags", + "moji": "🇨🇽", + "unicodeVersion": "6.0", "digest": "1261b32bfa22fa1441f5390ff499ac6b921d7ac59cc8acda3deb3a2beb4fb345" }, - { - "name": "flag_cy", - "unicode": "1F1E8-1F1FE", + "flag_cy": { + "category": "flags", + "moji": "🇨🇾", + "unicodeVersion": "6.0", "digest": "82b1baa05ecffa0ea1f9a83b518163cbd7910985a21955740520bb16b7bb624f" }, - { - "name": "cy", - "unicode": "1F1E8-1F1FE", - "digest": "82b1baa05ecffa0ea1f9a83b518163cbd7910985a21955740520bb16b7bb624f" - }, - { - "name": "flag_cz", - "unicode": "1F1E8-1F1FF", - "digest": "a169b18968992a52299b67c24fba495e84de28dec2ebb947a08e0d615ac54a5a" - }, - { - "name": "cz", - "unicode": "1F1E8-1F1FF", + "flag_cz": { + "category": "flags", + "moji": "🇨🇿", + "unicodeVersion": "6.0", "digest": "a169b18968992a52299b67c24fba495e84de28dec2ebb947a08e0d615ac54a5a" }, - { - "name": "flag_de", - "unicode": "1F1E9-1F1EA", + "flag_de": { + "category": "flags", + "moji": "🇩🇪", + "unicodeVersion": "6.0", "digest": "99d1906944966a188c72ae592362ed907e2a0bfe95263955c34a0941507b30c1" }, - { - "name": "de", - "unicode": "1F1E9-1F1EA", - "digest": "99d1906944966a188c72ae592362ed907e2a0bfe95263955c34a0941507b30c1" - }, - { - "name": "flag_dg", - "unicode": "1F1E9-1F1EC", - "digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e" - }, - { - "name": "dg", - "unicode": "1F1E9-1F1EC", + "flag_dg": { + "category": "flags", + "moji": "🇩🇬", + "unicodeVersion": "6.0", "digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e" }, - { - "name": "flag_dj", - "unicode": "1F1E9-1F1EF", + "flag_dj": { + "category": "flags", + "moji": "🇩🇯", + "unicodeVersion": "6.0", "digest": "e90ba4e98fca71ff0ca5e65c28b911cc52f043428f375d8f954ecbd3b0c8f4dd" }, - { - "name": "dj", - "unicode": "1F1E9-1F1EF", - "digest": "e90ba4e98fca71ff0ca5e65c28b911cc52f043428f375d8f954ecbd3b0c8f4dd" - }, - { - "name": "flag_dk", - "unicode": "1F1E9-1F1F0", - "digest": "65b3b5f31935a4969d81fedbb8279c7ad32da454d15c5eafcceba5d140927c77" - }, - { - "name": "dk", - "unicode": "1F1E9-1F1F0", + "flag_dk": { + "category": "flags", + "moji": "🇩🇰", + "unicodeVersion": "6.0", "digest": "65b3b5f31935a4969d81fedbb8279c7ad32da454d15c5eafcceba5d140927c77" }, - { - "name": "flag_dm", - "unicode": "1F1E9-1F1F2", + "flag_dm": { + "category": "flags", + "moji": "🇩🇲", + "unicodeVersion": "6.0", "digest": "f6225ded6d2cfd6c182ab1a53b8c49dc9df195df11eb7ff27b15f5d3721ba0eb" }, - { - "name": "dm", - "unicode": "1F1E9-1F1F2", - "digest": "f6225ded6d2cfd6c182ab1a53b8c49dc9df195df11eb7ff27b15f5d3721ba0eb" - }, - { - "name": "flag_do", - "unicode": "1F1E9-1F1F4", - "digest": "dc2ad6856cebbe47c5bd7f5dcf087e4f680d396b2d49440a9b71f0ad49fb8102" - }, - { - "name": "do", - "unicode": "1F1E9-1F1F4", + "flag_do": { + "category": "flags", + "moji": "🇩🇴", + "unicodeVersion": "6.0", "digest": "dc2ad6856cebbe47c5bd7f5dcf087e4f680d396b2d49440a9b71f0ad49fb8102" }, - { - "name": "flag_dz", - "unicode": "1F1E9-1F1FF", + "flag_dz": { + "category": "flags", + "moji": "🇩🇿", + "unicodeVersion": "6.0", "digest": "ea69fffc4d545f9c0fcef6768257501952955ba4d274c9b81843229a1265c5ed" }, - { - "name": "dz", - "unicode": "1F1E9-1F1FF", - "digest": "ea69fffc4d545f9c0fcef6768257501952955ba4d274c9b81843229a1265c5ed" - }, - { - "name": "flag_ea", - "unicode": "1F1EA-1F1E6", + "flag_ea": { + "category": "flags", + "moji": "🇪🇦", + "unicodeVersion": "6.0", "digest": "e63bfe15428c481dd23b569e7aaf0a76106e58a946995b4415a81097ecd53b7d" }, - { - "name": "ea", - "unicode": "1F1EA-1F1E6", - "digest": "e63bfe15428c481dd23b569e7aaf0a76106e58a946995b4415a81097ecd53b7d" - }, - { - "name": "flag_ec", - "unicode": "1F1EA-1F1E8", + "flag_ec": { + "category": "flags", + "moji": "🇪🇨", + "unicodeVersion": "6.0", "digest": "0cdabf85cd567047fda1d9a4508220cab829943a7c542c315078db0aac33edac" }, - { - "name": "ec", - "unicode": "1F1EA-1F1E8", - "digest": "0cdabf85cd567047fda1d9a4508220cab829943a7c542c315078db0aac33edac" - }, - { - "name": "flag_ee", - "unicode": "1F1EA-1F1EA", - "digest": "6dc4e3377e8e2af3ff40cf940a914bc7840980b4a14e7da86954343f2b1025fe" - }, - { - "name": "ee", - "unicode": "1F1EA-1F1EA", + "flag_ee": { + "category": "flags", + "moji": "🇪🇪", + "unicodeVersion": "6.0", "digest": "6dc4e3377e8e2af3ff40cf940a914bc7840980b4a14e7da86954343f2b1025fe" }, - { - "name": "flag_eg", - "unicode": "1F1EA-1F1EC", + "flag_eg": { + "category": "flags", + "moji": "🇪🇬", + "unicodeVersion": "6.0", "digest": "2ed6bc056015694d75993eb5ee3c1850921d5630681207b04dfbdb982ab346a2" }, - { - "name": "eg", - "unicode": "1F1EA-1F1EC", - "digest": "2ed6bc056015694d75993eb5ee3c1850921d5630681207b04dfbdb982ab346a2" - }, - { - "name": "flag_eh", - "unicode": "1F1EA-1F1ED", - "digest": "72adb55943e4df99c00843c65463718609d937480f73dcf4a4451d46b9967a5e" - }, - { - "name": "eh", - "unicode": "1F1EA-1F1ED", + "flag_eh": { + "category": "flags", + "moji": "🇪🇭", + "unicodeVersion": "6.0", "digest": "72adb55943e4df99c00843c65463718609d937480f73dcf4a4451d46b9967a5e" }, - { - "name": "flag_er", - "unicode": "1F1EA-1F1F7", - "digest": "3fa59331eb5300c8c1f7b1f1bc15cfcfe688da6fa4a79341854598086a44eebc" - }, - { - "name": "er", - "unicode": "1F1EA-1F1F7", + "flag_er": { + "category": "flags", + "moji": "🇪🇷", + "unicodeVersion": "6.0", "digest": "3fa59331eb5300c8c1f7b1f1bc15cfcfe688da6fa4a79341854598086a44eebc" }, - { - "name": "flag_es", - "unicode": "1F1EA-1F1F8", - "digest": "1fa1d5cb0a7e8b14aaec758b2e7bf49cdf8f3d09bbcc7dfd589053a432eeae25" - }, - { - "name": "es", - "unicode": "1F1EA-1F1F8", + "flag_es": { + "category": "flags", + "moji": "🇪🇸", + "unicodeVersion": "6.0", "digest": "1fa1d5cb0a7e8b14aaec758b2e7bf49cdf8f3d09bbcc7dfd589053a432eeae25" }, - { - "name": "flag_et", - "unicode": "1F1EA-1F1F9", - "digest": "72771decfb214394e4beb594e848ea590c3615800adbba24b5df4c5db6ee9617" - }, - { - "name": "et", - "unicode": "1F1EA-1F1F9", + "flag_et": { + "category": "flags", + "moji": "🇪🇹", + "unicodeVersion": "6.0", "digest": "72771decfb214394e4beb594e848ea590c3615800adbba24b5df4c5db6ee9617" }, - { - "name": "flag_eu", - "unicode": "1F1EA-1F1FA", - "digest": "4bfa1b2ef23764ead5ef7899806f93e13fd29a09c75e61431579a4116c836aa4" - }, - { - "name": "eu", - "unicode": "1F1EA-1F1FA", + "flag_eu": { + "category": "flags", + "moji": "🇪🇺", + "unicodeVersion": "6.0", "digest": "4bfa1b2ef23764ead5ef7899806f93e13fd29a09c75e61431579a4116c836aa4" }, - { - "name": "flag_fi", - "unicode": "1F1EB-1F1EE", - "digest": "d0208cdd5b153a2865f9f674179c62871d4675abb0fb639fba88fcd62553f54e" - }, - { - "name": "fi", - "unicode": "1F1EB-1F1EE", + "flag_fi": { + "category": "flags", + "moji": "🇫🇮", + "unicodeVersion": "6.0", "digest": "d0208cdd5b153a2865f9f674179c62871d4675abb0fb639fba88fcd62553f54e" }, - { - "name": "flag_fj", - "unicode": "1F1EB-1F1EF", + "flag_fj": { + "category": "flags", + "moji": "🇫🇯", + "unicodeVersion": "6.0", "digest": "6c5ec41114af3846b093a418f6e2b5ff7a83cb72cecde75a7dc62e8cb6dcfe45" }, - { - "name": "fj", - "unicode": "1F1EB-1F1EF", - "digest": "6c5ec41114af3846b093a418f6e2b5ff7a83cb72cecde75a7dc62e8cb6dcfe45" - }, - { - "name": "flag_fk", - "unicode": "1F1EB-1F1F0", - "digest": "c69ad641d53785deff5c3934b7dcfcd3dc32ffc31b6d3e799d0555b03c23fc15" - }, - { - "name": "fk", - "unicode": "1F1EB-1F1F0", + "flag_fk": { + "category": "flags", + "moji": "🇫🇰", + "unicodeVersion": "6.0", "digest": "c69ad641d53785deff5c3934b7dcfcd3dc32ffc31b6d3e799d0555b03c23fc15" }, - { - "name": "flag_fm", - "unicode": "1F1EB-1F1F2", + "flag_fm": { + "category": "flags", + "moji": "🇫🇲", + "unicodeVersion": "6.0", "digest": "1e29fb06b273f253c23a9e4aa8ff84bfe22cffb5fa158a0c6f4cdeabe0216990" }, - { - "name": "fm", - "unicode": "1F1EB-1F1F2", - "digest": "1e29fb06b273f253c23a9e4aa8ff84bfe22cffb5fa158a0c6f4cdeabe0216990" - }, - { - "name": "flag_fo", - "unicode": "1F1EB-1F1F4", - "digest": "f4907d2f606f4f9d3bef06c6d38e8e88f2a148197b1573668866431a007afc2e" - }, - { - "name": "fo", - "unicode": "1F1EB-1F1F4", + "flag_fo": { + "category": "flags", + "moji": "🇫🇴", + "unicodeVersion": "6.0", "digest": "f4907d2f606f4f9d3bef06c6d38e8e88f2a148197b1573668866431a007afc2e" }, - { - "name": "flag_fr", - "unicode": "1F1EB-1F1F7", + "flag_fr": { + "category": "flags", + "moji": "🇫🇷", + "unicodeVersion": "6.0", "digest": "5a1308ab3cbf6bffcab12588cf3325151a6c72990db7408c2b8605d89f94ed6e" }, - { - "name": "fr", - "unicode": "1F1EB-1F1F7", - "digest": "5a1308ab3cbf6bffcab12588cf3325151a6c72990db7408c2b8605d89f94ed6e" - }, - { - "name": "flag_ga", - "unicode": "1F1EC-1F1E6", - "digest": "ddc32dee2976507be878ec3d3d2408632ca21bc434cd9f58db4f6ac9774a2db5" - }, - { - "name": "ga", - "unicode": "1F1EC-1F1E6", + "flag_ga": { + "category": "flags", + "moji": "🇬🇦", + "unicodeVersion": "6.0", "digest": "ddc32dee2976507be878ec3d3d2408632ca21bc434cd9f58db4f6ac9774a2db5" }, - { - "name": "flag_gb", - "unicode": "1F1EC-1F1E7", + "flag_gb": { + "category": "flags", + "moji": "🇬🇧", + "unicodeVersion": "6.0", "digest": "6b3bb254d134870b02cb066b06e206f652638a915c84b8649ceb30ec67fbebde" }, - { - "name": "gb", - "unicode": "1F1EC-1F1E7", - "digest": "6b3bb254d134870b02cb066b06e206f652638a915c84b8649ceb30ec67fbebde" - }, - { - "name": "flag_gd", - "unicode": "1F1EC-1F1E9", + "flag_gd": { + "category": "flags", + "moji": "🇬🇩", + "unicodeVersion": "6.0", "digest": "b6a210541ca22d816405f2a7d0d5241dc4d5488c8a36e15bd1e3063f9c41327f" }, - { - "name": "gd", - "unicode": "1F1EC-1F1E9", - "digest": "b6a210541ca22d816405f2a7d0d5241dc4d5488c8a36e15bd1e3063f9c41327f" - }, - { - "name": "flag_ge", - "unicode": "1F1EC-1F1EA", + "flag_ge": { + "category": "flags", + "moji": "🇬🇪", + "unicodeVersion": "6.0", "digest": "e9a5035b7a46b925737e7f7b0ae2419cc4af0e980fbee5bd916edeef13823367" }, - { - "name": "ge", - "unicode": "1F1EC-1F1EA", - "digest": "e9a5035b7a46b925737e7f7b0ae2419cc4af0e980fbee5bd916edeef13823367" - }, - { - "name": "flag_gf", - "unicode": "1F1EC-1F1EB", - "digest": "ce1bcd8c303897c1c22c5994182f21240b4aa635f0d7ce9944f76cbdbf0e4956" - }, - { - "name": "gf", - "unicode": "1F1EC-1F1EB", + "flag_gf": { + "category": "flags", + "moji": "🇬🇫", + "unicodeVersion": "6.0", "digest": "ce1bcd8c303897c1c22c5994182f21240b4aa635f0d7ce9944f76cbdbf0e4956" }, - { - "name": "flag_gg", - "unicode": "1F1EC-1F1EC", + "flag_gg": { + "category": "flags", + "moji": "🇬🇬", + "unicodeVersion": "6.0", "digest": "a435aab3609533ab2d68acd97deba844bfb0fc27b2adac68668223011f23ae5d" }, - { - "name": "gg", - "unicode": "1F1EC-1F1EC", - "digest": "a435aab3609533ab2d68acd97deba844bfb0fc27b2adac68668223011f23ae5d" - }, - { - "name": "flag_gh", - "unicode": "1F1EC-1F1ED", - "digest": "7cad43b40f69b9b00cc1b38036789ce774fd3d597c89f0bf38433847ea69be26" - }, - { - "name": "gh", - "unicode": "1F1EC-1F1ED", + "flag_gh": { + "category": "flags", + "moji": "🇬🇭", + "unicodeVersion": "6.0", "digest": "7cad43b40f69b9b00cc1b38036789ce774fd3d597c89f0bf38433847ea69be26" }, - { - "name": "flag_gi", - "unicode": "1F1EC-1F1EE", - "digest": "70e9b17d18bf3e0e4d03f4f824323a57909416e4082ca9d8a0796a6959de4f07" - }, - { - "name": "gi", - "unicode": "1F1EC-1F1EE", + "flag_gi": { + "category": "flags", + "moji": "🇬🇮", + "unicodeVersion": "6.0", "digest": "70e9b17d18bf3e0e4d03f4f824323a57909416e4082ca9d8a0796a6959de4f07" }, - { - "name": "flag_gl", - "unicode": "1F1EC-1F1F1", - "digest": "1963d8cca1c1f06b7536b7fb8f5a4782ac0bb05afdf6e481101bce45c58cdd4b" - }, - { - "name": "gl", - "unicode": "1F1EC-1F1F1", + "flag_gl": { + "category": "flags", + "moji": "🇬🇱", + "unicodeVersion": "6.0", "digest": "1963d8cca1c1f06b7536b7fb8f5a4782ac0bb05afdf6e481101bce45c58cdd4b" }, - { - "name": "flag_gm", - "unicode": "1F1EC-1F1F2", + "flag_gm": { + "category": "flags", + "moji": "🇬🇲", + "unicodeVersion": "6.0", "digest": "6c776a8daa3f4daa2597b0025aec06fc0a53aed262e845d4da3897cd7a89c6a1" }, - { - "name": "gm", - "unicode": "1F1EC-1F1F2", - "digest": "6c776a8daa3f4daa2597b0025aec06fc0a53aed262e845d4da3897cd7a89c6a1" - }, - { - "name": "flag_gn", - "unicode": "1F1EC-1F1F3", - "digest": "134cf7c839370d171ae80a72e5d18d32ea1967df19c191d1a4ea446d649e9558" - }, - { - "name": "gn", - "unicode": "1F1EC-1F1F3", + "flag_gn": { + "category": "flags", + "moji": "🇬🇳", + "unicodeVersion": "6.0", "digest": "134cf7c839370d171ae80a72e5d18d32ea1967df19c191d1a4ea446d649e9558" }, - { - "name": "flag_gp", - "unicode": "1F1EC-1F1F5", - "digest": "be3e906b039ba4884053c78f4f14de9aa87c5573860ccb69ec766068ae3887c2" - }, - { - "name": "gp", - "unicode": "1F1EC-1F1F5", + "flag_gp": { + "category": "flags", + "moji": "🇬🇵", + "unicodeVersion": "6.0", "digest": "be3e906b039ba4884053c78f4f14de9aa87c5573860ccb69ec766068ae3887c2" }, - { - "name": "flag_gq", - "unicode": "1F1EC-1F1F6", - "digest": "d476059c4ab41f5a1ef88583087362a5bc57cede930126f37041d1546564ab70" - }, - { - "name": "gq", - "unicode": "1F1EC-1F1F6", + "flag_gq": { + "category": "flags", + "moji": "🇬🇶", + "unicodeVersion": "6.0", "digest": "d476059c4ab41f5a1ef88583087362a5bc57cede930126f37041d1546564ab70" }, - { - "name": "flag_gr", - "unicode": "1F1EC-1F1F7", - "digest": "b9fa9304647aaa08167a07858bb18d778dcc399375f86f580b8d4244794678bc" - }, - { - "name": "gr", - "unicode": "1F1EC-1F1F7", + "flag_gr": { + "category": "flags", + "moji": "🇬🇷", + "unicodeVersion": "6.0", "digest": "b9fa9304647aaa08167a07858bb18d778dcc399375f86f580b8d4244794678bc" }, - { - "name": "flag_gs", - "unicode": "1F1EC-1F1F8", - "digest": "de33fbef6e294eb7af36e5b94d8ff573b354a4ff1ebdccf50ca528b86ed601d9" - }, - { - "name": "gs", - "unicode": "1F1EC-1F1F8", + "flag_gs": { + "category": "flags", + "moji": "🇬🇸", + "unicodeVersion": "6.0", "digest": "de33fbef6e294eb7af36e5b94d8ff573b354a4ff1ebdccf50ca528b86ed601d9" }, - { - "name": "flag_gt", - "unicode": "1F1EC-1F1F9", - "digest": "4160843e5d642df597c8423eb8e3b74deafe304f3d141c8a4d2fc07509e44832" - }, - { - "name": "gt", - "unicode": "1F1EC-1F1F9", + "flag_gt": { + "category": "flags", + "moji": "🇬🇹", + "unicodeVersion": "6.0", "digest": "4160843e5d642df597c8423eb8e3b74deafe304f3d141c8a4d2fc07509e44832" }, - { - "name": "flag_gu", - "unicode": "1F1EC-1F1FA", - "digest": "3b0cb257ba5b1c3e15d9102410c5f7418da03372e91ce90513de25b9f45283e3" - }, - { - "name": "gu", - "unicode": "1F1EC-1F1FA", + "flag_gu": { + "category": "flags", + "moji": "🇬🇺", + "unicodeVersion": "6.0", "digest": "3b0cb257ba5b1c3e15d9102410c5f7418da03372e91ce90513de25b9f45283e3" }, - { - "name": "flag_gw", - "unicode": "1F1EC-1F1FC", + "flag_gw": { + "category": "flags", + "moji": "🇬🇼", + "unicodeVersion": "6.0", "digest": "bdf07a8f93c0f0a573af5f5361be404a3ba65b729c1a4c05b7632c03d85efc72" }, - { - "name": "gw", - "unicode": "1F1EC-1F1FC", - "digest": "bdf07a8f93c0f0a573af5f5361be404a3ba65b729c1a4c05b7632c03d85efc72" - }, - { - "name": "flag_gy", - "unicode": "1F1EC-1F1FE", - "digest": "b47d8c98b747556f827ad0d1169264eb68ecaf9d2fb76595e8c31866361cbfc6" - }, - { - "name": "gy", - "unicode": "1F1EC-1F1FE", + "flag_gy": { + "category": "flags", + "moji": "🇬🇾", + "unicodeVersion": "6.0", "digest": "b47d8c98b747556f827ad0d1169264eb68ecaf9d2fb76595e8c31866361cbfc6" }, - { - "name": "flag_hk", - "unicode": "1F1ED-1F1F0", - "digest": "8e5a54b2e4bd4f5182085299b9648062463da05d535cf0e46a7d9c58eaeb171f" - }, - { - "name": "hk", - "unicode": "1F1ED-1F1F0", + "flag_hk": { + "category": "flags", + "moji": "🇭🇰", + "unicodeVersion": "6.0", "digest": "8e5a54b2e4bd4f5182085299b9648062463da05d535cf0e46a7d9c58eaeb171f" }, - { - "name": "flag_hm", - "unicode": "1F1ED-1F1F2", + "flag_hm": { + "category": "flags", + "moji": "🇭🇲", + "unicodeVersion": "6.0", "digest": "63c3e080c5e82a72c6d4cf5997ac823dc02184719ec59aadea6dd41b127abf22" }, - { - "name": "hm", - "unicode": "1F1ED-1F1F2", - "digest": "63c3e080c5e82a72c6d4cf5997ac823dc02184719ec59aadea6dd41b127abf22" - }, - { - "name": "flag_hn", - "unicode": "1F1ED-1F1F3", - "digest": "87c1d160db810b5ed208fb33add54f96c17b0f08d87b81f6f09429abf6ec93ac" - }, - { - "name": "hn", - "unicode": "1F1ED-1F1F3", + "flag_hn": { + "category": "flags", + "moji": "🇭🇳", + "unicodeVersion": "6.0", "digest": "87c1d160db810b5ed208fb33add54f96c17b0f08d87b81f6f09429abf6ec93ac" }, - { - "name": "flag_hr", - "unicode": "1F1ED-1F1F7", + "flag_hr": { + "category": "flags", + "moji": "🇭🇷", + "unicodeVersion": "6.0", "digest": "8b68112f79baea38565673acf4f1cb90675a5829ff17e4cf9415c928b62aed88" }, - { - "name": "hr", - "unicode": "1F1ED-1F1F7", - "digest": "8b68112f79baea38565673acf4f1cb90675a5829ff17e4cf9415c928b62aed88" - }, - { - "name": "flag_ht", - "unicode": "1F1ED-1F1F9", - "digest": "05dbd548c310ef1ebd1724aa85d821f8320106b16ddbf1f6442ea37e4407d5e1" - }, - { - "name": "ht", - "unicode": "1F1ED-1F1F9", + "flag_ht": { + "category": "flags", + "moji": "🇭🇹", + "unicodeVersion": "6.0", "digest": "05dbd548c310ef1ebd1724aa85d821f8320106b16ddbf1f6442ea37e4407d5e1" }, - { - "name": "flag_hu", - "unicode": "1F1ED-1F1FA", + "flag_hu": { + "category": "flags", + "moji": "🇭🇺", + "unicodeVersion": "6.0", "digest": "5079f3d6f1459e6df8dda5c19d2367ead8f5a755b8874ac999bae58e3c9f47a7" }, - { - "name": "hu", - "unicode": "1F1ED-1F1FA", - "digest": "5079f3d6f1459e6df8dda5c19d2367ead8f5a755b8874ac999bae58e3c9f47a7" - }, - { - "name": "flag_ic", - "unicode": "1F1EE-1F1E8", - "digest": "8dcb18c4b75a60867a68d2f6edbf81e782aafb4b9a0404c8081f872dfe71e432" - }, - { - "name": "ic", - "unicode": "1F1EE-1F1E8", + "flag_ic": { + "category": "flags", + "moji": "🇮🇨", + "unicodeVersion": "6.0", "digest": "8dcb18c4b75a60867a68d2f6edbf81e782aafb4b9a0404c8081f872dfe71e432" }, - { - "name": "flag_id", - "unicode": "1F1EE-1F1E9", + "flag_id": { + "category": "flags", + "moji": "🇮🇩", + "unicodeVersion": "6.0", "digest": "1b0eb69a158ed3afe24be448d44751f95dcc5cbc7d1393a5753293f16ef0a66c" }, - { - "name": "indonesia", - "unicode": "1F1EE-1F1E9", - "digest": "1b0eb69a158ed3afe24be448d44751f95dcc5cbc7d1393a5753293f16ef0a66c" - }, - { - "name": "flag_ie", - "unicode": "1F1EE-1F1EA", - "digest": "5fc8c101ad7296224455f72f73c335aa4f676023b68645bafaf69087f69af390" - }, - { - "name": "ie", - "unicode": "1F1EE-1F1EA", + "flag_ie": { + "category": "flags", + "moji": "🇮🇪", + "unicodeVersion": "6.0", "digest": "5fc8c101ad7296224455f72f73c335aa4f676023b68645bafaf69087f69af390" }, - { - "name": "flag_il", - "unicode": "1F1EE-1F1F1", + "flag_il": { + "category": "flags", + "moji": "🇮🇱", + "unicodeVersion": "6.0", "digest": "5aea4207415b7615dcdd69413705aefda700aefd0d27010cd0a0a338d879d9b8" }, - { - "name": "il", - "unicode": "1F1EE-1F1F1", - "digest": "5aea4207415b7615dcdd69413705aefda700aefd0d27010cd0a0a338d879d9b8" - }, - { - "name": "flag_im", - "unicode": "1F1EE-1F1F2", - "digest": "1ee9b3a5f1a52fc6d8369bfd81995fc0567e7a61deacd013701b3ec5fd64502e" - }, - { - "name": "im", - "unicode": "1F1EE-1F1F2", + "flag_im": { + "category": "flags", + "moji": "🇮🇲", + "unicodeVersion": "6.0", "digest": "1ee9b3a5f1a52fc6d8369bfd81995fc0567e7a61deacd013701b3ec5fd64502e" }, - { - "name": "flag_in", - "unicode": "1F1EE-1F1F3", + "flag_in": { + "category": "flags", + "moji": "🇮🇳", + "unicodeVersion": "6.0", "digest": "202ede502f34d55d180726ac2f29141c6875516f1b3e7ee99f266b16c2fe4bfd" }, - { - "name": "in", - "unicode": "1F1EE-1F1F3", - "digest": "202ede502f34d55d180726ac2f29141c6875516f1b3e7ee99f266b16c2fe4bfd" - }, - { - "name": "flag_io", - "unicode": "1F1EE-1F1F4", - "digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e" - }, - { - "name": "io", - "unicode": "1F1EE-1F1F4", + "flag_io": { + "category": "flags", + "moji": "🇮🇴", + "unicodeVersion": "6.0", "digest": "dd45e1afe792fca57d4161434bf611bcb7170072d63e4a27fb9dcd6e8912621e" }, - { - "name": "flag_iq", - "unicode": "1F1EE-1F1F6", + "flag_iq": { + "category": "flags", + "moji": "🇮🇶", + "unicodeVersion": "6.0", "digest": "bef294772b5ffccd6c061c19d60af66f61b248d78705faf347ade9ebfca2b46d" }, - { - "name": "iq", - "unicode": "1F1EE-1F1F6", - "digest": "bef294772b5ffccd6c061c19d60af66f61b248d78705faf347ade9ebfca2b46d" - }, - { - "name": "flag_ir", - "unicode": "1F1EE-1F1F7", - "digest": "d4faca93577a5546330ab6a09252307e19fb420d89912c0b48ceb90bf409d48e" - }, - { - "name": "ir", - "unicode": "1F1EE-1F1F7", + "flag_ir": { + "category": "flags", + "moji": "🇮🇷", + "unicodeVersion": "6.0", "digest": "d4faca93577a5546330ab6a09252307e19fb420d89912c0b48ceb90bf409d48e" }, - { - "name": "flag_is", - "unicode": "1F1EE-1F1F8", + "flag_is": { + "category": "flags", + "moji": "🇮🇸", + "unicodeVersion": "6.0", "digest": "b2fc04226b274009b4d99d92bcb72b255b534b6fd4b76d82dce1575ad975a456" }, - { - "name": "is", - "unicode": "1F1EE-1F1F8", - "digest": "b2fc04226b274009b4d99d92bcb72b255b534b6fd4b76d82dce1575ad975a456" - }, - { - "name": "flag_it", - "unicode": "1F1EE-1F1F9", + "flag_it": { + "category": "flags", + "moji": "🇮🇹", + "unicodeVersion": "6.0", "digest": "735760f193855d55460a0fb93dad55ff67253cab63176eceb90b9bde1faead1e" }, - { - "name": "it", - "unicode": "1F1EE-1F1F9", - "digest": "735760f193855d55460a0fb93dad55ff67253cab63176eceb90b9bde1faead1e" - }, - { - "name": "flag_je", - "unicode": "1F1EF-1F1EA", + "flag_je": { + "category": "flags", + "moji": "🇯🇪", + "unicodeVersion": "6.0", "digest": "671a487a60571d928d2abaf306d0a9ba50239ec54ada14ea29a9a99df658d3cc" }, - { - "name": "je", - "unicode": "1F1EF-1F1EA", - "digest": "671a487a60571d928d2abaf306d0a9ba50239ec54ada14ea29a9a99df658d3cc" - }, - { - "name": "flag_jm", - "unicode": "1F1EF-1F1F2", - "digest": "fb9047199d030b78fc0dcfc58d9b524fdb929238d922809da88147b7cebf4211" - }, - { - "name": "jm", - "unicode": "1F1EF-1F1F2", + "flag_jm": { + "category": "flags", + "moji": "🇯🇲", + "unicodeVersion": "6.0", "digest": "fb9047199d030b78fc0dcfc58d9b524fdb929238d922809da88147b7cebf4211" }, - { - "name": "flag_jo", - "unicode": "1F1EF-1F1F4", + "flag_jo": { + "category": "flags", + "moji": "🇯🇴", + "unicodeVersion": "6.0", "digest": "19f7d536d0293ebf3db49e05a158097cbde467115ef96523a0553808fd0b4178" }, - { - "name": "jo", - "unicode": "1F1EF-1F1F4", - "digest": "19f7d536d0293ebf3db49e05a158097cbde467115ef96523a0553808fd0b4178" - }, - { - "name": "flag_jp", - "unicode": "1F1EF-1F1F5", - "digest": "51e971f777fe481ca9f7e077ecb2ce252c3cc0086b76384e7b965cdc337f3f9e" - }, - { - "name": "jp", - "unicode": "1F1EF-1F1F5", + "flag_jp": { + "category": "flags", + "moji": "🇯🇵", + "unicodeVersion": "6.0", "digest": "51e971f777fe481ca9f7e077ecb2ce252c3cc0086b76384e7b965cdc337f3f9e" }, - { - "name": "flag_ke", - "unicode": "1F1F0-1F1EA", - "digest": "0cec8f068548cfd3e7a20c10af84f97ca415fd6f8ab8b50783bf982e77d7260e" - }, - { - "name": "ke", - "unicode": "1F1F0-1F1EA", + "flag_ke": { + "category": "flags", + "moji": "🇰🇪", + "unicodeVersion": "6.0", "digest": "0cec8f068548cfd3e7a20c10af84f97ca415fd6f8ab8b50783bf982e77d7260e" }, - { - "name": "flag_kg", - "unicode": "1F1F0-1F1EC", - "digest": "5803ea6ab028261923fd7570c670a50518c6f462a2fb4d463531b12c3e382e6f" - }, - { - "name": "kg", - "unicode": "1F1F0-1F1EC", + "flag_kg": { + "category": "flags", + "moji": "🇰🇬", + "unicodeVersion": "6.0", "digest": "5803ea6ab028261923fd7570c670a50518c6f462a2fb4d463531b12c3e382e6f" }, - { - "name": "flag_kh", - "unicode": "1F1F0-1F1ED", + "flag_kh": { + "category": "flags", + "moji": "🇰🇭", + "unicodeVersion": "6.0", "digest": "287d357afe47179853fd485fb102834ead145598ed892664fc62d245cac16080" }, - { - "name": "kh", - "unicode": "1F1F0-1F1ED", - "digest": "287d357afe47179853fd485fb102834ead145598ed892664fc62d245cac16080" - }, - { - "name": "flag_ki", - "unicode": "1F1F0-1F1EE", - "digest": "ae4aee0d9cd7a21d4e250d45a484f5f641acdab3d79b437337b25fe34a0b49b0" - }, - { - "name": "ki", - "unicode": "1F1F0-1F1EE", + "flag_ki": { + "category": "flags", + "moji": "🇰🇮", + "unicodeVersion": "6.0", "digest": "ae4aee0d9cd7a21d4e250d45a484f5f641acdab3d79b437337b25fe34a0b49b0" }, - { - "name": "flag_km", - "unicode": "1F1F0-1F1F2", - "digest": "2d1730acbf5421fd02bd5483e26a86d82ec2fa99f0ff75bfd728a9df7914ad3b" - }, - { - "name": "km", - "unicode": "1F1F0-1F1F2", + "flag_km": { + "category": "flags", + "moji": "🇰🇲", + "unicodeVersion": "6.0", "digest": "2d1730acbf5421fd02bd5483e26a86d82ec2fa99f0ff75bfd728a9df7914ad3b" }, - { - "name": "flag_kn", - "unicode": "1F1F0-1F1F3", - "digest": "b9ed979db9c6d243b00f61f19a9ec0f2c2390b2e5cace5ad61d9371dc8c670ac" - }, - { - "name": "kn", - "unicode": "1F1F0-1F1F3", + "flag_kn": { + "category": "flags", + "moji": "🇰🇳", + "unicodeVersion": "6.0", "digest": "b9ed979db9c6d243b00f61f19a9ec0f2c2390b2e5cace5ad61d9371dc8c670ac" }, - { - "name": "flag_kp", - "unicode": "1F1F0-1F1F5", - "digest": "1bab0b9cab8028a95ce7231ad8d88ebcd31601cfa321284bba017ead47f6c729" - }, - { - "name": "kp", - "unicode": "1F1F0-1F1F5", + "flag_kp": { + "category": "flags", + "moji": "🇰🇵", + "unicodeVersion": "6.0", "digest": "1bab0b9cab8028a95ce7231ad8d88ebcd31601cfa321284bba017ead47f6c729" }, - { - "name": "flag_kr", - "unicode": "1F1F0-1F1F7", - "digest": "33be8c09ebe273e203aa703cc827d52a6d9bf1699f5445bba13a77af2df45fa6" - }, - { - "name": "kr", - "unicode": "1F1F0-1F1F7", + "flag_kr": { + "category": "flags", + "moji": "🇰🇷", + "unicodeVersion": "6.0", "digest": "33be8c09ebe273e203aa703cc827d52a6d9bf1699f5445bba13a77af2df45fa6" }, - { - "name": "flag_kw", - "unicode": "1F1F0-1F1FC", - "digest": "04d901a92ea55b13dc4983a9e3adb52dc89c9f3decee86fd06022aa902678b6d" - }, - { - "name": "kw", - "unicode": "1F1F0-1F1FC", + "flag_kw": { + "category": "flags", + "moji": "🇰🇼", + "unicodeVersion": "6.0", "digest": "04d901a92ea55b13dc4983a9e3adb52dc89c9f3decee86fd06022aa902678b6d" }, - { - "name": "flag_ky", - "unicode": "1F1F0-1F1FE", - "digest": "10f4d02f33cadd34da89de71a3b763809bad480cd9ae9d2ec000db026bd94cd1" - }, - { - "name": "ky", - "unicode": "1F1F0-1F1FE", + "flag_ky": { + "category": "flags", + "moji": "🇰🇾", + "unicodeVersion": "6.0", "digest": "10f4d02f33cadd34da89de71a3b763809bad480cd9ae9d2ec000db026bd94cd1" }, - { - "name": "flag_kz", - "unicode": "1F1F0-1F1FF", - "digest": "dfaff69a78cf635f7fad41bd5bdcc8003298454708a6178ba7348b1b40c360c1" - }, - { - "name": "kz", - "unicode": "1F1F0-1F1FF", + "flag_kz": { + "category": "flags", + "moji": "🇰🇿", + "unicodeVersion": "6.0", "digest": "dfaff69a78cf635f7fad41bd5bdcc8003298454708a6178ba7348b1b40c360c1" }, - { - "name": "flag_la", - "unicode": "1F1F1-1F1E6", - "digest": "4fcfbdc694cf99ae3f832500cdcdedb88c444b6df88bc9b7141f4f26ba3d5bfd" - }, - { - "name": "la", - "unicode": "1F1F1-1F1E6", + "flag_la": { + "category": "flags", + "moji": "🇱🇦", + "unicodeVersion": "6.0", "digest": "4fcfbdc694cf99ae3f832500cdcdedb88c444b6df88bc9b7141f4f26ba3d5bfd" }, - { - "name": "flag_lb", - "unicode": "1F1F1-1F1E7", - "digest": "af4b1f784bea0ec7a712495491dffbd1152cc857a99fd433f76bfeb313819a62" - }, - { - "name": "lb", - "unicode": "1F1F1-1F1E7", + "flag_lb": { + "category": "flags", + "moji": "🇱🇧", + "unicodeVersion": "6.0", "digest": "af4b1f784bea0ec7a712495491dffbd1152cc857a99fd433f76bfeb313819a62" }, - { - "name": "flag_lc", - "unicode": "1F1F1-1F1E8", + "flag_lc": { + "category": "flags", + "moji": "🇱🇨", + "unicodeVersion": "6.0", "digest": "40784aa558b75d07ae499c004e2cc5d0b2efdfc3e5be705b5a9f6b70d681c396" }, - { - "name": "lc", - "unicode": "1F1F1-1F1E8", - "digest": "40784aa558b75d07ae499c004e2cc5d0b2efdfc3e5be705b5a9f6b70d681c396" - }, - { - "name": "flag_li", - "unicode": "1F1F1-1F1EE", - "digest": "c4eb4c43f457ce60ff9d046adb512c1d3462203403eeb595bff3ebc010ed6633" - }, - { - "name": "li", - "unicode": "1F1F1-1F1EE", + "flag_li": { + "category": "flags", + "moji": "🇱🇮", + "unicodeVersion": "6.0", "digest": "c4eb4c43f457ce60ff9d046adb512c1d3462203403eeb595bff3ebc010ed6633" }, - { - "name": "flag_lk", - "unicode": "1F1F1-1F1F0", + "flag_lk": { + "category": "flags", + "moji": "🇱🇰", + "unicodeVersion": "6.0", "digest": "a5285cdfdc3715fa3941f5f0eb03dc425969eaaf22c719c27ab4418628d09bc5" }, - { - "name": "lk", - "unicode": "1F1F1-1F1F0", - "digest": "a5285cdfdc3715fa3941f5f0eb03dc425969eaaf22c719c27ab4418628d09bc5" - }, - { - "name": "flag_lr", - "unicode": "1F1F1-1F1F7", - "digest": "ed04334264953b4da570db8c392b99d2fab4e0b7efc2331427016c6a08e818be" - }, - { - "name": "lr", - "unicode": "1F1F1-1F1F7", + "flag_lr": { + "category": "flags", + "moji": "🇱🇷", + "unicodeVersion": "6.0", "digest": "ed04334264953b4da570db8c392b99d2fab4e0b7efc2331427016c6a08e818be" }, - { - "name": "flag_ls", - "unicode": "1F1F1-1F1F8", + "flag_ls": { + "category": "flags", + "moji": "🇱🇸", + "unicodeVersion": "6.0", "digest": "cd56022106d027317cc9bf4c848758cf29ffe277ce71fdb9c1cf89ac4fd6e6db" }, - { - "name": "ls", - "unicode": "1F1F1-1F1F8", - "digest": "cd56022106d027317cc9bf4c848758cf29ffe277ce71fdb9c1cf89ac4fd6e6db" - }, - { - "name": "flag_lt", - "unicode": "1F1F1-1F1F9", - "digest": "3c4395b068e421100fd97a102f170cb8d5c093885eef7cb40d3faff4f4e47fe9" - }, - { - "name": "lt", - "unicode": "1F1F1-1F1F9", + "flag_lt": { + "category": "flags", + "moji": "🇱🇹", + "unicodeVersion": "6.0", "digest": "3c4395b068e421100fd97a102f170cb8d5c093885eef7cb40d3faff4f4e47fe9" }, - { - "name": "flag_lu", - "unicode": "1F1F1-1F1FA", + "flag_lu": { + "category": "flags", + "moji": "🇱🇺", + "unicodeVersion": "6.0", "digest": "df15a2c47eecad17e0cc169bdf0d31c6a51eb22de7ca4e70d2431359a33f930d" }, - { - "name": "lu", - "unicode": "1F1F1-1F1FA", - "digest": "df15a2c47eecad17e0cc169bdf0d31c6a51eb22de7ca4e70d2431359a33f930d" - }, - { - "name": "flag_lv", - "unicode": "1F1F1-1F1FB", + "flag_lv": { + "category": "flags", + "moji": "🇱🇻", + "unicodeVersion": "6.0", "digest": "9b53c6ce23287935200da8ca8a8af78013a4b1572f9821e7e1724cbad248e7e2" }, - { - "name": "lv", - "unicode": "1F1F1-1F1FB", - "digest": "9b53c6ce23287935200da8ca8a8af78013a4b1572f9821e7e1724cbad248e7e2" - }, - { - "name": "flag_ly", - "unicode": "1F1F1-1F1FE", + "flag_ly": { + "category": "flags", + "moji": "🇱🇾", + "unicodeVersion": "6.0", "digest": "42efa9f3526ef006d6723fa17538a98ab9556ae25f14df1b06d21361bf7e1a44" }, - { - "name": "ly", - "unicode": "1F1F1-1F1FE", - "digest": "42efa9f3526ef006d6723fa17538a98ab9556ae25f14df1b06d21361bf7e1a44" - }, - { - "name": "flag_ma", - "unicode": "1F1F2-1F1E6", - "digest": "96c07296cfd7aa1cb642faed8ace26744105b81ca880157a4ef4caee0befe26e" - }, - { - "name": "ma", - "unicode": "1F1F2-1F1E6", + "flag_ma": { + "category": "flags", + "moji": "🇲🇦", + "unicodeVersion": "6.0", "digest": "96c07296cfd7aa1cb642faed8ace26744105b81ca880157a4ef4caee0befe26e" }, - { - "name": "flag_mc", - "unicode": "1F1F2-1F1E8", + "flag_mc": { + "category": "flags", + "moji": "🇲🇨", + "unicodeVersion": "6.0", "digest": "6b44608842fe849ae2b4bae5eb87ccd436459a427051dfda25080196273d4b9f" }, - { - "name": "mc", - "unicode": "1F1F2-1F1E8", - "digest": "6b44608842fe849ae2b4bae5eb87ccd436459a427051dfda25080196273d4b9f" - }, - { - "name": "flag_md", - "unicode": "1F1F2-1F1E9", - "digest": "78c7b01c698873a9129d52ba38b3eb4cfc683ef2ae10b7b922b17c07f1c938c8" - }, - { - "name": "md", - "unicode": "1F1F2-1F1E9", + "flag_md": { + "category": "flags", + "moji": "🇲🇩", + "unicodeVersion": "6.0", "digest": "78c7b01c698873a9129d52ba38b3eb4cfc683ef2ae10b7b922b17c07f1c938c8" }, - { - "name": "flag_me", - "unicode": "1F1F2-1F1EA", - "digest": "01aa0f9df89302edc4ae319b5dd78069ba8807c3f38cc7bfe01bff67c8efd416" - }, - { - "name": "me", - "unicode": "1F1F2-1F1EA", + "flag_me": { + "category": "flags", + "moji": "🇲🇪", + "unicodeVersion": "6.0", "digest": "01aa0f9df89302edc4ae319b5dd78069ba8807c3f38cc7bfe01bff67c8efd416" }, - { - "name": "flag_mf", - "unicode": "1F1F2-1F1EB", - "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9" - }, - { - "name": "mf", - "unicode": "1F1F2-1F1EB", + "flag_mf": { + "category": "flags", + "moji": "🇲🇫", + "unicodeVersion": "6.0", "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9" }, - { - "name": "flag_mg", - "unicode": "1F1F2-1F1EC", + "flag_mg": { + "category": "flags", + "moji": "🇲🇬", + "unicodeVersion": "6.0", "digest": "56ebcd2a2e144d656d3b38a62595138fe6e50f9c1144f70b0a120cce7a72eb5b" }, - { - "name": "mg", - "unicode": "1F1F2-1F1EC", - "digest": "56ebcd2a2e144d656d3b38a62595138fe6e50f9c1144f70b0a120cce7a72eb5b" - }, - { - "name": "flag_mh", - "unicode": "1F1F2-1F1ED", - "digest": "008660adc4c2e4d04830498988184d1ef8a372a6c085da369a94ee6b820dbbb7" - }, - { - "name": "mh", - "unicode": "1F1F2-1F1ED", + "flag_mh": { + "category": "flags", + "moji": "🇲🇭", + "unicodeVersion": "6.0", "digest": "008660adc4c2e4d04830498988184d1ef8a372a6c085da369a94ee6b820dbbb7" }, - { - "name": "flag_mk", - "unicode": "1F1F2-1F1F0", - "digest": "f3c4c5106ace81c21fc0c6a7cc5c5e04e9453468fbc6ccbc851bb8dd61ff237f" - }, - { - "name": "mk", - "unicode": "1F1F2-1F1F0", + "flag_mk": { + "category": "flags", + "moji": "🇲🇰", + "unicodeVersion": "6.0", "digest": "f3c4c5106ace81c21fc0c6a7cc5c5e04e9453468fbc6ccbc851bb8dd61ff237f" }, - { - "name": "flag_ml", - "unicode": "1F1F2-1F1F1", + "flag_ml": { + "category": "flags", + "moji": "🇲🇱", + "unicodeVersion": "6.0", "digest": "e70a6b30e46adc2e19684308a848fef2c3ad76e2cac4bb493ee3270ad39f9d1b" }, - { - "name": "ml", - "unicode": "1F1F2-1F1F1", - "digest": "e70a6b30e46adc2e19684308a848fef2c3ad76e2cac4bb493ee3270ad39f9d1b" - }, - { - "name": "flag_mm", - "unicode": "1F1F2-1F1F2", - "digest": "720f5d38887202ba049cd5a46c183679be6a01f169d99e6e656c73b515793a7d" - }, - { - "name": "mm", - "unicode": "1F1F2-1F1F2", + "flag_mm": { + "category": "flags", + "moji": "🇲🇲", + "unicodeVersion": "6.0", "digest": "720f5d38887202ba049cd5a46c183679be6a01f169d99e6e656c73b515793a7d" }, - { - "name": "flag_mn", - "unicode": "1F1F2-1F1F3", + "flag_mn": { + "category": "flags", + "moji": "🇲🇳", + "unicodeVersion": "6.0", "digest": "5f0fd6fcb2ed73a5a6d9396c3703612503c1f16283bbb4e9362a1c8324b762ad" }, - { - "name": "mn", - "unicode": "1F1F2-1F1F3", - "digest": "5f0fd6fcb2ed73a5a6d9396c3703612503c1f16283bbb4e9362a1c8324b762ad" - }, - { - "name": "flag_mo", - "unicode": "1F1F2-1F1F4", - "digest": "fc2a9e7323867cf195f551e59afdab778c56b84c96af28c20207c9870caa2c39" - }, - { - "name": "mo", - "unicode": "1F1F2-1F1F4", + "flag_mo": { + "category": "flags", + "moji": "🇲🇴", + "unicodeVersion": "6.0", "digest": "fc2a9e7323867cf195f551e59afdab778c56b84c96af28c20207c9870caa2c39" }, - { - "name": "flag_mp", - "unicode": "1F1F2-1F1F5", + "flag_mp": { + "category": "flags", + "moji": "🇲🇵", + "unicodeVersion": "6.0", "digest": "ddce3be9d72914240c42e1b97ea97af01016d0a3879999cb0e447552682c06ba" }, - { - "name": "mp", - "unicode": "1F1F2-1F1F5", - "digest": "ddce3be9d72914240c42e1b97ea97af01016d0a3879999cb0e447552682c06ba" - }, - { - "name": "flag_mq", - "unicode": "1F1F2-1F1F6", - "digest": "888f455b1322d6fb83dc9f469f5505fea3dd6ece77d17d0d7345319c3ebcec0e" - }, - { - "name": "mq", - "unicode": "1F1F2-1F1F6", + "flag_mq": { + "category": "flags", + "moji": "🇲🇶", + "unicodeVersion": "6.0", "digest": "888f455b1322d6fb83dc9f469f5505fea3dd6ece77d17d0d7345319c3ebcec0e" }, - { - "name": "flag_mr", - "unicode": "1F1F2-1F1F7", + "flag_mr": { + "category": "flags", + "moji": "🇲🇷", + "unicodeVersion": "6.0", "digest": "72621914c92dd9c9f3ac9973ee3589583bfe42b841cdd35f47af75e2f629726c" }, - { - "name": "mr", - "unicode": "1F1F2-1F1F7", - "digest": "72621914c92dd9c9f3ac9973ee3589583bfe42b841cdd35f47af75e2f629726c" - }, - { - "name": "flag_ms", - "unicode": "1F1F2-1F1F8", - "digest": "5944996295132f41ec55261ff7927518bd47aec95d274a6ff257c357b43657bc" - }, - { - "name": "ms", - "unicode": "1F1F2-1F1F8", + "flag_ms": { + "category": "flags", + "moji": "🇲🇸", + "unicodeVersion": "6.0", "digest": "5944996295132f41ec55261ff7927518bd47aec95d274a6ff257c357b43657bc" }, - { - "name": "flag_mt", - "unicode": "1F1F2-1F1F9", + "flag_mt": { + "category": "flags", + "moji": "🇲🇹", + "unicodeVersion": "6.0", "digest": "95f0550e8823441a4e69b26c540baea94f3ddcc282100fd0239021c00df0b469" }, - { - "name": "mt", - "unicode": "1F1F2-1F1F9", - "digest": "95f0550e8823441a4e69b26c540baea94f3ddcc282100fd0239021c00df0b469" - }, - { - "name": "flag_mu", - "unicode": "1F1F2-1F1FA", - "digest": "5fda78a6df0ea7f5cac5fb4c8fd68529c14c5e15bac4e0b167493cb6ac459253" - }, - { - "name": "mu", - "unicode": "1F1F2-1F1FA", + "flag_mu": { + "category": "flags", + "moji": "🇲🇺", + "unicodeVersion": "6.0", "digest": "5fda78a6df0ea7f5cac5fb4c8fd68529c14c5e15bac4e0b167493cb6ac459253" }, - { - "name": "flag_mv", - "unicode": "1F1F2-1F1FB", + "flag_mv": { + "category": "flags", + "moji": "🇲🇻", + "unicodeVersion": "6.0", "digest": "f75c8f6fd3a68f2944a04c833c649d4b576997f491100cf3f3160fe77117fabb" }, - { - "name": "mv", - "unicode": "1F1F2-1F1FB", - "digest": "f75c8f6fd3a68f2944a04c833c649d4b576997f491100cf3f3160fe77117fabb" - }, - { - "name": "flag_mw", - "unicode": "1F1F2-1F1FC", - "digest": "d46b484a97e5b90b6b259f8de1712b553f93f0dfb6391209200358bb9429ebf5" - }, - { - "name": "mw", - "unicode": "1F1F2-1F1FC", + "flag_mw": { + "category": "flags", + "moji": "🇲🇼", + "unicodeVersion": "6.0", "digest": "d46b484a97e5b90b6b259f8de1712b553f93f0dfb6391209200358bb9429ebf5" }, - { - "name": "flag_mx", - "unicode": "1F1F2-1F1FD", + "flag_mx": { + "category": "flags", + "moji": "🇲🇽", + "unicodeVersion": "6.0", "digest": "dc57c10307fc0aa09bd7fcd25ee0fca561f3b382276faa8432a927c1baea53fd" }, - { - "name": "mx", - "unicode": "1F1F2-1F1FD", - "digest": "dc57c10307fc0aa09bd7fcd25ee0fca561f3b382276faa8432a927c1baea53fd" - }, - { - "name": "flag_my", - "unicode": "1F1F2-1F1FE", - "digest": "15ca00660a1eb0096fdaa00b85a7b95fcf192bf2ee4781ba72c36d2d2cb015ef" - }, - { - "name": "my", - "unicode": "1F1F2-1F1FE", + "flag_my": { + "category": "flags", + "moji": "🇲🇾", + "unicodeVersion": "6.0", "digest": "15ca00660a1eb0096fdaa00b85a7b95fcf192bf2ee4781ba72c36d2d2cb015ef" }, - { - "name": "flag_mz", - "unicode": "1F1F2-1F1FF", + "flag_mz": { + "category": "flags", + "moji": "🇲🇿", + "unicodeVersion": "6.0", "digest": "0c8605a9319dcf86672a833b4c4d6acea5f6aa25a3f8e1dfac78fbf7c452ba97" }, - { - "name": "mz", - "unicode": "1F1F2-1F1FF", - "digest": "0c8605a9319dcf86672a833b4c4d6acea5f6aa25a3f8e1dfac78fbf7c452ba97" - }, - { - "name": "flag_na", - "unicode": "1F1F3-1F1E6", + "flag_na": { + "category": "flags", + "moji": "🇳🇦", + "unicodeVersion": "6.0", "digest": "e63cde5ee49d3ada1e33d2ab15dc24fbb129b90d65b6fd1d7c07455f71a53601" }, - { - "name": "na", - "unicode": "1F1F3-1F1E6", - "digest": "e63cde5ee49d3ada1e33d2ab15dc24fbb129b90d65b6fd1d7c07455f71a53601" - }, - { - "name": "flag_nc", - "unicode": "1F1F3-1F1E8", + "flag_nc": { + "category": "flags", + "moji": "🇳🇨", + "unicodeVersion": "6.0", "digest": "a4a350ce7404ba7bdda9a341e7a48fcfe16312be4964b1bd6eed7115acd2e329" }, - { - "name": "nc", - "unicode": "1F1F3-1F1E8", - "digest": "a4a350ce7404ba7bdda9a341e7a48fcfe16312be4964b1bd6eed7115acd2e329" - }, - { - "name": "flag_ne", - "unicode": "1F1F3-1F1EA", - "digest": "6b32483b4445bc52855509f618c570b9c9606de5649e4878b71b44ff2acbc9fd" - }, - { - "name": "ne", - "unicode": "1F1F3-1F1EA", + "flag_ne": { + "category": "flags", + "moji": "🇳🇪", + "unicodeVersion": "6.0", "digest": "6b32483b4445bc52855509f618c570b9c9606de5649e4878b71b44ff2acbc9fd" }, - { - "name": "flag_nf", - "unicode": "1F1F3-1F1EB", + "flag_nf": { + "category": "flags", + "moji": "🇳🇫", + "unicodeVersion": "6.0", "digest": "96b1ec33acbd2b1ffe42703c11a2a633b036e6779849b0e6fa8f399167820584" }, - { - "name": "nf", - "unicode": "1F1F3-1F1EB", - "digest": "96b1ec33acbd2b1ffe42703c11a2a633b036e6779849b0e6fa8f399167820584" - }, - { - "name": "flag_ng", - "unicode": "1F1F3-1F1EC", - "digest": "f97d0630cbfa5e75440251df7529a67b58c22598643390cbeea82fb04a1cd956" - }, - { - "name": "nigeria", - "unicode": "1F1F3-1F1EC", + "flag_ng": { + "category": "flags", + "moji": "🇳🇬", + "unicodeVersion": "6.0", "digest": "f97d0630cbfa5e75440251df7529a67b58c22598643390cbeea82fb04a1cd956" }, - { - "name": "flag_ni", - "unicode": "1F1F3-1F1EE", - "digest": "c52fb5f9134122a91defa75425be2c6b3c909e051d546244e0e7bdf5f9ee1710" - }, - { - "name": "ni", - "unicode": "1F1F3-1F1EE", + "flag_ni": { + "category": "flags", + "moji": "🇳🇮", + "unicodeVersion": "6.0", "digest": "c52fb5f9134122a91defa75425be2c6b3c909e051d546244e0e7bdf5f9ee1710" }, - { - "name": "flag_nl", - "unicode": "1F1F3-1F1F1", - "digest": "b8918f9c0c92513aa0ec6ba6cee5448270168cbe6f0a970fb06e7ceb9f52ec71" - }, - { - "name": "nl", - "unicode": "1F1F3-1F1F1", + "flag_nl": { + "category": "flags", + "moji": "🇳🇱", + "unicodeVersion": "6.0", "digest": "b8918f9c0c92513aa0ec6ba6cee5448270168cbe6f0a970fb06e7ceb9f52ec71" }, - { - "name": "flag_no", - "unicode": "1F1F3-1F1F4", + "flag_no": { + "category": "flags", + "moji": "🇳🇴", + "unicodeVersion": "6.0", "digest": "05ce84095f8d93407d611b39d8b6a67fd9f11df6cfab7a185bcb4eec186d85ef" }, - { - "name": "no", - "unicode": "1F1F3-1F1F4", - "digest": "05ce84095f8d93407d611b39d8b6a67fd9f11df6cfab7a185bcb4eec186d85ef" - }, - { - "name": "flag_np", - "unicode": "1F1F3-1F1F5", - "digest": "cc41c2f97ec2b38fe5781d553792f6aab5d37cc3be02586f361fe89d12683bee" - }, - { - "name": "np", - "unicode": "1F1F3-1F1F5", + "flag_np": { + "category": "flags", + "moji": "🇳🇵", + "unicodeVersion": "6.0", "digest": "cc41c2f97ec2b38fe5781d553792f6aab5d37cc3be02586f361fe89d12683bee" }, - { - "name": "flag_nr", - "unicode": "1F1F3-1F1F7", - "digest": "7837edf59ec33a25380d76afea5f04cfcab4f17df4e33fca0dcaacb517c5cbec" - }, - { - "name": "nr", - "unicode": "1F1F3-1F1F7", + "flag_nr": { + "category": "flags", + "moji": "🇳🇷", + "unicodeVersion": "6.0", "digest": "7837edf59ec33a25380d76afea5f04cfcab4f17df4e33fca0dcaacb517c5cbec" }, - { - "name": "flag_nu", - "unicode": "1F1F3-1F1FA", - "digest": "fd9ab45c6f32bc4da47542392e5beba73ddac302a4a9a00e6deedc913a4c087d" - }, - { - "name": "nu", - "unicode": "1F1F3-1F1FA", + "flag_nu": { + "category": "flags", + "moji": "🇳🇺", + "unicodeVersion": "6.0", "digest": "fd9ab45c6f32bc4da47542392e5beba73ddac302a4a9a00e6deedc913a4c087d" }, - { - "name": "flag_nz", - "unicode": "1F1F3-1F1FF", - "digest": "0719830dcca400cefb30ce399bb03f49dd84c9a98f7d6a28270f9278e2a7af75" - }, - { - "name": "nz", - "unicode": "1F1F3-1F1FF", + "flag_nz": { + "category": "flags", + "moji": "🇳🇿", + "unicodeVersion": "6.0", "digest": "0719830dcca400cefb30ce399bb03f49dd84c9a98f7d6a28270f9278e2a7af75" }, - { - "name": "flag_om", - "unicode": "1F1F4-1F1F2", - "digest": "3f9039becd52e3454fdf7611cdb0d7fb1196e053eea29ef87daab6c21a94f1ee" - }, - { - "name": "om", - "unicode": "1F1F4-1F1F2", + "flag_om": { + "category": "flags", + "moji": "🇴🇲", + "unicodeVersion": "6.0", "digest": "3f9039becd52e3454fdf7611cdb0d7fb1196e053eea29ef87daab6c21a94f1ee" }, - { - "name": "flag_pa", - "unicode": "1F1F5-1F1E6", - "digest": "1adf0e5d4084e072aa44bd9978829e77546e0be75785e9be69f92e326bd714a7" - }, - { - "name": "pa", - "unicode": "1F1F5-1F1E6", + "flag_pa": { + "category": "flags", + "moji": "🇵🇦", + "unicodeVersion": "6.0", "digest": "1adf0e5d4084e072aa44bd9978829e77546e0be75785e9be69f92e326bd714a7" }, - { - "name": "flag_pe", - "unicode": "1F1F5-1F1EA", - "digest": "f8a4e257676f4ab8962ffe5509b8417777a8be2f0e9dc7735d3e014ff221aab1" - }, - { - "name": "pe", - "unicode": "1F1F5-1F1EA", + "flag_pe": { + "category": "flags", + "moji": "🇵🇪", + "unicodeVersion": "6.0", "digest": "f8a4e257676f4ab8962ffe5509b8417777a8be2f0e9dc7735d3e014ff221aab1" }, - { - "name": "flag_pf", - "unicode": "1F1F5-1F1EB", - "digest": "1ace6cc71d130cdf09246297740a911f14828c322e35330cc548ca5975015c23" - }, - { - "name": "pf", - "unicode": "1F1F5-1F1EB", + "flag_pf": { + "category": "flags", + "moji": "🇵🇫", + "unicodeVersion": "6.0", "digest": "1ace6cc71d130cdf09246297740a911f14828c322e35330cc548ca5975015c23" }, - { - "name": "flag_pg", - "unicode": "1F1F5-1F1EC", - "digest": "9c37719d9f51ef31fec0f898d38e522b4253cd00344408e3f660132514efddb7" - }, - { - "name": "pg", - "unicode": "1F1F5-1F1EC", + "flag_pg": { + "category": "flags", + "moji": "🇵🇬", + "unicodeVersion": "6.0", "digest": "9c37719d9f51ef31fec0f898d38e522b4253cd00344408e3f660132514efddb7" }, - { - "name": "flag_ph", - "unicode": "1F1F5-1F1ED", - "digest": "f1af628cf6d1d290cedef3d564b2386e2d6f14ba4426d3fefc0312cb8772e517" - }, - { - "name": "ph", - "unicode": "1F1F5-1F1ED", + "flag_ph": { + "category": "flags", + "moji": "🇵🇭", + "unicodeVersion": "6.0", "digest": "f1af628cf6d1d290cedef3d564b2386e2d6f14ba4426d3fefc0312cb8772e517" }, - { - "name": "flag_pk", - "unicode": "1F1F5-1F1F0", + "flag_pk": { + "category": "flags", + "moji": "🇵🇰", + "unicodeVersion": "6.0", "digest": "61c77f73d2a10a5acb289fadfe0d25d1a1c343e1223bd802099ff4e0e9356521" }, - { - "name": "pk", - "unicode": "1F1F5-1F1F0", - "digest": "61c77f73d2a10a5acb289fadfe0d25d1a1c343e1223bd802099ff4e0e9356521" - }, - { - "name": "flag_pl", - "unicode": "1F1F5-1F1F1", - "digest": "38c2c8618446e1f72cf983ab33e736d943f0db7c4cce52a187299e8cec2ea895" - }, - { - "name": "pl", - "unicode": "1F1F5-1F1F1", + "flag_pl": { + "category": "flags", + "moji": "🇵🇱", + "unicodeVersion": "6.0", "digest": "38c2c8618446e1f72cf983ab33e736d943f0db7c4cce52a187299e8cec2ea895" }, - { - "name": "flag_pm", - "unicode": "1F1F5-1F1F2", + "flag_pm": { + "category": "flags", + "moji": "🇵🇲", + "unicodeVersion": "6.0", "digest": "656be9ea1a79c3885a759c7ce353d338345a198d7939556949affaf5490cb644" }, - { - "name": "pm", - "unicode": "1F1F5-1F1F2", - "digest": "656be9ea1a79c3885a759c7ce353d338345a198d7939556949affaf5490cb644" - }, - { - "name": "flag_pn", - "unicode": "1F1F5-1F1F3", - "digest": "2792260d8087ab0253b1214c1420f0160ab2eef9afe7315f9e7ff0b87cd15d72" - }, - { - "name": "pn", - "unicode": "1F1F5-1F1F3", + "flag_pn": { + "category": "flags", + "moji": "🇵🇳", + "unicodeVersion": "6.0", "digest": "2792260d8087ab0253b1214c1420f0160ab2eef9afe7315f9e7ff0b87cd15d72" }, - { - "name": "flag_pr", - "unicode": "1F1F5-1F1F7", + "flag_pr": { + "category": "flags", + "moji": "🇵🇷", + "unicodeVersion": "6.0", "digest": "c4cfa1f2201dcda9de310a8247e6ce32d2798ae426a14dd70a9ebb00a2804d46" }, - { - "name": "pr", - "unicode": "1F1F5-1F1F7", - "digest": "c4cfa1f2201dcda9de310a8247e6ce32d2798ae426a14dd70a9ebb00a2804d46" - }, - { - "name": "flag_ps", - "unicode": "1F1F5-1F1F8", - "digest": "197f2ec6294bf0ee4a08cf2f2d1e237ee867c98b3085454a3f42abc955eeb289" - }, - { - "name": "ps", - "unicode": "1F1F5-1F1F8", + "flag_ps": { + "category": "flags", + "moji": "🇵🇸", + "unicodeVersion": "6.0", "digest": "197f2ec6294bf0ee4a08cf2f2d1e237ee867c98b3085454a3f42abc955eeb289" }, - { - "name": "flag_pt", - "unicode": "1F1F5-1F1F9", + "flag_pt": { + "category": "flags", + "moji": "🇵🇹", + "unicodeVersion": "6.0", "digest": "86a50827963756b5bf471ed9df5b3f2a2058b4c5d778a303414b6b0556e2082b" }, - { - "name": "pt", - "unicode": "1F1F5-1F1F9", - "digest": "86a50827963756b5bf471ed9df5b3f2a2058b4c5d778a303414b6b0556e2082b" - }, - { - "name": "flag_pw", - "unicode": "1F1F5-1F1FC", - "digest": "a6321c47a0cd188fbfdf3b55f17a7170c63080d28d50e4f5463eb1ee09af2412" - }, - { - "name": "pw", - "unicode": "1F1F5-1F1FC", + "flag_pw": { + "category": "flags", + "moji": "🇵🇼", + "unicodeVersion": "6.0", "digest": "a6321c47a0cd188fbfdf3b55f17a7170c63080d28d50e4f5463eb1ee09af2412" }, - { - "name": "flag_py", - "unicode": "1F1F5-1F1FE", + "flag_py": { + "category": "flags", + "moji": "🇵🇾", + "unicodeVersion": "6.0", "digest": "1a169e8d8703c510c5a2265b57dbed2f811b03ec375bcb341ab4cd0b100a9dd6" }, - { - "name": "py", - "unicode": "1F1F5-1F1FE", - "digest": "1a169e8d8703c510c5a2265b57dbed2f811b03ec375bcb341ab4cd0b100a9dd6" - }, - { - "name": "flag_qa", - "unicode": "1F1F6-1F1E6", - "digest": "de6283965cd98a244b7fa6288174f9ff0d8feb497f191f2e4ab3b690138a3d5d" - }, - { - "name": "qa", - "unicode": "1F1F6-1F1E6", + "flag_qa": { + "category": "flags", + "moji": "🇶🇦", + "unicodeVersion": "6.0", "digest": "de6283965cd98a244b7fa6288174f9ff0d8feb497f191f2e4ab3b690138a3d5d" }, - { - "name": "flag_re", - "unicode": "1F1F7-1F1EA", + "flag_re": { + "category": "flags", + "moji": "🇷🇪", + "unicodeVersion": "6.0", "digest": "260e1b97abc1562e5a73d7e53652ffed8059fc9b1c969741c466f48ec6ab0e80" }, - { - "name": "re", - "unicode": "1F1F7-1F1EA", - "digest": "260e1b97abc1562e5a73d7e53652ffed8059fc9b1c969741c466f48ec6ab0e80" - }, - { - "name": "flag_ro", - "unicode": "1F1F7-1F1F4", - "digest": "6d648e03955fa2a9fd2bad6f60ec96d3e20ee57f5855f3721a4d4e0c8e99f95c" - }, - { - "name": "ro", - "unicode": "1F1F7-1F1F4", + "flag_ro": { + "category": "flags", + "moji": "🇷🇴", + "unicodeVersion": "6.0", "digest": "6d648e03955fa2a9fd2bad6f60ec96d3e20ee57f5855f3721a4d4e0c8e99f95c" }, - { - "name": "flag_rs", - "unicode": "1F1F7-1F1F8", + "flag_rs": { + "category": "flags", + "moji": "🇷🇸", + "unicodeVersion": "6.0", "digest": "95cd5e197ed364e403eeb7f1d18a83487d89166910ba8119ea994e5e19d6a7ee" }, - { - "name": "rs", - "unicode": "1F1F7-1F1F8", - "digest": "95cd5e197ed364e403eeb7f1d18a83487d89166910ba8119ea994e5e19d6a7ee" - }, - { - "name": "flag_ru", - "unicode": "1F1F7-1F1FA", - "digest": "a4a81617a59d9eaf3c526431ca6f90ed334a7c1f516bf70cbd3f1fdc6e6103d7" - }, - { - "name": "ru", - "unicode": "1F1F7-1F1FA", + "flag_ru": { + "category": "flags", + "moji": "🇷🇺", + "unicodeVersion": "6.0", "digest": "a4a81617a59d9eaf3c526431ca6f90ed334a7c1f516bf70cbd3f1fdc6e6103d7" }, - { - "name": "flag_rw", - "unicode": "1F1F7-1F1FC", + "flag_rw": { + "category": "flags", + "moji": "🇷🇼", + "unicodeVersion": "6.0", "digest": "7a369f60db0876ffef111c319a3e8c9eaed620c875c51b98ed9ad5207b836dca" }, - { - "name": "rw", - "unicode": "1F1F7-1F1FC", - "digest": "7a369f60db0876ffef111c319a3e8c9eaed620c875c51b98ed9ad5207b836dca" - }, - { - "name": "flag_sa", - "unicode": "1F1F8-1F1E6", + "flag_sa": { + "category": "flags", + "moji": "🇸🇦", + "unicodeVersion": "6.0", "digest": "b249fbfd7ed415943f60bbd841965cf721979f960ccbe09396aebac1eca913d7" }, - { - "name": "saudiarabia", - "unicode": "1F1F8-1F1E6", - "digest": "b249fbfd7ed415943f60bbd841965cf721979f960ccbe09396aebac1eca913d7" - }, - { - "name": "saudi", - "unicode": "1F1F8-1F1E6", - "digest": "b249fbfd7ed415943f60bbd841965cf721979f960ccbe09396aebac1eca913d7" - }, - { - "name": "flag_sb", - "unicode": "1F1F8-1F1E7", - "digest": "526b411260024ea7b6ea6c47f2549345c6cc6960e9a29bfa9aaec0772664d2dc" - }, - { - "name": "sb", - "unicode": "1F1F8-1F1E7", + "flag_sb": { + "category": "flags", + "moji": "🇸🇧", + "unicodeVersion": "6.0", "digest": "526b411260024ea7b6ea6c47f2549345c6cc6960e9a29bfa9aaec0772664d2dc" }, - { - "name": "flag_sc", - "unicode": "1F1F8-1F1E8", + "flag_sc": { + "category": "flags", + "moji": "🇸🇨", + "unicodeVersion": "6.0", "digest": "d036b0d068745926120eaf746fa2e4433306e2e14c6b540d0cd6265e34471056" }, - { - "name": "sc", - "unicode": "1F1F8-1F1E8", - "digest": "d036b0d068745926120eaf746fa2e4433306e2e14c6b540d0cd6265e34471056" - }, - { - "name": "flag_sd", - "unicode": "1F1F8-1F1E9", - "digest": "889615bdb9b1f9c59c5f83ed4d22d54a0ed5dd5de263e729c58544cb06c55885" - }, - { - "name": "sd", - "unicode": "1F1F8-1F1E9", + "flag_sd": { + "category": "flags", + "moji": "🇸🇩", + "unicodeVersion": "6.0", "digest": "889615bdb9b1f9c59c5f83ed4d22d54a0ed5dd5de263e729c58544cb06c55885" }, - { - "name": "flag_se", - "unicode": "1F1F8-1F1EA", - "digest": "f471d80cfff340960a752c8c152ed4fb482df2a3712b0a56dfab31b9b806926a" - }, - { - "name": "se", - "unicode": "1F1F8-1F1EA", + "flag_se": { + "category": "flags", + "moji": "🇸🇪", + "unicodeVersion": "6.0", "digest": "f471d80cfff340960a752c8c152ed4fb482df2a3712b0a56dfab31b9b806926a" }, - { - "name": "flag_sg", - "unicode": "1F1F8-1F1EC", - "digest": "82f58a09f98593cc87e545f7e5c03d2aedaf82e54e73f71f58c18e994c3085ac" - }, - { - "name": "sg", - "unicode": "1F1F8-1F1EC", + "flag_sg": { + "category": "flags", + "moji": "🇸🇬", + "unicodeVersion": "6.0", "digest": "82f58a09f98593cc87e545f7e5c03d2aedaf82e54e73f71f58c18e994c3085ac" }, - { - "name": "flag_sh", - "unicode": "1F1F8-1F1ED", - "digest": "53914b1fa8c1b4f30bae6c1f6717f138fb4dbf482c3e20e33f7aea4ecfc0438d" - }, - { - "name": "sh", - "unicode": "1F1F8-1F1ED", + "flag_sh": { + "category": "flags", + "moji": "🇸🇭", + "unicodeVersion": "6.0", "digest": "53914b1fa8c1b4f30bae6c1f6717f138fb4dbf482c3e20e33f7aea4ecfc0438d" }, - { - "name": "flag_si", - "unicode": "1F1F8-1F1EE", - "digest": "65d491daa69f9a11cec9ccc4df3a669f12ef95a5c312137776d4472719940ba3" - }, - { - "name": "si", - "unicode": "1F1F8-1F1EE", + "flag_si": { + "category": "flags", + "moji": "🇸🇮", + "unicodeVersion": "6.0", "digest": "65d491daa69f9a11cec9ccc4df3a669f12ef95a5c312137776d4472719940ba3" }, - { - "name": "flag_sj", - "unicode": "1F1F8-1F1EF", - "digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6" - }, - { - "name": "sj", - "unicode": "1F1F8-1F1EF", + "flag_sj": { + "category": "flags", + "moji": "🇸🇯", + "unicodeVersion": "6.0", "digest": "bbf6daa6174c6fbbbf541c8274f31b6757c3a16007c2687015ea041fd1e2c6b6" }, - { - "name": "flag_sk", - "unicode": "1F1F8-1F1F0", + "flag_sk": { + "category": "flags", + "moji": "🇸🇰", + "unicodeVersion": "6.0", "digest": "d4fd03eca5bd3c9fb324ee04fae37c9a2d852bac8335369e3e720ef9b98fff36" }, - { - "name": "sk", - "unicode": "1F1F8-1F1F0", - "digest": "d4fd03eca5bd3c9fb324ee04fae37c9a2d852bac8335369e3e720ef9b98fff36" - }, - { - "name": "flag_sl", - "unicode": "1F1F8-1F1F1", - "digest": "1455c98c11c248623d82be5484ab1c4dcd1dae449adc393eb1aa2d8c74aa3f02" - }, - { - "name": "sl", - "unicode": "1F1F8-1F1F1", + "flag_sl": { + "category": "flags", + "moji": "🇸🇱", + "unicodeVersion": "6.0", "digest": "1455c98c11c248623d82be5484ab1c4dcd1dae449adc393eb1aa2d8c74aa3f02" }, - { - "name": "flag_sm", - "unicode": "1F1F8-1F1F2", + "flag_sm": { + "category": "flags", + "moji": "🇸🇲", + "unicodeVersion": "6.0", "digest": "daec5864ac50c625d7bf49d6c1a170a094cf0d1b9a0bdf62a62406e7ec500a94" }, - { - "name": "sm", - "unicode": "1F1F8-1F1F2", - "digest": "daec5864ac50c625d7bf49d6c1a170a094cf0d1b9a0bdf62a62406e7ec500a94" - }, - { - "name": "flag_sn", - "unicode": "1F1F8-1F1F3", - "digest": "4e4d43c467e5eb84c70f535f37f4f468319bd4b06c6ec3db3b54f69efdafd334" - }, - { - "name": "sn", - "unicode": "1F1F8-1F1F3", + "flag_sn": { + "category": "flags", + "moji": "🇸🇳", + "unicodeVersion": "6.0", "digest": "4e4d43c467e5eb84c70f535f37f4f468319bd4b06c6ec3db3b54f69efdafd334" }, - { - "name": "flag_so", - "unicode": "1F1F8-1F1F4", + "flag_so": { + "category": "flags", + "moji": "🇸🇴", + "unicodeVersion": "6.0", "digest": "c1434dca361563a8e3ba88f1ad19c3f6c9cbb8f3ebc17ce128fde2351ff67d0c" }, - { - "name": "so", - "unicode": "1F1F8-1F1F4", - "digest": "c1434dca361563a8e3ba88f1ad19c3f6c9cbb8f3ebc17ce128fde2351ff67d0c" - }, - { - "name": "flag_sr", - "unicode": "1F1F8-1F1F7", - "digest": "f3c6bfee2a052f03d56ba917b88595450cef111ffa9e92c7f39ef8c3c3bd12d1" - }, - { - "name": "sr", - "unicode": "1F1F8-1F1F7", + "flag_sr": { + "category": "flags", + "moji": "🇸🇷", + "unicodeVersion": "6.0", "digest": "f3c6bfee2a052f03d56ba917b88595450cef111ffa9e92c7f39ef8c3c3bd12d1" }, - { - "name": "flag_ss", - "unicode": "1F1F8-1F1F8", + "flag_ss": { + "category": "flags", + "moji": "🇸🇸", + "unicodeVersion": "6.0", "digest": "c0ed7e4f41206f5363e8ebdc6c3f28080e2f07d99e6fb73c1f6226d83310e69d" }, - { - "name": "ss", - "unicode": "1F1F8-1F1F8", - "digest": "c0ed7e4f41206f5363e8ebdc6c3f28080e2f07d99e6fb73c1f6226d83310e69d" - }, - { - "name": "flag_st", - "unicode": "1F1F8-1F1F9", + "flag_st": { + "category": "flags", + "moji": "🇸🇹", + "unicodeVersion": "6.0", "digest": "b022ae5d6885e28c6e9c83c17dd0c24c731d4f3d5773c49051768cdd4df51330" }, - { - "name": "st", - "unicode": "1F1F8-1F1F9", - "digest": "b022ae5d6885e28c6e9c83c17dd0c24c731d4f3d5773c49051768cdd4df51330" - }, - { - "name": "flag_sv", - "unicode": "1F1F8-1F1FB", + "flag_sv": { + "category": "flags", + "moji": "🇸🇻", + "unicodeVersion": "6.0", "digest": "5bafdd04d243ee3f3998f4ec0a3d03ff5a3975e771b1f94f89d7713193d7a242" }, - { - "name": "sv", - "unicode": "1F1F8-1F1FB", - "digest": "5bafdd04d243ee3f3998f4ec0a3d03ff5a3975e771b1f94f89d7713193d7a242" - }, - { - "name": "flag_sx", - "unicode": "1F1F8-1F1FD", - "digest": "fb92e9f514bcc2f7abbd4e146edde50f030c940c833f184618cbb48e56af22bd" - }, - { - "name": "sx", - "unicode": "1F1F8-1F1FD", + "flag_sx": { + "category": "flags", + "moji": "🇸🇽", + "unicodeVersion": "6.0", "digest": "fb92e9f514bcc2f7abbd4e146edde50f030c940c833f184618cbb48e56af22bd" }, - { - "name": "flag_sy", - "unicode": "1F1F8-1F1FE", + "flag_sy": { + "category": "flags", + "moji": "🇸🇾", + "unicodeVersion": "6.0", "digest": "ee330da644d4ce1fdba98be5eaab5054aed8d91a34ab617199a4b2b77f62a10b" }, - { - "name": "sy", - "unicode": "1F1F8-1F1FE", - "digest": "ee330da644d4ce1fdba98be5eaab5054aed8d91a34ab617199a4b2b77f62a10b" - }, - { - "name": "flag_sz", - "unicode": "1F1F8-1F1FF", - "digest": "7fe0c7429efd9682cc39e57f4bba8d1491d301643ba999d57c4e1bc37517ed64" - }, - { - "name": "sz", - "unicode": "1F1F8-1F1FF", + "flag_sz": { + "category": "flags", + "moji": "🇸🇿", + "unicodeVersion": "6.0", "digest": "7fe0c7429efd9682cc39e57f4bba8d1491d301643ba999d57c4e1bc37517ed64" }, - { - "name": "flag_ta", - "unicode": "1F1F9-1F1E6", - "digest": "b47e245a2708072a4dbaf190c9606baa4daf02e51627eeae6f20c3b4c95024c0" - }, - { - "name": "ta", - "unicode": "1F1F9-1F1E6", + "flag_ta": { + "category": "flags", + "moji": "🇹🇦", + "unicodeVersion": "6.0", "digest": "b47e245a2708072a4dbaf190c9606baa4daf02e51627eeae6f20c3b4c95024c0" }, - { - "name": "flag_tc", - "unicode": "1F1F9-1F1E8", - "digest": "18cfff14c2503b9d24c91c668583d4a14efb17657d800eca86ae49b547c9da5c" - }, - { - "name": "tc", - "unicode": "1F1F9-1F1E8", + "flag_tc": { + "category": "flags", + "moji": "🇹🇨", + "unicodeVersion": "6.0", "digest": "18cfff14c2503b9d24c91c668583d4a14efb17657d800eca86ae49b547c9da5c" }, - { - "name": "flag_td", - "unicode": "1F1F9-1F1E9", + "flag_td": { + "category": "flags", + "moji": "🇹🇩", + "unicodeVersion": "6.0", "digest": "73d1db3365736915c4cdf9ba9343d9fd78962203b60334e8f3724d4b330b17db" }, - { - "name": "td", - "unicode": "1F1F9-1F1E9", - "digest": "73d1db3365736915c4cdf9ba9343d9fd78962203b60334e8f3724d4b330b17db" - }, - { - "name": "flag_tf", - "unicode": "1F1F9-1F1EB", - "digest": "3bffeb4bc9ceb9cbb150de88e957b6e46509862ca7d616d5693124af084eb435" - }, - { - "name": "tf", - "unicode": "1F1F9-1F1EB", + "flag_tf": { + "category": "flags", + "moji": "🇹🇫", + "unicodeVersion": "6.0", "digest": "3bffeb4bc9ceb9cbb150de88e957b6e46509862ca7d616d5693124af084eb435" }, - { - "name": "flag_tg", - "unicode": "1F1F9-1F1EC", - "digest": "eb13a0e85baf73326f3ae3bc75e8406eca42000d7e42b0641120e64c0ab7ebaa" - }, - { - "name": "tg", - "unicode": "1F1F9-1F1EC", + "flag_tg": { + "category": "flags", + "moji": "🇹🇬", + "unicodeVersion": "6.0", "digest": "eb13a0e85baf73326f3ae3bc75e8406eca42000d7e42b0641120e64c0ab7ebaa" }, - { - "name": "flag_th", - "unicode": "1F1F9-1F1ED", - "digest": "a4e42efa4bb94e90f3a92ae9ce14affaacd3a142c1e0da40d8cc839500e771fd" - }, - { - "name": "th", - "unicode": "1F1F9-1F1ED", + "flag_th": { + "category": "flags", + "moji": "🇹🇭", + "unicodeVersion": "6.0", "digest": "a4e42efa4bb94e90f3a92ae9ce14affaacd3a142c1e0da40d8cc839500e771fd" }, - { - "name": "flag_tj", - "unicode": "1F1F9-1F1EF", - "digest": "ff926fa3e86e095683a61c4754355a5b4dd0ecb74393306bd791d130fd1a909d" - }, - { - "name": "tj", - "unicode": "1F1F9-1F1EF", + "flag_tj": { + "category": "flags", + "moji": "🇹🇯", + "unicodeVersion": "6.0", "digest": "ff926fa3e86e095683a61c4754355a5b4dd0ecb74393306bd791d130fd1a909d" }, - { - "name": "flag_tk", - "unicode": "1F1F9-1F1F0", - "digest": "3fa732d457ded6c83cd5f73d934f64c4e687eb0cde7c157d2fdcdccaf3b5fb52" - }, - { - "name": "tk", - "unicode": "1F1F9-1F1F0", + "flag_tk": { + "category": "flags", + "moji": "🇹🇰", + "unicodeVersion": "6.0", "digest": "3fa732d457ded6c83cd5f73d934f64c4e687eb0cde7c157d2fdcdccaf3b5fb52" }, - { - "name": "flag_tl", - "unicode": "1F1F9-1F1F1", - "digest": "0ec2a4d22fb832060693089e518bbe370a4e13bfc28748f110fc13726409f473" - }, - { - "name": "tl", - "unicode": "1F1F9-1F1F1", + "flag_tl": { + "category": "flags", + "moji": "🇹🇱", + "unicodeVersion": "6.0", "digest": "0ec2a4d22fb832060693089e518bbe370a4e13bfc28748f110fc13726409f473" }, - { - "name": "flag_tm", - "unicode": "1F1F9-1F1F2", - "digest": "b4724aa7ad13352f16a0936e61cbb85f0bd147583fc66597aff7e8ee7cf19c21" - }, - { - "name": "turkmenistan", - "unicode": "1F1F9-1F1F2", + "flag_tm": { + "category": "flags", + "moji": "🇹🇲", + "unicodeVersion": "6.0", "digest": "b4724aa7ad13352f16a0936e61cbb85f0bd147583fc66597aff7e8ee7cf19c21" }, - { - "name": "flag_tn", - "unicode": "1F1F9-1F1F3", + "flag_tn": { + "category": "flags", + "moji": "🇹🇳", + "unicodeVersion": "6.0", "digest": "5ab308ffdde40f504d6ee080817bbddbe4f3f4ddb71f508c75e0144a8c8044d9" }, - { - "name": "tn", - "unicode": "1F1F9-1F1F3", - "digest": "5ab308ffdde40f504d6ee080817bbddbe4f3f4ddb71f508c75e0144a8c8044d9" - }, - { - "name": "flag_to", - "unicode": "1F1F9-1F1F4", - "digest": "75b7e7198fa42f87986882b8ca251a229afcaa0a1188ae7b9f5ece87dc31a723" - }, - { - "name": "to", - "unicode": "1F1F9-1F1F4", + "flag_to": { + "category": "flags", + "moji": "🇹🇴", + "unicodeVersion": "6.0", "digest": "75b7e7198fa42f87986882b8ca251a229afcaa0a1188ae7b9f5ece87dc31a723" }, - { - "name": "flag_tr", - "unicode": "1F1F9-1F1F7", - "digest": "9cc48a8f8fa9c17c1627272f68d4740da0e7ce17a2cf8c6b5c08cc9b95e1390c" - }, - { - "name": "tr", - "unicode": "1F1F9-1F1F7", + "flag_tr": { + "category": "flags", + "moji": "🇹🇷", + "unicodeVersion": "6.0", "digest": "9cc48a8f8fa9c17c1627272f68d4740da0e7ce17a2cf8c6b5c08cc9b95e1390c" }, - { - "name": "flag_tt", - "unicode": "1F1F9-1F1F9", + "flag_tt": { + "category": "flags", + "moji": "🇹🇹", + "unicodeVersion": "6.0", "digest": "f9e63543121bb3cd2e41bc7b0c2c4ba662bc1cc0520b79fc4e201ec6456fdf59" }, - { - "name": "tt", - "unicode": "1F1F9-1F1F9", - "digest": "f9e63543121bb3cd2e41bc7b0c2c4ba662bc1cc0520b79fc4e201ec6456fdf59" - }, - { - "name": "flag_tv", - "unicode": "1F1F9-1F1FB", - "digest": "6431e5f06cc7995ae7208c429ecf39339b545854cb6d6b7447f465fe53614dfc" - }, - { - "name": "tuvalu", - "unicode": "1F1F9-1F1FB", + "flag_tv": { + "category": "flags", + "moji": "🇹🇻", + "unicodeVersion": "6.0", "digest": "6431e5f06cc7995ae7208c429ecf39339b545854cb6d6b7447f465fe53614dfc" }, - { - "name": "flag_tw", - "unicode": "1F1F9-1F1FC", + "flag_tw": { + "category": "flags", + "moji": "🇹🇼", + "unicodeVersion": "6.0", "digest": "8395ab3c6a595023b006518a5345ac3612f2893d3a8f011b7e5802414236b03c" }, - { - "name": "tw", - "unicode": "1F1F9-1F1FC", - "digest": "8395ab3c6a595023b006518a5345ac3612f2893d3a8f011b7e5802414236b03c" - }, - { - "name": "flag_tz", - "unicode": "1F1F9-1F1FF", - "digest": "716181733cd9ac7a8f51a9a64bc5d21020e8112f6768e8c49c4d651a3ee0b8a4" - }, - { - "name": "tz", - "unicode": "1F1F9-1F1FF", + "flag_tz": { + "category": "flags", + "moji": "🇹🇿", + "unicodeVersion": "6.0", "digest": "716181733cd9ac7a8f51a9a64bc5d21020e8112f6768e8c49c4d651a3ee0b8a4" }, - { - "name": "flag_ua", - "unicode": "1F1FA-1F1E6", + "flag_ua": { + "category": "flags", + "moji": "🇺🇦", + "unicodeVersion": "6.0", "digest": "304570736345e28734f5ff84a2b0481c2bb00bf29d9892bd749b57dec7741e30" }, - { - "name": "ua", - "unicode": "1F1FA-1F1E6", - "digest": "304570736345e28734f5ff84a2b0481c2bb00bf29d9892bd749b57dec7741e30" - }, - { - "name": "flag_ug", - "unicode": "1F1FA-1F1EC", - "digest": "a1bafb74c54ee8c92cb025b55aebdb6081eec3fda6a7f86f2ee14d1b801a8e9c" - }, - { - "name": "ug", - "unicode": "1F1FA-1F1EC", + "flag_ug": { + "category": "flags", + "moji": "🇺🇬", + "unicodeVersion": "6.0", "digest": "a1bafb74c54ee8c92cb025b55aebdb6081eec3fda6a7f86f2ee14d1b801a8e9c" }, - { - "name": "flag_um", - "unicode": "1F1FA-1F1F2", + "flag_um": { + "category": "flags", + "moji": "🇺🇲", + "unicodeVersion": "6.0", "digest": "b3c9ac72211f481f50cde09e10b92aa03b1ea90abf85418e60a35b84963273ee" }, - { - "name": "um", - "unicode": "1F1FA-1F1F2", - "digest": "b3c9ac72211f481f50cde09e10b92aa03b1ea90abf85418e60a35b84963273ee" - }, - { - "name": "flag_us", - "unicode": "1F1FA-1F1F8", - "digest": "da79f9af0a188178a82e7dc3a62298fa416f4cfbcae432838df1abebca5c0d63" - }, - { - "name": "us", - "unicode": "1F1FA-1F1F8", + "flag_us": { + "category": "flags", + "moji": "🇺🇸", + "unicodeVersion": "6.0", "digest": "da79f9af0a188178a82e7dc3a62298fa416f4cfbcae432838df1abebca5c0d63" }, - { - "name": "flag_uy", - "unicode": "1F1FA-1F1FE", + "flag_uy": { + "category": "flags", + "moji": "🇺🇾", + "unicodeVersion": "6.0", "digest": "8348e901d775722497ee911c9c9b4bd767710760c507630a67ecb6d47cc646c7" }, - { - "name": "uy", - "unicode": "1F1FA-1F1FE", - "digest": "8348e901d775722497ee911c9c9b4bd767710760c507630a67ecb6d47cc646c7" - }, - { - "name": "flag_uz", - "unicode": "1F1FA-1F1FF", - "digest": "2a1dc1e9469e01c58ea91f545ef3fe0bdfe5544a73a80407f8960d01b1e5db5c" - }, - { - "name": "uz", - "unicode": "1F1FA-1F1FF", + "flag_uz": { + "category": "flags", + "moji": "🇺🇿", + "unicodeVersion": "6.0", "digest": "2a1dc1e9469e01c58ea91f545ef3fe0bdfe5544a73a80407f8960d01b1e5db5c" }, - { - "name": "flag_va", - "unicode": "1F1FB-1F1E6", + "flag_va": { + "category": "flags", + "moji": "🇻🇦", + "unicodeVersion": "6.0", "digest": "0e8134ec94bff032bfc63b0b08587d5298c9b7f31edd5a5b35633ae911434e61" }, - { - "name": "va", - "unicode": "1F1FB-1F1E6", - "digest": "0e8134ec94bff032bfc63b0b08587d5298c9b7f31edd5a5b35633ae911434e61" - }, - { - "name": "flag_vc", - "unicode": "1F1FB-1F1E8", - "digest": "e0290e1be72c8939ee6c398f00a107703b21b97d91b9bf465e553ffbf00304a7" - }, - { - "name": "vc", - "unicode": "1F1FB-1F1E8", + "flag_vc": { + "category": "flags", + "moji": "🇻🇨", + "unicodeVersion": "6.0", "digest": "e0290e1be72c8939ee6c398f00a107703b21b97d91b9bf465e553ffbf00304a7" }, - { - "name": "flag_ve", - "unicode": "1F1FB-1F1EA", + "flag_ve": { + "category": "flags", + "moji": "🇻🇪", + "unicodeVersion": "6.0", "digest": "76a6a6c2353def1f984d1a6980831e63f3aea5af2201b574197834e7c203d57a" }, - { - "name": "ve", - "unicode": "1F1FB-1F1EA", - "digest": "76a6a6c2353def1f984d1a6980831e63f3aea5af2201b574197834e7c203d57a" - }, - { - "name": "flag_vg", - "unicode": "1F1FB-1F1EC", - "digest": "56fc9317b8dd62cccc60010819f8b895dd4569a9b06368a9250f815c39177b8a" - }, - { - "name": "vg", - "unicode": "1F1FB-1F1EC", + "flag_vg": { + "category": "flags", + "moji": "🇻🇬", + "unicodeVersion": "6.0", "digest": "56fc9317b8dd62cccc60010819f8b895dd4569a9b06368a9250f815c39177b8a" }, - { - "name": "flag_vi", - "unicode": "1F1FB-1F1EE", + "flag_vi": { + "category": "flags", + "moji": "🇻🇮", + "unicodeVersion": "6.0", "digest": "2526a3e13b8ccd301f0763580430898c227bd209e3ce482c7951140b28948375" }, - { - "name": "vi", - "unicode": "1F1FB-1F1EE", - "digest": "2526a3e13b8ccd301f0763580430898c227bd209e3ce482c7951140b28948375" - }, - { - "name": "flag_vn", - "unicode": "1F1FB-1F1F3", + "flag_vn": { + "category": "flags", + "moji": "🇻🇳", + "unicodeVersion": "6.0", "digest": "0cf6b9896bbe4da8ed7718d0abfd56cef1a8321e26f89d3ad1b48488eaffb7a5" }, - { - "name": "vn", - "unicode": "1F1FB-1F1F3", - "digest": "0cf6b9896bbe4da8ed7718d0abfd56cef1a8321e26f89d3ad1b48488eaffb7a5" - }, - { - "name": "flag_vu", - "unicode": "1F1FB-1F1FA", + "flag_vu": { + "category": "flags", + "moji": "🇻🇺", + "unicodeVersion": "6.0", "digest": "9dfa282ce1aafc62beacab76e1fc19a141c8bdeaa30898f69b083067b775d362" }, - { - "name": "vu", - "unicode": "1F1FB-1F1FA", - "digest": "9dfa282ce1aafc62beacab76e1fc19a141c8bdeaa30898f69b083067b775d362" - }, - { - "name": "flag_wf", - "unicode": "1F1FC-1F1EB", - "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9" - }, - { - "name": "wf", - "unicode": "1F1FC-1F1EB", + "flag_wf": { + "category": "flags", + "moji": "🇼🇫", + "unicodeVersion": "6.0", "digest": "a0124683aa88cd7da886da70c65796c5ad84eb3751e356e9b2aa8ac249cf0bf9" }, - { - "name": "flag_white", - "unicode": "1F3F3", + "flag_white": { + "category": "objects", + "moji": "🏳", + "unicodeVersion": "6.0", "digest": "d9be4b7ceb8309c48f88cfd07a9f7ce6758ea6e620e73293cf14baec03ca381c" }, - { - "name": "waving_white_flag", - "unicode": "1F3F3", - "digest": "d9be4b7ceb8309c48f88cfd07a9f7ce6758ea6e620e73293cf14baec03ca381c" - }, - { - "name": "flag_ws", - "unicode": "1F1FC-1F1F8", - "digest": "53addd0dc304a3c8893389ed227986ef2431828b8c071926aa09f9efd815b649" - }, - { - "name": "ws", - "unicode": "1F1FC-1F1F8", + "flag_ws": { + "category": "flags", + "moji": "🇼🇸", + "unicodeVersion": "6.0", "digest": "53addd0dc304a3c8893389ed227986ef2431828b8c071926aa09f9efd815b649" }, - { - "name": "flag_xk", - "unicode": "1F1FD-1F1F0", - "digest": "eba1a832e489e1c2734e773e685df5d128271fa5559d23c060e68be067bf6469" - }, - { - "name": "xk", - "unicode": "1F1FD-1F1F0", + "flag_xk": { + "category": "flags", + "moji": "🇽🇰", + "unicodeVersion": "6.0", "digest": "eba1a832e489e1c2734e773e685df5d128271fa5559d23c060e68be067bf6469" }, - { - "name": "flag_ye", - "unicode": "1F1FE-1F1EA", - "digest": "edfa14266785042b6d5fe0f64fafa630b16a3ee7d010501de7cc8554c959afb0" - }, - { - "name": "ye", - "unicode": "1F1FE-1F1EA", + "flag_ye": { + "category": "flags", + "moji": "🇾🇪", + "unicodeVersion": "6.0", "digest": "edfa14266785042b6d5fe0f64fafa630b16a3ee7d010501de7cc8554c959afb0" }, - { - "name": "flag_yt", - "unicode": "1F1FE-1F1F9", + "flag_yt": { + "category": "flags", + "moji": "🇾🇹", + "unicodeVersion": "6.0", "digest": "472ebc676b5d31dec2ac5e02ce69014a3dd94609d30a95f39f3a752f49c85e8b" }, - { - "name": "yt", - "unicode": "1F1FE-1F1F9", - "digest": "472ebc676b5d31dec2ac5e02ce69014a3dd94609d30a95f39f3a752f49c85e8b" - }, - { - "name": "flag_za", - "unicode": "1F1FF-1F1E6", - "digest": "dad162942a43392b4cff6929bd5cbf58c382a03dbc0e552f03c07ad2d8ff08ce" - }, - { - "name": "za", - "unicode": "1F1FF-1F1E6", + "flag_za": { + "category": "flags", + "moji": "🇿🇦", + "unicodeVersion": "6.0", "digest": "dad162942a43392b4cff6929bd5cbf58c382a03dbc0e552f03c07ad2d8ff08ce" }, - { - "name": "flag_zm", - "unicode": "1F1FF-1F1F2", - "digest": "1521ecaf1d1fdc8c15f0c96a6b04e6d4050f26f943a826b3d3d661f6ded6d438" - }, - { - "name": "zm", - "unicode": "1F1FF-1F1F2", + "flag_zm": { + "category": "flags", + "moji": "🇿🇲", + "unicodeVersion": "6.0", "digest": "1521ecaf1d1fdc8c15f0c96a6b04e6d4050f26f943a826b3d3d661f6ded6d438" }, - { - "name": "flag_zw", - "unicode": "1F1FF-1F1FC", - "digest": "46d05b597c5c77c8e2dc7bd6d8dd62ebca01bc9c9dc9915dafe694ca56402825" - }, - { - "name": "zw", - "unicode": "1F1FF-1F1FC", + "flag_zw": { + "category": "flags", + "moji": "🇿🇼", + "unicodeVersion": "6.0", "digest": "46d05b597c5c77c8e2dc7bd6d8dd62ebca01bc9c9dc9915dafe694ca56402825" }, - { - "name": "flags", - "unicode": "1F38F", + "flags": { + "category": "objects", + "moji": "🎏", + "unicodeVersion": "6.0", "digest": "f860aa4df587cf140c3e9735bbd101e9fd5a1bfcea42e420d85ac0a9877fa21d" }, - { - "name": "flashlight", - "unicode": "1F526", + "flashlight": { + "category": "objects", + "moji": "🔦", + "unicodeVersion": "6.0", "digest": "e929bbe76e0fd2dc5bd6476858a0bbc717fd21467710435d35d80efb38033d73" }, - { - "name": "fleur-de-lis", - "unicode": "269C", + "fleur-de-lis": { + "category": "symbols", + "moji": "⚜", + "unicodeVersion": "4.1", "digest": "ebf49007f367dc05580e9dab942e93e9dda12fa1dc2caa410ac7f8d8cd55d2a3" }, - { - "name": "floppy_disk", - "unicode": "1F4BE", + "floppy_disk": { + "category": "objects", + "moji": "💾", + "unicodeVersion": "6.0", "digest": "4ee0b5bba41b9e301ed125d3ee1c263bef171ca499e6e1b89276b09af2bc03a0" }, - { - "name": "flower_playing_cards", - "unicode": "1F3B4", + "flower_playing_cards": { + "category": "symbols", + "moji": "🎴", + "unicodeVersion": "6.0", "digest": "edba47c2e3051b2c7effd98794ec977174052782edcb491daec82a2b0d853869" }, - { - "name": "flushed", - "unicode": "1F633", + "flushed": { + "category": "people", + "moji": "😳", + "unicodeVersion": "6.0", "digest": "e759d46bab92af5494d78b6c712c06568759afe397e7828ca0a0de1e3eab0165" }, - { - "name": "fog", - "unicode": "1F32B", + "fog": { + "category": "nature", + "moji": "🌫", + "unicodeVersion": "7.0", "digest": "0cbd4733961d30fe0f40f95dd1f37254aebbef26f82dd18ad2000e799eb2898e" }, - { - "name": "foggy", - "unicode": "1F301", + "foggy": { + "category": "travel", + "moji": "🌁", + "unicodeVersion": "6.0", "digest": "bc3631a4e9e8473b92e842008937add2cd9ffad5b7d772ce759fb5ff6c0e3dca" }, - { - "name": "football", - "unicode": "1F3C8", + "football": { + "category": "activity", + "moji": "🏈", + "unicodeVersion": "6.0", "digest": "ebd790471c3a28d3077818e3b31d915ffe443e06e299bc5cf0dd2534d080634c" }, - { - "name": "footprints", - "unicode": "1F463", + "footprints": { + "category": "people", + "moji": "👣", + "unicodeVersion": "6.0", "digest": "85bbf2bc0ae8e6259d83a06f513600095d7fcfc44372670f5b2405d380b78811" }, - { - "name": "fork_and_knife", - "unicode": "1F374", + "fork_and_knife": { + "category": "food", + "moji": "🍴", + "unicodeVersion": "6.0", "digest": "f228accd36ddccb4ec636207c19d7185191ec79723b780a1bd5c3d00a4b1ef3b" }, - { - "name": "fork_knife_plate", - "unicode": "1F37D", - "digest": "ec6be99dac8efd3d145807fa60d2b6d8f6d3c02cb95552b55cc0fac39a4db48e" - }, - { - "name": "fork_and_knife_with_plate", - "unicode": "1F37D", + "fork_knife_plate": { + "category": "food", + "moji": "🍽", + "unicodeVersion": "7.0", "digest": "ec6be99dac8efd3d145807fa60d2b6d8f6d3c02cb95552b55cc0fac39a4db48e" }, - { - "name": "fountain", - "unicode": "26F2", + "fountain": { + "category": "travel", + "moji": "⛲", + "unicodeVersion": "5.2", "digest": "87043f9256e1d4615159307fcfd21bf6ae2aba0bada7de2bd50d7d6f2ab82395" }, - { - "name": "four", - "unicode": "0034-20E3", + "four": { + "category": "symbols", + "moji": "4️⃣", + "unicodeVersion": "3.0", "digest": "c2c82a966bbb599aae557d930a4fc42604f2081aa45528872f5caf4942ee79d9" }, - { - "name": "four_leaf_clover", - "unicode": "1F340", + "four_leaf_clover": { + "category": "nature", + "moji": "🍀", + "unicodeVersion": "6.0", "digest": "ebee16e86bc9be843dfc72ab5372fb462f06be4486b5b25d7d4cac9b2c8b01c8" }, - { - "name": "fox", - "unicode": "1F98A", - "digest": "e9903cb0396f7e49bdd2c384b38e614c13bfa576b3ecc1ec7b9819e4a40d91d1" - }, - { - "name": "fox_face", - "unicode": "1F98A", + "fox": { + "category": "nature", + "moji": "🦊", + "unicodeVersion": "9.0", "digest": "e9903cb0396f7e49bdd2c384b38e614c13bfa576b3ecc1ec7b9819e4a40d91d1" }, - { - "name": "frame_photo", - "unicode": "1F5BC", - "digest": "d5074f748a15055ec1fb812c1e5e169e6e3cc73c522c54be1359b0e26c0fc75c" - }, - { - "name": "frame_with_picture", - "unicode": "1F5BC", + "frame_photo": { + "category": "objects", + "moji": "🖼", + "unicodeVersion": "7.0", "digest": "d5074f748a15055ec1fb812c1e5e169e6e3cc73c522c54be1359b0e26c0fc75c" }, - { - "name": "free", - "unicode": "1F193", + "free": { + "category": "symbols", + "moji": "🆓", + "unicodeVersion": "6.0", "digest": "9973522457158362fc5bdd7da858e6371e28a8403d1ef9e4b6427195c7f72cfa" }, - { - "name": "french_bread", - "unicode": "1F956", - "digest": "47518a4312f57207b8e8c38188d4a2bd8b16830a885cfcf2d281cfab50c1bc6e" - }, - { - "name": "baguette_bread", - "unicode": "1F956", + "french_bread": { + "category": "food", + "moji": "🥖", + "unicodeVersion": "9.0", "digest": "47518a4312f57207b8e8c38188d4a2bd8b16830a885cfcf2d281cfab50c1bc6e" }, - { - "name": "fried_shrimp", - "unicode": "1F364", + "fried_shrimp": { + "category": "food", + "moji": "🍤", + "unicodeVersion": "6.0", "digest": "0792bdc4484852de970c8f43bc3a1a339dc0e48090ec77d6de97cbfcdd17f9e1" }, - { - "name": "fries", - "unicode": "1F35F", + "fries": { + "category": "food", + "moji": "🍟", + "unicodeVersion": "6.0", "digest": "47915aea67251d358d91a0e4dc3dcc347155336007d6b931a192be72a743b4e9" }, - { - "name": "frog", - "unicode": "1F438", + "frog": { + "category": "nature", + "moji": "🐸", + "unicodeVersion": "6.0", "digest": "d024b2ce771df64040534fb0906737d18b562bc3578dee62c2f25ec03c7caffd" }, - { - "name": "frowning", - "unicode": "1F626", - "digest": "c01af48537b0011d313d8f65103e1401fce4f5c0269c68e0e9806926c59acc44" - }, - { - "name": "anguished", - "unicode": "1F626", + "frowning": { + "category": "people", + "moji": "😦", + "unicodeVersion": "6.1", "digest": "c01af48537b0011d313d8f65103e1401fce4f5c0269c68e0e9806926c59acc44" }, - { - "name": "frowning2", - "unicode": "2639", - "digest": "6568ee393b950c852d440112e86908c456b89fb7780e27778c5fcec168373fbf" - }, - { - "name": "white_frowning_face", - "unicode": "2639", + "frowning2": { + "category": "people", + "moji": "☹", + "unicodeVersion": "1.1", "digest": "6568ee393b950c852d440112e86908c456b89fb7780e27778c5fcec168373fbf" }, - { - "name": "fuelpump", - "unicode": "26FD", + "fuelpump": { + "category": "travel", + "moji": "⛽", + "unicodeVersion": "5.2", "digest": "105e736469f19911b8bab4ab6d29f949ded4b061b54e3dd763726577d6453095" }, - { - "name": "full_moon", - "unicode": "1F315", + "full_moon": { + "category": "nature", + "moji": "🌕", + "unicodeVersion": "6.0", "digest": "aaa87f4676a5aaa29c1b721a3b582e89db6c1f35a25c52e4b480bd193ef39c43" }, - { - "name": "full_moon_with_face", - "unicode": "1F31D", + "full_moon_with_face": { + "category": "nature", + "moji": "🌝", + "unicodeVersion": "6.0", "digest": "05c4b9c339fcdf81ae67027641522baa99c370d87873ff4c8133b8349e627e33" }, - { - "name": "game_die", - "unicode": "1F3B2", + "game_die": { + "category": "activity", + "moji": "🎲", + "unicodeVersion": "6.0", "digest": "00d19ce8e21dba2cdfeb18709fa8741f3af9d6207f81d5657b68e05e64f105a8" }, - { - "name": "gear", - "unicode": "2699", + "gear": { + "category": "objects", + "moji": "⚙", + "unicodeVersion": "4.1", "digest": "c5ba354c0f7a36dce95477091984e352ecc59af8c9f26a94ad8e296dc042b9de" }, - { - "name": "gem", - "unicode": "1F48E", + "gem": { + "category": "objects", + "moji": "💎", + "unicodeVersion": "6.0", "digest": "180e66f19d9285e02d0a5e859722c608206826e80323942b9938fc49d44973b1" }, - { - "name": "gemini", - "unicode": "264A", + "gemini": { + "category": "symbols", + "moji": "♊", + "unicodeVersion": "1.1", "digest": "278239c598d490a110f1f3f52fc3b85259be8e76034b38228ef3f68d7ddd8cdd" }, - { - "name": "ghost", - "unicode": "1F47B", + "ghost": { + "category": "people", + "moji": "👻", + "unicodeVersion": "6.0", "digest": "80d528fcf8ef9198631527547e43a608a4332a799f9e5550b8318dec67c9c4d2" }, - { - "name": "gift", - "unicode": "1F381", + "gift": { + "category": "objects", + "moji": "🎁", + "unicodeVersion": "6.0", "digest": "4061a84a59f0300473299678c43e533341eb965db09597fffc6e221fd7b77376" }, - { - "name": "gift_heart", - "unicode": "1F49D", + "gift_heart": { + "category": "symbols", + "moji": "💝", + "unicodeVersion": "6.0", "digest": "5420199b515b9b32c964a3c19d87e07461639e3068a939dae26c6436335c0cee" }, - { - "name": "girl", - "unicode": "1F467", + "girl": { + "category": "people", + "moji": "👧", + "unicodeVersion": "6.0", "digest": "8d2d0b72a91e6e44921b71030ffc4c89c0f50f1364787784afe1e7e568cf1bc6" }, - { - "name": "girl_tone1", - "unicode": "1F467-1F3FB", + "girl_tone1": { + "category": "people", + "moji": "👧🏻", + "unicodeVersion": "8.0", "digest": "bda12a6b38994a578ee65166bbdd93ea04df4101697b52ed236de8d687df09de" }, - { - "name": "girl_tone2", - "unicode": "1F467-1F3FC", + "girl_tone2": { + "category": "people", + "moji": "👧🏼", + "unicodeVersion": "8.0", "digest": "de7a0925c30b7181a289f71b1a849c1b7751ee8c104e8f2029bd9c2fe3f91c64" }, - { - "name": "girl_tone3", - "unicode": "1F467-1F3FD", + "girl_tone3": { + "category": "people", + "moji": "👧🏽", + "unicodeVersion": "8.0", "digest": "e41272816db0e642d003dce7cb262e1593a592251f46729f7830f4515149e1f2" }, - { - "name": "girl_tone4", - "unicode": "1F467-1F3FE", + "girl_tone4": { + "category": "people", + "moji": "👧🏾", + "unicodeVersion": "8.0", "digest": "8d6a4513ecbf08408c0ecc5336767777a2216f7a19437faf9e51f65101822469" }, - { - "name": "girl_tone5", - "unicode": "1F467-1F3FF", + "girl_tone5": { + "category": "people", + "moji": "👧🏿", + "unicodeVersion": "8.0", "digest": "f55e4b16a41b6f5e3c817a301420360ba4486e4e82e1092a56a3e3cc4069087d" }, - { - "name": "globe_with_meridians", - "unicode": "1F310", + "globe_with_meridians": { + "category": "symbols", + "moji": "🌐", + "unicodeVersion": "6.0", "digest": "725bebeb3c09a9e3701ebe49e672dcfbf2b73575e05f0821263511577b013b75" }, - { - "name": "goal", - "unicode": "1F945", - "digest": "7088c432f276ff6f447dc0d431b9062b394fb401de1072fe59ca56267bfd6717" - }, - { - "name": "goal_net", - "unicode": "1F945", + "goal": { + "category": "activity", + "moji": "🥅", + "unicodeVersion": "9.0", "digest": "7088c432f276ff6f447dc0d431b9062b394fb401de1072fe59ca56267bfd6717" }, - { - "name": "goat", - "unicode": "1F410", + "goat": { + "category": "nature", + "moji": "🐐", + "unicodeVersion": "6.0", "digest": "d07e384d08529ddcaddd2710f2ad913e5665dc15d5f99c28e16dadd245a111e8" }, - { - "name": "golf", - "unicode": "26F3", + "golf": { + "category": "activity", + "moji": "⛳", + "unicodeVersion": "5.2", "digest": "eed79364754eec97855e3c7b584f347ae139d9ddb4eb7fb66c00867610b8f1c1" }, - { - "name": "golfer", - "unicode": "1F3CC", + "golfer": { + "category": "activity", + "moji": "🏌", + "unicodeVersion": "7.0", "digest": "7d7ecc6e226596f646030a4109c2b0001ef0cc690e4863e450bf5d29e7a90344" }, - { - "name": "gorilla", - "unicode": "1F98D", + "gorilla": { + "category": "nature", + "moji": "🦍", + "unicodeVersion": "9.0", "digest": "4a564dc14f8ae5450d094f6410ec7f099a7f07dc5254b6395f44a35527bdb4b7" }, - { - "name": "grapes", - "unicode": "1F347", + "grapes": { + "category": "food", + "moji": "🍇", + "unicodeVersion": "6.0", "digest": "74d1a09ab411234a84d025a2e717e7ec5791bc02aad29853896d21c0f0283c50" }, - { - "name": "green_apple", - "unicode": "1F34F", + "green_apple": { + "category": "food", + "moji": "🍏", + "unicodeVersion": "6.0", "digest": "457490e9b2b20894f50768262d63f1021717079da104d4847076b3fa779e9a21" }, - { - "name": "green_book", - "unicode": "1F4D7", + "green_book": { + "category": "objects", + "moji": "📗", + "unicodeVersion": "6.0", "digest": "370f635b200efe5e4a9f17da58bd22500e258e61d17795cef375f19c9a45468f" }, - { - "name": "green_heart", - "unicode": "1F49A", + "green_heart": { + "category": "symbols", + "moji": "💚", + "unicodeVersion": "6.0", "digest": "f71e30416d9019873f2ed38ef375c48386424ff60b5a07b89b15dc9e0a3970f9" }, - { - "name": "grey_exclamation", - "unicode": "2755", + "grey_exclamation": { + "category": "symbols", + "moji": "❕", + "unicodeVersion": "6.0", "digest": "2fa1d356e12c17cc4025e43afb6c3070385f677102a35223302fda46c47a9b03" }, - { - "name": "grey_question", - "unicode": "2754", + "grey_question": { + "category": "symbols", + "moji": "❔", + "unicodeVersion": "6.0", "digest": "e1035bcbf0f66d238ef478ba451f5cf2c51627fbf101ed03bad3b2bf38db8aa2" }, - { - "name": "grimacing", - "unicode": "1F62C", + "grimacing": { + "category": "people", + "moji": "😬", + "unicodeVersion": "6.1", "digest": "2cedad13b8b2a1d4385ca6fa88a251eb7757a4c65dd6d362267864a01247846b" }, - { - "name": "grin", - "unicode": "1F601", + "grin": { + "category": "people", + "moji": "😁", + "unicodeVersion": "6.0", "digest": "634b2f37e32e57ed6edc7f371993a92e34137dd21ba393de5227cfbbe2422815" }, - { - "name": "grinning", - "unicode": "1F600", + "grinning": { + "category": "people", + "moji": "😀", + "unicodeVersion": "6.1", "digest": "cef76aa41771db9fd1d6bd9b4233c22c1fb1931494af54cab29e6347ed9b678d" }, - { - "name": "guardsman", - "unicode": "1F482", + "guardsman": { + "category": "people", + "moji": "💂", + "unicodeVersion": "6.0", "digest": "17bc7fad6b8c8dbd015bb709380d129f8b8e1e971062d15e6ab0b2e63e500564" }, - { - "name": "guardsman_tone1", - "unicode": "1F482-1F3FB", + "guardsman_tone1": { + "category": "people", + "moji": "💂🏻", + "unicodeVersion": "8.0", "digest": "c531ecb101bdf9ce1db18e1567882e6db927410237100b0a2492a1401860246e" }, - { - "name": "guardsman_tone2", - "unicode": "1F482-1F3FC", + "guardsman_tone2": { + "category": "people", + "moji": "💂🏼", + "unicodeVersion": "8.0", "digest": "602168c5204af0f1de8b4aa5863b192ef20c19d263999377aa5eb60f98311732" }, - { - "name": "guardsman_tone3", - "unicode": "1F482-1F3FD", + "guardsman_tone3": { + "category": "people", + "moji": "💂🏽", + "unicodeVersion": "8.0", "digest": "d0a85de46dd02c7bd6cb14bff0f22d2db9083d4b171a8806c83363b49f3dd9ef" }, - { - "name": "guardsman_tone4", - "unicode": "1F482-1F3FE", + "guardsman_tone4": { + "category": "people", + "moji": "💂🏾", + "unicodeVersion": "8.0", "digest": "1c9d4d72b6b50bdac8271613b6d2a38340ec2067bc344e8ee2a3c863fd5c23a1" }, - { - "name": "guardsman_tone5", - "unicode": "1F482-1F3FF", + "guardsman_tone5": { + "category": "people", + "moji": "💂🏿", + "unicodeVersion": "8.0", "digest": "9899a796d01842e495d716fbe737a16d85724f7d3e23f50807ec2bc70f057318" }, - { - "name": "guitar", - "unicode": "1F3B8", + "guitar": { + "category": "activity", + "moji": "🎸", + "unicodeVersion": "6.0", "digest": "a1027ceae4dd3ea270740587c9d373329e5677e375c9e00af6ae3275e0b67500" }, - { - "name": "gun", - "unicode": "1F52B", + "gun": { + "category": "objects", + "moji": "🔫", + "unicodeVersion": "6.0", "digest": "fc12b577df2283e7b336f23774f9cfe5b79f1d26ddd28a64a560519b28d94ca5" }, - { - "name": "haircut", - "unicode": "1F487", + "haircut": { + "category": "people", + "moji": "💇", + "unicodeVersion": "6.0", "digest": "b243a04f5ca889accd45e7abe095ac5caa92274ed95103f5966a36b415fff412" }, - { - "name": "haircut_tone1", - "unicode": "1F487-1F3FB", + "haircut_tone1": { + "category": "people", + "moji": "💇🏻", + "unicodeVersion": "8.0", "digest": "a58d0cff1427b80dfd7a9ea5267b4a181e9faaac6a51a0165db522f668b4cf91" }, - { - "name": "haircut_tone2", - "unicode": "1F487-1F3FC", + "haircut_tone2": { + "category": "people", + "moji": "💇🏼", + "unicodeVersion": "8.0", "digest": "675083ff40001405f8de99268477d50dd8594ff6ca40ddfd442dd42ad76e8216" }, - { - "name": "haircut_tone3", - "unicode": "1F487-1F3FD", + "haircut_tone3": { + "category": "people", + "moji": "💇🏽", + "unicodeVersion": "8.0", "digest": "70d7581e49c315a3771dd61a3713229886db32aaaeb3af078a69cc042f809150" }, - { - "name": "haircut_tone4", - "unicode": "1F487-1F3FE", + "haircut_tone4": { + "category": "people", + "moji": "💇🏾", + "unicodeVersion": "8.0", "digest": "ec5e3e909eb3bc375ef9cc0fe0e0f90b33f44f273ada91ccf415bbc43b8ffbfc" }, - { - "name": "haircut_tone5", - "unicode": "1F487-1F3FF", + "haircut_tone5": { + "category": "people", + "moji": "💇🏿", + "unicodeVersion": "8.0", "digest": "7c89739ee458546a808fded7f96d9354c47a76883ebb262d5f5abeafd021260e" }, - { - "name": "hamburger", - "unicode": "1F354", + "hamburger": { + "category": "food", + "moji": "🍔", + "unicodeVersion": "6.0", "digest": "48204235238bd89d3a69f319f65135102f3d6b181eec241d4d86b302bbffa9bf" }, - { - "name": "hammer", - "unicode": "1F528", + "hammer": { + "category": "objects", + "moji": "🔨", + "unicodeVersion": "6.0", "digest": "d0e7830539d935fcd82820c4e0c1d724f0756dfc83a51171fe0f4b36b69fac42" }, - { - "name": "hammer_pick", - "unicode": "2692", + "hammer_pick": { + "category": "objects", + "moji": "⚒", + "unicodeVersion": "4.1", "digest": "aa0445f43bca58d17afa7f3577632ca7775f5a28336385b3020b268b15b18142" }, - { - "name": "hammer_and_pick", - "unicode": "2692", - "digest": "aa0445f43bca58d17afa7f3577632ca7775f5a28336385b3020b268b15b18142" - }, - { - "name": "hamster", - "unicode": "1F439", + "hamster": { + "category": "nature", + "moji": "🐹", + "unicodeVersion": "6.0", "digest": "a7e7582e8b1bccd5b7df27ccb05e353a3f0e39bdeb40877732706b9d74a70de1" }, - { - "name": "hand_splayed", - "unicode": "1F590", - "digest": "c51a30cb7e575d29ffed16780a6c95ae3f300b8ac523012f4a6e116d68c1fd15" - }, - { - "name": "raised_hand_with_fingers_splayed", - "unicode": "1F590", + "hand_splayed": { + "category": "people", + "moji": "🖐", + "unicodeVersion": "7.0", "digest": "c51a30cb7e575d29ffed16780a6c95ae3f300b8ac523012f4a6e116d68c1fd15" }, - { - "name": "hand_splayed_tone1", - "unicode": "1F590-1F3FB", + "hand_splayed_tone1": { + "category": "people", + "moji": "🖐🏻", + "unicodeVersion": "8.0", "digest": "c31fb44a982ed8808e1c311ec1b0b9c5afcb47f16bb1fc731dc483adf8f0d049" }, - { - "name": "raised_hand_with_fingers_splayed_tone1", - "unicode": "1F590-1F3FB", - "digest": "c31fb44a982ed8808e1c311ec1b0b9c5afcb47f16bb1fc731dc483adf8f0d049" - }, - { - "name": "hand_splayed_tone2", - "unicode": "1F590-1F3FC", - "digest": "56a236881184e9ffad54613fa08a67368c432af738f5254fb1cd87b20368acdf" - }, - { - "name": "raised_hand_with_fingers_splayed_tone2", - "unicode": "1F590-1F3FC", + "hand_splayed_tone2": { + "category": "people", + "moji": "🖐🏼", + "unicodeVersion": "8.0", "digest": "56a236881184e9ffad54613fa08a67368c432af738f5254fb1cd87b20368acdf" }, - { - "name": "hand_splayed_tone3", - "unicode": "1F590-1F3FD", + "hand_splayed_tone3": { + "category": "people", + "moji": "🖐🏽", + "unicodeVersion": "8.0", "digest": "9242ca97dfd2bbc1947228f6535029afb31f8feb72c14ff4b7f2deea30217425" }, - { - "name": "raised_hand_with_fingers_splayed_tone3", - "unicode": "1F590-1F3FD", - "digest": "9242ca97dfd2bbc1947228f6535029afb31f8feb72c14ff4b7f2deea30217425" - }, - { - "name": "hand_splayed_tone4", - "unicode": "1F590-1F3FE", - "digest": "43348d9fd3d43b3c45cebaf663bf181bcad3b6df841a5aeed838180db2cdd481" - }, - { - "name": "raised_hand_with_fingers_splayed_tone4", - "unicode": "1F590-1F3FE", + "hand_splayed_tone4": { + "category": "people", + "moji": "🖐🏾", + "unicodeVersion": "8.0", "digest": "43348d9fd3d43b3c45cebaf663bf181bcad3b6df841a5aeed838180db2cdd481" }, - { - "name": "hand_splayed_tone5", - "unicode": "1F590-1F3FF", + "hand_splayed_tone5": { + "category": "people", + "moji": "🖐🏿", + "unicodeVersion": "8.0", "digest": "4b3a0aba7829772fec09f26d6facc19a2f822d2998015297b18b5cab85190ee2" }, - { - "name": "raised_hand_with_fingers_splayed_tone5", - "unicode": "1F590-1F3FF", - "digest": "4b3a0aba7829772fec09f26d6facc19a2f822d2998015297b18b5cab85190ee2" - }, - { - "name": "handbag", - "unicode": "1F45C", + "handbag": { + "category": "people", + "moji": "👜", + "unicodeVersion": "6.0", "digest": "45410a3eed0c2e3f68748d7649fa9e33a90f4e80d5291206bdd0b40380c6da45" }, - { - "name": "handball", - "unicode": "1F93E", + "handball": { + "category": "activity", + "moji": "🤾", + "unicodeVersion": "9.0", "digest": "94ceb28024eb3259d8b137cafd7438773e717fbc04f5da810f85e43ca0fa9e00" }, - { - "name": "handball_tone1", - "unicode": "1F93E-1F3FB", + "handball_tone1": { + "category": "activity", + "moji": "🤾🏻", + "unicodeVersion": "9.0", "digest": "8bec4de0d05c80e335e44d65598d186ca92696977353c9fd9c2a5efa122cb842" }, - { - "name": "handball_tone2", - "unicode": "1F93E-1F3FC", + "handball_tone2": { + "category": "activity", + "moji": "🤾🏼", + "unicodeVersion": "9.0", "digest": "2ff4131e1e2f089b315d8e176c9348877c26c2bd03706fb75d41bc61bc99bf93" }, - { - "name": "handball_tone3", - "unicode": "1F93E-1F3FD", + "handball_tone3": { + "category": "activity", + "moji": "🤾🏽", + "unicodeVersion": "9.0", "digest": "224a71f94dd37d3729325d11412334667a81422e21f6d7c008730ff350f51a80" }, - { - "name": "handball_tone4", - "unicode": "1F93E-1F3FE", + "handball_tone4": { + "category": "activity", + "moji": "🤾🏾", + "unicodeVersion": "9.0", "digest": "a5f7a9db790565981bad2d0d9e09554c8c509a8179b4705a418300d58a7894b4" }, - { - "name": "handball_tone5", - "unicode": "1F93E-1F3FF", + "handball_tone5": { + "category": "activity", + "moji": "🤾🏿", + "unicodeVersion": "9.0", "digest": "00404572d4683f2e8e8a494aa733e96fbec1723634d0a8cb8d75f2829a789d27" }, - { - "name": "handshake", - "unicode": "1F91D", + "handshake": { + "category": "people", + "moji": "🤝", + "unicodeVersion": "9.0", "digest": "cb4b08b70560908f96bda0aecd2f4c966bea180f9b7200e4c81d342dc8d36087" }, - { - "name": "shaking_hands", - "unicode": "1F91D", - "digest": "cb4b08b70560908f96bda0aecd2f4c966bea180f9b7200e4c81d342dc8d36087" - }, - { - "name": "handshake_tone1", - "unicode": "1F91D-1F3FB", + "handshake_tone1": { + "category": "people", + "moji": "🤝🏻", + "unicodeVersion": "9.0", "digest": "40470e224683ba375ed8698c0cbd560556be5a8898237ddf504377a3a7e89ff0" }, - { - "name": "shaking_hands_tone1", - "unicode": "1F91D-1F3FB", - "digest": "40470e224683ba375ed8698c0cbd560556be5a8898237ddf504377a3a7e89ff0" - }, - { - "name": "handshake_tone2", - "unicode": "1F91D-1F3FC", - "digest": "77ed378243bf682f1f4f1a8caeabcbedf772f54631cc40ea46c099e46a499b18" - }, - { - "name": "shaking_hands_tone2", - "unicode": "1F91D-1F3FC", + "handshake_tone2": { + "category": "people", + "moji": "🤝🏼", + "unicodeVersion": "9.0", "digest": "77ed378243bf682f1f4f1a8caeabcbedf772f54631cc40ea46c099e46a499b18" }, - { - "name": "handshake_tone3", - "unicode": "1F91D-1F3FD", + "handshake_tone3": { + "category": "people", + "moji": "🤝🏽", + "unicodeVersion": "9.0", "digest": "81b95050f0878b617f5d2640e34031c26a0072e46ca5a688eb4356e48bc74c92" }, - { - "name": "shaking_hands_tone3", - "unicode": "1F91D-1F3FD", - "digest": "81b95050f0878b617f5d2640e34031c26a0072e46ca5a688eb4356e48bc74c92" - }, - { - "name": "handshake_tone4", - "unicode": "1F91D-1F3FE", - "digest": "74919a6f026fbbd0ccdbdbd4288d1b2ef3bda8930e9142c07736db4a7f3ef345" - }, - { - "name": "shaking_hands_tone4", - "unicode": "1F91D-1F3FE", + "handshake_tone4": { + "category": "people", + "moji": "🤝🏾", + "unicodeVersion": "9.0", "digest": "74919a6f026fbbd0ccdbdbd4288d1b2ef3bda8930e9142c07736db4a7f3ef345" }, - { - "name": "handshake_tone5", - "unicode": "1F91D-1F3FF", - "digest": "a30d662bfad0074ca7e32cf6f7229b643b636c4beaec496777eb7e1d5b6fc470" - }, - { - "name": "shaking_hands_tone5", - "unicode": "1F91D-1F3FF", + "handshake_tone5": { + "category": "people", + "moji": "🤝🏿", + "unicodeVersion": "9.0", "digest": "a30d662bfad0074ca7e32cf6f7229b643b636c4beaec496777eb7e1d5b6fc470" }, - { - "name": "hash", - "unicode": "0023-20E3", + "hash": { + "category": "symbols", + "moji": "#⃣", + "unicodeVersion": "3.0", "digest": "01c8b577953010bff0c20f797c2c96ab5d98d4e6ac179c4895a78f34ea904655" }, - { - "name": "hatched_chick", - "unicode": "1F425", + "hatched_chick": { + "category": "nature", + "moji": "🐥", + "unicodeVersion": "6.0", "digest": "006571b9e9e839ec9fcb1a911b935c8ca71eb8bcdce9775bee6a2a4c7c927277" }, - { - "name": "hatching_chick", - "unicode": "1F423", + "hatching_chick": { + "category": "nature", + "moji": "🐣", + "unicodeVersion": "6.0", "digest": "fd7f69fa186407f80de59dec5116e318325a5743ee0e8bba1db541f1e57e7f74" }, - { - "name": "head_bandage", - "unicode": "1F915", - "digest": "d09019a73e203b38cc43729a96163147de88e09eab8adb073888e55366854c72" - }, - { - "name": "face_with_head_bandage", - "unicode": "1F915", + "head_bandage": { + "category": "people", + "moji": "🤕", + "unicodeVersion": "8.0", "digest": "d09019a73e203b38cc43729a96163147de88e09eab8adb073888e55366854c72" }, - { - "name": "headphones", - "unicode": "1F3A7", + "headphones": { + "category": "activity", + "moji": "🎧", + "unicodeVersion": "6.0", "digest": "34f9d5598158d5d6f978a5ea5c5aa9948bb2990625565a3afad7710f864fbe2f" }, - { - "name": "hear_no_evil", - "unicode": "1F649", + "hear_no_evil": { + "category": "nature", + "moji": "🙉", + "unicodeVersion": "6.0", "digest": "53b030b6d6f4ed1a734fa7d48b46f42eb1b2b01653202c1838b742082f08c4bf" }, - { - "name": "heart", - "unicode": "2764", + "heart": { + "category": "symbols", + "moji": "❤", + "unicodeVersion": "1.1", "digest": "92be652ec3e50c6e7393440b5d52b88a367f98a28dffe12660095ed3253aa6c0" }, - { - "name": "heart_decoration", - "unicode": "1F49F", + "heart_decoration": { + "category": "symbols", + "moji": "💟", + "unicodeVersion": "6.0", "digest": "6ec5bbf3aa75c6f43eb3dc05e9204366936e8b6b4219310bacdc2fc45f51e245" }, - { - "name": "heart_exclamation", - "unicode": "2763", + "heart_exclamation": { + "category": "symbols", + "moji": "❣", + "unicodeVersion": "1.1", "digest": "5985ea4d82232a2a07052a59db268aed9ac943895d0c82f637595bb5386329a6" }, - { - "name": "heavy_heart_exclamation_mark_ornament", - "unicode": "2763", - "digest": "5985ea4d82232a2a07052a59db268aed9ac943895d0c82f637595bb5386329a6" - }, - { - "name": "heart_eyes", - "unicode": "1F60D", + "heart_eyes": { + "category": "people", + "moji": "😍", + "unicodeVersion": "6.0", "digest": "0eff616517a6252ec89d47d9b4ad85589bcf2bdc7f490578934350acb84b2fcc" }, - { - "name": "heart_eyes_cat", - "unicode": "1F63B", + "heart_eyes_cat": { + "category": "people", + "moji": "😻", + "unicodeVersion": "6.0", "digest": "8a1f28b97d661ca4cff5ee13889ca61b5fa745ccb590e80832b7d7701df101d6" }, - { - "name": "heartbeat", - "unicode": "1F493", + "heartbeat": { + "category": "symbols", + "moji": "💓", + "unicodeVersion": "6.0", "digest": "c9ec024943439d476df6f5ec3a6b30508365a7af3427671a80de3ef2f4f95ffe" }, - { - "name": "heartpulse", - "unicode": "1F497", + "heartpulse": { + "category": "symbols", + "moji": "💗", + "unicodeVersion": "6.0", "digest": "281d8aebfea37db5b7fe82d9115be167006881fe29ab64a5b09ac92ac27a2309" }, - { - "name": "hearts", - "unicode": "2665", + "hearts": { + "category": "symbols", + "moji": "♥", + "unicodeVersion": "1.1", "digest": "271429d12c40be921897005b7bdd08f9518960af1e1e6f56bb0060f1f183651e" }, - { - "name": "heavy_check_mark", - "unicode": "2714", + "heavy_check_mark": { + "category": "symbols", + "moji": "✔", + "unicodeVersion": "1.1", "digest": "e347728e1290eb9e7b0742d628e2fd124fc049e0774f8a6ddf8e5286e7318718" }, - { - "name": "heavy_division_sign", - "unicode": "2797", + "heavy_division_sign": { + "category": "symbols", + "moji": "➗", + "unicodeVersion": "6.0", "digest": "c1e8c40f0788f140b1c5fcb81ed9b5ce1bcfa5988bb8140ed2808e9cb7e0d651" }, - { - "name": "heavy_dollar_sign", - "unicode": "1F4B2", + "heavy_dollar_sign": { + "category": "symbols", + "moji": "💲", + "unicodeVersion": "6.0", "digest": "7cdeef38348654b93d566e01a48973281cb404a63d0b75b3bad51032887f3f55" }, - { - "name": "heavy_minus_sign", - "unicode": "2796", + "heavy_minus_sign": { + "category": "symbols", + "moji": "➖", + "unicodeVersion": "6.0", "digest": "e5335cc6b22abdce49a6127c34269b65a4a6643ddd3253d9baac425089143e7d" }, - { - "name": "heavy_multiplication_x", - "unicode": "2716", + "heavy_multiplication_x": { + "category": "symbols", + "moji": "✖", + "unicodeVersion": "1.1", "digest": "64bbe9e9716a922e405d2f6d3b6d803863a53fac80ff8cd775899971046cb1ca" }, - { - "name": "heavy_plus_sign", - "unicode": "2795", + "heavy_plus_sign": { + "category": "symbols", + "moji": "➕", + "unicodeVersion": "6.0", "digest": "d0d8ade2020ceb252205180b85c66e665856e6cb505518d395b9913b0b24b746" }, - { - "name": "helicopter", - "unicode": "1F681", + "helicopter": { + "category": "travel", + "moji": "🚁", + "unicodeVersion": "6.0", "digest": "4bd6fd13650fbe3a19cfffeffe6c21b1cda74bd6af64c5dc5999185e35444bc3" }, - { - "name": "helmet_with_cross", - "unicode": "26D1", - "digest": "8286107391d44b9cd7fce5dc83bfdebbcdcf5a8214c46a8990732ec40263ed77" - }, - { - "name": "helmet_with_white_cross", - "unicode": "26D1", + "helmet_with_cross": { + "category": "people", + "moji": "⛑", + "unicodeVersion": "5.2", "digest": "8286107391d44b9cd7fce5dc83bfdebbcdcf5a8214c46a8990732ec40263ed77" }, - { - "name": "herb", - "unicode": "1F33F", + "herb": { + "category": "nature", + "moji": "🌿", + "unicodeVersion": "6.0", "digest": "9fe8ed65515ede59d0926dcf98f14e2498785e1965610aa0dd56eca9b4bedad9" }, - { - "name": "hibiscus", - "unicode": "1F33A", + "hibiscus": { + "category": "nature", + "moji": "🌺", + "unicodeVersion": "6.0", "digest": "c442e8eacbd8727bd154bd39692a9a2a03ea2f674b9670ad8361f78a038afe49" }, - { - "name": "high_brightness", - "unicode": "1F506", + "high_brightness": { + "category": "symbols", + "moji": "🔆", + "unicodeVersion": "6.0", "digest": "35ced42426dcfd5214c2c6c577dce84bb708156433945e6b6adaff7ea530cc57" }, - { - "name": "high_heel", - "unicode": "1F460", + "high_heel": { + "category": "people", + "moji": "👠", + "unicodeVersion": "6.0", "digest": "1e7c7aba50eb1d02cf1d9aa372caca741a6005cf47f68dfa75b7310c3cb18f05" }, - { - "name": "hockey", - "unicode": "1F3D2", + "hockey": { + "category": "activity", + "moji": "🏒", + "unicodeVersion": "8.0", "digest": "2d00fb17baa617e799db8e9b1771cc365bb4545c7633df0123e66e1a6e2ed25d" }, - { - "name": "hole", - "unicode": "1F573", + "hole": { + "category": "objects", + "moji": "🕳", + "unicodeVersion": "7.0", "digest": "8b5539f6f24f09d5d68ffd56be5aa2a8a2f753a8dfbf64892fb02c8f2703e920" }, - { - "name": "homes", - "unicode": "1F3D8", - "digest": "cd512f2b4ce747325607d47da48e083dbfe38a44b85b2522bc372bd105afd25f" - }, - { - "name": "house_buildings", - "unicode": "1F3D8", + "homes": { + "category": "travel", + "moji": "🏘", + "unicodeVersion": "7.0", "digest": "cd512f2b4ce747325607d47da48e083dbfe38a44b85b2522bc372bd105afd25f" }, - { - "name": "honey_pot", - "unicode": "1F36F", + "honey_pot": { + "category": "food", + "moji": "🍯", + "unicodeVersion": "6.0", "digest": "f6eec8c32fbd1b461446dc6c5d5031c43e6ee9685dc9b1ea1b839114e48c4eee" }, - { - "name": "horse", - "unicode": "1F434", + "horse": { + "category": "nature", + "moji": "🐴", + "unicodeVersion": "6.0", "digest": "e377649a9549835770a2a721a92570f699255f88efa646029638eb8ec5f10e3d" }, - { - "name": "horse_racing", - "unicode": "1F3C7", + "horse_racing": { + "category": "activity", + "moji": "🏇", + "unicodeVersion": "6.0", "digest": "3b98e94e9c028ad85b9a750cc61db5ee3ac23cf5ad9243ea3e996b1f772bad54" }, - { - "name": "horse_racing_tone1", - "unicode": "1F3C7-1F3FB", + "horse_racing_tone1": { + "category": "activity", + "moji": "🏇🏻", + "unicodeVersion": "8.0", "digest": "382d8e4502ed34fc1bbf1779ce483bc2e22b83f89c91746c11a5d7aea656d446" }, - { - "name": "horse_racing_tone2", - "unicode": "1F3C7-1F3FC", + "horse_racing_tone2": { + "category": "activity", + "moji": "🏇🏼", + "unicodeVersion": "8.0", "digest": "198df9973b492ea63e5cfc210dd9591750ccce04a6380adc1dc5b4cb0462a8cd" }, - { - "name": "horse_racing_tone3", - "unicode": "1F3C7-1F3FD", + "horse_racing_tone3": { + "category": "activity", + "moji": "🏇🏽", + "unicodeVersion": "8.0", "digest": "a67f95fc92c366750ebad3c4db92982893d67a5ed78163c8cc809ac40d2ab9a3" }, - { - "name": "horse_racing_tone4", - "unicode": "1F3C7-1F3FE", + "horse_racing_tone4": { + "category": "activity", + "moji": "🏇🏾", + "unicodeVersion": "8.0", "digest": "986b1706c4a3395b58a8ae3b7609ffdd4424dfefcbf26c88c8085f4f6379734e" }, - { - "name": "horse_racing_tone5", - "unicode": "1F3C7-1F3FF", + "horse_racing_tone5": { + "category": "activity", + "moji": "🏇🏿", + "unicodeVersion": "8.0", "digest": "66656b5e3d0f43f16f983f9db6214b07aac73b143eeff6475782f98aa5b9ba53" }, - { - "name": "hospital", - "unicode": "1F3E5", + "hospital": { + "category": "travel", + "moji": "🏥", + "unicodeVersion": "6.0", "digest": "034573e76df444f5b0eb7aff3a4103e4b49a1813869155ab3ae29a6fc0c6c8a2" }, - { - "name": "hot_pepper", - "unicode": "1F336", + "hot_pepper": { + "category": "food", + "moji": "🌶", + "unicodeVersion": "7.0", "digest": "0b05777d42698196a10db17d04030175b1dfa772d06288f71d666d5f8d3fddbc" }, - { - "name": "hotdog", - "unicode": "1F32D", + "hotdog": { + "category": "food", + "moji": "🌭", + "unicodeVersion": "8.0", "digest": "7a25bbd1a7531fd34a22c654c0931d9e74bea2bbe7baa9f9cbd88f43baa79fb5" }, - { - "name": "hot_dog", - "unicode": "1F32D", - "digest": "7a25bbd1a7531fd34a22c654c0931d9e74bea2bbe7baa9f9cbd88f43baa79fb5" - }, - { - "name": "hotel", - "unicode": "1F3E8", + "hotel": { + "category": "travel", + "moji": "🏨", + "unicodeVersion": "6.0", "digest": "2d78e0ad4cfb0caad778c7de49fefd6e8356afe902a43e3f1c40bceb6b0be422" }, - { - "name": "hotsprings", - "unicode": "2668", + "hotsprings": { + "category": "symbols", + "moji": "♨", + "unicodeVersion": "1.1", "digest": "4c10c3a974b44693e8cbe91365c8b8d7f14f62db234cc516b6e54c08a6bacaed" }, - { - "name": "hourglass", - "unicode": "231B", + "hourglass": { + "category": "objects", + "moji": "⌛", + "unicodeVersion": "1.1", "digest": "f0bae8392aaf6f75a83f5d8914936b8650665b24ba1b232fa546b71545dd9acd" }, - { - "name": "hourglass_flowing_sand", - "unicode": "23F3", + "hourglass_flowing_sand": { + "category": "objects", + "moji": "⏳", + "unicodeVersion": "6.0", "digest": "2d077729f40fc04007a933e97356bd511cbd8be76b8c55962ca3fa0d8b828e23" }, - { - "name": "house", - "unicode": "1F3E0", + "house": { + "category": "travel", + "moji": "🏠", + "unicodeVersion": "6.0", "digest": "b4ac25979fbe161ada0d2a75769aa7552d2371d37d78cddba4ffdc7f076d3279" }, - { - "name": "house_abandoned", - "unicode": "1F3DA", - "digest": "6e1a58533fbfe88a0eb03668c9f17c5c654a6cc7734ed798d4a885400f823610" - }, - { - "name": "derelict_house_building", - "unicode": "1F3DA", + "house_abandoned": { + "category": "travel", + "moji": "🏚", + "unicodeVersion": "7.0", "digest": "6e1a58533fbfe88a0eb03668c9f17c5c654a6cc7734ed798d4a885400f823610" }, - { - "name": "house_with_garden", - "unicode": "1F3E1", + "house_with_garden": { + "category": "travel", + "moji": "🏡", + "unicodeVersion": "6.0", "digest": "817463f23ec0a849393ba75c333e822b4d253cd4db998c127e90d1b924f35d20" }, - { - "name": "hugging", - "unicode": "1F917", + "hugging": { + "category": "people", + "moji": "🤗", + "unicodeVersion": "8.0", "digest": "69810a98b1247e1f1e496aa757e428189ef5cc086764fabd8189cf1eef82234f" }, - { - "name": "hugging_face", - "unicode": "1F917", - "digest": "69810a98b1247e1f1e496aa757e428189ef5cc086764fabd8189cf1eef82234f" - }, - { - "name": "hushed", - "unicode": "1F62F", + "hushed": { + "category": "people", + "moji": "😯", + "unicodeVersion": "6.1", "digest": "22586107f7399eff64538a52929dade152633aa268fc5ec4e6fe1c0e00a7bd89" }, - { - "name": "ice_cream", - "unicode": "1F368", + "ice_cream": { + "category": "food", + "moji": "🍨", + "unicodeVersion": "6.0", "digest": "d1a8e685f2ecf83dead28733859e369d6ce120a2669cdab97dc4423547d472ac" }, - { - "name": "ice_skate", - "unicode": "26F8", + "ice_skate": { + "category": "activity", + "moji": "⛸", + "unicodeVersion": "5.2", "digest": "41ef65c143bc068868fa64080ffd447d91aa3fe2a39e69ecaa97022820af4dcd" }, - { - "name": "icecream", - "unicode": "1F366", + "icecream": { + "category": "food", + "moji": "🍦", + "unicodeVersion": "6.0", "digest": "22cfe17b80cbd2a0377ee90da45bd40d33533c914b2639d363fbb1f00714e194" }, - { - "name": "id", - "unicode": "1F194", + "id": { + "category": "symbols", + "moji": "🆔", + "unicodeVersion": "6.0", "digest": "bcf0922e083821d3be7951893084ea0d72a0110ef0b20d11dfec24dd70633893" }, - { - "name": "ideograph_advantage", - "unicode": "1F250", + "ideograph_advantage": { + "category": "symbols", + "moji": "🉐", + "unicodeVersion": "6.0", "digest": "0b6bf59f63fda1afa92d652814a778a056c3f4abdd9cf3f6796068bd71783051" }, - { - "name": "imp", - "unicode": "1F47F", + "imp": { + "category": "people", + "moji": "👿", + "unicodeVersion": "6.0", "digest": "52598cf2441988f875ccb4e479637baefc679e3ca64e9a6400e56488b0fde811" }, - { - "name": "inbox_tray", - "unicode": "1F4E5", + "inbox_tray": { + "category": "objects", + "moji": "📥", + "unicodeVersion": "6.0", "digest": "d5d9497022b5318fcfbfdfcd56df9c65dd8f4a4cb5e6283ca260836df57da301" }, - { - "name": "incoming_envelope", - "unicode": "1F4E8", + "incoming_envelope": { + "category": "objects", + "moji": "📨", + "unicodeVersion": "6.0", "digest": "310b7bdcca93452fe10c72c03d0aafa12b98e5d3408896d275d06d3693812c7a" }, - { - "name": "information_desk_person", - "unicode": "1F481", + "information_desk_person": { + "category": "people", + "moji": "💁", + "unicodeVersion": "6.0", "digest": "9f12a4a58a650e8e1d3836ef857003c3ccd42ad4203a2479eb95100bf6559064" }, - { - "name": "information_desk_person_tone1", - "unicode": "1F481-1F3FB", + "information_desk_person_tone1": { + "category": "people", + "moji": "💁🏻", + "unicodeVersion": "8.0", "digest": "6674f2e059eff7cfd7fd6abc800da37c4f1087feb4ff26c9e4e31aa29fdf9921" }, - { - "name": "information_desk_person_tone2", - "unicode": "1F481-1F3FC", + "information_desk_person_tone2": { + "category": "people", + "moji": "💁🏼", + "unicodeVersion": "8.0", "digest": "9983412ecd130b7e9cfb078167016c06fd043b6f9f3c26d21733ca3f059fd109" }, - { - "name": "information_desk_person_tone3", - "unicode": "1F481-1F3FD", + "information_desk_person_tone3": { + "category": "people", + "moji": "💁🏽", + "unicodeVersion": "8.0", "digest": "d8907bf47af5722127afca8fc0da587eab33044a6c60a94890983deb8d6f7a66" }, - { - "name": "information_desk_person_tone4", - "unicode": "1F481-1F3FE", + "information_desk_person_tone4": { + "category": "people", + "moji": "💁🏾", + "unicodeVersion": "8.0", "digest": "3be086d4edfe9ca8e4a364b4e8d09b81b5b594b5eeb9ffdf6370179fb3118658" }, - { - "name": "information_desk_person_tone5", - "unicode": "1F481-1F3FF", + "information_desk_person_tone5": { + "category": "people", + "moji": "💁🏿", + "unicodeVersion": "8.0", "digest": "2fde4e98dd11c5c29c89cad7cbb7bd2d5077dfad07913b20e01955b2d0dfad40" }, - { - "name": "information_source", - "unicode": "2139", + "information_source": { + "category": "symbols", + "moji": "ℹ", + "unicodeVersion": "3.0", "digest": "b6bf3cce86d42c2e3c46470baab4af01e900b8ae337b605c3da07c3eba671269" }, - { - "name": "innocent", - "unicode": "1F607", + "innocent": { + "category": "people", + "moji": "😇", + "unicodeVersion": "6.0", "digest": "20f8d856bc3e46f4b1173cea05d4577e1c61f06b2daba46e57db90f4066bb428" }, - { - "name": "interrobang", - "unicode": "2049", + "interrobang": { + "category": "symbols", + "moji": "⁉", + "unicodeVersion": "3.0", "digest": "92a2d5b4c0bd6714e402f6f12fe19774cb41d081b5e9c23c415ce794224d8117" }, - { - "name": "iphone", - "unicode": "1F4F1", + "iphone": { + "category": "objects", + "moji": "📱", + "unicodeVersion": "6.0", "digest": "1ebc54215713cd4bf1c1e50770999f2512bb4fea29e37d0bb3a8aa2460ff875d" }, - { - "name": "island", - "unicode": "1F3DD", - "digest": "7f9eb5c0cd865762f7a0f187e09c1be442de7010e7c2e113d56aae998597c90d" - }, - { - "name": "desert_island", - "unicode": "1F3DD", + "island": { + "category": "travel", + "moji": "🏝", + "unicodeVersion": "7.0", "digest": "7f9eb5c0cd865762f7a0f187e09c1be442de7010e7c2e113d56aae998597c90d" }, - { - "name": "izakaya_lantern", - "unicode": "1F3EE", + "izakaya_lantern": { + "category": "objects", + "moji": "🏮", + "unicodeVersion": "6.0", "digest": "fbdc290e666d43d0776a73b955c26df4518692b35e72742e073705fc4ca2ae88" }, - { - "name": "jack_o_lantern", - "unicode": "1F383", + "jack_o_lantern": { + "category": "nature", + "moji": "🎃", + "unicodeVersion": "6.0", "digest": "78d666c2e80f64bfb6796f53e5ba4960a83ec36192110e8661031bee2b5e370a" }, - { - "name": "japan", - "unicode": "1F5FE", + "japan": { + "category": "travel", + "moji": "🗾", + "unicodeVersion": "6.0", "digest": "e7d9d6ebf9047fdd3c52e074ba259659c6d8e51a6abae3cdb8d6cf6dbf9a93fe" }, - { - "name": "japanese_castle", - "unicode": "1F3EF", + "japanese_castle": { + "category": "travel", + "moji": "🏯", + "unicodeVersion": "6.0", "digest": "938ae132c403330288223b88d28c19a47224d4f254fbc2366ecef73d9633112c" }, - { - "name": "japanese_goblin", - "unicode": "1F47A", + "japanese_goblin": { + "category": "people", + "moji": "👺", + "unicodeVersion": "6.0", "digest": "63d4bcf58b9d0c29612994432aad2ae35819fdd2890674e60a2f1d51601b742e" }, - { - "name": "japanese_ogre", - "unicode": "1F479", + "japanese_ogre": { + "category": "people", + "moji": "👹", + "unicodeVersion": "6.0", "digest": "434ceedd102e7dcbc07e086811673dd63659ddf8c3ec4d029a3d759a0abfcbdb" }, - { - "name": "jeans", - "unicode": "1F456", + "jeans": { + "category": "people", + "moji": "👖", + "unicodeVersion": "6.0", "digest": "f986ad32e419cca81c995f8371f0189d1490172a97ebbeac60054a1af08949c5" }, - { - "name": "joy", - "unicode": "1F602", + "joy": { + "category": "people", + "moji": "😂", + "unicodeVersion": "6.0", "digest": "75d7a05043523d290c46d3b313b19ed3c95271f1110bcf234cf13d4273625b08" }, - { - "name": "joy_cat", - "unicode": "1F639", + "joy_cat": { + "category": "people", + "moji": "😹", + "unicodeVersion": "6.0", "digest": "a65c999604147e5e20170fcb14f80a1ff0a633f991492e1f790b2ad4caec7b7e" }, - { - "name": "joystick", - "unicode": "1F579", + "joystick": { + "category": "objects", + "moji": "🕹", + "unicodeVersion": "7.0", "digest": "671ee588f397a96f27056a67e6a06d6e8d22c2109ec57b2859badb5fec9cf8dd" }, - { - "name": "juggling", - "unicode": "1F939", + "juggling": { + "category": "activity", + "moji": "🤹", + "unicodeVersion": "9.0", "digest": "1f5dafa78de8b37f3df88fdf3084d2380666bd74ab2f449754d8724f6f8dbfa5" }, - { - "name": "juggler", - "unicode": "1F939", - "digest": "1f5dafa78de8b37f3df88fdf3084d2380666bd74ab2f449754d8724f6f8dbfa5" - }, - { - "name": "juggling_tone1", - "unicode": "1F939-1F3FB", - "digest": "b0b4d020148c896be69c28b08e3c486f6db270d138c7ccf4be362b29eb99878d" - }, - { - "name": "juggler_tone1", - "unicode": "1F939-1F3FB", + "juggling_tone1": { + "category": "activity", + "moji": "🤹🏻", + "unicodeVersion": "9.0", "digest": "b0b4d020148c896be69c28b08e3c486f6db270d138c7ccf4be362b29eb99878d" }, - { - "name": "juggling_tone2", - "unicode": "1F939-1F3FC", + "juggling_tone2": { + "category": "activity", + "moji": "🤹🏼", + "unicodeVersion": "9.0", "digest": "cfe0c1649b2fdca03673e0e64f3a7d06d4bd49b8954c769aeb7eb88b70ec99f4" }, - { - "name": "juggler_tone2", - "unicode": "1F939-1F3FC", - "digest": "cfe0c1649b2fdca03673e0e64f3a7d06d4bd49b8954c769aeb7eb88b70ec99f4" - }, - { - "name": "juggling_tone3", - "unicode": "1F939-1F3FD", - "digest": "7f87022722008bb265abe245e8157dc7a61944f5da62b3cf86f26ee1b3bdef63" - }, - { - "name": "juggler_tone3", - "unicode": "1F939-1F3FD", + "juggling_tone3": { + "category": "activity", + "moji": "🤹🏽", + "unicodeVersion": "9.0", "digest": "7f87022722008bb265abe245e8157dc7a61944f5da62b3cf86f26ee1b3bdef63" }, - { - "name": "juggling_tone4", - "unicode": "1F939-1F3FE", + "juggling_tone4": { + "category": "activity", + "moji": "🤹🏾", + "unicodeVersion": "9.0", "digest": "1f00da8c05582c95501cc6c3fe5ce0f9bfbc16789dcee59844a8fe7831198583" }, - { - "name": "juggler_tone4", - "unicode": "1F939-1F3FE", - "digest": "1f00da8c05582c95501cc6c3fe5ce0f9bfbc16789dcee59844a8fe7831198583" - }, - { - "name": "juggling_tone5", - "unicode": "1F939-1F3FF", - "digest": "a195bf734788eb7961c00dbc05255a49da8b9d5042fada29b26cc20393d3ce52" - }, - { - "name": "juggler_tone5", - "unicode": "1F939-1F3FF", + "juggling_tone5": { + "category": "activity", + "moji": "🤹🏿", + "unicodeVersion": "9.0", "digest": "a195bf734788eb7961c00dbc05255a49da8b9d5042fada29b26cc20393d3ce52" }, - { - "name": "kaaba", - "unicode": "1F54B", + "kaaba": { + "category": "travel", + "moji": "🕋", + "unicodeVersion": "8.0", "digest": "a4618782f9583f077bd383965f1c91b9985a949bb7b6cec7af22914e7f5e9ab6" }, - { - "name": "key", - "unicode": "1F511", + "key": { + "category": "objects", + "moji": "🔑", + "unicodeVersion": "6.0", "digest": "66719fa77a50a0827c8d47237e2704c03e38186e6fef80627a765473b2294c2e" }, - { - "name": "key2", - "unicode": "1F5DD", + "key2": { + "category": "objects", + "moji": "🗝", + "unicodeVersion": "7.0", "digest": "f57240a014a9da5da3d4d98c17d0a55e0ff2e5f2d22731d2fc867105cff54c6e" }, - { - "name": "old_key", - "unicode": "1F5DD", - "digest": "f57240a014a9da5da3d4d98c17d0a55e0ff2e5f2d22731d2fc867105cff54c6e" - }, - { - "name": "keyboard", - "unicode": "2328", + "keyboard": { + "category": "objects", + "moji": "⌨", + "unicodeVersion": "1.1", "digest": "34da8ff62ca964142f9281b80123dbba74deaac8d77fa61758c30cfb36c31386" }, - { - "name": "kimono", - "unicode": "1F458", + "kimono": { + "category": "people", + "moji": "👘", + "unicodeVersion": "6.0", "digest": "637182590e256c8fb74ce4c0565f5180c07f06e3bdebf30138ed3259b209c27f" }, - { - "name": "kiss", - "unicode": "1F48B", + "kiss": { + "category": "people", + "moji": "💋", + "unicodeVersion": "6.0", "digest": "62f9b9ffcb01558cd5bb829344a1d1d399511663ff5235405c1f786c9416a94d" }, - { - "name": "kiss_mm", - "unicode": "1F468-2764-1F48B-1F468", - "digest": "6b0ae32ecb7ec0f0f43dc7a1350711185cce114c52752395f364ddbfb4f1fff4" - }, - { - "name": "couplekiss_mm", - "unicode": "1F468-2764-1F48B-1F468", + "kiss_mm": { + "category": "people", + "moji": "👨❤️💋👨", + "unicodeVersion": "6.0", "digest": "6b0ae32ecb7ec0f0f43dc7a1350711185cce114c52752395f364ddbfb4f1fff4" }, - { - "name": "kiss_ww", - "unicode": "1F469-2764-1F48B-1F469", + "kiss_ww": { + "category": "people", + "moji": "👩❤️💋👩", + "unicodeVersion": "6.0", "digest": "6de420cf752e706b1b7e9522b1b9be62eda069cb028c8fd587caf39f6a142e6a" }, - { - "name": "couplekiss_ww", - "unicode": "1F469-2764-1F48B-1F469", - "digest": "6de420cf752e706b1b7e9522b1b9be62eda069cb028c8fd587caf39f6a142e6a" - }, - { - "name": "kissing", - "unicode": "1F617", + "kissing": { + "category": "people", + "moji": "😗", + "unicodeVersion": "6.1", "digest": "b4a505f9e3d7fbd0ac60111f0e678cf425a5fd1abc65a3e9db59ae4abcfb8e85" }, - { - "name": "kissing_cat", - "unicode": "1F63D", + "kissing_cat": { + "category": "people", + "moji": "😽", + "unicodeVersion": "6.0", "digest": "a00431bf10601db4998e78433279167e52cbd36aed885399482529d5cdab8636" }, - { - "name": "kissing_closed_eyes", - "unicode": "1F61A", + "kissing_closed_eyes": { + "category": "people", + "moji": "😚", + "unicodeVersion": "6.0", "digest": "ae474db7daf80fe0b82ae1f2a11672cfcd9f9126e100f6e6d4b8a0d135dce39d" }, - { - "name": "kissing_heart", - "unicode": "1F618", + "kissing_heart": { + "category": "people", + "moji": "😘", + "unicodeVersion": "6.0", "digest": "bce372573bd3b347b555c1cd22087e03e650df73c8e0284ab668bf6633251632" }, - { - "name": "kissing_smiling_eyes", - "unicode": "1F619", + "kissing_smiling_eyes": { + "category": "people", + "moji": "😙", + "unicodeVersion": "6.1", "digest": "f0f8636cb1a02b93cc72ce1b194b890fca823d91e35926b889be3ecfae79207f" }, - { - "name": "kiwi", - "unicode": "1F95D", - "digest": "70a3a05f333d9455d2da12eed970bc3baae416286848fed8e5dd31b5be0819be" - }, - { - "name": "kiwifruit", - "unicode": "1F95D", + "kiwi": { + "category": "food", + "moji": "🥝", + "unicodeVersion": "9.0", "digest": "70a3a05f333d9455d2da12eed970bc3baae416286848fed8e5dd31b5be0819be" }, - { - "name": "knife", - "unicode": "1F52A", + "knife": { + "category": "objects", + "moji": "🔪", + "unicodeVersion": "6.0", "digest": "e6189e4843c6e80875b4952fcddb0c858f7c6039b9214bbec6a261a1358425df" }, - { - "name": "koala", - "unicode": "1F428", + "koala": { + "category": "nature", + "moji": "🐨", + "unicodeVersion": "6.0", "digest": "c58f7e0abae42c2218a85efed0e04151df67187815bebca7f3db6f435e0dab4d" }, - { - "name": "koko", - "unicode": "1F201", + "koko": { + "category": "symbols", + "moji": "🈁", + "unicodeVersion": "6.0", "digest": "5f45eb49bbf298e1fadedfe6cccc297850fcaaa4535e4cc911d48d979af55807" }, - { - "name": "label", - "unicode": "1F3F7", + "label": { + "category": "objects", + "moji": "🏷", + "unicodeVersion": "7.0", "digest": "9550ed50cedbc56eb1bd22a8a0809d837048a33d6e2e6e7d65c50d95fa05a85d" }, - { - "name": "large_blue_circle", - "unicode": "1F535", + "large_blue_circle": { + "category": "symbols", + "moji": "🔵", + "unicodeVersion": "6.0", "digest": "0df3fb3b09a6269459a3d9a1fe78db572190a948680844cfe758f53b6a482ff4" }, - { - "name": "large_blue_diamond", - "unicode": "1F537", + "large_blue_diamond": { + "category": "symbols", + "moji": "🔷", + "unicodeVersion": "6.0", "digest": "7f646b4e9de2788ed09e45f72cb512c269dda4989029b39bf9a2556659321651" }, - { - "name": "large_orange_diamond", - "unicode": "1F536", + "large_orange_diamond": { + "category": "symbols", + "moji": "🔶", + "unicodeVersion": "6.0", "digest": "80ae005ef9d79190c777f00de0993f8b3cb783f7051d76e971640c8c0827c338" }, - { - "name": "last_quarter_moon", - "unicode": "1F317", + "last_quarter_moon": { + "category": "nature", + "moji": "🌗", + "unicodeVersion": "6.0", "digest": "3d1f276607c685d50f4b70d00a57750a57ad9ad84256dafd2dc8eef8c72300c3" }, - { - "name": "last_quarter_moon_with_face", - "unicode": "1F31C", + "last_quarter_moon_with_face": { + "category": "nature", + "moji": "🌜", + "unicodeVersion": "6.0", "digest": "d516825ba52dc67f5a01433fb9df2aa77742d38efde4225983ebc4882cbdfe5d" }, - { - "name": "laughing", - "unicode": "1F606", + "laughing": { + "category": "people", + "moji": "😆", + "unicodeVersion": "6.0", "digest": "e9ea994b39650740c4961f070ed492d86b3acf6e6a830a6dadaa3a6872e81b81" }, - { - "name": "satisfied", - "unicode": "1F606", - "digest": "e9ea994b39650740c4961f070ed492d86b3acf6e6a830a6dadaa3a6872e81b81" - }, - { - "name": "leaves", - "unicode": "1F343", + "leaves": { + "category": "nature", + "moji": "🍃", + "unicodeVersion": "6.0", "digest": "56a7a0e767a6f214d340d1b5989efd99fec52c6aa306ec5c3328e32234a1631b" }, - { - "name": "ledger", - "unicode": "1F4D2", + "ledger": { + "category": "objects", + "moji": "📒", + "unicodeVersion": "6.0", "digest": "e58cb714353e96a2891a5d97910ff79660e637af909b81c49c919d3735db55b4" }, - { - "name": "left_facing_fist", - "unicode": "1F91B", + "left_facing_fist": { + "category": "people", + "moji": "🤛", + "unicodeVersion": "9.0", "digest": "7861be485beefae0de341df2f21576666e22f63511a033e785752f30c07291da" }, - { - "name": "left_fist", - "unicode": "1F91B", - "digest": "7861be485beefae0de341df2f21576666e22f63511a033e785752f30c07291da" - }, - { - "name": "left_facing_fist_tone1", - "unicode": "1F91B-1F3FB", + "left_facing_fist_tone1": { + "category": "people", + "moji": "🤛🏻", + "unicodeVersion": "9.0", "digest": "2e4c4dd96b0e4b46fe0f9ce5666344d266d0f17a8544cbae73d96638d1955296" }, - { - "name": "left_fist_tone1", - "unicode": "1F91B-1F3FB", - "digest": "2e4c4dd96b0e4b46fe0f9ce5666344d266d0f17a8544cbae73d96638d1955296" - }, - { - "name": "left_facing_fist_tone2", - "unicode": "1F91B-1F3FC", - "digest": "b96a63a801175ce98a75f0edad7b5574251a3fbbd894d8ab3f21aeeda366cc13" - }, - { - "name": "left_fist_tone2", - "unicode": "1F91B-1F3FC", + "left_facing_fist_tone2": { + "category": "people", + "moji": "🤛🏼", + "unicodeVersion": "9.0", "digest": "b96a63a801175ce98a75f0edad7b5574251a3fbbd894d8ab3f21aeeda366cc13" }, - { - "name": "left_facing_fist_tone3", - "unicode": "1F91B-1F3FD", + "left_facing_fist_tone3": { + "category": "people", + "moji": "🤛🏽", + "unicodeVersion": "9.0", "digest": "99df84635513c2ebfef24df1bd3705233e02149eef788c7b82ca0548df6f6ea5" }, - { - "name": "left_fist_tone3", - "unicode": "1F91B-1F3FD", - "digest": "99df84635513c2ebfef24df1bd3705233e02149eef788c7b82ca0548df6f6ea5" - }, - { - "name": "left_facing_fist_tone4", - "unicode": "1F91B-1F3FE", - "digest": "68954842ca725aec0aa39bce4aa81aad17ac30f5f298561dfa411feb07414cd3" - }, - { - "name": "left_fist_tone4", - "unicode": "1F91B-1F3FE", + "left_facing_fist_tone4": { + "category": "people", + "moji": "🤛🏾", + "unicodeVersion": "9.0", "digest": "68954842ca725aec0aa39bce4aa81aad17ac30f5f298561dfa411feb07414cd3" }, - { - "name": "left_facing_fist_tone5", - "unicode": "1F91B-1F3FF", - "digest": "a419b33fae82612dc860ff48950c0547a1642d4f0c94b6547324440837d3bb21" - }, - { - "name": "left_fist_tone5", - "unicode": "1F91B-1F3FF", + "left_facing_fist_tone5": { + "category": "people", + "moji": "🤛🏿", + "unicodeVersion": "9.0", "digest": "a419b33fae82612dc860ff48950c0547a1642d4f0c94b6547324440837d3bb21" }, - { - "name": "left_luggage", - "unicode": "1F6C5", + "left_luggage": { + "category": "symbols", + "moji": "🛅", + "unicodeVersion": "6.0", "digest": "6625077767a51163ea20cbc299f3c13fd5ccf1b5ce365ee702ef1fef6be3dadf" }, - { - "name": "left_right_arrow", - "unicode": "2194", + "left_right_arrow": { + "category": "symbols", + "moji": "↔", + "unicodeVersion": "1.1", "digest": "560fcf1b794eb0d5269c73b3f8da57540cbb8a6f1a9af7a9d10b202252247e34" }, - { - "name": "leftwards_arrow_with_hook", - "unicode": "21A9", + "leftwards_arrow_with_hook": { + "category": "symbols", + "moji": "↩", + "unicodeVersion": "1.1", "digest": "504714c5559b1bd35aa469be83069a923d1a25f364cac08c10df0195749e7b26" }, - { - "name": "lemon", - "unicode": "1F34B", + "lemon": { + "category": "food", + "moji": "🍋", + "unicodeVersion": "6.0", "digest": "ccca25bb6ac47770dba3aaf75144128f9a73299061969b25a35ad1733dcde5fe" }, - { - "name": "leo", - "unicode": "264C", + "leo": { + "category": "symbols", + "moji": "♌", + "unicodeVersion": "1.1", "digest": "f2ed930e279699962f189e0cac519cc29d339b3e82debfdc90c5b0935a7543bb" }, - { - "name": "leopard", - "unicode": "1F406", + "leopard": { + "category": "nature", + "moji": "🐆", + "unicodeVersion": "6.0", "digest": "d4a8964b6f2cdf6ddf074d0f1f2f65783a1a43eb4af426905fad0e60899939c7" }, - { - "name": "level_slider", - "unicode": "1F39A", + "level_slider": { + "category": "objects", + "moji": "🎚", + "unicodeVersion": "7.0", "digest": "48842324f54d971ebf548a89a82ac7f29e235702081c91b477b1a92d427290e7" }, - { - "name": "levitate", - "unicode": "1F574", - "digest": "453c24bf2544ed3ef3c710a7fabbd5fdace4dc65cddd377274d30d921523b50b" - }, - { - "name": "man_in_business_suit_levitating", - "unicode": "1F574", + "levitate": { + "category": "activity", + "moji": "🕴", + "unicodeVersion": "7.0", "digest": "453c24bf2544ed3ef3c710a7fabbd5fdace4dc65cddd377274d30d921523b50b" }, - { - "name": "libra", - "unicode": "264E", + "libra": { + "category": "symbols", + "moji": "♎", + "unicodeVersion": "1.1", "digest": "e330ba05bb449db074bc23d1514246ca5e249110f44ddb5804e5510eef6deac1" }, - { - "name": "lifter", - "unicode": "1F3CB", + "lifter": { + "category": "activity", + "moji": "🏋", + "unicodeVersion": "7.0", "digest": "d6c94a32eb863d14a2a01add8ab95040f42a55d9e3f90641a0fe143d58127558" }, - { - "name": "weight_lifter", - "unicode": "1F3CB", - "digest": "d6c94a32eb863d14a2a01add8ab95040f42a55d9e3f90641a0fe143d58127558" - }, - { - "name": "lifter_tone1", - "unicode": "1F3CB-1F3FB", - "digest": "870acf2f554fce360b58d3e98b4c0558d7ec7775587776c0f9d40c6fb1bdacf9" - }, - { - "name": "weight_lifter_tone1", - "unicode": "1F3CB-1F3FB", + "lifter_tone1": { + "category": "activity", + "moji": "🏋🏻", + "unicodeVersion": "8.0", "digest": "870acf2f554fce360b58d3e98b4c0558d7ec7775587776c0f9d40c6fb1bdacf9" }, - { - "name": "lifter_tone2", - "unicode": "1F3CB-1F3FC", - "digest": "1a7ece8512e42241cdd95c85ccc509bc0ff9c7c6ffaff2be343c77f417a27576" - }, - { - "name": "weight_lifter_tone2", - "unicode": "1F3CB-1F3FC", + "lifter_tone2": { + "category": "activity", + "moji": "🏋🏼", + "unicodeVersion": "8.0", "digest": "1a7ece8512e42241cdd95c85ccc509bc0ff9c7c6ffaff2be343c77f417a27576" }, - { - "name": "lifter_tone3", - "unicode": "1F3CB-1F3FD", - "digest": "4bc633ee82a0fb59feba379fb6901a489e4ac849d758f9c8e7a1a0a26eaa380c" - }, - { - "name": "weight_lifter_tone3", - "unicode": "1F3CB-1F3FD", + "lifter_tone3": { + "category": "activity", + "moji": "🏋🏽", + "unicodeVersion": "8.0", "digest": "4bc633ee82a0fb59feba379fb6901a489e4ac849d758f9c8e7a1a0a26eaa380c" }, - { - "name": "lifter_tone4", - "unicode": "1F3CB-1F3FE", - "digest": "d086fe5577b5ba80676f2224d886f8ebe4588314f429f12a34c52c971ed71b5c" - }, - { - "name": "weight_lifter_tone4", - "unicode": "1F3CB-1F3FE", + "lifter_tone4": { + "category": "activity", + "moji": "🏋🏾", + "unicodeVersion": "8.0", "digest": "d086fe5577b5ba80676f2224d886f8ebe4588314f429f12a34c52c971ed71b5c" }, - { - "name": "lifter_tone5", - "unicode": "1F3CB-1F3FF", - "digest": "79b0edf6ce1fd024dd7f458e322ad8588af0b789a04cc1cf38380dc8b9c76f55" - }, - { - "name": "weight_lifter_tone5", - "unicode": "1F3CB-1F3FF", + "lifter_tone5": { + "category": "activity", + "moji": "🏋🏿", + "unicodeVersion": "8.0", "digest": "79b0edf6ce1fd024dd7f458e322ad8588af0b789a04cc1cf38380dc8b9c76f55" }, - { - "name": "light_rail", - "unicode": "1F688", + "light_rail": { + "category": "travel", + "moji": "🚈", + "unicodeVersion": "6.0", "digest": "2f30b23a738371690b2f00d96ddb5ceb90a1442b5478754626a3dfa263ed2fc1" }, - { - "name": "link", - "unicode": "1F517", + "link": { + "category": "objects", + "moji": "🔗", + "unicodeVersion": "6.0", "digest": "7bf567aabd1fc38b3d70422f9db3a13b50950cf6207e70962c9938827c196ccb" }, - { - "name": "lion_face", - "unicode": "1F981", - "digest": "dd24f2668e973ec973e97dc111f59a2cc14e9b608387401191dd53368d28d4fa" - }, - { - "name": "lion", - "unicode": "1F981", + "lion_face": { + "category": "nature", + "moji": "🦁", + "unicodeVersion": "8.0", "digest": "dd24f2668e973ec973e97dc111f59a2cc14e9b608387401191dd53368d28d4fa" }, - { - "name": "lips", - "unicode": "1F444", + "lips": { + "category": "people", + "moji": "👄", + "unicodeVersion": "6.0", "digest": "8740d8086525c7a836d64625a6915cc1c59af69ba143456dbb59e0179276895e" }, - { - "name": "lipstick", - "unicode": "1F484", + "lipstick": { + "category": "people", + "moji": "💄", + "unicodeVersion": "6.0", "digest": "751dcb22706a796033b13a2ccb94304236ec13207ad4d011e02d230ae33ab5c1" }, - { - "name": "lizard", - "unicode": "1F98E", + "lizard": { + "category": "nature", + "moji": "🦎", + "unicodeVersion": "9.0", "digest": "fb9191f9eab58b8403d4c4626ccbb14ba05c1f6944011751a8edcc4dd03c66e6" }, - { - "name": "lock", - "unicode": "1F512", + "lock": { + "category": "objects", + "moji": "🔒", + "unicodeVersion": "6.0", "digest": "043b4fc0b8c79d47a07d91308e628e1ac262aea6c1ec05e6b84bf7bcdf89dc83" }, - { - "name": "lock_with_ink_pen", - "unicode": "1F50F", + "lock_with_ink_pen": { + "category": "objects", + "moji": "🔏", + "unicodeVersion": "6.0", "digest": "7b5e959b26cf7296c7b230fc2be9feb9e38391c5001951a019d16b169a71aba9" }, - { - "name": "lollipop", - "unicode": "1F36D", + "lollipop": { + "category": "food", + "moji": "🍭", + "unicodeVersion": "6.0", "digest": "17b6a0df47ec758a2f9c087b46a6902cee344d39407ef4c321e408505cbb72ca" }, - { - "name": "loop", - "unicode": "27BF", + "loop": { + "category": "symbols", + "moji": "➿", + "unicodeVersion": "6.0", "digest": "9f20ecc34b3c871789ba7d0712aa31e7a74b6c1558ac8bea385bc40590056726" }, - { - "name": "loud_sound", - "unicode": "1F50A", + "loud_sound": { + "category": "symbols", + "moji": "🔊", + "unicodeVersion": "6.0", "digest": "64b12db9ddd8adf74a9fc2bd83c7979ea865113347f7ce8666e9ccf5019e715f" }, - { - "name": "loudspeaker", - "unicode": "1F4E2", + "loudspeaker": { + "category": "symbols", + "moji": "📢", + "unicodeVersion": "6.0", "digest": "1e1f35d16dd2898ebaa6f2b2868203df6e09c8a70df069c92d6d1b5cb2ac0976" }, - { - "name": "love_hotel", - "unicode": "1F3E9", + "love_hotel": { + "category": "travel", + "moji": "🏩", + "unicodeVersion": "6.0", "digest": "ff8966a50fd47a216855488eb09a367d231fea21f49e7e5325191d32fb494473" }, - { - "name": "love_letter", - "unicode": "1F48C", + "love_letter": { + "category": "objects", + "moji": "💌", + "unicodeVersion": "6.0", "digest": "037261c8ca4d72f7205e51664591696da2ae7ceb19f1c1c9f6123da5a5979d29" }, - { - "name": "low_brightness", - "unicode": "1F505", + "low_brightness": { + "category": "symbols", + "moji": "🔅", + "unicodeVersion": "6.0", "digest": "a065d00a416e297c168b0a675cafcf492fedf94865cb21801a1be5a3914593d4" }, - { - "name": "lying_face", - "unicode": "1F925", - "digest": "ce836170165e1b70938273f289c02c2106873cd9ab5472dbcd487c2f9f53f13d" - }, - { - "name": "liar", - "unicode": "1F925", + "lying_face": { + "category": "people", + "moji": "🤥", + "unicodeVersion": "9.0", "digest": "ce836170165e1b70938273f289c02c2106873cd9ab5472dbcd487c2f9f53f13d" }, - { - "name": "m", - "unicode": "24C2", + "m": { + "category": "symbols", + "moji": "Ⓜ", + "unicodeVersion": "1.1", "digest": "54588ac2b7fcd53a96f17124e9de69b617613fcd5af9ad2930a094cb795bb9f4" }, - { - "name": "mag", - "unicode": "1F50D", + "mag": { + "category": "objects", + "moji": "🔍", + "unicodeVersion": "6.0", "digest": "a6e31a2efa7d9427aaa30b45d9f4181ee55c44be08aea2df165a86e0e6d9eaa1" }, - { - "name": "mag_right", - "unicode": "1F50E", + "mag_right": { + "category": "objects", + "moji": "🔎", + "unicodeVersion": "6.0", "digest": "c7d8ceeb05db261e5eaab31dc4da432d0d5592a2ed71e526c5a542daa230bbaf" }, - { - "name": "mahjong", - "unicode": "1F004", + "mahjong": { + "category": "symbols", + "moji": "🀄", + "unicodeVersion": "5.1", "digest": "755d69f988434ce1c17531a8b7ac92ead6f5607c2635a22f10e0ad70f09fc3e6" }, - { - "name": "mailbox", - "unicode": "1F4EB", + "mailbox": { + "category": "objects", + "moji": "📫", + "unicodeVersion": "6.0", "digest": "2069091be90a530a43ef29d5ec7688c351bf4d5b08d63a0d20d72b67d639ec62" }, - { - "name": "mailbox_closed", - "unicode": "1F4EA", + "mailbox_closed": { + "category": "objects", + "moji": "📪", + "unicodeVersion": "6.0", "digest": "d88d65bfebb8216535fd055c69f319564b2cf0b0901820f8312f581864557ed4" }, - { - "name": "mailbox_with_mail", - "unicode": "1F4EC", + "mailbox_with_mail": { + "category": "objects", + "moji": "📬", + "unicodeVersion": "6.0", "digest": "69e966b4659128991a70c6a2dd4d647551bedb91bdf5ce688958686bbec56381" }, - { - "name": "mailbox_with_no_mail", - "unicode": "1F4ED", + "mailbox_with_no_mail": { + "category": "objects", + "moji": "📭", + "unicodeVersion": "6.0", "digest": "9e92d8ee88f660ce56da61077c80ec26c5d8f54ebd2306c4cfa16f6c1b981f83" }, - { - "name": "man", - "unicode": "1F468", + "man": { + "category": "people", + "moji": "👨", + "unicodeVersion": "6.0", "digest": "42b882d2c6aa095f1afcf901203838d95c1908bdc725519779186b9c33c728d7" }, - { - "name": "man_dancing", - "unicode": "1F57A", - "digest": "9f632ee0c886d5f03c61e5f3a27668262c0cc2693b857a91c23c1e5ea3785b9e" - }, - { - "name": "male_dancer", - "unicode": "1F57A", + "man_dancing": { + "category": "people", + "moji": "🕺", + "unicodeVersion": "9.0", "digest": "9f632ee0c886d5f03c61e5f3a27668262c0cc2693b857a91c23c1e5ea3785b9e" }, - { - "name": "man_dancing_tone1", - "unicode": "1F57A-1F3FB", - "digest": "6c56a16cb105bcdd97472645b3a351cebdbb1132cbfd18b0118f289db5fbe741" - }, - { - "name": "male_dancer_tone1", - "unicode": "1F57A-1F3FB", + "man_dancing_tone1": { + "category": "activity", + "moji": "🕺🏻", + "unicodeVersion": "9.0", "digest": "6c56a16cb105bcdd97472645b3a351cebdbb1132cbfd18b0118f289db5fbe741" }, - { - "name": "man_dancing_tone2", - "unicode": "1F57A-1F3FC", - "digest": "ed7e78c14d205a03fdd5581e5213add69a55e13b4cbaf76a6d5a0d6c80f53327" - }, - { - "name": "male_dancer_tone2", - "unicode": "1F57A-1F3FC", + "man_dancing_tone2": { + "category": "activity", + "moji": "🕺🏼", + "unicodeVersion": "9.0", "digest": "ed7e78c14d205a03fdd5581e5213add69a55e13b4cbaf76a6d5a0d6c80f53327" }, - { - "name": "man_dancing_tone3", - "unicode": "1F57A-1F3FD", + "man_dancing_tone3": { + "category": "activity", + "moji": "🕺🏽", + "unicodeVersion": "9.0", "digest": "13b45403e11800163406206eedeb8b579cc83eca2f60246be97e099164387bc8" }, - { - "name": "male_dancer_tone3", - "unicode": "1F57A-1F3FD", - "digest": "13b45403e11800163406206eedeb8b579cc83eca2f60246be97e099164387bc8" - }, - { - "name": "man_dancing_tone4", - "unicode": "1F57A-1F3FE", - "digest": "f6feb1b0b83565fadcdd1a8737d3daa08893e919547d2a06de899160162d9c4a" - }, - { - "name": "male_dancer_tone4", - "unicode": "1F57A-1F3FE", + "man_dancing_tone4": { + "category": "activity", + "moji": "🕺🏾", + "unicodeVersion": "9.0", "digest": "f6feb1b0b83565fadcdd1a8737d3daa08893e919547d2a06de899160162d9c4a" }, - { - "name": "man_dancing_tone5", - "unicode": "1F57A-1F3FF", + "man_dancing_tone5": { + "category": "activity", + "moji": "🕺🏿", + "unicodeVersion": "9.0", "digest": "fe20a9ed9ba991653b4d0683de347ed7c226a5d75610307584a2ddd6fcd1e3f2" }, - { - "name": "male_dancer_tone5", - "unicode": "1F57A-1F3FF", - "digest": "fe20a9ed9ba991653b4d0683de347ed7c226a5d75610307584a2ddd6fcd1e3f2" - }, - { - "name": "man_in_tuxedo", - "unicode": "1F935", + "man_in_tuxedo": { + "category": "people", + "moji": "🤵", + "unicodeVersion": "9.0", "digest": "4d451a971dfefedc4830ba78e19b123f250e09ae65baddccdc56c0f8aa3a9b50" }, - { - "name": "man_in_tuxedo_tone1", - "unicode": "1F935-1F3FB", - "digest": "2814833334fb211ae2ecb1fb5964e9752282d0fb4d7f3477de5dd2a4f812a793" - }, - { - "name": "tuxedo_tone1", - "unicode": "1F935-1F3FB", + "man_in_tuxedo_tone1": { + "category": "people", + "moji": "🤵🏻", + "unicodeVersion": "9.0", "digest": "2814833334fb211ae2ecb1fb5964e9752282d0fb4d7f3477de5dd2a4f812a793" }, - { - "name": "man_in_tuxedo_tone2", - "unicode": "1F935-1F3FC", + "man_in_tuxedo_tone2": { + "category": "people", + "moji": "🤵🏼", + "unicodeVersion": "9.0", "digest": "cd1bab9ee0e2335d3cd99d51216cccdc4fc3c2cf20129b8b7e11a51a77258f68" }, - { - "name": "tuxedo_tone2", - "unicode": "1F935-1F3FC", - "digest": "cd1bab9ee0e2335d3cd99d51216cccdc4fc3c2cf20129b8b7e11a51a77258f68" - }, - { - "name": "man_in_tuxedo_tone3", - "unicode": "1F935-1F3FD", - "digest": "f387775f925fe60b9f3e7cad63a55d4d196ddd41658029a70440d14c17cb99f9" - }, - { - "name": "tuxedo_tone3", - "unicode": "1F935-1F3FD", + "man_in_tuxedo_tone3": { + "category": "people", + "moji": "🤵🏽", + "unicodeVersion": "9.0", "digest": "f387775f925fe60b9f3e7cad63a55d4d196ddd41658029a70440d14c17cb99f9" }, - { - "name": "man_in_tuxedo_tone4", - "unicode": "1F935-1F3FE", + "man_in_tuxedo_tone4": { + "category": "people", + "moji": "🤵🏾", + "unicodeVersion": "9.0", "digest": "08debd7a573d1201aee8a2f281ef7cb638d4a2a096222150391f36963f07c622" }, - { - "name": "tuxedo_tone4", - "unicode": "1F935-1F3FE", - "digest": "08debd7a573d1201aee8a2f281ef7cb638d4a2a096222150391f36963f07c622" - }, - { - "name": "man_in_tuxedo_tone5", - "unicode": "1F935-1F3FF", - "digest": "e3b10e0619f0911cf9b665a265f4ef829b8f6ba6e9c3a021d0539a27e315f8fe" - }, - { - "name": "tuxedo_tone5", - "unicode": "1F935-1F3FF", + "man_in_tuxedo_tone5": { + "category": "people", + "moji": "🤵🏿", + "unicodeVersion": "9.0", "digest": "e3b10e0619f0911cf9b665a265f4ef829b8f6ba6e9c3a021d0539a27e315f8fe" }, - { - "name": "man_tone1", - "unicode": "1F468-1F3FB", + "man_tone1": { + "category": "people", + "moji": "👨🏻", + "unicodeVersion": "8.0", "digest": "7053e265fa7d2594de54a6c5d06c21795b9a7dfb36a1c5594ca43c4c6cc56504" }, - { - "name": "man_tone2", - "unicode": "1F468-1F3FC", + "man_tone2": { + "category": "people", + "moji": "👨🏼", + "unicodeVersion": "8.0", "digest": "7ebc64de40d3ac60fb761be5cf94f53fa10b4f03fb66add46c90f5d98eaf71eb" }, - { - "name": "man_tone3", - "unicode": "1F468-1F3FD", + "man_tone3": { + "category": "people", + "moji": "👨🏽", + "unicodeVersion": "8.0", "digest": "77ceef4d3740ed4751acb83dd45b6b754cf625c522c6757309cd4d61202d7149" }, - { - "name": "man_tone4", - "unicode": "1F468-1F3FE", + "man_tone4": { + "category": "people", + "moji": "👨🏾", + "unicodeVersion": "8.0", "digest": "41e6037c393f61cca61b9a81b27ed14a95d75fe380e3a00153c33a371a836ffd" }, - { - "name": "man_tone5", - "unicode": "1F468-1F3FF", + "man_tone5": { + "category": "people", + "moji": "👨🏿", + "unicodeVersion": "8.0", "digest": "a8cebfd39a5b9c79af7cc37f205e1135376056fee287af967c9f55d415572d99" }, - { - "name": "man_with_gua_pi_mao", - "unicode": "1F472", + "man_with_gua_pi_mao": { + "category": "people", + "moji": "👲", + "unicodeVersion": "6.0", "digest": "3dae285e900c69986a48db0fa89d4f371a49f38608059cdae52be098030c5ac4" }, - { - "name": "man_with_gua_pi_mao_tone1", - "unicode": "1F472-1F3FB", + "man_with_gua_pi_mao_tone1": { + "category": "people", + "moji": "👲🏻", + "unicodeVersion": "8.0", "digest": "35404d8e266920c78edd9e7143fb052b42f65242a5698494c4f4365e9183cc67" }, - { - "name": "man_with_gua_pi_mao_tone2", - "unicode": "1F472-1F3FC", + "man_with_gua_pi_mao_tone2": { + "category": "people", + "moji": "👲🏼", + "unicodeVersion": "8.0", "digest": "82d4f968665a93c7543372c8a1eeb0f25d0ea6842d5e518bd91c226c6c3ab8c2" }, - { - "name": "man_with_gua_pi_mao_tone3", - "unicode": "1F472-1F3FD", + "man_with_gua_pi_mao_tone3": { + "category": "people", + "moji": "👲🏽", + "unicodeVersion": "8.0", "digest": "f44159f0c672b9b833449382896180e799abf574f5b3c6cd9541caa992fa18ce" }, - { - "name": "man_with_gua_pi_mao_tone4", - "unicode": "1F472-1F3FE", + "man_with_gua_pi_mao_tone4": { + "category": "people", + "moji": "👲🏾", + "unicodeVersion": "8.0", "digest": "c79060188f9461ca34eaa225b7682d8c410883609509fb731c992db69bfeeb50" }, - { - "name": "man_with_gua_pi_mao_tone5", - "unicode": "1F472-1F3FF", + "man_with_gua_pi_mao_tone5": { + "category": "people", + "moji": "👲🏿", + "unicodeVersion": "8.0", "digest": "de9e4acdb10f7abddeeabc0b48d91139fc8b544a601c530db811f099991b0d38" }, - { - "name": "man_with_turban", - "unicode": "1F473", + "man_with_turban": { + "category": "people", + "moji": "👳", + "unicodeVersion": "6.0", "digest": "db72c944e93983f38d00e3e936ebb5b243c6069f1f1236d46f6a9f1beb8d6634" }, - { - "name": "man_with_turban_tone1", - "unicode": "1F473-1F3FB", + "man_with_turban_tone1": { + "category": "people", + "moji": "👳🏻", + "unicodeVersion": "8.0", "digest": "b6d7489c4cd151af09fff48b62c54c336303e14866e6ef38f94cd834b085d09e" }, - { - "name": "man_with_turban_tone2", - "unicode": "1F473-1F3FC", + "man_with_turban_tone2": { + "category": "people", + "moji": "👳🏼", + "unicodeVersion": "8.0", "digest": "7854ef973c21847f452d7e78e5c460ea300e12b539ce92c69dabe8f1bf3a4382" }, - { - "name": "man_with_turban_tone3", - "unicode": "1F473-1F3FD", + "man_with_turban_tone3": { + "category": "people", + "moji": "👳🏽", + "unicodeVersion": "8.0", "digest": "1dbd9bd78f5263cbadee7d0d5754c14cfbc914f7329e25fbd97d9f5b8ce0737e" }, - { - "name": "man_with_turban_tone4", - "unicode": "1F473-1F3FE", + "man_with_turban_tone4": { + "category": "people", + "moji": "👳🏾", + "unicodeVersion": "8.0", "digest": "4f4804da4a7c98ad4f9db3ae3eaf674c8977c638e73414e33ef1f65098e413a3" }, - { - "name": "man_with_turban_tone5", - "unicode": "1F473-1F3FF", + "man_with_turban_tone5": { + "category": "people", + "moji": "👳🏿", + "unicodeVersion": "8.0", "digest": "240282aa346ef9b1d0d475ea93a02597697f0f56f086305879b532b0b933210a" }, - { - "name": "mans_shoe", - "unicode": "1F45E", + "mans_shoe": { + "category": "people", + "moji": "👞", + "unicodeVersion": "6.0", "digest": "f53fe74abd9906cd3e2dd7e7bddbe1feb9f8f7be28b807fabe452f1f60ca1b84" }, - { - "name": "map", - "unicode": "1F5FA", + "map": { + "category": "objects", + "moji": "🗺", + "unicodeVersion": "7.0", "digest": "84f496a062b5c3ae1e8013506175a69036038c8130891bcf780a69ce7fcbe4de" }, - { - "name": "world_map", - "unicode": "1F5FA", - "digest": "84f496a062b5c3ae1e8013506175a69036038c8130891bcf780a69ce7fcbe4de" - }, - { - "name": "maple_leaf", - "unicode": "1F341", + "maple_leaf": { + "category": "nature", + "moji": "🍁", + "unicodeVersion": "6.0", "digest": "72629a205e33f89337815ad7e51bb5c73947d1a9f98afe5072bdf4846827ae72" }, - { - "name": "martial_arts_uniform", - "unicode": "1F94B", - "digest": "a1ae797b31081425b388ab31efc635d8eb73a40980fd0fae4708aa5313e2a964" - }, - { - "name": "karate_uniform", - "unicode": "1F94B", + "martial_arts_uniform": { + "category": "activity", + "moji": "🥋", + "unicodeVersion": "9.0", "digest": "a1ae797b31081425b388ab31efc635d8eb73a40980fd0fae4708aa5313e2a964" }, - { - "name": "mask", - "unicode": "1F637", + "mask": { + "category": "people", + "moji": "😷", + "unicodeVersion": "6.0", "digest": "1b58af9ae599308aabf41bbd38f599fa896bd9fe5df7a40be9f2dc7e0e230600" }, - { - "name": "massage", - "unicode": "1F486", + "massage": { + "category": "people", + "moji": "💆", + "unicodeVersion": "6.0", "digest": "6ee48b4d8cec0bf31e11d7803ad9fc1f909457c8c00cb320b5671395af3c170c" }, - { - "name": "massage_tone1", - "unicode": "1F486-1F3FB", + "massage_tone1": { + "category": "people", + "moji": "💆🏻", + "unicodeVersion": "8.0", "digest": "9da162c2f39628156b87db986a6ada59372a9e9a6b3f0488d21c9e65ec3309bb" }, - { - "name": "massage_tone2", - "unicode": "1F486-1F3FC", + "massage_tone2": { + "category": "people", + "moji": "💆🏼", + "unicodeVersion": "8.0", "digest": "ac259188549b5b429b8c4929e1da2314859e8857ee49720551467aedfcc96567" }, - { - "name": "massage_tone3", - "unicode": "1F486-1F3FD", + "massage_tone3": { + "category": "people", + "moji": "💆🏽", + "unicodeVersion": "8.0", "digest": "cfd9c105b6debc10448f172afcb20d4192899f7ae5aa8af54c834153a5466364" }, - { - "name": "massage_tone4", - "unicode": "1F486-1F3FE", + "massage_tone4": { + "category": "people", + "moji": "💆🏾", + "unicodeVersion": "8.0", "digest": "38ab715c621c58454f3cb09153a96380118cf082568554b6edc5f83fb62e9297" }, - { - "name": "massage_tone5", - "unicode": "1F486-1F3FF", + "massage_tone5": { + "category": "people", + "moji": "💆🏿", + "unicodeVersion": "8.0", "digest": "32480457734121b0c83e9be6d693ae379c95535f43f963c0c2f0f20434ee12c6" }, - { - "name": "meat_on_bone", - "unicode": "1F356", + "meat_on_bone": { + "category": "food", + "moji": "🍖", + "unicodeVersion": "6.0", "digest": "d71a8e0b118d5e6ca60690793ce9649afb78e707fcbd7be890a75564c94434fd" }, - { - "name": "medal", - "unicode": "1F3C5", + "medal": { + "category": "activity", + "moji": "🏅", + "unicodeVersion": "7.0", "digest": "9600cbe57e08da090c60629bcafd2821c87322e738c2454f8e883ceb756e7391" }, - { - "name": "sports_medal", - "unicode": "1F3C5", - "digest": "9600cbe57e08da090c60629bcafd2821c87322e738c2454f8e883ceb756e7391" - }, - { - "name": "mega", - "unicode": "1F4E3", + "mega": { + "category": "symbols", + "moji": "📣", + "unicodeVersion": "6.0", "digest": "4b1def6b5b051c5045514063f0ac006222ad81fbfe56d840e14bb950713e331b" }, - { - "name": "melon", - "unicode": "1F348", + "melon": { + "category": "food", + "moji": "🍈", + "unicodeVersion": "6.0", "digest": "0cdd663e6f2129808856cdf0746e6571b62aac641f224adb553baf3bb63ba3bd" }, - { - "name": "menorah", - "unicode": "1F54E", + "menorah": { + "category": "symbols", + "moji": "🕎", + "unicodeVersion": "8.0", "digest": "49fca8c3bc00ea69653ee2f8d4e21e561856ba39716c13e9d107db3e805a2997" }, - { - "name": "mens", - "unicode": "1F6B9", + "mens": { + "category": "symbols", + "moji": "🚹", + "unicodeVersion": "6.0", "digest": "7d92292586ee12a5d1a557c37da4d14708dc3ce701cf32d3280dcc83d91e5df8" }, - { - "name": "metal", - "unicode": "1F918", - "digest": "ffb750caf187f5d821c990108e2699ac3e216492bcff6ee543f4a7aa55b9fd29" - }, - { - "name": "sign_of_the_horns", - "unicode": "1F918", + "metal": { + "category": "people", + "moji": "🤘", + "unicodeVersion": "8.0", "digest": "ffb750caf187f5d821c990108e2699ac3e216492bcff6ee543f4a7aa55b9fd29" }, - { - "name": "metal_tone1", - "unicode": "1F918-1F3FB", + "metal_tone1": { + "category": "people", + "moji": "🤘🏻", + "unicodeVersion": "8.0", "digest": "5505f0b0340f9ba572db8897e40adf598cfa784686ad5ee360a7351bf44ddc1d" }, - { - "name": "sign_of_the_horns_tone1", - "unicode": "1F918-1F3FB", - "digest": "5505f0b0340f9ba572db8897e40adf598cfa784686ad5ee360a7351bf44ddc1d" - }, - { - "name": "metal_tone2", - "unicode": "1F918-1F3FC", - "digest": "8f9eee3ad5fc7eeeb30118d16d27467b16fd87297e0ecf02656db77e701f5aeb" - }, - { - "name": "sign_of_the_horns_tone2", - "unicode": "1F918-1F3FC", + "metal_tone2": { + "category": "people", + "moji": "🤘🏼", + "unicodeVersion": "8.0", "digest": "8f9eee3ad5fc7eeeb30118d16d27467b16fd87297e0ecf02656db77e701f5aeb" }, - { - "name": "metal_tone3", - "unicode": "1F918-1F3FD", + "metal_tone3": { + "category": "people", + "moji": "🤘🏽", + "unicodeVersion": "8.0", "digest": "8270a7ecf5eb11431a07ef04cc476c2651ac8aacb0d4768e5cb69355f8a5e84e" }, - { - "name": "sign_of_the_horns_tone3", - "unicode": "1F918-1F3FD", - "digest": "8270a7ecf5eb11431a07ef04cc476c2651ac8aacb0d4768e5cb69355f8a5e84e" - }, - { - "name": "metal_tone4", - "unicode": "1F918-1F3FE", + "metal_tone4": { + "category": "people", + "moji": "🤘🏾", + "unicodeVersion": "8.0", "digest": "f24f7b137dd6c7899dc0a8794204bbde7ad43ec1e63b419c90dd70a8b77871e8" }, - { - "name": "sign_of_the_horns_tone4", - "unicode": "1F918-1F3FE", - "digest": "f24f7b137dd6c7899dc0a8794204bbde7ad43ec1e63b419c90dd70a8b77871e8" - }, - { - "name": "metal_tone5", - "unicode": "1F918-1F3FF", + "metal_tone5": { + "category": "people", + "moji": "🤘🏿", + "unicodeVersion": "8.0", "digest": "07b0726a632653b980df775f460cd3fe1ea8d4a7b0b46fe29e089b66579482d2" }, - { - "name": "sign_of_the_horns_tone5", - "unicode": "1F918-1F3FF", - "digest": "07b0726a632653b980df775f460cd3fe1ea8d4a7b0b46fe29e089b66579482d2" - }, - { - "name": "metro", - "unicode": "1F687", + "metro": { + "category": "travel", + "moji": "🚇", + "unicodeVersion": "6.0", "digest": "b380247b61b5e2ca1b9b70fabff65907b2c3a5191a14b169ae094af94659b9b1" }, - { - "name": "microphone", - "unicode": "1F3A4", + "microphone": { + "category": "activity", + "moji": "🎤", + "unicodeVersion": "6.0", "digest": "9ef4fc2e40d5391c4bb2d30f34f59662cff7cbb1b04341c9dac210d0e21b44ae" }, - { - "name": "microphone2", - "unicode": "1F399", - "digest": "8a30464d51f7f101335778444c43270ac0679900f49463e6556682d9db1cb4dc" - }, - { - "name": "studio_microphone", - "unicode": "1F399", + "microphone2": { + "category": "objects", + "moji": "🎙", + "unicodeVersion": "7.0", "digest": "8a30464d51f7f101335778444c43270ac0679900f49463e6556682d9db1cb4dc" }, - { - "name": "microscope", - "unicode": "1F52C", + "microscope": { + "category": "objects", + "moji": "🔬", + "unicodeVersion": "6.0", "digest": "4ca4322c6ba99b8c15acdb8b605f84f87398769e504b262b134c1f3868b2692f" }, - { - "name": "middle_finger", - "unicode": "1F595", + "middle_finger": { + "category": "people", + "moji": "🖕", + "unicodeVersion": "7.0", "digest": "0c3f1cc0ec7323f6d19508ad22fa90050845f7b5cc83f599ab2cacb89cf5dd0e" }, - { - "name": "reversed_hand_with_middle_finger_extended", - "unicode": "1F595", - "digest": "0c3f1cc0ec7323f6d19508ad22fa90050845f7b5cc83f599ab2cacb89cf5dd0e" - }, - { - "name": "middle_finger_tone1", - "unicode": "1F595-1F3FB", - "digest": "4ebecf1058a3059aaa826eaad39c1a791120f115f65dde6d6ae32fc5561f60f7" - }, - { - "name": "reversed_hand_with_middle_finger_extended_tone1", - "unicode": "1F595-1F3FB", + "middle_finger_tone1": { + "category": "people", + "moji": "🖕🏻", + "unicodeVersion": "8.0", "digest": "4ebecf1058a3059aaa826eaad39c1a791120f115f65dde6d6ae32fc5561f60f7" }, - { - "name": "middle_finger_tone2", - "unicode": "1F595-1F3FC", - "digest": "85ff506a08c38663c2dfa2e3a90584c02a36aa3dda33af47cdb49834bf9baf83" - }, - { - "name": "reversed_hand_with_middle_finger_extended_tone2", - "unicode": "1F595-1F3FC", + "middle_finger_tone2": { + "category": "people", + "moji": "🖕🏼", + "unicodeVersion": "8.0", "digest": "85ff506a08c38663c2dfa2e3a90584c02a36aa3dda33af47cdb49834bf9baf83" }, - { - "name": "middle_finger_tone3", - "unicode": "1F595-1F3FD", - "digest": "cac697ff5207bf8a4e091912f3127f4e73c88ef69b5c6561d1d7b12ed60be8f1" - }, - { - "name": "reversed_hand_with_middle_finger_extended_tone3", - "unicode": "1F595-1F3FD", + "middle_finger_tone3": { + "category": "people", + "moji": "🖕🏽", + "unicodeVersion": "8.0", "digest": "cac697ff5207bf8a4e091912f3127f4e73c88ef69b5c6561d1d7b12ed60be8f1" }, - { - "name": "middle_finger_tone4", - "unicode": "1F595-1F3FE", - "digest": "9324a5a4e3986b798ad8c61f31c18fb507ca7a4abfd6e9ae1408b80b185bf8c7" - }, - { - "name": "reversed_hand_with_middle_finger_extended_tone4", - "unicode": "1F595-1F3FE", + "middle_finger_tone4": { + "category": "people", + "moji": "🖕🏾", + "unicodeVersion": "8.0", "digest": "9324a5a4e3986b798ad8c61f31c18fb507ca7a4abfd6e9ae1408b80b185bf8c7" }, - { - "name": "middle_finger_tone5", - "unicode": "1F595-1F3FF", - "digest": "078f917cd4d8be08a880724e9400449980d92740ccbee4a57f5046a9cf7f6575" - }, - { - "name": "reversed_hand_with_middle_finger_extended_tone5", - "unicode": "1F595-1F3FF", + "middle_finger_tone5": { + "category": "people", + "moji": "🖕🏿", + "unicodeVersion": "8.0", "digest": "078f917cd4d8be08a880724e9400449980d92740ccbee4a57f5046a9cf7f6575" }, - { - "name": "military_medal", - "unicode": "1F396", + "military_medal": { + "category": "activity", + "moji": "🎖", + "unicodeVersion": "7.0", "digest": "5da18351dc14b66cfc070148c83b7c8e67e6b1e3f515ae501133c38ee5c28d3d" }, - { - "name": "milk", - "unicode": "1F95B", - "digest": "38b28ea40399601fabc95bac5eaaf5a9e4e25548ec80325bd5069395ea884f85" - }, - { - "name": "glass_of_milk", - "unicode": "1F95B", + "milk": { + "category": "food", + "moji": "🥛", + "unicodeVersion": "9.0", "digest": "38b28ea40399601fabc95bac5eaaf5a9e4e25548ec80325bd5069395ea884f85" }, - { - "name": "milky_way", - "unicode": "1F30C", + "milky_way": { + "category": "travel", + "moji": "🌌", + "unicodeVersion": "6.0", "digest": "17405ff31d94b13a1fb0adcda204b8adb95ca340bc3980d9ad9f42ba1e366e7d" }, - { - "name": "minibus", - "unicode": "1F690", + "minibus": { + "category": "travel", + "moji": "🚐", + "unicodeVersion": "6.0", "digest": "08ccb4b1bf397b7c9aed901e2b5dcdd6cb8ca5c5487ef26775bb3120f7b92524" }, - { - "name": "minidisc", - "unicode": "1F4BD", + "minidisc": { + "category": "objects", + "moji": "💽", + "unicodeVersion": "6.0", "digest": "bebf82c0b91ef66321e7ae7a0abf322e59b2f7d8e6fbf9a94243210c00229c59" }, - { - "name": "mobile_phone_off", - "unicode": "1F4F4", + "mobile_phone_off": { + "category": "symbols", + "moji": "📴", + "unicodeVersion": "6.0", "digest": "6f9d8d6a32fc998f5d8144a5ff7e2ad00de37ad464cd97285e7c72efb09a1feb" }, - { - "name": "money_mouth", - "unicode": "1F911", + "money_mouth": { + "category": "people", + "moji": "🤑", + "unicodeVersion": "8.0", "digest": "5a43973dadf48a89201b1816fea9972c5cfe501a26fe457b6f7eee0a6362018e" }, - { - "name": "money_mouth_face", - "unicode": "1F911", - "digest": "5a43973dadf48a89201b1816fea9972c5cfe501a26fe457b6f7eee0a6362018e" - }, - { - "name": "money_with_wings", - "unicode": "1F4B8", + "money_with_wings": { + "category": "objects", + "moji": "💸", + "unicodeVersion": "6.0", "digest": "15fcf0595021374ba091ca00efdb4167770da4d421eab930964108545f4edab9" }, - { - "name": "moneybag", - "unicode": "1F4B0", + "moneybag": { + "category": "objects", + "moji": "💰", + "unicodeVersion": "6.0", "digest": "02d708e2f603b0df6f6c169b5c49b3452e1c02e7d72e96f228b73d0b0a20bff4" }, - { - "name": "monkey", - "unicode": "1F412", + "monkey": { + "category": "nature", + "moji": "🐒", + "unicodeVersion": "6.0", "digest": "3588a544d6d9e9995b45d60327a1a42002fa1faa4d48224b140facd249af1c67" }, - { - "name": "monkey_face", - "unicode": "1F435", + "monkey_face": { + "category": "nature", + "moji": "🐵", + "unicodeVersion": "6.0", "digest": "9e263ef5ca42bb76d1b1d1e3cbf020bcf05023a6e9f91301d30c9eb406363a2a" }, - { - "name": "monorail", - "unicode": "1F69D", + "monorail": { + "category": "travel", + "moji": "🚝", + "unicodeVersion": "6.0", "digest": "2c9f185babcb4001fcef2b8dfc4a32126729843084d0076c3e3ccdc845ab23ad" }, - { - "name": "mortar_board", - "unicode": "1F393", + "mortar_board": { + "category": "people", + "moji": "🎓", + "unicodeVersion": "6.0", "digest": "d7fbe41d4b340d3564e484aec46a22c9613521414b2ba6eece2180db4d23e410" }, - { - "name": "mosque", - "unicode": "1F54C", + "mosque": { + "category": "travel", + "moji": "🕌", + "unicodeVersion": "8.0", "digest": "5f3d3de7feac953a70a318113531c2857d760a516c3d8d6f42d2a3b3b67ed196" }, - { - "name": "motor_scooter", - "unicode": "1F6F5", - "digest": "e2dc7c981744a71f46858bd0858ff91af704ac06425ed80377bc3b119e57c872" - }, - { - "name": "motorbike", - "unicode": "1F6F5", + "motor_scooter": { + "category": "travel", + "moji": "🛵", + "unicodeVersion": "9.0", "digest": "e2dc7c981744a71f46858bd0858ff91af704ac06425ed80377bc3b119e57c872" }, - { - "name": "motorboat", - "unicode": "1F6E5", + "motorboat": { + "category": "travel", + "moji": "🛥", + "unicodeVersion": "7.0", "digest": "81c156643528c5a94a12d6d478e52a019f5a4e3eb58ee365cdd9d2361a7fdb01" }, - { - "name": "motorcycle", - "unicode": "1F3CD", + "motorcycle": { + "category": "travel", + "moji": "🏍", + "unicodeVersion": "7.0", "digest": "354aa8157732184ad50eff9330f7a8915309dc9b7893cc308226adb429311a62" }, - { - "name": "racing_motorcycle", - "unicode": "1F3CD", - "digest": "354aa8157732184ad50eff9330f7a8915309dc9b7893cc308226adb429311a62" - }, - { - "name": "motorway", - "unicode": "1F6E3", + "motorway": { + "category": "travel", + "moji": "🛣", + "unicodeVersion": "7.0", "digest": "148c3c13c7c4565453d16e504e0d4b8d007e4f2cad1ab56b1b51fefe39162d17" }, - { - "name": "mount_fuji", - "unicode": "1F5FB", + "mount_fuji": { + "category": "travel", + "moji": "🗻", + "unicodeVersion": "6.0", "digest": "f8093b9dba62b22c6c88f137be88b2fd3971c560714db15ec053cf697a3820bc" }, - { - "name": "mountain", - "unicode": "26F0", + "mountain": { + "category": "travel", + "moji": "⛰", + "unicodeVersion": "5.2", "digest": "07423804ad79da68f140948d29df193f5d5343b7b2c23758c086697c4d3a50da" }, - { - "name": "mountain_bicyclist", - "unicode": "1F6B5", + "mountain_bicyclist": { + "category": "activity", + "moji": "🚵", + "unicodeVersion": "6.0", "digest": "91084b6c887cb7e34f3d7ec30656ecb82c36cc987f53a6c83ccb4c6f7950f96a" }, - { - "name": "mountain_bicyclist_tone1", - "unicode": "1F6B5-1F3FB", + "mountain_bicyclist_tone1": { + "category": "activity", + "moji": "🚵🏻", + "unicodeVersion": "8.0", "digest": "5d57fcfad61bca26c3e8965eb57602a1993a3117ebdda0f24569af730310ab6e" }, - { - "name": "mountain_bicyclist_tone2", - "unicode": "1F6B5-1F3FC", + "mountain_bicyclist_tone2": { + "category": "activity", + "moji": "🚵🏼", + "unicodeVersion": "8.0", "digest": "c0da7fb85d99aa01a665f64063cd7e2d994f8a16d3f6fbf52df5d471e771a98a" }, - { - "name": "mountain_bicyclist_tone3", - "unicode": "1F6B5-1F3FD", + "mountain_bicyclist_tone3": { + "category": "activity", + "moji": "🚵🏽", + "unicodeVersion": "8.0", "digest": "b099e7ee84eae44ebc99023fa06bdf37ffa0d69767c7c0163a89f7ced2a26765" }, - { - "name": "mountain_bicyclist_tone4", - "unicode": "1F6B5-1F3FE", + "mountain_bicyclist_tone4": { + "category": "activity", + "moji": "🚵🏾", + "unicodeVersion": "8.0", "digest": "9d09f7b3899ea44e736f237a161ef8d5170dccfa162a872c59532ceaf65ee007" }, - { - "name": "mountain_bicyclist_tone5", - "unicode": "1F6B5-1F3FF", + "mountain_bicyclist_tone5": { + "category": "activity", + "moji": "🚵🏿", + "unicodeVersion": "8.0", "digest": "71e374981d955056748a60c6d1820b45e9688a156b55318b4ea54a3a67ca801c" }, - { - "name": "mountain_cableway", - "unicode": "1F6A0", + "mountain_cableway": { + "category": "travel", + "moji": "🚠", + "unicodeVersion": "6.0", "digest": "e261c3292758b1c0063c5a0d0c7f5c9803306d2265e08677027e1210506ced94" }, - { - "name": "mountain_railway", - "unicode": "1F69E", + "mountain_railway": { + "category": "travel", + "moji": "🚞", + "unicodeVersion": "6.0", "digest": "b0987f8f391b3cbc7a56b9b8945ebfca240e01d12f8fd163877ebebe51d6b277" }, - { - "name": "mountain_snow", - "unicode": "1F3D4", - "digest": "49aac2b851aa6f2bd2ca641efa8060f93e89395357f49d211658d46f5a2b0189" - }, - { - "name": "snow_capped_mountain", - "unicode": "1F3D4", + "mountain_snow": { + "category": "travel", + "moji": "🏔", + "unicodeVersion": "7.0", "digest": "49aac2b851aa6f2bd2ca641efa8060f93e89395357f49d211658d46f5a2b0189" }, - { - "name": "mouse", - "unicode": "1F42D", + "mouse": { + "category": "nature", + "moji": "🐭", + "unicodeVersion": "6.0", "digest": "007dd108507b45224f7a1fad3c1de6ecc75f38d71fc142744611eb13555f5eff" }, - { - "name": "mouse2", - "unicode": "1F401", + "mouse2": { + "category": "nature", + "moji": "🐁", + "unicodeVersion": "6.0", "digest": "f3ed37b639b7c16aae49502bd423f9fdeabaf15bc6f0f74063954b189e176b5d" }, - { - "name": "mouse_three_button", - "unicode": "1F5B1", + "mouse_three_button": { + "category": "objects", + "moji": "🖱", + "unicodeVersion": "7.0", "digest": "3724341ac5ad0d01027ef1575db64f1db7619f590ca6ada960d1f2c18dc7fc6a" }, - { - "name": "three_button_mouse", - "unicode": "1F5B1", - "digest": "3724341ac5ad0d01027ef1575db64f1db7619f590ca6ada960d1f2c18dc7fc6a" - }, - { - "name": "movie_camera", - "unicode": "1F3A5", + "movie_camera": { + "category": "objects", + "moji": "🎥", + "unicodeVersion": "6.0", "digest": "f7e285eda35b4431c07951e071643ddc34147cd76640e0d516fbfd11208346e9" }, - { - "name": "moyai", - "unicode": "1F5FF", + "moyai": { + "category": "objects", + "moji": "🗿", + "unicodeVersion": "6.0", "digest": "2c1d0662c95928936e6b9ab5a40c6110ff1cea5339f2803c7b63aabc76115afb" }, - { - "name": "mrs_claus", - "unicode": "1F936", - "digest": "1f72f586ca75bd7ebb4150cdcc8199a930c32fa4b81510cb8d200f1b3ddd4076" - }, - { - "name": "mother_christmas", - "unicode": "1F936", + "mrs_claus": { + "category": "people", + "moji": "🤶", + "unicodeVersion": "9.0", "digest": "1f72f586ca75bd7ebb4150cdcc8199a930c32fa4b81510cb8d200f1b3ddd4076" }, - { - "name": "mrs_claus_tone1", - "unicode": "1F936-1F3FB", + "mrs_claus_tone1": { + "category": "people", + "moji": "🤶🏻", + "unicodeVersion": "9.0", "digest": "244596919e0fed050203cf9e040899de323d7821235929f175852439927bd129" }, - { - "name": "mother_christmas_tone1", - "unicode": "1F936-1F3FB", - "digest": "244596919e0fed050203cf9e040899de323d7821235929f175852439927bd129" - }, - { - "name": "mrs_claus_tone2", - "unicode": "1F936-1F3FC", + "mrs_claus_tone2": { + "category": "people", + "moji": "🤶🏼", + "unicodeVersion": "9.0", "digest": "8cde96e8521f3a90262a7f5f8a2989a9590d9a02cda2c37e92335dc05975c18d" }, - { - "name": "mother_christmas_tone2", - "unicode": "1F936-1F3FC", - "digest": "8cde96e8521f3a90262a7f5f8a2989a9590d9a02cda2c37e92335dc05975c18d" - }, - { - "name": "mrs_claus_tone3", - "unicode": "1F936-1F3FD", + "mrs_claus_tone3": { + "category": "people", + "moji": "🤶🏽", + "unicodeVersion": "9.0", "digest": "c39cd4346d4581799dd0e9a6447c91a954a75747bf2682c8e4d79c3b0fcf7405" }, - { - "name": "mother_christmas_tone3", - "unicode": "1F936-1F3FD", - "digest": "c39cd4346d4581799dd0e9a6447c91a954a75747bf2682c8e4d79c3b0fcf7405" - }, - { - "name": "mrs_claus_tone4", - "unicode": "1F936-1F3FE", - "digest": "84c85cf54559ea2d78d196fee96149a249af4f959b78e223a0ec4fb72abdbcab" - }, - { - "name": "mother_christmas_tone4", - "unicode": "1F936-1F3FE", + "mrs_claus_tone4": { + "category": "people", + "moji": "🤶🏾", + "unicodeVersion": "9.0", "digest": "84c85cf54559ea2d78d196fee96149a249af4f959b78e223a0ec4fb72abdbcab" }, - { - "name": "mrs_claus_tone5", - "unicode": "1F936-1F3FF", + "mrs_claus_tone5": { + "category": "people", + "moji": "🤶🏿", + "unicodeVersion": "9.0", "digest": "ce26c0e0645713b17e7497d9f2d0484cc5477564dae99320cabf04d160d3b2ff" }, - { - "name": "mother_christmas_tone5", - "unicode": "1F936-1F3FF", - "digest": "ce26c0e0645713b17e7497d9f2d0484cc5477564dae99320cabf04d160d3b2ff" - }, - { - "name": "muscle", - "unicode": "1F4AA", + "muscle": { + "category": "people", + "moji": "💪", + "unicodeVersion": "6.0", "digest": "e4ce52757b2b7982e2516e0e8bf2e2253617cc9f3e6178f1887c61c9039461ba" }, - { - "name": "muscle_tone1", - "unicode": "1F4AA-1F3FB", + "muscle_tone1": { + "category": "people", + "moji": "💪🏻", + "unicodeVersion": "8.0", "digest": "4a2fa226a05bb847b62cdd163eb6c2d514d3c2330a727991cf550c0d32b0e818" }, - { - "name": "muscle_tone2", - "unicode": "1F4AA-1F3FC", + "muscle_tone2": { + "category": "people", + "moji": "💪🏼", + "unicodeVersion": "8.0", "digest": "a8d5ecce335c782ca5f5e55763c06cfefa1c16c24cd6602237cf125d4ff95e47" }, - { - "name": "muscle_tone3", - "unicode": "1F4AA-1F3FD", + "muscle_tone3": { + "category": "people", + "moji": "💪🏽", + "unicodeVersion": "8.0", "digest": "070354b443faec3969663b770545fc4cf5ec75148557b2b9d6fc82ab22b43bd1" }, - { - "name": "muscle_tone4", - "unicode": "1F4AA-1F3FE", + "muscle_tone4": { + "category": "people", + "moji": "💪🏾", + "unicodeVersion": "8.0", "digest": "8eafcdb6a607aeafa673c257df0d2a1b20f00fc0868d811babcbe784490a0dd3" }, - { - "name": "muscle_tone5", - "unicode": "1F4AA-1F3FF", + "muscle_tone5": { + "category": "people", + "moji": "💪🏿", + "unicodeVersion": "8.0", "digest": "85a1e2b5c89907694240e9c5b9d876a741fa7ba38918c5718273e289cbc40efe" }, - { - "name": "mushroom", - "unicode": "1F344", + "mushroom": { + "category": "nature", + "moji": "🍄", + "unicodeVersion": "6.0", "digest": "aaca8cf7c5cfa4487b5fef365a231f98be4bbf041197fc022161bcc8ce6f57c8" }, - { - "name": "musical_keyboard", - "unicode": "1F3B9", + "musical_keyboard": { + "category": "activity", + "moji": "🎹", + "unicodeVersion": "6.0", "digest": "fb0a726728900377d76d94aac9c94dce29107e8e3f1dcb0599d95bce7169b492" }, - { - "name": "musical_note", - "unicode": "1F3B5", + "musical_note": { + "category": "symbols", + "moji": "🎵", + "unicodeVersion": "6.0", "digest": "41288e79b4070bb980281d0e0d1c14d8b144b4aedb2eaadb9f2bebcb4ef892b4" }, - { - "name": "musical_score", - "unicode": "1F3BC", + "musical_score": { + "category": "activity", + "moji": "🎼", + "unicodeVersion": "6.0", "digest": "f0f91b9fa4a2bff7a5a1a11afa6f31cfe7e5fa8b0d6f3cce904b781a28ed0277" }, - { - "name": "mute", - "unicode": "1F507", + "mute": { + "category": "symbols", + "moji": "🔇", + "unicodeVersion": "6.0", "digest": "def277da49d744b55c7cdde269a15aa05315898f615e721ee7e9205d7b8030d6" }, - { - "name": "nail_care", - "unicode": "1F485", + "nail_care": { + "category": "people", + "moji": "💅", + "unicodeVersion": "6.0", "digest": "48b33b1dbbd25b4f34ab2ca07bb99ddaaaa741990142c5623310f76b78c076f9" }, - { - "name": "nail_care_tone1", - "unicode": "1F485-1F3FB", + "nail_care_tone1": { + "category": "people", + "moji": "💅🏻", + "unicodeVersion": "8.0", "digest": "a9ac92a34f407e7dd7c71377e6275e66657f7f42e4b911c540d1a66a02d92ac5" }, - { - "name": "nail_care_tone2", - "unicode": "1F485-1F3FC", + "nail_care_tone2": { + "category": "people", + "moji": "💅🏼", + "unicodeVersion": "8.0", "digest": "f295ec85980aaa75818fad619c3d25042146ecbbf361db9e9bb96e7bc202bc73" }, - { - "name": "nail_care_tone3", - "unicode": "1F485-1F3FD", + "nail_care_tone3": { + "category": "people", + "moji": "💅🏽", + "unicodeVersion": "8.0", "digest": "02ec373052a250977298bae85262177910126cc10de9480f1afa328ac2f65a95" }, - { - "name": "nail_care_tone4", - "unicode": "1F485-1F3FE", + "nail_care_tone4": { + "category": "people", + "moji": "💅🏾", + "unicodeVersion": "8.0", "digest": "f3d95390ab59caedfda66122bbd0acf3aabedc142fc48352d68900766a7e6f5c" }, - { - "name": "nail_care_tone5", - "unicode": "1F485-1F3FF", + "nail_care_tone5": { + "category": "people", + "moji": "💅🏿", + "unicodeVersion": "8.0", "digest": "009423c97f2aafd24fb8c7c485c58b30bbf9ae6797cc14b80d472b207327b518" }, - { - "name": "name_badge", - "unicode": "1F4DB", + "name_badge": { + "category": "symbols", + "moji": "📛", + "unicodeVersion": "6.0", "digest": "f9f6a4895ff0be8fb2ccc7ad195b94e9650f742f66ead999e90724cfb77af628" }, - { - "name": "nauseated_face", - "unicode": "1F922", - "digest": "f8471cf4720948d8246ec9d30e29783e819f90e3cfe8b1ba628671a1aad1a91c" - }, - { - "name": "sick", - "unicode": "1F922", + "nauseated_face": { + "category": "people", + "moji": "🤢", + "unicodeVersion": "9.0", "digest": "f8471cf4720948d8246ec9d30e29783e819f90e3cfe8b1ba628671a1aad1a91c" }, - { - "name": "necktie", - "unicode": "1F454", + "necktie": { + "category": "people", + "moji": "👔", + "unicodeVersion": "6.0", "digest": "01bb18dc8bfe787daa9613b5d09988cd5a065449ef906099ce3cb308c8a7da68" }, - { - "name": "negative_squared_cross_mark", - "unicode": "274E", + "negative_squared_cross_mark": { + "category": "symbols", + "moji": "❎", + "unicodeVersion": "6.0", "digest": "1cdaf4abc9adafa089c91c2e33a24e9e647aea0f857e767941a899a16ec53b74" }, - { - "name": "nerd", - "unicode": "1F913", - "digest": "9e5f3c93db25cf1d0f9d6e6bd2993161afec6c30573ba3fe85e13b8c84483d66" - }, - { - "name": "nerd_face", - "unicode": "1F913", + "nerd": { + "category": "people", + "moji": "🤓", + "unicodeVersion": "8.0", "digest": "9e5f3c93db25cf1d0f9d6e6bd2993161afec6c30573ba3fe85e13b8c84483d66" }, - { - "name": "neutral_face", - "unicode": "1F610", + "neutral_face": { + "category": "people", + "moji": "😐", + "unicodeVersion": "6.0", "digest": "7449430a60619956573e9dc80834045296f2b99853737b6c7794c785ff53d64e" }, - { - "name": "new", - "unicode": "1F195", + "new": { + "category": "symbols", + "moji": "🆕", + "unicodeVersion": "6.0", "digest": "e20bc3e9f40726afd0cfb7268d02f1e1a07343364fd08b252d59f38de067bf06" }, - { - "name": "new_moon", - "unicode": "1F311", + "new_moon": { + "category": "nature", + "moji": "🌑", + "unicodeVersion": "6.0", "digest": "dbfc5dcae34b45f15ff767e297cba3a12cb83f3b542db8cfc8dbd9669e0df46c" }, - { - "name": "new_moon_with_face", - "unicode": "1F31A", + "new_moon_with_face": { + "category": "nature", + "moji": "🌚", + "unicodeVersion": "6.0", "digest": "c66d347d2222ac8d77d323a07699aff6b168328648db4f885b1ed0e2831fd59b" }, - { - "name": "newspaper", - "unicode": "1F4F0", + "newspaper": { + "category": "objects", + "moji": "📰", + "unicodeVersion": "6.0", "digest": "c05e986d9cdac11afa30c6a21a72572ddf50fc64e87ae0c4e0ad57ffe70acc5c" }, - { - "name": "newspaper2", - "unicode": "1F5DE", - "digest": "63db7bcf51effc73e5124392740736383774a4bcfbc1156cf55599504760883d" - }, - { - "name": "rolled_up_newspaper", - "unicode": "1F5DE", + "newspaper2": { + "category": "objects", + "moji": "🗞", + "unicodeVersion": "7.0", "digest": "63db7bcf51effc73e5124392740736383774a4bcfbc1156cf55599504760883d" }, - { - "name": "ng", - "unicode": "1F196", + "ng": { + "category": "symbols", + "moji": "🆖", + "unicodeVersion": "6.0", "digest": "34d5a11c70f48ea719e602908534f446b192622e775d4160f0e1ec52c342a35c" }, - { - "name": "night_with_stars", - "unicode": "1F303", + "night_with_stars": { + "category": "travel", + "moji": "🌃", + "unicodeVersion": "6.0", "digest": "39d9c079be80ee6ce1667531be528a2aa7f8bd46c7b6c2a6ee279d9a207c84a4" }, - { - "name": "nine", - "unicode": "0039-20E3", + "nine": { + "category": "symbols", + "moji": "9️⃣", + "unicodeVersion": "3.0", "digest": "8bb40750eda8506ef877c9a3b8e2039d26f20eef345742f635740574a7e8daa6" }, - { - "name": "no_bell", - "unicode": "1F515", + "no_bell": { + "category": "symbols", + "moji": "🔕", + "unicodeVersion": "6.0", "digest": "6542a9a5656c79c153f8c37f12d48f677c89b02ed0989ae37fa5e51ce6895422" }, - { - "name": "no_bicycles", - "unicode": "1F6B3", + "no_bicycles": { + "category": "symbols", + "moji": "🚳", + "unicodeVersion": "6.0", "digest": "af71c183545da2ff4c05609f9d572edb64b63ccba7c6a4b208d271558aa92b0a" }, - { - "name": "no_entry", - "unicode": "26D4", + "no_entry": { + "category": "symbols", + "moji": "⛔", + "unicodeVersion": "5.2", "digest": "dc0bac1ed9ab8e9af143f0fce5043fe68f7f46bd80856cdec95d20c3999b637d" }, - { - "name": "no_entry_sign", - "unicode": "1F6AB", + "no_entry_sign": { + "category": "symbols", + "moji": "🚫", + "unicodeVersion": "6.0", "digest": "2c1fceef23b62effca68e0e087b8f020125d25b98d61492b1540055d1914fdc3" }, - { - "name": "no_good", - "unicode": "1F645", + "no_good": { + "category": "people", + "moji": "🙅", + "unicodeVersion": "6.0", "digest": "6eb970b104389be5d18657d7c04be5149958c26855c52ea68574af852c5f85c4" }, - { - "name": "no_good_tone1", - "unicode": "1F645-1F3FB", + "no_good_tone1": { + "category": "people", + "moji": "🙅🏻", + "unicodeVersion": "8.0", "digest": "c20a24a1e536240b4dcf90ecb530796de621d7ba1fb9e3fa0f849d048c509c03" }, - { - "name": "no_good_tone2", - "unicode": "1F645-1F3FC", + "no_good_tone2": { + "category": "people", + "moji": "🙅🏼", + "unicodeVersion": "8.0", "digest": "f31a4628c1f2e6a39288fda8eb19c9ec89983e3726e17a09384d9ecc13ef0b4c" }, - { - "name": "no_good_tone3", - "unicode": "1F645-1F3FD", + "no_good_tone3": { + "category": "people", + "moji": "🙅🏽", + "unicodeVersion": "8.0", "digest": "959dec1bfdaf37b20a86ab2bcbdbacd3179c87b163042377d966eab47564c0fb" }, - { - "name": "no_good_tone4", - "unicode": "1F645-1F3FE", + "no_good_tone4": { + "category": "people", + "moji": "🙅🏾", + "unicodeVersion": "8.0", "digest": "efd931f0080adf2e04129c83a8b24fda0ae7a9fa7c4b463686c0b99023620db8" }, - { - "name": "no_good_tone5", - "unicode": "1F645-1F3FF", + "no_good_tone5": { + "category": "people", + "moji": "🙅🏿", + "unicodeVersion": "8.0", "digest": "f35df2b26af9baef47c1f8cc97a1b28a58aa7fcb2a13fdac7b2d9189f1e40105" }, - { - "name": "no_mobile_phones", - "unicode": "1F4F5", + "no_mobile_phones": { + "category": "symbols", + "moji": "📵", + "unicodeVersion": "6.0", "digest": "a472decd6ac7f9777961c09e00458746b2c04965585e3bee4556be3968e55bcd" }, - { - "name": "no_mouth", - "unicode": "1F636", + "no_mouth": { + "category": "people", + "moji": "😶", + "unicodeVersion": "6.0", "digest": "72dda8b1e3ad4b05d9b095f9bd05e95d7ba013906c68914976a4554e8edf5866" }, - { - "name": "no_pedestrians", - "unicode": "1F6B7", + "no_pedestrians": { + "category": "symbols", + "moji": "🚷", + "unicodeVersion": "6.0", "digest": "062b4a71b338fe09775e465bfba8ac04efbb3640330e8cabe88f3af62b0f4225" }, - { - "name": "no_smoking", - "unicode": "1F6AD", + "no_smoking": { + "category": "symbols", + "moji": "🚭", + "unicodeVersion": "6.0", "digest": "ae2ebb331f79f6074091c0ee9cd69fce16d5e12a131d18973fc05520097e14ee" }, - { - "name": "non-potable_water", - "unicode": "1F6B1", + "non-potable_water": { + "category": "symbols", + "moji": "🚱", + "unicodeVersion": "6.0", "digest": "32eba0a99b498133c2e4450036f768d3dccaaf5b50adc9ad988757adc777a6a1" }, - { - "name": "nose", - "unicode": "1F443", + "nose": { + "category": "people", + "moji": "👃", + "unicodeVersion": "6.0", "digest": "9f800e24658ea3cebe1144d5d808cf13a88261f1a7f1f81a10d03b3d9d00e541" }, - { - "name": "nose_tone1", - "unicode": "1F443-1F3FB", + "nose_tone1": { + "category": "people", + "moji": "👃🏻", + "unicodeVersion": "8.0", "digest": "a2d0af22284b1d264eb780943b8360f463996a5c9c9584b8473edf8d442d9173" }, - { - "name": "nose_tone2", - "unicode": "1F443-1F3FC", + "nose_tone2": { + "category": "people", + "moji": "👃🏼", + "unicodeVersion": "8.0", "digest": "244dcaa8540024cf521f29f36bd48f933bf82f4833e35e6fa0abf113022038f3" }, - { - "name": "nose_tone3", - "unicode": "1F443-1F3FD", + "nose_tone3": { + "category": "people", + "moji": "👃🏽", + "unicodeVersion": "8.0", "digest": "c935b64866f0d49da52035aa09f36ff56d238eb7f5b92205386451056e8ea74f" }, - { - "name": "nose_tone4", - "unicode": "1F443-1F3FE", + "nose_tone4": { + "category": "people", + "moji": "👃🏾", + "unicodeVersion": "8.0", "digest": "a87e95fd9319c49e66b6dea0e57319d0ed9921b8d94df037767bf3d5dc7c94f3" }, - { - "name": "nose_tone5", - "unicode": "1F443-1F3FF", + "nose_tone5": { + "category": "people", + "moji": "👃🏿", + "unicodeVersion": "8.0", "digest": "1e0f9842e0f8ad5805eabd3f35a6038b7a2e49d566a1f5c17271f9cdf467ca60" }, - { - "name": "notebook", - "unicode": "1F4D3", + "notebook": { + "category": "objects", + "moji": "📓", + "unicodeVersion": "6.0", "digest": "fc679d3728f86073d1607a926885dd8b0261132f5c4a0322f1e46ea9f95c8cb8" }, - { - "name": "notebook_with_decorative_cover", - "unicode": "1F4D4", + "notebook_with_decorative_cover": { + "category": "objects", + "moji": "📔", + "unicodeVersion": "6.0", "digest": "d822eda4b49cbfa399b36f134c1a0b8dcfdd27ed89f12c50bc18f6f0a9aa56ef" }, - { - "name": "notepad_spiral", - "unicode": "1F5D2", + "notepad_spiral": { + "category": "objects", + "moji": "🗒", + "unicodeVersion": "7.0", "digest": "c6a8e16aa62474cef13e5659fddb4afc57e3f79635e32e6020edbee2b5b50f18" }, - { - "name": "spiral_note_pad", - "unicode": "1F5D2", - "digest": "c6a8e16aa62474cef13e5659fddb4afc57e3f79635e32e6020edbee2b5b50f18" - }, - { - "name": "notes", - "unicode": "1F3B6", + "notes": { + "category": "symbols", + "moji": "🎶", + "unicodeVersion": "6.0", "digest": "98467e0adc134d45676ef1c6c459e5853a9db50c8a6e91b6aec7d449aa737f48" }, - { - "name": "nut_and_bolt", - "unicode": "1F529", + "nut_and_bolt": { + "category": "objects", + "moji": "🔩", + "unicodeVersion": "6.0", "digest": "a77bd72f29a7302195dcec240174b15586de79e3204258e3fb401a6ea90563b3" }, - { - "name": "o", - "unicode": "2B55", + "o": { + "category": "symbols", + "moji": "⭕", + "unicodeVersion": "5.2", "digest": "2387e5fd9ae4c2972d40298d32319b8fa55c50dbfc1c04c5c36088213e6951dd" }, - { - "name": "o2", - "unicode": "1F17E", + "o2": { + "category": "symbols", + "moji": "🅾", + "unicodeVersion": "6.0", "digest": "6a9ccb0bf394e4d05ffda19327cee18f7b9ed80367fc7f41c93da9bb7efab0bf" }, - { - "name": "ocean", - "unicode": "1F30A", + "ocean": { + "category": "nature", + "moji": "🌊", + "unicodeVersion": "6.0", "digest": "1a9ca9848d4fb75852addfc10bf84eccf7caa5339714b90e3de4cb6f2518465e" }, - { - "name": "octagonal_sign", - "unicode": "1F6D1", - "digest": "9f6927048e1f9da57f89d1ae1eb86fa4ab7abdbabca756a738a799e948d0b3f9" - }, - { - "name": "stop_sign", - "unicode": "1F6D1", + "octagonal_sign": { + "category": "symbols", + "moji": "🛑", + "unicodeVersion": "9.0", "digest": "9f6927048e1f9da57f89d1ae1eb86fa4ab7abdbabca756a738a799e948d0b3f9" }, - { - "name": "octopus", - "unicode": "1F419", + "octopus": { + "category": "nature", + "moji": "🐙", + "unicodeVersion": "6.0", "digest": "0fcc65c12f4b29ea75a8c4823d20838a7e6db6978fdcb536943072aa1460bc59" }, - { - "name": "oden", - "unicode": "1F362", + "oden": { + "category": "food", + "moji": "🍢", + "unicodeVersion": "6.0", "digest": "089974cb13a0bef6a245fc73029c5ed5153fd4caae0177b835f779e32200b8aa" }, - { - "name": "office", - "unicode": "1F3E2", + "office": { + "category": "travel", + "moji": "🏢", + "unicodeVersion": "6.0", "digest": "3633a2e91036362e273eef4e0cfbdbbb4cb1208afe2cfa110ebef7b78109a66f" }, - { - "name": "oil", - "unicode": "1F6E2", - "digest": "00b94d33bcc9b9e8a5d4bd6e7f7e2fced9497ce05919edd5e58eafbc011c2caa" - }, - { - "name": "oil_drum", - "unicode": "1F6E2", + "oil": { + "category": "objects", + "moji": "🛢", + "unicodeVersion": "7.0", "digest": "00b94d33bcc9b9e8a5d4bd6e7f7e2fced9497ce05919edd5e58eafbc011c2caa" }, - { - "name": "ok", - "unicode": "1F197", + "ok": { + "category": "symbols", + "moji": "🆗", + "unicodeVersion": "6.0", "digest": "5f320f9b96e98a2f17ebe240daff9b9fd2ae0727cd6c8e4633b1744356e89365" }, - { - "name": "ok_hand", - "unicode": "1F44C", + "ok_hand": { + "category": "people", + "moji": "👌", + "unicodeVersion": "6.0", "digest": "d63002dce3cc3655b67b8765b7c28d370edba0e3758b2329b60e0e61c4d8e78d" }, - { - "name": "ok_hand_tone1", - "unicode": "1F44C-1F3FB", + "ok_hand_tone1": { + "category": "people", + "moji": "👌🏻", + "unicodeVersion": "8.0", "digest": "ef1508efcf483b09807554fe0e451c2948224f9deb85463e8e0dad6875b54012" }, - { - "name": "ok_hand_tone2", - "unicode": "1F44C-1F3FC", + "ok_hand_tone2": { + "category": "people", + "moji": "👌🏼", + "unicodeVersion": "8.0", "digest": "1215a101a082fd8e04c5d2f7e3c59d0f480cb0bedd79aeab5d36676bfe760088" }, - { - "name": "ok_hand_tone3", - "unicode": "1F44C-1F3FD", + "ok_hand_tone3": { + "category": "people", + "moji": "👌🏽", + "unicodeVersion": "8.0", "digest": "6fe0ed9fb42e86bb2bed4cb37b2acacacda1471fb1ee845ad55e54fb0897fbf4" }, - { - "name": "ok_hand_tone4", - "unicode": "1F44C-1F3FE", + "ok_hand_tone4": { + "category": "people", + "moji": "👌🏾", + "unicodeVersion": "8.0", "digest": "bfb9041c49d95e901a667264abaf9b398f6c4aa8b52bf5191c122db20c13c020" }, - { - "name": "ok_hand_tone5", - "unicode": "1F44C-1F3FF", + "ok_hand_tone5": { + "category": "people", + "moji": "👌🏿", + "unicodeVersion": "8.0", "digest": "1c218dc04d698da2cbdd7bea1ca3f845f9b386e967b7247c52f4b0f6ec8f5320" }, - { - "name": "ok_woman", - "unicode": "1F646", + "ok_woman": { + "category": "people", + "moji": "🙆", + "unicodeVersion": "6.0", "digest": "3f8bd4ce2c4497155d697e5a71ebdc9339f65633d07fa9a7903e1bd76cfa4ba1" }, - { - "name": "ok_woman_tone1", - "unicode": "1F646-1F3FB", + "ok_woman_tone1": { + "category": "people", + "moji": "🙆🏻", + "unicodeVersion": "8.0", "digest": "1660cd904ccd2ecdc6f4ba00527f7d4ec8c33f3c6183344616f97badae4c3730" }, - { - "name": "ok_woman_tone2", - "unicode": "1F646-1F3FC", + "ok_woman_tone2": { + "category": "people", + "moji": "🙆🏼", + "unicodeVersion": "8.0", "digest": "7ba5fddd1e141424fac6778894dfc5af28e125839c58937c69496f99cd2c4002" }, - { - "name": "ok_woman_tone3", - "unicode": "1F646-1F3FD", + "ok_woman_tone3": { + "category": "people", + "moji": "🙆🏽", + "unicodeVersion": "8.0", "digest": "1d972b8377c52f598406f59ab1e5be41aaf8f027e1fefba3deda66312ccd6a9b" }, - { - "name": "ok_woman_tone4", - "unicode": "1F646-1F3FE", + "ok_woman_tone4": { + "category": "people", + "moji": "🙆🏾", + "unicodeVersion": "8.0", "digest": "a176328d8f53503aa743448968afd21d72ffd3510555526a3fb38d6b30ee7c15" }, - { - "name": "ok_woman_tone5", - "unicode": "1F646-1F3FF", + "ok_woman_tone5": { + "category": "people", + "moji": "🙆🏿", + "unicodeVersion": "8.0", "digest": "13cfc1b589c57e81f768ee07a14b737cafc71407a7eb0956728b2ec4b1df14c4" }, - { - "name": "older_man", - "unicode": "1F474", + "older_man": { + "category": "people", + "moji": "👴", + "unicodeVersion": "6.0", "digest": "4c0462b199bf26181c9e4d2d4cb878a32b0294566941212efc67362d0645f948" }, - { - "name": "older_man_tone1", - "unicode": "1F474-1F3FB", + "older_man_tone1": { + "category": "people", + "moji": "👴🏻", + "unicodeVersion": "8.0", "digest": "99baa083f78cb01166d0a928d0b53682be14be04c29fc17bef14aac1a73a61e6" }, - { - "name": "older_man_tone2", - "unicode": "1F474-1F3FC", + "older_man_tone2": { + "category": "people", + "moji": "👴🏼", + "unicodeVersion": "8.0", "digest": "5b4ce713e8820ba517fe92c25f3b93e6a6bf3704d1f982c461d5f31fc02b9d3d" }, - { - "name": "older_man_tone3", - "unicode": "1F474-1F3FD", + "older_man_tone3": { + "category": "people", + "moji": "👴🏽", + "unicodeVersion": "8.0", "digest": "0eff72b3226c3a703c635798ee84129a695c896fa011fe1adbc105312eecc083" }, - { - "name": "older_man_tone4", - "unicode": "1F474-1F3FE", + "older_man_tone4": { + "category": "people", + "moji": "👴🏾", + "unicodeVersion": "8.0", "digest": "ad9ba82b0c5d3b171b0639ee4265370dbddff5e0eeb70729db122659bb8c8f84" }, - { - "name": "older_man_tone5", - "unicode": "1F474-1F3FF", + "older_man_tone5": { + "category": "people", + "moji": "👴🏿", + "unicodeVersion": "8.0", "digest": "5eb0a7467cc40e75752e11fd5126b275863dc037557a0d0d3b24b681e00c2386" }, - { - "name": "older_woman", - "unicode": "1F475", - "digest": "c261fdf3b01e0c7d949e177144531add5895197fbadf1acbba8eb17d18766bf6" - }, - { - "name": "grandma", - "unicode": "1F475", + "older_woman": { + "category": "people", + "moji": "👵", + "unicodeVersion": "6.0", "digest": "c261fdf3b01e0c7d949e177144531add5895197fbadf1acbba8eb17d18766bf6" }, - { - "name": "older_woman_tone1", - "unicode": "1F475-1F3FB", - "digest": "1f2bb9e42270a58194498254da27ac2b7a50edaa771b90ee194ccd6d24660c62" - }, - { - "name": "grandma_tone1", - "unicode": "1F475-1F3FB", + "older_woman_tone1": { + "category": "people", + "moji": "👵🏻", + "unicodeVersion": "8.0", "digest": "1f2bb9e42270a58194498254da27ac2b7a50edaa771b90ee194ccd6d24660c62" }, - { - "name": "older_woman_tone2", - "unicode": "1F475-1F3FC", - "digest": "2e28198e9b7ac08c55980677ed66655fd899e157f14184958bebd87fcd714940" - }, - { - "name": "grandma_tone2", - "unicode": "1F475-1F3FC", + "older_woman_tone2": { + "category": "people", + "moji": "👵🏼", + "unicodeVersion": "8.0", "digest": "2e28198e9b7ac08c55980677ed66655fd899e157f14184958bebd87fcd714940" }, - { - "name": "older_woman_tone3", - "unicode": "1F475-1F3FD", - "digest": "c968be0170f7e0c65d4f796337034cfb1daba897884da6fad85635ab5b6edf67" - }, - { - "name": "grandma_tone3", - "unicode": "1F475-1F3FD", + "older_woman_tone3": { + "category": "people", + "moji": "👵🏽", + "unicodeVersion": "8.0", "digest": "c968be0170f7e0c65d4f796337034cfb1daba897884da6fad85635ab5b6edf67" }, - { - "name": "older_woman_tone4", - "unicode": "1F475-1F3FE", - "digest": "3596a6fa9a643bf79255afcd29657b03850df8499db9669b92ce013af908af44" - }, - { - "name": "grandma_tone4", - "unicode": "1F475-1F3FE", + "older_woman_tone4": { + "category": "people", + "moji": "👵🏾", + "unicodeVersion": "8.0", "digest": "3596a6fa9a643bf79255afcd29657b03850df8499db9669b92ce013af908af44" }, - { - "name": "older_woman_tone5", - "unicode": "1F475-1F3FF", + "older_woman_tone5": { + "category": "people", + "moji": "👵🏿", + "unicodeVersion": "8.0", "digest": "c8998cb3dbd15e22bd1d6dad613d109ce371d9ffca3657e1a8afe5aeb30c1275" }, - { - "name": "grandma_tone5", - "unicode": "1F475-1F3FF", - "digest": "c8998cb3dbd15e22bd1d6dad613d109ce371d9ffca3657e1a8afe5aeb30c1275" - }, - { - "name": "om_symbol", - "unicode": "1F549", + "om_symbol": { + "category": "symbols", + "moji": "🕉", + "unicodeVersion": "7.0", "digest": "5ead73bea546ba9ba6da522f7280cc289c75ff5467742bdba31f92d0e1b3f4e6" }, - { - "name": "on", - "unicode": "1F51B", + "on": { + "category": "symbols", + "moji": "🔛", + "unicodeVersion": "6.0", "digest": "9cc61a6b31a30c32dab594191bf23f91e341c4105384ab22158a6d43e6364631" }, - { - "name": "oncoming_automobile", - "unicode": "1F698", + "oncoming_automobile": { + "category": "travel", + "moji": "🚘", + "unicodeVersion": "6.0", "digest": "557c9cacdc3f95215d4f7a6f097a2baa7c007cb9c519492a6717077af4ca6b56" }, - { - "name": "oncoming_bus", - "unicode": "1F68D", + "oncoming_bus": { + "category": "travel", + "moji": "🚍", + "unicodeVersion": "6.0", "digest": "059f28ce6bfb337e107db5982cbd2004844450ef20b4a54b9ca3cb738360ab05" }, - { - "name": "oncoming_police_car", - "unicode": "1F694", + "oncoming_police_car": { + "category": "travel", + "moji": "🚔", + "unicodeVersion": "6.0", "digest": "aee79306a0d129cfc1980f58db80391eb46d2d7d5f814bf431414dc7680cab72" }, - { - "name": "oncoming_taxi", - "unicode": "1F696", + "oncoming_taxi": { + "category": "travel", + "moji": "🚖", + "unicodeVersion": "6.0", "digest": "84351489fc86d980b8d3eb9ec4e81120fe700b3ac01346daebe2b7aeb9607a55" }, - { - "name": "one", - "unicode": "0031-20E3", + "one": { + "category": "symbols", + "moji": "1️⃣", + "unicodeVersion": "3.0", "digest": "d5d3fff04e68a114ff6464ee06fc831f3f381713045165f62a88d5e8215c195b" }, - { - "name": "open_file_folder", - "unicode": "1F4C2", + "open_file_folder": { + "category": "objects", + "moji": "📂", + "unicodeVersion": "6.0", "digest": "96cfc322ee4903ae8cec07604811742245fd7d14f00bb70276d39d29c48bed28" }, - { - "name": "open_hands", - "unicode": "1F450", + "open_hands": { + "category": "people", + "moji": "👐", + "unicodeVersion": "6.0", "digest": "a6c131da2040b48103cea14f280e728675da50fa448d2b3f3438fcbb5bf5596a" }, - { - "name": "open_hands_tone1", - "unicode": "1F450-1F3FB", + "open_hands_tone1": { + "category": "people", + "moji": "👐🏻", + "unicodeVersion": "8.0", "digest": "867128dff2fa9b860c10c6b792f989f0c057928783696062378f834c0ef89d85" }, - { - "name": "open_hands_tone2", - "unicode": "1F450-1F3FC", + "open_hands_tone2": { + "category": "people", + "moji": "👐🏼", + "unicodeVersion": "8.0", "digest": "487ff2745b03d49bb3b1d0acd86ba530fd8cc3f467ca3fa504f88f0ef1cbbc01" }, - { - "name": "open_hands_tone3", - "unicode": "1F450-1F3FD", + "open_hands_tone3": { + "category": "people", + "moji": "👐🏽", + "unicodeVersion": "8.0", "digest": "cb8cddc8b8661f874ac9478289d16cc41406b947bb87f3363df518a588a53e16" }, - { - "name": "open_hands_tone4", - "unicode": "1F450-1F3FE", + "open_hands_tone4": { + "category": "people", + "moji": "👐🏾", + "unicodeVersion": "8.0", "digest": "17dcc2c07230846a769f3c79ce618a757c88b9b58c95c6c5b2d7f968814d447d" }, - { - "name": "open_hands_tone5", - "unicode": "1F450-1F3FF", + "open_hands_tone5": { + "category": "people", + "moji": "👐🏿", + "unicodeVersion": "8.0", "digest": "36b2493d67c84cea4f3f85a3088c6abcfd35cf99f7aeaeedfafa420ee878e3d2" }, - { - "name": "open_mouth", - "unicode": "1F62E", + "open_mouth": { + "category": "people", + "moji": "😮", + "unicodeVersion": "6.1", "digest": "1906c5100ae0c8326ca5c4f9422976958a38dadd8d77724d68538a25d9623035" }, - { - "name": "ophiuchus", - "unicode": "26CE", + "ophiuchus": { + "category": "symbols", + "moji": "⛎", + "unicodeVersion": "6.0", "digest": "6112e2a1656b1cb8bd9a8b0dfa6cbf66d30cae671710a9ef75c821de344aab2b" }, - { - "name": "orange_book", - "unicode": "1F4D9", + "orange_book": { + "category": "objects", + "moji": "📙", + "unicodeVersion": "6.0", "digest": "41141b08d2beceded21a94795431603c47fd7d42a3a472a2aa8b2bb25fa87ebf" }, - { - "name": "orthodox_cross", - "unicode": "2626", + "orthodox_cross": { + "category": "symbols", + "moji": "☦", + "unicodeVersion": "1.1", "digest": "c16372102f0169dd6d32eb2b27a633aaee74e4e0fddcf723c15ad97f9dc6075c" }, - { - "name": "outbox_tray", - "unicode": "1F4E4", + "outbox_tray": { + "category": "objects", + "moji": "📤", + "unicodeVersion": "6.0", "digest": "e47cb481a0ffcb39996f32fd313e19b362a91d8dda15ffca48ac23a3b5bb5baf" }, - { - "name": "owl", - "unicode": "1F989", + "owl": { + "category": "nature", + "moji": "🦉", + "unicodeVersion": "9.0", "digest": "f62ec1ad23ad9038966eea8d8b79660ac212f291af2e89bcdb0fdc683caf41e5" }, - { - "name": "ox", - "unicode": "1F402", + "ox": { + "category": "nature", + "moji": "🐂", + "unicodeVersion": "6.0", "digest": "d13bc60552190bb9936bf32d681bdc742439b702a09cfc62137ea09a98624aed" }, - { - "name": "package", - "unicode": "1F4E6", + "package": { + "category": "objects", + "moji": "📦", + "unicodeVersion": "6.0", "digest": "e82bf5accebb65136e897c15607eef635fb79fd7b2d8c8e19a9eb00b6786918c" }, - { - "name": "page_facing_up", - "unicode": "1F4C4", + "page_facing_up": { + "category": "objects", + "moji": "📄", + "unicodeVersion": "6.0", "digest": "3884868bdcb2f29615b09a13a30385cbc5269379094a54b5a7e8a5f4e8ce905a" }, - { - "name": "page_with_curl", - "unicode": "1F4C3", + "page_with_curl": { + "category": "objects", + "moji": "📃", + "unicodeVersion": "6.0", "digest": "3d6257670189f841ad1fa45415c34feb2433b2cb35bb435c4ee122ce89b39669" }, - { - "name": "pager", - "unicode": "1F4DF", + "pager": { + "category": "objects", + "moji": "📟", + "unicodeVersion": "6.0", "digest": "e21c756cc1c58ebc1b37ebcd38e22a25b31e2e81306c6f18285d6a7671f9eb12" }, - { - "name": "paintbrush", - "unicode": "1F58C", - "digest": "fc0da7a25b726b8be9dd6467953e27293d2313a21eeff21424c2a19be614fff2" - }, - { - "name": "lower_left_paintbrush", - "unicode": "1F58C", + "paintbrush": { + "category": "objects", + "moji": "🖌", + "unicodeVersion": "7.0", "digest": "fc0da7a25b726b8be9dd6467953e27293d2313a21eeff21424c2a19be614fff2" }, - { - "name": "palm_tree", - "unicode": "1F334", + "palm_tree": { + "category": "nature", + "moji": "🌴", + "unicodeVersion": "6.0", "digest": "90fedafd62fe0abf51325174d0f293ebb9a4794913b9ba93b12f2d0119056df1" }, - { - "name": "pancakes", - "unicode": "1F95E", + "pancakes": { + "category": "food", + "moji": "🥞", + "unicodeVersion": "9.0", "digest": "5256b4832431e8a88555796b1a9726f12d909a26fb2bdc3a0abff76412c45903" }, - { - "name": "panda_face", - "unicode": "1F43C", + "panda_face": { + "category": "nature", + "moji": "🐼", + "unicodeVersion": "6.0", "digest": "56a4b84abe983bd6569be1b81ac5e43071015fd308389a16b92231310ae56a5b" }, - { - "name": "paperclip", - "unicode": "1F4CE", + "paperclip": { + "category": "objects", + "moji": "📎", + "unicodeVersion": "6.0", "digest": "d1e2ce94a12b7e8b7a9bba49e47ddc7432ec0288545d3b6817c7a499e806e3f0" }, - { - "name": "paperclips", - "unicode": "1F587", - "digest": "70cefa0d0777f070e393e9f95c24146fe2dd627f30fa3845baa19310d9291fe2" - }, - { - "name": "linked_paperclips", - "unicode": "1F587", + "paperclips": { + "category": "objects", + "moji": "🖇", + "unicodeVersion": "7.0", "digest": "70cefa0d0777f070e393e9f95c24146fe2dd627f30fa3845baa19310d9291fe2" }, - { - "name": "park", - "unicode": "1F3DE", + "park": { + "category": "travel", + "moji": "🏞", + "unicodeVersion": "7.0", "digest": "444dce8014e0817ddd756c36a38adfbbf7ae4c6aa509e4cae291828f0716d5e7" }, - { - "name": "national_park", - "unicode": "1F3DE", - "digest": "444dce8014e0817ddd756c36a38adfbbf7ae4c6aa509e4cae291828f0716d5e7" - }, - { - "name": "parking", - "unicode": "1F17F", + "parking": { + "category": "symbols", + "moji": "🅿", + "unicodeVersion": "5.2", "digest": "9f1da460a7dd58b26beab8cf701be2691fb812208fbc941c71daa35be1507c2f" }, - { - "name": "part_alternation_mark", - "unicode": "303D", + "part_alternation_mark": { + "category": "symbols", + "moji": "〽", + "unicodeVersion": "3.2", "digest": "956da19353bb38fd4dfe0ab5360679a9035d566858fb5de62887b85c75fb8eef" }, - { - "name": "partly_sunny", - "unicode": "26C5", + "partly_sunny": { + "category": "nature", + "moji": "⛅", + "unicodeVersion": "5.2", "digest": "8fb9a6d2caf9e0cce58447762f0dfd6aa0b581b2e83fea6411348e0cbc8cf3c4" }, - { - "name": "passport_control", - "unicode": "1F6C2", + "passport_control": { + "category": "symbols", + "moji": "🛂", + "unicodeVersion": "6.0", "digest": "d9be6eed2c90e1c89171c42d70a06485fdf86a4c68833371832cc1f6897fadd0" }, - { - "name": "pause_button", - "unicode": "23F8", - "digest": "143221d99e82399ed7824b6c5e185700896492058b65c04e4c668291de78b203" - }, - { - "name": "double_vertical_bar", - "unicode": "23F8", + "pause_button": { + "category": "symbols", + "moji": "⏸", + "unicodeVersion": "7.0", "digest": "143221d99e82399ed7824b6c5e185700896492058b65c04e4c668291de78b203" }, - { - "name": "peace", - "unicode": "262E", + "peace": { + "category": "symbols", + "moji": "☮", + "unicodeVersion": "1.1", "digest": "65181429e373c1f0507bbd98425c1bec0c042d648fb285a392460cbce60f44d4" }, - { - "name": "peace_symbol", - "unicode": "262E", - "digest": "65181429e373c1f0507bbd98425c1bec0c042d648fb285a392460cbce60f44d4" - }, - { - "name": "peach", - "unicode": "1F351", + "peach": { + "category": "food", + "moji": "🍑", + "unicodeVersion": "6.0", "digest": "768d1f4f29e1e06aff5abb29043be83087ded16427ce6a2d0f682814e665e311" }, - { - "name": "peanuts", - "unicode": "1F95C", - "digest": "e2384846b6e4a6c3a56e991ebb749cb68b330ac00a9e9d888b2c39105ff7ff5d" - }, - { - "name": "shelled_peanut", - "unicode": "1F95C", + "peanuts": { + "category": "food", + "moji": "🥜", + "unicodeVersion": "9.0", "digest": "e2384846b6e4a6c3a56e991ebb749cb68b330ac00a9e9d888b2c39105ff7ff5d" }, - { - "name": "pear", - "unicode": "1F350", + "pear": { + "category": "food", + "moji": "🍐", + "unicodeVersion": "6.0", "digest": "b7c9cf90bb979649b863d2f4132f1b51f6f8107d42e08fb8b4033fea32844948" }, - { - "name": "pen_ballpoint", - "unicode": "1F58A", + "pen_ballpoint": { + "category": "objects", + "moji": "🖊", + "unicodeVersion": "7.0", "digest": "aacb20b220f26704e10303deeea33be0eec2d3811dcba7795902ca44b6ae9876" }, - { - "name": "lower_left_ballpoint_pen", - "unicode": "1F58A", - "digest": "aacb20b220f26704e10303deeea33be0eec2d3811dcba7795902ca44b6ae9876" - }, - { - "name": "pen_fountain", - "unicode": "1F58B", - "digest": "3619913eab2b6291f518b40481bb3eca0820d68b0a1b3c11fb6a69c62b75a626" - }, - { - "name": "lower_left_fountain_pen", - "unicode": "1F58B", + "pen_fountain": { + "category": "objects", + "moji": "🖋", + "unicodeVersion": "7.0", "digest": "3619913eab2b6291f518b40481bb3eca0820d68b0a1b3c11fb6a69c62b75a626" }, - { - "name": "pencil", - "unicode": "1F4DD", + "pencil": { + "category": "objects", + "moji": "📝", + "unicodeVersion": "6.0", "digest": "accbc3f1439b7faa4411e502385f78a16c8e71851f71fc13582753291ffb507c" }, - { - "name": "memo", - "unicode": "1F4DD", - "digest": "accbc3f1439b7faa4411e502385f78a16c8e71851f71fc13582753291ffb507c" - }, - { - "name": "pencil2", - "unicode": "270F", + "pencil2": { + "category": "objects", + "moji": "✏", + "unicodeVersion": "1.1", "digest": "9ca1b56b5726f472b1f1b23050ed163e213916dac379d22e38e4c8358fe871e0" }, - { - "name": "penguin", - "unicode": "1F427", + "penguin": { + "category": "nature", + "moji": "🐧", + "unicodeVersion": "6.0", "digest": "a1800ab931d6dc84a9c89bfab2c815198025c276d952509c55b18dd20bd9d316" }, - { - "name": "pensive", - "unicode": "1F614", + "pensive": { + "category": "people", + "moji": "😔", + "unicodeVersion": "6.0", "digest": "d237deff9f5ead8a0b281b7e5c6f4b82e98cc30c80c86c22c3fdc6160090b2f2" }, - { - "name": "performing_arts", - "unicode": "1F3AD", + "performing_arts": { + "category": "activity", + "moji": "🎭", + "unicodeVersion": "6.0", "digest": "d7c7bc9213e308ca26286cbbd8012e656b0f9b00293758faf1bfccc4c5ceabed" }, - { - "name": "persevere", - "unicode": "1F623", + "persevere": { + "category": "people", + "moji": "😣", + "unicodeVersion": "6.0", "digest": "c361509c9b8663af19a02a1ffff61b1b0d0b4bd75d693ce3d406b0ca1bde1ca0" }, - { - "name": "person_frowning", - "unicode": "1F64D", + "person_frowning": { + "category": "people", + "moji": "🙍", + "unicodeVersion": "6.0", "digest": "b37be8bd95f21a6860ad3f171b8086125ab37331b382d87bcdb4cd684800546b" }, - { - "name": "person_frowning_tone1", - "unicode": "1F64D-1F3FB", + "person_frowning_tone1": { + "category": "people", + "moji": "🙍🏻", + "unicodeVersion": "8.0", "digest": "3d5e78a367f9673baed2a86bc11cf04fd44394aadb65291fa51ade8dca318427" }, - { - "name": "person_frowning_tone2", - "unicode": "1F64D-1F3FC", + "person_frowning_tone2": { + "category": "people", + "moji": "🙍🏼", + "unicodeVersion": "8.0", "digest": "7456c414c65ad6b6f11855f68a2eedc18113526f86862c4373202397cb1bed2c" }, - { - "name": "person_frowning_tone3", - "unicode": "1F64D-1F3FD", + "person_frowning_tone3": { + "category": "people", + "moji": "🙍🏽", + "unicodeVersion": "8.0", "digest": "c86cf2d6951f1e6a7c786a74caaf68a777cf00e88023e23849d4383f864ae437" }, - { - "name": "person_frowning_tone4", - "unicode": "1F64D-1F3FE", + "person_frowning_tone4": { + "category": "people", + "moji": "🙍🏾", + "unicodeVersion": "8.0", "digest": "944e96ced645ced8db6bb50120c7e37ed46b6960d595cbfe964c81803efa83aa" }, - { - "name": "person_frowning_tone5", - "unicode": "1F64D-1F3FF", + "person_frowning_tone5": { + "category": "people", + "moji": "🙍🏿", + "unicodeVersion": "8.0", "digest": "4bd0ea571be6ef9f0493784ef0d12d5e47bc2d6ac610fb42c450bf3d87fb2948" }, - { - "name": "person_with_blond_hair", - "unicode": "1F471", + "person_with_blond_hair": { + "category": "people", + "moji": "👱", + "unicodeVersion": "6.0", "digest": "a7f94ede2e43308108c2260d83fc10121dda09a67f94a0a840e6d7bba7fd5616" }, - { - "name": "person_with_blond_hair_tone1", - "unicode": "1F471-1F3FB", + "person_with_blond_hair_tone1": { + "category": "people", + "moji": "👱🏻", + "unicodeVersion": "8.0", "digest": "00a116357a7878554c83e5bade4bddfa9cfabf76a229efa19cbb58e0d216219c" }, - { - "name": "person_with_blond_hair_tone2", - "unicode": "1F471-1F3FC", + "person_with_blond_hair_tone2": { + "category": "people", + "moji": "👱🏼", + "unicodeVersion": "8.0", "digest": "df509ebe92ed3138b9d5bd4645eff4b13f77f714cf62bb949c59eff1adc00019" }, - { - "name": "person_with_blond_hair_tone3", - "unicode": "1F471-1F3FD", + "person_with_blond_hair_tone3": { + "category": "people", + "moji": "👱🏽", + "unicodeVersion": "8.0", "digest": "6f328513f440a0c8cd1dc44596a5028fd8f306bdaf57c1e6f3aa94a3aa262b3c" }, - { - "name": "person_with_blond_hair_tone4", - "unicode": "1F471-1F3FE", + "person_with_blond_hair_tone4": { + "category": "people", + "moji": "👱🏾", + "unicodeVersion": "8.0", "digest": "32df1a577815b009696643ad80d063cc97b35d54add6d4e5517fc936f6da9ee8" }, - { - "name": "person_with_blond_hair_tone5", - "unicode": "1F471-1F3FF", + "person_with_blond_hair_tone5": { + "category": "people", + "moji": "👱🏿", + "unicodeVersion": "8.0", "digest": "2e270bb39187d8e36a33f4aa4d6045308189595fafc157cf7993e82d7ce93442" }, - { - "name": "person_with_pouting_face", - "unicode": "1F64E", + "person_with_pouting_face": { + "category": "people", + "moji": "🙎", + "unicodeVersion": "6.0", "digest": "57e9a6e5f82121516dc189173f2a63b218f726cd51014e24a18c2bdfeeec3a0b" }, - { - "name": "person_with_pouting_face_tone1", - "unicode": "1F64E-1F3FB", + "person_with_pouting_face_tone1": { + "category": "people", + "moji": "🙎🏻", + "unicodeVersion": "8.0", "digest": "d10dadb1ac03fc2e221eff77b4c47935dc0b4fe897af3de30461e7226c3b4bbc" }, - { - "name": "person_with_pouting_face_tone2", - "unicode": "1F64E-1F3FC", + "person_with_pouting_face_tone2": { + "category": "people", + "moji": "🙎🏼", + "unicodeVersion": "8.0", "digest": "efface531537ab934b3b96985210a2dac88de812e82e804d6ec12174e536d1cc" }, - { - "name": "person_with_pouting_face_tone3", - "unicode": "1F64E-1F3FD", + "person_with_pouting_face_tone3": { + "category": "people", + "moji": "🙎🏽", + "unicodeVersion": "8.0", "digest": "7ff26ece237216b949bfa96d16bd12cfd248c6fd3e4ed89aa6c735c09eafaeff" }, - { - "name": "person_with_pouting_face_tone4", - "unicode": "1F64E-1F3FE", + "person_with_pouting_face_tone4": { + "category": "people", + "moji": "🙎🏾", + "unicodeVersion": "8.0", "digest": "045c04105df41d94ff4942133c7394e42ff35ef76c4ccb711497ab77ae6219f2" }, - { - "name": "person_with_pouting_face_tone5", - "unicode": "1F64E-1F3FF", + "person_with_pouting_face_tone5": { + "category": "people", + "moji": "🙎🏿", + "unicodeVersion": "8.0", "digest": "783ee37f146fcf61d38af5009f5823cf6526fe99ed891979f454016bce9dd4ba" }, - { - "name": "pick", - "unicode": "26CF", + "pick": { + "category": "objects", + "moji": "⛏", + "unicodeVersion": "5.2", "digest": "7f0ec5445b4d5c66cf46e2a7332946cce34bd70e9929ac7a119251a7f57f555d" }, - { - "name": "pig", - "unicode": "1F437", + "pig": { + "category": "nature", + "moji": "🐷", + "unicodeVersion": "6.0", "digest": "51362570ab36805c8f67622ee4543e38811f8abb20f732a1af2ffbff2d63d042" }, - { - "name": "pig2", - "unicode": "1F416", + "pig2": { + "category": "nature", + "moji": "🐖", + "unicodeVersion": "6.0", "digest": "67010e255f28061b9d9210bcdab6edc072642ad134122a1d0c7e3a6b1795a45b" }, - { - "name": "pig_nose", - "unicode": "1F43D", + "pig_nose": { + "category": "nature", + "moji": "🐽", + "unicodeVersion": "6.0", "digest": "0b21cac238bf4910939fbea9bed35552378c1b605a3867d7b85c1556dbda22a9" }, - { - "name": "pill", - "unicode": "1F48A", + "pill": { + "category": "objects", + "moji": "💊", + "unicodeVersion": "6.0", "digest": "cb00be361aaba6dbcf8da58bd20b76221dd75031362ecae99496b088ed413a7f" }, - { - "name": "pineapple", - "unicode": "1F34D", + "pineapple": { + "category": "food", + "moji": "🍍", + "unicodeVersion": "6.0", "digest": "621d4d4c52b59e566c2e29ed7845c8bd2d1da0946577527342097808d170dd70" }, - { - "name": "ping_pong", - "unicode": "1F3D3", - "digest": "943a858bd054c81a08a08951f8351c27c8009b85a9359729c7362868298b58e1" - }, - { - "name": "table_tennis", - "unicode": "1F3D3", + "ping_pong": { + "category": "activity", + "moji": "🏓", + "unicodeVersion": "8.0", "digest": "943a858bd054c81a08a08951f8351c27c8009b85a9359729c7362868298b58e1" }, - { - "name": "pisces", - "unicode": "2653", + "pisces": { + "category": "symbols", + "moji": "♓", + "unicodeVersion": "1.1", "digest": "453c3915122a4b6b32867056d2447be48675a84469145c88d52f8007fcb0861a" }, - { - "name": "pizza", - "unicode": "1F355", + "pizza": { + "category": "food", + "moji": "🍕", + "unicodeVersion": "6.0", "digest": "169bc6c1e1d7fdab1b8bf2eab0eeec4f9a7ae08b7b9b38f33b0b0c642e72053a" }, - { - "name": "place_of_worship", - "unicode": "1F6D0", + "place_of_worship": { + "category": "symbols", + "moji": "🛐", + "unicodeVersion": "8.0", "digest": "daf271d36a38ee8c0f8b9de84c128ab8b25a5b7df8f107308d0353c961f2c644" }, - { - "name": "worship_symbol", - "unicode": "1F6D0", - "digest": "daf271d36a38ee8c0f8b9de84c128ab8b25a5b7df8f107308d0353c961f2c644" - }, - { - "name": "play_pause", - "unicode": "23EF", + "play_pause": { + "category": "symbols", + "moji": "⏯", + "unicodeVersion": "6.0", "digest": "af1498f34a3d6e0da8bbd26ebaa447e697e2df08c8eb255437cf7905c93f8c42" }, - { - "name": "point_down", - "unicode": "1F447", + "point_down": { + "category": "people", + "moji": "👇", + "unicodeVersion": "6.0", "digest": "4ecdb3f31c16dc38113b8854ec1a7884613b688a185ebdf967eab9a81018f76d" }, - { - "name": "point_down_tone1", - "unicode": "1F447-1F3FB", + "point_down_tone1": { + "category": "people", + "moji": "👇🏻", + "unicodeVersion": "8.0", "digest": "c74a7c94367cddbfa840542dc0924adeb0d108be0c7fde8c25fb95d69115d283" }, - { - "name": "point_down_tone2", - "unicode": "1F447-1F3FC", + "point_down_tone2": { + "category": "people", + "moji": "👇🏼", + "unicodeVersion": "8.0", "digest": "dc4bda0726d85418b974addb42738f437fbb9cf16e5815cdbab3859c4ada6cae" }, - { - "name": "point_down_tone3", - "unicode": "1F447-1F3FD", + "point_down_tone3": { + "category": "people", + "moji": "👇🏽", + "unicodeVersion": "8.0", "digest": "e460f81a501376d2f0ed1d45e358c5ed03ba049e8f466e4298afb4f3ca6d24dc" }, - { - "name": "point_down_tone4", - "unicode": "1F447-1F3FE", + "point_down_tone4": { + "category": "people", + "moji": "👇🏾", + "unicodeVersion": "8.0", "digest": "4bc91cd771f24e0f897a9d8b18f323fec9a82da0fc2429c4a7e4e6a9d885a0a3" }, - { - "name": "point_down_tone5", - "unicode": "1F447-1F3FF", + "point_down_tone5": { + "category": "people", + "moji": "👇🏿", + "unicodeVersion": "8.0", "digest": "7e47c6bc73250f36dc7ae1c1c09e7b41f30647b9d0ff703a53a75cc046b5057d" }, - { - "name": "point_left", - "unicode": "1F448", + "point_left": { + "category": "people", + "moji": "👈", + "unicodeVersion": "6.0", "digest": "b5a7e864a0016afbadb3bec41f51ecf8c4af73cc20462e1a08b357f90bca6879" }, - { - "name": "point_left_tone1", - "unicode": "1F448-1F3FB", + "point_left_tone1": { + "category": "people", + "moji": "👈🏻", + "unicodeVersion": "8.0", "digest": "9f1868272a10a2b738c065be5d30241643324550cfd47baf01c7a09060e66d31" }, - { - "name": "point_left_tone2", - "unicode": "1F448-1F3FC", + "point_left_tone2": { + "category": "people", + "moji": "👈🏼", + "unicodeVersion": "8.0", "digest": "bf0d58c68178a2c2c01d4a6235a1a66b90073cea170f9f6fe2668b6dd68424f7" }, - { - "name": "point_left_tone3", - "unicode": "1F448-1F3FD", + "point_left_tone3": { + "category": "people", + "moji": "👈🏽", + "unicodeVersion": "8.0", "digest": "34d28c97bc8f9d111d14e328153c4298fc32cf18e39e20aacaec17846645ed90" }, - { - "name": "point_left_tone4", - "unicode": "1F448-1F3FE", + "point_left_tone4": { + "category": "people", + "moji": "👈🏾", + "unicodeVersion": "8.0", "digest": "c40c8436316915d516c53bb1c98a469528cefd98baa719be7e748c4608cbbcc9" }, - { - "name": "point_left_tone5", - "unicode": "1F448-1F3FF", + "point_left_tone5": { + "category": "people", + "moji": "👈🏿", + "unicodeVersion": "8.0", "digest": "c410fe32e4ce0ded74845a54b86090e59e5820d457837b16e175b36cc71ecb46" }, - { - "name": "point_right", - "unicode": "1F449", + "point_right": { + "category": "people", + "moji": "👉", + "unicodeVersion": "6.0", "digest": "44d9251ab41f2f48c2250c44a47f92b3476a71f13fbbbfb637547db837fd5a49" }, - { - "name": "point_right_tone1", - "unicode": "1F449-1F3FB", + "point_right_tone1": { + "category": "people", + "moji": "👉🏻", + "unicodeVersion": "8.0", "digest": "9fcce259eb81c0b52ec7796b98a1653194e3a9021a1d338df1dbbab7522fc406" }, - { - "name": "point_right_tone2", - "unicode": "1F449-1F3FC", + "point_right_tone2": { + "category": "people", + "moji": "👉🏼", + "unicodeVersion": "8.0", "digest": "9d00a0b1cfc435674dc56065b3d28d28839196977504cf20581205351d8708f2" }, - { - "name": "point_right_tone3", - "unicode": "1F449-1F3FD", + "point_right_tone3": { + "category": "people", + "moji": "👉🏽", + "unicodeVersion": "8.0", "digest": "e3026a70630ba73d76892a055a80cac2f78d509faddce737f802d2abefa074ba" }, - { - "name": "point_right_tone4", - "unicode": "1F449-1F3FE", + "point_right_tone4": { + "category": "people", + "moji": "👉🏾", + "unicodeVersion": "8.0", "digest": "ea508fde90561460361773b4e1b8e80874667b19ac115926206e7c592587cb76" }, - { - "name": "point_right_tone5", - "unicode": "1F449-1F3FF", + "point_right_tone5": { + "category": "people", + "moji": "👉🏿", + "unicodeVersion": "8.0", "digest": "d59cdb2864eb2929941ecd233f8b8afcddc30fbd4594e5f9acf6386ae06ac12c" }, - { - "name": "point_up", - "unicode": "261D", + "point_up": { + "category": "people", + "moji": "☝", + "unicodeVersion": "1.1", "digest": "b69ff4f650989709f2185822d278c7773672bd9eb4a625da80f3038a2b9ce42b" }, - { - "name": "point_up_2", - "unicode": "1F446", + "point_up_2": { + "category": "people", + "moji": "👆", + "unicodeVersion": "6.0", "digest": "e83cd9eff2af5125a25f5a306c3ee3cfea240add683b5c36a86a994a8d8c805c" }, - { - "name": "point_up_2_tone1", - "unicode": "1F446-1F3FB", + "point_up_2_tone1": { + "category": "people", + "moji": "👆🏻", + "unicodeVersion": "8.0", "digest": "b02ec3e7e04a83bfb769cffb951cbf32aa78e56fa5a51c097f9326df9e08ed33" }, - { - "name": "point_up_2_tone2", - "unicode": "1F446-1F3FC", + "point_up_2_tone2": { + "category": "people", + "moji": "👆🏼", + "unicodeVersion": "8.0", "digest": "32994b85c8b4a1383ca985ebc3382be88866cea1ff1315adfb71fb05e992a232" }, - { - "name": "point_up_2_tone3", - "unicode": "1F446-1F3FD", + "point_up_2_tone3": { + "category": "people", + "moji": "👆🏽", + "unicodeVersion": "8.0", "digest": "9e263bcfb82ada34ff85291f36e64e66b86760fb11a4e0c554e801644d417d6d" }, - { - "name": "point_up_2_tone4", - "unicode": "1F446-1F3FE", + "point_up_2_tone4": { + "category": "people", + "moji": "👆🏾", + "unicodeVersion": "8.0", "digest": "3edc92130a0851ac7b5236772ce7918d088689221df287098688e1ed5b3ff181" }, - { - "name": "point_up_2_tone5", - "unicode": "1F446-1F3FF", + "point_up_2_tone5": { + "category": "people", + "moji": "👆🏿", + "unicodeVersion": "8.0", "digest": "cabb3b7da9290840ef59d0c8b22625bdb2e94842f01b0a575ccbc348f3069d77" }, - { - "name": "point_up_tone1", - "unicode": "261D-1F3FB", + "point_up_tone1": { + "category": "people", + "moji": "☝🏻", + "unicodeVersion": "8.0", "digest": "e496fda349072f8b321ceb7a251175f7244c3076661f5ede48ea75ba1acf8339" }, - { - "name": "point_up_tone2", - "unicode": "261D-1F3FC", + "point_up_tone2": { + "category": "people", + "moji": "☝🏼", + "unicodeVersion": "8.0", "digest": "5a8081323f3baa67e6431e21e16a36559b339f5175d586644e34947f738dd07a" }, - { - "name": "point_up_tone3", - "unicode": "261D-1F3FD", + "point_up_tone3": { + "category": "people", + "moji": "☝🏽", + "unicodeVersion": "8.0", "digest": "07bf0cea812eb226b443334e026e13d1ec23e013478f4af862a3919703107842" }, - { - "name": "point_up_tone4", - "unicode": "261D-1F3FE", + "point_up_tone4": { + "category": "people", + "moji": "☝🏾", + "unicodeVersion": "8.0", "digest": "1fbbd71433108143ee157d0fdadd183f7f013bafa96f0dd93b181e1fd5fd4af2" }, - { - "name": "point_up_tone5", - "unicode": "261D-1F3FF", + "point_up_tone5": { + "category": "people", + "moji": "☝🏿", + "unicodeVersion": "8.0", "digest": "ad068ef32df32f8297955490a9a90590a0f93ed5702a052cd0d8f6484c6cc679" }, - { - "name": "police_car", - "unicode": "1F693", + "police_car": { + "category": "travel", + "moji": "🚓", + "unicodeVersion": "6.0", "digest": "0909be1bd615ae331a7cce71e16dee3ca663c721d5170072c593cb7c76f9f661" }, - { - "name": "poodle", - "unicode": "1F429", + "poodle": { + "category": "nature", + "moji": "🐩", + "unicodeVersion": "6.0", "digest": "f1742fdf3fd26a8a5cfeaba57026518dacaad364cbd03344c4000a35af13e47a" }, - { - "name": "poop", - "unicode": "1F4A9", - "digest": "857a61c872138d359a7fe8257bb26118afa49d75186eca2addb415d07c92b3ec" - }, - { - "name": "shit", - "unicode": "1F4A9", + "poop": { + "category": "people", + "moji": "💩", + "unicodeVersion": "6.0", "digest": "857a61c872138d359a7fe8257bb26118afa49d75186eca2addb415d07c92b3ec" }, - { - "name": "hankey", - "unicode": "1F4A9", - "digest": "857a61c872138d359a7fe8257bb26118afa49d75186eca2addb415d07c92b3ec" - }, - { - "name": "poo", - "unicode": "1F4A9", - "digest": "857a61c872138d359a7fe8257bb26118afa49d75186eca2addb415d07c92b3ec" - }, - { - "name": "popcorn", - "unicode": "1F37F", + "popcorn": { + "category": "food", + "moji": "🍿", + "unicodeVersion": "8.0", "digest": "684f1b7ef34ea7ca933aed41569bc6595a19ef0d546a1b7b9e69f8335540b323" }, - { - "name": "post_office", - "unicode": "1F3E3", + "post_office": { + "category": "travel", + "moji": "🏣", + "unicodeVersion": "6.0", "digest": "54398ee396c1314a7993b1cb1cba264946b5c9d5a7dbb43fd67286854d1d1a0f" }, - { - "name": "postal_horn", - "unicode": "1F4EF", + "postal_horn": { + "category": "objects", + "moji": "📯", + "unicodeVersion": "6.0", "digest": "0ea12f44f3bae9a14bde3b37361b48bd738d2f613bb1b53a9204959b70e643f8" }, - { - "name": "postbox", - "unicode": "1F4EE", + "postbox": { + "category": "objects", + "moji": "📮", + "unicodeVersion": "6.0", "digest": "bbc424ae8d46de380d7023a43ea064002fd614657d00330d3503275827ac87e2" }, - { - "name": "potable_water", - "unicode": "1F6B0", + "potable_water": { + "category": "symbols", + "moji": "🚰", + "unicodeVersion": "6.0", "digest": "dbe80d9637837377cc2a290da2e895f81a3108cc18b049e3d87212402c1c2098" }, - { - "name": "potato", - "unicode": "1F954", + "potato": { + "category": "food", + "moji": "🥔", + "unicodeVersion": "9.0", "digest": "a56a69f36f3a0793f278726d92c0cea2960554f3062ef1a0904526a04511d8e1" }, - { - "name": "pouch", - "unicode": "1F45D", + "pouch": { + "category": "people", + "moji": "👝", + "unicodeVersion": "6.0", "digest": "9f012b90310b4a072b6a8fa2c64def087b5f7ffffaafc36e1856ba943a170351" }, - { - "name": "poultry_leg", - "unicode": "1F357", + "poultry_leg": { + "category": "food", + "moji": "🍗", + "unicodeVersion": "6.0", "digest": "1445ec4f5e68a19e5a84e5537dca8190d62409070c954d112e6097f1a6b7f054" }, - { - "name": "pound", - "unicode": "1F4B7", + "pound": { + "category": "objects", + "moji": "💷", + "unicodeVersion": "6.0", "digest": "eb11b83eb52adb0a15e69a3bc15788a2dc7825dedee81ac3af84963c9dd517b5" }, - { - "name": "pouting_cat", - "unicode": "1F63E", + "pouting_cat": { + "category": "people", + "moji": "😾", + "unicodeVersion": "6.0", "digest": "8822abedf3499cf98278d7eeea0764d1100ec25cad71b4b2e877f9346f8c8138" }, - { - "name": "pray", - "unicode": "1F64F", + "pray": { + "category": "people", + "moji": "🙏", + "unicodeVersion": "6.0", "digest": "735b79dab34ac2cf81fd42fdcd7eb1f13c24655e5e343816d5764896c03edeea" }, - { - "name": "pray_tone1", - "unicode": "1F64F-1F3FB", + "pray_tone1": { + "category": "people", + "moji": "🙏🏻", + "unicodeVersion": "8.0", "digest": "e8b6103450215e8566797f150978355e297deade4eb47a6371f7a7bc558fed9d" }, - { - "name": "pray_tone2", - "unicode": "1F64F-1F3FC", + "pray_tone2": { + "category": "people", + "moji": "🙏🏼", + "unicodeVersion": "8.0", "digest": "ee8baacd95d7e8dbad8a1f2d9a12e36c98f3d518db5d3b117d0a18290815e62b" }, - { - "name": "pray_tone3", - "unicode": "1F64F-1F3FD", + "pray_tone3": { + "category": "people", + "moji": "🙏🏽", + "unicodeVersion": "8.0", "digest": "ae8c0caa9aca0a6c44069e76a7535c961d0284cd701812f76bbd2bd79ce2bd53" }, - { - "name": "pray_tone4", - "unicode": "1F64F-1F3FE", + "pray_tone4": { + "category": "people", + "moji": "🙏🏾", + "unicodeVersion": "8.0", "digest": "64f7b3178b8cd6f6a877ed583539eefe068fa87a0dd658fdcd58c8bc809f7e17" }, - { - "name": "pray_tone5", - "unicode": "1F64F-1F3FF", + "pray_tone5": { + "category": "people", + "moji": "🙏🏿", + "unicodeVersion": "8.0", "digest": "5bc8cdce937ac06779c87021423efcec4f602aa4a39dba90b00de81033005332" }, - { - "name": "prayer_beads", - "unicode": "1F4FF", + "prayer_beads": { + "category": "objects", + "moji": "📿", + "unicodeVersion": "8.0", "digest": "80177091264430cbcf7c994fbe5ee17319d1a58d933636cc752a54dafcf98a05" }, - { - "name": "pregnant_woman", - "unicode": "1F930", + "pregnant_woman": { + "category": "people", + "moji": "🤰", + "unicodeVersion": "9.0", "digest": "49abb86409103338bdb6ae43c13a78ca2dc9cd158a26df35eadd0da3c84a4352" }, - { - "name": "expecting_woman", - "unicode": "1F930", - "digest": "49abb86409103338bdb6ae43c13a78ca2dc9cd158a26df35eadd0da3c84a4352" - }, - { - "name": "pregnant_woman_tone1", - "unicode": "1F930-1F3FB", - "digest": "5a9f8ed2b631ecf8af111803a5c11f4c156435a5293cb50329c7b98697c8da25" - }, - { - "name": "expecting_woman_tone1", - "unicode": "1F930-1F3FB", + "pregnant_woman_tone1": { + "category": "people", + "moji": "🤰🏻", + "unicodeVersion": "9.0", "digest": "5a9f8ed2b631ecf8af111803a5c11f4c156435a5293cb50329c7b98697c8da25" }, - { - "name": "pregnant_woman_tone2", - "unicode": "1F930-1F3FC", + "pregnant_woman_tone2": { + "category": "people", + "moji": "🤰🏼", + "unicodeVersion": "9.0", "digest": "279a2eafff603b11629c955b05f5bd3d7da9a271d4fb3f02e9ccd457b8d2d815" }, - { - "name": "expecting_woman_tone2", - "unicode": "1F930-1F3FC", - "digest": "279a2eafff603b11629c955b05f5bd3d7da9a271d4fb3f02e9ccd457b8d2d815" - }, - { - "name": "pregnant_woman_tone3", - "unicode": "1F930-1F3FD", + "pregnant_woman_tone3": { + "category": "people", + "moji": "🤰🏽", + "unicodeVersion": "9.0", "digest": "93bb63ec2312db315e3f0065520b715cc413ac0fd65538ec9b5cd97df2a42b20" }, - { - "name": "expecting_woman_tone3", - "unicode": "1F930-1F3FD", - "digest": "93bb63ec2312db315e3f0065520b715cc413ac0fd65538ec9b5cd97df2a42b20" - }, - { - "name": "pregnant_woman_tone4", - "unicode": "1F930-1F3FE", + "pregnant_woman_tone4": { + "category": "people", + "moji": "🤰🏾", + "unicodeVersion": "9.0", "digest": "b8dc3dcec894bfd832a249459b10850f8786b6778d8887a677d1291865623da2" }, - { - "name": "expecting_woman_tone4", - "unicode": "1F930-1F3FE", - "digest": "b8dc3dcec894bfd832a249459b10850f8786b6778d8887a677d1291865623da2" - }, - { - "name": "pregnant_woman_tone5", - "unicode": "1F930-1F3FF", - "digest": "73ee432752f81980f353a7f9b9f7a5ece62512dca08e15c1876b89227face21c" - }, - { - "name": "expecting_woman_tone5", - "unicode": "1F930-1F3FF", + "pregnant_woman_tone5": { + "category": "people", + "moji": "🤰🏿", + "unicodeVersion": "9.0", "digest": "73ee432752f81980f353a7f9b9f7a5ece62512dca08e15c1876b89227face21c" }, - { - "name": "prince", - "unicode": "1F934", + "prince": { + "category": "people", + "moji": "🤴", + "unicodeVersion": "9.0", "digest": "34a0e0625f0a9825d3674192d6233b6cae4d8130451293df09f91a6a4165869c" }, - { - "name": "prince_tone1", - "unicode": "1F934-1F3FB", + "prince_tone1": { + "category": "people", + "moji": "🤴🏻", + "unicodeVersion": "9.0", "digest": "ccecdfeccb2ab1fceceae14f3fba875c8c7099785a4c40131c08a697b5b675fc" }, - { - "name": "prince_tone2", - "unicode": "1F934-1F3FC", + "prince_tone2": { + "category": "people", + "moji": "🤴🏼", + "unicodeVersion": "9.0", "digest": "c373fd3e0c1798415e3d8d88fab6c98c1bbdedcbe6f52f3a3899f6e2124a768d" }, - { - "name": "prince_tone3", - "unicode": "1F934-1F3FD", + "prince_tone3": { + "category": "people", + "moji": "🤴🏽", + "unicodeVersion": "9.0", "digest": "71d15695ca954d55aa69d3c753c7d31a8ba5329713a8ddbc90dafc11e524c4ef" }, - { - "name": "prince_tone4", - "unicode": "1F934-1F3FE", + "prince_tone4": { + "category": "people", + "moji": "🤴🏾", + "unicodeVersion": "9.0", "digest": "08f6cb32424f15cc3aaf83c31a5dac7c01a6be2f37ea8f13aed579ce6fb4db19" }, - { - "name": "prince_tone5", - "unicode": "1F934-1F3FF", + "prince_tone5": { + "category": "people", + "moji": "🤴🏿", + "unicodeVersion": "9.0", "digest": "77d521148efa33fa4d3409693d050fecfd948411e807327484f174e289834649" }, - { - "name": "princess", - "unicode": "1F478", + "princess": { + "category": "people", + "moji": "👸", + "unicodeVersion": "6.0", "digest": "efabd28480a843c735f0868734da2f9ce28133933b02ab07b645498f494f3f80" }, - { - "name": "princess_tone1", - "unicode": "1F478-1F3FB", + "princess_tone1": { + "category": "people", + "moji": "👸🏻", + "unicodeVersion": "8.0", "digest": "52b88b99ba64f82e8f36e2a1827c85145e4fcd6863478c2345fe9fa9e8901cdf" }, - { - "name": "princess_tone2", - "unicode": "1F478-1F3FC", + "princess_tone2": { + "category": "people", + "moji": "👸🏼", + "unicodeVersion": "8.0", "digest": "7e44289404693668f20e681fcdc2e516512d54a69c627eedae958f69dfe6eea9" }, - { - "name": "princess_tone3", - "unicode": "1F478-1F3FD", + "princess_tone3": { + "category": "people", + "moji": "👸🏽", + "unicodeVersion": "8.0", "digest": "96c9a9857348d7a1a8be899c50d55b352b9a9fd5c65e4777bfa199fe7929d41c" }, - { - "name": "princess_tone4", - "unicode": "1F478-1F3FE", + "princess_tone4": { + "category": "people", + "moji": "👸🏾", + "unicodeVersion": "8.0", "digest": "67696f96be60f2a36598072172d2db197d007e6c1ac3acef526a5ce6d59bf3f7" }, - { - "name": "princess_tone5", - "unicode": "1F478-1F3FF", + "princess_tone5": { + "category": "people", + "moji": "👸🏿", + "unicodeVersion": "8.0", "digest": "007f624e2fad91bb57ce32ecd35213a796d71807f3b12f3f1575bf50e6a50eeb" }, - { - "name": "printer", - "unicode": "1F5A8", + "printer": { + "category": "objects", + "moji": "🖨", + "unicodeVersion": "7.0", "digest": "5e5307e3dc7ec4e16c9978fb00934c99c4adefca7d32732a244d1f2de71ce6f8" }, - { - "name": "projector", - "unicode": "1F4FD", + "projector": { + "category": "objects", + "moji": "📽", + "unicodeVersion": "7.0", "digest": "7f8e1fdb89584849a56ee34c62cab808af48b7bd4823467d090af4657a2e0420" }, - { - "name": "film_projector", - "unicode": "1F4FD", - "digest": "7f8e1fdb89584849a56ee34c62cab808af48b7bd4823467d090af4657a2e0420" - }, - { - "name": "punch", - "unicode": "1F44A", + "punch": { + "category": "people", + "moji": "👊", + "unicodeVersion": "6.0", "digest": "c7e7edf6d64f755db3f02874354f08337b3971aff329476d19ac946e0b421329" }, - { - "name": "punch_tone1", - "unicode": "1F44A-1F3FB", + "punch_tone1": { + "category": "people", + "moji": "👊🏻", + "unicodeVersion": "8.0", "digest": "c9ba508b0c36041047473782acfedab5af40dd7946b33daf4d8d54c726e06a11" }, - { - "name": "punch_tone2", - "unicode": "1F44A-1F3FC", + "punch_tone2": { + "category": "people", + "moji": "👊🏼", + "unicodeVersion": "8.0", "digest": "d53011cd2f3334c7b3fffdfe1e2b8cc1c832c74306e1ac6d03f954a1309d7d0b" }, - { - "name": "punch_tone3", - "unicode": "1F44A-1F3FD", + "punch_tone3": { + "category": "people", + "moji": "👊🏽", + "unicodeVersion": "8.0", "digest": "f7522347094e0130ed8e304678106574dbd7dd2b6b3aeb4d8a7a0fef880920b2" }, - { - "name": "punch_tone4", - "unicode": "1F44A-1F3FE", + "punch_tone4": { + "category": "people", + "moji": "👊🏾", + "unicodeVersion": "8.0", "digest": "3e62bdd426f3e6ff175ce3b8dd6f6d3998d9c1506128defa96b528b455295b47" }, - { - "name": "punch_tone5", - "unicode": "1F44A-1F3FF", + "punch_tone5": { + "category": "people", + "moji": "👊🏿", + "unicodeVersion": "8.0", "digest": "7d9bff777dc4ec41ac132b1252fa08cf92a398c8dc146c4a5327b45d568982d8" }, - { - "name": "purple_heart", - "unicode": "1F49C", + "purple_heart": { + "category": "symbols", + "moji": "💜", + "unicodeVersion": "6.0", "digest": "a6bf01de806525942be480e45a4b2879f91df8129b78a1b8734d4f917bcab773" }, - { - "name": "purse", - "unicode": "1F45B", + "purse": { + "category": "people", + "moji": "👛", + "unicodeVersion": "6.0", "digest": "2b785f36e01875d66cfda2192c8c53606e7224a7c869a4826b62cb61613d60c8" }, - { - "name": "pushpin", - "unicode": "1F4CC", + "pushpin": { + "category": "objects", + "moji": "📌", + "unicodeVersion": "6.0", "digest": "c3f7d7008be6bab8dc02284d4d759abf7aafbb3dbbe3a53f0f5b2ff685af88f8" }, - { - "name": "put_litter_in_its_place", - "unicode": "1F6AE", + "put_litter_in_its_place": { + "category": "symbols", + "moji": "🚮", + "unicodeVersion": "6.0", "digest": "f52a57d6f1bada7b6e6b9a6458597d70cb701c01e1120d8cb1d7ff65e01d405c" }, - { - "name": "question", - "unicode": "2753", + "question": { + "category": "symbols", + "moji": "❓", + "unicodeVersion": "6.0", "digest": "40050a1fd29bed321fd601d13dc33de5d6084121f1d873b29bde9dc3d823a310" }, - { - "name": "rabbit", - "unicode": "1F430", + "rabbit": { + "category": "nature", + "moji": "🐰", + "unicodeVersion": "6.0", "digest": "678ad953a7ab8f618c59051449a67c965d1f04f42dd6f6669adaf3fadebd080c" }, - { - "name": "rabbit2", - "unicode": "1F407", + "rabbit2": { + "category": "nature", + "moji": "🐇", + "unicodeVersion": "6.0", "digest": "19b1f5108292472434cc7a49efac4ea9275779735c7aeb0f15c36021d5998ca0" }, - { - "name": "race_car", - "unicode": "1F3CE", - "digest": "46f4814259d3d17ff35c04110e73e5327aee99f4711cd459ca1ee951508da3a6" - }, - { - "name": "racing_car", - "unicode": "1F3CE", + "race_car": { + "category": "travel", + "moji": "🏎", + "unicodeVersion": "7.0", "digest": "46f4814259d3d17ff35c04110e73e5327aee99f4711cd459ca1ee951508da3a6" }, - { - "name": "racehorse", - "unicode": "1F40E", + "racehorse": { + "category": "nature", + "moji": "🐎", + "unicodeVersion": "6.0", "digest": "a57b7aca35347ada8225eeee06b70cfd040484104963b4df56ea8fec690576b0" }, - { - "name": "radio", - "unicode": "1F4FB", + "radio": { + "category": "objects", + "moji": "📻", + "unicodeVersion": "6.0", "digest": "9245951dd779cdd141089891b15a90d3999a6358acf1fc296aa505100f812108" }, - { - "name": "radio_button", - "unicode": "1F518", + "radio_button": { + "category": "symbols", + "moji": "🔘", + "unicodeVersion": "6.0", "digest": "565bec59198df2592e96564c6e314d3cde33c47b453db1bec6c5d027b5cb4fd9" }, - { - "name": "radioactive", - "unicode": "2622", - "digest": "0ed6634057824e0cfd10b2533753e3632b0624341a7eac8d9835706480335581" - }, - { - "name": "radioactive_sign", - "unicode": "2622", + "radioactive": { + "category": "symbols", + "moji": "☢", + "unicodeVersion": "1.1", "digest": "0ed6634057824e0cfd10b2533753e3632b0624341a7eac8d9835706480335581" }, - { - "name": "rage", - "unicode": "1F621", + "rage": { + "category": "people", + "moji": "😡", + "unicodeVersion": "6.0", "digest": "d97ba6bd08eec46dbc7199f530c945b73a87a878e35397b0a3e4f2b45039e89e" }, - { - "name": "railway_car", - "unicode": "1F683", + "railway_car": { + "category": "travel", + "moji": "🚃", + "unicodeVersion": "6.0", "digest": "2cddc08d555e7fc24e312c3d255ed013fbf9cd2974a6918369c32554049ba2be" }, - { - "name": "railway_track", - "unicode": "1F6E4", - "digest": "0da351b6d4e75c6beeaef1225e151d9580d4b5c41dfa1cf192715bf3cec981d7" - }, - { - "name": "railroad_track", - "unicode": "1F6E4", + "railway_track": { + "category": "travel", + "moji": "🛤", + "unicodeVersion": "7.0", "digest": "0da351b6d4e75c6beeaef1225e151d9580d4b5c41dfa1cf192715bf3cec981d7" }, - { - "name": "rainbow", - "unicode": "1F308", + "rainbow": { + "category": "travel", + "moji": "🌈", + "unicodeVersion": "6.0", "digest": "a93aceb54e965f35e397e8c8716b1831614933308d026012d5464ee42783ed4d" }, - { - "name": "raised_back_of_hand", - "unicode": "1F91A", + "raised_back_of_hand": { + "category": "people", + "moji": "🤚", + "unicodeVersion": "9.0", "digest": "20973a697e826625deba5ee3c4f25eb5e1737f2e860ac6fe4ee4d0e0c84b5e12" }, - { - "name": "back_of_hand", - "unicode": "1F91A", - "digest": "20973a697e826625deba5ee3c4f25eb5e1737f2e860ac6fe4ee4d0e0c84b5e12" - }, - { - "name": "raised_back_of_hand_tone1", - "unicode": "1F91A-1F3FB", - "digest": "06af5941255ca69d10d99d0a512bbda6141a296453835dbccf259ce0afe1dd3d" - }, - { - "name": "back_of_hand_tone1", - "unicode": "1F91A-1F3FB", + "raised_back_of_hand_tone1": { + "category": "people", + "moji": "🤚🏻", + "unicodeVersion": "9.0", "digest": "06af5941255ca69d10d99d0a512bbda6141a296453835dbccf259ce0afe1dd3d" }, - { - "name": "raised_back_of_hand_tone2", - "unicode": "1F91A-1F3FC", - "digest": "429ed19555c9e5197b729b3e7bd8013346551051cb0b3fbc8a4372717c9a027d" - }, - { - "name": "back_of_hand_tone2", - "unicode": "1F91A-1F3FC", + "raised_back_of_hand_tone2": { + "category": "people", + "moji": "🤚🏼", + "unicodeVersion": "9.0", "digest": "429ed19555c9e5197b729b3e7bd8013346551051cb0b3fbc8a4372717c9a027d" }, - { - "name": "raised_back_of_hand_tone3", - "unicode": "1F91A-1F3FD", - "digest": "487a1c3f19e77c99b520ec073de2acc4a9e585b739a84b3989f7de85d2c2045c" - }, - { - "name": "back_of_hand_tone3", - "unicode": "1F91A-1F3FD", + "raised_back_of_hand_tone3": { + "category": "people", + "moji": "🤚🏽", + "unicodeVersion": "9.0", "digest": "487a1c3f19e77c99b520ec073de2acc4a9e585b739a84b3989f7de85d2c2045c" }, - { - "name": "raised_back_of_hand_tone4", - "unicode": "1F91A-1F3FE", - "digest": "154254d8500c55ec3de698be4a352f9bcf06e2950cabc4eabaccad0f39a1e1e9" - }, - { - "name": "back_of_hand_tone4", - "unicode": "1F91A-1F3FE", + "raised_back_of_hand_tone4": { + "category": "people", + "moji": "🤚🏾", + "unicodeVersion": "9.0", "digest": "154254d8500c55ec3de698be4a352f9bcf06e2950cabc4eabaccad0f39a1e1e9" }, - { - "name": "raised_back_of_hand_tone5", - "unicode": "1F91A-1F3FF", - "digest": "6e9c0855ecd5f14adca5e5862427c3d39ffcf86f7ddd3aaa1fefc3cefc7483c8" - }, - { - "name": "back_of_hand_tone5", - "unicode": "1F91A-1F3FF", + "raised_back_of_hand_tone5": { + "category": "people", + "moji": "🤚🏿", + "unicodeVersion": "9.0", "digest": "6e9c0855ecd5f14adca5e5862427c3d39ffcf86f7ddd3aaa1fefc3cefc7483c8" }, - { - "name": "raised_hand", - "unicode": "270B", + "raised_hand": { + "category": "people", + "moji": "✋", + "unicodeVersion": "6.0", "digest": "5cf11be683aea985d5ba51fbd44722c2327311bfe26b61c3d441c90f5d5a195a" }, - { - "name": "raised_hand_tone1", - "unicode": "270B-1F3FB", + "raised_hand_tone1": { + "category": "people", + "moji": "✋🏻", + "unicodeVersion": "8.0", "digest": "865afca29b57577fed8fe8c2be57b74254a008c8cf34194680be2759239b5f5d" }, - { - "name": "raised_hand_tone2", - "unicode": "270B-1F3FC", + "raised_hand_tone2": { + "category": "people", + "moji": "✋🏼", + "unicodeVersion": "8.0", "digest": "832169a0b626a682a58a3b998f68413657b4962c1fab05f1fdc2668e82727210" }, - { - "name": "raised_hand_tone3", - "unicode": "270B-1F3FD", + "raised_hand_tone3": { + "category": "people", + "moji": "✋🏽", + "unicodeVersion": "8.0", "digest": "3959a873ad7671de82c615c4ed840b011e67baafb2bab7dd16859608d3e83cb1" }, - { - "name": "raised_hand_tone4", - "unicode": "270B-1F3FE", + "raised_hand_tone4": { + "category": "people", + "moji": "✋🏾", + "unicodeVersion": "8.0", "digest": "db542f65d076ccf3dbfca27cb7c2f135a8bf7a487a81a04873e70172bdfcd579" }, - { - "name": "raised_hand_tone5", - "unicode": "270B-1F3FF", + "raised_hand_tone5": { + "category": "people", + "moji": "✋🏿", + "unicodeVersion": "8.0", "digest": "88ca884d14baaae48df21d75c22d82fb15bdc395e42026f5ca34cd65e5ae8674" }, - { - "name": "raised_hands", - "unicode": "1F64C", + "raised_hands": { + "category": "people", + "moji": "🙌", + "unicodeVersion": "6.0", "digest": "2ee73466a3f5079e542857fe6f5497e9f87753a81854985ce3356a8d3da1d8b8" }, - { - "name": "raised_hands_tone1", - "unicode": "1F64C-1F3FB", + "raised_hands_tone1": { + "category": "people", + "moji": "🙌🏻", + "unicodeVersion": "8.0", "digest": "43e73c60f040a66374b8ec98f3629a90d13ae9f472446ed7676cd5573e824f4b" }, - { - "name": "raised_hands_tone2", - "unicode": "1F64C-1F3FC", + "raised_hands_tone2": { + "category": "people", + "moji": "🙌🏼", + "unicodeVersion": "8.0", "digest": "fcc5255bb2b06dc82d6878e74cf34e8ce118c70004a06d39a980683772b98c52" }, - { - "name": "raised_hands_tone3", - "unicode": "1F64C-1F3FD", + "raised_hands_tone3": { + "category": "people", + "moji": "🙌🏽", + "unicodeVersion": "8.0", "digest": "3ee3e0aafef486e766a166935e8147fb75a7329cfebc96dec876cc45e83a8754" }, - { - "name": "raised_hands_tone4", - "unicode": "1F64C-1F3FE", + "raised_hands_tone4": { + "category": "people", + "moji": "🙌🏾", + "unicodeVersion": "8.0", "digest": "78a8cbf6b2b85be4d6b18f0ff6a77f197963117955725fb7e57e0441effb928f" }, - { - "name": "raised_hands_tone5", - "unicode": "1F64C-1F3FF", + "raised_hands_tone5": { + "category": "people", + "moji": "🙌🏿", + "unicodeVersion": "8.0", "digest": "2a5ed7334a17172db0cd820a559e7f75df40ec44de6c25d194c76e1b58c634cb" }, - { - "name": "raising_hand", - "unicode": "1F64B", + "raising_hand": { + "category": "people", + "moji": "🙋", + "unicodeVersion": "6.0", "digest": "512750b00704f1ccefd3c757743540b785ad7670dbbe4a2c4dca8d93e6701920" }, - { - "name": "raising_hand_tone1", - "unicode": "1F64B-1F3FB", + "raising_hand_tone1": { + "category": "people", + "moji": "🙋🏻", + "unicodeVersion": "8.0", "digest": "2897722f091c273dd3714cff7423c2475bc3070416c28014ca03322b9ece48bc" }, - { - "name": "raising_hand_tone2", - "unicode": "1F64B-1F3FC", + "raising_hand_tone2": { + "category": "people", + "moji": "🙋🏼", + "unicodeVersion": "8.0", "digest": "59199b334b3845911382c1f29bd7c0d5ef9d2486417345e265b166ead7d3e1c1" }, - { - "name": "raising_hand_tone3", - "unicode": "1F64B-1F3FD", + "raising_hand_tone3": { + "category": "people", + "moji": "🙋🏽", + "unicodeVersion": "8.0", "digest": "f95b338d5efcf14ef12f415a2c1bba93df48628ddc94f34f70c31e1b3c2e1d28" }, - { - "name": "raising_hand_tone4", - "unicode": "1F64B-1F3FE", + "raising_hand_tone4": { + "category": "people", + "moji": "🙋🏾", + "unicodeVersion": "8.0", "digest": "951ddbfdb57d5a60551b59b3d0f7ca00a64912f4a101a73afaebd68445cd6cec" }, - { - "name": "raising_hand_tone5", - "unicode": "1F64B-1F3FF", + "raising_hand_tone5": { + "category": "people", + "moji": "🙋🏿", + "unicodeVersion": "8.0", "digest": "9370f93704d8f89ca6dc946715eab5e7dba82bf04dd68c00f5c0abb8bc16371e" }, - { - "name": "ram", - "unicode": "1F40F", + "ram": { + "category": "nature", + "moji": "🐏", + "unicodeVersion": "6.0", "digest": "2875ab28e1018b39062aeb0c5ce488c48a98f13e9f2364470a0a700b126604f2" }, - { - "name": "ramen", - "unicode": "1F35C", + "ramen": { + "category": "food", + "moji": "🍜", + "unicodeVersion": "6.0", "digest": "425662a49c4c13577c0de8d45d004e5ba204aaadbaabae62a5c283ecd7a9a2c5" }, - { - "name": "rat", - "unicode": "1F400", + "rat": { + "category": "nature", + "moji": "🐀", + "unicodeVersion": "6.0", "digest": "14380d65498c6ce037c02a93bca2b24f25a368d85278d6015b8c9f7cd261f8e2" }, - { - "name": "record_button", - "unicode": "23FA", + "record_button": { + "category": "symbols", + "moji": "⏺", + "unicodeVersion": "7.0", "digest": "92be12161ba206bb2e06a39131711c7b17368d55b4aae0b48f0ac5b6b1cde76b" }, - { - "name": "recycle", - "unicode": "267B", + "recycle": { + "category": "symbols", + "moji": "♻", + "unicodeVersion": "3.2", "digest": "c377e8537367b05b5de9be860a0fcabd7aed2bf4ba146eefc423671a21530369" }, - { - "name": "red_car", - "unicode": "1F697", + "red_car": { + "category": "travel", + "moji": "🚗", + "unicodeVersion": "6.0", "digest": "8a99832a195263c0e922af53d52dea37aa3e07032b3c2a1977f8527b4a144b9c" }, - { - "name": "red_circle", - "unicode": "1F534", + "red_circle": { + "category": "symbols", + "moji": "🔴", + "unicodeVersion": "6.0", "digest": "9dcf0132f6f2cc81702f0e3b15b37984e8439796705bf98f68ba449b3dfa5307" }, - { - "name": "registered", - "unicode": "00AE", + "registered": { + "category": "symbols", + "moji": "®", + "unicodeVersion": "1.1", "digest": "9661b1df529ecb752d130820c55c403e5de263748eb02f7fea327818bc282d94" }, - { - "name": "relaxed", - "unicode": "263A", + "relaxed": { + "category": "people", + "moji": "☺", + "unicodeVersion": "1.1", "digest": "2d5aed4fb8504c6d6660ef8d3bfe0cc053dcd6099c2f53748c202dc970c639bc" }, - { - "name": "relieved", - "unicode": "1F60C", + "relieved": { + "category": "people", + "moji": "😌", + "unicodeVersion": "6.0", "digest": "b4ce2ba6c220d887fe5e333c05ed773df9b6df0ac456879fd8f5103ff68604a5" }, - { - "name": "reminder_ribbon", - "unicode": "1F397", + "reminder_ribbon": { + "category": "activity", + "moji": "🎗", + "unicodeVersion": "7.0", "digest": "c3de2a7c9350b77a0b86c0dcce9dcd9953ea8a97aa1e7aed149755924742f54d" }, - { - "name": "repeat", - "unicode": "1F501", + "repeat": { + "category": "symbols", + "moji": "🔁", + "unicodeVersion": "6.0", "digest": "b9512d508613ed0eb3181eb1030f7f6fd6b994476ecdfa308733c6df975fb99e" }, - { - "name": "repeat_one", - "unicode": "1F502", + "repeat_one": { + "category": "symbols", + "moji": "🔂", + "unicodeVersion": "6.0", "digest": "53409cf24dd4bb0d7b50ae359f15d06b87b7f4a292ed5c3a09652fa421a90bf2" }, - { - "name": "restroom", - "unicode": "1F6BB", + "restroom": { + "category": "symbols", + "moji": "🚻", + "unicodeVersion": "6.0", "digest": "2e7a1bfc9a9d49b0272230a91db7369e24d54bf1de8e683d36b85f1d8c037f77" }, - { - "name": "revolving_hearts", - "unicode": "1F49E", + "revolving_hearts": { + "category": "symbols", + "moji": "💞", + "unicodeVersion": "6.0", "digest": "c43d3197cb4cf06659f643638f6c4e91a2889e0f6531b7d81ea826c2a8b784fc" }, - { - "name": "rewind", - "unicode": "23EA", + "rewind": { + "category": "symbols", + "moji": "⏪", + "unicodeVersion": "6.0", "digest": "d20c918c1e528ff0947312738501ca9a6fb6ff4016aad07db7a8125d00fd65cd" }, - { - "name": "rhino", - "unicode": "1F98F", - "digest": "163fa3acd78eead72c431a1f48b8465a6d45272a9169560e456d30b4df93dc6b" - }, - { - "name": "rhinoceros", - "unicode": "1F98F", + "rhino": { + "category": "nature", + "moji": "🦏", + "unicodeVersion": "9.0", "digest": "163fa3acd78eead72c431a1f48b8465a6d45272a9169560e456d30b4df93dc6b" }, - { - "name": "ribbon", - "unicode": "1F380", + "ribbon": { + "category": "objects", + "moji": "🎀", + "unicodeVersion": "6.0", "digest": "74315fe907f9f0203afe139cd4552aa442eecfa2a64fac12db3e1292fc5a8828" }, - { - "name": "rice", - "unicode": "1F35A", + "rice": { + "category": "food", + "moji": "🍚", + "unicodeVersion": "6.0", "digest": "f544f12606de59d28739798003f14ebd8869856add8e24496ec5dda3e131daf4" }, - { - "name": "rice_ball", - "unicode": "1F359", + "rice_ball": { + "category": "food", + "moji": "🍙", + "unicodeVersion": "6.0", "digest": "2cba6f5364cd366859bc8948897b65fc97b225ea7973d9be3b24aba388fed8e8" }, - { - "name": "rice_cracker", - "unicode": "1F358", + "rice_cracker": { + "category": "food", + "moji": "🍘", + "unicodeVersion": "6.0", "digest": "ac0f805d41d4f322154c1968bd3ce3e9aabcd39d908182e52fd7d28458dbef92" }, - { - "name": "rice_scene", - "unicode": "1F391", + "rice_scene": { + "category": "travel", + "moji": "🎑", + "unicodeVersion": "6.0", "digest": "b942a06d3da0570aca59bab0af57cd8c16863934f12a38f70339fd0a36f675f5" }, - { - "name": "right_facing_fist", - "unicode": "1F91C", - "digest": "f815d1cc0c0345ddcc8886ae9c133582d7dc779732ac9b93dde1ab4fdd3b251d" - }, - { - "name": "right_fist", - "unicode": "1F91C", + "right_facing_fist": { + "category": "people", + "moji": "🤜", + "unicodeVersion": "9.0", "digest": "f815d1cc0c0345ddcc8886ae9c133582d7dc779732ac9b93dde1ab4fdd3b251d" }, - { - "name": "right_facing_fist_tone1", - "unicode": "1F91C-1F3FB", - "digest": "0f9269b70cf68071d97389e059a2bdacffd73f2afd2ce6cfd7447bb1a4e9abbb" - }, - { - "name": "right_fist_tone1", - "unicode": "1F91C-1F3FB", + "right_facing_fist_tone1": { + "category": "people", + "moji": "🤜🏻", + "unicodeVersion": "9.0", "digest": "0f9269b70cf68071d97389e059a2bdacffd73f2afd2ce6cfd7447bb1a4e9abbb" }, - { - "name": "right_facing_fist_tone2", - "unicode": "1F91C-1F3FC", - "digest": "32a9833db853972e49e65aa227fb0512c57362da190aa1cc40e1d64f238e837e" - }, - { - "name": "right_fist_tone2", - "unicode": "1F91C-1F3FC", + "right_facing_fist_tone2": { + "category": "people", + "moji": "🤜🏼", + "unicodeVersion": "9.0", "digest": "32a9833db853972e49e65aa227fb0512c57362da190aa1cc40e1d64f238e837e" }, - { - "name": "right_facing_fist_tone3", - "unicode": "1F91C-1F3FD", - "digest": "be4706f8bb088411f5cbbf9065a0ae5b773c97456bd975c2b6789765657847b9" - }, - { - "name": "right_fist_tone3", - "unicode": "1F91C-1F3FD", + "right_facing_fist_tone3": { + "category": "people", + "moji": "🤜🏽", + "unicodeVersion": "9.0", "digest": "be4706f8bb088411f5cbbf9065a0ae5b773c97456bd975c2b6789765657847b9" }, - { - "name": "right_facing_fist_tone4", - "unicode": "1F91C-1F3FE", + "right_facing_fist_tone4": { + "category": "people", + "moji": "🤜🏾", + "unicodeVersion": "9.0", "digest": "1680862891a9d85c4b6f76232a80e2ef7428bcec93087c86eae2efaba9c6a3f7" }, - { - "name": "right_fist_tone4", - "unicode": "1F91C-1F3FE", - "digest": "1680862891a9d85c4b6f76232a80e2ef7428bcec93087c86eae2efaba9c6a3f7" - }, - { - "name": "right_facing_fist_tone5", - "unicode": "1F91C-1F3FF", - "digest": "388715a4bc2178c52bbb3bc2729f57be50acab5d751784c9f3220e86c6b1fbcc" - }, - { - "name": "right_fist_tone5", - "unicode": "1F91C-1F3FF", + "right_facing_fist_tone5": { + "category": "people", + "moji": "🤜🏿", + "unicodeVersion": "9.0", "digest": "388715a4bc2178c52bbb3bc2729f57be50acab5d751784c9f3220e86c6b1fbcc" }, - { - "name": "ring", - "unicode": "1F48D", + "ring": { + "category": "people", + "moji": "💍", + "unicodeVersion": "6.0", "digest": "b5322907222797b5e1786209cda88513e76cd397a40f0a7da24847245c95ef9d" }, - { - "name": "robot", - "unicode": "1F916", + "robot": { + "category": "people", + "moji": "🤖", + "unicodeVersion": "8.0", "digest": "4d788e6ec89279588b036fca6b17f5a981291681df8f90306ecf5c039de40848" }, - { - "name": "robot_face", - "unicode": "1F916", - "digest": "4d788e6ec89279588b036fca6b17f5a981291681df8f90306ecf5c039de40848" - }, - { - "name": "rocket", - "unicode": "1F680", + "rocket": { + "category": "travel", + "moji": "🚀", + "unicodeVersion": "6.0", "digest": "b82e68a95aa89a6de344d6e256fef86a848ebc91de560b043b3e1f7fd072d57d" }, - { - "name": "rofl", - "unicode": "1F923", - "digest": "f4f99ba2ac67b97338f904f9384ff03fb832a2e427bf6e74611bf5fee45f1f48" - }, - { - "name": "rolling_on_the_floor_laughing", - "unicode": "1F923", + "rofl": { + "category": "people", + "moji": "🤣", + "unicodeVersion": "9.0", "digest": "f4f99ba2ac67b97338f904f9384ff03fb832a2e427bf6e74611bf5fee45f1f48" }, - { - "name": "roller_coaster", - "unicode": "1F3A2", + "roller_coaster": { + "category": "travel", + "moji": "🎢", + "unicodeVersion": "6.0", "digest": "a65e9ace1d7900499777af1225995f17af90a398bb414764c20b6e09a8c23a2c" }, - { - "name": "rolling_eyes", - "unicode": "1F644", + "rolling_eyes": { + "category": "people", + "moji": "🙄", + "unicodeVersion": "8.0", "digest": "23dea8100da488a05721a4e82823eb438393b0ea762211c9ecef011d127aa1b7" }, - { - "name": "face_with_rolling_eyes", - "unicode": "1F644", - "digest": "23dea8100da488a05721a4e82823eb438393b0ea762211c9ecef011d127aa1b7" - }, - { - "name": "rooster", - "unicode": "1F413", + "rooster": { + "category": "nature", + "moji": "🐓", + "unicodeVersion": "6.0", "digest": "2b90c5cf6fa46da13eb77285443d600afcea0c48bd1d215d60167e7dc510da5d" }, - { - "name": "rose", - "unicode": "1F339", + "rose": { + "category": "nature", + "moji": "🌹", + "unicodeVersion": "6.0", "digest": "73799e459dba188de4de704605d824242feeb65d587c5bf9109acf528d037146" }, - { - "name": "rosette", - "unicode": "1F3F5", + "rosette": { + "category": "activity", + "moji": "🏵", + "unicodeVersion": "7.0", "digest": "2537def4deef422d4e669b28b1a0675259306ab38601019df3ec3482b14e52d5" }, - { - "name": "rotating_light", - "unicode": "1F6A8", + "rotating_light": { + "category": "travel", + "moji": "🚨", + "unicodeVersion": "6.0", "digest": "91fcdb85a752ae904d335a978c7e7936aed4c75d414b35219b5a74430e51555f" }, - { - "name": "round_pushpin", - "unicode": "1F4CD", + "round_pushpin": { + "category": "objects", + "moji": "📍", + "unicodeVersion": "6.0", "digest": "8ffca77bbdc6f1f726daf3abd6eff338a5ad1aa9b09dbbd8782c1e7ef5452f30" }, - { - "name": "rowboat", - "unicode": "1F6A3", + "rowboat": { + "category": "activity", + "moji": "🚣", + "unicodeVersion": "6.0", "digest": "83715d83a061926d4ad3bb569b21f5d337e3ebd4c9bcdfe493e661c12adc0a16" }, - { - "name": "rowboat_tone1", - "unicode": "1F6A3-1F3FB", + "rowboat_tone1": { + "category": "activity", + "moji": "🚣🏻", + "unicodeVersion": "8.0", "digest": "e279ac816442c0876fba1f42c700b80f2fb6de671e1a8a9e9d11b71eed5c58e8" }, - { - "name": "rowboat_tone2", - "unicode": "1F6A3-1F3FC", + "rowboat_tone2": { + "category": "activity", + "moji": "🚣🏼", + "unicodeVersion": "8.0", "digest": "6a48eba352ed4971d26498b6c622e5772389c89c5205ed02acde8e995dddcc3b" }, - { - "name": "rowboat_tone3", - "unicode": "1F6A3-1F3FD", + "rowboat_tone3": { + "category": "activity", + "moji": "🚣🏽", + "unicodeVersion": "8.0", "digest": "875948f6d8354ebd95ce9a66fde30f06a8366dcd89d5ca3e660845f8801e9305" }, - { - "name": "rowboat_tone4", - "unicode": "1F6A3-1F3FE", + "rowboat_tone4": { + "category": "activity", + "moji": "🚣🏾", + "unicodeVersion": "8.0", "digest": "8c7ac7346b0020d0ff5e2f4a1efb1b7785eac637f17556663ec33e2335083f0a" }, - { - "name": "rowboat_tone5", - "unicode": "1F6A3-1F3FF", + "rowboat_tone5": { + "category": "activity", + "moji": "🚣🏿", + "unicodeVersion": "8.0", "digest": "a399dbb647892b22323e0bf17bc36a9b5f1708ebedf9ba525233ee7b9d48339a" }, - { - "name": "rugby_football", - "unicode": "1F3C9", + "rugby_football": { + "category": "activity", + "moji": "🏉", + "unicodeVersion": "6.0", "digest": "cc6f00ade3e0bbb7899e7bfb138b57216dd66de26d7967d5ffa501f382ed09f4" }, - { - "name": "runner", - "unicode": "1F3C3", + "runner": { + "category": "people", + "moji": "🏃", + "unicodeVersion": "6.0", "digest": "e9af7b591be60ade2049dbada0f062ba2d3e17f02bec76cbd34ce68854a2a10c" }, - { - "name": "runner_tone1", - "unicode": "1F3C3-1F3FB", + "runner_tone1": { + "category": "people", + "moji": "🏃🏻", + "unicodeVersion": "8.0", "digest": "21091cbb09c558712ecf63548bf28b7995df42bdb85235088799a517800e52f5" }, - { - "name": "runner_tone2", - "unicode": "1F3C3-1F3FC", + "runner_tone2": { + "category": "people", + "moji": "🏃🏼", + "unicodeVersion": "8.0", "digest": "1fe3d194f675a46fe67799394192e66c407dd81163363692c5e7da32ddb9af2b" }, - { - "name": "runner_tone3", - "unicode": "1F3C3-1F3FD", + "runner_tone3": { + "category": "people", + "moji": "🏃🏽", + "unicodeVersion": "8.0", "digest": "8cea1bf4ef3be71f42dc5bae978d5b7a197a3851543225349ef0dda29a370537" }, - { - "name": "runner_tone4", - "unicode": "1F3C3-1F3FE", + "runner_tone4": { + "category": "people", + "moji": "🏃🏾", + "unicodeVersion": "8.0", "digest": "c33f0b8b5a71d295fb6ba322e79446964a8eca9e4573efd591e4273808b088a0" }, - { - "name": "runner_tone5", - "unicode": "1F3C3-1F3FF", + "runner_tone5": { + "category": "people", + "moji": "🏃🏿", + "unicodeVersion": "8.0", "digest": "9f59e6dd0fdf2f17bceb41f5c355b4e6f3c8bb8cbd8af0992f0b5630ff8892e8" }, - { - "name": "running_shirt_with_sash", - "unicode": "1F3BD", + "running_shirt_with_sash": { + "category": "activity", + "moji": "🎽", + "unicodeVersion": "6.0", "digest": "7542307d3595aca45e8ccae66b6e58b6e92870144b738263d5379ec6dc992b76" }, - { - "name": "sa", - "unicode": "1F202", + "sa": { + "category": "symbols", + "moji": "🈂", + "unicodeVersion": "6.0", "digest": "6042bcabd1516ef3847d695aba22851c49421244432d256e24eba04e8a223dab" }, - { - "name": "sagittarius", - "unicode": "2650", + "sagittarius": { + "category": "symbols", + "moji": "♐", + "unicodeVersion": "1.1", "digest": "a02593e025023f2e82a01c587a8c0bbb1eff88cbcabf535a1558413eb32ed1d5" }, - { - "name": "sailboat", - "unicode": "26F5", + "sailboat": { + "category": "travel", + "moji": "⛵", + "unicodeVersion": "5.2", "digest": "c95ef4dc939cbdcb757ef3cd90331310e8c0a426add8cc800bae2540148a3195" }, - { - "name": "sake", - "unicode": "1F376", + "sake": { + "category": "food", + "moji": "🍶", + "unicodeVersion": "6.0", "digest": "0a786075f3d9da48ae91afccf6ae0d097888da9509d354ee1d3cb99afcc88fe4" }, - { - "name": "salad", - "unicode": "1F957", - "digest": "fe321487ab847abe670e68a83f1d9e096129741c689c769ee7de4a65aeac29f8" - }, - { - "name": "green_salad", - "unicode": "1F957", + "salad": { + "category": "food", + "moji": "🥗", + "unicodeVersion": "9.0", "digest": "fe321487ab847abe670e68a83f1d9e096129741c689c769ee7de4a65aeac29f8" }, - { - "name": "sandal", - "unicode": "1F461", + "sandal": { + "category": "people", + "moji": "👡", + "unicodeVersion": "6.0", "digest": "03c3077cb4bd900934f9bdf921165b465e5cc9a6bee53e45a091411bceb8892d" }, - { - "name": "santa", - "unicode": "1F385", + "santa": { + "category": "people", + "moji": "🎅", + "unicodeVersion": "6.0", "digest": "178513e3d815917e59958870f5885b3414b43a16b8056980c863a468dfe00179" }, - { - "name": "santa_tone1", - "unicode": "1F385-1F3FB", + "santa_tone1": { + "category": "people", + "moji": "🎅🏻", + "unicodeVersion": "8.0", "digest": "bf900bbc19bbd329229add9326e28e8197b69d6ddceb69f42162b0200fde5d16" }, - { - "name": "santa_tone2", - "unicode": "1F385-1F3FC", + "santa_tone2": { + "category": "people", + "moji": "🎅🏼", + "unicodeVersion": "8.0", "digest": "7340f2171adab97198e3eecac8b0d84c4c2a41f84606301a0d10e9fe655c93d1" }, - { - "name": "santa_tone3", - "unicode": "1F385-1F3FD", + "santa_tone3": { + "category": "people", + "moji": "🎅🏽", + "unicodeVersion": "8.0", "digest": "7368ab75454ec28d8f7d6baef6ad69b5278445a9f50753f6624731bffde32054" }, - { - "name": "santa_tone4", - "unicode": "1F385-1F3FE", + "santa_tone4": { + "category": "people", + "moji": "🎅🏾", + "unicodeVersion": "8.0", "digest": "0ee60188353e0ee7772079c192bebbc6d49e74e63906f840c66da4eb35f4f245" }, - { - "name": "santa_tone5", - "unicode": "1F385-1F3FF", + "santa_tone5": { + "category": "people", + "moji": "🎅🏿", + "unicodeVersion": "8.0", "digest": "e4378a0cc5d21e9b9fe6e35c32d1ebc6fb8c2e1c09554cd096aeaefd3a6eb511" }, - { - "name": "satellite", - "unicode": "1F4E1", + "satellite": { + "category": "objects", + "moji": "📡", + "unicodeVersion": "6.0", "digest": "c9d63118dcb445856917bb080460ab695cc78e715dcbba30ba18dffa9e906b27" }, - { - "name": "satellite_orbital", - "unicode": "1F6F0", + "satellite_orbital": { + "category": "travel", + "moji": "🛰", + "unicodeVersion": "7.0", "digest": "beb2f50e7f2b010e76bed9daa95d7329a93c783d3ebc4f0b797dd721c5e3d32d" }, - { - "name": "saxophone", - "unicode": "1F3B7", + "saxophone": { + "category": "activity", + "moji": "🎷", + "unicodeVersion": "6.0", "digest": "dfd138634f6702a3b89b5a2a50016720eef3f800b0d1d8c9fe097808c9491e96" }, - { - "name": "scales", - "unicode": "2696", + "scales": { + "category": "objects", + "moji": "⚖", + "unicodeVersion": "4.1", "digest": "2280c026f16c6b92e0daa00bc14e718770f8d231c571ab439bde84d837cf31cc" }, - { - "name": "school", - "unicode": "1F3EB", + "school": { + "category": "travel", + "moji": "🏫", + "unicodeVersion": "6.0", "digest": "af198b068a86ccad3daec4c6873e6b4735086c1ecbb3848182e70bae9aa3ee24" }, - { - "name": "school_satchel", - "unicode": "1F392", + "school_satchel": { + "category": "people", + "moji": "🎒", + "unicodeVersion": "6.0", "digest": "f670ae8aea67eb9d8aaa0bf2748c1cc3e503dcc1dbe999133afcdf21af046b24" }, - { - "name": "scissors", - "unicode": "2702", + "scissors": { + "category": "objects", + "moji": "✂", + "unicodeVersion": "1.1", "digest": "95225be28f05d8b5a6b6e6bf58d973f61f183ad4fef55a558dc1b810796b85c8" }, - { - "name": "scooter", - "unicode": "1F6F4", + "scooter": { + "category": "travel", + "moji": "🛴", + "unicodeVersion": "9.0", "digest": "4a7db148880398db75e059711cb53edefb6b8fa9d442009f52856b887ab1dde4" }, - { - "name": "scorpion", - "unicode": "1F982", + "scorpion": { + "category": "nature", + "moji": "🦂", + "unicodeVersion": "8.0", "digest": "d41119d1ea5daf727c17dbea7dadec1718c72fc9f98ae88252161df5fde0938a" }, - { - "name": "scorpius", - "unicode": "264F", + "scorpius": { + "category": "symbols", + "moji": "♏", + "unicodeVersion": "1.1", "digest": "a36404b408814c2ecb8fa8b61f5c5432dfcf54cae8c09cc67b8d0fadf7cbdc03" }, - { - "name": "scream", - "unicode": "1F631", + "scream": { + "category": "people", + "moji": "😱", + "unicodeVersion": "6.0", "digest": "916e4903a4b694da4b00f190f872a4e100e7736b7a2e6171fa1636f46bf646e6" }, - { - "name": "scream_cat", - "unicode": "1F640", + "scream_cat": { + "category": "people", + "moji": "🙀", + "unicodeVersion": "6.0", "digest": "f1d3a6ff538064e7d5e0321bbc33aba44e8da703dc1894ef1403c0cd6d63d781" }, - { - "name": "scroll", - "unicode": "1F4DC", + "scroll": { + "category": "objects", + "moji": "📜", + "unicodeVersion": "6.0", "digest": "9b2cb00860bcc2d20017cafb2ed9681b6232dc07273d489d75d53ce29e4ba3ab" }, - { - "name": "seat", - "unicode": "1F4BA", + "seat": { + "category": "travel", + "moji": "💺", + "unicodeVersion": "6.0", "digest": "ae68d86fc2a07cae332451b23bd1ceba3f6526a6c56d8c1089777fa4632850e1" }, - { - "name": "second_place", - "unicode": "1F948", + "second_place": { + "category": "activity", + "moji": "🥈", + "unicodeVersion": "9.0", "digest": "9e2336fc16e532829b55380252f94655b58817d47c909fc2570002c5b06b9c40" }, - { - "name": "second_place_medal", - "unicode": "1F948", - "digest": "9e2336fc16e532829b55380252f94655b58817d47c909fc2570002c5b06b9c40" - }, - { - "name": "secret", - "unicode": "3299", + "secret": { + "category": "symbols", + "moji": "㊙", + "unicodeVersion": "1.1", "digest": "1d0b9adde2657f41421b135962de20820cf4b4eb0204044f9859522ab9d211b0" }, - { - "name": "see_no_evil", - "unicode": "1F648", + "see_no_evil": { + "category": "nature", + "moji": "🙈", + "unicodeVersion": "6.0", "digest": "3ff66d2e84b36d071d0a34f8e41cfd620a56b83131474ea50ed7803b635551ed" }, - { - "name": "seedling", - "unicode": "1F331", + "seedling": { + "category": "nature", + "moji": "🌱", + "unicodeVersion": "6.0", "digest": "c0ec5e6d20e1afdc4e78eeddb1301c8b708ad6278e7287a4e4e825417c858e75" }, - { - "name": "selfie", - "unicode": "1F933", + "selfie": { + "category": "people", + "moji": "🤳", + "unicodeVersion": "9.0", "digest": "2a1bc9f18ad4d6fb893d91c88ef1b2d9bd063dc2bb1a4b08c248c30f52545d4e" }, - { - "name": "selfie_tone1", - "unicode": "1F933-1F3FB", + "selfie_tone1": { + "category": "people", + "moji": "🤳🏻", + "unicodeVersion": "9.0", "digest": "26dc212ffed30c276bd6a66a72bc4513e68098a2205fb4ca5b51ccfa1de5b544" }, - { - "name": "selfie_tone2", - "unicode": "1F933-1F3FC", + "selfie_tone2": { + "category": "people", + "moji": "🤳🏼", + "unicodeVersion": "9.0", "digest": "71eceaefda46e3521f374f76693e7fa8f215067498067900080e2925ca94d7de" }, - { - "name": "selfie_tone3", - "unicode": "1F933-1F3FD", + "selfie_tone3": { + "category": "people", + "moji": "🤳🏽", + "unicodeVersion": "9.0", "digest": "53eabbd4f6b8ebbd2f7af7bf5cd64309c4039ac1c5b2180290a547deaafcebdf" }, - { - "name": "selfie_tone4", - "unicode": "1F933-1F3FE", + "selfie_tone4": { + "category": "people", + "moji": "🤳🏾", + "unicodeVersion": "9.0", "digest": "0baad378b09652b99c5d458db2e03b4db14a1557db4ea0969806a0ca1d33d40c" }, - { - "name": "selfie_tone5", - "unicode": "1F933-1F3FF", + "selfie_tone5": { + "category": "people", + "moji": "🤳🏿", + "unicodeVersion": "9.0", "digest": "9a07608f34ec4dad48764a855f83f3965709d7b2fd2342e6dc9ed61f23f4adfd" }, - { - "name": "seven", - "unicode": "0037-20E3", + "seven": { + "category": "symbols", + "moji": "7️⃣", + "unicodeVersion": "3.0", "digest": "ae85172d2c76c44afb4e3b45d277d400abb2dc895244b9abfbd1dac1cd7c53c2" }, - { - "name": "shallow_pan_of_food", - "unicode": "1F958", + "shallow_pan_of_food": { + "category": "food", + "moji": "🥘", + "unicodeVersion": "9.0", "digest": "7c7ad9d5d3f7226427d310b5853e8257fad899febe58dcbc5adb4677964f5c6d" }, - { - "name": "paella", - "unicode": "1F958", - "digest": "7c7ad9d5d3f7226427d310b5853e8257fad899febe58dcbc5adb4677964f5c6d" - }, - { - "name": "shamrock", - "unicode": "2618", + "shamrock": { + "category": "nature", + "moji": "☘", + "unicodeVersion": "4.1", "digest": "68ed70c26e04a818439a1742d2da6bc169edd02db86b6e6f8014b651f3235488" }, - { - "name": "shark", - "unicode": "1F988", + "shark": { + "category": "nature", + "moji": "🦈", + "unicodeVersion": "9.0", "digest": "23a2364b6356e7bbb84c138e9cf58e2c68cd8caabb337a0c4d365ce87bf5d2da" }, - { - "name": "shaved_ice", - "unicode": "1F367", + "shaved_ice": { + "category": "food", + "moji": "🍧", + "unicodeVersion": "6.0", "digest": "54048e77268b7548d03088517bf8558d11324db901ca57f9bec93f1873663a74" }, - { - "name": "sheep", - "unicode": "1F411", + "sheep": { + "category": "nature", + "moji": "🐑", + "unicodeVersion": "6.0", "digest": "c867c8e6e51768f1f51f4fe5abd3fbd5c1d69b01a3cb48b5fb94b6e2338a271c" }, - { - "name": "shell", - "unicode": "1F41A", + "shell": { + "category": "nature", + "moji": "🐚", + "unicodeVersion": "6.0", "digest": "8983652d33ad6ab91195518cecb5a268a1c0ae603d271f0ddd756ff50058ddb3" }, - { - "name": "shield", - "unicode": "1F6E1", + "shield": { + "category": "objects", + "moji": "🛡", + "unicodeVersion": "7.0", "digest": "763d0a56a62c51c730ccb0fbea38ab597cbf41a85ab968198e6ec35630d50aa5" }, - { - "name": "shinto_shrine", - "unicode": "26E9", + "shinto_shrine": { + "category": "travel", + "moji": "⛩", + "unicodeVersion": "5.2", "digest": "38a6d756c5aa9703510afa5076d75192f7814bbb6632394d4b8253d9ceda7f8c" }, - { - "name": "ship", - "unicode": "1F6A2", + "ship": { + "category": "travel", + "moji": "🚢", + "unicodeVersion": "6.0", "digest": "79c680845892a3e81ec6af2160ee07c29147155943e5daba6c76d04252014c20" }, - { - "name": "shirt", - "unicode": "1F455", + "shirt": { + "category": "people", + "moji": "👕", + "unicodeVersion": "6.0", "digest": "46c7253e15d7cac03699ddb1550fbb7565bbe487310f7e218c0583aa69f9d3c5" }, - { - "name": "shopping_bags", - "unicode": "1F6CD", + "shopping_bags": { + "category": "objects", + "moji": "🛍", + "unicodeVersion": "7.0", "digest": "95a3f03c675207bb1354270d02a630c204455c47b3edca23c48523a40cf3ea3b" }, - { - "name": "shopping_cart", - "unicode": "1F6D2", + "shopping_cart": { + "category": "objects", + "moji": "🛒", + "unicodeVersion": "9.0", "digest": "4599b63f6861cdb4d8272cac84435c24c1d4d6a73c66d51e04a1cd14a1d333e6" }, - { - "name": "shopping_trolley", - "unicode": "1F6D2", - "digest": "4599b63f6861cdb4d8272cac84435c24c1d4d6a73c66d51e04a1cd14a1d333e6" - }, - { - "name": "shower", - "unicode": "1F6BF", + "shower": { + "category": "objects", + "moji": "🚿", + "unicodeVersion": "6.0", "digest": "6b3c767c0eb472d4861c6c3cc2735a5e2c09681872ef42a11dc89f3c80b9da01" }, - { - "name": "shrimp", - "unicode": "1F990", + "shrimp": { + "category": "nature", + "moji": "🦐", + "unicodeVersion": "9.0", "digest": "b3651f3be3767125076a013fe903854f5b456a8afae865cb219cf528e0f44caa" }, - { - "name": "shrug", - "unicode": "1F937", + "shrug": { + "category": "people", + "moji": "🤷", + "unicodeVersion": "9.0", "digest": "6e264243cc3b6e396069dea4357a958bdcd4081cb1af0ed6aa47235bef88cf27" }, - { - "name": "shrug_tone1", - "unicode": "1F937-1F3FB", + "shrug_tone1": { + "category": "people", + "moji": "🤷🏻", + "unicodeVersion": "9.0", "digest": "0567b9fd95c8a857914003a5465a500ca79c8111811d45b865021b1b1d92d0b1" }, - { - "name": "shrug_tone2", - "unicode": "1F937-1F3FC", + "shrug_tone2": { + "category": "people", + "moji": "🤷🏼", + "unicodeVersion": "9.0", "digest": "1557c2f5e3d4599c806d74c0b78afcca940678787534b6862bb89a20601bac8a" }, - { - "name": "shrug_tone3", - "unicode": "1F937-1F3FD", + "shrug_tone3": { + "category": "people", + "moji": "🤷🏽", + "unicodeVersion": "9.0", "digest": "f02754541a7bf74ba7eebe6c27daf1e3e1dac25172c35b8ba45641e278dfda3d" }, - { - "name": "shrug_tone4", - "unicode": "1F937-1F3FE", + "shrug_tone4": { + "category": "people", + "moji": "🤷🏾", + "unicodeVersion": "9.0", "digest": "2b5121164cb5f4e253d8fb31f6445cf8afaf30dba41732edc511440cdb78d15c" }, - { - "name": "shrug_tone5", - "unicode": "1F937-1F3FF", + "shrug_tone5": { + "category": "people", + "moji": "🤷🏿", + "unicodeVersion": "9.0", "digest": "62d99a26bbad479f574f66208c41b9960cd41fb9d79d3a13fbdaa44682077115" }, - { - "name": "signal_strength", - "unicode": "1F4F6", + "signal_strength": { + "category": "symbols", + "moji": "📶", + "unicodeVersion": "6.0", "digest": "2c6f04ba4ecd2d2d423e19eb52cfbfd253f4db6e0908d91c1af4ea6192597447" }, - { - "name": "six", - "unicode": "0036-20E3", + "six": { + "category": "symbols", + "moji": "6️⃣", + "unicodeVersion": "3.0", "digest": "cede9324261208d0fd5d00fcdfc0df0331944bd9cff4f40b30a582a641526c1c" }, - { - "name": "six_pointed_star", - "unicode": "1F52F", + "six_pointed_star": { + "category": "symbols", + "moji": "🔯", + "unicodeVersion": "6.0", "digest": "9203e3b4f08af439ae0bfb6a7b29a02dceb027b6c2dc5463b524dfd314cbff4e" }, - { - "name": "ski", - "unicode": "1F3BF", + "ski": { + "category": "activity", + "moji": "🎿", + "unicodeVersion": "6.0", "digest": "80f0ca8660ba373fef823af9e98e148c4ddb1e217eb6d0a0ea2bae2288b57570" }, - { - "name": "skier", - "unicode": "26F7", + "skier": { + "category": "activity", + "moji": "⛷", + "unicodeVersion": "5.2", "digest": "4fff0aa155367f551a59aed9657b8afa159173882b25db9cd8434293d1eed76d" }, - { - "name": "skull", - "unicode": "1F480", - "digest": "cdd2031164281bf2b0083df4479651d96bc16d11e44bac4deaf402a9c0d6f40a" - }, - { - "name": "skeleton", - "unicode": "1F480", + "skull": { + "category": "people", + "moji": "💀", + "unicodeVersion": "6.0", "digest": "cdd2031164281bf2b0083df4479651d96bc16d11e44bac4deaf402a9c0d6f40a" }, - { - "name": "skull_crossbones", - "unicode": "2620", + "skull_crossbones": { + "category": "objects", + "moji": "☠", + "unicodeVersion": "1.1", "digest": "ae764ba21a1fcc4409f4cc9e75a261d70b87548f64158dbd3451374ad5724123" }, - { - "name": "skull_and_crossbones", - "unicode": "2620", - "digest": "ae764ba21a1fcc4409f4cc9e75a261d70b87548f64158dbd3451374ad5724123" - }, - { - "name": "sleeping", - "unicode": "1F634", + "sleeping": { + "category": "people", + "moji": "😴", + "unicodeVersion": "6.1", "digest": "1050a011509b56735c9f30a6fccc876256e2a4546dc6052e518151c8aca4b526" }, - { - "name": "sleeping_accommodation", - "unicode": "1F6CC", + "sleeping_accommodation": { + "category": "objects", + "moji": "🛌", + "unicodeVersion": "7.0", "digest": "2ce42c027d1d0947abc403c359fd668a7bc44f5ead2582e97f3db7dd4e22e5d5" }, - { - "name": "sleepy", - "unicode": "1F62A", + "sleepy": { + "category": "people", + "moji": "😪", + "unicodeVersion": "6.0", "digest": "2ee9bb1f72ef99e0e33095ec2bbf7a58ffea0ff7d40b840f4cdba57be9de74b0" }, - { - "name": "slight_frown", - "unicode": "1F641", - "digest": "d71d564a6c2d366a8e28a78ef4e07d387a77037fe8c99aa0ea1571299dc490c9" - }, - { - "name": "slightly_frowning_face", - "unicode": "1F641", + "slight_frown": { + "category": "people", + "moji": "🙁", + "unicodeVersion": "7.0", "digest": "d71d564a6c2d366a8e28a78ef4e07d387a77037fe8c99aa0ea1571299dc490c9" }, - { - "name": "slight_smile", - "unicode": "1F642", - "digest": "10f4b66a755f5c78762a330f20d1866e4a22f3f1d495161d758d3bab8d2f36fe" - }, - { - "name": "slightly_smiling_face", - "unicode": "1F642", + "slight_smile": { + "category": "people", + "moji": "🙂", + "unicodeVersion": "7.0", "digest": "10f4b66a755f5c78762a330f20d1866e4a22f3f1d495161d758d3bab8d2f36fe" }, - { - "name": "slot_machine", - "unicode": "1F3B0", + "slot_machine": { + "category": "activity", + "moji": "🎰", + "unicodeVersion": "6.0", "digest": "914184788f8cd865cd074dca25c22acee31f5498117bd9a6e78cae67e6601652" }, - { - "name": "small_blue_diamond", - "unicode": "1F539", + "small_blue_diamond": { + "category": "symbols", + "moji": "🔹", + "unicodeVersion": "6.0", "digest": "0b56d8e6b5ddf1f49fcc76e45e5fb2ee9f99ae6ffe682c26eaea4d9b7faac36c" }, - { - "name": "small_orange_diamond", - "unicode": "1F538", + "small_orange_diamond": { + "category": "symbols", + "moji": "🔸", + "unicodeVersion": "6.0", "digest": "a2235830550e289c1608f2dcf5ede48f5c1a0eff45570699c39708c9677ab950" }, - { - "name": "small_red_triangle", - "unicode": "1F53A", + "small_red_triangle": { + "category": "symbols", + "moji": "🔺", + "unicodeVersion": "6.0", "digest": "8c2985c4e9ce42d2f3b35539b879bc36206c5ef749f39fbd1eac51bd2676e1e5" }, - { - "name": "small_red_triangle_down", - "unicode": "1F53B", + "small_red_triangle_down": { + "category": "symbols", + "moji": "🔻", + "unicodeVersion": "6.0", "digest": "46bd328df2fbf5d0597596bbf00d2d5f6e0c65bcb8f3fb325df8ba0c25e445b5" }, - { - "name": "smile", - "unicode": "1F604", + "smile": { + "category": "people", + "moji": "😄", + "unicodeVersion": "6.0", "digest": "14905c372d5bf7719bd727c9efae31a03291acec79801652a23710c6848c5d14" }, - { - "name": "smile_cat", - "unicode": "1F638", + "smile_cat": { + "category": "people", + "moji": "😸", + "unicodeVersion": "6.0", "digest": "c35b76d6df100edb4022d762f47abfeb9f5e70886960c1d25908bd5d57ccb47e" }, - { - "name": "smiley", - "unicode": "1F603", + "smiley": { + "category": "people", + "moji": "😃", + "unicodeVersion": "6.0", "digest": "a89f31eb9d814636852517a7f4eadec59195e2ac2cc9f8d124f1a1cc0f775b4a" }, - { - "name": "smiley_cat", - "unicode": "1F63A", + "smiley_cat": { + "category": "people", + "moji": "😺", + "unicodeVersion": "6.0", "digest": "3e66a113c5e3e73fb94be29084cb27986b6bdb0e78ab44785bf2a35a550e71bf" }, - { - "name": "smiling_imp", - "unicode": "1F608", + "smiling_imp": { + "category": "people", + "moji": "😈", + "unicodeVersion": "6.0", "digest": "3e02131d16525938f6facc7e097365dec7e13c8a0049a3be35fc29c80cc291b3" }, - { - "name": "smirk", - "unicode": "1F60F", + "smirk": { + "category": "people", + "moji": "😏", + "unicodeVersion": "6.0", "digest": "3c180d46f5574d6fca3bb68eb02517da60b7008843cb3e90f2f9620d0c8ee943" }, - { - "name": "smirk_cat", - "unicode": "1F63C", + "smirk_cat": { + "category": "people", + "moji": "😼", + "unicodeVersion": "6.0", "digest": "0683c7f73e1f65984e91313607d7cca21d99acd4b2e9932f00e0fffd0ce90742" }, - { - "name": "smoking", - "unicode": "1F6AC", + "smoking": { + "category": "objects", + "moji": "🚬", + "unicodeVersion": "6.0", "digest": "baa9cb444bf0fe5c74358f981b19bc9e5c0415ced7f042baf93642282476ea61" }, - { - "name": "snail", - "unicode": "1F40C", + "snail": { + "category": "nature", + "moji": "🐌", + "unicodeVersion": "6.0", "digest": "5733bf3672ae4b2b3e090fa670aeac70dcbcc04ca5b13abc8c8e53b8b3d4ff33" }, - { - "name": "snake", - "unicode": "1F40D", + "snake": { + "category": "nature", + "moji": "🐍", + "unicodeVersion": "6.0", "digest": "18da2d97c771149ef5454dd23470e900903a62ab93f9e2ce301aad5a8181d773" }, - { - "name": "sneezing_face", - "unicode": "1F927", - "digest": "c20ef571dc7e35572fe3c18b7845aefc89af083ea925c48a29de3b7387af6e17" - }, - { - "name": "sneeze", - "unicode": "1F927", + "sneezing_face": { + "category": "people", + "moji": "🤧", + "unicodeVersion": "9.0", "digest": "c20ef571dc7e35572fe3c18b7845aefc89af083ea925c48a29de3b7387af6e17" }, - { - "name": "snowboarder", - "unicode": "1F3C2", + "snowboarder": { + "category": "activity", + "moji": "🏂", + "unicodeVersion": "6.0", "digest": "c6e074139b851aa53b1ba6464d84da14b3da7412fc44c6c196a8469d76915c19" }, - { - "name": "snowflake", - "unicode": "2744", + "snowflake": { + "category": "nature", + "moji": "❄", + "unicodeVersion": "1.1", "digest": "6556c918e181df01ba849e76c43972d5310439971e5d8fc2409d112c05bf0028" }, - { - "name": "snowman", - "unicode": "26C4", + "snowman": { + "category": "nature", + "moji": "⛄", + "unicodeVersion": "5.2", "digest": "6137456b2335e88e09c1859615eb22bb636355ef438f7a3949ad2f3d54478dd3" }, - { - "name": "snowman2", - "unicode": "2603", + "snowman2": { + "category": "nature", + "moji": "☃", + "unicodeVersion": "1.1", "digest": "33ec75c22a13c81fa3c6b24a77ac1a08dc0dbe70b3716cf17b6702014d8a63fe" }, - { - "name": "sob", - "unicode": "1F62D", + "sob": { + "category": "people", + "moji": "😭", + "unicodeVersion": "6.0", "digest": "d1ed4b31861f9f9fd4e9c95a9c17530e2320a1b4cad6ececb1545ce25d65e4ce" }, - { - "name": "soccer", - "unicode": "26BD", + "soccer": { + "category": "activity", + "moji": "⚽", + "unicodeVersion": "5.2", "digest": "6a3f2e6a9a0b64c3fbf8705995792091daf386a4112dba75507a1f556f662f84" }, - { - "name": "soon", - "unicode": "1F51C", + "soon": { + "category": "symbols", + "moji": "🔜", + "unicodeVersion": "6.0", "digest": "a49d1bcfbac3e6ccc05b9a9863eff74b0eb8b4d4b22b8b0f7b2787fcba1c73cc" }, - { - "name": "sos", - "unicode": "1F198", + "sos": { + "category": "symbols", + "moji": "🆘", + "unicodeVersion": "6.0", "digest": "2fa7e0274383aeed6019eb9177e778d7aab8b88575b078b0ffeb77cd18df14b3" }, - { - "name": "sound", - "unicode": "1F509", + "sound": { + "category": "symbols", + "moji": "🔉", + "unicodeVersion": "6.0", "digest": "faaca7b315b2495cbc381468580d25f1d11362441c35bb43d8a914f2ec8202d2" }, - { - "name": "space_invader", - "unicode": "1F47E", + "space_invader": { + "category": "activity", + "moji": "👾", + "unicodeVersion": "6.0", "digest": "e75379cb5063f9a8861d762ad1886097c1697fbb61f2e4e8f531047955a4a2dd" }, - { - "name": "spades", - "unicode": "2660", + "spades": { + "category": "symbols", + "moji": "♠", + "unicodeVersion": "1.1", "digest": "2c4d20f6a4893cfc62498d3f1f8f67577f39ed09f3e6682d8cb9cd8f365d30da" }, - { - "name": "spaghetti", - "unicode": "1F35D", + "spaghetti": { + "category": "food", + "moji": "🍝", + "unicodeVersion": "6.0", "digest": "6d3451dc0faa1913539edb99261448f51735f269b61193c53dfe63466c0191e8" }, - { - "name": "sparkle", - "unicode": "2747", + "sparkle": { + "category": "symbols", + "moji": "❇", + "unicodeVersion": "1.1", "digest": "7131163cd6c2f879110c86e9f068c33cf580f7c4b619449c41851fe6083402ee" }, - { - "name": "sparkler", - "unicode": "1F387", + "sparkler": { + "category": "travel", + "moji": "🎇", + "unicodeVersion": "6.0", "digest": "88539ed8a13bd66e0c265c0913bd3ec2ddc4d95484323595713beb102221a1f6" }, - { - "name": "sparkles", - "unicode": "2728", + "sparkles": { + "category": "nature", + "moji": "✨", + "unicodeVersion": "6.0", "digest": "cf84d16b1c0a381d5a7ae79031872747c9a6887eab6e92cc4a10a4b8600ef506" }, - { - "name": "sparkling_heart", - "unicode": "1F496", + "sparkling_heart": { + "category": "symbols", + "moji": "💖", + "unicodeVersion": "6.0", "digest": "b80b1ddef83b6528b309a194f6f2faf5acab603daeb9254523efc2b941bcb6d2" }, - { - "name": "speak_no_evil", - "unicode": "1F64A", + "speak_no_evil": { + "category": "nature", + "moji": "🙊", + "unicodeVersion": "6.0", "digest": "d2d7cfb4d471928a496bdc146890adc8422a68500b68115630b24c125d18e81f" }, - { - "name": "speaker", - "unicode": "1F508", + "speaker": { + "category": "symbols", + "moji": "🔈", + "unicodeVersion": "6.0", "digest": "dbca5f7181728d2ad67ff76fd566ffbdf53e333e7eeed341f54668bd47969413" }, - { - "name": "speaking_head", - "unicode": "1F5E3", + "speaking_head": { + "category": "people", + "moji": "🗣", + "unicodeVersion": "7.0", "digest": "4be1af79b4506c00af4df64663413bcbae195dab0bc63c5011feb8f9663ed544" }, - { - "name": "speaking_head_in_silhouette", - "unicode": "1F5E3", - "digest": "4be1af79b4506c00af4df64663413bcbae195dab0bc63c5011feb8f9663ed544" - }, - { - "name": "speech_balloon", - "unicode": "1F4AC", + "speech_balloon": { + "category": "symbols", + "moji": "💬", + "unicodeVersion": "6.0", "digest": "817100d9979456e7d2f253ac22e13b7a2302dc1590566214915b003e403c53ca" }, - { - "name": "speedboat", - "unicode": "1F6A4", + "speedboat": { + "category": "travel", + "moji": "🚤", + "unicodeVersion": "6.0", "digest": "a523b2320f0b24be1e9fdbc1ff828e28d8fd9a64d51e5888ab453ef0bc9f0576" }, - { - "name": "spider", - "unicode": "1F577", + "spider": { + "category": "nature", + "moji": "🕷", + "unicodeVersion": "7.0", "digest": "8411eac0c1b80926fd93cc1d6423e00b05d04c485b79ee232da8f1714e899a37" }, - { - "name": "spider_web", - "unicode": "1F578", + "spider_web": { + "category": "nature", + "moji": "🕸", + "unicodeVersion": "7.0", "digest": "2434bdfbe56dcc4a43699dd59b638af431486b52fb1d6d685451f3b231b2be23" }, - { - "name": "spoon", - "unicode": "1F944", + "spoon": { + "category": "food", + "moji": "🥄", + "unicodeVersion": "9.0", "digest": "4fa31d59e5bffd2c45a8e01fcd5652e78a5691cbfa744e69882bc67173ddea05" }, - { - "name": "spy", - "unicode": "1F575", - "digest": "99fe3cdeff934726ee5855b0e401bf32570084aaad4eb10df837fd410ca742aa" - }, - { - "name": "sleuth_or_spy", - "unicode": "1F575", + "spy": { + "category": "people", + "moji": "🕵", + "unicodeVersion": "7.0", "digest": "99fe3cdeff934726ee5855b0e401bf32570084aaad4eb10df837fd410ca742aa" }, - { - "name": "spy_tone1", - "unicode": "1F575-1F3FB", - "digest": "1720a99064061c43c7647b6bd517efa2ee2621b355a644adfb347d62849366a2" - }, - { - "name": "sleuth_or_spy_tone1", - "unicode": "1F575-1F3FB", + "spy_tone1": { + "category": "people", + "moji": "🕵🏻", + "unicodeVersion": "8.0", "digest": "1720a99064061c43c7647b6bd517efa2ee2621b355a644adfb347d62849366a2" }, - { - "name": "spy_tone2", - "unicode": "1F575-1F3FC", + "spy_tone2": { + "category": "people", + "moji": "🕵🏼", + "unicodeVersion": "8.0", "digest": "23ff0026723f2b5a46fbfb55e24c4a4a33af2bd96808b3ea3af76aae99965d68" }, - { - "name": "sleuth_or_spy_tone2", - "unicode": "1F575-1F3FC", - "digest": "23ff0026723f2b5a46fbfb55e24c4a4a33af2bd96808b3ea3af76aae99965d68" - }, - { - "name": "spy_tone3", - "unicode": "1F575-1F3FD", - "digest": "1d0cb3d54fb61e4763a4f0642ef32094bdd40832be0d42799ce9ba69773616df" - }, - { - "name": "sleuth_or_spy_tone3", - "unicode": "1F575-1F3FD", + "spy_tone3": { + "category": "people", + "moji": "🕵🏽", + "unicodeVersion": "8.0", "digest": "1d0cb3d54fb61e4763a4f0642ef32094bdd40832be0d42799ce9ba69773616df" }, - { - "name": "spy_tone4", - "unicode": "1F575-1F3FE", + "spy_tone4": { + "category": "people", + "moji": "🕵🏾", + "unicodeVersion": "8.0", "digest": "e36a4b52df6cb954fab9d9128111f1301c6d46bdeacf51993ffb5bb354cd0ad3" }, - { - "name": "sleuth_or_spy_tone4", - "unicode": "1F575-1F3FE", - "digest": "e36a4b52df6cb954fab9d9128111f1301c6d46bdeacf51993ffb5bb354cd0ad3" - }, - { - "name": "spy_tone5", - "unicode": "1F575-1F3FF", - "digest": "ffc6fefd9a537124ebf0a9ddf387414dce1291335026064644f6cf9315591129" - }, - { - "name": "sleuth_or_spy_tone5", - "unicode": "1F575-1F3FF", + "spy_tone5": { + "category": "people", + "moji": "🕵🏿", + "unicodeVersion": "8.0", "digest": "ffc6fefd9a537124ebf0a9ddf387414dce1291335026064644f6cf9315591129" }, - { - "name": "squid", - "unicode": "1F991", + "squid": { + "category": "nature", + "moji": "🦑", + "unicodeVersion": "9.0", "digest": "65a1b318c2c506b9d26cfd8282a5cf9922109595c8d12e92c3f7481ac7c08c49" }, - { - "name": "stadium", - "unicode": "1F3DF", + "stadium": { + "category": "travel", + "moji": "🏟", + "unicodeVersion": "7.0", "digest": "73bf955e767ba1518c9c92b2ba59a2aa1ec4b018652dffd97bcd74832a33789f" }, - { - "name": "star", - "unicode": "2B50", + "star": { + "category": "nature", + "moji": "⭐", + "unicodeVersion": "5.1", "digest": "d78e5c1b78caed103e100150c10b08a9ca3ee30c243943d6fc3cc08f422122e9" }, - { - "name": "star2", - "unicode": "1F31F", + "star2": { + "category": "nature", + "moji": "🌟", + "unicodeVersion": "6.0", "digest": "f91ac4afe3f5d4a52847ae8b4a9704b591e00399aebba553d150d7e34ee939fa" }, - { - "name": "star_and_crescent", - "unicode": "262A", + "star_and_crescent": { + "category": "symbols", + "moji": "☪", + "unicodeVersion": "1.1", "digest": "1bf3d29e50034f5e7c0dccff0a3a533b74bfa9b489e357b2739a473311f1332a" }, - { - "name": "star_of_david", - "unicode": "2721", + "star_of_david": { + "category": "symbols", + "moji": "✡", + "unicodeVersion": "1.1", "digest": "28a0bd0eeac9d0835ceb8425d72c2472464e863dd09b76a0ddc1c08cf1986402" }, - { - "name": "stars", - "unicode": "1F320", + "stars": { + "category": "travel", + "moji": "🌠", + "unicodeVersion": "6.0", "digest": "837d9045316b8fb5e533457eac61241534f641eb78d8cb75f688f80fb8e8a7f0" }, - { - "name": "station", - "unicode": "1F689", + "station": { + "category": "travel", + "moji": "🚉", + "unicodeVersion": "6.0", "digest": "27a163ac0aea4ed247a121cae826eafc475977c68b0d888e9405bea14326ff56" }, - { - "name": "statue_of_liberty", - "unicode": "1F5FD", + "statue_of_liberty": { + "category": "travel", + "moji": "🗽", + "unicodeVersion": "6.0", "digest": "f5a43599ab3f24ed3a78a745e06e2ac3e33107a292386ad81c67935ee5b22493" }, - { - "name": "steam_locomotive", - "unicode": "1F682", + "steam_locomotive": { + "category": "travel", + "moji": "🚂", + "unicodeVersion": "6.0", "digest": "52ad0073f37b978faf3884fb193046f2b0614e1557bbcc9de1b020e42aff2dba" }, - { - "name": "stew", - "unicode": "1F372", + "stew": { + "category": "food", + "moji": "🍲", + "unicodeVersion": "6.0", "digest": "c16f61236db314ad8d9f2dd241ec1e15c8d64e5872cce93ec4d0996490dd39df" }, - { - "name": "stop_button", - "unicode": "23F9", + "stop_button": { + "category": "symbols", + "moji": "⏹", + "unicodeVersion": "7.0", "digest": "83f9d0da3ad845fef41b4e8336815d30e9c8f042ab2a8340894ade2f428fc98a" }, - { - "name": "stopwatch", - "unicode": "23F1", + "stopwatch": { + "category": "objects", + "moji": "⏱", + "unicodeVersion": "6.0", "digest": "9b6b9491a24d8ab4f896eb876da7973f028bd5e7c51a3767ba7e61bb6fbb2be0" }, - { - "name": "straight_ruler", - "unicode": "1F4CF", + "straight_ruler": { + "category": "objects", + "moji": "📏", + "unicodeVersion": "6.0", "digest": "cee31101767bd3f961363599924dc3790675d05a1285a8396428d2f91771c111" }, - { - "name": "strawberry", - "unicode": "1F353", + "strawberry": { + "category": "food", + "moji": "🍓", + "unicodeVersion": "6.0", "digest": "5750a15e12f21259286ddbc3a8222a385b3b97a9f368897f42dd000060343174" }, - { - "name": "stuck_out_tongue", - "unicode": "1F61B", + "stuck_out_tongue": { + "category": "people", + "moji": "😛", + "unicodeVersion": "6.1", "digest": "92dc42980a6dfdd7204fc874a762d6a0bbf0fdbfb5a7c0698fca04782e99fde6" }, - { - "name": "stuck_out_tongue_closed_eyes", - "unicode": "1F61D", + "stuck_out_tongue_closed_eyes": { + "category": "people", + "moji": "😝", + "unicodeVersion": "6.0", "digest": "434d25ac24cad7ba699eae876a25d9a99b584449cca50b124bf6aa7f20a83d51" }, - { - "name": "stuck_out_tongue_winking_eye", - "unicode": "1F61C", + "stuck_out_tongue_winking_eye": { + "category": "people", + "moji": "😜", + "unicodeVersion": "6.0", "digest": "dbacd6428a2a2933212e6a4dc0c7f302177fb23b963626ccb26f27f91737f03d" }, - { - "name": "stuffed_flatbread", - "unicode": "1F959", + "stuffed_flatbread": { + "category": "food", + "moji": "🥙", + "unicodeVersion": "9.0", "digest": "9f841f2520640d69be4f20a3199023d5811842b28556b5e1152e5ec11f0fda07" }, - { - "name": "stuffed_pita", - "unicode": "1F959", - "digest": "9f841f2520640d69be4f20a3199023d5811842b28556b5e1152e5ec11f0fda07" - }, - { - "name": "sun_with_face", - "unicode": "1F31E", + "sun_with_face": { + "category": "nature", + "moji": "🌞", + "unicodeVersion": "6.0", "digest": "7256ff5263006c64c03f1eb66e3ddb56d67d785d65dacc37aa886d0cd4be63be" }, - { - "name": "sunflower", - "unicode": "1F33B", + "sunflower": { + "category": "nature", + "moji": "🌻", + "unicodeVersion": "6.0", "digest": "27d1161f50f932a6b26c404cf2e8f7083683ed0f2382d62b7472acccaa6eb695" }, - { - "name": "sunglasses", - "unicode": "1F60E", + "sunglasses": { + "category": "people", + "moji": "😎", + "unicodeVersion": "6.0", "digest": "966684382e5c59e98319e4c0ea7c304c61c2638ad5408faa49ce2c83c4416757" }, - { - "name": "sunny", - "unicode": "2600", + "sunny": { + "category": "nature", + "moji": "☀", + "unicodeVersion": "1.1", "digest": "460fea4cbbdd1595450c1033a2ee5de7fea2e2f147822efa49f7e204812415aa" }, - { - "name": "sunrise", - "unicode": "1F305", + "sunrise": { + "category": "travel", + "moji": "🌅", + "unicodeVersion": "6.0", "digest": "7718a49636b0cdd1862ed67c7a9d6e72f471c2591ff0d912485b1be55d1ea115" }, - { - "name": "sunrise_over_mountains", - "unicode": "1F304", + "sunrise_over_mountains": { + "category": "travel", + "moji": "🌄", + "unicodeVersion": "6.0", "digest": "743d0701cdbe2a814962363813c3153d3c5e62c3e410349f56d49dbb9581f356" }, - { - "name": "surfer", - "unicode": "1F3C4", + "surfer": { + "category": "activity", + "moji": "🏄", + "unicodeVersion": "6.0", "digest": "bb440775e9213430942015c37db8de58b5a561ee971b2a0f3993fc3f1d2554d4" }, - { - "name": "surfer_tone1", - "unicode": "1F3C4-1F3FB", + "surfer_tone1": { + "category": "activity", + "moji": "🏄🏻", + "unicodeVersion": "8.0", "digest": "a4937b030aca30b68bb644f37cf63c38aebce3c00b57d1c8a0ffe596b57d2f1e" }, - { - "name": "surfer_tone2", - "unicode": "1F3C4-1F3FC", + "surfer_tone2": { + "category": "activity", + "moji": "🏄🏼", + "unicodeVersion": "8.0", "digest": "1c2a954a9c5284dedf0327d6f3c954c9fdd3953b848076d298874775ad8bf0a3" }, - { - "name": "surfer_tone3", - "unicode": "1F3C4-1F3FD", + "surfer_tone3": { + "category": "activity", + "moji": "🏄🏽", + "unicodeVersion": "8.0", "digest": "418a3408b9ab026124f067c8597b500217e56bc28d9844a29eea5eee6f604ff8" }, - { - "name": "surfer_tone4", - "unicode": "1F3C4-1F3FE", + "surfer_tone4": { + "category": "activity", + "moji": "🏄🏾", + "unicodeVersion": "8.0", "digest": "530870b9ac9f4d45ff750e264feb90b44fb93ca2852f323987b06f5f12fb5a4d" }, - { - "name": "surfer_tone5", - "unicode": "1F3C4-1F3FF", + "surfer_tone5": { + "category": "activity", + "moji": "🏄🏿", + "unicodeVersion": "8.0", "digest": "40e11b1ae652cfd085d083377f1da24160065ed1b67403c6fa4655e6e44169ec" }, - { - "name": "sushi", - "unicode": "1F363", + "sushi": { + "category": "food", + "moji": "🍣", + "unicodeVersion": "6.0", "digest": "b924c621236ca3284b349b0509ae1043f2fc2c7f6d67615716f9717ada78c992" }, - { - "name": "suspension_railway", - "unicode": "1F69F", + "suspension_railway": { + "category": "travel", + "moji": "🚟", + "unicodeVersion": "6.0", "digest": "cd3d21da79864f0c018b863e82fb0561fff3c5e3c065303cfcb89c3663d638ba" }, - { - "name": "sweat", - "unicode": "1F613", + "sweat": { + "category": "people", + "moji": "😓", + "unicodeVersion": "6.0", "digest": "1aa771479aa1ac5eeea4bafbe93ebd85a0f692f6d869034f31e25b689c2e264d" }, - { - "name": "sweat_drops", - "unicode": "1F4A6", + "sweat_drops": { + "category": "nature", + "moji": "💦", + "unicodeVersion": "6.0", "digest": "b575b85415bc9852cf6415d417ebf799167fde03c6819ebcaa24ae1b3dde8dab" }, - { - "name": "sweat_smile", - "unicode": "1F605", + "sweat_smile": { + "category": "people", + "moji": "😅", + "unicodeVersion": "6.0", "digest": "171b0d0845d46c33bedb6d3b39fb1ff366e22ba90685eedabebd91bb2b0680de" }, - { - "name": "sweet_potato", - "unicode": "1F360", + "sweet_potato": { + "category": "food", + "moji": "🍠", + "unicodeVersion": "6.0", "digest": "4b91920f0b87d42763313bc476f4c821a74e4c12dc1c92165a859dddeaaf8844" }, - { - "name": "swimmer", - "unicode": "1F3CA", + "swimmer": { + "category": "activity", + "moji": "🏊", + "unicodeVersion": "6.0", "digest": "2c4ed4a51aad99d9957ae11a219d5164db9748fc3a65002c6085a9f15adfa9e2" }, - { - "name": "swimmer_tone1", - "unicode": "1F3CA-1F3FB", + "swimmer_tone1": { + "category": "activity", + "moji": "🏊🏻", + "unicodeVersion": "8.0", "digest": "48588f129ee4af52ca2e0f4594213391978601087cd607896b2f979ca077284b" }, - { - "name": "swimmer_tone2", - "unicode": "1F3CA-1F3FC", + "swimmer_tone2": { + "category": "activity", + "moji": "🏊🏼", + "unicodeVersion": "8.0", "digest": "fff209448524bd1ef4d6decabf6c1ead94c8d3d5b1bfb5e54f20cc8e139232fc" }, - { - "name": "swimmer_tone3", - "unicode": "1F3CA-1F3FD", + "swimmer_tone3": { + "category": "activity", + "moji": "🏊🏽", + "unicodeVersion": "8.0", "digest": "2003932cb2cf4ae9a10b23338bf375a9293fb18c0ecf91bdfae73be6eebb3800" }, - { - "name": "swimmer_tone4", - "unicode": "1F3CA-1F3FE", + "swimmer_tone4": { + "category": "activity", + "moji": "🏊🏾", + "unicodeVersion": "8.0", "digest": "20b4bff9baa1c694ad98067dde834c56092f023b9664bec382c2e512232bd480" }, - { - "name": "swimmer_tone5", - "unicode": "1F3CA-1F3FF", + "swimmer_tone5": { + "category": "activity", + "moji": "🏊🏿", + "unicodeVersion": "8.0", "digest": "0ff8eb57c2be8e80a1bc6ba75b8d9ffb9bd8d3be636150c4c03399ec1886f218" }, - { - "name": "symbols", - "unicode": "1F523", + "symbols": { + "category": "symbols", + "moji": "🔣", + "unicodeVersion": "6.0", "digest": "2a2a79816c4d0751a0d73586eec5e63b410653d3c85cc968906bf1fc03d89b94" }, - { - "name": "synagogue", - "unicode": "1F54D", + "synagogue": { + "category": "travel", + "moji": "🕍", + "unicodeVersion": "8.0", "digest": "98569cdd7c61528963b67b7891dfa46025c5e810cbb22ee18ddb3bd85de2da69" }, - { - "name": "syringe", - "unicode": "1F489", + "syringe": { + "category": "objects", + "moji": "💉", + "unicodeVersion": "6.0", "digest": "e1538e645ccc571227c994b71b3d1be2c4d072d8bd9c944a42ff4a11c91a34a6" }, - { - "name": "taco", - "unicode": "1F32E", + "taco": { + "category": "food", + "moji": "🌮", + "unicodeVersion": "8.0", "digest": "e1e45aefdb7445faeae75c3831df6a3d6f2590fcdd48a20d847593c246df613b" }, - { - "name": "tada", - "unicode": "1F389", + "tada": { + "category": "objects", + "moji": "🎉", + "unicodeVersion": "6.0", "digest": "1d2e6cbb2a3244240bc70209715d2213d1efee2e370cccfbcc046c333ae2d650" }, - { - "name": "tanabata_tree", - "unicode": "1F38B", + "tanabata_tree": { + "category": "nature", + "moji": "🎋", + "unicodeVersion": "6.0", "digest": "592f2907ffc1b914390e1a106c15120ff3607e99192158b94d237975647c5540" }, - { - "name": "tangerine", - "unicode": "1F34A", + "tangerine": { + "category": "food", + "moji": "🍊", + "unicodeVersion": "6.0", "digest": "40c9ddcde1b0bcfaeb466629a87825eb8c2037835720cbee5e2fda04be3c8d0a" }, - { - "name": "taurus", - "unicode": "2649", + "taurus": { + "category": "symbols", + "moji": "♉", + "unicodeVersion": "1.1", "digest": "21cf24cb6410ab6596e2df8b3e242cc07f9dbb247eabc00c590fe184b373d068" }, - { - "name": "taxi", - "unicode": "1F695", + "taxi": { + "category": "travel", + "moji": "🚕", + "unicodeVersion": "6.0", "digest": "c546cc743831cfbf0c15452767cf2a4faf3775066797e997ae7c1fcbe4eca479" }, - { - "name": "tea", - "unicode": "1F375", + "tea": { + "category": "food", + "moji": "🍵", + "unicodeVersion": "6.0", "digest": "00e3f1e389fa58c4fcd8c53ebbf83d25872f4315845ab1984b35410ae65553d9" }, - { - "name": "telephone", - "unicode": "260E", + "telephone": { + "category": "objects", + "moji": "☎", + "unicodeVersion": "1.1", "digest": "3a53851e641f8ad938ce3597b1afca2ea63c9314ff81f62563b99937496a13d7" }, - { - "name": "telephone_receiver", - "unicode": "1F4DE", + "telephone_receiver": { + "category": "objects", + "moji": "📞", + "unicodeVersion": "6.0", "digest": "1614d67f3d8814b0d75f39d55f9149e4d28ef57b343498625e62fcfff8365046" }, - { - "name": "telescope", - "unicode": "1F52D", + "telescope": { + "category": "objects", + "moji": "🔭", + "unicodeVersion": "6.0", "digest": "4adf40387870276c4f59fb050d441023e8dac784365b6a8c0282fb519780b495" }, - { - "name": "ten", - "unicode": "1F51F", + "ten": { + "category": "symbols", + "moji": "🔟", + "unicodeVersion": "6.0", "digest": "c7c9491021740d2c17edddb856f79579b0b943d8dc85a2f48dbaac84f35b8a40" }, - { - "name": "tennis", - "unicode": "1F3BE", + "tennis": { + "category": "activity", + "moji": "🎾", + "unicodeVersion": "6.0", "digest": "dc1600b4d8dce3d26259eb0d1c6ab042566565e3c1f2c96112210f1550a716fd" }, - { - "name": "tent", - "unicode": "26FA", + "tent": { + "category": "travel", + "moji": "⛺", + "unicodeVersion": "5.2", "digest": "30d9b17ac3219d4970ddf54d7c1a288b0ae50f7f3b82ed232c0b1b19ef585662" }, - { - "name": "thermometer", - "unicode": "1F321", + "thermometer": { + "category": "objects", + "moji": "🌡", + "unicodeVersion": "7.0", "digest": "66616babbcaef256d7b652796c760e8e893cb950c073348a408fe70904f80f25" }, - { - "name": "thermometer_face", - "unicode": "1F912", - "digest": "ac2b5caddd128563711a9dcc7f690cf210f684d5e8b64b09c0431d6902437126" - }, - { - "name": "face_with_thermometer", - "unicode": "1F912", + "thermometer_face": { + "category": "people", + "moji": "🤒", + "unicodeVersion": "8.0", "digest": "ac2b5caddd128563711a9dcc7f690cf210f684d5e8b64b09c0431d6902437126" }, - { - "name": "thinking", - "unicode": "1F914", + "thinking": { + "category": "people", + "moji": "🤔", + "unicodeVersion": "8.0", "digest": "4f0b84e5ab8a650cafb166e93688f0e9b31b9ade22a91035261ac90490edb9d3" }, - { - "name": "thinking_face", - "unicode": "1F914", - "digest": "4f0b84e5ab8a650cafb166e93688f0e9b31b9ade22a91035261ac90490edb9d3" - }, - { - "name": "third_place", - "unicode": "1F949", - "digest": "27c9bcba44ad95bee30882cc0722e8b0a798206306655dd648e884447ed26808" - }, - { - "name": "third_place_medal", - "unicode": "1F949", + "third_place": { + "category": "activity", + "moji": "🥉", + "unicodeVersion": "9.0", "digest": "27c9bcba44ad95bee30882cc0722e8b0a798206306655dd648e884447ed26808" }, - { - "name": "thought_balloon", - "unicode": "1F4AD", + "thought_balloon": { + "category": "symbols", + "moji": "💭", + "unicodeVersion": "6.0", "digest": "bf59624560c333561d636aedf2c8827089e275895cf434974daaabb3d5cea46e" }, - { - "name": "three", - "unicode": "0033-20E3", + "three": { + "category": "symbols", + "moji": "3️⃣", + "unicodeVersion": "3.0", "digest": "d3f85828787799c769655c38a519cad0743ab799ab276c7606e6e6894cc442e6" }, - { - "name": "thumbsdown", - "unicode": "1F44E", + "thumbsdown": { + "category": "people", + "moji": "👎", + "unicodeVersion": "6.0", "digest": "5954334e2dae5357312b3d629f10a496c728029e02216f8c8b887f9b51561c61" }, - { - "name": "-1", - "unicode": "1F44E", - "digest": "5954334e2dae5357312b3d629f10a496c728029e02216f8c8b887f9b51561c61" - }, - { - "name": "thumbsdown_tone1", - "unicode": "1F44E-1F3FB", - "digest": "3c2853491473fd7ae2d1b5415a425cc390d26a8754446f8736c1360e4cb18ba3" - }, - { - "name": "-1_tone1", - "unicode": "1F44E-1F3FB", + "thumbsdown_tone1": { + "category": "people", + "moji": "👎🏻", + "unicodeVersion": "8.0", "digest": "3c2853491473fd7ae2d1b5415a425cc390d26a8754446f8736c1360e4cb18ba3" }, - { - "name": "thumbsdown_tone2", - "unicode": "1F44E-1F3FC", + "thumbsdown_tone2": { + "category": "people", + "moji": "👎🏼", + "unicodeVersion": "8.0", "digest": "4e0f8f86a06b69e423df8d93f41ec393f12800633acc82c4cb6dff64ca0d8507" }, - { - "name": "-1_tone2", - "unicode": "1F44E-1F3FC", - "digest": "4e0f8f86a06b69e423df8d93f41ec393f12800633acc82c4cb6dff64ca0d8507" - }, - { - "name": "thumbsdown_tone3", - "unicode": "1F44E-1F3FD", - "digest": "e08fa35575f59978612d4330bbc35313eca9c4dfa04f4212626abc700819effe" - }, - { - "name": "-1_tone3", - "unicode": "1F44E-1F3FD", + "thumbsdown_tone3": { + "category": "people", + "moji": "👎🏽", + "unicodeVersion": "8.0", "digest": "e08fa35575f59978612d4330bbc35313eca9c4dfa04f4212626abc700819effe" }, - { - "name": "thumbsdown_tone4", - "unicode": "1F44E-1F3FE", + "thumbsdown_tone4": { + "category": "people", + "moji": "👎🏾", + "unicodeVersion": "8.0", "digest": "7c6d118d20d5add8ca003e4a53e42685a1f9436b872ed10d79f67ad418fb2a44" }, - { - "name": "-1_tone4", - "unicode": "1F44E-1F3FE", - "digest": "7c6d118d20d5add8ca003e4a53e42685a1f9436b872ed10d79f67ad418fb2a44" - }, - { - "name": "thumbsdown_tone5", - "unicode": "1F44E-1F3FF", - "digest": "8697c4a4ee4d6669dc2d47aa97699c42012ca59b80818ad6845878b37b4a9c58" - }, - { - "name": "-1_tone5", - "unicode": "1F44E-1F3FF", + "thumbsdown_tone5": { + "category": "people", + "moji": "👎🏿", + "unicodeVersion": "8.0", "digest": "8697c4a4ee4d6669dc2d47aa97699c42012ca59b80818ad6845878b37b4a9c58" }, - { - "name": "thumbsup", - "unicode": "1F44D", + "thumbsup": { + "category": "people", + "moji": "👍", + "unicodeVersion": "6.0", "digest": "59ec2457ab33e8897261d01a495f6cf5c668d0004807dc541c3b1be5294b1e61" }, - { - "name": "+1", - "unicode": "1F44D", - "digest": "59ec2457ab33e8897261d01a495f6cf5c668d0004807dc541c3b1be5294b1e61" - }, - { - "name": "thumbsup_tone1", - "unicode": "1F44D-1F3FB", + "thumbsup_tone1": { + "category": "people", + "moji": "👍🏻", + "unicodeVersion": "8.0", "digest": "f57e6c525e8830779ea5026590eec3ca10869dc438a0c779734b617d04f28d21" }, - { - "name": "+1_tone1", - "unicode": "1F44D-1F3FB", - "digest": "f57e6c525e8830779ea5026590eec3ca10869dc438a0c779734b617d04f28d21" - }, - { - "name": "thumbsup_tone2", - "unicode": "1F44D-1F3FC", + "thumbsup_tone2": { + "category": "people", + "moji": "👍🏼", + "unicodeVersion": "8.0", "digest": "980eeeb1d8f5d79dae35c7ff81a576e980aa13a440d07b10e32e98ed34cbf7f1" }, - { - "name": "+1_tone2", - "unicode": "1F44D-1F3FC", - "digest": "980eeeb1d8f5d79dae35c7ff81a576e980aa13a440d07b10e32e98ed34cbf7f1" - }, - { - "name": "thumbsup_tone3", - "unicode": "1F44D-1F3FD", - "digest": "b3881060569e56e1dd75ca7960feab0e58ae51f440458781948d65d461116b4e" - }, - { - "name": "+1_tone3", - "unicode": "1F44D-1F3FD", + "thumbsup_tone3": { + "category": "people", + "moji": "👍🏽", + "unicodeVersion": "8.0", "digest": "b3881060569e56e1dd75ca7960feab0e58ae51f440458781948d65d461116b4e" }, - { - "name": "thumbsup_tone4", - "unicode": "1F44D-1F3FE", + "thumbsup_tone4": { + "category": "people", + "moji": "👍🏾", + "unicodeVersion": "8.0", "digest": "86fbe2c95414bce5e38fb5c33da31305d7942fca2c9c79168dcffdbd895e9ad6" }, - { - "name": "+1_tone4", - "unicode": "1F44D-1F3FE", - "digest": "86fbe2c95414bce5e38fb5c33da31305d7942fca2c9c79168dcffdbd895e9ad6" - }, - { - "name": "thumbsup_tone5", - "unicode": "1F44D-1F3FF", - "digest": "49fa63ff725c746a18649df16c8fab69bad88bbb564884df79d1d15f553b7343" - }, - { - "name": "+1_tone5", - "unicode": "1F44D-1F3FF", + "thumbsup_tone5": { + "category": "people", + "moji": "👍🏿", + "unicodeVersion": "8.0", "digest": "49fa63ff725c746a18649df16c8fab69bad88bbb564884df79d1d15f553b7343" }, - { - "name": "thunder_cloud_rain", - "unicode": "26C8", - "digest": "dacc20b4f6b68e5834aa1b8391afa5e83b5e6eb28e2d2174d3a68186a770506d" - }, - { - "name": "thunder_cloud_and_rain", - "unicode": "26C8", + "thunder_cloud_rain": { + "category": "nature", + "moji": "⛈", + "unicodeVersion": "5.2", "digest": "dacc20b4f6b68e5834aa1b8391afa5e83b5e6eb28e2d2174d3a68186a770506d" }, - { - "name": "ticket", - "unicode": "1F3AB", + "ticket": { + "category": "activity", + "moji": "🎫", + "unicodeVersion": "6.0", "digest": "b4326fe7761940216e6c76ee2928110a6b37bf913da9d694e96557e7c7c10420" }, - { - "name": "tickets", - "unicode": "1F39F", - "digest": "fb73358c3697c04fcfde6a1e705b1c3b47635b93b9cadfe31d5657566c7d190a" - }, - { - "name": "admission_tickets", - "unicode": "1F39F", + "tickets": { + "category": "activity", + "moji": "🎟", + "unicodeVersion": "7.0", "digest": "fb73358c3697c04fcfde6a1e705b1c3b47635b93b9cadfe31d5657566c7d190a" }, - { - "name": "tiger", - "unicode": "1F42F", + "tiger": { + "category": "nature", + "moji": "🐯", + "unicodeVersion": "6.0", "digest": "e139531e6c930bc46242dc0ed274661229de026b5419d8ea8f99fdb0f8a719ab" }, - { - "name": "tiger2", - "unicode": "1F405", + "tiger2": { + "category": "nature", + "moji": "🐅", + "unicodeVersion": "6.0", "digest": "f930cc8714198310d9b0edca6baff243ac5a3320f75fadb56fa5acc6fe34ff24" }, - { - "name": "timer", - "unicode": "23F2", + "timer": { + "category": "objects", + "moji": "⏲", + "unicodeVersion": "6.0", "digest": "69b33f219523d89d81cbbc070ad7e528711e4b34e124a50acb12a0280a34d0b0" }, - { - "name": "timer_clock", - "unicode": "23F2", - "digest": "69b33f219523d89d81cbbc070ad7e528711e4b34e124a50acb12a0280a34d0b0" - }, - { - "name": "tired_face", - "unicode": "1F62B", + "tired_face": { + "category": "people", + "moji": "😫", + "unicodeVersion": "6.0", "digest": "775739bc9324517e614878ca0960d793df97775feeb62b14dbfb311a42a21802" }, - { - "name": "tm", - "unicode": "2122", + "tm": { + "category": "symbols", + "moji": "™", + "unicodeVersion": "1.1", "digest": "7d9fafdb72d91860478fc185719f289f359eab2c368a132cb936a269e2ab6a24" }, - { - "name": "toilet", - "unicode": "1F6BD", + "toilet": { + "category": "objects", + "moji": "🚽", + "unicodeVersion": "6.0", "digest": "0d1b0dd0078f51104e8632a0726e1b3f075561a1ffa8a2546602de15798415d0" }, - { - "name": "tokyo_tower", - "unicode": "1F5FC", + "tokyo_tower": { + "category": "travel", + "moji": "🗼", + "unicodeVersion": "6.0", "digest": "73eaf6fd59d16396673afef620c6d928857d5cf616e95a40eaf2861686e0956a" }, - { - "name": "tomato", - "unicode": "1F345", + "tomato": { + "category": "food", + "moji": "🍅", + "unicodeVersion": "6.0", "digest": "d092d8ad381d542e59b6a82b4f1ef0d10fc1ed48460952375c6c5c6258cea111" }, - { - "name": "tone1", - "unicode": "1F3FB", + "tone1": { + "category": "modifier", + "moji": "🏻", + "unicodeVersion": "8.0", "digest": "5c62003a098b774c068be45d658db3c0dd38483c0871f7c8ae293bc1222c4f0c" }, - { - "name": "tone2", - "unicode": "1F3FC", + "tone2": { + "category": "modifier", + "moji": "🏼", + "unicodeVersion": "8.0", "digest": "3c636ecbc4e58c7a360f2338daaf44e7da598fd07e0ba1514bb5c0f83fc8819f" }, - { - "name": "tone3", - "unicode": "1F3FD", + "tone3": { + "category": "modifier", + "moji": "🏽", + "unicodeVersion": "8.0", "digest": "398a1e5441b64c9c2d033bbc01d7a8d90b4db30ea9f30e28f0a9120c72a48df8" }, - { - "name": "tone4", - "unicode": "1F3FE", + "tone4": { + "category": "modifier", + "moji": "🏾", + "unicodeVersion": "8.0", "digest": "ff4a12195aeb7494c785b81266efad8cd60c8022c407a0fc032a02e8b83216b3" }, - { - "name": "tone5", - "unicode": "1F3FF", + "tone5": { + "category": "modifier", + "moji": "🏿", + "unicodeVersion": "8.0", "digest": "9e9f0125b5d57011b7456c84719e6be6cf71d06c1b198081d0937c0979164a81" }, - { - "name": "tongue", - "unicode": "1F445", + "tongue": { + "category": "people", + "moji": "👅", + "unicodeVersion": "6.0", "digest": "286e9d2583c371431d6fc979dd4ab48981676da26baada51a846657a3654c19b" }, - { - "name": "tools", - "unicode": "1F6E0", - "digest": "bf08d60dedc06de73d04dab05703bb8ad81989c72b5035d1a07821e51096f158" - }, - { - "name": "hammer_and_wrench", - "unicode": "1F6E0", + "tools": { + "category": "objects", + "moji": "🛠", + "unicodeVersion": "7.0", "digest": "bf08d60dedc06de73d04dab05703bb8ad81989c72b5035d1a07821e51096f158" }, - { - "name": "top", - "unicode": "1F51D", + "top": { + "category": "symbols", + "moji": "🔝", + "unicodeVersion": "6.0", "digest": "c9a9f25b17db014e76b6be54aa07ef89bb18f8adb41b3199d180a559ff1d9ea5" }, - { - "name": "tophat", - "unicode": "1F3A9", + "tophat": { + "category": "people", + "moji": "🎩", + "unicodeVersion": "6.0", "digest": "43a45dfb5d6b57a63a0491f4e3ec780774c0301b53ed39a303a0bd803d16ed71" }, - { - "name": "track_next", - "unicode": "23ED", - "digest": "88592ef6c720a32aeb752322fb4c794bf5110a72408e21e898630452115c731c" - }, - { - "name": "next_track", - "unicode": "23ED", + "track_next": { + "category": "symbols", + "moji": "⏭", + "unicodeVersion": "6.0", "digest": "88592ef6c720a32aeb752322fb4c794bf5110a72408e21e898630452115c731c" }, - { - "name": "track_previous", - "unicode": "23EE", - "digest": "98c1b3d643768d94857fb762f6d26cfb87282b449a67792242e8b7068643ac87" - }, - { - "name": "previous_track", - "unicode": "23EE", + "track_previous": { + "category": "symbols", + "moji": "⏮", + "unicodeVersion": "6.0", "digest": "98c1b3d643768d94857fb762f6d26cfb87282b449a67792242e8b7068643ac87" }, - { - "name": "trackball", - "unicode": "1F5B2", + "trackball": { + "category": "objects", + "moji": "🖲", + "unicodeVersion": "7.0", "digest": "32a819a3129429f797ad434d0c40e263dc236808e34878c599ed2304b43702f5" }, - { - "name": "tractor", - "unicode": "1F69C", + "tractor": { + "category": "travel", + "moji": "🚜", + "unicodeVersion": "6.0", "digest": "5e4686290f1a4c9953ae208340b7d276f25b3b2197a43e52469aeb6450e93997" }, - { - "name": "traffic_light", - "unicode": "1F6A5", + "traffic_light": { + "category": "travel", + "moji": "🚥", + "unicodeVersion": "6.0", "digest": "d96aacade33d1ad3e0414f8a920513010f36eb7e5889774251c1d91148917ead" }, - { - "name": "train", - "unicode": "1F68B", + "train": { + "category": "travel", + "moji": "🚋", + "unicodeVersion": "6.0", "digest": "7423d17e131df7aadaa350b5d39dcbce3b28de331ff8b6703a3b2d0093963f4b" }, - { - "name": "train2", - "unicode": "1F686", + "train2": { + "category": "travel", + "moji": "🚆", + "unicodeVersion": "6.0", "digest": "06e65d549e771632f3c64287a38ba67236f9800ccb6a23c3b592bc010e24e122" }, - { - "name": "tram", - "unicode": "1F68A", + "tram": { + "category": "travel", + "moji": "🚊", + "unicodeVersion": "6.0", "digest": "21a7699f1a94f06dcb4d1e896448b98a4205f8efe902a8ac169a5005d11ab100" }, - { - "name": "triangular_flag_on_post", - "unicode": "1F6A9", + "triangular_flag_on_post": { + "category": "objects", + "moji": "🚩", + "unicodeVersion": "6.0", "digest": "1f5ce3828a42f5b1717bac1521d0502cf7081ad9f15e8ed292c1a65f0d1386da" }, - { - "name": "triangular_ruler", - "unicode": "1F4D0", + "triangular_ruler": { + "category": "objects", + "moji": "📐", + "unicodeVersion": "6.0", "digest": "a0367dcf663ec934f1fc7c88bfaccc02b229a896f60930a66bb02241c933e501" }, - { - "name": "trident", - "unicode": "1F531", + "trident": { + "category": "symbols", + "moji": "🔱", + "unicodeVersion": "6.0", "digest": "ee45920845d3b35c2e45b934cf30ce97bfe2f24c5d72ef1ac6e0842e52b50fc1" }, - { - "name": "triumph", - "unicode": "1F624", + "triumph": { + "category": "people", + "moji": "😤", + "unicodeVersion": "6.0", "digest": "4aa44b8e1682c1269624a359f4b0bf613553683b883d947561ab169d7f85da0f" }, - { - "name": "trolleybus", - "unicode": "1F68E", + "trolleybus": { + "category": "travel", + "moji": "🚎", + "unicodeVersion": "6.0", "digest": "f610b4fd1123f06778a8e3bb8f738d5b0079aeb0b0926b6a63268c0dd0ee03ed" }, - { - "name": "trophy", - "unicode": "1F3C6", + "trophy": { + "category": "activity", + "moji": "🏆", + "unicodeVersion": "6.0", "digest": "50cfbedac18bf0fa5dec727643e15ec47f64068944b536e97518ee3be4f08006" }, - { - "name": "tropical_drink", - "unicode": "1F379", + "tropical_drink": { + "category": "food", + "moji": "🍹", + "unicodeVersion": "6.0", "digest": "54144fce60d650f426b1edf09e47c70b2762222398c1fe40231881f074603a69" }, - { - "name": "tropical_fish", - "unicode": "1F420", + "tropical_fish": { + "category": "nature", + "moji": "🐠", + "unicodeVersion": "6.0", "digest": "fd92100aaa9328da35e6090388824921b9726b474d1432a926d2cf9c45ad6528" }, - { - "name": "truck", - "unicode": "1F69A", + "truck": { + "category": "travel", + "moji": "🚚", + "unicodeVersion": "6.0", "digest": "0d1571e58e900abc453df0ff683fe7acb5906ecbdd52ab35b7101074359faf18" }, - { - "name": "trumpet", - "unicode": "1F3BA", + "trumpet": { + "category": "activity", + "moji": "🎺", + "unicodeVersion": "6.0", "digest": "cea3614c309f5573f328f4603120dbe930016a35f0dfa400b0d968fe9fff2d55" }, - { - "name": "tulip", - "unicode": "1F337", + "tulip": { + "category": "nature", + "moji": "🌷", + "unicodeVersion": "6.0", "digest": "e744e8dbbdc6b126bd5b15aad56b524191de5a604189f4ab6d96730dfef4d086" }, - { - "name": "tumbler_glass", - "unicode": "1F943", - "digest": "7a38658274b9ff28836725a1dbfad49b8fa3af5ec8385e629db6bfdc7d93907a" - }, - { - "name": "whisky", - "unicode": "1F943", + "tumbler_glass": { + "category": "food", + "moji": "🥃", + "unicodeVersion": "9.0", "digest": "7a38658274b9ff28836725a1dbfad49b8fa3af5ec8385e629db6bfdc7d93907a" }, - { - "name": "turkey", - "unicode": "1F983", + "turkey": { + "category": "nature", + "moji": "🦃", + "unicodeVersion": "8.0", "digest": "bf5daef15716b66636a5fdb6d059420521443c0603e2d56bd7c99c791a7285f4" }, - { - "name": "turtle", - "unicode": "1F422", + "turtle": { + "category": "nature", + "moji": "🐢", + "unicodeVersion": "6.0", "digest": "588c35fb42c9502a908e9805517d4cc8c4ba4e74c9beed4035779fea1efe14f8" }, - { - "name": "tv", - "unicode": "1F4FA", + "tv": { + "category": "objects", + "moji": "📺", + "unicodeVersion": "6.0", "digest": "1279f3f3955a58dbbf74e248fc914b0bdba9c4c6b6a5176e9d12bf2750ecfeb4" }, - { - "name": "twisted_rightwards_arrows", - "unicode": "1F500", + "twisted_rightwards_arrows": { + "category": "symbols", + "moji": "🔀", + "unicodeVersion": "6.0", "digest": "fed07eebc2cf0d977ca0826bbd80defafbbcf118508444148f47b58949ebe27c" }, - { - "name": "two", - "unicode": "0032-20E3", + "two": { + "category": "symbols", + "moji": "2️⃣", + "unicodeVersion": "3.0", "digest": "b346f51f6523b02ebcbd753256804e2f9cc1574c96aa634362bf9401dac2c661" }, - { - "name": "two_hearts", - "unicode": "1F495", + "two_hearts": { + "category": "symbols", + "moji": "💕", + "unicodeVersion": "6.0", "digest": "6ded120a59aed790b441ec8fbbdea6f5cbfb4fa48e9e4b224cc29c9fde2d2e4c" }, - { - "name": "two_men_holding_hands", - "unicode": "1F46C", + "two_men_holding_hands": { + "category": "people", + "moji": "👬", + "unicodeVersion": "6.0", "digest": "bfcf9e20a67d00262cdf6e85f1acd545dda91f2e370d68bfd41ce02f232a2987" }, - { - "name": "two_women_holding_hands", - "unicode": "1F46D", + "two_women_holding_hands": { + "category": "people", + "moji": "👭", + "unicodeVersion": "6.0", "digest": "9d9d2b37a7f8e16fde1468dd8b5645003ea81ae4bf8bcf68471e2381845dd0dd" }, - { - "name": "u5272", - "unicode": "1F239", + "u5272": { + "category": "symbols", + "moji": "🈹", + "unicodeVersion": "6.0", "digest": "01e6cb8f74ea3c19fdade59c2d13d158b90dc6b4b293421b2014b7478bf20870" }, - { - "name": "u5408", - "unicode": "1F234", + "u5408": { + "category": "symbols", + "moji": "🈴", + "unicodeVersion": "6.0", "digest": "084cdbd5436670ea4dc22010e269c1ab7b0432897b8675301e69120374bcdd14" }, - { - "name": "u55b6", - "unicode": "1F23A", + "u55b6": { + "category": "symbols", + "moji": "🈺", + "unicodeVersion": "6.0", "digest": "c1017023d20d4aae78d59342dd3bfc5282716ea0601d9a8c2476335cbf7a2e12" }, - { - "name": "u6307", - "unicode": "1F22F", + "u6307": { + "category": "symbols", + "moji": "🈯", + "unicodeVersion": "5.2", "digest": "f459b092b974f459db1fb9cc13617a448b2e4f2b4dc46cc316d8c46af6e7d8bd" }, - { - "name": "u6708", - "unicode": "1F237", + "u6708": { + "category": "symbols", + "moji": "🈷", + "unicodeVersion": "6.0", "digest": "928815abf5b30f92efe5168de0c7e6cf8c17899a03e358ab42f42667e0a4a04c" }, - { - "name": "u6709", - "unicode": "1F236", + "u6709": { + "category": "symbols", + "moji": "🈶", + "unicodeVersion": "6.0", "digest": "f63a48ee06c892d24acec8b5634c021658d2ebde67a42d8faa86f27804a9f26d" }, - { - "name": "u6e80", - "unicode": "1F235", + "u6e80": { + "category": "symbols", + "moji": "🈵", + "unicodeVersion": "6.0", "digest": "489181d90a5e43068459530673a153e4af04fdad8514ec341ff7afbcfd366c3b" }, - { - "name": "u7121", - "unicode": "1F21A", + "u7121": { + "category": "symbols", + "moji": "🈚", + "unicodeVersion": "5.2", "digest": "9c50fd2ba14221affd2dcd3746322c2137dd75458493f4d385b544eb5bd8d6cd" }, - { - "name": "u7533", - "unicode": "1F238", + "u7533": { + "category": "symbols", + "moji": "🈸", + "unicodeVersion": "6.0", "digest": "2b05819b380a2ea47cc5fde8fcce3d53922fd223d6f5bd83d696d44175b69f18" }, - { - "name": "u7981", - "unicode": "1F232", + "u7981": { + "category": "symbols", + "moji": "🈲", + "unicodeVersion": "6.0", "digest": "adbe12601b22972003ddebcb0bd1532b979aa9c78bfdc147511854b5014eabc0" }, - { - "name": "u7a7a", - "unicode": "1F233", + "u7a7a": { + "category": "symbols", + "moji": "🈳", + "unicodeVersion": "6.0", "digest": "b9ee0ec7bb0b86c3eb73d4dbbb91848c427bf356ae30a263b9b44bd9bd784482" }, - { - "name": "umbrella", - "unicode": "2614", + "umbrella": { + "category": "nature", + "moji": "☔", + "unicodeVersion": "4.0", "digest": "0328a2f48b7df47905e2655460e524c0794ef12d3d7c32a049a10892d5662f77" }, - { - "name": "umbrella2", - "unicode": "2602", + "umbrella2": { + "category": "nature", + "moji": "☂", + "unicodeVersion": "1.1", "digest": "2f6a58110dc590480a822a3ffa2b5bc86f295e0c994a4a632837d25d4cf9fc58" }, - { - "name": "unamused", - "unicode": "1F612", + "unamused": { + "category": "people", + "moji": "😒", + "unicodeVersion": "6.0", "digest": "0d597088e3e7880918d0166e5c69243b18fe64afa31685c39bfdbc71494aa132" }, - { - "name": "underage", - "unicode": "1F51E", + "underage": { + "category": "symbols", + "moji": "🔞", + "unicodeVersion": "6.0", "digest": "b6b194614ca714ac2b1c2c17b75fe5922c7fdadb3d1157ba89ab2a5d03494a67" }, - { - "name": "unicorn", - "unicode": "1F984", - "digest": "f71bb485a7c208e999dd45f2b36d7b7d517898c0627947926b05aa28603804ca" - }, - { - "name": "unicorn_face", - "unicode": "1F984", + "unicorn": { + "category": "nature", + "moji": "🦄", + "unicodeVersion": "8.0", "digest": "f71bb485a7c208e999dd45f2b36d7b7d517898c0627947926b05aa28603804ca" }, - { - "name": "unlock", - "unicode": "1F513", + "unlock": { + "category": "objects", + "moji": "🔓", + "unicodeVersion": "6.0", "digest": "9554ef3a6a315938b873e77970d9b0212e61f13c6cc36e4f17f87acc930a9a53" }, - { - "name": "up", - "unicode": "1F199", + "up": { + "category": "symbols", + "moji": "🆙", + "unicodeVersion": "6.0", "digest": "ff2554ccf08c7208b38794c5fa3d9a93a46ff191a49401195d8f740846121906" }, - { - "name": "upside_down", - "unicode": "1F643", - "digest": "5129121f0a28f5b334268c28565de26a5907559568deca11de6ec620b097dfe1" - }, - { - "name": "upside_down_face", - "unicode": "1F643", + "upside_down": { + "category": "people", + "moji": "🙃", + "unicodeVersion": "8.0", "digest": "5129121f0a28f5b334268c28565de26a5907559568deca11de6ec620b097dfe1" }, - { - "name": "urn", - "unicode": "26B1", - "digest": "9bebf589eed8dd361f6a03cd1b325078f2cd0e82270ef63a7dd1b6aee08cd1e6" - }, - { - "name": "funeral_urn", - "unicode": "26B1", + "urn": { + "category": "objects", + "moji": "⚱", + "unicodeVersion": "4.1", "digest": "9bebf589eed8dd361f6a03cd1b325078f2cd0e82270ef63a7dd1b6aee08cd1e6" }, - { - "name": "v", - "unicode": "270C", + "v": { + "category": "people", + "moji": "✌", + "unicodeVersion": "1.1", "digest": "9825bf440df289a8edf8ede494e8c778dc63c95f967f4d7bbea3245cf4f558ec" }, - { - "name": "v_tone1", - "unicode": "270C-1F3FB", + "v_tone1": { + "category": "people", + "moji": "✌🏻", + "unicodeVersion": "8.0", "digest": "76e358250d9ca519b60b8d7b6a32900700d784433dcc609e9442254a410f6e37" }, - { - "name": "v_tone2", - "unicode": "270C-1F3FC", + "v_tone2": { + "category": "people", + "moji": "✌🏼", + "unicodeVersion": "8.0", "digest": "4081b674be8416136022523fa9f29ec70a0f7e3aa05ca13152606609f3fd003c" }, - { - "name": "v_tone3", - "unicode": "270C-1F3FD", + "v_tone3": { + "category": "people", + "moji": "✌🏽", + "unicodeVersion": "8.0", "digest": "b6afb3a4c78384280610b953592d378241c75597a82aa6d16c86a993f8d8f3b0" }, - { - "name": "v_tone4", - "unicode": "270C-1F3FE", + "v_tone4": { + "category": "people", + "moji": "✌🏾", + "unicodeVersion": "8.0", "digest": "7ddc3cdd0138da2c8d7f6d8257ffdb8801496043e8a2395f93b0663447ac7fce" }, - { - "name": "v_tone5", - "unicode": "270C-1F3FF", + "v_tone5": { + "category": "people", + "moji": "✌🏿", + "unicodeVersion": "8.0", "digest": "a85dc5c589f0d1cf32f8bfa5c82e5c11c40b35439636914686a2f06f7359f539" }, - { - "name": "vertical_traffic_light", - "unicode": "1F6A6", + "vertical_traffic_light": { + "category": "travel", + "moji": "🚦", + "unicodeVersion": "6.0", "digest": "8cfd49a8f96b15a8313ef855f2e234ea3fa58332e68896dea34760740de9f020" }, - { - "name": "vhs", - "unicode": "1F4FC", + "vhs": { + "category": "objects", + "moji": "📼", + "unicodeVersion": "6.0", "digest": "3fb1acaf25805cf86f8d40ee2c17cf25da587b7ca93b931167ab43fce041eee8" }, - { - "name": "vibration_mode", - "unicode": "1F4F3", + "vibration_mode": { + "category": "symbols", + "moji": "📳", + "unicodeVersion": "6.0", "digest": "c9a8899222f46fe51dd8cee3e59f77c48268f0b7cfae2bcb34a791213acb1755" }, - { - "name": "video_camera", - "unicode": "1F4F9", + "video_camera": { + "category": "objects", + "moji": "📹", + "unicodeVersion": "6.0", "digest": "62e56f26c286a7964ef1021f0f23fcb4b38cdcfb5b5af569b472340c412c619a" }, - { - "name": "video_game", - "unicode": "1F3AE", + "video_game": { + "category": "activity", + "moji": "🎮", + "unicodeVersion": "6.0", "digest": "2787e302aa9e6fd7e9dc382c9bc7f5fbf244ef4940e08a4f9e80d33324f3032e" }, - { - "name": "violin", - "unicode": "1F3BB", + "violin": { + "category": "activity", + "moji": "🎻", + "unicodeVersion": "6.0", "digest": "1e69d531ce2b5d5bf1dd9470187dbbe76f479d14428834b6a9e2bf5296dc0ec9" }, - { - "name": "virgo", - "unicode": "264D", + "virgo": { + "category": "symbols", + "moji": "♍", + "unicodeVersion": "1.1", "digest": "0f75e9c228bc467fd0cec0f93f0e087c943bc5fb1d945fb0d4de53d07718388e" }, - { - "name": "volcano", - "unicode": "1F30B", + "volcano": { + "category": "travel", + "moji": "🌋", + "unicodeVersion": "6.0", "digest": "41c92ef88ca533df342a0ebe59d2b676873bfa944c3988495b8a96060a9b8e16" }, - { - "name": "volleyball", - "unicode": "1F3D0", + "volleyball": { + "category": "activity", + "moji": "🏐", + "unicodeVersion": "8.0", "digest": "774a83357f7aee890b4d4383236f0a90946dbd7c86aaabadc5753dcc9b4c9d69" }, - { - "name": "vs", - "unicode": "1F19A", + "vs": { + "category": "symbols", + "moji": "🆚", + "unicodeVersion": "6.0", "digest": "ac943e4c737459c2e1adbac8b71d3fdaebb704dbaf5713012e7a77beb09db1ef" }, - { - "name": "vulcan", - "unicode": "1F596", - "digest": "b4d409a0b019e7b06333cefd15ea46cb54aef5132d86e8ba361c1c3b911fe265" - }, - { - "name": "raised_hand_with_part_between_middle_and_ring_fingers", - "unicode": "1F596", + "vulcan": { + "category": "people", + "moji": "🖖", + "unicodeVersion": "7.0", "digest": "b4d409a0b019e7b06333cefd15ea46cb54aef5132d86e8ba361c1c3b911fe265" }, - { - "name": "vulcan_tone1", - "unicode": "1F596-1F3FB", - "digest": "cc6072c85031b5081995f98a57f09ab177168318f69a51f3acc63251760499a4" - }, - { - "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone1", - "unicode": "1F596-1F3FB", + "vulcan_tone1": { + "category": "people", + "moji": "🖖🏻", + "unicodeVersion": "8.0", "digest": "cc6072c85031b5081995f98a57f09ab177168318f69a51f3acc63251760499a4" }, - { - "name": "vulcan_tone2", - "unicode": "1F596-1F3FC", - "digest": "858bd5a1ac91dc4d7735f57ba4dd69d39138aa6dac1c80cfc05de30a59a5bc33" - }, - { - "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone2", - "unicode": "1F596-1F3FC", + "vulcan_tone2": { + "category": "people", + "moji": "🖖🏼", + "unicodeVersion": "8.0", "digest": "858bd5a1ac91dc4d7735f57ba4dd69d39138aa6dac1c80cfc05de30a59a5bc33" }, - { - "name": "vulcan_tone3", - "unicode": "1F596-1F3FD", + "vulcan_tone3": { + "category": "people", + "moji": "🖖🏽", + "unicodeVersion": "8.0", "digest": "2f74b6f3eab2a75063591b66f1c7350af0d23153e1427af91de20c48a5f4a54a" }, - { - "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone3", - "unicode": "1F596-1F3FD", - "digest": "2f74b6f3eab2a75063591b66f1c7350af0d23153e1427af91de20c48a5f4a54a" - }, - { - "name": "vulcan_tone4", - "unicode": "1F596-1F3FE", - "digest": "87cf8b87d3610f742857a9704b658462df32b4924d8f1ddba26f761e738c4e11" - }, - { - "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone4", - "unicode": "1F596-1F3FE", + "vulcan_tone4": { + "category": "people", + "moji": "🖖🏾", + "unicodeVersion": "8.0", "digest": "87cf8b87d3610f742857a9704b658462df32b4924d8f1ddba26f761e738c4e11" }, - { - "name": "vulcan_tone5", - "unicode": "1F596-1F3FF", + "vulcan_tone5": { + "category": "people", + "moji": "🖖🏿", + "unicodeVersion": "8.0", "digest": "11e9ff62f2385edeb477dbf66c63734536531def5771daf80b66a3425ac71493" }, - { - "name": "raised_hand_with_part_between_middle_and_ring_fingers_tone5", - "unicode": "1F596-1F3FF", - "digest": "11e9ff62f2385edeb477dbf66c63734536531def5771daf80b66a3425ac71493" - }, - { - "name": "walking", - "unicode": "1F6B6", + "walking": { + "category": "people", + "moji": "🚶", + "unicodeVersion": "6.0", "digest": "ae77471fe1e8a734d11711cdb589f64347c35d6ee2fc10f6db16ac550c0557fa" }, - { - "name": "walking_tone1", - "unicode": "1F6B6-1F3FB", + "walking_tone1": { + "category": "people", + "moji": "🚶🏻", + "unicodeVersion": "8.0", "digest": "3de871c234e1340ccf95338df7babd94d175cfcb17a57b5a74d950e0a31f03b1" }, - { - "name": "walking_tone2", - "unicode": "1F6B6-1F3FC", + "walking_tone2": { + "category": "people", + "moji": "🚶🏼", + "unicodeVersion": "8.0", "digest": "620eb7bfb753a331a5822b02bdaf08d8dde7b573efd210287a3d3dfdd84a40b9" }, - { - "name": "walking_tone3", - "unicode": "1F6B6-1F3FD", + "walking_tone3": { + "category": "people", + "moji": "🚶🏽", + "unicodeVersion": "8.0", "digest": "ff39545acc2256006128f8c186433c28052b8c9aaec46fe06f25cff02c71f6b8" }, - { - "name": "walking_tone4", - "unicode": "1F6B6-1F3FE", + "walking_tone4": { + "category": "people", + "moji": "🚶🏾", + "unicodeVersion": "8.0", "digest": "a9499d142392977a9b9e54fb957952359e9bdffce7ec2f1e8320523d185fb066" }, - { - "name": "walking_tone5", - "unicode": "1F6B6-1F3FF", + "walking_tone5": { + "category": "people", + "moji": "🚶🏿", + "unicodeVersion": "8.0", "digest": "b47a4c48ce40298f842f454fc1abccae70f69725d73ee2c80e4018f4c4065d7d" }, - { - "name": "waning_crescent_moon", - "unicode": "1F318", + "waning_crescent_moon": { + "category": "nature", + "moji": "🌘", + "unicodeVersion": "6.0", "digest": "2ec7896eefcf821e0ea013556a17af59e997503662c07f080d0a84ab13ef4cf1" }, - { - "name": "waning_gibbous_moon", - "unicode": "1F316", + "waning_gibbous_moon": { + "category": "nature", + "moji": "🌖", + "unicodeVersion": "6.0", "digest": "ce2f5aca8fccdacaaf174d10da4e493e853e4608cc4d159aa3081d108a8b58d5" }, - { - "name": "warning", - "unicode": "26A0", + "warning": { + "category": "symbols", + "moji": "⚠", + "unicodeVersion": "4.0", "digest": "745f1d203958f42bf37ecb5909cd0819934e300308ba0ff20964c8c203092f90" }, - { - "name": "wastebasket", - "unicode": "1F5D1", + "wastebasket": { + "category": "objects", + "moji": "🗑", + "unicodeVersion": "7.0", "digest": "221a1b6d9975051038d9d97e18a16556cdf4254a6bca4c29bf1c51f306c79f2a" }, - { - "name": "watch", - "unicode": "231A", + "watch": { + "category": "objects", + "moji": "⌚", + "unicodeVersion": "1.1", "digest": "acc0c96751404a789b3085f10425cf34f942185215df459515d2439cde3efc6b" }, - { - "name": "water_buffalo", - "unicode": "1F403", + "water_buffalo": { + "category": "nature", + "moji": "🐃", + "unicodeVersion": "6.0", "digest": "ba6a840d4f57f8f9f3e9f29b8a030faf02a3a3d912e3e31b067616b2ac48a3d1" }, - { - "name": "water_polo", - "unicode": "1F93D", + "water_polo": { + "category": "activity", + "moji": "🤽", + "unicodeVersion": "9.0", "digest": "fc77e1d2a84a9f4cf0cf19c1ea10cf137cf0940b9103a523121eda87677ad148" }, - { - "name": "water_polo_tone1", - "unicode": "1F93D-1F3FB", + "water_polo_tone1": { + "category": "activity", + "moji": "🤽🏻", + "unicodeVersion": "9.0", "digest": "3be28384edd29ada8109f07720d601a9d5866ed63e6234efe9ee1a194ed5d0c5" }, - { - "name": "water_polo_tone2", - "unicode": "1F93D-1F3FC", + "water_polo_tone2": { + "category": "activity", + "moji": "🤽🏼", + "unicodeVersion": "9.0", "digest": "afcd3f28c6719f869ca79a6fd1ccade2ea976ade844fbc1081fc72865bcb652f" }, - { - "name": "water_polo_tone3", - "unicode": "1F93D-1F3FD", + "water_polo_tone3": { + "category": "activity", + "moji": "🤽🏽", + "unicodeVersion": "9.0", "digest": "d19481c9b82d9413e99c2652e020fd763f2b54408dedaffec8dfe80973ded407" }, - { - "name": "water_polo_tone4", - "unicode": "1F93D-1F3FE", + "water_polo_tone4": { + "category": "activity", + "moji": "🤽🏾", + "unicodeVersion": "9.0", "digest": "375972d882b627e8d525e632e58b30346fc3e01858d7d08d62a9d3bf8132bbc7" }, - { - "name": "water_polo_tone5", - "unicode": "1F93D-1F3FF", + "water_polo_tone5": { + "category": "activity", + "moji": "🤽🏿", + "unicodeVersion": "9.0", "digest": "a8e1ced1c5382a8147a1d1801a133cada9a0e52e41de6272e56c3c1f426f6048" }, - { - "name": "watermelon", - "unicode": "1F349", + "watermelon": { + "category": "food", + "moji": "🍉", + "unicodeVersion": "6.0", "digest": "42a3821d2e4dd595c93f5db7a5c70b7af486b8f0ddd3b9d26bc4e743a88e699a" }, - { - "name": "wave", - "unicode": "1F44B", + "wave": { + "category": "people", + "moji": "👋", + "unicodeVersion": "6.0", "digest": "cddbd764d471604446cbaca91f77f6c4119d1cfc2c856732ca0eaac4593cb736" }, - { - "name": "wave_tone1", - "unicode": "1F44B-1F3FB", + "wave_tone1": { + "category": "people", + "moji": "👋🏻", + "unicodeVersion": "8.0", "digest": "cf40797437ddf68ec0275f337e6aac4bed81e28da7636d56c9f817ddf8e2b30a" }, - { - "name": "wave_tone2", - "unicode": "1F44B-1F3FC", + "wave_tone2": { + "category": "people", + "moji": "👋🏼", + "unicodeVersion": "8.0", "digest": "12c8a3e82c03ee35a734c642be482ba2d9d5948dacf91ec1fda243316dd4a0d0" }, - { - "name": "wave_tone3", - "unicode": "1F44B-1F3FD", + "wave_tone3": { + "category": "people", + "moji": "👋🏽", + "unicodeVersion": "8.0", "digest": "ebcaef43e21b475f76de811d4f4d1a67d9393973b57b03876e02164345a2ba4a" }, - { - "name": "wave_tone4", - "unicode": "1F44B-1F3FE", + "wave_tone4": { + "category": "people", + "moji": "👋🏾", + "unicodeVersion": "8.0", "digest": "7df7b70cf76766836ba146c3d91b6104930c384450cf2688426e60c1c06a1fc8" }, - { - "name": "wave_tone5", - "unicode": "1F44B-1F3FF", + "wave_tone5": { + "category": "people", + "moji": "👋🏿", + "unicodeVersion": "8.0", "digest": "8dfdba6aeff5d7dfd807467d431a137547726b34d021f1a5a0b74e155d270ea7" }, - { - "name": "wavy_dash", - "unicode": "3030", + "wavy_dash": { + "category": "symbols", + "moji": "〰", + "unicodeVersion": "1.1", "digest": "7b1968474f01d12fd09a1f2572282927138d9e9d6a3642de4bf68af80a8c3738" }, - { - "name": "waxing_crescent_moon", - "unicode": "1F312", + "waxing_crescent_moon": { + "category": "nature", + "moji": "🌒", + "unicodeVersion": "6.0", "digest": "852d7e55a19074d061fa3aa80d6b1e7e87a9280bdf44d94bbdbbe6d59178b1be" }, - { - "name": "waxing_gibbous_moon", - "unicode": "1F314", + "waxing_gibbous_moon": { + "category": "nature", + "moji": "🌔", + "unicodeVersion": "6.0", "digest": "a3a1c7cc72521a3f74929789a90e1c35d81ac86e21225c9f844d718d8940e3b3" }, - { - "name": "wc", - "unicode": "1F6BE", + "wc": { + "category": "symbols", + "moji": "🚾", + "unicodeVersion": "6.0", "digest": "4b95d54e0b53e4b705277917653503b32d6a143c2eaf6c547bc8e01c2dc23659" }, - { - "name": "weary", - "unicode": "1F629", + "weary": { + "category": "people", + "moji": "😩", + "unicodeVersion": "6.0", "digest": "3528f85540996cd5b562efe5421c495fc1bb414dc797bc20062783ae1b730847" }, - { - "name": "wedding", - "unicode": "1F492", + "wedding": { + "category": "travel", + "moji": "💒", + "unicodeVersion": "6.0", "digest": "980f3522cc4c19c3096e668032ea2cd19e7900cdc4b73bbb1c9b4c4d28dc78af" }, - { - "name": "whale", - "unicode": "1F433", + "whale": { + "category": "nature", + "moji": "🐳", + "unicodeVersion": "6.0", "digest": "6368fe4bc4a7f68aa2bd5386686a5f1b159feacbec16d59515f2b6e5d01adfbd" }, - { - "name": "whale2", - "unicode": "1F40B", + "whale2": { + "category": "nature", + "moji": "🐋", + "unicodeVersion": "6.0", "digest": "ccd3edf88167965f2abc18631ffb80e2532f728da35bc0c11144376685da18e8" }, - { - "name": "wheel_of_dharma", - "unicode": "2638", + "wheel_of_dharma": { + "category": "symbols", + "moji": "☸", + "unicodeVersion": "1.1", "digest": "4a0a13fcd507b9621686c8090bf340aa8770c064e0e3eb576fbae1229000d6da" }, - { - "name": "wheelchair", - "unicode": "267F", + "wheelchair": { + "category": "symbols", + "moji": "♿", + "unicodeVersion": "4.1", "digest": "f5250f2b4b5b4ffe6a6f77d30865c3f5d7173fc91aee547869589b2a96da91c8" }, - { - "name": "white_check_mark", - "unicode": "2705", + "white_check_mark": { + "category": "symbols", + "moji": "✅", + "unicodeVersion": "6.0", "digest": "45eb17bde6e503f22c8579d6e4d507ad6557a15f9eaad14aa716ec9ba1540876" }, - { - "name": "white_circle", - "unicode": "26AA", + "white_circle": { + "category": "symbols", + "moji": "⚪", + "unicodeVersion": "4.1", "digest": "2e7323fa4d1e3929e529d49210a0b82a043eae4f7c95128ec86b98c46fdb0e7c" }, - { - "name": "white_flower", - "unicode": "1F4AE", + "white_flower": { + "category": "symbols", + "moji": "💮", + "unicodeVersion": "6.0", "digest": "ace093b310eeefdecf4a4bdaf4fbcbb568457b0191ac80778a466ac5f3f4025a" }, - { - "name": "white_large_square", - "unicode": "2B1C", + "white_large_square": { + "category": "symbols", + "moji": "⬜", + "unicodeVersion": "5.1", "digest": "0db6957ee9ff7325b534b730fc05345a63d4ed9060f0f816807d0dcf004baa3e" }, - { - "name": "white_medium_small_square", - "unicode": "25FD", + "white_medium_small_square": { + "category": "symbols", + "moji": "◽", + "unicodeVersion": "3.2", "digest": "d79689981a7b38211c60a025a81e44fd39ac6ea4062e227cae3aab8f51572cd4" }, - { - "name": "white_medium_square", - "unicode": "25FB", + "white_medium_square": { + "category": "symbols", + "moji": "◻", + "unicodeVersion": "3.2", "digest": "6c4ce26d3f69667219f29ea18b04f3e79373024426275f25936e09a683e9a4fc" }, - { - "name": "white_small_square", - "unicode": "25AB", + "white_small_square": { + "category": "symbols", + "moji": "▫", + "unicodeVersion": "1.1", "digest": "ae0d35a6bbba4592b89b2f0f1f2d183efb2f93cf2a2136c0c195aab72f0bb1c8" }, - { - "name": "white_square_button", - "unicode": "1F533", + "white_square_button": { + "category": "symbols", + "moji": "🔳", + "unicodeVersion": "6.0", "digest": "797f3d9e44e88e940ffc118e52d0f709eec2ef14b13bdf873ad4b0c96cc0b042" }, - { - "name": "white_sun_cloud", - "unicode": "1F325", - "digest": "0e714038bb0a5b091dd4ad8829c5c72dece493e09da6d56ceadcd0b68e1c0fd5" - }, - { - "name": "white_sun_behind_cloud", - "unicode": "1F325", + "white_sun_cloud": { + "category": "nature", + "moji": "🌥", + "unicodeVersion": "7.0", "digest": "0e714038bb0a5b091dd4ad8829c5c72dece493e09da6d56ceadcd0b68e1c0fd5" }, - { - "name": "white_sun_rain_cloud", - "unicode": "1F326", + "white_sun_rain_cloud": { + "category": "nature", + "moji": "🌦", + "unicodeVersion": "7.0", "digest": "82fb2a91d43c7c511afed216e12f98e32aef4475e7f3c7ccc0f39732d2f7d5e5" }, - { - "name": "white_sun_behind_cloud_with_rain", - "unicode": "1F326", - "digest": "82fb2a91d43c7c511afed216e12f98e32aef4475e7f3c7ccc0f39732d2f7d5e5" - }, - { - "name": "white_sun_small_cloud", - "unicode": "1F324", - "digest": "0a6164cdadf2413555b7ef47b95f823f5a010f36d2dacfb1a38335a0f59e9601" - }, - { - "name": "white_sun_with_small_cloud", - "unicode": "1F324", + "white_sun_small_cloud": { + "category": "nature", + "moji": "🌤", + "unicodeVersion": "7.0", "digest": "0a6164cdadf2413555b7ef47b95f823f5a010f36d2dacfb1a38335a0f59e9601" }, - { - "name": "wilted_rose", - "unicode": "1F940", + "wilted_rose": { + "category": "nature", + "moji": "🥀", + "unicodeVersion": "9.0", "digest": "2c9e01ab9a61d057c71478b09ba7d82ae08f4a5a1c2212b7ad562b74f616677f" }, - { - "name": "wilted_flower", - "unicode": "1F940", - "digest": "2c9e01ab9a61d057c71478b09ba7d82ae08f4a5a1c2212b7ad562b74f616677f" - }, - { - "name": "wind_blowing_face", - "unicode": "1F32C", + "wind_blowing_face": { + "category": "nature", + "moji": "🌬", + "unicodeVersion": "7.0", "digest": "e4f63149cbc8829118571f6a93487b96d26665fc15d17d578cca4e5c752cd54f" }, - { - "name": "wind_chime", - "unicode": "1F390", + "wind_chime": { + "category": "objects", + "moji": "🎐", + "unicodeVersion": "6.0", "digest": "1b1b212fbd74a9edc62aee7ffab9bcf91d3a9f69bffb2be4b7fd527914c14ced" }, - { - "name": "wine_glass", - "unicode": "1F377", + "wine_glass": { + "category": "food", + "moji": "🍷", + "unicodeVersion": "6.0", "digest": "d99107d6809386bc5e219aa58ee4930d27b7c3a6d2b10deb9f523df369f766d1" }, - { - "name": "wink", - "unicode": "1F609", + "wink": { + "category": "people", + "moji": "😉", + "unicodeVersion": "6.0", "digest": "56e29994a47335a901d0c98fa141d26faae8f647a860517bd3615fa980921885" }, - { - "name": "wolf", - "unicode": "1F43A", + "wolf": { + "category": "nature", + "moji": "🐺", + "unicodeVersion": "6.0", "digest": "4a983f5ec8ec0872fcde7890e17605b1229064e5e194b6fca1c4259068d1caed" }, - { - "name": "woman", - "unicode": "1F469", + "woman": { + "category": "people", + "moji": "👩", + "unicodeVersion": "6.0", "digest": "a06a22a48eeb3aeb885321358fe234e97797ed33be17f52d232ce2830cfbcd97" }, - { - "name": "woman_tone1", - "unicode": "1F469-1F3FB", + "woman_tone1": { + "category": "people", + "moji": "👩🏻", + "unicodeVersion": "8.0", "digest": "c2e4b135c1dac6a0b002569a6ccd9d098f6cb18481c68b5d9115e11241a0978d" }, - { - "name": "woman_tone2", - "unicode": "1F469-1F3FC", + "woman_tone2": { + "category": "people", + "moji": "👩🏼", + "unicodeVersion": "8.0", "digest": "4848e650051214a53c4cd9f6d3d94158f77f65ecb34f891789de34ee0a713006" }, - { - "name": "woman_tone3", - "unicode": "1F469-1F3FD", + "woman_tone3": { + "category": "people", + "moji": "👩🏽", + "unicodeVersion": "8.0", "digest": "b6f751ad47da019cdfb9d6d78f9610adb92120abf204c30df79a9150b57dbdee" }, - { - "name": "woman_tone4", - "unicode": "1F469-1F3FE", + "woman_tone4": { + "category": "people", + "moji": "👩🏾", + "unicodeVersion": "8.0", "digest": "fd27d3a669dc34313fbfe518df7dc2ded3ade5dde695f8d773afe87bf8a8b0d4" }, - { - "name": "woman_tone5", - "unicode": "1F469-1F3FF", + "woman_tone5": { + "category": "people", + "moji": "👩🏿", + "unicodeVersion": "8.0", "digest": "9ae9b14dfff40fa60a565d89479727feeba4fd6ffea9acb353a81b14aba751d4" }, - { - "name": "womans_clothes", - "unicode": "1F45A", + "womans_clothes": { + "category": "people", + "moji": "👚", + "unicodeVersion": "6.0", "digest": "d12a27810780fe5cd8118ed4587e0c4e70dbe9bcd014c6866fe6a8c9c7c55698" }, - { - "name": "womans_hat", - "unicode": "1F452", + "womans_hat": { + "category": "people", + "moji": "👒", + "unicodeVersion": "6.0", "digest": "52a0255b3483085bd125d39b74516ab6a81003964f44995c2fac821e7ff93086" }, - { - "name": "womens", - "unicode": "1F6BA", + "womens": { + "category": "symbols", + "moji": "🚺", + "unicodeVersion": "6.0", "digest": "7e38964006f8b28dfa2b3e9b2b16553bb50c18a63455f556b0bff35ee172137e" }, - { - "name": "worried", - "unicode": "1F61F", + "worried": { + "category": "people", + "moji": "😟", + "unicodeVersion": "6.1", "digest": "5a073985e1344bc34201ef94a491f7f2b946f5828c9fdbc57eeb2dcd87ac3a6b" }, - { - "name": "wrench", - "unicode": "1F527", + "wrench": { + "category": "objects", + "moji": "🔧", + "unicodeVersion": "6.0", "digest": "81aae53bc892035b905bf3ec5b442a8ecc95027c5fa9eb51b7c3e7d8fad3f3f4" }, - { - "name": "wrestlers", - "unicode": "1F93C", - "digest": "9be983f3f9438f3ab8f6b643a958371d1e710c6d78e728f3465141811f05c2d5" - }, - { - "name": "wrestling", - "unicode": "1F93C", + "wrestlers": { + "category": "activity", + "moji": "🤼", + "unicodeVersion": "9.0", "digest": "9be983f3f9438f3ab8f6b643a958371d1e710c6d78e728f3465141811f05c2d5" }, - { - "name": "wrestlers_tone1", - "unicode": "1F93C-1F3FB", + "wrestlers_tone1": { + "category": "activity", + "moji": "🤼🏻", + "unicodeVersion": "9.0", "digest": "60461f83bfc93ce59dd027eab4782b7f206a7b142719fa72f301e047dc83a5d9" }, - { - "name": "wrestling_tone1", - "unicode": "1F93C-1F3FB", - "digest": "60461f83bfc93ce59dd027eab4782b7f206a7b142719fa72f301e047dc83a5d9" - }, - { - "name": "wrestlers_tone2", - "unicode": "1F93C-1F3FC", - "digest": "67ad93c86e6c58d552c18e7a0105cc81fd9bb0474da51f788eba2e4c14b4a636" - }, - { - "name": "wrestling_tone2", - "unicode": "1F93C-1F3FC", + "wrestlers_tone2": { + "category": "activity", + "moji": "🤼🏼", + "unicodeVersion": "9.0", "digest": "67ad93c86e6c58d552c18e7a0105cc81fd9bb0474da51f788eba2e4c14b4a636" }, - { - "name": "wrestlers_tone3", - "unicode": "1F93C-1F3FD", + "wrestlers_tone3": { + "category": "activity", + "moji": "🤼🏽", + "unicodeVersion": "9.0", "digest": "6bfd06c4435cabf2def153912040e05bf8db424fa383148ddda6d0ce8a8a3349" }, - { - "name": "wrestling_tone3", - "unicode": "1F93C-1F3FD", - "digest": "6bfd06c4435cabf2def153912040e05bf8db424fa383148ddda6d0ce8a8a3349" - }, - { - "name": "wrestlers_tone4", - "unicode": "1F93C-1F3FE", - "digest": "597312678834c4d288c238482879856d5eba4620deb1eaef495f428e2ba5f2a5" - }, - { - "name": "wrestling_tone4", - "unicode": "1F93C-1F3FE", + "wrestlers_tone4": { + "category": "activity", + "moji": "🤼🏾", + "unicodeVersion": "9.0", "digest": "597312678834c4d288c238482879856d5eba4620deb1eaef495f428e2ba5f2a5" }, - { - "name": "wrestlers_tone5", - "unicode": "1F93C-1F3FF", + "wrestlers_tone5": { + "category": "activity", + "moji": "🤼🏿", + "unicodeVersion": "9.0", "digest": "d6aebdf1e44fd825b9a5b3716aefbc53f4b4dbb73cb2a628c0f2994ebfd34614" }, - { - "name": "wrestling_tone5", - "unicode": "1F93C-1F3FF", - "digest": "d6aebdf1e44fd825b9a5b3716aefbc53f4b4dbb73cb2a628c0f2994ebfd34614" - }, - { - "name": "writing_hand", - "unicode": "270D", + "writing_hand": { + "category": "people", + "moji": "✍", + "unicodeVersion": "1.1", "digest": "110517ae4da5587e8b0662881658e27da4120bfacec54734fd6657831d4d782f" }, - { - "name": "writing_hand_tone1", - "unicode": "270D-1F3FB", + "writing_hand_tone1": { + "category": "people", + "moji": "✍🏻", + "unicodeVersion": "8.0", "digest": "2c7e2108e1990490b681343c1b01b4183d4f18fbdef792f113b2f87595e0dad0" }, - { - "name": "writing_hand_tone2", - "unicode": "270D-1F3FC", + "writing_hand_tone2": { + "category": "people", + "moji": "✍🏼", + "unicodeVersion": "8.0", "digest": "87ec8d44f472d301adbcbd50d8c852b609e46584057f59cc1527401db363c1bf" }, - { - "name": "writing_hand_tone3", - "unicode": "270D-1F3FD", + "writing_hand_tone3": { + "category": "people", + "moji": "✍🏽", + "unicodeVersion": "8.0", "digest": "4a48ddef91f7264e8fa9cca223554db22b3a2e3153e94b88d146644ea6dd661e" }, - { - "name": "writing_hand_tone4", - "unicode": "270D-1F3FE", + "writing_hand_tone4": { + "category": "people", + "moji": "✍🏾", + "unicodeVersion": "8.0", "digest": "e5254564a1f91e42ee59f359d8cd26f52abdc04dca8f3b37cb2f140cb7f71390" }, - { - "name": "writing_hand_tone5", - "unicode": "270D-1F3FF", + "writing_hand_tone5": { + "category": "people", + "moji": "✍🏿", + "unicodeVersion": "8.0", "digest": "61299bf86d83d323ca3e6052c535ae66c6f7b3d9866a37db0464223b8bc28523" }, - { - "name": "x", - "unicode": "274C", + "x": { + "category": "symbols", + "moji": "❌", + "unicodeVersion": "6.0", "digest": "3e5a7918e31ddefdf1ce73972365e2f0bfd2917d6a450c1a278c108349c9425d" }, - { - "name": "yellow_heart", - "unicode": "1F49B", + "yellow_heart": { + "category": "symbols", + "moji": "💛", + "unicodeVersion": "6.0", "digest": "a1098f2f04c29754cc9974324508386787d4d803b57cf691d42de414cb2679d6" }, - { - "name": "yen", - "unicode": "1F4B4", + "yen": { + "category": "objects", + "moji": "💴", + "unicodeVersion": "6.0", "digest": "944daaeb3f6369c807c0e63b106cee1360040f7800a70c0d942a992f25a55da7" }, - { - "name": "yin_yang", - "unicode": "262F", + "yin_yang": { + "category": "symbols", + "moji": "☯", + "unicodeVersion": "1.1", "digest": "5ee8d13dacf41306a09237bfcff6abeef110331b40eb7d6e80600628c1327545" }, - { - "name": "yum", - "unicode": "1F60B", + "yum": { + "category": "people", + "moji": "😋", + "unicodeVersion": "6.0", "digest": "31a89088c21bd7a74a3a26d731a907d1bc49436300a9f9c55248703cf7ef44c7" }, - { - "name": "zap", - "unicode": "26A1", + "zap": { + "category": "nature", + "moji": "⚡", + "unicodeVersion": "4.0", "digest": "9f8144ae6f866129aea41bbf694b0c858ef9352a139969e57cd8db73385f52c3" }, - { - "name": "zero", - "unicode": "0030-20E3", + "zero": { + "category": "symbols", + "moji": "0️⃣", + "unicodeVersion": "3.0", "digest": "1b27b5c904defadbdd28ace67a6be5c277ff043297db7cd9f672bbf84e37fa1a" }, - { - "name": "zipper_mouth", - "unicode": "1F910", - "digest": "81bee5aa1202dfd5a4c7badb71ec0e44b8f75c2cbef94e6fd35c593d8770ae43" - }, - { - "name": "zipper_mouth_face", - "unicode": "1F910", + "zipper_mouth": { + "category": "people", + "moji": "🤐", + "unicodeVersion": "8.0", "digest": "81bee5aa1202dfd5a4c7badb71ec0e44b8f75c2cbef94e6fd35c593d8770ae43" }, - { - "name": "zzz", - "unicode": "1F4A4", + "zzz": { + "category": "people", + "moji": "💤", + "unicodeVersion": "6.0", "digest": "b3313d0c44a59fa9d4ce9f7eb4d07ff71dfc8bb01798154250f27cdcf3c693b5" } -]
\ No newline at end of file +}
\ No newline at end of file diff --git a/lib/api/api.rb b/lib/api/api.rb index 8dbe8875fe8..1bf20f76ad6 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -5,6 +5,8 @@ module API version %w(v3 v4), using: :path version 'v3', using: :path do + helpers ::API::V3::Helpers + mount ::API::V3::AwardEmoji mount ::API::V3::Boards mount ::API::V3::Branches diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index 07a1bcdbe18..f9e0c2c4e16 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -3,12 +3,16 @@ module API include PaginationParams before { authenticate! } - AWARDABLES = %w[issue merge_request snippet].freeze + AWARDABLES = [ + { type: 'issue', find_by: :iid }, + { type: 'merge_request', find_by: :iid }, + { type: 'snippet', find_by: :id } + ].freeze resource :projects do - AWARDABLES.each do |awardable_type| - awardable_string = awardable_type.pluralize - awardable_id_string = "#{awardable_type}_id" + AWARDABLES.each do |awardable_params| + awardable_string = awardable_params[:type].pluralize + awardable_id_string = "#{awardable_params[:type]}_#{awardable_params[:find_by]}" params do requires :id, type: String, desc: 'The ID of a project' @@ -104,10 +108,10 @@ module API note_id = params.delete(:note_id) awardable.notes.find(note_id) - elsif params.include?(:issue_id) - user_project.issues.find(params[:issue_id]) - elsif params.include?(:merge_request_id) - user_project.merge_requests.find(params[:merge_request_id]) + elsif params.include?(:issue_iid) + user_project.issues.find_by!(iid: params[:issue_iid]) + elsif params.include?(:merge_request_iid) + user_project.merge_requests.find_by!(iid: params[:merge_request_iid]) else user_project.snippets.find(params[:snippet_id]) end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index b0aa10f8bf2..42401abfe0f 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -18,22 +18,34 @@ module API optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' optional :since, type: DateTime, desc: 'Only commits after or on this date will be returned' optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned' - optional :page, type: Integer, default: 0, desc: 'The page for pagination' - optional :per_page, type: Integer, default: 20, desc: 'The number of results per page' optional :path, type: String, desc: 'The file path' + use :pagination end get ":id/repository/commits" do - ref = params[:ref_name] || user_project.try(:default_branch) || 'master' - offset = params[:page] * params[:per_page] + path = params[:path] + before = params[:until] + after = params[:since] + ref = params[:ref_name] || user_project.try(:default_branch) || 'master' + offset = (params[:page] - 1) * params[:per_page] commits = user_project.repository.commits(ref, - path: params[:path], + path: path, limit: params[:per_page], offset: offset, - after: params[:since], - before: params[:until]) + before: before, + after: after) + + commit_count = + if path || before || after + user_project.repository.count_commits(ref: ref, path: path, before: before, after: after) + else + # Cacheable commit count. + user_project.repository.commit_count_for_ref(ref) + end + + paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count) - present commits, with: Entities::RepoCommit + present paginate(paginated_commits), with: Entities::RepoCommit end desc 'Commit multiple file changes as one commit' do diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 965f8fbab8f..0a12ee72d49 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -671,7 +671,7 @@ module API end class Environment < EnvironmentBasic - expose :project, using: Entities::Project + expose :project, using: Entities::BasicProjectDetails end class Deployment < Grape::Entity @@ -705,5 +705,99 @@ module API expose :id, :message, :starts_at, :ends_at, :color, :font expose :active?, as: :active end + + class PersonalAccessToken < Grape::Entity + expose :id, :name, :revoked, :created_at, :scopes + expose :active?, as: :active + expose :expires_at do |personal_access_token| + personal_access_token.expires_at ? personal_access_token.expires_at.strftime("%Y-%m-%d") : nil + end + end + + class PersonalAccessTokenWithToken < PersonalAccessToken + expose :token + end + + class ImpersonationToken < PersonalAccessTokenWithToken + expose :impersonation + end + + module JobRequest + class JobInfo < Grape::Entity + expose :name, :stage + expose :project_id, :project_name + end + + class GitInfo < Grape::Entity + expose :repo_url, :ref, :sha, :before_sha + expose :ref_type do |model| + if model.tag + 'tag' + else + 'branch' + end + end + end + + class RunnerInfo < Grape::Entity + expose :timeout + end + + class Step < Grape::Entity + expose :name, :script, :timeout, :when, :allow_failure + end + + class Image < Grape::Entity + expose :name + end + + class Artifacts < Grape::Entity + expose :name, :untracked, :paths, :when, :expire_in + end + + class Cache < Grape::Entity + expose :key, :untracked, :paths + end + + class Credentials < Grape::Entity + expose :type, :url, :username, :password + end + + class ArtifactFile < Grape::Entity + expose :filename, :size + end + + class Dependency < Grape::Entity + expose :id, :name + expose :artifacts_file, using: ArtifactFile, if: ->(job, _) { job.artifacts? } + end + + class Response < Grape::Entity + expose :id + expose :token + expose :allow_git_fetch + + expose :job_info, using: JobInfo do |model| + model + end + + expose :git_info, using: GitInfo do |model| + model + end + + expose :runner_info, using: RunnerInfo do |model| + model + end + + expose :variables + expose :steps, using: Step + expose :image, using: Image + expose :services, using: Image + expose :artifacts, using: Artifacts + expose :cache, using: Cache + expose :credentials, using: Credentials + expose :depends_on_builds, as: :dependencies, using: Dependency + end + end end end diff --git a/lib/api/files.rb b/lib/api/files.rb index 9c4e43d77cc..bb8f5c3076d 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -14,6 +14,19 @@ module API } end + def assign_file_vars! + authorize! :download_code, user_project + + @commit = user_project.commit(params[:ref]) + not_found!('Commit') unless @commit + + @repo = user_project.repository + @blob = @repo.blob_at(@commit.sha, params[:file_path]) + + not_found!('File') unless @blob + @blob.load_all_data!(@repo) + end + def commit_response(attrs) { file_path: attrs[:file_path], @@ -22,7 +35,7 @@ module API end params :simple_file_params do - requires :file_path, type: String, desc: 'The path to new file. Ex. lib/class.rb' + requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb' requires :branch, type: String, desc: 'The name of branch' requires :commit_message, type: String, desc: 'Commit Message' optional :author_email, type: String, desc: 'The email of the author' @@ -40,34 +53,35 @@ module API requires :id, type: String, desc: 'The project ID' end resource :projects do - desc 'Get a file from repository' + desc 'Get raw file contents from the repository' params do - requires :file_path, type: String, desc: 'The path to the file. Ex. lib/class.rb' - requires :ref, type: String, desc: 'The name of branch, tag, or commit' + requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb' + requires :ref, type: String, desc: 'The name of branch, tag commit' end - get ":id/repository/files" do - authorize! :download_code, user_project - - commit = user_project.commit(params[:ref]) - not_found!('Commit') unless commit + get ":id/repository/files/:file_path/raw" do + assign_file_vars! - repo = user_project.repository - blob = repo.blob_at(commit.sha, params[:file_path]) - not_found!('File') unless blob + send_git_blob @repo, @blob + end - blob.load_all_data!(repo) - status(200) + desc 'Get a file from the repository' + params do + requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb' + requires :ref, type: String, desc: 'The name of branch, tag or commit' + end + get ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do + assign_file_vars! { - file_name: blob.name, - file_path: blob.path, - size: blob.size, + file_name: @blob.name, + file_path: @blob.path, + size: @blob.size, encoding: "base64", - content: Base64.strict_encode64(blob.data), + content: Base64.strict_encode64(@blob.data), ref: params[:ref], - blob_id: blob.id, - commit_id: commit.id, - last_commit_id: repo.last_commit_id_for_path(commit.sha, params[:file_path]) + blob_id: @blob.id, + commit_id: @commit.id, + last_commit_id: @repo.last_commit_id_for_path(@commit.sha, params[:file_path]) } end @@ -75,7 +89,7 @@ module API params do use :extended_file_params end - post ":id/repository/files" do + post ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do authorize! :push_code, user_project file_params = declared_params(include_missing: false) @@ -93,7 +107,7 @@ module API params do use :extended_file_params end - put ":id/repository/files" do + put ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do authorize! :push_code, user_project file_params = declared_params(include_missing: false) @@ -112,7 +126,7 @@ module API params do use :simple_file_params end - delete ":id/repository/files" do + delete ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do authorize! :push_code, user_project file_params = declared_params(include_missing: false) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index f325f0a3050..a9b364da9e1 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -82,16 +82,16 @@ module API label || not_found!('Label') end - def find_project_issue(id) - IssuesFinder.new(current_user, project_id: user_project.id).find(id) + def find_project_issue(iid) + IssuesFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid) end - def find_project_merge_request(id) - MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id) + def find_project_merge_request(iid) + MergeRequestsFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid) end - def find_merge_request_with_access(id, access_level = :read_merge_request) - merge_request = user_project.merge_requests.find(id) + def find_merge_request_with_access(iid, access_level = :read_merge_request) + merge_request = user_project.merge_requests.find_by!(iid: iid) authorize! access_level, merge_request merge_request end diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index 080a6274957..2135a787b11 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -9,11 +9,11 @@ module API # In addition, they may have a '.git' extension and multiple namespaces # # Transform all these cases to 'namespace/project' - def clean_project_path(project_path, storage_paths = Repository.storages.values) + def clean_project_path(project_path, storages = Gitlab.config.repositories.storages.values) project_path = project_path.sub(/\.git\z/, '') - storage_paths.each do |storage_path| - storage_path = File.expand_path(storage_path) + storages.each do |storage| + storage_path = File.expand_path(storage['path']) if project_path.start_with?(storage_path) project_path = project_path.sub(storage_path, '') diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb index 119ca81b883..ec2bcaed929 100644 --- a/lib/api/helpers/runner.rb +++ b/lib/api/helpers/runner.rb @@ -1,6 +1,10 @@ module API module Helpers module Runner + JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze + JOB_TOKEN_PARAM = :token + UPDATE_RUNNER_EVERY = 10 * 60 + def runner_registration_token_valid? ActiveSupport::SecurityUtils.variable_size_secure_compare(params[:token], current_application_settings.runners_registration_token) @@ -18,6 +22,56 @@ module API def current_runner @runner ||= ::Ci::Runner.find_by_token(params[:token].to_s) end + + def update_runner_info + return unless update_runner? + + current_runner.contacted_at = Time.now + current_runner.assign_attributes(get_runner_version_from_params) + current_runner.save if current_runner.changed? + end + + def update_runner? + # Use a random threshold to prevent beating DB updates. + # It generates a distribution between [40m, 80m]. + # + contacted_at_max_age = UPDATE_RUNNER_EVERY + Random.rand(UPDATE_RUNNER_EVERY) + + current_runner.contacted_at.nil? || + (Time.now - current_runner.contacted_at) >= contacted_at_max_age + end + + def job_not_found! + if headers['User-Agent'].to_s =~ /gitlab(-ci-multi)?-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? / + no_content! + else + not_found! + end + end + + def validate_job!(job) + not_found! unless job + + yield if block_given? + + forbidden!('Project has been deleted!') unless job.project + forbidden!('Job has been erased!') if job.erased? + end + + def authenticate_job!(job) + validate_job!(job) do + forbidden! unless job_token_valid?(job) + end + end + + def job_token_valid?(job) + token = (params[JOB_TOKEN_PARAM] || env[JOB_TOKEN_HEADER]).to_s + token && job.valid_token?(token) + end + + def max_artifacts_size + current_application_settings.max_artifacts_size.megabytes.to_i + end end end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index bda74069ad5..4a9f2b26fb2 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -102,10 +102,10 @@ module API success Entities::Issue end params do - requires :issue_id, type: Integer, desc: 'The ID of a project issue' + requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue' end - get ":id/issues/:issue_id" do - issue = find_project_issue(params[:issue_id]) + get ":id/issues/:issue_iid" do + issue = find_project_issue(params[:issue_iid]) present issue, with: Entities::Issue, current_user: current_user, project: user_project end @@ -152,7 +152,7 @@ module API success Entities::Issue end params do - requires :issue_id, type: Integer, desc: 'The ID of a project issue' + requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue' optional :title, type: String, desc: 'The title of an issue' optional :updated_at, type: DateTime, desc: 'Date time when the issue was updated. Available only for admins and project owners.' @@ -161,8 +161,8 @@ module API at_least_one_of :title, :description, :assignee_id, :milestone_id, :labels, :created_at, :due_date, :confidential, :state_event end - put ':id/issues/:issue_id' do - issue = user_project.issues.find(params.delete(:issue_id)) + put ':id/issues/:issue_iid' do + issue = user_project.issues.find_by!(iid: params.delete(:issue_iid)) authorize! :update_issue, issue # Setting created_at time only allowed for admins and project owners @@ -189,11 +189,11 @@ module API success Entities::Issue end params do - requires :issue_id, type: Integer, desc: 'The ID of a project issue' + requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue' requires :to_project_id, type: Integer, desc: 'The ID of the new project' end - post ':id/issues/:issue_id/move' do - issue = user_project.issues.find_by(id: params[:issue_id]) + post ':id/issues/:issue_iid/move' do + issue = user_project.issues.find_by(iid: params[:issue_iid]) not_found!('Issue') unless issue new_project = Project.find_by(id: params[:to_project_id]) @@ -209,10 +209,10 @@ module API desc 'Delete a project issue' params do - requires :issue_id, type: Integer, desc: 'The ID of a project issue' + requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue' end - delete ":id/issues/:issue_id" do - issue = user_project.issues.find_by(id: params[:issue_id]) + delete ":id/issues/:issue_iid" do + issue = user_project.issues.find_by(iid: params[:issue_iid]) not_found!('Issue') unless issue authorize!(:destroy_issue, issue) diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index 33c05e8aa63..44118522abe 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -18,6 +18,8 @@ module API [scope] when Hashie::Mash scope.values + when Hashie::Array + scope else ['unknown'] end @@ -36,8 +38,23 @@ module API builds = user_project.builds.order('id DESC') builds = filter_builds(builds, params[:scope]) - present paginate(builds), with: Entities::Job, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present paginate(builds), with: Entities::Job + end + + desc 'Get pipeline jobs' do + success Entities::Job + end + params do + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' + use :optional_scope + use :pagination + end + get ':id/pipelines/:pipeline_id/jobs' do + pipeline = user_project.pipelines.find(params[:pipeline_id]) + builds = pipeline.builds + builds = filter_builds(builds, params[:scope]) + + present paginate(builds), with: Entities::Job end desc 'Get a specific job of a project' do @@ -51,8 +68,7 @@ module API build = get_build!(params[:job_id]) - present build, with: Entities::Job, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: Entities::Job end desc 'Download the artifacts file from a job' do @@ -119,8 +135,7 @@ module API build.cancel - present build, with: Entities::Job, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: Entities::Job end desc 'Retry a specific build of a project' do @@ -137,8 +152,7 @@ module API build = Ci::Build.retry(build, current_user) - present build, with: Entities::Job, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: Entities::Job end desc 'Erase job (remove artifacts and the trace)' do @@ -154,8 +168,7 @@ module API return forbidden!('Job is not erasable!') unless build.erasable? build.erase(erased_by: current_user) - present build, with: Entities::Job, - user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + present build, with: Entities::Job end desc 'Keep the artifacts to prevent them from being deleted' do @@ -173,8 +186,7 @@ module API build.keep_artifacts! status 200 - present build, with: Entities::Job, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: Entities::Job end desc 'Trigger a manual job' do @@ -194,8 +206,7 @@ module API build.play(current_user) status 200 - present build, with: Entities::Job, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: Entities::Job end end diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb index 4901a7cfea6..a59e39cca26 100644 --- a/lib/api/merge_request_diffs.rb +++ b/lib/api/merge_request_diffs.rb @@ -13,11 +13,11 @@ module API params do requires :id, type: String, desc: 'The ID of a project' - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request' use :pagination end - get ":id/merge_requests/:merge_request_id/versions" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + get ":id/merge_requests/:merge_request_iid/versions" do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) present paginate(merge_request.merge_request_diffs), with: Entities::MergeRequestDiff end @@ -29,12 +29,12 @@ module API params do requires :id, type: String, desc: 'The ID of a project' - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request' requires :version_id, type: Integer, desc: 'The ID of a merge request diff version' end - get ":id/merge_requests/:merge_request_id/versions/:version_id" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + get ":id/merge_requests/:merge_request_iid/versions/:version_id" do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 6fc33a7a54a..7a03955a045 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -101,23 +101,23 @@ module API desc 'Delete a merge request' params do - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request' end - delete ":id/merge_requests/:merge_request_id" do - merge_request = find_project_merge_request(params[:merge_request_id]) + delete ":id/merge_requests/:merge_request_iid" do + merge_request = find_project_merge_request(params[:merge_request_iid]) authorize!(:destroy_merge_request, merge_request) merge_request.destroy end params do - requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request' end desc 'Get a single merge request' do success Entities::MergeRequest end - get ':id/merge_requests/:merge_request_id' do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + get ':id/merge_requests/:merge_request_iid' do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project end @@ -125,8 +125,8 @@ module API desc 'Get the commits of a merge request' do success Entities::RepoCommit end - get ':id/merge_requests/:merge_request_id/commits' do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + get ':id/merge_requests/:merge_request_iid/commits' do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) commits = ::Kaminari.paginate_array(merge_request.commits) present paginate(commits), with: Entities::RepoCommit @@ -135,8 +135,8 @@ module API desc 'Show the merge request changes' do success Entities::MergeRequestChanges end - get ':id/merge_requests/:merge_request_id/changes' do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + get ':id/merge_requests/:merge_request_iid/changes' do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) present merge_request, with: Entities::MergeRequestChanges, current_user: current_user end @@ -154,8 +154,8 @@ module API :milestone_id, :labels, :state_event, :remove_source_branch end - put ':id/merge_requests/:merge_request_id' do - merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request) + put ':id/merge_requests/:merge_request_iid' do + merge_request = find_merge_request_with_access(params.delete(:merge_request_iid), :update_merge_request) mr_params = declared_params(include_missing: false) mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? @@ -180,8 +180,8 @@ module API desc: 'When true, this merge request will be merged when the pipeline succeeds' optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch' end - put ':id/merge_requests/:merge_request_id/merge' do - merge_request = find_project_merge_request(params[:merge_request_id]) + put ':id/merge_requests/:merge_request_iid/merge' do + merge_request = find_project_merge_request(params[:merge_request_iid]) # Merge request can not be merged # because user dont have permissions to push into target branch @@ -216,8 +216,8 @@ module API desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do success Entities::MergeRequest end - post ':id/merge_requests/:merge_request_id/cancel_merge_when_pipeline_succeeds' do - merge_request = find_project_merge_request(params[:merge_request_id]) + post ':id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do + merge_request = find_project_merge_request(params[:merge_request_iid]) unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user) @@ -232,8 +232,8 @@ module API params do use :pagination end - get ':id/merge_requests/:merge_request_id/comments' do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + get ':id/merge_requests/:merge_request_iid/comments' do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) present paginate(merge_request.notes.fresh), with: Entities::MRNote end @@ -243,8 +243,8 @@ module API params do requires :note, type: String, desc: 'The text of the comment' end - post ':id/merge_requests/:merge_request_id/comments' do - merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note) + post ':id/merge_requests/:merge_request_iid/comments' do + merge_request = find_merge_request_with_access(params[:merge_request_iid], :create_note) opts = { note: params[:note], @@ -267,8 +267,8 @@ module API params do use :pagination end - get ':id/merge_requests/:merge_request_id/closes_issues' do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + get ':id/merge_requests/:merge_request_iid/closes_issues' do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) present paginate(issues), with: issue_entity(user_project), current_user: current_user end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 36166780149..531ef5a63ea 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -17,19 +17,34 @@ module API end not_found! end + + def assign_blob_vars! + authorize! :download_code, user_project + + @repo = user_project.repository + + begin + @blob = Gitlab::Git::Blob.raw(@repo, params[:sha]) + @blob.load_all_data!(@repo) + rescue + not_found! 'Blob' + end + + not_found! 'Blob' unless @blob + end end desc 'Get a project repository tree' do success Entities::RepoTreeObject end params do - optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' + optional :ref, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' optional :path, type: String, desc: 'The path of the tree' optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree' use :pagination end get ':id/repository/tree' do - ref = params[:ref_name] || user_project.try(:default_branch) || 'master' + ref = params[:ref] || user_project.try(:default_branch) || 'master' path = params[:path] || nil commit = user_project.commit(ref) @@ -40,39 +55,29 @@ module API present paginate(entries), with: Entities::RepoTreeObject end - desc 'Get a raw file contents' + desc 'Get raw blob contents from the repository' params do requires :sha, type: String, desc: 'The commit, branch name, or tag name' - requires :filepath, type: String, desc: 'The path to the file to display' end - get [":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob"] do - repo = user_project.repository - - commit = repo.commit(params[:sha]) - not_found! "Commit" unless commit + get ':id/repository/blobs/:sha/raw' do + assign_blob_vars! - blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath]) - not_found! "File" unless blob - - send_git_blob repo, blob + send_git_blob @repo, @blob end - desc 'Get a raw blob contents by blob sha' + desc 'Get a blob from the repository' params do requires :sha, type: String, desc: 'The commit, branch name, or tag name' end - get ':id/repository/raw_blobs/:sha' do - repo = user_project.repository - - begin - blob = Gitlab::Git::Blob.raw(repo, params[:sha]) - rescue - not_found! 'Blob' - end - - not_found! 'Blob' unless blob + get ':id/repository/blobs/:sha' do + assign_blob_vars! - send_git_blob repo, blob + { + size: @blob.size, + encoding: "base64", + content: Base64.strict_encode64(@blob.data), + sha: @blob.id + } end desc 'Get an archive of the repository' diff --git a/lib/api/runner.rb b/lib/api/runner.rb index 47858f1866b..c700d2ef4a1 100644 --- a/lib/api/runner.rb +++ b/lib/api/runner.rb @@ -48,5 +48,203 @@ module API Ci::Runner.find_by_token(params[:token]).destroy end end + + resource :jobs do + desc 'Request a job' do + success Entities::JobRequest::Response + end + params do + requires :token, type: String, desc: %q(Runner's authentication token) + optional :last_update, type: String, desc: %q(Runner's queue last_update token) + optional :info, type: Hash, desc: %q(Runner's metadata) + end + post '/request' do + authenticate_runner! + not_found! unless current_runner.active? + update_runner_info + + if current_runner.is_runner_queue_value_latest?(params[:last_update]) + header 'X-GitLab-Last-Update', params[:last_update] + Gitlab::Metrics.add_event(:build_not_found_cached) + return job_not_found! + end + + new_update = current_runner.ensure_runner_queue_value + result = ::Ci::RegisterJobService.new(current_runner).execute + + if result.valid? + if result.build + Gitlab::Metrics.add_event(:build_found, + project: result.build.project.path_with_namespace) + present result.build, with: Entities::JobRequest::Response + else + Gitlab::Metrics.add_event(:build_not_found) + header 'X-GitLab-Last-Update', new_update + job_not_found! + end + else + # We received build that is invalid due to concurrency conflict + Gitlab::Metrics.add_event(:build_invalid) + conflict! + end + end + + desc 'Updates a job' do + http_codes [[200, 'Job was updated'], [403, 'Forbidden']] + end + params do + requires :token, type: String, desc: %q(Runners's authentication token) + requires :id, type: Integer, desc: %q(Job's ID) + optional :trace, type: String, desc: %q(Job's full trace) + optional :state, type: String, desc: %q(Job's status: success, failed) + end + put '/:id' do + job = Ci::Build.find_by_id(params[:id]) + authenticate_job!(job) + + job.update_attributes(trace: params[:trace]) if params[:trace] + + Gitlab::Metrics.add_event(:update_build, + project: job.project.path_with_namespace) + + case params[:state].to_s + when 'success' + job.success + when 'failed' + job.drop + end + end + + desc 'Appends a patch to the job trace' do + http_codes [[202, 'Trace was patched'], + [400, 'Missing Content-Range header'], + [403, 'Forbidden'], + [416, 'Range not satisfiable']] + end + params do + requires :id, type: Integer, desc: %q(Job's ID) + optional :token, type: String, desc: %q(Job's authentication token) + end + patch '/:id/trace' do + job = Ci::Build.find_by_id(params[:id]) + authenticate_job!(job) + + error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range') + content_range = request.headers['Content-Range'] + content_range = content_range.split('-') + + current_length = job.trace_length + unless current_length == content_range[0].to_i + return error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{current_length}" }) + end + + job.append_trace(request.body.read, content_range[0].to_i) + + status 202 + header 'Job-Status', job.status + header 'Range', "0-#{job.trace_length}" + end + + desc 'Authorize artifacts uploading for job' do + http_codes [[200, 'Upload allowed'], + [403, 'Forbidden'], + [405, 'Artifacts support not enabled'], + [413, 'File too large']] + end + params do + requires :id, type: Integer, desc: %q(Job's ID) + optional :token, type: String, desc: %q(Job's authentication token) + optional :filesize, type: Integer, desc: %q(Artifacts filesize) + end + post '/:id/artifacts/authorize' do + not_allowed! unless Gitlab.config.artifacts.enabled + require_gitlab_workhorse! + Gitlab::Workhorse.verify_api_request!(headers) + + job = Ci::Build.find_by_id(params[:id]) + authenticate_job!(job) + forbidden!('Job is not running') unless job.running? + + if params[:filesize] + file_size = params[:filesize].to_i + file_to_large! unless file_size < max_artifacts_size + end + + status 200 + content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE + Gitlab::Workhorse.artifact_upload_ok + end + + desc 'Upload artifacts for job' do + success Entities::JobRequest::Response + http_codes [[201, 'Artifact uploaded'], + [400, 'Bad request'], + [403, 'Forbidden'], + [405, 'Artifacts support not enabled'], + [413, 'File too large']] + end + params do + requires :id, type: Integer, desc: %q(Job's ID) + optional :token, type: String, desc: %q(Job's authentication token) + optional :expire_in, type: String, desc: %q(Specify when artifacts should expire) + optional :file, type: File, desc: %q(Artifact's file) + optional 'file.path', type: String, desc: %q(path to locally stored body (generated by Workhorse)) + optional 'file.name', type: String, desc: %q(real filename as send in Content-Disposition (generated by Workhorse)) + optional 'file.type', type: String, desc: %q(real content type as send in Content-Type (generated by Workhorse)) + optional 'metadata.path', type: String, desc: %q(path to locally stored body (generated by Workhorse)) + optional 'metadata.name', type: String, desc: %q(filename (generated by Workhorse)) + end + post '/:id/artifacts' do + not_allowed! unless Gitlab.config.artifacts.enabled + require_gitlab_workhorse! + + job = Ci::Build.find_by_id(params[:id]) + authenticate_job!(job) + forbidden!('Job is not running!') unless job.running? + + artifacts_upload_path = ArtifactUploader.artifacts_upload_path + artifacts = uploaded_file(:file, artifacts_upload_path) + metadata = uploaded_file(:metadata, artifacts_upload_path) + + bad_request!('Missing artifacts file!') unless artifacts + file_to_large! unless artifacts.size < max_artifacts_size + + job.artifacts_file = artifacts + job.artifacts_metadata = metadata + job.artifacts_expire_in = params['expire_in'] || + Gitlab::CurrentSettings.current_application_settings.default_artifacts_expire_in + + if job.save + present job, with: Entities::JobRequest::Response + else + render_validation_error!(job) + end + end + + desc 'Download the artifacts file for job' do + http_codes [[200, 'Upload allowed'], + [403, 'Forbidden'], + [404, 'Artifact not found']] + end + params do + requires :id, type: Integer, desc: %q(Job's ID) + optional :token, type: String, desc: %q(Job's authentication token) + end + get '/:id/artifacts' do + job = Ci::Build.find_by_id(params[:id]) + authenticate_job!(job) + + artifacts_file = job.artifacts_file + unless artifacts_file.file_storage? + return redirect_to job.artifacts_file.url + end + + unless artifacts_file.exists? + not_found! + end + + present_file!(artifacts_file.path, artifacts_file.filename) + end + end end end diff --git a/lib/api/services.rb b/lib/api/services.rb index 1cf29d9a1a3..5aa2f5eba7b 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -422,6 +422,14 @@ module API desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' } ], + 'prometheus' => [ + { + required: true, + name: :api_url, + type: String, + desc: 'Prometheus API Base URL, like http://prometheus.example.com/' + } + ], 'pushover' => [ { required: true, @@ -558,6 +566,7 @@ module API SlackSlashCommandsService, PipelinesEmailService, PivotaltrackerService, + PrometheusService, PushoverService, RedmineService, SlackService, diff --git a/lib/api/time_tracking_endpoints.rb b/lib/api/time_tracking_endpoints.rb index 85b5f7d98b8..05b4b490e27 100644 --- a/lib/api/time_tracking_endpoints.rb +++ b/lib/api/time_tracking_endpoints.rb @@ -5,11 +5,11 @@ module API included do helpers do def issuable_name - declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request' + declared_params.has_key?(:issue_iid) ? 'issue' : 'merge_request' end def issuable_key - "#{issuable_name}_id".to_sym + "#{issuable_name}_iid".to_sym end def update_issuable_key @@ -50,7 +50,7 @@ module API issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request' issuable_collection_name = issuable_name.pluralize - issuable_key = "#{issuable_name}_id".to_sym + issuable_key = "#{issuable_name}_iid".to_sym desc "Set a time estimate for a project #{issuable_name}" params do diff --git a/lib/api/todos.rb b/lib/api/todos.rb index e59030428da..d9b8837a5bb 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -5,8 +5,8 @@ module API before { authenticate! } ISSUABLE_TYPES = { - 'merge_requests' => ->(id) { find_merge_request_with_access(id) }, - 'issues' => ->(id) { find_project_issue(id) } + 'merge_requests' => ->(iid) { find_merge_request_with_access(iid) }, + 'issues' => ->(iid) { find_project_issue(iid) } }.freeze params do @@ -14,13 +14,13 @@ module API end resource :projects do ISSUABLE_TYPES.each do |type, finder| - type_id_str = "#{type.singularize}_id".to_sym + type_id_str = "#{type.singularize}_iid".to_sym desc 'Create a todo on an issuable' do success Entities::Todo end params do - requires type_id_str, type: Integer, desc: 'The ID of an issuable' + requires type_id_str, type: Integer, desc: 'The IID of an issuable' end post ":id/#{type}/:#{type_id_str}/todo" do issuable = instance_exec(params[type_id_str], &finder) diff --git a/lib/api/users.rb b/lib/api/users.rb index 7bb4b76f830..549003f576a 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -9,6 +9,11 @@ module API resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do helpers do + def find_user(params) + id = params[:user_id] || params[:id] + User.find_by(id: id) || not_found!('User') + end + params :optional_attributes do optional :skype, type: String, desc: 'The Skype username' optional :linkedin, type: String, desc: 'The LinkedIn username' @@ -362,6 +367,76 @@ module API present paginate(events), with: Entities::Event end + + params do + requires :user_id, type: Integer, desc: 'The ID of the user' + end + segment ':user_id' do + resource :impersonation_tokens do + helpers do + def finder(options = {}) + user = find_user(params) + PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options)) + end + + def find_impersonation_token + finder.find_by(id: declared_params[:impersonation_token_id]) || not_found!('Impersonation Token') + end + end + + before { authenticated_as_admin! } + + desc 'Retrieve impersonation tokens. Available only for admins.' do + detail 'This feature was introduced in GitLab 9.0' + success Entities::ImpersonationToken + end + params do + use :pagination + optional :state, type: String, default: 'all', values: %w[all active inactive], desc: 'Filters (all|active|inactive) impersonation_tokens' + end + get { present paginate(finder(declared_params(include_missing: false)).execute), with: Entities::ImpersonationToken } + + desc 'Create a impersonation token. Available only for admins.' do + detail 'This feature was introduced in GitLab 9.0' + success Entities::ImpersonationToken + end + params do + requires :name, type: String, desc: 'The name of the impersonation token' + optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the impersonation token' + optional :scopes, type: Array, desc: 'The array of scopes of the impersonation token' + end + post do + impersonation_token = finder.build(declared_params(include_missing: false)) + + if impersonation_token.save + present impersonation_token, with: Entities::ImpersonationToken + else + render_validation_error!(impersonation_token) + end + end + + desc 'Retrieve impersonation token. Available only for admins.' do + detail 'This feature was introduced in GitLab 9.0' + success Entities::ImpersonationToken + end + params do + requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token' + end + get ':impersonation_token_id' do + present find_impersonation_token, with: Entities::ImpersonationToken + end + + desc 'Revoke a impersonation token. Available only for admins.' do + detail 'This feature was introduced in GitLab 9.0' + end + params do + requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token' + end + delete ':impersonation_token_id' do + find_impersonation_token.revoke! + end + end + end end resource :user do diff --git a/lib/api/v3/award_emoji.rb b/lib/api/v3/award_emoji.rb index 1e35283631f..cf9e1551f60 100644 --- a/lib/api/v3/award_emoji.rb +++ b/lib/api/v3/award_emoji.rb @@ -16,11 +16,64 @@ module API requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet" end - [":id/#{awardable_string}/:#{awardable_id_string}/award_emoji", - ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"].each do |endpoint| + [ + ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji", + ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji" + ].each do |endpoint| + + desc 'Get a list of project +awardable+ award emoji' do + detail 'This feature was introduced in 8.9' + success Entities::AwardEmoji + end + params do + use :pagination + end + get endpoint do + if can_read_awardable? + awards = awardable.award_emoji + present paginate(awards), with: Entities::AwardEmoji + else + not_found!("Award Emoji") + end + end + + desc 'Get a specific award emoji' do + detail 'This feature was introduced in 8.9' + success Entities::AwardEmoji + end + params do + requires :award_id, type: Integer, desc: 'The ID of the award' + end + get "#{endpoint}/:award_id" do + if can_read_awardable? + present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji + else + not_found!("Award Emoji") + end + end + + desc 'Award a new Emoji' do + detail 'This feature was introduced in 8.9' + success Entities::AwardEmoji + end + params do + requires :name, type: String, desc: 'The name of a award_emoji (without colons)' + end + post endpoint do + not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable? + + award = awardable.create_award_emoji(params[:name], current_user) + + if award.persisted? + present award, with: Entities::AwardEmoji + else + not_found!("Award Emoji #{award.errors.messages}") + end + end + desc 'Delete a +awardables+ award emoji' do detail 'This feature was introduced in 8.9' - success ::API::Entities::AwardEmoji + success Entities::AwardEmoji end params do requires :award_id, type: Integer, desc: 'The ID of an award emoji' @@ -30,13 +83,22 @@ module API unauthorized! unless award.user == current_user || current_user.admin? - present award.destroy, with: ::API::Entities::AwardEmoji + award.destroy + present award, with: Entities::AwardEmoji end end end end helpers do + def can_read_awardable? + can?(current_user, read_ability(awardable), awardable) + end + + def can_award_awardable? + awardable.user_can_award?(current_user, params[:name]) + end + def awardable @awardable ||= begin @@ -53,6 +115,15 @@ module API end end end + + def read_ability(awardable) + case awardable + when Note + read_ability(awardable.noteable) + else + :"read_#{awardable.class.to_s.underscore}" + end + end end end end diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb index c8feba13527..6f97102c6ef 100644 --- a/lib/api/v3/builds.rb +++ b/lib/api/v3/builds.rb @@ -36,8 +36,7 @@ module API builds = user_project.builds.order('id DESC') builds = filter_builds(builds, params[:scope]) - present paginate(builds), with: ::API::V3::Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present paginate(builds), with: ::API::V3::Entities::Build end desc 'Get builds for a specific commit of a project' do @@ -57,8 +56,7 @@ module API builds = user_project.builds.where(pipeline: pipelines).order('id DESC') builds = filter_builds(builds, params[:scope]) - present paginate(builds), with: ::API::V3::Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present paginate(builds), with: ::API::V3::Entities::Build end desc 'Get a specific build of a project' do @@ -72,8 +70,7 @@ module API build = get_build!(params[:build_id]) - present build, with: ::API::V3::Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: ::API::V3::Entities::Build end desc 'Download the artifacts file from build' do @@ -140,8 +137,7 @@ module API build.cancel - present build, with: ::API::V3::Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: ::API::V3::Entities::Build end desc 'Retry a specific build of a project' do @@ -158,8 +154,7 @@ module API build = Ci::Build.retry(build, current_user) - present build, with: ::API::V3::Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: ::API::V3::Entities::Build end desc 'Erase build (remove artifacts and build trace)' do @@ -175,8 +170,7 @@ module API return forbidden!('Build is not erasable!') unless build.erasable? build.erase(erased_by: current_user) - present build, with: ::API::V3::Entities::Build, - user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) + present build, with: ::API::V3::Entities::Build end desc 'Keep the artifacts to prevent them from being deleted' do @@ -194,8 +188,7 @@ module API build.keep_artifacts! status 200 - present build, with: ::API::V3::Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: ::API::V3::Entities::Build end desc 'Trigger a manual build' do @@ -215,8 +208,7 @@ module API build.play(current_user) status 200 - present build, with: ::API::V3::Entities::Build, - user_can_download_artifacts: can?(current_user, :read_build, user_project) + present build, with: ::API::V3::Entities::Build end end diff --git a/lib/api/v3/helpers.rb b/lib/api/v3/helpers.rb new file mode 100644 index 00000000000..0f234d4cdad --- /dev/null +++ b/lib/api/v3/helpers.rb @@ -0,0 +1,19 @@ +module API + module V3 + module Helpers + def find_project_issue(id) + IssuesFinder.new(current_user, project_id: user_project.id).find(id) + end + + def find_project_merge_request(id) + MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id) + end + + def find_merge_request_with_access(id, access_level = :read_merge_request) + merge_request = user_project.merge_requests.find(id) + authorize! access_level, merge_request + merge_request + end + end + end +end diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb index 3549ea225ef..44584e2eb70 100644 --- a/lib/api/v3/repositories.rb +++ b/lib/api/v3/repositories.rb @@ -38,6 +38,60 @@ module API present tree.sorted_entries, with: ::API::Entities::RepoTreeObject end + desc 'Get a raw file contents' + params do + requires :sha, type: String, desc: 'The commit, branch name, or tag name' + requires :filepath, type: String, desc: 'The path to the file to display' + end + get [":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob"] do + repo = user_project.repository + commit = repo.commit(params[:sha]) + not_found! "Commit" unless commit + blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath]) + not_found! "File" unless blob + send_git_blob repo, blob + end + + desc 'Get a raw blob contents by blob sha' + params do + requires :sha, type: String, desc: 'The commit, branch name, or tag name' + end + get ':id/repository/raw_blobs/:sha' do + repo = user_project.repository + begin + blob = Gitlab::Git::Blob.raw(repo, params[:sha]) + rescue + not_found! 'Blob' + end + not_found! 'Blob' unless blob + send_git_blob repo, blob + end + + desc 'Get an archive of the repository' + params do + optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded' + optional :format, type: String, desc: 'The archive format' + end + get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do + begin + send_git_archive user_project.repository, ref: params[:sha], format: params[:format] + rescue + not_found!('File') + end + end + + desc 'Compare two branches, tags, or commits' do + success ::API::Entities::Compare + end + params do + requires :from, type: String, desc: 'The commit, branch name, or tag name to start comparison' + requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison' + end + get ':id/repository/compare' do + compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to]) + present compare, with: ::API::Entities::Compare + end + desc 'Get repository contributors' do success ::API::Entities::Contributor end diff --git a/lib/api/v3/time_tracking_endpoints.rb b/lib/api/v3/time_tracking_endpoints.rb new file mode 100644 index 00000000000..81ae4e8137d --- /dev/null +++ b/lib/api/v3/time_tracking_endpoints.rb @@ -0,0 +1,116 @@ +module API + module V3 + module TimeTrackingEndpoints + extend ActiveSupport::Concern + + included do + helpers do + def issuable_name + declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request' + end + + def issuable_key + "#{issuable_name}_id".to_sym + end + + def update_issuable_key + "update_#{issuable_name}".to_sym + end + + def read_issuable_key + "read_#{issuable_name}".to_sym + end + + def load_issuable + @issuable ||= begin + case issuable_name + when 'issue' + find_project_issue(params.delete(issuable_key)) + when 'merge_request' + find_project_merge_request(params.delete(issuable_key)) + end + end + end + + def update_issuable(attrs) + custom_params = declared_params(include_missing: false) + custom_params.merge!(attrs) + + issuable = update_service.new(user_project, current_user, custom_params).execute(load_issuable) + if issuable.valid? + present issuable, with: ::API::Entities::IssuableTimeStats + else + render_validation_error!(issuable) + end + end + + def update_service + issuable_name == 'issue' ? ::Issues::UpdateService : ::MergeRequests::UpdateService + end + end + + issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request' + issuable_collection_name = issuable_name.pluralize + issuable_key = "#{issuable_name}_id".to_sym + + desc "Set a time estimate for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + requires :duration, type: String, desc: 'The duration to be parsed' + end + post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do + authorize! update_issuable_key, load_issuable + + status :ok + update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration))) + end + + desc "Reset the time estimate for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + end + post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do + authorize! update_issuable_key, load_issuable + + status :ok + update_issuable(time_estimate: 0) + end + + desc "Add spent time for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + requires :duration, type: String, desc: 'The duration to be parsed' + end + post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do + authorize! update_issuable_key, load_issuable + + update_issuable(spend_time: { + duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)), + user: current_user + }) + end + + desc "Reset spent time for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + end + post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do + authorize! update_issuable_key, load_issuable + + status :ok + update_issuable(spend_time: { duration: :reset, user: current_user }) + end + + desc "Show time stats for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + end + get ":id/#{issuable_collection_name}/:#{issuable_key}/time_stats" do + authorize! read_issuable_key, load_issuable + + present load_issuable, with: ::API::Entities::IssuableTimeStats + end + end + end + end +end diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 3c4ba5d50e6..cd745d35e7c 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -68,7 +68,8 @@ module Backup end def restore - Gitlab.config.repositories.storages.each do |name, path| + Gitlab.config.repositories.storages.each do |name, repository_storage| + path = repository_storage['path'] next unless File.exist?(path) # Move repos dir to 'repositories.old' dir @@ -199,7 +200,7 @@ module Backup private def repository_storage_paths_args - Gitlab.config.repositories.storages.values + Gitlab.config.repositories.storages.values.map { |rs| rs['path'] } end end end diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb index a8c1ca0c60a..d6138816e70 100644 --- a/lib/banzai/filter/emoji_filter.rb +++ b/lib/banzai/filter/emoji_filter.rb @@ -17,8 +17,8 @@ module Banzai next unless content.include?(':') || node.text.match(emoji_unicode_pattern) - html = emoji_name_image_filter(content) - html = emoji_unicode_image_filter(html) + html = emoji_unicode_element_unicode_filter(content) + html = emoji_name_element_unicode_filter(html) next if html == content @@ -27,33 +27,30 @@ module Banzai doc end - # Replace :emoji: with corresponding images. + # Replace :emoji: with corresponding gl-emoji unicode. # # text - String text to replace :emoji: in. # - # Returns a String with :emoji: replaced with images. - def emoji_name_image_filter(text) + # Returns a String with :emoji: replaced with gl-emoji unicode. + def emoji_name_element_unicode_filter(text) text.gsub(emoji_pattern) do |match| name = $1 - emoji_image_tag(name, emoji_url(name)) + Gitlab::Emoji.gl_emoji_tag(name) end end - # Replace unicode emoji with corresponding images if they exist. + # Replace unicode emoji with corresponding gl-emoji unicode. # # text - String text to replace unicode emoji in. # - # Returns a String with unicode emoji replaced with images. - def emoji_unicode_image_filter(text) + # Returns a String with unicode emoji replaced with gl-emoji unicode. + def emoji_unicode_element_unicode_filter(text) text.gsub(emoji_unicode_pattern) do |moji| - emoji_image_tag(Gitlab::Emoji.emojis_by_moji[moji]['name'], emoji_unicode_url(moji)) + emoji_info = Gitlab::Emoji.emojis_by_moji[moji] + Gitlab::Emoji.gl_emoji_tag(emoji_info['name']) end end - def emoji_image_tag(emoji_name, emoji_url) - "<img class='emoji' title=':#{emoji_name}:' alt=':#{emoji_name}:' src='#{emoji_url}' height='20' width='20' align='absmiddle' />" - end - # Build a regexp that matches all valid :emoji: names. def self.emoji_pattern @emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/ @@ -66,52 +63,13 @@ module Banzai private - def emoji_url(name) - emoji_path = emoji_filename(name) - - if context[:asset_host] - # Asset host is specified. - url_to_image(emoji_path) - elsif context[:asset_root] - # Gitlab url is specified - File.join(context[:asset_root], url_to_image(emoji_path)) - else - # All other cases - url_to_image(emoji_path) - end - end - - def emoji_unicode_url(moji) - emoji_unicode_path = emoji_unicode_filename(moji) - - if context[:asset_host] - url_to_image(emoji_unicode_path) - elsif context[:asset_root] - File.join(context[:asset_root], url_to_image(emoji_unicode_path)) - else - url_to_image(emoji_unicode_path) - end - end - - def url_to_image(image) - ActionController::Base.helpers.url_to_image(image) - end - def emoji_pattern self.class.emoji_pattern end - def emoji_filename(name) - "#{Gitlab::Emoji.emoji_filename(name)}.png" - end - def emoji_unicode_pattern self.class.emoji_unicode_pattern end - - def emoji_unicode_filename(name) - "#{Gitlab::Emoji.emoji_unicode_filename(name)}.png" - end end end end diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index b51e76d93f2..746e76a1b1f 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -24,7 +24,7 @@ module Ci new_update = current_runner.ensure_runner_queue_value - result = Ci::RegisterBuildService.new(current_runner).execute + result = Ci::RegisterJobService.new(current_runner).execute if result.valid? if result.build diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 0a0bd0e781c..eee5601b0ed 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -2,9 +2,17 @@ module Gitlab module Auth MissingPersonalTokenError = Class.new(StandardError) - SCOPES = [:api, :read_user].freeze + # Scopes used for GitLab API access + API_SCOPES = [:api, :read_user].freeze + + # Scopes used for OpenID Connect + OPENID_SCOPES = [:openid].freeze + + # Default scopes for OAuth applications that don't define their own DEFAULT_SCOPES = [:api].freeze - OPTIONAL_SCOPES = SCOPES - DEFAULT_SCOPES + + # Other available scopes + OPTIONAL_SCOPES = (API_SCOPES + OPENID_SCOPES - DEFAULT_SCOPES).freeze class << self def find_for_git_client(login, password, project:, ip:) @@ -18,8 +26,8 @@ module Gitlab build_access_token_check(login, password) || lfs_token_check(login, password) || oauth_access_token_check(login, password) || - personal_access_token_check(login, password) || user_with_password_for_git(login, password) || + personal_access_token_check(password) || Gitlab::Auth::Result.new rate_limit!(ip, success: result.success?, login: login) @@ -40,7 +48,7 @@ module Gitlab Gitlab::LDAP::Authentication.login(login, password) else - user if user.valid_password?(password) + user if user.active? && user.valid_password?(password) end end end @@ -105,14 +113,13 @@ module Gitlab end end - def personal_access_token_check(login, password) - if login && password - token = PersonalAccessToken.active.find_by_token(password) - validation = User.by_login(login) + def personal_access_token_check(password) + return unless password.present? - if valid_personal_access_token?(token, validation) - Gitlab::Auth::Result.new(validation, nil, :personal_token, full_authentication_abilities) - end + token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password) + + if token && valid_api_token?(token) + Gitlab::Auth::Result.new(token.user, nil, :personal_token, full_authentication_abilities) end end @@ -120,10 +127,6 @@ module Gitlab token && token.accessible? && valid_api_token?(token) end - def valid_personal_access_token?(token, user) - token && token.user == user && valid_api_token?(token) - end - def valid_api_token?(token) AccessTokenValidationService.new(token).include_any_scope?(['api']) end diff --git a/lib/gitlab/award_emoji.rb b/lib/gitlab/award_emoji.rb deleted file mode 100644 index 7555326d384..00000000000 --- a/lib/gitlab/award_emoji.rb +++ /dev/null @@ -1,84 +0,0 @@ -module Gitlab - class AwardEmoji - CATEGORIES = { - objects: "Objects", - travel: "Travel", - symbols: "Symbols", - nature: "Nature", - people: "People", - activity: "Activity", - flags: "Flags", - food: "Food" - }.with_indifferent_access - - def self.normalize_emoji_name(name) - aliases[name] || name - end - - def self.emoji_by_category - unless @emoji_by_category - @emoji_by_category = Hash.new { |h, key| h[key] = [] } - - emojis.each do |emoji_name, data| - data["name"] = emoji_name - - # Skip Fitzpatrick(tone) modifiers - next if data["category"] == "modifier" - - category = data["category"] - - @emoji_by_category[category] << data - end - - @emoji_by_category = @emoji_by_category.sort.to_h - end - - @emoji_by_category - end - - def self.emojis - @emojis ||= - begin - json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' ) - JSON.parse(File.read(json_path)) - end - end - - def self.aliases - @aliases ||= - begin - json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json') - JSON.parse(File.read(json_path)) - end - end - - # Returns an Array of Emoji names and their asset URLs. - def self.urls - @urls ||= begin - path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json') - # Construct the full asset path ourselves because - # ActionView::Helpers::AssetUrlHelper.asset_url is slow for hundreds - # of entries since it has to do a lot of extra work (e.g. regexps). - prefix = Gitlab::Application.config.assets.prefix - digest = Gitlab::Application.config.assets.digest - base = - if defined?(Gitlab::Application.config.relative_url_root) && Gitlab::Application.config.relative_url_root - Gitlab::Application.config.relative_url_root - else - '' - end - - JSON.parse(File.read(path)).map do |hash| - fname = - if digest - "#{hash['unicode']}-#{hash['digest']}" - else - hash['unicode'] - end - - { name: hash['name'], path: File.join(base, prefix, "#{fname}.png") } - end - end - end - end -end diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb new file mode 100644 index 00000000000..c62aeb60fa9 --- /dev/null +++ b/lib/gitlab/ci/build/image.rb @@ -0,0 +1,33 @@ +module Gitlab + module Ci + module Build + class Image + attr_reader :name + + class << self + def from_image(job) + image = Gitlab::Ci::Build::Image.new(job.options[:image]) + return unless image.valid? + image + end + + def from_services(job) + services = job.options[:services].to_a.map do |service| + Gitlab::Ci::Build::Image.new(service) + end + + services.select(&:valid?).compact + end + end + + def initialize(image) + @name = image + end + + def valid? + @name.present? + end + end + end + end +end diff --git a/lib/gitlab/ci/build/step.rb b/lib/gitlab/ci/build/step.rb new file mode 100644 index 00000000000..1877429ac46 --- /dev/null +++ b/lib/gitlab/ci/build/step.rb @@ -0,0 +1,46 @@ +module Gitlab + module Ci + module Build + class Step + WHEN_ON_FAILURE = 'on_failure'.freeze + WHEN_ON_SUCCESS = 'on_success'.freeze + WHEN_ALWAYS = 'always'.freeze + + attr_reader :name + attr_writer :script + attr_accessor :timeout, :when, :allow_failure + + class << self + def from_commands(job) + self.new(:script).tap do |step| + step.script = job.commands + step.timeout = job.timeout + step.when = WHEN_ON_SUCCESS + end + end + + def from_after_script(job) + after_script = job.options[:after_script] + return unless after_script + + self.new(:after_script).tap do |step| + step.script = after_script + step.timeout = job.timeout + step.when = WHEN_ALWAYS + step.allow_failure = true + end + end + end + + def initialize(name) + @name = name + @allow_failure = false + end + + def script + @script.split("\n") + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb index 066643ccfcc..f074df9c7a1 100644 --- a/lib/gitlab/ci/config/entry/cache.rb +++ b/lib/gitlab/ci/config/entry/cache.rb @@ -22,6 +22,12 @@ module Gitlab entry :paths, Entry::Paths, description: 'Specify which paths should be cached across builds.' + + helpers :key + + def value + super.merge(key: key_value) + end end end end diff --git a/lib/gitlab/ci/config/entry/key.rb b/lib/gitlab/ci/config/entry/key.rb index 0e4c9fe6edc..f27ad0a7759 100644 --- a/lib/gitlab/ci/config/entry/key.rb +++ b/lib/gitlab/ci/config/entry/key.rb @@ -11,6 +11,10 @@ module Gitlab validations do validates :config, key: true end + + def self.default + 'default' + end end end end diff --git a/lib/gitlab/ci/config/entry/node.rb b/lib/gitlab/ci/config/entry/node.rb index 55a5447ab51..a6a914d79c1 100644 --- a/lib/gitlab/ci/config/entry/node.rb +++ b/lib/gitlab/ci/config/entry/node.rb @@ -70,6 +70,12 @@ module Gitlab true end + def inspect + val = leaf? ? config : descendants + unspecified = specified? ? '' : '(unspecified) ' + "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>" + end + def self.default end diff --git a/lib/gitlab/ci/config/entry/undefined.rb b/lib/gitlab/ci/config/entry/undefined.rb index b33b8238230..1171ac10f22 100644 --- a/lib/gitlab/ci/config/entry/undefined.rb +++ b/lib/gitlab/ci/config/entry/undefined.rb @@ -29,6 +29,10 @@ module Gitlab def relevant? false end + + def inspect + "#<#{self.class.name}>" + end end end end diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb index bbbca8acc40..42703545c4f 100644 --- a/lib/gitlab/emoji.rb +++ b/lib/gitlab/emoji.rb @@ -1,7 +1,7 @@ module Gitlab module Emoji extend self - + def emojis Gemojione.index.instance_variable_get(:@emoji_by_name) end @@ -18,6 +18,10 @@ module Gitlab emojis.keys end + def emojis_aliases + @emoji_aliases ||= JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'aliases.json'))) + end + def emoji_filename(name) emojis[name]["unicode"] end @@ -25,5 +29,42 @@ module Gitlab def emoji_unicode_filename(moji) emojis_by_moji[moji]["unicode"] end + + def emoji_unicode_version(name) + @emoji_unicode_versions_by_name ||= JSON.parse(File.read(Rails.root.join('node_modules', 'emoji-unicode-version', 'emoji-unicode-version-map.json'))) + @emoji_unicode_versions_by_name[name] + end + + def normalize_emoji_name(name) + emojis_aliases[name] || name + end + + def emoji_image_tag(name, src) + "<img class='emoji' title=':#{name}:' alt=':#{name}:' src='#{src}' height='20' width='20' align='absmiddle' />" + end + + # CSS sprite fallback takes precedence over image fallback + def gl_emoji_tag(name, image: false, sprite: false, force_fallback: false) + emoji_name = emojis_aliases[name] || name + emoji_info = emojis[emoji_name] + emoji_fallback_image_source = ActionController::Base.helpers.url_to_image("emoji/#{emoji_info['name']}.png") + emoji_fallback_sprite_class = "emoji-#{emoji_name}" + + data = { + name: emoji_name, + unicode_version: emoji_unicode_version(emoji_name) + } + data[:fallback_src] = emoji_fallback_image_source if image + data[:fallback_sprite_class] = emoji_fallback_sprite_class if sprite + ActionController::Base.helpers.content_tag 'gl-emoji', + class: ("emoji-icon #{emoji_fallback_sprite_class}" if force_fallback && sprite), + data: data do + if force_fallback && !sprite + emoji_image_tag(emoji_name, emoji_fallback_image_source) + else + emoji_info['moji'] + end + end + end end end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 6540730ca7a..228ef7bb7a9 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -354,6 +354,18 @@ module Gitlab lines.map! { |c| Rugged::Commit.new(rugged, c.strip) } end + def count_commits(options) + cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} rev-list] + cmd << "--after=#{options[:after].iso8601}" if options[:after] + cmd << "--before=#{options[:before].iso8601}" if options[:before] + cmd += %W[--count #{options[:ref]}] + cmd += %W[-- #{options[:path]}] if options[:path].present? + + raw_output = IO.popen(cmd) { |io| io.read } + + raw_output.to_i + end + def sha_from_ref(ref) rev_parse_target(ref).oid end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 9c384069661..6c275a8d5de 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -4,16 +4,17 @@ module Gitlab gon.api_version = 'v3' # v4 Is not officially released yet, therefore can't be considered as "frozen" gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s gon.max_file_size = current_application_settings.max_attachment_size + gon.asset_host = ActionController::Base.asset_host gon.relative_url_root = Gitlab.config.gitlab.relative_url_root gon.shortcuts_path = help_page_path('shortcuts') gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class - gon.award_menu_url = emojis_path gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css') gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js') if current_user gon.current_user_id = current_user.id gon.current_username = current_user.username + gon.current_user_fullname = current_user.name end end end diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb index 5764ab15652..6023fa1820f 100644 --- a/lib/gitlab/middleware/go.rb +++ b/lib/gitlab/middleware/go.rb @@ -30,21 +30,69 @@ module Gitlab end def go_body(request) - base_url = Gitlab.config.gitlab.url - # Go subpackages may be in the form of namespace/project/path1/path2/../pathN - # We can just ignore the paths and leave the namespace/project - path_info = request.env["PATH_INFO"] - path_info.sub!(/^\//, '') - project_path = path_info.split('/').first(2).join('/') - request_url = URI.join(base_url, project_path) - domain_path = strip_url(request_url.to_s) + project_url = URI.join(Gitlab.config.gitlab.url, project_path(request)) + import_prefix = strip_url(project_url.to_s) - "<!DOCTYPE html><html><head><meta content='#{domain_path} git #{request_url}.git' name='go-import'></head></html>\n" + "<!DOCTYPE html><html><head><meta content='#{import_prefix} git #{project_url}.git' name='go-import'></head></html>\n" end def strip_url(url) url.gsub(/\Ahttps?:\/\//, '') end + + def project_path(request) + path_info = request.env["PATH_INFO"] + path_info.sub!(/^\//, '') + + # Go subpackages may be in the form of `namespace/project/path1/path2/../pathN`. + # In a traditional project with a single namespace, this would denote repo + # `namespace/project` with subpath `path1/path2/../pathN`, but with nested + # groups, this could also be `namespace/project/path1` with subpath + # `path2/../pathN`, for example. + + # We find all potential project paths out of the path segments + path_segments = path_info.split('/') + simple_project_path = path_segments.first(2).join('/') + + # If the path is at most 2 segments long, it is a simple `namespace/project` path and we're done + return simple_project_path if path_segments.length <= 2 + + project_paths = [] + begin + project_paths << path_segments.join('/') + path_segments.pop + end while path_segments.length >= 2 + + # We see if a project exists with any of these potential paths + project = project_for_paths(project_paths, request) + + if project + # If a project is found and the user has access, we return the full project path + project.full_path + else + # If not, we return the first two components as if it were a simple `namespace/project` path, + # so that we don't reveal the existence of a nested project the user doesn't have access to. + # This means that for an unauthenticated request to `group/subgroup/project/subpackage` + # for a private `group/subgroup/project` with subpackage path `subpackage`, GitLab will respond + # as if the user is looking for project `group/subgroup`, with subpackage path `project/subpackage`. + # Since `go get` doesn't authenticate by default, this means that + # `go get gitlab.com/group/subgroup/project/subpackage` will not work for private projects. + # `go get gitlab.com/group/subgroup/project.git/subpackage` will work, since Go is smart enough + # to figure that out. `import 'gitlab.com/...'` behaves the same as `go get`. + simple_project_path + end + end + + def project_for_paths(paths, request) + project = Project.where_full_path_in(paths).first + return unless Ability.allowed?(current_user(request), :read_project, project) + + project + end + + def current_user(request) + request.env['warden']&.authenticate + end end end end diff --git a/lib/gitlab/prometheus.rb b/lib/gitlab/prometheus.rb new file mode 100644 index 00000000000..62239779454 --- /dev/null +++ b/lib/gitlab/prometheus.rb @@ -0,0 +1,70 @@ +module Gitlab + PrometheusError = Class.new(StandardError) + + # Helper methods to interact with Prometheus network services & resources + class Prometheus + attr_reader :api_url + + def initialize(api_url:) + @api_url = api_url + end + + def ping + json_api_get('query', query: '1') + end + + def query(query) + get_result('vector') do + json_api_get('query', query: query) + end + end + + def query_range(query, start: 8.hours.ago) + get_result('matrix') do + json_api_get('query_range', + query: query, + start: start.to_f, + end: Time.now.utc.to_f, + step: 1.minute.to_i) + end + end + + private + + def json_api_get(type, args = {}) + get(join_api_url(type, args)) + rescue Errno::ECONNREFUSED + raise PrometheusError, 'Connection refused' + end + + def join_api_url(type, args = {}) + url = URI.parse(api_url) + rescue URI::Error + raise PrometheusError, "Invalid API URL: #{api_url}" + else + url.path = [url.path.sub(%r{/+\z}, ''), 'api', 'v1', type].join('/') + url.query = args.to_query + + url.to_s + end + + def get(url) + handle_response(HTTParty.get(url)) + end + + def handle_response(response) + if response.code == 200 && response['status'] == 'success' + response['data'] || {} + elsif response.code == 400 + raise PrometheusError, response['error'] || 'Bad data received' + else + raise PrometheusError, "#{response.code} - #{response.body}" + end + end + + def get_result(expected_type) + data = yield + data['result'] if data['resultType'] == expected_type + end + end +end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 3ff9f9eb5e7..eae1a0abf06 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -8,6 +8,7 @@ module Gitlab VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'.freeze INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'.freeze INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'.freeze + NOTIFICATION_CHANNEL = 'workhorse:notifications'.freeze # Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32 # bytes https://tools.ietf.org/html/rfc4868#section-2.6 @@ -154,6 +155,18 @@ module Gitlab Rails.root.join('.gitlab_workhorse_secret') end + def set_key_and_notify(key, value, expire: nil, overwrite: true) + Gitlab::Redis.with do |redis| + result = redis.set(key, value, ex: expire, nx: !overwrite) + if result + redis.publish(NOTIFICATION_CHANNEL, "#{key}=#{value}") + value + else + redis.get(key) + end + end + end + protected def encode(hash) diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake index 993112aee3b..1f93b5a4dd2 100644 --- a/lib/tasks/gemojione.rake +++ b/lib/tasks/gemojione.rake @@ -5,29 +5,29 @@ namespace :gemojione do require 'json' dir = Gemojione.images_path - digests = [] - aliases = Hash.new { |hash, key| hash[key] = [] } - aliases_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json') - - JSON.parse(File.read(aliases_path)).each do |alias_name, real_name| - aliases[real_name] << alias_name - end - - Gitlab::AwardEmoji.emojis.map do |name, emoji_hash| - fpath = File.join(dir, "#{emoji_hash['unicode']}.png") - digest = Digest::SHA256.file(fpath).hexdigest - - digests << { name: name, unicode: emoji_hash['unicode'], digest: digest } + resultant_emoji_map = {} + + Gitlab::Emoji.emojis.each do |name, emoji_hash| + # Ignore aliases + unless Gitlab::Emoji.emojis_aliases.key?(name) + fpath = File.join(dir, "#{emoji_hash['unicode']}.png") + hash_digest = Digest::SHA256.file(fpath).hexdigest + + entry = { + category: emoji_hash['category'], + moji: emoji_hash['moji'], + unicodeVersion: Gitlab::Emoji.emoji_unicode_version(name), + digest: hash_digest, + } - aliases[name].each do |alias_name| - digests << { name: alias_name, unicode: emoji_hash['unicode'], digest: digest } + resultant_emoji_map[name] = entry end end out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json') File.open(out, 'w') do |handle| - handle.write(JSON.pretty_generate(digests)) + handle.write(JSON.pretty_generate(resultant_emoji_map)) end end @@ -55,21 +55,40 @@ namespace :gemojione do SPRITESHEET_WIDTH = 860 SPRITESHEET_HEIGHT = 840 + # Setup a map to rename image files + emoji_unicode_string_to_name_map = {} + Gitlab::Emoji.emojis.each do |name, emoji_hash| + # Ignore aliases + unless Gitlab::Emoji.emojis_aliases.key?(name) + emoji_unicode_string_to_name_map[emoji_hash['unicode']] = name + end + end + + # Copy the Gemojione assets to the temporary folder for renaming + emoji_dir = "app/assets/images/emoji" + FileUtils.rm_rf(emoji_dir) + FileUtils.mkdir_p(emoji_dir, mode: 0700) + FileUtils.cp_r(File.join(Gemojione.images_path, '.'), emoji_dir) + Dir[File.join(emoji_dir, "**/*.png")].each do |png| + image_path = png + rename_to_named_emoji_image!(emoji_unicode_string_to_name_map, image_path) + end + Dir.mktmpdir do |tmpdir| - # Copy the Gemojione assets to the temporary folder for resizing - FileUtils.cp_r(Gemojione.images_path, tmpdir) + FileUtils.cp_r(File.join(emoji_dir, '.'), tmpdir) Dir.chdir(tmpdir) do Dir["**/*.png"].each do |png| - resize!(File.join(tmpdir, png), SIZE) + tmp_image_path = File.join(tmpdir, png) + resize!(tmp_image_path, SIZE) end end - style_path = Rails.root.join(*%w(app assets stylesheets pages emojis.scss)) + style_path = Rails.root.join(*%w(app assets stylesheets framework emoji-sprites.scss)) # Combine the resized assets into a packed sprite and re-generate the SCSS SpriteFactory.cssurl = "image-url('$IMAGE')" - SpriteFactory.run!(File.join(tmpdir, 'png'), { + SpriteFactory.run!(tmpdir, { output_style: style_path, output_image: "app/assets/images/emoji.png", selector: '.emoji-', @@ -83,7 +102,7 @@ namespace :gemojione do # let's simplify it system(%Q(sed -i '' "s/width: #{SIZE}px; height: #{SIZE}px; background: image-url('emoji.png')/background-position:/" #{style_path})) system(%Q(sed -i '' "s/ no-repeat//" #{style_path})) - system(%Q(sed -i '' "s/ 0px/ 0/" #{style_path})) + system(%Q(sed -i '' "s/ 0px/ 0/g" #{style_path})) # Append a generic rule that applies to all Emojis File.open(style_path, 'a') do |f| @@ -92,6 +111,8 @@ namespace :gemojione do .emoji-icon { background-image: image-url('emoji.png'); background-repeat: no-repeat; + color: transparent; + text-indent: -99em; height: #{SIZE}px; width: #{SIZE}px; @@ -112,16 +133,17 @@ namespace :gemojione do # Now do it again but for Retina Dir.mktmpdir do |tmpdir| # Copy the Gemojione assets to the temporary folder for resizing - FileUtils.cp_r(Gemojione.images_path, tmpdir) + FileUtils.cp_r(File.join(emoji_dir, '.'), tmpdir) Dir.chdir(tmpdir) do Dir["**/*.png"].each do |png| - resize!(File.join(tmpdir, png), RETINA) + tmp_image_path = File.join(tmpdir, png) + resize!(tmp_image_path, RETINA) end end # Combine the resized assets into a packed sprite and re-generate the SCSS - SpriteFactory.run!(File.join(tmpdir), { + SpriteFactory.run!(tmpdir, { output_image: "app/assets/images/emoji@2x.png", style: false, nocomments: true, @@ -155,4 +177,20 @@ namespace :gemojione do image.write(image_path) { self.quality = 100 } image.destroy! end + + EMOJI_IMAGE_PATH_RE = /(.*?)(([0-9a-f]-?)+)\.png$/i + def rename_to_named_emoji_image!(emoji_unicode_string_to_name_map, image_path) + # Rename file from unicode to emoji name + matches = EMOJI_IMAGE_PATH_RE.match(image_path) + preceding_path = matches[1] + unicode_string = matches[2] + name = emoji_unicode_string_to_name_map[unicode_string] + if name + new_png_path = File.join(preceding_path, "#{name}.png") + FileUtils.mv(image_path, new_png_path) + new_png_path + else + puts "Warning: emoji_unicode_string_to_name_map missing entry for #{unicode_string}. Full path: #{image_path}" + end + end end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 38edd49b6ed..a6f8c4ced5d 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -354,7 +354,8 @@ namespace :gitlab do def check_repo_base_exists puts "Repo base directory exists?" - Gitlab.config.repositories.storages.each do |name, repo_base_path| + Gitlab.config.repositories.storages.each do |name, repository_storage| + repo_base_path = repository_storage['path'] print "#{name}... " if File.exist?(repo_base_path) @@ -378,7 +379,8 @@ namespace :gitlab do def check_repo_base_is_not_symlink puts "Repo storage directories are symlinks?" - Gitlab.config.repositories.storages.each do |name, repo_base_path| + Gitlab.config.repositories.storages.each do |name, repository_storage| + repo_base_path = repository_storage['path'] print "#{name}... " unless File.exist?(repo_base_path) @@ -401,7 +403,8 @@ namespace :gitlab do def check_repo_base_permissions puts "Repo paths access is drwxrws---?" - Gitlab.config.repositories.storages.each do |name, repo_base_path| + Gitlab.config.repositories.storages.each do |name, repository_storage| + repo_base_path = repository_storage['path'] print "#{name}... " unless File.exist?(repo_base_path) @@ -431,7 +434,8 @@ namespace :gitlab do gitlab_shell_owner_group = Gitlab.config.gitlab_shell.owner_group puts "Repo paths owned by #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group}?" - Gitlab.config.repositories.storages.each do |name, repo_base_path| + Gitlab.config.repositories.storages.each do |name, repository_storage| + repo_base_path = repository_storage['path'] print "#{name}... " unless File.exist?(repo_base_path) @@ -810,8 +814,8 @@ namespace :gitlab do namespace :repo do desc "GitLab | Check the integrity of the repositories managed by GitLab" task check: :environment do - Gitlab.config.repositories.storages.each do |name, path| - namespace_dirs = Dir.glob(File.join(path, '*')) + Gitlab.config.repositories.storages.each do |name, repository_storage| + namespace_dirs = Dir.glob(File.join(repository_storage['path'], '*')) namespace_dirs.each do |namespace_dir| repo_dirs = Dir.glob(File.join(namespace_dir, '*')) diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index daf7382dd02..f76bef5f4bf 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -6,7 +6,8 @@ namespace :gitlab do remove_flag = ENV['REMOVE'] namespaces = Namespace.pluck(:path) - Gitlab.config.repositories.storages.each do |name, git_base_path| + Gitlab.config.repositories.storages.each do |name, repository_storage| + git_base_path = repository_storage['path'] all_dirs = Dir.glob(git_base_path + '/*') puts git_base_path.color(:yellow) @@ -47,7 +48,8 @@ namespace :gitlab do warn_user_is_not_gitlab move_suffix = "+orphaned+#{Time.now.to_i}" - Gitlab.config.repositories.storages.each do |name, repo_root| + Gitlab.config.repositories.storages.each do |name, repository_storage| + repo_root = repository_storage['path'] # Look for global repos (legacy, depth 1) and normal repos (depth 2) IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find| find.each_line do |path| diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index 66e7b7685f7..48bd9139ce8 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -11,7 +11,8 @@ namespace :gitlab do # desc "GitLab | Import bare repositories from repositories -> storages into GitLab project instance" task repos: :environment do - Gitlab.config.repositories.storages.each do |name, git_base_path| + Gitlab.config.repositories.storages.each_value do |repository_storage| + git_base_path = repository_storage['path'] repos_to_import = Dir.glob(git_base_path + '/**/*.git') repos_to_import.each do |repo_path| diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake index ae78fe64eb8..a2a2db487b7 100644 --- a/lib/tasks/gitlab/info.rake +++ b/lib/tasks/gitlab/info.rake @@ -14,6 +14,8 @@ namespace :gitlab do rake_version = run_and_match(%w(rake --version), /[\d\.]+/).try(:to_s) # check redis version redis_version = run_and_match(%w(redis-cli --version), /redis-cli (\d+\.\d+\.\d+)/).to_a + # check Git version + git_version = run_and_match([Gitlab.config.git.bin_path, '--version'], /git version ([\d\.]+)/).to_a puts "" puts "System information".color(:yellow) @@ -26,6 +28,7 @@ namespace :gitlab do puts "Bundler Version:#{bunder_version || "unknown".color(:red)}" puts "Rake Version:\t#{rake_version || "unknown".color(:red)}" puts "Redis Version:\t#{redis_version[1] || "unknown".color(:red)}" + puts "Git Version:\t#{git_version[1] || "unknown".color(:red)}" puts "Sidekiq Version:#{Sidekiq::VERSION}" # check database adapter @@ -62,8 +65,8 @@ namespace :gitlab do puts "GitLab Shell".color(:yellow) puts "Version:\t#{gitlab_shell_version || "unknown".color(:red)}" puts "Repository storage paths:" - Gitlab.config.repositories.storages.each do |name, path| - puts "- #{name}: \t#{path}" + Gitlab.config.repositories.storages.each do |name, repository_storage| + puts "- #{name}: \t#{repository_storage['path']}" end puts "Hooks:\t\t#{Gitlab.config.gitlab_shell.hooks_path}" puts "Git:\t\t#{Gitlab.config.git.bin_path}" diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb index 2a999ad6959..bb755ae689b 100644 --- a/lib/tasks/gitlab/task_helpers.rb +++ b/lib/tasks/gitlab/task_helpers.rb @@ -130,8 +130,8 @@ module Gitlab end def all_repos - Gitlab.config.repositories.storages.each do |name, path| - IO.popen(%W(find #{path} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find| + Gitlab.config.repositories.storages.each_value do |repository_storage| + IO.popen(%W(find #{repository_storage['path']} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find| find.each_line do |path| yield path.chomp end @@ -140,7 +140,7 @@ module Gitlab end def repository_storage_paths_args - Gitlab.config.repositories.storages.values + Gitlab.config.repositories.storages.values.map { |rs| rs['path'] } end def user_home diff --git a/package.json b/package.json index 6c6a490b0f9..efa3a63e693 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ "bootstrap-sass": "^3.3.6", "compression-webpack-plugin": "^0.3.2", "d3": "^3.5.11", + "document-register-element": "^1.3.0", "dropzone": "^4.2.0", + "emoji-unicode-version": "^0.2.1", "es6-promise": "^4.0.5", "jquery": "^2.2.1", "jquery-ujs": "^1.2.1", @@ -29,6 +31,8 @@ "raw-loader": "^0.5.1", "select2": "3.5.2-browserify", "stats-webpack-plugin": "^0.4.3", + "string.fromcodepoint": "^0.2.1", + "string.prototype.codepointat": "^0.2.0", "timeago.js": "^2.0.5", "underscore": "^1.8.3", "vue": "^2.1.10", diff --git a/rubocop/cop/migration/add_concurrent_index.rb b/rubocop/cop/migration/add_concurrent_index.rb new file mode 100644 index 00000000000..332fb7dcbd7 --- /dev/null +++ b/rubocop/cop/migration/add_concurrent_index.rb @@ -0,0 +1,34 @@ +require_relative '../../migration_helpers' + +module RuboCop + module Cop + module Migration + # Cop that checks if `add_concurrent_index` is used with `up`/`down` methods + # and not `change`. + class AddConcurrentIndex < RuboCop::Cop::Cop + include MigrationHelpers + + MSG = '`add_concurrent_index` is not reversible so you must manually define ' \ + 'the `up` and `down` methods in your migration class, using `remove_index` in `down`'.freeze + + def on_send(node) + return unless in_migration?(node) + + name = node.children[1] + + return unless name == :add_concurrent_index + + node.each_ancestor(:def) do |def_node| + next unless method_name(def_node) == :change + + add_offense(def_node, :name) + end + end + + def method_name(node) + node.children.first + end + end + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index ea8e0f64b0d..a50a522cf9d 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -3,4 +3,5 @@ require_relative 'cop/gem_fetcher' require_relative 'cop/migration/add_column' require_relative 'cop/migration/add_column_with_default' require_relative 'cop/migration/add_concurrent_foreign_key' +require_relative 'cop/migration/add_concurrent_index' require_relative 'cop/migration/add_index' diff --git a/spec/controllers/admin/applications_controller_spec.rb b/spec/controllers/admin/applications_controller_spec.rb new file mode 100644 index 00000000000..e311b8a63b2 --- /dev/null +++ b/spec/controllers/admin/applications_controller_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Admin::ApplicationsController do + let(:admin) { create(:admin) } + let(:application) { create(:oauth_application, owner_id: nil, owner_type: nil) } + + before do + sign_in(admin) + end + + describe 'GET #new' do + it 'renders the application form' do + get :new + + expect(response).to render_template :new + expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes) + end + end + + describe 'GET #edit' do + it 'renders the application form' do + get :edit, id: application.id + + expect(response).to render_template :edit + expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes) + end + end + + describe 'POST #create' do + it 'creates the application' do + expect do + post :create, doorkeeper_application: attributes_for(:application) + end.to change { Doorkeeper::Application.count }.by(1) + + application = Doorkeeper::Application.last + + expect(response).to redirect_to(admin_application_path(application)) + end + + it 'renders the application form on errors' do + expect do + post :create, doorkeeper_application: attributes_for(:application).merge(redirect_uri: nil) + end.not_to change { Doorkeeper::Application.count } + + expect(response).to render_template :new + expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes) + end + end + + describe 'PATCH #update' do + it 'updates the application' do + patch :update, id: application.id, doorkeeper_application: { redirect_uri: 'http://example.com/' } + + expect(response).to redirect_to(admin_application_path(application)) + expect(application.reload.redirect_uri).to eq 'http://example.com/' + end + + it 'renders the application form on errors' do + patch :update, id: application.id, doorkeeper_application: { redirect_uri: nil } + + expect(response).to render_template :edit + expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes) + end + end +end diff --git a/spec/controllers/profiles/personal_access_tokens_spec.rb b/spec/controllers/profiles/personal_access_tokens_spec.rb index 9d5f4c99f6d..dfed1de2046 100644 --- a/spec/controllers/profiles/personal_access_tokens_spec.rb +++ b/spec/controllers/profiles/personal_access_tokens_spec.rb @@ -2,48 +2,55 @@ require 'spec_helper' describe Profiles::PersonalAccessTokensController do let(:user) { create(:user) } + let(:token_attributes) { attributes_for(:personal_access_token) } + + before { sign_in(user) } describe '#create' do def created_token PersonalAccessToken.order(:created_at).last end - before { sign_in(user) } - - it "allows creation of a token" do + it "allows creation of a token with scopes" do name = FFaker::Product.brand + scopes = %w[api read_user] - post :create, personal_access_token: { name: name } + post :create, personal_access_token: token_attributes.merge(scopes: scopes, name: name) expect(created_token).not_to be_nil expect(created_token.name).to eq(name) - expect(created_token.expires_at).to be_nil + expect(created_token.scopes).to eq(scopes) expect(PersonalAccessToken.active).to include(created_token) end it "allows creation of a token with an expiry date" do - expires_at = 5.days.from_now + expires_at = 5.days.from_now.to_date - post :create, personal_access_token: { name: FFaker::Product.brand, expires_at: expires_at } + post :create, personal_access_token: token_attributes.merge(expires_at: expires_at) expect(created_token).not_to be_nil - expect(created_token.expires_at.to_i).to eq(expires_at.to_i) + expect(created_token.expires_at).to eq(expires_at) end + end - context "scopes" do - it "allows creation of a token with scopes" do - post :create, personal_access_token: { name: FFaker::Product.brand, scopes: %w(api read_user) } + describe '#index' do + let!(:active_personal_access_token) { create(:personal_access_token, user: user) } + let!(:inactive_personal_access_token) { create(:personal_access_token, :revoked, user: user) } + let!(:impersonation_personal_access_token) { create(:personal_access_token, :impersonation, user: user) } - expect(created_token).not_to be_nil - expect(created_token.scopes).to eq(%w(api read_user)) - end + before { get :index } - it "allows creation of a token with no scopes" do - post :create, personal_access_token: { name: FFaker::Product.brand, scopes: [] } + it "retrieves active personal access tokens" do + expect(assigns(:active_personal_access_tokens)).to include(active_personal_access_token) + end + + it "retrieves inactive personal access tokens" do + expect(assigns(:inactive_personal_access_tokens)).to include(inactive_personal_access_token) + end - expect(created_token).not_to be_nil - expect(created_token.scopes).to eq([]) - end + it "does not retrieve impersonation personal access tokens" do + expect(assigns(:active_personal_access_tokens)).not_to include(impersonation_personal_access_token) + expect(assigns(:inactive_personal_access_tokens)).not_to include(impersonation_personal_access_token) end end end diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb index 3d0533cb516..15667e8d4b1 100644 --- a/spec/controllers/projects/boards/issues_controller_spec.rb +++ b/spec/controllers/projects/boards/issues_controller_spec.rb @@ -43,6 +43,7 @@ describe Projects::Boards::IssuesController do expect(response).to match_response_schema('issues') expect(parsed_response.length).to eq 2 + expect(development.issues.map(&:relative_position)).not_to include(nil) end end diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 84d119f1867..83d80b376fb 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -187,6 +187,52 @@ describe Projects::EnvironmentsController do end end + describe 'GET #metrics' do + before do + allow(controller).to receive(:environment).and_return(environment) + end + + context 'when environment has no metrics' do + before do + expect(environment).to receive(:metrics).and_return(nil) + end + + it 'returns a metrics page' do + get :metrics, environment_params + + expect(response).to be_ok + end + + context 'when requesting metrics as JSON' do + it 'returns a metrics JSON document' do + get :metrics, environment_params(format: :json) + + expect(response).to have_http_status(204) + expect(json_response).to eq({}) + end + end + end + + context 'when environment has some metrics' do + before do + expect(environment).to receive(:metrics).and_return({ + success: true, + metrics: {}, + last_update: 42 + }) + end + + it 'returns a metrics JSON document' do + get :metrics, environment_params(format: :json) + + expect(response).to be_ok + expect(json_response['success']).to be(true) + expect(json_response['metrics']).to eq({}) + expect(json_response['last_update']).to eq(42) + end + end + end + def environment_params(opts = {}) opts.reverse_merge(namespace_id: project.namespace, project_id: project, diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb new file mode 100644 index 00000000000..f73471f8ca8 --- /dev/null +++ b/spec/controllers/projects/settings/repository_controller_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Projects::Settings::RepositoryController do + let(:project) { create(:project_empty_repo, :public) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + end + + describe 'GET show' do + it 'renders show with 200 status code' do + get :show, namespace_id: project.namespace, project_id: project + + expect(response).to have_http_status(200) + expect(response).to render_template(:show) + end + end +end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 202759664a0..a1ec41322ad 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -158,14 +158,6 @@ describe ProjectsController do expect(response).to render_template('_activity') end - it "renders the readme view" do - allow(controller).to receive(:current_user).and_return(user) - allow(user).to receive(:project_view).and_return('readme') - - get :show, namespace_id: public_project.namespace, id: public_project - expect(response).to render_template('_readme') - end - it "renders the files view" do allow(controller).to receive(:current_user).and_return(user) allow(user).to receive(:project_view).and_return('files') diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index c9584ddf18c..f67d26da0ac 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -1,4 +1,9 @@ require 'spec_helper' +shared_examples 'content not cached without revalidation' do + it 'ensures content will not be cached without revalidation' do + expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate') + end +end describe UploadsController do let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } @@ -50,6 +55,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png' + response + end + end end end @@ -59,6 +71,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png' + response + end + end end end @@ -76,6 +95,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png' + response + end + end end context "when signed in" do @@ -88,6 +114,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png' + response + end + end end end @@ -133,6 +166,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png' + response + end + end end end @@ -157,6 +197,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png' + response + end + end end context "when signed in" do @@ -169,6 +216,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png' + response + end + end end end @@ -205,6 +259,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png' + response + end + end end end @@ -234,6 +295,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + response + end + end end context "when signed in" do @@ -246,6 +314,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + response + end + end end end @@ -291,6 +366,13 @@ describe UploadsController do expect(response).to have_http_status(200) end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + response + end + end end end diff --git a/spec/factories/chat_teams.rb b/spec/factories/chat_teams.rb new file mode 100644 index 00000000000..82f44fa3d15 --- /dev/null +++ b/spec/factories/chat_teams.rb @@ -0,0 +1,9 @@ +FactoryGirl.define do + factory :chat_team, class: ChatTeam do + sequence :team_id do |n| + "abcdefghijklm#{n}" + end + + namespace factory: :group + end +end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 279583c2c44..6b0d084614b 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -15,8 +15,8 @@ FactoryGirl.define do options do { - image: "ruby:2.1", - services: ["postgres"] + image: 'ruby:2.1', + services: ['postgres'] } end @@ -166,5 +166,31 @@ FactoryGirl.define do allow(build).to receive(:commit).and_return build(:commit) end end + + trait :extended_options do + options do + { + image: 'ruby:2.1', + services: ['postgres'], + after_script: "ls\ndate", + artifacts: { + name: 'artifacts_file', + untracked: false, + paths: ['out/'], + when: 'always', + expire_in: '7d' + }, + cache: { + key: 'cache_key', + untracked: false, + paths: ['vendor/*'] + } + } + end + end + + trait :no_options do + options { {} } + end end end diff --git a/spec/factories/oauth_access_grants.rb b/spec/factories/oauth_access_grants.rb new file mode 100644 index 00000000000..543b3e99274 --- /dev/null +++ b/spec/factories/oauth_access_grants.rb @@ -0,0 +1,11 @@ +FactoryGirl.define do + factory :oauth_access_grant do + resource_owner_id { create(:user).id } + application + token { Doorkeeper::OAuth::Helpers::UniqueToken.generate } + expires_in 2.hours + + redirect_uri { application.redirect_uri } + scopes { application.scopes } + end +end diff --git a/spec/factories/oauth_access_tokens.rb b/spec/factories/oauth_access_tokens.rb index ccf02d0719b..a46bc1d8ce8 100644 --- a/spec/factories/oauth_access_tokens.rb +++ b/spec/factories/oauth_access_tokens.rb @@ -2,6 +2,7 @@ FactoryGirl.define do factory :oauth_access_token do resource_owner application - token '123456' + token { Doorkeeper::OAuth::Helpers::UniqueToken.generate } + scopes { application.scopes } end end diff --git a/spec/factories/oauth_applications.rb b/spec/factories/oauth_applications.rb index d116a573830..86cdc208268 100644 --- a/spec/factories/oauth_applications.rb +++ b/spec/factories/oauth_applications.rb @@ -1,7 +1,7 @@ FactoryGirl.define do factory :oauth_application, class: 'Doorkeeper::Application', aliases: [:application] do name { FFaker::Name.name } - uid { FFaker::Name.name } + uid { Doorkeeper::OAuth::Helpers::UniqueToken.generate } redirect_uri { FFaker::Internet.uri('http') } owner owner_type 'User' diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb index 811eab7e15b..7b15ba47de1 100644 --- a/spec/factories/personal_access_tokens.rb +++ b/spec/factories/personal_access_tokens.rb @@ -6,5 +6,22 @@ FactoryGirl.define do revoked false expires_at { 5.days.from_now } scopes ['api'] + impersonation false + + trait :impersonation do + impersonation true + end + + trait :revoked do + revoked true + end + + trait :expired do + expires_at { 1.day.ago } + end + + trait :invalid do + token nil + end end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 70c65bc693a..c6f91e05d83 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -195,4 +195,15 @@ FactoryGirl.define do factory :kubernetes_project, parent: :empty_project do kubernetes_service end + + factory :prometheus_project, parent: :empty_project do + after :create do |project| + project.create_prometheus_service( + active: true, + properties: { + api_url: 'https://prometheus.example.com' + } + ) + end + end end diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb new file mode 100644 index 00000000000..9ff5c2f9d40 --- /dev/null +++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +describe 'Admin > Users > Impersonation Tokens', feature: true, js: true do + let(:admin) { create(:admin) } + let!(:user) { create(:user) } + + def active_impersonation_tokens + find(".table.active-tokens") + end + + def inactive_impersonation_tokens + find(".table.inactive-tokens") + end + + before { login_as(admin) } + + describe "token creation" do + it "allows creation of a token" do + name = FFaker::Product.brand + + visit admin_user_impersonation_tokens_path(user_id: user.username) + fill_in "Name", with: name + + # Set date to 1st of next month + find_field("Expires at").trigger('focus') + find(".pika-next").click + click_on "1" + + # Scopes + check "api" + check "read_user" + + expect { click_on "Create Impersonation Token" }.to change { PersonalAccessTokensFinder.new(impersonation: true).execute.count } + expect(active_impersonation_tokens).to have_text(name) + expect(active_impersonation_tokens).to have_text('In') + expect(active_impersonation_tokens).to have_text('api') + expect(active_impersonation_tokens).to have_text('read_user') + end + end + + describe 'active tokens' do + let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) } + let!(:personal_access_token) { create(:personal_access_token, user: user) } + + it 'only shows impersonation tokens' do + visit admin_user_impersonation_tokens_path(user_id: user.username) + + expect(active_impersonation_tokens).to have_text(impersonation_token.name) + expect(active_impersonation_tokens).not_to have_text(personal_access_token.name) + end + end + + describe "inactive tokens" do + let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) } + + it "allows revocation of an active impersonation token" do + visit admin_user_impersonation_tokens_path(user_id: user.username) + + click_on "Revoke" + + expect(inactive_impersonation_tokens).to have_text(impersonation_token.name) + end + + it "moves expired tokens to the 'inactive' section" do + impersonation_token.update(expires_at: 5.days.ago) + + visit admin_user_impersonation_tokens_path(user_id: user.username) + + expect(inactive_impersonation_tokens).to have_text(impersonation_token.name) + end + end +end diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index b740e191f48..55e10a1a89b 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -57,7 +57,7 @@ describe "User Feed", feature: true do end it 'has XHTML summaries in notes' do - expect(body).to match /Bug confirmed <img[^>]*\/>/ + expect(body).to match /Bug confirmed <gl-emoji[^>]*>/ end it 'has XHTML summaries in merge request descriptions' do diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index e247bfa2980..ecc356f2505 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -71,16 +71,16 @@ describe 'Issue Boards', feature: true, js: true do let!(:list1) { create(:list, board: board, label: planning, position: 0) } let!(:list2) { create(:list, board: board, label: development, position: 1) } - let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning]) } - let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning]) } - let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning]) } - let!(:issue3) { create(:labeled_issue, project: project, labels: [planning]) } - let!(:issue4) { create(:labeled_issue, project: project, labels: [planning]) } - let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone) } - let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) } - let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) } + let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) } + let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning], relative_position: 8) } + let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning], relative_position: 7) } + let!(:issue3) { create(:labeled_issue, project: project, labels: [planning], relative_position: 6) } + let!(:issue4) { create(:labeled_issue, project: project, labels: [planning], relative_position: 5) } + let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone, relative_position: 4) } + let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development], relative_position: 3) } + let!(:issue7) { create(:labeled_issue, project: project, labels: [development], relative_position: 2) } let!(:issue8) { create(:closed_issue, project: project) } - let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting]) } + let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting], relative_position: 1) } before do visit namespace_project_board_path(project.namespace, project, board) diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb new file mode 100644 index 00000000000..c50155a6d14 --- /dev/null +++ b/spec/features/boards/issue_ordering_spec.rb @@ -0,0 +1,166 @@ +require 'rails_helper' + +describe 'Issue Boards', :feature, :js do + include WaitForVueResource + include DragTo + + let(:project) { create(:empty_project, :public) } + let(:board) { create(:board, project: project) } + let(:user) { create(:user) } + let(:label) { create(:label, project: project) } + let!(:list1) { create(:list, board: board, label: label, position: 0) } + let!(:issue1) { create(:labeled_issue, project: project, title: 'testing 1', labels: [label], relative_position: 3) } + let!(:issue2) { create(:labeled_issue, project: project, title: 'testing 2', labels: [label], relative_position: 2) } + let!(:issue3) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label], relative_position: 1) } + + before do + project.team << [user, :master] + + login_as(user) + end + + context 'un-ordered issues' do + let!(:issue4) { create(:labeled_issue, project: project, labels: [label]) } + + before do + visit namespace_project_board_path(project.namespace, project, board) + wait_for_vue_resource + + expect(page).to have_selector('.board', count: 2) + end + + it 'has un-ordered issue as last issue' do + page.within(first('.board')) do + expect(all('.card').last).to have_content(issue4.title) + end + end + + it 'moves un-ordered issue to top of list' do + drag(from_index: 3, to_index: 0) + + page.within(first('.board')) do + expect(first('.card')).to have_content(issue4.title) + end + end + end + + context 'ordering in list' do + before do + visit namespace_project_board_path(project.namespace, project, board) + wait_for_vue_resource + + expect(page).to have_selector('.board', count: 2) + end + + it 'moves from middle to top' do + drag(from_index: 1, to_index: 0) + + wait_for_vue_resource + + expect(first('.card')).to have_content(issue2.title) + end + + it 'moves from middle to bottom' do + drag(from_index: 1, to_index: 2) + + wait_for_vue_resource + + expect(all('.card').last).to have_content(issue2.title) + end + + it 'moves from top to bottom' do + drag(from_index: 0, to_index: 2) + + wait_for_vue_resource + + expect(all('.card').last).to have_content(issue3.title) + end + + it 'moves from bottom to top' do + drag(from_index: 2, to_index: 0) + + wait_for_vue_resource + + expect(first('.card')).to have_content(issue1.title) + end + + it 'moves from top to middle' do + drag(from_index: 0, to_index: 1) + + wait_for_vue_resource + + expect(first('.card')).to have_content(issue2.title) + end + + it 'moves from bottom to middle' do + drag(from_index: 2, to_index: 1) + + wait_for_vue_resource + + expect(all('.card').last).to have_content(issue2.title) + end + end + + context 'ordering when changing list' do + let(:label2) { create(:label, project: project) } + let!(:list2) { create(:list, board: board, label: label2, position: 1) } + let!(:issue4) { create(:labeled_issue, project: project, title: 'testing 1', labels: [label2], relative_position: 3.0) } + let!(:issue5) { create(:labeled_issue, project: project, title: 'testing 2', labels: [label2], relative_position: 2.0) } + let!(:issue6) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label2], relative_position: 1.0) } + + before do + visit namespace_project_board_path(project.namespace, project, board) + wait_for_vue_resource + + expect(page).to have_selector('.board', count: 3) + end + + it 'moves to top of another list' do + drag(list_from_index: 0, list_to_index: 1) + + wait_for_vue_resource + + expect(first('.board')).to have_selector('.card', count: 2) + expect(all('.board')[1]).to have_selector('.card', count: 4) + + page.within(all('.board')[1]) do + expect(first('.card')).to have_content(issue3.title) + end + end + + it 'moves to bottom of another list' do + drag(list_from_index: 0, list_to_index: 1, to_index: 2) + + wait_for_vue_resource + + expect(first('.board')).to have_selector('.card', count: 2) + expect(all('.board')[1]).to have_selector('.card', count: 4) + + page.within(all('.board')[1]) do + expect(all('.card').last).to have_content(issue3.title) + end + end + + it 'moves to index of another list' do + drag(list_from_index: 0, list_to_index: 1, to_index: 1) + + wait_for_vue_resource + + expect(first('.board')).to have_selector('.card', count: 2) + expect(all('.board')[1]).to have_selector('.card', count: 4) + + page.within(all('.board')[1]) do + expect(all('.card')[1]).to have_content(issue3.title) + end + end + end + + def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0) + drag_to(selector: selector, + scrollable: '#board-app', + list_from_index: list_from_index, + from_index: from_index, + to_index: to_index, + list_to_index: list_to_index) + end +end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 59e87b3f69c..3332e07ec31 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -11,8 +11,8 @@ describe 'Issue Boards', feature: true, js: true do let!(:bug) { create(:label, project: project, name: 'Bug') } let!(:regression) { create(:label, project: project, name: 'Regression') } let!(:stretch) { create(:label, project: project, name: 'Stretch') } - let!(:issue1) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development]) } - let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch]) } + let!(:issue1) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development], relative_position: 2) } + let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) } let(:board) { create(:board, project: project) } let!(:list) { create(:list, board: board, label: development, position: 0) } let(:card) { first('.board').first('.card') } diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb index fbab4fa9c4f..4638812b2d9 100644 --- a/spec/features/copy_as_gfm_spec.rb +++ b/spec/features/copy_as_gfm_spec.rb @@ -252,7 +252,7 @@ describe 'Copy as GFM', feature: true, js: true do <<-GFM.strip_heredoc <a name="named-anchor"></a> - + <sub>sub</sub> <dl> diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 9f173dbc99d..d243f9478bb 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -143,7 +143,7 @@ feature 'Group', feature: true do visit path - expect(page).to have_css('.group-home-desc > p > img') + expect(page).to have_css('.group-home-desc > p > gl-emoji') end it 'sanitizes unwanted tags' do diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index 3ab3d2d4229..f424186cf30 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -25,14 +25,14 @@ describe 'Awards Emoji', feature: true do end it 'increments the thumbsdown emoji', js: true do - find('[data-emoji="thumbsdown"]').click + find('[data-name="thumbsdown"]').click wait_for_ajax expect(thumbsdown_emoji).to have_text("1") end context 'click the thumbsup emoji' do it 'increments the thumbsup emoji', js: true do - find('[data-emoji="thumbsup"]').click + find('[data-name="thumbsup"]').click wait_for_ajax expect(thumbsup_emoji).to have_text("1") end @@ -44,7 +44,7 @@ describe 'Awards Emoji', feature: true do context 'click the thumbsdown emoji' do it 'increments the thumbsdown emoji', js: true do - find('[data-emoji="thumbsdown"]').click + find('[data-name="thumbsdown"]').click wait_for_ajax expect(thumbsdown_emoji).to have_text("1") end @@ -123,9 +123,9 @@ describe 'Awards Emoji', feature: true do end unless status - first('[data-emoji="smiley"]').click + first('[data-name="smiley"]').click else - find('[data-emoji="smiley"]').click + find('[data-name="smiley"]').click end wait_for_ajax diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index 93763f092fb..4dcc56a97d1 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' -describe 'Dropdown assignee', js: true, feature: true do +describe 'Dropdown assignee', :feature, :js do + include FilteredSearchHelpers include WaitForAjax let!(:project) { create(:empty_project) } @@ -9,17 +10,10 @@ describe 'Dropdown assignee', js: true, feature: true do let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') } let(:filtered_search) { find('.filtered-search') } let(:js_dropdown_assignee) { '#js-dropdown-assignee' } - - def send_keys_to_filtered_search(input) - input.split("").each do |i| - filtered_search.send_keys(i) - sleep 5 - wait_for_ajax - end - end + let(:filter_dropdown) { find("#{js_dropdown_assignee} .filter-dropdown") } def dropdown_assignee_size - page.all('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item').size + filter_dropdown.all('.filter-dropdown-item').size end def click_assignee(text) @@ -56,63 +50,80 @@ describe 'Dropdown assignee', js: true, feature: true do end it 'should hide loading indicator when loaded' do - send_keys_to_filtered_search('assignee:') + filtered_search.set('assignee:') - expect(page).not_to have_css('#js-dropdown-assignee .filter-dropdown-loading') + expect(find(js_dropdown_assignee)).to have_css('.filter-dropdown-loading') + expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading') end it 'should load all the assignees when opened' do - send_keys_to_filtered_search('assignee:') + filtered_search.set('assignee:') expect(dropdown_assignee_size).to eq(3) end it 'shows current user at top of dropdown' do - send_keys_to_filtered_search('assignee:') + filtered_search.set('assignee:') - expect(first('#js-dropdown-assignee .filter-dropdown li')).to have_content(user.name) + expect(filter_dropdown.first('.filter-dropdown-item')).to have_content(user.name) end end describe 'filtering' do before do - send_keys_to_filtered_search('assignee:') + filtered_search.set('assignee:') + + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name) end it 'filters by name' do - send_keys_to_filtered_search('j') + filtered_search.send_keys('j') - expect(dropdown_assignee_size).to eq(2) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user.name) end it 'filters by case insensitive name' do - send_keys_to_filtered_search('J') + filtered_search.send_keys('J') - expect(dropdown_assignee_size).to eq(2) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user.name) end it 'filters by username with symbol' do - send_keys_to_filtered_search('@ot') + filtered_search.send_keys('@ot') - expect(dropdown_assignee_size).to eq(2) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name) end it 'filters by case insensitive username with symbol' do - send_keys_to_filtered_search('@OT') + filtered_search.send_keys('@OT') - expect(dropdown_assignee_size).to eq(2) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name) end it 'filters by username without symbol' do - send_keys_to_filtered_search('ot') + filtered_search.send_keys('ot') - expect(dropdown_assignee_size).to eq(2) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name) end it 'filters by case insensitive username without symbol' do - send_keys_to_filtered_search('OT') + filtered_search.send_keys('OT') - expect(dropdown_assignee_size).to eq(2) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name) + expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name) end end @@ -125,22 +136,25 @@ describe 'Dropdown assignee', js: true, feature: true do click_assignee(user_jacob.name) expect(page).to have_css(js_dropdown_assignee, visible: false) - expect(filtered_search.value).to eq("assignee:@#{user_jacob.username} ") + expect_tokens([{ name: 'assignee', value: "@#{user_jacob.username}" }]) + expect_filtered_search_input_empty end it 'fills in the assignee username when the assignee has been filtered' do - send_keys_to_filtered_search('roo') + filtered_search.send_keys('roo') click_assignee(user.name) expect(page).to have_css(js_dropdown_assignee, visible: false) - expect(filtered_search.value).to eq("assignee:@#{user.username} ") + expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_filtered_search_input_empty end it 'selects `no assignee`' do find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click expect(page).to have_css(js_dropdown_assignee, visible: false) - expect(filtered_search.value).to eq("assignee:none ") + expect_tokens([{ name: 'assignee', value: 'none' }]) + expect_filtered_search_input_empty end end @@ -173,7 +187,7 @@ describe 'Dropdown assignee', js: true, feature: true do describe 'caching requests' do it 'caches requests after the first load' do filtered_search.set('assignee') - send_keys_to_filtered_search(':') + filtered_search.send_keys(':') initial_size = dropdown_assignee_size expect(initial_size).to be > 0 @@ -182,7 +196,7 @@ describe 'Dropdown assignee', js: true, feature: true do project.team << [new_user, :master] find('.filtered-search-input-container .clear-search').click filtered_search.set('assignee') - send_keys_to_filtered_search(':') + filtered_search.send_keys(':') expect(dropdown_assignee_size).to eq(initial_size) end diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index 59e302f0e2d..19a00618b12 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' describe 'Dropdown author', js: true, feature: true do + include FilteredSearchHelpers include WaitForAjax let!(:project) { create(:empty_project) } @@ -121,14 +122,16 @@ describe 'Dropdown author', js: true, feature: true do click_author(user_jacob.name) expect(page).to have_css(js_dropdown_author, visible: false) - expect(filtered_search.value).to eq("author:@#{user_jacob.username} ") + expect_tokens([{ name: 'author', value: "@#{user_jacob.username}" }]) + expect_filtered_search_input_empty end it 'fills in the author username when the author has been filtered' do click_author(user.name) expect(page).to have_css(js_dropdown_author, visible: false) - expect(filtered_search.value).to eq("author:@#{user.username} ") + expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb index 04dd54ab459..01b657bcada 100644 --- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' describe 'Dropdown hint', js: true, feature: true do + include FilteredSearchHelpers include WaitForAjax let!(:project) { create(:empty_project) } @@ -66,7 +67,8 @@ describe 'Dropdown hint', js: true, feature: true do expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-author', visible: true) - expect(filtered_search.value).to eq('author:') + expect_tokens([{ name: 'author' }]) + expect_filtered_search_input_empty end it 'opens the assignee dropdown when you click on assignee' do @@ -74,7 +76,8 @@ describe 'Dropdown hint', js: true, feature: true do expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-assignee', visible: true) - expect(filtered_search.value).to eq('assignee:') + expect_tokens([{ name: 'assignee' }]) + expect_filtered_search_input_empty end it 'opens the milestone dropdown when you click on milestone' do @@ -82,7 +85,8 @@ describe 'Dropdown hint', js: true, feature: true do expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-milestone', visible: true) - expect(filtered_search.value).to eq('milestone:') + expect_tokens([{ name: 'milestone' }]) + expect_filtered_search_input_empty end it 'opens the label dropdown when you click on label' do @@ -90,7 +94,8 @@ describe 'Dropdown hint', js: true, feature: true do expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-label', visible: true) - expect(filtered_search.value).to eq('label:') + expect_tokens([{ name: 'label' }]) + expect_filtered_search_input_empty end end @@ -101,7 +106,8 @@ describe 'Dropdown hint', js: true, feature: true do expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-author', visible: true) - expect(filtered_search.value).to eq('author:') + expect_tokens([{ name: 'author' }]) + expect_filtered_search_input_empty end it 'opens the assignee dropdown when you click on assignee' do @@ -110,7 +116,8 @@ describe 'Dropdown hint', js: true, feature: true do expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-assignee', visible: true) - expect(filtered_search.value).to eq('assignee:') + expect_tokens([{ name: 'assignee' }]) + expect_filtered_search_input_empty end it 'opens the milestone dropdown when you click on milestone' do @@ -119,7 +126,8 @@ describe 'Dropdown hint', js: true, feature: true do expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-milestone', visible: true) - expect(filtered_search.value).to eq('milestone:') + expect_tokens([{ name: 'milestone' }]) + expect_filtered_search_input_empty end it 'opens the label dropdown when you click on label' do @@ -128,7 +136,46 @@ describe 'Dropdown hint', js: true, feature: true do expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-label', visible: true) - expect(filtered_search.value).to eq('label:') + expect_tokens([{ name: 'label' }]) + expect_filtered_search_input_empty + end + end + + describe 'reselecting from dropdown' do + it 'reuses existing author text' do + filtered_search.send_keys('author:') + filtered_search.send_keys(:backspace) + click_hint('author') + + expect_tokens([{ name: 'author' }]) + expect_filtered_search_input_empty + end + + it 'reuses existing assignee text' do + filtered_search.send_keys('assignee:') + filtered_search.send_keys(:backspace) + click_hint('assignee') + + expect_tokens([{ name: 'assignee' }]) + expect_filtered_search_input_empty + end + + it 'reuses existing milestone text' do + filtered_search.send_keys('milestone:') + filtered_search.send_keys(:backspace) + click_hint('milestone') + + expect_tokens([{ name: 'milestone' }]) + expect_filtered_search_input_empty + end + + it 'reuses existing label text' do + filtered_search.send_keys('label:') + filtered_search.send_keys(:backspace) + click_hint('label') + + expect_tokens([{ name: 'label' }]) + expect_filtered_search_input_empty end end end diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index ab3b868fd3a..b192064b693 100644 --- a/spec/features/issues/filtered_search/dropdown_label_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -51,7 +51,8 @@ describe 'Dropdown label', js: true, feature: true do filtered_search.native.send_keys(:down, :down, :enter) - expect(filtered_search.value).to eq("label:~#{bug_label.title} ") + expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }]) + expect_filtered_search_input_empty end end @@ -92,7 +93,7 @@ describe 'Dropdown label', js: true, feature: true do end it 'filters by case-insensitive name with or without symbol' do - search_for_label('b') + filtered_search.send_keys('b') expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible @@ -101,7 +102,7 @@ describe 'Dropdown label', js: true, feature: true do clear_search_field init_label_search - search_for_label('~bu') + filtered_search.send_keys('~bu') expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible @@ -180,7 +181,8 @@ describe 'Dropdown label', js: true, feature: true do click_label(bug_label.title) expect(page).not_to have_css(js_dropdown_label) - expect(filtered_search.value).to eq("label:~#{bug_label.title} ") + expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }]) + expect_filtered_search_input_empty end it 'fills in the label name when the label is partially filled' do @@ -188,49 +190,56 @@ describe 'Dropdown label', js: true, feature: true do click_label(bug_label.title) expect(page).not_to have_css(js_dropdown_label) - expect(filtered_search.value).to eq("label:~#{bug_label.title} ") + expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }]) + expect_filtered_search_input_empty end it 'fills in the label name that contains multiple words' do click_label(two_words_label.title) expect(page).not_to have_css(js_dropdown_label) - expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\" ") + expect_tokens([{ name: 'label', value: "\"#{two_words_label.title}\"" }]) + expect_filtered_search_input_empty end it 'fills in the label name that contains multiple words and is very long' do click_label(long_label.title) expect(page).not_to have_css(js_dropdown_label) - expect(filtered_search.value).to eq("label:~\"#{long_label.title}\" ") + expect_tokens([{ name: 'label', value: "\"#{long_label.title}\"" }]) + expect_filtered_search_input_empty end it 'fills in the label name that contains double quotes' do click_label(wont_fix_label.title) expect(page).not_to have_css(js_dropdown_label) - expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}' ") + expect_tokens([{ name: 'label', value: "~'#{wont_fix_label.title}'" }]) + expect_filtered_search_input_empty end it 'fills in the label name with the correct capitalization' do click_label(uppercase_label.title) expect(page).not_to have_css(js_dropdown_label) - expect(filtered_search.value).to eq("label:~#{uppercase_label.title} ") + expect_tokens([{ name: 'label', value: "~#{uppercase_label.title}" }]) + expect_filtered_search_input_empty end it 'fills in the label name with special characters' do click_label(special_label.title) expect(page).not_to have_css(js_dropdown_label) - expect(filtered_search.value).to eq("label:~#{special_label.title} ") + expect_tokens([{ name: 'label', value: "~#{special_label.title}" }]) + expect_filtered_search_input_empty end it 'selects `no label`' do find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click expect(page).not_to have_css(js_dropdown_label) - expect(filtered_search.value).to eq("label:none ") + expect_tokens([{ name: 'label', value: 'none' }]) + expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index 0ce16715b86..0324fcad0a0 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' describe 'Dropdown milestone', js: true, feature: true do + include FilteredSearchHelpers include WaitForAjax let!(:project) { create(:empty_project) } @@ -127,7 +128,8 @@ describe 'Dropdown milestone', js: true, feature: true do click_milestone(milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%#{milestone.title} ") + expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }]) + expect_filtered_search_input_empty end it 'fills in the milestone name when the milestone is partially filled' do @@ -135,56 +137,64 @@ describe 'Dropdown milestone', js: true, feature: true do click_milestone(milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%#{milestone.title} ") + expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }]) + expect_filtered_search_input_empty end it 'fills in the milestone name that contains multiple words' do click_milestone(two_words_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\" ") + expect_tokens([{ name: 'milestone', value: "%\"#{two_words_milestone.title}\"" }]) + expect_filtered_search_input_empty end it 'fills in the milestone name that contains multiple words and is very long' do click_milestone(long_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\" ") + expect_tokens([{ name: 'milestone', value: "%\"#{long_milestone.title}\"" }]) + expect_filtered_search_input_empty end it 'fills in the milestone name that contains double quotes' do click_milestone(wont_fix_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}' ") + expect_tokens([{ name: 'milestone', value: "%'#{wont_fix_milestone.title}'" }]) + expect_filtered_search_input_empty end it 'fills in the milestone name with the correct capitalization' do click_milestone(uppercase_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title} ") + expect_tokens([{ name: 'milestone', value: "%#{uppercase_milestone.title}" }]) + expect_filtered_search_input_empty end it 'fills in the milestone name with special characters' do click_milestone(special_milestone.title) expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:%#{special_milestone.title} ") + expect_tokens([{ name: 'milestone', value: "%#{special_milestone.title}" }]) + expect_filtered_search_input_empty end it 'selects `no milestone`' do click_static_milestone('No Milestone') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:none ") + expect_tokens([{ name: 'milestone', value: 'none' }]) + expect_filtered_search_input_empty end it 'selects `upcoming milestone`' do click_static_milestone('Upcoming') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect(filtered_search.value).to eq("milestone:upcoming ") + expect_tokens([{ name: 'milestone', value: 'upcoming' }]) + expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 0420e64d42c..f079a9627e4 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -1,4 +1,4 @@ -require 'rails_helper' +require 'spec_helper' describe 'Filter issues', js: true, feature: true do include FilteredSearchHelpers @@ -97,7 +97,9 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched author' do input_filtered_search("author:@#{user.username}") + expect_tokens([{ name: 'author', value: user.username }]) expect_issues_list_count(5) + expect_filtered_search_input_empty end it 'filters issues by invalid author' do @@ -110,36 +112,50 @@ describe 'Filter issues', js: true, feature: true do end context 'author with other filters' do + let(:search_term) { 'issue' } + it 'filters issues by searched author and text' do - search = "author:@#{user.username} issue" - input_filtered_search(search) + input_filtered_search("author:@#{user.username} #{search_term}") + expect_tokens([{ name: 'author', value: user.username }]) expect_issues_list_count(3) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched author, assignee and text' do - search = "author:@#{user.username} assignee:@#{user.username} issue" - input_filtered_search(search) + input_filtered_search("author:@#{user.username} assignee:@#{user.username} #{search_term}") + expect_tokens([ + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username } + ]) expect_issues_list_count(3) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched author, assignee, label, and text' do - search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue" - input_filtered_search(search) + input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}") + expect_tokens([ + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username }, + { name: 'label', value: caps_sensitive_label.title } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched author, assignee, label, milestone and text' do - search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue" - input_filtered_search(search) + input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}") + expect_tokens([ + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username }, + { name: 'label', value: caps_sensitive_label.title }, + { name: 'milestone', value: milestone.title } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end end @@ -151,19 +167,19 @@ describe 'Filter issues', js: true, feature: true do describe 'filter issues by assignee' do context 'only assignee' do it 'filters issues by searched assignee' do - search = "assignee:@#{user.username}" - input_filtered_search(search) + input_filtered_search("assignee:@#{user.username}") + expect_tokens([{ name: 'assignee', value: user.username }]) expect_issues_list_count(5) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'filters issues by no assignee' do - search = "assignee:none" - input_filtered_search(search) + input_filtered_search('assignee:none') + expect_tokens([{ name: 'assignee', value: 'none' }]) expect_issues_list_count(8, 1) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'filters issues by invalid assignee' do @@ -176,36 +192,50 @@ describe 'Filter issues', js: true, feature: true do end context 'assignee with other filters' do + let(:search_term) { 'searchTerm' } + it 'filters issues by searched assignee and text' do - search = "assignee:@#{user.username} searchTerm" - input_filtered_search(search) + input_filtered_search("assignee:@#{user.username} #{search_term}") + expect_tokens([{ name: 'assignee', value: user.username }]) expect_issues_list_count(2) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched assignee, author and text' do - search = "assignee:@#{user.username} author:@#{user.username} searchTerm" - input_filtered_search(search) + input_filtered_search("assignee:@#{user.username} author:@#{user.username} #{search_term}") + expect_tokens([ + { name: 'assignee', value: user.username }, + { name: 'author', value: user.username } + ]) expect_issues_list_count(2) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched assignee, author, label, text' do - search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm" - input_filtered_search(search) + input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}") + expect_tokens([ + { name: 'assignee', value: user.username }, + { name: 'author', value: user.username }, + { name: 'label', value: caps_sensitive_label.title } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched assignee, author, label, milestone and text' do - search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm" - input_filtered_search(search) + input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}") + expect_tokens([ + { name: 'assignee', value: user.username }, + { name: 'author', value: user.username }, + { name: 'label', value: caps_sensitive_label.title }, + { name: 'milestone', value: milestone.title } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end end @@ -217,21 +247,23 @@ describe 'Filter issues', js: true, feature: true do end describe 'filter issues by label' do + let(:search_term) { 'bug' } + context 'only label' do it 'filters issues by searched label' do - search = "label:~#{bug_label.title}" - input_filtered_search(search) + input_filtered_search("label:~#{bug_label.title}") + expect_tokens([{ name: 'label', value: bug_label.title }]) expect_issues_list_count(2) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'filters issues by no label' do - search = "label:none" - input_filtered_search(search) + input_filtered_search('label:none') + expect_tokens([{ name: 'label', value: 'none' }]) expect_issues_list_count(9, 1) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'filters issues by invalid label' do @@ -239,11 +271,14 @@ describe 'Filter issues', js: true, feature: true do end it 'filters issues by multiple labels' do - search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title}" - input_filtered_search(search) + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title}") + expect_tokens([ + { name: 'label', value: bug_label.title }, + { name: 'label', value: caps_sensitive_label.title } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'filters issues by label containing special characters' do @@ -251,21 +286,20 @@ describe 'Filter issues', js: true, feature: true do special_issue = create(:issue, title: "Issue with special character label", project: project) special_issue.labels << special_label - search = "label:~#{special_label.title}" - input_filtered_search(search) - + input_filtered_search("label:~#{special_label.title}") + expect_tokens([{ name: 'label', value: special_label.title }]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'does not show issues' do - new_label = create(:label, project: project, title: "new_label") + new_label = create(:label, project: project, title: 'new_label') - search = "label:~#{new_label.title}" - input_filtered_search(search) + input_filtered_search("label:~#{new_label.title}") + expect_tokens([{ name: 'label', value: new_label.title }]) expect_no_issues_list() - expect_filtered_search_input(search) + expect_filtered_search_input_empty end end @@ -275,29 +309,29 @@ describe 'Filter issues', js: true, feature: true do special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project) special_multiple_issue.labels << special_multiple_label - search = "label:~'#{special_multiple_label.title}'" - input_filtered_search(search) + input_filtered_search("label:~'#{special_multiple_label.title}'") + # filtered search defaults quotations to double quotes + expect_tokens([{ name: 'label', value: "\"#{special_multiple_label.title}\"" }]) expect_issues_list_count(1) - # filtered search defaults quotations to double quotes - expect_filtered_search_input("label:~\"#{special_multiple_label.title}\"") + expect_filtered_search_input_empty end it 'single quotes' do - search = "label:~'#{multiple_words_label.title}'" - input_filtered_search(search) + input_filtered_search("label:~'#{multiple_words_label.title}'") + expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }]) expect_issues_list_count(1) - expect_filtered_search_input("label:~\"#{multiple_words_label.title}\"") + expect_filtered_search_input_empty end it 'double quotes' do - search = "label:~\"#{multiple_words_label.title}\"" - input_filtered_search(search) + input_filtered_search("label:~\"#{multiple_words_label.title}\"") + expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'single quotes containing double quotes' do @@ -305,11 +339,11 @@ describe 'Filter issues', js: true, feature: true do double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project) double_quotes_label_issue.labels << double_quotes_label - search = "label:~'#{double_quotes_label.title}'" - input_filtered_search(search) + input_filtered_search("label:~'#{double_quotes_label.title}'") + expect_tokens([{ name: 'label', value: "'#{double_quotes_label.title}'" }]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'double quotes containing single quotes' do @@ -317,86 +351,115 @@ describe 'Filter issues', js: true, feature: true do single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project) single_quotes_label_issue.labels << single_quotes_label - search = "label:~\"#{single_quotes_label.title}\"" - input_filtered_search(search) + input_filtered_search("label:~\"#{single_quotes_label.title}\"") + expect_tokens([{ name: 'label', value: "\"#{single_quotes_label.title}\"" }]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end end context 'label with other filters' do it 'filters issues by searched label and text' do - search = "label:~#{caps_sensitive_label.title} bug" - input_filtered_search(search) + input_filtered_search("label:~#{caps_sensitive_label.title} #{search_term}") + expect_tokens([{ name: 'label', value: caps_sensitive_label.title }]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched label, author and text' do - search = "label:~#{caps_sensitive_label.title} author:@#{user.username} bug" - input_filtered_search(search) + input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}") + expect_tokens([ + { name: 'label', value: caps_sensitive_label.title }, + { name: 'author', value: user.username } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched label, author, assignee and text' do - search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" - input_filtered_search(search) + input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}") + expect_tokens([ + { name: 'label', value: caps_sensitive_label.title }, + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched label, author, assignee, milestone and text' do - search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" - input_filtered_search(search) + input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}") + expect_tokens([ + { name: 'label', value: caps_sensitive_label.title }, + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username }, + { name: 'milestone', value: milestone.title } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end end context 'multiple labels with other filters' do it 'filters issues by searched label, label2, and text' do - search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug" - input_filtered_search(search) + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} #{search_term}") + expect_tokens([ + { name: 'label', value: bug_label.title }, + { name: 'label', value: caps_sensitive_label.title } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched label, label2, author and text' do - search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug" - input_filtered_search(search) + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}") + expect_tokens([ + { name: 'label', value: bug_label.title }, + { name: 'label', value: caps_sensitive_label.title }, + { name: 'author', value: user.username } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched label, label2, author, assignee and text' do - search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" - input_filtered_search(search) + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}") + expect_tokens([ + { name: 'label', value: bug_label.title }, + { name: 'label', value: caps_sensitive_label.title }, + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched label, label2, author, assignee, milestone and text' do - search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" - input_filtered_search(search) + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}") + expect_tokens([ + { name: 'label', value: bug_label.title }, + { name: 'label', value: caps_sensitive_label.title }, + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username }, + { name: 'milestone', value: milestone.title } + ]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end end context 'issue label clicked' do before do find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click - sleep 1 end it 'filters' do @@ -404,7 +467,8 @@ describe 'Filter issues', js: true, feature: true do end it 'displays in search bar' do - expect(find('.filtered-search').value).to eq("label:~\"#{multiple_words_label.title}\"") + expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }]) + expect_filtered_search_input_empty end end @@ -420,19 +484,25 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched milestone' do input_filtered_search("milestone:%#{milestone.title}") + expect_tokens([{ name: 'milestone', value: milestone.title }]) expect_issues_list_count(5) + expect_filtered_search_input_empty end it 'filters issues by no milestone' do input_filtered_search("milestone:none") + expect_tokens([{ name: 'milestone', value: 'none' }]) expect_issues_list_count(7, 1) + expect_filtered_search_input_empty end it 'filters issues by upcoming milestones' do input_filtered_search("milestone:upcoming") + expect_tokens([{ name: 'milestone', value: 'upcoming' }]) expect_issues_list_count(1) + expect_filtered_search_input_empty end it 'filters issues by invalid milestones' do @@ -447,55 +517,69 @@ describe 'Filter issues', js: true, feature: true do special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project) create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone) - search = "milestone:%#{special_milestone.title}" - input_filtered_search(search) + input_filtered_search("milestone:%#{special_milestone.title}") + expect_tokens([{ name: 'milestone', value: special_milestone.title }]) expect_issues_list_count(1) - expect_filtered_search_input(search) + expect_filtered_search_input_empty end it 'does not show issues' do new_milestone = create(:milestone, title: "new", project: project) - search = "milestone:%#{new_milestone.title}" - input_filtered_search(search) + input_filtered_search("milestone:%#{new_milestone.title}") + expect_tokens([{ name: 'milestone', value: new_milestone.title }]) expect_no_issues_list() - expect_filtered_search_input(search) + expect_filtered_search_input_empty end end context 'milestone with other filters' do + let(:search_term) { 'bug' } + it 'filters issues by searched milestone and text' do - search = "milestone:%#{milestone.title} bug" - input_filtered_search(search) + input_filtered_search("milestone:%#{milestone.title} #{search_term}") + expect_tokens([{ name: 'milestone', value: milestone.title }]) expect_issues_list_count(2) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched milestone, author and text' do - search = "milestone:%#{milestone.title} author:@#{user.username} bug" - input_filtered_search(search) + input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} #{search_term}") + expect_tokens([ + { name: 'milestone', value: milestone.title }, + { name: 'author', value: user.username } + ]) expect_issues_list_count(2) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched milestone, author, assignee and text' do - search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} bug" - input_filtered_search(search) + input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} #{search_term}") + expect_tokens([ + { name: 'milestone', value: milestone.title }, + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username } + ]) expect_issues_list_count(2) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end it 'filters issues by searched milestone, author, assignee, label and text' do - search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug" - input_filtered_search(search) - + input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} #{search_term}") + + expect_tokens([ + { name: 'milestone', value: milestone.title }, + { name: 'author', value: user.username }, + { name: 'assignee', value: user.username }, + { name: 'label', value: bug_label.title } + ]) expect_issues_list_count(2) - expect_filtered_search_input(search) + expect_filtered_search_input(search_term) end end @@ -506,44 +590,6 @@ describe 'Filter issues', js: true, feature: true do end end - describe 'overwrites selected filter' do - it 'changes author' do - input_filtered_search("author:@#{user.username}", submit: false) - - select_search_at_index(3) - - page.within '#js-dropdown-author' do - click_button user2.username - end - - expect(filtered_search.value).to eq("author:@#{user2.username} ") - end - - it 'changes label' do - input_filtered_search("author:@#{user.username} label:~#{bug_label.title}", submit: false) - - select_search_at_index(27) - - page.within '#js-dropdown-label' do - click_button label.name - end - - expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name} ") - end - - it 'changes label correctly space is in previous label' do - input_filtered_search("label:~\"#{multiple_words_label.title}\"", submit: false) - - select_search_at_index(0) - - page.within '#js-dropdown-label' do - click_button label.name - end - - expect(filtered_search.value).to eq("label:~#{label.name} ") - end - end - describe 'filter issues by text' do context 'only text' do it 'filters issues by searched text' do @@ -605,80 +651,81 @@ describe 'Filter issues', js: true, feature: true do context 'searched text with other filters' do it 'filters issues by searched text and author' do + # After searching, all search terms are placed at the end input_filtered_search("bug author:@#{user.username}") expect_issues_list_count(2) - expect_filtered_search_input("author:@#{user.username} bug") + expect_filtered_search_input('bug') end it 'filters issues by searched text, author and more text' do input_filtered_search("bug author:@#{user.username} report") expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} bug report") + expect_filtered_search_input('bug report') end it 'filters issues by searched text, author and assignee' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}") expect_issues_list_count(2) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug") + expect_filtered_search_input('bug') end it 'filters issues by searched text, author, more text and assignee' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}") expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report") + expect_filtered_search_input('bug report') end it 'filters issues by searched text, author, more text, assignee and even more text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with") expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report with") + expect_filtered_search_input('bug report with') end it 'filters issues by searched text, author, assignee and label' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}") expect_issues_list_count(2) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug") + expect_filtered_search_input('bug') end it 'filters issues by searched text, author, text, assignee, text, label and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything") expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug report with everything") + expect_filtered_search_input('bug report with everything') end it 'filters issues by searched text, author, assignee, label and milestone' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}") expect_issues_list_count(2) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug") + expect_filtered_search_input('bug') end it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you") expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug report with everything you") + expect_filtered_search_input('bug report with everything you') end it 'filters issues by searched text, author, assignee, multiple labels and milestone' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}") expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug") + expect_filtered_search_input('bug') end it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought") expect_issues_list_count(1) - expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug report with everything you thought") + expect_filtered_search_input('bug report with everything you thought') end end @@ -717,8 +764,8 @@ describe 'Filter issues', js: true, feature: true do before do input_filtered_search('bug') - # Wait for search results to load - sleep 2 + # This ensures that the search is performed + expect_issues_list_count(4, 1) end it 'open state' do diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index 90eb60eb337..59244d65eec 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' describe 'Search bar', js: true, feature: true do + include FilteredSearchHelpers include WaitForAjax let!(:project) { create(:empty_project) } @@ -32,7 +33,8 @@ describe 'Search bar', js: true, feature: true do it 'selects item' do filtered_search.native.send_keys(:down, :down, :enter) - expect(filtered_search.value).to eq('author:') + expect_tokens([{ name: 'author' }]) + expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb new file mode 100644 index 00000000000..a78dbaf53ed --- /dev/null +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -0,0 +1,322 @@ +require 'rails_helper' + +describe 'Visual tokens', js: true, feature: true do + include FilteredSearchHelpers + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user, name: 'administrator', username: 'root') } + let!(:user_rock) { create(:user, name: 'The Rock', username: 'rock') } + let!(:milestone_nine) { create(:milestone, title: '9.0', project: project) } + let!(:milestone_ten) { create(:milestone, title: '10.0', project: project) } + let!(:label) { create(:label, project: project, title: 'abc') } + let!(:cc_label) { create(:label, project: project, title: 'Community Contribution') } + + let(:filtered_search) { find('.filtered-search') } + let(:filter_author_dropdown) { find("#js-dropdown-author .filter-dropdown") } + let(:filter_assignee_dropdown) { find("#js-dropdown-assignee .filter-dropdown") } + let(:filter_milestone_dropdown) { find("#js-dropdown-milestone .filter-dropdown") } + let(:filter_label_dropdown) { find("#js-dropdown-label .filter-dropdown") } + + def is_input_focused + page.evaluate_script("document.activeElement.classList.contains('filtered-search')") + end + + before do + project.add_user(user, :master) + project.add_user(user_rock, :master) + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'editing author token' do + before do + input_filtered_search('author:@root assignee:none', submit: false) + first('.tokens-container .filtered-search-token').double_click + end + + it 'opens author dropdown' do + expect(page).to have_css('#js-dropdown-author', visible: true) + end + + it 'makes value editable' do + expect_filtered_search_input('@root') + end + + it 'filters value' do + filtered_search.send_keys(:backspace) + + expect(page).to have_css('#js-dropdown-author .filter-dropdown .filter-dropdown-item', count: 1) + end + + it 'ends editing mode when document is clicked' do + find('#content-body').click + + expect_filtered_search_input_empty + expect(page).to have_css('#js-dropdown-author', visible: false) + end + + it 'ends editing mode when scroll container is clicked' do + find('.scroll-container').click + + expect_filtered_search_input_empty + expect(page).to have_css('#js-dropdown-author', visible: false) + end + + describe 'selecting different author from dropdown' do + before do + filter_author_dropdown.find('.filter-dropdown-item .dropdown-light-content', text: "@#{user_rock.username}").click + end + + it 'changes value in visual token' do + expect(first('.tokens-container .filtered-search-token .value').text).to eq("@#{user_rock.username}") + end + + it 'moves input to the right' do + expect(is_input_focused).to eq(true) + end + end + end + + describe 'editing assignee token' do + before do + input_filtered_search('assignee:@root author:none', submit: false) + first('.tokens-container .filtered-search-token').double_click + end + + it 'opens assignee dropdown' do + expect(page).to have_css('#js-dropdown-assignee', visible: true) + end + + it 'makes value editable' do + expect_filtered_search_input('@root') + end + + it 'filters value' do + filtered_search.send_keys(:backspace) + + expect(page).to have_css('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item', count: 1) + end + + it 'ends editing mode when document is clicked' do + find('#content-body').click + + expect_filtered_search_input_empty + expect(page).to have_css('#js-dropdown-assignee', visible: false) + end + + it 'ends editing mode when scroll container is clicked' do + find('.scroll-container').click + + expect_filtered_search_input_empty + expect(page).to have_css('#js-dropdown-assignee', visible: false) + end + + describe 'selecting static option from dropdown' do + before do + find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'No Assignee').click + end + + it 'changes value in visual token' do + expect(first('.tokens-container .filtered-search-token .value').text).to eq('none') + end + + it 'moves input to the right' do + expect(is_input_focused).to eq(true) + end + end + end + + describe 'editing milestone token' do + before do + input_filtered_search('milestone:%10.0 author:none', submit: false) + first('.tokens-container .filtered-search-token').double_click + first('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item') + end + + it 'opens milestone dropdown' do + expect(filter_milestone_dropdown.find('.filter-dropdown-item', text: milestone_ten.title)).to be_visible + expect(filter_milestone_dropdown.find('.filter-dropdown-item', text: milestone_nine.title)).to be_visible + expect(page).to have_css('#js-dropdown-milestone', visible: true) + end + + it 'selects static option from dropdown' do + find("#js-dropdown-milestone").find('.filter-dropdown-item', text: 'Upcoming').click + + expect(first('.tokens-container .filtered-search-token .value').text).to eq('upcoming') + expect(is_input_focused).to eq(true) + end + + it 'makes value editable' do + expect_filtered_search_input('%10.0') + end + + it 'filters value' do + filtered_search.send_keys(:backspace) + + expect(page).to have_css('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item', count: 1) + end + + it 'ends editing mode when document is clicked' do + find('#content-body').click + + expect_filtered_search_input_empty + expect(page).to have_css('#js-dropdown-milestone', visible: false) + end + + it 'ends editing mode when scroll container is clicked' do + find('.scroll-container').click + + expect_filtered_search_input_empty + expect(page).to have_css('#js-dropdown-milestone', visible: false) + end + end + + describe 'editing label token' do + before do + input_filtered_search("label:~#{label.title} author:none", submit: false) + first('.tokens-container .filtered-search-token').double_click + first('#js-dropdown-label .filter-dropdown .filter-dropdown-item') + end + + it 'opens label dropdown' do + expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible + expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible + expect(page).to have_css('#js-dropdown-label', visible: true) + end + + it 'selects option from dropdown' do + expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible + expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible + + find("#js-dropdown-label").find('.filter-dropdown-item', text: cc_label.title).click + + expect(first('.tokens-container .filtered-search-token .value').text).to eq("~\"#{cc_label.title}\"") + expect(is_input_focused).to eq(true) + end + + it 'makes value editable' do + expect_filtered_search_input("~#{label.title}") + end + + it 'filters value' do + expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible + expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible + + filtered_search.send_keys(:backspace) + + filter_label_dropdown.find('.filter-dropdown-item') + + expect(page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size).to eq(1) + end + + it 'ends editing mode when document is clicked' do + find('#content-body').click + + expect_filtered_search_input_empty + expect(page).to have_css('#js-dropdown-label', visible: false) + end + + it 'ends editing mode when scroll container is clicked' do + find('.scroll-container').click + + expect_filtered_search_input_empty + expect(page).to have_css('#js-dropdown-label', visible: false) + end + end + + describe 'editing multiple tokens' do + before do + input_filtered_search('author:@root assignee:none', submit: false) + first('.tokens-container .filtered-search-token').double_click + end + + it 'opens author dropdown' do + expect(page).to have_css('#js-dropdown-author', visible: true) + end + + it 'opens assignee dropdown' do + find('.tokens-container .filtered-search-token', text: 'Assignee').double_click + expect(page).to have_css('#js-dropdown-assignee', visible: true) + end + end + + describe 'add new token after editing existing token' do + before do + input_filtered_search('author:@root assignee:none', submit: false) + first('.tokens-container .filtered-search-token').double_click + filtered_search.send_keys(' ') + end + + describe 'opens dropdowns' do + it 'opens hint dropdown' do + expect(page).to have_css('#js-dropdown-hint', visible: true) + end + + it 'opens author dropdown' do + filtered_search.send_keys('author:') + expect(page).to have_css('#js-dropdown-author', visible: true) + end + + it 'opens assignee dropdown' do + filtered_search.send_keys('assignee:') + expect(page).to have_css('#js-dropdown-assignee', visible: true) + end + + it 'opens milestone dropdown' do + filtered_search.send_keys('milestone:') + expect(page).to have_css('#js-dropdown-milestone', visible: true) + end + + it 'opens label dropdown' do + filtered_search.send_keys('label:') + expect(page).to have_css('#js-dropdown-label', visible: true) + end + end + + describe 'creates visual tokens' do + it 'creates author token' do + filtered_search.send_keys('author:@thomas ') + token = page.all('.tokens-container .filtered-search-token')[1] + + expect(token.find('.name').text).to eq('Author') + expect(token.find('.value').text).to eq('@thomas') + end + + it 'creates assignee token' do + filtered_search.send_keys('assignee:@thomas ') + token = page.all('.tokens-container .filtered-search-token')[1] + + expect(token.find('.name').text).to eq('Assignee') + expect(token.find('.value').text).to eq('@thomas') + end + + it 'creates milestone token' do + filtered_search.send_keys('milestone:none ') + token = page.all('.tokens-container .filtered-search-token')[1] + + expect(token.find('.name').text).to eq('Milestone') + expect(token.find('.value').text).to eq('none') + end + + it 'creates label token' do + filtered_search.send_keys('label:~Backend ') + token = page.all('.tokens-container .filtered-search-token')[1] + + expect(token.find('.name').text).to eq('Label') + expect(token.find('.value').text).to eq('~Backend') + end + end + + it 'does not tokenize incomplete token' do + filtered_search.send_keys('author:') + + find('#content-body').click + token = page.all('.tokens-container .js-visual-token')[1] + + expect_filtered_search_input_empty + expect(token.find('.name').text).to eq('Author') + end + end +end diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 741ca95f1ca..d4e0ef91856 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -3,6 +3,7 @@ require 'rails_helper' describe 'New/edit issue', feature: true, js: true do let!(:project) { create(:project) } let!(:user) { create(:user)} + let!(:user2) { create(:user)} let!(:milestone) { create(:milestone, project: project) } let!(:label) { create(:label, project: project) } let!(:label2) { create(:label, project: project) } @@ -10,6 +11,7 @@ describe 'New/edit issue', feature: true, js: true do before do project.team << [user, :master] + project.team << [user2, :master] login_as(user) end @@ -22,14 +24,23 @@ describe 'New/edit issue', feature: true, js: true do fill_in 'issue_title', with: 'title' fill_in 'issue_description', with: 'title' + expect(find('a', text: 'Assign to me')).to be_visible click_button 'Assignee' page.within '.dropdown-menu-user' do - click_link user.name + click_link user2.name end + expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user2.id.to_s) + page.within '.js-assignee-search' do + expect(page).to have_content user2.name + end + expect(find('a', text: 'Assign to me')).to be_visible + + click_link 'Assign to me' expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s) page.within '.js-assignee-search' do expect(page).to have_content user.name end + expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible click_button 'Milestone' page.within '.issue-milestone' do @@ -94,6 +105,7 @@ describe 'New/edit issue', feature: true, js: true do it 'allows user to update issue' do expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s) expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) + expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible page.within '.js-user-search' do expect(page).to have_content user.name diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb index 5608cda28f8..265a0cfc198 100644 --- a/spec/features/merge_requests/filter_by_milestone_spec.rb +++ b/spec/features/merge_requests/filter_by_milestone_spec.rb @@ -25,6 +25,9 @@ feature 'Merge Request filtering by Milestone', feature: true do visit_merge_requests(project) input_filtered_search('milestone:none') + expect_tokens([{ name: 'milestone', value: 'none' }]) + expect_filtered_search_input_empty + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_css('.merge-request', count: 1) end diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb index 6579a88d4ab..70e3997e716 100644 --- a/spec/features/merge_requests/filter_merge_requests_spec.rb +++ b/spec/features/merge_requests/filter_merge_requests_spec.rb @@ -24,6 +24,11 @@ describe 'Filter merge requests', feature: true do describe 'for assignee from mr#index' do let(:search_query) { "assignee:@#{user.username}" } + def expect_assignee_visual_tokens + expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_filtered_search_input_empty + end + before do input_filtered_search(search_query) @@ -32,25 +37,30 @@ describe 'Filter merge requests', feature: true do context 'assignee', js: true do it 'updates to current user' do - expect_filtered_search_input(search_query) + expect_assignee_visual_tokens() end it 'does not change when closed link is clicked' do find('.issues-state-filters a', text: "Closed").click - expect_filtered_search_input(search_query) + expect_assignee_visual_tokens() end it 'does not change when all link is clicked' do find('.issues-state-filters a', text: "All").click - expect_filtered_search_input(search_query) + expect_assignee_visual_tokens() end end end describe 'for milestone from mr#index' do - let(:search_query) { "milestone:%#{milestone.title}" } + let(:search_query) { "milestone:%\"#{milestone.title}\"" } + + def expect_milestone_visual_tokens + expect_tokens([{ name: 'milestone', value: "%\"#{milestone.title}\"" }]) + expect_filtered_search_input_empty + end before do input_filtered_search(search_query) @@ -60,19 +70,19 @@ describe 'Filter merge requests', feature: true do context 'milestone', js: true do it 'updates to current milestone' do - expect_filtered_search_input(search_query) + expect_milestone_visual_tokens() end it 'does not change when closed link is clicked' do find('.issues-state-filters a', text: "Closed").click - expect_filtered_search_input(search_query) + expect_milestone_visual_tokens() end it 'does not change when all link is clicked' do find('.issues-state-filters a', text: "All").click - expect_filtered_search_input(search_query) + expect_milestone_visual_tokens() end end end @@ -82,35 +92,44 @@ describe 'Filter merge requests', feature: true do input_filtered_search('label:none') expect_mr_list_count(1) - expect_filtered_search_input('label:none') + expect_tokens([{ name: 'label', value: 'none' }]) + expect_filtered_search_input_empty end it 'filters by a label' do input_filtered_search("label:~#{label.title}") expect_mr_list_count(0) - expect_filtered_search_input("label:~#{label.title}") + expect_tokens([{ name: 'label', value: "~#{label.title}" }]) + expect_filtered_search_input_empty end it "filters by `won't fix` and another label" do input_filtered_search("label:~\"#{wontfix.title}\" label:~#{label.title}") expect_mr_list_count(0) - expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}") + expect_tokens([ + { name: 'label', value: "~\"#{wontfix.title}\"" }, + { name: 'label', value: "~#{label.title}" } + ]) + expect_filtered_search_input_empty end it "filters by `won't fix` label followed by another label after page load" do input_filtered_search("label:~\"#{wontfix.title}\"") expect_mr_list_count(0) - expect_filtered_search_input("label:~\"#{wontfix.title}\"") - - input_filtered_search_keys(" label:~#{label.title}") + expect_tokens([{ name: 'label', value: "~\"#{wontfix.title}\"" }]) + expect_filtered_search_input_empty - expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}") + input_filtered_search_keys("label:~#{label.title}") expect_mr_list_count(0) - expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}") + expect_tokens([ + { name: 'label', value: "~\"#{wontfix.title}\"" }, + { name: 'label', value: "~#{label.title}" } + ]) + expect_filtered_search_input_empty end end @@ -121,9 +140,10 @@ describe 'Filter merge requests', feature: true do input_filtered_search("assignee:@#{user.username}") expect_mr_list_count(1) - expect_filtered_search_input("assignee:@#{user.username}") + expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_filtered_search_input_empty - input_filtered_search_keys(" label:~#{label.title}") + input_filtered_search_keys("label:~#{label.title} ") expect_mr_list_count(1) @@ -131,20 +151,28 @@ describe 'Filter merge requests', feature: true do end context 'assignee and label', js: true do + def expect_assignee_label_visual_tokens + expect_tokens([ + { name: 'assignee', value: "@#{user.username}" }, + { name: 'label', value: "~#{label.title}" } + ]) + expect_filtered_search_input_empty + end + it 'updates to current assignee and label' do - expect_filtered_search_input(search_query) + expect_assignee_label_visual_tokens() end it 'does not change when closed link is clicked' do find('.issues-state-filters a', text: "Closed").click - expect_filtered_search_input(search_query) + expect_assignee_label_visual_tokens() end it 'does not change when all link is clicked' do find('.issues-state-filters a', text: "All").click - expect_filtered_search_input(search_query) + expect_assignee_label_visual_tokens() end end end @@ -195,6 +223,8 @@ describe 'Filter merge requests', feature: true do input_filtered_search_keys(' label:~bug') expect_mr_list_count(1) + expect_tokens([{ name: 'label', value: '~bug' }]) + expect_filtered_search_input('Bug') end it 'filters by text and milestone' do @@ -206,6 +236,8 @@ describe 'Filter merge requests', feature: true do input_filtered_search_keys(' milestone:%8') expect_mr_list_count(1) + expect_tokens([{ name: 'milestone', value: '%8' }]) + expect_filtered_search_input('Bug') end it 'filters by text and assignee' do @@ -217,6 +249,8 @@ describe 'Filter merge requests', feature: true do input_filtered_search_keys(" assignee:@#{user.username}") expect_mr_list_count(1) + expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_filtered_search_input('Bug') end it 'filters by text and author' do @@ -228,6 +262,8 @@ describe 'Filter merge requests', feature: true do input_filtered_search_keys(" author:@#{user.username}") expect_mr_list_count(1) + expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_filtered_search_input('Bug') end end end @@ -266,7 +302,8 @@ describe 'Filter merge requests', feature: true do it 'filter by current user' do visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: user.id) - expect_filtered_search_input("assignee:@#{user.username}") + expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_filtered_search_input_empty end it 'filter by new user' do @@ -275,7 +312,8 @@ describe 'Filter merge requests', feature: true do visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: new_user.id) - expect_filtered_search_input("assignee:@#{new_user.username}") + expect_tokens([{ name: 'assignee', value: "@#{new_user.username}" }]) + expect_filtered_search_input_empty end end @@ -283,7 +321,8 @@ describe 'Filter merge requests', feature: true do it 'filter by current user' do visit namespace_project_merge_requests_path(project.namespace, project, author_id: user.id) - expect_filtered_search_input("author:@#{user.username}") + expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_filtered_search_input_empty end it 'filter by new user' do @@ -292,7 +331,8 @@ describe 'Filter merge requests', feature: true do visit namespace_project_merge_requests_path(project.namespace, project, author_id: new_user.id) - expect_filtered_search_input("author:@#{new_user.username}") + expect_tokens([{ name: 'author', value: "@#{new_user.username}" }]) + expect_filtered_search_input_empty end end end diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb index 7594cbf54e8..1ecdb8b5983 100644 --- a/spec/features/merge_requests/form_spec.rb +++ b/spec/features/merge_requests/form_spec.rb @@ -4,12 +4,14 @@ describe 'New/edit merge request', feature: true, js: true do let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } let(:fork_project) { create(:project, forked_from_project: project) } let!(:user) { create(:user)} + let!(:user2) { create(:user)} let!(:milestone) { create(:milestone, project: project) } let!(:label) { create(:label, project: project) } let!(:label2) { create(:label, project: project) } before do project.team << [user, :master] + project.team << [user2, :master] end context 'owned projects' do @@ -33,8 +35,14 @@ describe 'New/edit merge request', feature: true, js: true do it 'creates new merge request' do click_button 'Assignee' page.within '.dropdown-menu-user' do - click_link user.name + click_link user2.name + end + expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user2.id.to_s) + page.within '.js-assignee-search' do + expect(page).to have_content user2.name end + + click_link 'Assign to me' expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) page.within '.js-assignee-search' do expect(page).to have_content user.name diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb index 58f11499e3f..6fed1568fcf 100644 --- a/spec/features/merge_requests/reset_filters_spec.rb +++ b/spec/features/merge_requests/reset_filters_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Issues filter reset button', feature: true, js: true do +feature 'Merge requests filter clear button', feature: true, js: true do include FilteredSearchHelpers include MergeRequestHelpers include WaitForAjax @@ -24,67 +24,93 @@ feature 'Issues filter reset button', feature: true, js: true do context 'when a milestone filter has been applied' do it 'resets the milestone filter' do visit_merge_requests(project, milestone_title: milestone.title) + expect(page).to have_css(merge_request_css, count: 1) + expect(get_filtered_search_placeholder).to eq('') reset_filters + expect(page).to have_css(merge_request_css, count: 2) + expect(get_filtered_search_placeholder).to eq(default_placeholder) end end context 'when a label filter has been applied' do it 'resets the label filter' do visit_merge_requests(project, label_name: bug.name) + expect(page).to have_css(merge_request_css, count: 1) + expect(get_filtered_search_placeholder).to eq('') reset_filters + expect(page).to have_css(merge_request_css, count: 2) + expect(get_filtered_search_placeholder).to eq(default_placeholder) end end context 'when a text search has been conducted' do it 'resets the text search filter' do visit_merge_requests(project, search: 'Bug') + expect(page).to have_css(merge_request_css, count: 1) + expect(get_filtered_search_placeholder).to eq('') reset_filters + expect(page).to have_css(merge_request_css, count: 2) + expect(get_filtered_search_placeholder).to eq(default_placeholder) end end context 'when author filter has been applied' do it 'resets the author filter' do visit_merge_requests(project, author_username: user.username) + expect(page).to have_css(merge_request_css, count: 1) + expect(get_filtered_search_placeholder).to eq('') reset_filters + expect(page).to have_css(merge_request_css, count: 2) + expect(get_filtered_search_placeholder).to eq(default_placeholder) end end context 'when assignee filter has been applied' do it 'resets the assignee filter' do visit_merge_requests(project, assignee_username: user.username) + expect(page).to have_css(merge_request_css, count: 1) + expect(get_filtered_search_placeholder).to eq('') reset_filters + expect(page).to have_css(merge_request_css, count: 2) + expect(get_filtered_search_placeholder).to eq(default_placeholder) end end context 'when all filters have been applied' do - it 'resets all filters' do + it 'clears all filters' do visit_merge_requests(project, assignee_username: user.username, author_username: user.username, milestone_title: milestone.title, label_name: bug.name, search: 'Bug') + expect(page).to have_css(merge_request_css, count: 0) + expect(get_filtered_search_placeholder).to eq('') reset_filters + expect(page).to have_css(merge_request_css, count: 2) + expect(get_filtered_search_placeholder).to eq(default_placeholder) end end context 'when no filters have been applied' do - it 'the reset link should not be visible' do + it 'the clear button should not be visible' do visit_merge_requests(project) + expect(page).to have_css(merge_request_css, count: 2) + expect(get_filtered_search_placeholder).to eq(default_placeholder) expect(page).not_to have_css(clear_search_css) end end diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index b575aeff0d8..c2db7d8da3c 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -37,7 +37,12 @@ describe 'Merge request', :feature, :js do context 'view merge request' do let!(:environment) { create(:environment, project: project) } - let!(:deployment) { create(:deployment, environment: environment, ref: 'feature', sha: merge_request.diff_head_sha) } + + let!(:deployment) do + create(:deployment, environment: environment, + ref: 'feature', + sha: merge_request.diff_head_sha) + end before do visit namespace_project_merge_request_path(project.namespace, project, merge_request) @@ -96,6 +101,26 @@ describe 'Merge request', :feature, :js do end end + context 'when merge request is in the blocked pipeline state' do + before do + create(:ci_pipeline, project: project, + sha: merge_request.diff_head_sha, + ref: merge_request.source_branch, + status: :manual) + + visit namespace_project_merge_request_path(project.namespace, + project, + merge_request) + end + + it 'shows information about blocked pipeline' do + expect(page).to have_content("Pipeline blocked") + expect(page).to have_content( + "The pipeline for this merge request requires a manual action") + expect(page).to have_css('.ci-status-icon-manual') + end + end + context 'view merge request with MWBS button' do before do commit_status = create(:commit_status, project: project, status: 'pending') diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index eb7b8a24669..0917d4dc3ef 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -4,11 +4,11 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do let(:user) { create(:user) } def active_personal_access_tokens - find(".table.active-personal-access-tokens") + find(".table.active-tokens") end def inactive_personal_access_tokens - find(".table.inactive-personal-access-tokens") + find(".table.inactive-tokens") end def created_personal_access_token @@ -26,7 +26,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do end describe "token creation" do - it "allows creation of a token" do + it "allows creation of a personal access token" do name = FFaker::Product.brand visit profile_personal_access_tokens_path @@ -43,7 +43,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do click_on "Create Personal Access Token" expect(active_personal_access_tokens).to have_text(name) - expect(active_personal_access_tokens).to have_text(Date.today.next_month.at_beginning_of_month.to_s(:medium)) + expect(active_personal_access_tokens).to have_text('In') expect(active_personal_access_tokens).to have_text('api') expect(active_personal_access_tokens).to have_text('read_user') end @@ -60,6 +60,18 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do end end + describe 'active tokens' do + let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) } + let!(:personal_access_token) { create(:personal_access_token, user: user) } + + it 'only shows personal access tokens' do + visit profile_personal_access_tokens_path + + expect(active_personal_access_tokens).to have_text(personal_access_token.name) + expect(active_personal_access_tokens).not_to have_text(impersonation_token.name) + end + end + describe "inactive tokens" do let!(:personal_access_token) { create(:personal_access_token, user: user) } diff --git a/spec/features/projects/environments/environment_metrics_spec.rb b/spec/features/projects/environments/environment_metrics_spec.rb new file mode 100644 index 00000000000..ee925e811e1 --- /dev/null +++ b/spec/features/projects/environments/environment_metrics_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +feature 'Environment > Metrics', :feature do + include PrometheusHelpers + + given(:user) { create(:user) } + given(:project) { create(:prometheus_project) } + given(:pipeline) { create(:ci_pipeline, project: project) } + given(:build) { create(:ci_build, pipeline: pipeline) } + given(:environment) { create(:environment, project: project) } + given(:current_time) { Time.now.utc } + + background do + project.add_developer(user) + create(:deployment, environment: environment, deployable: build) + stub_all_prometheus_requests(environment.slug) + + login_as(user) + visit_environment(environment) + end + + around do |example| + Timecop.freeze(current_time) { example.run } + end + + context 'with deployments and related deployable present' do + scenario 'shows metrics' do + click_link('See metrics') + + expect(page).to have_css('svg.prometheus-graph') + end + end + + def visit_environment(environment) + visit namespace_project_environment_path(environment.project.namespace, + environment.project, + environment) + end +end diff --git a/spec/features/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index 65373e3f77d..e2d16e0830a 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -37,13 +37,7 @@ feature 'Environment', :feature do scenario 'does show deployment SHA' do expect(page).to have_link(deployment.short_sha) - end - - scenario 'does not show a re-deploy button for deployment without build' do expect(page).not_to have_link('Re-deploy') - end - - scenario 'does not show terminal button' do expect(page).not_to have_terminal_button end end @@ -58,13 +52,7 @@ feature 'Environment', :feature do scenario 'does show build name' do expect(page).to have_link("#{build.name} (##{build.id})") - end - - scenario 'does show re-deploy button' do expect(page).to have_link('Re-deploy') - end - - scenario 'does not show terminal button' do expect(page).not_to have_terminal_button end @@ -117,9 +105,6 @@ feature 'Environment', :feature do it 'displays a web terminal' do expect(page).to have_selector('#terminal') - end - - it 'displays a link to the environment external url' do expect(page).to have_link(nil, href: environment.external_url) end end @@ -147,10 +132,6 @@ feature 'Environment', :feature do on_stop: 'close_app') end - scenario 'does show stop button' do - expect(page).to have_link('Stop') - end - scenario 'does allow to stop environment' do click_link('Stop') diff --git a/spec/features/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 169695b0759..169695b0759 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb index 7414ce21f59..de3c6eceb82 100644 --- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb +++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb @@ -32,7 +32,7 @@ feature 'Issue prioritization', feature: true do visit namespace_project_issues_path(project.namespace, project, sort: 'priority') # Ensure we are indicating that issues are sorted by priority - expect(page).to have_selector('.dropdown-toggle', text: 'Priority') + expect(page).to have_selector('.dropdown-toggle', text: 'Label priority') page.within('.issues-holder') do issue_titles = all('.issues-list .issue-title-text').map(&:text) @@ -70,7 +70,7 @@ feature 'Issue prioritization', feature: true do login_as user visit namespace_project_issues_path(project.namespace, project, sort: 'priority') - expect(page).to have_selector('.dropdown-toggle', text: 'Priority') + expect(page).to have_selector('.dropdown-toggle', text: 'Label priority') page.within('.issues-holder') do issue_titles = all('.issues-list .issue-title-text').map(&:text) diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb index 0b4dcaa39c6..b64c15e0adc 100644 --- a/spec/features/projects/members/user_requests_access_spec.rb +++ b/spec/features/projects/members/user_requests_access_spec.rb @@ -57,6 +57,12 @@ feature 'Projects > Members > User requests access', feature: true do end def open_project_settings_menu - find('#project-settings-button').click + page.within('.layout-nav .nav-links') do + click_link('Settings') + end + + page.within('.page-with-layout-nav .sub-nav') do + click_link('Members') + end end end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index c30d38b6508..3a1240f95b5 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -18,7 +18,7 @@ feature 'Project', feature: true do it 'passes through html-pipeline' do project.update_attribute(:description, 'This project is the :poop:') visit path - expect(page).to have_css('.project-home-desc > p > img') + expect(page).to have_css('.project-home-desc > p > gl-emoji') end it 'sanitizes unwanted tags' do diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 7da05defa81..a6560a81096 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe "Search", feature: true do + include FilteredSearchHelpers include WaitForAjax let(:user) { create(:user) } @@ -170,7 +171,8 @@ describe "Search", feature: true do sleep 2 expect(page).to have_selector('.filtered-search') - expect(find('.filtered-search').value).to eq("assignee:@#{user.username}") + expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_filtered_search_input_empty end it 'takes user to her issues page when issues authored is clicked' do @@ -178,7 +180,8 @@ describe "Search", feature: true do sleep 2 expect(page).to have_selector('.filtered-search') - expect(find('.filtered-search').value).to eq("author:@#{user.username}") + expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_filtered_search_input_empty end it 'takes user to her MR page when MR assigned is clicked' do @@ -186,7 +189,8 @@ describe "Search", feature: true do sleep 2 expect(page).to have_selector('.merge-requests-holder') - expect(find('.filtered-search').value).to eq("assignee:@#{user.username}") + expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) + expect_filtered_search_input_empty end it 'takes user to her MR page when MR authored is clicked' do @@ -194,7 +198,8 @@ describe "Search", feature: true do sleep 2 expect(page).to have_selector('.merge-requests-holder') - expect(find('.filtered-search').value).to eq("author:@#{user.username}") + expect_tokens([{ name: 'author', value: "@#{user.username}" }]) + expect_filtered_search_input_empty end end diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 24af062d763..1a66d1a6a1e 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -110,6 +110,20 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_denied_for(:external) } end + describe "GET /:project_path/settings/repository" do + subject { namespace_project_settings_repository_path(project.namespace, project) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_denied_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:visitor) } + it { is_expected.to be_denied_for(:external) } + end + describe "GET /:project_path/blob" do let(:commit) { project.repository.commit } subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) } diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index c511dcfa18e..ad3bd60a313 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -110,6 +110,20 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for(:external) } end + describe "GET /:project_path/settings/repository" do + subject { namespace_project_settings_repository_path(project.namespace, project) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_denied_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } + end + describe "GET /:project_path/blob" do let(:commit) { project.repository.commit } subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore'))} diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index d8cc012c27e..e06aab4e0b2 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -110,6 +110,20 @@ describe "Public Project Access", feature: true do it { is_expected.to be_denied_for(:external) } end + describe "GET /:project_path/settings/repository" do + subject { namespace_project_settings_repository_path(project.namespace, project) } + + it { is_expected.to be_allowed_for(:admin) } + it { is_expected.to be_allowed_for(:owner).of(project) } + it { is_expected.to be_allowed_for(:master).of(project) } + it { is_expected.to be_denied_for(:developer).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:visitor) } + it { is_expected.to be_denied_for(:external) } + end + describe "GET /:project_path/pipelines" do subject { namespace_project_pipelines_path(project.namespace, project) } diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb index fec28c55d30..4d5bd476301 100644 --- a/spec/features/todos/todos_sorting_spec.rb +++ b/spec/features/todos/todos_sorting_spec.rb @@ -56,8 +56,8 @@ describe "Dashboard > User sorts todos", feature: true do expect(results_list.all('p')[4]).to have_content("merge_request_1") end - it "sorts by priority" do - click_link "Priority" + it "sorts by label priority" do + click_link "Label priority" results_list = page.find('.todos-list') expect(results_list.all('p')[0]).to have_content("issue_3") @@ -85,8 +85,8 @@ describe "Dashboard > User sorts todos", feature: true do visit dashboard_todos_path end - it "doesn't mix issues and merge requests priorities" do - click_link "Priority" + it "doesn't mix issues and merge requests label priorities" do + click_link "Label priority" results_list = page.find('.todos-list') expect(results_list.all('p')[0]).to have_content("issue_1") diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb index 4a7511589d6..c1ae6db00c6 100644 --- a/spec/features/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -1,28 +1,175 @@ require 'spec_helper' -describe 'Triggers' do +feature 'Triggers', feature: true, js: true do + let(:trigger_title) { 'trigger desc' } let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:guest_user) { create(:user) } before { login_as(user) } before do - @project = FactoryGirl.create :empty_project + @project = create(:empty_project) @project.team << [user, :master] + @project.team << [user2, :master] + @project.team << [guest_user, :guest] visit namespace_project_settings_ci_cd_path(@project.namespace, @project) end - context 'create a trigger' do - before do - click_on 'Add trigger' - expect(@project.triggers.count).to eq(1) + describe 'create trigger workflow' do + scenario 'prevents adding new trigger with no description' do + fill_in 'trigger_description', with: '' + click_button 'Add trigger' + + # See if input has error due to empty value + expect(page.find('form.gl-show-field-errors .gl-field-error')['style']).to eq 'display: block;' + end + + scenario 'adds new trigger with description' do + fill_in 'trigger_description', with: 'trigger desc' + click_button 'Add trigger' + + # See if "trigger creation successful" message displayed and description and owner are correct + expect(page.find('.flash-notice')).to have_content 'Trigger was created successfully.' + expect(page.find('.triggers-list')).to have_content 'trigger desc' + expect(page.find('.triggers-list .trigger-owner')).to have_content @user.name + end + end + + describe 'edit trigger workflow' do + let(:new_trigger_title) { 'new trigger' } + + scenario 'click on edit trigger opens edit trigger page' do + create(:ci_trigger, owner: user, project: @project, description: trigger_title) + visit namespace_project_settings_ci_cd_path(@project.namespace, @project) + + # See if edit page has correct descrption + find('a[title="Edit"]').click + expect(page.find('#trigger_description').value).to have_content 'trigger desc' + end + + scenario 'edit trigger and save' do + create(:ci_trigger, owner: user, project: @project, description: trigger_title) + visit namespace_project_settings_ci_cd_path(@project.namespace, @project) + + # See if edit page opens, then fill in new description and save + find('a[title="Edit"]').click + fill_in 'trigger_description', with: new_trigger_title + click_button 'Save trigger' + + # See if "trigger updated successfully" message displayed and description and owner are correct + expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.' + expect(page.find('.triggers-list')).to have_content new_trigger_title + expect(page.find('.triggers-list .trigger-owner')).to have_content @user.name + end + + scenario 'edit "legacy" trigger and save' do + # Create new trigger without owner association, i.e. Legacy trigger + create(:ci_trigger, owner: nil, project: @project) + visit namespace_project_settings_ci_cd_path(@project.namespace, @project) + + # See if the trigger can be edited and description is blank + find('a[title="Edit"]').click + expect(page.find('#trigger_description').value).to have_content '' + + # See if trigger can be updated with description and saved successfully + fill_in 'trigger_description', with: new_trigger_title + click_button 'Save trigger' + expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.' + expect(page.find('.triggers-list')).to have_content new_trigger_title + end + end + + describe 'trigger "Take ownership" workflow' do + before(:each) do + create(:ci_trigger, owner: user2, project: @project, description: trigger_title) + visit namespace_project_settings_ci_cd_path(@project.namespace, @project) + end + + scenario 'button "Take ownership" has correct alert' do + expected_alert = 'By taking ownership you will bind this trigger to your user account. With this the trigger will have access to all your projects as if it was you. Are you sure?' + expect(page.find('a.btn-trigger-take-ownership')['data-confirm']).to eq expected_alert end - it 'contains trigger token' do - expect(page).to have_content(@project.triggers.first.token) + scenario 'take trigger ownership' do + # See if "Take ownership" on trigger works post trigger creation + find('a.btn-trigger-take-ownership').click + page.accept_confirm do + expect(page.find('.flash-notice')).to have_content 'Trigger was re-assigned.' + expect(page.find('.triggers-list')).to have_content trigger_title + expect(page.find('.triggers-list .trigger-owner')).to have_content @user.name + end end + end + + describe 'trigger "Revoke" workflow' do + before(:each) do + create(:ci_trigger, owner: user2, project: @project, description: trigger_title) + visit namespace_project_settings_ci_cd_path(@project.namespace, @project) + end + + scenario 'button "Revoke" has correct alert' do + expected_alert = 'By revoking a trigger you will break any processes making use of it. Are you sure?' + expect(page.find('a.btn-trigger-revoke')['data-confirm']).to eq expected_alert + end + + scenario 'revoke trigger' do + # See if "Revoke" on trigger works post trigger creation + find('a.btn-trigger-revoke').click + page.accept_confirm do + expect(page.find('.flash-notice')).to have_content 'Trigger removed' + expect(page).to have_selector('p.settings-message.text-center.append-bottom-default') + end + end + end + + describe 'show triggers workflow' do + scenario 'contains trigger description placeholder' do + expect(page.find('#trigger_description')['placeholder']).to eq 'Trigger description' + end + + scenario 'show "legacy" badge for legacy trigger' do + create(:ci_trigger, owner: nil, project: @project) + visit namespace_project_settings_ci_cd_path(@project.namespace, @project) + + # See if trigger without owner (i.e. legacy) shows "legacy" badge and is editable + expect(page.find('.triggers-list')).to have_content 'legacy' + expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]') + end + + scenario 'show "invalid" badge for trigger with owner having insufficient permissions' do + create(:ci_trigger, owner: guest_user, project: @project, description: trigger_title) + visit namespace_project_settings_ci_cd_path(@project.namespace, @project) + + # See if trigger without owner (i.e. legacy) shows "legacy" badge and is non-editable + expect(page.find('.triggers-list')).to have_content 'invalid' + expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]') + end + + scenario 'do not show "Edit" or full token for not owned trigger' do + # Create trigger with user different from current_user + create(:ci_trigger, owner: user2, project: @project, description: trigger_title) + visit namespace_project_settings_ci_cd_path(@project.namespace, @project) + + # See if trigger not owned by current_user shows only first few token chars and doesn't have copy-to-clipboard button + expect(page.find('.triggers-list')).to have_content(@project.triggers.first.token[0..3]) + expect(page.find('.triggers-list')).not_to have_selector('button.btn-clipboard') + + # See if trigger owner name doesn't match with current_user and trigger is non-editable + expect(page.find('.triggers-list .trigger-owner')).not_to have_content @user.name + expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]') + end + + scenario 'show "Edit" and full token for owned trigger' do + create(:ci_trigger, owner: user, project: @project, description: trigger_title) + visit namespace_project_settings_ci_cd_path(@project.namespace, @project) + + # See if trigger shows full token and has copy-to-clipboard button + expect(page.find('.triggers-list')).to have_content @project.triggers.first.token + expect(page.find('.triggers-list')).to have_selector('button.btn-clipboard') - it 'revokes the trigger' do - click_on 'Revoke' - expect(@project.triggers.count).to eq(0) + # See if trigger owner name matches with current_user and is editable + expect(page.find('.triggers-list .trigger-owner')).to have_content @user.name + expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]') end end end diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb new file mode 100644 index 00000000000..fd92664ca24 --- /dev/null +++ b/spec/finders/personal_access_tokens_finder_spec.rb @@ -0,0 +1,196 @@ +require 'spec_helper' + +describe PersonalAccessTokensFinder do + def finder(options = {}) + described_class.new(options) + end + + describe '#execute' do + let(:user) { create(:user) } + let(:params) { {} } + let!(:active_personal_access_token) { create(:personal_access_token, user: user) } + let!(:expired_personal_access_token) { create(:personal_access_token, :expired, user: user) } + let!(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) } + let!(:active_impersonation_token) { create(:personal_access_token, :impersonation, user: user) } + let!(:expired_impersonation_token) { create(:personal_access_token, :expired, :impersonation, user: user) } + let!(:revoked_impersonation_token) { create(:personal_access_token, :revoked, :impersonation, user: user) } + + subject { finder(params).execute } + + describe 'without user' do + it do + is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token, + revoked_personal_access_token, expired_personal_access_token, + revoked_impersonation_token, expired_impersonation_token) + end + + describe 'without impersonation' do + before { params[:impersonation] = false } + + it { is_expected.to contain_exactly(active_personal_access_token, revoked_personal_access_token, expired_personal_access_token) } + + describe 'with active state' do + before { params[:state] = 'active' } + + it { is_expected.to contain_exactly(active_personal_access_token) } + end + + describe 'with inactive state' do + before { params[:state] = 'inactive' } + + it { is_expected.to contain_exactly(revoked_personal_access_token, expired_personal_access_token) } + end + end + + describe 'with impersonation' do + before { params[:impersonation] = true } + + it { is_expected.to contain_exactly(active_impersonation_token, revoked_impersonation_token, expired_impersonation_token) } + + describe 'with active state' do + before { params[:state] = 'active' } + + it { is_expected.to contain_exactly(active_impersonation_token) } + end + + describe 'with inactive state' do + before { params[:state] = 'inactive' } + + it { is_expected.to contain_exactly(revoked_impersonation_token, expired_impersonation_token) } + end + end + + describe 'with active state' do + before { params[:state] = 'active' } + + it { is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token) } + end + + describe 'with inactive state' do + before { params[:state] = 'inactive' } + + it do + is_expected.to contain_exactly(expired_personal_access_token, revoked_personal_access_token, + expired_impersonation_token, revoked_impersonation_token) + end + end + + describe 'with id' do + subject { finder(params).find_by(id: active_personal_access_token.id) } + + it { is_expected.to eq(active_personal_access_token) } + + describe 'with impersonation' do + before { params[:impersonation] = true } + + it { is_expected.to be_nil } + end + end + + describe 'with token' do + subject { finder(params).find_by(token: active_personal_access_token.token) } + + it { is_expected.to eq(active_personal_access_token) } + + describe 'with impersonation' do + before { params[:impersonation] = true } + + it { is_expected.to be_nil } + end + end + end + + describe 'with user' do + let(:user2) { create(:user) } + let!(:other_user_active_personal_access_token) { create(:personal_access_token, user: user2) } + let!(:other_user_expired_personal_access_token) { create(:personal_access_token, :expired, user: user2) } + let!(:other_user_revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user2) } + let!(:other_user_active_impersonation_token) { create(:personal_access_token, :impersonation, user: user2) } + let!(:other_user_expired_impersonation_token) { create(:personal_access_token, :expired, :impersonation, user: user2) } + let!(:other_user_revoked_impersonation_token) { create(:personal_access_token, :revoked, :impersonation, user: user2) } + + before { params[:user] = user } + + it do + is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token, + revoked_personal_access_token, expired_personal_access_token, + revoked_impersonation_token, expired_impersonation_token) + end + + describe 'without impersonation' do + before { params[:impersonation] = false } + + it { is_expected.to contain_exactly(active_personal_access_token, revoked_personal_access_token, expired_personal_access_token) } + + describe 'with active state' do + before { params[:state] = 'active' } + + it { is_expected.to contain_exactly(active_personal_access_token) } + end + + describe 'with inactive state' do + before { params[:state] = 'inactive' } + + it { is_expected.to contain_exactly(revoked_personal_access_token, expired_personal_access_token) } + end + end + + describe 'with impersonation' do + before { params[:impersonation] = true } + + it { is_expected.to contain_exactly(active_impersonation_token, revoked_impersonation_token, expired_impersonation_token) } + + describe 'with active state' do + before { params[:state] = 'active' } + + it { is_expected.to contain_exactly(active_impersonation_token) } + end + + describe 'with inactive state' do + before { params[:state] = 'inactive' } + + it { is_expected.to contain_exactly(revoked_impersonation_token, expired_impersonation_token) } + end + end + + describe 'with active state' do + before { params[:state] = 'active' } + + it { is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token) } + end + + describe 'with inactive state' do + before { params[:state] = 'inactive' } + + it do + is_expected.to contain_exactly(expired_personal_access_token, revoked_personal_access_token, + expired_impersonation_token, revoked_impersonation_token) + end + end + + describe 'with id' do + subject { finder(params).find_by(id: active_personal_access_token.id) } + + it { is_expected.to eq(active_personal_access_token) } + + describe 'with impersonation' do + before { params[:impersonation] = true } + + it { is_expected.to be_nil } + end + end + + describe 'with token' do + subject { finder(params).find_by(token: active_personal_access_token.token) } + + it { is_expected.to eq(active_personal_access_token) } + + describe 'with impersonation' do + before { params[:impersonation] = true } + + it { is_expected.to be_nil } + end + end + end + end +end diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json index 8e19cee5440..21c078e0f44 100644 --- a/spec/fixtures/api/schemas/issue.json +++ b/spec/fixtures/api/schemas/issue.json @@ -11,6 +11,7 @@ "title": { "type": "string" }, "confidential": { "type": "boolean" }, "due_date": { "type": ["date", "null"] }, + "relative_position": { "type": "integer" }, "labels": { "type": "array", "items": { diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index 594b40303bc..81ba693f2f3 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -61,6 +61,13 @@ describe EventsHelper do '</code></pre>' expect(helper.event_note(input)).to eq(expected) end + + it 'preserves style attribute within a tag' do + input = '<span class="" style="background-color: #44ad8e; color: #FFFFFF;"></span>' + expected = '<p><span style="background-color: #44ad8e; color: #FFFFFF;"></span></p>' + + expect(helper.event_note(input)).to eq(expected) + end end describe '#event_commit_title' do diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index b8ec3521edb..9ffd4b9371c 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -113,7 +113,7 @@ describe GitlabMarkdownHelper do it 'replaces commit message with emoji to link' do actual = link_to_gfm(':book:Book', '/foo') expect(actual). - to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://#{Gitlab.config.gitlab.host}/assets/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>) + to eq '<gl-emoji data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo">Book</a>' end end diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb index baab30f482f..cf182e6d221 100644 --- a/spec/initializers/6_validations_spec.rb +++ b/spec/initializers/6_validations_spec.rb @@ -14,7 +14,7 @@ describe '6_validations', lib: true do context 'with correct settings' do before do - mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/d') + mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/d' }) end it 'passes through' do @@ -24,7 +24,7 @@ describe '6_validations', lib: true do context 'with invalid storage names' do before do - mock_storages('name with spaces' => 'tmp/tests/paths/a/b/c') + mock_storages('name with spaces' => { 'path' => 'tmp/tests/paths/a/b/c' }) end it 'throws an error' do @@ -34,7 +34,7 @@ describe '6_validations', lib: true do context 'with nested storage paths' do before do - mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/c/d') + mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c/d' }) end it 'throws an error' do @@ -44,7 +44,7 @@ describe '6_validations', lib: true do context 'with similar but un-nested storage paths' do before do - mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/c2') + mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c2' }) end it 'passes through' do @@ -52,6 +52,26 @@ describe '6_validations', lib: true do end end + context 'with incomplete settings' do + before do + mock_storages('foo' => {}) + end + + it 'throws an error suggesting the user to update its settings' do + expect { validate_storages }.to raise_error('foo is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example. Please fix this in your gitlab.yml before starting GitLab.') + end + end + + context 'with deprecated settings structure' do + before do + mock_storages('foo' => 'tmp/tests/paths/a/b/c') + end + + it 'throws an error suggesting the user to update its settings' do + expect { validate_storages }.to raise_error("foo is not a valid storage, because it has no `path` key. It may be configured as:\n\nfoo:\n path: tmp/tests/paths/a/b/c\n\nRefer to gitlab.yml.example for an updated example. Please fix this in your gitlab.yml before starting GitLab.") + end + end + def mock_storages(storages) allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) end diff --git a/spec/initializers/doorkeeper_spec.rb b/spec/initializers/doorkeeper_spec.rb new file mode 100644 index 00000000000..74bdbb01166 --- /dev/null +++ b/spec/initializers/doorkeeper_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' +require_relative '../../config/initializers/doorkeeper' + +describe Doorkeeper.configuration do + describe '#default_scopes' do + it 'matches Gitlab::Auth::DEFAULT_SCOPES' do + expect(subject.default_scopes).to eq Gitlab::Auth::DEFAULT_SCOPES + end + end + + describe '#optional_scopes' do + it 'matches Gitlab::Auth::OPTIONAL_SCOPES' do + expect(subject.optional_scopes).to eq Gitlab::Auth::OPTIONAL_SCOPES + end + end + + describe '#resource_owner_authenticator' do + subject { controller.instance_exec(&Doorkeeper.configuration.authenticate_resource_owner) } + + let(:controller) { double } + + before do + allow(controller).to receive(:current_user).and_return(current_user) + allow(controller).to receive(:session).and_return({}) + allow(controller).to receive(:request).and_return(OpenStruct.new(fullpath: '/return-path')) + allow(controller).to receive(:redirect_to) + allow(controller).to receive(:new_user_session_url).and_return('/login') + end + + context 'with a user present' do + let(:current_user) { create(:user) } + + it 'returns the user' do + expect(subject).to eq current_user + end + + it 'does not redirect' do + expect(controller).not_to receive(:redirect_to) + + subject + end + + it 'does not store the return path' do + subject + + expect(controller.session).not_to include :user_return_to + end + end + + context 'without a user present' do + let(:current_user) { nil } + + # NOTE: this is required for doorkeeper-openid_connect + it 'returns nil' do + expect(subject).to eq nil + end + + it 'redirects to the login form' do + expect(controller).to receive(:redirect_to).with('/login') + + subject + end + + it 'stores the return path' do + subject + + expect(controller.session[:user_return_to]).to eq '/return-path' + end + end + end +end diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb index ad7f032d1e5..65c97da2efd 100644 --- a/spec/initializers/secret_token_spec.rb +++ b/spec/initializers/secret_token_spec.rb @@ -6,6 +6,9 @@ describe 'create_tokens', lib: true do let(:secrets) { ActiveSupport::OrderedOptions.new } + HEX_KEY = /\h{128}/ + RSA_KEY = /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m + before do allow(File).to receive(:write) allow(File).to receive(:delete) @@ -15,7 +18,7 @@ describe 'create_tokens', lib: true do allow(self).to receive(:exit) end - context 'setting secret_key_base and otp_key_base' do + context 'setting secret keys' do context 'when none of the secrets exist' do before do stub_env('SECRET_KEY_BASE', nil) @@ -24,19 +27,29 @@ describe 'create_tokens', lib: true do allow(self).to receive(:warn_missing_secret) end - it 'generates different secrets for secret_key_base, otp_key_base, and db_key_base' do + it 'generates different hashes for secret_key_base, otp_key_base, and db_key_base' do create_tokens keys = secrets.values_at(:secret_key_base, :otp_key_base, :db_key_base) expect(keys.uniq).to eq(keys) - expect(keys.map(&:length)).to all(eq(128)) + expect(keys).to all(match(HEX_KEY)) + end + + it 'generates an RSA key for jws_private_key' do + create_tokens + + keys = secrets.values_at(:jws_private_key) + + expect(keys.uniq).to eq(keys) + expect(keys).to all(match(RSA_KEY)) end it 'warns about the secrets to add to secrets.yml' do expect(self).to receive(:warn_missing_secret).with('secret_key_base') expect(self).to receive(:warn_missing_secret).with('otp_key_base') expect(self).to receive(:warn_missing_secret).with('db_key_base') + expect(self).to receive(:warn_missing_secret).with('jws_private_key') create_tokens end @@ -48,6 +61,7 @@ describe 'create_tokens', lib: true do expect(new_secrets['secret_key_base']).to eq(secrets.secret_key_base) expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base) expect(new_secrets['db_key_base']).to eq(secrets.db_key_base) + expect(new_secrets['jws_private_key']).to eq(secrets.jws_private_key) end create_tokens @@ -63,6 +77,7 @@ describe 'create_tokens', lib: true do context 'when the other secrets all exist' do before do secrets.db_key_base = 'db_key_base' + secrets.jws_private_key = 'jws_private_key' allow(File).to receive(:exist?).with('.secret').and_return(true) allow(File).to receive(:read).with('.secret').and_return('file_key') @@ -73,6 +88,7 @@ describe 'create_tokens', lib: true do stub_env('SECRET_KEY_BASE', 'env_key') secrets.secret_key_base = 'secret_key_base' secrets.otp_key_base = 'otp_key_base' + secrets.jws_private_key = 'jws_private_key' end it 'does not issue a warning' do @@ -98,6 +114,7 @@ describe 'create_tokens', lib: true do before do secrets.secret_key_base = 'secret_key_base' secrets.otp_key_base = 'otp_key_base' + secrets.jws_private_key = 'jws_private_key' end it 'does not write any files' do @@ -112,6 +129,7 @@ describe 'create_tokens', lib: true do expect(secrets.secret_key_base).to eq('secret_key_base') expect(secrets.otp_key_base).to eq('otp_key_base') expect(secrets.db_key_base).to eq('db_key_base') + expect(secrets.jws_private_key).to eq('jws_private_key') end it 'deletes the .secret file' do @@ -135,6 +153,7 @@ describe 'create_tokens', lib: true do expect(new_secrets['secret_key_base']).to eq('file_key') expect(new_secrets['otp_key_base']).to eq('file_key') expect(new_secrets['db_key_base']).to eq('db_key_base') + expect(new_secrets['jws_private_key']).to eq('jws_private_key') end create_tokens diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index e5826f9c29f..f4b1d777203 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,11 +1,11 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */ -/* global AwardsHandler */ -require('~/awards_handler'); -require('./fixtures/emoji_menu'); +require('es6-promise').polyfill(); + +const AwardsHandler = require('~/awards_handler'); (function() { - var awardsHandler, lazyAssert, urlRoot; + var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu; awardsHandler = null; @@ -13,14 +13,6 @@ require('./fixtures/emoji_menu'); window.gon || (window.gon = {}); - gl.emojiAliases = function() { - return { - '+1': 'thumbsup', - '-1': 'thumbsdown' - }; - }; - - gon.award_menu_url = '/emojis'; urlRoot = gon.relative_url_root; lazyAssert = function(done, assertFn) { @@ -32,22 +24,40 @@ require('./fixtures/emoji_menu'); }; describe('AwardsHandler', function() { - preloadFixtures('issues/open-issue.html.raw'); + preloadFixtures('issues/issue_with_comment.html.raw'); beforeEach(function() { - loadFixtures('issues/open-issue.html.raw'); + loadFixtures('issues/issue_with_comment.html.raw'); awardsHandler = new AwardsHandler; spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) { return function(url, emoji, cb) { return cb(); }; })(this)); - spyOn(jQuery, 'get').and.callFake(function(req, cb) { - return cb(window.emojiMenu); - }); + + let isEmojiMenuBuilt = false; + openAndWaitForEmojiMenu = function() { + return new Promise((resolve, reject) => { + if (isEmojiMenuBuilt) { + resolve(); + } else { + $('.js-add-award').eq(0).click(); + const $menu = $('.emoji-menu'); + $menu.one('build-emoji-menu-finish', () => { + isEmojiMenuBuilt = true; + resolve(); + }); + + // Fail after 1 second + setTimeout(reject, 1000); + } + }); + }; }); afterEach(function() { // restore original url root value gon.relative_url_root = urlRoot; + + awardsHandler.destroy(); }); describe('::showEmojiMenu', function() { it('should show emoji menu when Add emoji button clicked', function(done) { @@ -62,10 +72,9 @@ require('./fixtures/emoji_menu'); }); }); it('should also show emoji menu for the smiley icon in notes', function(done) { - $('.note-action-button').click(); + $('.js-add-award.note-action-button').click(); return lazyAssert(done, function() { - var $emojiMenu; - $emojiMenu = $('.emoji-menu'); + var $emojiMenu = $('.emoji-menu'); return expect($emojiMenu.length).toBe(1); }); }); @@ -86,7 +95,7 @@ require('./fixtures/emoji_menu'); var $emojiButton, $votesBlock; $votesBlock = $('.js-awards-block').eq(0); awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); - $emojiButton = $votesBlock.find('[data-emoji=heart]'); + $emojiButton = $votesBlock.find('[data-name=heart]'); expect($emojiButton.length).toBe(1); expect($emojiButton.next('.js-counter').text()).toBe('1'); return expect($votesBlock.hasClass('hidden')).toBe(false); @@ -96,14 +105,14 @@ require('./fixtures/emoji_menu'); $votesBlock = $('.js-awards-block').eq(0); awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); - $emojiButton = $votesBlock.find('[data-emoji=heart]'); + $emojiButton = $votesBlock.find('[data-name=heart]'); return expect($emojiButton.length).toBe(0); }); return it('should decrement the emoji counter', function() { var $emojiButton, $votesBlock; $votesBlock = $('.js-awards-block').eq(0); awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); - $emojiButton = $votesBlock.find('[data-emoji=heart]'); + $emojiButton = $votesBlock.find('[data-name=heart]'); $emojiButton.next('.js-counter').text(5); awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); expect($emojiButton.length).toBe(1); @@ -120,8 +129,8 @@ require('./fixtures/emoji_menu'); var $thumbsDownEmoji, $thumbsUpEmoji, $votesBlock, awardUrl; awardUrl = awardsHandler.getAwardUrl(); $votesBlock = $('.js-awards-block').eq(0); - $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent(); - $thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent(); + $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); + $thumbsDownEmoji = $votesBlock.find('[data-name=thumbsdown]').parent(); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); expect($thumbsUpEmoji.hasClass('active')).toBe(true); expect($thumbsDownEmoji.hasClass('active')).toBe(false); @@ -138,9 +147,9 @@ require('./fixtures/emoji_menu'); awardUrl = awardsHandler.getAwardUrl(); $votesBlock = $('.js-awards-block').eq(0); awardsHandler.addAward($votesBlock, awardUrl, 'fire', false); - expect($votesBlock.find('[data-emoji=fire]').length).toBe(1); - awardsHandler.removeEmoji($votesBlock.find('[data-emoji=fire]').closest('button')); - return expect($votesBlock.find('[data-emoji=fire]').length).toBe(0); + expect($votesBlock.find('[data-name=fire]').length).toBe(1); + awardsHandler.removeEmoji($votesBlock.find('[data-name=fire]').closest('button')); + return expect($votesBlock.find('[data-name=fire]').length).toBe(0); }); }); describe('::addYouToUserList', function() { @@ -148,7 +157,7 @@ require('./fixtures/emoji_menu'); var $thumbsUpEmoji, $votesBlock, awardUrl; awardUrl = awardsHandler.getAwardUrl(); $votesBlock = $('.js-awards-block').eq(0); - $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent(); + $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); $thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); @@ -158,7 +167,7 @@ require('./fixtures/emoji_menu'); var $thumbsUpEmoji, $votesBlock, awardUrl; awardUrl = awardsHandler.getAwardUrl(); $votesBlock = $('.js-awards-block').eq(0); - $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent(); + $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); $thumbsUpEmoji.attr('data-title', 'sam'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); @@ -170,7 +179,7 @@ require('./fixtures/emoji_menu'); var $thumbsUpEmoji, $votesBlock, awardUrl; awardUrl = awardsHandler.getAwardUrl(); $votesBlock = $('.js-awards-block').eq(0); - $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent(); + $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); $thumbsUpEmoji.attr('data-title', 'You, sam, jerry, max, and andy'); $thumbsUpEmoji.addClass('active'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); @@ -181,7 +190,7 @@ require('./fixtures/emoji_menu'); var $thumbsUpEmoji, $votesBlock, awardUrl; awardUrl = awardsHandler.getAwardUrl(); $votesBlock = $('.js-awards-block').eq(0); - $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent(); + $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); $thumbsUpEmoji.attr('data-title', 'You and sam'); $thumbsUpEmoji.addClass('active'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); @@ -190,42 +199,58 @@ require('./fixtures/emoji_menu'); }); }); describe('search', function() { - return it('should filter the emoji', function() { - $('.js-add-award').eq(0).click(); - expect($('[data-emoji=angel]').is(':visible')).toBe(true); - expect($('[data-emoji=anger]').is(':visible')).toBe(true); - $('#emoji_search').val('ali').trigger('keyup'); - expect($('[data-emoji=angel]').is(':visible')).toBe(false); - expect($('[data-emoji=anger]').is(':visible')).toBe(false); - return expect($('[data-emoji=alien]').is(':visible')).toBe(true); + return it('should filter the emoji', function(done) { + return openAndWaitForEmojiMenu() + .then(() => { + expect($('[data-name=angel]').is(':visible')).toBe(true); + expect($('[data-name=anger]').is(':visible')).toBe(true); + $('#emoji_search').val('ali').trigger('input'); + expect($('[data-name=angel]').is(':visible')).toBe(false); + expect($('[data-name=anger]').is(':visible')).toBe(false); + expect($('[data-name=alien]').is(':visible')).toBe(true); + }) + .then(done) + .catch(() => { + done.fail('Failed to open and build emoji menu'); + }); }); }); - return describe('emoji menu', function() { - var openEmojiMenuAndAddEmoji, selector; - selector = '[data-emoji=sunglasses]'; - openEmojiMenuAndAddEmoji = function() { - var $block, $emoji, $menu; - $('.js-add-award').eq(0).click(); - $menu = $('.emoji-menu'); - $block = $('.js-awards-block'); - $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + selector); - expect($emoji.length).toBe(1); - expect($block.find(selector).length).toBe(0); - $emoji.click(); - expect($menu.hasClass('.is-visible')).toBe(false); - return expect($block.find(selector).length).toBe(1); + describe('emoji menu', function() { + const emojiSelector = '[data-name="sunglasses"]'; + const openEmojiMenuAndAddEmoji = function() { + return openAndWaitForEmojiMenu() + .then(() => { + const $menu = $('.emoji-menu'); + const $block = $('.js-awards-block'); + const $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + emojiSelector); + + expect($emoji.length).toBe(1); + expect($block.find(emojiSelector).length).toBe(0); + $emoji.click(); + expect($menu.hasClass('.is-visible')).toBe(false); + expect($block.find(emojiSelector).length).toBe(1); + }); }; - it('should add selected emoji to awards block', function() { - return openEmojiMenuAndAddEmoji(); + it('should add selected emoji to awards block', function(done) { + return openEmojiMenuAndAddEmoji() + .then(done) + .catch(() => { + done.fail('Failed to open and build emoji menu'); + }); }); - return it('should remove already selected emoji', function() { - var $block, $emoji; - openEmojiMenuAndAddEmoji(); - $('.js-add-award').eq(0).click(); - $block = $('.js-awards-block'); - $emoji = $('.emoji-menu').find(".emoji-menu-list:not(.frequent-emojis) " + selector); - $emoji.click(); - return expect($block.find(selector).length).toBe(0); + it('should remove already selected emoji', function(done) { + return openEmojiMenuAndAddEmoji() + .then(() => { + $('.js-add-award').eq(0).click(); + const $block = $('.js-awards-block'); + const $emoji = $('.emoji-menu').find(`.emoji-menu-list:not(.frequent-emojis) ${emojiSelector}`); + $emoji.click(); + expect($block.find(emojiSelector).length).toBe(0); + }) + .then(done) + .catch((err) => { + done.fail('Failed to open and build emoji menu'); + }); }); }); }); diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 9dd741a680b..49a2ca4a78f 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -5,6 +5,7 @@ /* global Cookies */ /* global listObj */ /* global listObjDuplicate */ +/* global ListIssue */ require('~/lib/utils/url_utility'); require('~/boards/models/issue'); @@ -14,6 +15,7 @@ require('~/boards/models/user'); require('~/boards/services/board_service'); require('~/boards/stores/boards_store'); require('./mock_data'); +require('es6-promise').polyfill(); describe('Store', () => { beforeEach(() => { @@ -21,6 +23,10 @@ describe('Store', () => { gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); gl.issueBoards.BoardsStore.create(); + spyOn(gl.boardService, 'moveIssue').and.callFake(() => new Promise((resolve) => { + resolve(); + })); + Cookies.set('issue_board_welcome_hidden', 'false', { expires: 365 * 10, path: '' @@ -154,5 +160,74 @@ describe('Store', () => { done(); }, 0); }); + + it('moves issue to top of another list', (done) => { + const listOne = gl.issueBoards.BoardsStore.addList(listObj); + const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); + + expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); + + setTimeout(() => { + listOne.issues[0].id = 2; + + expect(listOne.issues.length).toBe(1); + expect(listTwo.issues.length).toBe(1); + + gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 0); + + expect(listOne.issues.length).toBe(0); + expect(listTwo.issues.length).toBe(2); + expect(listTwo.issues[0].id).toBe(2); + expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, null, 1); + + done(); + }, 0); + }); + + it('moves issue to bottom of another list', (done) => { + const listOne = gl.issueBoards.BoardsStore.addList(listObj); + const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); + + expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); + + setTimeout(() => { + listOne.issues[0].id = 2; + + expect(listOne.issues.length).toBe(1); + expect(listTwo.issues.length).toBe(1); + + gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 1); + + expect(listOne.issues.length).toBe(0); + expect(listTwo.issues.length).toBe(2); + expect(listTwo.issues[1].id).toBe(2); + expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, 1, null); + + done(); + }, 0); + }); + + it('moves issue in list', (done) => { + const issue = new ListIssue({ + title: 'Testing', + iid: 2, + confidential: false, + labels: [] + }); + const list = gl.issueBoards.BoardsStore.addList(listObj); + + setTimeout(() => { + list.addIssue(issue); + + expect(list.issues.length).toBe(2); + + gl.issueBoards.BoardsStore.moveIssueInList(list, issue, 0, 1, [1, 2]); + + expect(list.issues[0].id).toBe(2); + expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, null, null, 1, null); + + done(); + }); + }); }); }); diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index aab4d9c501e..c96dfe94a4a 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -79,4 +79,20 @@ describe('Issue model', () => { issue.removeLabels([issue.labels[0], issue.labels[1]]); expect(issue.labels.length).toBe(0); }); + + it('sets position to infinity if no position is stored', () => { + expect(issue.position).toBe(Infinity); + }); + + it('sets position', () => { + const relativePositionIssue = new ListIssue({ + title: 'Testing', + iid: 1, + confidential: false, + relative_position: 1, + labels: [] + }); + + expect(relativePositionIssue.position).toBe(1); + }); }); diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index c8a18af7198..d49d3af33d9 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -103,6 +103,7 @@ describe('List model', () => { listDup.updateIssueLabel(list, issue); - expect(gl.boardService.moveIssue).toHaveBeenCalledWith(issue.id, list.id, listDup.id); + expect(gl.boardService.moveIssue) + .toHaveBeenCalledWith(issue.id, list.id, listDup.id, undefined, undefined); }); }); diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js index 0bd50588f5a..fe7f3d2e9c4 100644 --- a/spec/javascripts/build_spec.js +++ b/spec/javascripts/build_spec.js @@ -9,12 +9,6 @@ require('vendor/jquery.nicescroll'); describe('Build', () => { const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`; - // see spec/factories/ci/builds.rb - const BUILD_TRACE = 'BUILD TRACE'; - // see lib/ci/ansi2html.rb - const INITIAL_BUILD_TRACE_STATE = window.btoa(JSON.stringify({ - offset: BUILD_TRACE.length, n_open_tags: 0, fg_color: null, bg_color: null, style_mask: 0, - })); preloadFixtures('builds/build-with-artifacts.html.raw'); @@ -42,7 +36,7 @@ describe('Build', () => { expect(this.build.buildUrl).toBe(`${BUILD_URL}.json`); expect(this.build.buildStatus).toBe('success'); expect(this.build.buildStage).toBe('test'); - expect(this.build.state).toBe(INITIAL_BUILD_TRACE_STATE); + expect(this.build.state).toBe(''); }); it('only shows the jobs matching the current stage', () => { @@ -108,7 +102,7 @@ describe('Build', () => { expect($.ajax.calls.count()).toBe(2); let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1); expect(url).toBe( - `${BUILD_URL}/trace.json?state=${encodeURIComponent(INITIAL_BUILD_TRACE_STATE)}`, + `${BUILD_URL}/trace.json?state=`, ); expect(dataType).toBe('json'); expect(success).toEqual(jasmine.any(Function)); diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js index fa9d03c8a9a..c16f77c53a2 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js @@ -18,9 +18,7 @@ require('~/filtered_search/dropdown_user'); it('should not return the double quote found in value', () => { spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ - lastToken: { - value: '"johnny appleseed', - }, + lastToken: '"johnny appleseed', }); expect(dropdownUser.getSearchInput()).toBe('johnny appleseed'); @@ -28,9 +26,7 @@ require('~/filtered_search/dropdown_user'); it('should not return the single quote found in value', () => { spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ - lastToken: { - value: '\'larry boy', - }, + lastToken: '\'larry boy', }); expect(dropdownUser.getSearchInput()).toBe('larry boy'); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js index 1e2d7582d5b..5c65903701b 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js @@ -45,7 +45,7 @@ require('~/filtered_search/filtered_search_dropdown_manager'); }); it('should filter without symbol', () => { - input.value = ':roo'; + input.value = 'roo'; const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); expect(updatedItem.droplab_hidden).toBe(false); @@ -58,69 +58,62 @@ require('~/filtered_search/filtered_search_dropdown_manager'); expect(updatedItem.droplab_hidden).toBe(false); }); - it('should filter with colon', () => { - input.value = 'roo'; - - const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); - expect(updatedItem.droplab_hidden).toBe(false); - }); - describe('filters multiple word title', () => { const multipleWordItem = { title: 'Community Contributions', }; it('should filter with double quote', () => { - input.value = 'label:"'; + input.value = '"'; const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with double quote and symbol', () => { - input.value = 'label:~"'; + input.value = '~"'; const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with double quote and multiple words', () => { - input.value = 'label:"community con'; + input.value = '"community con'; const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with double quote, symbol and multiple words', () => { - input.value = 'label:~"community con'; + input.value = '~"community con'; const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with single quote', () => { - input.value = 'label:\''; + input.value = '\''; const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with single quote and symbol', () => { - input.value = 'label:~\''; + input.value = '~\''; const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with single quote and multiple words', () => { - input.value = 'label:\'community con'; + input.value = '\'community con'; const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with single quote, symbol and multiple words', () => { - input.value = 'label:~\'community con'; + input.value = '~\'community con'; const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js index ed0b0196ec4..a1da3396d7b 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js @@ -1,4 +1,5 @@ require('~/extensions/array'); +require('~/filtered_search/filtered_search_visual_tokens'); require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_dropdown_manager'); @@ -14,24 +15,44 @@ require('~/filtered_search/filtered_search_dropdown_manager'); } beforeEach(() => { - const input = document.createElement('input'); - input.classList.add('filtered-search'); - document.body.appendChild(input); - }); - - afterEach(() => { - document.querySelector('.filtered-search').outerHTML = ''; + setFixtures(` + <ul class="tokens-container"> + <li class="input-token"> + <input class="filtered-search"> + </li> + </ul> + `); }); describe('input has no existing value', () => { it('should add just tokenName', () => { gl.FilteredSearchDropdownManager.addWordToInput('milestone'); - expect(getInputValue()).toBe('milestone:'); + + const token = document.querySelector('.tokens-container .js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('milestone'); + expect(getInputValue()).toBe(''); }); it('should add tokenName and tokenValue', () => { + gl.FilteredSearchDropdownManager.addWordToInput('label'); + + let token = document.querySelector('.tokens-container .js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('label'); + expect(getInputValue()).toBe(''); + gl.FilteredSearchDropdownManager.addWordToInput('label', 'none'); - expect(getInputValue()).toBe('label:none '); + // We have to get that reference again + // Because gl.FilteredSearchDropdownManager deletes the previous token + token = document.querySelector('.tokens-container .js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('label'); + expect(token.querySelector('.value').innerText).toBe('none'); + expect(getInputValue()).toBe(''); }); }); @@ -39,19 +60,40 @@ require('~/filtered_search/filtered_search_dropdown_manager'); it('should be able to just add tokenName', () => { setInputValue('a'); gl.FilteredSearchDropdownManager.addWordToInput('author'); - expect(getInputValue()).toBe('author:'); + + const token = document.querySelector('.tokens-container .js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('author'); + expect(getInputValue()).toBe(''); }); it('should replace tokenValue', () => { - setInputValue('author:roo'); - gl.FilteredSearchDropdownManager.addWordToInput('author', '@root'); - expect(getInputValue()).toBe('author:@root '); + gl.FilteredSearchDropdownManager.addWordToInput('author'); + + setInputValue('roo'); + gl.FilteredSearchDropdownManager.addWordToInput(null, '@root'); + + const token = document.querySelector('.tokens-container .js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('author'); + expect(token.querySelector('.value').innerText).toBe('@root'); + expect(getInputValue()).toBe(''); }); it('should add tokenValues containing spaces', () => { - setInputValue('label:~"test'); + gl.FilteredSearchDropdownManager.addWordToInput('label'); + + setInputValue('"test '); gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); - expect(getInputValue()).toBe('label:~\'"test me"\' '); + + const token = document.querySelector('.tokens-container .js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toBe('label'); + expect(token.querySelector('.value').innerText).toBe('~\'"test me"\''); + expect(getInputValue()).toBe(''); }); }); }); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 98959dda242..81c1d81d181 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -4,64 +4,244 @@ require('~/filtered_search/filtered_search_token_keys'); require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_dropdown_manager'); require('~/filtered_search/filtered_search_manager'); +const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper'); (() => { describe('Filtered Search Manager', () => { - describe('search', () => { - let manager; - const defaultParams = '?scope=all&utf8=✓&state=opened'; + let input; + let manager; + let tokensContainer; + const placeholder = 'Search or filter results...'; - function getInput() { - return document.querySelector('.filtered-search'); - } + function dispatchBackspaceEvent(element, eventType) { + const backspaceKey = 8; + const event = new Event(eventType); + event.keyCode = backspaceKey; + element.dispatchEvent(event); + } - beforeEach(() => { - setFixtures(` - <input type='text' class='filtered-search' /> - `); + function dispatchDeleteEvent(element, eventType) { + const deleteKey = 46; + const event = new Event(eventType); + event.keyCode = deleteKey; + element.dispatchEvent(event); + } - spyOn(gl.FilteredSearchManager.prototype, 'bindEvents').and.callFake(() => {}); - spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {}); - spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); - spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); - spyOn(gl.utils, 'getParameterByName').and.returnValue(null); + beforeEach(() => { + setFixtures(` + <div class="filtered-search-input-container"> + <form> + <ul class="tokens-container list-unstyled"> + ${FilteredSearchSpecHelper.createInputHTML(placeholder)} + </ul> + <button class="clear-search" type="button"> + <i class="fa fa-times"></i> + </button> + </form> + </div> + `); - manager = new gl.FilteredSearchManager(); - }); + spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {}); + spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); + spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {}); + spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); + spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {}); + spyOn(gl.utils, 'getParameterByName').and.returnValue(null); + spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough(); - afterEach(() => { - getInput().outerHTML = ''; - }); + input = document.querySelector('.filtered-search'); + tokensContainer = document.querySelector('.tokens-container'); + manager = new gl.FilteredSearchManager(); + }); - it('should search with a single word', () => { - getInput().value = 'searchTerm'; + describe('search', () => { + const defaultParams = '?scope=all&utf8=✓&state=opened'; + + it('should search with a single word', (done) => { + input.value = 'searchTerm'; spyOn(gl.utils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=searchTerm`); + done(); }); manager.search(); }); - it('should search with multiple words', () => { - getInput().value = 'awesome search terms'; + it('should search with multiple words', (done) => { + input.value = 'awesome search terms'; spyOn(gl.utils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`); + done(); }); manager.search(); }); - it('should search with special characters', () => { - getInput().value = '~!@#$%^&*()_+{}:<>,.?/'; + it('should search with special characters', (done) => { + input.value = '~!@#$%^&*()_+{}:<>,.?/'; spyOn(gl.utils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`); + done(); }); manager.search(); }); }); + + describe('handleInputPlaceholder', () => { + it('should render placeholder when there is no input', () => { + expect(input.placeholder).toEqual(placeholder); + }); + + it('should not render placeholder when there is input', () => { + input.value = 'test words'; + + const event = new Event('input'); + input.dispatchEvent(event); + + expect(input.placeholder).toEqual(''); + }); + + it('should not render placeholder when there are tokens and no input', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), + ); + + const event = new Event('input'); + input.dispatchEvent(event); + + expect(input.placeholder).toEqual(''); + }); + }); + + describe('checkForBackspace', () => { + describe('tokens and no input', () => { + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), + ); + }); + + it('removes last token', () => { + spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); + dispatchBackspaceEvent(input, 'keyup'); + + expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled(); + }); + + it('sets the input', () => { + spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); + dispatchDeleteEvent(input, 'keyup'); + + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled(); + expect(input.value).toEqual('~bug'); + }); + }); + + it('does not remove token or change input when there is existing input', () => { + spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); + spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); + + input.value = 'text'; + dispatchDeleteEvent(input, 'keyup'); + + expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); + expect(input.value).toEqual('text'); + }); + }); + + describe('removeSelectedToken', () => { + function getVisualTokens() { + return tokensContainer.querySelectorAll('.js-visual-token'); + } + + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true), + ); + }); + + it('removes selected token when the backspace key is pressed', () => { + expect(getVisualTokens().length).toEqual(1); + + dispatchBackspaceEvent(document, 'keydown'); + + expect(getVisualTokens().length).toEqual(0); + }); + + it('removes selected token when the delete key is pressed', () => { + expect(getVisualTokens().length).toEqual(1); + + dispatchDeleteEvent(document, 'keydown'); + + expect(getVisualTokens().length).toEqual(0); + }); + + it('updates the input placeholder after removal', () => { + manager.handleInputPlaceholder(); + + expect(input.placeholder).toEqual(''); + expect(getVisualTokens().length).toEqual(1); + + dispatchBackspaceEvent(document, 'keydown'); + + expect(input.placeholder).not.toEqual(''); + expect(getVisualTokens().length).toEqual(0); + }); + + it('updates the clear button after removal', () => { + manager.toggleClearSearchButton(); + + const clearButton = document.querySelector('.clear-search'); + + expect(clearButton.classList.contains('hidden')).toEqual(false); + expect(getVisualTokens().length).toEqual(1); + + dispatchBackspaceEvent(document, 'keydown'); + + expect(clearButton.classList.contains('hidden')).toEqual(true); + expect(getVisualTokens().length).toEqual(0); + }); + }); + + describe('unselects token', () => { + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')} + `); + }); + + it('unselects token when input is clicked', () => { + const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); + + expect(selectedToken.classList.contains('selected')).toEqual(true); + expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); + + // Click directly on input attached to document + // so that the click event will propagate properly + document.querySelector('.filtered-search').click(); + + expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); + expect(selectedToken.classList.contains('selected')).toEqual(false); + }); + + it('unselects token when document.body is clicked', () => { + const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); + + expect(selectedToken.classList.contains('selected')).toEqual(true); + expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); + + document.body.click(); + + expect(selectedToken.classList.contains('selected')).toEqual(false); + expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); + }); + }); }); })(); diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js new file mode 100644 index 00000000000..bbda1476fed --- /dev/null +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -0,0 +1,600 @@ +require('~/filtered_search/filtered_search_visual_tokens'); +const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper'); + +describe('Filtered Search Visual Tokens', () => { + let tokensContainer; + + beforeEach(() => { + setFixtures(` + <ul class="tokens-container"> + ${FilteredSearchSpecHelper.createInputHTML()} + </ul> + `); + tokensContainer = document.querySelector('.tokens-container'); + }); + + describe('getLastVisualTokenBeforeInput', () => { + it('returns when there are no visual tokens', () => { + const { lastVisualToken, isLastVisualTokenValid } + = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + expect(lastVisualToken).toEqual(null); + expect(isLastVisualTokenValid).toEqual(true); + }); + + describe('input is the last item in tokensContainer', () => { + it('returns when there is one visual token', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), + ); + + const { lastVisualToken, isLastVisualTokenValid } + = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token')); + expect(isLastVisualTokenValid).toEqual(true); + }); + + it('returns when there is an incomplete visual token', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('Author'), + ); + + const { lastVisualToken, isLastVisualTokenValid } + = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token')); + expect(isLastVisualTokenValid).toEqual(false); + }); + + it('returns when there are multiple visual tokens', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')} + `); + + const { lastVisualToken, isLastVisualTokenValid } + = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + const items = document.querySelectorAll('.tokens-container .js-visual-token'); + + expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true); + expect(isLastVisualTokenValid).toEqual(true); + }); + + it('returns when there are multiple visual tokens and an incomplete visual token', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} + ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee')} + `); + + const { lastVisualToken, isLastVisualTokenValid } + = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + const items = document.querySelectorAll('.tokens-container .js-visual-token'); + + expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true); + expect(isLastVisualTokenValid).toEqual(false); + }); + }); + + describe('input is a middle item in tokensContainer', () => { + it('returns last token before input', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${FilteredSearchSpecHelper.createInputHTML()} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')} + `); + + const { lastVisualToken, isLastVisualTokenValid } + = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token')); + expect(isLastVisualTokenValid).toEqual(true); + }); + + it('returns last partial token before input', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')} + ${FilteredSearchSpecHelper.createInputHTML()} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')} + `); + + const { lastVisualToken, isLastVisualTokenValid } + = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token')); + expect(isLastVisualTokenValid).toEqual(false); + }); + }); + }); + + describe('unselectTokens', () => { + it('does nothing when there are no tokens', () => { + const beforeHTML = tokensContainer.innerHTML; + gl.FilteredSearchVisualTokens.unselectTokens(); + + expect(tokensContainer.innerHTML).toEqual(beforeHTML); + }); + + it('removes the selected class from buttons', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@author')} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '%123', true)} + `); + + const selected = tokensContainer.querySelector('.js-visual-token .selected'); + expect(selected.classList.contains('selected')).toEqual(true); + + gl.FilteredSearchVisualTokens.unselectTokens(); + + expect(selected.classList.contains('selected')).toEqual(false); + }); + }); + + describe('selectToken', () => { + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')} + `); + }); + + it('removes the selected class if it has selected class', () => { + const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable'); + firstTokenButton.classList.add('selected'); + + gl.FilteredSearchVisualTokens.selectToken(firstTokenButton); + + expect(firstTokenButton.classList.contains('selected')).toEqual(false); + }); + + describe('has no selected class', () => { + it('adds selected class', () => { + const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable'); + + gl.FilteredSearchVisualTokens.selectToken(firstTokenButton); + + expect(firstTokenButton.classList.contains('selected')).toEqual(true); + }); + + it('removes selected class from other tokens', () => { + const tokenButtons = tokensContainer.querySelectorAll('.js-visual-token .selectable'); + tokenButtons[1].classList.add('selected'); + + gl.FilteredSearchVisualTokens.selectToken(tokenButtons[0]); + + expect(tokenButtons[0].classList.contains('selected')).toEqual(true); + expect(tokenButtons[1].classList.contains('selected')).toEqual(false); + }); + }); + }); + + describe('removeSelectedToken', () => { + it('does not remove when there are no selected tokens', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none'), + ); + + expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null); + + gl.FilteredSearchVisualTokens.removeSelectedToken(); + + expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null); + }); + + it('removes selected token', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true), + ); + + expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null); + + gl.FilteredSearchVisualTokens.removeSelectedToken(); + + expect(tokensContainer.querySelector('.js-visual-token .selectable')).toEqual(null); + }); + }); + + describe('createVisualTokenElementHTML', () => { + let tokenElement; + + beforeEach(() => { + setFixtures(` + <div class="test-area"> + ${gl.FilteredSearchVisualTokens.createVisualTokenElementHTML()} + </div> + `); + + tokenElement = document.querySelector('.test-area').firstElementChild; + }); + + it('contains name div', () => { + expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything()); + }); + + it('contains value div', () => { + expect(tokenElement.querySelector('.value')).toEqual(jasmine.anything()); + }); + + it('contains selectable class', () => { + expect(tokenElement.classList.contains('selectable')).toEqual(true); + }); + + it('contains button role', () => { + expect(tokenElement.getAttribute('role')).toEqual('button'); + }); + }); + + describe('addVisualTokenElement', () => { + it('renders search visual tokens', () => { + gl.FilteredSearchVisualTokens.addVisualTokenElement('search term', null, true); + const token = tokensContainer.querySelector('.js-visual-token'); + + expect(token.classList.contains('filtered-search-term')).toEqual(true); + expect(token.querySelector('.name').innerText).toEqual('search term'); + expect(token.querySelector('.value')).toEqual(null); + }); + + it('renders filter visual token name', () => { + gl.FilteredSearchVisualTokens.addVisualTokenElement('milestone'); + const token = tokensContainer.querySelector('.js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toEqual('milestone'); + expect(token.querySelector('.value')).toEqual(null); + }); + + it('renders filter visual token name and value', () => { + gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend'); + const token = tokensContainer.querySelector('.js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toEqual('label'); + expect(token.querySelector('.value').innerText).toEqual('Frontend'); + }); + + it('inserts visual token before input', () => { + tokensContainer.appendChild(FilteredSearchSpecHelper.createFilterVisualToken('assignee', '@root')); + + gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend'); + const tokens = tokensContainer.querySelectorAll('.js-visual-token'); + const labelToken = tokens[0]; + const assigneeToken = tokens[1]; + + expect(labelToken.classList.contains('filtered-search-token')).toEqual(true); + expect(labelToken.querySelector('.name').innerText).toEqual('label'); + expect(labelToken.querySelector('.value').innerText).toEqual('Frontend'); + + expect(assigneeToken.classList.contains('filtered-search-token')).toEqual(true); + expect(assigneeToken.querySelector('.name').innerText).toEqual('assignee'); + expect(assigneeToken.querySelector('.value').innerText).toEqual('@root'); + }); + }); + + describe('addValueToPreviousVisualTokenElement', () => { + it('does not add when previous visual token element has no value', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root'), + ); + + const original = tokensContainer.innerHTML; + gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value'); + + expect(original).toEqual(tokensContainer.innerHTML); + }); + + it('does not add when previous visual token element is a search', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} + `); + + const original = tokensContainer.innerHTML; + gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value'); + + expect(original).toEqual(tokensContainer.innerHTML); + }); + + it('adds value to previous visual filter token', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label'), + ); + + const original = tokensContainer.innerHTML; + gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value'); + const updatedToken = tokensContainer.querySelector('.js-visual-token'); + + expect(updatedToken.querySelector('.name').innerText).toEqual('label'); + expect(updatedToken.querySelector('.value').innerText).toEqual('value'); + expect(original).not.toEqual(tokensContainer.innerHTML); + }); + }); + + describe('addFilterVisualToken', () => { + it('creates visual token with just tokenName', () => { + gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone'); + const token = tokensContainer.querySelector('.js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toEqual('milestone'); + expect(token.querySelector('.value')).toEqual(null); + }); + + it('creates visual token with just tokenValue', () => { + gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone'); + gl.FilteredSearchVisualTokens.addFilterVisualToken('%8.17'); + const token = tokensContainer.querySelector('.js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toEqual('milestone'); + expect(token.querySelector('.value').innerText).toEqual('%8.17'); + }); + + it('creates full visual token', () => { + gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', '@john'); + const token = tokensContainer.querySelector('.js-visual-token'); + + expect(token.classList.contains('filtered-search-token')).toEqual(true); + expect(token.querySelector('.name').innerText).toEqual('assignee'); + expect(token.querySelector('.value').innerText).toEqual('@john'); + }); + }); + + describe('addSearchVisualToken', () => { + it('creates search visual token', () => { + gl.FilteredSearchVisualTokens.addSearchVisualToken('search term'); + const token = tokensContainer.querySelector('.js-visual-token'); + + expect(token.classList.contains('filtered-search-term')).toEqual(true); + expect(token.querySelector('.name').innerText).toEqual('search term'); + expect(token.querySelector('.value')).toEqual(null); + }); + + it('appends to previous search visual token if previous token was a search token', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} + `); + + gl.FilteredSearchVisualTokens.addSearchVisualToken('append this'); + const token = tokensContainer.querySelector('.filtered-search-term'); + + expect(token.querySelector('.name').innerText).toEqual('search term append this'); + expect(token.querySelector('.value')).toEqual(null); + }); + }); + + describe('getLastTokenPartial', () => { + it('should get last token value', () => { + const value = '~bug'; + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', value), + ); + + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(value); + }); + + it('should get last token name if there is no value', () => { + const name = 'assignee'; + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createNameFilterVisualTokenHTML(name), + ); + + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(name); + }); + + it('should return empty when there are no tokens', () => { + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(''); + }); + }); + + describe('removeLastTokenPartial', () => { + it('should remove the last token value if it exists', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~"Community Contribution"'), + ); + + expect(tokensContainer.querySelector('.js-visual-token .value')).not.toEqual(null); + + gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + + expect(tokensContainer.querySelector('.js-visual-token .value')).toEqual(null); + }); + + it('should remove the last token name if there is no value', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('milestone'), + ); + + expect(tokensContainer.querySelector('.js-visual-token .name')).not.toEqual(null); + + gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + + expect(tokensContainer.querySelector('.js-visual-token .name')).toEqual(null); + }); + + it('should not remove anything when there are no tokens', () => { + const html = tokensContainer.innerHTML; + gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + + expect(tokensContainer.innerHTML).toEqual(html); + }); + }); + + describe('tokenizeInput', () => { + it('does not do anything if there is no input', () => { + const original = tokensContainer.innerHTML; + gl.FilteredSearchVisualTokens.tokenizeInput(); + + expect(tokensContainer.innerHTML).toEqual(original); + }); + + it('adds search visual token if previous visual token is valid', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('assignee', 'none'), + ); + + const input = document.querySelector('.filtered-search'); + input.value = 'some value'; + gl.FilteredSearchVisualTokens.tokenizeInput(); + + const newToken = tokensContainer.querySelector('.filtered-search-term'); + + expect(input.value).toEqual(''); + expect(newToken.querySelector('.name').innerText).toEqual('some value'); + expect(newToken.querySelector('.value')).toEqual(null); + }); + + it('adds value to previous visual token element if previous visual token is invalid', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee'), + ); + + const input = document.querySelector('.filtered-search'); + input.value = '@john'; + gl.FilteredSearchVisualTokens.tokenizeInput(); + + const updatedToken = tokensContainer.querySelector('.filtered-search-token'); + + expect(input.value).toEqual(''); + expect(updatedToken.querySelector('.name').innerText).toEqual('assignee'); + expect(updatedToken.querySelector('.value').innerText).toEqual('@john'); + }); + }); + + describe('editToken', () => { + let input; + let token; + + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search')} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'upcoming')} + `); + + input = document.querySelector('.filtered-search'); + token = document.querySelector('.js-visual-token'); + }); + + it('tokenize\'s existing input', () => { + input.value = 'some text'; + spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callThrough(); + + gl.FilteredSearchVisualTokens.editToken(token); + + expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled(); + expect(input.value).not.toEqual('some text'); + }); + + it('moves input to the token position', () => { + expect(tokensContainer.children[3].querySelector('.filtered-search')).not.toEqual(null); + + gl.FilteredSearchVisualTokens.editToken(token); + + expect(tokensContainer.children[1].querySelector('.filtered-search')).not.toEqual(null); + expect(tokensContainer.children[3].querySelector('.filtered-search')).toEqual(null); + }); + + it('input contains the visual token value', () => { + gl.FilteredSearchVisualTokens.editToken(token); + + expect(input.value).toEqual('none'); + }); + + describe('selected token is a search term token', () => { + beforeEach(() => { + token = document.querySelector('.filtered-search-term'); + }); + + it('token is removed', () => { + expect(tokensContainer.querySelector('.filtered-search-term')).not.toEqual(null); + + gl.FilteredSearchVisualTokens.editToken(token); + + expect(tokensContainer.querySelector('.filtered-search-term')).toEqual(null); + }); + + it('input has the same value as removed token', () => { + expect(input.value).toEqual(''); + + gl.FilteredSearchVisualTokens.editToken(token); + + expect(input.value).toEqual('search'); + }); + }); + }); + + describe('moveInputTotheRight', () => { + it('does nothing if the input is already the right most element', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( + FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none'), + ); + + spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callFake(() => {}); + spyOn(gl.FilteredSearchVisualTokens, 'getLastVisualTokenBeforeInput').and.callThrough(); + + gl.FilteredSearchVisualTokens.moveInputToTheRight(); + + expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled(); + expect(gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput).not.toHaveBeenCalled(); + }); + + it('tokenize\'s input', () => { + tokensContainer.innerHTML = ` + ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')} + ${FilteredSearchSpecHelper.createInputHTML()} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + `; + + document.querySelector('.filtered-search').value = 'none'; + + gl.FilteredSearchVisualTokens.moveInputToTheRight(); + const value = tokensContainer.querySelector('.js-visual-token .value'); + + expect(value.innerText).toEqual('none'); + }); + + it('converts input into search term token if last token is valid', () => { + tokensContainer.innerHTML = ` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')} + ${FilteredSearchSpecHelper.createInputHTML()} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + `; + + document.querySelector('.filtered-search').value = 'test'; + + gl.FilteredSearchVisualTokens.moveInputToTheRight(); + const searchValue = tokensContainer.querySelector('.filtered-search-term .name'); + + expect(searchValue.innerText).toEqual('test'); + }); + + it('moves the input to the right most element', () => { + tokensContainer.innerHTML = ` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')} + ${FilteredSearchSpecHelper.createInputHTML()} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + `; + + gl.FilteredSearchVisualTokens.moveInputToTheRight(); + + expect(tokensContainer.children[2].querySelector('.filtered-search')).not.toEqual(null); + }); + + it('tokenizes input even if input is the right most element', () => { + tokensContainer.innerHTML = ` + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')} + ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')} + ${FilteredSearchSpecHelper.createInputHTML('', '~bug')} + `; + + gl.FilteredSearchVisualTokens.moveInputToTheRight(); + + const token = tokensContainer.children[1]; + expect(token.querySelector('.value').innerText).toEqual('~bug'); + }); + }); +}); diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js deleted file mode 100644 index a50812d9517..00000000000 --- a/spec/javascripts/fixtures/emoji_menu.js +++ /dev/null @@ -1,4 +0,0 @@ -/* eslint-disable space-before-function-paren */ -(function() { - window.emojiMenu = "<div class='emoji-menu'>\n <input type=\"text\" name=\"emoji_search\" id=\"emoji_search\" value=\"\" class=\"emoji-search search-input form-control\" />\n <div class='emoji-menu-content'>\n <h5 class='emoji-menu-title'>\n Emoticons\n </h5>\n <ul class='clearfix emoji-menu-list'>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47D\" title=\"alien\" data-aliases=\"\" data-emoji=\"alien\" data-unicode-name=\"1F47D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47C\" title=\"angel\" data-aliases=\"\" data-emoji=\"angel\" data-unicode-name=\"1F47C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A2\" title=\"anger\" data-aliases=\"\" data-emoji=\"anger\" data-unicode-name=\"1F4A2\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F620\" title=\"angry\" data-aliases=\"\" data-emoji=\"angry\" data-unicode-name=\"1F620\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F627\" title=\"anguished\" data-aliases=\"\" data-emoji=\"anguished\" data-unicode-name=\"1F627\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F632\" title=\"astonished\" data-aliases=\"\" data-emoji=\"astonished\" data-unicode-name=\"1F632\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45F\" title=\"athletic_shoe\" data-aliases=\"\" data-emoji=\"athletic_shoe\" data-unicode-name=\"1F45F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F476\" title=\"baby\" data-aliases=\"\" data-emoji=\"baby\" data-unicode-name=\"1F476\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F459\" title=\"bikini\" data-aliases=\"\" data-emoji=\"bikini\" data-unicode-name=\"1F459\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F499\" title=\"blue_heart\" data-aliases=\"\" data-emoji=\"blue_heart\" data-unicode-name=\"1F499\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60A\" title=\"blush\" data-aliases=\"\" data-emoji=\"blush\" data-unicode-name=\"1F60A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A5\" title=\"boom\" data-aliases=\"\" data-emoji=\"boom\" data-unicode-name=\"1F4A5\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F462\" title=\"boot\" data-aliases=\"\" data-emoji=\"boot\" data-unicode-name=\"1F462\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F647\" title=\"bow\" data-aliases=\"\" data-emoji=\"bow\" data-unicode-name=\"1F647\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F466\" title=\"boy\" data-aliases=\"\" data-emoji=\"boy\" data-unicode-name=\"1F466\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F470\" title=\"bride_with_veil\" data-aliases=\"\" data-emoji=\"bride_with_veil\" data-unicode-name=\"1F470\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4BC\" title=\"briefcase\" data-aliases=\"\" data-emoji=\"briefcase\" data-unicode-name=\"1F4BC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F494\" title=\"broken_heart\" data-aliases=\"\" data-emoji=\"broken_heart\" data-unicode-name=\"1F494\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F464\" title=\"bust_in_silhouette\" data-aliases=\"\" data-emoji=\"bust_in_silhouette\" data-unicode-name=\"1F464\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F465\" title=\"busts_in_silhouette\" data-aliases=\"\" data-emoji=\"busts_in_silhouette\" data-unicode-name=\"1F465\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44F\" title=\"clap\" data-aliases=\"\" data-emoji=\"clap\" data-unicode-name=\"1F44F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F302\" title=\"closed_umbrella\" data-aliases=\"\" data-emoji=\"closed_umbrella\" data-unicode-name=\"1F302\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F630\" title=\"cold_sweat\" data-aliases=\"\" data-emoji=\"cold_sweat\" data-unicode-name=\"1F630\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F616\" title=\"confounded\" data-aliases=\"\" data-emoji=\"confounded\" data-unicode-name=\"1F616\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F615\" title=\"confused\" data-aliases=\"\" data-emoji=\"confused\" data-unicode-name=\"1F615\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F477\" title=\"construction_worker\" data-aliases=\"\" data-emoji=\"construction_worker\" data-unicode-name=\"1F477\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46E\" title=\"cop\" data-aliases=\"\" data-emoji=\"cop\" data-unicode-name=\"1F46E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46B\" title=\"couple\" data-aliases=\"\" data-emoji=\"couple\" data-unicode-name=\"1F46B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F491\" title=\"couple_with_heart\" data-aliases=\"\" data-emoji=\"couple_with_heart\" data-unicode-name=\"1F491\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48F\" title=\"couplekiss\" data-aliases=\"\" data-emoji=\"couplekiss\" data-unicode-name=\"1F48F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F451\" title=\"crown\" data-aliases=\"\" data-emoji=\"crown\" data-unicode-name=\"1F451\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F622\" title=\"cry\" data-aliases=\"\" data-emoji=\"cry\" data-unicode-name=\"1F622\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63F\" title=\"crying_cat_face\" data-aliases=\"\" data-emoji=\"crying_cat_face\" data-unicode-name=\"1F63F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F498\" title=\"cupid\" data-aliases=\"\" data-emoji=\"cupid\" data-unicode-name=\"1F498\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F483\" title=\"dancer\" data-aliases=\"\" data-emoji=\"dancer\" data-unicode-name=\"1F483\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46F\" title=\"dancers\" data-aliases=\"\" data-emoji=\"dancers\" data-unicode-name=\"1F46F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A8\" title=\"dash\" data-aliases=\"\" data-emoji=\"dash\" data-unicode-name=\"1F4A8\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61E\" title=\"disappointed\" data-aliases=\"\" data-emoji=\"disappointed\" data-unicode-name=\"1F61E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F625\" title=\"disappointed_relieved\" data-aliases=\"\" data-emoji=\"disappointed_relieved\" data-unicode-name=\"1F625\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AB\" title=\"dizzy\" data-aliases=\"\" data-emoji=\"dizzy\" data-unicode-name=\"1F4AB\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F635\" title=\"dizzy_face\" data-aliases=\"\" data-emoji=\"dizzy_face\" data-unicode-name=\"1F635\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F457\" title=\"dress\" data-aliases=\"\" data-emoji=\"dress\" data-unicode-name=\"1F457\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A7\" title=\"droplet\" data-aliases=\"\" data-emoji=\"droplet\" data-unicode-name=\"1F4A7\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F442\" title=\"ear\" data-aliases=\"\" data-emoji=\"ear\" data-unicode-name=\"1F442\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F611\" title=\"expressionless\" data-aliases=\"\" data-emoji=\"expressionless\" data-unicode-name=\"1F611\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F453\" title=\"eyeglasses\" data-aliases=\"\" data-emoji=\"eyeglasses\" data-unicode-name=\"1F453\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F440\" title=\"eyes\" data-aliases=\"\" data-emoji=\"eyes\" data-unicode-name=\"1F440\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46A\" title=\"family\" data-aliases=\"\" data-emoji=\"family\" data-unicode-name=\"1F46A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F628\" title=\"fearful\" data-aliases=\"\" data-emoji=\"fearful\" data-unicode-name=\"1F628\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F525\" title=\"fire\" data-aliases=\":flame:\" data-emoji=\"fire\" data-unicode-name=\"1F525\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270A\" title=\"fist\" data-aliases=\"\" data-emoji=\"fist\" data-unicode-name=\"270A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F633\" title=\"flushed\" data-aliases=\"\" data-emoji=\"flushed\" data-unicode-name=\"1F633\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F463\" title=\"footprints\" data-aliases=\"\" data-emoji=\"footprints\" data-unicode-name=\"1F463\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F626\" title=\"frowning\" data-aliases=\":anguished:\" data-emoji=\"frowning\" data-unicode-name=\"1F626\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48E\" title=\"gem\" data-aliases=\"\" data-emoji=\"gem\" data-unicode-name=\"1F48E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F467\" title=\"girl\" data-aliases=\"\" data-emoji=\"girl\" data-unicode-name=\"1F467\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49A\" title=\"green_heart\" data-aliases=\"\" data-emoji=\"green_heart\" data-unicode-name=\"1F49A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62C\" title=\"grimacing\" data-aliases=\"\" data-emoji=\"grimacing\" data-unicode-name=\"1F62C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F601\" title=\"grin\" data-aliases=\"\" data-emoji=\"grin\" data-unicode-name=\"1F601\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F600\" title=\"grinning\" data-aliases=\"\" data-emoji=\"grinning\" data-unicode-name=\"1F600\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F482\" title=\"guardsman\" data-aliases=\"\" data-emoji=\"guardsman\" data-unicode-name=\"1F482\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F487\" title=\"haircut\" data-aliases=\"\" data-emoji=\"haircut\" data-unicode-name=\"1F487\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45C\" title=\"handbag\" data-aliases=\"\" data-emoji=\"handbag\" data-unicode-name=\"1F45C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F649\" title=\"hear_no_evil\" data-aliases=\"\" data-emoji=\"hear_no_evil\" data-unicode-name=\"1F649\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2764\" title=\"heart\" data-aliases=\"\" data-emoji=\"heart\" data-unicode-name=\"2764\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60D\" title=\"heart_eyes\" data-aliases=\"\" data-emoji=\"heart_eyes\" data-unicode-name=\"1F60D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63B\" title=\"heart_eyes_cat\" data-aliases=\"\" data-emoji=\"heart_eyes_cat\" data-unicode-name=\"1F63B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F493\" title=\"heartbeat\" data-aliases=\"\" data-emoji=\"heartbeat\" data-unicode-name=\"1F493\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F497\" title=\"heartpulse\" data-aliases=\"\" data-emoji=\"heartpulse\" data-unicode-name=\"1F497\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F460\" title=\"high_heel\" data-aliases=\"\" data-emoji=\"high_heel\" data-unicode-name=\"1F460\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62F\" title=\"hushed\" data-aliases=\"\" data-emoji=\"hushed\" data-unicode-name=\"1F62F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47F\" title=\"imp\" data-aliases=\"\" data-emoji=\"imp\" data-unicode-name=\"1F47F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F481\" title=\"information_desk_person\" data-aliases=\"\" data-emoji=\"information_desk_person\" data-unicode-name=\"1F481\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F607\" title=\"innocent\" data-aliases=\"\" data-emoji=\"innocent\" data-unicode-name=\"1F607\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47A\" title=\"japanese_goblin\" data-aliases=\"\" data-emoji=\"japanese_goblin\" data-unicode-name=\"1F47A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F479\" title=\"japanese_ogre\" data-aliases=\"\" data-emoji=\"japanese_ogre\" data-unicode-name=\"1F479\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F456\" title=\"jeans\" data-aliases=\"\" data-emoji=\"jeans\" data-unicode-name=\"1F456\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F602\" title=\"joy\" data-aliases=\"\" data-emoji=\"joy\" data-unicode-name=\"1F602\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F639\" title=\"joy_cat\" data-aliases=\"\" data-emoji=\"joy_cat\" data-unicode-name=\"1F639\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F458\" title=\"kimono\" data-aliases=\"\" data-emoji=\"kimono\" data-unicode-name=\"1F458\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48B\" title=\"kiss\" data-aliases=\"\" data-emoji=\"kiss\" data-unicode-name=\"1F48B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F617\" title=\"kissing\" data-aliases=\"\" data-emoji=\"kissing\" data-unicode-name=\"1F617\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63D\" title=\"kissing_cat\" data-aliases=\"\" data-emoji=\"kissing_cat\" data-unicode-name=\"1F63D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61A\" title=\"kissing_closed_eyes\" data-aliases=\"\" data-emoji=\"kissing_closed_eyes\" data-unicode-name=\"1F61A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F618\" title=\"kissing_heart\" data-aliases=\"\" data-emoji=\"kissing_heart\" data-unicode-name=\"1F618\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F619\" title=\"kissing_smiling_eyes\" data-aliases=\"\" data-emoji=\"kissing_smiling_eyes\" data-unicode-name=\"1F619\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F606\" title=\"laughing\" data-aliases=\":satisfied:\" data-emoji=\"laughing\" data-unicode-name=\"1F606\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F444\" title=\"lips\" data-aliases=\"\" data-emoji=\"lips\" data-unicode-name=\"1F444\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F484\" title=\"lipstick\" data-aliases=\"\" data-emoji=\"lipstick\" data-unicode-name=\"1F484\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48C\" title=\"love_letter\" data-aliases=\"\" data-emoji=\"love_letter\" data-unicode-name=\"1F48C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F468\" title=\"man\" data-aliases=\"\" data-emoji=\"man\" data-unicode-name=\"1F468\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F472\" title=\"man_with_gua_pi_mao\" data-aliases=\"\" data-emoji=\"man_with_gua_pi_mao\" data-unicode-name=\"1F472\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F473\" title=\"man_with_turban\" data-aliases=\"\" data-emoji=\"man_with_turban\" data-unicode-name=\"1F473\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45E\" title=\"mans_shoe\" data-aliases=\"\" data-emoji=\"mans_shoe\" data-unicode-name=\"1F45E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F637\" title=\"mask\" data-aliases=\"\" data-emoji=\"mask\" data-unicode-name=\"1F637\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F486\" title=\"massage\" data-aliases=\"\" data-emoji=\"massage\" data-unicode-name=\"1F486\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AA\" title=\"muscle\" data-aliases=\"\" data-emoji=\"muscle\" data-unicode-name=\"1F4AA\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F485\" title=\"nail_care\" data-aliases=\"\" data-emoji=\"nail_care\" data-unicode-name=\"1F485\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F454\" title=\"necktie\" data-aliases=\"\" data-emoji=\"necktie\" data-unicode-name=\"1F454\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F610\" title=\"neutral_face\" data-aliases=\"\" data-emoji=\"neutral_face\" data-unicode-name=\"1F610\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F645\" title=\"no_good\" data-aliases=\"\" data-emoji=\"no_good\" data-unicode-name=\"1F645\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F636\" title=\"no_mouth\" data-aliases=\"\" data-emoji=\"no_mouth\" data-unicode-name=\"1F636\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F443\" title=\"nose\" data-aliases=\"\" data-emoji=\"nose\" data-unicode-name=\"1F443\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44C\" title=\"ok_hand\" data-aliases=\"\" data-emoji=\"ok_hand\" data-unicode-name=\"1F44C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F646\" title=\"ok_woman\" data-aliases=\"\" data-emoji=\"ok_woman\" data-unicode-name=\"1F646\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F474\" title=\"older_man\" data-aliases=\"\" data-emoji=\"older_man\" data-unicode-name=\"1F474\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F475\" title=\"older_woman\" data-aliases=\":grandma:\" data-emoji=\"older_woman\" data-unicode-name=\"1F475\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F450\" title=\"open_hands\" data-aliases=\"\" data-emoji=\"open_hands\" data-unicode-name=\"1F450\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62E\" title=\"open_mouth\" data-aliases=\"\" data-emoji=\"open_mouth\" data-unicode-name=\"1F62E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F614\" title=\"pensive\" data-aliases=\"\" data-emoji=\"pensive\" data-unicode-name=\"1F614\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F623\" title=\"persevere\" data-aliases=\"\" data-emoji=\"persevere\" data-unicode-name=\"1F623\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64D\" title=\"person_frowning\" data-aliases=\"\" data-emoji=\"person_frowning\" data-unicode-name=\"1F64D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F471\" title=\"person_with_blond_hair\" data-aliases=\"\" data-emoji=\"person_with_blond_hair\" data-unicode-name=\"1F471\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64E\" title=\"person_with_pouting_face\" data-aliases=\"\" data-emoji=\"person_with_pouting_face\" data-unicode-name=\"1F64E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F447\" title=\"point_down\" data-aliases=\"\" data-emoji=\"point_down\" data-unicode-name=\"1F447\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F448\" title=\"point_left\" data-aliases=\"\" data-emoji=\"point_left\" data-unicode-name=\"1F448\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F449\" title=\"point_right\" data-aliases=\"\" data-emoji=\"point_right\" data-unicode-name=\"1F449\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-261D\" title=\"point_up\" data-aliases=\"\" data-emoji=\"point_up\" data-unicode-name=\"261D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F446\" title=\"point_up_2\" data-aliases=\"\" data-emoji=\"point_up_2\" data-unicode-name=\"1F446\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A9\" title=\"poop\" data-aliases=\":shit: :hankey: :poo:\" data-emoji=\"poop\" data-unicode-name=\"1F4A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45D\" title=\"pouch\" data-aliases=\"\" data-emoji=\"pouch\" data-unicode-name=\"1F45D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63E\" title=\"pouting_cat\" data-aliases=\"\" data-emoji=\"pouting_cat\" data-unicode-name=\"1F63E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64F\" title=\"pray\" data-aliases=\"\" data-emoji=\"pray\" data-unicode-name=\"1F64F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F478\" title=\"princess\" data-aliases=\"\" data-emoji=\"princess\" data-unicode-name=\"1F478\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44A\" title=\"punch\" data-aliases=\"\" data-emoji=\"punch\" data-unicode-name=\"1F44A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49C\" title=\"purple_heart\" data-aliases=\"\" data-emoji=\"purple_heart\" data-unicode-name=\"1F49C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45B\" title=\"purse\" data-aliases=\"\" data-emoji=\"purse\" data-unicode-name=\"1F45B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F621\" title=\"rage\" data-aliases=\"\" data-emoji=\"rage\" data-unicode-name=\"1F621\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270B\" title=\"raised_hand\" data-aliases=\"\" data-emoji=\"raised_hand\" data-unicode-name=\"270B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64C\" title=\"raised_hands\" data-aliases=\"\" data-emoji=\"raised_hands\" data-unicode-name=\"1F64C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64B\" title=\"raising_hand\" data-aliases=\"\" data-emoji=\"raising_hand\" data-unicode-name=\"1F64B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-263A\" title=\"relaxed\" data-aliases=\"\" data-emoji=\"relaxed\" data-unicode-name=\"263A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60C\" title=\"relieved\" data-aliases=\"\" data-emoji=\"relieved\" data-unicode-name=\"1F60C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49E\" title=\"revolving_hearts\" data-aliases=\"\" data-emoji=\"revolving_hearts\" data-unicode-name=\"1F49E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F380\" title=\"ribbon\" data-aliases=\"\" data-emoji=\"ribbon\" data-unicode-name=\"1F380\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48D\" title=\"ring\" data-aliases=\"\" data-emoji=\"ring\" data-unicode-name=\"1F48D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3C3\" title=\"runner\" data-aliases=\"\" data-emoji=\"runner\" data-unicode-name=\"1F3C3\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3BD\" title=\"running_shirt_with_sash\" data-aliases=\"\" data-emoji=\"running_shirt_with_sash\" data-unicode-name=\"1F3BD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F461\" title=\"sandal\" data-aliases=\"\" data-emoji=\"sandal\" data-unicode-name=\"1F461\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F631\" title=\"scream\" data-aliases=\"\" data-emoji=\"scream\" data-unicode-name=\"1F631\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F640\" title=\"scream_cat\" data-aliases=\"\" data-emoji=\"scream_cat\" data-unicode-name=\"1F640\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F648\" title=\"see_no_evil\" data-aliases=\"\" data-emoji=\"see_no_evil\" data-unicode-name=\"1F648\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F455\" title=\"shirt\" data-aliases=\"\" data-emoji=\"shirt\" data-unicode-name=\"1F455\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F480\" title=\"skull\" data-aliases=\":skeleton:\" data-emoji=\"skull\" data-unicode-name=\"1F480\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F634\" title=\"sleeping\" data-aliases=\"\" data-emoji=\"sleeping\" data-unicode-name=\"1F634\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62A\" title=\"sleepy\" data-aliases=\"\" data-emoji=\"sleepy\" data-unicode-name=\"1F62A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F604\" title=\"smile\" data-aliases=\"\" data-emoji=\"smile\" data-unicode-name=\"1F604\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F638\" title=\"smile_cat\" data-aliases=\"\" data-emoji=\"smile_cat\" data-unicode-name=\"1F638\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F603\" title=\"smiley\" data-aliases=\"\" data-emoji=\"smiley\" data-unicode-name=\"1F603\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63A\" title=\"smiley_cat\" data-aliases=\"\" data-emoji=\"smiley_cat\" data-unicode-name=\"1F63A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F608\" title=\"smiling_imp\" data-aliases=\"\" data-emoji=\"smiling_imp\" data-unicode-name=\"1F608\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60F\" title=\"smirk\" data-aliases=\"\" data-emoji=\"smirk\" data-unicode-name=\"1F60F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63C\" title=\"smirk_cat\" data-aliases=\"\" data-emoji=\"smirk_cat\" data-unicode-name=\"1F63C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62D\" title=\"sob\" data-aliases=\"\" data-emoji=\"sob\" data-unicode-name=\"1F62D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2728\" title=\"sparkles\" data-aliases=\"\" data-emoji=\"sparkles\" data-unicode-name=\"2728\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F496\" title=\"sparkling_heart\" data-aliases=\"\" data-emoji=\"sparkling_heart\" data-unicode-name=\"1F496\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64A\" title=\"speak_no_evil\" data-aliases=\"\" data-emoji=\"speak_no_evil\" data-unicode-name=\"1F64A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AC\" title=\"speech_balloon\" data-aliases=\"\" data-emoji=\"speech_balloon\" data-unicode-name=\"1F4AC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F31F\" title=\"star2\" data-aliases=\"\" data-emoji=\"star2\" data-unicode-name=\"1F31F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61B\" title=\"stuck_out_tongue\" data-aliases=\"\" data-emoji=\"stuck_out_tongue\" data-unicode-name=\"1F61B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61D\" title=\"stuck_out_tongue_closed_eyes\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_closed_eyes\" data-unicode-name=\"1F61D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61C\" title=\"stuck_out_tongue_winking_eye\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_winking_eye\" data-unicode-name=\"1F61C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60E\" title=\"sunglasses\" data-aliases=\"\" data-emoji=\"sunglasses\" data-unicode-name=\"1F60E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F613\" title=\"sweat\" data-aliases=\"\" data-emoji=\"sweat\" data-unicode-name=\"1F613\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A6\" title=\"sweat_drops\" data-aliases=\"\" data-emoji=\"sweat_drops\" data-unicode-name=\"1F4A6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F605\" title=\"sweat_smile\" data-aliases=\"\" data-emoji=\"sweat_smile\" data-unicode-name=\"1F605\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AD\" title=\"thought_balloon\" data-aliases=\"\" data-emoji=\"thought_balloon\" data-unicode-name=\"1F4AD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44E\" title=\"thumbsdown\" data-aliases=\":-1:\" data-emoji=\"thumbsdown\" data-unicode-name=\"1F44E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44D\" title=\"thumbsup\" data-aliases=\":+1:\" data-emoji=\"thumbsup\" data-unicode-name=\"1F44D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62B\" title=\"tired_face\" data-aliases=\"\" data-emoji=\"tired_face\" data-unicode-name=\"1F62B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F445\" title=\"tongue\" data-aliases=\"\" data-emoji=\"tongue\" data-unicode-name=\"1F445\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3A9\" title=\"tophat\" data-aliases=\"\" data-emoji=\"tophat\" data-unicode-name=\"1F3A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F624\" title=\"triumph\" data-aliases=\"\" data-emoji=\"triumph\" data-unicode-name=\"1F624\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F495\" title=\"two_hearts\" data-aliases=\"\" data-emoji=\"two_hearts\" data-unicode-name=\"1F495\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46C\" title=\"two_men_holding_hands\" data-aliases=\"\" data-emoji=\"two_men_holding_hands\" data-unicode-name=\"1F46C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46D\" title=\"two_women_holding_hands\" data-aliases=\"\" data-emoji=\"two_women_holding_hands\" data-unicode-name=\"1F46D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F612\" title=\"unamused\" data-aliases=\"\" data-emoji=\"unamused\" data-unicode-name=\"1F612\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270C\" title=\"v\" data-aliases=\"\" data-emoji=\"v\" data-unicode-name=\"270C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F6B6\" title=\"walking\" data-aliases=\"\" data-emoji=\"walking\" data-unicode-name=\"1F6B6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44B\" title=\"wave\" data-aliases=\"\" data-emoji=\"wave\" data-unicode-name=\"1F44B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F629\" title=\"weary\" data-aliases=\"\" data-emoji=\"weary\" data-unicode-name=\"1F629\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F609\" title=\"wink\" data-aliases=\"\" data-emoji=\"wink\" data-unicode-name=\"1F609\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F469\" title=\"woman\" data-aliases=\"\" data-emoji=\"woman\" data-unicode-name=\"1F469\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45A\" title=\"womans_clothes\" data-aliases=\"\" data-emoji=\"womans_clothes\" data-unicode-name=\"1F45A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F452\" title=\"womans_hat\" data-aliases=\"\" data-emoji=\"womans_hat\" data-unicode-name=\"1F452\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61F\" title=\"worried\" data-aliases=\"\" data-emoji=\"worried\" data-unicode-name=\"1F61F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49B\" title=\"yellow_heart\" data-aliases=\"\" data-emoji=\"yellow_heart\" data-unicode-name=\"1F49B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60B\" title=\"yum\" data-aliases=\"\" data-emoji=\"yum\" data-unicode-name=\"1F60B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A4\" title=\"zzz\" data-aliases=\"\" data-emoji=\"zzz\" data-unicode-name=\"1F4A4\"></div>\n </button>\n </li>\n </ul>\n </div>\n</div>"; -}).call(window); diff --git a/spec/javascripts/fixtures/environments/metrics.html.haml b/spec/javascripts/fixtures/environments/metrics.html.haml new file mode 100644 index 00000000000..483063fb889 --- /dev/null +++ b/spec/javascripts/fixtures/environments/metrics.html.haml @@ -0,0 +1,12 @@ +%div + .top-area + .row + .col-sm-6 + %h3.page-title + Metrics for environment + .row + .col-sm-12 + %svg.prometheus-graph{ 'graph-type' => 'cpu_values' } + .row + .col-sm-12 + %svg.prometheus-graph{ 'graph-type' => 'memory_values' }
\ No newline at end of file diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js new file mode 100644 index 00000000000..e94e220b19f --- /dev/null +++ b/spec/javascripts/gl_emoji_spec.js @@ -0,0 +1,367 @@ + +require('~/extensions/string'); +require('~/extensions/array'); + +const glEmoji = require('~/behaviors/gl_emoji'); + +const glEmojiTag = glEmoji.glEmojiTag; +const isEmojiUnicodeSupported = glEmoji.isEmojiUnicodeSupported; +const isFlagEmoji = glEmoji.isFlagEmoji; +const isKeycapEmoji = glEmoji.isKeycapEmoji; +const isSkinToneComboEmoji = glEmoji.isSkinToneComboEmoji; +const isHorceRacingSkinToneComboEmoji = glEmoji.isHorceRacingSkinToneComboEmoji; +const isPersonZwjEmoji = glEmoji.isPersonZwjEmoji; + +const emptySupportMap = { + personZwj: false, + horseRacing: false, + flag: false, + skinToneModifier: false, + '9.0': false, + '8.0': false, + '7.0': false, + 6.1: false, + '6.0': false, + 5.2: false, + 5.1: false, + 4.1: false, + '4.0': false, + 3.2: false, + '3.0': false, + 1.1: false, +}; + +const emojiFixtureMap = { + bomb: { + name: 'bomb', + moji: '💣', + unicodeVersion: '6.0', + }, + construction_worker_tone5: { + name: 'construction_worker_tone5', + moji: '👷🏿', + unicodeVersion: '8.0', + }, + five: { + name: 'five', + moji: '5️⃣', + unicodeVersion: '3.0', + }, +}; + +function markupToDomElement(markup) { + const div = document.createElement('div'); + div.innerHTML = markup; + return div.firstElementChild; +} + +function testGlEmojiImageFallback(element, name, src) { + expect(element.tagName.toLowerCase()).toBe('img'); + expect(element.getAttribute('src')).toBe(src); + expect(element.getAttribute('title')).toBe(`:${name}:`); + expect(element.getAttribute('alt')).toBe(`:${name}:`); +} + +const defaults = { + forceFallback: false, + sprite: false, +}; + +function testGlEmojiElement(element, name, unicodeVersion, unicodeMoji, options = {}) { + const opts = Object.assign({}, defaults, options); + expect(element.tagName.toLowerCase()).toBe('gl-emoji'); + expect(element.dataset.name).toBe(name); + expect(element.dataset.fallbackSrc.length).toBeGreaterThan(0); + expect(element.dataset.unicodeVersion).toBe(unicodeVersion); + + const fallbackSpriteClass = `emoji-${name}`; + if (opts.sprite) { + expect(element.dataset.fallbackSpriteClass).toBe(fallbackSpriteClass); + } + + if (opts.forceFallback && opts.sprite) { + expect(element.getAttribute('class')).toBe(`emoji-icon ${fallbackSpriteClass}`); + } + + if (opts.forceFallback && !opts.sprite) { + // Check for image fallback + testGlEmojiImageFallback(element.firstElementChild, name, element.dataset.fallbackSrc); + } else { + // Otherwise make sure things are still unicode text + expect(element.textContent.trim()).toBe(unicodeMoji); + } +} + +describe('gl_emoji', () => { + describe('glEmojiTag', () => { + it('bomb emoji', () => { + const emojiKey = 'bomb'; + const markup = glEmojiTag(emojiFixtureMap[emojiKey].name); + const glEmojiElement = markupToDomElement(markup); + testGlEmojiElement( + glEmojiElement, + emojiFixtureMap[emojiKey].name, + emojiFixtureMap[emojiKey].unicodeVersion, + emojiFixtureMap[emojiKey].moji, + ); + }); + + it('bomb emoji with image fallback', () => { + const emojiKey = 'bomb'; + const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, { + forceFallback: true, + }); + const glEmojiElement = markupToDomElement(markup); + testGlEmojiElement( + glEmojiElement, + emojiFixtureMap[emojiKey].name, + emojiFixtureMap[emojiKey].unicodeVersion, + emojiFixtureMap[emojiKey].moji, + { + forceFallback: true, + }, + ); + }); + + it('bomb emoji with sprite fallback readiness', () => { + const emojiKey = 'bomb'; + const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, { + sprite: true, + }); + const glEmojiElement = markupToDomElement(markup); + testGlEmojiElement( + glEmojiElement, + emojiFixtureMap[emojiKey].name, + emojiFixtureMap[emojiKey].unicodeVersion, + emojiFixtureMap[emojiKey].moji, + { + sprite: true, + }, + ); + }); + it('bomb emoji with sprite fallback', () => { + const emojiKey = 'bomb'; + const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, { + forceFallback: true, + sprite: true, + }); + const glEmojiElement = markupToDomElement(markup); + testGlEmojiElement( + glEmojiElement, + emojiFixtureMap[emojiKey].name, + emojiFixtureMap[emojiKey].unicodeVersion, + emojiFixtureMap[emojiKey].moji, + { + forceFallback: true, + sprite: true, + }, + ); + }); + }); + + describe('isFlagEmoji', () => { + it('should detect flag_ac', () => { + expect(isFlagEmoji('🇦🇨')).toBeTruthy(); + }); + it('should detect flag_us', () => { + expect(isFlagEmoji('🇺🇸')).toBeTruthy(); + }); + it('should detect flag_zw', () => { + expect(isFlagEmoji('🇿🇼')).toBeTruthy(); + }); + it('should not detect flags', () => { + expect(isFlagEmoji('🎏')).toBeFalsy(); + }); + it('should not detect triangular_flag_on_post', () => { + expect(isFlagEmoji('🚩')).toBeFalsy(); + }); + it('should not detect single letter', () => { + expect(isFlagEmoji('🇦')).toBeFalsy(); + }); + it('should not detect >2 letters', () => { + expect(isFlagEmoji('🇦🇧🇨')).toBeFalsy(); + }); + }); + + describe('isKeycapEmoji', () => { + it('should detect one(keycap)', () => { + expect(isKeycapEmoji('1️⃣')).toBeTruthy(); + }); + it('should detect nine(keycap)', () => { + expect(isKeycapEmoji('9️⃣')).toBeTruthy(); + }); + it('should not detect ten(keycap)', () => { + expect(isKeycapEmoji('🔟')).toBeFalsy(); + }); + it('should not detect hash(keycap)', () => { + expect(isKeycapEmoji('#⃣')).toBeFalsy(); + }); + }); + + describe('isSkinToneComboEmoji', () => { + it('should detect hand_splayed_tone5', () => { + expect(isSkinToneComboEmoji('🖐🏿')).toBeTruthy(); + }); + it('should not detect hand_splayed', () => { + expect(isSkinToneComboEmoji('🖐')).toBeFalsy(); + }); + it('should detect lifter_tone1', () => { + expect(isSkinToneComboEmoji('🏋🏻')).toBeTruthy(); + }); + it('should not detect lifter', () => { + expect(isSkinToneComboEmoji('🏋')).toBeFalsy(); + }); + it('should detect rowboat_tone4', () => { + expect(isSkinToneComboEmoji('🚣🏾')).toBeTruthy(); + }); + it('should not detect rowboat', () => { + expect(isSkinToneComboEmoji('🚣')).toBeFalsy(); + }); + it('should not detect individual tone emoji', () => { + expect(isSkinToneComboEmoji('🏻')).toBeFalsy(); + }); + }); + + describe('isHorceRacingSkinToneComboEmoji', () => { + it('should detect horse_racing_tone2', () => { + expect(isHorceRacingSkinToneComboEmoji('🏇🏼')).toBeTruthy(); + }); + it('should not detect horse_racing', () => { + expect(isHorceRacingSkinToneComboEmoji('🏇')).toBeFalsy(); + }); + }); + + describe('isPersonZwjEmoji', () => { + it('should detect couple_mm', () => { + expect(isPersonZwjEmoji('👨❤️👨')).toBeTruthy(); + }); + it('should not detect couple_with_heart', () => { + expect(isPersonZwjEmoji('💑')).toBeFalsy(); + }); + it('should not detect couplekiss', () => { + expect(isPersonZwjEmoji('💏')).toBeFalsy(); + }); + it('should detect family_mmb', () => { + expect(isPersonZwjEmoji('👨👨👦')).toBeTruthy(); + }); + it('should detect family_mwgb', () => { + expect(isPersonZwjEmoji('👨👩👧👦')).toBeTruthy(); + }); + it('should not detect family', () => { + expect(isPersonZwjEmoji('👪')).toBeFalsy(); + }); + it('should detect kiss_ww', () => { + expect(isPersonZwjEmoji('👩❤️💋👩')).toBeTruthy(); + }); + it('should not detect girl', () => { + expect(isPersonZwjEmoji('👧')).toBeFalsy(); + }); + it('should not detect girl_tone5', () => { + expect(isPersonZwjEmoji('👧🏿')).toBeFalsy(); + }); + it('should not detect man', () => { + expect(isPersonZwjEmoji('👨')).toBeFalsy(); + }); + it('should not detect woman', () => { + expect(isPersonZwjEmoji('👩')).toBeFalsy(); + }); + }); + + describe('isEmojiUnicodeSupported', () => { + it('bomb(6.0) with 6.0 support', () => { + const emojiKey = 'bomb'; + const unicodeSupportMap = Object.assign({}, emptySupportMap, { + '6.0': true, + }); + const isSupported = isEmojiUnicodeSupported( + unicodeSupportMap, + emojiFixtureMap[emojiKey].moji, + emojiFixtureMap[emojiKey].unicodeVersion, + ); + expect(isSupported).toBeTruthy(); + }); + + it('bomb(6.0) without 6.0 support', () => { + const emojiKey = 'bomb'; + const unicodeSupportMap = emptySupportMap; + const isSupported = isEmojiUnicodeSupported( + unicodeSupportMap, + emojiFixtureMap[emojiKey].moji, + emojiFixtureMap[emojiKey].unicodeVersion, + ); + expect(isSupported).toBeFalsy(); + }); + + it('bomb(6.0) without 6.0 but with 9.0 support', () => { + const emojiKey = 'bomb'; + const unicodeSupportMap = Object.assign({}, emptySupportMap, { + '9.0': true, + }); + const isSupported = isEmojiUnicodeSupported( + unicodeSupportMap, + emojiFixtureMap[emojiKey].moji, + emojiFixtureMap[emojiKey].unicodeVersion, + ); + expect(isSupported).toBeFalsy(); + }); + + it('construction_worker_tone5(8.0) without skin tone modifier support', () => { + const emojiKey = 'construction_worker_tone5'; + const unicodeSupportMap = Object.assign({}, emptySupportMap, { + skinToneModifier: false, + '9.0': true, + '8.0': true, + '7.0': true, + 6.1: true, + '6.0': true, + 5.2: true, + 5.1: true, + 4.1: true, + '4.0': true, + 3.2: true, + '3.0': true, + 1.1: true, + }); + const isSupported = isEmojiUnicodeSupported( + unicodeSupportMap, + emojiFixtureMap[emojiKey].moji, + emojiFixtureMap[emojiKey].unicodeVersion, + ); + expect(isSupported).toBeFalsy(); + }); + + it('use native keycap on >=57 chrome', () => { + const emojiKey = 'five'; + const unicodeSupportMap = Object.assign({}, emptySupportMap, { + '3.0': true, + meta: { + isChrome: true, + chromeVersion: 57, + }, + }); + const isSupported = isEmojiUnicodeSupported( + unicodeSupportMap, + emojiFixtureMap[emojiKey].moji, + emojiFixtureMap[emojiKey].unicodeVersion, + ); + expect(isSupported).toBeTruthy(); + }); + + it('fallback keycap on <57 chrome', () => { + const emojiKey = 'five'; + const unicodeSupportMap = Object.assign({}, emptySupportMap, { + '3.0': true, + meta: { + isChrome: true, + chromeVersion: 50, + }, + }); + const isSupported = isEmojiUnicodeSupported( + unicodeSupportMap, + emojiFixtureMap[emojiKey].moji, + emojiFixtureMap[emojiKey].unicodeVersion, + ); + expect(isSupported).toBeFalsy(); + }); + }); +}); diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js b/spec/javascripts/helpers/filtered_search_spec_helper.js new file mode 100644 index 00000000000..ce83a256ddd --- /dev/null +++ b/spec/javascripts/helpers/filtered_search_spec_helper.js @@ -0,0 +1,52 @@ +class FilteredSearchSpecHelper { + static createFilterVisualTokenHTML(name, value, isSelected) { + return FilteredSearchSpecHelper.createFilterVisualToken(name, value, isSelected).outerHTML; + } + + static createFilterVisualToken(name, value, isSelected = false) { + const li = document.createElement('li'); + li.classList.add('js-visual-token', 'filtered-search-token'); + + li.innerHTML = ` + <div class="selectable ${isSelected ? 'selected' : ''}" role="button"> + <div class="name">${name}</div> + <div class="value">${value}</div> + </div> + `; + + return li; + } + + static createNameFilterVisualTokenHTML(name) { + return ` + <li class="js-visual-token filtered-search-token"> + <div class="name">${name}</div> + </li> + `; + } + + static createSearchVisualTokenHTML(name) { + return ` + <li class="js-visual-token filtered-search-term"> + <div class="name">${name}</div> + </li> + `; + } + + static createInputHTML(placeholder = '', value = '') { + return ` + <li class="input-token"> + <input type='text' class='filtered-search' placeholder='${placeholder}' value='${value}'/> + </li> + `; + } + + static createTokensContainerHTML(html, inputPlaceholder) { + return ` + ${html} + ${FilteredSearchSpecHelper.createInputHTML(inputPlaceholder)} + `; + } +} + +module.exports = FilteredSearchSpecHelper; diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js new file mode 100644 index 00000000000..823b4bab7fc --- /dev/null +++ b/spec/javascripts/monitoring/prometheus_graph_spec.js @@ -0,0 +1,78 @@ +import 'jquery'; +import es6Promise from 'es6-promise'; +import '~/lib/utils/common_utils'; +import PrometheusGraph from '~/monitoring/prometheus_graph'; +import { prometheusMockData } from './prometheus_mock_data'; + +es6Promise.polyfill(); + +describe('PrometheusGraph', () => { + const fixtureName = 'static/environments/metrics.html.raw'; + const prometheusGraphContainer = '.prometheus-graph'; + const prometheusGraphContents = `${prometheusGraphContainer}[graph-type=cpu_values]`; + + preloadFixtures(fixtureName); + + beforeEach(() => { + loadFixtures(fixtureName); + this.prometheusGraph = new PrometheusGraph(); + const self = this; + const fakeInit = (metricsResponse) => { + self.prometheusGraph.transformData(metricsResponse); + self.prometheusGraph.createGraph(); + }; + spyOn(this.prometheusGraph, 'init').and.callFake(fakeInit); + }); + + it('initializes graph properties', () => { + // Test for the measurements + expect(this.prometheusGraph.margin).toBeDefined(); + expect(this.prometheusGraph.marginLabelContainer).toBeDefined(); + expect(this.prometheusGraph.originalWidth).toBeDefined(); + expect(this.prometheusGraph.originalHeight).toBeDefined(); + expect(this.prometheusGraph.height).toBeDefined(); + expect(this.prometheusGraph.width).toBeDefined(); + expect(this.prometheusGraph.backOffRequestCounter).toBeDefined(); + // Test for the graph properties (colors, radius, etc.) + expect(this.prometheusGraph.graphSpecificProperties).toBeDefined(); + expect(this.prometheusGraph.commonGraphProperties).toBeDefined(); + }); + + it('transforms the data', () => { + this.prometheusGraph.init(prometheusMockData.metrics); + expect(this.prometheusGraph.data).toBeDefined(); + expect(this.prometheusGraph.data.cpu_values.length).toBe(121); + expect(this.prometheusGraph.data.memory_values.length).toBe(121); + }); + + it('creates two graphs', () => { + this.prometheusGraph.init(prometheusMockData.metrics); + expect($(prometheusGraphContainer).length).toBe(2); + }); + + describe('Graph contents', () => { + beforeEach(() => { + this.prometheusGraph.init(prometheusMockData.metrics); + }); + + it('has axis, an area, a line and a overlay', () => { + const $graphContainer = $(prometheusGraphContents).find('.x-axis').parent(); + expect($graphContainer.find('.x-axis')).toBeDefined(); + expect($graphContainer.find('.y-axis')).toBeDefined(); + expect($graphContainer.find('.prometheus-graph-overlay')).toBeDefined(); + expect($graphContainer.find('.metric-line')).toBeDefined(); + expect($graphContainer.find('.metric-area')).toBeDefined(); + }); + + it('has legends, labels and an extra axis that labels the metrics', () => { + const $prometheusGraphContents = $(prometheusGraphContents); + const $axisLabelContainer = $(prometheusGraphContents).find('.label-x-axis-line').parent(); + expect($prometheusGraphContents.find('.label-x-axis-line')).toBeDefined(); + expect($prometheusGraphContents.find('.label-y-axis-line')).toBeDefined(); + expect($prometheusGraphContents.find('.label-axis-text')).toBeDefined(); + expect($prometheusGraphContents.find('.rect-axis-text')).toBeDefined(); + expect($axisLabelContainer.find('rect').length).toBe(2); + expect($axisLabelContainer.find('text').length).toBe(4); + }); + }); +}); diff --git a/spec/javascripts/monitoring/prometheus_mock_data.js b/spec/javascripts/monitoring/prometheus_mock_data.js new file mode 100644 index 00000000000..1cdc14faaa8 --- /dev/null +++ b/spec/javascripts/monitoring/prometheus_mock_data.js @@ -0,0 +1,1014 @@ +/* eslint-disable import/prefer-default-export*/ +export const prometheusMockData = { + status: 200, + metrics: { + success: true, + metrics: { + memory_values: [ + { + metric: { + }, + values: [ + [ + 1488462917.256, + '10.12890625', + ], + [ + 1488462977.256, + '10.140625', + ], + [ + 1488463037.256, + '10.140625', + ], + [ + 1488463097.256, + '10.14453125', + ], + [ + 1488463157.256, + '10.1484375', + ], + [ + 1488463217.256, + '10.15625', + ], + [ + 1488463277.256, + '10.15625', + ], + [ + 1488463337.256, + '10.15625', + ], + [ + 1488463397.256, + '10.1640625', + ], + [ + 1488463457.256, + '10.171875', + ], + [ + 1488463517.256, + '10.171875', + ], + [ + 1488463577.256, + '10.171875', + ], + [ + 1488463637.256, + '10.18359375', + ], + [ + 1488463697.256, + '10.1953125', + ], + [ + 1488463757.256, + '10.203125', + ], + [ + 1488463817.256, + '10.20703125', + ], + [ + 1488463877.256, + '10.20703125', + ], + [ + 1488463937.256, + '10.20703125', + ], + [ + 1488463997.256, + '10.20703125', + ], + [ + 1488464057.256, + '10.2109375', + ], + [ + 1488464117.256, + '10.2109375', + ], + [ + 1488464177.256, + '10.2109375', + ], + [ + 1488464237.256, + '10.2109375', + ], + [ + 1488464297.256, + '10.21484375', + ], + [ + 1488464357.256, + '10.22265625', + ], + [ + 1488464417.256, + '10.22265625', + ], + [ + 1488464477.256, + '10.2265625', + ], + [ + 1488464537.256, + '10.23046875', + ], + [ + 1488464597.256, + '10.23046875', + ], + [ + 1488464657.256, + '10.234375', + ], + [ + 1488464717.256, + '10.234375', + ], + [ + 1488464777.256, + '10.234375', + ], + [ + 1488464837.256, + '10.234375', + ], + [ + 1488464897.256, + '10.234375', + ], + [ + 1488464957.256, + '10.234375', + ], + [ + 1488465017.256, + '10.23828125', + ], + [ + 1488465077.256, + '10.23828125', + ], + [ + 1488465137.256, + '10.2421875', + ], + [ + 1488465197.256, + '10.2421875', + ], + [ + 1488465257.256, + '10.2421875', + ], + [ + 1488465317.256, + '10.2421875', + ], + [ + 1488465377.256, + '10.2421875', + ], + [ + 1488465437.256, + '10.2421875', + ], + [ + 1488465497.256, + '10.2421875', + ], + [ + 1488465557.256, + '10.2421875', + ], + [ + 1488465617.256, + '10.2421875', + ], + [ + 1488465677.256, + '10.2421875', + ], + [ + 1488465737.256, + '10.2421875', + ], + [ + 1488465797.256, + '10.24609375', + ], + [ + 1488465857.256, + '10.25', + ], + [ + 1488465917.256, + '10.25390625', + ], + [ + 1488465977.256, + '9.98828125', + ], + [ + 1488466037.256, + '9.9921875', + ], + [ + 1488466097.256, + '9.9921875', + ], + [ + 1488466157.256, + '9.99609375', + ], + [ + 1488466217.256, + '10', + ], + [ + 1488466277.256, + '10.00390625', + ], + [ + 1488466337.256, + '10.0078125', + ], + [ + 1488466397.256, + '10.01171875', + ], + [ + 1488466457.256, + '10.0234375', + ], + [ + 1488466517.256, + '10.02734375', + ], + [ + 1488466577.256, + '10.02734375', + ], + [ + 1488466637.256, + '10.03125', + ], + [ + 1488466697.256, + '10.03125', + ], + [ + 1488466757.256, + '10.03125', + ], + [ + 1488466817.256, + '10.03125', + ], + [ + 1488466877.256, + '10.03125', + ], + [ + 1488466937.256, + '10.03125', + ], + [ + 1488466997.256, + '10.03125', + ], + [ + 1488467057.256, + '10.0390625', + ], + [ + 1488467117.256, + '10.0390625', + ], + [ + 1488467177.256, + '10.04296875', + ], + [ + 1488467237.256, + '10.05078125', + ], + [ + 1488467297.256, + '10.05859375', + ], + [ + 1488467357.256, + '10.06640625', + ], + [ + 1488467417.256, + '10.06640625', + ], + [ + 1488467477.256, + '10.0703125', + ], + [ + 1488467537.256, + '10.07421875', + ], + [ + 1488467597.256, + '10.0859375', + ], + [ + 1488467657.256, + '10.0859375', + ], + [ + 1488467717.256, + '10.09765625', + ], + [ + 1488467777.256, + '10.1015625', + ], + [ + 1488467837.256, + '10.10546875', + ], + [ + 1488467897.256, + '10.10546875', + ], + [ + 1488467957.256, + '10.125', + ], + [ + 1488468017.256, + '10.13671875', + ], + [ + 1488468077.256, + '10.1484375', + ], + [ + 1488468137.256, + '10.15625', + ], + [ + 1488468197.256, + '10.16796875', + ], + [ + 1488468257.256, + '10.171875', + ], + [ + 1488468317.256, + '10.171875', + ], + [ + 1488468377.256, + '10.171875', + ], + [ + 1488468437.256, + '10.171875', + ], + [ + 1488468497.256, + '10.171875', + ], + [ + 1488468557.256, + '10.171875', + ], + [ + 1488468617.256, + '10.171875', + ], + [ + 1488468677.256, + '10.17578125', + ], + [ + 1488468737.256, + '10.17578125', + ], + [ + 1488468797.256, + '10.265625', + ], + [ + 1488468857.256, + '10.19921875', + ], + [ + 1488468917.256, + '10.19921875', + ], + [ + 1488468977.256, + '10.19921875', + ], + [ + 1488469037.256, + '10.19921875', + ], + [ + 1488469097.256, + '10.19921875', + ], + [ + 1488469157.256, + '10.203125', + ], + [ + 1488469217.256, + '10.43359375', + ], + [ + 1488469277.256, + '10.20703125', + ], + [ + 1488469337.256, + '10.2109375', + ], + [ + 1488469397.256, + '10.22265625', + ], + [ + 1488469457.256, + '10.21484375', + ], + [ + 1488469517.256, + '10.21484375', + ], + [ + 1488469577.256, + '10.21484375', + ], + [ + 1488469637.256, + '10.22265625', + ], + [ + 1488469697.256, + '10.234375', + ], + [ + 1488469757.256, + '10.234375', + ], + [ + 1488469817.256, + '10.234375', + ], + [ + 1488469877.256, + '10.2421875', + ], + [ + 1488469937.256, + '10.25', + ], + [ + 1488469997.256, + '10.25390625', + ], + [ + 1488470057.256, + '10.26171875', + ], + [ + 1488470117.256, + '10.2734375', + ], + ], + }, + ], + memory_current: [ + { + metric: { + }, + value: [ + 1488470117.737, + '10.2734375', + ], + }, + ], + cpu_values: [ + { + metric: { + }, + values: [ + [ + 1488462918.15, + '0.0002996458625058103', + ], + [ + 1488462978.15, + '0.0002652382333333314', + ], + [ + 1488463038.15, + '0.0003485461333333421', + ], + [ + 1488463098.15, + '0.0003420421999999886', + ], + [ + 1488463158.15, + '0.00023107150000001297', + ], + [ + 1488463218.15, + '0.00030463981666664826', + ], + [ + 1488463278.15, + '0.0002477177833333677', + ], + [ + 1488463338.15, + '0.00026936656666665115', + ], + [ + 1488463398.15, + '0.000406264750000022', + ], + [ + 1488463458.15, + '0.00029592802026561453', + ], + [ + 1488463518.15, + '0.00023426999683316343', + ], + [ + 1488463578.15, + '0.0003057080666666915', + ], + [ + 1488463638.15, + '0.0003408470500000149', + ], + [ + 1488463698.15, + '0.00025497336666665166', + ], + [ + 1488463758.15, + '0.0003009282833333534', + ], + [ + 1488463818.15, + '0.0003119383499999924', + ], + [ + 1488463878.15, + '0.00028719019999998705', + ], + [ + 1488463938.15, + '0.000327864749999988', + ], + [ + 1488463998.15, + '0.0002514917333333422', + ], + [ + 1488464058.15, + '0.0003614651166666742', + ], + [ + 1488464118.15, + '0.0003221668000000122', + ], + [ + 1488464178.15, + '0.00023323083333330884', + ], + [ + 1488464238.15, + '0.00028531499475009274', + ], + [ + 1488464298.15, + '0.0002627695294921391', + ], + [ + 1488464358.15, + '0.00027145463333333453', + ], + [ + 1488464418.15, + '0.00025669488333335266', + ], + [ + 1488464478.15, + '0.00022307761666665965', + ], + [ + 1488464538.15, + '0.0003307265833333517', + ], + [ + 1488464598.15, + '0.0002817050666666709', + ], + [ + 1488464658.15, + '0.00022357458333332285', + ], + [ + 1488464718.15, + '0.00032648590000000275', + ], + [ + 1488464778.15, + '0.00028410750000000816', + ], + [ + 1488464838.15, + '0.0003038076999999954', + ], + [ + 1488464898.15, + '0.00037568226666667335', + ], + [ + 1488464958.15, + '0.00020160354999999202', + ], + [ + 1488465018.15, + '0.0003229403333333399', + ], + [ + 1488465078.15, + '0.00033516069999999236', + ], + [ + 1488465138.15, + '0.0003365978333333371', + ], + [ + 1488465198.15, + '0.00020262178333331585', + ], + [ + 1488465258.15, + '0.00040567498333331876', + ], + [ + 1488465318.15, + '0.00029114155000001436', + ], + [ + 1488465378.15, + '0.0002498841000000122', + ], + [ + 1488465438.15, + '0.00027296763333331715', + ], + [ + 1488465498.15, + '0.0002958794000000135', + ], + [ + 1488465558.15, + '0.0002922354666666867', + ], + [ + 1488465618.15, + '0.00034186624999999653', + ], + [ + 1488465678.15, + '0.0003397984166666627', + ], + [ + 1488465738.15, + '0.0002658284166666469', + ], + [ + 1488465798.15, + '0.00026221139999999346', + ], + [ + 1488465858.15, + '0.00029467960000001034', + ], + [ + 1488465918.15, + '0.0002634141333333358', + ], + [ + 1488465978.15, + '0.0003202958333333209', + ], + [ + 1488466038.15, + '0.00037890760000000394', + ], + [ + 1488466098.15, + '0.00023453356666666518', + ], + [ + 1488466158.15, + '0.0002866827333333433', + ], + [ + 1488466218.15, + '0.0003335935499999998', + ], + [ + 1488466278.15, + '0.00022787131666666125', + ], + [ + 1488466338.15, + '0.00033821938333333064', + ], + [ + 1488466398.15, + '0.00029233375000001043', + ], + [ + 1488466458.15, + '0.00026562758333333514', + ], + [ + 1488466518.15, + '0.0003142600999999819', + ], + [ + 1488466578.15, + '0.00027392178333333444', + ], + [ + 1488466638.15, + '0.00028178598333334173', + ], + [ + 1488466698.15, + '0.0002463400666666911', + ], + [ + 1488466758.15, + '0.00040234373333332125', + ], + [ + 1488466818.15, + '0.00023677453333332822', + ], + [ + 1488466878.15, + '0.00030852703333333523', + ], + [ + 1488466938.15, + '0.0003582272833333455', + ], + [ + 1488466998.15, + '0.0002176380833332973', + ], + [ + 1488467058.15, + '0.00026180203333335447', + ], + [ + 1488467118.15, + '0.00027862966666667436', + ], + [ + 1488467178.15, + '0.0002769731166666567', + ], + [ + 1488467238.15, + '0.0002832899166666477', + ], + [ + 1488467298.15, + '0.0003446533500000311', + ], + [ + 1488467358.15, + '0.0002691345999999761', + ], + [ + 1488467418.15, + '0.000284919933333357', + ], + [ + 1488467478.15, + '0.0002396026166666528', + ], + [ + 1488467538.15, + '0.00035625295000002075', + ], + [ + 1488467598.15, + '0.00036759816666664946', + ], + [ + 1488467658.15, + '0.00030326608333333855', + ], + [ + 1488467718.15, + '0.00023584972418043393', + ], + [ + 1488467778.15, + '0.00025744508892115107', + ], + [ + 1488467838.15, + '0.00036737541666663395', + ], + [ + 1488467898.15, + '0.00034325741666666094', + ], + [ + 1488467958.15, + '0.00026390046666667407', + ], + [ + 1488468018.15, + '0.0003302534500000102', + ], + [ + 1488468078.15, + '0.00035243794999999527', + ], + [ + 1488468138.15, + '0.00020149738333333407', + ], + [ + 1488468198.15, + '0.0003183469666666679', + ], + [ + 1488468258.15, + '0.0003835329166666845', + ], + [ + 1488468318.15, + '0.0002485075333333124', + ], + [ + 1488468378.15, + '0.0003011457166666768', + ], + [ + 1488468438.15, + '0.00032242785497684965', + ], + [ + 1488468498.15, + '0.0002659713747457531', + ], + [ + 1488468558.15, + '0.0003476860333333202', + ], + [ + 1488468618.15, + '0.00028336403333334794', + ], + [ + 1488468678.15, + '0.00017132354999998728', + ], + [ + 1488468738.15, + '0.0003001915833333276', + ], + [ + 1488468798.15, + '0.0003025715666666725', + ], + [ + 1488468858.15, + '0.0003012370166666815', + ], + [ + 1488468918.15, + '0.00030203619999997025', + ], + [ + 1488468978.15, + '0.0002804355000000314', + ], + [ + 1488469038.15, + '0.00033194884999998564', + ], + [ + 1488469098.15, + '0.00025201496666665455', + ], + [ + 1488469158.15, + '0.0002777531500000189', + ], + [ + 1488469218.15, + '0.0003314885833333392', + ], + [ + 1488469278.15, + '0.0002234891422095589', + ], + [ + 1488469338.15, + '0.000349117355867791', + ], + [ + 1488469398.15, + '0.0004036731333333303', + ], + [ + 1488469458.15, + '0.00024553911666667835', + ], + [ + 1488469518.15, + '0.0003056456833333184', + ], + [ + 1488469578.15, + '0.0002618737166666681', + ], + [ + 1488469638.15, + '0.00022972643333331414', + ], + [ + 1488469698.15, + '0.0003713522500000307', + ], + [ + 1488469758.15, + '0.00018322576666666515', + ], + [ + 1488469818.15, + '0.00034534762753952466', + ], + [ + 1488469878.15, + '0.00028200510008501677', + ], + [ + 1488469938.15, + '0.0002773708499999768', + ], + [ + 1488469998.15, + '0.00027547160000001013', + ], + [ + 1488470058.15, + '0.00031713610000000023', + ], + [ + 1488470118.15, + '0.00035276853333332525', + ], + ], + }, + ], + cpu_current: [ + { + metric: { + }, + value: [ + 1488470118.566, + '0.00035276853333332525', + ], + }, + ], + last_update: '2017-03-02T15:55:18.981Z', + }, + }, +}; diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb index c8e62f528df..707212e07fd 100644 --- a/spec/lib/banzai/filter/emoji_filter_spec.rb +++ b/spec/lib/banzai/filter/emoji_filter_spec.rb @@ -14,12 +14,12 @@ describe Banzai::Filter::EmojiFilter, lib: true do it 'replaces supported name emoji' do doc = filter('<p>:heart:</p>') - expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png' + expect(doc.css('gl-emoji').first.text).to eq '❤' end it 'replaces supported unicode emoji' do doc = filter('<p>❤️</p>') - expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png' + expect(doc.css('gl-emoji').first.text).to eq '❤' end it 'ignores unsupported emoji' do @@ -30,152 +30,78 @@ describe Banzai::Filter::EmojiFilter, lib: true do it 'correctly encodes the URL' do doc = filter('<p>:+1:</p>') - expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png' + expect(doc.css('gl-emoji').first.text).to eq '👍' end it 'correctly encodes unicode to the URL' do doc = filter('<p>👍</p>') - expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png' + expect(doc.css('gl-emoji').first.text).to eq '👍' end it 'matches at the start of a string' do doc = filter(':+1:') - expect(doc.css('img').size).to eq 1 + expect(doc.css('gl-emoji').size).to eq 1 end it 'unicode matches at the start of a string' do doc = filter("'👍'") - expect(doc.css('img').size).to eq 1 + expect(doc.css('gl-emoji').size).to eq 1 end it 'matches at the end of a string' do doc = filter('This gets a :-1:') - expect(doc.css('img').size).to eq 1 + expect(doc.css('gl-emoji').size).to eq 1 end it 'unicode matches at the end of a string' do doc = filter('This gets a 👍') - expect(doc.css('img').size).to eq 1 + expect(doc.css('gl-emoji').size).to eq 1 end it 'matches with adjacent text' do doc = filter('+1 (:+1:)') - expect(doc.css('img').size).to eq 1 + expect(doc.css('gl-emoji').size).to eq 1 end it 'unicode matches with adjacent text' do doc = filter('+1 (👍)') - expect(doc.css('img').size).to eq 1 + expect(doc.css('gl-emoji').size).to eq 1 end it 'matches multiple emoji in a row' do doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:') - expect(doc.css('img').size).to eq 3 + expect(doc.css('gl-emoji').size).to eq 3 end it 'unicode matches multiple emoji in a row' do doc = filter("'🙈🙉🙊'") - expect(doc.css('img').size).to eq 3 + expect(doc.css('gl-emoji').size).to eq 3 end it 'mixed matches multiple emoji in a row' do doc = filter("'🙈:see_no_evil:🙉:hear_no_evil:🙊:speak_no_evil:'") - expect(doc.css('img').size).to eq 6 + expect(doc.css('gl-emoji').size).to eq 6 end - it 'has a title attribute' do + it 'has a data-name attribute' do doc = filter(':-1:') - expect(doc.css('img').first.attr('title')).to eq ':-1:' + expect(doc.css('gl-emoji').first.attr('data-name')).to eq 'thumbsdown' end - it 'unicode has a title attribute' do - doc = filter("'👎'") - expect(doc.css('img').first.attr('title')).to eq ':thumbsdown:' - end - - it 'has an alt attribute' do + it 'has a data-unicode-version attribute' do doc = filter(':-1:') - expect(doc.css('img').first.attr('alt')).to eq ':-1:' - end - - it 'unicode has an alt attribute' do - doc = filter("'👎'") - expect(doc.css('img').first.attr('alt')).to eq ':thumbsdown:' - end - - it 'has an align attribute' do - doc = filter(':8ball:') - expect(doc.css('img').first.attr('align')).to eq 'absmiddle' - end - - it 'unicode has an align attribute' do - doc = filter("'🎱'") - expect(doc.css('img').first.attr('align')).to eq 'absmiddle' - end - - it 'has an emoji class' do - doc = filter(':cat:') - expect(doc.css('img').first.attr('class')).to eq 'emoji' - end - - it 'unicode has an emoji class' do - doc = filter("'🐱'") - expect(doc.css('img').first.attr('class')).to eq 'emoji' - end - - it 'has height and width attributes' do - doc = filter(':dog:') - img = doc.css('img').first - - expect(img.attr('width')).to eq '20' - expect(img.attr('height')).to eq '20' - end - - it 'unicode has height and width attributes' do - doc = filter("'🐶'") - img = doc.css('img').first - - expect(img.attr('width')).to eq '20' - expect(img.attr('height')).to eq '20' + expect(doc.css('gl-emoji').first.attr('data-unicode-version')).to eq '6.0' end it 'keeps whitespace intact' do doc = filter('This deserves a :+1:, big time.') - expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/) + expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/) end it 'unicode keeps whitespace intact' do doc = filter('This deserves a 🎱, big time.') - expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/) - end - - it 'uses a custom asset_root context' do - root = Gitlab.config.gitlab.url + 'gitlab/root' - - doc = filter(':smile:', asset_root: root) - expect(doc.css('img').first.attr('src')).to start_with(root) - end - - it 'uses a custom asset_host context' do - ActionController::Base.asset_host = 'https://cdn.example.com' - - doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?') - expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com') - end - - it 'uses a custom asset_root context' do - root = Gitlab.config.gitlab.url + 'gitlab/root' - - doc = filter("'🎱'", asset_root: root) - expect(doc.css('img').first.attr('src')).to start_with(root) - end - - it 'uses a custom asset_host context' do - ActionController::Base.asset_host = 'https://cdn.example.com' - - doc = filter("'🎱'", asset_host: 'https://this-is-ignored-i-guess?') - expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com') + expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/) end end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index daf8f5c1d6c..03c4879ed6f 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -3,6 +3,24 @@ require 'spec_helper' describe Gitlab::Auth, lib: true do let(:gl_auth) { described_class } + describe 'constants' do + it 'API_SCOPES contains all scopes for API access' do + expect(subject::API_SCOPES).to eq [:api, :read_user] + end + + it 'OPENID_SCOPES contains all scopes for OpenID Connect' do + expect(subject::OPENID_SCOPES).to eq [:openid] + end + + it 'DEFAULT_SCOPES contains all default scopes' do + expect(subject::DEFAULT_SCOPES).to eq [:api] + end + + it 'OPTIONAL_SCOPES contains all non-default scopes' do + expect(subject::OPTIONAL_SCOPES).to eq [:read_user, :openid] + end + end + describe 'find_for_git_client' do context 'build token' do subject { gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: project, ip: 'ip') } @@ -118,25 +136,37 @@ describe Gitlab::Auth, lib: true do end context 'while using personal access tokens as passwords' do - let(:user) { create(:user) } - let(:token_w_api_scope) { create(:personal_access_token, user: user, scopes: ['api']) } - it 'succeeds for personal access tokens with the `api` scope' do - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.email) - expect(gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities)) + personal_access_token = create(:personal_access_token, scopes: ['api']) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') + expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities)) + end + + it 'succeeds if it is an impersonation token' do + impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api']) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') + expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(impersonation_token.user, nil, :personal_token, full_authentication_abilities)) end it 'fails for personal access tokens with other scopes' do - personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) + personal_access_token = create(:personal_access_token, scopes: ['read_user']) - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: user.email) - expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') + expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) end - it 'does not try password auth before personal access tokens' do - expect(gl_auth).not_to receive(:find_with_user_password) + it 'fails for impersonation token with other scopes' do + impersonation_token = create(:personal_access_token, scopes: ['read_user']) - gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip') + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') + expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) + end + + it 'fails if password is nil' do + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') + expect(gl_auth.find_for_git_client('', nil, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) end end @@ -210,6 +240,18 @@ describe Gitlab::Auth, lib: true do end end + it "does not find user in blocked state" do + user.block + + expect( gl_auth.find_with_user_password(username, password) ).not_to eql user + end + + it "does not find user in ldap_blocked state" do + user.ldap_block + + expect( gl_auth.find_with_user_password(username, password) ).not_to eql user + end + context "with ldap enabled" do before do allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) diff --git a/spec/lib/gitlab/award_emoji_spec.rb b/spec/lib/gitlab/award_emoji_spec.rb deleted file mode 100644 index 00a110e31f8..00000000000 --- a/spec/lib/gitlab/award_emoji_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -require 'spec_helper' - -describe Gitlab::AwardEmoji do - describe '.urls' do - after do - Gitlab::AwardEmoji.instance_variable_set(:@urls, nil) - end - - subject { Gitlab::AwardEmoji.urls } - - it { is_expected.to be_an_instance_of(Array) } - it { is_expected.not_to be_empty } - - context 'every Hash in the Array' do - it 'has the correct keys and values' do - subject.each do |hash| - expect(hash[:name]).to be_an_instance_of(String) - expect(hash[:path]).to be_an_instance_of(String) - end - end - end - - context 'handles relative root' do - it 'includes the full path' do - allow(Gitlab::Application.config).to receive(:relative_url_root).and_return('/gitlab') - - subject.each do |hash| - expect(hash[:name]).to be_an_instance_of(String) - expect(hash[:path]).to start_with('/gitlab') - end - end - end - end - - describe '.emoji_by_category' do - it "only contains known categories" do - undefined_categories = Gitlab::AwardEmoji.emoji_by_category.keys - Gitlab::AwardEmoji::CATEGORIES.keys - expect(undefined_categories).to be_empty - end - end -end diff --git a/spec/lib/gitlab/ci/build/image_spec.rb b/spec/lib/gitlab/ci/build/image_spec.rb new file mode 100644 index 00000000000..382385dfd6b --- /dev/null +++ b/spec/lib/gitlab/ci/build/image_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Image do + let(:job) { create(:ci_build, :no_options) } + + describe '#from_image' do + subject { described_class.from_image(job) } + + context 'when image is defined in job' do + let(:image_name) { 'ruby:2.1' } + let(:job) { create(:ci_build, options: { image: image_name } ) } + + it 'fabricates an object of the proper class' do + is_expected.to be_kind_of(described_class) + end + + it 'populates fabricated object with the proper name attribute' do + expect(subject.name).to eq(image_name) + end + + context 'when image name is empty' do + let(:image_name) { '' } + + it 'does not fabricate an object' do + is_expected.to be_nil + end + end + end + + context 'when image is not defined in job' do + it 'does not fabricate an object' do + is_expected.to be_nil + end + end + end + + describe '#from_services' do + subject { described_class.from_services(job) } + + context 'when services are defined in job' do + let(:service_image_name) { 'postgres' } + let(:job) { create(:ci_build, options: { services: [service_image_name] }) } + + it 'fabricates an non-empty array of objects' do + is_expected.to be_kind_of(Array) + is_expected.not_to be_empty + expect(subject.first.name).to eq(service_image_name) + end + + context 'when service image name is empty' do + let(:service_image_name) { '' } + + it 'fabricates an empty array' do + is_expected.to be_kind_of(Array) + is_expected.to be_empty + end + end + end + + context 'when services are not defined in job' do + it 'fabricates an empty array' do + is_expected.to be_kind_of(Array) + is_expected.to be_empty + end + end + end +end diff --git a/spec/lib/gitlab/ci/build/step_spec.rb b/spec/lib/gitlab/ci/build/step_spec.rb new file mode 100644 index 00000000000..2a314a744ca --- /dev/null +++ b/spec/lib/gitlab/ci/build/step_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Step do + let(:job) { create(:ci_build, :no_options, commands: "ls -la\ndate") } + + describe '#from_commands' do + subject { described_class.from_commands(job) } + + it 'fabricates an object' do + expect(subject.name).to eq(:script) + expect(subject.script).to eq(['ls -la', 'date']) + expect(subject.timeout).to eq(job.timeout) + expect(subject.when).to eq('on_success') + expect(subject.allow_failure).to be_falsey + end + end + + describe '#from_after_script' do + subject { described_class.from_after_script(job) } + + context 'when after_script is empty' do + it 'doesn not fabricate an object' do + is_expected.to be_nil + end + end + + context 'when after_script is not empty' do + let(:job) { create(:ci_build, options: { after_script: "ls -la\ndate" }) } + + it 'fabricates an object' do + expect(subject.name).to eq(:after_script) + expect(subject.script).to eq(['ls -la', 'date']) + expect(subject.timeout).to eq(job.timeout) + expect(subject.when).to eq('always') + expect(subject.allow_failure).to be_truthy + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb index 70a327c5183..2ed120f356a 100644 --- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb @@ -24,6 +24,20 @@ describe Gitlab::Ci::Config::Entry::Cache do expect(entry).to be_valid end end + + context 'when key is missing' do + let(:config) do + { untracked: true, + paths: ['some/path/'] } + end + + describe '#value' do + it 'sets key with the default' do + expect(entry.value[:key]) + .to eq(Gitlab::Ci::Config::Entry::Key.default) + end + end + end end context 'when entry value is not correct' do diff --git a/spec/lib/gitlab/ci/config/entry/factory_spec.rb b/spec/lib/gitlab/ci/config/entry/factory_spec.rb index 3395b3c645b..8dd48e4efae 100644 --- a/spec/lib/gitlab/ci/config/entry/factory_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/factory_spec.rb @@ -60,13 +60,13 @@ describe Gitlab::Ci::Config::Entry::Factory do end context 'when creating entry with nil value' do - it 'creates an undefined entry' do + it 'creates an unspecified entry' do entry = factory .value(nil) .create! expect(entry) - .to be_an_instance_of Gitlab::Ci::Config::Entry::Unspecified + .not_to be_specified end end diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 1f757f12a56..684d01e9056 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -188,7 +188,7 @@ describe Gitlab::Ci::Config::Entry::Global do it 'contains unspecified nodes' do expect(global.descendants.first) - .to be_an_instance_of Gitlab::Ci::Config::Entry::Unspecified + .not_to be_specified end end diff --git a/spec/lib/gitlab/ci/config/entry/key_spec.rb b/spec/lib/gitlab/ci/config/entry/key_spec.rb index 0dd36fe1f44..5d4de60bc8a 100644 --- a/spec/lib/gitlab/ci/config/entry/key_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/key_spec.rb @@ -31,4 +31,10 @@ describe Gitlab::Ci::Config::Entry::Key do end end end + + describe '.default' do + it 'returns default key' do + expect(described_class.default).to eq 'default' + end + end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 3f11f0a4516..bc139d5ef28 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -824,6 +824,32 @@ describe Gitlab::Git::Repository, seed_helper: true do it { is_expected.to eq(17) } end + describe '#count_commits' do + context 'with after timestamp' do + it 'returns the number of commits after timestamp' do + options = { ref: 'master', limit: nil, after: Time.iso8601('2013-03-03T20:15:01+00:00') } + + expect(repository.count_commits(options)).to eq(25) + end + end + + context 'with before timestamp' do + it 'returns the number of commits after timestamp' do + options = { ref: 'feature', limit: nil, before: Time.iso8601('2015-03-03T20:15:01+00:00') } + + expect(repository.count_commits(options)).to eq(9) + end + end + + context 'with path' do + it 'returns the number of commits with path ' do + options = { ref: 'master', limit: nil, path: "encoding" } + + expect(repository.count_commits(options)).to eq(2) + end + end + end + describe "branch_names_contains" do subject { repository.branch_names_contains(SeedRepo::LastCommit::ID) } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 1a1280e5198..e47956a365f 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -136,6 +136,7 @@ project: - slack_slash_commands_service - irker_service - pivotaltracker_service +- prometheus_service - hipchat_service - flowdock_service - assembla_service diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 3bd1f335a89..c718e792461 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -21,6 +21,7 @@ Issue: - milestone_id - weight - time_estimate +- relative_position Event: - id - target_type diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb index fd3769d75b5..c2ab015d5cb 100644 --- a/spec/lib/gitlab/middleware/go_spec.rb +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -15,16 +15,93 @@ describe Gitlab::Middleware::Go, lib: true do end describe 'when go-get=1' do - it 'returns a document' do - env = { 'rack.input' => '', - 'QUERY_STRING' => 'go-get=1', - 'PATH_INFO' => '/group/project/path' } - resp = middleware.call(env) - expect(resp[0]).to eq(200) - expect(resp[1]['Content-Type']).to eq('text/html') - expected_body = "<!DOCTYPE html><html><head><meta content='#{Gitlab.config.gitlab.host}/group/project git http://#{Gitlab.config.gitlab.host}/group/project.git' name='go-import'></head></html>\n" - expect(resp[2].body).to eq([expected_body]) + let(:current_user) { nil } + + context 'with simple 2-segment project path' do + let!(:project) { create(:project, :private) } + + context 'with subpackages' do + let(:path) { "#{project.full_path}/subpackage" } + + it 'returns the full project path' do + expect_response_with_path(go, project.full_path) + end + end + + context 'without subpackages' do + let(:path) { project.full_path } + + it 'returns the full project path' do + expect_response_with_path(go, project.full_path) + end + end + end + + context 'with a nested project path' do + let(:group) { create(:group, :nested) } + let!(:project) { create(:project, :public, namespace: group) } + + shared_examples 'a nested project' do + context 'when the project is public' do + it 'returns the full project path' do + expect_response_with_path(go, project.full_path) + end + end + + context 'when the project is private' do + before do + project.update_attribute(:visibility_level, Project::PRIVATE) + end + + context 'with access to the project' do + let(:current_user) { project.creator } + + before do + project.team.add_master(current_user) + end + + it 'returns the full project path' do + expect_response_with_path(go, project.full_path) + end + end + + context 'without access to the project' do + it 'returns the 2-segment group path' do + expect_response_with_path(go, group.full_path) + end + end + end + end + + context 'with subpackages' do + let(:path) { "#{project.full_path}/subpackage" } + + it_behaves_like 'a nested project' + end + + context 'without subpackages' do + let(:path) { project.full_path } + + it_behaves_like 'a nested project' + end end end + + def go + env = { + 'rack.input' => '', + 'QUERY_STRING' => 'go-get=1', + 'PATH_INFO' => "/#{path}", + 'warden' => double(authenticate: current_user) + } + middleware.call(env) + end + + def expect_response_with_path(response, path) + expect(response[0]).to eq(200) + expect(response[1]['Content-Type']).to eq('text/html') + expected_body = "<!DOCTYPE html><html><head><meta content='#{Gitlab.config.gitlab.host}/#{path} git http://#{Gitlab.config.gitlab.host}/#{path}.git' name='go-import'></head></html>\n" + expect(response[2].body).to eq([expected_body]) + end end end diff --git a/spec/lib/gitlab/prometheus_spec.rb b/spec/lib/gitlab/prometheus_spec.rb new file mode 100644 index 00000000000..280264188e2 --- /dev/null +++ b/spec/lib/gitlab/prometheus_spec.rb @@ -0,0 +1,143 @@ +require 'spec_helper' + +describe Gitlab::Prometheus, lib: true do + include PrometheusHelpers + + subject { described_class.new(api_url: 'https://prometheus.example.com') } + + describe '#ping' do + it 'issues a "query" request to the API endpoint' do + req_stub = stub_prometheus_request(prometheus_query_url('1'), body: prometheus_value_body('vector')) + + expect(subject.ping).to eq({ "resultType" => "vector", "result" => [{ "metric" => {}, "value" => [1488772511.004, "0.000041021495238095323"] }] }) + expect(req_stub).to have_been_requested + end + end + + # This shared examples expect: + # - query_url: A query URL + # - execute_query: A query call + shared_examples 'failure response' do + context 'when request returns 400 with an error message' do + it 'raises a Gitlab::PrometheusError error' do + req_stub = stub_prometheus_request(query_url, status: 400, body: { error: 'bar!' }) + + expect { execute_query } + .to raise_error(Gitlab::PrometheusError, 'bar!') + expect(req_stub).to have_been_requested + end + end + + context 'when request returns 400 without an error message' do + it 'raises a Gitlab::PrometheusError error' do + req_stub = stub_prometheus_request(query_url, status: 400) + + expect { execute_query } + .to raise_error(Gitlab::PrometheusError, 'Bad data received') + expect(req_stub).to have_been_requested + end + end + + context 'when request returns 500' do + it 'raises a Gitlab::PrometheusError error' do + req_stub = stub_prometheus_request(query_url, status: 500, body: { message: 'FAIL!' }) + + expect { execute_query } + .to raise_error(Gitlab::PrometheusError, '500 - {"message":"FAIL!"}') + expect(req_stub).to have_been_requested + end + end + end + + describe '#query' do + let(:prometheus_query) { prometheus_cpu_query('env-slug') } + let(:query_url) { prometheus_query_url(prometheus_query) } + + context 'when request returns vector results' do + it 'returns data from the API call' do + req_stub = stub_prometheus_request(query_url, body: prometheus_value_body('vector')) + + expect(subject.query(prometheus_query)).to eq [{ "metric" => {}, "value" => [1488772511.004, "0.000041021495238095323"] }] + expect(req_stub).to have_been_requested + end + end + + context 'when request returns matrix results' do + it 'returns nil' do + req_stub = stub_prometheus_request(query_url, body: prometheus_value_body('matrix')) + + expect(subject.query(prometheus_query)).to be_nil + expect(req_stub).to have_been_requested + end + end + + context 'when request returns no data' do + it 'returns []' do + req_stub = stub_prometheus_request(query_url, body: prometheus_empty_body('vector')) + + expect(subject.query(prometheus_query)).to be_empty + expect(req_stub).to have_been_requested + end + end + + it_behaves_like 'failure response' do + let(:execute_query) { subject.query(prometheus_query) } + end + end + + describe '#query_range' do + let(:prometheus_query) { prometheus_memory_query('env-slug') } + let(:query_url) { prometheus_query_range_url(prometheus_query) } + + around do |example| + Timecop.freeze { example.run } + end + + context 'when a start time is passed' do + let(:query_url) { prometheus_query_range_url(prometheus_query, start: 2.hours.ago) } + + it 'passed it in the requested URL' do + req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('vector')) + + subject.query_range(prometheus_query, start: 2.hours.ago) + expect(req_stub).to have_been_requested + end + end + + context 'when request returns vector results' do + it 'returns nil' do + req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('vector')) + + expect(subject.query_range(prometheus_query)).to be_nil + expect(req_stub).to have_been_requested + end + end + + context 'when request returns matrix results' do + it 'returns data from the API call' do + req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('matrix')) + + expect(subject.query_range(prometheus_query)).to eq([ + { + "metric" => {}, + "values" => [[1488758662.506, "0.00002996364761904785"], [1488758722.506, "0.00003090239047619091"]] + } + ]) + expect(req_stub).to have_been_requested + end + end + + context 'when request returns no data' do + it 'returns []' do + req_stub = stub_prometheus_request(query_url, body: prometheus_empty_body('matrix')) + + expect(subject.query_range(prometheus_query)).to be_empty + expect(req_stub).to have_been_requested + end + end + + it_behaves_like 'failure response' do + let(:execute_query) { subject.query_range(prometheus_query) } + end + end +end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index a32c6131030..8e5e8288c49 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -199,4 +199,58 @@ describe Gitlab::Workhorse, lib: true do end end end + + describe '.set_key_and_notify' do + let(:key) { 'test-key' } + let(:value) { 'test-value' } + + subject { described_class.set_key_and_notify(key, value, overwrite: overwrite) } + + shared_examples 'set and notify' do + it 'set and return the same value' do + is_expected.to eq(value) + end + + it 'set and notify' do + expect_any_instance_of(Redis).to receive(:publish) + .with(described_class::NOTIFICATION_CHANNEL, "test-key=test-value") + + subject + end + end + + context 'when we set a new key' do + let(:overwrite) { true } + + it_behaves_like 'set and notify' + end + + context 'when we set an existing key' do + let(:old_value) { 'existing-key' } + + before do + described_class.set_key_and_notify(key, old_value, overwrite: true) + end + + context 'and overwrite' do + let(:overwrite) { true } + + it_behaves_like 'set and notify' + end + + context 'and do not overwrite' do + let(:overwrite) { false } + + it 'try to set but return the previous value' do + is_expected.to eq(old_value) + end + + it 'does not notify' do + expect_any_instance_of(Redis).not_to receive(:publish) + + subject + end + end + end + end end diff --git a/spec/models/chat_team_spec.rb b/spec/models/chat_team_spec.rb index 1aab161ec13..5283561a83f 100644 --- a/spec/models/chat_team_spec.rb +++ b/spec/models/chat_team_spec.rb @@ -1,9 +1,14 @@ require 'spec_helper' describe ChatTeam, type: :model do + subject { create(:chat_team) } + # Associations it { is_expected.to belong_to(:namespace) } + # Validations + it { is_expected.to validate_uniqueness_of(:namespace) } + # Fields it { is_expected.to respond_to(:name) } it { is_expected.to respond_to(:team_id) } diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 2db42a94077..fd6ea2d6722 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -345,11 +345,11 @@ describe Ci::Build, :models do describe '#expanded_environment_name' do subject { build.expanded_environment_name } - context 'when environment uses $CI_BUILD_REF_NAME' do + context 'when environment uses $CI_COMMIT_REF_NAME' do let(:build) do create(:ci_build, ref: 'master', - environment: 'review/$CI_BUILD_REF_NAME') + environment: 'review/$CI_COMMIT_REF_NAME') end it { is_expected.to eq('review/master') } @@ -915,7 +915,7 @@ describe Ci::Build, :models do end context 'referenced with a variable' do - let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") } + let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_COMMIT_REF_NAME") } it { is_expected.to eq(@environment) } end @@ -1286,23 +1286,25 @@ describe Ci::Build, :models do [ { key: 'CI', value: 'true', public: true }, { key: 'GITLAB_CI', value: 'true', public: true }, - { key: 'CI_BUILD_ID', value: build.id.to_s, public: true }, - { key: 'CI_BUILD_TOKEN', value: build.token, public: false }, - { key: 'CI_BUILD_REF', value: build.sha, public: true }, - { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true }, - { key: 'CI_BUILD_REF_NAME', value: 'master', public: true }, - { key: 'CI_BUILD_REF_SLUG', value: 'master', public: true }, - { key: 'CI_BUILD_NAME', value: 'test', public: true }, - { key: 'CI_BUILD_STAGE', value: 'test', public: true }, { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, + { key: 'CI_JOB_ID', value: build.id.to_s, public: true }, + { key: 'CI_JOB_NAME', value: 'test', public: true }, + { key: 'CI_JOB_STAGE', value: 'test', public: true }, + { key: 'CI_JOB_TOKEN', value: build.token, public: false }, + { key: 'CI_COMMIT_SHA', value: build.sha, public: true }, + { key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true }, + { key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true }, { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true }, { key: 'CI_PROJECT_NAME', value: project.path, public: true }, { key: 'CI_PROJECT_PATH', value: project.full_path, public: true }, { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, - { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true } + { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }, + { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true }, + { key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false }, + { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false }, ] end @@ -1317,7 +1319,7 @@ describe Ci::Build, :models do build.yaml_variables = [] end - it { is_expected.to eq(predefined_variables) } + it { is_expected.to include(*predefined_variables) } end context 'when build has user' do @@ -1355,7 +1357,7 @@ describe Ci::Build, :models do end let(:manual_variable) do - { key: 'CI_BUILD_MANUAL', value: 'true', public: true } + { key: 'CI_JOB_MANUAL', value: 'true', public: true } end it { is_expected.to include(manual_variable) } @@ -1363,7 +1365,7 @@ describe Ci::Build, :models do context 'when build is for tag' do let(:tag_variable) do - { key: 'CI_BUILD_TAG', value: 'master', public: true } + { key: 'CI_COMMIT_TAG', value: 'master', public: true } end before do @@ -1392,7 +1394,7 @@ describe Ci::Build, :models do { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false } end let(:predefined_trigger_variable) do - { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } + { key: 'CI_PIPELINE_TRIGGERED', value: 'true', public: true } end before do @@ -1416,7 +1418,7 @@ describe Ci::Build, :models do context 'when config is not found' do let(:config) { nil } - it { is_expected.to eq(predefined_variables) } + it { is_expected.to include(*predefined_variables) } end context 'when config does not have a questioned job' do @@ -1428,7 +1430,7 @@ describe Ci::Build, :models do }) end - it { is_expected.to eq(predefined_variables) } + it { is_expected.to include(*predefined_variables) } end context 'when config has variables' do @@ -1446,7 +1448,8 @@ describe Ci::Build, :models do [{ key: 'KEY', value: 'value', public: true }] end - it { is_expected.to eq(predefined_variables + variables) } + it { is_expected.to include(*predefined_variables) } + it { is_expected.to include(*variables) } end end end diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb index 074cf1a0bd7..1bcb673cb16 100644 --- a/spec/models/ci/trigger_spec.rb +++ b/spec/models/ci/trigger_spec.rb @@ -22,4 +22,62 @@ describe Ci::Trigger, models: true do expect(trigger.token).to eq('token') end end + + describe '#short_token' do + let(:trigger) { create(:ci_trigger, token: '12345678') } + + subject { trigger.short_token } + + it 'returns shortened token' do + is_expected.to eq('1234') + end + end + + describe '#legacy?' do + let(:trigger) { create(:ci_trigger, owner: owner, project: project) } + + subject { trigger } + + context 'when owner is blank' do + let(:owner) { nil } + + it { is_expected.to be_legacy } + end + + context 'when owner is set' do + let(:owner) { create(:user) } + + it { is_expected.not_to be_legacy } + end + end + + describe '#can_access_project?' do + let(:trigger) { create(:ci_trigger, owner: owner, project: project) } + + context 'when owner is blank' do + let(:owner) { nil } + + subject { trigger.can_access_project? } + + it { is_expected.to eq(true) } + end + + context 'when owner is set' do + let(:owner) { create(:user) } + + subject { trigger.can_access_project? } + + context 'and is member of the project' do + before do + project.team << [owner, :developer] + end + + it { is_expected.to eq(true) } + end + + context 'and is not member of the project' do + it { is_expected.to eq(false) } + end + end + end end diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb new file mode 100644 index 00000000000..69906382545 --- /dev/null +++ b/spec/models/concerns/relative_positioning_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe Issue, 'RelativePositioning' do + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let(:issue1) { create(:issue, project: project) } + let(:new_issue) { create(:issue, project: project) } + + before do + [issue, issue1].each do |issue| + issue.move_to_end && issue.save + end + end + + describe '#min_relative_position' do + it 'returns maximum position' do + expect(issue.min_relative_position).to eq issue.relative_position + end + end + + describe '#max_relative_position' do + it 'returns maximum position' do + expect(issue.max_relative_position).to eq issue1.relative_position + end + end + + describe '#prev_relative_position' do + it 'returns previous position if there is an issue above' do + expect(issue1.prev_relative_position).to eq issue.relative_position + end + + it 'returns minimum position if there is no issue above' do + expect(issue.prev_relative_position).to eq RelativePositioning::MIN_POSITION + end + end + + describe '#next_relative_position' do + it 'returns next position if there is an issue below' do + expect(issue.next_relative_position).to eq issue1.relative_position + end + + it 'returns next position if there is no issue below' do + expect(issue1.next_relative_position).to eq RelativePositioning::MAX_POSITION + end + end + + describe '#move_before' do + it 'moves issue before' do + [issue1, issue].each(&:move_to_end) + + issue.move_before(issue1) + + expect(issue.relative_position).to be < issue1.relative_position + end + end + + describe '#move_after' do + it 'moves issue after' do + [issue, issue1].each(&:move_to_end) + + issue.move_after(issue1) + + expect(issue.relative_position).to be > issue1.relative_position + end + end + + describe '#move_to_end' do + it 'moves issue to the end' do + new_issue.move_to_end + + expect(new_issue.relative_position).to be > issue1.relative_position + end + end + + describe '#move_between' do + it 'positions issue between two other' do + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to be > issue.relative_position + expect(new_issue.relative_position).to be < issue1.relative_position + end + + it 'positions issue between on top' do + new_issue.move_between(nil, issue) + + expect(new_issue.relative_position).to be < issue.relative_position + end + + it 'positions issue between to end' do + new_issue.move_between(issue1, nil) + + expect(new_issue.relative_position).to be > issue1.relative_position + end + + it 'positions issues even when after and before positions are the same' do + issue1.update relative_position: issue.relative_position + + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to be > issue.relative_position + expect(issue.relative_position).to be < issue1.relative_position + end + end +end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index dce18f008f8..b4305e92812 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -271,7 +271,11 @@ describe Environment, models: true do context 'when the environment is unavailable' do let(:project) { create(:kubernetes_project) } - before { environment.stop } + + before do + environment.stop + end + it { is_expected.to be_falsy } end end @@ -281,20 +285,85 @@ describe Environment, models: true do subject { environment.terminals } context 'when the environment has terminals' do - before { allow(environment).to receive(:has_terminals?).and_return(true) } + before do + allow(environment).to receive(:has_terminals?).and_return(true) + end it 'returns the terminals from the deployment service' do - expect(project.deployment_service). - to receive(:terminals).with(environment). - and_return(:fake_terminals) + expect(project.deployment_service) + .to receive(:terminals).with(environment) + .and_return(:fake_terminals) is_expected.to eq(:fake_terminals) end end context 'when the environment does not have terminals' do - before { allow(environment).to receive(:has_terminals?).and_return(false) } - it { is_expected.to eq(nil) } + before do + allow(environment).to receive(:has_terminals?).and_return(false) + end + + it { is_expected.to be_nil } + end + end + + describe '#has_metrics?' do + subject { environment.has_metrics? } + + context 'when the enviroment is available' do + context 'with a deployment service' do + let(:project) { create(:prometheus_project) } + + context 'and a deployment' do + let!(:deployment) { create(:deployment, environment: environment) } + it { is_expected.to be_truthy } + end + + context 'but no deployments' do + it { is_expected.to be_falsy } + end + end + + context 'without a monitoring service' do + it { is_expected.to be_falsy } + end + end + + context 'when the environment is unavailable' do + let(:project) { create(:prometheus_project) } + + before do + environment.stop + end + + it { is_expected.to be_falsy } + end + end + + describe '#metrics' do + let(:project) { create(:prometheus_project) } + subject { environment.metrics } + + context 'when the environment has metrics' do + before do + allow(environment).to receive(:has_metrics?).and_return(true) + end + + it 'returns the metrics from the deployment service' do + expect(project.monitoring_service) + .to receive(:metrics).with(environment) + .and_return(:fake_metrics) + + is_expected.to eq(:fake_metrics) + end + end + + context 'when the environment does not have metrics' do + before do + allow(environment).to receive(:has_metrics?).and_return(false) + end + + it { is_expected.to be_nil } end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 3f9c4289de9..757f3921450 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -28,6 +28,20 @@ describe Namespace, models: true do expect(nested).not_to be_valid expect(nested.errors[:parent_id].first).to eq('has too deep level of nesting') end + + describe 'reserved path validation' do + context 'nested group' do + let(:group) { build(:group, :nested, path: 'tree') } + + it { expect(group).not_to be_valid } + end + + context 'top-level group' do + let(:group) { build(:group, path: 'tree') } + + it { expect(group).to be_valid } + end + end end describe "Respond to" do @@ -151,7 +165,7 @@ describe Namespace, models: true do describe :rm_dir do let!(:project) { create(:empty_project, namespace: namespace) } - let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.full_path) } + let!(:path) { File.join(Gitlab.config.repositories.storages.default['path'], namespace.full_path) } it "removes its dirs when deleted" do namespace.destroy diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb index 46eb71cef14..823623d96fa 100644 --- a/spec/models/personal_access_token_spec.rb +++ b/spec/models/personal_access_token_spec.rb @@ -1,15 +1,61 @@ require 'spec_helper' describe PersonalAccessToken, models: true do - describe ".generate" do - it "generates a random token" do - personal_access_token = PersonalAccessToken.generate({}) - expect(personal_access_token.token).to be_present + describe '.build' do + let(:personal_access_token) { build(:personal_access_token) } + let(:invalid_personal_access_token) { build(:personal_access_token, :invalid) } + + it 'is a valid personal access token' do + expect(personal_access_token).to be_valid + end + + it 'ensures that the token is generated' do + invalid_personal_access_token.save! + + expect(invalid_personal_access_token).to be_valid + expect(invalid_personal_access_token.token).not_to be_nil end + end + + describe ".active?" do + let(:active_personal_access_token) { build(:personal_access_token) } + let(:revoked_personal_access_token) { build(:personal_access_token, :revoked) } + let(:expired_personal_access_token) { build(:personal_access_token, :expired) } + + it "returns false if the personal_access_token is revoked" do + expect(revoked_personal_access_token).not_to be_active + end + + it "returns false if the personal_access_token is expired" do + expect(expired_personal_access_token).not_to be_active + end + + it "returns true if the personal_access_token is not revoked and not expired" do + expect(active_personal_access_token).to be_active + end + end + + context "validations" do + let(:personal_access_token) { build(:personal_access_token) } + + it "requires at least one scope" do + personal_access_token.scopes = [] + + expect(personal_access_token).not_to be_valid + expect(personal_access_token.errors[:scopes].first).to eq "can't be blank" + end + + it "allows creating a token with API scopes" do + personal_access_token.scopes = [:api, :read_user] + + expect(personal_access_token).to be_valid + end + + it "rejects creating a token with non-API scopes" do + personal_access_token.scopes = [:openid, :api] - it "doesn't save the record" do - personal_access_token = PersonalAccessToken.generate({}) - expect(personal_access_token).not_to be_persisted + expect(personal_access_token).not_to be_valid + expect(personal_access_token.errors[:scopes].first).to eq "can only contain API scopes" end end end diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 585c899cdf9..bf7950ef1c9 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -74,8 +74,10 @@ describe KubernetesService, models: true, caching: true do describe '#initialize_properties' do context 'with a project' do - it 'defaults to the project name' do - expect(described_class.new(project: project).namespace).to eq(project.name) + let(:namespace_name) { "#{project.path}-#{project.id}" } + + it 'defaults to the project name with ID' do + expect(described_class.new(project: project).namespace).to eq(namespace_name) end end diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb new file mode 100644 index 00000000000..d15079b686b --- /dev/null +++ b/spec/models/project_services/prometheus_service_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe PrometheusService, models: true, caching: true do + include PrometheusHelpers + include ReactiveCachingHelpers + + let(:project) { create(:prometheus_project) } + let(:service) { project.prometheus_service } + + describe "Associations" do + it { is_expected.to belong_to :project } + end + + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:api_url) } + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:api_url) } + end + end + + describe '#test' do + let!(:req_stub) { stub_prometheus_request(prometheus_query_url('1'), body: prometheus_value_body('vector')) } + + context 'success' do + it 'reads the discovery endpoint' do + expect(service.test[:success]).to be_truthy + expect(req_stub).to have_been_requested + end + end + + context 'failure' do + let!(:req_stub) { stub_prometheus_request(prometheus_query_url('1'), status: 404) } + + it 'fails to read the discovery endpoint' do + expect(service.test[:success]).to be_falsy + expect(req_stub).to have_been_requested + end + end + end + + describe '#metrics' do + let(:environment) { build_stubbed(:environment, slug: 'env-slug') } + subject { service.metrics(environment) } + + around do |example| + Timecop.freeze { example.run } + end + + context 'with valid data' do + before do + stub_reactive_cache(service, prometheus_data, 'env-slug') + end + + it 'returns reactive data' do + is_expected.to eq(prometheus_data) + end + end + end + + describe '#calculate_reactive_cache' do + let(:environment) { build_stubbed(:environment, slug: 'env-slug') } + + around do |example| + Timecop.freeze { example.run } + end + + subject do + service.calculate_reactive_cache(environment.slug) + end + + context 'when service is inactive' do + before do + service.active = false + end + + it { is_expected.to be_nil } + end + + context 'when Prometheus responds with valid data' do + before do + stub_all_prometheus_requests(environment.slug) + end + + it { expect(subject.to_json).to eq(prometheus_data.to_json) } + end + + [404, 500].each do |status| + context "when Prometheus responds with #{status}" do + before do + stub_all_prometheus_requests(environment.slug, status: status, body: 'QUERY FAILED!') + end + + it { is_expected.to eq(success: false, result: %(#{status} - "QUERY FAILED!")) } + end + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 84bdcbe8e59..e120e21af06 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -179,7 +179,7 @@ describe Project, models: true do let(:project2) { build(:empty_project, repository_storage: 'missing') } before do - storages = { 'custom' => 'tmp/tests/custom_repositories' } + storages = { 'custom' => { 'path' => 'tmp/tests/custom_repositories' } } allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) end @@ -381,7 +381,7 @@ describe Project, models: true do before do FileUtils.mkdir('tmp/tests/custom_repositories') - storages = { 'custom' => 'tmp/tests/custom_repositories' } + storages = { 'custom' => { 'path' => 'tmp/tests/custom_repositories' } } allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) end @@ -947,8 +947,8 @@ describe Project, models: true do before do storages = { - 'default' => 'tmp/tests/repositories', - 'picked' => 'tmp/tests/repositories', + 'default' => { 'path' => 'tmp/tests/repositories' }, + 'picked' => { 'path' => 'tmp/tests/repositories' }, } allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index eb992e1354e..274e4f00a0a 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1042,7 +1042,7 @@ describe Repository, models: true do it 'expires the cache for all branches' do expect(cache).to receive(:expire). - at_least(repository.branches.length). + at_least(repository.branches.length * 2). times repository.expire_branch_cache @@ -1050,14 +1050,14 @@ describe Repository, models: true do it 'expires the cache for all branches when the root branch is given' do expect(cache).to receive(:expire). - at_least(repository.branches.length). + at_least(repository.branches.length * 2). times repository.expire_branch_cache(repository.root_ref) end it 'expires the cache for a specific branch' do - expect(cache).to receive(:expire).once + expect(cache).to receive(:expire).twice repository.expire_branch_cache('foo') end @@ -1742,6 +1742,29 @@ describe Repository, models: true do end end + describe '#commit_count_for_ref' do + let(:project) { create :empty_project } + + context 'with a non-existing repository' do + it 'returns 0' do + expect(project.repository.commit_count_for_ref('master')).to eq(0) + end + end + + context 'with empty repository' do + it 'returns 0' do + project.create_repository + expect(project.repository.commit_count_for_ref('master')).to eq(0) + end + end + + context 'when searching for the root ref' do + it 'returns the same count as #commit_count' do + expect(repository.commit_count_for_ref(repository.root_ref)).to eq(repository.commit_count) + end + end + end + describe '#cache_method_output', caching: true do context 'with a non-existing repository' do let(:value) do diff --git a/spec/policies/ci/trigger_policy_spec.rb b/spec/policies/ci/trigger_policy_spec.rb new file mode 100644 index 00000000000..63ad5eb7322 --- /dev/null +++ b/spec/policies/ci/trigger_policy_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' + +describe Ci::TriggerPolicy, :models do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:trigger) { create(:ci_trigger, project: project, owner: owner) } + + let(:policies) do + described_class.abilities(user, trigger).to_set + end + + shared_examples 'allows to admin and manage trigger' do + it 'does include ability to admin trigger' do + expect(policies).to include :admin_trigger + end + + it 'does include ability to manage trigger' do + expect(policies).to include :manage_trigger + end + end + + shared_examples 'allows to manage trigger' do + it 'does not include ability to admin trigger' do + expect(policies).not_to include :admin_trigger + end + + it 'does include ability to manage trigger' do + expect(policies).to include :manage_trigger + end + end + + shared_examples 'disallows to admin and manage trigger' do + it 'does not include ability to admin trigger' do + expect(policies).not_to include :admin_trigger + end + + it 'does not include ability to manage trigger' do + expect(policies).not_to include :manage_trigger + end + end + + describe '#rules' do + context 'when owner is undefined' do + let(:owner) { nil } + + context 'when user is master of the project' do + before do + project.team << [user, :master] + end + + it_behaves_like 'allows to admin and manage trigger' + end + + context 'when user is developer of the project' do + before do + project.team << [user, :developer] + end + + it_behaves_like 'disallows to admin and manage trigger' + end + + context 'when user is not member of the project' do + it_behaves_like 'disallows to admin and manage trigger' + end + end + + context 'when owner is an user' do + let(:owner) { user } + + context 'when user is master of the project' do + before do + project.team << [user, :master] + end + + it_behaves_like 'allows to admin and manage trigger' + end + end + + context 'when owner is another user' do + let(:owner) { create(:user) } + + context 'when user is master of the project' do + before do + project.team << [user, :master] + end + + it_behaves_like 'allows to manage trigger' + end + + context 'when user is developer of the project' do + before do + project.team << [user, :developer] + end + + it_behaves_like 'disallows to admin and manage trigger' + end + + context 'when user is not member of the project' do + it_behaves_like 'disallows to admin and manage trigger' + end + end + end +end diff --git a/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb new file mode 100644 index 00000000000..6443f86b6a1 --- /dev/null +++ b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Projects::Settings::DeployKeysPresenter do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:deploy_key) { create(:deploy_key, public: true) } + + let!(:deploy_keys_project) do + create(:deploy_keys_project, project: project, deploy_key: deploy_key) + end + + subject(:presenter) do + described_class.new(project, current_user: user) + end + + it 'inherits from Gitlab::View::Presenter::Simple' do + expect(described_class.superclass).to eq(Gitlab::View::Presenter::Simple) + end + + describe '#enabled_keys' do + it 'returns currently enabled keys' do + expect(presenter.enabled_keys).to eq [deploy_keys_project.deploy_key] + end + + it 'does not contain enabled_keys inside available_keys' do + expect(presenter.available_keys).not_to include deploy_key + end + + it 'returns the enabled_keys size' do + expect(presenter.enabled_keys_size).to eq(1) + end + + it 'returns true if there is any enabled_keys' do + expect(presenter.any_keys_enabled?).to eq(true) + end + end + + describe '#available_keys/#available_project_keys' do + let(:other_deploy_key) { create(:another_deploy_key) } + + before do + project_key = create(:deploy_keys_project, deploy_key: other_deploy_key) + project_key.project.add_developer(user) + end + + it 'returns the current available_keys' do + expect(presenter.available_keys).not_to be_empty + end + + it 'returns the current available_project_keys' do + expect(presenter.available_project_keys).not_to be_empty + end + + it 'returns false if any available_project_keys are enabled' do + expect(presenter.any_available_project_keys_enabled?).to eq(true) + end + + it 'returns the available_project_keys size' do + expect(presenter.available_project_keys_size).to eq(1) + end + + it 'shows if there is an available key' do + expect(presenter.key_available?(deploy_key)).to eq(false) + end + end +end diff --git a/spec/requests/api/api_internal_helpers_spec.rb b/spec/requests/api/api_internal_helpers_spec.rb index be4bc39ada2..f5265ea60ff 100644 --- a/spec/requests/api/api_internal_helpers_spec.rb +++ b/spec/requests/api/api_internal_helpers_spec.rb @@ -21,7 +21,7 @@ describe ::API::Helpers::InternalHelpers do # Relative and absolute storage paths, with and without trailing / ['.', './', Dir.pwd, Dir.pwd + '/'].each do |storage_path| context "storage path is #{storage_path}" do - subject { clean_project_path(project_path, [storage_path]) } + subject { clean_project_path(project_path, [{ 'path' => storage_path }]) } it { is_expected.to eq(expected) } end diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 9756991162e..f4d4a8a2cc7 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -15,7 +15,7 @@ describe API::AwardEmoji, api: true do describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do context 'on an issue' do it "returns an array of award_emoji" do - get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user) + get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user) expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -31,7 +31,7 @@ describe API::AwardEmoji, api: true do context 'on a merge request' do it "returns an array of award_emoji" do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -57,7 +57,7 @@ describe API::AwardEmoji, api: true do it 'returns a status code 404' do user1 = create(:user) - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user1) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji", user1) expect(response).to have_http_status(404) end @@ -68,7 +68,7 @@ describe API::AwardEmoji, api: true do let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } it 'returns an array of award emoji' do - get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user) + get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user) expect(response).to have_http_status(200) expect(json_response).to be_an Array @@ -79,7 +79,7 @@ describe API::AwardEmoji, api: true do describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do context 'on an issue' do it "returns the award emoji" do - get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user) + get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}", user) expect(response).to have_http_status(200) expect(json_response['name']).to eq(award_emoji.name) @@ -88,7 +88,7 @@ describe API::AwardEmoji, api: true do end it "returns a 404 error if the award is not found" do - get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user) + get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/12345", user) expect(response).to have_http_status(404) end @@ -96,7 +96,7 @@ describe API::AwardEmoji, api: true do context 'on a merge request' do it 'returns the award emoji' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user) expect(response).to have_http_status(200) expect(json_response['name']).to eq(downvote.name) @@ -123,7 +123,7 @@ describe API::AwardEmoji, api: true do it 'returns a status code 404' do user1 = create(:user) - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user1) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user1) expect(response).to have_http_status(404) end @@ -134,7 +134,7 @@ describe API::AwardEmoji, api: true do let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } it 'returns an award emoji' do - get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user) + get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}", user) expect(response).to have_http_status(200) expect(json_response).not_to be_an Array @@ -147,7 +147,7 @@ describe API::AwardEmoji, api: true do context "on an issue" do it "creates a new award emoji" do - post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish' + post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'blowfish' expect(response).to have_http_status(201) expect(json_response['name']).to eq('blowfish') @@ -155,13 +155,13 @@ describe API::AwardEmoji, api: true do end it "returns a 400 bad request error if the name is not given" do - post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user) + post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user) expect(response).to have_http_status(400) end it "returns a 401 unauthorized error if the user is not authenticated" do - post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup' + post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji"), name: 'thumbsup' expect(response).to have_http_status(401) end @@ -173,15 +173,15 @@ describe API::AwardEmoji, api: true do end it "normalizes +1 as thumbsup award" do - post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1' + post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: '+1' expect(issue.award_emoji.last.name).to eq("thumbsup") end context 'when the emoji already has been awarded' do it 'returns a 404 status code' do - post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup' - post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup' + post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'thumbsup' + post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), name: 'thumbsup' expect(response).to have_http_status(404) expect(json_response["message"]).to match("has already been taken") @@ -207,7 +207,7 @@ describe API::AwardEmoji, api: true do it 'creates a new award emoji' do expect do - post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket' end.to change { note.award_emoji.count }.from(0).to(1) expect(response).to have_http_status(201) @@ -215,21 +215,21 @@ describe API::AwardEmoji, api: true do end it "it returns 404 error when user authored note" do - post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup' expect(response).to have_http_status(404) end it "normalizes +1 as thumbsup award" do - post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: '+1' expect(note.award_emoji.last.name).to eq("thumbsup") end context 'when the emoji already has been awarded' do it 'returns a 404 status code' do - post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' - post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), name: 'rocket' expect(response).to have_http_status(404) expect(json_response["message"]).to match("has already been taken") @@ -241,14 +241,14 @@ describe API::AwardEmoji, api: true do context 'when the awardable is an Issue' do it 'deletes the award' do expect do - delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user) + delete api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}", user) expect(response).to have_http_status(204) end.to change { issue.award_emoji.count }.from(1).to(0) end it 'returns a 404 error when the award emoji can not be found' do - delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user) + delete api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/12345", user) expect(response).to have_http_status(404) end @@ -257,14 +257,14 @@ describe API::AwardEmoji, api: true do context 'when the awardable is a Merge Request' do it 'deletes the award' do expect do - delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user) + delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user) expect(response).to have_http_status(204) end.to change { merge_request.award_emoji.count }.from(1).to(0) end it 'returns a 404 error when note id not found' do - delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes/12345", user) + delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes/12345", user) expect(response).to have_http_status(404) end @@ -289,7 +289,7 @@ describe API::AwardEmoji, api: true do it 'deletes the award' do expect do - delete api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user) + delete api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}", user) expect(response).to have_http_status(204) end.to change { note.award_emoji.count }.from(1).to(0) diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 5190fcca2d1..585449e62b6 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -19,6 +19,7 @@ describe API::Commits, api: true do it "returns project commits" do commit = project.repository.commit + get api("/projects/#{project.id}/repository/commits", user) expect(response).to have_http_status(200) @@ -27,6 +28,16 @@ describe API::Commits, api: true do expect(json_response.first['committer_name']).to eq(commit.committer_name) expect(json_response.first['committer_email']).to eq(commit.committer_email) end + + it 'include correct pagination headers' do + commit_count = project.repository.count_commits(ref: 'master').to_s + + get api("/projects/#{project.id}/repository/commits", user) + + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + expect(response.headers['X-Page']).to eql('1') + end end context "unauthorized user" do @@ -39,14 +50,26 @@ describe API::Commits, api: true do context "since optional parameter" do it "returns project commits since provided parameter" do commits = project.repository.commits("master") - since = commits.second.created_at + after = commits.second.created_at - get api("/projects/#{project.id}/repository/commits?since=#{since.utc.iso8601}", user) + get api("/projects/#{project.id}/repository/commits?since=#{after.utc.iso8601}", user) expect(json_response.size).to eq 2 expect(json_response.first["id"]).to eq(commits.first.id) expect(json_response.second["id"]).to eq(commits.second.id) end + + it 'include correct pagination headers' do + commits = project.repository.commits("master") + after = commits.second.created_at + commit_count = project.repository.count_commits(ref: 'master', after: after).to_s + + get api("/projects/#{project.id}/repository/commits?since=#{after.utc.iso8601}", user) + + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + expect(response.headers['X-Page']).to eql('1') + end end context "until optional parameter" do @@ -65,6 +88,18 @@ describe API::Commits, api: true do expect(json_response.first["id"]).to eq(commits.second.id) expect(json_response.second["id"]).to eq(commits.third.id) end + + it 'include correct pagination headers' do + commits = project.repository.commits("master") + before = commits.second.created_at + commit_count = project.repository.count_commits(ref: 'master', before: before).to_s + + get api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user) + + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + expect(response.headers['X-Page']).to eql('1') + end end context "invalid xmlschema date parameters" do @@ -79,11 +114,66 @@ describe API::Commits, api: true do context "path optional parameter" do it "returns project commits matching provided path parameter" do path = 'files/ruby/popen.rb' + commit_count = project.repository.count_commits(ref: 'master', path: path).to_s get api("/projects/#{project.id}/repository/commits?path=#{path}", user) expect(json_response.size).to eq(3) expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + end + + it 'include correct pagination headers' do + path = 'files/ruby/popen.rb' + commit_count = project.repository.count_commits(ref: 'master', path: path).to_s + + get api("/projects/#{project.id}/repository/commits?path=#{path}", user) + + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + expect(response.headers['X-Page']).to eql('1') + end + end + + context 'with pagination params' do + let(:page) { 1 } + let(:per_page) { 5 } + let(:ref_name) { 'master' } + let!(:request) do + get api("/projects/#{project.id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user) + end + + it 'returns correct headers' do + commit_count = project.repository.count_commits(ref: ref_name).to_s + + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq(commit_count) + expect(response.headers['X-Page']).to eq('1') + expect(response.headers['Link']).to match(/page=1&per_page=5/) + expect(response.headers['Link']).to match(/page=2&per_page=5/) + end + + context 'viewing the first page' do + it 'returns the first 5 commits' do + commit = project.repository.commit + + expect(json_response.size).to eq(per_page) + expect(json_response.first['id']).to eq(commit.id) + expect(response.headers['X-Page']).to eq('1') + end + end + + context 'viewing the third page' do + let(:page) { 3 } + + it 'returns the third 5 commits' do + commit = project.repository.commits('HEAD', offset: (page - 1) * per_page).first + + expect(json_response.size).to eq(per_page) + expect(json_response.first['id']).to eq(commit.id) + expect(response.headers['X-Page']).to eq('3') + end end end end diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb index 2974875510a..f6fd567eca5 100644 --- a/spec/requests/api/doorkeeper_access_spec.rb +++ b/spec/requests/api/doorkeeper_access_spec.rb @@ -39,4 +39,22 @@ describe API::API, api: true do end end end + + describe "when user is blocked" do + it "returns authentication error" do + user.block + get api("/user"), access_token: token.token + + expect(response).to have_http_status(401) + end + end + + describe "when user is ldap_blocked" do + it "returns authentication error" do + user.ldap_block + get api("/user"), access_token: token.token + + expect(response).to have_http_status(401) + end + end end diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index f2fd1dfc8db..b54ee8e8b85 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -15,6 +15,8 @@ describe API::Environments, api: true do describe 'GET /projects/:id/environments' do context 'as member of the project' do it 'returns project environments' do + project_data_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace) + get api("/projects/#{project.id}/environments", user) expect(response).to have_http_status(200) @@ -23,8 +25,7 @@ describe API::Environments, api: true do expect(json_response.size).to eq(1) expect(json_response.first['name']).to eq(environment.name) expect(json_response.first['external_url']).to eq(environment.external_url) - expect(json_response.first['project']['id']).to eq(project.id) - expect(json_response.first['project']['visibility']).to be_present + expect(json_response.first['project'].keys).to contain_exactly(*project_data_keys) end end diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 91f8a35e045..a7fad7f0bdb 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -5,10 +5,9 @@ describe API::Files, api: true do let(:user) { create(:user) } let!(:project) { create(:project, :repository, namespace: user.namespace ) } let(:guest) { create(:user) { |u| project.add_guest(u) } } - let(:file_path) { 'files/ruby/popen.rb' } + let(:file_path) { "files%2Fruby%2Fpopen%2Erb" } let(:params) do { - file_path: file_path, ref: 'master' } end @@ -30,36 +29,54 @@ describe API::Files, api: true do before { project.team << [user, :developer] } - describe "GET /projects/:id/repository/files" do - let(:route) { "/projects/#{project.id}/repository/files" } + def route(file_path = nil) + "/projects/#{project.id}/repository/files/#{file_path}" + end + describe "GET /projects/:id/repository/files/:file_path" do shared_examples_for 'repository files' do - it "returns file info" do - get api(route, current_user), params + it 'returns file attributes as json' do + get api(route(file_path), current_user), params expect(response).to have_http_status(200) - expect(json_response['file_path']).to eq(file_path) + expect(json_response['file_path']).to eq(CGI.unescape(file_path)) expect(json_response['file_name']).to eq('popen.rb') expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") end - context 'when no params are given' do + it 'returns file by commit sha' do + # This file is deleted on HEAD + file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" + params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" + + get api(route(file_path), current_user), params + + expect(response).to have_http_status(200) + expect(json_response['file_name']).to eq('commit.js.coffee') + expect(Base64.decode64(json_response['content']).lines.first).to eq("class Commit\n") + end + + it 'returns raw file info' do + url = route(file_path) + "/raw" + expect(Gitlab::Workhorse).to receive(:send_git_blob) + + get api(url, current_user), params + + expect(response).to have_http_status(200) + end + + context 'when mandatory params are not given' do it_behaves_like '400 response' do - let(:request) { get api(route, current_user) } + let(:request) { get api(route("any%2Ffile"), current_user) } end end context 'when file_path does not exist' do - let(:params) do - { - file_path: 'app/models/application.rb', - ref: 'master', - } - end + let(:params) { { ref: 'master' } } it_behaves_like '404 response' do - let(:request) { get api(route, current_user), params } + let(:request) { get api(route('app%2Fmodels%2Fapplication%2Erb'), current_user), params } let(:message) { '404 File Not Found' } end end @@ -68,7 +85,7 @@ describe API::Files, api: true do include_context 'disabled repository' it_behaves_like '403 response' do - let(:request) { get api(route, current_user), params } + let(:request) { get api(route(file_path), current_user), params } end end end @@ -82,7 +99,7 @@ describe API::Files, api: true do context 'when unauthenticated', 'and project is private' do it_behaves_like '404 response' do - let(:request) { get api(route), params } + let(:request) { get api(route(file_path)), params } let(:message) { '404 Project Not Found' } end end @@ -95,33 +112,106 @@ describe API::Files, api: true do context 'when authenticated', 'as a guest' do it_behaves_like '403 response' do - let(:request) { get api(route, guest), params } + let(:request) { get api(route(file_path), guest), params } end end end - describe "POST /projects/:id/repository/files" do + describe "GET /projects/:id/repository/files/:file_path/raw" do + shared_examples_for 'repository raw files' do + it 'returns raw file info' do + url = route(file_path) + "/raw" + expect(Gitlab::Workhorse).to receive(:send_git_blob) + + get api(url, current_user), params + + expect(response).to have_http_status(200) + end + + it 'returns file by commit sha' do + # This file is deleted on HEAD + file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" + params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" + expect(Gitlab::Workhorse).to receive(:send_git_blob) + + get api(route(file_path) + "/raw", current_user), params + + expect(response).to have_http_status(200) + end + + context 'when mandatory params are not given' do + it_behaves_like '400 response' do + let(:request) { get api(route("any%2Ffile"), current_user) } + end + end + + context 'when file_path does not exist' do + let(:params) { { ref: 'master' } } + + it_behaves_like '404 response' do + let(:request) { get api(route('app%2Fmodels%2Fapplication%2Erb'), current_user), params } + let(:message) { '404 File Not Found' } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route(file_path), current_user), params } + end + end + end + + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository raw files' do + let(:project) { create(:project, :public) } + let(:current_user) { nil } + end + end + + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route(file_path)), params } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository raw files' do + let(:current_user) { user } + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route(file_path), guest), params } + end + end + end + + describe "POST /projects/:id/repository/files/:file_path" do + let!(:file_path) { "new_subfolder%2Fnewfile%2Erb" } let(:valid_params) do { - file_path: 'newfile.rb', - branch: 'master', - content: 'puts 8', - commit_message: 'Added newfile' + branch: "master", + content: "puts 8", + commit_message: "Added newfile" } end it "creates a new file in project repo" do - post api("/projects/#{project.id}/repository/files", user), valid_params + post api(route(file_path), user), valid_params expect(response).to have_http_status(201) - expect(json_response['file_path']).to eq('newfile.rb') + expect(json_response["file_path"]).to eq(CGI.unescape(file_path)) last_commit = project.repository.commit.raw expect(last_commit.author_email).to eq(user.email) expect(last_commit.author_name).to eq(user.name) end - it "returns a 400 bad request if no params given" do - post api("/projects/#{project.id}/repository/files", user) + it "returns a 400 bad request if no mandatory params given" do + post api(route("any%2Etxt"), user) expect(response).to have_http_status(400) end @@ -130,7 +220,7 @@ describe API::Files, api: true do allow_any_instance_of(Repository).to receive(:create_file). and_return(false) - post api("/projects/#{project.id}/repository/files", user), valid_params + post api(route("any%2Etxt"), user), valid_params expect(response).to have_http_status(400) end @@ -139,7 +229,7 @@ describe API::Files, api: true do it "creates a new file with the specified author" do valid_params.merge!(author_email: author_email, author_name: author_name) - post api("/projects/#{project.id}/repository/files", user), valid_params + post api(route("new_file_with_author%2Etxt"), user), valid_params expect(response).to have_http_status(201) last_commit = project.repository.commit.raw @@ -152,7 +242,7 @@ describe API::Files, api: true do let!(:project) { create(:project_empty_repo, namespace: user.namespace ) } it "creates a new file in project repo" do - post api("/projects/#{project.id}/repository/files", user), valid_params + post api(route("newfile%2Erb"), user), valid_params expect(response).to have_http_status(201) expect(json_response['file_path']).to eq('newfile.rb') @@ -166,7 +256,6 @@ describe API::Files, api: true do describe "PUT /projects/:id/repository/files" do let(:valid_params) do { - file_path: file_path, branch: 'master', content: 'puts 8', commit_message: 'Changed file' @@ -174,17 +263,17 @@ describe API::Files, api: true do end it "updates existing file in project repo" do - put api("/projects/#{project.id}/repository/files", user), valid_params + put api(route(file_path), user), valid_params expect(response).to have_http_status(200) - expect(json_response['file_path']).to eq(file_path) + expect(json_response['file_path']).to eq(CGI.unescape(file_path)) last_commit = project.repository.commit.raw expect(last_commit.author_email).to eq(user.email) expect(last_commit.author_name).to eq(user.name) end it "returns a 400 bad request if no params given" do - put api("/projects/#{project.id}/repository/files", user) + put api(route(file_path), user) expect(response).to have_http_status(400) end @@ -193,7 +282,7 @@ describe API::Files, api: true do it "updates a file with the specified author" do valid_params.merge!(author_email: author_email, author_name: author_name, content: "New content") - put api("/projects/#{project.id}/repository/files", user), valid_params + put api(route(file_path), user), valid_params expect(response).to have_http_status(200) last_commit = project.repository.commit.raw @@ -206,20 +295,19 @@ describe API::Files, api: true do describe "DELETE /projects/:id/repository/files" do let(:valid_params) do { - file_path: file_path, branch: 'master', commit_message: 'Changed file' } end it "deletes existing file in project repo" do - delete api("/projects/#{project.id}/repository/files", user), valid_params + delete api(route(file_path), user), valid_params expect(response).to have_http_status(204) end it "returns a 400 bad request if no params given" do - delete api("/projects/#{project.id}/repository/files", user) + delete api(route(file_path), user) expect(response).to have_http_status(400) end @@ -227,7 +315,7 @@ describe API::Files, api: true do it "returns a 400 if fails to create file" do allow_any_instance_of(Repository).to receive(:delete_file).and_return(false) - delete api("/projects/#{project.id}/repository/files", user), valid_params + delete api(route(file_path), user), valid_params expect(response).to have_http_status(400) end @@ -236,7 +324,7 @@ describe API::Files, api: true do it "removes a file with the specified author" do valid_params.merge!(author_email: author_email, author_name: author_name) - delete api("/projects/#{project.id}/repository/files", user), valid_params + delete api(route(file_path), user), valid_params expect(response).to have_http_status(204) end @@ -244,10 +332,9 @@ describe API::Files, api: true do end describe "POST /projects/:id/repository/files with binary file" do - let(:file_path) { 'test.bin' } + let(:file_path) { 'test%2Ebin' } let(:put_params) do { - file_path: file_path, branch: 'master', content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=', commit_message: 'Binary file with a \n should not be touched', @@ -256,21 +343,20 @@ describe API::Files, api: true do end let(:get_params) do { - file_path: file_path, ref: 'master', } end before do - post api("/projects/#{project.id}/repository/files", user), put_params + post api(route(file_path), user), put_params end it "remains unchanged" do - get api("/projects/#{project.id}/repository/files", user), get_params + get api(route(file_path), user), get_params expect(response).to have_http_status(200) - expect(json_response['file_path']).to eq(file_path) - expect(json_response['file_name']).to eq(file_path) + expect(json_response['file_path']).to eq(CGI.unescape(file_path)) + expect(json_response['file_name']).to eq(CGI.unescape(file_path)) expect(json_response['content']).to eq(put_params[:content]) end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index aca7c6a0734..2fc11a3b782 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -757,9 +757,9 @@ describe API::Issues, api: true do end end - describe "GET /projects/:id/issues/:issue_id" do + describe "GET /projects/:id/issues/:issue_iid" do it 'exposes known attributes' do - get api("/projects/#{project.id}/issues/#{issue.id}", user) + get api("/projects/#{project.id}/issues/#{issue.iid}", user) expect(response).to have_http_status(200) expect(json_response['id']).to eq(issue.id) @@ -777,8 +777,8 @@ describe API::Issues, api: true do expect(json_response['confidential']).to be_falsy end - it "returns a project issue by id" do - get api("/projects/#{project.id}/issues/#{issue.id}", user) + it "returns a project issue by internal id" do + get api("/projects/#{project.id}/issues/#{issue.iid}", user) expect(response).to have_http_status(200) expect(json_response['title']).to eq(issue.title) @@ -790,40 +790,52 @@ describe API::Issues, api: true do expect(response).to have_http_status(404) end + it "returns 404 if the issue ID is used" do + get api("/projects/#{project.id}/issues/#{issue.id}", user) + + expect(response).to have_http_status(404) + end + context 'confidential issues' do it "returns 404 for non project members" do - get api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member) + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member) + expect(response).to have_http_status(404) end it "returns 404 for project members with guest role" do - get api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest) + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest) + expect(response).to have_http_status(404) end it "returns confidential issue for project members" do - get api("/projects/#{project.id}/issues/#{confidential_issue.id}", user) + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user) + expect(response).to have_http_status(200) expect(json_response['title']).to eq(confidential_issue.title) expect(json_response['iid']).to eq(confidential_issue.iid) end it "returns confidential issue for author" do - get api("/projects/#{project.id}/issues/#{confidential_issue.id}", author) + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author) + expect(response).to have_http_status(200) expect(json_response['title']).to eq(confidential_issue.title) expect(json_response['iid']).to eq(confidential_issue.iid) end it "returns confidential issue for assignee" do - get api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee) + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", assignee) + expect(response).to have_http_status(200) expect(json_response['title']).to eq(confidential_issue.title) expect(json_response['iid']).to eq(confidential_issue.iid) end it "returns confidential issue for admin" do - get api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin) + get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin) + expect(response).to have_http_status(200) expect(json_response['title']).to eq(confidential_issue.title) expect(json_response['iid']).to eq(confidential_issue.iid) @@ -1004,23 +1016,29 @@ describe API::Issues, api: true do end end - describe "PUT /projects/:id/issues/:issue_id to update only title" do + describe "PUT /projects/:id/issues/:issue_iid to update only title" do it "updates a project issue" do - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), title: 'updated title' expect(response).to have_http_status(200) expect(json_response['title']).to eq('updated title') end - it "returns 404 error if issue id not found" do + it "returns 404 error if issue iid not found" do put api("/projects/#{project.id}/issues/44444", user), title: 'updated title' expect(response).to have_http_status(404) end - it 'allows special label names' do + it "returns 404 error if issue id is used instead of the iid" do put api("/projects/#{project.id}/issues/#{issue.id}", user), + title: 'updated title' + expect(response).to have_http_status(404) + end + + it 'allows special label names' do + put api("/projects/#{project.id}/issues/#{issue.iid}", user), title: 'updated title', labels: 'label, label?, label&foo, ?, &' @@ -1034,40 +1052,40 @@ describe API::Issues, api: true do context 'confidential issues' do it "returns 403 for non project members" do - put api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member), + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member), title: 'updated title' expect(response).to have_http_status(403) end it "returns 403 for project members with guest role" do - put api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest), + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest), title: 'updated title' expect(response).to have_http_status(403) end it "updates a confidential issue for project members" do - put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user), title: 'updated title' expect(response).to have_http_status(200) expect(json_response['title']).to eq('updated title') end it "updates a confidential issue for author" do - put api("/projects/#{project.id}/issues/#{confidential_issue.id}", author), + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author), title: 'updated title' expect(response).to have_http_status(200) expect(json_response['title']).to eq('updated title') end it "updates a confidential issue for admin" do - put api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin), + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin), title: 'updated title' expect(response).to have_http_status(200) expect(json_response['title']).to eq('updated title') end it 'sets an issue to confidential' do - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), confidential: true expect(response).to have_http_status(200) @@ -1075,7 +1093,7 @@ describe API::Issues, api: true do end it 'makes a confidential issue public' do - put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user), confidential: false expect(response).to have_http_status(200) @@ -1083,7 +1101,7 @@ describe API::Issues, api: true do end it 'does not update a confidential issue with wrong confidential flag' do - put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user), confidential: 'foo' expect(response).to have_http_status(400) @@ -1092,7 +1110,7 @@ describe API::Issues, api: true do end end - describe 'PUT /projects/:id/issues/:issue_id with spam filtering' do + describe 'PUT /projects/:id/issues/:issue_iid with spam filtering' do let(:params) do { title: 'updated title', @@ -1105,7 +1123,7 @@ describe API::Issues, api: true do allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true) allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true) - put api("/projects/#{project.id}/issues/#{issue.id}", user), params + put api("/projects/#{project.id}/issues/#{issue.iid}", user), params expect(response).to have_http_status(400) expect(json_response['message']).to eq({ "error" => "Spam detected" }) @@ -1119,12 +1137,12 @@ describe API::Issues, api: true do end end - describe 'PUT /projects/:id/issues/:issue_id to update labels' do + describe 'PUT /projects/:id/issues/:issue_iid to update labels' do let!(:label) { create(:label, title: 'dummy', project: project) } let!(:label_link) { create(:label_link, label: label, target: issue) } it 'does not update labels if not present' do - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), title: 'updated title' expect(response).to have_http_status(200) expect(json_response['labels']).to eq([label.title]) @@ -1135,7 +1153,7 @@ describe API::Issues, api: true do label.toggle_subscription(user2, project) perform_enqueued_jobs do - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), title: 'updated title', labels: label.title end @@ -1143,14 +1161,14 @@ describe API::Issues, api: true do end it 'removes all labels' do - put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' + put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: '' expect(response).to have_http_status(200) expect(json_response['labels']).to eq([]) end it 'updates labels' do - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: 'foo,bar' expect(response).to have_http_status(200) expect(json_response['labels']).to include 'foo' @@ -1158,7 +1176,7 @@ describe API::Issues, api: true do end it 'allows special label names' do - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' expect(response.status).to eq(200) expect(json_response['labels']).to include 'label:foo' @@ -1172,7 +1190,7 @@ describe API::Issues, api: true do end it 'returns 400 if title is too long' do - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), title: 'g' * 256 expect(response).to have_http_status(400) expect(json_response['message']['title']).to eq([ @@ -1181,9 +1199,9 @@ describe API::Issues, api: true do end end - describe "PUT /projects/:id/issues/:issue_id to update state and label" do + describe "PUT /projects/:id/issues/:issue_iid to update state and label" do it "updates a project issue" do - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: 'label2', state_event: "close" expect(response).to have_http_status(200) @@ -1192,7 +1210,7 @@ describe API::Issues, api: true do end it 'reopens a project isssue' do - put api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen' + put api("/projects/#{project.id}/issues/#{closed_issue.iid}", user), state_event: 'reopen' expect(response).to have_http_status(200) expect(json_response['state']).to eq 'reopened' @@ -1201,7 +1219,7 @@ describe API::Issues, api: true do context 'when an admin or owner makes the request' do it 'accepts the update date to be set' do update_time = 2.weeks.ago - put api("/projects/#{project.id}/issues/#{issue.id}", user), + put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: 'label3', state_event: 'close', updated_at: update_time expect(response).to have_http_status(200) @@ -1211,25 +1229,25 @@ describe API::Issues, api: true do end end - describe 'PUT /projects/:id/issues/:issue_id to update due date' do + describe 'PUT /projects/:id/issues/:issue_iid to update due date' do it 'creates a new project issue' do due_date = 2.weeks.from_now.strftime('%Y-%m-%d') - put api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date + put api("/projects/#{project.id}/issues/#{issue.iid}", user), due_date: due_date expect(response).to have_http_status(200) expect(json_response['due_date']).to eq(due_date) end end - describe "DELETE /projects/:id/issues/:issue_id" do + describe "DELETE /projects/:id/issues/:issue_iid" do it "rejects a non member from deleting an issue" do - delete api("/projects/#{project.id}/issues/#{issue.id}", non_member) + delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member) expect(response).to have_http_status(403) end it "rejects a developer from deleting an issue" do - delete api("/projects/#{project.id}/issues/#{issue.id}", author) + delete api("/projects/#{project.id}/issues/#{issue.iid}", author) expect(response).to have_http_status(403) end @@ -1238,7 +1256,7 @@ describe API::Issues, api: true do let(:project) { create(:empty_project, namespace: owner.namespace) } it "deletes the issue if an admin requests it" do - delete api("/projects/#{project.id}/issues/#{issue.id}", owner) + delete api("/projects/#{project.id}/issues/#{issue.iid}", owner) expect(response).to have_http_status(204) end @@ -1251,14 +1269,20 @@ describe API::Issues, api: true do expect(response).to have_http_status(404) end end + + it 'returns 404 when using the issue ID instead of IID' do + delete api("/projects/#{project.id}/issues/#{issue.id}", user) + + expect(response).to have_http_status(404) + end end - describe '/projects/:id/issues/:issue_id/move' do + describe '/projects/:id/issues/:issue_iid/move' do let!(:target_project) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace ) } let!(:target_project2) { create(:empty_project, creator_id: non_member.id, namespace: non_member.namespace ) } it 'moves an issue' do - post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), to_project_id: target_project.id expect(response).to have_http_status(201) @@ -1267,7 +1291,7 @@ describe API::Issues, api: true do context 'when source and target projects are the same' do it 'returns 400 when trying to move an issue' do - post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), to_project_id: project.id expect(response).to have_http_status(400) @@ -1277,7 +1301,7 @@ describe API::Issues, api: true do context 'when the user does not have the permission to move issues' do it 'returns 400 when trying to move an issue' do - post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), to_project_id: target_project2.id expect(response).to have_http_status(400) @@ -1286,13 +1310,23 @@ describe API::Issues, api: true do end it 'moves the issue to another namespace if I am admin' do - post api("/projects/#{project.id}/issues/#{issue.id}/move", admin), + post api("/projects/#{project.id}/issues/#{issue.iid}/move", admin), to_project_id: target_project2.id expect(response).to have_http_status(201) expect(json_response['project_id']).to eq(target_project2.id) end + context 'when using the issue ID instead of iid' do + it 'returns 404 when trying to move an issue' do + post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: target_project.id + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Issue Not Found') + end + end + context 'when issue does not exist' do it 'returns 404 when trying to move an issue' do post api("/projects/#{project.id}/issues/123/move", user), @@ -1305,7 +1339,7 @@ describe API::Issues, api: true do context 'when source project does not exist' do it 'returns 404 when trying to move an issue' do - post api("/projects/123/issues/#{issue.id}/move", user), + post api("/projects/123/issues/#{issue.iid}/move", user), to_project_id: target_project.id expect(response).to have_http_status(404) @@ -1315,7 +1349,7 @@ describe API::Issues, api: true do context 'when target project does not exist' do it 'returns 404 when trying to move an issue' do - post api("/projects/#{project.id}/issues/#{issue.id}/move", user), + post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), to_project_id: 123 expect(response).to have_http_status(404) @@ -1323,16 +1357,16 @@ describe API::Issues, api: true do end end - describe 'POST :id/issues/:issue_id/subscribe' do + describe 'POST :id/issues/:issue_iid/subscribe' do it 'subscribes to an issue' do - post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user2) + post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user2) expect(response).to have_http_status(201) expect(json_response['subscribed']).to eq(true) end it 'returns 304 if already subscribed' do - post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user) + post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user) expect(response).to have_http_status(304) end @@ -1343,8 +1377,14 @@ describe API::Issues, api: true do expect(response).to have_http_status(404) end + it 'returns 404 if the issue ID is used instead of the iid' do + post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user) + + expect(response).to have_http_status(404) + end + it 'returns 404 if the issue is confidential' do - post api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscribe", non_member) + post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/subscribe", non_member) expect(response).to have_http_status(404) end @@ -1352,14 +1392,14 @@ describe API::Issues, api: true do describe 'POST :id/issues/:issue_id/unsubscribe' do it 'unsubscribes from an issue' do - post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user) + post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user) expect(response).to have_http_status(201) expect(json_response['subscribed']).to eq(false) end it 'returns 304 if not subscribed' do - post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user2) + post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user2) expect(response).to have_http_status(304) end @@ -1370,8 +1410,14 @@ describe API::Issues, api: true do expect(response).to have_http_status(404) end + it 'returns 404 if using the issue ID instead of iid' do + post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user) + + expect(response).to have_http_status(404) + end + it 'returns 404 if the issue is confidential' do - post api("/projects/#{project.id}/issues/#{confidential_issue.id}/unsubscribe", non_member) + post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/unsubscribe", non_member) expect(response).to have_http_status(404) end diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index a4d27734cc2..9450701064b 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -51,7 +51,7 @@ describe API::Jobs, api: true do end context 'filter project with array of scope elements' do - let(:query) { { 'scope[0]' => 'pending', 'scope[1]' => 'running' } } + let(:query) { { scope: %w(pending running) } } it do expect(response).to have_http_status(200) @@ -60,7 +60,7 @@ describe API::Jobs, api: true do end context 'respond 400 when scope contains invalid state' do - let(:query) { { 'scope[0]' => 'unknown', 'scope[1]' => 'running' } } + let(:query) { { scope: %w(unknown running) } } it { expect(response).to have_http_status(400) } end @@ -75,6 +75,78 @@ describe API::Jobs, api: true do end end + describe 'GET /projects/:id/pipelines/:pipeline_id/jobs' do + let(:query) { Hash.new } + + before do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query + end + + context 'authorized user' do + it 'returns pipeline jobs' do + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + end + + it 'returns correct values' do + expect(json_response).not_to be_empty + expect(json_response.first['commit']['id']).to eq project.commit.id + end + + it 'returns pipeline data' do + json_build = json_response.first + + expect(json_build['pipeline']).not_to be_empty + expect(json_build['pipeline']['id']).to eq build.pipeline.id + expect(json_build['pipeline']['ref']).to eq build.pipeline.ref + expect(json_build['pipeline']['sha']).to eq build.pipeline.sha + expect(json_build['pipeline']['status']).to eq build.pipeline.status + end + + context 'filter jobs with one scope element' do + let(:query) { { 'scope' => 'pending' } } + + it do + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + end + end + + context 'filter jobs with array of scope elements' do + let(:query) { { scope: %w(pending running) } } + + it do + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + end + end + + context 'respond 400 when scope contains invalid state' do + let(:query) { { scope: %w(unknown running) } } + + it { expect(response).to have_http_status(400) } + end + + context 'jobs in different pipelines' do + let!(:pipeline2) { create(:ci_empty_pipeline, project: project) } + let!(:build2) { create(:ci_build, pipeline: pipeline2) } + + it 'excludes jobs from other pipelines' do + json_response.each { |job| expect(job['pipeline']['id']).to eq(pipeline.id) } + end + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'does not return jobs' do + expect(response).to have_http_status(401) + end + end + end + describe 'GET /projects/:id/jobs/:job_id' do before do get api("/projects/#{project.id}/jobs/#{build.id}", api_user) diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb index 1d02e827183..79f3151ba52 100644 --- a/spec/requests/api/merge_request_diffs_spec.rb +++ b/spec/requests/api/merge_request_diffs_spec.rb @@ -13,9 +13,9 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do project.team << [user, :master] end - describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do + describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions' do it 'returns 200 for a valid merge request' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions", user) merge_request_diff = merge_request.merge_request_diffs.first expect(response.status).to eq 200 @@ -26,16 +26,22 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) end - it 'returns a 404 when merge_request_id not found' do + it 'returns a 404 when merge_request id is used instead of the iid' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) + expect(response).to have_http_status(404) + end + + it 'returns a 404 when merge_request_iid not found' do get api("/projects/#{project.id}/merge_requests/999/versions", user) expect(response).to have_http_status(404) end end - describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do + describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions/:version_id' do + let(:merge_request_diff) { merge_request.merge_request_diffs.first } + it 'returns a 200 for a valid merge request' do - merge_request_diff = merge_request.merge_request_diffs.first - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/#{merge_request_diff.id}", user) expect(response.status).to eq 200 expect(json_response['id']).to eq(merge_request_diff.id) @@ -43,8 +49,18 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size) end - it 'returns a 404 when merge_request_id not found' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user) + it 'returns a 404 when merge_request id is used instead of the iid' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) + expect(response).to have_http_status(404) + end + + it 'returns a 404 when merge_request version_id is not found' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/999", user) + expect(response).to have_http_status(404) + end + + it 'returns a 404 when merge_request_iid is not found' do + get api("/projects/#{project.id}/merge_requests/12345/versions/#{merge_request_diff.id}", user) expect(response).to have_http_status(404) end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 1083abf2ad3..9aba1d75612 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -153,9 +153,9 @@ describe API::MergeRequests, api: true do end end - describe "GET /projects/:id/merge_requests/:merge_request_id" do + describe "GET /projects/:id/merge_requests/:merge_request_iid" do it 'exposes known attributes' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) expect(response).to have_http_status(200) expect(json_response['id']).to eq(merge_request.id) @@ -184,7 +184,7 @@ describe API::MergeRequests, api: true do end it "returns merge_request" do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) expect(response).to have_http_status(200) expect(json_response['title']).to eq(merge_request.title) expect(json_response['iid']).to eq(merge_request.iid) @@ -194,25 +194,31 @@ describe API::MergeRequests, api: true do expect(json_response['force_close_merge_request']).to be_falsy end - it "returns a 404 error if merge_request_id not found" do + it "returns a 404 error if merge_request_iid not found" do get api("/projects/#{project.id}/merge_requests/999", user) expect(response).to have_http_status(404) end + it "returns a 404 error if merge_request `id` is used instead of iid" do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) + + expect(response).to have_http_status(404) + end + context 'Work in Progress' do let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) } it "returns merge_request" do - get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.iid}", user) expect(response).to have_http_status(200) expect(json_response['work_in_progress']).to eq(true) end end end - describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do + describe 'GET /projects/:id/merge_requests/:merge_request_iid/commits' do it 'returns a 200 when merge request is valid' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits", user) commit = merge_request.commits.first expect(response.status).to eq 200 @@ -223,24 +229,36 @@ describe API::MergeRequests, api: true do expect(json_response.first['title']).to eq(commit.title) end - it 'returns a 404 when merge_request_id not found' do + it 'returns a 404 when merge_request_iid not found' do get api("/projects/#{project.id}/merge_requests/999/commits", user) expect(response).to have_http_status(404) end + + it 'returns a 404 when merge_request id is used instead of iid' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user) + + expect(response).to have_http_status(404) + end end - describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do + describe 'GET /projects/:id/merge_requests/:merge_request_iid/changes' do it 'returns the change information of the merge_request' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user) expect(response.status).to eq 200 expect(json_response['changes'].size).to eq(merge_request.diffs.size) end - it 'returns a 404 when merge_request_id not found' do + it 'returns a 404 when merge_request_iid not found' do get api("/projects/#{project.id}/merge_requests/999/changes", user) expect(response).to have_http_status(404) end + + it 'returns a 404 when merge_request id is used instead of iid' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user) + + expect(response).to have_http_status(404) + end end describe "POST /projects/:id/merge_requests" do @@ -400,7 +418,7 @@ describe API::MergeRequests, api: true do end end - describe "DELETE /projects/:id/merge_requests/:merge_request_id" do + describe "DELETE /projects/:id/merge_requests/:merge_request_iid" do context "when the user is developer" do let(:developer) { create(:user) } @@ -409,25 +427,37 @@ describe API::MergeRequests, api: true do end it "denies the deletion of the merge request" do - delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", developer) + delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", developer) expect(response).to have_http_status(403) end end context "when the user is project owner" do it "destroys the merge request owners can destroy" do - delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) + delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) expect(response).to have_http_status(204) end + + it "returns 404 for an invalid merge request IID" do + delete api("/projects/#{project.id}/merge_requests/12345", user) + + expect(response).to have_http_status(404) + end + + it "returns 404 if the merge request id is used instead of iid" do + delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) + + expect(response).to have_http_status(404) + end end end - describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do + describe "PUT /projects/:id/merge_requests/:merge_request_iid/merge" do let(:pipeline) { create(:ci_pipeline_without_jobs) } it "returns merge_request in case of success" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) expect(response).to have_http_status(200) end @@ -436,7 +466,7 @@ describe API::MergeRequests, api: true do allow_any_instance_of(MergeRequest). to receive(:can_be_merged?).and_return(false) - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) expect(response).to have_http_status(406) expect(json_response['message']).to eq('Branch cannot be merged') @@ -444,14 +474,14 @@ describe API::MergeRequests, api: true do it "returns 405 if merge_request is not open" do merge_request.close - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) expect(response).to have_http_status(405) expect(json_response['message']).to eq('405 Method Not Allowed') end it "returns 405 if merge_request is a work in progress" do merge_request.update_attribute(:title, "WIP: #{merge_request.title}") - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) expect(response).to have_http_status(405) expect(json_response['message']).to eq('405 Method Not Allowed') end @@ -459,7 +489,7 @@ describe API::MergeRequests, api: true do it 'returns 405 if the build failed for a merge request that requires success' do allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false) - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) expect(response).to have_http_status(405) expect(json_response['message']).to eq('405 Method Not Allowed') @@ -468,20 +498,20 @@ describe API::MergeRequests, api: true do it "returns 401 if user has no permissions to merge" do user2 = create(:user) project.team << [user2, :reporter] - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2) + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user2) expect(response).to have_http_status(401) expect(json_response['message']).to eq('401 Unauthorized') end it "returns 409 if the SHA parameter doesn't match" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha.reverse + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), sha: merge_request.diff_head_sha.reverse expect(response).to have_http_status(409) expect(json_response['message']).to start_with('SHA does not match HEAD of source branch') end it "succeeds if the SHA parameter matches" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), sha: merge_request.diff_head_sha expect(response).to have_http_status(200) end @@ -490,18 +520,30 @@ describe API::MergeRequests, api: true do allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline) allow(pipeline).to receive(:active?).and_return(true) - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_pipeline_succeeds: true + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), merge_when_pipeline_succeeds: true expect(response).to have_http_status(200) expect(json_response['title']).to eq('Test') expect(json_response['merge_when_pipeline_succeeds']).to eq(true) end + + it "returns 404 for an invalid merge request IID" do + put api("/projects/#{project.id}/merge_requests/12345/merge", user) + + expect(response).to have_http_status(404) + end + + it "returns 404 if the merge request id is used instead of iid" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + + expect(response).to have_http_status(404) + end end - describe "PUT /projects/:id/merge_requests/:merge_request_id" do + describe "PUT /projects/:id/merge_requests/:merge_request_iid" do context "to close a MR" do it "returns merge_request" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: "close" expect(response).to have_http_status(200) expect(json_response['state']).to eq('closed') @@ -509,38 +551,38 @@ describe API::MergeRequests, api: true do end it "updates title and returns merge_request" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title" + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), title: "New title" expect(response).to have_http_status(200) expect(json_response['title']).to eq('New title') end it "updates description and returns merge_request" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description" + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), description: "New description" expect(response).to have_http_status(200) expect(json_response['description']).to eq('New description') end it "updates milestone_id and returns merge_request" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), milestone_id: milestone.id expect(response).to have_http_status(200) expect(json_response['milestone']['id']).to eq(milestone.id) end it "returns merge_request with renamed target_branch" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki" + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), target_branch: "wiki" expect(response).to have_http_status(200) expect(json_response['target_branch']).to eq('wiki') end it "returns merge_request that removes the source branch" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), remove_source_branch: true + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), remove_source_branch: true expect(response).to have_http_status(200) expect(json_response['force_remove_source_branch']).to be_truthy end it 'allows special label names' do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), title: 'new issue', labels: 'label, label?, label&foo, ?, &' @@ -553,7 +595,7 @@ describe API::MergeRequests, api: true do end it 'does not update state when title is empty' do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: 'close', title: nil merge_request.reload expect(response).to have_http_status(400) @@ -561,19 +603,31 @@ describe API::MergeRequests, api: true do end it 'does not update state when target_branch is empty' do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: 'close', target_branch: nil merge_request.reload expect(response).to have_http_status(400) expect(merge_request.state).to eq('opened') end + + it "returns 404 for an invalid merge request IID" do + put api("/projects/#{project.id}/merge_requests/12345", user), state_event: "close" + + expect(response).to have_http_status(404) + end + + it "returns 404 if the merge request id is used instead of iid" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" + + expect(response).to have_http_status(404) + end end - describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do + describe "POST /projects/:id/merge_requests/:merge_request_iid/comments" do it "returns comment" do original_count = merge_request.notes.size - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment" + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user), note: "My comment" expect(response).to have_http_status(201) expect(json_response['note']).to eq('My comment') @@ -583,23 +637,29 @@ describe API::MergeRequests, api: true do end it "returns 400 if note is missing" do - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user) expect(response).to have_http_status(400) end - it "returns 404 if note is attached to non existent merge request" do + it "returns 404 if merge request iid is invalid" do post api("/projects/#{project.id}/merge_requests/404/comments", user), note: 'My comment' expect(response).to have_http_status(404) end + + it "returns 404 if merge request id is used instead of iid" do + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), + note: 'My comment' + expect(response).to have_http_status(404) + end end - describe "GET :id/merge_requests/:merge_request_id/comments" do + describe "GET :id/merge_requests/:merge_request_iid/comments" do let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } it "returns merge_request comments ordered by created_at" do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -610,20 +670,25 @@ describe API::MergeRequests, api: true do expect(json_response.last['note']).to eq("another comment on a MR") end - it "returns a 404 error if merge_request_id not found" do + it "returns a 404 error if merge_request_iid is invalid" do get api("/projects/#{project.id}/merge_requests/999/comments", user) expect(response).to have_http_status(404) end + + it "returns a 404 error if merge_request id is used instead of iid" do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) + expect(response).to have_http_status(404) + end end - describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do + describe 'GET :id/merge_requests/:merge_request_iid/closes_issues' do it 'returns the issue that will be closed on merge' do issue = create(:issue, project: project) mr = merge_request.tap do |mr| mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}") end - get api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user) + get api("/projects/#{project.id}/merge_requests/#{mr.iid}/closes_issues", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -633,7 +698,7 @@ describe API::MergeRequests, api: true do end it 'returns an empty array when there are no issues to be closed' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -647,7 +712,7 @@ describe API::MergeRequests, api: true do merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project) merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}") - get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user) + get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.iid}/closes_issues", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -663,22 +728,34 @@ describe API::MergeRequests, api: true do guest = create(:user) project.team << [guest, :guest] - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", guest) expect(response).to have_http_status(403) end + + it "returns 404 for an invalid merge request IID" do + get api("/projects/#{project.id}/merge_requests/12345/closes_issues", user) + + expect(response).to have_http_status(404) + end + + it "returns 404 if the merge request id is used instead of iid" do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user) + + expect(response).to have_http_status(404) + end end - describe 'POST :id/merge_requests/:merge_request_id/subscribe' do + describe 'POST :id/merge_requests/:merge_request_iid/subscribe' do it 'subscribes to a merge request' do - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", admin) + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", admin) expect(response).to have_http_status(201) expect(json_response['subscribed']).to eq(true) end it 'returns 304 if already subscribed' do - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", user) + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", user) expect(response).to have_http_status(304) end @@ -689,26 +766,32 @@ describe API::MergeRequests, api: true do expect(response).to have_http_status(404) end + it 'returns 404 if the merge request id is used instead of iid' do + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", user) + + expect(response).to have_http_status(404) + end + it 'returns 403 if user has no access to read code' do guest = create(:user) project.team << [guest, :guest] - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", guest) + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", guest) expect(response).to have_http_status(403) end end - describe 'POST :id/merge_requests/:merge_request_id/unsubscribe' do + describe 'POST :id/merge_requests/:merge_request_iid/unsubscribe' do it 'unsubscribes from a merge request' do - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", user) + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", user) expect(response).to have_http_status(201) expect(json_response['subscribed']).to eq(false) end it 'returns 304 if not subscribed' do - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", admin) + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", admin) expect(response).to have_http_status(304) end @@ -719,11 +802,17 @@ describe API::MergeRequests, api: true do expect(response).to have_http_status(404) end + it 'returns 404 if the merge request id is used instead of iid' do + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", user) + + expect(response).to have_http_status(404) + end + it 'returns 403 if user has no access to read code' do guest = create(:user) project.team << [guest, :guest] - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", guest) + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", guest) expect(response).to have_http_status(403) end diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb index 7e2cc50e591..367225df717 100644 --- a/spec/requests/api/oauth_tokens_spec.rb +++ b/spec/requests/api/oauth_tokens_spec.rb @@ -29,5 +29,27 @@ describe API::API, api: true do expect(json_response['access_token']).not_to be_nil end end + + context "when user is blocked" do + it "does not create an access token" do + user = create(:user) + user.block + + request_oauth_token(user) + + expect(response).to have_http_status(401) + end + end + + context "when user is ldap_blocked" do + it "does not create an access token" do + user = create(:user) + user.ldap_block + + request_oauth_token(user) + + expect(response).to have_http_status(401) + end + end end end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 7652606a491..4783d011d54 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -30,7 +30,7 @@ describe API::Repositories, api: true do context 'when ref does not exist' do it_behaves_like '404 response' do - let(:request) { get api("#{route}?ref_name=foo", current_user) } + let(:request) { get api("#{route}?ref=foo", current_user) } let(:message) { '404 Tree Not Found' } end end @@ -66,7 +66,7 @@ describe API::Repositories, api: true do context 'when ref does not exist' do it_behaves_like '404 response' do - let(:request) { get api("#{route}?recursive=1&ref_name=foo", current_user) } + let(:request) { get api("#{route}?recursive=1&ref=foo", current_user) } let(:message) { '404 Tree Not Found' } end end @@ -100,82 +100,70 @@ describe API::Repositories, api: true do end end - { - 'blobs/:sha' => 'blobs/master', - 'commits/:sha/blob' => 'commits/master/blob' - }.each do |desc_path, example_path| - describe "GET /projects/:id/repository/#{desc_path}" do - let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" } + describe "GET /projects/:id/repository/blobs/:sha" do + let(:route) { "/projects/#{project.id}/repository/blobs/#{sample_blob.oid}" } - shared_examples_for 'repository blob' do - it 'returns the repository blob' do - get api(route, current_user) - - expect(response).to have_http_status(200) - end - - context 'when sha does not exist' do - it_behaves_like '404 response' do - let(:request) { get api(route.sub('master', 'invalid_branch_name'), current_user) } - let(:message) { '404 Commit Not Found' } - end - end + shared_examples_for 'repository blob' do + it 'returns blob attributes as json' do + get api(route, current_user) - context 'when filepath does not exist' do - it_behaves_like '404 response' do - let(:request) { get api(route.sub('README.md', 'README.invalid'), current_user) } - let(:message) { '404 File Not Found' } - end - end + expect(response).to have_http_status(200) + expect(json_response['size']).to eq(111) + expect(json_response['encoding']).to eq("base64") + expect(Base64.decode64(json_response['content']).lines.first).to eq("class Commit\n") + expect(json_response['sha']).to eq(sample_blob.oid) + end - context 'when no filepath is given' do - it_behaves_like '400 response' do - let(:request) { get api(route.sub('?filepath=README.md', ''), current_user) } - end + context 'when sha does not exist' do + it_behaves_like '404 response' do + let(:request) { get api(route.sub(sample_blob.oid, '123456'), current_user) } + let(:message) { '404 Blob Not Found' } end + end - context 'when repository is disabled' do - include_context 'disabled repository' + context 'when repository is disabled' do + include_context 'disabled repository' - it_behaves_like '403 response' do - let(:request) { get api(route, current_user) } - end + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } end end + end - context 'when unauthenticated', 'and project is public' do - it_behaves_like 'repository blob' do - let(:project) { create(:project, :public, :repository) } - let(:current_user) { nil } - end + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository blob' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } end + end - context 'when unauthenticated', 'and project is private' do - it_behaves_like '404 response' do - let(:request) { get api(route) } - let(:message) { '404 Project Not Found' } - end + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get api(route) } + let(:message) { '404 Project Not Found' } end + end - context 'when authenticated', 'as a developer' do - it_behaves_like 'repository blob' do - let(:current_user) { user } - end + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository blob' do + let(:current_user) { user } end + end - context 'when authenticated', 'as a guest' do - it_behaves_like '403 response' do - let(:request) { get api(route, guest) } - end + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } end end end - describe "GET /projects/:id/repository/raw_blobs/:sha" do - let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" } + describe "GET /projects/:id/repository/blobs/:sha/raw" do + let(:route) { "/projects/#{project.id}/repository/blobs/#{sample_blob.oid}/raw" } shared_examples_for 'repository raw blob' do it 'returns the repository raw blob' do + expect(Gitlab::Workhorse).to receive(:send_git_blob) + get api(route, current_user) expect(response).to have_http_status(200) diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index e83202e4196..15d458e0795 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -16,6 +16,7 @@ describe API::Runner do context 'when no token is provided' do it 'returns 400 error' do post api('/runners') + expect(response).to have_http_status 400 end end @@ -23,6 +24,7 @@ describe API::Runner do context 'when invalid token is provided' do it 'returns 403 error' do post api('/runners'), token: 'invalid' + expect(response).to have_http_status 403 end end @@ -108,7 +110,7 @@ describe API::Runner do context "when info parameter '#{param}' info is present" do let(:value) { "#{param}_value" } - it %q(updates provided Runner's parameter) do + it "updates provided Runner's parameter" do post api('/runners'), token: registration_token, info: { param => value } @@ -148,4 +150,874 @@ describe API::Runner do end end end + + describe '/api/v4/jobs' do + let(:project) { create(:empty_project, shared_runners_enabled: false) } + let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') } + let(:runner) { create(:ci_runner) } + let!(:job) do + create(:ci_build, :artifacts, :extended_options, + pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, commands: "ls\ndate") + end + + before { project.runners << runner } + + describe 'POST /api/v4/jobs/request' do + let!(:last_update) {} + let!(:new_update) { } + let(:user_agent) { 'gitlab-runner 9.0.0 (9-0-stable; go1.7.4; linux/amd64)' } + + before { stub_container_registry_config(enabled: false) } + + shared_examples 'no jobs available' do + before { request_job } + + context 'when runner sends version in User-Agent' do + context 'for stable version' do + it 'gives 204 and set X-GitLab-Last-Update' do + expect(response).to have_http_status(204) + expect(response.header).to have_key('X-GitLab-Last-Update') + end + end + + context 'when last_update is up-to-date' do + let(:last_update) { runner.ensure_runner_queue_value } + + it 'gives 204 and set the same X-GitLab-Last-Update' do + expect(response).to have_http_status(204) + expect(response.header['X-GitLab-Last-Update']).to eq(last_update) + end + end + + context 'when last_update is outdated' do + let(:last_update) { runner.ensure_runner_queue_value } + let(:new_update) { runner.tick_runner_queue } + + it 'gives 204 and set a new X-GitLab-Last-Update' do + expect(response).to have_http_status(204) + expect(response.header['X-GitLab-Last-Update']).to eq(new_update) + end + end + + context 'when beta version is sent' do + let(:user_agent) { 'gitlab-runner 9.0.0~beta.167.g2b2bacc (master; go1.7.4; linux/amd64)' } + + it { expect(response).to have_http_status(204) } + end + + context 'when pre-9-0 version is sent' do + let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0 (1-6-stable; go1.6.3; linux/amd64)' } + + it { expect(response).to have_http_status(204) } + end + + context 'when pre-9-0 beta version is sent' do + let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0~beta.167.g2b2bacc (master; go1.6.3; linux/amd64)' } + + it { expect(response).to have_http_status(204) } + end + end + + context "when runner doesn't send version in User-Agent" do + let(:user_agent) { 'Go-http-client/1.1' } + + it { expect(response).to have_http_status(404) } + end + + context "when runner doesn't have a User-Agent" do + let(:user_agent) { nil } + + it { expect(response).to have_http_status(404) } + end + end + + context 'when no token is provided' do + it 'returns 400 error' do + post api('/jobs/request') + + expect(response).to have_http_status 400 + end + end + + context 'when invalid token is provided' do + it 'returns 403 error' do + post api('/jobs/request'), token: 'invalid' + + expect(response).to have_http_status 403 + end + end + + context 'when valid token is provided' do + context 'when Runner is not active' do + let(:runner) { create(:ci_runner, :inactive) } + + it 'returns 404 error' do + request_job + + expect(response).to have_http_status 404 + end + end + + context 'when jobs are finished' do + before { job.success } + + it_behaves_like 'no jobs available' + end + + context 'when other projects have pending jobs' do + before do + job.success + create(:ci_build, :pending) + end + + it_behaves_like 'no jobs available' + end + + context 'when shared runner requests job for project without shared_runners_enabled' do + let(:runner) { create(:ci_runner, :shared) } + + it_behaves_like 'no jobs available' + end + + context 'when there is a pending job' do + let(:expected_job_info) do + { 'name' => job.name, + 'stage' => job.stage, + 'project_id' => job.project.id, + 'project_name' => job.project.name } + end + + let(:expected_git_info) do + { 'repo_url' => job.repo_url, + 'ref' => job.ref, + 'sha' => job.sha, + 'before_sha' => job.before_sha, + 'ref_type' => 'branch' } + end + + let(:expected_steps) do + [{ 'name' => 'script', + 'script' => %w(ls date), + 'timeout' => job.timeout, + 'when' => 'on_success', + 'allow_failure' => false }, + { 'name' => 'after_script', + 'script' => %w(ls date), + 'timeout' => job.timeout, + 'when' => 'always', + 'allow_failure' => true }] + end + + let(:expected_variables) do + [{ 'key' => 'CI_BUILD_NAME', 'value' => 'spinach', 'public' => true }, + { 'key' => 'CI_BUILD_STAGE', 'value' => 'test', 'public' => true }, + { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true }] + end + + let(:expected_artifacts) do + [{ 'name' => 'artifacts_file', + 'untracked' => false, + 'paths' => %w(out/), + 'when' => 'always', + 'expire_in' => '7d' }] + end + + let(:expected_cache) do + [{ 'key' => 'cache_key', + 'untracked' => false, + 'paths' => ['vendor/*'] }] + end + + it 'picks a job' do + request_job info: { platform: :darwin } + + expect(response).to have_http_status(201) + expect(response.headers).not_to have_key('X-GitLab-Last-Update') + expect(runner.reload.platform).to eq('darwin') + expect(json_response['id']).to eq(job.id) + expect(json_response['token']).to eq(job.token) + expect(json_response['job_info']).to eq(expected_job_info) + expect(json_response['git_info']).to eq(expected_git_info) + expect(json_response['image']).to eq({ 'name' => 'ruby:2.1' }) + expect(json_response['services']).to eq([{ 'name' => 'postgres' }]) + expect(json_response['steps']).to eq(expected_steps) + expect(json_response['artifacts']).to eq(expected_artifacts) + expect(json_response['cache']).to eq(expected_cache) + expect(json_response['variables']).to include(*expected_variables) + end + + context 'when job is made for tag' do + let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + + it 'sets branch as ref_type' do + request_job + + expect(response).to have_http_status(201) + expect(json_response['git_info']['ref_type']).to eq('tag') + end + end + + context 'when job is made for branch' do + it 'sets tag as ref_type' do + request_job + + expect(response).to have_http_status(201) + expect(json_response['git_info']['ref_type']).to eq('branch') + end + end + + it 'updates runner info' do + expect { request_job }.to change { runner.reload.contacted_at } + end + + %w(name version revision platform architecture).each do |param| + context "when info parameter '#{param}' is present" do + let(:value) { "#{param}_value" } + + it "updates provided Runner's parameter" do + request_job info: { param => value } + + expect(response).to have_http_status(201) + expect(runner.reload.read_attribute(param.to_sym)).to eq(value) + end + end + end + + context 'when concurrently updating a job' do + before do + expect_any_instance_of(Ci::Build).to receive(:run!). + and_raise(ActiveRecord::StaleObjectError.new(nil, nil)) + end + + it 'returns a conflict' do + request_job + + expect(response).to have_http_status(409) + expect(response.headers).not_to have_key('X-GitLab-Last-Update') + end + end + + context 'when project and pipeline have multiple jobs' do + let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } + + before { job.success } + + it 'returns dependent jobs' do + request_job + + expect(response).to have_http_status(201) + expect(json_response['id']).to eq(test_job.id) + expect(json_response['dependencies'].count).to eq(1) + expect(json_response['dependencies'][0]).to include('id' => job.id, 'name' => 'spinach') + end + end + + context 'when job has no tags' do + before { job.update(tags: []) } + + context 'when runner is allowed to pick untagged jobs' do + before { runner.update_column(:run_untagged, true) } + + it 'picks job' do + request_job + + expect(response).to have_http_status 201 + end + end + + context 'when runner is not allowed to pick untagged jobs' do + before { runner.update_column(:run_untagged, false) } + + it_behaves_like 'no jobs available' + end + end + + context 'when triggered job is available' do + let(:expected_variables) do + [{ 'key' => 'CI_BUILD_NAME', 'value' => 'spinach', 'public' => true }, + { 'key' => 'CI_BUILD_STAGE', 'value' => 'test', 'public' => true }, + { 'key' => 'CI_BUILD_TRIGGERED', 'value' => 'true', 'public' => true }, + { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true }, + { 'key' => 'SECRET_KEY', 'value' => 'secret_value', 'public' => false }, + { 'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1', 'public' => false }] + end + + before do + trigger = create(:ci_trigger, project: project) + create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [job], trigger: trigger) + project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') + end + + it 'returns variables for triggers' do + request_job + + expect(response).to have_http_status(201) + expect(json_response['variables']).to include(*expected_variables) + end + end + + describe 'registry credentials support' do + let(:registry_url) { 'registry.example.com:5005' } + let(:registry_credentials) do + { 'type' => 'registry', + 'url' => registry_url, + 'username' => 'gitlab-ci-token', + 'password' => job.token } + end + + context 'when registry is enabled' do + before { stub_container_registry_config(enabled: true, host_port: registry_url) } + + it 'sends registry credentials key' do + request_job + + expect(json_response).to have_key('credentials') + expect(json_response['credentials']).to include(registry_credentials) + end + end + + context 'when registry is disabled' do + before { stub_container_registry_config(enabled: false, host_port: registry_url) } + + it 'does not send registry credentials' do + request_job + + expect(json_response).to have_key('credentials') + expect(json_response['credentials']).not_to include(registry_credentials) + end + end + end + end + + def request_job(token = runner.token, **params) + new_params = params.merge(token: token, last_update: last_update) + post api('/jobs/request'), new_params, { 'User-Agent' => user_agent } + end + end + end + + describe 'PUT /api/v4/jobs/:id' do + let(:job) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) } + + before { job.run! } + + context 'when status is given' do + it 'mark job as succeeded' do + update_job(state: 'success') + + expect(job.reload.status).to eq 'success' + end + + it 'mark job as failed' do + update_job(state: 'failed') + + expect(job.reload.status).to eq 'failed' + end + end + + context 'when tace is given' do + it 'updates a running build' do + update_job(trace: 'BUILD TRACE UPDATED') + + expect(response).to have_http_status(200) + expect(job.reload.trace).to eq 'BUILD TRACE UPDATED' + end + end + + context 'when no trace is given' do + it 'does not override trace information' do + update_job + + expect(job.reload.trace).to eq 'BUILD TRACE' + end + end + + context 'when job has been erased' do + let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) } + + it 'responds with forbidden' do + update_job + + expect(response).to have_http_status(403) + end + end + + def update_job(token = job.token, **params) + new_params = params.merge(token: token) + put api("/jobs/#{job.id}"), new_params + end + end + + describe 'PATCH /api/v4/jobs/:id/trace' do + let(:job) { create(:ci_build, :running, :trace, runner_id: runner.id, pipeline: pipeline) } + let(:headers) { { API::Helpers::Runner::JOB_TOKEN_HEADER => job.token, 'Content-Type' => 'text/plain' } } + let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) } + let(:update_interval) { 10.seconds.to_i } + + before { initial_patch_the_trace } + + context 'when request is valid' do + it 'gets correct response' do + expect(response.status).to eq 202 + expect(job.reload.trace).to eq 'BUILD TRACE appended' + expect(response.header).to have_key 'Range' + expect(response.header).to have_key 'Job-Status' + end + + context 'when job has been updated recently' do + it { expect{ patch_the_trace }.not_to change { job.updated_at }} + + it "changes the job's trace" do + patch_the_trace + + expect(job.reload.trace).to eq 'BUILD TRACE appended appended' + end + + context 'when Runner makes a force-patch' do + it { expect{ force_patch_the_trace }.not_to change { job.updated_at }} + + it "doesn't change the build.trace" do + force_patch_the_trace + + expect(job.reload.trace).to eq 'BUILD TRACE appended' + end + end + end + + context 'when job was not updated recently' do + let(:update_interval) { 15.minutes.to_i } + + it { expect { patch_the_trace }.to change { job.updated_at } } + + it 'changes the job.trace' do + patch_the_trace + + expect(job.reload.trace).to eq 'BUILD TRACE appended appended' + end + + context 'when Runner makes a force-patch' do + it { expect { force_patch_the_trace }.to change { job.updated_at } } + + it "doesn't change the job.trace" do + force_patch_the_trace + + expect(job.reload.trace).to eq 'BUILD TRACE appended' + end + end + end + + context 'when project for the build has been deleted' do + let(:job) do + create(:ci_build, :running, :trace, runner_id: runner.id, pipeline: pipeline) do |job| + job.project.update(pending_delete: true) + end + end + + it 'responds with forbidden' do + expect(response.status).to eq(403) + end + end + end + + context 'when Runner makes a force-patch' do + before do + force_patch_the_trace + end + + it 'gets correct response' do + expect(response.status).to eq 202 + expect(job.reload.trace).to eq 'BUILD TRACE appended' + expect(response.header).to have_key 'Range' + expect(response.header).to have_key 'Job-Status' + end + end + + context 'when content-range start is too big' do + let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20' }) } + + it 'gets 416 error response with range headers' do + expect(response.status).to eq 416 + expect(response.header).to have_key 'Range' + expect(response.header['Range']).to eq '0-11' + end + end + + context 'when content-range start is too small' do + let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20' }) } + + it 'gets 416 error response with range headers' do + expect(response.status).to eq 416 + expect(response.header).to have_key 'Range' + expect(response.header['Range']).to eq '0-11' + end + end + + context 'when Content-Range header is missing' do + let(:headers_with_range) { headers } + + it { expect(response.status).to eq 400 } + end + + context 'when job has been errased' do + let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) } + + it { expect(response.status).to eq 403 } + end + + def patch_the_trace(content = ' appended', request_headers = nil) + unless request_headers + offset = job.trace_length + limit = offset + content.length - 1 + request_headers = headers.merge({ 'Content-Range' => "#{offset}-#{limit}" }) + end + + Timecop.travel(job.updated_at + update_interval) do + patch api("/jobs/#{job.id}/trace"), content, request_headers + job.reload + end + end + + def initial_patch_the_trace + patch_the_trace(' appended', headers_with_range) + end + + def force_patch_the_trace + 2.times { patch_the_trace('') } + end + end + + describe 'artifacts' do + let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) } + let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } + let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } } + let(:headers_with_token) { headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.token) } + let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') } + + before { job.run! } + + describe 'POST /api/v4/jobs/:id/artifacts/authorize' do + context 'when using token as parameter' do + it 'authorizes posting artifacts to running job' do + authorize_artifacts_with_token_in_params + + expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response['TempPath']).not_to be_nil + end + + it 'fails to post too large artifact' do + stub_application_setting(max_artifacts_size: 0) + + authorize_artifacts_with_token_in_params(filesize: 100) + + expect(response).to have_http_status(413) + end + end + + context 'when using token as header' do + it 'authorizes posting artifacts to running job' do + authorize_artifacts_with_token_in_headers + + expect(response).to have_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response['TempPath']).not_to be_nil + end + + it 'fails to post too large artifact' do + stub_application_setting(max_artifacts_size: 0) + + authorize_artifacts_with_token_in_headers(filesize: 100) + + expect(response).to have_http_status(413) + end + end + + context 'when using runners token' do + it 'fails to authorize artifacts posting' do + authorize_artifacts(token: job.project.runners_token) + + expect(response).to have_http_status(403) + end + end + + it 'reject requests that did not go through gitlab-workhorse' do + headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) + + authorize_artifacts + + expect(response).to have_http_status(500) + end + + context 'authorization token is invalid' do + it 'responds with forbidden' do + authorize_artifacts(token: 'invalid', filesize: 100 ) + + expect(response).to have_http_status(403) + end + end + + def authorize_artifacts(params = {}, request_headers = headers) + post api("/jobs/#{job.id}/artifacts/authorize"), params, request_headers + end + + def authorize_artifacts_with_token_in_params(params = {}, request_headers = headers) + params = params.merge(token: job.token) + authorize_artifacts(params, request_headers) + end + + def authorize_artifacts_with_token_in_headers(params = {}, request_headers = headers_with_token) + authorize_artifacts(params, request_headers) + end + end + + describe 'POST /api/v4/jobs/:id/artifacts' do + context 'when artifacts are being stored inside of tmp path' do + before do + # by configuring this path we allow to pass temp file from any path + allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return('/') + end + + context 'when job has been erased' do + let(:job) { create(:ci_build, erased_at: Time.now) } + + before do + upload_artifacts(file_upload, headers_with_token) + end + + it 'responds with forbidden' do + upload_artifacts(file_upload, headers_with_token) + + expect(response).to have_http_status(403) + end + end + + context 'when job is running' do + shared_examples 'successful artifacts upload' do + it 'updates successfully' do + expect(response).to have_http_status(201) + end + end + + context 'when uses regular file post' do + before { upload_artifacts(file_upload, headers_with_token, false) } + + it_behaves_like 'successful artifacts upload' + end + + context 'when uses accelerated file post' do + before { upload_artifacts(file_upload, headers_with_token, true) } + + it_behaves_like 'successful artifacts upload' + end + + context 'when updates artifact' do + before do + upload_artifacts(file_upload2, headers_with_token) + upload_artifacts(file_upload, headers_with_token) + end + + it_behaves_like 'successful artifacts upload' + end + + context 'when using runners token' do + it 'responds with forbidden' do + upload_artifacts(file_upload, headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.project.runners_token)) + + expect(response).to have_http_status(403) + end + end + end + + context 'when artifacts file is too large' do + it 'fails to post too large artifact' do + stub_application_setting(max_artifacts_size: 0) + + upload_artifacts(file_upload, headers_with_token) + + expect(response).to have_http_status(413) + end + end + + context 'when artifacts post request does not contain file' do + it 'fails to post artifacts without file' do + post api("/jobs/#{job.id}/artifacts"), {}, headers_with_token + + expect(response).to have_http_status(400) + end + end + + context 'GitLab Workhorse is not configured' do + it 'fails to post artifacts without GitLab-Workhorse' do + post api("/jobs/#{job.id}/artifacts"), { token: job.token }, {} + + expect(response).to have_http_status(403) + end + end + + context 'when setting an expire date' do + let(:default_artifacts_expire_in) {} + let(:post_data) do + { 'file.path' => file_upload.path, + 'file.name' => file_upload.original_filename, + 'expire_in' => expire_in } + end + + before do + stub_application_setting(default_artifacts_expire_in: default_artifacts_expire_in) + + post(api("/jobs/#{job.id}/artifacts"), post_data, headers_with_token) + end + + context 'when an expire_in is given' do + let(:expire_in) { '7 days' } + + it 'updates when specified' do + expect(response).to have_http_status(201) + expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(7.days.from_now) + end + end + + context 'when no expire_in is given' do + let(:expire_in) { nil } + + it 'ignores if not specified' do + expect(response).to have_http_status(201) + expect(job.reload.artifacts_expire_at).to be_nil + end + + context 'with application default' do + context 'when default is 5 days' do + let(:default_artifacts_expire_in) { '5 days' } + + it 'sets to application default' do + expect(response).to have_http_status(201) + expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(5.days.from_now) + end + end + + context 'when default is 0' do + let(:default_artifacts_expire_in) { '0' } + + it 'does not set expire_in' do + expect(response).to have_http_status(201) + expect(job.reload.artifacts_expire_at).to be_nil + end + end + end + end + end + + context 'posts artifacts file and metadata file' do + let!(:artifacts) { file_upload } + let!(:metadata) { file_upload2 } + + let(:stored_artifacts_file) { job.reload.artifacts_file.file } + let(:stored_metadata_file) { job.reload.artifacts_metadata.file } + let(:stored_artifacts_size) { job.reload.artifacts_size } + + before do + post(api("/jobs/#{job.id}/artifacts"), post_data, headers_with_token) + end + + context 'when posts data accelerated by workhorse is correct' do + let(:post_data) do + { 'file.path' => artifacts.path, + 'file.name' => artifacts.original_filename, + 'metadata.path' => metadata.path, + 'metadata.name' => metadata.original_filename } + end + + it 'stores artifacts and artifacts metadata' do + expect(response).to have_http_status(201) + expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename) + expect(stored_metadata_file.original_filename).to eq(metadata.original_filename) + expect(stored_artifacts_size).to eq(71759) + end + end + + context 'when there is no artifacts file in post data' do + let(:post_data) do + { 'metadata' => metadata } + end + + it 'is expected to respond with bad request' do + expect(response).to have_http_status(400) + end + + it 'does not store metadata' do + expect(stored_metadata_file).to be_nil + end + end + end + end + + context 'when artifacts are being stored outside of tmp path' do + before do + # by configuring this path we allow to pass file from @tmpdir only + # but all temporary files are stored in system tmp directory + @tmpdir = Dir.mktmpdir + allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir) + end + + after { FileUtils.remove_entry @tmpdir } + + it' "fails to post artifacts for outside of tmp path"' do + upload_artifacts(file_upload, headers_with_token) + + expect(response).to have_http_status(400) + end + end + + def upload_artifacts(file, headers = {}, accelerated = true) + params = if accelerated + { 'file.path' => file.path, 'file.name' => file.original_filename } + else + { 'file' => file } + end + post api("/jobs/#{job.id}/artifacts"), params, headers + end + end + + describe 'GET /api/v4/jobs/:id/artifacts' do + let(:token) { job.token } + + before { download_artifact } + + context 'when job has artifacts' do + let(:job) { create(:ci_build, :artifacts) } + let(:download_headers) do + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } + end + + context 'when using job token' do + it 'download artifacts' do + expect(response).to have_http_status(200) + expect(response.headers).to include download_headers + end + end + + context 'when using runnners token' do + let(:token) { job.project.runners_token } + + it 'responds with forbidden' do + expect(response).to have_http_status(403) + end + end + end + + context 'when job does not has artifacts' do + it 'responds with not found' do + expect(response).to have_http_status(404) + end + end + + def download_artifact(params = {}, request_headers = headers) + params = params.merge(token: token) + get api("/jobs/#{job.id}/artifacts"), params, request_headers + end + end + end + end end diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb index 794e2b5c04d..28fab2011a5 100644 --- a/spec/requests/api/session_spec.rb +++ b/spec/requests/api/session_spec.rb @@ -87,5 +87,23 @@ describe API::Session, api: true do expect(response).to have_http_status(400) end end + + context "when user is blocked" do + it "returns authentication error" do + user.block + post api("/session"), email: user.username, password: user.password + + expect(response).to have_http_status(401) + end + end + + context "when user is ldap_blocked" do + it "returns authentication error" do + user.ldap_block + post api("/session"), email: user.username, password: user.password + + expect(response).to have_http_status(401) + end + end end end diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index 1e401935662..b789284fa8d 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -163,7 +163,7 @@ describe API::Todos, api: true do shared_examples 'an issuable' do |issuable_type| it 'creates a todo on an issuable' do - post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", john_doe) + post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", john_doe) expect(response.status).to eq(201) expect(json_response['project']).to be_a Hash @@ -180,7 +180,7 @@ describe API::Todos, api: true do it 'returns 304 there already exist a todo on that issuable' do create(:todo, project: project_1, author: author_1, user: john_doe, target: issuable) - post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", john_doe) + post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", john_doe) expect(response.status).to eq(304) end @@ -195,7 +195,7 @@ describe API::Todos, api: true do guest = create(:user) project_1.team << [guest, :guest] - post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", guest) + post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", guest) if issuable_type == 'merge_requests' expect(response).to have_http_status(403) diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 881c48c75e0..04e7837fd7a 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -10,6 +10,8 @@ describe API::Users, api: true do let(:omniauth_user) { create(:omniauth_user) } let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') } let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } + let(:not_existing_user_id) { (User.maximum('id') || 0 ) + 10 } + let(:not_existing_pat_id) { (PersonalAccessToken.maximum('id') || 0 ) + 10 } describe "GET /users" do context "when unauthenticated" do @@ -1155,4 +1157,187 @@ describe API::Users, api: true do expect(json_response['message']).to eq('404 User Not Found') end end + + describe 'GET /users/:user_id/impersonation_tokens' do + let!(:active_personal_access_token) { create(:personal_access_token, user: user) } + let!(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) } + let!(:expired_personal_access_token) { create(:personal_access_token, :expired, user: user) } + let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) } + let!(:revoked_impersonation_token) { create(:personal_access_token, :impersonation, :revoked, user: user) } + + it 'returns a 404 error if user not found' do + get api("/users/#{not_existing_user_id}/impersonation_tokens", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns a 403 error when authenticated as normal user' do + get api("/users/#{not_existing_user_id}/impersonation_tokens", user) + + expect(response).to have_http_status(403) + expect(json_response['message']).to eq('403 Forbidden') + end + + it 'returns an array of all impersonated tokens' do + get api("/users/#{user.id}/impersonation_tokens", admin) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(2) + end + + it 'returns an array of active impersonation tokens if state active' do + get api("/users/#{user.id}/impersonation_tokens?state=active", admin) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response).to all(include('active' => true)) + end + + it 'returns an array of inactive personal access tokens if active is set to false' do + get api("/users/#{user.id}/impersonation_tokens?state=inactive", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response).to all(include('active' => false)) + end + end + + describe 'POST /users/:user_id/impersonation_tokens' do + let(:name) { 'my new pat' } + let(:expires_at) { '2016-12-28' } + let(:scopes) { %w(api read_user) } + let(:impersonation) { true } + + it 'returns validation error if impersonation token misses some attributes' do + post api("/users/#{user.id}/impersonation_tokens", admin) + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('name is missing') + end + + it 'returns a 404 error if user not found' do + post api("/users/#{not_existing_user_id}/impersonation_tokens", admin), + name: name, + expires_at: expires_at + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns a 403 error when authenticated as normal user' do + post api("/users/#{user.id}/impersonation_tokens", user), + name: name, + expires_at: expires_at + + expect(response).to have_http_status(403) + expect(json_response['message']).to eq('403 Forbidden') + end + + it 'creates a impersonation token' do + post api("/users/#{user.id}/impersonation_tokens", admin), + name: name, + expires_at: expires_at, + scopes: scopes, + impersonation: impersonation + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq(name) + expect(json_response['scopes']).to eq(scopes) + expect(json_response['expires_at']).to eq(expires_at) + expect(json_response['id']).to be_present + expect(json_response['created_at']).to be_present + expect(json_response['active']).to be_falsey + expect(json_response['revoked']).to be_falsey + expect(json_response['token']).to be_present + expect(json_response['impersonation']).to eq(impersonation) + end + end + + describe 'GET /users/:user_id/impersonation_tokens/:impersonation_token_id' do + let!(:personal_access_token) { create(:personal_access_token, user: user) } + let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) } + + it 'returns 404 error if user not found' do + get api("/users/#{not_existing_user_id}/impersonation_tokens/1", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns a 404 error if impersonation token not found' do + get api("/users/#{user.id}/impersonation_tokens/#{not_existing_pat_id}", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Impersonation Token Not Found') + end + + it 'returns a 404 error if token is not impersonation token' do + get api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Impersonation Token Not Found') + end + + it 'returns a 403 error when authenticated as normal user' do + get api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", user) + + expect(response).to have_http_status(403) + expect(json_response['message']).to eq('403 Forbidden') + end + + it 'returns a personal access token' do + get api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin) + + expect(response).to have_http_status(200) + expect(json_response['token']).to be_present + expect(json_response['impersonation']).to be_truthy + end + end + + describe 'DELETE /users/:user_id/impersonation_tokens/:impersonation_token_id' do + let!(:personal_access_token) { create(:personal_access_token, user: user) } + let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) } + + it 'returns a 404 error if user not found' do + delete api("/users/#{not_existing_user_id}/impersonation_tokens/1", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns a 404 error if impersonation token not found' do + delete api("/users/#{user.id}/impersonation_tokens/#{not_existing_pat_id}", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Impersonation Token Not Found') + end + + it 'returns a 404 error if token is not impersonation token' do + delete api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Impersonation Token Not Found') + end + + it 'returns a 403 error when authenticated as normal user' do + delete api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", user) + + expect(response).to have_http_status(403) + expect(json_response['message']).to eq('403 Forbidden') + end + + it 'revokes a impersonation token' do + delete api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin) + + expect(response).to have_http_status(204) + expect(impersonation_token.revoked).to be_falsey + expect(impersonation_token.reload.revoked).to be_truthy + end + end end diff --git a/spec/requests/api/v3/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb index 91145c8e72c..eeb4d128c1b 100644 --- a/spec/requests/api/v3/award_emoji_spec.rb +++ b/spec/requests/api/v3/award_emoji_spec.rb @@ -13,6 +13,231 @@ describe API::V3::AwardEmoji, api: true do before { project.team << [user, :master] } + describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do + context 'on an issue' do + it "returns an array of award_emoji" do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(award_emoji.name) + end + + it "returns a 404 error when issue id not found" do + get v3_api("/projects/#{project.id}/issues/12345/award_emoji", user) + + expect(response).to have_http_status(404) + end + end + + context 'on a merge request' do + it "returns an array of award_emoji" do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(downvote.name) + end + end + + context 'on a snippet' do + let(:snippet) { create(:project_snippet, :public, project: project) } + let!(:award) { create(:award_emoji, awardable: snippet) } + + it 'returns the awarded emoji' do + get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(award.name) + end + end + + context 'when the user has no access' do + it 'returns a status code 404' do + user1 = create(:user) + + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user1) + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji' do + let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } + + it 'returns an array of award emoji' do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(rocket.name) + end + end + + describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do + context 'on an issue' do + it "returns the award emoji" do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(award_emoji.name) + expect(json_response['awardable_id']).to eq(issue.id) + expect(json_response['awardable_type']).to eq("Issue") + end + + it "returns a 404 error if the award is not found" do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user) + + expect(response).to have_http_status(404) + end + end + + context 'on a merge request' do + it 'returns the award emoji' do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(downvote.name) + expect(json_response['awardable_id']).to eq(merge_request.id) + expect(json_response['awardable_type']).to eq("MergeRequest") + end + end + + context 'on a snippet' do + let(:snippet) { create(:project_snippet, :public, project: project) } + let!(:award) { create(:award_emoji, awardable: snippet) } + + it 'returns the awarded emoji' do + get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(award.name) + expect(json_response['awardable_id']).to eq(snippet.id) + expect(json_response['awardable_type']).to eq("Snippet") + end + end + + context 'when the user has no access' do + it 'returns a status code 404' do + user1 = create(:user) + + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user1) + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji/:award_id' do + let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } + + it 'returns an award emoji' do + get v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user) + + expect(response).to have_http_status(200) + expect(json_response).not_to be_an Array + expect(json_response['name']).to eq(rocket.name) + end + end + + describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do + let(:issue2) { create(:issue, project: project, author: user) } + + context "on an issue" do + it "creates a new award emoji" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish' + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq('blowfish') + expect(json_response['user']['username']).to eq(user.username) + end + + it "returns a 400 bad request error if the name is not given" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user) + + expect(response).to have_http_status(400) + end + + it "returns a 401 unauthorized error if the user is not authenticated" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup' + + expect(response).to have_http_status(401) + end + + it "returns a 404 error if the user authored issue" do + post v3_api("/projects/#{project.id}/issues/#{issue2.id}/award_emoji", user), name: 'thumbsup' + + expect(response).to have_http_status(404) + end + + it "normalizes +1 as thumbsup award" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1' + + expect(issue.award_emoji.last.name).to eq("thumbsup") + end + + context 'when the emoji already has been awarded' do + it 'returns a 404 status code' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup' + post v3_api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup' + + expect(response).to have_http_status(404) + expect(json_response["message"]).to match("has already been taken") + end + end + end + + context 'on a snippet' do + it 'creates a new award emoji' do + snippet = create(:project_snippet, :public, project: project) + + post v3_api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user), name: 'blowfish' + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq('blowfish') + expect(json_response['user']['username']).to eq(user.username) + end + end + end + + describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do + let(:note2) { create(:note, project: project, noteable: issue, author: user) } + + it 'creates a new award emoji' do + expect do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' + end.to change { note.award_emoji.count }.from(0).to(1) + + expect(response).to have_http_status(201) + expect(json_response['user']['username']).to eq(user.username) + end + + it "it returns 404 error when user authored note" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup' + + expect(response).to have_http_status(404) + end + + it "normalizes +1 as thumbsup award" do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1' + + expect(note.award_emoji.last.name).to eq("thumbsup") + end + + context 'when the emoji already has been awarded' do + it 'returns a 404 status code' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' + post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket' + + expect(response).to have_http_status(404) + expect(json_response["message"]).to match("has already been taken") + end + end + end + describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do context 'when the awardable is an Issue' do it 'deletes the award' do diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb index 2a8105d5a2b..1941ca0d7d8 100644 --- a/spec/requests/api/v3/issues_spec.rb +++ b/spec/requests/api/v3/issues_spec.rb @@ -1288,6 +1288,6 @@ describe API::V3::Issues, api: true do describe 'time tracking endpoints' do let(:issuable) { issue } - include_examples 'time tracking endpoints', 'issue' + include_examples 'V3 time tracking endpoints', 'issue' end end diff --git a/spec/requests/api/v3/merge_request_diffs_spec.rb b/spec/requests/api/v3/merge_request_diffs_spec.rb index e1887138aab..c53800eef30 100644 --- a/spec/requests/api/v3/merge_request_diffs_spec.rb +++ b/spec/requests/api/v3/merge_request_diffs_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do +describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs', api: true do include ApiHelpers let!(:user) { create(:user) } @@ -15,7 +15,7 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do it 'returns 200 for a valid merge request' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) merge_request_diff = merge_request.merge_request_diffs.first expect(response.status).to eq 200 @@ -25,7 +25,7 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do end it 'returns a 404 when merge_request_id not found' do - get api("/projects/#{project.id}/merge_requests/999/versions", user) + get v3_api("/projects/#{project.id}/merge_requests/999/versions", user) expect(response).to have_http_status(404) end end @@ -33,7 +33,7 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do it 'returns a 200 for a valid merge request' do merge_request_diff = merge_request.merge_request_diffs.first - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) expect(response.status).to eq 200 expect(json_response['id']).to eq(merge_request_diff.id) @@ -42,7 +42,8 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs', api: true do end it 'returns a 404 when merge_request_id not found' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user) + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user) + expect(response).to have_http_status(404) end end diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index b7ed643bc21..d73e9635c9b 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -712,7 +712,7 @@ describe API::MergeRequests, api: true do describe 'Time tracking' do let(:issuable) { merge_request } - include_examples 'time tracking endpoints', 'merge_request' + include_examples 'V3 time tracking endpoints', 'merge_request' end def mr_with_later_created_and_updated_at_time diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb index c696721c1c9..fef6fb641fa 100644 --- a/spec/requests/api/v3/repositories_spec.rb +++ b/spec/requests/api/v3/repositories_spec.rb @@ -3,6 +3,8 @@ require 'mime/types' describe API::V3::Repositories, api: true do include ApiHelpers + include RepoHelpers + include WorkhorseHelpers let(:user) { create(:user) } let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } } @@ -96,6 +98,226 @@ describe API::V3::Repositories, api: true do end end + { + 'blobs/:sha' => 'blobs/master', + 'commits/:sha/blob' => 'commits/master/blob' + }.each do |desc_path, example_path| + describe "GET /projects/:id/repository/#{desc_path}" do + let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" } + shared_examples_for 'repository blob' do + it 'returns the repository blob' do + get v3_api(route, current_user) + expect(response).to have_http_status(200) + end + context 'when sha does not exist' do + it_behaves_like '404 response' do + let(:request) { get v3_api(route.sub('master', 'invalid_branch_name'), current_user) } + let(:message) { '404 Commit Not Found' } + end + end + context 'when filepath does not exist' do + it_behaves_like '404 response' do + let(:request) { get v3_api(route.sub('README.md', 'README.invalid'), current_user) } + let(:message) { '404 File Not Found' } + end + end + context 'when no filepath is given' do + it_behaves_like '400 response' do + let(:request) { get v3_api(route.sub('?filepath=README.md', ''), current_user) } + end + end + context 'when repository is disabled' do + include_context 'disabled repository' + it_behaves_like '403 response' do + let(:request) { get v3_api(route, current_user) } + end + end + end + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository blob' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get v3_api(route) } + let(:message) { '404 Project Not Found' } + end + end + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository blob' do + let(:current_user) { user } + end + end + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get v3_api(route, guest) } + end + end + end + end + describe "GET /projects/:id/repository/raw_blobs/:sha" do + let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" } + shared_examples_for 'repository raw blob' do + it 'returns the repository raw blob' do + get v3_api(route, current_user) + expect(response).to have_http_status(200) + end + context 'when sha does not exist' do + it_behaves_like '404 response' do + let(:request) { get v3_api(route.sub(sample_blob.oid, '123456'), current_user) } + let(:message) { '404 Blob Not Found' } + end + end + context 'when repository is disabled' do + include_context 'disabled repository' + it_behaves_like '403 response' do + let(:request) { get v3_api(route, current_user) } + end + end + end + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository raw blob' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get v3_api(route) } + let(:message) { '404 Project Not Found' } + end + end + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository raw blob' do + let(:current_user) { user } + end + end + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get v3_api(route, guest) } + end + end + end + describe "GET /projects/:id/repository/archive(.:format)?:sha" do + let(:route) { "/projects/#{project.id}/repository/archive" } + shared_examples_for 'repository archive' do + it 'returns the repository archive' do + get v3_api(route, current_user) + expect(response).to have_http_status(200) + repo_name = project.repository.name.gsub("\.git", "") + type, params = workhorse_send_data + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/) + end + it 'returns the repository archive archive.zip' do + get v3_api("/projects/#{project.id}/repository/archive.zip", user) + expect(response).to have_http_status(200) + repo_name = project.repository.name.gsub("\.git", "") + type, params = workhorse_send_data + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/) + end + it 'returns the repository archive archive.tar.bz2' do + get v3_api("/projects/#{project.id}/repository/archive.tar.bz2", user) + expect(response).to have_http_status(200) + repo_name = project.repository.name.gsub("\.git", "") + type, params = workhorse_send_data + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/) + end + context 'when sha does not exist' do + it_behaves_like '404 response' do + let(:request) { get v3_api("#{route}?sha=xxx", current_user) } + let(:message) { '404 File Not Found' } + end + end + end + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository archive' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get v3_api(route) } + let(:message) { '404 Project Not Found' } + end + end + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository archive' do + let(:current_user) { user } + end + end + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get v3_api(route, guest) } + end + end + end + + describe 'GET /projects/:id/repository/compare' do + let(:route) { "/projects/#{project.id}/repository/compare" } + shared_examples_for 'repository compare' do + it "compares branches" do + get v3_api(route, current_user), from: 'master', to: 'feature' + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present + end + it "compares tags" do + get v3_api(route, current_user), from: 'v1.0.0', to: 'v1.1.0' + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present + end + it "compares commits" do + get v3_api(route, current_user), from: sample_commit.id, to: sample_commit.parent_id + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_empty + expect(json_response['diffs']).to be_empty + expect(json_response['compare_same_ref']).to be_falsey + end + it "compares commits in reverse order" do + get v3_api(route, current_user), from: sample_commit.parent_id, to: sample_commit.id + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present + end + it "compares same refs" do + get v3_api(route, current_user), from: 'master', to: 'master' + expect(response).to have_http_status(200) + expect(json_response['commits']).to be_empty + expect(json_response['diffs']).to be_empty + expect(json_response['compare_same_ref']).to be_truthy + end + end + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'repository compare' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:request) { get v3_api(route) } + let(:message) { '404 Project Not Found' } + end + end + context 'when authenticated', 'as a developer' do + it_behaves_like 'repository compare' do + let(:current_user) { user } + end + end + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:request) { get v3_api(route, guest) } + end + end + end + describe 'GET /projects/:id/repository/contributors' do let(:route) { "/projects/#{project.id}/repository/contributors" } diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb index 7e8c8753d02..3a760a8f25c 100644 --- a/spec/requests/api/v3/services_spec.rb +++ b/spec/requests/api/v3/services_spec.rb @@ -6,7 +6,9 @@ describe API::V3::Services, api: true do let(:user) { create(:user) } let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } - Service.available_services_names.each do |service| + available_services = Service.available_services_names + available_services.delete('prometheus') + available_services.each do |service| describe "DELETE /projects/:id/services/#{service.dasherize}" do include_context service diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 87786e85621..006d6a6af1c 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -221,12 +221,20 @@ describe 'Git HTTP requests', lib: true do end context "when the user is blocked" do - it "responds with status 404" do + it "responds with status 401" do user.block project.team << [user, :master] download(path, env) do |response| - expect(response).to have_http_status(404) + expect(response).to have_http_status(401) + end + end + + it "responds with status 401 for unknown projects (no project existence information leak)" do + user.block + + download('doesnt/exist.git', env) do |response| + expect(response).to have_http_status(401) end end end diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb new file mode 100644 index 00000000000..5206634bca5 --- /dev/null +++ b/spec/requests/openid_connect_spec.rb @@ -0,0 +1,134 @@ +require 'spec_helper' + +describe 'OpenID Connect requests' do + include ApiHelpers + + let(:user) { create :user } + let(:access_grant) { create :oauth_access_grant, application: application, resource_owner_id: user.id } + let(:access_token) { create :oauth_access_token, application: application, resource_owner_id: user.id } + + def request_access_token + login_as user + + post '/oauth/token', + grant_type: 'authorization_code', + code: access_grant.token, + redirect_uri: application.redirect_uri, + client_id: application.uid, + client_secret: application.secret + end + + def request_user_info + get '/oauth/userinfo', nil, 'Authorization' => "Bearer #{access_token.token}" + end + + def hashed_subject + Digest::SHA256.hexdigest("#{user.id}-#{Rails.application.secrets.secret_key_base}") + end + + context 'Application without OpenID scope' do + let(:application) { create :oauth_application, scopes: 'api' } + + it 'token response does not include an ID token' do + request_access_token + + expect(json_response).to include 'access_token' + expect(json_response).not_to include 'id_token' + end + + it 'userinfo response is unauthorized' do + request_user_info + + expect(response).to have_http_status 403 + expect(response.body).to be_blank + end + end + + context 'Application with OpenID scope' do + let(:application) { create :oauth_application, scopes: 'openid' } + + it 'token response includes an ID token' do + request_access_token + + expect(json_response).to include 'id_token' + end + + context 'UserInfo payload' do + let(:user) do + create( + :user, + name: 'Alice', + username: 'alice', + emails: [private_email, public_email], + email: private_email.email, + public_email: public_email.email, + website_url: 'https://example.com', + avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png"), + ) + end + + let(:public_email) { build :email, email: 'public@example.com' } + let(:private_email) { build :email, email: 'private@example.com' } + + it 'includes all user information' do + request_user_info + + expect(json_response).to eq({ + 'sub' => hashed_subject, + 'name' => 'Alice', + 'nickname' => 'alice', + 'email' => 'public@example.com', + 'email_verified' => true, + 'website' => 'https://example.com', + 'profile' => 'http://localhost/alice', + 'picture' => "http://localhost/uploads/user/avatar/#{user.id}/dk.png", + }) + end + end + + context 'ID token payload' do + before do + request_access_token + @payload = JSON::JWT.decode(json_response['id_token'], :skip_verification) + end + + it 'includes the Gitlab root URL' do + expect(@payload['iss']).to eq Gitlab.config.gitlab.url + end + + it 'includes the hashed user ID' do + expect(@payload['sub']).to eq hashed_subject + end + + it 'includes the time of the last authentication' do + expect(@payload['auth_time']).to eq user.current_sign_in_at.to_i + end + + it 'does not include any unknown properties' do + expect(@payload.keys).to eq %w[iss sub aud exp iat auth_time] + end + end + + context 'when user is blocked' do + it 'returns authentication error' do + access_grant + user.block + + expect do + request_access_token + end.to throw_symbol :warden + end + end + + context 'when user is ldap_blocked' do + it 'returns authentication error' do + access_grant + user.ldap_block + + expect do + request_access_token + end.to throw_symbol :warden + end + end + end +end diff --git a/spec/routing/openid_connect_spec.rb b/spec/routing/openid_connect_spec.rb new file mode 100644 index 00000000000..2c3bc08f1a1 --- /dev/null +++ b/spec/routing/openid_connect_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +# oauth_discovery_keys GET /oauth/discovery/keys(.:format) doorkeeper/openid_connect/discovery#keys +# oauth_discovery_provider GET /.well-known/openid-configuration(.:format) doorkeeper/openid_connect/discovery#provider +# oauth_discovery_webfinger GET /.well-known/webfinger(.:format) doorkeeper/openid_connect/discovery#webfinger +describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do + it "to #provider" do + expect(get('/.well-known/openid-configuration')).to route_to('doorkeeper/openid_connect/discovery#provider') + end + + it "to #webfinger" do + expect(get('/.well-known/webfinger')).to route_to('doorkeeper/openid_connect/discovery#webfinger') + end + + it "to #keys" do + expect(get('/oauth/discovery/keys')).to route_to('doorkeeper/openid_connect/discovery#keys') + end +end + +# oauth_userinfo GET /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show +# POST /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show +describe Doorkeeper::OpenidConnect::UserinfoController, 'routing' do + it "to #show" do + expect(get('/oauth/userinfo')).to route_to('doorkeeper/openid_connect/userinfo#show') + end + + it "to #show" do + expect(post('/oauth/userinfo')).to route_to('doorkeeper/openid_connect/userinfo#show') + end +end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index d31f1bdfb7c..4baccacd448 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -120,7 +120,6 @@ describe 'project routing' do end end - # emojis_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/emojis(.:format) projects/autocomplete_sources#emojis # members_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/members(.:format) projects/autocomplete_sources#members # issues_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/issues(.:format) projects/autocomplete_sources#issues # merge_requests_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/merge_requests(.:format) projects/autocomplete_sources#merge_requests @@ -128,7 +127,7 @@ describe 'project routing' do # milestones_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/milestones(.:format) projects/autocomplete_sources#milestones # commands_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/commands(.:format) projects/autocomplete_sources#commands describe Projects::AutocompleteSourcesController, 'routing' do - [:emojis, :members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action| + [:members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action| it "to ##{action}" do expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq') end diff --git a/spec/rubocop/cop/migration/add_concurrent_index_spec.rb b/spec/rubocop/cop/migration/add_concurrent_index_spec.rb new file mode 100644 index 00000000000..19a5718b0b1 --- /dev/null +++ b/spec/rubocop/cop/migration/add_concurrent_index_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/migration/add_concurrent_index' + +describe RuboCop::Cop::Migration::AddConcurrentIndex do + include CopHelper + + subject(:cop) { described_class.new } + + context 'in migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + end + + it 'registers an offense when add_concurrent_index is used inside a change method' do + inspect_source(cop, 'def change; add_concurrent_index :table, :column; end') + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + end + end + + it 'registers no offense when add_concurrent_index is used inside an up method' do + inspect_source(cop, 'def up; add_concurrent_index :table, :column; end') + + expect(cop.offenses.size).to eq(0) + end + end + + context 'outside of migration' do + it 'registers no offense' do + inspect_source(cop, 'def change; add_concurrent_index :table, :column; end') + + expect(cop.offenses.size).to eq(0) + end + end +end diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index 305278843f5..01baedc4761 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -43,32 +43,6 @@ describe Boards::Issues::ListService, services: true do described_class.new(project, user, params).execute end - context 'sets default order to priority' do - it 'returns opened issues when list id is missing' do - params = { board_id: board.id } - - issues = described_class.new(project, user, params).execute - - expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] - end - - it 'returns closed issues when listing issues from Done' do - params = { board_id: board.id, id: done.id } - - issues = described_class.new(project, user, params).execute - - expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1] - end - - it 'returns opened issues that have label list applied when listing issues from a label list' do - params = { board_id: board.id, id: list1.id } - - issues = described_class.new(project, user, params).execute - - expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2] - end - end - context 'with list that does not belong to the board' do it 'raises an error' do list = create(:list) diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb index 77f75167b3d..727ea04ea5c 100644 --- a/spec/services/boards/issues/move_service_spec.rb +++ b/spec/services/boards/issues/move_service_spec.rb @@ -78,8 +78,10 @@ describe Boards::Issues::MoveService, services: true do end context 'when moving to same list' do - let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) } - let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } } + let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) } + let(:issue1) { create(:labeled_issue, project: project, labels: [bug, development]) } + let(:issue2) { create(:labeled_issue, project: project, labels: [bug, development]) } + let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } } it 'returns false' do expect(described_class.new(project, user, params).execute(issue)).to eq false @@ -90,6 +92,18 @@ describe Boards::Issues::MoveService, services: true do expect(issue.reload.labels).to contain_exactly(bug, development) end + + it 'sorts issues' do + [issue, issue1, issue2].each do |issue| + issue.move_to_end && issue.save! + end + + params.merge!(move_after_iid: issue1.iid, move_before_iid: issue2.iid) + + described_class.new(project, user, params).execute(issue) + + expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) + end end end end diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_job_service_spec.rb index cd7dd53025c..62ba0b01339 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' module Ci - describe RegisterBuildService, services: true do + describe RegisterJobService, services: true do let!(:project) { FactoryGirl.create :empty_project, shared_runners_enabled: false } let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project } let!(:pending_build) { FactoryGirl.create :ci_build, pipeline: pipeline } @@ -181,7 +181,7 @@ module Ci let!(:other_build) { create :ci_build, pipeline: pipeline } before do - allow_any_instance_of(Ci::RegisterBuildService).to receive(:builds_for_specific_runner) + allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner) .and_return([pending_build, other_build]) end @@ -193,7 +193,7 @@ module Ci context 'when single build is in queue' do before do - allow_any_instance_of(Ci::RegisterBuildService).to receive(:builds_for_specific_runner) + allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner) .and_return([pending_build]) end @@ -204,7 +204,7 @@ module Ci context 'when there is no build in queue' do before do - allow_any_instance_of(Ci::RegisterBuildService).to receive(:builds_for_specific_runner) + allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner) .and_return([]) end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 2a0f00ce937..bd71618e6f4 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -150,6 +150,13 @@ describe GitPushService, services: true do execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) end end + + context "Sends System Push data" do + it "when pushing on a branch" do + expect(SystemHookPushWorker).to receive(:perform_async).with(@push_data, :push_hooks) + execute_service(project, user, @oldrev, @newrev, @ref ) + end + end end describe "Updates git attributes" do diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index d83b09fd32c..fa472f3e2c3 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -58,6 +58,22 @@ describe Issues::UpdateService, services: true do expect(issue.due_date).to eq Date.tomorrow end + it 'sorts issues as specified by parameters' do + issue1 = create(:issue, project: project, assignee_id: user3.id) + issue2 = create(:issue, project: project, assignee_id: user3.id) + + [issue, issue1, issue2].each do |issue| + issue.move_to_end + issue.save + end + + opts[:move_between_iids] = [issue1.iid, issue2.iid] + + update_issue(opts) + + expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) + end + context 'when current user cannot admin issues in the project' do let(:guest) { create(:user) } before do diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index fb9a8462f84..a8395cb48ea 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -752,7 +752,7 @@ describe TodoService, services: true do issue = create(:issue, project: project, assignee: john_doe, author: author, description: mentions) expect(john_doe.todos_pending_count).to eq(0) - expect(john_doe).to receive(:update_todos_count_cache) + expect(john_doe).to receive(:update_todos_count_cache).and_call_original service.new_issue(issue, author) diff --git a/spec/support/api/time_tracking_shared_examples.rb b/spec/support/api/time_tracking_shared_examples.rb index 210cd5817e0..16a3cf06be7 100644 --- a/spec/support/api/time_tracking_shared_examples.rb +++ b/spec/support/api/time_tracking_shared_examples.rb @@ -7,13 +7,13 @@ shared_examples 'time tracking endpoints' do |issuable_name| describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do context 'with an unauthorized user' do - subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", non_member), duration: '1w') } + subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", non_member), duration: '1w') } it_behaves_like 'an unauthorized API user' end it "sets the time estimate for #{issuable_name}" do - post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w' + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: '1w' expect(response).to have_http_status(200) expect(json_response['human_time_estimate']).to eq('1w') @@ -21,12 +21,12 @@ shared_examples 'time tracking endpoints' do |issuable_name| describe 'updating the current estimate' do before do - post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w' + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: '1w' end context 'when duration has a bad format' do it 'does not modify the original estimate' do - post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: 'foo' + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: 'foo' expect(response).to have_http_status(400) expect(issuable.reload.human_time_estimate).to eq('1w') @@ -35,7 +35,7 @@ shared_examples 'time tracking endpoints' do |issuable_name| context 'with a valid duration' do it 'updates the estimate' do - post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '3w1h' + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), duration: '3w1h' expect(response).to have_http_status(200) expect(issuable.reload.human_time_estimate).to eq('3w 1h') @@ -46,13 +46,13 @@ shared_examples 'time tracking endpoints' do |issuable_name| describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_time_estimate" do context 'with an unauthorized user' do - subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", non_member)) } + subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_time_estimate", non_member)) } it_behaves_like 'an unauthorized API user' end it "resets the time estimate for #{issuable_name}" do - post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", user) + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_time_estimate", user) expect(response).to have_http_status(200) expect(json_response['time_estimate']).to eq(0) @@ -62,7 +62,7 @@ shared_examples 'time tracking endpoints' do |issuable_name| describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/add_spent_time" do context 'with an unauthorized user' do subject do - post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", non_member), + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", non_member), duration: '2h' end @@ -70,7 +70,7 @@ shared_examples 'time tracking endpoints' do |issuable_name| end it "add spent time for #{issuable_name}" do - post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), duration: '2h' expect(response).to have_http_status(201) @@ -81,7 +81,7 @@ shared_examples 'time tracking endpoints' do |issuable_name| it 'subtracts time of the total spent time' do issuable.update_attributes!(spend_time: { duration: 7200, user: user }) - post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), duration: '-1h' expect(response).to have_http_status(201) @@ -93,7 +93,7 @@ shared_examples 'time tracking endpoints' do |issuable_name| it 'does not modify the total time spent' do issuable.update_attributes!(spend_time: { duration: 7200, user: user }) - post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), duration: '-1w' expect(response).to have_http_status(400) @@ -104,13 +104,13 @@ shared_examples 'time tracking endpoints' do |issuable_name| describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do context 'with an unauthorized user' do - subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", non_member)) } + subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", non_member)) } it_behaves_like 'an unauthorized API user' end it "resets spent time for #{issuable_name}" do - post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", user) + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", user) expect(response).to have_http_status(200) expect(json_response['total_time_spent']).to eq(0) @@ -122,7 +122,7 @@ shared_examples 'time tracking endpoints' do |issuable_name| issuable.update_attributes!(spend_time: { duration: 1800, user: user }, time_estimate: 3600) - get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user) + get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_stats", user) expect(response).to have_http_status(200) expect(json_response['total_time_spent']).to eq(1800) diff --git a/spec/support/api/v3/time_tracking_shared_examples.rb b/spec/support/api/v3/time_tracking_shared_examples.rb new file mode 100644 index 00000000000..f982b10d999 --- /dev/null +++ b/spec/support/api/v3/time_tracking_shared_examples.rb @@ -0,0 +1,128 @@ +shared_examples 'V3 time tracking endpoints' do |issuable_name| + issuable_collection_name = issuable_name.pluralize + + describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do + context 'with an unauthorized user' do + subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", non_member), duration: '1w') } + + it_behaves_like 'an unauthorized API user' + end + + it "sets the time estimate for #{issuable_name}" do + post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w' + + expect(response).to have_http_status(200) + expect(json_response['human_time_estimate']).to eq('1w') + end + + describe 'updating the current estimate' do + before do + post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w' + end + + context 'when duration has a bad format' do + it 'does not modify the original estimate' do + post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: 'foo' + + expect(response).to have_http_status(400) + expect(issuable.reload.human_time_estimate).to eq('1w') + end + end + + context 'with a valid duration' do + it 'updates the estimate' do + post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '3w1h' + + expect(response).to have_http_status(200) + expect(issuable.reload.human_time_estimate).to eq('3w 1h') + end + end + end + end + + describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_time_estimate" do + context 'with an unauthorized user' do + subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", non_member)) } + + it_behaves_like 'an unauthorized API user' + end + + it "resets the time estimate for #{issuable_name}" do + post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", user) + + expect(response).to have_http_status(200) + expect(json_response['time_estimate']).to eq(0) + end + end + + describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/add_spent_time" do + context 'with an unauthorized user' do + subject do + post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", non_member), + duration: '2h' + end + + it_behaves_like 'an unauthorized API user' + end + + it "add spent time for #{issuable_name}" do + post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), + duration: '2h' + + expect(response).to have_http_status(201) + expect(json_response['human_total_time_spent']).to eq('2h') + end + + context 'when subtracting time' do + it 'subtracts time of the total spent time' do + issuable.update_attributes!(spend_time: { duration: 7200, user: user }) + + post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), + duration: '-1h' + + expect(response).to have_http_status(201) + expect(json_response['total_time_spent']).to eq(3600) + end + end + + context 'when time to subtract is greater than the total spent time' do + it 'does not modify the total time spent' do + issuable.update_attributes!(spend_time: { duration: 7200, user: user }) + + post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), + duration: '-1w' + + expect(response).to have_http_status(400) + expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/) + end + end + end + + describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do + context 'with an unauthorized user' do + subject { post(v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", non_member)) } + + it_behaves_like 'an unauthorized API user' + end + + it "resets spent time for #{issuable_name}" do + post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", user) + + expect(response).to have_http_status(200) + expect(json_response['total_time_spent']).to eq(0) + end + end + + describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do + it "returns the time stats for #{issuable_name}" do + issuable.update_attributes!(spend_time: { duration: 1800, user: user }, + time_estimate: 3600) + + get v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user) + + expect(response).to have_http_status(200) + expect(json_response['total_time_spent']).to eq(1800) + expect(json_response['time_estimate']).to eq(3600) + end + end +end diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb index 58f6636e680..f21b85ec10b 100644 --- a/spec/support/filtered_search_helpers.rb +++ b/spec/support/filtered_search_helpers.rb @@ -3,16 +3,20 @@ module FilteredSearchHelpers page.find('.filtered-search') end + # Enables input to be set (similar to copy and paste) def input_filtered_search(search_term, submit: true) - filtered_search.set(search_term) + # Add an extra space to engage visual tokens + filtered_search.set("#{search_term} ") if submit filtered_search.send_keys(:enter) end end + # Enables input to be added character by character def input_filtered_search_keys(search_term) - filtered_search.send_keys(search_term) + # Add an extra space to engage visual tokens + filtered_search.send_keys("#{search_term} ") filtered_search.send_keys(:enter) end @@ -34,4 +38,32 @@ module FilteredSearchHelpers # This ensures the dropdown is shown expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading') end + + def expect_filtered_search_input_empty + expect(find('.filtered-search').value).to eq('') + end + + # Iterates through each visual token inside + # .tokens-container to make sure the correct names and values are rendered + def expect_tokens(tokens) + page.find '.filtered-search-input-container .tokens-container' do + page.all(:css, '.tokens-container li').each_with_index do |el, index| + token_name = tokens[index][:name] + token_value = tokens[index][:value] + + expect(el.find('.name')).to have_content(token_name) + if token_value + expect(el.find('.value')).to have_content(token_value) + end + end + end + end + + def default_placeholder + 'Search or filter results...' + end + + def get_filtered_search_placeholder + find('.filtered-search')['placeholder'] + end end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index 97b8b342eb2..bbbbaf4c5e8 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -26,10 +26,11 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('img.emoji', count: 10) + expect(actual).to have_selector('gl-emoji', count: 10) - image = actual.at_css('img.emoji') - expect(image['src'].to_s).to start_with(Gitlab.config.gitlab.url + '/assets') + emoji_element = actual.at_css('gl-emoji') + expect(emoji_element['data-name'].to_s).not_to be_empty + expect(emoji_element['data-unicode-version'].to_s).not_to be_empty end end diff --git a/spec/support/prometheus_helpers.rb b/spec/support/prometheus_helpers.rb new file mode 100644 index 00000000000..a52d8f37d14 --- /dev/null +++ b/spec/support/prometheus_helpers.rb @@ -0,0 +1,117 @@ +module PrometheusHelpers + def prometheus_memory_query(environment_slug) + %{sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})/1024/1024} + end + + def prometheus_cpu_query(environment_slug) + %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m]))} + end + + def prometheus_query_url(prometheus_query) + query = { query: prometheus_query }.to_query + + "https://prometheus.example.com/api/v1/query?#{query}" + end + + def prometheus_query_range_url(prometheus_query, start: 8.hours.ago) + query = { + query: prometheus_query, + start: start.to_f, + end: Time.now.utc.to_f, + step: 1.minute.to_i + }.to_query + + "https://prometheus.example.com/api/v1/query_range?#{query}" + end + + def stub_prometheus_request(url, body: {}, status: 200) + WebMock.stub_request(:get, url) + .to_return({ + status: status, + headers: { 'Content-Type' => 'application/json' }, + body: body.to_json + }) + end + + def stub_all_prometheus_requests(environment_slug, body: nil, status: 200) + stub_prometheus_request( + prometheus_query_url(prometheus_memory_query(environment_slug)), + status: status, + body: body || prometheus_value_body + ) + stub_prometheus_request( + prometheus_query_range_url(prometheus_memory_query(environment_slug)), + status: status, + body: body || prometheus_values_body + ) + stub_prometheus_request( + prometheus_query_url(prometheus_cpu_query(environment_slug)), + status: status, + body: body || prometheus_value_body + ) + stub_prometheus_request( + prometheus_query_range_url(prometheus_cpu_query(environment_slug)), + status: status, + body: body || prometheus_values_body + ) + end + + def prometheus_data(last_update: Time.now.utc) + { + success: true, + metrics: { + memory_values: prometheus_values_body('matrix').dig(:data, :result), + memory_current: prometheus_value_body('vector').dig(:data, :result), + cpu_values: prometheus_values_body('matrix').dig(:data, :result), + cpu_current: prometheus_value_body('vector').dig(:data, :result) + }, + last_update: last_update + } + end + + def prometheus_empty_body(type) + { + "status": "success", + "data": { + "resultType": type, + "result": [] + } + } + end + + def prometheus_value_body(type = 'vector') + { + "status": "success", + "data": { + "resultType": type, + "result": [ + { + "metric": {}, + "value": [ + 1488772511.004, + "0.000041021495238095323" + ] + } + ] + } + } + end + + def prometheus_values_body(type = 'matrix') + { + "status": "success", + "data": { + "resultType": type, + "result": [ + { + "metric": {}, + "values": [ + [1488758662.506, "0.00002996364761904785"], + [1488758722.506, "0.00003090239047619091"] + ] + } + ] + } + } + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index c3aa3ef44c2..f1d226b6ae3 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -143,7 +143,7 @@ module TestEnv end def repos_path - Gitlab.config.repositories.storages.default + Gitlab.config.repositories.storages.default['path'] end def backup_path diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index dfbfbd05f43..10458966cb9 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -227,8 +227,8 @@ describe 'gitlab:app namespace rake task' do FileUtils.mkdir('tmp/tests/default_storage') FileUtils.mkdir('tmp/tests/custom_storage') storages = { - 'default' => 'tmp/tests/default_storage', - 'custom' => 'tmp/tests/custom_storage' + 'default' => { 'path' => 'tmp/tests/default_storage' }, + 'custom' => { 'path' => 'tmp/tests/custom_storage' } } allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) diff --git a/spec/tasks/gitlab/info_rake_spec.rb b/spec/tasks/gitlab/info_rake_spec.rb new file mode 100644 index 00000000000..ca74378a12a --- /dev/null +++ b/spec/tasks/gitlab/info_rake_spec.rb @@ -0,0 +1,37 @@ +require 'rake_helper' + +describe 'gitlab:env:info' do + before do + Rake.application.rake_require 'tasks/gitlab/info' + + stub_warn_user_is_not_gitlab + allow(Gitlab::Popen).to receive(:popen) + end + + describe 'git version' do + before do + allow(Gitlab::Popen).to receive(:popen).with([Gitlab.config.git.bin_path, '--version']) + .and_return(git_version) + end + + context 'when git installed' do + let(:git_version) { 'git version 2.10.0' } + + it 'prints git version' do + run_rake_task('gitlab:env:info') + + expect($stdout.string).to match(/Git Version:(.*)2.10.0/) + end + end + + context 'when git not installed' do + let(:git_version) { '' } + + it 'prints unknown' do + run_rake_task('gitlab:env:info') + + expect($stdout.string).to match(/Git Version:(.*)unknown/) + end + end + end +end diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb index e741e3cf9b6..f2919f20e85 100644 --- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb +++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb @@ -3,11 +3,13 @@ require 'spec_helper' describe 'projects/commit/_commit_box.html.haml' do include Devise::Test::ControllerHelpers + let(:user) { create(:user) } let(:project) { create(:project) } before do assign(:project, project) assign(:commit, project.commit) + allow(view).to receive(:can_collaborate_with_project?).and_return(false) end it 'shows the commit SHA' do @@ -25,4 +27,30 @@ describe 'projects/commit/_commit_box.html.haml' do expect(rendered).to have_text("Pipeline ##{third_pipeline.id} for #{Commit.truncate_sha(project.commit.sha)} failed") end + + context 'viewing a commit' do + context 'as a developer' do + before do + expect(view).to receive(:can_collaborate_with_project?).and_return(true) + end + + it 'has a link to create a new tag' do + render + + expect(rendered).to have_link('Tag') + end + end + + context 'as a non-developer' do + before do + expect(view).to receive(:can_collaborate_with_project?).and_return(false) + end + + it 'does not have a link to create a new tag' do + render + + expect(rendered).not_to have_link('Tag') + end + end + end end diff --git a/spec/views/projects/pipelines/_stage.html.haml_spec.rb b/spec/views/projects/pipelines/_stage.html.haml_spec.rb index d25de8af5d2..65f9d0125e6 100644 --- a/spec/views/projects/pipelines/_stage.html.haml_spec.rb +++ b/spec/views/projects/pipelines/_stage.html.haml_spec.rb @@ -50,4 +50,23 @@ describe 'projects/pipelines/_stage', :view do expect(rendered).to have_text 'test:build', count: 1 end end + + context 'when there are multiple builds' do + before do + HasStatus::AVAILABLE_STATUSES.each do |status| + create_build(status) + end + end + + it 'shows them in order' do + render + + expect(rendered).to have_text(HasStatus::ORDERED_STATUSES.join(" ")) + end + + def create_build(status) + create(:ci_build, name: status, status: status, + pipeline: pipeline, stage: stage.name) + end + end end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 5919b99a6ed..7bcb5521202 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -105,6 +105,6 @@ describe PostReceive do end def pwd(project) - File.join(Gitlab.config.repositories.storages.default, project.path_with_namespace) + File.join(Gitlab.config.repositories.storages.default['path'], project.path_with_namespace) end end diff --git a/spec/workers/system_hook_push_worker_spec.rb b/spec/workers/system_hook_push_worker_spec.rb new file mode 100644 index 00000000000..b1d446ed25f --- /dev/null +++ b/spec/workers/system_hook_push_worker_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe SystemHookPushWorker do + include RepoHelpers + + subject { described_class.new } + + describe '#perform' do + it 'executes SystemHooksService with expected values' do + push_data = double('push_data') + system_hook_service = double('system_hook_service') + + expect(SystemHooksService).to receive(:new).and_return(system_hook_service) + expect(system_hook_service).to receive(:execute_hooks).with(push_data, :push_hooks) + + subject.perform(push_data, :push_hooks) + end + end +end diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb index c78a69eda67..262d6e5a9ab 100644 --- a/spec/workers/update_merge_requests_worker_spec.rb +++ b/spec/workers/update_merge_requests_worker_spec.rb @@ -23,16 +23,5 @@ describe UpdateMergeRequestsWorker do perform end - - it 'executes SystemHooksService with expected values' do - push_data = double('push_data') - expect(Gitlab::DataBuilder::Push).to receive(:build).with(project, user, oldrev, newrev, ref, []).and_return(push_data) - - system_hook_service = double('system_hook_service') - expect(SystemHooksService).to receive(:new).and_return(system_hook_service) - expect(system_hook_service).to receive(:execute_hooks).with(push_data, :push_hooks) - - perform - end end end diff --git a/yarn.lock b/yarn.lock index 9d38799fc35..55b8f1566ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1395,6 +1395,10 @@ doctrine@1.5.0, doctrine@^1.2.2: esutils "^2.0.2" isarray "^1.0.0" +document-register-element@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/document-register-element/-/document-register-element-1.3.0.tgz#fb3babb523c74662be47be19c6bc33e71990d940" + dom-serialize@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" @@ -1439,6 +1443,10 @@ elliptic@^6.0.0: hash.js "^1.0.0" inherits "^2.0.1" +emoji-unicode-version@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/emoji-unicode-version/-/emoji-unicode-version-0.2.1.tgz#0ebf3666b5414097971d34994e299fce75cdbafc" + emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" @@ -4115,6 +4123,14 @@ string-width@^2.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^3.0.0" +string.fromcodepoint@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string.fromcodepoint/-/string.fromcodepoint-0.2.1.tgz#8d978333c0bc92538f50f383e4888f3e5619d653" + +string.prototype.codepointat@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz#6b26e9bd3afcaa7be3b4269b526de1b82000ac78" + string_decoder@^0.10.25, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" |