diff options
2423 files changed, 29774 insertions, 23923 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 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 627a3f43a64..0062ac97180 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -4.1.1 +5.0.0 @@ -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' @@ -79,7 +80,7 @@ gem 'kaminari', '~> 0.17.0' gem 'hamlit', '~> 2.6.1' # Files attachments -gem 'carrierwave', '~> 0.10.0' +gem 'carrierwave', '~> 0.11.0' # Drag and Drop UI gem 'dropzonejs-rails', '~> 0.7.1' @@ -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.3.3' -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 65120df205c..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) @@ -103,11 +104,12 @@ GEM capybara-screenshot (1.0.11) capybara (>= 1.0, < 3) launchy - carrierwave (0.10.0) + carrierwave (0.11.2) activemodel (>= 3.2.0) activesupport (>= 3.2.0) json (>= 1.7) mime-types (>= 1.16) + mimemagic (>= 0.3.0) cause (0.1) charlock_holmes (0.7.3) chronic (0.10.2) @@ -166,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) @@ -231,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) @@ -375,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) @@ -583,7 +594,7 @@ GEM recaptcha (3.0.0) json recursive-open-struct (1.0.0) - redcarpet (3.3.3) + redcarpet (3.4.0) redis (3.2.2) redis-actionpack (5.0.1) actionpack (>= 4.0, < 6) @@ -683,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) @@ -788,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) @@ -850,7 +863,7 @@ DEPENDENCIES bundler-audit (~> 0.5.0) capybara (~> 2.6.2) capybara-screenshot (~> 1.0.0) - carrierwave (~> 0.10.0) + carrierwave (~> 0.11.0) charlock_holmes (~> 0.7.3) chronic (~> 0.10.2) chronic_duration (~> 0.10.6) @@ -865,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) @@ -877,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) @@ -955,7 +969,7 @@ DEPENDENCIES rblineprof (~> 0.3.6) rdoc (~> 4.2) recaptcha (~> 3.0) - redcarpet (~> 3.3.3) + redcarpet (~> 3.4) redis (~> 3.2) redis-namespace (~> 1.5.2) redis-rails (~> 5.0.1) 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/bind_in_out.js b/app/assets/javascripts/behaviors/bind_in_out.js new file mode 100644 index 00000000000..886f127b06b --- /dev/null +++ b/app/assets/javascripts/behaviors/bind_in_out.js @@ -0,0 +1,47 @@ +class BindInOut { + constructor(bindIn, bindOut) { + this.in = bindIn; + this.out = bindOut; + + this.eventWrapper = {}; + this.eventType = /(INPUT|TEXTAREA)/.test(bindIn.tagName) ? 'keyup' : 'change'; + } + + addEvents() { + this.eventWrapper.updateOut = this.updateOut.bind(this); + + this.in.addEventListener(this.eventType, this.eventWrapper.updateOut); + + return this; + } + + updateOut() { + this.out.textContent = this.in.value; + + return this; + } + + removeEvents() { + this.in.removeEventListener(this.eventType, this.eventWrapper.updateOut); + + return this; + } + + static initAll() { + const ins = document.querySelectorAll('*[data-bind-in]'); + + return [].map.call(ins, anIn => BindInOut.init(anIn)); + } + + static init(anIn, anOut) { + const out = anOut || document.querySelector(`*[data-bind-out="${anIn.dataset.bindIn}"]`); + + if (!out) return null; + + const bindInOut = new BindInOut(anIn, out); + + return bindInOut.addEvents().updateOut(); + } +} + +export default BindInOut; 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/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index a7181904ac9..0726c6c9636 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -21,8 +21,7 @@ // %a.js-toggle-button // %div.js-toggle-content // - $('body').on('click', '.js-toggle-button', function(e) { - e.preventDefault(); + $('body').on('click', '.js-toggle-button', function() { toggleContainer($(this).closest('.js-toggle-container')); }); 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 2bc3d85fba4..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) { @@ -110,7 +113,7 @@ require('./lib/utils/common_utils'); return `<dl>\n${lines.join('\n')}\n</dl>`; }, - 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) { + 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr, summary, details'(el, text) { const tag = el.nodeName.toLowerCase(); return `<${tag}>${text}</${tag}>`; }, diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js new file mode 100644 index 00000000000..788daa96b3d --- /dev/null +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -0,0 +1,155 @@ +/* global CommentsStore Cookies notes */ +import Vue from 'vue'; +import collapseIcon from '../icons/collapse_icon.svg'; + +(() => { + const DiffNoteAvatars = Vue.extend({ + props: ['discussionId'], + data() { + return { + isVisible: false, + lineType: '', + storeState: CommentsStore.state, + shownAvatars: 3, + collapseIcon, + }; + }, + template: ` + <div class="diff-comment-avatar-holders" + v-show="notesCount !== 0"> + <div v-if="!isVisible"> + <img v-for="note in notesSubset" + class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar" + width="19" + height="19" + role="button" + data-container="body" + data-placement="top" + :data-line-type="lineType" + :title="note.authorName + ': ' + note.noteTruncated" + :src="note.authorAvatar" + @click="clickedAvatar($event)" /> + <span v-if="notesCount > shownAvatars" + class="diff-comments-more-count has-tooltip js-diff-comment-avatar" + data-container="body" + data-placement="top" + ref="extraComments" + role="button" + :data-line-type="lineType" + :title="extraNotesTitle" + @click="clickedAvatar($event)">{{ moreText }}</span> + </div> + <button class="diff-notes-collapse js-diff-comment-avatar" + type="button" + aria-label="Show comments" + :data-line-type="lineType" + @click="clickedAvatar($event)" + v-if="isVisible" + v-html="collapseIcon"> + </button> + </div> + `, + mounted() { + this.$nextTick(() => { + this.addNoCommentClass(); + this.setDiscussionVisible(); + + this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new'; + }); + + $(document).on('toggle.comments', () => { + this.$nextTick(() => { + this.setDiscussionVisible(); + }); + }); + }, + destroyed() { + $(document).off('toggle.comments'); + }, + watch: { + storeState: { + handler() { + this.$nextTick(() => { + $('.has-tooltip', this.$el).tooltip('fixTitle'); + + // We need to add/remove a class to an element that is outside the Vue instance + this.addNoCommentClass(); + }); + }, + deep: true, + }, + }, + computed: { + notesSubset() { + let notes = []; + + if (this.discussion) { + notes = Object.keys(this.discussion.notes) + .slice(0, this.shownAvatars) + .map(noteId => this.discussion.notes[noteId]); + } + + return notes; + }, + extraNotesTitle() { + if (this.discussion) { + const extra = this.discussion.notesCount() - this.shownAvatars; + + return `${extra} more comment${extra > 1 ? 's' : ''}`; + } + + return ''; + }, + discussion() { + return this.storeState[this.discussionId]; + }, + notesCount() { + if (this.discussion) { + return this.discussion.notesCount(); + } + + return 0; + }, + moreText() { + const plusSign = this.notesCount < 100 ? '+' : ''; + + return `${plusSign}${this.notesCount - this.shownAvatars}`; + }, + }, + methods: { + clickedAvatar(e) { + notes.addDiffNote(e); + + // Toggle the active state of the toggle all button + this.toggleDiscussionsToggleState(); + + this.$nextTick(() => { + this.setDiscussionVisible(); + + $('.has-tooltip', this.$el).tooltip('fixTitle'); + $('.has-tooltip', this.$el).tooltip('hide'); + }); + }, + addNoCommentClass() { + const notesCount = this.notesCount; + + $(this.$el).closest('.js-avatar-container') + .toggleClass('js-no-comment-btn', notesCount > 0) + .nextUntil('.js-avatar-container') + .toggleClass('js-no-comment-btn', notesCount > 0); + }, + toggleDiscussionsToggleState() { + const $notesHolders = $(this.$el).closest('.code').find('.notes_holder'); + const $visibleNotesHolders = $notesHolders.filter(':visible'); + const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments'); + + $toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length); + }, + setDiscussionVisible() { + this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible'); + }, + }, + }); + + Vue.component('diff-note-avatars', DiffNoteAvatars); +})(); diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js index d1873d6c7a2..fbd980f0fce 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js @@ -11,7 +11,10 @@ const Vue = require('vue'); discussionId: String, resolved: Boolean, canResolve: Boolean, - resolvedBy: String + resolvedBy: String, + authorName: String, + authorAvatar: String, + noteTruncated: String, }, data: function () { return { @@ -98,7 +101,16 @@ const Vue = require('vue'); CommentsStore.delete(this.discussionId, this.noteId); }, created: function () { - CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy); + CommentsStore.create({ + discussionId: this.discussionId, + noteId: this.noteId, + canResolve: this.canResolve, + resolved: this.resolved, + resolvedBy: this.resolvedBy, + authorName: this.authorName, + authorAvatar: this.authorAvatar, + noteTruncated: this.noteTruncated, + }); this.note = this.discussion.getNote(this.noteId); } diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index cadf8b96b87..7d8316dfd63 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -13,6 +13,7 @@ require('./components/jump_to_discussion'); require('./components/resolve_btn'); require('./components/resolve_count'); require('./components/resolve_discussion_btn'); +require('./components/diff_note_avatars'); $(() => { const projectPath = document.querySelector('.merge-request').dataset.projectPath; @@ -24,6 +25,15 @@ $(() => { window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath); gl.diffNotesCompileComponents = () => { + $('diff-note-avatars').each(function () { + const tmp = Vue.extend({ + template: $(this).get(0).outerHTML + }); + const tmpApp = new tmp().$mount(); + + $(this).replaceWith(tmpApp.$el); + }); + const $components = $(COMPONENT_SELECTOR).filter(function () { return $(this).closest('resolve-count').length !== 1; }); diff --git a/app/assets/javascripts/diff_notes/icons/collapse_icon.svg b/app/assets/javascripts/diff_notes/icons/collapse_icon.svg new file mode 100644 index 00000000000..bd4b393cfaa --- /dev/null +++ b/app/assets/javascripts/diff_notes/icons/collapse_icon.svg @@ -0,0 +1 @@ +<svg width="11" height="11" viewBox="0 0 9 13"><path d="M2.57568253,6.49866948 C2.50548852,6.57199715 2.44637866,6.59708255 2.39835118,6.57392645 C2.3503237,6.55077034 2.32631032,6.48902165 2.32631032,6.38867852 L2.32631032,-2.13272614 C2.32631032,-2.23306927 2.3503237,-2.29481796 2.39835118,-2.31797406 C2.44637866,-2.34113017 2.50548852,-2.31604477 2.57568253,-2.24271709 L6.51022184,1.86747129 C6.53977721,1.8983461 6.56379059,1.93500939 6.5822627,1.97746225 L6.5822627,2.27849013 C6.56379059,2.31708364 6.53977721,2.35374693 6.51022184,2.38848109 L2.57568253,6.49866948 Z" transform="translate(4.454287, 2.127976) rotate(90.000000) translate(-4.454287, -2.127976) "></path><path d="M3.74312342,2.09553332 C3.74312342,1.99519019 3.77821989,1.9083561 3.8484139,1.83502843 C3.91860791,1.76170075 4.00173115,1.72503747 4.09778611,1.72503747 L4.80711151,1.72503747 C4.90316647,1.72503747 4.98628971,1.76170075 5.05648372,1.83502843 C5.12667773,1.9083561 5.16177421,1.99519019 5.16177421,2.09553332 L5.16177421,10.2464421 C5.16177421,10.3467853 5.12667773,10.4336194 5.05648372,10.506947 C4.98628971,10.5802747 4.90316647,10.616938 4.80711151,10.616938 L4.09778611,10.616938 C4.00173115,10.616938 3.91860791,10.5802747 3.8484139,10.506947 C3.77821989,10.4336194 3.74312342,10.3467853 3.74312342,10.2464421 L3.74312342,2.09553332 Z" transform="translate(4.452449, 6.170988) rotate(-90.000000) translate(-4.452449, -6.170988) "></path><path d="M2.57568253,14.6236695 C2.50548852,14.6969971 2.44637866,14.7220826 2.39835118,14.6989264 C2.3503237,14.6757703 2.32631032,14.6140216 2.32631032,14.5136785 L2.32631032,5.99227386 C2.32631032,5.89193073 2.3503237,5.83018204 2.39835118,5.80702594 C2.44637866,5.78386983 2.50548852,5.80895523 2.57568253,5.88228291 L6.51022184,9.99247129 C6.53977721,10.0233461 6.56379059,10.0600094 6.5822627,10.1024622 L6.5822627,10.4034901 C6.56379059,10.4420836 6.53977721,10.4787469 6.51022184,10.5134811 L2.57568253,14.6236695 Z" transform="translate(4.454287, 10.252976) scale(1, -1) rotate(90.000000) translate(-4.454287, -10.252976) "></path></svg> diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js index fa518ba4d33..dce1a9b58bd 100644 --- a/app/assets/javascripts/diff_notes/models/discussion.js +++ b/app/assets/javascripts/diff_notes/models/discussion.js @@ -10,8 +10,8 @@ class DiscussionModel { this.canResolve = false; } - createNote (noteId, canResolve, resolved, resolved_by) { - Vue.set(this.notes, noteId, new NoteModel(this.id, noteId, canResolve, resolved, resolved_by)); + createNote (noteObj) { + Vue.set(this.notes, noteObj.noteId, new NoteModel(this.id, noteObj)); } deleteNote (noteId) { diff --git a/app/assets/javascripts/diff_notes/models/note.js b/app/assets/javascripts/diff_notes/models/note.js index f3a7cba5ef6..04465aa507e 100644 --- a/app/assets/javascripts/diff_notes/models/note.js +++ b/app/assets/javascripts/diff_notes/models/note.js @@ -1,12 +1,15 @@ /* eslint-disable camelcase, no-unused-vars */ class NoteModel { - constructor(discussionId, noteId, canResolve, resolved, resolved_by) { + constructor(discussionId, noteObj) { this.discussionId = discussionId; - this.id = noteId; - this.canResolve = canResolve; - this.resolved = resolved; - this.resolved_by = resolved_by; + this.id = noteObj.noteId; + this.canResolve = noteObj.canResolve; + this.resolved = noteObj.resolved; + this.resolved_by = noteObj.resolvedBy; + this.authorName = noteObj.authorName; + this.authorAvatar = noteObj.authorAvatar; + this.noteTruncated = noteObj.noteTruncated; } } diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js index c80d979b977..69c4d7a8434 100644 --- a/app/assets/javascripts/diff_notes/stores/comments.js +++ b/app/assets/javascripts/diff_notes/stores/comments.js @@ -21,10 +21,10 @@ return discussion; }, - create: function (discussionId, noteId, canResolve, resolved, resolved_by) { - const discussion = this.createDiscussion(discussionId); + create: function (noteObj) { + const discussion = this.createDiscussion(noteObj.discussionId); - discussion.createNote(noteId, canResolve, resolved, resolved_by); + discussion.createNote(noteObj); }, update: function (discussionId, noteId, resolved, resolved_by) { const discussion = this.state[discussionId]; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index ef5785b5532..017980271b1 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,10 +1,10 @@ +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 */ /* global ShortcutsNavigation */ /* global Build */ /* global Issuable */ -/* global Issue */ /* global ShortcutsIssuable */ /* global ZenMode */ /* global Milestone */ @@ -34,7 +34,9 @@ /* global ProjectShow */ /* global Labels */ /* global Shortcuts */ +import Issue from './issue'; +import BindInOut from './behaviors/bind_in_out'; import GroupsList from './groups_list'; import ProjectsList from './projects_list'; @@ -229,9 +231,14 @@ const UserCallout = require('./user_callout'); new UsersSelect(); break; case 'groups:new': + case 'admin:groups:new': + case 'groups:create': + case 'admin:groups:create': + BindInOut.initAll(); + case 'groups:new': + case 'admin:groups:new': case 'groups:edit': case 'admin:groups:edit': - case 'admin:groups:new': new GroupAvatar(); break; case 'projects:tree:show': @@ -280,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; @@ -291,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/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 6d86888dcb8..bf84f2a0a8f 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -38,6 +38,9 @@ FilesCommentButton.prototype.render = function(e) { var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button; $currentTarget = $(e.currentTarget); + + if ($currentTarget.hasClass('js-no-comment-btn')) return; + lineContentElement = this.getLineContent($currentTarget); buttonParentElement = this.getButtonParent($currentTarget); 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..a5a6b56a0d3 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,59 @@ 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(untilInput = false) { + const tokens = [].slice.call(document.querySelectorAll('.tokens-container li')); + const values = []; + + if (untilInput) { + const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token')); + // Add one to include input-token to the tokens array + tokens.splice(inputIndex + 1); + } + + tokens.forEach((token) => { + if (token.classList.contains('js-visual-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); + } + } else if (token.classList.contains('input-token')) { + const { isLastVisualTokenValid } = + gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + + const input = document.querySelector('.filtered-search'); + const inputValue = input && input.value; + + if (isLastVisualTokenValid) { + values.push(inputValue); + } else { + const previous = values.pop(); + values.push(`${previous}${inputValue}`); + } + } + }); + + 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..e1a97070439 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(true); + 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..638fe744668 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}`); @@ -219,10 +363,13 @@ tokenChange() { const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; - const currentDropdownRef = dropdown.reference; - this.setDropdownWrapper(); - currentDropdownRef.dispatchInputEvent(); + if (dropdown) { + const currentDropdownRef = dropdown.reference; + + this.setDropdownWrapper(); + currentDropdownRef.dispatchInputEvent(); + } } } 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/issue.js b/app/assets/javascripts/issue.js index 52457f70d90..ef4029a8623 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -5,131 +5,125 @@ require('./flash'); require('vendor/jquery.waitforimages'); require('./task_list'); -(function() { - var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; - - this.Issue = (function() { - function Issue() { - this.submitNoteForm = bind(this.submitNoteForm, this); - if ($('a.btn-close').length) { - this.taskList = new gl.TaskList({ - dataType: 'issue', - fieldName: 'description', - selector: '.detail-page-description', - onSuccess: (result) => { - document.querySelector('#task_status').innerText = result.task_status; - document.querySelector('#task_status_short').innerText = result.task_status_short; - } - }); - this.initIssueBtnEventListeners(); - } - this.initMergeRequests(); - this.initRelatedBranches(); - this.initCanCreateBranch(); +class Issue { + constructor() { + if ($('a.btn-close').length) { + this.taskList = new gl.TaskList({ + dataType: 'issue', + fieldName: 'description', + selector: '.detail-page-description', + onSuccess: (result) => { + document.querySelector('#task_status').innerText = result.task_status; + document.querySelector('#task_status_short').innerText = result.task_status_short; + } + }); + Issue.initIssueBtnEventListeners(); } + Issue.initMergeRequests(); + Issue.initRelatedBranches(); + Issue.initCanCreateBranch(); + } - Issue.prototype.initIssueBtnEventListeners = function() { - var _this, issueFailMessage; - _this = this; - issueFailMessage = 'Unable to update this issue at this time.'; - return $('a.btn-close, a.btn-reopen').on('click', function(e) { - var $this, isClose, shouldSubmit, url; - e.preventDefault(); - e.stopImmediatePropagation(); - $this = $(this); - isClose = $this.hasClass('btn-close'); - shouldSubmit = $this.hasClass('btn-comment'); - if (shouldSubmit) { - _this.submitNoteForm($this.closest('form')); - } - $this.prop('disabled', true); - url = $this.attr('href'); - return $.ajax({ - type: 'PUT', - url: url, - error: function(jqXHR, textStatus, errorThrown) { - var issueStatus; - issueStatus = isClose ? 'close' : 'open'; - return new Flash(issueFailMessage, 'alert'); - }, - success: function(data, textStatus, jqXHR) { - if ('id' in data) { - $(document).trigger('issuable:change'); - const currentTotal = Number($('.issue_counter').text()); - if (isClose) { - $('a.btn-close').addClass('hidden'); - $('a.btn-reopen').removeClass('hidden'); - $('div.status-box-closed').removeClass('hidden'); - $('div.status-box-open').addClass('hidden'); - $('.issue_counter').text(currentTotal - 1); - } else { - $('a.btn-reopen').addClass('hidden'); - $('a.btn-close').removeClass('hidden'); - $('div.status-box-closed').addClass('hidden'); - $('div.status-box-open').removeClass('hidden'); - $('.issue_counter').text(currentTotal + 1); - } + static initIssueBtnEventListeners() { + var issueFailMessage; + issueFailMessage = 'Unable to update this issue at this time.'; + return $('a.btn-close, a.btn-reopen').on('click', function(e) { + var $this, isClose, shouldSubmit, url; + e.preventDefault(); + e.stopImmediatePropagation(); + $this = $(this); + isClose = $this.hasClass('btn-close'); + shouldSubmit = $this.hasClass('btn-comment'); + if (shouldSubmit) { + Issue.submitNoteForm($this.closest('form')); + } + $this.prop('disabled', true); + url = $this.attr('href'); + return $.ajax({ + type: 'PUT', + url: url, + error: function(jqXHR, textStatus, errorThrown) { + var issueStatus; + issueStatus = isClose ? 'close' : 'open'; + return new Flash(issueFailMessage, 'alert'); + }, + success: function(data, textStatus, jqXHR) { + if ('id' in data) { + $(document).trigger('issuable:change'); + const currentTotal = Number($('.issue_counter').text()); + if (isClose) { + $('a.btn-close').addClass('hidden'); + $('a.btn-reopen').removeClass('hidden'); + $('div.status-box-closed').removeClass('hidden'); + $('div.status-box-open').addClass('hidden'); + $('.issue_counter').text(currentTotal - 1); } else { - new Flash(issueFailMessage, 'alert'); + $('a.btn-reopen').addClass('hidden'); + $('a.btn-close').removeClass('hidden'); + $('div.status-box-closed').addClass('hidden'); + $('div.status-box-open').removeClass('hidden'); + $('.issue_counter').text(currentTotal + 1); } - return $this.prop('disabled', false); + } else { + new Flash(issueFailMessage, 'alert'); } - }); + return $this.prop('disabled', false); + } }); - }; + }); + } - Issue.prototype.submitNoteForm = function(form) { - var noteText; - noteText = form.find("textarea.js-note-text").val(); - if (noteText.trim().length > 0) { - return form.submit(); - } - }; + static submitNoteForm(form) { + var noteText; + noteText = form.find("textarea.js-note-text").val(); + if (noteText.trim().length > 0) { + return form.submit(); + } + } - Issue.prototype.initMergeRequests = function() { - var $container; - $container = $('#merge-requests'); - return $.getJSON($container.data('url')).error(function() { - return new Flash('Failed to load referenced merge requests', 'alert'); - }).success(function(data) { - if ('html' in data) { - return $container.html(data.html); - } - }); - }; + static initMergeRequests() { + var $container; + $container = $('#merge-requests'); + return $.getJSON($container.data('url')).error(function() { + return new Flash('Failed to load referenced merge requests', 'alert'); + }).success(function(data) { + if ('html' in data) { + return $container.html(data.html); + } + }); + } - Issue.prototype.initRelatedBranches = function() { - var $container; - $container = $('#related-branches'); - return $.getJSON($container.data('url')).error(function() { - return new Flash('Failed to load related branches', 'alert'); - }).success(function(data) { - if ('html' in data) { - return $container.html(data.html); - } - }); - }; + static initRelatedBranches() { + var $container; + $container = $('#related-branches'); + return $.getJSON($container.data('url')).error(function() { + return new Flash('Failed to load related branches', 'alert'); + }).success(function(data) { + if ('html' in data) { + return $container.html(data.html); + } + }); + } - Issue.prototype.initCanCreateBranch = function() { - var $container; - $container = $('#new-branch'); - // If the user doesn't have the required permissions the container isn't - // rendered at all. - if ($container.length === 0) { - return; + static initCanCreateBranch() { + var $container; + $container = $('#new-branch'); + // If the user doesn't have the required permissions the container isn't + // rendered at all. + if ($container.length === 0) { + return; + } + return $.getJSON($container.data('path')).error(function() { + $container.find('.unavailable').show(); + return new Flash('Failed to check if a new branch can be created.', 'alert'); + }).success(function(data) { + if (data.can_create_branch) { + $container.find('.available').show(); + } else { + return $container.find('.unavailable').show(); } - return $.getJSON($container.data('path')).error(function() { - $container.find('.unavailable').show(); - return new Flash('Failed to check if a new branch can be created.', 'alert'); - }).success(function(data) { - if (data.can_create_branch) { - $container.find('.available').show(); - } else { - return $container.find('.unavailable').show(); - } - }); - }; + }); + } +} - return Issue; - })(); -}).call(window); +export default Issue; diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js deleted file mode 100644 index ebe1e2ae98d..00000000000 --- a/app/assets/javascripts/lib/raphael.js +++ /dev/null @@ -1,9 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren */ - -/*= require raphael */ -/*= require g.raphael */ -/*= require g.bar */ - -(function() { - -}).call(window); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 798553c16ac..79164edff0e 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; @@ -41,6 +49,7 @@ require('./behaviors/details_behavior'); require('./behaviors/quick_submit'); require('./behaviors/requires_input'); require('./behaviors/toggler_behavior'); +require('./behaviors/bind_in_out'); // blob require('./blob/blob_ci_yaml'); @@ -61,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'); @@ -99,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'); @@ -340,11 +342,11 @@ require('./zen_mode'); var notesHolders = $this.closest('.diff-file').find('.notes_holder'); $this.toggleClass('active'); if ($this.hasClass('active')) { - notesHolders.show().find('.hide').show(); + notesHolders.show().find('.hide, .content').show(); } else { - notesHolders.hide(); + notesHolders.hide().find('.content').hide(); } - $this.trigger('blur'); + $(document).trigger('toggle.comments'); return e.preventDefault(); }); $document.off('click', '.js-confirm-danger'); 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/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index 43dc9838977..5aad3908eb6 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -1,424 +1,347 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */ -/* global Raphael */ +/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */ -(function() { - var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; +import Raphael from './raphael'; - this.BranchGraph = (function() { - function BranchGraph(element1, options1) { - this.element = element1; - this.options = options1; - this.scrollTop = bind(this.scrollTop, this); - this.scrollBottom = bind(this.scrollBottom, this); - this.scrollRight = bind(this.scrollRight, this); - this.scrollLeft = bind(this.scrollLeft, this); - this.scrollUp = bind(this.scrollUp, this); - this.scrollDown = bind(this.scrollDown, this); - this.preparedCommits = {}; - this.mtime = 0; - this.mspace = 0; - this.parents = {}; - this.colors = ["#000"]; - this.offsetX = 150; - this.offsetY = 20; - this.unitTime = 30; - this.unitSpace = 10; - this.prev_start = -1; - this.load(); - } - - BranchGraph.prototype.load = function() { - return $.ajax({ - url: this.options.url, - method: "get", - dataType: "json", - success: $.proxy(function(data) { - $(".loading", this.element).hide(); - this.prepareData(data.days, data.commits); - return this.buildGraph(); - }, this) - }); - }; +export default (function() { + function BranchGraph(element1, options1) { + this.element = element1; + this.options = options1; + this.scrollTop = this.scrollTop.bind(this); + this.scrollBottom = this.scrollBottom.bind(this); + this.scrollRight = this.scrollRight.bind(this); + this.scrollLeft = this.scrollLeft.bind(this); + this.scrollUp = this.scrollUp.bind(this); + this.scrollDown = this.scrollDown.bind(this); + this.preparedCommits = {}; + this.mtime = 0; + this.mspace = 0; + this.parents = {}; + this.colors = ["#000"]; + this.offsetX = 150; + this.offsetY = 20; + this.unitTime = 30; + this.unitSpace = 10; + this.prev_start = -1; + this.load(); + } - BranchGraph.prototype.prepareData = function(days, commits) { - var c, ch, cw, j, len, ref; - this.days = days; - this.commits = commits; - this.collectParents(); - this.graphHeight = $(this.element).height(); - this.graphWidth = $(this.element).width(); - ch = Math.max(this.graphHeight, this.offsetY + this.unitTime * this.mtime + 150); - cw = Math.max(this.graphWidth, this.offsetX + this.unitSpace * this.mspace + 300); - this.r = Raphael(this.element.get(0), cw, ch); - this.top = this.r.set(); - this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320); - ref = this.commits; - for (j = 0, len = ref.length; j < len; j += 1) { - c = ref[j]; - if (c.id in this.parents) { - c.isParent = true; - } - this.preparedCommits[c.id] = c; - this.markCommit(c); - } - return this.collectColors(); - }; - - BranchGraph.prototype.collectParents = function() { - var c, j, len, p, ref, results; - ref = this.commits; - results = []; - for (j = 0, len = ref.length; j < len; j += 1) { - c = ref[j]; - this.mtime = Math.max(this.mtime, c.time); - this.mspace = Math.max(this.mspace, c.space); - results.push((function() { - var l, len1, ref1, results1; - ref1 = c.parents; - results1 = []; - for (l = 0, len1 = ref1.length; l < len1; l += 1) { - p = ref1[l]; - this.parents[p[0]] = true; - results1.push(this.mspace = Math.max(this.mspace, p[1])); - } - return results1; - }).call(this)); - } - return results; - }; + BranchGraph.prototype.load = function() { + return $.ajax({ + url: this.options.url, + method: "get", + dataType: "json", + success: $.proxy(function(data) { + $(".loading", this.element).hide(); + this.prepareData(data.days, data.commits); + return this.buildGraph(); + }, this) + }); + }; - BranchGraph.prototype.collectColors = function() { - var k, results; - k = 0; - results = []; - while (k < this.mspace) { - this.colors.push(Raphael.getColor(.8)); - // Skipping a few colors in the spectrum to get more contrast between colors - Raphael.getColor(); - Raphael.getColor(); - results.push(k += 1); + BranchGraph.prototype.prepareData = function(days, commits) { + var c, ch, cw, j, len, ref; + this.days = days; + this.commits = commits; + this.collectParents(); + this.graphHeight = $(this.element).height(); + this.graphWidth = $(this.element).width(); + ch = Math.max(this.graphHeight, this.offsetY + this.unitTime * this.mtime + 150); + cw = Math.max(this.graphWidth, this.offsetX + this.unitSpace * this.mspace + 300); + this.r = Raphael(this.element.get(0), cw, ch); + this.top = this.r.set(); + this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320); + ref = this.commits; + for (j = 0, len = ref.length; j < len; j += 1) { + c = ref[j]; + if (c.id in this.parents) { + c.isParent = true; } - return results; - }; + this.preparedCommits[c.id] = c; + this.markCommit(c); + } + return this.collectColors(); + }; - BranchGraph.prototype.buildGraph = function() { - var cuday, cumonth, day, j, len, mm, r, ref; - r = this.r; - cuday = 0; - cumonth = ""; - r.rect(0, 0, 40, this.barHeight).attr({ - fill: "#222" - }); - r.rect(40, 0, 30, this.barHeight).attr({ - fill: "#444" - }); - ref = this.days; - for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) { - day = ref[mm]; - if (cuday !== day[0] || cumonth !== day[1]) { - // Dates - r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({ - font: "12px Monaco, monospace", - fill: "#BBB" - }); - cuday = day[0]; - } - if (cumonth !== day[1]) { - // Months - r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({ - font: "12px Monaco, monospace", - fill: "#EEE" - }); - cumonth = day[1]; + BranchGraph.prototype.collectParents = function() { + var c, j, len, p, ref, results; + ref = this.commits; + results = []; + for (j = 0, len = ref.length; j < len; j += 1) { + c = ref[j]; + this.mtime = Math.max(this.mtime, c.time); + this.mspace = Math.max(this.mspace, c.space); + results.push((function() { + var l, len1, ref1, results1; + ref1 = c.parents; + results1 = []; + for (l = 0, len1 = ref1.length; l < len1; l += 1) { + p = ref1[l]; + this.parents[p[0]] = true; + results1.push(this.mspace = Math.max(this.mspace, p[1])); } - } - this.renderPartialGraph(); - return this.bindEvents(); - }; + return results1; + }).call(this)); + } + return results; + }; - BranchGraph.prototype.renderPartialGraph = function() { - var commit, end, i, isGraphEdge, start, x, y; - start = Math.floor((this.element.scrollTop() - this.offsetY) / this.unitTime) - 10; - if (start < 0) { - isGraphEdge = true; - start = 0; + BranchGraph.prototype.collectColors = function() { + var k, results; + k = 0; + results = []; + while (k < this.mspace) { + this.colors.push(Raphael.getColor(.8)); + // Skipping a few colors in the spectrum to get more contrast between colors + Raphael.getColor(); + Raphael.getColor(); + results.push(k += 1); + } + return results; + }; + + BranchGraph.prototype.buildGraph = function() { + var cuday, cumonth, day, j, len, mm, r, ref; + r = this.r; + cuday = 0; + cumonth = ""; + r.rect(0, 0, 40, this.barHeight).attr({ + fill: "#222" + }); + r.rect(40, 0, 30, this.barHeight).attr({ + fill: "#444" + }); + ref = this.days; + for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) { + day = ref[mm]; + if (cuday !== day[0] || cumonth !== day[1]) { + // Dates + r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({ + font: "12px Monaco, monospace", + fill: "#BBB" + }); + cuday = day[0]; } - end = start + 40; - if (this.commits.length < end) { - isGraphEdge = true; - end = this.commits.length; + if (cumonth !== day[1]) { + // Months + r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({ + font: "12px Monaco, monospace", + fill: "#EEE" + }); + cumonth = day[1]; } - if (this.prev_start === -1 || Math.abs(this.prev_start - start) > 10 || isGraphEdge) { - i = start; - this.prev_start = start; - while (i < end) { - commit = this.commits[i]; - i += 1; - if (commit.hasDrawn !== true) { - x = this.offsetX + this.unitSpace * (this.mspace - commit.space); - y = this.offsetY + this.unitTime * commit.time; - this.drawDot(x, y, commit); - this.drawLines(x, y, commit); - this.appendLabel(x, y, commit); - this.appendAnchor(x, y, commit); - commit.hasDrawn = true; - } + } + this.renderPartialGraph(); + return this.bindEvents(); + }; + + BranchGraph.prototype.renderPartialGraph = function() { + var commit, end, i, isGraphEdge, start, x, y; + start = Math.floor((this.element.scrollTop() - this.offsetY) / this.unitTime) - 10; + if (start < 0) { + isGraphEdge = true; + start = 0; + } + end = start + 40; + if (this.commits.length < end) { + isGraphEdge = true; + end = this.commits.length; + } + if (this.prev_start === -1 || Math.abs(this.prev_start - start) > 10 || isGraphEdge) { + i = start; + this.prev_start = start; + while (i < end) { + commit = this.commits[i]; + i += 1; + if (commit.hasDrawn !== true) { + x = this.offsetX + this.unitSpace * (this.mspace - commit.space); + y = this.offsetY + this.unitTime * commit.time; + this.drawDot(x, y, commit); + this.drawLines(x, y, commit); + this.appendLabel(x, y, commit); + this.appendAnchor(x, y, commit); + commit.hasDrawn = true; } - return this.top.toFront(); } - }; - - BranchGraph.prototype.bindEvents = function() { - var element; - element = this.element; - return $(element).scroll((function(_this) { - return function(event) { - return _this.renderPartialGraph(); - }; - })(this)); - }; - - BranchGraph.prototype.scrollDown = function() { - this.element.scrollTop(this.element.scrollTop() + 50); - return this.renderPartialGraph(); - }; - - BranchGraph.prototype.scrollUp = function() { - this.element.scrollTop(this.element.scrollTop() - 50); - return this.renderPartialGraph(); - }; - - BranchGraph.prototype.scrollLeft = function() { - this.element.scrollLeft(this.element.scrollLeft() - 50); - return this.renderPartialGraph(); - }; - - BranchGraph.prototype.scrollRight = function() { - this.element.scrollLeft(this.element.scrollLeft() + 50); - return this.renderPartialGraph(); - }; - - BranchGraph.prototype.scrollBottom = function() { - return this.element.scrollTop(this.element.find('svg').height()); - }; + return this.top.toFront(); + } + }; - BranchGraph.prototype.scrollTop = function() { - return this.element.scrollTop(0); - }; + BranchGraph.prototype.bindEvents = function() { + var element; + element = this.element; + return $(element).scroll((function(_this) { + return function(event) { + return _this.renderPartialGraph(); + }; + })(this)); + }; - BranchGraph.prototype.appendLabel = function(x, y, commit) { - var label, r, rect, shortrefs, text, textbox, triangle; - if (!commit.refs) { - return; - } - r = this.r; - shortrefs = commit.refs; - // Truncate if longer than 15 chars - if (shortrefs.length > 17) { - shortrefs = shortrefs.substr(0, 15) + "…"; - } - text = r.text(x + 4, y, shortrefs).attr({ - "text-anchor": "start", - font: "10px Monaco, monospace", - fill: "#FFF", - title: commit.refs - }); - textbox = text.getBBox(); - // Create rectangle based on the size of the textbox - rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({ - fill: "#000", - "fill-opacity": .5, - stroke: "none" - }); - triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr({ - fill: "#000", - "fill-opacity": .5, - stroke: "none" - }); - label = r.set(rect, text); - label.transform(["t", -rect.getBBox().width - 15, 0]); - // Set text to front - return text.toFront(); - }; + BranchGraph.prototype.scrollDown = function() { + this.element.scrollTop(this.element.scrollTop() + 50); + return this.renderPartialGraph(); + }; - BranchGraph.prototype.appendAnchor = function(x, y, commit) { - var anchor, options, r, top; - r = this.r; - top = this.top; - options = this.options; - anchor = r.circle(x, y, 10).attr({ - fill: "#000", - opacity: 0, - cursor: "pointer" - }).click(function() { - return window.open(options.commit_url.replace("%s", commit.id), "_blank"); - }).hover(function() { - this.tooltip = r.commitTooltip(x + 5, y, commit); - return top.push(this.tooltip.insertBefore(this)); - }, function() { - return this.tooltip && this.tooltip.remove() && delete this.tooltip; - }); - return top.push(anchor); - }; + BranchGraph.prototype.scrollUp = function() { + this.element.scrollTop(this.element.scrollTop() - 50); + return this.renderPartialGraph(); + }; - BranchGraph.prototype.drawDot = function(x, y, commit) { - var avatar_box_x, avatar_box_y, r; - r = this.r; - r.circle(x, y, 3).attr({ - fill: this.colors[commit.space], - stroke: "none" - }); - avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10; - avatar_box_y = y - 10; - r.rect(avatar_box_x, avatar_box_y, 20, 20).attr({ - stroke: this.colors[commit.space], - "stroke-width": 2 - }); - r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20); - return r.text(this.offsetX + this.unitSpace * this.mspace + 35, y, commit.message.split("\n")[0]).attr({ - "text-anchor": "start", - font: "14px Monaco, monospace" - }); - }; + BranchGraph.prototype.scrollLeft = function() { + this.element.scrollLeft(this.element.scrollLeft() - 50); + return this.renderPartialGraph(); + }; - BranchGraph.prototype.drawLines = function(x, y, commit) { - var arrow, color, i, j, len, offset, parent, parentCommit, parentX1, parentX2, parentY, r, ref, results, route; - r = this.r; - ref = commit.parents; - results = []; - for (i = j = 0, len = ref.length; j < len; i = (j += 1)) { - parent = ref[i]; - parentCommit = this.preparedCommits[parent[0]]; - parentY = this.offsetY + this.unitTime * parentCommit.time; - parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space); - parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]); - // Set line color - if (parentCommit.space <= commit.space) { - color = this.colors[commit.space]; - } else { - color = this.colors[parentCommit.space]; - } - // Build line shape - if (parent[1] === commit.space) { - offset = [0, 5]; - arrow = "l-2,5,4,0,-2,-5,0,5"; - } else if (parent[1] < commit.space) { - offset = [3, 3]; - arrow = "l5,0,-2,4,-3,-4,4,2"; - } else { - offset = [-3, 3]; - arrow = "l-5,0,2,4,3,-4,-4,2"; - } - // Start point - route = ["M", x + offset[0], y + offset[1]]; - // Add arrow if not first parent - if (i > 0) { - route.push(arrow); - } - // Circumvent if overlap - if (commit.space !== parentCommit.space || commit.space !== parent[1]) { - route.push("L", parentX2, y + 10, "L", parentX2, parentY - 5); - } - // End point - route.push("L", parentX1, parentY); - results.push(r.path(route).attr({ - stroke: color, - "stroke-width": 2 - })); - } - return results; - }; + BranchGraph.prototype.scrollRight = function() { + this.element.scrollLeft(this.element.scrollLeft() + 50); + return this.renderPartialGraph(); + }; - BranchGraph.prototype.markCommit = function(commit) { - var r, x, y; - if (commit.id === this.options.commit_id) { - r = this.r; - x = this.offsetX + this.unitSpace * (this.mspace - commit.space); - y = this.offsetY + this.unitTime * commit.time; - r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr({ - fill: "#000", - "fill-opacity": .5, - stroke: "none" - }); - // Displayed in the center - return this.element.scrollTop(y - this.graphHeight / 2); - } - }; + BranchGraph.prototype.scrollBottom = function() { + return this.element.scrollTop(this.element.find('svg').height()); + }; - return BranchGraph; - })(); + BranchGraph.prototype.scrollTop = function() { + return this.element.scrollTop(0); + }; - Raphael.prototype.commitTooltip = function(x, y, commit) { - var boxHeight, boxWidth, icon, idText, messageText, nameText, rect, textSet, tooltip; - boxWidth = 300; - boxHeight = 200; - icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20); - nameText = this.text(x + 25, y + 10, commit.author.name); - idText = this.text(x, y + 35, commit.id); - messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, " \n ")); - textSet = this.set(icon, nameText, idText, messageText).attr({ + BranchGraph.prototype.appendLabel = function(x, y, commit) { + var label, r, rect, shortrefs, text, textbox, triangle; + if (!commit.refs) { + return; + } + r = this.r; + shortrefs = commit.refs; + // Truncate if longer than 15 chars + if (shortrefs.length > 17) { + shortrefs = shortrefs.substr(0, 15) + "…"; + } + text = r.text(x + 4, y, shortrefs).attr({ "text-anchor": "start", - font: "12px Monaco, monospace" - }); - nameText.attr({ - font: "14px Arial", - "font-weight": "bold" + font: "10px Monaco, monospace", + fill: "#FFF", + title: commit.refs }); - idText.attr({ - fill: "#AAA" + textbox = text.getBBox(); + // Create rectangle based on the size of the textbox + rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({ + fill: "#000", + "fill-opacity": .5, + stroke: "none" }); - messageText.node.style["white-space"] = "pre"; - this.textWrap(messageText, boxWidth - 50); - rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({ - fill: "#FFF", - stroke: "#000", - "stroke-linecap": "round", - "stroke-width": 2 + triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr({ + fill: "#000", + "fill-opacity": .5, + stroke: "none" }); - tooltip = this.set(rect, textSet); - rect.attr({ - height: tooltip.getBBox().height + 10, - width: tooltip.getBBox().width + 10 + label = r.set(rect, text); + label.transform(["t", -rect.getBBox().width - 15, 0]); + // Set text to front + return text.toFront(); + }; + + BranchGraph.prototype.appendAnchor = function(x, y, commit) { + var anchor, options, r, top; + r = this.r; + top = this.top; + options = this.options; + anchor = r.circle(x, y, 10).attr({ + fill: "#000", + opacity: 0, + cursor: "pointer" + }).click(function() { + return window.open(options.commit_url.replace("%s", commit.id), "_blank"); + }).hover(function() { + this.tooltip = r.commitTooltip(x + 5, y, commit); + return top.push(this.tooltip.insertBefore(this)); + }, function() { + return this.tooltip && this.tooltip.remove() && delete this.tooltip; }); - tooltip.transform(["t", 20, 20]); - return tooltip; + return top.push(anchor); }; - Raphael.prototype.textWrap = function(t, width) { - var abc, b, content, h, j, len, letterWidth, s, word, words, x; - content = t.attr("text"); - abc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - t.attr({ - text: abc + BranchGraph.prototype.drawDot = function(x, y, commit) { + var avatar_box_x, avatar_box_y, r; + r = this.r; + r.circle(x, y, 3).attr({ + fill: this.colors[commit.space], + stroke: "none" }); - letterWidth = t.getBBox().width / abc.length; - t.attr({ - text: content + avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10; + avatar_box_y = y - 10; + r.rect(avatar_box_x, avatar_box_y, 20, 20).attr({ + stroke: this.colors[commit.space], + "stroke-width": 2 }); - words = content.split(" "); - x = 0; - s = []; - for (j = 0, len = words.length; j < len; j += 1) { - word = words[j]; - if (x + (word.length * letterWidth) > width) { - s.push("\n"); - x = 0; + r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20); + return r.text(this.offsetX + this.unitSpace * this.mspace + 35, y, commit.message.split("\n")[0]).attr({ + "text-anchor": "start", + font: "14px Monaco, monospace" + }); + }; + + BranchGraph.prototype.drawLines = function(x, y, commit) { + var arrow, color, i, j, len, offset, parent, parentCommit, parentX1, parentX2, parentY, r, ref, results, route; + r = this.r; + ref = commit.parents; + results = []; + for (i = j = 0, len = ref.length; j < len; i = (j += 1)) { + parent = ref[i]; + parentCommit = this.preparedCommits[parent[0]]; + parentY = this.offsetY + this.unitTime * parentCommit.time; + parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space); + parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]); + // Set line color + if (parentCommit.space <= commit.space) { + color = this.colors[commit.space]; + } else { + color = this.colors[parentCommit.space]; } - if (word === "\n") { - s.push("\n"); - x = 0; + // Build line shape + if (parent[1] === commit.space) { + offset = [0, 5]; + arrow = "l-2,5,4,0,-2,-5,0,5"; + } else if (parent[1] < commit.space) { + offset = [3, 3]; + arrow = "l5,0,-2,4,-3,-4,4,2"; } else { - s.push(word + " "); - x += word.length * letterWidth; + offset = [-3, 3]; + arrow = "l-5,0,2,4,3,-4,-4,2"; + } + // Start point + route = ["M", x + offset[0], y + offset[1]]; + // Add arrow if not first parent + if (i > 0) { + route.push(arrow); + } + // Circumvent if overlap + if (commit.space !== parentCommit.space || commit.space !== parent[1]) { + route.push("L", parentX2, y + 10, "L", parentX2, parentY - 5); } + // End point + route.push("L", parentX1, parentY); + results.push(r.path(route).attr({ + stroke: color, + "stroke-width": 2 + })); + } + return results; + }; + + BranchGraph.prototype.markCommit = function(commit) { + var r, x, y; + if (commit.id === this.options.commit_id) { + r = this.r; + x = this.offsetX + this.unitSpace * (this.mspace - commit.space); + y = this.offsetY + this.unitTime * commit.time; + r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr({ + fill: "#000", + "fill-opacity": .5, + stroke: "none" + }); + // Displayed in the center + return this.element.scrollTop(y - this.graphHeight / 2); } - t.attr({ - text: s.join("").trim() - }); - b = t.getBBox(); - h = Math.abs(b.y2) + 1; - return t.attr({ - y: h - }); }; -}).call(window); + + return BranchGraph; +})(); diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js index 8e7027b44e7..a3fd22aff2a 100644 --- a/app/assets/javascripts/network/network.js +++ b/app/assets/javascripts/network/network.js @@ -1,20 +1,19 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */ -/* global BranchGraph */ -(function() { - this.Network = (function() { - function Network(opts) { - var vph; - $("#filter_ref").click(function() { - return $(this).closest('form').submit(); - }); - this.branch_graph = new BranchGraph($(".network-graph"), opts); - vph = $(window).height() - 250; - $('.network-graph').css({ - 'height': vph + 'px' - }); - } +import BranchGraph from './branch_graph'; - return Network; - })(); -}).call(window); +export default (function() { + function Network(opts) { + var vph; + $("#filter_ref").click(function() { + return $(this).closest('form').submit(); + }); + this.branch_graph = new BranchGraph($(".network-graph"), opts); + vph = $(window).height() - 250; + $('.network-graph').css({ + 'height': vph + 'px' + }); + } + + return Network; +})(); diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index e5947586583..8aae2ad201c 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -1,21 +1,17 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */ -/* global Network */ /* global ShortcutsNetwork */ -require('./branch_graph'); -require('./network'); +import Network from './network'; -(function() { - $(function() { - if (!$(".network-graph").length) return; +$(function() { + if (!$(".network-graph").length) return; - var network_graph; - network_graph = new Network({ - url: $(".network-graph").attr('data-url'), - commit_url: $(".network-graph").attr('data-commit-url'), - ref: $(".network-graph").attr('data-ref'), - commit_id: $(".network-graph").attr('data-commit-id') - }); - return new ShortcutsNetwork(network_graph.branch_graph); + var network_graph; + network_graph = new Network({ + url: $(".network-graph").attr('data-url'), + commit_url: $(".network-graph").attr('data-commit-url'), + ref: $(".network-graph").attr('data-ref'), + commit_id: $(".network-graph").attr('data-commit-id') }); -}).call(window); + return new ShortcutsNetwork(network_graph.branch_graph); +}); diff --git a/app/assets/javascripts/network/raphael.js b/app/assets/javascripts/network/raphael.js new file mode 100644 index 00000000000..09dcf716148 --- /dev/null +++ b/app/assets/javascripts/network/raphael.js @@ -0,0 +1,74 @@ +import Raphael from 'raphael/raphael'; + +Raphael.prototype.commitTooltip = function commitTooltip(x, y, commit) { + const boxWidth = 300; + const icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20); + const nameText = this.text(x + 25, y + 10, commit.author.name); + const idText = this.text(x, y + 35, commit.id); + const messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, ' \n ')); + const textSet = this.set(icon, nameText, idText, messageText).attr({ + 'text-anchor': 'start', + font: '12px Monaco, monospace', + }); + nameText.attr({ + font: '14px Arial', + 'font-weight': 'bold', + }); + idText.attr({ + fill: '#AAA', + }); + messageText.node.style['white-space'] = 'pre'; + this.textWrap(messageText, boxWidth - 50); + const rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({ + fill: '#FFF', + stroke: '#000', + 'stroke-linecap': 'round', + 'stroke-width': 2, + }); + const tooltip = this.set(rect, textSet); + rect.attr({ + height: tooltip.getBBox().height + 10, + width: tooltip.getBBox().width + 10, + }); + tooltip.transform(['t', 20, 20]); + return tooltip; +}; + +Raphael.prototype.textWrap = function testWrap(t, width) { + const content = t.attr('text'); + const abc = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + t.attr({ + text: abc, + }); + const letterWidth = t.getBBox().width / abc.length; + t.attr({ + text: content, + }); + const words = content.split(' '); + let x = 0; + const s = []; + for (let j = 0, len = words.length; j < len; j += 1) { + const word = words[j]; + if (x + (word.length * letterWidth) > width) { + s.push('\n'); + x = 0; + } + if (word === '\n') { + s.push('\n'); + x = 0; + } else { + s.push(`${word} `); + x += word.length * letterWidth; + } + } + t.attr({ + text: s.join('').trim(), + }); + const b = t.getBBox(); + const h = Math.abs(b.y2) + 1; + return t.attr({ + y: h, + }); +}; + +export default Raphael; diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index df7a7d2a459..eeab69da941 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -312,7 +312,7 @@ require('./task_list'); */ Notes.prototype.renderDiscussionNote = function(note) { - var discussionContainer, form, note_html, row; + var discussionContainer, form, note_html, row, lineType, diffAvatarContainer; if (!this.isNewNote(note)) { return; } @@ -322,6 +322,8 @@ require('./task_list'); form = $("#new-discussion-note-form-" + note.original_discussion_id); } row = form.closest("tr"); + lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; + diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line'); note_html = $(note.html); note_html.renderGFM(); // is this the first note of discussion? @@ -330,10 +332,26 @@ require('./task_list'); discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']"); } if (discussionContainer.length === 0) { - // insert the note and the reply button after the temp row - row.after(note.diff_discussion_html); - // remove the note (will be added again below) - row.next().find(".note").remove(); + if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) { + // insert the note and the reply button after the temp row + row.after(note.diff_discussion_html); + + // remove the note (will be added again below) + row.next().find(".note").remove(); + } else { + // Merge new discussion HTML in + var $discussion = $(note.diff_discussion_html); + var $notes = $discussion.find('.notes[data-discussion-id="' + note.discussion_id + '"]'); + var contentContainerClass = '.' + $notes.closest('.notes_content') + .attr('class') + .split(' ') + .join('.'); + + // remove the note (will be added again below) + $notes.find('.note').remove(); + + row.find(contentContainerClass + ' .content').append($notes.closest('.content').children()); + } // Before that, the container didn't exist discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); // Add note to 'Changes' page discussions @@ -347,14 +365,40 @@ require('./task_list'); discussionContainer.append(note_html); } - if (typeof gl.diffNotesCompileComponents !== 'undefined') { + if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_id) { gl.diffNotesCompileComponents(); + this.renderDiscussionAvatar(diffAvatarContainer, note); } gl.utils.localTimeAgo($('.js-timeago'), false); return this.updateNotesCount(1); }; + Notes.prototype.getLineHolder = function(changesDiscussionContainer) { + return $(changesDiscussionContainer).closest('.notes_holder') + .prevAll('.line_holder') + .first() + .get(0); + }; + + Notes.prototype.renderDiscussionAvatar = function(diffAvatarContainer, note) { + var commentButton = diffAvatarContainer.find('.js-add-diff-note-button'); + var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders'); + + if (!avatarHolder.length) { + avatarHolder = document.createElement('diff-note-avatars'); + avatarHolder.setAttribute('discussion-id', note.discussion_id); + + diffAvatarContainer.append(avatarHolder); + + gl.diffNotesCompileComponents(); + } + + if (commentButton.length) { + commentButton.remove(); + } + }; + /* Called in response the main target form has been successfully submitted. @@ -592,9 +636,14 @@ require('./task_list'); */ Notes.prototype.removeNote = function(e) { - var noteId; - noteId = $(e.currentTarget).closest(".note").attr("id"); - $(".note[id='" + noteId + "']").each((function(_this) { + var noteElId, noteId, dataNoteId, $note, lineHolder; + $note = $(e.currentTarget).closest('.note'); + noteElId = $note.attr('id'); + noteId = $note.attr('data-note-id'); + lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]') + .closest('.notes_holder') + .prev('.line_holder'); + $(".note[id='" + noteElId + "']").each((function(_this) { // A same note appears in the "Discussion" and in the "Changes" tab, we have // to remove all. Using $(".note[id='noteId']") ensure we get all the notes, // where $("#noteId") would return only one. @@ -604,17 +653,26 @@ require('./task_list'); notes = note.closest(".notes"); if (typeof gl.diffNotesCompileComponents !== 'undefined') { - if (gl.diffNoteApps[noteId]) { - gl.diffNoteApps[noteId].$destroy(); + if (gl.diffNoteApps[noteElId]) { + gl.diffNoteApps[noteElId].$destroy(); } } + note.remove(); + // check if this is the last note for this line - if (notes.find(".note").length === 1) { + if (notes.find(".note").length === 0) { + var notesTr = notes.closest("tr"); + // "Discussions" tab notes.closest(".timeline-entry").remove(); - // "Changes" tab / commit view - notes.closest("tr").remove(); + + if (!_this.isParallelView() || notesTr.find('.note').length === 0) { + // "Changes" tab / commit view + notesTr.remove(); + } else { + notes.closest('.content').empty(); + } } return note.remove(); }; @@ -707,15 +765,16 @@ require('./task_list'); */ Notes.prototype.addDiffNote = function(e) { - var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent; + var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar; e.preventDefault(); - $link = $(e.currentTarget); + $link = $(e.currentTarget || e.target); row = $link.closest("tr"); nextRow = row.next(); hasNotes = nextRow.is(".notes_holder"); addForm = false; notesContentSelector = ".notes_content"; rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>"; + isDiffCommentAvatar = $link.hasClass('js-diff-comment-avatar'); // In parallel view, look inside the correct left/right pane if (this.isParallelView()) { lineType = $link.data("lineType"); @@ -723,7 +782,9 @@ require('./task_list'); rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line old\"></td><td class=\"notes_content parallel old\"><div class=\"content\"></div></td><td class=\"notes_line new\"></td><td class=\"notes_content parallel new\"><div class=\"content\"></div></td></tr>"; } notesContentSelector += " .content"; - if (hasNotes) { + notesContent = nextRow.find(notesContentSelector); + + if (hasNotes && !isDiffCommentAvatar) { nextRow.show(); notesContent = nextRow.find(notesContentSelector); if (notesContent.length) { @@ -740,13 +801,21 @@ require('./task_list'); } } } - } else { + } else if (!isDiffCommentAvatar) { // add a notes row and insert the form row.after(rowCssToAdd); nextRow = row.next(); notesContent = nextRow.find(notesContentSelector); addForm = true; + } else { + nextRow.show(); + notesContent.toggle(!notesContent.is(':visible')); + + if (!nextRow.find('.content:not(:empty)').is(':visible')) { + nextRow.hide(); + } } + if (addForm) { newForm = this.formClone.clone(); newForm.appendTo(notesContent); 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 6e8a5cc688b..fe8b37d2c6e 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -96,7 +96,7 @@ .dropdown-menu-toggle { @extend .dropdown-toggle; - padding-right: 20px; + padding-right: 25px; position: relative; width: 163px; text-overflow: ellipsis; @@ -159,12 +159,12 @@ li { text-align: left; list-style: none; - padding: 0 8px; + padding: 0 10px; } .divider { height: 1px; - margin: 8px; + margin: 6px 10px; padding: 0; background-color: $dropdown-divider-color; } @@ -181,7 +181,7 @@ display: block; position: relative; padding: 5px 8px; - color: $dropdown-link-color; + color: $gl-text-color; line-height: initial; text-overflow: ellipsis; border-radius: 2px; @@ -218,10 +218,12 @@ } .dropdown-header { - color: $gl-text-color-secondary; + color: $gl-text-color; font-size: 13px; + font-weight: 600; line-height: 22px; - padding: 0 10px; + text-transform: capitalize; + padding: 0 16px; } .separator + .dropdown-header { @@ -324,14 +326,17 @@ .dropdown-menu-selectable { a { - padding-left: 25px; + padding-left: 26px; &.is-indeterminate, &.is-active { + font-weight: 600; + color: $gl-text-color; + &::before { position: absolute; - left: 5px; - top: 8px; + left: 6px; + top: 6px; font: normal normal normal 14px/1 FontAwesome; font-size: inherit; text-rendering: auto; @@ -353,7 +358,7 @@ .dropdown-title { position: relative; - padding: 0 25px 10px; + padding: 2px 25px 10px; margin: 0 10px 10px; font-weight: 600; line-height: 1; @@ -383,7 +388,7 @@ right: 5px; width: 20px; height: 20px; - top: -3px; + top: -1px; } .dropdown-menu-back { 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 d2be8dc7a39..8f2150066c7 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -1,10 +1,24 @@ .filter-item { - margin-right: 6px; vertical-align: top; &.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) { @@ -14,6 +28,20 @@ width: 132px; } } + + .filter-item:not(:last-child) { + margin-right: 6px; + } + + .sort-filter { + display: inline-block; + float: right; + } + + .dropdown-menu-sort { + left: auto; + right: 0; + } } @media (max-width: $screen-xs-max) { @@ -21,6 +49,11 @@ display: block; margin: 0 0 10px; } + + .dropdown-menu-toggle, + .update-issues-btn .btn { + width: 100%; + } } .filtered-search-container { @@ -31,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 { @@ -38,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%; @@ -54,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 { @@ -76,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; @@ -98,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; @@ -192,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/nav.scss b/app/assets/stylesheets/framework/nav.scss index 674d3bb45aa..ea45aaa0253 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -294,7 +294,7 @@ .nav-control { @media (max-width: $screen-sm-max) { - margin-right: 75px; + margin-right: 2px; } } } 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/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index ea2d26dd5a0..12a86a64645 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -86,6 +86,16 @@ position: fixed; } +/* + * Fix <summary> elements on firefox + * See https://github.com/necolas/normalize.css/issues/640 + * and https://github.com/twbs/bootstrap/issues/21060 + * + */ +summary { + display: list-item; +} + @import "bootstrap/responsive-utilities"; // Labels 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/diff.scss b/app/assets/stylesheets/pages/diff.scss index 5d0c247dea8..eab79c2a481 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -113,6 +113,10 @@ td.line_content.parallel { width: 46%; } + + .add-diff-note { + margin-left: -55px; + } } .old_line, @@ -490,3 +494,103 @@ } } } + +.diff-comment-avatar-holders { + position: absolute; + height: 19px; + width: 19px; + margin-left: -15px; + + &:hover { + .diff-comment-avatar, + .diff-comments-more-count { + @for $i from 1 through 4 { + $x-pos: 14px; + + &:nth-child(#{$i}) { + @if $i == 4 { + $x-pos: 14.5px; + } + + transform: translateX((($i * $x-pos) - $x-pos)); + + &:hover { + transform: translateX((($i * $x-pos) - $x-pos)) scale(1.2); + } + } + } + } + + .diff-comments-more-count { + padding-left: 2px; + padding-right: 2px; + width: auto; + } + } +} + +.diff-comment-avatar, +.diff-comments-more-count { + position: absolute; + left: 0; + width: 19px; + height: 19px; + margin-right: 0; + border-color: $white-light; + cursor: pointer; + transition: all .1s ease-out; + + @for $i from 1 through 4 { + &:nth-child(#{$i}) { + z-index: (4 - $i); + } + } +} + +.diff-comments-more-count { + width: 19px; + min-width: 19px; + padding-left: 0; + padding-right: 0; + overflow: hidden; +} + +.diff-comments-more-count, +.diff-notes-collapse { + background-color: $gray-darkest; + color: $white-light; + border: 1px solid $white-light; + border-radius: 1em; + font-family: $regular_font; + font-size: 9px; + line-height: 17px; + text-align: center; +} + +.diff-notes-collapse { + position: relative; + width: 19px; + height: 19px; + padding: 0; + transition: transform .1s ease-out; + + svg { + position: absolute; + left: 50%; + top: 50%; + margin-left: -5.5px; + margin-top: -5.5px; + } + + path { + fill: $white-light; + } + + &:hover { + transform: scale(1.2); + } + + &:focus { + outline: 0; + } +} diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 77e09e66340..0e2b8dba780 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -143,3 +143,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/groups.scss b/app/assets/stylesheets/pages/groups.scss index d377526e655..84d21e48463 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -73,3 +73,19 @@ } } } + +.mattermost-icon svg { + width: 16px; + height: 16px; + vertical-align: text-bottom; +} + +.mattermost-team-name { + color: $gl-text-color-secondary; +} + +.mattermost-info { + display: block; + color: $gl-text-color-secondary; + margin-top: 10px; +} 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/todos.scss b/app/assets/stylesheets/pages/todos.scss index af9ddb9ff80..5f0aede4f5e 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -170,7 +170,11 @@ @media (max-width: $screen-sm-max) { .todos-filters { .dropdown-menu-toggle { - width: 135px; + width: 130px; + } + + .dropdown-menu-toggle-sort { + width: auto; } } } @@ -200,10 +204,6 @@ } .todos-filters { - .row-content-block { - padding-bottom: 50px; - } - .dropdown-menu-toggle { width: 100%; } 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/groups_controller.rb b/app/controllers/groups_controller.rb index 15db5b7762d..4663b6e7fc6 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -32,7 +32,13 @@ class GroupsController < Groups::ApplicationController @group = Groups::CreateService.new(current_user, group_params).execute if @group.persisted? - redirect_to @group, notice: "Group '#{@group.name}' was successfully created." + notice = if @group.chat_team.present? + "Group '#{@group.name}' and its Mattermost team were successfully created." + else + "Group '#{@group.name}' was successfully created." + end + + redirect_to @group, notice: notice else render action: "new" end @@ -142,7 +148,9 @@ class GroupsController < Groups::ApplicationController :request_access_enabled, :share_with_group_lock, :visibility_level, - :parent_id + :parent_id, + :create_chat_team, + :chat_team_name ] 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/appearance.rb b/app/models/appearance.rb index e4106e1c2e9..c79326e8427 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -10,4 +10,5 @@ class Appearance < ActiveRecord::Base mount_uploader :logo, AttachmentUploader mount_uploader :header_logo, AttachmentUploader + has_many :uploads, as: :model, dependent: :destroy end diff --git a/app/models/chat_team.rb b/app/models/chat_team.rb new file mode 100644 index 00000000000..c52b6f15913 --- /dev/null +++ b/app/models/chat_team.rb @@ -0,0 +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 f2989eff22d..3722047251d 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -63,6 +63,10 @@ module Ci end state_machine :status do + event :actionize do + transition created: :manual + end + after_transition any => [:pending] do |build| build.run_after_commit do BuildQueueWorker.perform_async(id) @@ -94,16 +98,21 @@ module Ci .fabricate! end - def manual? - self.when == 'manual' - end - def other_actions pipeline.manual_actions.where.not(name: name) end def playable? - project.builds_enabled? && commands.present? && manual? && skipped? + project.builds_enabled? && has_commands? && + action? && manual? + end + + def action? + self.when == 'manual' + end + + def has_commands? + commands.present? end def play(current_user) @@ -122,7 +131,7 @@ module Ci end def retryable? - project.builds_enabled? && commands.present? && + project.builds_enabled? && has_commands? && (success? || failed? || canceled?) end @@ -508,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 @@ -534,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 }, @@ -545,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 manual? + + 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/pipeline.rb b/app/models/ci/pipeline.rb index 80e11a5b58f..67206415f7b 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -49,6 +49,10 @@ module Ci transition any - [:canceled] => :canceled end + event :block do + transition any - [:manual] => :manual + end + # IMPORTANT # Do not add any operations to this state_machine # Create a separate worker for each new operation @@ -321,6 +325,7 @@ module Ci when 'failed' then drop when 'canceled' then cancel when 'skipped' then skip + when 'manual' then block end end 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/commit_status.rb b/app/models/commit_status.rb index fc750a3e5e9..7e23e14794f 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -29,9 +29,11 @@ class CommitStatus < ActiveRecord::Base end scope :exclude_ignored, -> do - # We want to ignore failed_but_allowed jobs + # We want to ignore failed but allowed to fail jobs. + # + # TODO, we also skip ignored optional manual actions. where("allow_failure = ? OR status IN (?)", - false, all_state_names - [:failed, :canceled]) + false, all_state_names - [:failed, :canceled, :manual]) end scope :retried, -> { where.not(id: latest) } @@ -42,11 +44,11 @@ class CommitStatus < ActiveRecord::Base state_machine :status do event :enqueue do - transition [:created, :skipped] => :pending + transition [:created, :skipped, :manual] => :pending end event :process do - transition skipped: :created + transition [:skipped, :manual] => :created end event :run do @@ -66,7 +68,7 @@ class CommitStatus < ActiveRecord::Base end event :cancel do - transition [:created, :pending, :running] => :canceled + transition [:created, :pending, :running, :manual] => :canceled end before_transition created: [:pending, :running] do |commit_status| @@ -86,7 +88,7 @@ class CommitStatus < ActiveRecord::Base commit_status.run_after_commit do pipeline.try do |pipeline| - if complete? + if complete? || manual? PipelineProcessWorker.perform_async(pipeline.id) else PipelineUpdateWorker.perform_async(pipeline.id) 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 aea359e70bb..5101cc7e687 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -2,22 +2,21 @@ module HasStatus extend ActiveSupport::Concern DEFAULT_STATUS = 'created'.freeze - AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped].freeze - STARTED_STATUSES = %w[running success failed skipped].freeze + BLOCKED_STATUS = 'manual'.freeze + AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped manual].freeze + 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[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 - scope = if respond_to?(:exclude_ignored) - exclude_ignored - else - all - end + scope = respond_to?(:exclude_ignored) ? exclude_ignored : all + builds = scope.select('count(*)').to_sql created = scope.created.select('count(*)').to_sql success = scope.success.select('count(*)').to_sql + manual = scope.manual.select('count(*)').to_sql pending = scope.pending.select('count(*)').to_sql running = scope.running.select('count(*)').to_sql skipped = scope.skipped.select('count(*)').to_sql @@ -30,7 +29,8 @@ module HasStatus WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success' WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled' WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending' - WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running' + WHEN (#{running})+(#{pending})>0 THEN 'running' + WHEN (#{manual})>0 THEN 'manual' ELSE 'failed' END)" end @@ -63,6 +63,7 @@ module HasStatus state :success, value: 'success' state :canceled, value: 'canceled' state :skipped, value: 'skipped' + state :manual, value: 'manual' end scope :created, -> { where(status: 'created') } @@ -73,12 +74,13 @@ module HasStatus scope :failed, -> { where(status: 'failed') } scope :canceled, -> { where(status: 'canceled') } scope :skipped, -> { where(status: 'skipped') } + scope :manual, -> { where(status: 'manual') } scope :running_or_pending, -> { where(status: [:running, :pending]) } scope :finished, -> { where(status: [:success, :failed, :canceled]) } scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) } scope :cancelable, -> do - where(status: [:running, :pending, :created]) + where(status: [:running, :pending, :created, :manual]) end end @@ -94,6 +96,10 @@ module HasStatus COMPLETED_STATUSES.include?(status) end + def blocked? + BLOCKED_STATUS == status + end + private def calculate_duration 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/group.rb b/app/models/group.rb index 7d23f655225..bd0ecae3da4 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -28,6 +28,7 @@ class Group < Namespace validates :avatar, file_size: { maximum: 200.kilobytes.to_i } mount_uploader :avatar, AvatarUploader + has_many :uploads, as: :model, dependent: :destroy after_create :post_create_hook after_destroy :post_destroy_hook @@ -212,4 +213,14 @@ class Group < Namespace def users_with_parents User.where(id: members_with_parents.select(:user_id)) end + + def mattermost_team_params + max_length = 59 + + { + name: path[0..max_length], + display_name: name[0..max_length], + type: public? ? 'O' : 'I' # Open vs Invite-only + } + end end 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/namespace.rb b/app/models/namespace.rb index 3137dd32f93..d350f1d6770 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -20,6 +20,7 @@ class Namespace < ActiveRecord::Base belongs_to :parent, class_name: "Namespace" has_many :children, class_name: "Namespace", foreign_key: :parent_id + has_one :chat_team, dependent: :destroy validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :name, 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 1ac4a178a9b..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" @@ -212,6 +213,7 @@ class Project < ActiveRecord::Base before_save :ensure_runners_token mount_uploader :avatar, AvatarUploader + has_many :uploads, as: :model, dependent: :destroy # Scopes default_scope { where(pending_delete: false) } @@ -391,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 @@ -770,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/upload.rb b/app/models/upload.rb new file mode 100644 index 00000000000..13987931b05 --- /dev/null +++ b/app/models/upload.rb @@ -0,0 +1,63 @@ +class Upload < ActiveRecord::Base + # Upper limit for foreground checksum processing + CHECKSUM_THRESHOLD = 100.megabytes + + belongs_to :model, polymorphic: true + + validates :size, presence: true + validates :path, presence: true + validates :model, presence: true + validates :uploader, presence: true + + before_save :calculate_checksum, if: :foreground_checksum? + after_commit :schedule_checksum, unless: :foreground_checksum? + + def self.remove_path(path) + where(path: path).destroy_all + end + + def self.record(uploader) + remove_path(uploader.relative_path) + + create( + size: uploader.file.size, + path: uploader.relative_path, + model: uploader.model, + uploader: uploader.class.to_s + ) + end + + def absolute_path + return path unless relative_path? + + uploader_class.absolute_path(self) + end + + def calculate_checksum + return unless exist? + + self.checksum = Digest::SHA256.file(absolute_path).hexdigest + end + + def exist? + File.exist?(absolute_path) + end + + private + + def foreground_checksum? + size <= CHECKSUM_THRESHOLD + end + + def schedule_checksum + UploadChecksumWorker.perform_async(id) + end + + def relative_path? + !path.start_with?('/') + end + + def uploader_class + Object.const_get(uploader) + end +end diff --git a/app/models/user.rb b/app/models/user.rb index dfba51d3b00..76fb4cd470e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -191,6 +191,7 @@ class User < ActiveRecord::Base end mount_uploader :avatar, AvatarUploader + has_many :uploads, as: :model, dependent: :destroy # Scopes scope :admins, -> { where(admin: true) } @@ -324,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/serializers/build_entity.rb b/app/serializers/build_entity.rb index b5384e6462b..5bcbe285052 100644 --- a/app/serializers/build_entity.rb +++ b/app/serializers/build_entity.rb @@ -12,7 +12,7 @@ class BuildEntity < Grape::Entity path_to(:retry_namespace_project_build, build) end - expose :play_path, if: ->(build, _) { build.manual? } do |build| + expose :play_path, if: ->(build, _) { build.playable? } do |build| path_to(:play_namespace_project_build, build) 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/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 79eb97b7b55..2935d00c075 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -22,6 +22,8 @@ module Ci def process_stage(index) current_status = status_for_prior_stages(index) + return if HasStatus::BLOCKED_STATUS == current_status + if HasStatus::COMPLETED_STATUSES.include?(current_status) created_builds_in_stage(index).select do |build| Gitlab::OptimisticLocking.retry_lock(build) do |subject| @@ -33,7 +35,7 @@ module Ci def process_build(build, current_status) if valid_statuses_for_when(build.when).include?(current_status) - build.enqueue + build.action? ? build.actionize : build.enqueue true else build.skip @@ -49,6 +51,8 @@ module Ci %w[failed] when 'always' %w[success failed skipped] + when 'manual' + %w[success] else [] 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/groups/create_service.rb b/app/services/groups/create_service.rb index febeb661fb5..c4e9b8fd8e0 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -2,6 +2,7 @@ module Groups class CreateService < Groups::BaseService def initialize(user, params = {}) @current_user, @params = user, params.dup + @chat_team = @params.delete(:create_chat_team) end def execute @@ -20,9 +21,23 @@ module Groups end @group.name ||= @group.path.dup + + if create_chat_team? + response = Mattermost::CreateTeamService.new(@group, current_user).execute + return @group if @group.errors.any? + + @group.build_chat_team(name: response['name'], team_id: response['id']) + end + @group.save @group.add_owner(current_user) @group end + + private + + def create_chat_team? + Gitlab.config.mattermost.enabled && @chat_team && group.chat_team.nil? + end end end 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/services/mattermost/create_team_service.rb b/app/services/mattermost/create_team_service.rb new file mode 100644 index 00000000000..e3206810f3a --- /dev/null +++ b/app/services/mattermost/create_team_service.rb @@ -0,0 +1,14 @@ +module Mattermost + class CreateTeamService < ::BaseService + def initialize(group, current_user) + @group, @current_user = group, current_user + end + + def execute + # The user that creates the team will be Team Admin + Mattermost::Team.new(current_user).create(@group.mattermost_team_params) + rescue Mattermost::ClientError => e + @group.errors.add(:mattermost_team, e.message) + end + end +end diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index 6aa1f5a8c50..109eb2fea0b 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -1,4 +1,5 @@ class AttachmentUploader < GitlabUploader + include RecordsUploads include UploaderHelper storage :file diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index b4c393c6f2c..66d3bcb998a 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -1,4 +1,5 @@ class AvatarUploader < GitlabUploader + include RecordsUploads include UploaderHelper storage :file diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 0d2edaeff3b..d6ccf0dc92c 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -1,9 +1,31 @@ class FileUploader < GitlabUploader + include RecordsUploads include UploaderHelper + MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)} storage :file + def self.absolute_path(upload_record) + File.join( + self.dynamic_path_segment(upload_record.model), + upload_record.path + ) + end + + # Returns the part of `store_dir` that can change based on the model's current + # path + # + # This is used to build Upload paths dynamically based on the model's current + # namespace and path, allowing us to ignore renames or transfers. + # + # model - Object that responds to `path_with_namespace` + # + # Returns a String without a trailing slash + def self.dynamic_path_segment(model) + File.join(CarrierWave.root, base_dir, model.path_with_namespace) + end + attr_accessor :project attr_reader :secret @@ -13,13 +35,21 @@ class FileUploader < GitlabUploader end def store_dir - File.join(base_dir, @project.path_with_namespace, @secret) + File.join(dynamic_path_segment, @secret) end def cache_dir File.join(base_dir, 'tmp', @project.path_with_namespace, @secret) end + def model + project + end + + def relative_path + self.file.path.sub("#{dynamic_path_segment}/", '') + end + def to_markdown to_h[:markdown] end @@ -40,6 +70,10 @@ class FileUploader < GitlabUploader private + def dynamic_path_segment + self.class.dynamic_path_segment(model) + end + def generate_secret SecureRandom.hex end diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb index bd7de4ed562..d662ba6820c 100644 --- a/app/uploaders/gitlab_uploader.rb +++ b/app/uploaders/gitlab_uploader.rb @@ -1,4 +1,8 @@ class GitlabUploader < CarrierWave::Uploader::Base + def self.absolute_path(upload_record) + File.join(CarrierWave.root, upload_record.path) + end + def self.base_dir 'uploads' end @@ -18,4 +22,15 @@ class GitlabUploader < CarrierWave::Uploader::Base def move_to_store true end + + # Designed to be overridden by child uploaders that have a dynamic path + # segment -- that is, a path that changes based on mutable attributes of its + # associated model + # + # For example, `FileUploader` builds the storage path based on the associated + # project model's `path_with_namespace` value, which can change when the + # project or its containing namespace is moved or renamed. + def relative_path + self.file.path.sub("#{root}/", '') + end end diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb new file mode 100644 index 00000000000..4c127f29250 --- /dev/null +++ b/app/uploaders/records_uploads.rb @@ -0,0 +1,34 @@ +module RecordsUploads + extend ActiveSupport::Concern + + included do + after :store, :record_upload + before :remove, :destroy_upload + end + + private + + # After storing an attachment, create a corresponding Upload record + # + # NOTE: We're ignoring the argument passed to this callback because we want + # the `SanitizedFile` object from `CarrierWave::Uploader::Base#file`, not the + # `Tempfile` object the callback gets. + # + # Called `after :store` + def record_upload(_tempfile) + return unless file_storage? + return unless file.exists? + + Upload.record(self) + end + + # Before removing an attachment, destroy any Upload records at the same path + # + # Called `before :remove` + def destroy_upload(*args) + return unless file_storage? + return unless file + + Upload.remove_path(relative_path) + end +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/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 9a4e423f896..10867140d4f 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -6,10 +6,8 @@ .top-area = render 'shared/issuable/nav', type: :issues .nav-controls - = link_to params.merge(rss_url_options), class: 'btn' do + = link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do = icon('rss') - %span.icon-label - Subscribe = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" = render 'shared/issuable/filter', type: :issues diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 16a5713948a..d7e0a8e4b2c 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -46,16 +46,16 @@ = hidden_field_tag(:action_id, params[:action_id]) = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', data: { data: todo_actions_options, default_label: 'Action' } }) - .pull-right - .dropdown.inline.prepend-left-10 - %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } + .filter-item.sort-filter + .dropdown + %button.dropdown-menu-toggle.dropdown-menu-toggle-sort{ type: 'button', 'data-toggle' => 'dropdown' } %span.light - if @sort.present? = sort_options_hash[@sort] - else = sort_title_recently_created = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort + %ul.dropdown-menu.dropdown-menu-sort %li = link_to todos_filter_path(sort: sort_value_priority) do = sort_title_priority diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml index 2deadbeeceb..ee452add394 100644 --- a/app/views/discussions/_diff_discussion.html.haml +++ b/app/views/discussions/_diff_discussion.html.haml @@ -2,5 +2,5 @@ %tr.notes_holder{ class: ('hide' unless expanded) } %td.notes_line{ colspan: 2 } %td.notes_content - .content + .content{ class: ('hide' unless expanded) } = render "discussions/notes", discussion: discussion 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/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml new file mode 100644 index 00000000000..20de1b4c973 --- /dev/null +++ b/app/views/groups/_create_chat_team.html.haml @@ -0,0 +1,16 @@ +.form-group + = f.label :create_chat_team, class: 'control-label' do + %span.mattermost-icon + = custom_icon('icon_mattermost') + Mattermost + .col-sm-10 + .checkbox.js-toggle-container + = f.label :create_chat_team do + .js-toggle-button= f.check_box(:create_chat_team, { checked: true }, true, false) + Create a Mattermost team for this group + %br + %small.light.js-toggle-content + Mattermost URL: + = Settings.mattermost.host + %span> / + %span{ "data-bind-out" => "create_chat_team" } diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 38d63fd9acc..000c7af2326 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -16,6 +16,8 @@ = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group + = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled + .form-group .col-sm-offset-2.col-sm-10 = render 'shared/group_tips' 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/_activity.html.haml b/app/views/projects/_activity.html.haml index fb990dd9592..aa0cb3e1a50 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -1,5 +1,4 @@ - @no_container = true -= render "projects/head" %div{ class: container_class } .nav-block.activity-filter-block diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml index 3c0f01cbf6f..27c8e3c7fca 100644 --- a/app/views/projects/activity.html.haml +++ b/app/views/projects/activity.html.haml @@ -1,4 +1,5 @@ - page_title "Activity" += render "projects/head" = render 'projects/last_push' 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/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 5ea85f9fd4c..09286a1b3c6 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -46,7 +46,7 @@ %span.label.label-info triggered - if build.try(:allow_failure) %span.label.label-danger allowed to fail - - if build.manual? + - if build.action? %span.label.label-info manual - if pipeline_link 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/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index a680b1ca017..506246f2ee6 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -1,9 +1,9 @@ - if can?(current_user, :create_deployment, deployment) - actions = deployment.manual_actions - if actions.present? - .inline + .btn-group .dropdown - %a.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' } + %button.dropdown.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' } = custom_icon('icon_play') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right @@ -12,4 +12,3 @@ = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do = custom_icon('icon_play') %span= action.name.humanize - diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index c468202569f..260c9023daf 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -17,6 +17,6 @@ #{time_ago_with_tooltip(deployment.created_at)} %td.hidden-xs - .pull-right + .pull-right.btn-group = render 'projects/deployments/actions', deployment: deployment = render 'projects/deployments/rollback', deployment: deployment diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index cd18ba2ed00..ed279cfe168 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -1,8 +1,11 @@ - email = local_assigns.fetch(:email, false) - plain = local_assigns.fetch(:plain, false) +- discussions = local_assigns.fetch(:discussions, nil) - type = line.type - line_code = diff_file.line_code(line) -%tr.line_holder{ plain ? { class: type} : { class: type, id: line_code } } +- if discussions && !line.meta? + - discussion = discussions[line_code] +%tr.line_holder{ class: type, id: (line_code unless plain) } - case type - when 'match' = diff_match_line line.old_pos, line.new_pos, text: line.text @@ -11,12 +14,14 @@ %td.new_line.diff-line-num %td.line_content.match= line.text - else - %td.old_line.diff-line-num{ class: type, data: { linenumber: line.old_pos } } + %td.old_line.diff-line-num.js-avatar-container{ class: type, data: { linenumber: line.old_pos } } - link_text = type == "new" ? " " : line.old_pos - if plain = link_text - else %a{ href: "##{line_code}", data: { linenumber: link_text } } + - if discussion && !plain + %diff-note-avatars{ "discussion-id" => discussion.id } %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } - link_text = type == "old" ? " " : line.new_pos - if plain @@ -29,9 +34,6 @@ - else = diff_line_content(line.text) -- discussions = local_assigns.fetch(:discussions, nil) -- if discussions && !line.meta? - - discussion = discussions[line_code] - - if discussion - - discussion_expanded = local_assigns.fetch(:discussion_expanded, discussion.expanded?) - = render "discussions/diff_discussion", discussion: discussion, expanded: discussion_expanded +- if discussion + - discussion_expanded = local_assigns.fetch(:discussion_expanded, discussion.expanded?) + = render "discussions/diff_discussion", discussion: discussion, expanded: discussion_expanded diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 997bf0fc560..6448748113b 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -4,6 +4,9 @@ - diff_file.parallel_diff_lines.each do |line| - left = line[:left] - right = line[:right] + - last_line = right.new_pos if right + - unless @diff_notes_disabled + - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file) %tr.line_holder.parallel - if left - case left.type @@ -15,8 +18,10 @@ - else - left_line_code = diff_file.line_code(left) - left_position = diff_file.position(left) - %td.old_line.diff-line-num{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } + %td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } %a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } } + - if discussion_left + %diff-note-avatars{ "discussion-id" => discussion_left.id } %td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text) - else %td.old_line.diff-line-num.empty-cell @@ -32,17 +37,17 @@ - else - right_line_code = diff_file.line_code(right) - right_position = diff_file.position(right) - %td.new_line.diff-line-num{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } + %td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } %a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } } + - if discussion_right + %diff-note-avatars{ "discussion-id" => discussion_right.id } %td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text) - else %td.old_line.diff-line-num.empty-cell %td.line_content.parallel - - unless @diff_notes_disabled - - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file) - - if discussion_left || discussion_right - = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right + - if discussion_left || discussion_right + = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right - if !diff_file.new_file && !diff_file.deleted_file && diff_file.diff_lines.any? - last_line = diff_file.diff_lines.last - if last_line.new_pos < total_lines 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..f463a429f65 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) @@ -15,7 +16,7 @@ - if can?(current_user, :create_deployment, @environment) && @environment.can_stop? = link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post - .deployments-container + .environments-container - if @deployments.blank? .blank-state.blank-state-no-icon %h2.blank-state-title 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/network/show.html.haml b/app/views/projects/network/show.html.haml index a4a24a217d3..ed6077f6c6b 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,6 +1,5 @@ - page_title "Graph", @ref - content_for :page_specific_javascripts do - = page_specific_javascript_tag('lib/raphael.js') = page_specific_javascript_bundle_tag('network') = render "projects/commits/head" = render "head" diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index a73e8f345e0..a7618370a5d 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -2,7 +2,7 @@ - return if note.cross_reference_not_visible_for?(current_user) - note_editable = note_editable?(note) -%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} } +%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} } .timeline-entry-inner .timeline-icon %a{ href: user_path(note.author) } @@ -30,11 +30,15 @@ - if note.resolvable? - can_resolve = can?(current_user, :resolve_note, note) - %resolve-btn{ "discussion-id" => "#{note.discussion_id}", + %resolve-btn{ "project-path" => project_path(note.project), + "discussion-id" => note.discussion_id, ":note-id" => note.id, ":resolved" => note.resolved?, ":can-resolve" => can_resolve, - "resolved-by" => "#{note.resolved_by.try(:name)}", + ":author-name" => "'#{j(note.author.name)}'", + "author-avatar" => note.author.avatar_url, + ":note-truncated" => "'#{truncate(note.note, length: 17)}'", + ":resolved-by" => "'#{j(note.resolved_by.try(:name))}'", "v-show" => "#{can_resolve || note.resolved?}", "inline-template" => true, "ref" => "note_#{note.id}" } 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/_group_form.html.haml b/app/views/shared/_group_form.html.haml index 02b7b2447ed..c2d9ac87b20 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -18,7 +18,8 @@ = f.text_field :path, placeholder: 'open-source', class: 'form-control', autofocus: local_assigns[:autofocus] || false, required: true, pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS, - title: 'Please choose a group name with no special characters.' + title: 'Please choose a group name with no special characters.', + "data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}" - if parent = f.hidden_field :parent_id, value: parent.id diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 1744a597c51..bd994cdad01 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -45,11 +45,11 @@ - if current_user && defined?(@project) .label-subscription.inline - if label.is_a?(ProjectLabel) - %button.js-subscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', title: label_subscription_toggle_button_text(label, @project), data: { toggle: 'tooltip', status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } + %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', data: { status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } %span= label_subscription_toggle_button_text(label, @project) = icon('spinner spin', class: 'label-subscribe-button-loading') - else - %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', class: ('hidden' if status.unsubscribed?), title: 'Unsubscribe', data: { toggle: 'tooltip', url: group_label_unsubscribe_path(label, @project) } } + %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', class: ('hidden' if status.unsubscribed?), data: { url: group_label_unsubscribe_path(label, @project) } } %span Unsubscribe = icon('spinner spin', class: 'label-subscribe-button-loading') 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/icons/_collapse.svg.erb b/app/views/shared/icons/_collapse.svg.erb new file mode 100644 index 00000000000..917753fb343 --- /dev/null +++ b/app/views/shared/icons/_collapse.svg.erb @@ -0,0 +1 @@ +<svg width="<%= size %>" height="<%= size %>" viewBox="0 0 9 13"><path d="M2.57568253,6.49866948 C2.50548852,6.57199715 2.44637866,6.59708255 2.39835118,6.57392645 C2.3503237,6.55077034 2.32631032,6.48902165 2.32631032,6.38867852 L2.32631032,-2.13272614 C2.32631032,-2.23306927 2.3503237,-2.29481796 2.39835118,-2.31797406 C2.44637866,-2.34113017 2.50548852,-2.31604477 2.57568253,-2.24271709 L6.51022184,1.86747129 C6.53977721,1.8983461 6.56379059,1.93500939 6.5822627,1.97746225 L6.5822627,2.27849013 C6.56379059,2.31708364 6.53977721,2.35374693 6.51022184,2.38848109 L2.57568253,6.49866948 Z" transform="translate(4.454287, 2.127976) rotate(90.000000) translate(-4.454287, -2.127976) "></path><path d="M3.74312342,2.09553332 C3.74312342,1.99519019 3.77821989,1.9083561 3.8484139,1.83502843 C3.91860791,1.76170075 4.00173115,1.72503747 4.09778611,1.72503747 L4.80711151,1.72503747 C4.90316647,1.72503747 4.98628971,1.76170075 5.05648372,1.83502843 C5.12667773,1.9083561 5.16177421,1.99519019 5.16177421,2.09553332 L5.16177421,10.2464421 C5.16177421,10.3467853 5.12667773,10.4336194 5.05648372,10.506947 C4.98628971,10.5802747 4.90316647,10.616938 4.80711151,10.616938 L4.09778611,10.616938 C4.00173115,10.616938 3.91860791,10.5802747 3.8484139,10.506947 C3.77821989,10.4336194 3.74312342,10.3467853 3.74312342,10.2464421 L3.74312342,2.09553332 Z" transform="translate(4.452449, 6.170988) rotate(-90.000000) translate(-4.452449, -6.170988) "></path><path d="M2.57568253,14.6236695 C2.50548852,14.6969971 2.44637866,14.7220826 2.39835118,14.6989264 C2.3503237,14.6757703 2.32631032,14.6140216 2.32631032,14.5136785 L2.32631032,5.99227386 C2.32631032,5.89193073 2.3503237,5.83018204 2.39835118,5.80702594 C2.44637866,5.78386983 2.50548852,5.80895523 2.57568253,5.88228291 L6.51022184,9.99247129 C6.53977721,10.0233461 6.56379059,10.0600094 6.5822627,10.1024622 L6.5822627,10.4034901 C6.56379059,10.4420836 6.53977721,10.4787469 6.51022184,10.5134811 L2.57568253,14.6236695 Z" transform="translate(4.454287, 10.252976) scale(1, -1) rotate(90.000000) translate(-4.454287, -10.252976) "></path></svg> diff --git a/app/views/shared/icons/_icon_mattermost.svg b/app/views/shared/icons/_icon_mattermost.svg new file mode 100644 index 00000000000..d1c541523ab --- /dev/null +++ b/app/views/shared/icons/_icon_mattermost.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><path d="M250.05 34c1.9.04 3.8.11 5.6.2l-29.79 35.51c-.07.01-.15.03-.23.04C149.26 84.1 98.22 146.5 98.22 222.97c0 41.56 23.07 90.5 59.75 119.1 28.61 22.32 64.29 36.9 101.21 36.9 93.4 0 160.15-68.61 160.15-156 0-34.91-15.99-72.77-41.76-100.76l-1.63-47.39c54.45 39.15 89.95 103.02 90.06 175.17v.01c0 119.29-96.7 216-216 216-119.29 0-216-96.71-216-216S130.71 34 250 34h.05zm64.1 20.29c.66-.04 1.32.03 1.96.25 3.01 1 3.85 3.57 3.93 6.45l3.84 146.88c.76 28.66-17.16 68.44-60.39 68.56-30.97.08-63.68-20.83-63.68-60.13.01-14.73 5.61-31.26 19.25-48.11l90.03-111.18c1.15-1.42 3.08-2.58 5.06-2.72z"/></svg> 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/app/workers/upload_checksum_worker.rb b/app/workers/upload_checksum_worker.rb new file mode 100644 index 00000000000..78931f1258f --- /dev/null +++ b/app/workers/upload_checksum_worker.rb @@ -0,0 +1,12 @@ +class UploadChecksumWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def perform(upload_id) + upload = Upload.find(upload_id) + upload.calculate_checksum + upload.save! + rescue ActiveRecord::RecordNotFound + Rails.logger.error("UploadChecksumWorker: couldn't find upload #{upload_id}, skipping") + 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/21605-allow-html5-details.yml b/changelogs/unreleased/21605-allow-html5-details.yml new file mode 100644 index 00000000000..b0c654783d9 --- /dev/null +++ b/changelogs/unreleased/21605-allow-html5-details.yml @@ -0,0 +1,4 @@ +--- +title: SanitizationFilter allows html5 details and summary tags +merge_request: 6568 +author: diff --git a/changelogs/unreleased/22562-todos-filters.yml b/changelogs/unreleased/22562-todos-filters.yml new file mode 100644 index 00000000000..9cca138744a --- /dev/null +++ b/changelogs/unreleased/22562-todos-filters.yml @@ -0,0 +1,4 @@ +--- +title: Fix Sort dropdown reflow issue +merge_request: 9533 +author: Jarkko Tuunanen 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/24998-fix-typo-gitlab-config-file.yml b/changelogs/unreleased/24998-fix-typo-gitlab-config-file.yml new file mode 100644 index 00000000000..3b90466e3af --- /dev/null +++ b/changelogs/unreleased/24998-fix-typo-gitlab-config-file.yml @@ -0,0 +1,4 @@ +--- +title: Fix typo in Gitlab config file +merge_request: 9702 +author: medied 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/26202-change-dropdown-style-slightly.yml b/changelogs/unreleased/26202-change-dropdown-style-slightly.yml new file mode 100644 index 00000000000..827224abf5a --- /dev/null +++ b/changelogs/unreleased/26202-change-dropdown-style-slightly.yml @@ -0,0 +1,4 @@ +--- +title: Changed dropdown style slightly +merge_request: +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/29034-fix-github-importer.yml b/changelogs/unreleased/29034-fix-github-importer.yml new file mode 100644 index 00000000000..6d08db3d55d --- /dev/null +++ b/changelogs/unreleased/29034-fix-github-importer.yml @@ -0,0 +1,4 @@ +--- +title: Fix name colision when importing GitHub pull requests from forked repositories +merge_request: 9719 +author: diff --git a/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml b/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml new file mode 100644 index 00000000000..ad0c513f525 --- /dev/null +++ b/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml @@ -0,0 +1,4 @@ +--- +title: Refactor dropdown_milestone_spec.rb +merge_request: +author: George Andrinopoulos 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/backup_storage_class.yml b/changelogs/unreleased/backup_storage_class.yml new file mode 100644 index 00000000000..fc9989fc251 --- /dev/null +++ b/changelogs/unreleased/backup_storage_class.yml @@ -0,0 +1,4 @@ +--- +title: Add storage class configuration option for Amazon S3 remote backups +merge_request: +author: Jon Keys 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/es6-class-issue.yml b/changelogs/unreleased/es6-class-issue.yml new file mode 100644 index 00000000000..9d1c3ac7421 --- /dev/null +++ b/changelogs/unreleased/es6-class-issue.yml @@ -0,0 +1,4 @@ +--- +title: Convert Issue into ES6 class +merge_request: 9636 +author: winniehell 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-29093.yml b/changelogs/unreleased/fix-29093.yml new file mode 100644 index 00000000000..791129afe93 --- /dev/null +++ b/changelogs/unreleased/fix-29093.yml @@ -0,0 +1,4 @@ +--- +title: Fix 'Object not found - no match for id (sha)' when importing GitHub Pull Requests +merge_request: +author: 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/pipeline-blocking-actions.yml b/changelogs/unreleased/pipeline-blocking-actions.yml new file mode 100644 index 00000000000..6bde501de18 --- /dev/null +++ b/changelogs/unreleased/pipeline-blocking-actions.yml @@ -0,0 +1,4 @@ +--- +title: Make it possible to configure blocking manual actions +merge_request: 9585 +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/remove-subscribe-label-tooltip.yml b/changelogs/unreleased/remove-subscribe-label-tooltip.yml new file mode 100644 index 00000000000..90b71d3be51 --- /dev/null +++ b/changelogs/unreleased/remove-subscribe-label-tooltip.yml @@ -0,0 +1,4 @@ +--- +title: Remove tooltips from label subscription buttons +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 8003c055b27..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" @@ -101,7 +100,6 @@ module Gitlab config.assets.precompile << "katex.js" config.assets.precompile << "xterm/xterm.css" config.assets.precompile << "lib/ace.js" - config.assets.precompile << "lib/raphael.js" config.assets.precompile << "u2f.js" config.assets.precompile << "vendor/assets/fonts/*" diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 8f99a4d541f..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: @@ -483,6 +484,8 @@ production: &base # multipart_chunk_size: 104857600 # # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional # # encryption: 'AES256' + # # Specifies Amazon S3 storage class to use for backups, this is optional + # # storage_class: 'STANDARD' ## GitLab Shell settings gitlab_shell: @@ -572,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: @@ -586,7 +590,7 @@ test: new_issue_url: "http://redmine/projects/:issues_tracker_id/issues/new" jira: title: "JIRA" - url: https://sample_company.atlasian.net + url: https://sample_company.atlassian.net project_key: PROJECT ldap: enabled: false diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 5aeaa7eab81..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 @@ -399,6 +404,7 @@ if Settings.backup['upload']['connection'] end Settings.backup['upload']['multipart_chunk_size'] ||= 104857600 Settings.backup['upload']['encryption'] ||= nil +Settings.backup['upload']['storage_class'] ||= nil # # Git 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 97620cc9c7f..9d2066a6490 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -29,6 +29,7 @@ - [email_receiver, 2] - [emails_on_push, 2] - [mailers, 2] + - [upload_checksum, 1] - [use_key, 1] - [repository_fork, 1] - [repository_import, 1] @@ -51,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/20170120131253_create_chat_teams.rb b/db/migrate/20170120131253_create_chat_teams.rb new file mode 100644 index 00000000000..7995d383986 --- /dev/null +++ b/db/migrate/20170120131253_create_chat_teams.rb @@ -0,0 +1,18 @@ +class CreateChatTeams < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + DOWNTIME_REASON = "Adding a foreign key" + + disable_ddl_transaction! + + def change + create_table :chat_teams do |t| + t.references :namespace, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade } + t.string :team_id + t.string :name + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20170130221926_create_uploads.rb b/db/migrate/20170130221926_create_uploads.rb new file mode 100644 index 00000000000..6f06c5dd840 --- /dev/null +++ b/db/migrate/20170130221926_create_uploads.rb @@ -0,0 +1,20 @@ +class CreateUploads < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :uploads do |t| + t.integer :size, limit: 8, null: false + t.string :path, null: false + t.string :checksum, limit: 64 + t.references :model, polymorphic: true + t.string :uploader, null: false + t.datetime :created_at, null: false + end + + add_index :uploads, :path + add_index :uploads, :checksum + add_index :uploads, [:model_id, :model_type] + 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/post_migrate/20170306170512_migrate_legacy_manual_actions.rb b/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb new file mode 100644 index 00000000000..9020e0d054c --- /dev/null +++ b/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb @@ -0,0 +1,19 @@ +class MigrateLegacyManualActions < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + execute <<-EOS + UPDATE ci_builds SET status = 'manual', allow_failure = true + WHERE ci_builds.when = 'manual' AND ci_builds.status = 'skipped'; + EOS + end + + def down + execute <<-EOS + UPDATE ci_builds SET status = 'skipped', allow_failure = false + WHERE ci_builds.when = 'manual' AND ci_builds.status = 'manual'; + EOS + end +end diff --git a/db/schema.rb b/db/schema.rb index 911cb22c8e5..3ec5461f600 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170305203726) do +ActiveRecord::Schema.define(version: 20170306170512) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -61,6 +61,7 @@ ActiveRecord::Schema.define(version: 20170305203726) 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: 20170305203726) 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" @@ -175,6 +175,16 @@ ActiveRecord::Schema.define(version: 20170305203726) do add_index "chat_names", ["service_id", "team_id", "chat_id"], name: "index_chat_names_on_service_id_and_team_id_and_chat_id", unique: true, using: :btree add_index "chat_names", ["user_id", "service_id"], name: "index_chat_names_on_user_id_and_service_id", unique: true, using: :btree + create_table "chat_teams", force: :cascade do |t| + t.integer "namespace_id", null: false + t.string "team_id" + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "chat_teams", ["namespace_id"], name: "index_chat_teams_on_namespace_id", unique: true, using: :btree + create_table "ci_application_settings", force: :cascade do |t| t.boolean "all_broken_builds" t.boolean "add_pusher" @@ -520,6 +530,7 @@ ActiveRecord::Schema.define(version: 20170305203726) 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 @@ -531,6 +542,7 @@ ActiveRecord::Schema.define(version: 20170305203726) 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"} @@ -761,8 +773,8 @@ ActiveRecord::Schema.define(version: 20170305203726) 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 @@ -868,6 +880,11 @@ ActiveRecord::Schema.define(version: 20170305203726) 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" @@ -884,10 +901,11 @@ ActiveRecord::Schema.define(version: 20170305203726) 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 @@ -1214,6 +1232,20 @@ ActiveRecord::Schema.define(version: 20170305203726) do add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree + create_table "uploads", force: :cascade do |t| + t.integer "size", limit: 8, null: false + t.string "path", null: false + t.string "checksum", limit: 64 + t.integer "model_id" + t.string "model_type" + t.string "uploader", null: false + t.datetime "created_at", null: false + end + + add_index "uploads", ["checksum"], name: "index_uploads_on_checksum", using: :btree + add_index "uploads", ["model_id", "model_type"], name: "index_uploads_on_model_id_and_model_type", using: :btree + add_index "uploads", ["path"], name: "index_uploads_on_path", using: :btree + create_table "user_agent_details", force: :cascade do |t| t.string "user_agent", null: false t.string "ip_address", null: false @@ -1338,6 +1370,7 @@ ActiveRecord::Schema.define(version: 20170305203726) do add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_foreign_key "boards", "projects" + add_foreign_key "chat_teams", "namespaces", on_delete: :cascade add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade add_foreign_key "issue_metrics", "issues", on_delete: :cascade add_foreign_key "label_priorities", "labels", on_delete: :cascade @@ -1349,6 +1382,7 @@ ActiveRecord::Schema.define(version: 20170305203726) 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/README.md b/doc/README.md index 46a1ed0e148..57d85d770e7 100644 --- a/doc/README.md +++ b/doc/README.md @@ -19,7 +19,7 @@ - [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab. - [Permissions](user/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do. - [Profile Settings](profile/README.md) -- [Project Services](user/project/integrations//project_services.md) Integrate a project with external services, such as CI and chat. +- [Project Services](user/project/integrations/project_services.md) Integrate a project with external services, such as CI and chat. - [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. - [Snippets](user/snippets.md) Snippets allow you to create little bits of code. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. 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. + + ![Example Crowd application configuration](img/crowd_application.png) + +## 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/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index dad8e956c0e..3245988fc14 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -19,8 +19,8 @@ you need to use with GitLab. ## GitLab Pages Ports If you're using GitLab Pages you will need some additional port configurations. -GitLab Pages requires a separate VIP. Configure DNS to point the -`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new VIP. See the +GitLab Pages requires a separate virtual IP address. Configure DNS to point the +`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the [GitLab Pages documentation][gitlab-pages] for more information. | LB Port | Backend Port | Protocol | @@ -32,7 +32,7 @@ GitLab Pages requires a separate VIP. Configure DNS to point the Some organizations have policies against opening SSH port 22. In this case, it may be helpful to configure an alternate SSH hostname that allows users -to use SSH on port 443. An alternate SSH hostname will require a new VIP +to use SSH on port 443. An alternate SSH hostname will require a new virtual IP address compared to the other GitLab HTTP configuration above. Configure DNS for an alternate SSH hostname such as altssh.gitlab.example.com. 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 a586b095ef5..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** @@ -545,13 +556,30 @@ The above script will: Manual actions are a special type of job that are not executed automatically; they need to be explicitly started by a user. Manual actions can be started -from pipeline, build, environment, and deployment views. You can execute the -same manual action multiple times. +from pipeline, build, environment, and deployment views. An example usage of manual actions is deployment to production. Read more at the [environments documentation][env-manual]. +Manual actions can be either optional or blocking. Blocking manual action will +block execution of the pipeline at stage this action is defined in. It is +possible to resume execution of the pipeline when someone executes a blocking +manual actions by clicking a _play_ button. + +When pipeline is blocked it will not be merged if Merge When Pipeline Succeeds +is set. Blocked pipelines also do have a special status, called _manual_. + +Manual actions are non-blocking by default. If you want to make manual action +blocking, it is necessary to add `allow_failure: false` to the job's definition +in `.gitlab-ci.yml`. + +Optional manual actions have `allow_failure: true` set by default. + +**Statuses of optional actions do not contribute to overall pipeline status.** + +> Blocking manual actions were introduced in GitLab 9.0 + ### environment > 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/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 96ec1b178b6..65fcfc77ab1 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -159,6 +159,8 @@ For installations from source: remote_directory: 'my.s3.bucket' # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional # encryption: 'AES256' + # Specifies Amazon S3 storage class to use for backups, this is optional + # storage_class: 'STANDARD' ``` If you are uploading your backups to S3 you will probably want to create a new 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/user/markdown.md b/doc/user/markdown.md index c14db17b0e6..db06224bac2 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -576,7 +576,7 @@ Quote break. You can also use raw HTML in your Markdown, and it'll mostly work pretty well. -See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/1.11.0/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows `span` elements. +See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/1.11.0/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows `span`, `abbr`, `details` and `summary` elements. ```no-highlight <dl> diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index 91b35c73b34..b6221620e58 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -249,4 +249,4 @@ Once the right permissions were set, the error will go away. [ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040 [docker-docs]: https://docs.docker.com/engine/userguide/intro/ -[private-docker]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-docker-registry +[private-docker]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md index dba5fb6c17a..55fcd5f00f2 100644 --- a/doc/user/project/pages/getting_started_part_three.md +++ b/doc/user/project/pages/getting_started_part_three.md @@ -53,14 +53,14 @@ In case you want to point a root domain (`example.com`) to your GitLab Pages site, deployed to `namespace.gitlab.io`, you need to log into your domain's admin control panel and add a DNS `A` record pointing your domain to Pages' server IP address. For projects on -GitLab.com, this IP is `104.208.235.32`. For projects leaving in +GitLab.com, this IP is `52.167.214.135`. For projects leaving in other GitLab instances (CE or EE), please contact your sysadmin asking for this information (which IP address is Pages server running on your instance). **Practical Example:** -![DNS A record pointing to GitLab.com Pages server](img/dns_a_record_example.png) +![DNS A record pointing to GitLab.com Pages server](img/dns_add_new_a_record_example_updated.png) #### DNS CNAME record @@ -82,7 +82,7 @@ without any `/project-name`. | From | DNS Record | To | | ---- | ---------- | -- | -| domain.com | A | 104.208.235.32 | +| domain.com | A | 52.167.214.135 | | subdomain.domain.com | CNAME | namespace.gitlab.io | > **Notes**: @@ -92,6 +92,7 @@ without any `/project-name`. > - **Do not** add any special chars after the default Pages domain. E.g., **do not** point your `subdomain.domain.com` to `namespace.gitlab.io.` or `namespace.gitlab.io/`. +> - GitLab Pages IP on GitLab.com [has been changed](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) from `104.208.235.32` to `52.167.214.135`. ### SSL/TLS Certificates diff --git a/doc/user/project/pages/img/dns_a_record_example.png b/doc/user/project/pages/img/dns_a_record_example.png Binary files differdeleted file mode 100644 index b923730388a..00000000000 --- a/doc/user/project/pages/img/dns_a_record_example.png +++ /dev/null diff --git a/doc/user/project/pages/img/dns_add_new_a_record_example_updated.png b/doc/user/project/pages/img/dns_add_new_a_record_example_updated.png Binary files differnew file mode 100644 index 00000000000..2661a497b91 --- /dev/null +++ b/doc/user/project/pages/img/dns_add_new_a_record_example_updated.png diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index 1366756d593..abe6b4cbd8e 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -10,10 +10,11 @@ Here's some info we've gathered to get you started. ## General info - [Product webpage](https://pages.gitlab.io) -- ["We're bringing GitLab Pages to CE" blog post](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/) +- ["We're bringing GitLab Pages to CE"](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/) - [Pages group - templates](https://gitlab.com/pages) - [General user documentation](introduction.md) - [Admin documentation - Set GitLab Pages on your own GitLab instance](../../../administration/pages/index.md) +- ["We are changing the IP of GitLab Pages on GitLab.com"](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) ## Getting started 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/features/support/capybara.rb b/features/support/capybara.rb index 47372df152d..a5fcbb65131 100644 --- a/features/support/capybara.rb +++ b/features/support/capybara.rb @@ -2,7 +2,7 @@ require 'spinach/capybara' require 'capybara/poltergeist' # Give CI some extra time -timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 15 +timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10 Capybara.javascript_driver = :poltergeist Capybara.register_driver :poltergeist do |app| 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/fixtures/emojis/emoji-unicode-version-map.json b/fixtures/emojis/emoji-unicode-version-map.json new file mode 100644 index 00000000000..5164fe39426 --- /dev/null +++ b/fixtures/emojis/emoji-unicode-version-map.json @@ -0,0 +1,2377 @@ +{ + "100": "6.0", + "1234": "6.0", + "grinning": "6.1", + "grin": "6.0", + "joy": "6.0", + "rofl": "9.0", + "rolling_on_the_floor_laughing": "9.0", + "smiley": "6.0", + "smile": "6.0", + "sweat_smile": "6.0", + "laughing": "6.0", + "satisfied": "6.0", + "wink": "6.0", + "blush": "6.0", + "yum": "6.0", + "sunglasses": "6.0", + "heart_eyes": "6.0", + "kissing_heart": "6.0", + "kissing": "6.1", + "kissing_smiling_eyes": "6.1", + "kissing_closed_eyes": "6.0", + "relaxed": "1.1", + "slight_smile": "7.0", + "slightly_smiling_face": "7.0", + "hugging": "8.0", + "hugging_face": "8.0", + "thinking": "8.0", + "thinking_face": "8.0", + "neutral_face": "6.0", + "expressionless": "6.1", + "no_mouth": "6.0", + "rolling_eyes": "8.0", + "face_with_rolling_eyes": "8.0", + "smirk": "6.0", + "persevere": "6.0", + "disappointed_relieved": "6.0", + "open_mouth": "6.1", + "zipper_mouth": "8.0", + "zipper_mouth_face": "8.0", + "hushed": "6.1", + "sleepy": "6.0", + "tired_face": "6.0", + "sleeping": "6.1", + "relieved": "6.0", + "nerd": "8.0", + "nerd_face": "8.0", + "stuck_out_tongue": "6.1", + "stuck_out_tongue_winking_eye": "6.0", + "stuck_out_tongue_closed_eyes": "6.0", + "drooling_face": "9.0", + "drool": "9.0", + "unamused": "6.0", + "sweat": "6.0", + "pensive": "6.0", + "confused": "6.1", + "upside_down": "8.0", + "upside_down_face": "8.0", + "money_mouth": "8.0", + "money_mouth_face": "8.0", + "astonished": "6.0", + "frowning2": "1.1", + "white_frowning_face": "1.1", + "slight_frown": "7.0", + "slightly_frowning_face": "7.0", + "confounded": "6.0", + "disappointed": "6.0", + "worried": "6.1", + "triumph": "6.0", + "cry": "6.0", + "sob": "6.0", + "frowning": "6.1", + "anguished": "6.1", + "fearful": "6.0", + "weary": "6.0", + "grimacing": "6.1", + "cold_sweat": "6.0", + "scream": "6.0", + "flushed": "6.0", + "dizzy_face": "6.0", + "rage": "6.0", + "angry": "6.0", + "innocent": "6.0", + "cowboy": "9.0", + "face_with_cowboy_hat": "9.0", + "clown": "9.0", + "clown_face": "9.0", + "lying_face": "9.0", + "liar": "9.0", + "mask": "6.0", + "thermometer_face": "8.0", + "face_with_thermometer": "8.0", + "head_bandage": "8.0", + "face_with_head_bandage": "8.0", + "nauseated_face": "9.0", + "sick": "9.0", + "sneezing_face": "9.0", + "sneeze": "9.0", + "smiling_imp": "6.0", + "imp": "6.0", + "japanese_ogre": "6.0", + "japanese_goblin": "6.0", + "skull": "6.0", + "skeleton": "6.0", + "skull_crossbones": "1.1", + "skull_and_crossbones": "1.1", + "ghost": "6.0", + "alien": "6.0", + "space_invader": "6.0", + "robot": "8.0", + "robot_face": "8.0", + "poop": "6.0", + "shit": "6.0", + "hankey": "6.0", + "poo": "6.0", + "smiley_cat": "6.0", + "smile_cat": "6.0", + "joy_cat": "6.0", + "heart_eyes_cat": "6.0", + "smirk_cat": "6.0", + "kissing_cat": "6.0", + "scream_cat": "6.0", + "crying_cat_face": "6.0", + "pouting_cat": "6.0", + "see_no_evil": "6.0", + "hear_no_evil": "6.0", + "speak_no_evil": "6.0", + "boy": "6.0", + "boy_tone1": "8.0", + "boy_tone2": "8.0", + "boy_tone3": "8.0", + "boy_tone4": "8.0", + "boy_tone5": "8.0", + "girl": "6.0", + "girl_tone1": "8.0", + "girl_tone2": "8.0", + "girl_tone3": "8.0", + "girl_tone4": "8.0", + "girl_tone5": "8.0", + "man": "6.0", + "man_tone1": "8.0", + "man_tone2": "8.0", + "man_tone3": "8.0", + "man_tone4": "8.0", + "man_tone5": "8.0", + "woman": "6.0", + "woman_tone1": "8.0", + "woman_tone2": "8.0", + "woman_tone3": "8.0", + "woman_tone4": "8.0", + "woman_tone5": "8.0", + "older_man": "6.0", + "older_man_tone1": "8.0", + "older_man_tone2": "8.0", + "older_man_tone3": "8.0", + "older_man_tone4": "8.0", + "older_man_tone5": "8.0", + "older_woman": "6.0", + "grandma": "6.0", + "older_woman_tone1": "8.0", + "grandma_tone1": "8.0", + "older_woman_tone2": "8.0", + "grandma_tone2": "8.0", + "older_woman_tone3": "8.0", + "grandma_tone3": "8.0", + "older_woman_tone4": "8.0", + "grandma_tone4": "8.0", + "older_woman_tone5": "8.0", + "grandma_tone5": "8.0", + "baby": "6.0", + "baby_tone1": "8.0", + "baby_tone2": "8.0", + "baby_tone3": "8.0", + "baby_tone4": "8.0", + "baby_tone5": "8.0", + "angel": "6.0", + "angel_tone1": "8.0", + "angel_tone2": "8.0", + "angel_tone3": "8.0", + "angel_tone4": "8.0", + "angel_tone5": "8.0", + "cop": "6.0", + "cop_tone1": "8.0", + "cop_tone2": "8.0", + "cop_tone3": "8.0", + "cop_tone4": "8.0", + "cop_tone5": "8.0", + "spy": "7.0", + "sleuth_or_spy": "7.0", + "spy_tone1": "8.0", + "sleuth_or_spy_tone1": "8.0", + "spy_tone2": "8.0", + "sleuth_or_spy_tone2": "8.0", + "spy_tone3": "8.0", + "sleuth_or_spy_tone3": "8.0", + "spy_tone4": "8.0", + "sleuth_or_spy_tone4": "8.0", + "spy_tone5": "8.0", + "sleuth_or_spy_tone5": "8.0", + "guardsman": "6.0", + "guardsman_tone1": "8.0", + "guardsman_tone2": "8.0", + "guardsman_tone3": "8.0", + "guardsman_tone4": "8.0", + "guardsman_tone5": "8.0", + "construction_worker": "6.0", + "construction_worker_tone1": "8.0", + "construction_worker_tone2": "8.0", + "construction_worker_tone3": "8.0", + "construction_worker_tone4": "8.0", + "construction_worker_tone5": "8.0", + "man_with_turban": "6.0", + "man_with_turban_tone1": "8.0", + "man_with_turban_tone2": "8.0", + "man_with_turban_tone3": "8.0", + "man_with_turban_tone4": "8.0", + "man_with_turban_tone5": "8.0", + "person_with_blond_hair": "6.0", + "person_with_blond_hair_tone1": "8.0", + "person_with_blond_hair_tone2": "8.0", + "person_with_blond_hair_tone3": "8.0", + "person_with_blond_hair_tone4": "8.0", + "person_with_blond_hair_tone5": "8.0", + "santa": "6.0", + "santa_tone1": "8.0", + "santa_tone2": "8.0", + "santa_tone3": "8.0", + "santa_tone4": "8.0", + "santa_tone5": "8.0", + "mrs_claus": "9.0", + "mother_christmas": "9.0", + "mrs_claus_tone1": "9.0", + "mother_christmas_tone1": "9.0", + "mrs_claus_tone2": "9.0", + "mother_christmas_tone2": "9.0", + "mrs_claus_tone3": "9.0", + "mother_christmas_tone3": "9.0", + "mrs_claus_tone4": "9.0", + "mother_christmas_tone4": "9.0", + "mrs_claus_tone5": "9.0", + "mother_christmas_tone5": "9.0", + "princess": "6.0", + "princess_tone1": "8.0", + "princess_tone2": "8.0", + "princess_tone3": "8.0", + "princess_tone4": "8.0", + "princess_tone5": "8.0", + "prince": "9.0", + "prince_tone1": "9.0", + "prince_tone2": "9.0", + "prince_tone3": "9.0", + "prince_tone4": "9.0", + "prince_tone5": "9.0", + "bride_with_veil": "6.0", + "bride_with_veil_tone1": "8.0", + "bride_with_veil_tone2": "8.0", + "bride_with_veil_tone3": "8.0", + "bride_with_veil_tone4": "8.0", + "bride_with_veil_tone5": "8.0", + "man_in_tuxedo": "9.0", + "man_in_tuxedo_tone1": "9.0", + "tuxedo_tone1": "9.0", + "man_in_tuxedo_tone2": "9.0", + "tuxedo_tone2": "9.0", + "man_in_tuxedo_tone3": "9.0", + "tuxedo_tone3": "9.0", + "man_in_tuxedo_tone4": "9.0", + "tuxedo_tone4": "9.0", + "man_in_tuxedo_tone5": "9.0", + "tuxedo_tone5": "9.0", + "pregnant_woman": "9.0", + "expecting_woman": "9.0", + "pregnant_woman_tone1": "9.0", + "expecting_woman_tone1": "9.0", + "pregnant_woman_tone2": "9.0", + "expecting_woman_tone2": "9.0", + "pregnant_woman_tone3": "9.0", + "expecting_woman_tone3": "9.0", + "pregnant_woman_tone4": "9.0", + "expecting_woman_tone4": "9.0", + "pregnant_woman_tone5": "9.0", + "expecting_woman_tone5": "9.0", + "man_with_gua_pi_mao": "6.0", + "man_with_gua_pi_mao_tone1": "8.0", + "man_with_gua_pi_mao_tone2": "8.0", + "man_with_gua_pi_mao_tone3": "8.0", + "man_with_gua_pi_mao_tone4": "8.0", + "man_with_gua_pi_mao_tone5": "8.0", + "person_frowning": "6.0", + "person_frowning_tone1": "8.0", + "person_frowning_tone2": "8.0", + "person_frowning_tone3": "8.0", + "person_frowning_tone4": "8.0", + "person_frowning_tone5": "8.0", + "person_with_pouting_face": "6.0", + "person_with_pouting_face_tone1": "8.0", + "person_with_pouting_face_tone2": "8.0", + "person_with_pouting_face_tone3": "8.0", + "person_with_pouting_face_tone4": "8.0", + "person_with_pouting_face_tone5": "8.0", + "no_good": "6.0", + "no_good_tone1": "8.0", + "no_good_tone2": "8.0", + "no_good_tone3": "8.0", + "no_good_tone4": "8.0", + "no_good_tone5": "8.0", + "ok_woman": "6.0", + "ok_woman_tone1": "8.0", + "ok_woman_tone2": "8.0", + "ok_woman_tone3": "8.0", + "ok_woman_tone4": "8.0", + "ok_woman_tone5": "8.0", + "information_desk_person": "6.0", + "information_desk_person_tone1": "8.0", + "information_desk_person_tone2": "8.0", + "information_desk_person_tone3": "8.0", + "information_desk_person_tone4": "8.0", + "information_desk_person_tone5": "8.0", + "raising_hand": "6.0", + "raising_hand_tone1": "8.0", + "raising_hand_tone2": "8.0", + "raising_hand_tone3": "8.0", + "raising_hand_tone4": "8.0", + "raising_hand_tone5": "8.0", + "bow": "6.0", + "bow_tone1": "8.0", + "bow_tone2": "8.0", + "bow_tone3": "8.0", + "bow_tone4": "8.0", + "bow_tone5": "8.0", + "face_palm": "9.0", + "facepalm": "9.0", + "face_palm_tone1": "9.0", + "facepalm_tone1": "9.0", + "face_palm_tone2": "9.0", + "facepalm_tone2": "9.0", + "face_palm_tone3": "9.0", + "facepalm_tone3": "9.0", + "face_palm_tone4": "9.0", + "facepalm_tone4": "9.0", + "face_palm_tone5": "9.0", + "facepalm_tone5": "9.0", + "shrug": "9.0", + "shrug_tone1": "9.0", + "shrug_tone2": "9.0", + "shrug_tone3": "9.0", + "shrug_tone4": "9.0", + "shrug_tone5": "9.0", + "massage": "6.0", + "massage_tone1": "8.0", + "massage_tone2": "8.0", + "massage_tone3": "8.0", + "massage_tone4": "8.0", + "massage_tone5": "8.0", + "haircut": "6.0", + "haircut_tone1": "8.0", + "haircut_tone2": "8.0", + "haircut_tone3": "8.0", + "haircut_tone4": "8.0", + "haircut_tone5": "8.0", + "walking": "6.0", + "walking_tone1": "8.0", + "walking_tone2": "8.0", + "walking_tone3": "8.0", + "walking_tone4": "8.0", + "walking_tone5": "8.0", + "runner": "6.0", + "runner_tone1": "8.0", + "runner_tone2": "8.0", + "runner_tone3": "8.0", + "runner_tone4": "8.0", + "runner_tone5": "8.0", + "dancer": "6.0", + "dancer_tone1": "8.0", + "dancer_tone2": "8.0", + "dancer_tone3": "8.0", + "dancer_tone4": "8.0", + "dancer_tone5": "8.0", + "man_dancing": "9.0", + "male_dancer": "9.0", + "man_dancing_tone1": "9.0", + "male_dancer_tone1": "9.0", + "man_dancing_tone2": "9.0", + "male_dancer_tone2": "9.0", + "man_dancing_tone3": "9.0", + "male_dancer_tone3": "9.0", + "man_dancing_tone4": "9.0", + "male_dancer_tone4": "9.0", + "man_dancing_tone5": "9.0", + "male_dancer_tone5": "9.0", + "dancers": "6.0", + "levitate": "7.0", + "man_in_business_suit_levitating": "7.0", + "speaking_head": "7.0", + "speaking_head_in_silhouette": "7.0", + "bust_in_silhouette": "6.0", + "busts_in_silhouette": "6.0", + "fencer": "9.0", + "fencing": "9.0", + "horse_racing": "6.0", + "horse_racing_tone1": "8.0", + "horse_racing_tone2": "8.0", + "horse_racing_tone3": "8.0", + "horse_racing_tone4": "8.0", + "horse_racing_tone5": "8.0", + "skier": "5.2", + "snowboarder": "6.0", + "golfer": "7.0", + "surfer": "6.0", + "surfer_tone1": "8.0", + "surfer_tone2": "8.0", + "surfer_tone3": "8.0", + "surfer_tone4": "8.0", + "surfer_tone5": "8.0", + "rowboat": "6.0", + "rowboat_tone1": "8.0", + "rowboat_tone2": "8.0", + "rowboat_tone3": "8.0", + "rowboat_tone4": "8.0", + "rowboat_tone5": "8.0", + "swimmer": "6.0", + "swimmer_tone1": "8.0", + "swimmer_tone2": "8.0", + "swimmer_tone3": "8.0", + "swimmer_tone4": "8.0", + "swimmer_tone5": "8.0", + "basketball_player": "5.2", + "person_with_ball": "5.2", + "basketball_player_tone1": "8.0", + "person_with_ball_tone1": "8.0", + "basketball_player_tone2": "8.0", + "person_with_ball_tone2": "8.0", + "basketball_player_tone3": "8.0", + "person_with_ball_tone3": "8.0", + "basketball_player_tone4": "8.0", + "person_with_ball_tone4": "8.0", + "basketball_player_tone5": "8.0", + "person_with_ball_tone5": "8.0", + "lifter": "7.0", + "weight_lifter": "7.0", + "lifter_tone1": "8.0", + "weight_lifter_tone1": "8.0", + "lifter_tone2": "8.0", + "weight_lifter_tone2": "8.0", + "lifter_tone3": "8.0", + "weight_lifter_tone3": "8.0", + "lifter_tone4": "8.0", + "weight_lifter_tone4": "8.0", + "lifter_tone5": "8.0", + "weight_lifter_tone5": "8.0", + "bicyclist": "6.0", + "bicyclist_tone1": "8.0", + "bicyclist_tone2": "8.0", + "bicyclist_tone3": "8.0", + "bicyclist_tone4": "8.0", + "bicyclist_tone5": "8.0", + "mountain_bicyclist": "6.0", + "mountain_bicyclist_tone1": "8.0", + "mountain_bicyclist_tone2": "8.0", + "mountain_bicyclist_tone3": "8.0", + "mountain_bicyclist_tone4": "8.0", + "mountain_bicyclist_tone5": "8.0", + "race_car": "7.0", + "racing_car": "7.0", + "motorcycle": "7.0", + "racing_motorcycle": "7.0", + "cartwheel": "9.0", + "person_doing_cartwheel": "9.0", + "cartwheel_tone1": "9.0", + "person_doing_cartwheel_tone1": "9.0", + "cartwheel_tone2": "9.0", + "person_doing_cartwheel_tone2": "9.0", + "cartwheel_tone3": "9.0", + "person_doing_cartwheel_tone3": "9.0", + "cartwheel_tone4": "9.0", + "person_doing_cartwheel_tone4": "9.0", + "cartwheel_tone5": "9.0", + "person_doing_cartwheel_tone5": "9.0", + "wrestlers": "9.0", + "wrestling": "9.0", + "wrestlers_tone1": "9.0", + "wrestling_tone1": "9.0", + "wrestlers_tone2": "9.0", + "wrestling_tone2": "9.0", + "wrestlers_tone3": "9.0", + "wrestling_tone3": "9.0", + "wrestlers_tone4": "9.0", + "wrestling_tone4": "9.0", + "wrestlers_tone5": "9.0", + "wrestling_tone5": "9.0", + "water_polo": "9.0", + "water_polo_tone1": "9.0", + "water_polo_tone2": "9.0", + "water_polo_tone3": "9.0", + "water_polo_tone4": "9.0", + "water_polo_tone5": "9.0", + "handball": "9.0", + "handball_tone1": "9.0", + "handball_tone2": "9.0", + "handball_tone3": "9.0", + "handball_tone4": "9.0", + "handball_tone5": "9.0", + "juggling": "9.0", + "juggler": "9.0", + "juggling_tone1": "9.0", + "juggler_tone1": "9.0", + "juggling_tone2": "9.0", + "juggler_tone2": "9.0", + "juggling_tone3": "9.0", + "juggler_tone3": "9.0", + "juggling_tone4": "9.0", + "juggler_tone4": "9.0", + "juggling_tone5": "9.0", + "juggler_tone5": "9.0", + "couple": "6.0", + "two_men_holding_hands": "6.0", + "two_women_holding_hands": "6.0", + "couplekiss": "6.0", + "kiss_mm": "6.0", + "couplekiss_mm": "6.0", + "kiss_ww": "6.0", + "couplekiss_ww": "6.0", + "couple_with_heart": "6.0", + "couple_mm": "6.0", + "couple_with_heart_mm": "6.0", + "couple_ww": "6.0", + "couple_with_heart_ww": "6.0", + "family": "6.0", + "family_mwg": "6.0", + "family_mwgb": "6.0", + "family_mwbb": "6.0", + "family_mwgg": "6.0", + "family_mmb": "6.0", + "family_mmg": "6.0", + "family_mmgb": "6.0", + "family_mmbb": "6.0", + "family_mmgg": "6.0", + "family_wwb": "6.0", + "family_wwg": "6.0", + "family_wwgb": "6.0", + "family_wwbb": "6.0", + "family_wwgg": "6.0", + "tone1": "8.0", + "tone2": "8.0", + "tone3": "8.0", + "tone4": "8.0", + "tone5": "8.0", + "muscle": "6.0", + "muscle_tone1": "8.0", + "muscle_tone2": "8.0", + "muscle_tone3": "8.0", + "muscle_tone4": "8.0", + "muscle_tone5": "8.0", + "selfie": "9.0", + "selfie_tone1": "9.0", + "selfie_tone2": "9.0", + "selfie_tone3": "9.0", + "selfie_tone4": "9.0", + "selfie_tone5": "9.0", + "point_left": "6.0", + "point_left_tone1": "8.0", + "point_left_tone2": "8.0", + "point_left_tone3": "8.0", + "point_left_tone4": "8.0", + "point_left_tone5": "8.0", + "point_right": "6.0", + "point_right_tone1": "8.0", + "point_right_tone2": "8.0", + "point_right_tone3": "8.0", + "point_right_tone4": "8.0", + "point_right_tone5": "8.0", + "point_up": "1.1", + "point_up_tone1": "8.0", + "point_up_tone2": "8.0", + "point_up_tone3": "8.0", + "point_up_tone4": "8.0", + "point_up_tone5": "8.0", + "point_up_2": "6.0", + "point_up_2_tone1": "8.0", + "point_up_2_tone2": "8.0", + "point_up_2_tone3": "8.0", + "point_up_2_tone4": "8.0", + "point_up_2_tone5": "8.0", + "middle_finger": "7.0", + "reversed_hand_with_middle_finger_extended": "7.0", + "middle_finger_tone1": "8.0", + "reversed_hand_with_middle_finger_extended_tone1": "8.0", + "middle_finger_tone2": "8.0", + "reversed_hand_with_middle_finger_extended_tone2": "8.0", + "middle_finger_tone3": "8.0", + "reversed_hand_with_middle_finger_extended_tone3": "8.0", + "middle_finger_tone4": "8.0", + "reversed_hand_with_middle_finger_extended_tone4": "8.0", + "middle_finger_tone5": "8.0", + "reversed_hand_with_middle_finger_extended_tone5": "8.0", + "point_down": "6.0", + "point_down_tone1": "8.0", + "point_down_tone2": "8.0", + "point_down_tone3": "8.0", + "point_down_tone4": "8.0", + "point_down_tone5": "8.0", + "v": "1.1", + "v_tone1": "8.0", + "v_tone2": "8.0", + "v_tone3": "8.0", + "v_tone4": "8.0", + "v_tone5": "8.0", + "fingers_crossed": "9.0", + "hand_with_index_and_middle_finger_crossed": "9.0", + "fingers_crossed_tone1": "9.0", + "hand_with_index_and_middle_fingers_crossed_tone1": "9.0", + "fingers_crossed_tone2": "9.0", + "hand_with_index_and_middle_fingers_crossed_tone2": "9.0", + "fingers_crossed_tone3": "9.0", + "hand_with_index_and_middle_fingers_crossed_tone3": "9.0", + "fingers_crossed_tone4": "9.0", + "hand_with_index_and_middle_fingers_crossed_tone4": "9.0", + "fingers_crossed_tone5": "9.0", + "hand_with_index_and_middle_fingers_crossed_tone5": "9.0", + "vulcan": "7.0", + "raised_hand_with_part_between_middle_and_ring_fingers": "7.0", + "vulcan_tone1": "8.0", + "raised_hand_with_part_between_middle_and_ring_fingers_tone1": "8.0", + "vulcan_tone2": "8.0", + "raised_hand_with_part_between_middle_and_ring_fingers_tone2": "8.0", + "vulcan_tone3": "8.0", + "raised_hand_with_part_between_middle_and_ring_fingers_tone3": "8.0", + "vulcan_tone4": "8.0", + "raised_hand_with_part_between_middle_and_ring_fingers_tone4": "8.0", + "vulcan_tone5": "8.0", + "raised_hand_with_part_between_middle_and_ring_fingers_tone5": "8.0", + "metal": "8.0", + "sign_of_the_horns": "8.0", + "metal_tone1": "8.0", + "sign_of_the_horns_tone1": "8.0", + "metal_tone2": "8.0", + "sign_of_the_horns_tone2": "8.0", + "metal_tone3": "8.0", + "sign_of_the_horns_tone3": "8.0", + "metal_tone4": "8.0", + "sign_of_the_horns_tone4": "8.0", + "metal_tone5": "8.0", + "sign_of_the_horns_tone5": "8.0", + "call_me": "9.0", + "call_me_hand": "9.0", + "call_me_tone1": "9.0", + "call_me_hand_tone1": "9.0", + "call_me_tone2": "9.0", + "call_me_hand_tone2": "9.0", + "call_me_tone3": "9.0", + "call_me_hand_tone3": "9.0", + "call_me_tone4": "9.0", + "call_me_hand_tone4": "9.0", + "call_me_tone5": "9.0", + "call_me_hand_tone5": "9.0", + "hand_splayed": "7.0", + "raised_hand_with_fingers_splayed": "7.0", + "hand_splayed_tone1": "8.0", + "raised_hand_with_fingers_splayed_tone1": "8.0", + "hand_splayed_tone2": "8.0", + "raised_hand_with_fingers_splayed_tone2": "8.0", + "hand_splayed_tone3": "8.0", + "raised_hand_with_fingers_splayed_tone3": "8.0", + "hand_splayed_tone4": "8.0", + "raised_hand_with_fingers_splayed_tone4": "8.0", + "hand_splayed_tone5": "8.0", + "raised_hand_with_fingers_splayed_tone5": "8.0", + "raised_hand": "6.0", + "raised_hand_tone1": "8.0", + "raised_hand_tone2": "8.0", + "raised_hand_tone3": "8.0", + "raised_hand_tone4": "8.0", + "raised_hand_tone5": "8.0", + "ok_hand": "6.0", + "ok_hand_tone1": "8.0", + "ok_hand_tone2": "8.0", + "ok_hand_tone3": "8.0", + "ok_hand_tone4": "8.0", + "ok_hand_tone5": "8.0", + "thumbsup": "6.0", + "+1": "6.0", + "thumbup": "6.0", + "thumbsup_tone1": "8.0", + "+1_tone1": "8.0", + "thumbup_tone1": "8.0", + "thumbsup_tone2": "8.0", + "+1_tone2": "8.0", + "thumbup_tone2": "8.0", + "thumbsup_tone3": "8.0", + "+1_tone3": "8.0", + "thumbup_tone3": "8.0", + "thumbsup_tone4": "8.0", + "+1_tone4": "8.0", + "thumbup_tone4": "8.0", + "thumbsup_tone5": "8.0", + "+1_tone5": "8.0", + "thumbup_tone5": "8.0", + "thumbsdown": "6.0", + "-1": "6.0", + "thumbdown": "6.0", + "thumbsdown_tone1": "8.0", + "-1_tone1": "8.0", + "thumbdown_tone1": "8.0", + "thumbsdown_tone2": "8.0", + "-1_tone2": "8.0", + "thumbdown_tone2": "8.0", + "thumbsdown_tone3": "8.0", + "-1_tone3": "8.0", + "thumbdown_tone3": "8.0", + "thumbsdown_tone4": "8.0", + "-1_tone4": "8.0", + "thumbdown_tone4": "8.0", + "thumbsdown_tone5": "8.0", + "-1_tone5": "8.0", + "thumbdown_tone5": "8.0", + "fist": "6.0", + "fist_tone1": "8.0", + "fist_tone2": "8.0", + "fist_tone3": "8.0", + "fist_tone4": "8.0", + "fist_tone5": "8.0", + "punch": "6.0", + "punch_tone1": "8.0", + "punch_tone2": "8.0", + "punch_tone3": "8.0", + "punch_tone4": "8.0", + "punch_tone5": "8.0", + "left_facing_fist": "9.0", + "left_fist": "9.0", + "left_facing_fist_tone1": "9.0", + "left_fist_tone1": "9.0", + "left_facing_fist_tone2": "9.0", + "left_fist_tone2": "9.0", + "left_facing_fist_tone3": "9.0", + "left_fist_tone3": "9.0", + "left_facing_fist_tone4": "9.0", + "left_fist_tone4": "9.0", + "left_facing_fist_tone5": "9.0", + "left_fist_tone5": "9.0", + "right_facing_fist": "9.0", + "right_fist": "9.0", + "right_facing_fist_tone1": "9.0", + "right_fist_tone1": "9.0", + "right_facing_fist_tone2": "9.0", + "right_fist_tone2": "9.0", + "right_facing_fist_tone3": "9.0", + "right_fist_tone3": "9.0", + "right_facing_fist_tone4": "9.0", + "right_fist_tone4": "9.0", + "right_facing_fist_tone5": "9.0", + "right_fist_tone5": "9.0", + "raised_back_of_hand": "9.0", + "back_of_hand": "9.0", + "raised_back_of_hand_tone1": "9.0", + "back_of_hand_tone1": "9.0", + "raised_back_of_hand_tone2": "9.0", + "back_of_hand_tone2": "9.0", + "raised_back_of_hand_tone3": "9.0", + "back_of_hand_tone3": "9.0", + "raised_back_of_hand_tone4": "9.0", + "back_of_hand_tone4": "9.0", + "raised_back_of_hand_tone5": "9.0", + "back_of_hand_tone5": "9.0", + "wave": "6.0", + "wave_tone1": "8.0", + "wave_tone2": "8.0", + "wave_tone3": "8.0", + "wave_tone4": "8.0", + "wave_tone5": "8.0", + "clap": "6.0", + "clap_tone1": "8.0", + "clap_tone2": "8.0", + "clap_tone3": "8.0", + "clap_tone4": "8.0", + "clap_tone5": "8.0", + "writing_hand": "1.1", + "writing_hand_tone1": "8.0", + "writing_hand_tone2": "8.0", + "writing_hand_tone3": "8.0", + "writing_hand_tone4": "8.0", + "writing_hand_tone5": "8.0", + "open_hands": "6.0", + "open_hands_tone1": "8.0", + "open_hands_tone2": "8.0", + "open_hands_tone3": "8.0", + "open_hands_tone4": "8.0", + "open_hands_tone5": "8.0", + "raised_hands": "6.0", + "raised_hands_tone1": "8.0", + "raised_hands_tone2": "8.0", + "raised_hands_tone3": "8.0", + "raised_hands_tone4": "8.0", + "raised_hands_tone5": "8.0", + "pray": "6.0", + "pray_tone1": "8.0", + "pray_tone2": "8.0", + "pray_tone3": "8.0", + "pray_tone4": "8.0", + "pray_tone5": "8.0", + "handshake": "9.0", + "shaking_hands": "9.0", + "handshake_tone1": "9.0", + "shaking_hands_tone1": "9.0", + "handshake_tone2": "9.0", + "shaking_hands_tone2": "9.0", + "handshake_tone3": "9.0", + "shaking_hands_tone3": "9.0", + "handshake_tone4": "9.0", + "shaking_hands_tone4": "9.0", + "handshake_tone5": "9.0", + "shaking_hands_tone5": "9.0", + "nail_care": "6.0", + "nail_care_tone1": "8.0", + "nail_care_tone2": "8.0", + "nail_care_tone3": "8.0", + "nail_care_tone4": "8.0", + "nail_care_tone5": "8.0", + "ear": "6.0", + "ear_tone1": "8.0", + "ear_tone2": "8.0", + "ear_tone3": "8.0", + "ear_tone4": "8.0", + "ear_tone5": "8.0", + "nose": "6.0", + "nose_tone1": "8.0", + "nose_tone2": "8.0", + "nose_tone3": "8.0", + "nose_tone4": "8.0", + "nose_tone5": "8.0", + "footprints": "6.0", + "eyes": "6.0", + "eye": "7.0", + "eye_in_speech_bubble": "7.0", + "tongue": "6.0", + "lips": "6.0", + "kiss": "6.0", + "cupid": "6.0", + "heart": "1.1", + "heartbeat": "6.0", + "broken_heart": "6.0", + "two_hearts": "6.0", + "sparkling_heart": "6.0", + "heartpulse": "6.0", + "blue_heart": "6.0", + "green_heart": "6.0", + "yellow_heart": "6.0", + "purple_heart": "6.0", + "black_heart": "9.0", + "gift_heart": "6.0", + "revolving_hearts": "6.0", + "heart_decoration": "6.0", + "heart_exclamation": "1.1", + "heavy_heart_exclamation_mark_ornament": "1.1", + "love_letter": "6.0", + "zzz": "6.0", + "anger": "6.0", + "bomb": "6.0", + "boom": "6.0", + "sweat_drops": "6.0", + "dash": "6.0", + "dizzy": "6.0", + "speech_balloon": "6.0", + "speech_left": "7.0", + "left_speech_bubble": "7.0", + "anger_right": "7.0", + "right_anger_bubble": "7.0", + "thought_balloon": "6.0", + "hole": "7.0", + "eyeglasses": "6.0", + "dark_sunglasses": "7.0", + "necktie": "6.0", + "shirt": "6.0", + "jeans": "6.0", + "dress": "6.0", + "kimono": "6.0", + "bikini": "6.0", + "womans_clothes": "6.0", + "purse": "6.0", + "handbag": "6.0", + "pouch": "6.0", + "shopping_bags": "7.0", + "school_satchel": "6.0", + "mans_shoe": "6.0", + "athletic_shoe": "6.0", + "high_heel": "6.0", + "sandal": "6.0", + "boot": "6.0", + "crown": "6.0", + "womans_hat": "6.0", + "tophat": "6.0", + "mortar_board": "6.0", + "helmet_with_cross": "5.2", + "helmet_with_white_cross": "5.2", + "prayer_beads": "8.0", + "lipstick": "6.0", + "ring": "6.0", + "gem": "6.0", + "monkey_face": "6.0", + "monkey": "6.0", + "gorilla": "9.0", + "dog": "6.0", + "dog2": "6.0", + "poodle": "6.0", + "wolf": "6.0", + "fox": "9.0", + "fox_face": "9.0", + "cat": "6.0", + "cat2": "6.0", + "lion_face": "8.0", + "lion": "8.0", + "tiger": "6.0", + "tiger2": "6.0", + "leopard": "6.0", + "horse": "6.0", + "racehorse": "6.0", + "deer": "9.0", + "unicorn": "8.0", + "unicorn_face": "8.0", + "cow": "6.0", + "ox": "6.0", + "water_buffalo": "6.0", + "cow2": "6.0", + "pig": "6.0", + "pig2": "6.0", + "boar": "6.0", + "pig_nose": "6.0", + "ram": "6.0", + "sheep": "6.0", + "goat": "6.0", + "dromedary_camel": "6.0", + "camel": "6.0", + "elephant": "6.0", + "rhino": "9.0", + "rhinoceros": "9.0", + "mouse": "6.0", + "mouse2": "6.0", + "rat": "6.0", + "hamster": "6.0", + "rabbit": "6.0", + "rabbit2": "6.0", + "chipmunk": "7.0", + "bat": "9.0", + "bear": "6.0", + "koala": "6.0", + "panda_face": "6.0", + "feet": "6.0", + "paw_prints": "6.0", + "turkey": "8.0", + "chicken": "6.0", + "rooster": "6.0", + "hatching_chick": "6.0", + "baby_chick": "6.0", + "hatched_chick": "6.0", + "bird": "6.0", + "penguin": "6.0", + "dove": "7.0", + "dove_of_peace": "7.0", + "eagle": "9.0", + "duck": "9.0", + "owl": "9.0", + "frog": "6.0", + "crocodile": "6.0", + "turtle": "6.0", + "lizard": "9.0", + "snake": "6.0", + "dragon_face": "6.0", + "dragon": "6.0", + "whale": "6.0", + "whale2": "6.0", + "dolphin": "6.0", + "fish": "6.0", + "tropical_fish": "6.0", + "blowfish": "6.0", + "shark": "9.0", + "octopus": "6.0", + "shell": "6.0", + "crab": "8.0", + "shrimp": "9.0", + "squid": "9.0", + "butterfly": "9.0", + "snail": "6.0", + "bug": "6.0", + "ant": "6.0", + "bee": "6.0", + "beetle": "6.0", + "spider": "7.0", + "spider_web": "7.0", + "scorpion": "8.0", + "bouquet": "6.0", + "cherry_blossom": "6.0", + "white_flower": "6.0", + "rosette": "7.0", + "rose": "6.0", + "wilted_rose": "9.0", + "wilted_flower": "9.0", + "hibiscus": "6.0", + "sunflower": "6.0", + "blossom": "6.0", + "tulip": "6.0", + "seedling": "6.0", + "evergreen_tree": "6.0", + "deciduous_tree": "6.0", + "palm_tree": "6.0", + "cactus": "6.0", + "ear_of_rice": "6.0", + "herb": "6.0", + "shamrock": "4.1", + "four_leaf_clover": "6.0", + "maple_leaf": "6.0", + "fallen_leaf": "6.0", + "leaves": "6.0", + "grapes": "6.0", + "melon": "6.0", + "watermelon": "6.0", + "tangerine": "6.0", + "lemon": "6.0", + "banana": "6.0", + "pineapple": "6.0", + "apple": "6.0", + "green_apple": "6.0", + "pear": "6.0", + "peach": "6.0", + "cherries": "6.0", + "strawberry": "6.0", + "kiwi": "9.0", + "kiwifruit": "9.0", + "tomato": "6.0", + "avocado": "9.0", + "eggplant": "6.0", + "potato": "9.0", + "carrot": "9.0", + "corn": "6.0", + "hot_pepper": "7.0", + "cucumber": "9.0", + "mushroom": "6.0", + "peanuts": "9.0", + "shelled_peanut": "9.0", + "chestnut": "6.0", + "bread": "6.0", + "croissant": "9.0", + "french_bread": "9.0", + "baguette_bread": "9.0", + "pancakes": "9.0", + "cheese": "8.0", + "cheese_wedge": "8.0", + "meat_on_bone": "6.0", + "poultry_leg": "6.0", + "bacon": "9.0", + "hamburger": "6.0", + "fries": "6.0", + "pizza": "6.0", + "hotdog": "8.0", + "hot_dog": "8.0", + "taco": "8.0", + "burrito": "8.0", + "stuffed_flatbread": "9.0", + "stuffed_pita": "9.0", + "egg": "9.0", + "cooking": "6.0", + "shallow_pan_of_food": "9.0", + "paella": "9.0", + "stew": "6.0", + "salad": "9.0", + "green_salad": "9.0", + "popcorn": "8.0", + "bento": "6.0", + "rice_cracker": "6.0", + "rice_ball": "6.0", + "rice": "6.0", + "curry": "6.0", + "ramen": "6.0", + "spaghetti": "6.0", + "sweet_potato": "6.0", + "oden": "6.0", + "sushi": "6.0", + "fried_shrimp": "6.0", + "fish_cake": "6.0", + "dango": "6.0", + "icecream": "6.0", + "shaved_ice": "6.0", + "ice_cream": "6.0", + "doughnut": "6.0", + "cookie": "6.0", + "birthday": "6.0", + "cake": "6.0", + "chocolate_bar": "6.0", + "candy": "6.0", + "lollipop": "6.0", + "custard": "6.0", + "pudding": "6.0", + "flan": "6.0", + "honey_pot": "6.0", + "baby_bottle": "6.0", + "milk": "9.0", + "glass_of_milk": "9.0", + "coffee": "4.0", + "tea": "6.0", + "sake": "6.0", + "champagne": "8.0", + "bottle_with_popping_cork": "8.0", + "wine_glass": "6.0", + "cocktail": "6.0", + "tropical_drink": "6.0", + "beer": "6.0", + "beers": "6.0", + "champagne_glass": "9.0", + "clinking_glass": "9.0", + "tumbler_glass": "9.0", + "whisky": "9.0", + "fork_knife_plate": "7.0", + "fork_and_knife_with_plate": "7.0", + "fork_and_knife": "6.0", + "spoon": "9.0", + "knife": "6.0", + "amphora": "8.0", + "earth_africa": "6.0", + "earth_americas": "6.0", + "earth_asia": "6.0", + "globe_with_meridians": "6.0", + "map": "7.0", + "world_map": "7.0", + "japan": "6.0", + "mountain_snow": "7.0", + "snow_capped_mountain": "7.0", + "mountain": "5.2", + "volcano": "6.0", + "mount_fuji": "6.0", + "camping": "7.0", + "beach": "7.0", + "beach_with_umbrella": "7.0", + "desert": "7.0", + "island": "7.0", + "desert_island": "7.0", + "park": "7.0", + "national_park": "7.0", + "stadium": "7.0", + "classical_building": "7.0", + "construction_site": "7.0", + "building_construction": "7.0", + "homes": "7.0", + "house_buildings": "7.0", + "cityscape": "7.0", + "house_abandoned": "7.0", + "derelict_house_building": "7.0", + "house": "6.0", + "house_with_garden": "6.0", + "office": "6.0", + "post_office": "6.0", + "european_post_office": "6.0", + "hospital": "6.0", + "bank": "6.0", + "hotel": "6.0", + "love_hotel": "6.0", + "convenience_store": "6.0", + "school": "6.0", + "department_store": "6.0", + "factory": "6.0", + "japanese_castle": "6.0", + "european_castle": "6.0", + "wedding": "6.0", + "tokyo_tower": "6.0", + "statue_of_liberty": "6.0", + "church": "5.2", + "mosque": "8.0", + "synagogue": "8.0", + "shinto_shrine": "5.2", + "kaaba": "8.0", + "fountain": "5.2", + "tent": "5.2", + "foggy": "6.0", + "night_with_stars": "6.0", + "sunrise_over_mountains": "6.0", + "sunrise": "6.0", + "city_dusk": "6.0", + "city_sunset": "6.0", + "city_sunrise": "6.0", + "bridge_at_night": "6.0", + "hotsprings": "1.1", + "milky_way": "6.0", + "carousel_horse": "6.0", + "ferris_wheel": "6.0", + "roller_coaster": "6.0", + "barber": "6.0", + "circus_tent": "6.0", + "performing_arts": "6.0", + "frame_photo": "7.0", + "frame_with_picture": "7.0", + "art": "6.0", + "slot_machine": "6.0", + "steam_locomotive": "6.0", + "railway_car": "6.0", + "bullettrain_side": "6.0", + "bullettrain_front": "6.0", + "train2": "6.0", + "metro": "6.0", + "light_rail": "6.0", + "station": "6.0", + "tram": "6.0", + "monorail": "6.0", + "mountain_railway": "6.0", + "train": "6.0", + "bus": "6.0", + "oncoming_bus": "6.0", + "trolleybus": "6.0", + "minibus": "6.0", + "ambulance": "6.0", + "fire_engine": "6.0", + "police_car": "6.0", + "oncoming_police_car": "6.0", + "taxi": "6.0", + "oncoming_taxi": "6.0", + "red_car": "6.0", + "oncoming_automobile": "6.0", + "blue_car": "6.0", + "truck": "6.0", + "articulated_lorry": "6.0", + "tractor": "6.0", + "bike": "6.0", + "scooter": "9.0", + "motor_scooter": "9.0", + "motorbike": "9.0", + "busstop": "6.0", + "motorway": "7.0", + "railway_track": "7.0", + "railroad_track": "7.0", + "fuelpump": "5.2", + "rotating_light": "6.0", + "traffic_light": "6.0", + "vertical_traffic_light": "6.0", + "construction": "6.0", + "octagonal_sign": "9.0", + "stop_sign": "9.0", + "anchor": "4.1", + "sailboat": "5.2", + "canoe": "9.0", + "kayak": "9.0", + "speedboat": "6.0", + "cruise_ship": "7.0", + "passenger_ship": "7.0", + "ferry": "5.2", + "motorboat": "7.0", + "ship": "6.0", + "airplane": "1.1", + "airplane_small": "7.0", + "small_airplane": "7.0", + "airplane_departure": "7.0", + "airplane_arriving": "7.0", + "seat": "6.0", + "helicopter": "6.0", + "suspension_railway": "6.0", + "mountain_cableway": "6.0", + "aerial_tramway": "6.0", + "rocket": "6.0", + "satellite_orbital": "7.0", + "bellhop": "7.0", + "bellhop_bell": "7.0", + "door": "6.0", + "sleeping_accommodation": "7.0", + "bed": "7.0", + "couch": "7.0", + "couch_and_lamp": "7.0", + "toilet": "6.0", + "shower": "6.0", + "bath": "6.0", + "bath_tone1": "8.0", + "bath_tone2": "8.0", + "bath_tone3": "8.0", + "bath_tone4": "8.0", + "bath_tone5": "8.0", + "bathtub": "6.0", + "hourglass": "1.1", + "hourglass_flowing_sand": "6.0", + "watch": "1.1", + "alarm_clock": "6.0", + "stopwatch": "6.0", + "timer": "6.0", + "timer_clock": "6.0", + "clock": "7.0", + "mantlepiece_clock": "7.0", + "clock12": "6.0", + "clock1230": "6.0", + "clock1": "6.0", + "clock130": "6.0", + "clock2": "6.0", + "clock230": "6.0", + "clock3": "6.0", + "clock330": "6.0", + "clock4": "6.0", + "clock430": "6.0", + "clock5": "6.0", + "clock530": "6.0", + "clock6": "6.0", + "clock630": "6.0", + "clock7": "6.0", + "clock730": "6.0", + "clock8": "6.0", + "clock830": "6.0", + "clock9": "6.0", + "clock930": "6.0", + "clock10": "6.0", + "clock1030": "6.0", + "clock11": "6.0", + "clock1130": "6.0", + "new_moon": "6.0", + "waxing_crescent_moon": "6.0", + "first_quarter_moon": "6.0", + "waxing_gibbous_moon": "6.0", + "full_moon": "6.0", + "waning_gibbous_moon": "6.0", + "last_quarter_moon": "6.0", + "waning_crescent_moon": "6.0", + "crescent_moon": "6.0", + "new_moon_with_face": "6.0", + "first_quarter_moon_with_face": "6.0", + "last_quarter_moon_with_face": "6.0", + "thermometer": "7.0", + "sunny": "1.1", + "full_moon_with_face": "6.0", + "sun_with_face": "6.0", + "star": "5.1", + "star2": "6.0", + "stars": "6.0", + "cloud": "1.1", + "partly_sunny": "5.2", + "thunder_cloud_rain": "5.2", + "thunder_cloud_and_rain": "5.2", + "white_sun_small_cloud": "7.0", + "white_sun_with_small_cloud": "7.0", + "white_sun_cloud": "7.0", + "white_sun_behind_cloud": "7.0", + "white_sun_rain_cloud": "7.0", + "white_sun_behind_cloud_with_rain": "7.0", + "cloud_rain": "7.0", + "cloud_with_rain": "7.0", + "cloud_snow": "7.0", + "cloud_with_snow": "7.0", + "cloud_lightning": "7.0", + "cloud_with_lightning": "7.0", + "cloud_tornado": "7.0", + "cloud_with_tornado": "7.0", + "fog": "7.0", + "wind_blowing_face": "7.0", + "cyclone": "6.0", + "rainbow": "6.0", + "closed_umbrella": "6.0", + "umbrella2": "1.1", + "umbrella": "4.0", + "beach_umbrella": "5.2", + "umbrella_on_ground": "5.2", + "zap": "4.0", + "snowflake": "1.1", + "snowman2": "1.1", + "snowman": "5.2", + "comet": "1.1", + "fire": "6.0", + "flame": "6.0", + "droplet": "6.0", + "ocean": "6.0", + "jack_o_lantern": "6.0", + "christmas_tree": "6.0", + "fireworks": "6.0", + "sparkler": "6.0", + "sparkles": "6.0", + "balloon": "6.0", + "tada": "6.0", + "confetti_ball": "6.0", + "tanabata_tree": "6.0", + "bamboo": "6.0", + "dolls": "6.0", + "flags": "6.0", + "wind_chime": "6.0", + "rice_scene": "6.0", + "ribbon": "6.0", + "gift": "6.0", + "reminder_ribbon": "7.0", + "tickets": "7.0", + "admission_tickets": "7.0", + "ticket": "6.0", + "military_medal": "7.0", + "trophy": "6.0", + "medal": "7.0", + "sports_medal": "7.0", + "first_place": "9.0", + "first_place_medal": "9.0", + "second_place": "9.0", + "second_place_medal": "9.0", + "third_place": "9.0", + "third_place_medal": "9.0", + "soccer": "5.2", + "baseball": "5.2", + "basketball": "6.0", + "volleyball": "8.0", + "football": "6.0", + "rugby_football": "6.0", + "tennis": "6.0", + "8ball": "6.0", + "bowling": "6.0", + "cricket": "8.0", + "cricket_bat_ball": "8.0", + "field_hockey": "8.0", + "hockey": "8.0", + "ping_pong": "8.0", + "table_tennis": "8.0", + "badminton": "8.0", + "boxing_glove": "9.0", + "boxing_gloves": "9.0", + "martial_arts_uniform": "9.0", + "karate_uniform": "9.0", + "goal": "9.0", + "goal_net": "9.0", + "dart": "6.0", + "golf": "5.2", + "ice_skate": "5.2", + "fishing_pole_and_fish": "6.0", + "running_shirt_with_sash": "6.0", + "ski": "6.0", + "video_game": "6.0", + "joystick": "7.0", + "game_die": "6.0", + "spades": "1.1", + "hearts": "1.1", + "diamonds": "1.1", + "clubs": "1.1", + "black_joker": "6.0", + "mahjong": "5.1", + "flower_playing_cards": "6.0", + "mute": "6.0", + "speaker": "6.0", + "sound": "6.0", + "loud_sound": "6.0", + "loudspeaker": "6.0", + "mega": "6.0", + "postal_horn": "6.0", + "bell": "6.0", + "no_bell": "6.0", + "musical_score": "6.0", + "musical_note": "6.0", + "notes": "6.0", + "microphone2": "7.0", + "studio_microphone": "7.0", + "level_slider": "7.0", + "control_knobs": "7.0", + "microphone": "6.0", + "headphones": "6.0", + "radio": "6.0", + "saxophone": "6.0", + "guitar": "6.0", + "musical_keyboard": "6.0", + "trumpet": "6.0", + "violin": "6.0", + "drum": "9.0", + "drum_with_drumsticks": "9.0", + "iphone": "6.0", + "calling": "6.0", + "telephone": "1.1", + "telephone_receiver": "6.0", + "pager": "6.0", + "fax": "6.0", + "battery": "6.0", + "electric_plug": "6.0", + "computer": "6.0", + "desktop": "7.0", + "desktop_computer": "7.0", + "printer": "7.0", + "keyboard": "1.1", + "mouse_three_button": "7.0", + "three_button_mouse": "7.0", + "trackball": "7.0", + "minidisc": "6.0", + "floppy_disk": "6.0", + "cd": "6.0", + "dvd": "6.0", + "movie_camera": "6.0", + "film_frames": "7.0", + "projector": "7.0", + "film_projector": "7.0", + "clapper": "6.0", + "tv": "6.0", + "camera": "6.0", + "camera_with_flash": "7.0", + "video_camera": "6.0", + "vhs": "6.0", + "mag": "6.0", + "mag_right": "6.0", + "microscope": "6.0", + "telescope": "6.0", + "satellite": "6.0", + "candle": "7.0", + "bulb": "6.0", + "flashlight": "6.0", + "izakaya_lantern": "6.0", + "notebook_with_decorative_cover": "6.0", + "closed_book": "6.0", + "book": "6.0", + "green_book": "6.0", + "blue_book": "6.0", + "orange_book": "6.0", + "books": "6.0", + "notebook": "6.0", + "ledger": "6.0", + "page_with_curl": "6.0", + "scroll": "6.0", + "page_facing_up": "6.0", + "newspaper": "6.0", + "newspaper2": "7.0", + "rolled_up_newspaper": "7.0", + "bookmark_tabs": "6.0", + "bookmark": "6.0", + "label": "7.0", + "moneybag": "6.0", + "yen": "6.0", + "dollar": "6.0", + "euro": "6.0", + "pound": "6.0", + "money_with_wings": "6.0", + "credit_card": "6.0", + "chart": "6.0", + "currency_exchange": "6.0", + "heavy_dollar_sign": "6.0", + "envelope": "1.1", + "e-mail": "6.0", + "email": "6.0", + "incoming_envelope": "6.0", + "envelope_with_arrow": "6.0", + "outbox_tray": "6.0", + "inbox_tray": "6.0", + "package": "6.0", + "mailbox": "6.0", + "mailbox_closed": "6.0", + "mailbox_with_mail": "6.0", + "mailbox_with_no_mail": "6.0", + "postbox": "6.0", + "ballot_box": "7.0", + "ballot_box_with_ballot": "7.0", + "pencil2": "1.1", + "black_nib": "1.1", + "pen_fountain": "7.0", + "lower_left_fountain_pen": "7.0", + "pen_ballpoint": "7.0", + "lower_left_ballpoint_pen": "7.0", + "paintbrush": "7.0", + "lower_left_paintbrush": "7.0", + "crayon": "7.0", + "lower_left_crayon": "7.0", + "pencil": "6.0", + "briefcase": "6.0", + "file_folder": "6.0", + "open_file_folder": "6.0", + "dividers": "7.0", + "card_index_dividers": "7.0", + "date": "6.0", + "calendar": "6.0", + "notepad_spiral": "7.0", + "spiral_note_pad": "7.0", + "calendar_spiral": "7.0", + "spiral_calendar_pad": "7.0", + "card_index": "6.0", + "chart_with_upwards_trend": "6.0", + "chart_with_downwards_trend": "6.0", + "bar_chart": "6.0", + "clipboard": "6.0", + "pushpin": "6.0", + "round_pushpin": "6.0", + "paperclip": "6.0", + "paperclips": "7.0", + "linked_paperclips": "7.0", + "straight_ruler": "6.0", + "triangular_ruler": "6.0", + "scissors": "1.1", + "card_box": "7.0", + "card_file_box": "7.0", + "file_cabinet": "7.0", + "wastebasket": "7.0", + "lock": "6.0", + "unlock": "6.0", + "lock_with_ink_pen": "6.0", + "closed_lock_with_key": "6.0", + "key": "6.0", + "key2": "7.0", + "old_key": "7.0", + "hammer": "6.0", + "pick": "5.2", + "hammer_pick": "4.1", + "hammer_and_pick": "4.1", + "tools": "7.0", + "hammer_and_wrench": "7.0", + "dagger": "7.0", + "dagger_knife": "7.0", + "crossed_swords": "4.1", + "gun": "6.0", + "bow_and_arrow": "8.0", + "archery": "8.0", + "shield": "7.0", + "wrench": "6.0", + "nut_and_bolt": "6.0", + "gear": "4.1", + "compression": "7.0", + "alembic": "4.1", + "scales": "4.1", + "link": "6.0", + "chains": "5.2", + "syringe": "6.0", + "pill": "6.0", + "smoking": "6.0", + "coffin": "4.1", + "urn": "4.1", + "funeral_urn": "4.1", + "moyai": "6.0", + "oil": "7.0", + "oil_drum": "7.0", + "crystal_ball": "6.0", + "shopping_cart": "9.0", + "shopping_trolley": "9.0", + "atm": "6.0", + "put_litter_in_its_place": "6.0", + "potable_water": "6.0", + "wheelchair": "4.1", + "mens": "6.0", + "womens": "6.0", + "restroom": "6.0", + "baby_symbol": "6.0", + "wc": "6.0", + "passport_control": "6.0", + "customs": "6.0", + "baggage_claim": "6.0", + "left_luggage": "6.0", + "warning": "4.0", + "children_crossing": "6.0", + "no_entry": "5.2", + "no_entry_sign": "6.0", + "no_bicycles": "6.0", + "no_smoking": "6.0", + "do_not_litter": "6.0", + "non-potable_water": "6.0", + "no_pedestrians": "6.0", + "no_mobile_phones": "6.0", + "underage": "6.0", + "radioactive": "1.1", + "radioactive_sign": "1.1", + "biohazard": "1.1", + "biohazard_sign": "1.1", + "arrow_up": "4.0", + "arrow_upper_right": "1.1", + "arrow_right": "1.1", + "arrow_lower_right": "1.1", + "arrow_down": "4.0", + "arrow_lower_left": "1.1", + "arrow_left": "4.0", + "arrow_upper_left": "1.1", + "arrow_up_down": "1.1", + "left_right_arrow": "1.1", + "leftwards_arrow_with_hook": "1.1", + "arrow_right_hook": "1.1", + "arrow_heading_up": "3.2", + "arrow_heading_down": "3.2", + "arrows_clockwise": "6.0", + "arrows_counterclockwise": "6.0", + "back": "6.0", + "end": "6.0", + "on": "6.0", + "soon": "6.0", + "top": "6.0", + "place_of_worship": "8.0", + "worship_symbol": "8.0", + "atom": "4.1", + "atom_symbol": "4.1", + "om_symbol": "7.0", + "star_of_david": "1.1", + "wheel_of_dharma": "1.1", + "yin_yang": "1.1", + "cross": "1.1", + "latin_cross": "1.1", + "orthodox_cross": "1.1", + "star_and_crescent": "1.1", + "peace": "1.1", + "peace_symbol": "1.1", + "menorah": "8.0", + "six_pointed_star": "6.0", + "aries": "1.1", + "taurus": "1.1", + "gemini": "1.1", + "cancer": "1.1", + "leo": "1.1", + "virgo": "1.1", + "libra": "1.1", + "scorpius": "1.1", + "sagittarius": "1.1", + "capricorn": "1.1", + "aquarius": "1.1", + "pisces": "1.1", + "ophiuchus": "6.0", + "twisted_rightwards_arrows": "6.0", + "repeat": "6.0", + "repeat_one": "6.0", + "arrow_forward": "1.1", + "fast_forward": "6.0", + "track_next": "6.0", + "next_track": "6.0", + "play_pause": "6.0", + "arrow_backward": "1.1", + "rewind": "6.0", + "track_previous": "6.0", + "previous_track": "6.0", + "arrow_up_small": "6.0", + "arrow_double_up": "6.0", + "arrow_down_small": "6.0", + "arrow_double_down": "6.0", + "pause_button": "7.0", + "double_vertical_bar": "7.0", + "stop_button": "7.0", + "record_button": "7.0", + "eject": "4.0", + "eject_symbol": "4.0", + "cinema": "6.0", + "low_brightness": "6.0", + "high_brightness": "6.0", + "signal_strength": "6.0", + "vibration_mode": "6.0", + "mobile_phone_off": "6.0", + "recycle": "3.2", + "name_badge": "6.0", + "fleur-de-lis": "4.1", + "beginner": "6.0", + "trident": "6.0", + "o": "5.2", + "white_check_mark": "6.0", + "ballot_box_with_check": "1.1", + "heavy_check_mark": "1.1", + "heavy_multiplication_x": "1.1", + "x": "6.0", + "negative_squared_cross_mark": "6.0", + "heavy_plus_sign": "6.0", + "heavy_minus_sign": "6.0", + "heavy_division_sign": "6.0", + "curly_loop": "6.0", + "loop": "6.0", + "part_alternation_mark": "3.2", + "eight_spoked_asterisk": "1.1", + "eight_pointed_black_star": "1.1", + "sparkle": "1.1", + "bangbang": "1.1", + "interrobang": "3.0", + "question": "6.0", + "grey_question": "6.0", + "grey_exclamation": "6.0", + "exclamation": "5.2", + "wavy_dash": "1.1", + "copyright": "1.1", + "registered": "1.1", + "tm": "1.1", + "hash": "3.0", + "asterisk": "3.0", + "keycap_asterisk": "3.0", + "zero": "3.0", + "one": "3.0", + "two": "3.0", + "three": "3.0", + "four": "3.0", + "five": "3.0", + "six": "3.0", + "seven": "3.0", + "eight": "3.0", + "nine": "3.0", + "keycap_ten": "6.0", + "capital_abcd": "6.0", + "abcd": "6.0", + "symbols": "6.0", + "abc": "6.0", + "a": "6.0", + "ab": "6.0", + "b": "6.0", + "cl": "6.0", + "cool": "6.0", + "free": "6.0", + "information_source": "3.0", + "id": "6.0", + "m": "1.1", + "new": "6.0", + "ng": "6.0", + "o2": "6.0", + "ok": "6.0", + "parking": "5.2", + "sos": "6.0", + "up": "6.0", + "vs": "6.0", + "koko": "6.0", + "sa": "6.0", + "u6708": "6.0", + "u6709": "6.0", + "u6307": "5.2", + "ideograph_advantage": "6.0", + "u5272": "6.0", + "u7121": "5.2", + "u7981": "6.0", + "accept": "6.0", + "u7533": "6.0", + "u5408": "6.0", + "u7a7a": "6.0", + "congratulations": "1.1", + "secret": "1.1", + "u55b6": "6.0", + "u6e80": "6.0", + "black_small_square": "1.1", + "white_small_square": "1.1", + "white_medium_square": "3.2", + "black_medium_square": "3.2", + "white_medium_small_square": "3.2", + "black_medium_small_square": "3.2", + "black_large_square": "5.1", + "white_large_square": "5.1", + "large_orange_diamond": "6.0", + "large_blue_diamond": "6.0", + "small_orange_diamond": "6.0", + "small_blue_diamond": "6.0", + "small_red_triangle": "6.0", + "small_red_triangle_down": "6.0", + "diamond_shape_with_a_dot_inside": "6.0", + "radio_button": "6.0", + "black_square_button": "6.0", + "white_square_button": "6.0", + "white_circle": "4.1", + "black_circle": "4.1", + "red_circle": "6.0", + "blue_circle": "6.0", + "checkered_flag": "6.0", + "triangular_flag_on_post": "6.0", + "crossed_flags": "6.0", + "flag_black": "6.0", + "waving_black_flag": "6.0", + "flag_white": "6.0", + "waving_white_flag": "6.0", + "rainbow_flag": "6.0", + "gay_pride_flag": "6.0", + "flag_ac": "6.0", + "ac": "6.0", + "flag_ad": "6.0", + "ad": "6.0", + "flag_ae": "6.0", + "ae": "6.0", + "flag_af": "6.0", + "af": "6.0", + "flag_ag": "6.0", + "ag": "6.0", + "flag_ai": "6.0", + "ai": "6.0", + "flag_al": "6.0", + "al": "6.0", + "flag_am": "6.0", + "am": "6.0", + "flag_ao": "6.0", + "ao": "6.0", + "flag_aq": "6.0", + "aq": "6.0", + "flag_ar": "6.0", + "ar": "6.0", + "flag_as": "6.0", + "as": "6.0", + "flag_at": "6.0", + "at": "6.0", + "flag_au": "6.0", + "au": "6.0", + "flag_aw": "6.0", + "aw": "6.0", + "flag_ax": "6.0", + "ax": "6.0", + "flag_az": "6.0", + "az": "6.0", + "flag_ba": "6.0", + "ba": "6.0", + "flag_bb": "6.0", + "bb": "6.0", + "flag_bd": "6.0", + "bd": "6.0", + "flag_be": "6.0", + "be": "6.0", + "flag_bf": "6.0", + "bf": "6.0", + "flag_bg": "6.0", + "bg": "6.0", + "flag_bh": "6.0", + "bh": "6.0", + "flag_bi": "6.0", + "bi": "6.0", + "flag_bj": "6.0", + "bj": "6.0", + "flag_bl": "6.0", + "bl": "6.0", + "flag_bm": "6.0", + "bm": "6.0", + "flag_bn": "6.0", + "bn": "6.0", + "flag_bo": "6.0", + "bo": "6.0", + "flag_bq": "6.0", + "bq": "6.0", + "flag_br": "6.0", + "br": "6.0", + "flag_bs": "6.0", + "bs": "6.0", + "flag_bt": "6.0", + "bt": "6.0", + "flag_bv": "6.0", + "bv": "6.0", + "flag_bw": "6.0", + "bw": "6.0", + "flag_by": "6.0", + "by": "6.0", + "flag_bz": "6.0", + "bz": "6.0", + "flag_ca": "6.0", + "ca": "6.0", + "flag_cc": "6.0", + "cc": "6.0", + "flag_cd": "6.0", + "congo": "6.0", + "flag_cf": "6.0", + "cf": "6.0", + "flag_cg": "6.0", + "cg": "6.0", + "flag_ch": "6.0", + "ch": "6.0", + "flag_ci": "6.0", + "ci": "6.0", + "flag_ck": "6.0", + "ck": "6.0", + "flag_cl": "6.0", + "chile": "6.0", + "flag_cm": "6.0", + "cm": "6.0", + "flag_cn": "6.0", + "cn": "6.0", + "flag_co": "6.0", + "co": "6.0", + "flag_cp": "6.0", + "cp": "6.0", + "flag_cr": "6.0", + "cr": "6.0", + "flag_cu": "6.0", + "cu": "6.0", + "flag_cv": "6.0", + "cv": "6.0", + "flag_cw": "6.0", + "cw": "6.0", + "flag_cx": "6.0", + "cx": "6.0", + "flag_cy": "6.0", + "cy": "6.0", + "flag_cz": "6.0", + "cz": "6.0", + "flag_de": "6.0", + "de": "6.0", + "flag_dg": "6.0", + "dg": "6.0", + "flag_dj": "6.0", + "dj": "6.0", + "flag_dk": "6.0", + "dk": "6.0", + "flag_dm": "6.0", + "dm": "6.0", + "flag_do": "6.0", + "do": "6.0", + "flag_dz": "6.0", + "dz": "6.0", + "flag_ea": "6.0", + "ea": "6.0", + "flag_ec": "6.0", + "ec": "6.0", + "flag_ee": "6.0", + "ee": "6.0", + "flag_eg": "6.0", + "eg": "6.0", + "flag_eh": "6.0", + "eh": "6.0", + "flag_er": "6.0", + "er": "6.0", + "flag_es": "6.0", + "es": "6.0", + "flag_et": "6.0", + "et": "6.0", + "flag_eu": "6.0", + "eu": "6.0", + "flag_fi": "6.0", + "fi": "6.0", + "flag_fj": "6.0", + "fj": "6.0", + "flag_fk": "6.0", + "fk": "6.0", + "flag_fm": "6.0", + "fm": "6.0", + "flag_fo": "6.0", + "fo": "6.0", + "flag_fr": "6.0", + "fr": "6.0", + "flag_ga": "6.0", + "ga": "6.0", + "flag_gb": "6.0", + "gb": "6.0", + "flag_gd": "6.0", + "gd": "6.0", + "flag_ge": "6.0", + "ge": "6.0", + "flag_gf": "6.0", + "gf": "6.0", + "flag_gg": "6.0", + "gg": "6.0", + "flag_gh": "6.0", + "gh": "6.0", + "flag_gi": "6.0", + "gi": "6.0", + "flag_gl": "6.0", + "gl": "6.0", + "flag_gm": "6.0", + "gm": "6.0", + "flag_gn": "6.0", + "gn": "6.0", + "flag_gp": "6.0", + "gp": "6.0", + "flag_gq": "6.0", + "gq": "6.0", + "flag_gr": "6.0", + "gr": "6.0", + "flag_gs": "6.0", + "gs": "6.0", + "flag_gt": "6.0", + "gt": "6.0", + "flag_gu": "6.0", + "gu": "6.0", + "flag_gw": "6.0", + "gw": "6.0", + "flag_gy": "6.0", + "gy": "6.0", + "flag_hk": "6.0", + "hk": "6.0", + "flag_hm": "6.0", + "hm": "6.0", + "flag_hn": "6.0", + "hn": "6.0", + "flag_hr": "6.0", + "hr": "6.0", + "flag_ht": "6.0", + "ht": "6.0", + "flag_hu": "6.0", + "hu": "6.0", + "flag_ic": "6.0", + "ic": "6.0", + "flag_id": "6.0", + "indonesia": "6.0", + "flag_ie": "6.0", + "ie": "6.0", + "flag_il": "6.0", + "il": "6.0", + "flag_im": "6.0", + "im": "6.0", + "flag_in": "6.0", + "in": "6.0", + "flag_io": "6.0", + "io": "6.0", + "flag_iq": "6.0", + "iq": "6.0", + "flag_ir": "6.0", + "ir": "6.0", + "flag_is": "6.0", + "is": "6.0", + "flag_it": "6.0", + "it": "6.0", + "flag_je": "6.0", + "je": "6.0", + "flag_jm": "6.0", + "jm": "6.0", + "flag_jo": "6.0", + "jo": "6.0", + "flag_jp": "6.0", + "jp": "6.0", + "flag_ke": "6.0", + "ke": "6.0", + "flag_kg": "6.0", + "kg": "6.0", + "flag_kh": "6.0", + "kh": "6.0", + "flag_ki": "6.0", + "ki": "6.0", + "flag_km": "6.0", + "km": "6.0", + "flag_kn": "6.0", + "kn": "6.0", + "flag_kp": "6.0", + "kp": "6.0", + "flag_kr": "6.0", + "kr": "6.0", + "flag_kw": "6.0", + "kw": "6.0", + "flag_ky": "6.0", + "ky": "6.0", + "flag_kz": "6.0", + "kz": "6.0", + "flag_la": "6.0", + "la": "6.0", + "flag_lb": "6.0", + "lb": "6.0", + "flag_lc": "6.0", + "lc": "6.0", + "flag_li": "6.0", + "li": "6.0", + "flag_lk": "6.0", + "lk": "6.0", + "flag_lr": "6.0", + "lr": "6.0", + "flag_ls": "6.0", + "ls": "6.0", + "flag_lt": "6.0", + "lt": "6.0", + "flag_lu": "6.0", + "lu": "6.0", + "flag_lv": "6.0", + "lv": "6.0", + "flag_ly": "6.0", + "ly": "6.0", + "flag_ma": "6.0", + "ma": "6.0", + "flag_mc": "6.0", + "mc": "6.0", + "flag_md": "6.0", + "md": "6.0", + "flag_me": "6.0", + "me": "6.0", + "flag_mf": "6.0", + "mf": "6.0", + "flag_mg": "6.0", + "mg": "6.0", + "flag_mh": "6.0", + "mh": "6.0", + "flag_mk": "6.0", + "mk": "6.0", + "flag_ml": "6.0", + "ml": "6.0", + "flag_mm": "6.0", + "mm": "6.0", + "flag_mn": "6.0", + "mn": "6.0", + "flag_mo": "6.0", + "mo": "6.0", + "flag_mp": "6.0", + "mp": "6.0", + "flag_mq": "6.0", + "mq": "6.0", + "flag_mr": "6.0", + "mr": "6.0", + "flag_ms": "6.0", + "ms": "6.0", + "flag_mt": "6.0", + "mt": "6.0", + "flag_mu": "6.0", + "mu": "6.0", + "flag_mv": "6.0", + "mv": "6.0", + "flag_mw": "6.0", + "mw": "6.0", + "flag_mx": "6.0", + "mx": "6.0", + "flag_my": "6.0", + "my": "6.0", + "flag_mz": "6.0", + "mz": "6.0", + "flag_na": "6.0", + "na": "6.0", + "flag_nc": "6.0", + "nc": "6.0", + "flag_ne": "6.0", + "ne": "6.0", + "flag_nf": "6.0", + "nf": "6.0", + "flag_ng": "6.0", + "nigeria": "6.0", + "flag_ni": "6.0", + "ni": "6.0", + "flag_nl": "6.0", + "nl": "6.0", + "flag_no": "6.0", + "no": "6.0", + "flag_np": "6.0", + "np": "6.0", + "flag_nr": "6.0", + "nr": "6.0", + "flag_nu": "6.0", + "nu": "6.0", + "flag_nz": "6.0", + "nz": "6.0", + "flag_om": "6.0", + "om": "6.0", + "flag_pa": "6.0", + "pa": "6.0", + "flag_pe": "6.0", + "pe": "6.0", + "flag_pf": "6.0", + "pf": "6.0", + "flag_pg": "6.0", + "pg": "6.0", + "flag_ph": "6.0", + "ph": "6.0", + "flag_pk": "6.0", + "pk": "6.0", + "flag_pl": "6.0", + "pl": "6.0", + "flag_pm": "6.0", + "pm": "6.0", + "flag_pn": "6.0", + "pn": "6.0", + "flag_pr": "6.0", + "pr": "6.0", + "flag_ps": "6.0", + "ps": "6.0", + "flag_pt": "6.0", + "pt": "6.0", + "flag_pw": "6.0", + "pw": "6.0", + "flag_py": "6.0", + "py": "6.0", + "flag_qa": "6.0", + "qa": "6.0", + "flag_re": "6.0", + "re": "6.0", + "flag_ro": "6.0", + "ro": "6.0", + "flag_rs": "6.0", + "rs": "6.0", + "flag_ru": "6.0", + "ru": "6.0", + "flag_rw": "6.0", + "rw": "6.0", + "flag_sa": "6.0", + "saudiarabia": "6.0", + "saudi": "6.0", + "flag_sb": "6.0", + "sb": "6.0", + "flag_sc": "6.0", + "sc": "6.0", + "flag_sd": "6.0", + "sd": "6.0", + "flag_se": "6.0", + "se": "6.0", + "flag_sg": "6.0", + "sg": "6.0", + "flag_sh": "6.0", + "sh": "6.0", + "flag_si": "6.0", + "si": "6.0", + "flag_sj": "6.0", + "sj": "6.0", + "flag_sk": "6.0", + "sk": "6.0", + "flag_sl": "6.0", + "sl": "6.0", + "flag_sm": "6.0", + "sm": "6.0", + "flag_sn": "6.0", + "sn": "6.0", + "flag_so": "6.0", + "so": "6.0", + "flag_sr": "6.0", + "sr": "6.0", + "flag_ss": "6.0", + "ss": "6.0", + "flag_st": "6.0", + "st": "6.0", + "flag_sv": "6.0", + "sv": "6.0", + "flag_sx": "6.0", + "sx": "6.0", + "flag_sy": "6.0", + "sy": "6.0", + "flag_sz": "6.0", + "sz": "6.0", + "flag_ta": "6.0", + "ta": "6.0", + "flag_tc": "6.0", + "tc": "6.0", + "flag_td": "6.0", + "td": "6.0", + "flag_tf": "6.0", + "tf": "6.0", + "flag_tg": "6.0", + "tg": "6.0", + "flag_th": "6.0", + "th": "6.0", + "flag_tj": "6.0", + "tj": "6.0", + "flag_tk": "6.0", + "tk": "6.0", + "flag_tl": "6.0", + "tl": "6.0", + "flag_tm": "6.0", + "turkmenistan": "6.0", + "flag_tn": "6.0", + "tn": "6.0", + "flag_to": "6.0", + "to": "6.0", + "flag_tr": "6.0", + "tr": "6.0", + "flag_tt": "6.0", + "tt": "6.0", + "flag_tv": "6.0", + "tuvalu": "6.0", + "flag_tw": "6.0", + "tw": "6.0", + "flag_tz": "6.0", + "tz": "6.0", + "flag_ua": "6.0", + "ua": "6.0", + "flag_ug": "6.0", + "ug": "6.0", + "flag_um": "6.0", + "um": "6.0", + "flag_us": "6.0", + "us": "6.0", + "flag_uy": "6.0", + "uy": "6.0", + "flag_uz": "6.0", + "uz": "6.0", + "flag_va": "6.0", + "va": "6.0", + "flag_vc": "6.0", + "vc": "6.0", + "flag_ve": "6.0", + "ve": "6.0", + "flag_vg": "6.0", + "vg": "6.0", + "flag_vi": "6.0", + "vi": "6.0", + "flag_vn": "6.0", + "vn": "6.0", + "flag_vu": "6.0", + "vu": "6.0", + "flag_wf": "6.0", + "wf": "6.0", + "flag_ws": "6.0", + "ws": "6.0", + "flag_xk": "6.0", + "xk": "6.0", + "flag_ye": "6.0", + "ye": "6.0", + "flag_yt": "6.0", + "yt": "6.0", + "flag_za": "6.0", + "za": "6.0", + "flag_zm": "6.0", + "zm": "6.0", + "flag_zw": "6.0", + "zw": "6.0", + "regional_indicator_z": "6.0", + "regional_indicator_y": "6.0", + "regional_indicator_x": "6.0", + "regional_indicator_w": "6.0", + "regional_indicator_v": "6.0", + "regional_indicator_u": "6.0", + "regional_indicator_t": "6.0", + "regional_indicator_s": "6.0", + "regional_indicator_r": "6.0", + "regional_indicator_q": "6.0", + "regional_indicator_p": "6.0", + "regional_indicator_o": "6.0", + "regional_indicator_n": "6.0", + "regional_indicator_m": "6.0", + "regional_indicator_l": "6.0", + "regional_indicator_k": "6.0", + "regional_indicator_j": "6.0", + "regional_indicator_i": "6.0", + "regional_indicator_h": "6.0", + "regional_indicator_g": "6.0", + "regional_indicator_f": "6.0", + "regional_indicator_e": "6.0", + "regional_indicator_d": "6.0", + "regional_indicator_c": "6.0", + "regional_indicator_b": "6.0", + "regional_indicator_a": "6.0", + "large_blue_circle": "6.0", + "ten": "6.0" +}
\ 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/deployments.rb b/lib/api/deployments.rb index c5feb49b22f..2f1ad12c38c 100644 --- a/lib/api/deployments.rb +++ b/lib/api/deployments.rb @@ -1,5 +1,5 @@ module API - # Deployments RESTfull API endpoints + # Deployments RESTful API endpoints class Deployments < Grape::API include PaginationParams 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/deployments.rb b/lib/api/v3/deployments.rb index 545485fac0a..95114ad1fe1 100644 --- a/lib/api/v3/deployments.rb +++ b/lib/api/v3/deployments.rb @@ -1,40 +1,42 @@ module API - # Deployments RESTfull API endpoints - class Deployments < Grape::API - include PaginationParams + module V3 + # Deployments RESTful API endpoints + class Deployments < Grape::API + include PaginationParams - before { authenticate! } + before { authenticate! } - params do - requires :id, type: String, desc: 'The project ID' - end - resource :projects do - desc 'Get all deployments of the project' do - detail 'This feature was introduced in GitLab 8.11.' - success ::API::V3::Deployments - end params do - use :pagination + requires :id, type: String, desc: 'The project ID' end - get ':id/deployments' do - authorize! :read_deployment, user_project + resource :projects do + desc 'Get all deployments of the project' do + detail 'This feature was introduced in GitLab 8.11.' + success ::API::V3::Deployments + end + params do + use :pagination + end + get ':id/deployments' do + authorize! :read_deployment, user_project - present paginate(user_project.deployments), with: ::API::V3::Deployments - end + present paginate(user_project.deployments), with: ::API::V3::Deployments + end - desc 'Gets a specific deployment' do - detail 'This feature was introduced in GitLab 8.11.' - success ::API::V3::Deployments - end - params do - requires :deployment_id, type: Integer, desc: 'The deployment ID' - end - get ':id/deployments/:deployment_id' do - authorize! :read_deployment, user_project + desc 'Gets a specific deployment' do + detail 'This feature was introduced in GitLab 8.11.' + success ::API::V3::Deployments + end + params do + requires :deployment_id, type: Integer, desc: 'The deployment ID' + end + get ':id/deployments/:deployment_id' do + authorize! :read_deployment, user_project - deployment = user_project.deployments.find(params[:deployment_id]) + deployment = user_project.deployments.find(params[:deployment_id]) - present deployment, with: ::API::V3::Deployments + present deployment, with: ::API::V3::Deployments + end end 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/manager.rb b/lib/backup/manager.rb index 5cc164a6325..7b4476fa4db 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -51,7 +51,8 @@ module Backup if directory.files.create(key: tar_file, body: File.open(tar_file), public: false, multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size, - encryption: Gitlab.config.backup.upload.encryption) + encryption: Gitlab.config.backup.upload.encryption, + storage_class: Gitlab.config.backup.upload.storage_class) $progress.puts "done".color(:green) else puts "uploading backup to #{remote_directory} failed".color(:red) 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/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index af1e575fc89..d5f9e252f62 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -35,6 +35,10 @@ module Banzai # Allow span elements whitelist[:elements].push('span') + # Allow html5 details/summary elements + whitelist[:elements].push('details') + whitelist[:elements].push('summary') + # Allow abbr elements with title attribute whitelist[:elements].push('abbr') whitelist[:attributes]['abbr'] = %w(title) 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/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index e390919ae1d..15a461a16dd 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -58,7 +58,7 @@ module Ci commands: job[:commands], tag_list: job[:tags] || [], name: job[:name].to_s, - allow_failure: job[:allow_failure] || false, + allow_failure: job[:ignore], when: job[:when] || 'on_success', environment: job[:environment_name], coverage_regex: job[:coverage], 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/job.rb b/lib/gitlab/ci/config/entry/job.rb index 7f7662f2776..176301bcca1 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -104,6 +104,14 @@ module Gitlab (before_script_value.to_a + script_value.to_a).join("\n") end + def manual_action? + self.when == 'manual' + end + + def ignored? + allow_failure.nil? ? manual_action? : allow_failure + end + private def inherit!(deps) @@ -135,7 +143,8 @@ module Gitlab environment_name: environment_defined? ? environment_value[:name] : nil, coverage: coverage_defined? ? coverage_value : nil, artifacts: artifacts_value, - after_script: after_script_value } + after_script: after_script_value, + ignore: ignored? } 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/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index 0f4b7b24cef..3495b8d0448 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -5,22 +5,10 @@ module Gitlab class Play < SimpleDelegator include Status::Extended - def text - 'manual' - end - def label 'manual play action' end - def icon - 'icon_status_manual' - end - - def group - 'manual' - end - def has_action? can?(user, :update_build, subject) end diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index 90401cad0d2..e8530f2aaae 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -5,22 +5,10 @@ module Gitlab class Stop < SimpleDelegator include Status::Extended - def text - 'manual' - end - def label 'manual stop action' end - def icon - 'icon_status_manual' - end - - def group - 'manual' - end - def has_action? can?(user, :update_build, subject) end diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb new file mode 100644 index 00000000000..5f28521901d --- /dev/null +++ b/lib/gitlab/ci/status/manual.rb @@ -0,0 +1,19 @@ +module Gitlab + module Ci + module Status + class Manual < Status::Core + def text + 'manual' + end + + def label + 'manual action' + end + + def icon + 'icon_status_manual' + end + end + end + end +end diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb index e50e54b6e99..182a30fd74d 100644 --- a/lib/gitlab/data_builder/pipeline.rb +++ b/lib/gitlab/data_builder/pipeline.rb @@ -39,7 +39,7 @@ module Gitlab started_at: build.started_at, finished_at: build.finished_at, when: build.when, - manual: build.manual?, + manual: build.action?, user: build.user.try(:hook_attrs), runner: build.runner && runner_hook_attrs(build.runner), artifacts_file: { diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb index bbbca8acc40..35871fd1b7b 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('fixtures', 'emojis', '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/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb index 0a8d05b5fe1..5d29e698b27 100644 --- a/lib/gitlab/github_import/branch_formatter.rb +++ b/lib/gitlab/github_import/branch_formatter.rb @@ -18,7 +18,7 @@ module Gitlab end def commit_exists? - project.repository.commit(sha).present? + project.repository.branch_names_contains(sha).include?(ref) end def short_id diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 4ea0200e89b..28812fd0cb9 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -38,7 +38,11 @@ module Gitlab def source_branch_name @source_branch_name ||= begin - source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}" + if cross_project? + "pull/#{number}/#{source_branch_repo.full_name}/#{source_branch_ref}" + else + source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}" + end end end @@ -52,6 +56,10 @@ module Gitlab end end + def cross_project? + source_branch.repo.id != target_branch.repo.id + end + private def state 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/mattermost/client.rb b/lib/mattermost/client.rb index ad6df246091..3d60618006c 100644 --- a/lib/mattermost/client.rb +++ b/lib/mattermost/client.rb @@ -26,7 +26,7 @@ module Mattermost def session_get(path, options = {}) with_session do |session| - get(session, path, options) + get(session, path, options) end end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index 5388966605d..688a79c0441 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -153,7 +153,7 @@ module Mattermost yield rescue HTTParty::Error => e raise Mattermost::ConnectionError.new(e.message) - rescue Errno::ECONNREFUSED + rescue Errno::ECONNREFUSED => e raise Mattermost::ConnectionError.new(e.message) end end diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb index afc152aa02e..2cdbbdece16 100644 --- a/lib/mattermost/team.rb +++ b/lib/mattermost/team.rb @@ -1,7 +1,18 @@ module Mattermost class Team < Client + # Returns **all** teams for an admin def all session_get('/api/v3/teams/all').values end + + # Creates a team on the linked Mattermost instance, the team admin will be the + # `current_user` passed to the Mattermost::Client instance + def create(name:, display_name:, type:) + session_post('/api/v3/teams/create', body: { + name: name, + display_name: display_name, + type: type + }.to_json) + end end end diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake index 993112aee3b..5293f5af12d 100644 --- a/lib/tasks/gemojione.rake +++ b/lib/tasks/gemojione.rake @@ -1,33 +1,36 @@ namespace :gemojione do desc 'Generates Emoji SHA256 digests' - task digests: :environment do + task digests: ['yarn:check', 'environment'] do require 'digest/sha2' 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 + # We don't have `node_modules` available in built versions of GitLab + FileUtils.cp_r(Rails.root.join('node_modules', 'emoji-unicode-version', 'emoji-unicode-version-map.json'), File.join(Rails.root, 'fixtures', 'emojis')) - digests << { name: name, unicode: emoji_hash['unicode'], digest: digest } + dir = Gemojione.images_path + 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 +58,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 +105,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 +114,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 +136,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 +180,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 6b2991f9608..efa3a63e693 100644 --- a/package.json +++ b/package.json @@ -18,16 +18,21 @@ "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", "js-cookie": "^2.1.3", "mousetrap": "^1.4.6", "pikaday": "^1.5.1", + "raphael": "^2.2.7", "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/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb index 699c6f77cec..cd6961a7bd5 100644 --- a/spec/controllers/projects/uploads_controller_spec.rb +++ b/spec/controllers/projects/uploads_controller_spec.rb @@ -35,6 +35,19 @@ describe Projects::UploadsController do expect(response.body).to match '\"alt\":\"rails_sample\"' expect(response.body).to match "\"url\":\"/uploads" end + + # NOTE: This is as close as we're getting to an Integration test for this + # behavior. We're avoiding a proper Feature test because those should be + # testing things entirely user-facing, which the Upload model is very much + # not. + it 'creates a corresponding Upload record' do + upload = Upload.last + + aggregate_failures do + expect(upload).to exist + expect(upload.model).to eq project + end + end end context 'with valid non-image file' do 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 cabe128acf7..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 @@ -57,7 +57,7 @@ FactoryGirl.define do end trait :manual do - status 'skipped' + status 'manual' self.when 'manual' end @@ -71,8 +71,11 @@ FactoryGirl.define do allow_failure true end + trait :ignored do + allowed_to_fail + end + trait :playable do - skipped manual end @@ -163,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/commit_statuses.rb b/spec/factories/commit_statuses.rb index 756b341ecba..169590deb8e 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -35,6 +35,10 @@ FactoryGirl.define do status 'created' end + trait :manual do + status 'manual' + end + after(:build) do |build, evaluator| build.project = build.pipeline.project 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..0db2fe04edd 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -38,7 +38,7 @@ FactoryGirl.define do trait :empty_repo do after(:create) do |project| - project.create_repository + raise "Failed to create repository!" unless project.create_repository # We delete hooks so that gitlab-shell will not try to authenticate with # an API that isn't running @@ -48,7 +48,7 @@ FactoryGirl.define do trait :broken_repo do after(:create) do |project| - project.create_repository + raise "Failed to create repository!" unless project.create_repository FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.path_with_namespace}.git", 'refs')) end @@ -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 fec86128d03..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> @@ -275,6 +275,10 @@ describe 'Copy as GFM', feature: true, js: true do <rp>rp</rp> <abbr>abbr</abbr> + + <summary>summary</summary> + + <details>details</details> GFM ) diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb index aa75e1140f6..8c61cdebc4b 100644 --- a/spec/features/dashboard_issues_spec.rb +++ b/spec/features/dashboard_issues_spec.rb @@ -48,7 +48,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do it 'updates atom feed link' do visit_issues(milestone_title: '', assignee_id: user.id) - link = find('.nav-controls a', text: 'Subscribe') + link = find('.nav-controls a[title="Subscribe"]') params = CGI.parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 37b7c20239f..d243f9478bb 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -43,6 +43,44 @@ feature 'Group', feature: true do expect(page).to have_namespace_error_message end end + + describe 'Mattermost team creation' do + before do + allow(Settings.mattermost).to receive_messages(enabled: mattermost_enabled) + + visit new_group_path + end + + context 'Mattermost enabled' do + let(:mattermost_enabled) { true } + + it 'displays a team creation checkbox' do + expect(page).to have_selector('#group_create_chat_team') + end + + it 'checks the checkbox by default' do + expect(find('#group_create_chat_team')['checked']).to eq(true) + end + + it 'updates the team URL on graph path update', :js do + out_span = find('span[data-bind-out="create_chat_team"]') + + expect(out_span.text).to be_empty + + fill_in('group_path', with: 'test-group') + + expect(out_span.text).to eq('test-group') + end + end + + context 'Mattermost disabled' do + let(:mattermost_enabled) { false } + + it 'doesnt show a team creation checkbox if Mattermost not enabled' do + expect(page).not_to have_selector('#group_create_chat_team') + end + end + end end describe 'create a nested group' do @@ -105,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..85ffffe4b6d 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' -describe 'Dropdown milestone', js: true, feature: true do - include WaitForAjax +describe 'Dropdown milestone', :feature, :js do + include FilteredSearchHelpers let!(:project) { create(:empty_project) } let!(:user) { create(:user) } @@ -14,18 +14,10 @@ describe 'Dropdown milestone', js: true, feature: true do let(:filtered_search) { find('.filtered-search') } let(:js_dropdown_milestone) { '#js-dropdown-milestone' } - - def send_keys_to_filtered_search(input) - input.split("").each do |i| - filtered_search.send_keys(i) - sleep 3 - wait_for_ajax - sleep 3 - end - end + let(:filter_dropdown) { find("#{js_dropdown_milestone} .filter-dropdown") } def dropdown_milestone_size - page.all('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item').size + filter_dropdown.all('.filter-dropdown-item').size end def click_milestone(text) @@ -64,13 +56,14 @@ describe 'Dropdown milestone', js: true, feature: true do end it 'should hide loading indicator when loaded' do - send_keys_to_filtered_search('milestone:') + filtered_search.set('milestone:') - expect(page).not_to have_css('#js-dropdown-milestone .filter-dropdown-loading') + expect(find(js_dropdown_milestone)).to have_css('.filter-dropdown-loading') + expect(find(js_dropdown_milestone)).not_to have_css('.filter-dropdown-loading') end it 'should load all the milestones when opened' do - send_keys_to_filtered_search('milestone:') + filtered_search.set('milestone:') expect(dropdown_milestone_size).to be > 0 end @@ -78,41 +71,48 @@ describe 'Dropdown milestone', js: true, feature: true do describe 'filtering' do before do - filtered_search.set('milestone') + filtered_search.set('milestone:') + + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(uppercase_milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(two_words_milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(wont_fix_milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(special_milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(long_milestone.title) end it 'filters by name' do - send_keys_to_filtered_search(':v1') + filtered_search.send_keys('v1') expect(dropdown_milestone_size).to eq(1) end it 'filters by case insensitive name' do - send_keys_to_filtered_search(':V1') + filtered_search.send_keys('V1') expect(dropdown_milestone_size).to eq(1) end it 'filters by name with symbol' do - send_keys_to_filtered_search(':%v1') + filtered_search.send_keys('%v1') expect(dropdown_milestone_size).to eq(1) end it 'filters by case insensitive name with symbol' do - send_keys_to_filtered_search(':%V1') + filtered_search.send_keys('%V1') expect(dropdown_milestone_size).to eq(1) end it 'filters by special characters' do - send_keys_to_filtered_search(':(+') + filtered_search.send_keys('(+') expect(dropdown_milestone_size).to eq(1) end it 'filters by special characters with symbol' do - send_keys_to_filtered_search(':%(+') + filtered_search.send_keys('%(+') expect(dropdown_milestone_size).to eq(1) end @@ -121,70 +121,86 @@ describe 'Dropdown milestone', js: true, feature: true do describe 'selecting from dropdown' do before do filtered_search.set('milestone:') + + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(uppercase_milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(two_words_milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(wont_fix_milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(special_milestone.title) + expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(long_milestone.title) end it 'fills in the milestone name when the milestone has not been filled' 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 - send_keys_to_filtered_search('v') + filtered_search.send_keys('v') 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 @@ -222,16 +238,14 @@ describe 'Dropdown milestone', js: true, feature: true do describe 'caching requests' do it 'caches requests after the first load' do - filtered_search.set('milestone') - send_keys_to_filtered_search(':') + filtered_search.set('milestone:') initial_size = dropdown_milestone_size expect(initial_size).to be > 0 create(:milestone, project: project) find('.filtered-search-input-container .clear-search').click - filtered_search.set('milestone') - send_keys_to_filtered_search(':') + filtered_search.set('milestone:') expect(dropdown_milestone_size).to eq(initial_size) 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..96e87c82d2c --- /dev/null +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -0,0 +1,352 @@ +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 'editing a search term while editing another filter token' do + before do + input_filtered_search('author assignee:', submit: false) + first('.tokens-container .filtered-search-term').double_click + end + + it 'opens hint dropdown' do + expect(page).to have_css('#js-dropdown-hint', visible: true) + end + + it 'opens author dropdown' do + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'author').click + + expect(page).to have_css('#js-dropdown-author', 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 + + describe 'search using incomplete visual tokens' do + before do + input_filtered_search('author:@root assignee:none', extra_space: false) + end + + it 'tokenizes the search term to complete visual token' do + expect_tokens([ + { name: 'author', value: '@root' }, + { name: 'assignee', value: 'none' } + ]) + 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/markdown_spec.rb b/spec/features/markdown_spec.rb index 32159559c37..894df13a2dc 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -115,6 +115,14 @@ describe 'GitLab Markdown', feature: true do expect(doc).to have_selector('span:contains("span tag")') end + it 'permits details elements' do + expect(doc).to have_selector('details:contains("Hiding the details")') + end + + it 'permits summary elements' do + expect(doc).to have_selector('details summary:contains("collapsible")') + end + it 'permits style attribute in th elements' do aggregate_failures do expect(doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center' diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb new file mode 100644 index 00000000000..7df102067d6 --- /dev/null +++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb @@ -0,0 +1,136 @@ +require 'spec_helper' + +feature 'Diff note avatars', feature: true, js: true do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") } + let(:path) { "files/ruby/popen.rb" } + let(:position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + old_line: nil, + new_line: 9, + diff_refs: merge_request.diff_refs + ) + end + let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) } + + before do + project.team << [user, :master] + login_as user + end + + %w(inline parallel).each do |view| + context "#{view} view" do + before do + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: view) + + wait_for_ajax + end + + it 'shows note avatar' do + page.within find("[id='#{position.line_code(project.repository)}']") do + find('.diff-notes-collapse').click + + expect(page).to have_selector('img.js-diff-comment-avatar', count: 1) + end + end + + it 'shows comment on note avatar' do + page.within find("[id='#{position.line_code(project.repository)}']") do + find('.diff-notes-collapse').click + + expect(first('img.js-diff-comment-avatar')["title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}") + end + end + + it 'toggles comments when clicking avatar' do + page.within find("[id='#{position.line_code(project.repository)}']") do + find('.diff-notes-collapse').click + end + + expect(page).to have_selector('.notes_holder', visible: false) + + page.within find("[id='#{position.line_code(project.repository)}']") do + first('img.js-diff-comment-avatar').click + end + + expect(page).to have_selector('.notes_holder') + end + + it 'removes avatar when note is deleted' do + page.within find(".note-row-#{note.id}") do + find('.js-note-delete').click + end + + wait_for_ajax + + page.within find("[id='#{position.line_code(project.repository)}']") do + expect(page).not_to have_selector('img.js-diff-comment-avatar') + end + end + + it 'adds avatar when commenting' do + click_button 'Reply...' + + page.within '.js-discussion-note-form' do + find('.js-note-text').native.send_keys('Test') + + click_button 'Comment' + + wait_for_ajax + end + + page.within find("[id='#{position.line_code(project.repository)}']") do + find('.diff-notes-collapse').click + + expect(page).to have_selector('img.js-diff-comment-avatar', count: 2) + end + end + + it 'adds multiple comments' do + 3.times do + click_button 'Reply...' + + page.within '.js-discussion-note-form' do + find('.js-note-text').native.send_keys('Test') + + find('.js-comment-button').trigger 'click' + + wait_for_ajax + end + end + + page.within find("[id='#{position.line_code(project.repository)}']") do + find('.diff-notes-collapse').click + + expect(page).to have_selector('img.js-diff-comment-avatar', count: 3) + expect(find('.diff-comments-more-count')).to have_content '+1' + end + end + + context 'multiple comments' do + before do + create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) + create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) + create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) + + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: view) + + wait_for_ajax + end + + it 'shows extra comment count' do + page.within find("[id='#{position.line_code(project.repository)}']") do + find('.diff-notes-collapse').click + + expect(find('.diff-comments-more-count')).to have_content '+1' + end + end + end + end + end +end 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 c203e1f20c1..e2d16e0830a 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -13,7 +13,7 @@ feature 'Environment', :feature do feature 'environment details page' do given!(:environment) { create(:environment, project: project) } given!(:deployment) { } - given!(:manual) { } + given!(:action) { } before do visit_environment(environment) @@ -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,28 +52,28 @@ 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 context 'with manual action' do - given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } + given(:action) do + create(:ci_build, :manual, pipeline: pipeline, + name: 'deploy to production') + end scenario 'does show a play button' do - expect(page).to have_link(manual.name.humanize) + expect(page).to have_link(action.name.humanize) end scenario 'does allow to play manual action' do - expect(manual).to be_skipped - expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } - expect(page).to have_content(manual.name) - expect(manual.reload).to be_pending + expect(action).to be_manual + + expect { click_link(action.name.humanize) } + .not_to change { Ci::Pipeline.count } + + expect(page).to have_content(action.name) + expect(action.reload).to be_pending end context 'with external_url' do @@ -111,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 @@ -130,11 +121,15 @@ feature 'Environment', :feature do context 'when environment is available' do context 'with stop action' do - given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } - given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } + given(:action) do + create(:ci_build, :manual, pipeline: pipeline, + name: 'close_app') + end - scenario 'does show stop button' do - expect(page).to have_link('Stop') + given(:deployment) do + create(:deployment, environment: environment, + deployable: build, + on_stop: 'close_app') end scenario 'does allow to stop environment' do diff --git a/spec/features/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 78be7d36f47..25f31b423b8 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -12,7 +12,7 @@ feature 'Environments page', :feature, :js do given!(:environment) { } given!(:deployment) { } - given!(:manual) { } + given!(:action) { } before do visit_environments(project) @@ -90,7 +90,7 @@ feature 'Environments page', :feature, :js do given(:pipeline) { create(:ci_pipeline, project: project) } given(:build) { create(:ci_build, pipeline: pipeline) } - given(:manual) do + given(:action) do create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') end @@ -102,19 +102,19 @@ feature 'Environments page', :feature, :js do scenario 'does show a play button' do find('.js-dropdown-play-icon-container').click - expect(page).to have_content(manual.name.humanize) + expect(page).to have_content(action.name.humanize) end scenario 'does allow to play manual action', js: true do - expect(manual).to be_skipped + expect(action).to be_manual find('.js-dropdown-play-icon-container').click - expect(page).to have_content(manual.name.humanize) + expect(page).to have_content(action.name.humanize) - expect { click_link(manual.name.humanize) } + expect { click_link(action.name.humanize) } .not_to change { Ci::Pipeline.count } - expect(manual.reload).to be_pending + expect(action.reload).to be_pending end scenario 'does show build name and id' do @@ -144,8 +144,15 @@ feature 'Environments page', :feature, :js do end context 'with stop action' do - given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } - given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } + given(:action) do + create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') + end + + given(:deployment) do + create(:deployment, environment: environment, + deployable: build, + on_stop: 'close_app') + end scenario 'does show stop button' do expect(page).to have_selector('.stop-env-link') 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/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index f3e7c2d1a9f..0cdbc32431d 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -79,6 +79,11 @@ As permissive as it is, we've allowed even more stuff: <span>span tag</span> +<details> +<summary>Summary lines are collapsible:</summary> +Hiding the details until expanded. +</details> + <a href="#" rel="bookmark">This is a link with a defined rel attribute, which should be removed</a> <a href="javascript:alert('Hi')">This is a link trying to be sneaky. It gets its link removed entirely.</a> 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/behaviors/bind_in_out_spec.js b/spec/javascripts/behaviors/bind_in_out_spec.js new file mode 100644 index 00000000000..dd9ab33289f --- /dev/null +++ b/spec/javascripts/behaviors/bind_in_out_spec.js @@ -0,0 +1,189 @@ +import BindInOut from '~/behaviors/bind_in_out'; +import ClassSpecHelper from '../helpers/class_spec_helper'; + +describe('BindInOut', function () { + describe('.constructor', function () { + beforeEach(function () { + this.in = {}; + this.out = {}; + + this.bindInOut = new BindInOut(this.in, this.out); + }); + + it('should set .in', function () { + expect(this.bindInOut.in).toBe(this.in); + }); + + it('should set .out', function () { + expect(this.bindInOut.out).toBe(this.out); + }); + + it('should set .eventWrapper', function () { + expect(this.bindInOut.eventWrapper).toEqual({}); + }); + + describe('if .in is an input', function () { + beforeEach(function () { + this.bindInOut = new BindInOut({ tagName: 'INPUT' }); + }); + + it('should set .eventType to keyup ', function () { + expect(this.bindInOut.eventType).toEqual('keyup'); + }); + }); + + describe('if .in is a textarea', function () { + beforeEach(function () { + this.bindInOut = new BindInOut({ tagName: 'TEXTAREA' }); + }); + + it('should set .eventType to keyup ', function () { + expect(this.bindInOut.eventType).toEqual('keyup'); + }); + }); + + describe('if .in is not an input or textarea', function () { + beforeEach(function () { + this.bindInOut = new BindInOut({ tagName: 'SELECT' }); + }); + + it('should set .eventType to change ', function () { + expect(this.bindInOut.eventType).toEqual('change'); + }); + }); + }); + + describe('.addEvents', function () { + beforeEach(function () { + this.in = jasmine.createSpyObj('in', ['addEventListener']); + + this.bindInOut = new BindInOut(this.in); + + this.addEvents = this.bindInOut.addEvents(); + }); + + it('should set .eventWrapper.updateOut', function () { + expect(this.bindInOut.eventWrapper.updateOut).toEqual(jasmine.any(Function)); + }); + + it('should call .addEventListener', function () { + expect(this.in.addEventListener) + .toHaveBeenCalledWith( + this.bindInOut.eventType, + this.bindInOut.eventWrapper.updateOut, + ); + }); + + it('should return the instance', function () { + expect(this.addEvents).toBe(this.bindInOut); + }); + }); + + describe('.updateOut', function () { + beforeEach(function () { + this.in = { value: 'the-value' }; + this.out = { textContent: 'not-the-value' }; + + this.bindInOut = new BindInOut(this.in, this.out); + + this.updateOut = this.bindInOut.updateOut(); + }); + + it('should set .out.textContent to .in.value', function () { + expect(this.out.textContent).toBe(this.in.value); + }); + + it('should return the instance', function () { + expect(this.updateOut).toBe(this.bindInOut); + }); + }); + + describe('.removeEvents', function () { + beforeEach(function () { + this.in = jasmine.createSpyObj('in', ['removeEventListener']); + this.updateOut = () => {}; + + this.bindInOut = new BindInOut(this.in); + this.bindInOut.eventWrapper.updateOut = this.updateOut; + + this.removeEvents = this.bindInOut.removeEvents(); + }); + + it('should call .removeEventListener', function () { + expect(this.in.removeEventListener) + .toHaveBeenCalledWith( + this.bindInOut.eventType, + this.updateOut, + ); + }); + + it('should return the instance', function () { + expect(this.removeEvents).toBe(this.bindInOut); + }); + }); + + describe('.initAll', function () { + beforeEach(function () { + this.ins = [0, 1, 2]; + this.instances = []; + + spyOn(document, 'querySelectorAll').and.returnValue(this.ins); + spyOn(Array.prototype, 'map').and.callThrough(); + spyOn(BindInOut, 'init'); + + this.initAll = BindInOut.initAll(); + }); + + ClassSpecHelper.itShouldBeAStaticMethod(BindInOut, 'initAll'); + + it('should call .querySelectorAll', function () { + expect(document.querySelectorAll).toHaveBeenCalledWith('*[data-bind-in]'); + }); + + it('should call .map', function () { + expect(Array.prototype.map).toHaveBeenCalledWith(jasmine.any(Function)); + }); + + it('should call .init for each element', function () { + expect(BindInOut.init.calls.count()).toEqual(3); + }); + + it('should return an array of instances', function () { + expect(this.initAll).toEqual(jasmine.any(Array)); + }); + }); + + describe('.init', function () { + beforeEach(function () { + spyOn(BindInOut.prototype, 'addEvents').and.callFake(function () { return this; }); + spyOn(BindInOut.prototype, 'updateOut').and.callFake(function () { return this; }); + + this.init = BindInOut.init({}, {}); + }); + + ClassSpecHelper.itShouldBeAStaticMethod(BindInOut, 'init'); + + it('should call .addEvents', function () { + expect(BindInOut.prototype.addEvents).toHaveBeenCalled(); + }); + + it('should call .updateOut', function () { + expect(BindInOut.prototype.updateOut).toHaveBeenCalled(); + }); + + describe('if no anOut is provided', function () { + beforeEach(function () { + this.anIn = { dataset: { bindIn: 'the-data-bind-in' } }; + + spyOn(document, 'querySelector'); + + BindInOut.init(this.anIn); + }); + + it('should call .querySelector', function () { + expect(document.querySelector) + .toHaveBeenCalledWith(`*[data-bind-out="${this.anIn.dataset.bindIn}"]`); + }); + }); + }); +}); 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/diff_comments_store_spec.js b/spec/javascripts/diff_comments_store_spec.js index f956394ef53..84cf98c930a 100644 --- a/spec/javascripts/diff_comments_store_spec.js +++ b/spec/javascripts/diff_comments_store_spec.js @@ -7,7 +7,16 @@ require('~/diff_notes/stores/comments'); (() => { function createDiscussion(noteId = 1, resolved = true) { - CommentsStore.create('a', noteId, true, resolved, 'test'); + CommentsStore.create({ + discussionId: 'a', + noteId, + canResolve: true, + resolved, + resolvedBy: 'test', + authorName: 'test', + authorAvatar: 'test', + noteTruncated: 'test...', + }); } beforeEach(() => { 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/issue_spec.js b/spec/javascripts/issue_spec.js index e7530f61385..8d25500b9fd 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,10 +1,9 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */ -/* global Issue */ +import Issue from '~/issue'; require('~/lib/utils/text_utility'); -require('~/issue'); -(function() { +describe('Issue', function() { var INVALID_URL = 'http://goesnowhere.nothing/whereami'; var $boxClosed, $boxOpen, $btnClose, $btnReopen; @@ -59,28 +58,26 @@ require('~/issue'); expect($btnReopen).toHaveText('Reopen issue'); } - describe('Issue', function() { - describe('task lists', function() { - beforeEach(function() { - loadFixtures('issues/issue-with-task-list.html.raw'); - this.issue = new Issue(); - }); - - it('modifies the Markdown field', function() { - spyOn(jQuery, 'ajax').and.stub(); - $('input[type=checkbox]').attr('checked', true).trigger('change'); - expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); - }); + describe('task lists', function() { + beforeEach(function() { + loadFixtures('issues/issue-with-task-list.html.raw'); + this.issue = new Issue(); + }); - it('submits an ajax request on tasklist:changed', function() { - spyOn(jQuery, 'ajax').and.callFake(function(req) { - expect(req.type).toBe('PATCH'); - expect(req.url).toBe(gl.TEST_HOST + '/frontend-fixtures/issues-project/issues/1.json'); // eslint-disable-line prefer-template - expect(req.data.issue.description).not.toBe(null); - }); + it('modifies the Markdown field', function() { + spyOn(jQuery, 'ajax').and.stub(); + $('input[type=checkbox]').attr('checked', true).trigger('change'); + expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); + }); - $('.js-task-list-field').trigger('tasklist:changed'); + it('submits an ajax request on tasklist:changed', function() { + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PATCH'); + expect(req.url).toBe(gl.TEST_HOST + '/frontend-fixtures/issues-project/issues/1.json'); // eslint-disable-line prefer-template + expect(req.data.issue.description).not.toBe(null); }); + + $('.js-task-list-field').trigger('tasklist:changed'); }); }); @@ -165,4 +162,4 @@ require('~/issue'); expect($('.issue_counter')).toHaveText(1); }); }); -}).call(window); +}); 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/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index b38e3b17e64..b4cd5f63a15 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -86,6 +86,16 @@ describe Banzai::Filter::SanitizationFilter, lib: true do expect(filter(act).to_html).to eq exp end + it 'allows `summary` elements' do + exp = act = '<summary>summary line</summary>' + expect(filter(act).to_html).to eq exp + end + + it 'allows `details` elements' do + exp = act = '<details>long text goes here</details>' + expect(filter(act).to_html).to eq exp + end + it 'removes `rel` attribute from `a` elements' do act = %q{<a href="#" rel="nofollow">Link</a>} exp = %q{<a href="#">Link</a>} diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 7145f0da1d3..53abc056602 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -15,9 +15,9 @@ module Ci end describe '#build_attributes' do - describe 'coverage entry' do - subject { described_class.new(config, path).build_attributes(:rspec) } + subject { described_class.new(config, path).build_attributes(:rspec) } + describe 'coverage entry' do describe 'code coverage regexp' do let(:config) do YAML.dump(rspec: { script: 'rspec', @@ -30,6 +30,56 @@ module Ci end end end + + describe 'allow failure entry' do + context 'when job is a manual action' do + context 'when allow_failure is defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + when: 'manual', + allow_failure: false }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + end + + context 'when allow_failure is not defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + when: 'manual' }) + end + + it 'is allowed to fail' do + expect(subject[:allow_failure]).to be true + end + end + end + + context 'when job is not a manual action' do + context 'when allow_failure is defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + allow_failure: false }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + end + + context 'when allow_failure is not defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec' }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + end + end + end end describe "#builds_for_ref" do 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 ebd80ac5e1d..684d01e9056 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -155,6 +155,7 @@ describe Gitlab::Ci::Config::Entry::Global do stage: 'test', cache: { key: 'k', untracked: true, paths: ['public/'] }, variables: { VAR: 'value' }, + ignore: false, after_script: ['make clean'] }, spinach: { name: :spinach, before_script: [], @@ -165,6 +166,7 @@ describe Gitlab::Ci::Config::Entry::Global do stage: 'test', cache: { key: 'k', untracked: true, paths: ['public/'] }, variables: {}, + ignore: false, after_script: ['make clean'] }, ) end @@ -186,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/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index d20f4ec207d..9249bb9c172 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -144,6 +144,7 @@ describe Gitlab::Ci::Config::Entry::Job do script: %w[rspec], commands: "ls\npwd\nrspec", stage: 'test', + ignore: false, after_script: %w[cleanup]) end end @@ -159,4 +160,82 @@ describe Gitlab::Ci::Config::Entry::Job do end end end + + describe '#manual_action?' do + context 'when job is a manual action' do + let(:config) { { script: 'deploy', when: 'manual' } } + + it 'is a manual action' do + expect(entry).to be_manual_action + end + end + + context 'when job is not a manual action' do + let(:config) { { script: 'deploy' } } + + it 'is not a manual action' do + expect(entry).not_to be_manual_action + end + end + end + + describe '#ignored?' do + context 'when job is a manual action' do + context 'when it is not specified if job is allowed to fail' do + let(:config) do + { script: 'deploy', when: 'manual' } + end + + it 'is an ignored job' do + expect(entry).to be_ignored + end + end + + context 'when job is allowed to fail' do + let(:config) do + { script: 'deploy', when: 'manual', allow_failure: true } + end + + it 'is an ignored job' do + expect(entry).to be_ignored + end + end + + context 'when job is not allowed to fail' do + let(:config) do + { script: 'deploy', when: 'manual', allow_failure: false } + end + + it 'is not an ignored job' do + expect(entry).not_to be_ignored + end + end + end + + context 'when job is not a manual action' do + context 'when it is not specified if job is allowed to fail' do + let(:config) { { script: 'deploy' } } + + it 'is not an ignored job' do + expect(entry).not_to be_ignored + end + end + + context 'when job is allowed to fail' do + let(:config) { { script: 'deploy', allow_failure: true } } + + it 'is an ignored job' do + expect(entry).to be_ignored + end + end + + context 'when job is not allowed to fail' do + let(:config) { { script: 'deploy', allow_failure: false } } + + it 'is not an ignored job' do + expect(entry).not_to be_ignored + end + end + end + end end diff --git a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb index aaebf783962..7d104372ac6 100644 --- a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb @@ -62,10 +62,12 @@ describe Gitlab::Ci::Config::Entry::Jobs do rspec: { name: :rspec, script: %w[rspec], commands: 'rspec', + ignore: false, stage: 'test' }, spinach: { name: :spinach, script: %w[spinach], commands: 'spinach', + ignore: false, stage: 'test' }) 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/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index 0c40fca0c1a..8b3bd08cf13 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -192,7 +192,7 @@ describe Gitlab::Ci::Status::Build::Factory do let(:build) { create(:ci_build, :playable) } it 'matches correct core status' do - expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped + expect(factory.core_status).to be_a Gitlab::Ci::Status::Manual end it 'matches correct extended statuses' do @@ -200,12 +200,13 @@ describe Gitlab::Ci::Status::Build::Factory do .to eq [Gitlab::Ci::Status::Build::Play] end - it 'fabricates a core skipped status' do + it 'fabricates a play detailed status' do expect(status).to be_a Gitlab::Ci::Status::Build::Play end it 'fabricates status with correct details' do expect(status.text).to eq 'manual' + expect(status.group).to eq 'manual' expect(status.icon).to eq 'icon_status_manual' expect(status.label).to eq 'manual play action' expect(status).to have_details @@ -218,7 +219,7 @@ describe Gitlab::Ci::Status::Build::Factory do let(:build) { create(:ci_build, :playable, :teardown_environment) } it 'matches correct core status' do - expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped + expect(factory.core_status).to be_a Gitlab::Ci::Status::Manual end it 'matches correct extended statuses' do @@ -226,12 +227,13 @@ describe Gitlab::Ci::Status::Build::Factory do .to eq [Gitlab::Ci::Status::Build::Stop] end - it 'fabricates a core skipped status' do + it 'fabricates a stop detailed status' do expect(status).to be_a Gitlab::Ci::Status::Build::Stop end it 'fabricates status with correct details' do expect(status.text).to eq 'manual' + expect(status.group).to eq 'manual' expect(status.icon).to eq 'icon_status_manual' expect(status.label).to eq 'manual stop action' expect(status).to have_details diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index f3e72ea1796..6c97a4fe5ca 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -6,22 +6,10 @@ describe Gitlab::Ci::Status::Build::Play do subject { described_class.new(status) } - describe '#text' do - it { expect(subject.text).to eq 'manual' } - end - describe '#label' do it { expect(subject.label).to eq 'manual play action' } end - describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_manual' } - end - - describe '#group' do - it { expect(subject.group).to eq 'manual' } - end - describe 'action details' do let(:user) { create(:user) } let(:build) { create(:ci_build) } diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb index 41c2b624774..8d021c35a69 100644 --- a/spec/lib/gitlab/ci/status/build/stop_spec.rb +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -8,22 +8,10 @@ describe Gitlab::Ci::Status::Build::Stop do described_class.new(status) end - describe '#text' do - it { expect(subject.text).to eq 'manual' } - end - describe '#label' do it { expect(subject.label).to eq 'manual stop action' } end - describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_manual' } - end - - describe '#group' do - it { expect(subject.group).to eq 'manual' } - end - describe 'action details' do let(:user) { create(:user) } let(:build) { create(:ci_build) } diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb index 38412fe2e4f..768f8926f1d 100644 --- a/spec/lib/gitlab/ci/status/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Canceled do end describe '#text' do - it { expect(subject.label).to eq 'canceled' } + it { expect(subject.text).to eq 'canceled' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb index 6d847484693..e96c13aede3 100644 --- a/spec/lib/gitlab/ci/status/created_spec.rb +++ b/spec/lib/gitlab/ci/status/created_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Created do end describe '#text' do - it { expect(subject.label).to eq 'created' } + it { expect(subject.text).to eq 'created' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb index 990d686d22c..e5da0a91159 100644 --- a/spec/lib/gitlab/ci/status/failed_spec.rb +++ b/spec/lib/gitlab/ci/status/failed_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Failed do end describe '#text' do - it { expect(subject.label).to eq 'failed' } + it { expect(subject.text).to eq 'failed' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/manual_spec.rb b/spec/lib/gitlab/ci/status/manual_spec.rb new file mode 100644 index 00000000000..3fd3727b92d --- /dev/null +++ b/spec/lib/gitlab/ci/status/manual_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Manual do + subject do + described_class.new(double('subject'), double('user')) + end + + describe '#text' do + it { expect(subject.text).to eq 'manual' } + end + + describe '#label' do + it { expect(subject.label).to eq 'manual action' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'icon_status_manual' } + end + + describe '#group' do + it { expect(subject.group).to eq 'manual' } + end +end diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb index 7bb6579c317..8d09cf2a05a 100644 --- a/spec/lib/gitlab/ci/status/pending_spec.rb +++ b/spec/lib/gitlab/ci/status/pending_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Pending do end describe '#text' do - it { expect(subject.label).to eq 'pending' } + it { expect(subject.text).to eq 'pending' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb index 852d6c06baf..10d3bf749c1 100644 --- a/spec/lib/gitlab/ci/status/running_spec.rb +++ b/spec/lib/gitlab/ci/status/running_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Running do end describe '#text' do - it { expect(subject.label).to eq 'running' } + it { expect(subject.text).to eq 'running' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb index e00b356a24b..10db93d3802 100644 --- a/spec/lib/gitlab/ci/status/skipped_spec.rb +++ b/spec/lib/gitlab/ci/status/skipped_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Skipped do end describe '#text' do - it { expect(subject.label).to eq 'skipped' } + it { expect(subject.text).to eq 'skipped' } end describe '#label' do diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb index 4a89e1faf40..230f24b94a4 100644 --- a/spec/lib/gitlab/ci/status/success_spec.rb +++ b/spec/lib/gitlab/ci/status/success_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Success do end describe '#text' do - it { expect(subject.label).to eq 'passed' } + it { expect(subject.text).to eq 'passed' } end describe '#label' do 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/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb index 36e7d739f7e..3a31f93efa5 100644 --- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb @@ -6,27 +6,27 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do let(:repo) { double } let(:raw) do { - ref: 'feature', + ref: 'branch-merged', repo: repo, sha: commit.id } end describe '#exists?' do - it 'returns true when both branch, and commit exists' do + it 'returns true when branch exists and commit is part of the branch' do branch = described_class.new(project, double(raw)) expect(branch.exists?).to eq true end - it 'returns false when branch does not exist' do - branch = described_class.new(project, double(raw.merge(ref: 'removed-branch'))) + it 'returns false when branch exists and commit is not part of the branch' do + branch = described_class.new(project, double(raw.merge(ref: 'feature'))) expect(branch.exists?).to eq false end - it 'returns false when commit does not exist' do - branch = described_class.new(project, double(raw.merge(sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'))) + it 'returns false when branch does not exist' do + branch = described_class.new(project, double(raw.merge(ref: 'removed-branch'))) expect(branch.exists?).to eq false end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 33d83d6d2f1..3f080de99dd 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -130,7 +130,7 @@ describe Gitlab::GithubImport::Importer, lib: true do let!(:user) { create(:user, email: octocat.email) } let(:repository) { double(id: 1, fork: false) } let(:source_sha) { create(:commit, project: project).id } - let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) } + let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha) } let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } let(:pull_request) do diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index e46be18aa99..951cbea7857 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -7,10 +7,12 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } let(:repository) { double(id: 1, fork: false) } let(:source_repo) { repository } - let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: source_sha) } + let(:source_branch) { double(ref: 'branch-merged', repo: source_repo, sha: source_sha) } + let(:forked_source_repo) { double(id: 2, fork: true, name: 'otherproject', full_name: 'company/otherproject') } let(:target_repo) { repository } let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) } let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } + let(:forked_branch) { double(ref: 'master', repo: forked_source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -49,7 +51,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do title: 'New feature', description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, - source_branch: 'feature', + source_branch: 'branch-merged', source_branch_sha: source_sha, target_project: project, target_branch: 'master', @@ -75,7 +77,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do title: 'New feature', description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, - source_branch: 'feature', + source_branch: 'branch-merged', source_branch_sha: source_sha, target_project: project, target_branch: 'master', @@ -102,7 +104,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do title: 'New feature', description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, - source_branch: 'feature', + source_branch: 'branch-merged', source_branch_sha: source_sha, target_project: project, target_branch: 'master', @@ -194,7 +196,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:raw_data) { double(base_data) } it 'returns branch ref' do - expect(pull_request.source_branch_name).to eq 'feature' + expect(pull_request.source_branch_name).to eq 'branch-merged' end end @@ -205,10 +207,18 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do expect(pull_request.source_branch_name).to eq 'pull/1347/removed-branch' end end + + context 'when source branch is from a fork' do + let(:raw_data) { double(base_data.merge(head: forked_branch)) } + + it 'prefixes branch name with pull request number and project with namespace to avoid collision' do + expect(pull_request.source_branch_name).to eq 'pull/1347/company/otherproject/master' + end + end end shared_examples 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' do - context 'when source branch exists' do + context 'when target branch exists' do let(:raw_data) { double(base_data) } it 'returns branch ref' do @@ -271,6 +281,24 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end + describe '#cross_project?' do + context 'when source and target repositories are different' do + let(:raw_data) { double(base_data.merge(head: forked_branch)) } + + it 'returns true' do + expect(pull_request.cross_project?).to eq true + end + end + + context 'when source and target repositories are the same' do + let(:raw_data) { double(base_data.merge(head: source_branch)) } + + it 'returns false' do + expect(pull_request.cross_project?).to eq false + end + end + end + describe '#url' do let(:raw_data) { double(base_data) } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index f20b6be51e1..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 @@ -199,6 +200,7 @@ project: - project_authorizations - route - statistics +- uploads award_emoji: - awardable - user 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/appearance_spec.rb b/spec/models/appearance_spec.rb index 0b72a2f979b..1060bf3cbf4 100644 --- a/spec/models/appearance_spec.rb +++ b/spec/models/appearance_spec.rb @@ -7,4 +7,6 @@ RSpec.describe Appearance, type: :model do it { is_expected.to validate_presence_of(:title) } it { is_expected.to validate_presence_of(:description) } + + it { is_expected.to have_many(:uploads).dependent(:destroy) } end diff --git a/spec/models/chat_team_spec.rb b/spec/models/chat_team_spec.rb new file mode 100644 index 00000000000..5283561a83f --- /dev/null +++ b/spec/models/chat_team_spec.rb @@ -0,0 +1,15 @@ +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) } +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 5743c555cbe..fd6ea2d6722 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -20,6 +20,30 @@ describe Ci::Build, :models do it { is_expected.to validate_presence_of :ref } it { is_expected.to respond_to :trace_html } + describe '#actionize' do + context 'when build is a created' do + before do + build.update_column(:status, :created) + end + + it 'makes build a manual action' do + expect(build.actionize).to be true + expect(build.reload).to be_manual + end + end + + context 'when build is not created' do + before do + build.update_column(:status, :pending) + end + + it 'does not change build status' do + expect(build.actionize).to be false + expect(build.reload).to be_pending + end + end + end + describe '#any_runners_online?' do subject { build.any_runners_online? } @@ -321,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') } @@ -587,13 +611,21 @@ describe Ci::Build, :models do it { is_expected.to be_falsey } end - context 'and build.status is failed' do + context 'and build status is failed' do before do build.status = 'failed' end it { is_expected.to be_truthy } end + + context 'when build is a manual action' do + before do + build.status = 'manual' + end + + it { is_expected.to be_falsey } + end end end @@ -682,12 +714,12 @@ describe Ci::Build, :models do end end - describe '#manual?' do + describe '#action?' do before do build.update(when: value) end - subject { build.manual? } + subject { build.action? } context 'when is set to manual' do let(:value) { 'manual' } @@ -703,14 +735,50 @@ describe Ci::Build, :models do end end + describe '#has_commands?' do + context 'when build has commands' do + let(:build) do + create(:ci_build, commands: 'rspec') + end + + it 'has commands' do + expect(build).to have_commands + end + end + + context 'when does not have commands' do + context 'when commands are an empty string' do + let(:build) do + create(:ci_build, commands: '') + end + + it 'has no commands' do + expect(build).not_to have_commands + end + end + + context 'when commands are not set at all' do + let(:build) do + create(:ci_build, commands: nil) + end + + it 'has no commands' do + expect(build).not_to have_commands + end + end + end + end + describe '#has_tags?' do context 'when build has tags' do subject { create(:ci_build, tag_list: ['tag']) } + it { is_expected.to have_tags } end context 'when build does not have tags' do subject { create(:ci_build, tag_list: []) } + it { is_expected.not_to have_tags } end end @@ -847,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 @@ -1218,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 @@ -1249,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 @@ -1287,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) } @@ -1295,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 @@ -1324,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 @@ -1348,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 @@ -1360,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 @@ -1378,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/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index c2fc8c02bb3..dd5f7098d06 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -24,6 +24,14 @@ describe Ci::Pipeline, models: true do it { is_expected.to respond_to :git_author_email } it { is_expected.to respond_to :short_sha } + describe '#block' do + it 'changes pipeline status to manual' do + expect(pipeline.block).to be true + expect(pipeline.reload).to be_manual + expect(pipeline.reload).to be_blocked + end + end + describe '#valid_commit_sha' do context 'commit.sha can not start with 00000000' do before do @@ -635,6 +643,14 @@ describe Ci::Pipeline, models: true do end end + context 'when pipeline is blocked' do + let(:pipeline) { create(:ci_pipeline, status: :manual) } + + it 'returns detailed status for blocked pipeline' do + expect(subject.text).to eq 'manual' + end + end + context 'when pipeline is successful but with warnings' do let(:pipeline) { create(:ci_pipeline, status: :success) } 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/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 36533bdd11e..ea5e4e21039 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -158,7 +158,7 @@ describe CommitStatus, :models do end end - describe '.exclude_ignored' do + describe '.after_stage' do subject { described_class.after_stage(0) } let(:statuses) do @@ -185,11 +185,32 @@ describe CommitStatus, :models do create_status(allow_failure: true, status: 'success'), create_status(allow_failure: true, status: 'failed'), create_status(allow_failure: false, status: 'success'), - create_status(allow_failure: false, status: 'failed')] + create_status(allow_failure: false, status: 'failed'), + create_status(allow_failure: true, status: 'manual'), + create_status(allow_failure: false, status: 'manual')] + end + + it 'returns statuses without what we want to ignore' do + is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9, 11)) + end + end + + describe '.failed_but_allowed' do + subject { described_class.failed_but_allowed.order(:id) } + + let(:statuses) do + [create_status(allow_failure: true, status: 'success'), + create_status(allow_failure: true, status: 'failed'), + create_status(allow_failure: false, status: 'success'), + create_status(allow_failure: false, status: 'failed'), + create_status(allow_failure: true, status: 'canceled'), + create_status(allow_failure: false, status: 'canceled'), + create_status(allow_failure: true, status: 'manual'), + create_status(allow_failure: false, status: 'manual')] end it 'returns statuses without what we want to ignore' do - is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9)) + is_expected.to eq(statuses.values_at(1, 4)) end end diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb index dbfe3cd2d36..f134da441c2 100644 --- a/spec/models/concerns/has_status_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -109,6 +109,24 @@ describe HasStatus do it { is_expected.to eq 'running' } end + + context 'when one status is a blocking manual action' do + let!(:statuses) do + [create(type, status: :failed), + create(type, status: :manual, allow_failure: false)] + end + + it { is_expected.to eq 'manual' } + end + + context 'when one status is a non-blocking manual action' do + let!(:statuses) do + [create(type, status: :failed), + create(type, status: :manual, allow_failure: true)] + end + + it { is_expected.to eq 'failed' } + end end context 'ci build statuses' do @@ -218,6 +236,18 @@ describe HasStatus do it_behaves_like 'not containing the job', status end end + + describe '.manual' do + subject { CommitStatus.manual } + + %i[manual].each do |status| + it_behaves_like 'containing the job', status + end + + %i[failed success skipped canceled].each do |status| + it_behaves_like 'not containing the job', status + end + end end describe '::DEFAULT_STATUS' do @@ -225,4 +255,10 @@ describe HasStatus do expect(described_class::DEFAULT_STATUS).to eq 'created' end end + + describe '::BLOCKED_STATUS' do + it 'is a status manual' do + expect(described_class::BLOCKED_STATUS).to eq 'manual' + 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/group_spec.rb b/spec/models/group_spec.rb index a4e6eb4e3a6..5d87938235a 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -13,6 +13,8 @@ describe Group, models: true do it { is_expected.to have_many(:shared_projects).through(:project_group_links) } it { is_expected.to have_many(:notification_settings).dependent(:destroy) } it { is_expected.to have_many(:labels).class_name('GroupLabel') } + it { is_expected.to have_many(:uploads).dependent(:destroy) } + it { is_expected.to have_one(:chat_team) } describe '#members & #requesters' do let(:requester) { create(:user) } 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 ee4f4092062..e120e21af06 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -71,6 +71,7 @@ describe Project, models: true do it { is_expected.to have_many(:project_group_links).dependent(:destroy) } it { is_expected.to have_many(:notification_settings).dependent(:destroy) } it { is_expected.to have_many(:forks).through(:forked_project_links) } + it { is_expected.to have_many(:uploads).dependent(:destroy) } context 'after initialized' do it "has a project_feature" do @@ -178,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 @@ -380,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 @@ -946,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/models/upload_spec.rb b/spec/models/upload_spec.rb new file mode 100644 index 00000000000..4c832c87d6a --- /dev/null +++ b/spec/models/upload_spec.rb @@ -0,0 +1,151 @@ +require 'rails_helper' + +describe Upload, type: :model do + describe 'assocations' do + it { is_expected.to belong_to(:model) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:size) } + it { is_expected.to validate_presence_of(:path) } + it { is_expected.to validate_presence_of(:model) } + it { is_expected.to validate_presence_of(:uploader) } + end + + describe 'callbacks' do + context 'for a file above the checksum threshold' do + it 'schedules checksum calculation' do + stub_const('UploadChecksumWorker', spy) + + upload = described_class.create( + path: __FILE__, + size: described_class::CHECKSUM_THRESHOLD + 1.kilobyte, + model: build_stubbed(:user), + uploader: double('ExampleUploader') + ) + + expect(UploadChecksumWorker) + .to have_received(:perform_async).with(upload.id) + end + end + + context 'for a file at or below the checksum threshold' do + it 'calculates checksum immediately before save' do + upload = described_class.new( + path: __FILE__, + size: described_class::CHECKSUM_THRESHOLD, + model: build_stubbed(:user), + uploader: double('ExampleUploader') + ) + + expect { upload.save } + .to change { upload.checksum }.from(nil) + .to(a_string_matching(/\A\h{64}\z/)) + end + end + end + + describe '.remove_path' do + it 'removes all records at the given path' do + described_class.create!( + size: File.size(__FILE__), + path: __FILE__, + model: build_stubbed(:user), + uploader: 'AvatarUploader' + ) + + expect { described_class.remove_path(__FILE__) }. + to change { described_class.count }.from(1).to(0) + end + end + + describe '.record' do + let(:fake_uploader) do + double( + file: double(size: 12_345), + relative_path: 'foo/bar.jpg', + model: build_stubbed(:user), + class: 'AvatarUploader' + ) + end + + it 'removes existing paths before creation' do + expect(described_class).to receive(:remove_path) + .with(fake_uploader.relative_path) + + described_class.record(fake_uploader) + end + + it 'creates a new record and assigns size, path, model, and uploader' do + upload = described_class.record(fake_uploader) + + aggregate_failures do + expect(upload).to be_persisted + expect(upload.size).to eq fake_uploader.file.size + expect(upload.path).to eq fake_uploader.relative_path + expect(upload.model_id).to eq fake_uploader.model.id + expect(upload.model_type).to eq fake_uploader.model.class.to_s + expect(upload.uploader).to eq fake_uploader.class + end + end + end + + describe '#absolute_path' do + it 'returns the path directly when already absolute' do + path = '/path/to/namespace/project/secret/file.jpg' + upload = described_class.new(path: path) + + expect(upload).not_to receive(:uploader_class) + + expect(upload.absolute_path).to eq path + end + + it "delegates to the uploader's absolute_path method" do + uploader = spy('FakeUploader') + upload = described_class.new(path: 'secret/file.jpg') + expect(upload).to receive(:uploader_class).and_return(uploader) + + upload.absolute_path + + expect(uploader).to have_received(:absolute_path).with(upload) + end + end + + describe '#calculate_checksum' do + it 'calculates the SHA256 sum' do + upload = described_class.new( + path: __FILE__, + size: described_class::CHECKSUM_THRESHOLD - 1.megabyte + ) + expected = Digest::SHA256.file(__FILE__).hexdigest + + expect { upload.calculate_checksum } + .to change { upload.checksum }.from(nil).to(expected) + end + + it 'returns nil for a non-existant file' do + upload = described_class.new( + path: __FILE__, + size: described_class::CHECKSUM_THRESHOLD - 1.megabyte + ) + + expect(upload).to receive(:exist?).and_return(false) + + expect(upload.calculate_checksum).to be_nil + end + end + + describe '#exist?' do + it 'returns true when the file exists' do + upload = described_class.new(path: __FILE__) + + expect(upload).to exist + end + + it 'returns false when the file does not exist' do + upload = described_class.new(path: "#{__FILE__}-nope") + + expect(upload).not_to exist + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index b99cde64675..adb5b538922 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -36,6 +36,7 @@ describe User, models: true do it { is_expected.to have_many(:builds).dependent(:nullify) } it { is_expected.to have_many(:pipelines).dependent(:nullify) } it { is_expected.to have_many(:chat_names).dependent(:destroy) } + it { is_expected.to have_many(:uploads).dependent(:destroy) } describe '#group_members' do it 'does not include group memberships for which user is a requester' 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/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index de68fb64726..d93616c4f50 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::ProcessPipelineService, :services do +describe Ci::ProcessPipelineService, '#execute', :services do let(:user) { create(:user) } let(:project) { create(:empty_project) } @@ -12,379 +12,518 @@ describe Ci::ProcessPipelineService, :services do project.add_developer(user) end - describe '#execute' do - context 'start queuing next builds' do - before do - create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage_idx: 0) - create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage_idx: 0) - create(:ci_build, :created, pipeline: pipeline, name: 'rspec', stage_idx: 1) - create(:ci_build, :created, pipeline: pipeline, name: 'rubocop', stage_idx: 1) - create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 2) - end + context 'when simple pipeline is defined' do + before do + create_build('linux', stage_idx: 0) + create_build('mac', stage_idx: 0) + create_build('rspec', stage_idx: 1) + create_build('rubocop', stage_idx: 1) + create_build('deploy', stage_idx: 2) + end - it 'processes a pipeline' do - expect(process_pipeline).to be_truthy - succeed_pending - expect(builds.success.count).to eq(2) + it 'processes a pipeline' do + expect(process_pipeline).to be_truthy - expect(process_pipeline).to be_truthy - succeed_pending - expect(builds.success.count).to eq(4) + succeed_pending + + expect(builds.success.count).to eq(2) + expect(process_pipeline).to be_truthy + + succeed_pending + + expect(builds.success.count).to eq(4) + expect(process_pipeline).to be_truthy + + succeed_pending + + expect(builds.success.count).to eq(5) + expect(process_pipeline).to be_falsey + end + + it 'does not process pipeline if existing stage is running' do + expect(process_pipeline).to be_truthy + expect(builds.pending.count).to eq(2) + + expect(process_pipeline).to be_falsey + expect(builds.pending.count).to eq(2) + end + end + + context 'custom stage with first job allowed to fail' do + before do + create_build('clean_job', stage_idx: 0, allow_failure: true) + create_build('test_job', stage_idx: 1, allow_failure: true) + end + it 'automatically triggers a next stage when build finishes' do + expect(process_pipeline).to be_truthy + expect(builds_statuses).to eq ['pending'] + + fail_running_or_pending + + expect(builds_statuses).to eq %w(failed pending) + end + end + + context 'when optional manual actions are defined' do + before do + create_build('build', stage_idx: 0) + create_build('test', stage_idx: 1) + create_build('test_failure', stage_idx: 2, when: 'on_failure') + create_build('deploy', stage_idx: 3) + create_build('production', stage_idx: 3, when: 'manual', allow_failure: true) + create_build('cleanup', stage_idx: 4, when: 'always') + create_build('clear:cache', stage_idx: 4, when: 'manual', allow_failure: true) + end + + context 'when builds are successful' do + it 'properly processes the pipeline' do expect(process_pipeline).to be_truthy - succeed_pending - expect(builds.success.count).to eq(5) + expect(builds_names).to eq ['build'] + expect(builds_statuses).to eq ['pending'] + + succeed_running_or_pending + + expect(builds_names).to eq %w(build test) + expect(builds_statuses).to eq %w(success pending) - expect(process_pipeline).to be_falsey + succeed_running_or_pending + + expect(builds_names).to eq %w(build test deploy production) + expect(builds_statuses).to eq %w(success success pending manual) + + succeed_running_or_pending + + expect(builds_names).to eq %w(build test deploy production cleanup clear:cache) + expect(builds_statuses).to eq %w(success success success manual pending manual) + + succeed_running_or_pending + + expect(builds_statuses).to eq %w(success success success manual success manual) + expect(pipeline.reload.status).to eq 'success' end + end - it 'does not process pipeline if existing stage is running' do + context 'when test job fails' do + it 'properly processes the pipeline' do expect(process_pipeline).to be_truthy - expect(builds.pending.count).to eq(2) + expect(builds_names).to eq ['build'] + expect(builds_statuses).to eq ['pending'] + + succeed_running_or_pending + + expect(builds_names).to eq %w(build test) + expect(builds_statuses).to eq %w(success pending) - expect(process_pipeline).to be_falsey - expect(builds.pending.count).to eq(2) + fail_running_or_pending + + expect(builds_names).to eq %w(build test test_failure) + expect(builds_statuses).to eq %w(success failed pending) + + succeed_running_or_pending + + expect(builds_names).to eq %w(build test test_failure cleanup) + expect(builds_statuses).to eq %w(success failed success pending) + + succeed_running_or_pending + + expect(builds_statuses).to eq %w(success failed success success) + expect(pipeline.reload.status).to eq 'failed' end end - context 'custom stage with first job allowed to fail' do - before do - create(:ci_build, :created, pipeline: pipeline, name: 'clean_job', stage_idx: 0, allow_failure: true) - create(:ci_build, :created, pipeline: pipeline, name: 'test_job', stage_idx: 1, allow_failure: true) + context 'when test and test_failure jobs fail' do + it 'properly processes the pipeline' do + expect(process_pipeline).to be_truthy + expect(builds_names).to eq ['build'] + expect(builds_statuses).to eq ['pending'] + + succeed_running_or_pending + + expect(builds_names).to eq %w(build test) + expect(builds_statuses).to eq %w(success pending) + + fail_running_or_pending + + expect(builds_names).to eq %w(build test test_failure) + expect(builds_statuses).to eq %w(success failed pending) + + fail_running_or_pending + + expect(builds_names).to eq %w(build test test_failure cleanup) + expect(builds_statuses).to eq %w(success failed failed pending) + + succeed_running_or_pending + + expect(builds_names).to eq %w(build test test_failure cleanup) + expect(builds_statuses).to eq %w(success failed failed success) + expect(pipeline.reload.status).to eq('failed') end + end - it 'automatically triggers a next stage when build finishes' do + context 'when deploy job fails' do + it 'properly processes the pipeline' do expect(process_pipeline).to be_truthy - expect(builds.pluck(:status)).to contain_exactly('pending') + expect(builds_names).to eq ['build'] + expect(builds_statuses).to eq ['pending'] + + succeed_running_or_pending - pipeline.builds.running_or_pending.each(&:drop) - expect(builds.pluck(:status)).to contain_exactly('failed', 'pending') + expect(builds_names).to eq %w(build test) + expect(builds_statuses).to eq %w(success pending) + + succeed_running_or_pending + + expect(builds_names).to eq %w(build test deploy production) + expect(builds_statuses).to eq %w(success success pending manual) + + fail_running_or_pending + + expect(builds_names).to eq %w(build test deploy production cleanup) + expect(builds_statuses).to eq %w(success success failed manual pending) + + succeed_running_or_pending + + expect(builds_statuses).to eq %w(success success failed manual success) + expect(pipeline.reload).to be_failed end end - context 'properly creates builds when "when" is defined' do - before do - create(:ci_build, :created, pipeline: pipeline, name: 'build', stage_idx: 0) - create(:ci_build, :created, pipeline: pipeline, name: 'test', stage_idx: 1) - create(:ci_build, :created, pipeline: pipeline, name: 'test_failure', stage_idx: 2, when: 'on_failure') - create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 3) - create(:ci_build, :created, pipeline: pipeline, name: 'production', stage_idx: 3, when: 'manual') - create(:ci_build, :created, pipeline: pipeline, name: 'cleanup', stage_idx: 4, when: 'always') - create(:ci_build, :created, pipeline: pipeline, name: 'clear cache', stage_idx: 4, when: 'manual') - end + context 'when build is canceled in the second stage' do + it 'does not schedule builds after build has been canceled' do + expect(process_pipeline).to be_truthy + expect(builds_names).to eq ['build'] + expect(builds_statuses).to eq ['pending'] - context 'when builds are successful' do - it 'properly creates builds' do - expect(process_pipeline).to be_truthy - expect(builds.pluck(:name)).to contain_exactly('build') - expect(builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test') - expect(builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') - expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') - expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success') - pipeline.reload - expect(pipeline.status).to eq('success') - end - end + succeed_running_or_pending - context 'when test job fails' do - it 'properly creates builds' do - expect(process_pipeline).to be_truthy - expect(builds.pluck(:name)).to contain_exactly('build') - expect(builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test') - expect(builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:drop) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') - expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success') - pipeline.reload - expect(pipeline.status).to eq('failed') - end - end + expect(builds.running_or_pending).not_to be_empty + expect(builds_names).to eq %w(build test) + expect(builds_statuses).to eq %w(success pending) - context 'when test and test_failure jobs fail' do - it 'properly creates builds' do - expect(process_pipeline).to be_truthy - expect(builds.pluck(:name)).to contain_exactly('build') - expect(builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test') - expect(builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:drop) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') - expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') - pipeline.builds.running_or_pending.each(&:drop) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success') - pipeline.reload - expect(pipeline.status).to eq('failed') - end - end + cancel_running_or_pending - context 'when deploy job fails' do - it 'properly creates builds' do - expect(process_pipeline).to be_truthy - expect(builds.pluck(:name)).to contain_exactly('build') - expect(builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test') - expect(builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') - expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') - pipeline.builds.running_or_pending.each(&:drop) - - expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') - expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success') - pipeline.reload - expect(pipeline.status).to eq('failed') - end + expect(builds.running_or_pending).to be_empty + expect(builds_names).to eq %w[build test] + expect(builds_statuses).to eq %w[success canceled] + expect(pipeline.reload).to be_canceled end + end - context 'when build is canceled in the second stage' do - it 'does not schedule builds after build has been canceled' do - expect(process_pipeline).to be_truthy - expect(builds.pluck(:name)).to contain_exactly('build') - expect(builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) + context 'when listing optional manual actions' do + it 'returns only for skipped builds' do + # currently all builds are created + expect(process_pipeline).to be_truthy + expect(manual_actions).to be_empty - expect(builds.running_or_pending).not_to be_empty + # succeed stage build + succeed_running_or_pending - expect(builds.pluck(:name)).to contain_exactly('build', 'test') - expect(builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:cancel) + expect(manual_actions).to be_empty - expect(builds.running_or_pending).to be_empty - expect(pipeline.reload.status).to eq('canceled') - end - end + # succeed stage test + succeed_running_or_pending + + expect(manual_actions).to be_one # production + + # succeed stage deploy + succeed_running_or_pending - context 'when listing manual actions' do - it 'returns only for skipped builds' do - # currently all builds are created - expect(process_pipeline).to be_truthy - expect(manual_actions).to be_empty + expect(manual_actions).to be_many # production and clear cache + end + end + end - # succeed stage build - pipeline.builds.running_or_pending.each(&:success) - expect(manual_actions).to be_empty + context 'when there are manual action in earlier stages' do + context 'when first stage has only optional manual actions' do + before do + create_build('build', stage_idx: 0, when: 'manual', allow_failure: true) + create_build('check', stage_idx: 1) + create_build('test', stage_idx: 2) - # succeed stage test - pipeline.builds.running_or_pending.each(&:success) - expect(manual_actions).to be_one # production + process_pipeline + end - # succeed stage deploy - pipeline.builds.running_or_pending.each(&:success) - expect(manual_actions).to be_many # production and clear cache - end + it 'starts from the second stage' do + expect(all_builds_statuses).to eq %w[manual pending created] end end - context 'when there are manual/on_failure jobs in earlier stages' do + context 'when second stage has only optional manual actions' do before do - builds + create_build('check', stage_idx: 0) + create_build('build', stage_idx: 1, when: 'manual', allow_failure: true) + create_build('test', stage_idx: 2) + process_pipeline - builds.each(&:reload) end - context 'when first stage has only manual jobs' do - let(:builds) do - [create_build('build', 0, 'manual'), - create_build('check', 1), - create_build('test', 2)] - end + it 'skips second stage and continues on third stage' do + expect(all_builds_statuses).to eq(%w[pending created created]) - it 'starts from the second stage' do - expect(builds.map(&:status)).to eq(%w[skipped pending created]) - end + builds.first.success + + expect(all_builds_statuses).to eq(%w[success manual pending]) end + end + end + + context 'when blocking manual actions are defined' do + before do + create_build('code:test', stage_idx: 0) + create_build('staging:deploy', stage_idx: 1, when: 'manual') + create_build('staging:test', stage_idx: 2, when: 'on_success') + create_build('production:deploy', stage_idx: 3, when: 'manual') + create_build('production:test', stage_idx: 4, when: 'always') + end - context 'when second stage has only manual jobs' do - let(:builds) do - [create_build('check', 0), - create_build('build', 1, 'manual'), - create_build('test', 2)] - end + context 'when first stage succeeds' do + it 'blocks pipeline on stage with first manual action' do + process_pipeline - it 'skips second stage and continues on third stage' do - expect(builds.map(&:status)).to eq(%w[pending created created]) + expect(builds_names).to eq %w[code:test] + expect(builds_statuses).to eq %w[pending] + expect(pipeline.reload.status).to eq 'pending' - builds.first.success - builds.each(&:reload) + succeed_running_or_pending - expect(builds.map(&:status)).to eq(%w[success skipped pending]) - end + expect(builds_names).to eq %w[code:test staging:deploy] + expect(builds_statuses).to eq %w[success manual] + expect(pipeline.reload).to be_manual end + end + + context 'when first stage fails' do + it 'does not take blocking action into account' do + process_pipeline + + expect(builds_names).to eq %w[code:test] + expect(builds_statuses).to eq %w[pending] + expect(pipeline.reload.status).to eq 'pending' - context 'when second stage has only on_failure jobs' do - let(:builds) do - [create_build('check', 0), - create_build('build', 1, 'on_failure'), - create_build('test', 2)] - end + fail_running_or_pending - it 'skips second stage and continues on third stage' do - expect(builds.map(&:status)).to eq(%w[pending created created]) + expect(builds_names).to eq %w[code:test production:test] + expect(builds_statuses).to eq %w[failed pending] - builds.first.success - builds.each(&:reload) + succeed_running_or_pending - expect(builds.map(&:status)).to eq(%w[success skipped pending]) - end + expect(builds_statuses).to eq %w[failed success] + expect(pipeline.reload).to be_failed end end - context 'when failed build in the middle stage is retried' do - context 'when failed build is the only unsuccessful build in the stage' do - before do - create(:ci_build, :created, pipeline: pipeline, name: 'build:1', stage_idx: 0) - create(:ci_build, :created, pipeline: pipeline, name: 'build:2', stage_idx: 0) - create(:ci_build, :created, pipeline: pipeline, name: 'test:1', stage_idx: 1) - create(:ci_build, :created, pipeline: pipeline, name: 'test:2', stage_idx: 1) - create(:ci_build, :created, pipeline: pipeline, name: 'deploy:1', stage_idx: 2) - create(:ci_build, :created, pipeline: pipeline, name: 'deploy:2', stage_idx: 2) - end + context 'when pipeline is promoted sequentially up to the end' do + it 'properly processes entire pipeline' do + process_pipeline + + expect(builds_names).to eq %w[code:test] + expect(builds_statuses).to eq %w[pending] + + succeed_running_or_pending + + expect(builds_names).to eq %w[code:test staging:deploy] + expect(builds_statuses).to eq %w[success manual] + expect(pipeline.reload).to be_manual + + play_manual_action('staging:deploy') + + expect(builds_statuses).to eq %w[success pending] + + succeed_running_or_pending + + expect(builds_names).to eq %w[code:test staging:deploy staging:test] + expect(builds_statuses).to eq %w[success success pending] + + succeed_running_or_pending - it 'does trigger builds in the next stage' do - expect(process_pipeline).to be_truthy - expect(builds.pluck(:name)).to contain_exactly('build:1', 'build:2') + expect(builds_names).to eq %w[code:test staging:deploy staging:test + production:deploy] + expect(builds_statuses).to eq %w[success success success manual] - pipeline.builds.running_or_pending.each(&:success) + expect(pipeline.reload).to be_manual + expect(pipeline.reload).to be_blocked + expect(pipeline.reload).not_to be_active + expect(pipeline.reload).not_to be_complete - expect(builds.pluck(:name)) - .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2') + play_manual_action('production:deploy') - pipeline.builds.find_by(name: 'test:1').success - pipeline.builds.find_by(name: 'test:2').drop + expect(builds_statuses).to eq %w[success success success pending] + expect(pipeline.reload).to be_running - expect(builds.pluck(:name)) - .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2') + succeed_running_or_pending - Ci::Build.retry(pipeline.builds.find_by(name: 'test:2'), user).success + expect(builds_names).to eq %w[code:test staging:deploy staging:test + production:deploy production:test] + expect(builds_statuses).to eq %w[success success success success pending] + expect(pipeline.reload).to be_running - expect(builds.pluck(:name)).to contain_exactly( - 'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2') - end + succeed_running_or_pending + + expect(builds_names).to eq %w[code:test staging:deploy staging:test + production:deploy production:test] + expect(builds_statuses).to eq %w[success success success success success] + expect(pipeline.reload).to be_success end end + end - context 'when there are builds that are not created yet' do - let(:pipeline) do - create(:ci_pipeline, config: config) - end + context 'when second stage has only on_failure jobs' do + before do + create_build('check', stage_idx: 0) + create_build('build', stage_idx: 1, when: 'on_failure') + create_build('test', stage_idx: 2) - let(:config) do - { rspec: { stage: 'test', script: 'rspec' }, - deploy: { stage: 'deploy', script: 'rsync' } } - end + process_pipeline + end + + it 'skips second stage and continues on third stage' do + expect(all_builds_statuses).to eq(%w[pending created created]) + + builds.first.success + + expect(all_builds_statuses).to eq(%w[success skipped pending]) + end + end + context 'when failed build in the middle stage is retried' do + context 'when failed build is the only unsuccessful build in the stage' do before do - create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage: 'build', stage_idx: 0) - create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage: 'build', stage_idx: 0) + create_build('build:1', stage_idx: 0) + create_build('build:2', stage_idx: 0) + create_build('test:1', stage_idx: 1) + create_build('test:2', stage_idx: 1) + create_build('deploy:1', stage_idx: 2) + create_build('deploy:2', stage_idx: 2) end - it 'processes the pipeline' do - # Currently we have five builds with state created - # - expect(builds.count).to eq(0) - expect(all_builds.count).to eq(2) + it 'does trigger builds in the next stage' do + expect(process_pipeline).to be_truthy + expect(builds_names).to eq ['build:1', 'build:2'] - # Process builds service will enqueue builds from the first stage. - # - process_pipeline + succeed_running_or_pending - expect(builds.count).to eq(2) - expect(all_builds.count).to eq(2) + expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2'] - # When builds succeed we will enqueue remaining builds. - # - # We will have 2 succeeded, 1 pending (from stage test), total 4 (two - # additional build from `.gitlab-ci.yml`). - # - succeed_pending - process_pipeline + pipeline.builds.find_by(name: 'test:1').success + pipeline.builds.find_by(name: 'test:2').drop - expect(builds.success.count).to eq(2) - expect(builds.pending.count).to eq(1) - expect(all_builds.count).to eq(4) + expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2'] - # When pending merge_when_pipeline_succeeds in stage test, we enqueue deploy stage. - # - succeed_pending - process_pipeline + Ci::Build.retry(pipeline.builds.find_by(name: 'test:2'), user).success - expect(builds.pending.count).to eq(1) - expect(builds.success.count).to eq(3) - expect(all_builds.count).to eq(4) + expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2', + 'test:2', 'deploy:1', 'deploy:2'] + end + end + end - # When the last one succeeds we have 4 successful builds. - # - succeed_pending - process_pipeline + context 'when there are builds that are not created yet' do + let(:pipeline) do + create(:ci_pipeline, config: config) + end - expect(builds.success.count).to eq(4) - expect(all_builds.count).to eq(4) - end + let(:config) do + { rspec: { stage: 'test', script: 'rspec' }, + deploy: { stage: 'deploy', script: 'rsync' } } + end + + before do + create_build('linux', stage: 'build', stage_idx: 0) + create_build('mac', stage: 'build', stage_idx: 0) + end + + it 'processes the pipeline' do + # Currently we have five builds with state created + # + expect(builds.count).to eq(0) + expect(all_builds.count).to eq(2) + + # Process builds service will enqueue builds from the first stage. + # + process_pipeline + + expect(builds.count).to eq(2) + expect(all_builds.count).to eq(2) + + # When builds succeed we will enqueue remaining builds. + # + # We will have 2 succeeded, 1 pending (from stage test), total 4 (two + # additional build from `.gitlab-ci.yml`). + # + succeed_pending + process_pipeline + + expect(builds.success.count).to eq(2) + expect(builds.pending.count).to eq(1) + expect(all_builds.count).to eq(4) + + # When pending merge_when_pipeline_succeeds in stage test, we enqueue deploy stage. + # + succeed_pending + process_pipeline + + expect(builds.pending.count).to eq(1) + expect(builds.success.count).to eq(3) + expect(all_builds.count).to eq(4) + + # When the last one succeeds we have 4 successful builds. + # + succeed_pending + process_pipeline + + expect(builds.success.count).to eq(4) + expect(all_builds.count).to eq(4) end end + def process_pipeline + described_class.new(pipeline.project, user).execute(pipeline) + end + def all_builds - pipeline.builds + pipeline.builds.order(:stage_idx, :id) end def builds all_builds.where.not(status: [:created, :skipped]) end - def process_pipeline - described_class.new(pipeline.project, user).execute(pipeline) + def builds_names + builds.pluck(:name) + end + + def builds_statuses + builds.pluck(:status) + end + + def all_builds_statuses + all_builds.pluck(:status) end def succeed_pending builds.pending.update_all(status: 'success') end + def succeed_running_or_pending + pipeline.builds.running_or_pending.each(&:success) + end + + def fail_running_or_pending + pipeline.builds.running_or_pending.each(&:drop) + end + + def cancel_running_or_pending + pipeline.builds.running_or_pending.each(&:cancel) + end + + def play_manual_action(name) + builds.find_by(name: name).play(user) + end + delegate :manual_actions, to: :pipeline - def create_build(name, stage_idx, when_value = nil) - create(:ci_build, - :created, - pipeline: pipeline, - name: name, - stage_idx: stage_idx, - when: when_value) + def create_build(name, **opts) + create(:ci_build, :created, pipeline: pipeline, name: name, **opts) 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/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb index 8b1ed6470e4..5445b65f4e8 100644 --- a/spec/services/ci/retry_pipeline_service_spec.rb +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -89,35 +89,74 @@ describe Ci::RetryPipelineService, '#execute', :services do end context 'when pipeline contains manual actions' do - context 'when there is a canceled manual action in first stage' do - before do - create_build('rspec 1', :failed, 0) - create_build('staging', :canceled, 0, :manual) - create_build('rspec 2', :canceled, 1) + context 'when there are optional manual actions only' do + context 'when there is a canceled manual action in first stage' do + before do + create_build('rspec 1', :failed, 0) + create_build('staging', :canceled, 0, when: :manual, allow_failure: true) + create_build('rspec 2', :canceled, 1) + end + + it 'retries failed builds and marks subsequent for processing' do + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(build('staging')).to be_manual + expect(build('rspec 2')).to be_created + expect(pipeline.reload).to be_running + end end + end - it 'retries builds failed builds and marks subsequent for processing' do - service.execute(pipeline) + context 'when pipeline has blocking manual actions defined' do + context 'when pipeline retry should enqueue builds' do + before do + create_build('test', :failed, 0) + create_build('deploy', :canceled, 0, when: :manual, allow_failure: false) + create_build('verify', :canceled, 1) + end + + it 'retries failed builds' do + service.execute(pipeline) + + expect(build('test')).to be_pending + expect(build('deploy')).to be_manual + expect(build('verify')).to be_created + expect(pipeline.reload).to be_running + end + end - expect(build('rspec 1')).to be_pending - expect(build('staging')).to be_skipped - expect(build('rspec 2')).to be_created - expect(pipeline.reload).to be_running + context 'when pipeline retry should block pipeline immediately' do + before do + create_build('test', :success, 0) + create_build('deploy:1', :success, 1, when: :manual, allow_failure: false) + create_build('deploy:2', :failed, 1, when: :manual, allow_failure: false) + create_build('verify', :canceled, 2) + end + + it 'reprocesses blocking manual action and blocks pipeline' do + service.execute(pipeline) + + expect(build('deploy:1')).to be_success + expect(build('deploy:2')).to be_manual + expect(build('verify')).to be_created + expect(pipeline.reload).to be_blocked + end end end context 'when there is a skipped manual action in last stage' do before do create_build('rspec 1', :canceled, 0) - create_build('rspec 2', :skipped, 0, :manual) - create_build('staging', :skipped, 1, :manual) + create_build('rspec 2', :skipped, 0, when: :manual, allow_failure: true) + create_build('staging', :skipped, 1, when: :manual, allow_failure: true) end it 'retries canceled job and reprocesses manual actions' do service.execute(pipeline) expect(build('rspec 1')).to be_pending - expect(build('rspec 2')).to be_skipped + expect(build('rspec 2')).to be_manual expect(build('staging')).to be_created expect(pipeline.reload).to be_running end @@ -126,7 +165,7 @@ describe Ci::RetryPipelineService, '#execute', :services do context 'when there is a created manual action in the last stage' do before do create_build('rspec 1', :canceled, 0) - create_build('staging', :created, 1, :manual) + create_build('staging', :created, 1, when: :manual, allow_failure: true) end it 'retries canceled job and does not update the manual action' do @@ -141,14 +180,14 @@ describe Ci::RetryPipelineService, '#execute', :services do context 'when there is a created manual action in the first stage' do before do create_build('rspec 1', :canceled, 0) - create_build('staging', :created, 0, :manual) + create_build('staging', :created, 0, when: :manual, allow_failure: true) end - it 'retries canceled job and skipps the manual action' do + it 'retries canceled job and processes the manual action' do service.execute(pipeline) expect(build('rspec 1')).to be_pending - expect(build('staging')).to be_skipped + expect(build('staging')).to be_manual expect(pipeline.reload).to be_running end end @@ -183,13 +222,12 @@ describe Ci::RetryPipelineService, '#execute', :services do statuses.latest.find_by(name: name) end - def create_build(name, status, stage_num, on = 'on_success') + def create_build(name, status, stage_num, **opts) create(:ci_build, name: name, status: status, stage: "stage_#{stage_num}", stage_idx: stage_num, - when: on, - pipeline: pipeline) do |build| + pipeline: pipeline, **opts) do |build| pipeline.update_status end 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/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index 14717a7455d..ec89b540e6a 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -4,11 +4,11 @@ describe Groups::CreateService, '#execute', services: true do let!(:user) { create(:user) } let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } } + subject { service.execute } + describe 'visibility level restrictions' do let!(:service) { described_class.new(user, group_params) } - subject { service.execute } - context "create groups without restricted visibility level" do it { is_expected.to be_persisted } end @@ -24,8 +24,6 @@ describe Groups::CreateService, '#execute', services: true do let!(:group) { create(:group) } let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) } - subject { service.execute } - context 'as group owner' do before { group.add_owner(user) } @@ -40,4 +38,20 @@ describe Groups::CreateService, '#execute', services: true do end end end + + describe 'creating a mattermost team' do + let!(:params) { group_params.merge(create_chat_team: "true") } + let!(:service) { described_class.new(user, params) } + + before do + Settings.mattermost['enabled'] = true + end + + it 'create the chat team with the group' do + allow_any_instance_of(Mattermost::Team).to receive(:create) + .and_return({ 'name' => 'tanuki', 'id' => 'lskdjfwlekfjsdifjj' }) + + expect { subject }.to change { ChatTeam.count }.from(0).to(1) + end + end end 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/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index ff367f54d2a..92729f68e5f 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -58,16 +58,16 @@ describe MergeRequests::RefreshService, services: true do it 'executes hooks with update action' do expect(refresh_service).to have_received(:execute_hooks). with(@merge_request, 'update', @oldrev) - end - it { expect(@merge_request.notes).not_to be_empty } - it { expect(@merge_request).to be_open } - it { expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey } - it { expect(@merge_request.diff_head_sha).to eq(@newrev) } - it { expect(@fork_merge_request).to be_open } - it { expect(@fork_merge_request.notes).to be_empty } - it { expect(@build_failed_todo).to be_done } - it { expect(@fork_build_failed_todo).to be_done } + expect(@merge_request.notes).not_to be_empty + expect(@merge_request).to be_open + expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey + expect(@merge_request.diff_head_sha).to eq(@newrev) + expect(@fork_merge_request).to be_open + expect(@fork_merge_request.notes).to be_empty + expect(@build_failed_todo).to be_done + expect(@fork_build_failed_todo).to be_done + end end context 'push to origin repo target branch' do @@ -76,12 +76,14 @@ describe MergeRequests::RefreshService, services: true do reload_mrs end - it { expect(@merge_request.notes.last.note).to include('merged') } - it { expect(@merge_request).to be_merged } - it { expect(@fork_merge_request).to be_merged } - it { expect(@fork_merge_request.notes.last.note).to include('merged') } - it { expect(@build_failed_todo).to be_done } - it { expect(@fork_build_failed_todo).to be_done } + it 'updates the merge state' do + expect(@merge_request.notes.last.note).to include('merged') + expect(@merge_request).to be_merged + expect(@fork_merge_request).to be_merged + expect(@fork_merge_request.notes.last.note).to include('merged') + expect(@build_failed_todo).to be_done + expect(@fork_build_failed_todo).to be_done + end end context 'manual merge of source branch' do @@ -95,13 +97,15 @@ describe MergeRequests::RefreshService, services: true do reload_mrs end - it { expect(@merge_request.notes.last.note).to include('merged') } - it { expect(@merge_request).to be_merged } - it { expect(@merge_request.diffs.size).to be > 0 } - it { expect(@fork_merge_request).to be_merged } - it { expect(@fork_merge_request.notes.last.note).to include('merged') } - it { expect(@build_failed_todo).to be_done } - it { expect(@fork_build_failed_todo).to be_done } + it 'updates the merge state' do + expect(@merge_request.notes.last.note).to include('merged') + expect(@merge_request).to be_merged + expect(@merge_request.diffs.size).to be > 0 + expect(@fork_merge_request).to be_merged + expect(@fork_merge_request.notes.last.note).to include('merged') + expect(@build_failed_todo).to be_done + expect(@fork_build_failed_todo).to be_done + end end context 'push to fork repo source branch' do @@ -117,14 +121,14 @@ describe MergeRequests::RefreshService, services: true do it 'executes hooks with update action' do expect(refresh_service).to have_received(:execute_hooks). with(@fork_merge_request, 'update', @oldrev) - end - it { expect(@merge_request.notes).to be_empty } - it { expect(@merge_request).to be_open } - it { expect(@fork_merge_request.notes.last.note).to include('added 28 commits') } - it { expect(@fork_merge_request).to be_open } - it { expect(@build_failed_todo).to be_pending } - it { expect(@fork_build_failed_todo).to be_pending } + expect(@merge_request.notes).to be_empty + expect(@merge_request).to be_open + expect(@fork_merge_request.notes.last.note).to include('added 28 commits') + expect(@fork_merge_request).to be_open + expect(@build_failed_todo).to be_pending + expect(@fork_build_failed_todo).to be_pending + end end context 'closed fork merge request' do @@ -139,12 +143,14 @@ describe MergeRequests::RefreshService, services: true do expect(refresh_service).not_to have_received(:execute_hooks) end - it { expect(@merge_request.notes).to be_empty } - it { expect(@merge_request).to be_open } - it { expect(@fork_merge_request.notes).to be_empty } - it { expect(@fork_merge_request).to be_closed } - it { expect(@build_failed_todo).to be_pending } - it { expect(@fork_build_failed_todo).to be_pending } + it 'updates merge request to closed state' do + expect(@merge_request.notes).to be_empty + expect(@merge_request).to be_open + expect(@fork_merge_request.notes).to be_empty + expect(@fork_merge_request).to be_closed + expect(@build_failed_todo).to be_pending + expect(@fork_build_failed_todo).to be_pending + end end end @@ -155,12 +161,14 @@ describe MergeRequests::RefreshService, services: true do reload_mrs end - it { expect(@merge_request.notes).to be_empty } - it { expect(@merge_request).to be_open } - it { expect(@fork_merge_request.notes).to be_empty } - it { expect(@fork_merge_request).to be_open } - it { expect(@build_failed_todo).to be_pending } - it { expect(@fork_build_failed_todo).to be_pending } + it 'updates the merge request state' do + expect(@merge_request.notes).to be_empty + expect(@merge_request).to be_open + expect(@fork_merge_request.notes).to be_empty + expect(@fork_merge_request).to be_open + expect(@build_failed_todo).to be_pending + expect(@fork_build_failed_todo).to be_pending + end end describe 'merge request diff' do @@ -179,12 +187,14 @@ describe MergeRequests::RefreshService, services: true do reload_mrs end - it { expect(@merge_request.notes.last.note).to include('merged') } - it { expect(@merge_request).to be_merged } - it { expect(@fork_merge_request).to be_open } - it { expect(@fork_merge_request.notes).to be_empty } - it { expect(@build_failed_todo).to be_done } - it { expect(@fork_build_failed_todo).to be_done } + it 'updates the merge request state' do + expect(@merge_request.notes.last.note).to include('merged') + expect(@merge_request).to be_merged + expect(@fork_merge_request).to be_open + expect(@fork_merge_request.notes).to be_empty + expect(@build_failed_todo).to be_done + expect(@fork_build_failed_todo).to be_done + end end context 'push new branch that exists in a merge request' do diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb index c42eeba4b9c..150c8ccaef7 100644 --- a/spec/services/projects/upload_service_spec.rb +++ b/spec/services/projects/upload_service_spec.rb @@ -10,7 +10,7 @@ describe Projects::UploadService, services: true do context 'for valid gif file' do before do gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') - @link_to_file = upload_file(@project.repository, gif) + @link_to_file = upload_file(@project, gif) end it { expect(@link_to_file).to have_key(:alt) } @@ -23,7 +23,7 @@ describe Projects::UploadService, services: true do before do png = fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png') - @link_to_file = upload_file(@project.repository, png) + @link_to_file = upload_file(@project, png) end it { expect(@link_to_file).to have_key(:alt) } @@ -35,7 +35,7 @@ describe Projects::UploadService, services: true do context 'for valid jpg file' do before do jpg = fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') - @link_to_file = upload_file(@project.repository, jpg) + @link_to_file = upload_file(@project, jpg) end it { expect(@link_to_file).to have_key(:alt) } @@ -47,7 +47,7 @@ describe Projects::UploadService, services: true do context 'for txt file' do before do txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') - @link_to_file = upload_file(@project.repository, txt) + @link_to_file = upload_file(@project, txt) end it { expect(@link_to_file).to have_key(:alt) } @@ -60,14 +60,14 @@ describe Projects::UploadService, services: true do before do txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') allow(txt).to receive(:size) { 1000.megabytes.to_i } - @link_to_file = upload_file(@project.repository, txt) + @link_to_file = upload_file(@project, txt) end it { expect(@link_to_file).to eq(nil) } end end - def upload_file(repository, file) - Projects::UploadService.new(repository, file).execute + def upload_file(project, file) + Projects::UploadService.new(project, file).execute end end 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/capybara.rb b/spec/support/capybara.rb index 16d5f2bf0b8..62740ec29fd 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -3,7 +3,7 @@ require 'capybara/rspec' require 'capybara/poltergeist' # Give CI some extra time -timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10 +timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10 Capybara.javascript_driver = :poltergeist Capybara.register_driver :poltergeist do |app| diff --git a/spec/support/carrierwave.rb b/spec/support/carrierwave.rb index 72af2c70324..b4b016e408f 100644 --- a/spec/support/carrierwave.rb +++ b/spec/support/carrierwave.rb @@ -1,7 +1,7 @@ -CarrierWave.root = 'tmp/tests/uploads' +CarrierWave.root = File.expand_path('tmp/tests/public', Rails.root) RSpec.configure do |config| config.after(:each) do - FileUtils.rm_rf('tmp/tests/uploads') + FileUtils.rm_rf(CarrierWave.root) end end diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb index 58f6636e680..6b009b132b6 100644 --- a/spec/support/filtered_search_helpers.rb +++ b/spec/support/filtered_search_helpers.rb @@ -3,16 +3,25 @@ module FilteredSearchHelpers page.find('.filtered-search') end - def input_filtered_search(search_term, submit: true) - filtered_search.set(search_term) + # Enables input to be set (similar to copy and paste) + def input_filtered_search(search_term, submit: true, extra_space: true) + search = search_term + if extra_space + # Add an extra space to engage visual tokens + search = "#{search_term} " + end + + filtered_search.set(search) 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 +43,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/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index b0f5be55c33..d9113ef4095 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -1,7 +1,19 @@ require 'spec_helper' describe FileUploader do - let(:uploader) { described_class.new(build_stubbed(:project)) } + let(:uploader) { described_class.new(build_stubbed(:empty_project)) } + + describe '.absolute_path' do + it 'returns the correct absolute path by building it dynamically' do + project = build_stubbed(:project) + upload = double(model: project, path: 'secret/foo.jpg') + + dynamic_segment = project.path_with_namespace + + expect(described_class.absolute_path(upload)) + .to end_with("#{dynamic_segment}/secret/foo.jpg") + end + end describe 'initialize' do it 'generates a secret if none is provided' do @@ -32,4 +44,13 @@ describe FileUploader do expect(uploader.move_to_store).to eq(true) end end + + describe '#relative_path' do + it 'removes the leading dynamic path segment' do + fixture = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg') + uploader.store!(fixture_file_upload(fixture)) + + expect(uploader.relative_path).to match(/\A\h{32}\/rails_sample.jpg\z/) + end + end end diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb new file mode 100644 index 00000000000..5c26e334a6e --- /dev/null +++ b/spec/uploaders/records_uploads_spec.rb @@ -0,0 +1,97 @@ +require 'rails_helper' + +describe RecordsUploads do + let(:uploader) do + class RecordsUploadsExampleUploader < GitlabUploader + include RecordsUploads + + storage :file + + def model + FactoryGirl.build_stubbed(:user) + end + end + + RecordsUploadsExampleUploader.new + end + + def upload_fixture(filename) + fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) + end + + describe 'callbacks' do + it 'calls `record_upload` after `store`' do + expect(uploader).to receive(:record_upload).once + + uploader.store!(upload_fixture('doc_sample.txt')) + end + + it 'calls `destroy_upload` after `remove`' do + expect(uploader).to receive(:destroy_upload).once + + uploader.store!(upload_fixture('doc_sample.txt')) + + uploader.remove! + end + end + + describe '#record_upload callback' do + it 'returns early when not using file storage' do + allow(uploader).to receive(:file_storage?).and_return(false) + expect(Upload).not_to receive(:record) + + uploader.store!(upload_fixture('rails_sample.jpg')) + end + + it "returns early when the file doesn't exist" do + allow(uploader).to receive(:file).and_return(double(exists?: false)) + expect(Upload).not_to receive(:record) + + uploader.store!(upload_fixture('rails_sample.jpg')) + end + + it 'creates an Upload record after store' do + expect(Upload).to receive(:record) + .with(uploader) + + uploader.store!(upload_fixture('rails_sample.jpg')) + end + + it 'it destroys Upload records at the same path before recording' do + existing = Upload.create!( + path: File.join('uploads', 'rails_sample.jpg'), + size: 512.kilobytes, + model: build_stubbed(:user), + uploader: uploader.class.to_s + ) + + uploader.store!(upload_fixture('rails_sample.jpg')) + + expect { existing.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect(Upload.count).to eq 1 + end + end + + describe '#destroy_upload callback' do + it 'returns early when not using file storage' do + uploader.store!(upload_fixture('rails_sample.jpg')) + + allow(uploader).to receive(:file_storage?).and_return(false) + expect(Upload).not_to receive(:remove_path) + + uploader.remove! + end + + it 'returns early when file is nil' do + expect(Upload).not_to receive(:remove_path) + + uploader.remove! + end + + it 'it destroys Upload records at the same path after removal' do + uploader.store!(upload_fixture('rails_sample.jpg')) + + expect { uploader.remove! }.to change { Upload.count }.from(1).to(0) + end + end +end diff --git a/spec/uploaders/uploader_helper_spec.rb b/spec/uploaders/uploader_helper_spec.rb index e9efd13b9aa..c47f09adb6d 100644 --- a/spec/uploaders/uploader_helper_spec.rb +++ b/spec/uploaders/uploader_helper_spec.rb @@ -1,10 +1,14 @@ require 'rails_helper' describe UploaderHelper do - class ExampleUploader < CarrierWave::Uploader::Base - include UploaderHelper + let(:uploader) do + example_uploader = Class.new(CarrierWave::Uploader::Base) do + include UploaderHelper - storage :file + storage :file + end + + example_uploader.new end def upload_fixture(filename) @@ -12,8 +16,6 @@ describe UploaderHelper do end describe '#image_or_video?' do - let(:uploader) { ExampleUploader.new } - it 'returns true for an image file' do uploader.store!(upload_fixture('dk.png')) 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/spec/workers/upload_checksum_worker_spec.rb b/spec/workers/upload_checksum_worker_spec.rb new file mode 100644 index 00000000000..911360da66c --- /dev/null +++ b/spec/workers/upload_checksum_worker_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +describe UploadChecksumWorker do + describe '#perform' do + it 'rescues ActiveRecord::RecordNotFound' do + expect { described_class.new.perform(999_999) }.not_to raise_error + end + + it 'calls calculate_checksum_without_delay and save!' do + upload = spy + expect(Upload).to receive(:find).with(999_999).and_return(upload) + + described_class.new.perform(999_999) + + expect(upload).to have_received(:calculate_checksum) + expect(upload).to have_received(:save!) + end + end +end diff --git a/vendor/assets/javascripts/g.bar.js b/vendor/assets/javascripts/g.bar.js deleted file mode 100644 index 166bd654d6e..00000000000 --- a/vendor/assets/javascripts/g.bar.js +++ /dev/null @@ -1,674 +0,0 @@ -/*! - * g.Raphael 0.51 - Charting library, based on Raphaël - * - * Copyright (c) 2009-2012 Dmitry Baranovskiy (http://g.raphaeljs.com) - * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. - */ -(function () { - var mmin = Math.min, - mmax = Math.max; - - function finger(x, y, width, height, dir, ending, isPath, paper) { - var path, - ends = { round: 'round', sharp: 'sharp', soft: 'soft', square: 'square' }; - - // dir 0 for horizontal and 1 for vertical - if ((dir && !height) || (!dir && !width)) { - return isPath ? "" : paper.path(); - } - - ending = ends[ending] || "square"; - height = Math.round(height); - width = Math.round(width); - x = Math.round(x); - y = Math.round(y); - - switch (ending) { - case "round": - if (!dir) { - var r = ~~(height / 2); - - if (width < r) { - r = width; - path = [ - "M", x + .5, y + .5 - ~~(height / 2), - "l", 0, 0, - "a", r, ~~(height / 2), 0, 0, 1, 0, height, - "l", 0, 0, - "z" - ]; - } else { - path = [ - "M", x + .5, y + .5 - r, - "l", width - r, 0, - "a", r, r, 0, 1, 1, 0, height, - "l", r - width, 0, - "z" - ]; - } - } else { - r = ~~(width / 2); - - if (height < r) { - r = height; - path = [ - "M", x - ~~(width / 2), y, - "l", 0, 0, - "a", ~~(width / 2), r, 0, 0, 1, width, 0, - "l", 0, 0, - "z" - ]; - } else { - path = [ - "M", x - r, y, - "l", 0, r - height, - "a", r, r, 0, 1, 1, width, 0, - "l", 0, height - r, - "z" - ]; - } - } - break; - case "sharp": - if (!dir) { - var half = ~~(height / 2); - - path = [ - "M", x, y + half, - "l", 0, -height, mmax(width - half, 0), 0, mmin(half, width), half, -mmin(half, width), half + (half * 2 < height), - "z" - ]; - } else { - half = ~~(width / 2); - path = [ - "M", x + half, y, - "l", -width, 0, 0, -mmax(height - half, 0), half, -mmin(half, height), half, mmin(half, height), half, - "z" - ]; - } - break; - case "square": - if (!dir) { - path = [ - "M", x, y + ~~(height / 2), - "l", 0, -height, width, 0, 0, height, - "z" - ]; - } else { - path = [ - "M", x + ~~(width / 2), y, - "l", 1 - width, 0, 0, -height, width - 1, 0, - "z" - ]; - } - break; - case "soft": - if (!dir) { - r = mmin(width, Math.round(height / 5)); - path = [ - "M", x + .5, y + .5 - ~~(height / 2), - "l", width - r, 0, - "a", r, r, 0, 0, 1, r, r, - "l", 0, height - r * 2, - "a", r, r, 0, 0, 1, -r, r, - "l", r - width, 0, - "z" - ]; - } else { - r = mmin(Math.round(width / 5), height); - path = [ - "M", x - ~~(width / 2), y, - "l", 0, r - height, - "a", r, r, 0, 0, 1, r, -r, - "l", width - 2 * r, 0, - "a", r, r, 0, 0, 1, r, r, - "l", 0, height - r, - "z" - ]; - } - } - - if (isPath) { - return path.join(","); - } else { - return paper.path(path); - } - } - -/*\ - * Paper.vbarchart - [ method ] - ** - * Creates a vertical bar chart - ** - > Parameters - ** - - x (number) x coordinate of the chart - - y (number) y coordinate of the chart - - width (number) width of the chart (respected by all elements in the set) - - height (number) height of the chart (respected by all elements in the set) - - values (array) values - - opts (object) options for the chart - o { - o type (string) type of endings of the bar. Default: 'square'. Other options are: 'round', 'sharp', 'soft'. - o gutter (number)(string) default '20%' (WHAT DOES IT DO?) - o vgutter (number) - o colors (array) colors be used repeatedly to plot the bars. If multicolumn bar is used each sequence of bars with use a different color. - o stacked (boolean) whether or not to tread values as in a stacked bar chart - o to - o stretch (boolean) - o } - ** - = (object) path element of the popup - > Usage - | r.vbarchart(0, 0, 620, 260, [76, 70, 67, 71, 69], {}) - \*/ - - function VBarchart(paper, x, y, width, height, values, opts) { - opts = opts || {}; - - var chartinst = this, - type = opts.type || "square", - gutter = parseFloat(opts.gutter || "20%"), - chart = paper.set(), - bars = paper.set(), - covers = paper.set(), - covers2 = paper.set(), - total = Math.max.apply(Math, values), - stacktotal = [], - multi = 0, - colors = opts.colors || chartinst.colors, - len = values.length; - - if (Raphael.is(values[0], "array")) { - total = []; - multi = len; - len = 0; - - for (var i = values.length; i--;) { - bars.push(paper.set()); - total.push(Math.max.apply(Math, values[i])); - len = Math.max(len, values[i].length); - } - - if (opts.stacked) { - for (var i = len; i--;) { - var tot = 0; - - for (var j = values.length; j--;) { - tot +=+ values[j][i] || 0; - } - - stacktotal.push(tot); - } - } - - for (var i = values.length; i--;) { - if (values[i].length < len) { - for (var j = len; j--;) { - values[i].push(0); - } - } - } - - total = Math.max.apply(Math, opts.stacked ? stacktotal : total); - } - - total = (opts.to) || total; - - var barwidth = width / (len * (100 + gutter) + gutter) * 100, - barhgutter = barwidth * gutter / 100, - barvgutter = opts.vgutter == null ? 20 : opts.vgutter, - stack = [], - X = x + barhgutter, - Y = (height - 2 * barvgutter) / total; - - if (!opts.stretch) { - barhgutter = Math.round(barhgutter); - barwidth = Math.floor(barwidth); - } - - !opts.stacked && (barwidth /= multi || 1); - - for (var i = 0; i < len; i++) { - stack = []; - - for (var j = 0; j < (multi || 1); j++) { - var h = Math.round((multi ? values[j][i] : values[i]) * Y), - top = y + height - barvgutter - h, - bar = finger(Math.round(X + barwidth / 2), top + h, barwidth, h, true, type, null, paper).attr({ stroke: "none", fill: colors[multi ? j : i] }); - - if (multi) { - bars[j].push(bar); - } else { - bars.push(bar); - } - - bar.y = top; - bar.x = Math.round(X + barwidth / 2); - bar.w = barwidth; - bar.h = h; - bar.value = multi ? values[j][i] : values[i]; - - if (!opts.stacked) { - X += barwidth; - } else { - stack.push(bar); - } - } - - if (opts.stacked) { - var cvr; - - covers2.push(cvr = paper.rect(stack[0].x - stack[0].w / 2, y, barwidth, height).attr(chartinst.shim)); - cvr.bars = paper.set(); - - var size = 0; - - for (var s = stack.length; s--;) { - stack[s].toFront(); - } - - for (var s = 0, ss = stack.length; s < ss; s++) { - var bar = stack[s], - cover, - h = (size + bar.value) * Y, - path = finger(bar.x, y + height - barvgutter - !!size * .5, barwidth, h, true, type, 1, paper); - - cvr.bars.push(bar); - size && bar.attr({path: path}); - bar.h = h; - bar.y = y + height - barvgutter - !!size * .5 - h; - covers.push(cover = paper.rect(bar.x - bar.w / 2, bar.y, barwidth, bar.value * Y).attr(chartinst.shim)); - cover.bar = bar; - cover.value = bar.value; - size += bar.value; - } - - X += barwidth; - } - - X += barhgutter; - } - - covers2.toFront(); - X = x + barhgutter; - - if (!opts.stacked) { - for (var i = 0; i < len; i++) { - for (var j = 0; j < (multi || 1); j++) { - var cover; - - covers.push(cover = paper.rect(Math.round(X), y + barvgutter, barwidth, height - barvgutter).attr(chartinst.shim)); - cover.bar = multi ? bars[j][i] : bars[i]; - cover.value = cover.bar.value; - X += barwidth; - } - - X += barhgutter; - } - } - - chart.label = function (labels, isBottom) { - labels = labels || []; - this.labels = paper.set(); - - var L, l = -Infinity; - - if (opts.stacked) { - for (var i = 0; i < len; i++) { - var tot = 0; - - for (var j = 0; j < (multi || 1); j++) { - tot += multi ? values[j][i] : values[i]; - - if (j == multi - 1) { - var label = paper.labelise(labels[i], tot, total); - - L = paper.text(bars[i * (multi || 1) + j].x, y + height - barvgutter / 2, label).attr(txtattr).insertBefore(covers[i * (multi || 1) + j]); - - var bb = L.getBBox(); - - if (bb.x - 7 < l) { - L.remove(); - } else { - this.labels.push(L); - l = bb.x + bb.width; - } - } - } - } - } else { - for (var i = 0; i < len; i++) { - for (var j = 0; j < (multi || 1); j++) { - var label = paper.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total); - - L = paper.text(bars[i * (multi || 1) + j].x, isBottom ? y + height - barvgutter / 2 : bars[i * (multi || 1) + j].y - 10, label).attr(txtattr).insertBefore(covers[i * (multi || 1) + j]); - - var bb = L.getBBox(); - - if (bb.x - 7 < l) { - L.remove(); - } else { - this.labels.push(L); - l = bb.x + bb.width; - } - } - } - } - return this; - }; - - chart.hover = function (fin, fout) { - covers2.hide(); - covers.show(); - covers.mouseover(fin).mouseout(fout); - return this; - }; - - chart.hoverColumn = function (fin, fout) { - covers.hide(); - covers2.show(); - fout = fout || function () {}; - covers2.mouseover(fin).mouseout(fout); - return this; - }; - - chart.click = function (f) { - covers2.hide(); - covers.show(); - covers.click(f); - return this; - }; - - chart.each = function (f) { - if (!Raphael.is(f, "function")) { - return this; - } - for (var i = covers.length; i--;) { - f.call(covers[i]); - } - return this; - }; - - chart.eachColumn = function (f) { - if (!Raphael.is(f, "function")) { - return this; - } - for (var i = covers2.length; i--;) { - f.call(covers2[i]); - } - return this; - }; - - chart.clickColumn = function (f) { - covers.hide(); - covers2.show(); - covers2.click(f); - return this; - }; - - chart.push(bars, covers, covers2); - chart.bars = bars; - chart.covers = covers; - return chart; - }; - - //inheritance - var F = function() {}; - F.prototype = Raphael.g; - HBarchart.prototype = VBarchart.prototype = new F; //prototype reused by hbarchart - - Raphael.fn.barchart = function(x, y, width, height, values, opts) { - return new VBarchart(this, x, y, width, height, values, opts); - }; - -/*\ - * Paper.barchart - [ method ] - ** - * Creates a horizontal bar chart - ** - > Parameters - ** - - x (number) x coordinate of the chart - - y (number) y coordinate of the chart - - width (number) width of the chart (respected by all elements in the set) - - height (number) height of the chart (respected by all elements in the set) - - values (array) values - - opts (object) options for the chart - o { - o type (string) type of endings of the bar. Default: 'square'. Other options are: 'round', 'sharp', 'soft'. - o gutter (number)(string) default '20%' (WHAT DOES IT DO?) - o vgutter (number) - o colors (array) colors be used repeatedly to plot the bars. If multicolumn bar is used each sequence of bars with use a different color. - o stacked (boolean) whether or not to tread values as in a stacked bar chart - o to - o stretch (boolean) - o } - ** - = (object) path element of the popup - > Usage - | r.barchart(0, 0, 620, 260, [76, 70, 67, 71, 69], {}) - \*/ - - function HBarchart(paper, x, y, width, height, values, opts) { - opts = opts || {}; - - var chartinst = this, - type = opts.type || "square", - gutter = parseFloat(opts.gutter || "20%"), - chart = paper.set(), - bars = paper.set(), - covers = paper.set(), - covers2 = paper.set(), - total = Math.max.apply(Math, values), - stacktotal = [], - multi = 0, - colors = opts.colors || chartinst.colors, - len = values.length; - - if (Raphael.is(values[0], "array")) { - total = []; - multi = len; - len = 0; - - for (var i = values.length; i--;) { - bars.push(paper.set()); - total.push(Math.max.apply(Math, values[i])); - len = Math.max(len, values[i].length); - } - - if (opts.stacked) { - for (var i = len; i--;) { - var tot = 0; - for (var j = values.length; j--;) { - tot +=+ values[j][i] || 0; - } - stacktotal.push(tot); - } - } - - for (var i = values.length; i--;) { - if (values[i].length < len) { - for (var j = len; j--;) { - values[i].push(0); - } - } - } - - total = Math.max.apply(Math, opts.stacked ? stacktotal : total); - } - - total = (opts.to) || total; - - var barheight = Math.floor(height / (len * (100 + gutter) + gutter) * 100), - bargutter = Math.floor(barheight * gutter / 100), - stack = [], - Y = y + bargutter, - X = (width - 1) / total; - - !opts.stacked && (barheight /= multi || 1); - - for (var i = 0; i < len; i++) { - stack = []; - - for (var j = 0; j < (multi || 1); j++) { - var val = multi ? values[j][i] : values[i], - bar = finger(x, Y + barheight / 2, Math.round(val * X), barheight - 1, false, type, null, paper).attr({stroke: "none", fill: colors[multi ? j : i]}); - - if (multi) { - bars[j].push(bar); - } else { - bars.push(bar); - } - - bar.x = x + Math.round(val * X); - bar.y = Y + barheight / 2; - bar.w = Math.round(val * X); - bar.h = barheight; - bar.value = +val; - - if (!opts.stacked) { - Y += barheight; - } else { - stack.push(bar); - } - } - - if (opts.stacked) { - var cvr = paper.rect(x, stack[0].y - stack[0].h / 2, width, barheight).attr(chartinst.shim); - - covers2.push(cvr); - cvr.bars = paper.set(); - - var size = 0; - - for (var s = stack.length; s--;) { - stack[s].toFront(); - } - - for (var s = 0, ss = stack.length; s < ss; s++) { - var bar = stack[s], - cover, - val = Math.round((size + bar.value) * X), - path = finger(x, bar.y, val, barheight - 1, false, type, 1, paper); - - cvr.bars.push(bar); - size && bar.attr({ path: path }); - bar.w = val; - bar.x = x + val; - covers.push(cover = paper.rect(x + size * X, bar.y - bar.h / 2, bar.value * X, barheight).attr(chartinst.shim)); - cover.bar = bar; - size += bar.value; - } - - Y += barheight; - } - - Y += bargutter; - } - - covers2.toFront(); - Y = y + bargutter; - - if (!opts.stacked) { - for (var i = 0; i < len; i++) { - for (var j = 0; j < (multi || 1); j++) { - var cover = paper.rect(x, Y, width, barheight).attr(chartinst.shim); - - covers.push(cover); - cover.bar = multi ? bars[j][i] : bars[i]; - cover.value = cover.bar.value; - Y += barheight; - } - - Y += bargutter; - } - } - - chart.label = function (labels, isRight) { - labels = labels || []; - this.labels = paper.set(); - - for (var i = 0; i < len; i++) { - for (var j = 0; j < multi; j++) { - var label = paper.labelise(multi ? labels[j] && labels[j][i] : labels[i], multi ? values[j][i] : values[i], total), - X = isRight ? bars[i * (multi || 1) + j].x - barheight / 2 + 3 : x + 5, - A = isRight ? "end" : "start", - L; - - this.labels.push(L = paper.text(X, bars[i * (multi || 1) + j].y, label).attr(txtattr).attr({ "text-anchor": A }).insertBefore(covers[0])); - - if (L.getBBox().x < x + 5) { - L.attr({x: x + 5, "text-anchor": "start"}); - } else { - bars[i * (multi || 1) + j].label = L; - } - } - } - - return this; - }; - - chart.hover = function (fin, fout) { - covers2.hide(); - covers.show(); - fout = fout || function () {}; - covers.mouseover(fin).mouseout(fout); - return this; - }; - - chart.hoverColumn = function (fin, fout) { - covers.hide(); - covers2.show(); - fout = fout || function () {}; - covers2.mouseover(fin).mouseout(fout); - return this; - }; - - chart.each = function (f) { - if (!Raphael.is(f, "function")) { - return this; - } - for (var i = covers.length; i--;) { - f.call(covers[i]); - } - return this; - }; - - chart.eachColumn = function (f) { - if (!Raphael.is(f, "function")) { - return this; - } - for (var i = covers2.length; i--;) { - f.call(covers2[i]); - } - return this; - }; - - chart.click = function (f) { - covers2.hide(); - covers.show(); - covers.click(f); - return this; - }; - - chart.clickColumn = function (f) { - covers.hide(); - covers2.show(); - covers2.click(f); - return this; - }; - - chart.push(bars, covers, covers2); - chart.bars = bars; - chart.covers = covers; - return chart; - }; - - Raphael.fn.hbarchart = function(x, y, width, height, values, opts) { - return new HBarchart(this, x, y, width, height, values, opts); - }; - -})(); diff --git a/vendor/assets/javascripts/g.raphael.js b/vendor/assets/javascripts/g.raphael.js deleted file mode 100644 index 27f27caf9f2..00000000000 --- a/vendor/assets/javascripts/g.raphael.js +++ /dev/null @@ -1,861 +0,0 @@ -/*! - * g.Raphael 0.51 - Charting library, based on Raphaël - * - * Copyright (c) 2009-2012 Dmitry Baranovskiy (http://g.raphaeljs.com) - * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. - */ - -/* - * Tooltips on Element prototype - */ -/*\ - * Element.popup - [ method ] - ** - * Puts the context Element in a 'popup' tooltip. Can also be used on sets. - ** - > Parameters - ** - - dir (string) location of Element relative to the tail: `'down'`, `'left'`, `'up'` [default], or `'right'`. - - size (number) amount of bevel/padding around the Element, as well as half the width and height of the tail [default: `5`] - - x (number) x coordinate of the popup's tail [default: Element's `x` or `cx`] - - y (number) y coordinate of the popup's tail [default: Element's `y` or `cy`] - ** - = (object) path element of the popup - \*/ -Raphael.el.popup = function (dir, size, x, y) { - var paper = this.paper || this[0].paper, - bb, xy, center, cw, ch; - - if (!paper) return; - - switch (this.type) { - case 'text': - case 'circle': - case 'ellipse': center = true; break; - default: center = false; - } - - dir = dir == null ? 'up' : dir; - size = size || 5; - bb = this.getBBox(); - - x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x); - y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y); - cw = Math.max(bb.width / 2 - size, 0); - ch = Math.max(bb.height / 2 - size, 0); - - this.translate(x - bb.x - (center ? bb.width / 2 : 0), y - bb.y - (center ? bb.height / 2 : 0)); - bb = this.getBBox(); - - var paths = { - up: [ - 'M', x, y, - 'l', -size, -size, -cw, 0, - 'a', size, size, 0, 0, 1, -size, -size, - 'l', 0, -bb.height, - 'a', size, size, 0, 0, 1, size, -size, - 'l', size * 2 + cw * 2, 0, - 'a', size, size, 0, 0, 1, size, size, - 'l', 0, bb.height, - 'a', size, size, 0, 0, 1, -size, size, - 'l', -cw, 0, - 'z' - ].join(','), - down: [ - 'M', x, y, - 'l', size, size, cw, 0, - 'a', size, size, 0, 0, 1, size, size, - 'l', 0, bb.height, - 'a', size, size, 0, 0, 1, -size, size, - 'l', -(size * 2 + cw * 2), 0, - 'a', size, size, 0, 0, 1, -size, -size, - 'l', 0, -bb.height, - 'a', size, size, 0, 0, 1, size, -size, - 'l', cw, 0, - 'z' - ].join(','), - left: [ - 'M', x, y, - 'l', -size, size, 0, ch, - 'a', size, size, 0, 0, 1, -size, size, - 'l', -bb.width, 0, - 'a', size, size, 0, 0, 1, -size, -size, - 'l', 0, -(size * 2 + ch * 2), - 'a', size, size, 0, 0, 1, size, -size, - 'l', bb.width, 0, - 'a', size, size, 0, 0, 1, size, size, - 'l', 0, ch, - 'z' - ].join(','), - right: [ - 'M', x, y, - 'l', size, -size, 0, -ch, - 'a', size, size, 0, 0, 1, size, -size, - 'l', bb.width, 0, - 'a', size, size, 0, 0, 1, size, size, - 'l', 0, size * 2 + ch * 2, - 'a', size, size, 0, 0, 1, -size, size, - 'l', -bb.width, 0, - 'a', size, size, 0, 0, 1, -size, -size, - 'l', 0, -ch, - 'z' - ].join(',') - }; - - xy = { - up: { x: -!center * (bb.width / 2), y: -size * 2 - (center ? bb.height / 2 : bb.height) }, - down: { x: -!center * (bb.width / 2), y: size * 2 + (center ? bb.height / 2 : bb.height) }, - left: { x: -size * 2 - (center ? bb.width / 2 : bb.width), y: -!center * (bb.height / 2) }, - right: { x: size * 2 + (center ? bb.width / 2 : bb.width), y: -!center * (bb.height / 2) } - }[dir]; - - this.translate(xy.x, xy.y); - return paper.path(paths[dir]).attr({ fill: "#000", stroke: "none" }).insertBefore(this.node ? this : this[0]); -}; - -/*\ - * Element.tag - [ method ] - ** - * Puts the context Element in a 'tag' tooltip. Can also be used on sets. - ** - > Parameters - ** - - angle (number) angle of orientation in degrees [default: `0`] - - r (number) radius of the loop [default: `5`] - - x (number) x coordinate of the center of the tag loop [default: Element's `x` or `cx`] - - y (number) y coordinate of the center of the tag loop [default: Element's `x` or `cx`] - ** - = (object) path element of the tag - \*/ -Raphael.el.tag = function (angle, r, x, y) { - var d = 3, - paper = this.paper || this[0].paper; - - if (!paper) return; - - var p = paper.path().attr({ fill: '#000', stroke: '#000' }), - bb = this.getBBox(), - dx, R, center, tmp; - - switch (this.type) { - case 'text': - case 'circle': - case 'ellipse': center = true; break; - default: center = false; - } - - angle = angle || 0; - x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x); - y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y); - r = r == null ? 5 : r; - R = .5522 * r; - - if (bb.height >= r * 2) { - p.attr({ - path: [ - "M", x, y + r, - "a", r, r, 0, 1, 1, 0, -r * 2, r, r, 0, 1, 1, 0, r * 2, - "m", 0, -r * 2 -d, - "a", r + d, r + d, 0, 1, 0, 0, (r + d) * 2, - "L", x + r + d, y + bb.height / 2 + d, - "l", bb.width + 2 * d, 0, 0, -bb.height - 2 * d, -bb.width - 2 * d, 0, - "L", x, y - r - d - ].join(",") - }); - } else { - dx = Math.sqrt(Math.pow(r + d, 2) - Math.pow(bb.height / 2 + d, 2)); - p.attr({ - path: [ - "M", x, y + r, - "c", -R, 0, -r, R - r, -r, -r, 0, -R, r - R, -r, r, -r, R, 0, r, r - R, r, r, 0, R, R - r, r, -r, r, - "M", x + dx, y - bb.height / 2 - d, - "a", r + d, r + d, 0, 1, 0, 0, bb.height + 2 * d, - "l", r + d - dx + bb.width + 2 * d, 0, 0, -bb.height - 2 * d, - "L", x + dx, y - bb.height / 2 - d - ].join(",") - }); - } - - angle = 360 - angle; - p.rotate(angle, x, y); - - if (this.attrs) { - //elements - this.attr(this.attrs.x ? 'x' : 'cx', x + r + d + (!center ? this.type == 'text' ? bb.width : 0 : bb.width / 2)).attr('y', center ? y : y - bb.height / 2); - this.rotate(angle, x, y); - angle > 90 && angle < 270 && this.attr(this.attrs.x ? 'x' : 'cx', x - r - d - (!center ? bb.width : bb.width / 2)).rotate(180, x, y); - } else { - //sets - if (angle > 90 && angle < 270) { - this.translate(x - bb.x - bb.width - r - d, y - bb.y - bb.height / 2); - this.rotate(angle - 180, bb.x + bb.width + r + d, bb.y + bb.height / 2); - } else { - this.translate(x - bb.x + r + d, y - bb.y - bb.height / 2); - this.rotate(angle, bb.x - r - d, bb.y + bb.height / 2); - } - } - - return p.insertBefore(this.node ? this : this[0]); -}; - -/*\ - * Element.drop - [ method ] - ** - * Puts the context Element in a 'drop' tooltip. Can also be used on sets. - ** - > Parameters - ** - - angle (number) angle of orientation in degrees [default: `0`] - - x (number) x coordinate of the drop's point [default: Element's `x` or `cx`] - - y (number) y coordinate of the drop's point [default: Element's `x` or `cx`] - ** - = (object) path element of the drop - \*/ -Raphael.el.drop = function (angle, x, y) { - var bb = this.getBBox(), - paper = this.paper || this[0].paper, - center, size, p, dx, dy; - - if (!paper) return; - - switch (this.type) { - case 'text': - case 'circle': - case 'ellipse': center = true; break; - default: center = false; - } - - angle = angle || 0; - - x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x); - y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y); - size = Math.max(bb.width, bb.height) + Math.min(bb.width, bb.height); - p = paper.path([ - "M", x, y, - "l", size, 0, - "A", size * .4, size * .4, 0, 1, 0, x + size * .7, y - size * .7, - "z" - ]).attr({fill: "#000", stroke: "none"}).rotate(22.5 - angle, x, y); - - angle = (angle + 90) * Math.PI / 180; - dx = (x + size * Math.sin(angle)) - (center ? 0 : bb.width / 2); - dy = (y + size * Math.cos(angle)) - (center ? 0 : bb.height / 2); - - this.attrs ? - this.attr(this.attrs.x ? 'x' : 'cx', dx).attr(this.attrs.y ? 'y' : 'cy', dy) : - this.translate(dx - bb.x, dy - bb.y); - - return p.insertBefore(this.node ? this : this[0]); -}; - -/*\ - * Element.flag - [ method ] - ** - * Puts the context Element in a 'flag' tooltip. Can also be used on sets. - ** - > Parameters - ** - - angle (number) angle of orientation in degrees [default: `0`] - - x (number) x coordinate of the flag's point [default: Element's `x` or `cx`] - - y (number) y coordinate of the flag's point [default: Element's `x` or `cx`] - ** - = (object) path element of the flag - \*/ -Raphael.el.flag = function (angle, x, y) { - var d = 3, - paper = this.paper || this[0].paper; - - if (!paper) return; - - var p = paper.path().attr({ fill: '#000', stroke: '#000' }), - bb = this.getBBox(), - h = bb.height / 2, - center; - - switch (this.type) { - case 'text': - case 'circle': - case 'ellipse': center = true; break; - default: center = false; - } - - angle = angle || 0; - x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x); - y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2: bb.y); - - p.attr({ - path: [ - "M", x, y, - "l", h + d, -h - d, bb.width + 2 * d, 0, 0, bb.height + 2 * d, -bb.width - 2 * d, 0, - "z" - ].join(",") - }); - - angle = 360 - angle; - p.rotate(angle, x, y); - - if (this.attrs) { - //elements - this.attr(this.attrs.x ? 'x' : 'cx', x + h + d + (!center ? this.type == 'text' ? bb.width : 0 : bb.width / 2)).attr('y', center ? y : y - bb.height / 2); - this.rotate(angle, x, y); - angle > 90 && angle < 270 && this.attr(this.attrs.x ? 'x' : 'cx', x - h - d - (!center ? bb.width : bb.width / 2)).rotate(180, x, y); - } else { - //sets - if (angle > 90 && angle < 270) { - this.translate(x - bb.x - bb.width - h - d, y - bb.y - bb.height / 2); - this.rotate(angle - 180, bb.x + bb.width + h + d, bb.y + bb.height / 2); - } else { - this.translate(x - bb.x + h + d, y - bb.y - bb.height / 2); - this.rotate(angle, bb.x - h - d, bb.y + bb.height / 2); - } - } - - return p.insertBefore(this.node ? this : this[0]); -}; - -/*\ - * Element.label - [ method ] - ** - * Puts the context Element in a 'label' tooltip. Can also be used on sets. - ** - = (object) path element of the label. - \*/ -Raphael.el.label = function () { - var bb = this.getBBox(), - paper = this.paper || this[0].paper, - r = Math.min(20, bb.width + 10, bb.height + 10) / 2; - - if (!paper) return; - - return paper.rect(bb.x - r / 2, bb.y - r / 2, bb.width + r, bb.height + r, r).attr({ stroke: 'none', fill: '#000' }).insertBefore(this.node ? this : this[0]); -}; - -/*\ - * Element.blob - [ method ] - ** - * Puts the context Element in a 'blob' tooltip. Can also be used on sets. - ** - > Parameters - ** - - angle (number) angle of orientation in degrees [default: `0`] - - x (number) x coordinate of the blob's tail [default: Element's `x` or `cx`] - - y (number) y coordinate of the blob's tail [default: Element's `x` or `cx`] - ** - = (object) path element of the blob - \*/ -Raphael.el.blob = function (angle, x, y) { - var bb = this.getBBox(), - rad = Math.PI / 180, - paper = this.paper || this[0].paper, - p, center, size; - - if (!paper) return; - - switch (this.type) { - case 'text': - case 'circle': - case 'ellipse': center = true; break; - default: center = false; - } - - p = paper.path().attr({ fill: "#000", stroke: "none" }); - angle = (+angle + 1 ? angle : 45) + 90; - size = Math.min(bb.height, bb.width); - x = typeof x == 'number' ? x : (center ? bb.x + bb.width / 2 : bb.x); - y = typeof y == 'number' ? y : (center ? bb.y + bb.height / 2 : bb.y); - - var w = Math.max(bb.width + size, size * 25 / 12), - h = Math.max(bb.height + size, size * 25 / 12), - x2 = x + size * Math.sin((angle - 22.5) * rad), - y2 = y + size * Math.cos((angle - 22.5) * rad), - x1 = x + size * Math.sin((angle + 22.5) * rad), - y1 = y + size * Math.cos((angle + 22.5) * rad), - dx = (x1 - x2) / 2, - dy = (y1 - y2) / 2, - rx = w / 2, - ry = h / 2, - k = -Math.sqrt(Math.abs(rx * rx * ry * ry - rx * rx * dy * dy - ry * ry * dx * dx) / (rx * rx * dy * dy + ry * ry * dx * dx)), - cx = k * rx * dy / ry + (x1 + x2) / 2, - cy = k * -ry * dx / rx + (y1 + y2) / 2; - - p.attr({ - x: cx, - y: cy, - path: [ - "M", x, y, - "L", x1, y1, - "A", rx, ry, 0, 1, 1, x2, y2, - "z" - ].join(",") - }); - - this.translate(cx - bb.x - bb.width / 2, cy - bb.y - bb.height / 2); - - return p.insertBefore(this.node ? this : this[0]); -}; - -/* - * Tooltips on Paper prototype - */ -/*\ - * Paper.label - [ method ] - ** - * Puts the given `text` into a 'label' tooltip. The text is given a default style according to @g.txtattr. See @Element.label - ** - > Parameters - ** - - x (number) x coordinate of the center of the label - - y (number) y coordinate of the center of the label - - text (string) text to place inside the label - ** - = (object) set containing the label path and the text element - > Usage - | paper.label(50, 50, "$9.99"); - \*/ -Raphael.fn.label = function (x, y, text) { - var set = this.set(); - - text = this.text(x, y, text).attr(Raphael.g.txtattr); - return set.push(text.label(), text); -}; - -/*\ - * Paper.popup - [ method ] - ** - * Puts the given `text` into a 'popup' tooltip. The text is given a default style according to @g.txtattr. See @Element.popup - * - * Note: The `dir` parameter has changed from g.Raphael 0.4.1 to 0.5. The options `0`, `1`, `2`, and `3` has been changed to `'down'`, `'left'`, `'up'`, and `'right'` respectively. - ** - > Parameters - ** - - x (number) x coordinate of the popup's tail - - y (number) y coordinate of the popup's tail - - text (string) text to place inside the popup - - dir (string) location of the text relative to the tail: `'down'`, `'left'`, `'up'` [default], or `'right'`. - - size (number) amount of padding around the Element [default: `5`] - ** - = (object) set containing the popup path and the text element - > Usage - | paper.popup(50, 50, "$9.99", 'down'); - \*/ -Raphael.fn.popup = function (x, y, text, dir, size) { - var set = this.set(); - - text = this.text(x, y, text).attr(Raphael.g.txtattr); - return set.push(text.popup(dir, size), text); -}; - -/*\ - * Paper.tag - [ method ] - ** - * Puts the given text into a 'tag' tooltip. The text is given a default style according to @g.txtattr. See @Element.tag - ** - > Parameters - ** - - x (number) x coordinate of the center of the tag loop - - y (number) y coordinate of the center of the tag loop - - text (string) text to place inside the tag - - angle (number) angle of orientation in degrees [default: `0`] - - r (number) radius of the loop [default: `5`] - ** - = (object) set containing the tag path and the text element - > Usage - | paper.tag(50, 50, "$9.99", 60); - \*/ -Raphael.fn.tag = function (x, y, text, angle, r) { - var set = this.set(); - - text = this.text(x, y, text).attr(Raphael.g.txtattr); - return set.push(text.tag(angle, r), text); -}; - -/*\ - * Paper.flag - [ method ] - ** - * Puts the given `text` into a 'flag' tooltip. The text is given a default style according to @g.txtattr. See @Element.flag - ** - > Parameters - ** - - x (number) x coordinate of the flag's point - - y (number) y coordinate of the flag's point - - text (string) text to place inside the flag - - angle (number) angle of orientation in degrees [default: `0`] - ** - = (object) set containing the flag path and the text element - > Usage - | paper.flag(50, 50, "$9.99", 60); - \*/ -Raphael.fn.flag = function (x, y, text, angle) { - var set = this.set(); - - text = this.text(x, y, text).attr(Raphael.g.txtattr); - return set.push(text.flag(angle), text); -}; - -/*\ - * Paper.drop - [ method ] - ** - * Puts the given text into a 'drop' tooltip. The text is given a default style according to @g.txtattr. See @Element.drop - ** - > Parameters - ** - - x (number) x coordinate of the drop's point - - y (number) y coordinate of the drop's point - - text (string) text to place inside the drop - - angle (number) angle of orientation in degrees [default: `0`] - ** - = (object) set containing the drop path and the text element - > Usage - | paper.drop(50, 50, "$9.99", 60); - \*/ -Raphael.fn.drop = function (x, y, text, angle) { - var set = this.set(); - - text = this.text(x, y, text).attr(Raphael.g.txtattr); - return set.push(text.drop(angle), text); -}; - -/*\ - * Paper.blob - [ method ] - ** - * Puts the given text into a 'blob' tooltip. The text is given a default style according to @g.txtattr. See @Element.blob - ** - > Parameters - ** - - x (number) x coordinate of the blob's tail - - y (number) y coordinate of the blob's tail - - text (string) text to place inside the blob - - angle (number) angle of orientation in degrees [default: `0`] - ** - = (object) set containing the blob path and the text element - > Usage - | paper.blob(50, 50, "$9.99", 60); - \*/ -Raphael.fn.blob = function (x, y, text, angle) { - var set = this.set(); - - text = this.text(x, y, text).attr(Raphael.g.txtattr); - return set.push(text.blob(angle), text); -}; - -/** - * Brightness functions on the Element prototype - */ -/*\ - * Element.lighter - [ method ] - ** - * Makes the context element lighter by increasing the brightness and reducing the saturation by a given factor. Can be called on Sets. - ** - > Parameters - ** - - times (number) adjustment factor [default: `2`] - ** - = (object) Element - > Usage - | paper.circle(50, 50, 20).attr({ - | fill: "#ff0000", - | stroke: "#fff", - | "stroke-width": 2 - | }).lighter(6); - \*/ -Raphael.el.lighter = function (times) { - times = times || 2; - - var fs = [this.attrs.fill, this.attrs.stroke]; - - this.fs = this.fs || [fs[0], fs[1]]; - - fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex); - fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex); - fs[0].b = Math.min(fs[0].b * times, 1); - fs[0].s = fs[0].s / times; - fs[1].b = Math.min(fs[1].b * times, 1); - fs[1].s = fs[1].s / times; - - this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"}); - return this; -}; - -/*\ - * Element.darker - [ method ] - ** - * Makes the context element darker by decreasing the brightness and increasing the saturation by a given factor. Can be called on Sets. - ** - > Parameters - ** - - times (number) adjustment factor [default: `2`] - ** - = (object) Element - > Usage - | paper.circle(50, 50, 20).attr({ - | fill: "#ff0000", - | stroke: "#fff", - | "stroke-width": 2 - | }).darker(6); - \*/ -Raphael.el.darker = function (times) { - times = times || 2; - - var fs = [this.attrs.fill, this.attrs.stroke]; - - this.fs = this.fs || [fs[0], fs[1]]; - - fs[0] = Raphael.rgb2hsb(Raphael.getRGB(fs[0]).hex); - fs[1] = Raphael.rgb2hsb(Raphael.getRGB(fs[1]).hex); - fs[0].s = Math.min(fs[0].s * times, 1); - fs[0].b = fs[0].b / times; - fs[1].s = Math.min(fs[1].s * times, 1); - fs[1].b = fs[1].b / times; - - this.attr({fill: "hsb(" + [fs[0].h, fs[0].s, fs[0].b] + ")", stroke: "hsb(" + [fs[1].h, fs[1].s, fs[1].b] + ")"}); - return this; -}; - -/*\ - * Element.resetBrightness - [ method ] - ** - * Resets brightness and saturation levels to their original values. See @Element.lighter and @Element.darker. Can be called on Sets. - ** - = (object) Element - > Usage - | paper.circle(50, 50, 20).attr({ - | fill: "#ff0000", - | stroke: "#fff", - | "stroke-width": 2 - | }).lighter(6).resetBrightness(); - \*/ -Raphael.el.resetBrightness = function () { - if (this.fs) { - this.attr({ fill: this.fs[0], stroke: this.fs[1] }); - delete this.fs; - } - return this; -}; - -//alias to set prototype -(function () { - var brightness = ['lighter', 'darker', 'resetBrightness'], - tooltips = ['popup', 'tag', 'flag', 'label', 'drop', 'blob']; - - for (var f in tooltips) (function (name) { - Raphael.st[name] = function () { - return Raphael.el[name].apply(this, arguments); - }; - })(tooltips[f]); - - for (var f in brightness) (function (name) { - Raphael.st[name] = function () { - for (var i = 0; i < this.length; i++) { - this[i][name].apply(this[i], arguments); - } - - return this; - }; - })(brightness[f]); -})(); - -//chart prototype for storing common functions -Raphael.g = { - /*\ - * g.shim - [ object ] - ** - * An attribute object that charts will set on all generated shims (shims being the invisible objects that mouse events are bound to) - ** - > Default value - | { stroke: 'none', fill: '#000', 'fill-opacity': 0 } - \*/ - shim: { stroke: 'none', fill: '#000', 'fill-opacity': 0 }, - - /*\ - * g.txtattr - [ object ] - ** - * An attribute object that charts and tooltips will set on any generated text - ** - > Default value - | { font: '12px Arial, sans-serif', fill: '#fff' } - \*/ - txtattr: { font: '12px Arial, sans-serif', fill: '#fff' }, - - /*\ - * g.colors - [ array ] - ** - * An array of color values that charts will iterate through when drawing chart data values. - ** - \*/ - colors: (function () { - var hues = [.6, .2, .05, .1333, .75, 0], - colors = []; - - for (var i = 0; i < 10; i++) { - if (i < hues.length) { - colors.push('hsb(' + hues[i] + ',.75, .75)'); - } else { - colors.push('hsb(' + hues[i - hues.length] + ', 1, .5)'); - } - } - - return colors; - })(), - - snapEnds: function(from, to, steps) { - var f = from, - t = to; - - if (f == t) { - return {from: f, to: t, power: 0}; - } - - function round(a) { - return Math.abs(a - .5) < .25 ? ~~(a) + .5 : Math.round(a); - } - - var d = (t - f) / steps, - r = ~~(d), - R = r, - i = 0; - - if (r) { - while (R) { - i--; - R = ~~(d * Math.pow(10, i)) / Math.pow(10, i); - } - - i ++; - } else { - if(d == 0 || !isFinite(d)) { - i = 1; - } else { - while (!r) { - i = i || 1; - r = ~~(d * Math.pow(10, i)) / Math.pow(10, i); - i++; - } - } - - i && i--; - } - - t = round(to * Math.pow(10, i)) / Math.pow(10, i); - - if (t < to) { - t = round((to + .5) * Math.pow(10, i)) / Math.pow(10, i); - } - - f = round((from - (i > 0 ? 0 : .5)) * Math.pow(10, i)) / Math.pow(10, i); - return { from: f, to: t, power: i }; - }, - - axis: function (x, y, length, from, to, steps, orientation, labels, type, dashsize, paper) { - dashsize = dashsize == null ? 2 : dashsize; - type = type || "t"; - steps = steps || 10; - paper = arguments[arguments.length-1] //paper is always last argument - - var path = type == "|" || type == " " ? ["M", x + .5, y, "l", 0, .001] : orientation == 1 || orientation == 3 ? ["M", x + .5, y, "l", 0, -length] : ["M", x, y + .5, "l", length, 0], - ends = this.snapEnds(from, to, steps), - f = ends.from, - t = ends.to, - i = ends.power, - j = 0, - txtattr = { font: "11px 'Fontin Sans', Fontin-Sans, sans-serif" }, - text = paper.set(), - d; - - d = (t - f) / steps; - - var label = f, - rnd = i > 0 ? i : 0; - dx = length / steps; - - if (+orientation == 1 || +orientation == 3) { - var Y = y, - addon = (orientation - 1 ? 1 : -1) * (dashsize + 3 + !!(orientation - 1)); - - while (Y >= y - length) { - type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), Y + .5, "l", dashsize * 2 + 1, 0])); - text.push(paper.text(x + addon, Y, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr).attr({ "text-anchor": orientation - 1 ? "start" : "end" })); - label += d; - Y -= dx; - } - - if (Math.round(Y + dx - (y - length))) { - type != "-" && type != " " && (path = path.concat(["M", x - (type == "+" || type == "|" ? dashsize : !(orientation - 1) * dashsize * 2), y - length + .5, "l", dashsize * 2 + 1, 0])); - text.push(paper.text(x + addon, y - length, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr).attr({ "text-anchor": orientation - 1 ? "start" : "end" })); - } - } else { - label = f; - rnd = (i > 0) * i; - addon = (orientation ? -1 : 1) * (dashsize + 9 + !orientation); - - var X = x, - dx = length / steps, - txt = 0, - prev = 0; - - while (X <= x + length) { - type != "-" && type != " " && (path = path.concat(["M", X + .5, y - (type == "+" ? dashsize : !!orientation * dashsize * 2), "l", 0, dashsize * 2 + 1])); - text.push(txt = paper.text(X, y + addon, (labels && labels[j++]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr)); - - var bb = txt.getBBox(); - - if (prev >= bb.x - 5) { - text.pop(text.length - 1).remove(); - } else { - prev = bb.x + bb.width; - } - - label += d; - X += dx; - } - - if (Math.round(X - dx - x - length)) { - type != "-" && type != " " && (path = path.concat(["M", x + length + .5, y - (type == "+" ? dashsize : !!orientation * dashsize * 2), "l", 0, dashsize * 2 + 1])); - text.push(paper.text(x + length, y + addon, (labels && labels[j]) || (Math.round(label) == label ? label : +label.toFixed(rnd))).attr(txtattr)); - } - } - - var res = paper.path(path); - - res.text = text; - res.all = paper.set([res, text]); - res.remove = function () { - this.text.remove(); - this.constructor.prototype.remove.call(this); - }; - - return res; - }, - - labelise: function(label, val, total) { - if (label) { - return (label + "").replace(/(##+(?:\.#+)?)|(%%+(?:\.%+)?)/g, function (all, value, percent) { - if (value) { - return (+val).toFixed(value.replace(/^#+\.?/g, "").length); - } - if (percent) { - return (val * 100 / total).toFixed(percent.replace(/^%+\.?/g, "").length) + "%"; - } - }); - } else { - return (+val).toFixed(0); - } - } -} diff --git a/vendor/assets/javascripts/raphael.js b/vendor/assets/javascripts/raphael.js deleted file mode 100644 index 3f3f8a0b7f6..00000000000 --- a/vendor/assets/javascripts/raphael.js +++ /dev/null @@ -1,8239 +0,0 @@ -// ┌────────────────────────────────────────────────────────────────────┐ \\ -// │ Raphaël 2.1.4 - JavaScript Vector Library │ \\ -// ├────────────────────────────────────────────────────────────────────┤ \\ -// │ Copyright © 2008-2012 Dmitry Baranovskiy (http://raphaeljs.com) │ \\ -// │ Copyright © 2008-2012 Sencha Labs (http://sencha.com) │ \\ -// ├────────────────────────────────────────────────────────────────────┤ \\ -// │ Licensed under the MIT (http://raphaeljs.com/license.html) license.│ \\ -// └────────────────────────────────────────────────────────────────────┘ \\ -// Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ┌────────────────────────────────────────────────────────────┐ \\ -// │ Eve 0.4.2 - JavaScript Events Library │ \\ -// ├────────────────────────────────────────────────────────────┤ \\ -// │ Author Dmitry Baranovskiy (http://dmitry.baranovskiy.com/) │ \\ -// └────────────────────────────────────────────────────────────┘ \\ - -(function (glob) { - var version = "0.4.2", - has = "hasOwnProperty", - separator = /[\.\/]/, - wildcard = "*", - fun = function () {}, - numsort = function (a, b) { - return a - b; - }, - current_event, - stop, - events = {n: {}}, - /*\ - * eve - [ method ] - - * Fires event with given `name`, given scope and other parameters. - - > Arguments - - - name (string) name of the *event*, dot (`.`) or slash (`/`) separated - - scope (object) context for the event handlers - - varargs (...) the rest of arguments will be sent to event handlers - - = (object) array of returned values from the listeners - \*/ - eve = function (name, scope) { - name = String(name); - var e = events, - oldstop = stop, - args = Array.prototype.slice.call(arguments, 2), - listeners = eve.listeners(name), - z = 0, - f = false, - l, - indexed = [], - queue = {}, - out = [], - ce = current_event, - errors = []; - current_event = name; - stop = 0; - for (var i = 0, ii = listeners.length; i < ii; i++) if ("zIndex" in listeners[i]) { - indexed.push(listeners[i].zIndex); - if (listeners[i].zIndex < 0) { - queue[listeners[i].zIndex] = listeners[i]; - } - } - indexed.sort(numsort); - while (indexed[z] < 0) { - l = queue[indexed[z++]]; - out.push(l.apply(scope, args)); - if (stop) { - stop = oldstop; - return out; - } - } - for (i = 0; i < ii; i++) { - l = listeners[i]; - if ("zIndex" in l) { - if (l.zIndex == indexed[z]) { - out.push(l.apply(scope, args)); - if (stop) { - break; - } - do { - z++; - l = queue[indexed[z]]; - l && out.push(l.apply(scope, args)); - if (stop) { - break; - } - } while (l) - } else { - queue[l.zIndex] = l; - } - } else { - out.push(l.apply(scope, args)); - if (stop) { - break; - } - } - } - stop = oldstop; - current_event = ce; - return out.length ? out : null; - }; - // Undocumented. Debug only. - eve._events = events; - /*\ - * eve.listeners - [ method ] - - * Internal method which gives you array of all event handlers that will be triggered by the given `name`. - - > Arguments - - - name (string) name of the event, dot (`.`) or slash (`/`) separated - - = (array) array of event handlers - \*/ - eve.listeners = function (name) { - var names = name.split(separator), - e = events, - item, - items, - k, - i, - ii, - j, - jj, - nes, - es = [e], - out = []; - for (i = 0, ii = names.length; i < ii; i++) { - nes = []; - for (j = 0, jj = es.length; j < jj; j++) { - e = es[j].n; - items = [e[names[i]], e[wildcard]]; - k = 2; - while (k--) { - item = items[k]; - if (item) { - nes.push(item); - out = out.concat(item.f || []); - } - } - } - es = nes; - } - return out; - }; - - /*\ - * eve.on - [ method ] - ** - * Binds given event handler with a given name. You can use wildcards “`*`” for the names: - | eve.on("*.under.*", f); - | eve("mouse.under.floor"); // triggers f - * Use @eve to trigger the listener. - ** - > Arguments - ** - - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards - - f (function) event handler function - ** - = (function) returned function accepts a single numeric parameter that represents z-index of the handler. It is an optional feature and only used when you need to ensure that some subset of handlers will be invoked in a given order, despite of the order of assignment. - > Example: - | eve.on("mouse", eatIt)(2); - | eve.on("mouse", scream); - | eve.on("mouse", catchIt)(1); - * This will ensure that `catchIt()` function will be called before `eatIt()`. - * - * If you want to put your handler before non-indexed handlers, specify a negative value. - * Note: I assume most of the time you don’t need to worry about z-index, but it’s nice to have this feature “just in case”. - \*/ - eve.on = function (name, f) { - name = String(name); - if (typeof f != "function") { - return function () {}; - } - var names = name.split(separator), - e = events; - for (var i = 0, ii = names.length; i < ii; i++) { - e = e.n; - e = e.hasOwnProperty(names[i]) && e[names[i]] || (e[names[i]] = {n: {}}); - } - e.f = e.f || []; - for (i = 0, ii = e.f.length; i < ii; i++) if (e.f[i] == f) { - return fun; - } - e.f.push(f); - return function (zIndex) { - if (+zIndex == +zIndex) { - f.zIndex = +zIndex; - } - }; - }; - /*\ - * eve.f - [ method ] - ** - * Returns function that will fire given event with optional arguments. - * Arguments that will be passed to the result function will be also - * concated to the list of final arguments. - | el.onclick = eve.f("click", 1, 2); - | eve.on("click", function (a, b, c) { - | console.log(a, b, c); // 1, 2, [event object] - | }); - > Arguments - - event (string) event name - - varargs (…) and any other arguments - = (function) possible event handler function - \*/ - eve.f = function (event) { - var attrs = [].slice.call(arguments, 1); - return function () { - eve.apply(null, [event, null].concat(attrs).concat([].slice.call(arguments, 0))); - }; - }; - /*\ - * eve.stop - [ method ] - ** - * Is used inside an event handler to stop the event, preventing any subsequent listeners from firing. - \*/ - eve.stop = function () { - stop = 1; - }; - /*\ - * eve.nt - [ method ] - ** - * Could be used inside event handler to figure out actual name of the event. - ** - > Arguments - ** - - subname (string) #optional subname of the event - ** - = (string) name of the event, if `subname` is not specified - * or - = (boolean) `true`, if current event’s name contains `subname` - \*/ - eve.nt = function (subname) { - if (subname) { - return new RegExp("(?:\\.|\\/|^)" + subname + "(?:\\.|\\/|$)").test(current_event); - } - return current_event; - }; - /*\ - * eve.nts - [ method ] - ** - * Could be used inside event handler to figure out actual name of the event. - ** - ** - = (array) names of the event - \*/ - eve.nts = function () { - return current_event.split(separator); - }; - /*\ - * eve.off - [ method ] - ** - * Removes given function from the list of event listeners assigned to given name. - * If no arguments specified all the events will be cleared. - ** - > Arguments - ** - - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards - - f (function) event handler function - \*/ - /*\ - * eve.unbind - [ method ] - ** - * See @eve.off - \*/ - eve.off = eve.unbind = function (name, f) { - if (!name) { - eve._events = events = {n: {}}; - return; - } - var names = name.split(separator), - e, - key, - splice, - i, ii, j, jj, - cur = [events]; - for (i = 0, ii = names.length; i < ii; i++) { - for (j = 0; j < cur.length; j += splice.length - 2) { - splice = [j, 1]; - e = cur[j].n; - if (names[i] != wildcard) { - if (e[names[i]]) { - splice.push(e[names[i]]); - } - } else { - for (key in e) if (e[has](key)) { - splice.push(e[key]); - } - } - cur.splice.apply(cur, splice); - } - } - for (i = 0, ii = cur.length; i < ii; i++) { - e = cur[i]; - while (e.n) { - if (f) { - if (e.f) { - for (j = 0, jj = e.f.length; j < jj; j++) if (e.f[j] == f) { - e.f.splice(j, 1); - break; - } - !e.f.length && delete e.f; - } - for (key in e.n) if (e.n[has](key) && e.n[key].f) { - var funcs = e.n[key].f; - for (j = 0, jj = funcs.length; j < jj; j++) if (funcs[j] == f) { - funcs.splice(j, 1); - break; - } - !funcs.length && delete e.n[key].f; - } - } else { - delete e.f; - for (key in e.n) if (e.n[has](key) && e.n[key].f) { - delete e.n[key].f; - } - } - e = e.n; - } - } - }; - /*\ - * eve.once - [ method ] - ** - * Binds given event handler with a given name to only run once then unbind itself. - | eve.once("login", f); - | eve("login"); // triggers f - | eve("login"); // no listeners - * Use @eve to trigger the listener. - ** - > Arguments - ** - - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards - - f (function) event handler function - ** - = (function) same return function as @eve.on - \*/ - eve.once = function (name, f) { - var f2 = function () { - eve.unbind(name, f2); - return f.apply(this, arguments); - }; - return eve.on(name, f2); - }; - /*\ - * eve.version - [ property (string) ] - ** - * Current version of the library. - \*/ - eve.version = version; - eve.toString = function () { - return "You are running Eve " + version; - }; - (typeof module != "undefined" && module.exports) ? (module.exports = eve) : (typeof define != "undefined" ? (define("eve", [], function() { return eve; })) : (glob.eve = eve)); -})(window || this); -// ┌─────────────────────────────────────────────────────────────────────┐ \\ -// │ "Raphaël 2.1.2" - JavaScript Vector Library │ \\ -// ├─────────────────────────────────────────────────────────────────────┤ \\ -// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com) │ \\ -// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com) │ \\ -// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\ -// └─────────────────────────────────────────────────────────────────────┘ \\ - -(function (glob, factory) { - // AMD support - if (typeof define === "function" && define.amd) { - // Define as an anonymous module - define(["eve"], function( eve ) { - return factory(glob, eve); - }); - } else { - // Browser globals (glob is window) - // Raphael adds itself to window - factory(glob, glob.eve || (typeof require == "function" && require('eve')) ); - } -}(this, function (window, eve) { - /*\ - * Raphael - [ method ] - ** - * Creates a canvas object on which to draw. - * You must do this first, as all future calls to drawing methods - * from this instance will be bound to this canvas. - > Parameters - ** - - container (HTMLElement|string) DOM element or its ID which is going to be a parent for drawing surface - - width (number) - - height (number) - - callback (function) #optional callback function which is going to be executed in the context of newly created paper - * or - - x (number) - - y (number) - - width (number) - - height (number) - - callback (function) #optional callback function which is going to be executed in the context of newly created paper - * or - - all (array) (first 3 or 4 elements in the array are equal to [containerID, width, height] or [x, y, width, height]. The rest are element descriptions in format {type: type, <attributes>}). See @Paper.add. - - callback (function) #optional callback function which is going to be executed in the context of newly created paper - * or - - onReadyCallback (function) function that is going to be called on DOM ready event. You can also subscribe to this event via Eve’s “DOMLoad” event. In this case method returns `undefined`. - = (object) @Paper - > Usage - | // Each of the following examples create a canvas - | // that is 320px wide by 200px high. - | // Canvas is created at the viewport’s 10,50 coordinate. - | var paper = Raphael(10, 50, 320, 200); - | // Canvas is created at the top left corner of the #notepad element - | // (or its top right corner in dir="rtl" elements) - | var paper = Raphael(document.getElementById("notepad"), 320, 200); - | // Same as above - | var paper = Raphael("notepad", 320, 200); - | // Image dump - | var set = Raphael(["notepad", 320, 200, { - | type: "rect", - | x: 10, - | y: 10, - | width: 25, - | height: 25, - | stroke: "#f00" - | }, { - | type: "text", - | x: 30, - | y: 40, - | text: "Dump" - | }]); - \*/ - function R(first) { - if (R.is(first, "function")) { - return loaded ? first() : eve.on("raphael.DOMload", first); - } else if (R.is(first, array)) { - return R._engine.create[apply](R, first.splice(0, 3 + R.is(first[0], nu))).add(first); - } else { - var args = Array.prototype.slice.call(arguments, 0); - if (R.is(args[args.length - 1], "function")) { - var f = args.pop(); - return loaded ? f.call(R._engine.create[apply](R, args)) : eve.on("raphael.DOMload", function () { - f.call(R._engine.create[apply](R, args)); - }); - } else { - return R._engine.create[apply](R, arguments); - } - } - } - R.version = "2.1.2"; - R.eve = eve; - var loaded, - separator = /[, ]+/, - elements = {circle: 1, rect: 1, path: 1, ellipse: 1, text: 1, image: 1}, - formatrg = /\{(\d+)\}/g, - proto = "prototype", - has = "hasOwnProperty", - g = { - doc: document, - win: window - }, - oldRaphael = { - was: Object.prototype[has].call(g.win, "Raphael"), - is: g.win.Raphael - }, - Paper = function () { - /*\ - * Paper.ca - [ property (object) ] - ** - * Shortcut for @Paper.customAttributes - \*/ - /*\ - * Paper.customAttributes - [ property (object) ] - ** - * If you have a set of attributes that you would like to represent - * as a function of some number you can do it easily with custom attributes: - > Usage - | paper.customAttributes.hue = function (num) { - | num = num % 1; - | return {fill: "hsb(" + num + ", 0.75, 1)"}; - | }; - | // Custom attribute “hue” will change fill - | // to be given hue with fixed saturation and brightness. - | // Now you can use it like this: - | var c = paper.circle(10, 10, 10).attr({hue: .45}); - | // or even like this: - | c.animate({hue: 1}, 1e3); - | - | // You could also create custom attribute - | // with multiple parameters: - | paper.customAttributes.hsb = function (h, s, b) { - | return {fill: "hsb(" + [h, s, b].join(",") + ")"}; - | }; - | c.attr({hsb: "0.5 .8 1"}); - | c.animate({hsb: [1, 0, 0.5]}, 1e3); - \*/ - this.ca = this.customAttributes = {}; - }, - paperproto, - appendChild = "appendChild", - apply = "apply", - concat = "concat", - supportsTouch = ('ontouchstart' in g.win) || g.win.DocumentTouch && g.doc instanceof DocumentTouch, //taken from Modernizr touch test - E = "", - S = " ", - Str = String, - split = "split", - events = "click dblclick mousedown mousemove mouseout mouseover mouseup touchstart touchmove touchend touchcancel"[split](S), - touchMap = { - mousedown: "touchstart", - mousemove: "touchmove", - mouseup: "touchend" - }, - lowerCase = Str.prototype.toLowerCase, - math = Math, - mmax = math.max, - mmin = math.min, - abs = math.abs, - pow = math.pow, - PI = math.PI, - nu = "number", - string = "string", - array = "array", - toString = "toString", - fillString = "fill", - objectToString = Object.prototype.toString, - paper = {}, - push = "push", - ISURL = R._ISURL = /^url\(['"]?(.+?)['"]?\)$/i, - colourRegExp = /^\s*((#[a-f\d]{6})|(#[a-f\d]{3})|rgba?\(\s*([\d\.]+%?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+%?(?:\s*,\s*[\d\.]+%?)?)\s*\)|hsba?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?)%?\s*\)|hsla?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?)%?\s*\))\s*$/i, - isnan = {"NaN": 1, "Infinity": 1, "-Infinity": 1}, - bezierrg = /^(?:cubic-)?bezier\(([^,]+),([^,]+),([^,]+),([^\)]+)\)/, - round = math.round, - setAttribute = "setAttribute", - toFloat = parseFloat, - toInt = parseInt, - upperCase = Str.prototype.toUpperCase, - availableAttrs = R._availableAttrs = { - "arrow-end": "none", - "arrow-start": "none", - blur: 0, - "clip-rect": "0 0 1e9 1e9", - cursor: "default", - cx: 0, - cy: 0, - fill: "#fff", - "fill-opacity": 1, - font: '10px "Arial"', - "font-family": '"Arial"', - "font-size": "10", - "font-style": "normal", - "font-weight": 400, - gradient: 0, - height: 0, - href: "http://raphaeljs.com/", - "letter-spacing": 0, - opacity: 1, - path: "M0,0", - r: 0, - rx: 0, - ry: 0, - src: "", - stroke: "#000", - "stroke-dasharray": "", - "stroke-linecap": "butt", - "stroke-linejoin": "butt", - "stroke-miterlimit": 0, - "stroke-opacity": 1, - "stroke-width": 1, - target: "_blank", - "text-anchor": "middle", - title: "Raphael", - transform: "", - width: 0, - x: 0, - y: 0 - }, - availableAnimAttrs = R._availableAnimAttrs = { - blur: nu, - "clip-rect": "csv", - cx: nu, - cy: nu, - fill: "colour", - "fill-opacity": nu, - "font-size": nu, - height: nu, - opacity: nu, - path: "path", - r: nu, - rx: nu, - ry: nu, - stroke: "colour", - "stroke-opacity": nu, - "stroke-width": nu, - transform: "transform", - width: nu, - x: nu, - y: nu - }, - whitespace = /[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]/g, - commaSpaces = /[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*/, - hsrg = {hs: 1, rg: 1}, - p2s = /,?([achlmqrstvxz]),?/gi, - pathCommand = /([achlmrqstvz])[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*)+)/ig, - tCommand = /([rstm])[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*)+)/ig, - pathValues = /(-?\d*\.?\d*(?:e[\-+]?\d+)?)[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*/ig, - radial_gradient = R._radial_gradient = /^r(?:\(([^,]+?)[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*([^\)]+?)\))?/, - eldata = {}, - sortByKey = function (a, b) { - return a.key - b.key; - }, - sortByNumber = function (a, b) { - return toFloat(a) - toFloat(b); - }, - fun = function () {}, - pipe = function (x) { - return x; - }, - rectPath = R._rectPath = function (x, y, w, h, r) { - if (r) { - return [["M", x + r, y], ["l", w - r * 2, 0], ["a", r, r, 0, 0, 1, r, r], ["l", 0, h - r * 2], ["a", r, r, 0, 0, 1, -r, r], ["l", r * 2 - w, 0], ["a", r, r, 0, 0, 1, -r, -r], ["l", 0, r * 2 - h], ["a", r, r, 0, 0, 1, r, -r], ["z"]]; - } - return [["M", x, y], ["l", w, 0], ["l", 0, h], ["l", -w, 0], ["z"]]; - }, - ellipsePath = function (x, y, rx, ry) { - if (ry == null) { - ry = rx; - } - return [["M", x, y], ["m", 0, -ry], ["a", rx, ry, 0, 1, 1, 0, 2 * ry], ["a", rx, ry, 0, 1, 1, 0, -2 * ry], ["z"]]; - }, - getPath = R._getPath = { - path: function (el) { - return el.attr("path"); - }, - circle: function (el) { - var a = el.attrs; - return ellipsePath(a.cx, a.cy, a.r); - }, - ellipse: function (el) { - var a = el.attrs; - return ellipsePath(a.cx, a.cy, a.rx, a.ry); - }, - rect: function (el) { - var a = el.attrs; - return rectPath(a.x, a.y, a.width, a.height, a.r); - }, - image: function (el) { - var a = el.attrs; - return rectPath(a.x, a.y, a.width, a.height); - }, - text: function (el) { - var bbox = el._getBBox(); - return rectPath(bbox.x, bbox.y, bbox.width, bbox.height); - }, - set : function(el) { - var bbox = el._getBBox(); - return rectPath(bbox.x, bbox.y, bbox.width, bbox.height); - } - }, - /*\ - * Raphael.mapPath - [ method ] - ** - * Transform the path string with given matrix. - > Parameters - - path (string) path string - - matrix (object) see @Matrix - = (string) transformed path string - \*/ - mapPath = R.mapPath = function (path, matrix) { - if (!matrix) { - return path; - } - var x, y, i, j, ii, jj, pathi; - path = path2curve(path); - for (i = 0, ii = path.length; i < ii; i++) { - pathi = path[i]; - for (j = 1, jj = pathi.length; j < jj; j += 2) { - x = matrix.x(pathi[j], pathi[j + 1]); - y = matrix.y(pathi[j], pathi[j + 1]); - pathi[j] = x; - pathi[j + 1] = y; - } - } - return path; - }; - - R._g = g; - /*\ - * Raphael.type - [ property (string) ] - ** - * Can be “SVG”, “VML” or empty, depending on browser support. - \*/ - R.type = (g.win.SVGAngle || g.doc.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1") ? "SVG" : "VML"); - if (R.type == "VML") { - var d = g.doc.createElement("div"), - b; - d.innerHTML = '<v:shape adj="1"/>'; - b = d.firstChild; - b.style.behavior = "url(#default#VML)"; - if (!(b && typeof b.adj == "object")) { - return (R.type = E); - } - d = null; - } - /*\ - * Raphael.svg - [ property (boolean) ] - ** - * `true` if browser supports SVG. - \*/ - /*\ - * Raphael.vml - [ property (boolean) ] - ** - * `true` if browser supports VML. - \*/ - R.svg = !(R.vml = R.type == "VML"); - R._Paper = Paper; - /*\ - * Raphael.fn - [ property (object) ] - ** - * You can add your own method to the canvas. For example if you want to draw a pie chart, - * you can create your own pie chart function and ship it as a Raphaël plugin. To do this - * you need to extend the `Raphael.fn` object. You should modify the `fn` object before a - * Raphaël instance is created, otherwise it will take no effect. Please note that the - * ability for namespaced plugins was removed in Raphael 2.0. It is up to the plugin to - * ensure any namespacing ensures proper context. - > Usage - | Raphael.fn.arrow = function (x1, y1, x2, y2, size) { - | return this.path( ... ); - | }; - | // or create namespace - | Raphael.fn.mystuff = { - | arrow: function () {…}, - | star: function () {…}, - | // etc… - | }; - | var paper = Raphael(10, 10, 630, 480); - | // then use it - | paper.arrow(10, 10, 30, 30, 5).attr({fill: "#f00"}); - | paper.mystuff.arrow(); - | paper.mystuff.star(); - \*/ - R.fn = paperproto = Paper.prototype = R.prototype; - R._id = 0; - R._oid = 0; - /*\ - * Raphael.is - [ method ] - ** - * Handful of replacements for `typeof` operator. - > Parameters - - o (…) any object or primitive - - type (string) name of the type, i.e. “string”, “function”, “number”, etc. - = (boolean) is given value is of given type - \*/ - R.is = function (o, type) { - type = lowerCase.call(type); - if (type == "finite") { - return !isnan[has](+o); - } - if (type == "array") { - return o instanceof Array; - } - return (type == "null" && o === null) || - (type == typeof o && o !== null) || - (type == "object" && o === Object(o)) || - (type == "array" && Array.isArray && Array.isArray(o)) || - objectToString.call(o).slice(8, -1).toLowerCase() == type; - }; - - function clone(obj) { - if (typeof obj == "function" || Object(obj) !== obj) { - return obj; - } - var res = new obj.constructor; - for (var key in obj) if (obj[has](key)) { - res[key] = clone(obj[key]); - } - return res; - } - - /*\ - * Raphael.angle - [ method ] - ** - * Returns angle between two or three points - > Parameters - - x1 (number) x coord of first point - - y1 (number) y coord of first point - - x2 (number) x coord of second point - - y2 (number) y coord of second point - - x3 (number) #optional x coord of third point - - y3 (number) #optional y coord of third point - = (number) angle in degrees. - \*/ - R.angle = function (x1, y1, x2, y2, x3, y3) { - if (x3 == null) { - var x = x1 - x2, - y = y1 - y2; - if (!x && !y) { - return 0; - } - return (180 + math.atan2(-y, -x) * 180 / PI + 360) % 360; - } else { - return R.angle(x1, y1, x3, y3) - R.angle(x2, y2, x3, y3); - } - }; - /*\ - * Raphael.rad - [ method ] - ** - * Transform angle to radians - > Parameters - - deg (number) angle in degrees - = (number) angle in radians. - \*/ - R.rad = function (deg) { - return deg % 360 * PI / 180; - }; - /*\ - * Raphael.deg - [ method ] - ** - * Transform angle to degrees - > Parameters - - rad (number) angle in radians - = (number) angle in degrees. - \*/ - R.deg = function (rad) { - return Math.round ((rad * 180 / PI% 360)* 1000) / 1000; - }; - /*\ - * Raphael.snapTo - [ method ] - ** - * Snaps given value to given grid. - > Parameters - - values (array|number) given array of values or step of the grid - - value (number) value to adjust - - tolerance (number) #optional tolerance for snapping. Default is `10`. - = (number) adjusted value. - \*/ - R.snapTo = function (values, value, tolerance) { - tolerance = R.is(tolerance, "finite") ? tolerance : 10; - if (R.is(values, array)) { - var i = values.length; - while (i--) if (abs(values[i] - value) <= tolerance) { - return values[i]; - } - } else { - values = +values; - var rem = value % values; - if (rem < tolerance) { - return value - rem; - } - if (rem > values - tolerance) { - return value - rem + values; - } - } - return value; - }; - - /*\ - * Raphael.createUUID - [ method ] - ** - * Returns RFC4122, version 4 ID - \*/ - var createUUID = R.createUUID = (function (uuidRegEx, uuidReplacer) { - return function () { - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(uuidRegEx, uuidReplacer).toUpperCase(); - }; - })(/[xy]/g, function (c) { - var r = math.random() * 16 | 0, - v = c == "x" ? r : (r & 3 | 8); - return v.toString(16); - }); - - /*\ - * Raphael.setWindow - [ method ] - ** - * Used when you need to draw in `<iframe>`. Switched window to the iframe one. - > Parameters - - newwin (window) new window object - \*/ - R.setWindow = function (newwin) { - eve("raphael.setWindow", R, g.win, newwin); - g.win = newwin; - g.doc = g.win.document; - if (R._engine.initWin) { - R._engine.initWin(g.win); - } - }; - var toHex = function (color) { - if (R.vml) { - // http://dean.edwards.name/weblog/2009/10/convert-any-colour-value-to-hex-in-msie/ - var trim = /^\s+|\s+$/g; - var bod; - try { - var docum = new ActiveXObject("htmlfile"); - docum.write("<body>"); - docum.close(); - bod = docum.body; - } catch(e) { - bod = createPopup().document.body; - } - var range = bod.createTextRange(); - toHex = cacher(function (color) { - try { - bod.style.color = Str(color).replace(trim, E); - var value = range.queryCommandValue("ForeColor"); - value = ((value & 255) << 16) | (value & 65280) | ((value & 16711680) >>> 16); - return "#" + ("000000" + value.toString(16)).slice(-6); - } catch(e) { - return "none"; - } - }); - } else { - var i = g.doc.createElement("i"); - i.title = "Rapha\xebl Colour Picker"; - i.style.display = "none"; - g.doc.body.appendChild(i); - toHex = cacher(function (color) { - i.style.color = color; - return g.doc.defaultView.getComputedStyle(i, E).getPropertyValue("color"); - }); - } - return toHex(color); - }, - hsbtoString = function () { - return "hsb(" + [this.h, this.s, this.b] + ")"; - }, - hsltoString = function () { - return "hsl(" + [this.h, this.s, this.l] + ")"; - }, - rgbtoString = function () { - return this.hex; - }, - prepareRGB = function (r, g, b) { - if (g == null && R.is(r, "object") && "r" in r && "g" in r && "b" in r) { - b = r.b; - g = r.g; - r = r.r; - } - if (g == null && R.is(r, string)) { - var clr = R.getRGB(r); - r = clr.r; - g = clr.g; - b = clr.b; - } - if (r > 1 || g > 1 || b > 1) { - r /= 255; - g /= 255; - b /= 255; - } - - return [r, g, b]; - }, - packageRGB = function (r, g, b, o) { - r *= 255; - g *= 255; - b *= 255; - var rgb = { - r: r, - g: g, - b: b, - hex: R.rgb(r, g, b), - toString: rgbtoString - }; - R.is(o, "finite") && (rgb.opacity = o); - return rgb; - }; - - /*\ - * Raphael.color - [ method ] - ** - * Parses the color string and returns object with all values for the given color. - > Parameters - - clr (string) color string in one of the supported formats (see @Raphael.getRGB) - = (object) Combined RGB & HSB object in format: - o { - o r (number) red, - o g (number) green, - o b (number) blue, - o hex (string) color in HTML/CSS format: #••••••, - o error (boolean) `true` if string can’t be parsed, - o h (number) hue, - o s (number) saturation, - o v (number) value (brightness), - o l (number) lightness - o } - \*/ - R.color = function (clr) { - var rgb; - if (R.is(clr, "object") && "h" in clr && "s" in clr && "b" in clr) { - rgb = R.hsb2rgb(clr); - clr.r = rgb.r; - clr.g = rgb.g; - clr.b = rgb.b; - clr.hex = rgb.hex; - } else if (R.is(clr, "object") && "h" in clr && "s" in clr && "l" in clr) { - rgb = R.hsl2rgb(clr); - clr.r = rgb.r; - clr.g = rgb.g; - clr.b = rgb.b; - clr.hex = rgb.hex; - } else { - if (R.is(clr, "string")) { - clr = R.getRGB(clr); - } - if (R.is(clr, "object") && "r" in clr && "g" in clr && "b" in clr) { - rgb = R.rgb2hsl(clr); - clr.h = rgb.h; - clr.s = rgb.s; - clr.l = rgb.l; - rgb = R.rgb2hsb(clr); - clr.v = rgb.b; - } else { - clr = {hex: "none"}; - clr.r = clr.g = clr.b = clr.h = clr.s = clr.v = clr.l = -1; - } - } - clr.toString = rgbtoString; - return clr; - }; - /*\ - * Raphael.hsb2rgb - [ method ] - ** - * Converts HSB values to RGB object. - > Parameters - - h (number) hue - - s (number) saturation - - v (number) value or brightness - = (object) RGB object in format: - o { - o r (number) red, - o g (number) green, - o b (number) blue, - o hex (string) color in HTML/CSS format: #•••••• - o } - \*/ - R.hsb2rgb = function (h, s, v, o) { - if (this.is(h, "object") && "h" in h && "s" in h && "b" in h) { - v = h.b; - s = h.s; - o = h.o; - h = h.h; - } - h *= 360; - var R, G, B, X, C; - h = (h % 360) / 60; - C = v * s; - X = C * (1 - abs(h % 2 - 1)); - R = G = B = v - C; - - h = ~~h; - R += [C, X, 0, 0, X, C][h]; - G += [X, C, C, X, 0, 0][h]; - B += [0, 0, X, C, C, X][h]; - return packageRGB(R, G, B, o); - }; - /*\ - * Raphael.hsl2rgb - [ method ] - ** - * Converts HSL values to RGB object. - > Parameters - - h (number) hue - - s (number) saturation - - l (number) luminosity - = (object) RGB object in format: - o { - o r (number) red, - o g (number) green, - o b (number) blue, - o hex (string) color in HTML/CSS format: #•••••• - o } - \*/ - R.hsl2rgb = function (h, s, l, o) { - if (this.is(h, "object") && "h" in h && "s" in h && "l" in h) { - l = h.l; - s = h.s; - h = h.h; - } - if (h > 1 || s > 1 || l > 1) { - h /= 360; - s /= 100; - l /= 100; - } - h *= 360; - var R, G, B, X, C; - h = (h % 360) / 60; - C = 2 * s * (l < .5 ? l : 1 - l); - X = C * (1 - abs(h % 2 - 1)); - R = G = B = l - C / 2; - - h = ~~h; - R += [C, X, 0, 0, X, C][h]; - G += [X, C, C, X, 0, 0][h]; - B += [0, 0, X, C, C, X][h]; - return packageRGB(R, G, B, o); - }; - /*\ - * Raphael.rgb2hsb - [ method ] - ** - * Converts RGB values to HSB object. - > Parameters - - r (number) red - - g (number) green - - b (number) blue - = (object) HSB object in format: - o { - o h (number) hue - o s (number) saturation - o b (number) brightness - o } - \*/ - R.rgb2hsb = function (r, g, b) { - b = prepareRGB(r, g, b); - r = b[0]; - g = b[1]; - b = b[2]; - - var H, S, V, C; - V = mmax(r, g, b); - C = V - mmin(r, g, b); - H = (C == 0 ? null : - V == r ? (g - b) / C : - V == g ? (b - r) / C + 2 : - (r - g) / C + 4 - ); - H = ((H + 360) % 6) * 60 / 360; - S = C == 0 ? 0 : C / V; - return {h: H, s: S, b: V, toString: hsbtoString}; - }; - /*\ - * Raphael.rgb2hsl - [ method ] - ** - * Converts RGB values to HSL object. - > Parameters - - r (number) red - - g (number) green - - b (number) blue - = (object) HSL object in format: - o { - o h (number) hue - o s (number) saturation - o l (number) luminosity - o } - \*/ - R.rgb2hsl = function (r, g, b) { - b = prepareRGB(r, g, b); - r = b[0]; - g = b[1]; - b = b[2]; - - var H, S, L, M, m, C; - M = mmax(r, g, b); - m = mmin(r, g, b); - C = M - m; - H = (C == 0 ? null : - M == r ? (g - b) / C : - M == g ? (b - r) / C + 2 : - (r - g) / C + 4); - H = ((H + 360) % 6) * 60 / 360; - L = (M + m) / 2; - S = (C == 0 ? 0 : - L < .5 ? C / (2 * L) : - C / (2 - 2 * L)); - return {h: H, s: S, l: L, toString: hsltoString}; - }; - R._path2string = function () { - return this.join(",").replace(p2s, "$1"); - }; - function repush(array, item) { - for (var i = 0, ii = array.length; i < ii; i++) if (array[i] === item) { - return array.push(array.splice(i, 1)[0]); - } - } - function cacher(f, scope, postprocessor) { - function newf() { - var arg = Array.prototype.slice.call(arguments, 0), - args = arg.join("\u2400"), - cache = newf.cache = newf.cache || {}, - count = newf.count = newf.count || []; - if (cache[has](args)) { - repush(count, args); - return postprocessor ? postprocessor(cache[args]) : cache[args]; - } - count.length >= 1e3 && delete cache[count.shift()]; - count.push(args); - cache[args] = f[apply](scope, arg); - return postprocessor ? postprocessor(cache[args]) : cache[args]; - } - return newf; - } - - var preload = R._preload = function (src, f) { - var img = g.doc.createElement("img"); - img.style.cssText = "position:absolute;left:-9999em;top:-9999em"; - img.onload = function () { - f.call(this); - this.onload = null; - g.doc.body.removeChild(this); - }; - img.onerror = function () { - g.doc.body.removeChild(this); - }; - g.doc.body.appendChild(img); - img.src = src; - }; - - function clrToString() { - return this.hex; - } - - /*\ - * Raphael.getRGB - [ method ] - ** - * Parses colour string as RGB object - > Parameters - - colour (string) colour string in one of formats: - # <ul> - # <li>Colour name (“<code>red</code>”, “<code>green</code>”, “<code>cornflowerblue</code>”, etc)</li> - # <li>#••• — shortened HTML colour: (“<code>#000</code>”, “<code>#fc0</code>”, etc)</li> - # <li>#•••••• — full length HTML colour: (“<code>#000000</code>”, “<code>#bd2300</code>”)</li> - # <li>rgb(•••, •••, •••) — red, green and blue channels’ values: (“<code>rgb(200, 100, 0)</code>”)</li> - # <li>rgb(•••%, •••%, •••%) — same as above, but in %: (“<code>rgb(100%, 175%, 0%)</code>”)</li> - # <li>hsb(•••, •••, •••) — hue, saturation and brightness values: (“<code>hsb(0.5, 0.25, 1)</code>”)</li> - # <li>hsb(•••%, •••%, •••%) — same as above, but in %</li> - # <li>hsl(•••, •••, •••) — same as hsb</li> - # <li>hsl(•••%, •••%, •••%) — same as hsb</li> - # </ul> - = (object) RGB object in format: - o { - o r (number) red, - o g (number) green, - o b (number) blue - o hex (string) color in HTML/CSS format: #••••••, - o error (boolean) true if string can’t be parsed - o } - \*/ - R.getRGB = cacher(function (colour) { - if (!colour || !!((colour = Str(colour)).indexOf("-") + 1)) { - return {r: -1, g: -1, b: -1, hex: "none", error: 1, toString: clrToString}; - } - if (colour == "none") { - return {r: -1, g: -1, b: -1, hex: "none", toString: clrToString}; - } - !(hsrg[has](colour.toLowerCase().substring(0, 2)) || colour.charAt() == "#") && (colour = toHex(colour)); - var res, - red, - green, - blue, - opacity, - t, - values, - rgb = colour.match(colourRegExp); - if (rgb) { - if (rgb[2]) { - blue = toInt(rgb[2].substring(5), 16); - green = toInt(rgb[2].substring(3, 5), 16); - red = toInt(rgb[2].substring(1, 3), 16); - } - if (rgb[3]) { - blue = toInt((t = rgb[3].charAt(3)) + t, 16); - green = toInt((t = rgb[3].charAt(2)) + t, 16); - red = toInt((t = rgb[3].charAt(1)) + t, 16); - } - if (rgb[4]) { - values = rgb[4][split](commaSpaces); - red = toFloat(values[0]); - values[0].slice(-1) == "%" && (red *= 2.55); - green = toFloat(values[1]); - values[1].slice(-1) == "%" && (green *= 2.55); - blue = toFloat(values[2]); - values[2].slice(-1) == "%" && (blue *= 2.55); - rgb[1].toLowerCase().slice(0, 4) == "rgba" && (opacity = toFloat(values[3])); - values[3] && values[3].slice(-1) == "%" && (opacity /= 100); - } - if (rgb[5]) { - values = rgb[5][split](commaSpaces); - red = toFloat(values[0]); - values[0].slice(-1) == "%" && (red *= 2.55); - green = toFloat(values[1]); - values[1].slice(-1) == "%" && (green *= 2.55); - blue = toFloat(values[2]); - values[2].slice(-1) == "%" && (blue *= 2.55); - (values[0].slice(-3) == "deg" || values[0].slice(-1) == "\xb0") && (red /= 360); - rgb[1].toLowerCase().slice(0, 4) == "hsba" && (opacity = toFloat(values[3])); - values[3] && values[3].slice(-1) == "%" && (opacity /= 100); - return R.hsb2rgb(red, green, blue, opacity); - } - if (rgb[6]) { - values = rgb[6][split](commaSpaces); - red = toFloat(values[0]); - values[0].slice(-1) == "%" && (red *= 2.55); - green = toFloat(values[1]); - values[1].slice(-1) == "%" && (green *= 2.55); - blue = toFloat(values[2]); - values[2].slice(-1) == "%" && (blue *= 2.55); - (values[0].slice(-3) == "deg" || values[0].slice(-1) == "\xb0") && (red /= 360); - rgb[1].toLowerCase().slice(0, 4) == "hsla" && (opacity = toFloat(values[3])); - values[3] && values[3].slice(-1) == "%" && (opacity /= 100); - return R.hsl2rgb(red, green, blue, opacity); - } - rgb = {r: red, g: green, b: blue, toString: clrToString}; - rgb.hex = "#" + (16777216 | blue | (green << 8) | (red << 16)).toString(16).slice(1); - R.is(opacity, "finite") && (rgb.opacity = opacity); - return rgb; - } - return {r: -1, g: -1, b: -1, hex: "none", error: 1, toString: clrToString}; - }, R); - /*\ - * Raphael.hsb - [ method ] - ** - * Converts HSB values to hex representation of the colour. - > Parameters - - h (number) hue - - s (number) saturation - - b (number) value or brightness - = (string) hex representation of the colour. - \*/ - R.hsb = cacher(function (h, s, b) { - return R.hsb2rgb(h, s, b).hex; - }); - /*\ - * Raphael.hsl - [ method ] - ** - * Converts HSL values to hex representation of the colour. - > Parameters - - h (number) hue - - s (number) saturation - - l (number) luminosity - = (string) hex representation of the colour. - \*/ - R.hsl = cacher(function (h, s, l) { - return R.hsl2rgb(h, s, l).hex; - }); - /*\ - * Raphael.rgb - [ method ] - ** - * Converts RGB values to hex representation of the colour. - > Parameters - - r (number) red - - g (number) green - - b (number) blue - = (string) hex representation of the colour. - \*/ - R.rgb = cacher(function (r, g, b) { - return "#" + (16777216 | b | (g << 8) | (r << 16)).toString(16).slice(1); - }); - /*\ - * Raphael.getColor - [ method ] - ** - * On each call returns next colour in the spectrum. To reset it back to red call @Raphael.getColor.reset - > Parameters - - value (number) #optional brightness, default is `0.75` - = (string) hex representation of the colour. - \*/ - R.getColor = function (value) { - var start = this.getColor.start = this.getColor.start || {h: 0, s: 1, b: value || .75}, - rgb = this.hsb2rgb(start.h, start.s, start.b); - start.h += .075; - if (start.h > 1) { - start.h = 0; - start.s -= .2; - start.s <= 0 && (this.getColor.start = {h: 0, s: 1, b: start.b}); - } - return rgb.hex; - }; - /*\ - * Raphael.getColor.reset - [ method ] - ** - * Resets spectrum position for @Raphael.getColor back to red. - \*/ - R.getColor.reset = function () { - delete this.start; - }; - - // http://schepers.cc/getting-to-the-point - function catmullRom2bezier(crp, z) { - var d = []; - for (var i = 0, iLen = crp.length; iLen - 2 * !z > i; i += 2) { - var p = [ - {x: +crp[i - 2], y: +crp[i - 1]}, - {x: +crp[i], y: +crp[i + 1]}, - {x: +crp[i + 2], y: +crp[i + 3]}, - {x: +crp[i + 4], y: +crp[i + 5]} - ]; - if (z) { - if (!i) { - p[0] = {x: +crp[iLen - 2], y: +crp[iLen - 1]}; - } else if (iLen - 4 == i) { - p[3] = {x: +crp[0], y: +crp[1]}; - } else if (iLen - 2 == i) { - p[2] = {x: +crp[0], y: +crp[1]}; - p[3] = {x: +crp[2], y: +crp[3]}; - } - } else { - if (iLen - 4 == i) { - p[3] = p[2]; - } else if (!i) { - p[0] = {x: +crp[i], y: +crp[i + 1]}; - } - } - d.push(["C", - (-p[0].x + 6 * p[1].x + p[2].x) / 6, - (-p[0].y + 6 * p[1].y + p[2].y) / 6, - (p[1].x + 6 * p[2].x - p[3].x) / 6, - (p[1].y + 6*p[2].y - p[3].y) / 6, - p[2].x, - p[2].y - ]); - } - - return d; - } - /*\ - * Raphael.parsePathString - [ method ] - ** - * Utility method - ** - * Parses given path string into an array of arrays of path segments. - > Parameters - - pathString (string|array) path string or array of segments (in the last case it will be returned straight away) - = (array) array of segments. - \*/ - R.parsePathString = function (pathString) { - if (!pathString) { - return null; - } - var pth = paths(pathString); - if (pth.arr) { - return pathClone(pth.arr); - } - - var paramCounts = {a: 7, c: 6, h: 1, l: 2, m: 2, r: 4, q: 4, s: 4, t: 2, v: 1, z: 0}, - data = []; - if (R.is(pathString, array) && R.is(pathString[0], array)) { // rough assumption - data = pathClone(pathString); - } - if (!data.length) { - Str(pathString).replace(pathCommand, function (a, b, c) { - var params = [], - name = b.toLowerCase(); - c.replace(pathValues, function (a, b) { - b && params.push(+b); - }); - if (name == "m" && params.length > 2) { - data.push([b][concat](params.splice(0, 2))); - name = "l"; - b = b == "m" ? "l" : "L"; - } - if (name == "r") { - data.push([b][concat](params)); - } else while (params.length >= paramCounts[name]) { - data.push([b][concat](params.splice(0, paramCounts[name]))); - if (!paramCounts[name]) { - break; - } - } - }); - } - data.toString = R._path2string; - pth.arr = pathClone(data); - return data; - }; - /*\ - * Raphael.parseTransformString - [ method ] - ** - * Utility method - ** - * Parses given path string into an array of transformations. - > Parameters - - TString (string|array) transform string or array of transformations (in the last case it will be returned straight away) - = (array) array of transformations. - \*/ - R.parseTransformString = cacher(function (TString) { - if (!TString) { - return null; - } - var paramCounts = {r: 3, s: 4, t: 2, m: 6}, - data = []; - if (R.is(TString, array) && R.is(TString[0], array)) { // rough assumption - data = pathClone(TString); - } - if (!data.length) { - Str(TString).replace(tCommand, function (a, b, c) { - var params = [], - name = lowerCase.call(b); - c.replace(pathValues, function (a, b) { - b && params.push(+b); - }); - data.push([b][concat](params)); - }); - } - data.toString = R._path2string; - return data; - }); - // PATHS - var paths = function (ps) { - var p = paths.ps = paths.ps || {}; - if (p[ps]) { - p[ps].sleep = 100; - } else { - p[ps] = { - sleep: 100 - }; - } - setTimeout(function () { - for (var key in p) if (p[has](key) && key != ps) { - p[key].sleep--; - !p[key].sleep && delete p[key]; - } - }); - return p[ps]; - }; - /*\ - * Raphael.findDotsAtSegment - [ method ] - ** - * Utility method - ** - * Find dot coordinates on the given cubic bezier curve at the given t. - > Parameters - - p1x (number) x of the first point of the curve - - p1y (number) y of the first point of the curve - - c1x (number) x of the first anchor of the curve - - c1y (number) y of the first anchor of the curve - - c2x (number) x of the second anchor of the curve - - c2y (number) y of the second anchor of the curve - - p2x (number) x of the second point of the curve - - p2y (number) y of the second point of the curve - - t (number) position on the curve (0..1) - = (object) point information in format: - o { - o x: (number) x coordinate of the point - o y: (number) y coordinate of the point - o m: { - o x: (number) x coordinate of the left anchor - o y: (number) y coordinate of the left anchor - o } - o n: { - o x: (number) x coordinate of the right anchor - o y: (number) y coordinate of the right anchor - o } - o start: { - o x: (number) x coordinate of the start of the curve - o y: (number) y coordinate of the start of the curve - o } - o end: { - o x: (number) x coordinate of the end of the curve - o y: (number) y coordinate of the end of the curve - o } - o alpha: (number) angle of the curve derivative at the point - o } - \*/ - R.findDotsAtSegment = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) { - var t1 = 1 - t, - t13 = pow(t1, 3), - t12 = pow(t1, 2), - t2 = t * t, - t3 = t2 * t, - x = t13 * p1x + t12 * 3 * t * c1x + t1 * 3 * t * t * c2x + t3 * p2x, - y = t13 * p1y + t12 * 3 * t * c1y + t1 * 3 * t * t * c2y + t3 * p2y, - mx = p1x + 2 * t * (c1x - p1x) + t2 * (c2x - 2 * c1x + p1x), - my = p1y + 2 * t * (c1y - p1y) + t2 * (c2y - 2 * c1y + p1y), - nx = c1x + 2 * t * (c2x - c1x) + t2 * (p2x - 2 * c2x + c1x), - ny = c1y + 2 * t * (c2y - c1y) + t2 * (p2y - 2 * c2y + c1y), - ax = t1 * p1x + t * c1x, - ay = t1 * p1y + t * c1y, - cx = t1 * c2x + t * p2x, - cy = t1 * c2y + t * p2y, - alpha = (90 - math.atan2(mx - nx, my - ny) * 180 / PI); - (mx > nx || my < ny) && (alpha += 180); - return { - x: x, - y: y, - m: {x: mx, y: my}, - n: {x: nx, y: ny}, - start: {x: ax, y: ay}, - end: {x: cx, y: cy}, - alpha: alpha - }; - }; - /*\ - * Raphael.bezierBBox - [ method ] - ** - * Utility method - ** - * Return bounding box of a given cubic bezier curve - > Parameters - - p1x (number) x of the first point of the curve - - p1y (number) y of the first point of the curve - - c1x (number) x of the first anchor of the curve - - c1y (number) y of the first anchor of the curve - - c2x (number) x of the second anchor of the curve - - c2y (number) y of the second anchor of the curve - - p2x (number) x of the second point of the curve - - p2y (number) y of the second point of the curve - * or - - bez (array) array of six points for bezier curve - = (object) point information in format: - o { - o min: { - o x: (number) x coordinate of the left point - o y: (number) y coordinate of the top point - o } - o max: { - o x: (number) x coordinate of the right point - o y: (number) y coordinate of the bottom point - o } - o } - \*/ - R.bezierBBox = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) { - if (!R.is(p1x, "array")) { - p1x = [p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y]; - } - var bbox = curveDim.apply(null, p1x); - return { - x: bbox.min.x, - y: bbox.min.y, - x2: bbox.max.x, - y2: bbox.max.y, - width: bbox.max.x - bbox.min.x, - height: bbox.max.y - bbox.min.y - }; - }; - /*\ - * Raphael.isPointInsideBBox - [ method ] - ** - * Utility method - ** - * Returns `true` if given point is inside bounding boxes. - > Parameters - - bbox (string) bounding box - - x (string) x coordinate of the point - - y (string) y coordinate of the point - = (boolean) `true` if point inside - \*/ - R.isPointInsideBBox = function (bbox, x, y) { - return x >= bbox.x && x <= bbox.x2 && y >= bbox.y && y <= bbox.y2; - }; - /*\ - * Raphael.isBBoxIntersect - [ method ] - ** - * Utility method - ** - * Returns `true` if two bounding boxes intersect - > Parameters - - bbox1 (string) first bounding box - - bbox2 (string) second bounding box - = (boolean) `true` if they intersect - \*/ - R.isBBoxIntersect = function (bbox1, bbox2) { - var i = R.isPointInsideBBox; - return i(bbox2, bbox1.x, bbox1.y) - || i(bbox2, bbox1.x2, bbox1.y) - || i(bbox2, bbox1.x, bbox1.y2) - || i(bbox2, bbox1.x2, bbox1.y2) - || i(bbox1, bbox2.x, bbox2.y) - || i(bbox1, bbox2.x2, bbox2.y) - || i(bbox1, bbox2.x, bbox2.y2) - || i(bbox1, bbox2.x2, bbox2.y2) - || (bbox1.x < bbox2.x2 && bbox1.x > bbox2.x || bbox2.x < bbox1.x2 && bbox2.x > bbox1.x) - && (bbox1.y < bbox2.y2 && bbox1.y > bbox2.y || bbox2.y < bbox1.y2 && bbox2.y > bbox1.y); - }; - function base3(t, p1, p2, p3, p4) { - var t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4, - t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3; - return t * t2 - 3 * p1 + 3 * p2; - } - function bezlen(x1, y1, x2, y2, x3, y3, x4, y4, z) { - if (z == null) { - z = 1; - } - z = z > 1 ? 1 : z < 0 ? 0 : z; - var z2 = z / 2, - n = 12, - Tvalues = [-0.1252,0.1252,-0.3678,0.3678,-0.5873,0.5873,-0.7699,0.7699,-0.9041,0.9041,-0.9816,0.9816], - Cvalues = [0.2491,0.2491,0.2335,0.2335,0.2032,0.2032,0.1601,0.1601,0.1069,0.1069,0.0472,0.0472], - sum = 0; - for (var i = 0; i < n; i++) { - var ct = z2 * Tvalues[i] + z2, - xbase = base3(ct, x1, x2, x3, x4), - ybase = base3(ct, y1, y2, y3, y4), - comb = xbase * xbase + ybase * ybase; - sum += Cvalues[i] * math.sqrt(comb); - } - return z2 * sum; - } - function getTatLen(x1, y1, x2, y2, x3, y3, x4, y4, ll) { - if (ll < 0 || bezlen(x1, y1, x2, y2, x3, y3, x4, y4) < ll) { - return; - } - var t = 1, - step = t / 2, - t2 = t - step, - l, - e = .01; - l = bezlen(x1, y1, x2, y2, x3, y3, x4, y4, t2); - while (abs(l - ll) > e) { - step /= 2; - t2 += (l < ll ? 1 : -1) * step; - l = bezlen(x1, y1, x2, y2, x3, y3, x4, y4, t2); - } - return t2; - } - function intersect(x1, y1, x2, y2, x3, y3, x4, y4) { - if ( - mmax(x1, x2) < mmin(x3, x4) || - mmin(x1, x2) > mmax(x3, x4) || - mmax(y1, y2) < mmin(y3, y4) || - mmin(y1, y2) > mmax(y3, y4) - ) { - return; - } - var nx = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4), - ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4), - denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); - - if (!denominator) { - return; - } - var px = nx / denominator, - py = ny / denominator, - px2 = +px.toFixed(2), - py2 = +py.toFixed(2); - if ( - px2 < +mmin(x1, x2).toFixed(2) || - px2 > +mmax(x1, x2).toFixed(2) || - px2 < +mmin(x3, x4).toFixed(2) || - px2 > +mmax(x3, x4).toFixed(2) || - py2 < +mmin(y1, y2).toFixed(2) || - py2 > +mmax(y1, y2).toFixed(2) || - py2 < +mmin(y3, y4).toFixed(2) || - py2 > +mmax(y3, y4).toFixed(2) - ) { - return; - } - return {x: px, y: py}; - } - function inter(bez1, bez2) { - return interHelper(bez1, bez2); - } - function interCount(bez1, bez2) { - return interHelper(bez1, bez2, 1); - } - function interHelper(bez1, bez2, justCount) { - var bbox1 = R.bezierBBox(bez1), - bbox2 = R.bezierBBox(bez2); - if (!R.isBBoxIntersect(bbox1, bbox2)) { - return justCount ? 0 : []; - } - var l1 = bezlen.apply(0, bez1), - l2 = bezlen.apply(0, bez2), - n1 = mmax(~~(l1 / 5), 1), - n2 = mmax(~~(l2 / 5), 1), - dots1 = [], - dots2 = [], - xy = {}, - res = justCount ? 0 : []; - for (var i = 0; i < n1 + 1; i++) { - var p = R.findDotsAtSegment.apply(R, bez1.concat(i / n1)); - dots1.push({x: p.x, y: p.y, t: i / n1}); - } - for (i = 0; i < n2 + 1; i++) { - p = R.findDotsAtSegment.apply(R, bez2.concat(i / n2)); - dots2.push({x: p.x, y: p.y, t: i / n2}); - } - for (i = 0; i < n1; i++) { - for (var j = 0; j < n2; j++) { - var di = dots1[i], - di1 = dots1[i + 1], - dj = dots2[j], - dj1 = dots2[j + 1], - ci = abs(di1.x - di.x) < .001 ? "y" : "x", - cj = abs(dj1.x - dj.x) < .001 ? "y" : "x", - is = intersect(di.x, di.y, di1.x, di1.y, dj.x, dj.y, dj1.x, dj1.y); - if (is) { - if (xy[is.x.toFixed(4)] == is.y.toFixed(4)) { - continue; - } - xy[is.x.toFixed(4)] = is.y.toFixed(4); - var t1 = di.t + abs((is[ci] - di[ci]) / (di1[ci] - di[ci])) * (di1.t - di.t), - t2 = dj.t + abs((is[cj] - dj[cj]) / (dj1[cj] - dj[cj])) * (dj1.t - dj.t); - if (t1 >= 0 && t1 <= 1.001 && t2 >= 0 && t2 <= 1.001) { - if (justCount) { - res++; - } else { - res.push({ - x: is.x, - y: is.y, - t1: mmin(t1, 1), - t2: mmin(t2, 1) - }); - } - } - } - } - } - return res; - } - /*\ - * Raphael.pathIntersection - [ method ] - ** - * Utility method - ** - * Finds intersections of two paths - > Parameters - - path1 (string) path string - - path2 (string) path string - = (array) dots of intersection - o [ - o { - o x: (number) x coordinate of the point - o y: (number) y coordinate of the point - o t1: (number) t value for segment of path1 - o t2: (number) t value for segment of path2 - o segment1: (number) order number for segment of path1 - o segment2: (number) order number for segment of path2 - o bez1: (array) eight coordinates representing beziér curve for the segment of path1 - o bez2: (array) eight coordinates representing beziér curve for the segment of path2 - o } - o ] - \*/ - R.pathIntersection = function (path1, path2) { - return interPathHelper(path1, path2); - }; - R.pathIntersectionNumber = function (path1, path2) { - return interPathHelper(path1, path2, 1); - }; - function interPathHelper(path1, path2, justCount) { - path1 = R._path2curve(path1); - path2 = R._path2curve(path2); - var x1, y1, x2, y2, x1m, y1m, x2m, y2m, bez1, bez2, - res = justCount ? 0 : []; - for (var i = 0, ii = path1.length; i < ii; i++) { - var pi = path1[i]; - if (pi[0] == "M") { - x1 = x1m = pi[1]; - y1 = y1m = pi[2]; - } else { - if (pi[0] == "C") { - bez1 = [x1, y1].concat(pi.slice(1)); - x1 = bez1[6]; - y1 = bez1[7]; - } else { - bez1 = [x1, y1, x1, y1, x1m, y1m, x1m, y1m]; - x1 = x1m; - y1 = y1m; - } - for (var j = 0, jj = path2.length; j < jj; j++) { - var pj = path2[j]; - if (pj[0] == "M") { - x2 = x2m = pj[1]; - y2 = y2m = pj[2]; - } else { - if (pj[0] == "C") { - bez2 = [x2, y2].concat(pj.slice(1)); - x2 = bez2[6]; - y2 = bez2[7]; - } else { - bez2 = [x2, y2, x2, y2, x2m, y2m, x2m, y2m]; - x2 = x2m; - y2 = y2m; - } - var intr = interHelper(bez1, bez2, justCount); - if (justCount) { - res += intr; - } else { - for (var k = 0, kk = intr.length; k < kk; k++) { - intr[k].segment1 = i; - intr[k].segment2 = j; - intr[k].bez1 = bez1; - intr[k].bez2 = bez2; - } - res = res.concat(intr); - } - } - } - } - } - return res; - } - /*\ - * Raphael.isPointInsidePath - [ method ] - ** - * Utility method - ** - * Returns `true` if given point is inside a given closed path. - > Parameters - - path (string) path string - - x (number) x of the point - - y (number) y of the point - = (boolean) true, if point is inside the path - \*/ - R.isPointInsidePath = function (path, x, y) { - var bbox = R.pathBBox(path); - return R.isPointInsideBBox(bbox, x, y) && - interPathHelper(path, [["M", x, y], ["H", bbox.x2 + 10]], 1) % 2 == 1; - }; - R._removedFactory = function (methodname) { - return function () { - eve("raphael.log", null, "Rapha\xebl: you are calling to method \u201c" + methodname + "\u201d of removed object", methodname); - }; - }; - /*\ - * Raphael.pathBBox - [ method ] - ** - * Utility method - ** - * Return bounding box of a given path - > Parameters - - path (string) path string - = (object) bounding box - o { - o x: (number) x coordinate of the left top point of the box - o y: (number) y coordinate of the left top point of the box - o x2: (number) x coordinate of the right bottom point of the box - o y2: (number) y coordinate of the right bottom point of the box - o width: (number) width of the box - o height: (number) height of the box - o cx: (number) x coordinate of the center of the box - o cy: (number) y coordinate of the center of the box - o } - \*/ - var pathDimensions = R.pathBBox = function (path) { - var pth = paths(path); - if (pth.bbox) { - return clone(pth.bbox); - } - if (!path) { - return {x: 0, y: 0, width: 0, height: 0, x2: 0, y2: 0}; - } - path = path2curve(path); - var x = 0, - y = 0, - X = [], - Y = [], - p; - for (var i = 0, ii = path.length; i < ii; i++) { - p = path[i]; - if (p[0] == "M") { - x = p[1]; - y = p[2]; - X.push(x); - Y.push(y); - } else { - var dim = curveDim(x, y, p[1], p[2], p[3], p[4], p[5], p[6]); - X = X[concat](dim.min.x, dim.max.x); - Y = Y[concat](dim.min.y, dim.max.y); - x = p[5]; - y = p[6]; - } - } - var xmin = mmin[apply](0, X), - ymin = mmin[apply](0, Y), - xmax = mmax[apply](0, X), - ymax = mmax[apply](0, Y), - width = xmax - xmin, - height = ymax - ymin, - bb = { - x: xmin, - y: ymin, - x2: xmax, - y2: ymax, - width: width, - height: height, - cx: xmin + width / 2, - cy: ymin + height / 2 - }; - pth.bbox = clone(bb); - return bb; - }, - pathClone = function (pathArray) { - var res = clone(pathArray); - res.toString = R._path2string; - return res; - }, - pathToRelative = R._pathToRelative = function (pathArray) { - var pth = paths(pathArray); - if (pth.rel) { - return pathClone(pth.rel); - } - if (!R.is(pathArray, array) || !R.is(pathArray && pathArray[0], array)) { // rough assumption - pathArray = R.parsePathString(pathArray); - } - var res = [], - x = 0, - y = 0, - mx = 0, - my = 0, - start = 0; - if (pathArray[0][0] == "M") { - x = pathArray[0][1]; - y = pathArray[0][2]; - mx = x; - my = y; - start++; - res.push(["M", x, y]); - } - for (var i = start, ii = pathArray.length; i < ii; i++) { - var r = res[i] = [], - pa = pathArray[i]; - if (pa[0] != lowerCase.call(pa[0])) { - r[0] = lowerCase.call(pa[0]); - switch (r[0]) { - case "a": - r[1] = pa[1]; - r[2] = pa[2]; - r[3] = pa[3]; - r[4] = pa[4]; - r[5] = pa[5]; - r[6] = +(pa[6] - x).toFixed(3); - r[7] = +(pa[7] - y).toFixed(3); - break; - case "v": - r[1] = +(pa[1] - y).toFixed(3); - break; - case "m": - mx = pa[1]; - my = pa[2]; - default: - for (var j = 1, jj = pa.length; j < jj; j++) { - r[j] = +(pa[j] - ((j % 2) ? x : y)).toFixed(3); - } - } - } else { - r = res[i] = []; - if (pa[0] == "m") { - mx = pa[1] + x; - my = pa[2] + y; - } - for (var k = 0, kk = pa.length; k < kk; k++) { - res[i][k] = pa[k]; - } - } - var len = res[i].length; - switch (res[i][0]) { - case "z": - x = mx; - y = my; - break; - case "h": - x += +res[i][len - 1]; - break; - case "v": - y += +res[i][len - 1]; - break; - default: - x += +res[i][len - 2]; - y += +res[i][len - 1]; - } - } - res.toString = R._path2string; - pth.rel = pathClone(res); - return res; - }, - pathToAbsolute = R._pathToAbsolute = function (pathArray) { - var pth = paths(pathArray); - if (pth.abs) { - return pathClone(pth.abs); - } - if (!R.is(pathArray, array) || !R.is(pathArray && pathArray[0], array)) { // rough assumption - pathArray = R.parsePathString(pathArray); - } - if (!pathArray || !pathArray.length) { - return [["M", 0, 0]]; - } - var res = [], - x = 0, - y = 0, - mx = 0, - my = 0, - start = 0; - if (pathArray[0][0] == "M") { - x = +pathArray[0][1]; - y = +pathArray[0][2]; - mx = x; - my = y; - start++; - res[0] = ["M", x, y]; - } - var crz = pathArray.length == 3 && pathArray[0][0] == "M" && pathArray[1][0].toUpperCase() == "R" && pathArray[2][0].toUpperCase() == "Z"; - for (var r, pa, i = start, ii = pathArray.length; i < ii; i++) { - res.push(r = []); - pa = pathArray[i]; - if (pa[0] != upperCase.call(pa[0])) { - r[0] = upperCase.call(pa[0]); - switch (r[0]) { - case "A": - r[1] = pa[1]; - r[2] = pa[2]; - r[3] = pa[3]; - r[4] = pa[4]; - r[5] = pa[5]; - r[6] = +(pa[6] + x); - r[7] = +(pa[7] + y); - break; - case "V": - r[1] = +pa[1] + y; - break; - case "H": - r[1] = +pa[1] + x; - break; - case "R": - var dots = [x, y][concat](pa.slice(1)); - for (var j = 2, jj = dots.length; j < jj; j++) { - dots[j] = +dots[j] + x; - dots[++j] = +dots[j] + y; - } - res.pop(); - res = res[concat](catmullRom2bezier(dots, crz)); - break; - case "M": - mx = +pa[1] + x; - my = +pa[2] + y; - default: - for (j = 1, jj = pa.length; j < jj; j++) { - r[j] = +pa[j] + ((j % 2) ? x : y); - } - } - } else if (pa[0] == "R") { - dots = [x, y][concat](pa.slice(1)); - res.pop(); - res = res[concat](catmullRom2bezier(dots, crz)); - r = ["R"][concat](pa.slice(-2)); - } else { - for (var k = 0, kk = pa.length; k < kk; k++) { - r[k] = pa[k]; - } - } - switch (r[0]) { - case "Z": - x = mx; - y = my; - break; - case "H": - x = r[1]; - break; - case "V": - y = r[1]; - break; - case "M": - mx = r[r.length - 2]; - my = r[r.length - 1]; - default: - x = r[r.length - 2]; - y = r[r.length - 1]; - } - } - res.toString = R._path2string; - pth.abs = pathClone(res); - return res; - }, - l2c = function (x1, y1, x2, y2) { - return [x1, y1, x2, y2, x2, y2]; - }, - q2c = function (x1, y1, ax, ay, x2, y2) { - var _13 = 1 / 3, - _23 = 2 / 3; - return [ - _13 * x1 + _23 * ax, - _13 * y1 + _23 * ay, - _13 * x2 + _23 * ax, - _13 * y2 + _23 * ay, - x2, - y2 - ]; - }, - a2c = function (x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) { - // for more information of where this math came from visit: - // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes - var _120 = PI * 120 / 180, - rad = PI / 180 * (+angle || 0), - res = [], - xy, - rotate = cacher(function (x, y, rad) { - var X = x * math.cos(rad) - y * math.sin(rad), - Y = x * math.sin(rad) + y * math.cos(rad); - return {x: X, y: Y}; - }); - if (!recursive) { - xy = rotate(x1, y1, -rad); - x1 = xy.x; - y1 = xy.y; - xy = rotate(x2, y2, -rad); - x2 = xy.x; - y2 = xy.y; - var cos = math.cos(PI / 180 * angle), - sin = math.sin(PI / 180 * angle), - x = (x1 - x2) / 2, - y = (y1 - y2) / 2; - var h = (x * x) / (rx * rx) + (y * y) / (ry * ry); - if (h > 1) { - h = math.sqrt(h); - rx = h * rx; - ry = h * ry; - } - var rx2 = rx * rx, - ry2 = ry * ry, - k = (large_arc_flag == sweep_flag ? -1 : 1) * - math.sqrt(abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x))), - cx = k * rx * y / ry + (x1 + x2) / 2, - cy = k * -ry * x / rx + (y1 + y2) / 2, - f1 = math.asin(((y1 - cy) / ry).toFixed(9)), - f2 = math.asin(((y2 - cy) / ry).toFixed(9)); - - f1 = x1 < cx ? PI - f1 : f1; - f2 = x2 < cx ? PI - f2 : f2; - f1 < 0 && (f1 = PI * 2 + f1); - f2 < 0 && (f2 = PI * 2 + f2); - if (sweep_flag && f1 > f2) { - f1 = f1 - PI * 2; - } - if (!sweep_flag && f2 > f1) { - f2 = f2 - PI * 2; - } - } else { - f1 = recursive[0]; - f2 = recursive[1]; - cx = recursive[2]; - cy = recursive[3]; - } - var df = f2 - f1; - if (abs(df) > _120) { - var f2old = f2, - x2old = x2, - y2old = y2; - f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1); - x2 = cx + rx * math.cos(f2); - y2 = cy + ry * math.sin(f2); - res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]); - } - df = f2 - f1; - var c1 = math.cos(f1), - s1 = math.sin(f1), - c2 = math.cos(f2), - s2 = math.sin(f2), - t = math.tan(df / 4), - hx = 4 / 3 * rx * t, - hy = 4 / 3 * ry * t, - m1 = [x1, y1], - m2 = [x1 + hx * s1, y1 - hy * c1], - m3 = [x2 + hx * s2, y2 - hy * c2], - m4 = [x2, y2]; - m2[0] = 2 * m1[0] - m2[0]; - m2[1] = 2 * m1[1] - m2[1]; - if (recursive) { - return [m2, m3, m4][concat](res); - } else { - res = [m2, m3, m4][concat](res).join()[split](","); - var newres = []; - for (var i = 0, ii = res.length; i < ii; i++) { - newres[i] = i % 2 ? rotate(res[i - 1], res[i], rad).y : rotate(res[i], res[i + 1], rad).x; - } - return newres; - } - }, - findDotAtSegment = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) { - var t1 = 1 - t; - return { - x: pow(t1, 3) * p1x + pow(t1, 2) * 3 * t * c1x + t1 * 3 * t * t * c2x + pow(t, 3) * p2x, - y: pow(t1, 3) * p1y + pow(t1, 2) * 3 * t * c1y + t1 * 3 * t * t * c2y + pow(t, 3) * p2y - }; - }, - curveDim = cacher(function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) { - var a = (c2x - 2 * c1x + p1x) - (p2x - 2 * c2x + c1x), - b = 2 * (c1x - p1x) - 2 * (c2x - c1x), - c = p1x - c1x, - t1 = (-b + math.sqrt(b * b - 4 * a * c)) / 2 / a, - t2 = (-b - math.sqrt(b * b - 4 * a * c)) / 2 / a, - y = [p1y, p2y], - x = [p1x, p2x], - dot; - abs(t1) > "1e12" && (t1 = .5); - abs(t2) > "1e12" && (t2 = .5); - if (t1 > 0 && t1 < 1) { - dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1); - x.push(dot.x); - y.push(dot.y); - } - if (t2 > 0 && t2 < 1) { - dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2); - x.push(dot.x); - y.push(dot.y); - } - a = (c2y - 2 * c1y + p1y) - (p2y - 2 * c2y + c1y); - b = 2 * (c1y - p1y) - 2 * (c2y - c1y); - c = p1y - c1y; - t1 = (-b + math.sqrt(b * b - 4 * a * c)) / 2 / a; - t2 = (-b - math.sqrt(b * b - 4 * a * c)) / 2 / a; - abs(t1) > "1e12" && (t1 = .5); - abs(t2) > "1e12" && (t2 = .5); - if (t1 > 0 && t1 < 1) { - dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1); - x.push(dot.x); - y.push(dot.y); - } - if (t2 > 0 && t2 < 1) { - dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2); - x.push(dot.x); - y.push(dot.y); - } - return { - min: {x: mmin[apply](0, x), y: mmin[apply](0, y)}, - max: {x: mmax[apply](0, x), y: mmax[apply](0, y)} - }; - }), - path2curve = R._path2curve = cacher(function (path, path2) { - var pth = !path2 && paths(path); - if (!path2 && pth.curve) { - return pathClone(pth.curve); - } - var p = pathToAbsolute(path), - p2 = path2 && pathToAbsolute(path2), - attrs = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null}, - attrs2 = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null}, - processPath = function (path, d, pcom) { - var nx, ny, tq = {T:1, Q:1}; - if (!path) { - return ["C", d.x, d.y, d.x, d.y, d.x, d.y]; - } - !(path[0] in tq) && (d.qx = d.qy = null); - switch (path[0]) { - case "M": - d.X = path[1]; - d.Y = path[2]; - break; - case "A": - path = ["C"][concat](a2c[apply](0, [d.x, d.y][concat](path.slice(1)))); - break; - case "S": - if (pcom == "C" || pcom == "S") { // In "S" case we have to take into account, if the previous command is C/S. - nx = d.x * 2 - d.bx; // And reflect the previous - ny = d.y * 2 - d.by; // command's control point relative to the current point. - } - else { // or some else or nothing - nx = d.x; - ny = d.y; - } - path = ["C", nx, ny][concat](path.slice(1)); - break; - case "T": - if (pcom == "Q" || pcom == "T") { // In "T" case we have to take into account, if the previous command is Q/T. - d.qx = d.x * 2 - d.qx; // And make a reflection similar - d.qy = d.y * 2 - d.qy; // to case "S". - } - else { // or something else or nothing - d.qx = d.x; - d.qy = d.y; - } - path = ["C"][concat](q2c(d.x, d.y, d.qx, d.qy, path[1], path[2])); - break; - case "Q": - d.qx = path[1]; - d.qy = path[2]; - path = ["C"][concat](q2c(d.x, d.y, path[1], path[2], path[3], path[4])); - break; - case "L": - path = ["C"][concat](l2c(d.x, d.y, path[1], path[2])); - break; - case "H": - path = ["C"][concat](l2c(d.x, d.y, path[1], d.y)); - break; - case "V": - path = ["C"][concat](l2c(d.x, d.y, d.x, path[1])); - break; - case "Z": - path = ["C"][concat](l2c(d.x, d.y, d.X, d.Y)); - break; - } - return path; - }, - fixArc = function (pp, i) { - if (pp[i].length > 7) { - pp[i].shift(); - var pi = pp[i]; - while (pi.length) { - pcoms1[i]="A"; // if created multiple C:s, their original seg is saved - p2 && (pcoms2[i]="A"); // the same as above - pp.splice(i++, 0, ["C"][concat](pi.splice(0, 6))); - } - pp.splice(i, 1); - ii = mmax(p.length, p2 && p2.length || 0); - } - }, - fixM = function (path1, path2, a1, a2, i) { - if (path1 && path2 && path1[i][0] == "M" && path2[i][0] != "M") { - path2.splice(i, 0, ["M", a2.x, a2.y]); - a1.bx = 0; - a1.by = 0; - a1.x = path1[i][1]; - a1.y = path1[i][2]; - ii = mmax(p.length, p2 && p2.length || 0); - } - }, - pcoms1 = [], // path commands of original path p - pcoms2 = [], // path commands of original path p2 - pfirst = "", // temporary holder for original path command - pcom = ""; // holder for previous path command of original path - for (var i = 0, ii = mmax(p.length, p2 && p2.length || 0); i < ii; i++) { - p[i] && (pfirst = p[i][0]); // save current path command - - if (pfirst != "C") // C is not saved yet, because it may be result of conversion - { - pcoms1[i] = pfirst; // Save current path command - i && ( pcom = pcoms1[i-1]); // Get previous path command pcom - } - p[i] = processPath(p[i], attrs, pcom); // Previous path command is inputted to processPath - - if (pcoms1[i] != "A" && pfirst == "C") pcoms1[i] = "C"; // A is the only command - // which may produce multiple C:s - // so we have to make sure that C is also C in original path - - fixArc(p, i); // fixArc adds also the right amount of A:s to pcoms1 - - if (p2) { // the same procedures is done to p2 - p2[i] && (pfirst = p2[i][0]); - if (pfirst != "C") - { - pcoms2[i] = pfirst; - i && (pcom = pcoms2[i-1]); - } - p2[i] = processPath(p2[i], attrs2, pcom); - - if (pcoms2[i]!="A" && pfirst=="C") pcoms2[i]="C"; - - fixArc(p2, i); - } - fixM(p, p2, attrs, attrs2, i); - fixM(p2, p, attrs2, attrs, i); - var seg = p[i], - seg2 = p2 && p2[i], - seglen = seg.length, - seg2len = p2 && seg2.length; - attrs.x = seg[seglen - 2]; - attrs.y = seg[seglen - 1]; - attrs.bx = toFloat(seg[seglen - 4]) || attrs.x; - attrs.by = toFloat(seg[seglen - 3]) || attrs.y; - attrs2.bx = p2 && (toFloat(seg2[seg2len - 4]) || attrs2.x); - attrs2.by = p2 && (toFloat(seg2[seg2len - 3]) || attrs2.y); - attrs2.x = p2 && seg2[seg2len - 2]; - attrs2.y = p2 && seg2[seg2len - 1]; - } - if (!p2) { - pth.curve = pathClone(p); - } - return p2 ? [p, p2] : p; - }, null, pathClone), - parseDots = R._parseDots = cacher(function (gradient) { - var dots = []; - for (var i = 0, ii = gradient.length; i < ii; i++) { - var dot = {}, - par = gradient[i].match(/^([^:]*):?([\d\.]*)/); - dot.color = R.getRGB(par[1]); - if (dot.color.error) { - return null; - } - dot.color = dot.color.hex; - par[2] && (dot.offset = par[2] + "%"); - dots.push(dot); - } - for (i = 1, ii = dots.length - 1; i < ii; i++) { - if (!dots[i].offset) { - var start = toFloat(dots[i - 1].offset || 0), - end = 0; - for (var j = i + 1; j < ii; j++) { - if (dots[j].offset) { - end = dots[j].offset; - break; - } - } - if (!end) { - end = 100; - j = ii; - } - end = toFloat(end); - var d = (end - start) / (j - i + 1); - for (; i < j; i++) { - start += d; - dots[i].offset = start + "%"; - } - } - } - return dots; - }), - tear = R._tear = function (el, paper) { - el == paper.top && (paper.top = el.prev); - el == paper.bottom && (paper.bottom = el.next); - el.next && (el.next.prev = el.prev); - el.prev && (el.prev.next = el.next); - }, - tofront = R._tofront = function (el, paper) { - if (paper.top === el) { - return; - } - tear(el, paper); - el.next = null; - el.prev = paper.top; - paper.top.next = el; - paper.top = el; - }, - toback = R._toback = function (el, paper) { - if (paper.bottom === el) { - return; - } - tear(el, paper); - el.next = paper.bottom; - el.prev = null; - paper.bottom.prev = el; - paper.bottom = el; - }, - insertafter = R._insertafter = function (el, el2, paper) { - tear(el, paper); - el2 == paper.top && (paper.top = el); - el2.next && (el2.next.prev = el); - el.next = el2.next; - el.prev = el2; - el2.next = el; - }, - insertbefore = R._insertbefore = function (el, el2, paper) { - tear(el, paper); - el2 == paper.bottom && (paper.bottom = el); - el2.prev && (el2.prev.next = el); - el.prev = el2.prev; - el2.prev = el; - el.next = el2; - }, - /*\ - * Raphael.toMatrix - [ method ] - ** - * Utility method - ** - * Returns matrix of transformations applied to a given path - > Parameters - - path (string) path string - - transform (string|array) transformation string - = (object) @Matrix - \*/ - toMatrix = R.toMatrix = function (path, transform) { - var bb = pathDimensions(path), - el = { - _: { - transform: E - }, - getBBox: function () { - return bb; - } - }; - extractTransform(el, transform); - return el.matrix; - }, - /*\ - * Raphael.transformPath - [ method ] - ** - * Utility method - ** - * Returns path transformed by a given transformation - > Parameters - - path (string) path string - - transform (string|array) transformation string - = (string) path - \*/ - transformPath = R.transformPath = function (path, transform) { - return mapPath(path, toMatrix(path, transform)); - }, - extractTransform = R._extractTransform = function (el, tstr) { - if (tstr == null) { - return el._.transform; - } - tstr = Str(tstr).replace(/\.{3}|\u2026/g, el._.transform || E); - var tdata = R.parseTransformString(tstr), - deg = 0, - dx = 0, - dy = 0, - sx = 1, - sy = 1, - _ = el._, - m = new Matrix; - _.transform = tdata || []; - if (tdata) { - for (var i = 0, ii = tdata.length; i < ii; i++) { - var t = tdata[i], - tlen = t.length, - command = Str(t[0]).toLowerCase(), - absolute = t[0] != command, - inver = absolute ? m.invert() : 0, - x1, - y1, - x2, - y2, - bb; - if (command == "t" && tlen == 3) { - if (absolute) { - x1 = inver.x(0, 0); - y1 = inver.y(0, 0); - x2 = inver.x(t[1], t[2]); - y2 = inver.y(t[1], t[2]); - m.translate(x2 - x1, y2 - y1); - } else { - m.translate(t[1], t[2]); - } - } else if (command == "r") { - if (tlen == 2) { - bb = bb || el.getBBox(1); - m.rotate(t[1], bb.x + bb.width / 2, bb.y + bb.height / 2); - deg += t[1]; - } else if (tlen == 4) { - if (absolute) { - x2 = inver.x(t[2], t[3]); - y2 = inver.y(t[2], t[3]); - m.rotate(t[1], x2, y2); - } else { - m.rotate(t[1], t[2], t[3]); - } - deg += t[1]; - } - } else if (command == "s") { - if (tlen == 2 || tlen == 3) { - bb = bb || el.getBBox(1); - m.scale(t[1], t[tlen - 1], bb.x + bb.width / 2, bb.y + bb.height / 2); - sx *= t[1]; - sy *= t[tlen - 1]; - } else if (tlen == 5) { - if (absolute) { - x2 = inver.x(t[3], t[4]); - y2 = inver.y(t[3], t[4]); - m.scale(t[1], t[2], x2, y2); - } else { - m.scale(t[1], t[2], t[3], t[4]); - } - sx *= t[1]; - sy *= t[2]; - } - } else if (command == "m" && tlen == 7) { - m.add(t[1], t[2], t[3], t[4], t[5], t[6]); - } - _.dirtyT = 1; - el.matrix = m; - } - } - - /*\ - * Element.matrix - [ property (object) ] - ** - * Keeps @Matrix object, which represents element transformation - \*/ - el.matrix = m; - - _.sx = sx; - _.sy = sy; - _.deg = deg; - _.dx = dx = m.e; - _.dy = dy = m.f; - - if (sx == 1 && sy == 1 && !deg && _.bbox) { - _.bbox.x += +dx; - _.bbox.y += +dy; - } else { - _.dirtyT = 1; - } - }, - getEmpty = function (item) { - var l = item[0]; - switch (l.toLowerCase()) { - case "t": return [l, 0, 0]; - case "m": return [l, 1, 0, 0, 1, 0, 0]; - case "r": if (item.length == 4) { - return [l, 0, item[2], item[3]]; - } else { - return [l, 0]; - } - case "s": if (item.length == 5) { - return [l, 1, 1, item[3], item[4]]; - } else if (item.length == 3) { - return [l, 1, 1]; - } else { - return [l, 1]; - } - } - }, - equaliseTransform = R._equaliseTransform = function (t1, t2) { - t2 = Str(t2).replace(/\.{3}|\u2026/g, t1); - t1 = R.parseTransformString(t1) || []; - t2 = R.parseTransformString(t2) || []; - var maxlength = mmax(t1.length, t2.length), - from = [], - to = [], - i = 0, j, jj, - tt1, tt2; - for (; i < maxlength; i++) { - tt1 = t1[i] || getEmpty(t2[i]); - tt2 = t2[i] || getEmpty(tt1); - if ((tt1[0] != tt2[0]) || - (tt1[0].toLowerCase() == "r" && (tt1[2] != tt2[2] || tt1[3] != tt2[3])) || - (tt1[0].toLowerCase() == "s" && (tt1[3] != tt2[3] || tt1[4] != tt2[4])) - ) { - return; - } - from[i] = []; - to[i] = []; - for (j = 0, jj = mmax(tt1.length, tt2.length); j < jj; j++) { - j in tt1 && (from[i][j] = tt1[j]); - j in tt2 && (to[i][j] = tt2[j]); - } - } - return { - from: from, - to: to - }; - }; - R._getContainer = function (x, y, w, h) { - var container; - container = h == null && !R.is(x, "object") ? g.doc.getElementById(x) : x; - if (container == null) { - return; - } - if (container.tagName) { - if (y == null) { - return { - container: container, - width: container.style.pixelWidth || container.offsetWidth, - height: container.style.pixelHeight || container.offsetHeight - }; - } else { - return { - container: container, - width: y, - height: w - }; - } - } - return { - container: 1, - x: x, - y: y, - width: w, - height: h - }; - }; - /*\ - * Raphael.pathToRelative - [ method ] - ** - * Utility method - ** - * Converts path to relative form - > Parameters - - pathString (string|array) path string or array of segments - = (array) array of segments. - \*/ - R.pathToRelative = pathToRelative; - R._engine = {}; - /*\ - * Raphael.path2curve - [ method ] - ** - * Utility method - ** - * Converts path to a new path where all segments are cubic bezier curves. - > Parameters - - pathString (string|array) path string or array of segments - = (array) array of segments. - \*/ - R.path2curve = path2curve; - /*\ - * Raphael.matrix - [ method ] - ** - * Utility method - ** - * Returns matrix based on given parameters. - > Parameters - - a (number) - - b (number) - - c (number) - - d (number) - - e (number) - - f (number) - = (object) @Matrix - \*/ - R.matrix = function (a, b, c, d, e, f) { - return new Matrix(a, b, c, d, e, f); - }; - function Matrix(a, b, c, d, e, f) { - if (a != null) { - this.a = +a; - this.b = +b; - this.c = +c; - this.d = +d; - this.e = +e; - this.f = +f; - } else { - this.a = 1; - this.b = 0; - this.c = 0; - this.d = 1; - this.e = 0; - this.f = 0; - } - } - (function (matrixproto) { - /*\ - * Matrix.add - [ method ] - ** - * Adds given matrix to existing one. - > Parameters - - a (number) - - b (number) - - c (number) - - d (number) - - e (number) - - f (number) - or - - matrix (object) @Matrix - \*/ - matrixproto.add = function (a, b, c, d, e, f) { - var out = [[], [], []], - m = [[this.a, this.c, this.e], [this.b, this.d, this.f], [0, 0, 1]], - matrix = [[a, c, e], [b, d, f], [0, 0, 1]], - x, y, z, res; - - if (a && a instanceof Matrix) { - matrix = [[a.a, a.c, a.e], [a.b, a.d, a.f], [0, 0, 1]]; - } - - for (x = 0; x < 3; x++) { - for (y = 0; y < 3; y++) { - res = 0; - for (z = 0; z < 3; z++) { - res += m[x][z] * matrix[z][y]; - } - out[x][y] = res; - } - } - this.a = out[0][0]; - this.b = out[1][0]; - this.c = out[0][1]; - this.d = out[1][1]; - this.e = out[0][2]; - this.f = out[1][2]; - }; - /*\ - * Matrix.invert - [ method ] - ** - * Returns inverted version of the matrix - = (object) @Matrix - \*/ - matrixproto.invert = function () { - var me = this, - x = me.a * me.d - me.b * me.c; - return new Matrix(me.d / x, -me.b / x, -me.c / x, me.a / x, (me.c * me.f - me.d * me.e) / x, (me.b * me.e - me.a * me.f) / x); - }; - /*\ - * Matrix.clone - [ method ] - ** - * Returns copy of the matrix - = (object) @Matrix - \*/ - matrixproto.clone = function () { - return new Matrix(this.a, this.b, this.c, this.d, this.e, this.f); - }; - /*\ - * Matrix.translate - [ method ] - ** - * Translate the matrix - > Parameters - - x (number) - - y (number) - \*/ - matrixproto.translate = function (x, y) { - this.add(1, 0, 0, 1, x, y); - }; - /*\ - * Matrix.scale - [ method ] - ** - * Scales the matrix - > Parameters - - x (number) - - y (number) #optional - - cx (number) #optional - - cy (number) #optional - \*/ - matrixproto.scale = function (x, y, cx, cy) { - y == null && (y = x); - (cx || cy) && this.add(1, 0, 0, 1, cx, cy); - this.add(x, 0, 0, y, 0, 0); - (cx || cy) && this.add(1, 0, 0, 1, -cx, -cy); - }; - /*\ - * Matrix.rotate - [ method ] - ** - * Rotates the matrix - > Parameters - - a (number) - - x (number) - - y (number) - \*/ - matrixproto.rotate = function (a, x, y) { - a = R.rad(a); - x = x || 0; - y = y || 0; - var cos = +math.cos(a).toFixed(9), - sin = +math.sin(a).toFixed(9); - this.add(cos, sin, -sin, cos, x, y); - this.add(1, 0, 0, 1, -x, -y); - }; - /*\ - * Matrix.x - [ method ] - ** - * Return x coordinate for given point after transformation described by the matrix. See also @Matrix.y - > Parameters - - x (number) - - y (number) - = (number) x - \*/ - matrixproto.x = function (x, y) { - return x * this.a + y * this.c + this.e; - }; - /*\ - * Matrix.y - [ method ] - ** - * Return y coordinate for given point after transformation described by the matrix. See also @Matrix.x - > Parameters - - x (number) - - y (number) - = (number) y - \*/ - matrixproto.y = function (x, y) { - return x * this.b + y * this.d + this.f; - }; - matrixproto.get = function (i) { - return +this[Str.fromCharCode(97 + i)].toFixed(4); - }; - matrixproto.toString = function () { - return R.svg ? - "matrix(" + [this.get(0), this.get(1), this.get(2), this.get(3), this.get(4), this.get(5)].join() + ")" : - [this.get(0), this.get(2), this.get(1), this.get(3), 0, 0].join(); - }; - matrixproto.toFilter = function () { - return "progid:DXImageTransform.Microsoft.Matrix(M11=" + this.get(0) + - ", M12=" + this.get(2) + ", M21=" + this.get(1) + ", M22=" + this.get(3) + - ", Dx=" + this.get(4) + ", Dy=" + this.get(5) + ", sizingmethod='auto expand')"; - }; - matrixproto.offset = function () { - return [this.e.toFixed(4), this.f.toFixed(4)]; - }; - function norm(a) { - return a[0] * a[0] + a[1] * a[1]; - } - function normalize(a) { - var mag = math.sqrt(norm(a)); - a[0] && (a[0] /= mag); - a[1] && (a[1] /= mag); - } - /*\ - * Matrix.split - [ method ] - ** - * Splits matrix into primitive transformations - = (object) in format: - o dx (number) translation by x - o dy (number) translation by y - o scalex (number) scale by x - o scaley (number) scale by y - o shear (number) shear - o rotate (number) rotation in deg - o isSimple (boolean) could it be represented via simple transformations - \*/ - matrixproto.split = function () { - var out = {}; - // translation - out.dx = this.e; - out.dy = this.f; - - // scale and shear - var row = [[this.a, this.c], [this.b, this.d]]; - out.scalex = math.sqrt(norm(row[0])); - normalize(row[0]); - - out.shear = row[0][0] * row[1][0] + row[0][1] * row[1][1]; - row[1] = [row[1][0] - row[0][0] * out.shear, row[1][1] - row[0][1] * out.shear]; - - out.scaley = math.sqrt(norm(row[1])); - normalize(row[1]); - out.shear /= out.scaley; - - // rotation - var sin = -row[0][1], - cos = row[1][1]; - if (cos < 0) { - out.rotate = R.deg(math.acos(cos)); - if (sin < 0) { - out.rotate = 360 - out.rotate; - } - } else { - out.rotate = R.deg(math.asin(sin)); - } - - out.isSimple = !+out.shear.toFixed(9) && (out.scalex.toFixed(9) == out.scaley.toFixed(9) || !out.rotate); - out.isSuperSimple = !+out.shear.toFixed(9) && out.scalex.toFixed(9) == out.scaley.toFixed(9) && !out.rotate; - out.noRotation = !+out.shear.toFixed(9) && !out.rotate; - return out; - }; - /*\ - * Matrix.toTransformString - [ method ] - ** - * Return transform string that represents given matrix - = (string) transform string - \*/ - matrixproto.toTransformString = function (shorter) { - var s = shorter || this[split](); - if (s.isSimple) { - s.scalex = +s.scalex.toFixed(4); - s.scaley = +s.scaley.toFixed(4); - s.rotate = +s.rotate.toFixed(4); - return (s.dx || s.dy ? "t" + [s.dx, s.dy] : E) + - (s.scalex != 1 || s.scaley != 1 ? "s" + [s.scalex, s.scaley, 0, 0] : E) + - (s.rotate ? "r" + [s.rotate, 0, 0] : E); - } else { - return "m" + [this.get(0), this.get(1), this.get(2), this.get(3), this.get(4), this.get(5)]; - } - }; - })(Matrix.prototype); - - // WebKit rendering bug workaround method - var version = navigator.userAgent.match(/Version\/(.*?)\s/) || navigator.userAgent.match(/Chrome\/(\d+)/); - if ((navigator.vendor == "Apple Computer, Inc.") && (version && version[1] < 4 || navigator.platform.slice(0, 2) == "iP") || - (navigator.vendor == "Google Inc." && version && version[1] < 8)) { - /*\ - * Paper.safari - [ method ] - ** - * There is an inconvenient rendering bug in Safari (WebKit): - * sometimes the rendering should be forced. - * This method should help with dealing with this bug. - \*/ - paperproto.safari = function () { - var rect = this.rect(-99, -99, this.width + 99, this.height + 99).attr({stroke: "none"}); - setTimeout(function () {rect.remove();}); - }; - } else { - paperproto.safari = fun; - } - - var preventDefault = function () { - this.returnValue = false; - }, - preventTouch = function () { - return this.originalEvent.preventDefault(); - }, - stopPropagation = function () { - this.cancelBubble = true; - }, - stopTouch = function () { - return this.originalEvent.stopPropagation(); - }, - getEventPosition = function (e) { - var scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop, - scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft; - - return { - x: e.clientX + scrollX, - y: e.clientY + scrollY - }; - }, - addEvent = (function () { - if (g.doc.addEventListener) { - return function (obj, type, fn, element) { - var f = function (e) { - var pos = getEventPosition(e); - return fn.call(element, e, pos.x, pos.y); - }; - obj.addEventListener(type, f, false); - - if (supportsTouch && touchMap[type]) { - var _f = function (e) { - var pos = getEventPosition(e), - olde = e; - - for (var i = 0, ii = e.targetTouches && e.targetTouches.length; i < ii; i++) { - if (e.targetTouches[i].target == obj) { - e = e.targetTouches[i]; - e.originalEvent = olde; - e.preventDefault = preventTouch; - e.stopPropagation = stopTouch; - break; - } - } - - return fn.call(element, e, pos.x, pos.y); - }; - obj.addEventListener(touchMap[type], _f, false); - } - - return function () { - obj.removeEventListener(type, f, false); - - if (supportsTouch && touchMap[type]) - obj.removeEventListener(touchMap[type], _f, false); - - return true; - }; - }; - } else if (g.doc.attachEvent) { - return function (obj, type, fn, element) { - var f = function (e) { - e = e || g.win.event; - var scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop, - scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft, - x = e.clientX + scrollX, - y = e.clientY + scrollY; - e.preventDefault = e.preventDefault || preventDefault; - e.stopPropagation = e.stopPropagation || stopPropagation; - return fn.call(element, e, x, y); - }; - obj.attachEvent("on" + type, f); - var detacher = function () { - obj.detachEvent("on" + type, f); - return true; - }; - return detacher; - }; - } - })(), - drag = [], - dragMove = function (e) { - var x = e.clientX, - y = e.clientY, - scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop, - scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft, - dragi, - j = drag.length; - while (j--) { - dragi = drag[j]; - if (supportsTouch && e.touches) { - var i = e.touches.length, - touch; - while (i--) { - touch = e.touches[i]; - if (touch.identifier == dragi.el._drag.id) { - x = touch.clientX; - y = touch.clientY; - (e.originalEvent ? e.originalEvent : e).preventDefault(); - break; - } - } - } else { - e.preventDefault(); - } - var node = dragi.el.node, - o, - next = node.nextSibling, - parent = node.parentNode, - display = node.style.display; - g.win.opera && parent.removeChild(node); - node.style.display = "none"; - o = dragi.el.paper.getElementByPoint(x, y); - node.style.display = display; - g.win.opera && (next ? parent.insertBefore(node, next) : parent.appendChild(node)); - o && eve("raphael.drag.over." + dragi.el.id, dragi.el, o); - x += scrollX; - y += scrollY; - eve("raphael.drag.move." + dragi.el.id, dragi.move_scope || dragi.el, x - dragi.el._drag.x, y - dragi.el._drag.y, x, y, e); - } - }, - dragUp = function (e) { - R.unmousemove(dragMove).unmouseup(dragUp); - var i = drag.length, - dragi; - while (i--) { - dragi = drag[i]; - dragi.el._drag = {}; - eve("raphael.drag.end." + dragi.el.id, dragi.end_scope || dragi.start_scope || dragi.move_scope || dragi.el, e); - } - drag = []; - }, - /*\ - * Raphael.el - [ property (object) ] - ** - * You can add your own method to elements. This is usefull when you want to hack default functionality or - * want to wrap some common transformation or attributes in one method. In difference to canvas methods, - * you can redefine element method at any time. Expending element methods wouldn’t affect set. - > Usage - | Raphael.el.red = function () { - | this.attr({fill: "#f00"}); - | }; - | // then use it - | paper.circle(100, 100, 20).red(); - \*/ - elproto = R.el = {}; - /*\ - * Element.click - [ method ] - ** - * Adds event handler for click for the element. - > Parameters - - handler (function) handler for the event - = (object) @Element - \*/ - /*\ - * Element.unclick - [ method ] - ** - * Removes event handler for click for the element. - > Parameters - - handler (function) #optional handler for the event - = (object) @Element - \*/ - - /*\ - * Element.dblclick - [ method ] - ** - * Adds event handler for double click for the element. - > Parameters - - handler (function) handler for the event - = (object) @Element - \*/ - /*\ - * Element.undblclick - [ method ] - ** - * Removes event handler for double click for the element. - > Parameters - - handler (function) #optional handler for the event - = (object) @Element - \*/ - - /*\ - * Element.mousedown - [ method ] - ** - * Adds event handler for mousedown for the element. - > Parameters - - handler (function) handler for the event - = (object) @Element - \*/ - /*\ - * Element.unmousedown - [ method ] - ** - * Removes event handler for mousedown for the element. - > Parameters - - handler (function) #optional handler for the event - = (object) @Element - \*/ - - /*\ - * Element.mousemove - [ method ] - ** - * Adds event handler for mousemove for the element. - > Parameters - - handler (function) handler for the event - = (object) @Element - \*/ - /*\ - * Element.unmousemove - [ method ] - ** - * Removes event handler for mousemove for the element. - > Parameters - - handler (function) #optional handler for the event - = (object) @Element - \*/ - - /*\ - * Element.mouseout - [ method ] - ** - * Adds event handler for mouseout for the element. - > Parameters - - handler (function) handler for the event - = (object) @Element - \*/ - /*\ - * Element.unmouseout - [ method ] - ** - * Removes event handler for mouseout for the element. - > Parameters - - handler (function) #optional handler for the event - = (object) @Element - \*/ - - /*\ - * Element.mouseover - [ method ] - ** - * Adds event handler for mouseover for the element. - > Parameters - - handler (function) handler for the event - = (object) @Element - \*/ - /*\ - * Element.unmouseover - [ method ] - ** - * Removes event handler for mouseover for the element. - > Parameters - - handler (function) #optional handler for the event - = (object) @Element - \*/ - - /*\ - * Element.mouseup - [ method ] - ** - * Adds event handler for mouseup for the element. - > Parameters - - handler (function) handler for the event - = (object) @Element - \*/ - /*\ - * Element.unmouseup - [ method ] - ** - * Removes event handler for mouseup for the element. - > Parameters - - handler (function) #optional handler for the event - = (object) @Element - \*/ - - /*\ - * Element.touchstart - [ method ] - ** - * Adds event handler for touchstart for the element. - > Parameters - - handler (function) handler for the event - = (object) @Element - \*/ - /*\ - * Element.untouchstart - [ method ] - ** - * Removes event handler for touchstart for the element. - > Parameters - - handler (function) #optional handler for the event - = (object) @Element - \*/ - - /*\ - * Element.touchmove - [ method ] - ** - * Adds event handler for touchmove for the element. - > Parameters - - handler (function) handler for the event - = (object) @Element - \*/ - /*\ - * Element.untouchmove - [ method ] - ** - * Removes event handler for touchmove for the element. - > Parameters - - handler (function) #optional handler for the event - = (object) @Element - \*/ - - /*\ - * Element.touchend - [ method ] - ** - * Adds event handler for touchend for the element. - > Parameters - - handler (function) handler for the event - = (object) @Element - \*/ - /*\ - * Element.untouchend - [ method ] - ** - * Removes event handler for touchend for the element. - > Parameters - - handler (function) #optional handler for the event - = (object) @Element - \*/ - - /*\ - * Element.touchcancel - [ method ] - ** - * Adds event handler for touchcancel for the element. - > Parameters - - handler (function) handler for the event - = (object) @Element - \*/ - /*\ - * Element.untouchcancel - [ method ] - ** - * Removes event handler for touchcancel for the element. - > Parameters - - handler (function) #optional handler for the event - = (object) @Element - \*/ - for (var i = events.length; i--;) { - (function (eventName) { - R[eventName] = elproto[eventName] = function (fn, scope) { - if (R.is(fn, "function")) { - this.events = this.events || []; - this.events.push({name: eventName, f: fn, unbind: addEvent(this.shape || this.node || g.doc, eventName, fn, scope || this)}); - } - return this; - }; - R["un" + eventName] = elproto["un" + eventName] = function (fn) { - var events = this.events || [], - l = events.length; - while (l--){ - if (events[l].name == eventName && (R.is(fn, "undefined") || events[l].f == fn)) { - events[l].unbind(); - events.splice(l, 1); - !events.length && delete this.events; - } - } - return this; - }; - })(events[i]); - } - - /*\ - * Element.data - [ method ] - ** - * Adds or retrieves given value asociated with given key. - ** - * See also @Element.removeData - > Parameters - - key (string) key to store data - - value (any) #optional value to store - = (object) @Element - * or, if value is not specified: - = (any) value - * or, if key and value are not specified: - = (object) Key/value pairs for all the data associated with the element. - > Usage - | for (var i = 0, i < 5, i++) { - | paper.circle(10 + 15 * i, 10, 10) - | .attr({fill: "#000"}) - | .data("i", i) - | .click(function () { - | alert(this.data("i")); - | }); - | } - \*/ - elproto.data = function (key, value) { - var data = eldata[this.id] = eldata[this.id] || {}; - if (arguments.length == 0) { - return data; - } - if (arguments.length == 1) { - if (R.is(key, "object")) { - for (var i in key) if (key[has](i)) { - this.data(i, key[i]); - } - return this; - } - eve("raphael.data.get." + this.id, this, data[key], key); - return data[key]; - } - data[key] = value; - eve("raphael.data.set." + this.id, this, value, key); - return this; - }; - /*\ - * Element.removeData - [ method ] - ** - * Removes value associated with an element by given key. - * If key is not provided, removes all the data of the element. - > Parameters - - key (string) #optional key - = (object) @Element - \*/ - elproto.removeData = function (key) { - if (key == null) { - eldata[this.id] = {}; - } else { - eldata[this.id] && delete eldata[this.id][key]; - } - return this; - }; - /*\ - * Element.getData - [ method ] - ** - * Retrieves the element data - = (object) data - \*/ - elproto.getData = function () { - return clone(eldata[this.id] || {}); - }; - /*\ - * Element.hover - [ method ] - ** - * Adds event handlers for hover for the element. - > Parameters - - f_in (function) handler for hover in - - f_out (function) handler for hover out - - icontext (object) #optional context for hover in handler - - ocontext (object) #optional context for hover out handler - = (object) @Element - \*/ - elproto.hover = function (f_in, f_out, scope_in, scope_out) { - return this.mouseover(f_in, scope_in).mouseout(f_out, scope_out || scope_in); - }; - /*\ - * Element.unhover - [ method ] - ** - * Removes event handlers for hover for the element. - > Parameters - - f_in (function) handler for hover in - - f_out (function) handler for hover out - = (object) @Element - \*/ - elproto.unhover = function (f_in, f_out) { - return this.unmouseover(f_in).unmouseout(f_out); - }; - var draggable = []; - /*\ - * Element.drag - [ method ] - ** - * Adds event handlers for drag of the element. - > Parameters - - onmove (function) handler for moving - - onstart (function) handler for drag start - - onend (function) handler for drag end - - mcontext (object) #optional context for moving handler - - scontext (object) #optional context for drag start handler - - econtext (object) #optional context for drag end handler - * Additionaly following `drag` events will be triggered: `drag.start.<id>` on start, - * `drag.end.<id>` on end and `drag.move.<id>` on every move. When element will be dragged over another element - * `drag.over.<id>` will be fired as well. - * - * Start event and start handler will be called in specified context or in context of the element with following parameters: - o x (number) x position of the mouse - o y (number) y position of the mouse - o event (object) DOM event object - * Move event and move handler will be called in specified context or in context of the element with following parameters: - o dx (number) shift by x from the start point - o dy (number) shift by y from the start point - o x (number) x position of the mouse - o y (number) y position of the mouse - o event (object) DOM event object - * End event and end handler will be called in specified context or in context of the element with following parameters: - o event (object) DOM event object - = (object) @Element - \*/ - elproto.drag = function (onmove, onstart, onend, move_scope, start_scope, end_scope) { - function start(e) { - (e.originalEvent || e).preventDefault(); - var x = e.clientX, - y = e.clientY, - scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop, - scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft; - this._drag.id = e.identifier; - if (supportsTouch && e.touches) { - var i = e.touches.length, touch; - while (i--) { - touch = e.touches[i]; - this._drag.id = touch.identifier; - if (touch.identifier == this._drag.id) { - x = touch.clientX; - y = touch.clientY; - break; - } - } - } - this._drag.x = x + scrollX; - this._drag.y = y + scrollY; - !drag.length && R.mousemove(dragMove).mouseup(dragUp); - drag.push({el: this, move_scope: move_scope, start_scope: start_scope, end_scope: end_scope}); - onstart && eve.on("raphael.drag.start." + this.id, onstart); - onmove && eve.on("raphael.drag.move." + this.id, onmove); - onend && eve.on("raphael.drag.end." + this.id, onend); - eve("raphael.drag.start." + this.id, start_scope || move_scope || this, e.clientX + scrollX, e.clientY + scrollY, e); - } - this._drag = {}; - draggable.push({el: this, start: start}); - this.mousedown(start); - return this; - }; - /*\ - * Element.onDragOver - [ method ] - ** - * Shortcut for assigning event handler for `drag.over.<id>` event, where id is id of the element (see @Element.id). - > Parameters - - f (function) handler for event, first argument would be the element you are dragging over - \*/ - elproto.onDragOver = function (f) { - f ? eve.on("raphael.drag.over." + this.id, f) : eve.unbind("raphael.drag.over." + this.id); - }; - /*\ - * Element.undrag - [ method ] - ** - * Removes all drag event handlers from given element. - \*/ - elproto.undrag = function () { - var i = draggable.length; - while (i--) if (draggable[i].el == this) { - this.unmousedown(draggable[i].start); - draggable.splice(i, 1); - eve.unbind("raphael.drag.*." + this.id); - } - !draggable.length && R.unmousemove(dragMove).unmouseup(dragUp); - drag = []; - }; - /*\ - * Paper.circle - [ method ] - ** - * Draws a circle. - ** - > Parameters - ** - - x (number) x coordinate of the centre - - y (number) y coordinate of the centre - - r (number) radius - = (object) Raphaël element object with type “circle” - ** - > Usage - | var c = paper.circle(50, 50, 40); - \*/ - paperproto.circle = function (x, y, r) { - var out = R._engine.circle(this, x || 0, y || 0, r || 0); - this.__set__ && this.__set__.push(out); - return out; - }; - /*\ - * Paper.rect - [ method ] - * - * Draws a rectangle. - ** - > Parameters - ** - - x (number) x coordinate of the top left corner - - y (number) y coordinate of the top left corner - - width (number) width - - height (number) height - - r (number) #optional radius for rounded corners, default is 0 - = (object) Raphaël element object with type “rect” - ** - > Usage - | // regular rectangle - | var c = paper.rect(10, 10, 50, 50); - | // rectangle with rounded corners - | var c = paper.rect(40, 40, 50, 50, 10); - \*/ - paperproto.rect = function (x, y, w, h, r) { - var out = R._engine.rect(this, x || 0, y || 0, w || 0, h || 0, r || 0); - this.__set__ && this.__set__.push(out); - return out; - }; - /*\ - * Paper.ellipse - [ method ] - ** - * Draws an ellipse. - ** - > Parameters - ** - - x (number) x coordinate of the centre - - y (number) y coordinate of the centre - - rx (number) horizontal radius - - ry (number) vertical radius - = (object) Raphaël element object with type “ellipse” - ** - > Usage - | var c = paper.ellipse(50, 50, 40, 20); - \*/ - paperproto.ellipse = function (x, y, rx, ry) { - var out = R._engine.ellipse(this, x || 0, y || 0, rx || 0, ry || 0); - this.__set__ && this.__set__.push(out); - return out; - }; - /*\ - * Paper.path - [ method ] - ** - * Creates a path element by given path data string. - > Parameters - - pathString (string) #optional path string in SVG format. - * Path string consists of one-letter commands, followed by comma seprarated arguments in numercal form. Example: - | "M10,20L30,40" - * Here we can see two commands: “M”, with arguments `(10, 20)` and “L” with arguments `(30, 40)`. Upper case letter mean command is absolute, lower case—relative. - * - # <p>Here is short list of commands available, for more details see <a href="http://www.w3.org/TR/SVG/paths.html#PathData" title="Details of a path's data attribute's format are described in the SVG specification.">SVG path string format</a>.</p> - # <table><thead><tr><th>Command</th><th>Name</th><th>Parameters</th></tr></thead><tbody> - # <tr><td>M</td><td>moveto</td><td>(x y)+</td></tr> - # <tr><td>Z</td><td>closepath</td><td>(none)</td></tr> - # <tr><td>L</td><td>lineto</td><td>(x y)+</td></tr> - # <tr><td>H</td><td>horizontal lineto</td><td>x+</td></tr> - # <tr><td>V</td><td>vertical lineto</td><td>y+</td></tr> - # <tr><td>C</td><td>curveto</td><td>(x1 y1 x2 y2 x y)+</td></tr> - # <tr><td>S</td><td>smooth curveto</td><td>(x2 y2 x y)+</td></tr> - # <tr><td>Q</td><td>quadratic Bézier curveto</td><td>(x1 y1 x y)+</td></tr> - # <tr><td>T</td><td>smooth quadratic Bézier curveto</td><td>(x y)+</td></tr> - # <tr><td>A</td><td>elliptical arc</td><td>(rx ry x-axis-rotation large-arc-flag sweep-flag x y)+</td></tr> - # <tr><td>R</td><td><a href="http://en.wikipedia.org/wiki/Catmull–Rom_spline#Catmull.E2.80.93Rom_spline">Catmull-Rom curveto</a>*</td><td>x1 y1 (x y)+</td></tr></tbody></table> - * * “Catmull-Rom curveto” is a not standard SVG command and added in 2.0 to make life easier. - * Note: there is a special case when path consist of just three commands: “M10,10R…z”. In this case path will smoothly connects to its beginning. - > Usage - | var c = paper.path("M10 10L90 90"); - | // draw a diagonal line: - | // move to 10,10, line to 90,90 - * For example of path strings, check out these icons: http://raphaeljs.com/icons/ - \*/ - paperproto.path = function (pathString) { - pathString && !R.is(pathString, string) && !R.is(pathString[0], array) && (pathString += E); - var out = R._engine.path(R.format[apply](R, arguments), this); - this.__set__ && this.__set__.push(out); - return out; - }; - /*\ - * Paper.image - [ method ] - ** - * Embeds an image into the surface. - ** - > Parameters - ** - - src (string) URI of the source image - - x (number) x coordinate position - - y (number) y coordinate position - - width (number) width of the image - - height (number) height of the image - = (object) Raphaël element object with type “image” - ** - > Usage - | var c = paper.image("apple.png", 10, 10, 80, 80); - \*/ - paperproto.image = function (src, x, y, w, h) { - var out = R._engine.image(this, src || "about:blank", x || 0, y || 0, w || 0, h || 0); - this.__set__ && this.__set__.push(out); - return out; - }; - /*\ - * Paper.text - [ method ] - ** - * Draws a text string. If you need line breaks, put “\n” in the string. - ** - > Parameters - ** - - x (number) x coordinate position - - y (number) y coordinate position - - text (string) The text string to draw - = (object) Raphaël element object with type “text” - ** - > Usage - | var t = paper.text(50, 50, "Raphaël\nkicks\nbutt!"); - \*/ - paperproto.text = function (x, y, text) { - var out = R._engine.text(this, x || 0, y || 0, Str(text)); - this.__set__ && this.__set__.push(out); - return out; - }; - /*\ - * Paper.set - [ method ] - ** - * Creates array-like object to keep and operate several elements at once. - * Warning: it doesn’t create any elements for itself in the page, it just groups existing elements. - * Sets act as pseudo elements — all methods available to an element can be used on a set. - = (object) array-like object that represents set of elements - ** - > Usage - | var st = paper.set(); - | st.push( - | paper.circle(10, 10, 5), - | paper.circle(30, 10, 5) - | ); - | st.attr({fill: "red"}); // changes the fill of both circles - \*/ - paperproto.set = function (itemsArray) { - !R.is(itemsArray, "array") && (itemsArray = Array.prototype.splice.call(arguments, 0, arguments.length)); - var out = new Set(itemsArray); - this.__set__ && this.__set__.push(out); - out["paper"] = this; - out["type"] = "set"; - return out; - }; - /*\ - * Paper.setStart - [ method ] - ** - * Creates @Paper.set. All elements that will be created after calling this method and before calling - * @Paper.setFinish will be added to the set. - ** - > Usage - | paper.setStart(); - | paper.circle(10, 10, 5), - | paper.circle(30, 10, 5) - | var st = paper.setFinish(); - | st.attr({fill: "red"}); // changes the fill of both circles - \*/ - paperproto.setStart = function (set) { - this.__set__ = set || this.set(); - }; - /*\ - * Paper.setFinish - [ method ] - ** - * See @Paper.setStart. This method finishes catching and returns resulting set. - ** - = (object) set - \*/ - paperproto.setFinish = function (set) { - var out = this.__set__; - delete this.__set__; - return out; - }; - /*\ - * Paper.getSize - [ method ] - ** - * Obtains current paper actual size. - ** - = (object) - \*/ - paperproto.getSize = function () { - var container = this.canvas.parentNode; - return { - width: container.offsetWidth, - height: container.offsetHeight - }; - }; - /*\ - * Paper.setSize - [ method ] - ** - * If you need to change dimensions of the canvas call this method - ** - > Parameters - ** - - width (number) new width of the canvas - - height (number) new height of the canvas - \*/ - paperproto.setSize = function (width, height) { - return R._engine.setSize.call(this, width, height); - }; - /*\ - * Paper.setViewBox - [ method ] - ** - * Sets the view box of the paper. Practically it gives you ability to zoom and pan whole paper surface by - * specifying new boundaries. - ** - > Parameters - ** - - x (number) new x position, default is `0` - - y (number) new y position, default is `0` - - w (number) new width of the canvas - - h (number) new height of the canvas - - fit (boolean) `true` if you want graphics to fit into new boundary box - \*/ - paperproto.setViewBox = function (x, y, w, h, fit) { - return R._engine.setViewBox.call(this, x, y, w, h, fit); - }; - /*\ - * Paper.top - [ property ] - ** - * Points to the topmost element on the paper - \*/ - /*\ - * Paper.bottom - [ property ] - ** - * Points to the bottom element on the paper - \*/ - paperproto.top = paperproto.bottom = null; - /*\ - * Paper.raphael - [ property ] - ** - * Points to the @Raphael object/function - \*/ - paperproto.raphael = R; - var getOffset = function (elem) { - var box = elem.getBoundingClientRect(), - doc = elem.ownerDocument, - body = doc.body, - docElem = doc.documentElement, - clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0, - top = box.top + (g.win.pageYOffset || docElem.scrollTop || body.scrollTop ) - clientTop, - left = box.left + (g.win.pageXOffset || docElem.scrollLeft || body.scrollLeft) - clientLeft; - return { - y: top, - x: left - }; - }; - /*\ - * Paper.getElementByPoint - [ method ] - ** - * Returns you topmost element under given point. - ** - = (object) Raphaël element object - > Parameters - ** - - x (number) x coordinate from the top left corner of the window - - y (number) y coordinate from the top left corner of the window - > Usage - | paper.getElementByPoint(mouseX, mouseY).attr({stroke: "#f00"}); - \*/ - paperproto.getElementByPoint = function (x, y) { - var paper = this, - svg = paper.canvas, - target = g.doc.elementFromPoint(x, y); - if (g.win.opera && target.tagName == "svg") { - var so = getOffset(svg), - sr = svg.createSVGRect(); - sr.x = x - so.x; - sr.y = y - so.y; - sr.width = sr.height = 1; - var hits = svg.getIntersectionList(sr, null); - if (hits.length) { - target = hits[hits.length - 1]; - } - } - if (!target) { - return null; - } - while (target.parentNode && target != svg.parentNode && !target.raphael) { - target = target.parentNode; - } - target == paper.canvas.parentNode && (target = svg); - target = target && target.raphael ? paper.getById(target.raphaelid) : null; - return target; - }; - - /*\ - * Paper.getElementsByBBox - [ method ] - ** - * Returns set of elements that have an intersecting bounding box - ** - > Parameters - ** - - bbox (object) bbox to check with - = (object) @Set - \*/ - paperproto.getElementsByBBox = function (bbox) { - var set = this.set(); - this.forEach(function (el) { - if (R.isBBoxIntersect(el.getBBox(), bbox)) { - set.push(el); - } - }); - return set; - }; - - /*\ - * Paper.getById - [ method ] - ** - * Returns you element by its internal ID. - ** - > Parameters - ** - - id (number) id - = (object) Raphaël element object - \*/ - paperproto.getById = function (id) { - var bot = this.bottom; - while (bot) { - if (bot.id == id) { - return bot; - } - bot = bot.next; - } - return null; - }; - /*\ - * Paper.forEach - [ method ] - ** - * Executes given function for each element on the paper - * - * If callback function returns `false` it will stop loop running. - ** - > Parameters - ** - - callback (function) function to run - - thisArg (object) context object for the callback - = (object) Paper object - > Usage - | paper.forEach(function (el) { - | el.attr({ stroke: "blue" }); - | }); - \*/ - paperproto.forEach = function (callback, thisArg) { - var bot = this.bottom; - while (bot) { - if (callback.call(thisArg, bot) === false) { - return this; - } - bot = bot.next; - } - return this; - }; - /*\ - * Paper.getElementsByPoint - [ method ] - ** - * Returns set of elements that have common point inside - ** - > Parameters - ** - - x (number) x coordinate of the point - - y (number) y coordinate of the point - = (object) @Set - \*/ - paperproto.getElementsByPoint = function (x, y) { - var set = this.set(); - this.forEach(function (el) { - if (el.isPointInside(x, y)) { - set.push(el); - } - }); - return set; - }; - function x_y() { - return this.x + S + this.y; - } - function x_y_w_h() { - return this.x + S + this.y + S + this.width + " \xd7 " + this.height; - } - /*\ - * Element.isPointInside - [ method ] - ** - * Determine if given point is inside this element’s shape - ** - > Parameters - ** - - x (number) x coordinate of the point - - y (number) y coordinate of the point - = (boolean) `true` if point inside the shape - \*/ - elproto.isPointInside = function (x, y) { - var rp = this.realPath = getPath[this.type](this); - if (this.attr('transform') && this.attr('transform').length) { - rp = R.transformPath(rp, this.attr('transform')); - } - return R.isPointInsidePath(rp, x, y); - }; - /*\ - * Element.getBBox - [ method ] - ** - * Return bounding box for a given element - ** - > Parameters - ** - - isWithoutTransform (boolean) flag, `true` if you want to have bounding box before transformations. Default is `false`. - = (object) Bounding box object: - o { - o x: (number) top left corner x - o y: (number) top left corner y - o x2: (number) bottom right corner x - o y2: (number) bottom right corner y - o width: (number) width - o height: (number) height - o } - \*/ - elproto.getBBox = function (isWithoutTransform) { - if (this.removed) { - return {}; - } - var _ = this._; - if (isWithoutTransform) { - if (_.dirty || !_.bboxwt) { - this.realPath = getPath[this.type](this); - _.bboxwt = pathDimensions(this.realPath); - _.bboxwt.toString = x_y_w_h; - _.dirty = 0; - } - return _.bboxwt; - } - if (_.dirty || _.dirtyT || !_.bbox) { - if (_.dirty || !this.realPath) { - _.bboxwt = 0; - this.realPath = getPath[this.type](this); - } - _.bbox = pathDimensions(mapPath(this.realPath, this.matrix)); - _.bbox.toString = x_y_w_h; - _.dirty = _.dirtyT = 0; - } - return _.bbox; - }; - /*\ - * Element.clone - [ method ] - ** - = (object) clone of a given element - ** - \*/ - elproto.clone = function () { - if (this.removed) { - return null; - } - var out = this.paper[this.type]().attr(this.attr()); - this.__set__ && this.__set__.push(out); - return out; - }; - /*\ - * Element.glow - [ method ] - ** - * Return set of elements that create glow-like effect around given element. See @Paper.set. - * - * Note: Glow is not connected to the element. If you change element attributes it won’t adjust itself. - ** - > Parameters - ** - - glow (object) #optional parameters object with all properties optional: - o { - o width (number) size of the glow, default is `10` - o fill (boolean) will it be filled, default is `false` - o opacity (number) opacity, default is `0.5` - o offsetx (number) horizontal offset, default is `0` - o offsety (number) vertical offset, default is `0` - o color (string) glow colour, default is `black` - o } - = (object) @Paper.set of elements that represents glow - \*/ - elproto.glow = function (glow) { - if (this.type == "text") { - return null; - } - glow = glow || {}; - var s = { - width: (glow.width || 10) + (+this.attr("stroke-width") || 1), - fill: glow.fill || false, - opacity: glow.opacity || .5, - offsetx: glow.offsetx || 0, - offsety: glow.offsety || 0, - color: glow.color || "#000" - }, - c = s.width / 2, - r = this.paper, - out = r.set(), - path = this.realPath || getPath[this.type](this); - path = this.matrix ? mapPath(path, this.matrix) : path; - for (var i = 1; i < c + 1; i++) { - out.push(r.path(path).attr({ - stroke: s.color, - fill: s.fill ? s.color : "none", - "stroke-linejoin": "round", - "stroke-linecap": "round", - "stroke-width": +(s.width / c * i).toFixed(3), - opacity: +(s.opacity / c).toFixed(3) - })); - } - return out.insertBefore(this).translate(s.offsetx, s.offsety); - }; - var curveslengths = {}, - getPointAtSegmentLength = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, length) { - if (length == null) { - return bezlen(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y); - } else { - return R.findDotsAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, getTatLen(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, length)); - } - }, - getLengthFactory = function (istotal, subpath) { - return function (path, length, onlystart) { - path = path2curve(path); - var x, y, p, l, sp = "", subpaths = {}, point, - len = 0; - for (var i = 0, ii = path.length; i < ii; i++) { - p = path[i]; - if (p[0] == "M") { - x = +p[1]; - y = +p[2]; - } else { - l = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6]); - if (len + l > length) { - if (subpath && !subpaths.start) { - point = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6], length - len); - sp += ["C" + point.start.x, point.start.y, point.m.x, point.m.y, point.x, point.y]; - if (onlystart) {return sp;} - subpaths.start = sp; - sp = ["M" + point.x, point.y + "C" + point.n.x, point.n.y, point.end.x, point.end.y, p[5], p[6]].join(); - len += l; - x = +p[5]; - y = +p[6]; - continue; - } - if (!istotal && !subpath) { - point = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6], length - len); - return {x: point.x, y: point.y, alpha: point.alpha}; - } - } - len += l; - x = +p[5]; - y = +p[6]; - } - sp += p.shift() + p; - } - subpaths.end = sp; - point = istotal ? len : subpath ? subpaths : R.findDotsAtSegment(x, y, p[0], p[1], p[2], p[3], p[4], p[5], 1); - point.alpha && (point = {x: point.x, y: point.y, alpha: point.alpha}); - return point; - }; - }; - var getTotalLength = getLengthFactory(1), - getPointAtLength = getLengthFactory(), - getSubpathsAtLength = getLengthFactory(0, 1); - /*\ - * Raphael.getTotalLength - [ method ] - ** - * Returns length of the given path in pixels. - ** - > Parameters - ** - - path (string) SVG path string. - ** - = (number) length. - \*/ - R.getTotalLength = getTotalLength; - /*\ - * Raphael.getPointAtLength - [ method ] - ** - * Return coordinates of the point located at the given length on the given path. - ** - > Parameters - ** - - path (string) SVG path string - - length (number) - ** - = (object) representation of the point: - o { - o x: (number) x coordinate - o y: (number) y coordinate - o alpha: (number) angle of derivative - o } - \*/ - R.getPointAtLength = getPointAtLength; - /*\ - * Raphael.getSubpath - [ method ] - ** - * Return subpath of a given path from given length to given length. - ** - > Parameters - ** - - path (string) SVG path string - - from (number) position of the start of the segment - - to (number) position of the end of the segment - ** - = (string) pathstring for the segment - \*/ - R.getSubpath = function (path, from, to) { - if (this.getTotalLength(path) - to < 1e-6) { - return getSubpathsAtLength(path, from).end; - } - var a = getSubpathsAtLength(path, to, 1); - return from ? getSubpathsAtLength(a, from).end : a; - }; - /*\ - * Element.getTotalLength - [ method ] - ** - * Returns length of the path in pixels. Only works for element of “path” type. - = (number) length. - \*/ - elproto.getTotalLength = function () { - var path = this.getPath(); - if (!path) { - return; - } - - if (this.node.getTotalLength) { - return this.node.getTotalLength(); - } - - return getTotalLength(path); - }; - /*\ - * Element.getPointAtLength - [ method ] - ** - * Return coordinates of the point located at the given length on the given path. Only works for element of “path” type. - ** - > Parameters - ** - - length (number) - ** - = (object) representation of the point: - o { - o x: (number) x coordinate - o y: (number) y coordinate - o alpha: (number) angle of derivative - o } - \*/ - elproto.getPointAtLength = function (length) { - var path = this.getPath(); - if (!path) { - return; - } - - return getPointAtLength(path, length); - }; - /*\ - * Element.getPath - [ method ] - ** - * Returns path of the element. Only works for elements of “path” type and simple elements like circle. - = (object) path - ** - \*/ - elproto.getPath = function () { - var path, - getPath = R._getPath[this.type]; - - if (this.type == "text" || this.type == "set") { - return; - } - - if (getPath) { - path = getPath(this); - } - - return path; - }; - /*\ - * Element.getSubpath - [ method ] - ** - * Return subpath of a given element from given length to given length. Only works for element of “path” type. - ** - > Parameters - ** - - from (number) position of the start of the segment - - to (number) position of the end of the segment - ** - = (string) pathstring for the segment - \*/ - elproto.getSubpath = function (from, to) { - var path = this.getPath(); - if (!path) { - return; - } - - return R.getSubpath(path, from, to); - }; - /*\ - * Raphael.easing_formulas - [ property ] - ** - * Object that contains easing formulas for animation. You could extend it with your own. By default it has following list of easing: - # <ul> - # <li>“linear”</li> - # <li>“<” or “easeIn” or “ease-in”</li> - # <li>“>” or “easeOut” or “ease-out”</li> - # <li>“<>” or “easeInOut” or “ease-in-out”</li> - # <li>“backIn” or “back-in”</li> - # <li>“backOut” or “back-out”</li> - # <li>“elastic”</li> - # <li>“bounce”</li> - # </ul> - # <p>See also <a href="http://raphaeljs.com/easing.html">Easing demo</a>.</p> - \*/ - var ef = R.easing_formulas = { - linear: function (n) { - return n; - }, - "<": function (n) { - return pow(n, 1.7); - }, - ">": function (n) { - return pow(n, .48); - }, - "<>": function (n) { - var q = .48 - n / 1.04, - Q = math.sqrt(.1734 + q * q), - x = Q - q, - X = pow(abs(x), 1 / 3) * (x < 0 ? -1 : 1), - y = -Q - q, - Y = pow(abs(y), 1 / 3) * (y < 0 ? -1 : 1), - t = X + Y + .5; - return (1 - t) * 3 * t * t + t * t * t; - }, - backIn: function (n) { - var s = 1.70158; - return n * n * ((s + 1) * n - s); - }, - backOut: function (n) { - n = n - 1; - var s = 1.70158; - return n * n * ((s + 1) * n + s) + 1; - }, - elastic: function (n) { - if (n == !!n) { - return n; - } - return pow(2, -10 * n) * math.sin((n - .075) * (2 * PI) / .3) + 1; - }, - bounce: function (n) { - var s = 7.5625, - p = 2.75, - l; - if (n < (1 / p)) { - l = s * n * n; - } else { - if (n < (2 / p)) { - n -= (1.5 / p); - l = s * n * n + .75; - } else { - if (n < (2.5 / p)) { - n -= (2.25 / p); - l = s * n * n + .9375; - } else { - n -= (2.625 / p); - l = s * n * n + .984375; - } - } - } - return l; - } - }; - ef.easeIn = ef["ease-in"] = ef["<"]; - ef.easeOut = ef["ease-out"] = ef[">"]; - ef.easeInOut = ef["ease-in-out"] = ef["<>"]; - ef["back-in"] = ef.backIn; - ef["back-out"] = ef.backOut; - - var animationElements = [], - requestAnimFrame = window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function (callback) { - setTimeout(callback, 16); - }, - animation = function () { - var Now = +new Date, - l = 0; - for (; l < animationElements.length; l++) { - var e = animationElements[l]; - if (e.el.removed || e.paused) { - continue; - } - var time = Now - e.start, - ms = e.ms, - easing = e.easing, - from = e.from, - diff = e.diff, - to = e.to, - t = e.t, - that = e.el, - set = {}, - now, - init = {}, - key; - if (e.initstatus) { - time = (e.initstatus * e.anim.top - e.prev) / (e.percent - e.prev) * ms; - e.status = e.initstatus; - delete e.initstatus; - e.stop && animationElements.splice(l--, 1); - } else { - e.status = (e.prev + (e.percent - e.prev) * (time / ms)) / e.anim.top; - } - if (time < 0) { - continue; - } - if (time < ms) { - var pos = easing(time / ms); - for (var attr in from) if (from[has](attr)) { - switch (availableAnimAttrs[attr]) { - case nu: - now = +from[attr] + pos * ms * diff[attr]; - break; - case "colour": - now = "rgb(" + [ - upto255(round(from[attr].r + pos * ms * diff[attr].r)), - upto255(round(from[attr].g + pos * ms * diff[attr].g)), - upto255(round(from[attr].b + pos * ms * diff[attr].b)) - ].join(",") + ")"; - break; - case "path": - now = []; - for (var i = 0, ii = from[attr].length; i < ii; i++) { - now[i] = [from[attr][i][0]]; - for (var j = 1, jj = from[attr][i].length; j < jj; j++) { - now[i][j] = +from[attr][i][j] + pos * ms * diff[attr][i][j]; - } - now[i] = now[i].join(S); - } - now = now.join(S); - break; - case "transform": - if (diff[attr].real) { - now = []; - for (i = 0, ii = from[attr].length; i < ii; i++) { - now[i] = [from[attr][i][0]]; - for (j = 1, jj = from[attr][i].length; j < jj; j++) { - now[i][j] = from[attr][i][j] + pos * ms * diff[attr][i][j]; - } - } - } else { - var get = function (i) { - return +from[attr][i] + pos * ms * diff[attr][i]; - }; - // now = [["r", get(2), 0, 0], ["t", get(3), get(4)], ["s", get(0), get(1), 0, 0]]; - now = [["m", get(0), get(1), get(2), get(3), get(4), get(5)]]; - } - break; - case "csv": - if (attr == "clip-rect") { - now = []; - i = 4; - while (i--) { - now[i] = +from[attr][i] + pos * ms * diff[attr][i]; - } - } - break; - default: - var from2 = [][concat](from[attr]); - now = []; - i = that.paper.customAttributes[attr].length; - while (i--) { - now[i] = +from2[i] + pos * ms * diff[attr][i]; - } - break; - } - set[attr] = now; - } - that.attr(set); - (function (id, that, anim) { - setTimeout(function () { - eve("raphael.anim.frame." + id, that, anim); - }); - })(that.id, that, e.anim); - } else { - (function(f, el, a) { - setTimeout(function() { - eve("raphael.anim.frame." + el.id, el, a); - eve("raphael.anim.finish." + el.id, el, a); - R.is(f, "function") && f.call(el); - }); - })(e.callback, that, e.anim); - that.attr(to); - animationElements.splice(l--, 1); - if (e.repeat > 1 && !e.next) { - for (key in to) if (to[has](key)) { - init[key] = e.totalOrigin[key]; - } - e.el.attr(init); - runAnimation(e.anim, e.el, e.anim.percents[0], null, e.totalOrigin, e.repeat - 1); - } - if (e.next && !e.stop) { - runAnimation(e.anim, e.el, e.next, null, e.totalOrigin, e.repeat); - } - } - } - R.svg && that && that.paper && that.paper.safari(); - animationElements.length && requestAnimFrame(animation); - }, - upto255 = function (color) { - return color > 255 ? 255 : color < 0 ? 0 : color; - }; - /*\ - * Element.animateWith - [ method ] - ** - * Acts similar to @Element.animate, but ensure that given animation runs in sync with another given element. - ** - > Parameters - ** - - el (object) element to sync with - - anim (object) animation to sync with - - params (object) #optional final attributes for the element, see also @Element.attr - - ms (number) #optional number of milliseconds for animation to run - - easing (string) #optional easing type. Accept on of @Raphael.easing_formulas or CSS format: `cubic‐bezier(XX, XX, XX, XX)` - - callback (function) #optional callback function. Will be called at the end of animation. - * or - - element (object) element to sync with - - anim (object) animation to sync with - - animation (object) #optional animation object, see @Raphael.animation - ** - = (object) original element - \*/ - elproto.animateWith = function (el, anim, params, ms, easing, callback) { - var element = this; - if (element.removed) { - callback && callback.call(element); - return element; - } - var a = params instanceof Animation ? params : R.animation(params, ms, easing, callback), - x, y; - runAnimation(a, element, a.percents[0], null, element.attr()); - for (var i = 0, ii = animationElements.length; i < ii; i++) { - if (animationElements[i].anim == anim && animationElements[i].el == el) { - animationElements[ii - 1].start = animationElements[i].start; - break; - } - } - return element; - // - // - // var a = params ? R.animation(params, ms, easing, callback) : anim, - // status = element.status(anim); - // return this.animate(a).status(a, status * anim.ms / a.ms); - }; - function CubicBezierAtTime(t, p1x, p1y, p2x, p2y, duration) { - var cx = 3 * p1x, - bx = 3 * (p2x - p1x) - cx, - ax = 1 - cx - bx, - cy = 3 * p1y, - by = 3 * (p2y - p1y) - cy, - ay = 1 - cy - by; - function sampleCurveX(t) { - return ((ax * t + bx) * t + cx) * t; - } - function solve(x, epsilon) { - var t = solveCurveX(x, epsilon); - return ((ay * t + by) * t + cy) * t; - } - function solveCurveX(x, epsilon) { - var t0, t1, t2, x2, d2, i; - for(t2 = x, i = 0; i < 8; i++) { - x2 = sampleCurveX(t2) - x; - if (abs(x2) < epsilon) { - return t2; - } - d2 = (3 * ax * t2 + 2 * bx) * t2 + cx; - if (abs(d2) < 1e-6) { - break; - } - t2 = t2 - x2 / d2; - } - t0 = 0; - t1 = 1; - t2 = x; - if (t2 < t0) { - return t0; - } - if (t2 > t1) { - return t1; - } - while (t0 < t1) { - x2 = sampleCurveX(t2); - if (abs(x2 - x) < epsilon) { - return t2; - } - if (x > x2) { - t0 = t2; - } else { - t1 = t2; - } - t2 = (t1 - t0) / 2 + t0; - } - return t2; - } - return solve(t, 1 / (200 * duration)); - } - elproto.onAnimation = function (f) { - f ? eve.on("raphael.anim.frame." + this.id, f) : eve.unbind("raphael.anim.frame." + this.id); - return this; - }; - function Animation(anim, ms) { - var percents = [], - newAnim = {}; - this.ms = ms; - this.times = 1; - if (anim) { - for (var attr in anim) if (anim[has](attr)) { - newAnim[toFloat(attr)] = anim[attr]; - percents.push(toFloat(attr)); - } - percents.sort(sortByNumber); - } - this.anim = newAnim; - this.top = percents[percents.length - 1]; - this.percents = percents; - } - /*\ - * Animation.delay - [ method ] - ** - * Creates a copy of existing animation object with given delay. - ** - > Parameters - ** - - delay (number) number of ms to pass between animation start and actual animation - ** - = (object) new altered Animation object - | var anim = Raphael.animation({cx: 10, cy: 20}, 2e3); - | circle1.animate(anim); // run the given animation immediately - | circle2.animate(anim.delay(500)); // run the given animation after 500 ms - \*/ - Animation.prototype.delay = function (delay) { - var a = new Animation(this.anim, this.ms); - a.times = this.times; - a.del = +delay || 0; - return a; - }; - /*\ - * Animation.repeat - [ method ] - ** - * Creates a copy of existing animation object with given repetition. - ** - > Parameters - ** - - repeat (number) number iterations of animation. For infinite animation pass `Infinity` - ** - = (object) new altered Animation object - \*/ - Animation.prototype.repeat = function (times) { - var a = new Animation(this.anim, this.ms); - a.del = this.del; - a.times = math.floor(mmax(times, 0)) || 1; - return a; - }; - function runAnimation(anim, element, percent, status, totalOrigin, times) { - percent = toFloat(percent); - var params, - isInAnim, - isInAnimSet, - percents = [], - next, - prev, - timestamp, - ms = anim.ms, - from = {}, - to = {}, - diff = {}; - if (status) { - for (i = 0, ii = animationElements.length; i < ii; i++) { - var e = animationElements[i]; - if (e.el.id == element.id && e.anim == anim) { - if (e.percent != percent) { - animationElements.splice(i, 1); - isInAnimSet = 1; - } else { - isInAnim = e; - } - element.attr(e.totalOrigin); - break; - } - } - } else { - status = +to; // NaN - } - for (var i = 0, ii = anim.percents.length; i < ii; i++) { - if (anim.percents[i] == percent || anim.percents[i] > status * anim.top) { - percent = anim.percents[i]; - prev = anim.percents[i - 1] || 0; - ms = ms / anim.top * (percent - prev); - next = anim.percents[i + 1]; - params = anim.anim[percent]; - break; - } else if (status) { - element.attr(anim.anim[anim.percents[i]]); - } - } - if (!params) { - return; - } - if (!isInAnim) { - for (var attr in params) if (params[has](attr)) { - if (availableAnimAttrs[has](attr) || element.paper.customAttributes[has](attr)) { - from[attr] = element.attr(attr); - (from[attr] == null) && (from[attr] = availableAttrs[attr]); - to[attr] = params[attr]; - switch (availableAnimAttrs[attr]) { - case nu: - diff[attr] = (to[attr] - from[attr]) / ms; - break; - case "colour": - from[attr] = R.getRGB(from[attr]); - var toColour = R.getRGB(to[attr]); - diff[attr] = { - r: (toColour.r - from[attr].r) / ms, - g: (toColour.g - from[attr].g) / ms, - b: (toColour.b - from[attr].b) / ms - }; - break; - case "path": - var pathes = path2curve(from[attr], to[attr]), - toPath = pathes[1]; - from[attr] = pathes[0]; - diff[attr] = []; - for (i = 0, ii = from[attr].length; i < ii; i++) { - diff[attr][i] = [0]; - for (var j = 1, jj = from[attr][i].length; j < jj; j++) { - diff[attr][i][j] = (toPath[i][j] - from[attr][i][j]) / ms; - } - } - break; - case "transform": - var _ = element._, - eq = equaliseTransform(_[attr], to[attr]); - if (eq) { - from[attr] = eq.from; - to[attr] = eq.to; - diff[attr] = []; - diff[attr].real = true; - for (i = 0, ii = from[attr].length; i < ii; i++) { - diff[attr][i] = [from[attr][i][0]]; - for (j = 1, jj = from[attr][i].length; j < jj; j++) { - diff[attr][i][j] = (to[attr][i][j] - from[attr][i][j]) / ms; - } - } - } else { - var m = (element.matrix || new Matrix), - to2 = { - _: {transform: _.transform}, - getBBox: function () { - return element.getBBox(1); - } - }; - from[attr] = [ - m.a, - m.b, - m.c, - m.d, - m.e, - m.f - ]; - extractTransform(to2, to[attr]); - to[attr] = to2._.transform; - diff[attr] = [ - (to2.matrix.a - m.a) / ms, - (to2.matrix.b - m.b) / ms, - (to2.matrix.c - m.c) / ms, - (to2.matrix.d - m.d) / ms, - (to2.matrix.e - m.e) / ms, - (to2.matrix.f - m.f) / ms - ]; - // from[attr] = [_.sx, _.sy, _.deg, _.dx, _.dy]; - // var to2 = {_:{}, getBBox: function () { return element.getBBox(); }}; - // extractTransform(to2, to[attr]); - // diff[attr] = [ - // (to2._.sx - _.sx) / ms, - // (to2._.sy - _.sy) / ms, - // (to2._.deg - _.deg) / ms, - // (to2._.dx - _.dx) / ms, - // (to2._.dy - _.dy) / ms - // ]; - } - break; - case "csv": - var values = Str(params[attr])[split](separator), - from2 = Str(from[attr])[split](separator); - if (attr == "clip-rect") { - from[attr] = from2; - diff[attr] = []; - i = from2.length; - while (i--) { - diff[attr][i] = (values[i] - from[attr][i]) / ms; - } - } - to[attr] = values; - break; - default: - values = [][concat](params[attr]); - from2 = [][concat](from[attr]); - diff[attr] = []; - i = element.paper.customAttributes[attr].length; - while (i--) { - diff[attr][i] = ((values[i] || 0) - (from2[i] || 0)) / ms; - } - break; - } - } - } - var easing = params.easing, - easyeasy = R.easing_formulas[easing]; - if (!easyeasy) { - easyeasy = Str(easing).match(bezierrg); - if (easyeasy && easyeasy.length == 5) { - var curve = easyeasy; - easyeasy = function (t) { - return CubicBezierAtTime(t, +curve[1], +curve[2], +curve[3], +curve[4], ms); - }; - } else { - easyeasy = pipe; - } - } - timestamp = params.start || anim.start || +new Date; - e = { - anim: anim, - percent: percent, - timestamp: timestamp, - start: timestamp + (anim.del || 0), - status: 0, - initstatus: status || 0, - stop: false, - ms: ms, - easing: easyeasy, - from: from, - diff: diff, - to: to, - el: element, - callback: params.callback, - prev: prev, - next: next, - repeat: times || anim.times, - origin: element.attr(), - totalOrigin: totalOrigin - }; - animationElements.push(e); - if (status && !isInAnim && !isInAnimSet) { - e.stop = true; - e.start = new Date - ms * status; - if (animationElements.length == 1) { - return animation(); - } - } - if (isInAnimSet) { - e.start = new Date - e.ms * status; - } - animationElements.length == 1 && requestAnimFrame(animation); - } else { - isInAnim.initstatus = status; - isInAnim.start = new Date - isInAnim.ms * status; - } - eve("raphael.anim.start." + element.id, element, anim); - } - /*\ - * Raphael.animation - [ method ] - ** - * Creates an animation object that can be passed to the @Element.animate or @Element.animateWith methods. - * See also @Animation.delay and @Animation.repeat methods. - ** - > Parameters - ** - - params (object) final attributes for the element, see also @Element.attr - - ms (number) number of milliseconds for animation to run - - easing (string) #optional easing type. Accept one of @Raphael.easing_formulas or CSS format: `cubic‐bezier(XX, XX, XX, XX)` - - callback (function) #optional callback function. Will be called at the end of animation. - ** - = (object) @Animation - \*/ - R.animation = function (params, ms, easing, callback) { - if (params instanceof Animation) { - return params; - } - if (R.is(easing, "function") || !easing) { - callback = callback || easing || null; - easing = null; - } - params = Object(params); - ms = +ms || 0; - var p = {}, - json, - attr; - for (attr in params) if (params[has](attr) && toFloat(attr) != attr && toFloat(attr) + "%" != attr) { - json = true; - p[attr] = params[attr]; - } - if (!json) { - // if percent-like syntax is used and end-of-all animation callback used - if(callback){ - // find the last one - var lastKey = 0; - for(var i in params){ - var percent = toInt(i); - if(params[has](i) && percent > lastKey){ - lastKey = percent; - } - } - lastKey += '%'; - // if already defined callback in the last keyframe, skip - !params[lastKey].callback && (params[lastKey].callback = callback); - } - return new Animation(params, ms); - } else { - easing && (p.easing = easing); - callback && (p.callback = callback); - return new Animation({100: p}, ms); - } - }; - /*\ - * Element.animate - [ method ] - ** - * Creates and starts animation for given element. - ** - > Parameters - ** - - params (object) final attributes for the element, see also @Element.attr - - ms (number) number of milliseconds for animation to run - - easing (string) #optional easing type. Accept one of @Raphael.easing_formulas or CSS format: `cubic‐bezier(XX, XX, XX, XX)` - - callback (function) #optional callback function. Will be called at the end of animation. - * or - - animation (object) animation object, see @Raphael.animation - ** - = (object) original element - \*/ - elproto.animate = function (params, ms, easing, callback) { - var element = this; - if (element.removed) { - callback && callback.call(element); - return element; - } - var anim = params instanceof Animation ? params : R.animation(params, ms, easing, callback); - runAnimation(anim, element, anim.percents[0], null, element.attr()); - return element; - }; - /*\ - * Element.setTime - [ method ] - ** - * Sets the status of animation of the element in milliseconds. Similar to @Element.status method. - ** - > Parameters - ** - - anim (object) animation object - - value (number) number of milliseconds from the beginning of the animation - ** - = (object) original element if `value` is specified - * Note, that during animation following events are triggered: - * - * On each animation frame event `anim.frame.<id>`, on start `anim.start.<id>` and on end `anim.finish.<id>`. - \*/ - elproto.setTime = function (anim, value) { - if (anim && value != null) { - this.status(anim, mmin(value, anim.ms) / anim.ms); - } - return this; - }; - /*\ - * Element.status - [ method ] - ** - * Gets or sets the status of animation of the element. - ** - > Parameters - ** - - anim (object) #optional animation object - - value (number) #optional 0 – 1. If specified, method works like a setter and sets the status of a given animation to the value. This will cause animation to jump to the given position. - ** - = (number) status - * or - = (array) status if `anim` is not specified. Array of objects in format: - o { - o anim: (object) animation object - o status: (number) status - o } - * or - = (object) original element if `value` is specified - \*/ - elproto.status = function (anim, value) { - var out = [], - i = 0, - len, - e; - if (value != null) { - runAnimation(anim, this, -1, mmin(value, 1)); - return this; - } else { - len = animationElements.length; - for (; i < len; i++) { - e = animationElements[i]; - if (e.el.id == this.id && (!anim || e.anim == anim)) { - if (anim) { - return e.status; - } - out.push({ - anim: e.anim, - status: e.status - }); - } - } - if (anim) { - return 0; - } - return out; - } - }; - /*\ - * Element.pause - [ method ] - ** - * Stops animation of the element with ability to resume it later on. - ** - > Parameters - ** - - anim (object) #optional animation object - ** - = (object) original element - \*/ - elproto.pause = function (anim) { - for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) { - if (eve("raphael.anim.pause." + this.id, this, animationElements[i].anim) !== false) { - animationElements[i].paused = true; - } - } - return this; - }; - /*\ - * Element.resume - [ method ] - ** - * Resumes animation if it was paused with @Element.pause method. - ** - > Parameters - ** - - anim (object) #optional animation object - ** - = (object) original element - \*/ - elproto.resume = function (anim) { - for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) { - var e = animationElements[i]; - if (eve("raphael.anim.resume." + this.id, this, e.anim) !== false) { - delete e.paused; - this.status(e.anim, e.status); - } - } - return this; - }; - /*\ - * Element.stop - [ method ] - ** - * Stops animation of the element. - ** - > Parameters - ** - - anim (object) #optional animation object - ** - = (object) original element - \*/ - elproto.stop = function (anim) { - for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) { - if (eve("raphael.anim.stop." + this.id, this, animationElements[i].anim) !== false) { - animationElements.splice(i--, 1); - } - } - return this; - }; - function stopAnimation(paper) { - for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.paper == paper) { - animationElements.splice(i--, 1); - } - } - eve.on("raphael.remove", stopAnimation); - eve.on("raphael.clear", stopAnimation); - elproto.toString = function () { - return "Rapha\xebl\u2019s object"; - }; - - // Set - var Set = function (items) { - this.items = []; - this.length = 0; - this.type = "set"; - if (items) { - for (var i = 0, ii = items.length; i < ii; i++) { - if (items[i] && (items[i].constructor == elproto.constructor || items[i].constructor == Set)) { - this[this.items.length] = this.items[this.items.length] = items[i]; - this.length++; - } - } - } - }, - setproto = Set.prototype; - /*\ - * Set.push - [ method ] - ** - * Adds each argument to the current set. - = (object) original element - \*/ - setproto.push = function () { - var item, - len; - for (var i = 0, ii = arguments.length; i < ii; i++) { - item = arguments[i]; - if (item && (item.constructor == elproto.constructor || item.constructor == Set)) { - len = this.items.length; - this[len] = this.items[len] = item; - this.length++; - } - } - return this; - }; - /*\ - * Set.pop - [ method ] - ** - * Removes last element and returns it. - = (object) element - \*/ - setproto.pop = function () { - this.length && delete this[this.length--]; - return this.items.pop(); - }; - /*\ - * Set.forEach - [ method ] - ** - * Executes given function for each element in the set. - * - * If function returns `false` it will stop loop running. - ** - > Parameters - ** - - callback (function) function to run - - thisArg (object) context object for the callback - = (object) Set object - \*/ - setproto.forEach = function (callback, thisArg) { - for (var i = 0, ii = this.items.length; i < ii; i++) { - if (callback.call(thisArg, this.items[i], i) === false) { - return this; - } - } - return this; - }; - for (var method in elproto) if (elproto[has](method)) { - setproto[method] = (function (methodname) { - return function () { - var arg = arguments; - return this.forEach(function (el) { - el[methodname][apply](el, arg); - }); - }; - })(method); - } - setproto.attr = function (name, value) { - if (name && R.is(name, array) && R.is(name[0], "object")) { - for (var j = 0, jj = name.length; j < jj; j++) { - this.items[j].attr(name[j]); - } - } else { - for (var i = 0, ii = this.items.length; i < ii; i++) { - this.items[i].attr(name, value); - } - } - return this; - }; - /*\ - * Set.clear - [ method ] - ** - * Removes all elements from the set - \*/ - setproto.clear = function () { - while (this.length) { - this.pop(); - } - }; - /*\ - * Set.splice - [ method ] - ** - * Removes given element from the set - ** - > Parameters - ** - - index (number) position of the deletion - - count (number) number of element to remove - - insertion… (object) #optional elements to insert - = (object) set elements that were deleted - \*/ - setproto.splice = function (index, count, insertion) { - index = index < 0 ? mmax(this.length + index, 0) : index; - count = mmax(0, mmin(this.length - index, count)); - var tail = [], - todel = [], - args = [], - i; - for (i = 2; i < arguments.length; i++) { - args.push(arguments[i]); - } - for (i = 0; i < count; i++) { - todel.push(this[index + i]); - } - for (; i < this.length - index; i++) { - tail.push(this[index + i]); - } - var arglen = args.length; - for (i = 0; i < arglen + tail.length; i++) { - this.items[index + i] = this[index + i] = i < arglen ? args[i] : tail[i - arglen]; - } - i = this.items.length = this.length -= count - arglen; - while (this[i]) { - delete this[i++]; - } - return new Set(todel); - }; - /*\ - * Set.exclude - [ method ] - ** - * Removes given element from the set - ** - > Parameters - ** - - element (object) element to remove - = (boolean) `true` if object was found & removed from the set - \*/ - setproto.exclude = function (el) { - for (var i = 0, ii = this.length; i < ii; i++) if (this[i] == el) { - this.splice(i, 1); - return true; - } - }; - setproto.animate = function (params, ms, easing, callback) { - (R.is(easing, "function") || !easing) && (callback = easing || null); - var len = this.items.length, - i = len, - item, - set = this, - collector; - if (!len) { - return this; - } - callback && (collector = function () { - !--len && callback.call(set); - }); - easing = R.is(easing, string) ? easing : collector; - var anim = R.animation(params, ms, easing, collector); - item = this.items[--i].animate(anim); - while (i--) { - this.items[i] && !this.items[i].removed && this.items[i].animateWith(item, anim, anim); - (this.items[i] && !this.items[i].removed) || len--; - } - return this; - }; - setproto.insertAfter = function (el) { - var i = this.items.length; - while (i--) { - this.items[i].insertAfter(el); - } - return this; - }; - setproto.getBBox = function () { - var x = [], - y = [], - x2 = [], - y2 = []; - for (var i = this.items.length; i--;) if (!this.items[i].removed) { - var box = this.items[i].getBBox(); - x.push(box.x); - y.push(box.y); - x2.push(box.x + box.width); - y2.push(box.y + box.height); - } - x = mmin[apply](0, x); - y = mmin[apply](0, y); - x2 = mmax[apply](0, x2); - y2 = mmax[apply](0, y2); - return { - x: x, - y: y, - x2: x2, - y2: y2, - width: x2 - x, - height: y2 - y - }; - }; - setproto.clone = function (s) { - s = this.paper.set(); - for (var i = 0, ii = this.items.length; i < ii; i++) { - s.push(this.items[i].clone()); - } - return s; - }; - setproto.toString = function () { - return "Rapha\xebl\u2018s set"; - }; - - setproto.glow = function(glowConfig) { - var ret = this.paper.set(); - this.forEach(function(shape, index){ - var g = shape.glow(glowConfig); - if(g != null){ - g.forEach(function(shape2, index2){ - ret.push(shape2); - }); - } - }); - return ret; - }; - - - /*\ - * Set.isPointInside - [ method ] - ** - * Determine if given point is inside this set’s elements - ** - > Parameters - ** - - x (number) x coordinate of the point - - y (number) y coordinate of the point - = (boolean) `true` if point is inside any of the set's elements - \*/ - setproto.isPointInside = function (x, y) { - var isPointInside = false; - this.forEach(function (el) { - if (el.isPointInside(x, y)) { - isPointInside = true; - return false; // stop loop - } - }); - return isPointInside; - }; - - /*\ - * Raphael.registerFont - [ method ] - ** - * Adds given font to the registered set of fonts for Raphaël. Should be used as an internal call from within Cufón’s font file. - * Returns original parameter, so it could be used with chaining. - # <a href="http://wiki.github.com/sorccu/cufon/about">More about Cufón and how to convert your font form TTF, OTF, etc to JavaScript file.</a> - ** - > Parameters - ** - - font (object) the font to register - = (object) the font you passed in - > Usage - | Cufon.registerFont(Raphael.registerFont({…})); - \*/ - R.registerFont = function (font) { - if (!font.face) { - return font; - } - this.fonts = this.fonts || {}; - var fontcopy = { - w: font.w, - face: {}, - glyphs: {} - }, - family = font.face["font-family"]; - for (var prop in font.face) if (font.face[has](prop)) { - fontcopy.face[prop] = font.face[prop]; - } - if (this.fonts[family]) { - this.fonts[family].push(fontcopy); - } else { - this.fonts[family] = [fontcopy]; - } - if (!font.svg) { - fontcopy.face["units-per-em"] = toInt(font.face["units-per-em"], 10); - for (var glyph in font.glyphs) if (font.glyphs[has](glyph)) { - var path = font.glyphs[glyph]; - fontcopy.glyphs[glyph] = { - w: path.w, - k: {}, - d: path.d && "M" + path.d.replace(/[mlcxtrv]/g, function (command) { - return {l: "L", c: "C", x: "z", t: "m", r: "l", v: "c"}[command] || "M"; - }) + "z" - }; - if (path.k) { - for (var k in path.k) if (path[has](k)) { - fontcopy.glyphs[glyph].k[k] = path.k[k]; - } - } - } - } - return font; - }; - /*\ - * Paper.getFont - [ method ] - ** - * Finds font object in the registered fonts by given parameters. You could specify only one word from the font name, like “Myriad” for “Myriad Pro”. - ** - > Parameters - ** - - family (string) font family name or any word from it - - weight (string) #optional font weight - - style (string) #optional font style - - stretch (string) #optional font stretch - = (object) the font object - > Usage - | paper.print(100, 100, "Test string", paper.getFont("Times", 800), 30); - \*/ - paperproto.getFont = function (family, weight, style, stretch) { - stretch = stretch || "normal"; - style = style || "normal"; - weight = +weight || {normal: 400, bold: 700, lighter: 300, bolder: 800}[weight] || 400; - if (!R.fonts) { - return; - } - var font = R.fonts[family]; - if (!font) { - var name = new RegExp("(^|\\s)" + family.replace(/[^\w\d\s+!~.:_-]/g, E) + "(\\s|$)", "i"); - for (var fontName in R.fonts) if (R.fonts[has](fontName)) { - if (name.test(fontName)) { - font = R.fonts[fontName]; - break; - } - } - } - var thefont; - if (font) { - for (var i = 0, ii = font.length; i < ii; i++) { - thefont = font[i]; - if (thefont.face["font-weight"] == weight && (thefont.face["font-style"] == style || !thefont.face["font-style"]) && thefont.face["font-stretch"] == stretch) { - break; - } - } - } - return thefont; - }; - /*\ - * Paper.print - [ method ] - ** - * Creates path that represent given text written using given font at given position with given size. - * Result of the method is path element that contains whole text as a separate path. - ** - > Parameters - ** - - x (number) x position of the text - - y (number) y position of the text - - string (string) text to print - - font (object) font object, see @Paper.getFont - - size (number) #optional size of the font, default is `16` - - origin (string) #optional could be `"baseline"` or `"middle"`, default is `"middle"` - - letter_spacing (number) #optional number in range `-1..1`, default is `0` - - line_spacing (number) #optional number in range `1..3`, default is `1` - = (object) resulting path element, which consist of all letters - > Usage - | var txt = r.print(10, 50, "print", r.getFont("Museo"), 30).attr({fill: "#fff"}); - \*/ - paperproto.print = function (x, y, string, font, size, origin, letter_spacing, line_spacing) { - origin = origin || "middle"; // baseline|middle - letter_spacing = mmax(mmin(letter_spacing || 0, 1), -1); - line_spacing = mmax(mmin(line_spacing || 1, 3), 1); - var letters = Str(string)[split](E), - shift = 0, - notfirst = 0, - path = E, - scale; - R.is(font, "string") && (font = this.getFont(font)); - if (font) { - scale = (size || 16) / font.face["units-per-em"]; - var bb = font.face.bbox[split](separator), - top = +bb[0], - lineHeight = bb[3] - bb[1], - shifty = 0, - height = +bb[1] + (origin == "baseline" ? lineHeight + (+font.face.descent) : lineHeight / 2); - for (var i = 0, ii = letters.length; i < ii; i++) { - if (letters[i] == "\n") { - shift = 0; - curr = 0; - notfirst = 0; - shifty += lineHeight * line_spacing; - } else { - var prev = notfirst && font.glyphs[letters[i - 1]] || {}, - curr = font.glyphs[letters[i]]; - shift += notfirst ? (prev.w || font.w) + (prev.k && prev.k[letters[i]] || 0) + (font.w * letter_spacing) : 0; - notfirst = 1; - } - if (curr && curr.d) { - path += R.transformPath(curr.d, ["t", shift * scale, shifty * scale, "s", scale, scale, top, height, "t", (x - top) / scale, (y - height) / scale]); - } - } - } - return this.path(path).attr({ - fill: "#000", - stroke: "none" - }); - }; - - /*\ - * Paper.add - [ method ] - ** - * Imports elements in JSON array in format `{type: type, <attributes>}` - ** - > Parameters - ** - - json (array) - = (object) resulting set of imported elements - > Usage - | paper.add([ - | { - | type: "circle", - | cx: 10, - | cy: 10, - | r: 5 - | }, - | { - | type: "rect", - | x: 10, - | y: 10, - | width: 10, - | height: 10, - | fill: "#fc0" - | } - | ]); - \*/ - paperproto.add = function (json) { - if (R.is(json, "array")) { - var res = this.set(), - i = 0, - ii = json.length, - j; - for (; i < ii; i++) { - j = json[i] || {}; - elements[has](j.type) && res.push(this[j.type]().attr(j)); - } - } - return res; - }; - - /*\ - * Raphael.format - [ method ] - ** - * Simple format function. Replaces construction of type “`{<number>}`” to the corresponding argument. - ** - > Parameters - ** - - token (string) string to format - - … (string) rest of arguments will be treated as parameters for replacement - = (string) formated string - > Usage - | var x = 10, - | y = 20, - | width = 40, - | height = 50; - | // this will draw a rectangular shape equivalent to "M10,20h40v50h-40z" - | paper.path(Raphael.format("M{0},{1}h{2}v{3}h{4}z", x, y, width, height, -width)); - \*/ - R.format = function (token, params) { - var args = R.is(params, array) ? [0][concat](params) : arguments; - token && R.is(token, string) && args.length - 1 && (token = token.replace(formatrg, function (str, i) { - return args[++i] == null ? E : args[i]; - })); - return token || E; - }; - /*\ - * Raphael.fullfill - [ method ] - ** - * A little bit more advanced format function than @Raphael.format. Replaces construction of type “`{<name>}`” to the corresponding argument. - ** - > Parameters - ** - - token (string) string to format - - json (object) object which properties will be used as a replacement - = (string) formated string - > Usage - | // this will draw a rectangular shape equivalent to "M10,20h40v50h-40z" - | paper.path(Raphael.fullfill("M{x},{y}h{dim.width}v{dim.height}h{dim['negative width']}z", { - | x: 10, - | y: 20, - | dim: { - | width: 40, - | height: 50, - | "negative width": -40 - | } - | })); - \*/ - R.fullfill = (function () { - var tokenRegex = /\{([^\}]+)\}/g, - objNotationRegex = /(?:(?:^|\.)(.+?)(?=\[|\.|$|\()|\[('|")(.+?)\2\])(\(\))?/g, // matches .xxxxx or ["xxxxx"] to run over object properties - replacer = function (all, key, obj) { - var res = obj; - key.replace(objNotationRegex, function (all, name, quote, quotedName, isFunc) { - name = name || quotedName; - if (res) { - if (name in res) { - res = res[name]; - } - typeof res == "function" && isFunc && (res = res()); - } - }); - res = (res == null || res == obj ? all : res) + ""; - return res; - }; - return function (str, obj) { - return String(str).replace(tokenRegex, function (all, key) { - return replacer(all, key, obj); - }); - }; - })(); - /*\ - * Raphael.ninja - [ method ] - ** - * If you want to leave no trace of Raphaël (Well, Raphaël creates only one global variable `Raphael`, but anyway.) You can use `ninja` method. - * Beware, that in this case plugins could stop working, because they are depending on global variable existance. - ** - = (object) Raphael object - > Usage - | (function (local_raphael) { - | var paper = local_raphael(10, 10, 320, 200); - | … - | })(Raphael.ninja()); - \*/ - R.ninja = function () { - oldRaphael.was ? (g.win.Raphael = oldRaphael.is) : delete Raphael; - return R; - }; - /*\ - * Raphael.st - [ property (object) ] - ** - * You can add your own method to elements and sets. It is wise to add a set method for each element method - * you added, so you will be able to call the same method on sets too. - ** - * See also @Raphael.el. - > Usage - | Raphael.el.red = function () { - | this.attr({fill: "#f00"}); - | }; - | Raphael.st.red = function () { - | this.forEach(function (el) { - | el.red(); - | }); - | }; - | // then use it - | paper.set(paper.circle(100, 100, 20), paper.circle(110, 100, 20)).red(); - \*/ - R.st = setproto; - - eve.on("raphael.DOMload", function () { - loaded = true; - }); - - // Firefox <3.6 fix: http://webreflection.blogspot.com/2009/11/195-chars-to-help-lazy-loading.html - (function (doc, loaded, f) { - if (doc.readyState == null && doc.addEventListener){ - doc.addEventListener(loaded, f = function () { - doc.removeEventListener(loaded, f, false); - doc.readyState = "complete"; - }, false); - doc.readyState = "loading"; - } - function isLoaded() { - (/in/).test(doc.readyState) ? setTimeout(isLoaded, 9) : R.eve("raphael.DOMload"); - } - isLoaded(); - })(document, "DOMContentLoaded"); - -// ┌─────────────────────────────────────────────────────────────────────┐ \\ -// │ Raphaël - JavaScript Vector Library │ \\ -// ├─────────────────────────────────────────────────────────────────────┤ \\ -// │ SVG Module │ \\ -// ├─────────────────────────────────────────────────────────────────────┤ \\ -// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com) │ \\ -// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com) │ \\ -// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\ -// └─────────────────────────────────────────────────────────────────────┘ \\ - -(function(){ - if (!R.svg) { - return; - } - var has = "hasOwnProperty", - Str = String, - toFloat = parseFloat, - toInt = parseInt, - math = Math, - mmax = math.max, - abs = math.abs, - pow = math.pow, - separator = /[, ]+/, - eve = R.eve, - E = "", - S = " "; - var xlink = "http://www.w3.org/1999/xlink", - markers = { - block: "M5,0 0,2.5 5,5z", - classic: "M5,0 0,2.5 5,5 3.5,3 3.5,2z", - diamond: "M2.5,0 5,2.5 2.5,5 0,2.5z", - open: "M6,1 1,3.5 6,6", - oval: "M2.5,0A2.5,2.5,0,0,1,2.5,5 2.5,2.5,0,0,1,2.5,0z" - }, - markerCounter = {}; - R.toString = function () { - return "Your browser supports SVG.\nYou are running Rapha\xebl " + this.version; - }; - var $ = function (el, attr) { - if (attr) { - if (typeof el == "string") { - el = $(el); - } - for (var key in attr) if (attr[has](key)) { - if (key.substring(0, 6) == "xlink:") { - el.setAttributeNS(xlink, key.substring(6), Str(attr[key])); - } else { - el.setAttribute(key, Str(attr[key])); - } - } - } else { - el = R._g.doc.createElementNS("http://www.w3.org/2000/svg", el); - el.style && (el.style.webkitTapHighlightColor = "rgba(0,0,0,0)"); - } - return el; - }, - addGradientFill = function (element, gradient) { - var type = "linear", - id = element.id + gradient, - fx = .5, fy = .5, - o = element.node, - SVG = element.paper, - s = o.style, - el = R._g.doc.getElementById(id); - if (!el) { - gradient = Str(gradient).replace(R._radial_gradient, function (all, _fx, _fy) { - type = "radial"; - if (_fx && _fy) { - fx = toFloat(_fx); - fy = toFloat(_fy); - var dir = ((fy > .5) * 2 - 1); - pow(fx - .5, 2) + pow(fy - .5, 2) > .25 && - (fy = math.sqrt(.25 - pow(fx - .5, 2)) * dir + .5) && - fy != .5 && - (fy = fy.toFixed(5) - 1e-5 * dir); - } - return E; - }); - gradient = gradient.split(/\s*\-\s*/); - if (type == "linear") { - var angle = gradient.shift(); - angle = -toFloat(angle); - if (isNaN(angle)) { - return null; - } - var vector = [0, 0, math.cos(R.rad(angle)), math.sin(R.rad(angle))], - max = 1 / (mmax(abs(vector[2]), abs(vector[3])) || 1); - vector[2] *= max; - vector[3] *= max; - if (vector[2] < 0) { - vector[0] = -vector[2]; - vector[2] = 0; - } - if (vector[3] < 0) { - vector[1] = -vector[3]; - vector[3] = 0; - } - } - var dots = R._parseDots(gradient); - if (!dots) { - return null; - } - id = id.replace(/[\(\)\s,\xb0#]/g, "_"); - - if (element.gradient && id != element.gradient.id) { - SVG.defs.removeChild(element.gradient); - delete element.gradient; - } - - if (!element.gradient) { - el = $(type + "Gradient", {id: id}); - element.gradient = el; - $(el, type == "radial" ? { - fx: fx, - fy: fy - } : { - x1: vector[0], - y1: vector[1], - x2: vector[2], - y2: vector[3], - gradientTransform: element.matrix.invert() - }); - SVG.defs.appendChild(el); - for (var i = 0, ii = dots.length; i < ii; i++) { - el.appendChild($("stop", { - offset: dots[i].offset ? dots[i].offset : i ? "100%" : "0%", - "stop-color": dots[i].color || "#fff" - })); - } - } - } - $(o, { - fill: "url('" + document.location + "#" + id + "')", - opacity: 1, - "fill-opacity": 1 - }); - s.fill = E; - s.opacity = 1; - s.fillOpacity = 1; - return 1; - }, - updatePosition = function (o) { - var bbox = o.getBBox(1); - $(o.pattern, {patternTransform: o.matrix.invert() + " translate(" + bbox.x + "," + bbox.y + ")"}); - }, - addArrow = function (o, value, isEnd) { - if (o.type == "path") { - var values = Str(value).toLowerCase().split("-"), - p = o.paper, - se = isEnd ? "end" : "start", - node = o.node, - attrs = o.attrs, - stroke = attrs["stroke-width"], - i = values.length, - type = "classic", - from, - to, - dx, - refX, - attr, - w = 3, - h = 3, - t = 5; - while (i--) { - switch (values[i]) { - case "block": - case "classic": - case "oval": - case "diamond": - case "open": - case "none": - type = values[i]; - break; - case "wide": h = 5; break; - case "narrow": h = 2; break; - case "long": w = 5; break; - case "short": w = 2; break; - } - } - if (type == "open") { - w += 2; - h += 2; - t += 2; - dx = 1; - refX = isEnd ? 4 : 1; - attr = { - fill: "none", - stroke: attrs.stroke - }; - } else { - refX = dx = w / 2; - attr = { - fill: attrs.stroke, - stroke: "none" - }; - } - if (o._.arrows) { - if (isEnd) { - o._.arrows.endPath && markerCounter[o._.arrows.endPath]--; - o._.arrows.endMarker && markerCounter[o._.arrows.endMarker]--; - } else { - o._.arrows.startPath && markerCounter[o._.arrows.startPath]--; - o._.arrows.startMarker && markerCounter[o._.arrows.startMarker]--; - } - } else { - o._.arrows = {}; - } - if (type != "none") { - var pathId = "raphael-marker-" + type, - markerId = "raphael-marker-" + se + type + w + h + "-obj" + o.id; - if (!R._g.doc.getElementById(pathId)) { - p.defs.appendChild($($("path"), { - "stroke-linecap": "round", - d: markers[type], - id: pathId - })); - markerCounter[pathId] = 1; - } else { - markerCounter[pathId]++; - } - var marker = R._g.doc.getElementById(markerId), - use; - if (!marker) { - marker = $($("marker"), { - id: markerId, - markerHeight: h, - markerWidth: w, - orient: "auto", - refX: refX, - refY: h / 2 - }); - use = $($("use"), { - "xlink:href": "#" + pathId, - transform: (isEnd ? "rotate(180 " + w / 2 + " " + h / 2 + ") " : E) + "scale(" + w / t + "," + h / t + ")", - "stroke-width": (1 / ((w / t + h / t) / 2)).toFixed(4) - }); - marker.appendChild(use); - p.defs.appendChild(marker); - markerCounter[markerId] = 1; - } else { - markerCounter[markerId]++; - use = marker.getElementsByTagName("use")[0]; - } - $(use, attr); - var delta = dx * (type != "diamond" && type != "oval"); - if (isEnd) { - from = o._.arrows.startdx * stroke || 0; - to = R.getTotalLength(attrs.path) - delta * stroke; - } else { - from = delta * stroke; - to = R.getTotalLength(attrs.path) - (o._.arrows.enddx * stroke || 0); - } - attr = {}; - attr["marker-" + se] = "url(#" + markerId + ")"; - if (to || from) { - attr.d = R.getSubpath(attrs.path, from, to); - } - $(node, attr); - o._.arrows[se + "Path"] = pathId; - o._.arrows[se + "Marker"] = markerId; - o._.arrows[se + "dx"] = delta; - o._.arrows[se + "Type"] = type; - o._.arrows[se + "String"] = value; - } else { - if (isEnd) { - from = o._.arrows.startdx * stroke || 0; - to = R.getTotalLength(attrs.path) - from; - } else { - from = 0; - to = R.getTotalLength(attrs.path) - (o._.arrows.enddx * stroke || 0); - } - o._.arrows[se + "Path"] && $(node, {d: R.getSubpath(attrs.path, from, to)}); - delete o._.arrows[se + "Path"]; - delete o._.arrows[se + "Marker"]; - delete o._.arrows[se + "dx"]; - delete o._.arrows[se + "Type"]; - delete o._.arrows[se + "String"]; - } - for (attr in markerCounter) if (markerCounter[has](attr) && !markerCounter[attr]) { - var item = R._g.doc.getElementById(attr); - item && item.parentNode.removeChild(item); - } - } - }, - dasharray = { - "": [0], - "none": [0], - "-": [3, 1], - ".": [1, 1], - "-.": [3, 1, 1, 1], - "-..": [3, 1, 1, 1, 1, 1], - ". ": [1, 3], - "- ": [4, 3], - "--": [8, 3], - "- .": [4, 3, 1, 3], - "--.": [8, 3, 1, 3], - "--..": [8, 3, 1, 3, 1, 3] - }, - addDashes = function (o, value, params) { - value = dasharray[Str(value).toLowerCase()]; - if (value) { - var width = o.attrs["stroke-width"] || "1", - butt = {round: width, square: width, butt: 0}[o.attrs["stroke-linecap"] || params["stroke-linecap"]] || 0, - dashes = [], - i = value.length; - while (i--) { - dashes[i] = value[i] * width + ((i % 2) ? 1 : -1) * butt; - } - $(o.node, {"stroke-dasharray": dashes.join(",")}); - } - }, - setFillAndStroke = function (o, params) { - var node = o.node, - attrs = o.attrs, - vis = node.style.visibility; - node.style.visibility = "hidden"; - for (var att in params) { - if (params[has](att)) { - if (!R._availableAttrs[has](att)) { - continue; - } - var value = params[att]; - attrs[att] = value; - switch (att) { - case "blur": - o.blur(value); - break; - case "title": - var title = node.getElementsByTagName("title"); - - // Use the existing <title>. - if (title.length && (title = title[0])) { - title.firstChild.nodeValue = value; - } else { - title = $("title"); - var val = R._g.doc.createTextNode(value); - title.appendChild(val); - node.appendChild(title); - } - break; - case "href": - case "target": - var pn = node.parentNode; - if (pn.tagName.toLowerCase() != "a") { - var hl = $("a"); - pn.insertBefore(hl, node); - hl.appendChild(node); - pn = hl; - } - if (att == "target") { - pn.setAttributeNS(xlink, "show", value == "blank" ? "new" : value); - } else { - pn.setAttributeNS(xlink, att, value); - } - break; - case "cursor": - node.style.cursor = value; - break; - case "transform": - o.transform(value); - break; - case "arrow-start": - addArrow(o, value); - break; - case "arrow-end": - addArrow(o, value, 1); - break; - case "clip-rect": - var rect = Str(value).split(separator); - if (rect.length == 4) { - o.clip && o.clip.parentNode.parentNode.removeChild(o.clip.parentNode); - var el = $("clipPath"), - rc = $("rect"); - el.id = R.createUUID(); - $(rc, { - x: rect[0], - y: rect[1], - width: rect[2], - height: rect[3] - }); - el.appendChild(rc); - o.paper.defs.appendChild(el); - $(node, {"clip-path": "url(#" + el.id + ")"}); - o.clip = rc; - } - if (!value) { - var path = node.getAttribute("clip-path"); - if (path) { - var clip = R._g.doc.getElementById(path.replace(/(^url\(#|\)$)/g, E)); - clip && clip.parentNode.removeChild(clip); - $(node, {"clip-path": E}); - delete o.clip; - } - } - break; - case "path": - if (o.type == "path") { - $(node, {d: value ? attrs.path = R._pathToAbsolute(value) : "M0,0"}); - o._.dirty = 1; - if (o._.arrows) { - "startString" in o._.arrows && addArrow(o, o._.arrows.startString); - "endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1); - } - } - break; - case "width": - node.setAttribute(att, value); - o._.dirty = 1; - if (attrs.fx) { - att = "x"; - value = attrs.x; - } else { - break; - } - case "x": - if (attrs.fx) { - value = -attrs.x - (attrs.width || 0); - } - case "rx": - if (att == "rx" && o.type == "rect") { - break; - } - case "cx": - node.setAttribute(att, value); - o.pattern && updatePosition(o); - o._.dirty = 1; - break; - case "height": - node.setAttribute(att, value); - o._.dirty = 1; - if (attrs.fy) { - att = "y"; - value = attrs.y; - } else { - break; - } - case "y": - if (attrs.fy) { - value = -attrs.y - (attrs.height || 0); - } - case "ry": - if (att == "ry" && o.type == "rect") { - break; - } - case "cy": - node.setAttribute(att, value); - o.pattern && updatePosition(o); - o._.dirty = 1; - break; - case "r": - if (o.type == "rect") { - $(node, {rx: value, ry: value}); - } else { - node.setAttribute(att, value); - } - o._.dirty = 1; - break; - case "src": - if (o.type == "image") { - node.setAttributeNS(xlink, "href", value); - } - break; - case "stroke-width": - if (o._.sx != 1 || o._.sy != 1) { - value /= mmax(abs(o._.sx), abs(o._.sy)) || 1; - } - node.setAttribute(att, value); - if (attrs["stroke-dasharray"]) { - addDashes(o, attrs["stroke-dasharray"], params); - } - if (o._.arrows) { - "startString" in o._.arrows && addArrow(o, o._.arrows.startString); - "endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1); - } - break; - case "stroke-dasharray": - addDashes(o, value, params); - break; - case "fill": - var isURL = Str(value).match(R._ISURL); - if (isURL) { - el = $("pattern"); - var ig = $("image"); - el.id = R.createUUID(); - $(el, {x: 0, y: 0, patternUnits: "userSpaceOnUse", height: 1, width: 1}); - $(ig, {x: 0, y: 0, "xlink:href": isURL[1]}); - el.appendChild(ig); - - (function (el) { - R._preload(isURL[1], function () { - var w = this.offsetWidth, - h = this.offsetHeight; - $(el, {width: w, height: h}); - $(ig, {width: w, height: h}); - o.paper.safari(); - }); - })(el); - o.paper.defs.appendChild(el); - $(node, {fill: "url(#" + el.id + ")"}); - o.pattern = el; - o.pattern && updatePosition(o); - break; - } - var clr = R.getRGB(value); - if (!clr.error) { - delete params.gradient; - delete attrs.gradient; - !R.is(attrs.opacity, "undefined") && - R.is(params.opacity, "undefined") && - $(node, {opacity: attrs.opacity}); - !R.is(attrs["fill-opacity"], "undefined") && - R.is(params["fill-opacity"], "undefined") && - $(node, {"fill-opacity": attrs["fill-opacity"]}); - } else if ((o.type == "circle" || o.type == "ellipse" || Str(value).charAt() != "r") && addGradientFill(o, value)) { - if ("opacity" in attrs || "fill-opacity" in attrs) { - var gradient = R._g.doc.getElementById(node.getAttribute("fill").replace(/^url\(#|\)$/g, E)); - if (gradient) { - var stops = gradient.getElementsByTagName("stop"); - $(stops[stops.length - 1], {"stop-opacity": ("opacity" in attrs ? attrs.opacity : 1) * ("fill-opacity" in attrs ? attrs["fill-opacity"] : 1)}); - } - } - attrs.gradient = value; - attrs.fill = "none"; - break; - } - clr[has]("opacity") && $(node, {"fill-opacity": clr.opacity > 1 ? clr.opacity / 100 : clr.opacity}); - case "stroke": - clr = R.getRGB(value); - node.setAttribute(att, clr.hex); - att == "stroke" && clr[has]("opacity") && $(node, {"stroke-opacity": clr.opacity > 1 ? clr.opacity / 100 : clr.opacity}); - if (att == "stroke" && o._.arrows) { - "startString" in o._.arrows && addArrow(o, o._.arrows.startString); - "endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1); - } - break; - case "gradient": - (o.type == "circle" || o.type == "ellipse" || Str(value).charAt() != "r") && addGradientFill(o, value); - break; - case "opacity": - if (attrs.gradient && !attrs[has]("stroke-opacity")) { - $(node, {"stroke-opacity": value > 1 ? value / 100 : value}); - } - // fall - case "fill-opacity": - if (attrs.gradient) { - gradient = R._g.doc.getElementById(node.getAttribute("fill").replace(/^url\(#|\)$/g, E)); - if (gradient) { - stops = gradient.getElementsByTagName("stop"); - $(stops[stops.length - 1], {"stop-opacity": value}); - } - break; - } - default: - att == "font-size" && (value = toInt(value, 10) + "px"); - var cssrule = att.replace(/(\-.)/g, function (w) { - return w.substring(1).toUpperCase(); - }); - node.style[cssrule] = value; - o._.dirty = 1; - node.setAttribute(att, value); - break; - } - } - } - - tuneText(o, params); - node.style.visibility = vis; - }, - leading = 1.2, - tuneText = function (el, params) { - if (el.type != "text" || !(params[has]("text") || params[has]("font") || params[has]("font-size") || params[has]("x") || params[has]("y"))) { - return; - } - var a = el.attrs, - node = el.node, - fontSize = node.firstChild ? toInt(R._g.doc.defaultView.getComputedStyle(node.firstChild, E).getPropertyValue("font-size"), 10) : 10; - - if (params[has]("text")) { - a.text = params.text; - while (node.firstChild) { - node.removeChild(node.firstChild); - } - var texts = Str(params.text).split("\n"), - tspans = [], - tspan; - for (var i = 0, ii = texts.length; i < ii; i++) { - tspan = $("tspan"); - i && $(tspan, {dy: fontSize * leading, x: a.x}); - tspan.appendChild(R._g.doc.createTextNode(texts[i])); - node.appendChild(tspan); - tspans[i] = tspan; - } - } else { - tspans = node.getElementsByTagName("tspan"); - for (i = 0, ii = tspans.length; i < ii; i++) if (i) { - $(tspans[i], {dy: fontSize * leading, x: a.x}); - } else { - $(tspans[0], {dy: 0}); - } - } - $(node, {x: a.x, y: a.y}); - el._.dirty = 1; - var bb = el._getBBox(), - dif = a.y - (bb.y + bb.height / 2); - dif && R.is(dif, "finite") && $(tspans[0], {dy: dif}); - }, - getRealNode = function (node) { - if (node.parentNode && node.parentNode.tagName.toLowerCase() === "a") { - return node.parentNode; - } else { - return node; - } - }, - Element = function (node, svg) { - var X = 0, - Y = 0; - /*\ - * Element.node - [ property (object) ] - ** - * Gives you a reference to the DOM object, so you can assign event handlers or just mess around. - ** - * Note: Don’t mess with it. - > Usage - | // draw a circle at coordinate 10,10 with radius of 10 - | var c = paper.circle(10, 10, 10); - | c.node.onclick = function () { - | c.attr("fill", "red"); - | }; - \*/ - this[0] = this.node = node; - /*\ - * Element.raphael - [ property (object) ] - ** - * Internal reference to @Raphael object. In case it is not available. - > Usage - | Raphael.el.red = function () { - | var hsb = this.paper.raphael.rgb2hsb(this.attr("fill")); - | hsb.h = 1; - | this.attr({fill: this.paper.raphael.hsb2rgb(hsb).hex}); - | } - \*/ - node.raphael = true; - /*\ - * Element.id - [ property (number) ] - ** - * Unique id of the element. Especially useful when you want to listen to events of the element, - * because all events are fired in format `<module>.<action>.<id>`. Also useful for @Paper.getById method. - \*/ - this.id = R._oid++; - node.raphaelid = this.id; - this.matrix = R.matrix(); - this.realPath = null; - /*\ - * Element.paper - [ property (object) ] - ** - * Internal reference to “paper” where object drawn. Mainly for use in plugins and element extensions. - > Usage - | Raphael.el.cross = function () { - | this.attr({fill: "red"}); - | this.paper.path("M10,10L50,50M50,10L10,50") - | .attr({stroke: "red"}); - | } - \*/ - this.paper = svg; - this.attrs = this.attrs || {}; - this._ = { - transform: [], - sx: 1, - sy: 1, - deg: 0, - dx: 0, - dy: 0, - dirty: 1 - }; - !svg.bottom && (svg.bottom = this); - /*\ - * Element.prev - [ property (object) ] - ** - * Reference to the previous element in the hierarchy. - \*/ - this.prev = svg.top; - svg.top && (svg.top.next = this); - svg.top = this; - /*\ - * Element.next - [ property (object) ] - ** - * Reference to the next element in the hierarchy. - \*/ - this.next = null; - }, - elproto = R.el; - - Element.prototype = elproto; - elproto.constructor = Element; - - R._engine.path = function (pathString, SVG) { - var el = $("path"); - SVG.canvas && SVG.canvas.appendChild(el); - var p = new Element(el, SVG); - p.type = "path"; - setFillAndStroke(p, { - fill: "none", - stroke: "#000", - path: pathString - }); - return p; - }; - /*\ - * Element.rotate - [ method ] - ** - * Deprecated! Use @Element.transform instead. - * Adds rotation by given angle around given point to the list of - * transformations of the element. - > Parameters - - deg (number) angle in degrees - - cx (number) #optional x coordinate of the centre of rotation - - cy (number) #optional y coordinate of the centre of rotation - * If cx & cy aren’t specified centre of the shape is used as a point of rotation. - = (object) @Element - \*/ - elproto.rotate = function (deg, cx, cy) { - if (this.removed) { - return this; - } - deg = Str(deg).split(separator); - if (deg.length - 1) { - cx = toFloat(deg[1]); - cy = toFloat(deg[2]); - } - deg = toFloat(deg[0]); - (cy == null) && (cx = cy); - if (cx == null || cy == null) { - var bbox = this.getBBox(1); - cx = bbox.x + bbox.width / 2; - cy = bbox.y + bbox.height / 2; - } - this.transform(this._.transform.concat([["r", deg, cx, cy]])); - return this; - }; - /*\ - * Element.scale - [ method ] - ** - * Deprecated! Use @Element.transform instead. - * Adds scale by given amount relative to given point to the list of - * transformations of the element. - > Parameters - - sx (number) horisontal scale amount - - sy (number) vertical scale amount - - cx (number) #optional x coordinate of the centre of scale - - cy (number) #optional y coordinate of the centre of scale - * If cx & cy aren’t specified centre of the shape is used instead. - = (object) @Element - \*/ - elproto.scale = function (sx, sy, cx, cy) { - if (this.removed) { - return this; - } - sx = Str(sx).split(separator); - if (sx.length - 1) { - sy = toFloat(sx[1]); - cx = toFloat(sx[2]); - cy = toFloat(sx[3]); - } - sx = toFloat(sx[0]); - (sy == null) && (sy = sx); - (cy == null) && (cx = cy); - if (cx == null || cy == null) { - var bbox = this.getBBox(1); - } - cx = cx == null ? bbox.x + bbox.width / 2 : cx; - cy = cy == null ? bbox.y + bbox.height / 2 : cy; - this.transform(this._.transform.concat([["s", sx, sy, cx, cy]])); - return this; - }; - /*\ - * Element.translate - [ method ] - ** - * Deprecated! Use @Element.transform instead. - * Adds translation by given amount to the list of transformations of the element. - > Parameters - - dx (number) horisontal shift - - dy (number) vertical shift - = (object) @Element - \*/ - elproto.translate = function (dx, dy) { - if (this.removed) { - return this; - } - dx = Str(dx).split(separator); - if (dx.length - 1) { - dy = toFloat(dx[1]); - } - dx = toFloat(dx[0]) || 0; - dy = +dy || 0; - this.transform(this._.transform.concat([["t", dx, dy]])); - return this; - }; - /*\ - * Element.transform - [ method ] - ** - * Adds transformation to the element which is separate to other attributes, - * i.e. translation doesn’t change `x` or `y` of the rectange. The format - * of transformation string is similar to the path string syntax: - | "t100,100r30,100,100s2,2,100,100r45s1.5" - * Each letter is a command. There are four commands: `t` is for translate, `r` is for rotate, `s` is for - * scale and `m` is for matrix. - * - * There are also alternative “absolute” translation, rotation and scale: `T`, `R` and `S`. They will not take previous transformation into account. For example, `...T100,0` will always move element 100 px horisontally, while `...t100,0` could move it vertically if there is `r90` before. Just compare results of `r90t100,0` and `r90T100,0`. - * - * So, the example line above could be read like “translate by 100, 100; rotate 30° around 100, 100; scale twice around 100, 100; - * rotate 45° around centre; scale 1.5 times relative to centre”. As you can see rotate and scale commands have origin - * coordinates as optional parameters, the default is the centre point of the element. - * Matrix accepts six parameters. - > Usage - | var el = paper.rect(10, 20, 300, 200); - | // translate 100, 100, rotate 45°, translate -100, 0 - | el.transform("t100,100r45t-100,0"); - | // if you want you can append or prepend transformations - | el.transform("...t50,50"); - | el.transform("s2..."); - | // or even wrap - | el.transform("t50,50...t-50-50"); - | // to reset transformation call method with empty string - | el.transform(""); - | // to get current value call it without parameters - | console.log(el.transform()); - > Parameters - - tstr (string) #optional transformation string - * If tstr isn’t specified - = (string) current transformation string - * else - = (object) @Element - \*/ - elproto.transform = function (tstr) { - var _ = this._; - if (tstr == null) { - return _.transform; - } - R._extractTransform(this, tstr); - - this.clip && $(this.clip, {transform: this.matrix.invert()}); - this.pattern && updatePosition(this); - this.node && $(this.node, {transform: this.matrix}); - - if (_.sx != 1 || _.sy != 1) { - var sw = this.attrs[has]("stroke-width") ? this.attrs["stroke-width"] : 1; - this.attr({"stroke-width": sw}); - } - - return this; - }; - /*\ - * Element.hide - [ method ] - ** - * Makes element invisible. See @Element.show. - = (object) @Element - \*/ - elproto.hide = function () { - !this.removed && this.paper.safari(this.node.style.display = "none"); - return this; - }; - /*\ - * Element.show - [ method ] - ** - * Makes element visible. See @Element.hide. - = (object) @Element - \*/ - elproto.show = function () { - !this.removed && this.paper.safari(this.node.style.display = ""); - return this; - }; - /*\ - * Element.remove - [ method ] - ** - * Removes element from the paper. - \*/ - elproto.remove = function () { - var node = getRealNode(this.node); - if (this.removed || !node.parentNode) { - return; - } - var paper = this.paper; - paper.__set__ && paper.__set__.exclude(this); - eve.unbind("raphael.*.*." + this.id); - if (this.gradient) { - paper.defs.removeChild(this.gradient); - } - R._tear(this, paper); - - node.parentNode.removeChild(node); - - // Remove custom data for element - this.removeData(); - - for (var i in this) { - this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null; - } - this.removed = true; - }; - elproto._getBBox = function () { - if (this.node.style.display == "none") { - this.show(); - var hide = true; - } - var canvasHidden = false, - containerStyle; - if (this.paper.canvas.parentElement) { - containerStyle = this.paper.canvas.parentElement.style; - } //IE10+ can't find parentElement - else if (this.paper.canvas.parentNode) { - containerStyle = this.paper.canvas.parentNode.style; - } - - if(containerStyle && containerStyle.display == "none") { - canvasHidden = true; - containerStyle.display = ""; - } - var bbox = {}; - try { - bbox = this.node.getBBox(); - } catch(e) { - // Firefox 3.0.x, 25.0.1 (probably more versions affected) play badly here - possible fix - bbox = { - x: this.node.clientLeft, - y: this.node.clientTop, - width: this.node.clientWidth, - height: this.node.clientHeight - } - } finally { - bbox = bbox || {}; - if(canvasHidden){ - containerStyle.display = "none"; - } - } - hide && this.hide(); - return bbox; - }; - /*\ - * Element.attr - [ method ] - ** - * Sets the attributes of the element. - > Parameters - - attrName (string) attribute’s name - - value (string) value - * or - - params (object) object of name/value pairs - * or - - attrName (string) attribute’s name - * or - - attrNames (array) in this case method returns array of current values for given attribute names - = (object) @Element if attrsName & value or params are passed in. - = (...) value of the attribute if only attrsName is passed in. - = (array) array of values of the attribute if attrsNames is passed in. - = (object) object of attributes if nothing is passed in. - > Possible parameters - # <p>Please refer to the <a href="http://www.w3.org/TR/SVG/" title="The W3C Recommendation for the SVG language describes these properties in detail.">SVG specification</a> for an explanation of these parameters.</p> - o arrow-end (string) arrowhead on the end of the path. The format for string is `<type>[-<width>[-<length>]]`. Possible types: `classic`, `block`, `open`, `oval`, `diamond`, `none`, width: `wide`, `narrow`, `medium`, length: `long`, `short`, `midium`. - o clip-rect (string) comma or space separated values: x, y, width and height - o cursor (string) CSS type of the cursor - o cx (number) the x-axis coordinate of the center of the circle, or ellipse - o cy (number) the y-axis coordinate of the center of the circle, or ellipse - o fill (string) colour, gradient or image - o fill-opacity (number) - o font (string) - o font-family (string) - o font-size (number) font size in pixels - o font-weight (string) - o height (number) - o href (string) URL, if specified element behaves as hyperlink - o opacity (number) - o path (string) SVG path string format - o r (number) radius of the circle, ellipse or rounded corner on the rect - o rx (number) horisontal radius of the ellipse - o ry (number) vertical radius of the ellipse - o src (string) image URL, only works for @Element.image element - o stroke (string) stroke colour - o stroke-dasharray (string) [“”, “`-`”, “`.`”, “`-.`”, “`-..`”, “`. `”, “`- `”, “`--`”, “`- .`”, “`--.`”, “`--..`”] - o stroke-linecap (string) [“`butt`”, “`square`”, “`round`”] - o stroke-linejoin (string) [“`bevel`”, “`round`”, “`miter`”] - o stroke-miterlimit (number) - o stroke-opacity (number) - o stroke-width (number) stroke width in pixels, default is '1' - o target (string) used with href - o text (string) contents of the text element. Use `\n` for multiline text - o text-anchor (string) [“`start`”, “`middle`”, “`end`”], default is “`middle`” - o title (string) will create tooltip with a given text - o transform (string) see @Element.transform - o width (number) - o x (number) - o y (number) - > Gradients - * Linear gradient format: “`‹angle›-‹colour›[-‹colour›[:‹offset›]]*-‹colour›`”, example: “`90-#fff-#000`” – 90° - * gradient from white to black or “`0-#fff-#f00:20-#000`” – 0° gradient from white via red (at 20%) to black. - * - * radial gradient: “`r[(‹fx›, ‹fy›)]‹colour›[-‹colour›[:‹offset›]]*-‹colour›`”, example: “`r#fff-#000`” – - * gradient from white to black or “`r(0.25, 0.75)#fff-#000`” – gradient from white to black with focus point - * at 0.25, 0.75. Focus point coordinates are in 0..1 range. Radial gradients can only be applied to circles and ellipses. - > Path String - # <p>Please refer to <a href="http://www.w3.org/TR/SVG/paths.html#PathData" title="Details of a path’s data attribute’s format are described in the SVG specification.">SVG documentation regarding path string</a>. Raphaël fully supports it.</p> - > Colour Parsing - # <ul> - # <li>Colour name (“<code>red</code>”, “<code>green</code>”, “<code>cornflowerblue</code>”, etc)</li> - # <li>#••• — shortened HTML colour: (“<code>#000</code>”, “<code>#fc0</code>”, etc)</li> - # <li>#•••••• — full length HTML colour: (“<code>#000000</code>”, “<code>#bd2300</code>”)</li> - # <li>rgb(•••, •••, •••) — red, green and blue channels’ values: (“<code>rgb(200, 100, 0)</code>”)</li> - # <li>rgb(•••%, •••%, •••%) — same as above, but in %: (“<code>rgb(100%, 175%, 0%)</code>”)</li> - # <li>rgba(•••, •••, •••, •••) — red, green and blue channels’ values: (“<code>rgba(200, 100, 0, .5)</code>”)</li> - # <li>rgba(•••%, •••%, •••%, •••%) — same as above, but in %: (“<code>rgba(100%, 175%, 0%, 50%)</code>”)</li> - # <li>hsb(•••, •••, •••) — hue, saturation and brightness values: (“<code>hsb(0.5, 0.25, 1)</code>”)</li> - # <li>hsb(•••%, •••%, •••%) — same as above, but in %</li> - # <li>hsba(•••, •••, •••, •••) — same as above, but with opacity</li> - # <li>hsl(•••, •••, •••) — almost the same as hsb, see <a href="http://en.wikipedia.org/wiki/HSL_and_HSV" title="HSL and HSV - Wikipedia, the free encyclopedia">Wikipedia page</a></li> - # <li>hsl(•••%, •••%, •••%) — same as above, but in %</li> - # <li>hsla(•••, •••, •••, •••) — same as above, but with opacity</li> - # <li>Optionally for hsb and hsl you could specify hue as a degree: “<code>hsl(240deg, 1, .5)</code>” or, if you want to go fancy, “<code>hsl(240°, 1, .5)</code>”</li> - # </ul> - \*/ - elproto.attr = function (name, value) { - if (this.removed) { - return this; - } - if (name == null) { - var res = {}; - for (var a in this.attrs) if (this.attrs[has](a)) { - res[a] = this.attrs[a]; - } - res.gradient && res.fill == "none" && (res.fill = res.gradient) && delete res.gradient; - res.transform = this._.transform; - return res; - } - if (value == null && R.is(name, "string")) { - if (name == "fill" && this.attrs.fill == "none" && this.attrs.gradient) { - return this.attrs.gradient; - } - if (name == "transform") { - return this._.transform; - } - var names = name.split(separator), - out = {}; - for (var i = 0, ii = names.length; i < ii; i++) { - name = names[i]; - if (name in this.attrs) { - out[name] = this.attrs[name]; - } else if (R.is(this.paper.customAttributes[name], "function")) { - out[name] = this.paper.customAttributes[name].def; - } else { - out[name] = R._availableAttrs[name]; - } - } - return ii - 1 ? out : out[names[0]]; - } - if (value == null && R.is(name, "array")) { - out = {}; - for (i = 0, ii = name.length; i < ii; i++) { - out[name[i]] = this.attr(name[i]); - } - return out; - } - if (value != null) { - var params = {}; - params[name] = value; - } else if (name != null && R.is(name, "object")) { - params = name; - } - for (var key in params) { - eve("raphael.attr." + key + "." + this.id, this, params[key]); - } - for (key in this.paper.customAttributes) if (this.paper.customAttributes[has](key) && params[has](key) && R.is(this.paper.customAttributes[key], "function")) { - var par = this.paper.customAttributes[key].apply(this, [].concat(params[key])); - this.attrs[key] = params[key]; - for (var subkey in par) if (par[has](subkey)) { - params[subkey] = par[subkey]; - } - } - setFillAndStroke(this, params); - return this; - }; - /*\ - * Element.toFront - [ method ] - ** - * Moves the element so it is the closest to the viewer’s eyes, on top of other elements. - = (object) @Element - \*/ - elproto.toFront = function () { - if (this.removed) { - return this; - } - var node = getRealNode(this.node); - node.parentNode.appendChild(node); - var svg = this.paper; - svg.top != this && R._tofront(this, svg); - return this; - }; - /*\ - * Element.toBack - [ method ] - ** - * Moves the element so it is the furthest from the viewer’s eyes, behind other elements. - = (object) @Element - \*/ - elproto.toBack = function () { - if (this.removed) { - return this; - } - var node = getRealNode(this.node); - var parentNode = node.parentNode; - parentNode.insertBefore(node, parentNode.firstChild); - R._toback(this, this.paper); - var svg = this.paper; - return this; - }; - /*\ - * Element.insertAfter - [ method ] - ** - * Inserts current object after the given one. - = (object) @Element - \*/ - elproto.insertAfter = function (element) { - if (this.removed || !element) { - return this; - } - - var node = getRealNode(this.node); - var afterNode = getRealNode(element.node || element[element.length - 1].node); - if (afterNode.nextSibling) { - afterNode.parentNode.insertBefore(node, afterNode.nextSibling); - } else { - afterNode.parentNode.appendChild(node); - } - R._insertafter(this, element, this.paper); - return this; - }; - /*\ - * Element.insertBefore - [ method ] - ** - * Inserts current object before the given one. - = (object) @Element - \*/ - elproto.insertBefore = function (element) { - if (this.removed || !element) { - return this; - } - - var node = getRealNode(this.node); - var beforeNode = getRealNode(element.node || element[0].node); - beforeNode.parentNode.insertBefore(node, beforeNode); - R._insertbefore(this, element, this.paper); - return this; - }; - elproto.blur = function (size) { - // Experimental. No Safari support. Use it on your own risk. - var t = this; - if (+size !== 0) { - var fltr = $("filter"), - blur = $("feGaussianBlur"); - t.attrs.blur = size; - fltr.id = R.createUUID(); - $(blur, {stdDeviation: +size || 1.5}); - fltr.appendChild(blur); - t.paper.defs.appendChild(fltr); - t._blur = fltr; - $(t.node, {filter: "url(#" + fltr.id + ")"}); - } else { - if (t._blur) { - t._blur.parentNode.removeChild(t._blur); - delete t._blur; - delete t.attrs.blur; - } - t.node.removeAttribute("filter"); - } - return t; - }; - R._engine.circle = function (svg, x, y, r) { - var el = $("circle"); - svg.canvas && svg.canvas.appendChild(el); - var res = new Element(el, svg); - res.attrs = {cx: x, cy: y, r: r, fill: "none", stroke: "#000"}; - res.type = "circle"; - $(el, res.attrs); - return res; - }; - R._engine.rect = function (svg, x, y, w, h, r) { - var el = $("rect"); - svg.canvas && svg.canvas.appendChild(el); - var res = new Element(el, svg); - res.attrs = {x: x, y: y, width: w, height: h, rx: r || 0, ry: r || 0, fill: "none", stroke: "#000"}; - res.type = "rect"; - $(el, res.attrs); - return res; - }; - R._engine.ellipse = function (svg, x, y, rx, ry) { - var el = $("ellipse"); - svg.canvas && svg.canvas.appendChild(el); - var res = new Element(el, svg); - res.attrs = {cx: x, cy: y, rx: rx, ry: ry, fill: "none", stroke: "#000"}; - res.type = "ellipse"; - $(el, res.attrs); - return res; - }; - R._engine.image = function (svg, src, x, y, w, h) { - var el = $("image"); - $(el, {x: x, y: y, width: w, height: h, preserveAspectRatio: "none"}); - el.setAttributeNS(xlink, "href", src); - svg.canvas && svg.canvas.appendChild(el); - var res = new Element(el, svg); - res.attrs = {x: x, y: y, width: w, height: h, src: src}; - res.type = "image"; - return res; - }; - R._engine.text = function (svg, x, y, text) { - var el = $("text"); - svg.canvas && svg.canvas.appendChild(el); - var res = new Element(el, svg); - res.attrs = { - x: x, - y: y, - "text-anchor": "middle", - text: text, - "font-family": R._availableAttrs["font-family"], - "font-size": R._availableAttrs["font-size"], - stroke: "none", - fill: "#000" - }; - res.type = "text"; - setFillAndStroke(res, res.attrs); - return res; - }; - R._engine.setSize = function (width, height) { - this.width = width || this.width; - this.height = height || this.height; - this.canvas.setAttribute("width", this.width); - this.canvas.setAttribute("height", this.height); - if (this._viewBox) { - this.setViewBox.apply(this, this._viewBox); - } - return this; - }; - R._engine.create = function () { - var con = R._getContainer.apply(0, arguments), - container = con && con.container, - x = con.x, - y = con.y, - width = con.width, - height = con.height; - if (!container) { - throw new Error("SVG container not found."); - } - var cnvs = $("svg"), - css = "overflow:hidden;", - isFloating; - x = x || 0; - y = y || 0; - width = width || 512; - height = height || 342; - $(cnvs, { - height: height, - version: 1.1, - width: width, - xmlns: "http://www.w3.org/2000/svg", - "xmlns:xlink": "http://www.w3.org/1999/xlink" - }); - if (container == 1) { - cnvs.style.cssText = css + "position:absolute;left:" + x + "px;top:" + y + "px"; - R._g.doc.body.appendChild(cnvs); - isFloating = 1; - } else { - cnvs.style.cssText = css + "position:relative"; - if (container.firstChild) { - container.insertBefore(cnvs, container.firstChild); - } else { - container.appendChild(cnvs); - } - } - container = new R._Paper; - container.width = width; - container.height = height; - container.canvas = cnvs; - container.clear(); - container._left = container._top = 0; - isFloating && (container.renderfix = function () {}); - container.renderfix(); - return container; - }; - R._engine.setViewBox = function (x, y, w, h, fit) { - eve("raphael.setViewBox", this, this._viewBox, [x, y, w, h, fit]); - var paperSize = this.getSize(), - size = mmax(w / paperSize.width, h / paperSize.height), - top = this.top, - aspectRatio = fit ? "xMidYMid meet" : "xMinYMin", - vb, - sw; - if (x == null) { - if (this._vbSize) { - size = 1; - } - delete this._vbSize; - vb = "0 0 " + this.width + S + this.height; - } else { - this._vbSize = size; - vb = x + S + y + S + w + S + h; - } - $(this.canvas, { - viewBox: vb, - preserveAspectRatio: aspectRatio - }); - while (size && top) { - sw = "stroke-width" in top.attrs ? top.attrs["stroke-width"] : 1; - top.attr({"stroke-width": sw}); - top._.dirty = 1; - top._.dirtyT = 1; - top = top.prev; - } - this._viewBox = [x, y, w, h, !!fit]; - return this; - }; - /*\ - * Paper.renderfix - [ method ] - ** - * Fixes the issue of Firefox and IE9 regarding subpixel rendering. If paper is dependant - * on other elements after reflow it could shift half pixel which cause for lines to lost their crispness. - * This method fixes the issue. - ** - Special thanks to Mariusz Nowak (http://www.medikoo.com/) for this method. - \*/ - R.prototype.renderfix = function () { - var cnvs = this.canvas, - s = cnvs.style, - pos; - try { - pos = cnvs.getScreenCTM() || cnvs.createSVGMatrix(); - } catch (e) { - pos = cnvs.createSVGMatrix(); - } - var left = -pos.e % 1, - top = -pos.f % 1; - if (left || top) { - if (left) { - this._left = (this._left + left) % 1; - s.left = this._left + "px"; - } - if (top) { - this._top = (this._top + top) % 1; - s.top = this._top + "px"; - } - } - }; - /*\ - * Paper.clear - [ method ] - ** - * Clears the paper, i.e. removes all the elements. - \*/ - R.prototype.clear = function () { - R.eve("raphael.clear", this); - var c = this.canvas; - while (c.firstChild) { - c.removeChild(c.firstChild); - } - this.bottom = this.top = null; - (this.desc = $("desc")).appendChild(R._g.doc.createTextNode("Created with Rapha\xebl " + R.version)); - c.appendChild(this.desc); - c.appendChild(this.defs = $("defs")); - }; - /*\ - * Paper.remove - [ method ] - ** - * Removes the paper from the DOM. - \*/ - R.prototype.remove = function () { - eve("raphael.remove", this); - this.canvas.parentNode && this.canvas.parentNode.removeChild(this.canvas); - for (var i in this) { - this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null; - } - }; - var setproto = R.st; - for (var method in elproto) if (elproto[has](method) && !setproto[has](method)) { - setproto[method] = (function (methodname) { - return function () { - var arg = arguments; - return this.forEach(function (el) { - el[methodname].apply(el, arg); - }); - }; - })(method); - } -})(); - -// ┌─────────────────────────────────────────────────────────────────────┐ \\ -// │ Raphaël - JavaScript Vector Library │ \\ -// ├─────────────────────────────────────────────────────────────────────┤ \\ -// │ VML Module │ \\ -// ├─────────────────────────────────────────────────────────────────────┤ \\ -// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com) │ \\ -// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com) │ \\ -// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\ -// └─────────────────────────────────────────────────────────────────────┘ \\ - -(function(){ - if (!R.vml) { - return; - } - var has = "hasOwnProperty", - Str = String, - toFloat = parseFloat, - math = Math, - round = math.round, - mmax = math.max, - mmin = math.min, - abs = math.abs, - fillString = "fill", - separator = /[, ]+/, - eve = R.eve, - ms = " progid:DXImageTransform.Microsoft", - S = " ", - E = "", - map = {M: "m", L: "l", C: "c", Z: "x", m: "t", l: "r", c: "v", z: "x"}, - bites = /([clmz]),?([^clmz]*)/gi, - blurregexp = / progid:\S+Blur\([^\)]+\)/g, - val = /-?[^,\s-]+/g, - cssDot = "position:absolute;left:0;top:0;width:1px;height:1px;behavior:url(#default#VML)", - zoom = 21600, - pathTypes = {path: 1, rect: 1, image: 1}, - ovalTypes = {circle: 1, ellipse: 1}, - path2vml = function (path) { - var total = /[ahqstv]/ig, - command = R._pathToAbsolute; - Str(path).match(total) && (command = R._path2curve); - total = /[clmz]/g; - if (command == R._pathToAbsolute && !Str(path).match(total)) { - var res = Str(path).replace(bites, function (all, command, args) { - var vals = [], - isMove = command.toLowerCase() == "m", - res = map[command]; - args.replace(val, function (value) { - if (isMove && vals.length == 2) { - res += vals + map[command == "m" ? "l" : "L"]; - vals = []; - } - vals.push(round(value * zoom)); - }); - return res + vals; - }); - return res; - } - var pa = command(path), p, r; - res = []; - for (var i = 0, ii = pa.length; i < ii; i++) { - p = pa[i]; - r = pa[i][0].toLowerCase(); - r == "z" && (r = "x"); - for (var j = 1, jj = p.length; j < jj; j++) { - r += round(p[j] * zoom) + (j != jj - 1 ? "," : E); - } - res.push(r); - } - return res.join(S); - }, - compensation = function (deg, dx, dy) { - var m = R.matrix(); - m.rotate(-deg, .5, .5); - return { - dx: m.x(dx, dy), - dy: m.y(dx, dy) - }; - }, - setCoords = function (p, sx, sy, dx, dy, deg) { - var _ = p._, - m = p.matrix, - fillpos = _.fillpos, - o = p.node, - s = o.style, - y = 1, - flip = "", - dxdy, - kx = zoom / sx, - ky = zoom / sy; - s.visibility = "hidden"; - if (!sx || !sy) { - return; - } - o.coordsize = abs(kx) + S + abs(ky); - s.rotation = deg * (sx * sy < 0 ? -1 : 1); - if (deg) { - var c = compensation(deg, dx, dy); - dx = c.dx; - dy = c.dy; - } - sx < 0 && (flip += "x"); - sy < 0 && (flip += " y") && (y = -1); - s.flip = flip; - o.coordorigin = (dx * -kx) + S + (dy * -ky); - if (fillpos || _.fillsize) { - var fill = o.getElementsByTagName(fillString); - fill = fill && fill[0]; - o.removeChild(fill); - if (fillpos) { - c = compensation(deg, m.x(fillpos[0], fillpos[1]), m.y(fillpos[0], fillpos[1])); - fill.position = c.dx * y + S + c.dy * y; - } - if (_.fillsize) { - fill.size = _.fillsize[0] * abs(sx) + S + _.fillsize[1] * abs(sy); - } - o.appendChild(fill); - } - s.visibility = "visible"; - }; - R.toString = function () { - return "Your browser doesn\u2019t support SVG. Falling down to VML.\nYou are running Rapha\xebl " + this.version; - }; - var addArrow = function (o, value, isEnd) { - var values = Str(value).toLowerCase().split("-"), - se = isEnd ? "end" : "start", - i = values.length, - type = "classic", - w = "medium", - h = "medium"; - while (i--) { - switch (values[i]) { - case "block": - case "classic": - case "oval": - case "diamond": - case "open": - case "none": - type = values[i]; - break; - case "wide": - case "narrow": h = values[i]; break; - case "long": - case "short": w = values[i]; break; - } - } - var stroke = o.node.getElementsByTagName("stroke")[0]; - stroke[se + "arrow"] = type; - stroke[se + "arrowlength"] = w; - stroke[se + "arrowwidth"] = h; - }, - setFillAndStroke = function (o, params) { - // o.paper.canvas.style.display = "none"; - o.attrs = o.attrs || {}; - var node = o.node, - a = o.attrs, - s = node.style, - xy, - newpath = pathTypes[o.type] && (params.x != a.x || params.y != a.y || params.width != a.width || params.height != a.height || params.cx != a.cx || params.cy != a.cy || params.rx != a.rx || params.ry != a.ry || params.r != a.r), - isOval = ovalTypes[o.type] && (a.cx != params.cx || a.cy != params.cy || a.r != params.r || a.rx != params.rx || a.ry != params.ry), - res = o; - - - for (var par in params) if (params[has](par)) { - a[par] = params[par]; - } - if (newpath) { - a.path = R._getPath[o.type](o); - o._.dirty = 1; - } - params.href && (node.href = params.href); - params.title && (node.title = params.title); - params.target && (node.target = params.target); - params.cursor && (s.cursor = params.cursor); - "blur" in params && o.blur(params.blur); - if (params.path && o.type == "path" || newpath) { - node.path = path2vml(~Str(a.path).toLowerCase().indexOf("r") ? R._pathToAbsolute(a.path) : a.path); - o._.dirty = 1; - if (o.type == "image") { - o._.fillpos = [a.x, a.y]; - o._.fillsize = [a.width, a.height]; - setCoords(o, 1, 1, 0, 0, 0); - } - } - "transform" in params && o.transform(params.transform); - if (isOval) { - var cx = +a.cx, - cy = +a.cy, - rx = +a.rx || +a.r || 0, - ry = +a.ry || +a.r || 0; - node.path = R.format("ar{0},{1},{2},{3},{4},{1},{4},{1}x", round((cx - rx) * zoom), round((cy - ry) * zoom), round((cx + rx) * zoom), round((cy + ry) * zoom), round(cx * zoom)); - o._.dirty = 1; - } - if ("clip-rect" in params) { - var rect = Str(params["clip-rect"]).split(separator); - if (rect.length == 4) { - rect[2] = +rect[2] + (+rect[0]); - rect[3] = +rect[3] + (+rect[1]); - var div = node.clipRect || R._g.doc.createElement("div"), - dstyle = div.style; - dstyle.clip = R.format("rect({1}px {2}px {3}px {0}px)", rect); - if (!node.clipRect) { - dstyle.position = "absolute"; - dstyle.top = 0; - dstyle.left = 0; - dstyle.width = o.paper.width + "px"; - dstyle.height = o.paper.height + "px"; - node.parentNode.insertBefore(div, node); - div.appendChild(node); - node.clipRect = div; - } - } - if (!params["clip-rect"]) { - node.clipRect && (node.clipRect.style.clip = "auto"); - } - } - if (o.textpath) { - var textpathStyle = o.textpath.style; - params.font && (textpathStyle.font = params.font); - params["font-family"] && (textpathStyle.fontFamily = '"' + params["font-family"].split(",")[0].replace(/^['"]+|['"]+$/g, E) + '"'); - params["font-size"] && (textpathStyle.fontSize = params["font-size"]); - params["font-weight"] && (textpathStyle.fontWeight = params["font-weight"]); - params["font-style"] && (textpathStyle.fontStyle = params["font-style"]); - } - if ("arrow-start" in params) { - addArrow(res, params["arrow-start"]); - } - if ("arrow-end" in params) { - addArrow(res, params["arrow-end"], 1); - } - if (params.opacity != null || - params["stroke-width"] != null || - params.fill != null || - params.src != null || - params.stroke != null || - params["stroke-width"] != null || - params["stroke-opacity"] != null || - params["fill-opacity"] != null || - params["stroke-dasharray"] != null || - params["stroke-miterlimit"] != null || - params["stroke-linejoin"] != null || - params["stroke-linecap"] != null) { - var fill = node.getElementsByTagName(fillString), - newfill = false; - fill = fill && fill[0]; - !fill && (newfill = fill = createNode(fillString)); - if (o.type == "image" && params.src) { - fill.src = params.src; - } - params.fill && (fill.on = true); - if (fill.on == null || params.fill == "none" || params.fill === null) { - fill.on = false; - } - if (fill.on && params.fill) { - var isURL = Str(params.fill).match(R._ISURL); - if (isURL) { - fill.parentNode == node && node.removeChild(fill); - fill.rotate = true; - fill.src = isURL[1]; - fill.type = "tile"; - var bbox = o.getBBox(1); - fill.position = bbox.x + S + bbox.y; - o._.fillpos = [bbox.x, bbox.y]; - - R._preload(isURL[1], function () { - o._.fillsize = [this.offsetWidth, this.offsetHeight]; - }); - } else { - fill.color = R.getRGB(params.fill).hex; - fill.src = E; - fill.type = "solid"; - if (R.getRGB(params.fill).error && (res.type in {circle: 1, ellipse: 1} || Str(params.fill).charAt() != "r") && addGradientFill(res, params.fill, fill)) { - a.fill = "none"; - a.gradient = params.fill; - fill.rotate = false; - } - } - } - if ("fill-opacity" in params || "opacity" in params) { - var opacity = ((+a["fill-opacity"] + 1 || 2) - 1) * ((+a.opacity + 1 || 2) - 1) * ((+R.getRGB(params.fill).o + 1 || 2) - 1); - opacity = mmin(mmax(opacity, 0), 1); - fill.opacity = opacity; - if (fill.src) { - fill.color = "none"; - } - } - node.appendChild(fill); - var stroke = (node.getElementsByTagName("stroke") && node.getElementsByTagName("stroke")[0]), - newstroke = false; - !stroke && (newstroke = stroke = createNode("stroke")); - if ((params.stroke && params.stroke != "none") || - params["stroke-width"] || - params["stroke-opacity"] != null || - params["stroke-dasharray"] || - params["stroke-miterlimit"] || - params["stroke-linejoin"] || - params["stroke-linecap"]) { - stroke.on = true; - } - (params.stroke == "none" || params.stroke === null || stroke.on == null || params.stroke == 0 || params["stroke-width"] == 0) && (stroke.on = false); - var strokeColor = R.getRGB(params.stroke); - stroke.on && params.stroke && (stroke.color = strokeColor.hex); - opacity = ((+a["stroke-opacity"] + 1 || 2) - 1) * ((+a.opacity + 1 || 2) - 1) * ((+strokeColor.o + 1 || 2) - 1); - var width = (toFloat(params["stroke-width"]) || 1) * .75; - opacity = mmin(mmax(opacity, 0), 1); - params["stroke-width"] == null && (width = a["stroke-width"]); - params["stroke-width"] && (stroke.weight = width); - width && width < 1 && (opacity *= width) && (stroke.weight = 1); - stroke.opacity = opacity; - - params["stroke-linejoin"] && (stroke.joinstyle = params["stroke-linejoin"] || "miter"); - stroke.miterlimit = params["stroke-miterlimit"] || 8; - params["stroke-linecap"] && (stroke.endcap = params["stroke-linecap"] == "butt" ? "flat" : params["stroke-linecap"] == "square" ? "square" : "round"); - if ("stroke-dasharray" in params) { - var dasharray = { - "-": "shortdash", - ".": "shortdot", - "-.": "shortdashdot", - "-..": "shortdashdotdot", - ". ": "dot", - "- ": "dash", - "--": "longdash", - "- .": "dashdot", - "--.": "longdashdot", - "--..": "longdashdotdot" - }; - stroke.dashstyle = dasharray[has](params["stroke-dasharray"]) ? dasharray[params["stroke-dasharray"]] : E; - } - newstroke && node.appendChild(stroke); - } - if (res.type == "text") { - res.paper.canvas.style.display = E; - var span = res.paper.span, - m = 100, - fontSize = a.font && a.font.match(/\d+(?:\.\d*)?(?=px)/); - s = span.style; - a.font && (s.font = a.font); - a["font-family"] && (s.fontFamily = a["font-family"]); - a["font-weight"] && (s.fontWeight = a["font-weight"]); - a["font-style"] && (s.fontStyle = a["font-style"]); - fontSize = toFloat(a["font-size"] || fontSize && fontSize[0]) || 10; - s.fontSize = fontSize * m + "px"; - res.textpath.string && (span.innerHTML = Str(res.textpath.string).replace(/</g, "<").replace(/&/g, "&").replace(/\n/g, "<br>")); - var brect = span.getBoundingClientRect(); - res.W = a.w = (brect.right - brect.left) / m; - res.H = a.h = (brect.bottom - brect.top) / m; - // res.paper.canvas.style.display = "none"; - res.X = a.x; - res.Y = a.y + res.H / 2; - - ("x" in params || "y" in params) && (res.path.v = R.format("m{0},{1}l{2},{1}", round(a.x * zoom), round(a.y * zoom), round(a.x * zoom) + 1)); - var dirtyattrs = ["x", "y", "text", "font", "font-family", "font-weight", "font-style", "font-size"]; - for (var d = 0, dd = dirtyattrs.length; d < dd; d++) if (dirtyattrs[d] in params) { - res._.dirty = 1; - break; - } - - // text-anchor emulation - switch (a["text-anchor"]) { - case "start": - res.textpath.style["v-text-align"] = "left"; - res.bbx = res.W / 2; - break; - case "end": - res.textpath.style["v-text-align"] = "right"; - res.bbx = -res.W / 2; - break; - default: - res.textpath.style["v-text-align"] = "center"; - res.bbx = 0; - break; - } - res.textpath.style["v-text-kern"] = true; - } - // res.paper.canvas.style.display = E; - }, - addGradientFill = function (o, gradient, fill) { - o.attrs = o.attrs || {}; - var attrs = o.attrs, - pow = Math.pow, - opacity, - oindex, - type = "linear", - fxfy = ".5 .5"; - o.attrs.gradient = gradient; - gradient = Str(gradient).replace(R._radial_gradient, function (all, fx, fy) { - type = "radial"; - if (fx && fy) { - fx = toFloat(fx); - fy = toFloat(fy); - pow(fx - .5, 2) + pow(fy - .5, 2) > .25 && (fy = math.sqrt(.25 - pow(fx - .5, 2)) * ((fy > .5) * 2 - 1) + .5); - fxfy = fx + S + fy; - } - return E; - }); - gradient = gradient.split(/\s*\-\s*/); - if (type == "linear") { - var angle = gradient.shift(); - angle = -toFloat(angle); - if (isNaN(angle)) { - return null; - } - } - var dots = R._parseDots(gradient); - if (!dots) { - return null; - } - o = o.shape || o.node; - if (dots.length) { - o.removeChild(fill); - fill.on = true; - fill.method = "none"; - fill.color = dots[0].color; - fill.color2 = dots[dots.length - 1].color; - var clrs = []; - for (var i = 0, ii = dots.length; i < ii; i++) { - dots[i].offset && clrs.push(dots[i].offset + S + dots[i].color); - } - fill.colors = clrs.length ? clrs.join() : "0% " + fill.color; - if (type == "radial") { - fill.type = "gradientTitle"; - fill.focus = "100%"; - fill.focussize = "0 0"; - fill.focusposition = fxfy; - fill.angle = 0; - } else { - // fill.rotate= true; - fill.type = "gradient"; - fill.angle = (270 - angle) % 360; - } - o.appendChild(fill); - } - return 1; - }, - Element = function (node, vml) { - this[0] = this.node = node; - node.raphael = true; - this.id = R._oid++; - node.raphaelid = this.id; - this.X = 0; - this.Y = 0; - this.attrs = {}; - this.paper = vml; - this.matrix = R.matrix(); - this._ = { - transform: [], - sx: 1, - sy: 1, - dx: 0, - dy: 0, - deg: 0, - dirty: 1, - dirtyT: 1 - }; - !vml.bottom && (vml.bottom = this); - this.prev = vml.top; - vml.top && (vml.top.next = this); - vml.top = this; - this.next = null; - }; - var elproto = R.el; - - Element.prototype = elproto; - elproto.constructor = Element; - elproto.transform = function (tstr) { - if (tstr == null) { - return this._.transform; - } - var vbs = this.paper._viewBoxShift, - vbt = vbs ? "s" + [vbs.scale, vbs.scale] + "-1-1t" + [vbs.dx, vbs.dy] : E, - oldt; - if (vbs) { - oldt = tstr = Str(tstr).replace(/\.{3}|\u2026/g, this._.transform || E); - } - R._extractTransform(this, vbt + tstr); - var matrix = this.matrix.clone(), - skew = this.skew, - o = this.node, - split, - isGrad = ~Str(this.attrs.fill).indexOf("-"), - isPatt = !Str(this.attrs.fill).indexOf("url("); - matrix.translate(1, 1); - if (isPatt || isGrad || this.type == "image") { - skew.matrix = "1 0 0 1"; - skew.offset = "0 0"; - split = matrix.split(); - if ((isGrad && split.noRotation) || !split.isSimple) { - o.style.filter = matrix.toFilter(); - var bb = this.getBBox(), - bbt = this.getBBox(1), - dx = bb.x - bbt.x, - dy = bb.y - bbt.y; - o.coordorigin = (dx * -zoom) + S + (dy * -zoom); - setCoords(this, 1, 1, dx, dy, 0); - } else { - o.style.filter = E; - setCoords(this, split.scalex, split.scaley, split.dx, split.dy, split.rotate); - } - } else { - o.style.filter = E; - skew.matrix = Str(matrix); - skew.offset = matrix.offset(); - } - if (oldt !== null) { // empty string value is true as well - this._.transform = oldt; - R._extractTransform(this, oldt); - } - return this; - }; - elproto.rotate = function (deg, cx, cy) { - if (this.removed) { - return this; - } - if (deg == null) { - return; - } - deg = Str(deg).split(separator); - if (deg.length - 1) { - cx = toFloat(deg[1]); - cy = toFloat(deg[2]); - } - deg = toFloat(deg[0]); - (cy == null) && (cx = cy); - if (cx == null || cy == null) { - var bbox = this.getBBox(1); - cx = bbox.x + bbox.width / 2; - cy = bbox.y + bbox.height / 2; - } - this._.dirtyT = 1; - this.transform(this._.transform.concat([["r", deg, cx, cy]])); - return this; - }; - elproto.translate = function (dx, dy) { - if (this.removed) { - return this; - } - dx = Str(dx).split(separator); - if (dx.length - 1) { - dy = toFloat(dx[1]); - } - dx = toFloat(dx[0]) || 0; - dy = +dy || 0; - if (this._.bbox) { - this._.bbox.x += dx; - this._.bbox.y += dy; - } - this.transform(this._.transform.concat([["t", dx, dy]])); - return this; - }; - elproto.scale = function (sx, sy, cx, cy) { - if (this.removed) { - return this; - } - sx = Str(sx).split(separator); - if (sx.length - 1) { - sy = toFloat(sx[1]); - cx = toFloat(sx[2]); - cy = toFloat(sx[3]); - isNaN(cx) && (cx = null); - isNaN(cy) && (cy = null); - } - sx = toFloat(sx[0]); - (sy == null) && (sy = sx); - (cy == null) && (cx = cy); - if (cx == null || cy == null) { - var bbox = this.getBBox(1); - } - cx = cx == null ? bbox.x + bbox.width / 2 : cx; - cy = cy == null ? bbox.y + bbox.height / 2 : cy; - - this.transform(this._.transform.concat([["s", sx, sy, cx, cy]])); - this._.dirtyT = 1; - return this; - }; - elproto.hide = function () { - !this.removed && (this.node.style.display = "none"); - return this; - }; - elproto.show = function () { - !this.removed && (this.node.style.display = E); - return this; - }; - // Needed to fix the vml setViewBox issues - elproto.auxGetBBox = R.el.getBBox; - elproto.getBBox = function(){ - var b = this.auxGetBBox(); - if (this.paper && this.paper._viewBoxShift) - { - var c = {}; - var z = 1/this.paper._viewBoxShift.scale; - c.x = b.x - this.paper._viewBoxShift.dx; - c.x *= z; - c.y = b.y - this.paper._viewBoxShift.dy; - c.y *= z; - c.width = b.width * z; - c.height = b.height * z; - c.x2 = c.x + c.width; - c.y2 = c.y + c.height; - return c; - } - return b; - }; - elproto._getBBox = function () { - if (this.removed) { - return {}; - } - return { - x: this.X + (this.bbx || 0) - this.W / 2, - y: this.Y - this.H, - width: this.W, - height: this.H - }; - }; - elproto.remove = function () { - if (this.removed || !this.node.parentNode) { - return; - } - this.paper.__set__ && this.paper.__set__.exclude(this); - R.eve.unbind("raphael.*.*." + this.id); - R._tear(this, this.paper); - this.node.parentNode.removeChild(this.node); - this.shape && this.shape.parentNode.removeChild(this.shape); - for (var i in this) { - this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null; - } - this.removed = true; - }; - elproto.attr = function (name, value) { - if (this.removed) { - return this; - } - if (name == null) { - var res = {}; - for (var a in this.attrs) if (this.attrs[has](a)) { - res[a] = this.attrs[a]; - } - res.gradient && res.fill == "none" && (res.fill = res.gradient) && delete res.gradient; - res.transform = this._.transform; - return res; - } - if (value == null && R.is(name, "string")) { - if (name == fillString && this.attrs.fill == "none" && this.attrs.gradient) { - return this.attrs.gradient; - } - var names = name.split(separator), - out = {}; - for (var i = 0, ii = names.length; i < ii; i++) { - name = names[i]; - if (name in this.attrs) { - out[name] = this.attrs[name]; - } else if (R.is(this.paper.customAttributes[name], "function")) { - out[name] = this.paper.customAttributes[name].def; - } else { - out[name] = R._availableAttrs[name]; - } - } - return ii - 1 ? out : out[names[0]]; - } - if (this.attrs && value == null && R.is(name, "array")) { - out = {}; - for (i = 0, ii = name.length; i < ii; i++) { - out[name[i]] = this.attr(name[i]); - } - return out; - } - var params; - if (value != null) { - params = {}; - params[name] = value; - } - value == null && R.is(name, "object") && (params = name); - for (var key in params) { - eve("raphael.attr." + key + "." + this.id, this, params[key]); - } - if (params) { - for (key in this.paper.customAttributes) if (this.paper.customAttributes[has](key) && params[has](key) && R.is(this.paper.customAttributes[key], "function")) { - var par = this.paper.customAttributes[key].apply(this, [].concat(params[key])); - this.attrs[key] = params[key]; - for (var subkey in par) if (par[has](subkey)) { - params[subkey] = par[subkey]; - } - } - // this.paper.canvas.style.display = "none"; - if (params.text && this.type == "text") { - this.textpath.string = params.text; - } - setFillAndStroke(this, params); - // this.paper.canvas.style.display = E; - } - return this; - }; - elproto.toFront = function () { - !this.removed && this.node.parentNode.appendChild(this.node); - this.paper && this.paper.top != this && R._tofront(this, this.paper); - return this; - }; - elproto.toBack = function () { - if (this.removed) { - return this; - } - if (this.node.parentNode.firstChild != this.node) { - this.node.parentNode.insertBefore(this.node, this.node.parentNode.firstChild); - R._toback(this, this.paper); - } - return this; - }; - elproto.insertAfter = function (element) { - if (this.removed) { - return this; - } - if (element.constructor == R.st.constructor) { - element = element[element.length - 1]; - } - if (element.node.nextSibling) { - element.node.parentNode.insertBefore(this.node, element.node.nextSibling); - } else { - element.node.parentNode.appendChild(this.node); - } - R._insertafter(this, element, this.paper); - return this; - }; - elproto.insertBefore = function (element) { - if (this.removed) { - return this; - } - if (element.constructor == R.st.constructor) { - element = element[0]; - } - element.node.parentNode.insertBefore(this.node, element.node); - R._insertbefore(this, element, this.paper); - return this; - }; - elproto.blur = function (size) { - var s = this.node.runtimeStyle, - f = s.filter; - f = f.replace(blurregexp, E); - if (+size !== 0) { - this.attrs.blur = size; - s.filter = f + S + ms + ".Blur(pixelradius=" + (+size || 1.5) + ")"; - s.margin = R.format("-{0}px 0 0 -{0}px", round(+size || 1.5)); - } else { - s.filter = f; - s.margin = 0; - delete this.attrs.blur; - } - return this; - }; - - R._engine.path = function (pathString, vml) { - var el = createNode("shape"); - el.style.cssText = cssDot; - el.coordsize = zoom + S + zoom; - el.coordorigin = vml.coordorigin; - var p = new Element(el, vml), - attr = {fill: "none", stroke: "#000"}; - pathString && (attr.path = pathString); - p.type = "path"; - p.path = []; - p.Path = E; - setFillAndStroke(p, attr); - vml.canvas.appendChild(el); - var skew = createNode("skew"); - skew.on = true; - el.appendChild(skew); - p.skew = skew; - p.transform(E); - return p; - }; - R._engine.rect = function (vml, x, y, w, h, r) { - var path = R._rectPath(x, y, w, h, r), - res = vml.path(path), - a = res.attrs; - res.X = a.x = x; - res.Y = a.y = y; - res.W = a.width = w; - res.H = a.height = h; - a.r = r; - a.path = path; - res.type = "rect"; - return res; - }; - R._engine.ellipse = function (vml, x, y, rx, ry) { - var res = vml.path(), - a = res.attrs; - res.X = x - rx; - res.Y = y - ry; - res.W = rx * 2; - res.H = ry * 2; - res.type = "ellipse"; - setFillAndStroke(res, { - cx: x, - cy: y, - rx: rx, - ry: ry - }); - return res; - }; - R._engine.circle = function (vml, x, y, r) { - var res = vml.path(), - a = res.attrs; - res.X = x - r; - res.Y = y - r; - res.W = res.H = r * 2; - res.type = "circle"; - setFillAndStroke(res, { - cx: x, - cy: y, - r: r - }); - return res; - }; - R._engine.image = function (vml, src, x, y, w, h) { - var path = R._rectPath(x, y, w, h), - res = vml.path(path).attr({stroke: "none"}), - a = res.attrs, - node = res.node, - fill = node.getElementsByTagName(fillString)[0]; - a.src = src; - res.X = a.x = x; - res.Y = a.y = y; - res.W = a.width = w; - res.H = a.height = h; - a.path = path; - res.type = "image"; - fill.parentNode == node && node.removeChild(fill); - fill.rotate = true; - fill.src = src; - fill.type = "tile"; - res._.fillpos = [x, y]; - res._.fillsize = [w, h]; - node.appendChild(fill); - setCoords(res, 1, 1, 0, 0, 0); - return res; - }; - R._engine.text = function (vml, x, y, text) { - var el = createNode("shape"), - path = createNode("path"), - o = createNode("textpath"); - x = x || 0; - y = y || 0; - text = text || ""; - path.v = R.format("m{0},{1}l{2},{1}", round(x * zoom), round(y * zoom), round(x * zoom) + 1); - path.textpathok = true; - o.string = Str(text); - o.on = true; - el.style.cssText = cssDot; - el.coordsize = zoom + S + zoom; - el.coordorigin = "0 0"; - var p = new Element(el, vml), - attr = { - fill: "#000", - stroke: "none", - font: R._availableAttrs.font, - text: text - }; - p.shape = el; - p.path = path; - p.textpath = o; - p.type = "text"; - p.attrs.text = Str(text); - p.attrs.x = x; - p.attrs.y = y; - p.attrs.w = 1; - p.attrs.h = 1; - setFillAndStroke(p, attr); - el.appendChild(o); - el.appendChild(path); - vml.canvas.appendChild(el); - var skew = createNode("skew"); - skew.on = true; - el.appendChild(skew); - p.skew = skew; - p.transform(E); - return p; - }; - R._engine.setSize = function (width, height) { - var cs = this.canvas.style; - this.width = width; - this.height = height; - width == +width && (width += "px"); - height == +height && (height += "px"); - cs.width = width; - cs.height = height; - cs.clip = "rect(0 " + width + " " + height + " 0)"; - if (this._viewBox) { - R._engine.setViewBox.apply(this, this._viewBox); - } - return this; - }; - R._engine.setViewBox = function (x, y, w, h, fit) { - R.eve("raphael.setViewBox", this, this._viewBox, [x, y, w, h, fit]); - var paperSize = this.getSize(), - width = paperSize.width, - height = paperSize.height, - H, W; - if (fit) { - H = height / h; - W = width / w; - if (w * H < width) { - x -= (width - w * H) / 2 / H; - } - if (h * W < height) { - y -= (height - h * W) / 2 / W; - } - } - this._viewBox = [x, y, w, h, !!fit]; - this._viewBoxShift = { - dx: -x, - dy: -y, - scale: paperSize - }; - this.forEach(function (el) { - el.transform("..."); - }); - return this; - }; - var createNode; - R._engine.initWin = function (win) { - var doc = win.document; - if (doc.styleSheets.length < 31) { - doc.createStyleSheet().addRule(".rvml", "behavior:url(#default#VML)"); - } else { - // no more room, add to the existing one - // http://msdn.microsoft.com/en-us/library/ms531194%28VS.85%29.aspx - doc.styleSheets[0].addRule(".rvml", "behavior:url(#default#VML)"); - } - try { - !doc.namespaces.rvml && doc.namespaces.add("rvml", "urn:schemas-microsoft-com:vml"); - createNode = function (tagName) { - return doc.createElement('<rvml:' + tagName + ' class="rvml">'); - }; - } catch (e) { - createNode = function (tagName) { - return doc.createElement('<' + tagName + ' xmlns="urn:schemas-microsoft.com:vml" class="rvml">'); - }; - } - }; - R._engine.initWin(R._g.win); - R._engine.create = function () { - var con = R._getContainer.apply(0, arguments), - container = con.container, - height = con.height, - s, - width = con.width, - x = con.x, - y = con.y; - if (!container) { - throw new Error("VML container not found."); - } - var res = new R._Paper, - c = res.canvas = R._g.doc.createElement("div"), - cs = c.style; - x = x || 0; - y = y || 0; - width = width || 512; - height = height || 342; - res.width = width; - res.height = height; - width == +width && (width += "px"); - height == +height && (height += "px"); - res.coordsize = zoom * 1e3 + S + zoom * 1e3; - res.coordorigin = "0 0"; - res.span = R._g.doc.createElement("span"); - res.span.style.cssText = "position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;"; - c.appendChild(res.span); - cs.cssText = R.format("top:0;left:0;width:{0};height:{1};display:inline-block;position:relative;clip:rect(0 {0} {1} 0);overflow:hidden", width, height); - if (container == 1) { - R._g.doc.body.appendChild(c); - cs.left = x + "px"; - cs.top = y + "px"; - cs.position = "absolute"; - } else { - if (container.firstChild) { - container.insertBefore(c, container.firstChild); - } else { - container.appendChild(c); - } - } - res.renderfix = function () {}; - return res; - }; - R.prototype.clear = function () { - R.eve("raphael.clear", this); - this.canvas.innerHTML = E; - this.span = R._g.doc.createElement("span"); - this.span.style.cssText = "position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline;"; - this.canvas.appendChild(this.span); - this.bottom = this.top = null; - }; - R.prototype.remove = function () { - R.eve("raphael.remove", this); - this.canvas.parentNode.removeChild(this.canvas); - for (var i in this) { - this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null; - } - return true; - }; - - var setproto = R.st; - for (var method in elproto) if (elproto[has](method) && !setproto[has](method)) { - setproto[method] = (function (methodname) { - return function () { - var arg = arguments; - return this.forEach(function (el) { - el[methodname].apply(el, arg); - }); - }; - })(method); - } -})(); - - // EXPOSE - // SVG and VML are appended just before the EXPOSE line - // Even with AMD, Raphael should be defined globally - oldRaphael.was ? (g.win.Raphael = R) : (Raphael = R); - - if(typeof exports == "object"){ - module.exports = R; - } - return R; -})); diff --git a/yarn.lock b/yarn.lock index fd8d67a6b0d..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" @@ -1746,6 +1754,10 @@ etag@~1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" +eve-raphael@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/eve-raphael/-/eve-raphael-0.5.0.tgz#17c754b792beef3fa6684d79cf5a47c63c4cda30" + event-emitter@~0.3.4: version "0.3.4" resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.4.tgz#8d63ddfb4cfe1fae3b32ca265c4c720222080bb5" @@ -3554,6 +3566,12 @@ range-parser@^1.0.3, range-parser@^1.2.0, range-parser@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" +raphael@^2.2.7: + version "2.2.7" + resolved "https://registry.yarnpkg.com/raphael/-/raphael-2.2.7.tgz#231b19141f8d086986d8faceb66f8b562ee2c810" + dependencies: + eve-raphael "0.5.0" + raw-body@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96" @@ -4105,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" |