diff options
148 files changed, 2297 insertions, 793 deletions
diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..5f8702cf8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/.env.production.sample b/.env.production.sample index d7c04e235..97bba5e3f 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -35,6 +35,10 @@ SMTP_PORT=587 SMTP_LOGIN= SMTP_PASSWORD= SMTP_FROM_ADDRESS=notifications@example.com +#SMTP_AUTH_METHOD=plain +#SMTP_OPENSSL_VERIFY_MODE=peer +#SMTP_ENABLE_STARTTLS_AUTO=true + # Optional asset host for multi-server setups # CDN_HOST=assets.example.com @@ -63,3 +67,7 @@ SMTP_FROM_ADDRESS=notifications@example.com # Streaming API integration # STREAMING_API_BASE_URL= + +# Advanced settings +# If you need to use pgBouncer, you need to disable prepared statements: +# PREPARED_STATEMENTS=false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..6d540c413 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,30 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore the default SQLite database. +/db/*.sqlite3 +/db/*.sqlite3-journal + +# Ignore all logfiles and tempfiles. +/log/* +!/log/.keep +/tmp +coverage +public/system +public/assets +.env +.env.production +node_modules/ +neo4j/ + +# Ignore Vagrant files +.vagrant/ + +# Ignore Capistrano customizations +config/deploy/* diff --git a/.ruby-version b/.ruby-version index 2bf1c1ccf..005119baa 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.3.1 +2.4.1 diff --git a/.travis.yml b/.travis.yml index b1b0c2bcd..a9824ccf7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ addons: postgresql: 9.4 rvm: - - 2.3.1 + - 2.4.1 services: - redis-server diff --git a/Dockerfile b/Dockerfile index 57a8f34e9..a05525b33 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.3.1-alpine +FROM ruby:2.4.1-alpine LABEL maintainer="https://github.com/tootsuite/mastodon" \ description="A GNU Social-compatible microblogging server" diff --git a/Gemfile b/Gemfile index 65bd5eb49..9a1792623 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ # frozen_string_literal: true source 'https://rubygems.org' -ruby '2.3.1' +ruby '2.4.1' gem 'rails', '~> 5.0.2' gem 'sass-rails', '~> 5.0' @@ -21,36 +21,37 @@ gem 'paperclip', '~> 5.1' gem 'paperclip-av-transcoder' gem 'aws-sdk', '>= 2.0' -gem 'http' -gem 'httplog' gem 'addressable' -gem 'nokogiri' -gem 'link_header' -gem 'ostatus2' -gem 'goldfinger' gem 'devise' gem 'devise-two-factor' gem 'doorkeeper' -gem 'rabl' -gem 'rqrcode' -gem 'twitter-text' -gem 'ox' -gem 'oj' -gem 'hiredis' -gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis'] gem 'fast_blank' +gem 'goldfinger' +gem 'hiredis' gem 'htmlentities' -gem 'simple_form' -gem 'will_paginate' +gem 'http' +gem 'http_accept_language' +gem 'httplog' +gem 'kaminari' +gem 'link_header' +gem 'nokogiri' +gem 'oj' +gem 'ostatus2' +gem 'ox' +gem 'rabl' gem 'rack-attack' gem 'rack-cors', require: 'rack/cors' +gem 'rack-timeout' +gem 'rails-settings-cached' +gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis'] +gem 'rqrcode' +gem 'ruby-oembed', require: 'oembed' gem 'sidekiq' gem 'sidekiq-unique-jobs' -gem 'rails-settings-cached' gem 'simple-navigation' +gem 'simple_form' gem 'statsd-instrument' -gem 'ruby-oembed', require: 'oembed' -gem 'rack-timeout' +gem 'twitter-text' gem 'tzinfo-data' gem 'react-rails' @@ -67,6 +68,7 @@ end group :test do gem 'faker' + gem 'rails-controller-testing' gem 'rspec-sidekiq' gem 'simplecov', require: false gem 'webmock' diff --git a/Gemfile.lock b/Gemfile.lock index f2a199931..f1bc9880e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -24,7 +24,7 @@ GEM erubis (~> 2.7.0) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - active_record_query_trace (1.5.3) + active_record_query_trace (1.5.4) activejob (5.0.2) activesupport (= 5.0.2) globalid (>= 0.3.6) @@ -39,7 +39,7 @@ GEM i18n (~> 0.7) minitest (~> 5.1) tzinfo (~> 1.1) - addressable (2.5.0) + addressable (2.5.1) public_suffix (~> 2.0, >= 2.0.2) airbrussh (1.1.2) sshkit (>= 1.6.1, != 1.7.0) @@ -47,17 +47,17 @@ GEM ast (2.3.0) attr_encrypted (3.0.3) encryptor (~> 3.0.0) - autoprefixer-rails (6.5.0.2) + autoprefixer-rails (6.7.7.1) execjs av (0.9.0) cocaine (~> 0.5.3) - aws-sdk (2.6.28) - aws-sdk-resources (= 2.6.28) - aws-sdk-core (2.6.28) + aws-sdk (2.9.6) + aws-sdk-resources (= 2.9.6) + aws-sdk-core (2.9.6) aws-sigv4 (~> 1.0) jmespath (~> 1.0) - aws-sdk-resources (2.6.28) - aws-sdk-core (= 2.6.28) + aws-sdk-resources (2.9.6) + aws-sdk-core (= 2.9.6) aws-sigv4 (1.0.0) babel-source (5.8.35) babel-transpiler (0.7.0) @@ -78,12 +78,11 @@ GEM railties (>= 4.0.0, < 5.1) sprockets (>= 3.6.0) builder (3.2.3) - bullet (5.3.0) + bullet (5.5.1) activesupport (>= 3.0.0) uniform_notifier (~> 1.10.0) - capistrano (3.7.2) + capistrano (3.8.0) airbrussh (>= 1.0.0) - capistrano-harrow i18n rake (>= 10.0.0) sshkit (>= 1.9.0) @@ -92,8 +91,7 @@ GEM sshkit (~> 1.2) capistrano-faster-assets (1.0.2) capistrano (>= 3.1) - capistrano-harrow (0.5.3) - capistrano-rails (1.2.2) + capistrano-rails (1.2.3) capistrano (~> 3.1) capistrano-bundler (~> 1.1) capistrano-rbenv (2.1.0) @@ -119,7 +117,7 @@ GEM crack (0.4.3) safe_yaml (~> 1.0.0) debug_inspector (0.0.2) - devise (4.2.0) + devise (4.2.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0, < 5.1) @@ -131,16 +129,16 @@ GEM devise (~> 4.0) railties rotp (~> 2.0) - diff-lcs (1.2.5) + diff-lcs (1.3) docile (1.1.5) - domain_name (0.5.20161129) + domain_name (0.5.20170404) unf (>= 0.0.5, < 1.0.0) - doorkeeper (4.2.0) + doorkeeper (4.2.5) railties (>= 4.2) - dotenv (2.1.1) - dotenv-rails (2.1.1) - dotenv (= 2.1.1) - railties (>= 4.0, < 5.1) + dotenv (2.2.0) + dotenv-rails (2.2.0) + dotenv (= 2.2.0) + railties (>= 3.2, < 5.1) easy_translate (0.5.0) json thread @@ -148,14 +146,14 @@ GEM encryptor (3.0.0) erubis (2.7.0) execjs (2.7.0) - fabrication (2.15.2) - faker (1.6.6) + fabrication (2.16.1) + faker (1.7.3) i18n (~> 0.5) fast_blank (1.0.0) - font-awesome-rails (4.6.3.1) + font-awesome-rails (4.7.0.1) railties (>= 3.2, < 5.1) - fuubar (2.1.1) - rspec (~> 3.0) + fuubar (2.2.0) + rspec-core (~> 3.0) ruby-progressbar (~> 1.4) globalid (0.3.7) activesupport (>= 4.1.0) @@ -163,20 +161,20 @@ GEM addressable (~> 2.4) http (~> 2.0) nokogiri (~> 1.6) - hamlit (2.7.2) - temple (~> 0.7.6) + hamlit (2.8.1) + temple (>= 0.8.0) thor tilt - hamlit-rails (0.1.0) + hamlit-rails (0.2.0) actionpack (>= 4.0.1) activesupport (>= 4.0.1) hamlit (>= 1.2.0) railties (>= 4.0.1) - hashdiff (0.3.0) + hashdiff (0.3.2) highline (1.7.8) hiredis (0.6.1) htmlentities (4.3.4) - http (2.1.0) + http (2.2.1) addressable (~> 2.3) http-cookie (~> 1.0) http-form_data (~> 1.0.1) @@ -184,11 +182,12 @@ GEM http-cookie (1.0.3) domain_name (~> 0.5) http-form_data (1.0.1) + http_accept_language (2.1.0) http_parser.rb (0.6.0) - httplog (0.3.2) + httplog (0.99.2) colorize i18n (0.8.1) - i18n-tasks (0.9.6) + i18n-tasks (0.9.13) activesupport (>= 4.0.2) ast (>= 2.1.0) easy_translate (>= 0.5.0) @@ -196,19 +195,31 @@ GEM highline (>= 1.7.3) i18n parser (>= 2.2.3.0) - term-ansicolor (>= 1.3.2) + rainbow (~> 2.2) terminal-table (>= 1.5.1) jmespath (1.3.1) - jquery-rails (4.1.1) + jquery-rails (4.3.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (1.8.3) + json (2.0.3) + kaminari (1.0.1) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.0.1) + kaminari-activerecord (= 1.0.1) + kaminari-core (= 1.0.1) + kaminari-actionview (1.0.1) + actionview + kaminari-core (= 1.0.1) + kaminari-activerecord (1.0.1) + activerecord + kaminari-core (= 1.0.1) + kaminari-core (1.0.1) launchy (2.4.3) addressable (~> 2.3) letter_opener (1.4.1) launchy (~> 2.2) - letter_opener_web (1.3.0) + letter_opener_web (1.3.1) actionmailer (>= 3.2) letter_opener (~> 1.0) railties (>= 3.2) @@ -230,11 +241,11 @@ GEM minitest (5.10.1) net-scp (1.2.1) net-ssh (>= 2.6.5) - net-ssh (4.0.1) + net-ssh (4.1.0) nio4r (2.0.0) nokogiri (1.7.1) mini_portile2 (~> 2.1.0) - oj (2.17.3) + oj (2.18.5) orm_adapter (0.5.0) ostatus2 (1.0.2) addressable (~> 2.4) @@ -250,26 +261,26 @@ GEM paperclip-av-transcoder (0.6.4) av (~> 0.9.0) paperclip (>= 2.5.2) - parser (2.3.1.2) + parser (2.4.0.0) ast (~> 2.2) - pg (0.18.4) - pghero (1.6.2) + pg (0.20.0) + pghero (1.6.4) activerecord powerpack (0.1.1) pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) - pry-rails (0.3.4) - pry (>= 0.9.10) - public_suffix (2.0.4) - puma (3.6.0) + pry-rails (0.3.6) + pry (>= 0.10.4) + public_suffix (2.0.5) + puma (3.8.2) rabl (0.13.1) activesupport (>= 2.3.14) rack (2.0.1) rack-attack (5.0.1) rack - rack-cors (0.4.0) + rack-cors (0.4.1) rack-protection (1.5.3) rack rack-test (0.6.3) @@ -287,6 +298,10 @@ GEM bundler (>= 1.3.0, < 2.0) railties (= 5.0.2) sprockets-rails (>= 2.0.0) + rails-controller-testing (1.0.1) + actionpack (~> 5.x) + actionview (~> 5.x) + activesupport (~> 5.x) rails-dom-testing (2.0.2) activesupport (>= 4.2.0, < 6.0) nokogiri (~> 1.6) @@ -305,42 +320,37 @@ GEM method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rainbow (2.1.0) + rainbow (2.2.1) rake (12.0.0) - react-rails (1.10.0) + react-rails (1.11.0) babel-transpiler (>= 0.7.0) - coffee-script-source (~> 1.8) connection_pool execjs railties (>= 3.2) tilt - redis (3.3.2) - redis-actionpack (5.0.0) - actionpack (>= 4.0.0, < 6) - redis-rack (~> 2.0.0.pre) - redis-store (~> 1.2.0.pre) - redis-activesupport (5.0.1) + redis (3.3.3) + redis-actionpack (5.0.1) + actionpack (>= 4.0, < 6) + redis-rack (>= 1, < 3) + redis-store (>= 1.1.0, < 1.4.0) + redis-activesupport (5.0.2) activesupport (>= 3, < 6) - redis-store (~> 1.2.0) - redis-rack (2.0.0) - rack (~> 2.0) - redis-store (~> 1.2.0) - redis-rails (5.0.1) - redis-actionpack (~> 5.0.0) - redis-activesupport (~> 5.0.0) - redis-store (~> 1.2.0) - redis-store (1.2.0) + redis-store (~> 1.3.0) + redis-rack (2.0.1) + rack (>= 2.0, < 3) + redis-store (>= 1.2, < 1.4) + redis-rails (5.0.2) + redis-actionpack (>= 5.0, < 6) + redis-activesupport (>= 5.0, < 6) + redis-store (>= 1.2, < 2) + redis-store (1.3.0) redis (>= 2.2) responders (2.3.0) railties (>= 4.2.0, < 5.1) rotp (2.1.2) rqrcode (0.10.1) chunky_png (~> 1.0) - rspec (3.5.0) - rspec-core (~> 3.5.0) - rspec-expectations (~> 3.5.0) - rspec-mocks (~> 3.5.0) - rspec-core (3.5.2) + rspec-core (3.5.4) rspec-support (~> 3.5.0) rspec-expectations (3.5.0) diff-lcs (>= 1.2.0, < 2.0) @@ -348,7 +358,7 @@ GEM rspec-mocks (3.5.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.5.0) - rspec-rails (3.5.1) + rspec-rails (3.5.2) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) @@ -356,40 +366,40 @@ GEM rspec-expectations (~> 3.5.0) rspec-mocks (~> 3.5.0) rspec-support (~> 3.5.0) - rspec-sidekiq (2.2.0) - rspec (~> 3.0, >= 3.0.0) + rspec-sidekiq (3.0.0) + rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) rspec-support (3.5.0) - rubocop (0.42.0) - parser (>= 2.3.1.1, < 3.0) + rubocop (0.48.1) + parser (>= 2.3.3.1, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) - ruby-oembed (0.10.1) + ruby-oembed (0.12.0) ruby-progressbar (1.8.1) safe_yaml (1.0.4) - sass (3.4.22) + sass (3.4.23) sass-rails (5.0.6) railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) - sidekiq (4.2.7) + sidekiq (4.2.10) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) redis (~> 3.2, >= 3.2.1) - sidekiq-unique-jobs (4.0.18) - sidekiq (>= 2.6) + sidekiq-unique-jobs (5.0.0) + sidekiq (>= 4.0) thor - simple-navigation (4.0.3) + simple-navigation (4.0.5) activesupport (>= 2.3.2) - simple_form (3.2.1) + simple_form (3.4.0) actionpack (> 4, < 5.1) activemodel (> 4, < 5.1) - simplecov (0.12.0) + simplecov (0.14.1) docile (~> 1.1.0) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) @@ -402,43 +412,39 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sshkit (1.11.5) + sshkit (1.13.1) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) statsd-instrument (2.1.2) - temple (0.7.7) - term-ansicolor (1.4.0) - tins (~> 1.0) - terminal-table (1.7.0) - unicode-display_width (~> 1.1) + temple (0.8.0) + terminal-table (1.7.3) + unicode-display_width (~> 1.1.1) thor (0.19.4) thread (0.2.2) thread_safe (0.3.6) - tilt (2.0.6) - tins (1.12.0) + tilt (2.0.7) twitter-text (1.14.5) unf (~> 0.1.0) - tzinfo (1.2.2) + tzinfo (1.2.3) thread_safe (~> 0.1) tzinfo-data (1.2017.2) tzinfo (>= 1.0.0) - uglifier (3.0.1) + uglifier (3.2.0) execjs (>= 0.3.0, < 3) unf (0.1.4) unf_ext unf_ext (0.0.7.2) - unicode-display_width (1.1.0) + unicode-display_width (1.1.3) uniform_notifier (1.10.0) - warden (1.2.6) + warden (1.2.7) rack (>= 1.0) - webmock (2.1.0) + webmock (2.3.2) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) - will_paginate (3.1.0) PLATFORMS ruby @@ -473,9 +479,11 @@ DEPENDENCIES hiredis htmlentities http + http_accept_language httplog i18n-tasks (~> 0.9.6) jquery-rails + kaminari letter_opener letter_opener_web link_header @@ -495,6 +503,7 @@ DEPENDENCIES rack-cors rack-timeout rails (~> 5.0.2) + rails-controller-testing rails-settings-cached rails_12factor react-rails @@ -516,10 +525,9 @@ DEPENDENCIES tzinfo-data uglifier (>= 1.3.0) webmock - will_paginate RUBY VERSION - ruby 2.3.1p112 + ruby 2.4.1p111 BUNDLED WITH - 1.14.3 + 1.14.6 diff --git a/README.md b/README.md index fa944a901..41990ff70 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Consult the example configuration file, `.env.production.sample` for the full li [![](https://images.microbadger.com/badges/version/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own version badge on microbadger.com") [![](https://images.microbadger.com/badges/image/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own image badge on microbadger.com") -The project now includes a `Dockerfile` and a `docker-compose.yml`. You need to turn `.env.production.sample` into `.env.production` with all the variables set before you can: +The project now includes a `Dockerfile` and a `docker-compose.yml` file (which requires at least docker-compose version `1.10.0`). You need to turn `.env.production.sample` into `.env.production` with all the variables set before you can: docker-compose build diff --git a/Vagrantfile b/Vagrantfile index cd7f74473..90f604640 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -46,12 +46,12 @@ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build export PATH="$HOME/.rbenv/bin::$PATH" eval "$(rbenv init -)" -echo "Compiling Ruby 2.3.1: warning, this takes a while!!!" -rbenv install 2.3.1 -rbenv global 2.3.1 - cd /vagrant +echo "Compiling Ruby $(cat .ruby-version): warning, this takes a while!!!" +rbenv install $(cat .ruby-version) +rbenv global $(cat .ruby-version) + # Configure database sudo -u postgres createuser -U postgres vagrant -s sudo -u postgres createdb -U postgres mastodon_development diff --git a/app.json b/app.json index 29c1f9f9c..6c4294c79 100644 --- a/app.json +++ b/app.json @@ -79,6 +79,18 @@ "SMTP_FROM_ADDRESS": { "description": "Address to send emails from", "required": false + }, + "SMTP_AUTH_METHOD": { + "description": "Authentication method to use with SMTP server. Default is 'plain'.", + "required": false + }, + "SMTP_OPENSSL_VERIFY_MODE": { + "description": "SMTP server certificate verification mode. Defaults is 'peer'.", + "required": false + }, + "SMTP_ENABLE_STARTTLS_AUTO": { + "description": "Enable STARTTLS if SMTP server supports it? Default is true.", + "required": false } }, "buildpacks": [ diff --git a/app/assets/javascripts/components/actions/notifications.jsx b/app/assets/javascripts/components/actions/notifications.jsx index 980b7d63e..11e814e1f 100644 --- a/app/assets/javascripts/components/actions/notifications.jsx +++ b/app/assets/javascripts/components/actions/notifications.jsx @@ -61,6 +61,8 @@ export function refreshNotifications() { params.since_id = ids.first().get('id'); } + params.exclude_types = getState().getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); + api(getState).get('/api/v1/notifications', { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); @@ -105,11 +107,11 @@ export function expandNotifications() { dispatch(expandNotificationsRequest()); - api(getState).get(url, { - params: { - limit: 5 - } - }).then(response => { + const params = {}; + + params.exclude_types = getState().getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); + + api(getState).get(url, params).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null)); diff --git a/app/assets/javascripts/components/components/account.jsx b/app/assets/javascripts/components/components/account.jsx index 7a1c9f5ce..782cf382d 100644 --- a/app/assets/javascripts/components/components/account.jsx +++ b/app/assets/javascripts/components/components/account.jsx @@ -65,7 +65,7 @@ const Account = React.createClass({ <div className='account'> <div style={{ display: 'flex' }}> <Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}> - <div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={36} /></div> + <div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={36} /></div> <DisplayName account={account} /> </Permalink> diff --git a/app/assets/javascripts/components/components/avatar.jsx b/app/assets/javascripts/components/components/avatar.jsx index 0237a1904..673b1a247 100644 --- a/app/assets/javascripts/components/components/avatar.jsx +++ b/app/assets/javascripts/components/components/avatar.jsx @@ -1,103 +1,18 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; -// From: http://stackoverflow.com/a/18320662 -const resample = (canvas, width, height, resize_canvas) => { - let width_source = canvas.width; - let height_source = canvas.height; - width = Math.round(width); - height = Math.round(height); - - let ratio_w = width_source / width; - let ratio_h = height_source / height; - let ratio_w_half = Math.ceil(ratio_w / 2); - let ratio_h_half = Math.ceil(ratio_h / 2); - - let ctx = canvas.getContext("2d"); - let img = ctx.getImageData(0, 0, width_source, height_source); - let img2 = ctx.createImageData(width, height); - let data = img.data; - let data2 = img2.data; - - for (let j = 0; j < height; j++) { - for (let i = 0; i < width; i++) { - let x2 = (i + j * width) * 4; - let weight = 0; - let weights = 0; - let weights_alpha = 0; - let gx_r = 0; - let gx_g = 0; - let gx_b = 0; - let gx_a = 0; - let center_y = (j + 0.5) * ratio_h; - let yy_start = Math.floor(j * ratio_h); - let yy_stop = Math.ceil((j + 1) * ratio_h); - - for (let yy = yy_start; yy < yy_stop; yy++) { - let dy = Math.abs(center_y - (yy + 0.5)) / ratio_h_half; - let center_x = (i + 0.5) * ratio_w; - let w0 = dy * dy; //pre-calc part of w - let xx_start = Math.floor(i * ratio_w); - let xx_stop = Math.ceil((i + 1) * ratio_w); - - for (let xx = xx_start; xx < xx_stop; xx++) { - let dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half; - let w = Math.sqrt(w0 + dx * dx); - - if (w >= 1) { - // pixel too far - continue; - } - - // hermite filter - weight = 2 * w * w * w - 3 * w * w + 1; - let pos_x = 4 * (xx + yy * width_source); - - // alpha - gx_a += weight * data[pos_x + 3]; - weights_alpha += weight; - - // colors - if (data[pos_x + 3] < 255) - weight = weight * data[pos_x + 3] / 250; - - gx_r += weight * data[pos_x]; - gx_g += weight * data[pos_x + 1]; - gx_b += weight * data[pos_x + 2]; - weights += weight; - } - } - - data2[x2] = gx_r / weights; - data2[x2 + 1] = gx_g / weights; - data2[x2 + 2] = gx_b / weights; - data2[x2 + 3] = gx_a / weights_alpha; - } - } - - // clear and resize canvas - if (resize_canvas === true) { - canvas.width = width; - canvas.height = height; - } else { - ctx.clearRect(0, 0, width_source, height_source); - } - - // draw - ctx.putImageData(img2, 0, 0); -}; - const Avatar = React.createClass({ propTypes: { src: React.PropTypes.string.isRequired, + staticSrc: React.PropTypes.string, size: React.PropTypes.number.isRequired, style: React.PropTypes.object, - animated: React.PropTypes.bool + animate: React.PropTypes.bool }, getDefaultProps () { return { - animated: true + animate: false }; }, @@ -117,38 +32,30 @@ const Avatar = React.createClass({ this.setState({ hovering: false }); }, - handleLoad () { - this.canvas.width = this.image.naturalWidth; - this.canvas.height = this.image.naturalHeight; - this.canvas.getContext('2d').drawImage(this.image, 0, 0); - - resample(this.canvas, this.props.size * window.devicePixelRatio, this.props.size * window.devicePixelRatio, true); - }, - - setImageRef (c) { - this.image = c; - }, - - setCanvasRef (c) { - this.canvas = c; - }, - render () { + const { src, size, staticSrc, animate } = this.props; const { hovering } = this.state; - if (this.props.animated) { - return ( - <div style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px` }}> - <img src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ borderRadius: '4px' }} /> - </div> - ); + const style = { + ...this.props.style, + width: `${size}px`, + height: `${size}px`, + backgroundSize: `${size}px ${size}px` + }; + + if (hovering || animate) { + style.backgroundImage = `url(${src})`; + } else { + style.backgroundImage = `url(${staticSrc})`; } return ( - <div onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px`, position: 'relative' }}> - <img ref={this.setImageRef} onLoad={this.handleLoad} src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ position: 'absolute', top: '0', left: '0', opacity: hovering ? '1' : '0', borderRadius: '4px' }} /> - <canvas ref={this.setCanvasRef} style={{ borderRadius: '4px', width: this.props.size, height: this.props.size, opacity: hovering ? '0' : '1' }} /> - </div> + <div + className='avatar' + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} + style={style} + /> ); } diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx index 60bf531e5..c4d5f829b 100644 --- a/app/assets/javascripts/components/components/status.jsx +++ b/app/assets/javascripts/components/components/status.jsx @@ -91,7 +91,7 @@ const Status = React.createClass({ <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px' }}> <div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}> - <Avatar src={status.getIn(['account', 'avatar'])} size={48} /> + <Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /> </div> <DisplayName account={status.get('account')} /> diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx index 6c25afdea..9cf03bb32 100644 --- a/app/assets/javascripts/components/components/status_content.jsx +++ b/app/assets/javascripts/components/components/status_content.jsx @@ -36,6 +36,7 @@ const StatusContent = React.createClass({ if (mention) { link.addEventListener('click', this.onMentionClick.bind(this, mention), false); + link.setAttribute('title', mention.get('acct')); } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); } else if (media) { @@ -91,7 +92,7 @@ const StatusContent = React.createClass({ const { status } = this.props; const { hidden } = this.state; - const content = { __html: emojify(status.get('content')) }; + const content = { __html: emojify(status.get('content')).replace(/\n/g, '') }; const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) }; const directionStyle = { direction: 'ltr' }; @@ -125,7 +126,7 @@ const StatusContent = React.createClass({ <div style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} /> </div> ); - } else { + } else if (this.props.onClick) { return ( <div className='status__content' @@ -135,6 +136,14 @@ const StatusContent = React.createClass({ dangerouslySetInnerHTML={content} /> ); + } else { + return ( + <div + className='status__content' + style={{ ...directionStyle }} + dangerouslySetInnerHTML={content} + /> + ); } }, diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index 00f20074d..fea8b1594 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -48,6 +48,8 @@ import hu from 'react-intl/locale-data/hu'; import uk from 'react-intl/locale-data/uk'; import fi from 'react-intl/locale-data/fi'; import eo from 'react-intl/locale-data/eo'; +import ru from 'react-intl/locale-data/ru'; + import getMessagesForLocale from '../locales'; import { hydrateStore } from '../actions/store'; import createStream from '../stream'; @@ -60,7 +62,9 @@ const browserHistory = useRouterHistory(createBrowserHistory)({ basename: '/web' }); -addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi, ...eo]); + +addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi, ...eo, ...ru]); + const Mastodon = React.createClass({ diff --git a/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx b/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx index 5591b45cf..9e05193fb 100644 --- a/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx +++ b/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx @@ -4,7 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; const AutosuggestAccount = ({ account }) => ( <div style={{ overflow: 'hidden' }} className='autosuggest-account'> - <div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} size={18} /></div> + <div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={18} /></div> <DisplayName account={account} /> </div> ); diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx index b016d3f28..cb4b62f6c 100644 --- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx +++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx @@ -83,11 +83,23 @@ const ComposeForm = React.createClass({ this.props.onChangeSpoilerText(e.target.value); }, + componentWillReceiveProps (nextProps) { + // If this is the update where we've finished uploading, + // save the last caret position so we can restore it below! + if (!nextProps.is_uploading && this.props.is_uploading) { + this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart; + } + }, + componentDidUpdate (prevProps) { - if (this.props.focusDate !== prevProps.focusDate) { - // If replying to zero or one users, places the cursor at the end of the textbox. - // If replying to more than one user, selects any usernames past the first; - // this provides a convenient shortcut to drop everyone else from the conversation. + // This statement does several things: + // - If we're beginning a reply, and, + // - Replying to zero or one users, places the cursor at the end of the textbox. + // - Replying to more than one user, selects any usernames past the first; + // this provides a convenient shortcut to drop everyone else from the conversation. + // - If we've just finished uploading an image, and have a saved caret position, + // restores the cursor to that position after the text changes! + if (this.props.focusDate !== prevProps.focusDate || (prevProps.is_uploading && !this.props.is_uploading && typeof this._restoreCaret === 'number')) { let selectionEnd, selectionStart; if (this.props.preselectDate !== prevProps.preselectDate) { @@ -118,7 +130,7 @@ const ComposeForm = React.createClass({ render () { const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props; - const disabled = this.props.is_submitting || this.props.is_uploading; + const disabled = this.props.is_submitting; let publishText = ''; let privacyWarning = ''; diff --git a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx index 1920b29bf..fa577ce26 100644 --- a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx +++ b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx @@ -47,7 +47,7 @@ const EmojiPickerDropdown = React.createClass({ </DropdownTrigger> <DropdownContent className='dropdown__left'> - <EmojiPicker emojione={settings} onChange={this.handleChange} /> + <EmojiPicker emojione={settings} onChange={this.handleChange} search={true} /> </DropdownContent> </Dropdown> ); diff --git a/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx b/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx index 076ac7cbb..1a748a23c 100644 --- a/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx +++ b/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx @@ -17,7 +17,7 @@ const NavigationBar = React.createClass({ render () { return ( <div className='navigation-bar'> - <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} size={40} /></Permalink> + <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} animate size={40} /></Permalink> <div style={{ flex: '1 1 auto', marginLeft: '8px' }}> <strong style={{ fontWeight: '500', display: 'block' }}>{this.props.account.get('acct')}</strong> diff --git a/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx b/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx index a72bd32c2..11a89449e 100644 --- a/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx +++ b/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx @@ -50,7 +50,7 @@ const ReplyIndicator = React.createClass({ <div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div> <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}> - <div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} /></div> + <div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div> <DisplayName account={status.get('account')} /> </a> </div> diff --git a/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx index 1766655c2..9c713287c 100644 --- a/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx +++ b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx @@ -33,7 +33,7 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => { <div> <div style={outerStyle}> <Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}> - <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={48} /></div> + <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div> <DisplayName account={account} /> </Permalink> diff --git a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx index 62c3e61e0..debbfd01f 100644 --- a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx +++ b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx @@ -4,16 +4,6 @@ const messages = defineMessages({ clear: { id: 'notifications.clear', defaultMessage: 'Clear notifications' } }); -const iconStyle = { - fontSize: '16px', - padding: '15px', - position: 'absolute', - right: '48px', - top: '0', - cursor: 'pointer', - zIndex: '2' -}; - const ClearColumnButton = React.createClass({ propTypes: { @@ -25,7 +15,7 @@ const ClearColumnButton = React.createClass({ const { intl } = this.props; return ( - <div title={intl.formatMessage(messages.clear)} className='column-icon' tabIndex='0' style={iconStyle} onClick={this.onClick}> + <div title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}> <i className='fa fa-eraser' /> </div> ); diff --git a/app/assets/javascripts/components/features/notifications/components/notification.jsx b/app/assets/javascripts/components/features/notifications/components/notification.jsx index 0de4df52e..0607466d0 100644 --- a/app/assets/javascripts/components/features/notifications/components/notification.jsx +++ b/app/assets/javascripts/components/features/notifications/components/notification.jsx @@ -21,7 +21,7 @@ const Notification = React.createClass({ renderFollow (account, link) { return ( - <div className='notification'> + <div className='notification notification-follow'> <div className='notification__message'> <div style={{ position: 'absolute', 'left': '-26px'}}> <i className='fa fa-fw fa-user-plus' /> @@ -41,7 +41,7 @@ const Notification = React.createClass({ renderFavourite (notification, link) { return ( - <div className='notification'> + <div className='notification notification-favourite'> <div className='notification__message'> <div style={{ position: 'absolute', 'left': '-26px'}}> <i className='fa fa-fw fa-star' style={{ color: '#ca8f04' }} /> @@ -57,7 +57,7 @@ const Notification = React.createClass({ renderReblog (notification, link) { return ( - <div className='notification'> + <div className='notification notification-reblog'> <div className='notification__message'> <div style={{ position: 'absolute', 'left': '-26px'}}> <i className='fa fa-fw fa-retweet' /> @@ -76,7 +76,7 @@ const Notification = React.createClass({ const account = notification.get('account'); const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username'); const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; - const link = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />; + const link = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />; switch(notification.get('type')) { case 'follow': diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx index caa46ff3c..2da57252e 100644 --- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx +++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx @@ -54,7 +54,7 @@ const DetailedStatus = React.createClass({ return ( <div style={{ padding: '14px 10px' }} className='detailed-status'> <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}> - <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} size={48} /></div> + <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div> <DisplayName account={status.get('account')} /> </a> diff --git a/app/assets/javascripts/components/locales/fr.jsx b/app/assets/javascripts/components/locales/fr.jsx index fdd9c0e00..568422ff3 100644 --- a/app/assets/javascripts/components/locales/fr.jsx +++ b/app/assets/javascripts/components/locales/fr.jsx @@ -14,6 +14,7 @@ const fr = { "status.show_less": "Replier", "status.open": "Déplier ce status", "status.report": "Signaler @{name}", + "status.load_more": "Charger plus", "video_player.toggle_sound": "Mettre/Couper le son", "account.mention": "Mentionner", "account.edit_profile": "Modifier le profil", @@ -41,6 +42,7 @@ const fr = { "column.notifications": "Notifications", "column.blocks": "Utilisateurs bloqués", "column.favourites": "Favoris", + "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateurs⋅trices pour débuter la conversation.", "tabs_bar.compose": "Composer", "tabs_bar.home": "Accueil", "tabs_bar.mentions": "Mentions", diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx index 1e7b8b548..f9e1fe5bd 100644 --- a/app/assets/javascripts/components/locales/index.jsx +++ b/app/assets/javascripts/components/locales/index.jsx @@ -7,6 +7,8 @@ import pt from './pt'; import uk from './uk'; import fi from './fi'; import eo from './eo'; +import ru from './ru'; + const locales = { en, @@ -17,7 +19,9 @@ const locales = { pt, uk, fi, - eo + eo, + ru + }; export default function getMessagesForLocale (locale) { diff --git a/app/assets/javascripts/components/locales/pt.jsx b/app/assets/javascripts/components/locales/pt.jsx index d68724b13..8d1b88c75 100644 --- a/app/assets/javascripts/components/locales/pt.jsx +++ b/app/assets/javascripts/components/locales/pt.jsx @@ -2,54 +2,71 @@ const pt = { "column_back_button.label": "Voltar", "lightbox.close": "Fechar", "loading_indicator.label": "Carregando...", - "status.mention": "Menção", - "status.delete": "Deletar", + "status.mention": "Mencionar @{name}", + "status.delete": "Eliminar", "status.reply": "Responder", - "status.reblog": "Reblogar", - "status.favourite": "Favoritar", - "status.reblogged_by": "{name} reblogou", - "video_player.toggle_sound": "Alterar som", - "account.mention": "Menção", + "status.reblog": "Partilhar", + "status.favourite": "Adicionar aos favoritos", + "status.reblogged_by": "{name} partilhou", + "status.sensitive_warning": "Conteúdo sensível", + "status.sensitive_toggle": "Clique para ver", + "status.show_more": "Mostrar mais", + "status.show_less": "Mostrar menos", + "status.open": "Expandir", + "status.report": "Reportar @{name}", + "video_player.toggle_sound": "Ligar/Desligar som", + "account.mention": "Mencionar @{name}", "account.edit_profile": "Editar perfil", - "account.unblock": "Desbloquear", - "account.unfollow": "Unfollow", - "account.block": "Bloquear", + "account.unblock": "Não bloquear @{name}", + "account.unfollow": "Não seguir", + "account.block": "Bloquear @{name}", "account.follow": "Seguir", - "account.block": "Bloquear", "account.posts": "Posts", "account.follows": "Segue", "account.followers": "Seguidores", - "account.follows_you": "Segue você", + "account.follows_you": "É teu seguidor", + "account.requested": "A aguardar aprovação", "getting_started.heading": "Primeiros passos", - "getting_started.about_addressing": "Podes seguir pessoas se sabes o nome de usuário deles e o domínio em que estão entrando um endereço similar a e-mail no campo no topo da barra lateral.", + "getting_started.about_addressing": "Podes seguir pessoas se sabes o nome de usuário deles e o domínio em que estão colocando um endereço similar a e-mail no campo no topo da barra lateral.", "getting_started.about_shortcuts": "Se o usuário alvo está no mesmo domínio, só o nome funcionará. A mesma regra se aplica a mencionar pessoas nas postagens.", - "getting_started.about_developer": "O desenvolvedor desse projeto pode ser seguido em Gargron@mastodon.social", + "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}. {apps}.", "column.home": "Home", - "column.mentions": "Menções", + "column.community": "Local", "column.public": "Público", - "tabs_bar.compose": "Compôr", + "column.notifications": "Notificações", + "tabs_bar.compose": "Criar", "tabs_bar.home": "Home", "tabs_bar.mentions": "Menções", "tabs_bar.public": "Público", "tabs_bar.notifications": "Notificações", - "compose_form.placeholder": "Que estás pensando?", + "compose_form.placeholder": "Em que estás a pensar?", "compose_form.publish": "Publicar", - "compose_form.sensitive": "Marcar conteúdo como sensível", - "compose_form.unlisted": "Modo não-listado", + "compose_form.sensitive": "Media com conteúdo sensível", + "compose_form.spoiler": "Esconder texto com aviso", + "compose_form.private": "Tornar privado", + "compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.", + "compose_form.unlisted": "Não mostrar na listagem pública", "navigation_bar.edit_profile": "Editar perfil", "navigation_bar.preferences": "Preferências", - "navigation_bar.public_timeline": "Timeline Pública", - "navigation_bar.logout": "Logout", + "navigation_bar.community_timeline": "Local", + "navigation_bar.public_timeline": "Público", + "navigation_bar.logout": "Sair", "reply_indicator.cancel": "Cancelar", - "search.placeholder": "Busca", + "search.placeholder": "Pesquisar", "search.account": "Conta", "search.hashtag": "Hashtag", "upload_button.label": "Adicionar media", - "upload_form.undo": "Desfazer", - "notification.follow": "{name} seguiu você", - "notification.favourite": "{name} favoritou seu post", - "notification.reblog": "{name} reblogou o seu post", - "notification.mention": "{name} mecionou você" + "upload_form.undo": "Anular", + "notification.follow": "{name} seguiu-te", + "notification.favourite": "{name} adicionou o teu post aos favoritos", + "notification.reblog": "{name} partilhou o teu post", + "notification.mention": "{name} mencionou-te", + "notifications.column_settings.alert": "Notificações no computador", + "notifications.column_settings.show": "Mostrar nas colunas", + "notifications.column_settings.follow": "Novos seguidores:", + "notifications.column_settings.favourite": "Favoritos:", + "notifications.column_settings.mention": "Menções:", + "notifications.column_settings.reblog": "Partilhas:", }; export default pt; diff --git a/app/assets/javascripts/components/locales/ru.jsx b/app/assets/javascripts/components/locales/ru.jsx new file mode 100644 index 000000000..e109005a7 --- /dev/null +++ b/app/assets/javascripts/components/locales/ru.jsx @@ -0,0 +1,68 @@ +const ru = { + "column_back_button.label": "Назад", + "lightbox.close": "Закрыть", + "loading_indicator.label": "Загрузка...", + "status.mention": "Упомянуть @{name}", + "status.delete": "Удалить", + "status.reply": "Ответить", + "status.reblog": "Продвинуть", + "status.favourite": "Нравится", + "status.reblogged_by": "{name} продвинул(а)", + "status.sensitive_warning": "Чувствительный контент", + "status.sensitive_toggle": "Нажмите для просмотра", + "video_player.toggle_sound": "Вкл./выкл. звук", + "account.mention": "Упомянуть @{name}", + "account.edit_profile": "Изменить профиль", + "account.unblock": "Разблокировать @{name}", + "account.unfollow": "Отписаться", + "account.block": "Блокировать @{name}", + "account.follow": "Подписаться", + "account.posts": "Посты", + "account.follows": "Подписки", + "account.followers": "Подписчики", + "account.follows_you": "Подписан(а) на Вас", + "account.requested": "Ожидает подтверждения", + "getting_started.heading": "Добро пожаловать", + "getting_started.about_addressing": "Вы можете подписаться на человека, зная имя пользователя и домен, на котором он находится, введя e-mail-подобный адрес в форму поиска.", + "getting_started.about_shortcuts": "Если пользователь находится на одном с Вами домене, можно использовать только имя. То же правило применимо к упоминанию пользователей в статусах.", + "getting_started.open_source_notice": "Mastodon - программа с открытым исходным кодом. Вы можете помочь проекту или сообщить о проблемах на GitHub по адресу {github}. {apps}.", + "column.home": "Главная", + "column.community": "Локальная лента", + "column.public": "Глобальная лента", + "column.notifications": "Уведомления", + "tabs_bar.compose": "Написать", + "tabs_bar.home": "Главная", + "tabs_bar.mentions": "Упоминания", + "tabs_bar.public": "Глобальная лента", + "tabs_bar.notifications": "Уведомления", + "compose_form.placeholder": "О чем Вы думаете?", + "compose_form.publish": "Протрубить", + "compose_form.sensitive": "Отметить как чувствительный контент", + "compose_form.spoiler": "Скрыть текст за предупреждением", + "compose_form.private": "Отметить как приватное", + "compose_form.privacy_disclaimer": "Ваш приватный статус будет доставлен упомянутым пользователям на доменах {domains}. Доверяете ли вы {domainsCount, plural, one {этому серверу} other {этим серверам}}? Приватность постов работает только на узлах Mastodon. Если {domains} {domainsCount, plural, one {не является узлом Mastodon} other {не являются узлами Mastodon}}, приватность поста не будет указана, и он может оказаться продвинут или иным образом показан не обозначенным Вами пользователям.", + "compose_form.unlisted": "Не отображать в публичных лентах", + "navigation_bar.edit_profile": "Изменить профиль", + "navigation_bar.preferences": "Опции", + "navigation_bar.community_timeline": "Локальная лента", + "navigation_bar.public_timeline": "Глобальная лента", + "navigation_bar.logout": "Выйти", + "reply_indicator.cancel": "Отмена", + "search.placeholder": "Поиск", + "search.account": "Аккаунт", + "search.hashtag": "Хэштег", + "upload_button.label": "Добавить медиаконтент", + "upload_form.undo": "Отменить", + "notification.follow": "{name} подписался(-лась) на Вас", + "notification.favourite": "{name} понравился Ваш статус", + "notification.reblog": "{name} продвинул(а) Ваш статус", + "notification.mention": "{name} упомянул(а) Вас", + "notifications.column_settings.alert": "Десктопные уведомления", + "notifications.column_settings.show": "Показывать в колонке", + "notifications.column_settings.follow": "Новые подписчики:", + "notifications.column_settings.favourite": "Нравится:", + "notifications.column_settings.mention": "Упоминания:", + "notifications.column_settings.reblog": "Продвижения:", +}; + +export default ru; diff --git a/app/assets/stylesheets/accounts.scss b/app/assets/stylesheets/accounts.scss index b3ae33500..50181d86e 100644 --- a/app/assets/stylesheets/accounts.scss +++ b/app/assets/stylesheets/accounts.scss @@ -72,6 +72,7 @@ position: relative; z-index: 2; flex-direction: row; + background: rgba(0,0,0,0.5); } .details-counters { @@ -83,7 +84,7 @@ .counter { width: 80px; color: $color3; - padding: 0 10px; + padding: 5px 10px 0px; margin-bottom: 10px; border-right: 1px solid $color3; cursor: default; @@ -173,7 +174,7 @@ text-align: center; overflow: hidden; - a, .current, .next_page, .previous_page, .gap { + a, .current, .page, .gap { font-size: 14px; color: $color5; font-weight: 500; @@ -193,12 +194,12 @@ cursor: default; } - .previous_page, .next_page { + .prev, .next { text-transform: uppercase; color: $color2; } - .previous_page { + .prev { float: left; padding-left: 0; @@ -208,7 +209,7 @@ } } - .next_page { + .next { float: right; padding-right: 0; @@ -226,11 +227,11 @@ @media screen and (max-width: 360px) { padding: 30px 20px; - a, .current, .next_page, .previous_page, .gap { + a, .current, .next, .prev, .gap { display: none; } - .next_page, .previous_page { + .next, .prev { display: inline-block; } } diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index d31f148a2..316398874 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -1,7 +1,7 @@ @import 'variables'; .app-body{ - -ms-overflow-style: -ms-autohiding-scrollbar; + -ms-overflow-style: -ms-autohiding-scrollbar; } .button { @@ -49,6 +49,22 @@ } } +.column-icon-clear { + font-size: 16px; + padding: 15px; + position: absolute; + right: 48px; + top: 0; + cursor: pointer; + z-index: 2; +} + +@media screen and (min-width: 1024px) { + .column-icon-clear { + top: 10px; + } +} + .icon-button { display: inline-block; padding: 0; @@ -149,6 +165,14 @@ } } +.avatar { + border-radius: 4px; + background: transparent no-repeat; + background-position: 50%; + background-clip: padding-box; + position: relative; +} + .lightbox .icon-button { color: $color1; } @@ -714,7 +738,7 @@ a.status__content__spoiler-link { @media screen and (min-width: 360px) { .columns-area { - margin: 10px; + padding: 10px; } } @@ -722,9 +746,12 @@ a.status__content__spoiler-link { width: 330px; position: relative; box-sizing: border-box; - background: $color1; display: flex; flex-direction: column; + + > .scrollable { + background: $color1; + } } .ui { @@ -756,6 +783,58 @@ a.status__content__spoiler-link { border-bottom: 2px solid transparent; } +.column, .drawer { + flex: 1 1 100%; + overflow: hidden; +} + +@media screen and (min-width: 360px) { + .tabs-bar { + margin: 10px; + margin-bottom: 0; + } + + .search { + margin-bottom: 10px; + } +} + +@media screen and (max-width: 1024px) { + .column, .drawer { + width: 100%; + padding: 0; + } + + .columns-area { + flex-direction: column; + } + + .search__input, .autosuggest-textarea__textarea { + font-size: 16px; + } +} + +@media screen and (min-width: 1024px) { + .columns-area { + padding: 0; + } + + .column, .drawer { + flex: 0 0 auto; + padding: 10px; + padding-left: 5px; + padding-right: 5px; + + &:first-child { + padding-left: 10px; + } + + &:last-child { + padding-right: 10px; + } + } +} + @media screen and (min-width: 2560px) { .columns-area { justify-content: center; @@ -815,37 +894,6 @@ a.status__content__spoiler-link { } } -.column, .drawer { - margin-left: 5px; - margin-right: 5px; - flex: 0 0 auto; - overflow: hidden; -} - -.column:first-child, .drawer:first-child { - margin-left: 0; -} - -.column:last-child, .drawer:last-child { - margin-right: 0; -} - -@media screen and (max-width: 1024px) { - .column, .drawer { - width: 100%; - margin: 0; - flex: 1 1 100%; - } - - .columns-area { - flex-direction: column; - } - - .search__input, .autosuggest-textarea__textarea { - font-size: 16px; - } -} - .tabs-bar { display: flex; background: lighten($color1, 8%); @@ -856,17 +904,18 @@ a.status__content__spoiler-link { .tabs-bar__link { display: block; flex: 1 1 auto; - padding: 10px 5px; + padding: 15px 10px; color: $color5; text-decoration: none; text-align: center; - font-size:12px; + font-size: 14px; font-weight: 500; border-bottom: 2px solid lighten($color1, 8%); transition: all 200ms linear; .fa { font-weight: 400; + font-size: 16px; } &.active { @@ -880,27 +929,13 @@ a.status__content__spoiler-link { } span { + margin-left: 5px; display: none; } } -@media screen and (min-width: 360px) { - .tabs-bar { - margin: 10px; - margin-bottom: 0; - } - - .search { - margin-bottom: 10px; - } -} - @media screen and (min-width: 600px) { .tabs-bar__link { - .fa { - margin-right: 5px; - } - span { display: inline; } @@ -1362,12 +1397,15 @@ button.icon-button.active i.fa-retweet { .empty-column-indicator { color: lighten($color1, 20%); + background: $color1; text-align: center; padding: 20px; - padding-top: 100px; font-size: 15px; font-weight: 400; cursor: default; + display: flex; + flex: 1 1 auto; + align-items: center; a { color: $color4; @@ -1395,7 +1433,7 @@ button.icon-button.active i.fa-retweet { .emoji-dialog { width: 280px; height: 220px; - background: $color2; + background: darken($color3, 10%); box-sizing: border-box; border-radius: 2px; overflow: hidden; @@ -1404,6 +1442,8 @@ button.icon-button.active i.fa-retweet { .emojione { margin: 0; + width: 100%; + height: auto; } .emoji-dialog-header { diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 7fd43489f..04e7ddacf 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -2,30 +2,25 @@ class AboutController < ApplicationController before_action :set_body_classes + before_action :set_instance_presenter, only: [:show, :more] - def index - @description = Setting.site_description - @open_registrations = Setting.open_registrations - @closed_registrations_message = Setting.closed_registrations_message + def show; end - @user = User.new - @user.build_account - end - - def more - @description = Setting.site_description - @extended_description = Setting.site_extended_description - @contact_account = Account.find_local(Setting.site_contact_username) - @contact_email = Setting.site_contact_email - @user_count = Rails.cache.fetch('user_count') { User.count } - @status_count = Rails.cache.fetch('local_status_count') { Status.local.count } - @domain_count = Rails.cache.fetch('distinct_domain_count') { Account.distinct.count(:domain) } - end + def more; end def terms; end private + def new_user + User.new.tap(&:build_account) + end + helper_method :new_user + + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end + def set_body_classes @body_classes = 'about-body' end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 619c04be2..d4f157614 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -35,11 +35,11 @@ class AccountsController < ApplicationController end def followers - @followers = @account.followers.order('follows.created_at desc').paginate(page: params[:page], per_page: 12) + @followers = @account.followers.order('follows.created_at desc').page(params[:page]).per(12) end def following - @following = @account.following.order('follows.created_at desc').paginate(page: params[:page], per_page: 12) + @following = @account.following.order('follows.created_at desc').page(params[:page]).per(12) end private @@ -53,7 +53,7 @@ class AccountsController < ApplicationController end def webfinger_account_url - webfinger_url(resource: "acct:#{@account.acct}@#{Rails.configuration.x.local_domain}") + webfinger_url(resource: @account.to_webfinger_s) end def check_account_suspension diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index df2c7bebf..71cb8edd8 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -1,51 +1,50 @@ # frozen_string_literal: true -class Admin::AccountsController < ApplicationController - before_action :require_admin! - before_action :set_account, except: :index - - layout 'admin' - - def index - @accounts = Account.alphabetic.paginate(page: params[:page], per_page: 40) - - @accounts = @accounts.local if params[:local].present? - @accounts = @accounts.remote if params[:remote].present? - @accounts = @accounts.where(domain: params[:by_domain]) if params[:by_domain].present? - @accounts = @accounts.silenced if params[:silenced].present? - @accounts = @accounts.recent if params[:recent].present? - @accounts = @accounts.suspended if params[:suspended].present? - end - - def show; end - - def suspend - Admin::SuspensionWorker.perform_async(@account.id) - redirect_to admin_accounts_path - end - - def unsuspend - @account.update(suspended: false) - redirect_to admin_accounts_path - end - - def silence - @account.update(silenced: true) - redirect_to admin_accounts_path - end - - def unsilence - @account.update(silenced: false) - redirect_to admin_accounts_path - end - - private - - def set_account - @account = Account.find(params[:id]) - end - - def account_params - params.require(:account).permit(:silenced, :suspended) +module Admin + class AccountsController < BaseController + before_action :set_account, except: :index + + def index + @accounts = Account.alphabetic.page(params[:page]) + + @accounts = @accounts.local if params[:local].present? + @accounts = @accounts.remote if params[:remote].present? + @accounts = @accounts.where(domain: params[:by_domain]) if params[:by_domain].present? + @accounts = @accounts.silenced if params[:silenced].present? + @accounts = @accounts.recent if params[:recent].present? + @accounts = @accounts.suspended if params[:suspended].present? + end + + def show; end + + def suspend + Admin::SuspensionWorker.perform_async(@account.id) + redirect_to admin_accounts_path + end + + def unsuspend + @account.update(suspended: false) + redirect_to admin_accounts_path + end + + def silence + @account.update(silenced: true) + redirect_to admin_accounts_path + end + + def unsilence + @account.update(silenced: false) + redirect_to admin_accounts_path + end + + private + + def set_account + @account = Account.find(params[:id]) + end + + def account_params + params.require(:account).permit(:silenced, :suspended) + end end end diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb new file mode 100644 index 000000000..11fe326bc --- /dev/null +++ b/app/controllers/admin/base_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Admin + class BaseController < ApplicationController + before_action :require_admin! + + layout 'admin' + end +end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 1f4432847..a8b56c085 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -1,32 +1,30 @@ # frozen_string_literal: true -class Admin::DomainBlocksController < ApplicationController - before_action :require_admin! - - layout 'admin' - - def index - @blocks = DomainBlock.paginate(page: params[:page], per_page: 40) - end +module Admin + class DomainBlocksController < BaseController + def index + @blocks = DomainBlock.page(params[:page]) + end - def new - @domain_block = DomainBlock.new - end + def new + @domain_block = DomainBlock.new + end - def create - @domain_block = DomainBlock.new(resource_params) + def create + @domain_block = DomainBlock.new(resource_params) - if @domain_block.save - DomainBlockWorker.perform_async(@domain_block.id) - redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed' - else - render action: :new + if @domain_block.save + DomainBlockWorker.perform_async(@domain_block.id) + redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed' + else + render action: :new + end end - end - private + private - def resource_params - params.require(:domain_block).permit(:domain, :severity) + def resource_params + params.require(:domain_block).permit(:domain, :severity) + end end end diff --git a/app/controllers/admin/pubsubhubbub_controller.rb b/app/controllers/admin/pubsubhubbub_controller.rb index b9e840ffe..31c80a174 100644 --- a/app/controllers/admin/pubsubhubbub_controller.rb +++ b/app/controllers/admin/pubsubhubbub_controller.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -class Admin::PubsubhubbubController < ApplicationController - before_action :require_admin! - - layout 'admin' - - def index - @subscriptions = Subscription.order('id desc').includes(:account).paginate(page: params[:page], per_page: 40) +module Admin + class PubsubhubbubController < BaseController + def index + @subscriptions = Subscription.order('id desc').includes(:account).page(params[:page]) + end end end diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 2b3b1809f..3c3082318 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -1,45 +1,44 @@ # frozen_string_literal: true -class Admin::ReportsController < ApplicationController - before_action :require_admin! - before_action :set_report, except: [:index] - - layout 'admin' - - def index - @reports = Report.includes(:account, :target_account).order('id desc').paginate(page: params[:page], per_page: 40) - @reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved - end - - def show - @statuses = Status.where(id: @report.status_ids) - end - - def resolve - @report.update(action_taken: true, action_taken_by_account_id: current_account.id) - redirect_to admin_report_path(@report) - end - - def suspend - Admin::SuspensionWorker.perform_async(@report.target_account.id) - Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id) - redirect_to admin_report_path(@report) - end - - def silence - @report.target_account.update(silenced: true) - Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id) - redirect_to admin_report_path(@report) - end - - def remove - RemovalWorker.perform_async(params[:status_id]) - redirect_to admin_report_path(@report) - end - - private - - def set_report - @report = Report.find(params[:id]) +module Admin + class ReportsController < BaseController + before_action :set_report, except: [:index] + + def index + @reports = Report.includes(:account, :target_account).order('id desc').page(params[:page]) + @reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved + end + + def show + @statuses = Status.where(id: @report.status_ids) + end + + def resolve + @report.update(action_taken: true, action_taken_by_account_id: current_account.id) + redirect_to admin_report_path(@report) + end + + def suspend + Admin::SuspensionWorker.perform_async(@report.target_account.id) + Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id) + redirect_to admin_report_path(@report) + end + + def silence + @report.target_account.update(silenced: true) + Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id) + redirect_to admin_report_path(@report) + end + + def remove + RemovalWorker.perform_async(params[:status_id]) + redirect_to admin_report_path(@report) + end + + private + + def set_report + @report = Report.find(params[:id]) + end end end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 7615c781d..6cca5c3e3 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -1,35 +1,33 @@ # frozen_string_literal: true -class Admin::SettingsController < ApplicationController - before_action :require_admin! - - layout 'admin' +module Admin + class SettingsController < BaseController + def index + @settings = Setting.all_as_records + end - def index - @settings = Setting.all_as_records - end + def update + @setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id]) + value = settings_params[:value] - def update - @setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id]) - value = settings_params[:value] + # Special cases + value = value == 'true' if @setting.var == 'open_registrations' - # Special cases - value = value == 'true' if @setting.var == 'open_registrations' + if @setting.value != value + @setting.value = value + @setting.save + end - if @setting.value != value - @setting.value = value - @setting.save + respond_to do |format| + format.html { redirect_to admin_settings_path } + format.json { respond_with_bip(@setting) } + end end - respond_to do |format| - format.html { redirect_to admin_settings_path } - format.json { respond_with_bip(@setting) } - end - end - - private + private - def settings_params - params.require(:setting).permit(:value) + def settings_params + params.require(:setting).permit(:value) + end end end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 454873116..2c44e36a7 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true class Api::V1::AccountsController < ApiController - before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute] + before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute, :update_credentials] before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock, :mute, :unmute] + before_action -> { doorkeeper_authorize! :write }, only: [:update_credentials] before_action :require_user!, except: [:show, :following, :followers, :statuses] - before_action :set_account, except: [:verify_credentials, :suggestions, :search] + before_action :set_account, except: [:verify_credentials, :update_credentials, :suggestions, :search] respond_to :json @@ -15,6 +16,12 @@ class Api::V1::AccountsController < ApiController render action: :show end + def update_credentials + current_account.update!(account_params) + @account = current_account + render action: :show + end + def following results = Follow.where(account: @account).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) accounts = Account.where(id: results.map(&:target_account_id)).map { |a| [a.id, a] }.to_h @@ -135,4 +142,8 @@ class Api::V1::AccountsController < ApiController def statuses_pagination_params(core_params) params.permit(:limit, :only_media, :exclude_replies).merge(core_params) end + + def account_params + params.permit(:display_name, :note, :avatar, :header) + end end diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 71c054334..3cff29982 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -9,7 +9,7 @@ class Api::V1::NotificationsController < ApiController DEFAULT_NOTIFICATIONS_LIMIT = 15 def index - @notifications = Notification.where(account: current_account).browserable.paginate_by_max_id(limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params[:max_id], params[:since_id]) + @notifications = Notification.where(account: current_account).browserable(exclude_types).paginate_by_max_id(limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params[:max_id], params[:since_id]) @notifications = cache_collection(@notifications, Notification) statuses = @notifications.select { |n| !n.target_status.nil? }.map(&:target_status) @@ -32,7 +32,13 @@ class Api::V1::NotificationsController < ApiController private + def exclude_types + val = params.permit(exclude_types: [])[:exclude_types] || [] + val = [val] unless val.is_a?(Enumerable) + val + end + def pagination_params(core_params) - params.permit(:limit).merge(core_params) + params.permit(:limit, exclude_types: []).merge(core_params) end end diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index db16f82e5..57604f1dc 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -7,6 +7,7 @@ class ApiController < ApplicationController protect_from_forgery with: :null_session skip_before_action :verify_authenticity_token + skip_before_action :store_current_location before_action :set_rate_limit_headers diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb index 6528ce45e..bcf3fd0a0 100644 --- a/app/controllers/concerns/localized.rb +++ b/app/controllers/concerns/localized.rb @@ -26,6 +26,8 @@ module Localized end def default_locale - ENV.fetch('DEFAULT_LOCALE') { I18n.default_locale } + ENV.fetch('DEFAULT_LOCALE') { + http_accept_language.compatible_language_from(I18n.available_locales) || I18n.default_locale + } end end diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb index 1e3f786ec..22e376836 100644 --- a/app/controllers/remote_follow_controller.rb +++ b/app/controllers/remote_follow_controller.rb @@ -25,7 +25,7 @@ class RemoteFollowController < ApplicationController session[:remote_follow] = @remote_follow.acct - redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: "#{@account.username}@#{Rails.configuration.x.local_domain}").to_s + redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: @account.to_webfinger_s).to_s else render :new end diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index 4fcec5322..ff688978c 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -39,7 +39,7 @@ class Settings::ExportsController < ApplicationController def accounts_list_to_csv(list) CSV.generate do |csv| list.each do |account| - csv << [(account.local? ? "#{account.username}@#{Rails.configuration.x.local_domain}" : account.acct)] + csv << [(account.local? ? account.local_username_and_domain : account.acct)] end end end diff --git a/app/controllers/xrd_controller.rb b/app/controllers/xrd_controller.rb index 6db87cefc..5964172e9 100644 --- a/app/controllers/xrd_controller.rb +++ b/app/controllers/xrd_controller.rb @@ -14,7 +14,7 @@ class XrdController < ApplicationController def webfinger @account = Account.find_local!(username_from_resource) - @canonical_account_uri = "acct:#{@account.username}@#{Rails.configuration.x.local_domain}" + @canonical_account_uri = @account.to_webfinger_s @magic_key = pem_to_magic_key(@account.keypair.public_key) respond_to do |format| diff --git a/app/helpers/about_helper.rb b/app/helpers/about_helper.rb deleted file mode 100644 index 0f57a7b5e..000000000 --- a/app/helpers/about_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -module AboutHelper -end diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb deleted file mode 100644 index af23a78d1..000000000 --- a/app/helpers/accounts_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module AccountsHelper - def pagination_options - { - previous_label: safe_join([fa_icon('chevron-left'), t('pagination.prev')], ' '), - next_label: safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), - inner_window: 1, - outer_window: 0, - } - end -end diff --git a/app/helpers/admin/domain_blocks_helper.rb b/app/helpers/admin/domain_blocks_helper.rb deleted file mode 100644 index d66c8d5e1..000000000 --- a/app/helpers/admin/domain_blocks_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -module Admin::DomainBlocksHelper -end diff --git a/app/helpers/admin/pubsubhubbub_helper.rb b/app/helpers/admin/pubsubhubbub_helper.rb deleted file mode 100644 index c2fc2e7da..000000000 --- a/app/helpers/admin/pubsubhubbub_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -module Admin::PubsubhubbubHelper -end diff --git a/app/helpers/atom_builder_helper.rb b/app/helpers/atom_builder_helper.rb index b750eeb07..185388ec9 100644 --- a/app/helpers/atom_builder_helper.rb +++ b/app/helpers/atom_builder_helper.rb @@ -160,7 +160,7 @@ module AtomBuilderHelper object_type xml, :person uri xml, TagManager.instance.uri_for(account) name xml, account.username - email xml, account.local? ? "#{account.acct}@#{Rails.configuration.x.local_domain}" : account.acct + email xml, account.local? ? account.local_username_and_domain : account.acct summary xml, account.note link_alternate xml, TagManager.instance.url_for(account) link_avatar xml, account diff --git a/app/helpers/authorize_follow_helper.rb b/app/helpers/authorize_follow_helper.rb deleted file mode 100644 index 99ee03c2f..000000000 --- a/app/helpers/authorize_follow_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -module AuthorizeFollowHelper -end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 74dc0e11d..327ca4e98 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -5,13 +5,15 @@ module SettingsHelper en: 'English', de: 'Deutsch', es: 'Español', + eo: 'Esperanto', pt: 'Português', fr: 'Français', hu: 'Magyar', uk: 'Українська', 'zh-CN': '简体中文', fi: 'Suomi', - eo: 'Esperanto', + ru: 'Русский', + }.freeze def human_locale(locale) diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb deleted file mode 100644 index 5b2b3ca59..000000000 --- a/app/helpers/tags_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -module TagsHelper -end diff --git a/app/helpers/xrd_helper.rb b/app/helpers/xrd_helper.rb deleted file mode 100644 index 2281a0278..000000000 --- a/app/helpers/xrd_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -module XrdHelper -end diff --git a/app/lib/atom_serializer.rb b/app/lib/atom_serializer.rb index b9dcee6b3..68d2fce68 100644 --- a/app/lib/atom_serializer.rb +++ b/app/lib/atom_serializer.rb @@ -20,7 +20,7 @@ class AtomSerializer append_element(author, 'activity:object-type', TagManager::TYPES[:person]) append_element(author, 'uri', uri) append_element(author, 'name', account.username) - append_element(author, 'email', account.local? ? "#{account.acct}@#{Rails.configuration.x.local_domain}" : account.acct) + append_element(author, 'email', account.local? ? account.local_username_and_domain : account.acct) append_element(author, 'summary', account.note) append_element(author, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(account)) append_element(author, 'link', nil, rel: :avatar, type: account.avatar_content_type, 'media:width': 120, 'media:height': 120, href: full_asset_url(account.avatar.url(:original))) @@ -67,7 +67,7 @@ class AtomSerializer append_element(entry, 'id', TagManager.instance.unique_tag(stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type)) append_element(entry, 'published', stream_entry.created_at.iso8601) append_element(entry, 'updated', stream_entry.updated_at.iso8601) - append_element(entry, 'title', stream_entry&.status&.title) + append_element(entry, 'title', stream_entry&.status&.title || 'Delete') entry << author(stream_entry.account) if root diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 58d9fb1fc..339a5c78b 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -66,7 +66,7 @@ class FeedManager timeline_key = key(:home, into_account.id) oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0 - from_account.statuses.select('id').where('id > ?', oldest_home_score).find_in_batches do |statuses| + from_account.statuses.select('id').where('id > ?', oldest_home_score).reorder(nil).find_in_batches do |statuses| redis.pipelined do statuses.each do |status| redis.zrem(timeline_key, status.id) diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index da7ad2027..c3f331ff7 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -15,7 +15,6 @@ class Formatter html = status.text html = encode(html) html = simple_format(html, {}, sanitize: false) - html = html.gsub(/\n/, '') html = link_urls(html) html = link_mentions(html, status.mentions) html = link_hashtags(html) diff --git a/app/models/account.rb b/app/models/account.rb index cbba8b5b6..8ceda7f97 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -12,12 +12,12 @@ class Account < ApplicationRecord validates :username, presence: true, uniqueness: { scope: :domain, case_sensitive: true }, unless: 'local?' # Avatar upload - has_attached_file :avatar, styles: { original: '120x120#' }, convert_options: { all: '-quality 80 -strip' } + has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '-quality 80 -strip' } validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES validates_attachment_size :avatar, less_than: 2.megabytes # Header upload - has_attached_file :header, styles: { original: '700x335#' }, convert_options: { all: '-quality 80 -strip' } + has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '-quality 80 -strip' } validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES validates_attachment_size :header, less_than: 2.megabytes @@ -120,6 +120,14 @@ class Account < ApplicationRecord local? ? username : "#{username}@#{domain}" end + def local_username_and_domain + "#{username}@#{Rails.configuration.x.local_domain}" + end + + def to_webfinger_s + "acct:#{local_username_and_domain}" + end + def subscribed? !subscription_expires_at.blank? end @@ -150,6 +158,22 @@ class Account < ApplicationRecord save! end + def avatar_original_url + avatar.url(:original) + end + + def avatar_static_url + avatar_content_type == 'image/gif' ? avatar.url(:static) : avatar_original_url + end + + def header_original_url + header.url(:original) + end + + def header_static_url + header_content_type == 'image/gif' ? header.url(:static) : header_original_url + end + def avatar_remote_url=(url) parsed_url = URI.parse(url) @@ -203,7 +227,7 @@ class Account < ApplicationRecord end def triadic_closures(account, limit = 5) - sql = <<SQL + sql = <<-SQL.squish WITH first_degree AS ( SELECT target_account_id FROM follows @@ -216,7 +240,7 @@ class Account < ApplicationRecord GROUP BY target_account_id, accounts.id ORDER BY count(account_id) DESC LIMIT ? -SQL + SQL Account.find_by_sql([sql, account.id, account.id, limit]) end @@ -226,7 +250,7 @@ SQL textsearch = '(setweight(to_tsvector(\'simple\', accounts.display_name), \'A\') || setweight(to_tsvector(\'simple\', accounts.username), \'B\') || setweight(to_tsvector(\'simple\', coalesce(accounts.domain, \'\')), \'C\'))' query = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')' - sql = <<SQL + sql = <<-SQL.squish SELECT accounts.*, ts_rank_cd(#{textsearch}, #{query}, 32) AS rank @@ -234,7 +258,7 @@ SQL WHERE #{query} @@ #{textsearch} ORDER BY rank DESC LIMIT ? -SQL + SQL Account.find_by_sql([sql, limit]) end @@ -244,7 +268,7 @@ SQL textsearch = '(setweight(to_tsvector(\'simple\', accounts.display_name), \'A\') || setweight(to_tsvector(\'simple\', accounts.username), \'B\') || setweight(to_tsvector(\'simple\', coalesce(accounts.domain, \'\')), \'C\'))' query = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')' - sql = <<SQL + sql = <<-SQL.squish SELECT accounts.*, (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank @@ -254,7 +278,7 @@ SQL GROUP BY accounts.id ORDER BY rank DESC LIMIT ? -SQL + SQL Account.find_by_sql([sql, account.id, account.id, limit]) end @@ -284,6 +308,18 @@ SQL def follow_mapping(query, field) query.pluck(field).inject({}) { |mapping, id| mapping[id] = true; mapping } end + + def avatar_styles(file) + styles = { original: '120x120#' } + styles[:static] = { format: 'png' } if file.content_type == 'image/gif' + styles + end + + def header_styles(file) + styles = { original: '700x335#' } + styles[:static] = { format: 'png' } if file.content_type == 'image/gif' + styles + end end before_create do diff --git a/app/models/notification.rb b/app/models/notification.rb index b7b474869..302d4382d 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -16,10 +16,17 @@ class Notification < ApplicationRecord validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] } + TYPE_CLASS_MAP = { + mention: 'Mention', + reblog: 'Status', + follow: 'Follow', + follow_request: 'FollowRequest', + favourite: 'Favourite', + }.freeze + STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :media_attachments, :tags, mentions: :account]].freeze scope :cache_ids, -> { select(:id, :updated_at, :activity_type, :activity_id) } - scope :browserable, -> { where.not(activity_type: ['FollowRequest']) } cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account @@ -28,12 +35,7 @@ class Notification < ApplicationRecord end def type - case activity_type - when 'Status' - :reblog - else - activity_type.underscore.to_sym - end + @type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym end def target_status @@ -50,6 +52,11 @@ class Notification < ApplicationRecord end class << self + def browserable(types = []) + types.concat([:follow_request]) + where.not(activity_type: activity_types_from_types(types)) + end + def reload_stale_associations!(cached_items) account_ids = cached_items.map(&:from_account_id).uniq accounts = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h @@ -58,6 +65,12 @@ class Notification < ApplicationRecord item.from_account = accounts[item.from_account_id] end end + + private + + def activity_types_from_types(types) + types.map { |type| TYPE_CLASS_MAP[type.to_sym] }.compact + end end after_initialize :set_from_account diff --git a/app/models/status.rb b/app/models/status.rb index 7e3dd3e28..16cd4383f 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -75,7 +75,7 @@ class Status < ApplicationRecord end def title - content + reblog? ? "#{account.acct} shared a status by #{reblog.account.acct}" : "New status by #{account.acct}" end def hidden? diff --git a/app/models/tag.rb b/app/models/tag.rb index 15625ca43..6209d7dab 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -17,7 +17,7 @@ class Tag < ApplicationRecord textsearch = 'to_tsvector(\'simple\', tags.name)' query = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')' - sql = <<SQL + sql = <<-SQL.squish SELECT tags.*, ts_rank_cd(#{textsearch}, #{query}) AS rank @@ -25,7 +25,7 @@ class Tag < ApplicationRecord WHERE #{query} @@ #{textsearch} ORDER BY rank DESC LIMIT ? -SQL + SQL Tag.find_by_sql([sql, limit]) end diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb new file mode 100644 index 000000000..cd809566f --- /dev/null +++ b/app/presenters/instance_presenter.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class InstancePresenter + delegate( + :closed_registrations_message, + :contact_email, + :open_registrations, + :site_description, + :site_extended_description, + to: Setting + ) + + def contact_account + Account.find_local(Setting.site_contact_username) + end + + def user_count + Rails.cache.fetch('user_count') { User.count } + end + + def status_count + Rails.cache.fetch('local_status_count') { Status.local.count } + end + + def domain_count + Rails.cache.fetch('distinct_domain_count') { Account.distinct.count(:domain) } + end +end diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb index 6a6a696d6..50ffc47c6 100644 --- a/app/services/fetch_remote_account_service.rb +++ b/app/services/fetch_remote_account_service.rb @@ -19,11 +19,16 @@ class FetchRemoteAccountService < BaseService xml = Nokogiri::XML(body) xml.encoding = 'utf-8' - url_parts = Addressable::URI.parse(url) - username = xml.at_xpath('//xmlns:author/xmlns:name').try(:content) - domain = url_parts.host + email = xml.at_xpath('//xmlns:author/xmlns:email').try(:content) + if email.nil? + url_parts = Addressable::URI.parse(url) + username = xml.at_xpath('//xmlns:author/xmlns:name').try(:content) + domain = url_parts.host + else + username, domain = email.split('@') + end - return nil if username.nil? + return nil if username.nil? || domain.nil? Rails.logger.debug "Going to webfinger #{username}@#{domain}" diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml new file mode 100644 index 000000000..c7a9a488b --- /dev/null +++ b/app/views/about/_registration.html.haml @@ -0,0 +1,30 @@ += simple_form_for(new_user, url: user_registration_path) do |f| + = f.simple_fields_for :account do |account_fields| + = account_fields.input :username, + autofocus: true, + placeholder: t('simple_form.labels.defaults.username'), + required: true, + input_html: { 'aria-label' => t('simple_form.labels.defaults.username') } + + = f.input :email, + placeholder: t('simple_form.labels.defaults.email'), + required: true, + input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } + = f.input :password, + autocomplete: "off", + placeholder: t('simple_form.labels.defaults.password'), + required: true, + input_html: { 'aria-label' => t('simple_form.labels.defaults.password') } + = f.input :password_confirmation, + autocomplete: "off", + placeholder: t('simple_form.labels.defaults.confirm_password'), + required: true, + input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') } + + .actions + = f.button :button, t('about.get_started'), type: :submit + + .info + = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' + · + = link_to t('about.about_this'), about_more_path diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml index 2de3bf986..8c12f57c1 100644 --- a/app/views/about/more.html.haml +++ b/app/views/about/more.html.haml @@ -7,42 +7,42 @@ .panel %h2= Rails.configuration.x.local_domain - - unless @description.blank? - %p= @description.html_safe + - unless @instance_presenter.site_description.blank? + %p= @instance_presenter.site_description.html_safe .information-board .section %span= t 'about.user_count_before' - %strong= number_with_delimiter @user_count + %strong= number_with_delimiter @instance_presenter.user_count %span= t 'about.user_count_after' .section %span= t 'about.status_count_before' - %strong= number_with_delimiter @status_count + %strong= number_with_delimiter @instance_presenter.status_count %span= t 'about.status_count_after' .section %span= t 'about.domain_count_before' - %strong= number_with_delimiter @domain_count + %strong= number_with_delimiter @instance_presenter.domain_count %span= t 'about.domain_count_after' - - unless @extended_description.blank? - .panel= @extended_description.html_safe + - unless @instance_presenter.site_extended_description.blank? + .panel= @instance_presenter.site_extended_description.html_safe .sidebar .panel .panel-header= t 'about.contact' .panel-body - - if @contact_account + - if @instance_presenter.contact_account .owner - .avatar= image_tag @contact_account.avatar.url + .avatar= image_tag @instance_presenter.contact_account.avatar.url .name - = link_to TagManager.instance.url_for(@contact_account) do - %span.display_name.emojify= display_name(@contact_account) - %span.username= "@#{@contact_account.acct}" + = link_to TagManager.instance.url_for(@instance_presenter.contact_account) do + %span.display_name.emojify= display_name(@instance_presenter.contact_account) + %span.username= "@#{@instance_presenter.contact_account.acct}" - - unless @contact_email.blank? + - unless @instance_presenter.contact_email.blank? .contact-email = t 'about.business_email' - %strong= @contact_email + %strong= @instance_presenter.contact_email .panel .panel-header= t 'about.links' .panel-list diff --git a/app/views/about/index.html.haml b/app/views/about/show.html.haml index f6b0c1668..8a0d00daa 100644 --- a/app/views/about/index.html.haml +++ b/app/views/about/show.html.haml @@ -8,7 +8,7 @@ %meta{ property: 'og:site_name', content: site_title }/ %meta{ property: 'og:type', content: 'website' }/ %meta{ property: 'og:title', content: Rails.configuration.x.local_domain }/ - %meta{ property: 'og:description', content: @description.blank? ? "Mastodon is a free, open-source social network server. A decentralized alternative to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly" : strip_tags(@description) }/ + %meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.blank? ? t('about.about_mastodon') : @instance_presenter.site_description) }/ %meta{ property: 'og:image', content: asset_url('mastodon_small.jpg') }/ %meta{ property: 'og:image:width', content: '400' }/ %meta{ property: 'og:image:height', content: '400' }/ @@ -24,28 +24,14 @@ .screenshot-with-signup .mascot= image_tag 'fluffy-elephant-friend.png' - - if @open_registrations - = simple_form_for(@user, url: user_registration_path) do |f| - = f.simple_fields_for :account do |ff| - = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') } - - = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } - = f.input :password, autocomplete: "off", placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') } - = f.input :password_confirmation, autocomplete: "off", placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') } - - .actions - = f.button :button, t('about.get_started'), type: :submit - - .info - = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' - · - = link_to t('about.about_this'), about_more_path + - if @instance_presenter.open_registrations + = render 'registration' - else .closed-registrations-message - - if @closed_registrations_message.blank? + - if @instance_presenter.closed_registrations_message.blank? %p= t('about.closed_registrations') - else - = @closed_registrations_message.html_safe + = @instance_presenter.closed_registrations_message.html_safe .info = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' · @@ -85,9 +71,9 @@ = fa_icon('li check-square') = t 'about.features.api' - - unless @description.blank? + - unless @instance_presenter.site_description.blank? %h3= t('about.description_headline', domain: Rails.configuration.x.local_domain) - %p= @description.html_safe + %p= @instance_presenter.site_description.html_safe .actions .info diff --git a/app/views/accounts/followers.html.haml b/app/views/accounts/followers.html.haml index 493491020..fa5071f38 100644 --- a/app/views/accounts/followers.html.haml +++ b/app/views/accounts/followers.html.haml @@ -9,4 +9,4 @@ - else = render partial: 'grid_card', collection: @followers, as: :account, cached: true -= will_paginate @followers, pagination_options += paginate @followers diff --git a/app/views/accounts/following.html.haml b/app/views/accounts/following.html.haml index 370cd6c48..987dcba1f 100644 --- a/app/views/accounts/following.html.haml +++ b/app/views/accounts/following.html.haml @@ -9,4 +9,4 @@ - else = render partial: 'grid_card', collection: @following, as: :account, cached: true -= will_paginate @following, pagination_options += paginate @following diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index e90897729..3b0d69dcd 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -31,4 +31,4 @@ .pagination - if @statuses.size == 20 - = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), short_account_url(@account, max_id: @statuses.last.id), class: 'next_page', rel: 'next' + = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), short_account_url(@account, max_id: @statuses.last.id), class: 'next', rel: 'next' diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index f8ed4ef97..4d636601e 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -46,4 +46,4 @@ = table_link_to 'globe', 'Public', TagManager.instance.url_for(account) = table_link_to 'pencil', 'Edit', admin_account_path(account.id) -= will_paginate @accounts, pagination_options += paginate @accounts diff --git a/app/views/admin/domain_blocks/index.html.haml b/app/views/admin/domain_blocks/index.html.haml index eb7894b86..fe6ff683f 100644 --- a/app/views/admin/domain_blocks/index.html.haml +++ b/app/views/admin/domain_blocks/index.html.haml @@ -13,5 +13,5 @@ %samp= block.domain %td= block.severity -= will_paginate @blocks, pagination_options += paginate @blocks = link_to 'Add new', new_admin_domain_block_path, class: 'button' diff --git a/app/views/admin/pubsubhubbub/index.html.haml b/app/views/admin/pubsubhubbub/index.html.haml index cb11a502c..2b8e36e6a 100644 --- a/app/views/admin/pubsubhubbub/index.html.haml +++ b/app/views/admin/pubsubhubbub/index.html.haml @@ -26,4 +26,4 @@ - else = l subscription.last_successful_delivery_at -= will_paginate @subscriptions, pagination_options += paginate @subscriptions diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml index 839259dc2..9c5c78935 100644 --- a/app/views/admin/reports/index.html.haml +++ b/app/views/admin/reports/index.html.haml @@ -29,4 +29,4 @@ %td= truncate(report.comment, length: 30, separator: ' ') %td= table_link_to 'circle', 'View', admin_report_path(report) -= will_paginate @reports, pagination_options += paginate @reports diff --git a/app/views/api/v1/accounts/show.rabl b/app/views/api/v1/accounts/show.rabl index 32df0457a..8826aa22d 100644 --- a/app/views/api/v1/accounts/show.rabl +++ b/app/views/api/v1/accounts/show.rabl @@ -4,8 +4,9 @@ attributes :id, :username, :acct, :display_name, :locked, :created_at node(:note) { |account| Formatter.instance.simplified_format(account) } node(:url) { |account| TagManager.instance.url_for(account) } -node(:avatar) { |account| full_asset_url(account.avatar.url(:original)) } -node(:header) { |account| full_asset_url(account.header.url(:original)) } -node(:followers_count) { |account| defined?(@followers_counts_map) ? (@followers_counts_map[account.id] || 0) : account.followers_count } -node(:following_count) { |account| defined?(@following_counts_map) ? (@following_counts_map[account.id] || 0) : account.following_count } -node(:statuses_count) { |account| defined?(@statuses_counts_map) ? (@statuses_counts_map[account.id] || 0) : account.statuses_count } +node(:avatar) { |account| full_asset_url(account.avatar_original_url) } +node(:avatar_static) { |account| full_asset_url(account.avatar_static_url) } +node(:header) { |account| full_asset_url(account.header_original_url) } +node(:header_static) { |account| full_asset_url(account.header_static_url) } + +attributes :followers_count, :following_count, :statuses_count diff --git a/app/views/kaminari/_next_page.html.haml b/app/views/kaminari/_next_page.html.haml new file mode 100644 index 000000000..30a3643d6 --- /dev/null +++ b/app/views/kaminari/_next_page.html.haml @@ -0,0 +1,9 @@ +-# Link to the "Next" page +-# available local variables +-# url: url to the next page +-# current_page: a page object for the currently displayed page +-# total_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span.next + = link_to_unless current_page.last?, safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), url, rel: 'next', remote: remote diff --git a/app/views/kaminari/_paginator.html.haml b/app/views/kaminari/_paginator.html.haml new file mode 100644 index 000000000..b1da236d5 --- /dev/null +++ b/app/views/kaminari/_paginator.html.haml @@ -0,0 +1,16 @@ +-# The container tag +-# available local variables +-# current_page: a page object for the currently displayed page +-# total_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +-# paginator: the paginator that renders the pagination tags inside += paginator.render do + %nav.pagination + = prev_page_tag unless current_page.first? + - each_page do |page| + - if page.display_tag? + = page_tag page + - elsif !page.was_truncated? + = gap_tag + = next_page_tag unless current_page.last? diff --git a/app/views/kaminari/_prev_page.html.haml b/app/views/kaminari/_prev_page.html.haml new file mode 100644 index 000000000..1089e3566 --- /dev/null +++ b/app/views/kaminari/_prev_page.html.haml @@ -0,0 +1,9 @@ +-# Link to the "Previous" page +-# available local variables +-# url: url to the previous page +-# current_page: a page object for the currently displayed page +-# total_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%span.prev + = link_to_unless current_page.first?, safe_join([fa_icon('chevron-left'), t('pagination.prev')], ' '), url, rel: 'prev', remote: remote diff --git a/app/views/shared/_landing_strip.html.haml b/app/views/shared/_landing_strip.html.haml index bb081e544..3536c5ca8 100644 --- a/app/views/shared/_landing_strip.html.haml +++ b/app/views/shared/_landing_strip.html.haml @@ -1,2 +1,5 @@ .landing-strip - = t('landing_strip_html', name: display_name(account), domain: Rails.configuration.x.local_domain, sign_up_path: new_user_registration_path) + = t('landing_strip_html', + name: content_tag(:span, display_name(account), class: :emojify), + domain: Rails.configuration.x.local_domain, + sign_up_path: new_user_registration_path) diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml index 434c5c8da..1333d4d82 100644 --- a/app/views/stream_entries/_status.html.haml +++ b/app/views/stream_entries/_status.html.haml @@ -13,7 +13,7 @@ = fa_icon('retweet fw') %span = link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do - %strong= display_name(status.account) + %strong.emojify= display_name(status.account) = t('stream_entries.reblogged') = render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: status.proper } diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml index 32a50e158..c894cdb2e 100644 --- a/app/views/tags/show.html.haml +++ b/app/views/tags/show.html.haml @@ -15,4 +15,4 @@ - if @statuses.size == 20 .pagination - = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), tag_url(@tag, max_id: @statuses.last.id), class: 'next_page', rel: 'next' + = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), tag_url(@tag, max_id: @statuses.last.id), class: 'next', rel: 'next' diff --git a/app/views/user_mailer/confirmation_instructions.fr.html.erb b/app/views/user_mailer/confirmation_instructions.fr.html.erb index 2665f1a20..6c45f1a21 100644 --- a/app/views/user_mailer/confirmation_instructions.fr.html.erb +++ b/app/views/user_mailer/confirmation_instructions.fr.html.erb @@ -1,5 +1,5 @@ <p>Bienvenue <%= @resource.email %> !</p> -<p>Vous pouvez confirmer l'email de votre compte Mastodon en cliquant sur le lien ci-dessous :</p> +<p>Vous pouvez confirmer le courriel de votre compte Mastodon en cliquant sur le lien ci-dessous :</p> <p><%= link_to 'Confirmer mon compte', confirmation_url(@resource, confirmation_token: @token) %></p> diff --git a/app/views/user_mailer/confirmation_instructions.fr.text.erb b/app/views/user_mailer/confirmation_instructions.fr.text.erb index 9d33450f8..dfa3f9f7c 100644 --- a/app/views/user_mailer/confirmation_instructions.fr.text.erb +++ b/app/views/user_mailer/confirmation_instructions.fr.text.erb @@ -1,5 +1,5 @@ Bienvenue <%= @resource.email %> ! -Vous pouvez confirmer l'email de votre compte Mastodon en cliquant sur le lien ci-dessous : +Vous pouvez confirmer le courriel de votre compte Mastodon en cliquant sur le lien ci-dessous : <%= confirmation_url(@resource, confirmation_token: @token) %> diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb index d5a33cada..ad4f1b004 100644 --- a/app/workers/import_worker.rb +++ b/app/workers/import_worker.rb @@ -25,7 +25,7 @@ class ImportWorker def process_blocks(import) from_account = import.account - CSV.foreach(import.data.path) do |row| + CSV.new(open(import.data.url)).each do |row| next if row.size != 1 begin @@ -41,7 +41,7 @@ class ImportWorker def process_follows(import) from_account = import.account - CSV.foreach(import.data.path) do |row| + CSV.new(open(import.data.url)).each do |row| next if row.size != 1 begin diff --git a/config/application.rb b/config/application.rb index 9a5c0d0d3..dc937ca0e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -24,7 +24,9 @@ module Mastodon # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] - config.i18n.available_locales = [:en, :de, :es, :pt, :fr, :hu, :uk, 'zh-CN', :fi, :eo] + + config.i18n.available_locales = [:en, :de, :es, :pt, :fr, :hu, :uk, 'zh-CN', :fi, :eo, :ru] + config.i18n.default_locale = :en # config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') diff --git a/config/database.yml b/config/database.yml index 5ec342f93..810b83278 100644 --- a/config/database.yml +++ b/config/database.yml @@ -22,3 +22,4 @@ production: password: <%= ENV['DB_PASS'] || '' %> host: <%= ENV['DB_HOST'] || 'localhost' %> port: <%= ENV['DB_PORT'] || 5432 %> + prepared_statements: <%= ENV['PREPARED_STATEMENTS'] || 'true' %> diff --git a/config/environments/production.rb b/config/environments/production.rb index dc5dd4afd..05cced67b 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -38,9 +38,9 @@ Rails.application.configure do # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = false - # Use the lowest log level to ensure availability of diagnostic information + # By default, use the lowest log level to ensure availability of diagnostic information # when problems arise. - config.log_level = :debug + config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'debug').to_sym # Prepend all log lines with the following tags. config.log_tags = [:request_id] @@ -99,7 +99,9 @@ Rails.application.configure do :user_name => ENV['SMTP_LOGIN'], :password => ENV['SMTP_PASSWORD'], :domain => ENV['SMTP_DOMAIN'] || config.x.local_domain, - :authentication => :plain, + :authentication => ENV['SMTP_AUTH_METHOD'] || :plain, + :openssl_verify_mode => ENV['SMTP_OPENSSL_VERIFY_MODE'] || 'peer', + :enable_starttls_auto => ENV['SMTP_ENABLE_STARTTLS_AUTO'] || true, } config.action_mailer.delivery_method = :smtp diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 4304bbd18..7ae143f93 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -33,7 +33,7 @@ search: ignore_unused: - 'activerecord.attributes.*' - - '{devise,will_paginate,doorkeeper}.*' + - '{devise,pagination,doorkeeper}.*' - '{datetime,time}.*' - 'simple_form.{yes,no}' - 'simple_form.{placeholders,hints,labels}.*' diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index ede6640bb..3c23e7b2e 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -74,7 +74,8 @@ Devise.setup do |config| # It will change confirmation, password recovery and other workflows # to behave the same regardless if the e-mail provided was right or wrong. # Does not affect registerable. - # config.paranoid = true + # See : https://github.com/plataformatec/devise/wiki/How-To:-Using-paranoid-mode,-avoid-user-enumeration-on-registerable + config.paranoid = true # By default Devise will store the user in session. You can skip storage for # particular strategies by setting this option. diff --git a/config/initializers/httplog.rb b/config/initializers/httplog.rb index 37f113d5d..5cfc16a8b 100644 --- a/config/initializers/httplog.rb +++ b/config/initializers/httplog.rb @@ -1,3 +1,5 @@ -HttpLog.options[:logger] = Rails.logger -HttpLog.options[:color] = { color: :yellow } -HttpLog.options[:compact_log] = true +HttpLog.configure do |config| + config.logger = Rails.logger + config.color = { color: :yellow } + config.compact_log = true +end diff --git a/config/initializers/kaminari_config.rb b/config/initializers/kaminari_config.rb new file mode 100644 index 000000000..bd455f382 --- /dev/null +++ b/config/initializers/kaminari_config.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +Kaminari.configure do |config| + config.default_per_page = 40 + config.window = 1 + config.left = 3 + config.right = 1 +end diff --git a/config/initializers/pagination.rb b/config/initializers/pagination.rb new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/config/initializers/pagination.rb diff --git a/config/locales/de.yml b/config/locales/de.yml index ed54bb699..75ac4e1bb 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -88,5 +88,3 @@ de: default: "%d.%m.%Y %H:%M" users: invalid_email: Inkorrekte E-mail-Addresse - will_paginate: - page_gap: "…" diff --git a/config/locales/devise.fr.yml b/config/locales/devise.fr.yml index ce44d041a..3b46b01e3 100644 --- a/config/locales/devise.fr.yml +++ b/config/locales/devise.fr.yml @@ -58,4 +58,4 @@ fr: not_locked: n'était pas verrouillé(e) not_saved: one: '1 erreur a empêché ce(tte) %{resource} d''être sauvegardé(e) :' - other: '%{count} erreurs ont empêché ce(tte) %{resource} d''être sauvegardé(e): ' + other: '%{count} erreurs ont empêché ce(tte) %{resource} d''être sauvegardé(e) : ' diff --git a/config/locales/devise.ru.yml b/config/locales/devise.ru.yml new file mode 100644 index 000000000..f829f9d8e --- /dev/null +++ b/config/locales/devise.ru.yml @@ -0,0 +1,61 @@ +--- +ru: + devise: + confirmations: + confirmed: Ваш адрес e-mail был успешно подтвержден. + send_instructions: Вы получите e-mail с инструкцией по подтверждению Вашего адреса e-mail в течение нескольких минут. + send_paranoid_instructions: Если Ваш адрес e-mail есть в нашей базе данных, вы получите e-mail с инструкцией по подтверждению Вашего адреса в течение нескольких минут. + failure: + already_authenticated: Вы уже авторизованы. + inactive: Ваш аккаунт еще не активирован. + invalid: Неверно введены %{authentication_keys} или пароль. + last_attempt: У Вас есть последняя попытка, после чего вход будет заблокирован. + locked: Ваш аккаунт заблокирован. + not_found_in_database: Неверно введены %{authentication_keys} или пароль. + timeout: Ваша сессия истекла. Пожалуйста, войдите снова, чтобы продолжить. + unauthenticated: Вам необходимо войти или зарегистрироваться. + unconfirmed: Вам необходимо подтвердить ваш адрес e-mail для продолжения. + mailer: + confirmation_instructions: + subject: 'Mastodon: Инструкция по подтверждению' + password_change: + subject: 'Mastodon: Пароль изменен' + reset_password_instructions: + subject: 'Mastodon: Инструкция по сбросу пароля' + unlock_instructions: + subject: 'Mastodon: Инструкция по разблокировке' + omniauth_callbacks: + failure: Не получилось аутентифицировать Вас с помощью %{kind} по следующей причине - "%{reason}". + success: Аутентификация с помощью аккаунта %{kind} прошла успешно. + passwords: + no_token: Вы можете получить доступ к этой странице, только перейдя по ссылке в e-mail для сброса пароля. Если Вы действительно перешли по такой ссылке, пожалуйста, удостоверьтесь, что ссылка была введена полностью и без изменений. + send_instructions: Вы получите e-mail с инструкцией по сбросу пароля в течение нескольких минут. + send_paranoid_instructions: Если Ваш адрес e-mail есть в нашей базе данных, Вы получите e-mail со ссылкой для сброса пароля в течение нескольких минут. + updated: Ваш пароль был успешно изменен. Вход выполнен. + updated_not_active: Ваш пароль был успешно изменен. + registrations: + destroyed: До свидания! Ваш аккаунт был успешно удален. Мы надеемся скоро увидеть Вас снова. + signed_up: Добро пожаловать! Вы успешно зарегистрировались. + signed_up_but_inactive: Вы успешно зарегистрировались. Тем не менее, мы не можем авторизовать Вас, поскольку Ваш аккаунт еще не активирован. + signed_up_but_locked: Вы успешно зарегистрировались. Тем не менее, мы не можем авторизовать Вас, поскольку Ваш аккаунт заблокирован. + signed_up_but_unconfirmed: Сообщение со ссылкой для подтверждения было выслано на Ваш адрес e-mail. Пожалуйста, пройдите по ссылке для активации Вашего аккаунта. + update_needs_confirmation: Вы успешно обновили Ваш аккаунт, но нам нужно подтвердить ваш новый адрес e-mail. Пожалуйста, проверьте почту и пройдите по ссылке для подтверждения Вашего нового адреса. + updated: Ваш аккаунт был успешно обновлен. + sessions: + already_signed_out: Выход прошел успешно. + signed_in: Вход прошел успешно. + signed_out: Выход прошел успешно. + unlocks: + send_instructions: Вы получите e-mail с инструкцией по разблокировке Вашего аккаунта в течение нескольких минут. + send_paranoid_instructions: Если Ваш аккаунт существует, Вы получите e-mail с инструкцией по его разблокировке в течение нескольких минут. + unlocked: Ваш аккаунт был успешно разблокирован. пожалуйста, войдите для продолжения. + errors: + messages: + already_confirmed: уже подтвержден, пожалуйста, попробуйте войти + confirmation_period_expired: не был подтвержден в течение %{period}, пожалуйста, запросите новый + expired: истек, пожалуйста, запросите новый + not_found: не найден + not_locked: не был заблокирован + not_saved: + one: '1 ошибка помешала сохранению этого %{resource}:' + other: "%{count} ошибки помешали сохранению этого %{resource}:" diff --git a/config/locales/doorkeeper.fr.yml b/config/locales/doorkeeper.fr.yml index be109df9c..cfc9083d7 100644 --- a/config/locales/doorkeeper.fr.yml +++ b/config/locales/doorkeeper.fr.yml @@ -23,11 +23,11 @@ fr: edit: Modifier submit: Envoyer confirmations: - destroy: Êtes-vous certain? + destroy: Êtes-vous certain ? edit: title: Modifier l'application form: - error: Oups! Vérifier votre formulaire pour des erreurs possibles + error: Oups ! Vérifier votre formulaire pour des erreurs possibles help: native_redirect_uri: Utiliser %{native_redirect_uri} pour les tests locaux redirect_uri: Utiliser une ligne par URL @@ -54,7 +54,7 @@ fr: title: Une erreur est survenue new: able_to: Cette application pourra - prompt: Autoriser %{client_name} à utiliser votre compte? + prompt: Autoriser %{client_name} à utiliser votre compte ? title: Autorisation requise show: title: Code d'autorisation @@ -109,5 +109,5 @@ fr: title: Autorisation OAuth requise scopes: follow: s’abonner, se désabonner, bloquer, et débloquer des comptes - read: lire les données de votre compte + read: lire les données de votre compte write: poster en tant que vous diff --git a/config/locales/doorkeeper.ru.yml b/config/locales/doorkeeper.ru.yml new file mode 100644 index 000000000..8862936dc --- /dev/null +++ b/config/locales/doorkeeper.ru.yml @@ -0,0 +1,113 @@ +--- +ru: + activerecord: + attributes: + doorkeeper/application: + name: Название + redirect_uri: URI перенаправления + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: не может содержать фрагмент. + invalid_uri: должен быть правильным URI. + relative_uri: должен быть абсолютным URI. + secured_uri: должен быть HTTPS/SSL URI. + doorkeeper: + applications: + buttons: + authorize: Авторизовать + cancel: Отменить + destroy: Удалить + edit: Изменить + submit: Принять + confirmations: + destroy: Вы уверены? + edit: + title: Изменить приложение + form: + error: Ой! Проверьте Вашу форму на возможные ошибки + help: + native_redirect_uri: Используйте %{native_redirect_uri} для локального тестирования + redirect_uri: Используйте по одной строке на URI + scopes: Разделяйте список разрешений пробелами. Оставьте незаполненным для использования разрешений по умолчанию. + index: + callback_url: Callback URL + name: Название + new: Новое Приложение + title: Ваши приложения + new: + title: Новое Приложение + show: + actions: Действия + application_id: Id приложения + callback_urls: Callback urls + scopes: Разрешения + secret: Секрет + title: 'Приложение: %{name}' + authorizations: + buttons: + authorize: Авторизовать + deny: Отказать + error: + title: Произошла ошибка + new: + able_to: Оно сможет + prompt: Приложение %{client_name} запрашивает доступ к Вашему аккаунту + title: Требуется авторизация + show: + title: Код авторизации + authorized_applications: + buttons: + revoke: Отозвать авторизацию + confirmations: + revoke: Вы уверены? + index: + application: Приложение + created_at: Авторизовано + date_format: "%Y-%m-%d %H:%M:%S" + scopes: Разрешения + title: Ваши авторизованные приложения + errors: + messages: + access_denied: Владелец ресурса или сервер авторизации ответил отказом на Ваш запрос. + credential_flow_not_configured: Поток с предоставлением клиенту пароля завершился неудачей, поскольку параметр Doorkeeper.configure.resource_owner_from_credentials не был сконфигурирован. + invalid_client: Клиентская аутентификация завершилась неудачей (неизвестный клиент, не включена клиентская аутентификация, или метод аутентификации не поддерживается. + invalid_grant: Предоставленный доступ некорректен, истек, отозван, не совпадает с URI перенаправления, использованным в запросе авторизации, или был выпущен для другого клиента. + invalid_redirect_uri: Включенный URI перенаправления некорректен. + invalid_request: В запросе не хватает обязательного параметра, присутствует неподдерживаемое значение параметра, либо он был сформирован неверно. + invalid_resource_owner: Предоставленные данные владельца ресурса некорректны, или владелец ресурса не может быть найден + invalid_scope: Запрошенное разрешение некорректно, неизвестно или неверно сформировано. + invalid_token: + expired: Токен доступа истек + revoked: Токен доступа был отменен + unknown: Токен доступа некорректен + resource_owner_authenticator_not_configured: Поиск владельца ресурса завершился неудачей, поскольку параметр Doorkeeper.configure.resource_owner_authenticator не был сконфигурирован. + server_error: Сервер авторизации встретился с неожиданной ошибкой, не позволившей ему выполнить запрос. + temporarily_unavailable: Сервер авторизации в данный момент не может выполнить запрос по причине временной перегрузки или профилактики. + unauthorized_client: Клиент не авторизован для выполнения этого запроса с использованием этого метода. + unsupported_grant_type: Тип авторизации не поддерживается сервером авторизации. + unsupported_response_type: Сервер авторизации не поддерживает этот тип ответа. + flash: + applications: + create: + notice: Приложение создано. + destroy: + notice: Приложение удалено. + update: + notice: Приложение обновлено. + authorized_applications: + destroy: + notice: Авторизация приложения отозвана. + layouts: + admin: + nav: + applications: Приложения + oauth2_provider: Провайдер OAuth2 + application: + title: Требуется авторизация OAuth + scopes: + follow: подписываться, отписываться, блокировать и разблокировать аккаунты + read: читать данные Вашего аккаунта + write: отправлять за Вас посты diff --git a/config/locales/en.yml b/config/locales/en.yml index 118798ba1..6c4738991 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -126,6 +126,7 @@ en: pagination: next: Next prev: Prev + truncate: "…" remote_follow: acct: Enter your username@domain you want to follow from missing_resource: Could not find the required redirect URL for your account @@ -169,5 +170,3 @@ en: users: invalid_email: The e-mail address is invalid invalid_otp_token: Invalid two-factor code - will_paginate: - page_gap: "…" diff --git a/config/locales/eo.yml b/config/locales/eo.yml index 3644b37bb..e82e42495 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -160,5 +160,3 @@ eo: users: invalid_email: La retpoŝt-adreso ne estas valida invalid_otp_token: La dufaktora aŭtentigila kodo ne estas valida - will_paginate: - page_gap: "…" diff --git a/config/locales/es.yml b/config/locales/es.yml index 19f2c71b8..42245d675 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -51,5 +51,3 @@ es: settings: edit_profile: Editar perfil preferences: Preferencias - will_paginate: - page_gap: "…" diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 56aa9df49..c11237226 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -160,5 +160,3 @@ fi: users: invalid_email: Virheellinen sähköposti invalid_otp_token: Virheellinen kaksivaihe tunnistus koodi - will_paginate: - page_gap: "…" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 9727f3b7e..92cf43944 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -4,7 +4,7 @@ fr: about_mastodon: Mastodon est un serveur <em>libre</em> de réseautage social. Alternative <em>décentralisée</em> aux plateformes commerciales, la monopolisation de vos communications par une entreprise unique est évitée. Tout un chacun peut faire tourner Mastodon et participer au <em>réseau social</em> de manière transparente. about_this: À propos de cette instance apps: Applications - business_email: E-mail professionnel + business_email: Courriel professionnel closed_registrations: Les inscriptions sont actuellement fermées sur cette instance. contact: Contact description_headline: Qu'est-ce que %{domain} ? @@ -40,9 +40,9 @@ fr: remote_follow: Suivre à distance unfollow: Ne plus suivre application_mailer: - settings: 'Changer les préférences e-mail: ${link}' + settings: 'Changer les préférences courriel : ${link}' signature: Notifications de Mastodon depuis %{instance} - view: 'Voir:' + view: 'Voir :' applications: invalid_url: L'URL fournie est invalide auth: @@ -58,21 +58,27 @@ fr: authorize_follow: error: Malheureusement, il y a eu une erreur en cherchant les détails du compte distant follow: Suivre - prompt_html: 'Vous (<strong>%{self}</strong>) avez demandé à suivre:' + prompt_html: 'Vous (<strong>%{self}</strong>) avez demandé à suivre :' title: Suivre %{acct} datetime: distance_in_words: about_x_hours: "%{count}h" - about_x_months: "%{count}mo" - about_x_years: "%{count}y" - almost_x_years: "%{count}y" + about_x_months: "%{count}mois" + about_x_years: + one: un an + other: "%{count} ans" + almost_x_years: + one: un an + other: "%{count} ans" half_a_minute: A l'instant - less_than_x_minutes: "%{count}m" + less_than_x_minutes: "%{count}min" less_than_x_seconds: A l'instant - over_x_years: "%{count}y" - x_days: "%{count}d" - x_minutes: "%{count}m" - x_months: "%{count}mo" + over_x_years: + one: un an + other: "%{count} ans" + x_days: "%{count}j" + x_minutes: "%{count}min" + x_months: "%{count}mois" x_seconds: "%{count}s" exports: blocks: Vous bloquez @@ -96,7 +102,7 @@ fr: landing_strip_html: <strong>%{name}</strong> utilise <strong>%{domain}</strong>. Vous pouvez le/la suivre et interagir si vous possédez un compte quelque part dans le "fediverse". Si ce n'est pas le cas, vous pouvez <a href="%{sign_up_path}">en créer un ici</a>. notification_mailer: digest: - body: 'Voici ce que vous avez raté sur ${instance} depuis votre dernière visite (%{}):' + body: 'Voici ce que vous avez raté sur ${instance} depuis votre dernière visite (%{}) :' mention: '%{name} vous a mentionné⋅e' new_followers_summary: one: Vous avez un⋅e nouvel⋅le abonné⋅e ! Youpi ! @@ -156,10 +162,8 @@ fr: disable: Désactiver enable: Activer instructions_html: "<strong>Scannez ce QR code grâce à Google Authenticator, Authy ou une application similaire sur votre téléphone</strong>. Désormais, cette application générera des jetons que vous devrez saisir à chaque connexion." - plaintext_secret_html: 'Code secret en clair: <samp>%{secret}</samp>' + plaintext_secret_html: 'Code secret en clair : <samp>%{secret}</samp>' warning: Si vous ne pouvez pas configurer une application d'authentification maintenant, vous devriez cliquer sur "Désactiver" pour ne pas bloquer l'accès à votre compte. users: - invalid_email: L'adresse e-mail est invalide + invalid_email: L'adresse courriel est invalide invalid_otp_token: Le code d'authentification à deux facteurs est invalide - will_paginate: - page_gap: "…" diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 915d02c19..96b73d43c 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -51,5 +51,3 @@ hu: settings: edit_profile: Profil szerkesztése preferences: Beállítások - will_paginate: - page_gap: "…" diff --git a/config/locales/no.yml b/config/locales/no.yml index b9a752d5a..9aa966d2a 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -160,5 +160,3 @@ users: invalid_email: E-post addressen er ugyldig invalid_otp_token: Ugyldig two-faktor kode - will_paginate: - page_gap: "…" diff --git a/config/locales/pt.yml b/config/locales/pt.yml index ad7d05e3b..f2c7458f7 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -51,5 +51,3 @@ pt: settings: edit_profile: Editar perfil preferences: Preferências - will_paginate: - page_gap: "…" diff --git a/config/locales/ru.yml b/config/locales/ru.yml new file mode 100644 index 000000000..fab178629 --- /dev/null +++ b/config/locales/ru.yml @@ -0,0 +1,163 @@ +--- +ru: + about: + about_mastodon: Mastodon - это <em>свободная</em> социальная сеть с <em>открытым исходным кодом</em>. Как <em>децентрализованная</em> альтернатива коммерческим платформам, Mastodon предотвращает риск монополизации Вашего общения одной компанией. Выберите сервер, которому Вы доверяете — что бы Вы ни выбрали, Вы сможете общаться со всеми остальными. Любой может запустить свой собственный узел Mastodon и участвовать в <em>социальной сети</em> совершенно бесшовно. + about_this: Об этом узле + apps: Приложения + business_email: 'Деловой e-mail:' + closed_registrations: В данный момент регистрация на этом узле закрыта. + contact: Связаться + description_headline: Что такое %{domain}? + domain_count_after: другими узлами + domain_count_before: Связывается с + features: + api: Открытый API для приложений и сервисов + blocks: Продвинутые инструменты блокирования и глушения + characters: 500 символов на пост + chronology: Хронологические ленты + ethics: 'Этичный дизайн: нет рекламы, нет слежения' + gifv: GIFV и короткие видео + privacy: Тонкие настройки приватности для каждого поста + public: Публичные ленты + features_headline: Что выделяет Mastodon + get_started: Начать + links: Ссылки + other_instances: Другие узлы + source_code: Исходный код + status_count_after: статусов + status_count_before: Автор + terms: Условия + user_count_after: пользователей + user_count_before: Здесь живет + accounts: + follow: Подписаться + followers: Подписчики + following: Подписан(а) + nothing_here: Здесь ничего нет! + people_followed_by: Люди, на которых подписан(а) %{name} + people_who_follow: Подписчики %{name} + posts: Посты + remote_follow: Подписаться на удаленном узле + unfollow: Отписаться + application_mailer: + settings: 'Изменить настройки e-mail: %{link}' + signature: Уведомления Mastodon от %{instance} + view: 'View:' + applications: + invalid_url: Введенный URL неверен + auth: + change_password: Изменить пароль + didnt_get_confirmation: Не получили инструкцию для подтверждения? + forgot_password: Забыли пароль? + login: Войти + logout: Выйти + register: Зарегистрироваться + resend_confirmation: Повторить отправку инструкции для подтверждения + reset_password: Сбросить пароль + set_new_password: Задать новый пароль + authorize_follow: + error: К сожалению, при поиске удаленного аккаунта возникла ошибка + follow: Подписаться + prompt_html: 'Вы (<strong>%{self}</strong>) запросили подписку:' + title: Подписаться на %{acct} + datetime: + distance_in_words: + about_x_hours: "%{count}ч" + about_x_months: "%{count}мес" + about_x_years: "%{count}г" + almost_x_years: "%{count}г" + half_a_minute: Только что + less_than_x_minutes: "%{count}мин" + less_than_x_seconds: Только что + over_x_years: "%{count}г" + x_days: "%{count}д" + x_minutes: "%{count}мин" + x_months: "%{count}мес" + x_seconds: "%{count}сек" + exports: + blocks: Вы заблокировали + csv: CSV + follows: Подписки + storage: Ваш медиаконтент + generic: + changes_saved_msg: Изменения успешно сохранены! + powered_by: работает на %{link} + save_changes: Сохранить изменения + validation_errors: + one: Что-то здесь не так! Пожалуйста, прочитайте об ошибке ниже + other: Что-то здесь не так! Пожалуйста, прочитайте о %{count} ошибках ниже + imports: + preface: Вы можете загрузить некоторые данные, например, списки людей, на которых Вы подписаны или которых блокируете, в Ваш аккаунт на этом узле из файлов, экспортированных с другого узла. + success: Ваши данные были успешно загружены и будут обработаны с должной скоростью + types: + blocking: Список блокируемых + following: Список подписок + upload: Загрузить + landing_strip_html: <strong>%{name}</strong> - пользователь на <strong>%{domain}</strong>. Вы можете подписаться на него/нее и общаться с ним/ней, если у Вас есть аккаунт на любом узле общей сети. Если у Вас его нет, вы можете <a href="%{sign_up_path}">зарегистрироваться здесь</a>. + notification_mailer: + digest: + body: 'Кратко о пропущенном Вами на %{instance} с Вашего последнего захода %{since}:' + mention: "%{name} упомянул(а) Вас в:" + new_followers_summary: + one: У Вас появился новый подписчик! Ура! + other: У Вас появилось %{count} новых подписчика(-ов)! Отлично! + subject: + one: "1 новое уведомление с Вашего последнего захода \U0001F418" + other: "%{count} новых уведомлений с Вашего последнего захода \U0001F418" + favourite: + body: 'Ваш статус понравился %{name}:' + subject: "%{name} понравился Ваш статус" + follow: + body: "%{name} теперь подписан(а) на Вас!" + subject: "%{name} теперь подписан(а) на Вас" + follow_request: + body: "%{name} запросил Вас о подписке" + subject: '%{name} хочет подписаться на Вас' + mention: + body: 'Вас упомянул(а) %{name} в:' + subject: Вы были упомянуты %{name} + reblog: + body: 'Ваш статус был продвинут %{name}:' + subject: "%{name} продвинул(а) Ваш статус" + pagination: + next: След + prev: Пред + remote_follow: + acct: Введите username@domain, откуда Вы хотите подписаться + missing_resource: Поиск требуемого перенаправления URL для Вашего аккаунта завершился неудачей + proceed: Продолжить подписку + prompt: 'Вы ходите подписаться на:' + settings: + authorized_apps: Авторизованные приложения + back: Назад в Mastodon + edit_profile: Изменить профиль + export: Экспорт данных + import: Импорт + preferences: Настройки + settings: Опции + two_factor_auth: Двухфакторная аутентификация + statuses: + open_in_web: Открыть в WWW + over_character_limit: превышен лимит символов (%{max}) + show_more: Подробнее + visibilities: + private: Показывать только подписчикам + public: Публичный + unlisted: Публичный, но без отображения в публичных лентах + stream_entries: + click_to_show: Показать + reblogged: продвинул(а) + sensitive_content: Чувствительный контент + time: + formats: + default: "%b %d, %Y, %H:%M" + two_factor_auth: + description_html: При включении <strong>двухфакторной аутентификации</strong>, вход потребует от Вас использования Вашего телефона, который сгенерирует входные токены. + disable: Отключить + enable: Включить + instructions_html: "<strong>Отсканируйте этот QR-код с помощью Google Authenticator или другого подобного приложения на Вашем телефоне</strong>. С этого момента приложение будет генерировать токены, которые будет необходимо ввести для входа." + plaintext_secret_html: 'Секрет открытым текстом: <samp>%{secret}</samp>' + warning: Если сейчас у Вас не получается настроить аутентификатор, нажмите "отключить", иначе Вы не сможете войти! + users: + invalid_email: Введенный e-mail неверен + invalid_otp_token: Введен неверный код diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml new file mode 100644 index 000000000..6f4873bfd --- /dev/null +++ b/config/locales/simple_form.ru.yml @@ -0,0 +1,46 @@ +--- +ru: + simple_form: + hints: + defaults: + avatar: PNG, GIF или JPG. Максимально 2MB. Будет уменьшено до 120x120px + display_name: Максимально 30 символов + header: PNG, GIF или JPG. Максимально 2MB. Будет уменьшено до 700x335px + locked: Потребует от Вас ручного подтверждения подписчиков, изменит приватность постов по умолчанию на "только для подписчиков" + note: Максимально 160 символов + imports: + data: Файл CSV, экспортированный с другого узла Mastodon + labels: + defaults: + avatar: Аватар + confirm_new_password: Повторите новый пароль + confirm_password: Повторите пароль + current_password: Текущий пароль + data: Данные + display_name: Показываемое имя + email: Адрес e-mail + header: Заголовок + locale: Язык + locked: Сделать аккаунт приватным + new_password: Новый пароль + note: О Вас + otp_attempt: Двухфакторный код + password: Пароль + setting_default_privacy: Приватность постов + type: Тип импорта + username: Имя пользователя + interactions: + must_be_follower: Заблокировать уведомления не от подписчиков + must_be_following: Заблокировать уведомления от людей, на которых Вы не подписаны + notification_emails: + digest: Присылать дайджест по e-mail + favourite: Уведомлять по e-mail, когда кому-то нравится Ваш статус + follow: Уведомлять по e-mail, когда кто-то подписался на Вас + follow_request: Уведомлять по e-mail, когда кто-то запрашивает разрешение на подписку + mention: Уведомлять по e-mail, когда кто-то упомянул Вас + reblog: Уведомлять по e-mail, когда кто-то продвинул Ваш статус + 'no': 'Нет' + required: + mark: "*" + text: обязательно + 'yes': 'Да' diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 27e8135df..f7176e86d 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -51,5 +51,3 @@ uk: settings: edit_profile: Редагувати профіль preferences: Налаштування - will_paginate: - page_gap: "…" diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 78c4d46e2..48028d00c 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -150,5 +150,3 @@ zh-CN: users: invalid_email: 无效的邮箱 invalid_otp_token: 无效的两步验证码 - will_paginate: - page_gap: "…" diff --git a/config/routes.rb b/config/routes.rb index 9cbecf077..9adcdb862 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -163,6 +163,7 @@ Rails.application.routes.draw do collection do get :relationships get :verify_credentials + patch :update_credentials get :search end @@ -188,11 +189,14 @@ Rails.application.routes.draw do get '/web/(*any)', to: 'home#index', as: :web - get '/about', to: 'about#index' + get '/about', to: 'about#show' get '/about/more', to: 'about#more' get '/terms', to: 'about#terms' root 'home#index' - match '*unmatched_route', via: :all, to: 'application#raise_not_found' + match '*unmatched_route', + via: :all, + to: 'application#raise_not_found', + format: false end diff --git a/config/settings.yml b/config/settings.yml index e4501e6e6..d364120db 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -1,4 +1,11 @@ # config/app.yml for rails-settings-cached +# +# This file contains default values, and does not need to be edited +# when configuring an instance. These settings may be changed by an +# Administrator using the Web UI. +# +# For more information, see docs/Running-Mastodon/Administration-guide.md +# defaults: &defaults site_title: 'Mastodon' site_description: '' diff --git a/docker-compose.yml b/docker-compose.yml index d6ba66dde..910bf8cfe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,20 @@ version: '2' services: + db: restart: always image: postgres:alpine +### Uncomment to enable DB persistance +# volumes: +# - ./postgres:/var/lib/postgresql/data + redis: restart: always image: redis:alpine +### Uncomment to enable REDIS persistance +# volumes: +# - ./redis:/data + web: restart: always build: . @@ -19,6 +28,7 @@ services: volumes: - ./public/assets:/mastodon/public/assets - ./public/system:/mastodon/public/system + streaming: restart: always build: . @@ -29,6 +39,7 @@ services: depends_on: - db - redis + sidekiq: restart: always build: . diff --git a/docs/Running-Mastodon/Administration-guide.md b/docs/Running-Mastodon/Administration-guide.md index 09b0f1df1..8bcfe7c9e 100644 --- a/docs/Running-Mastodon/Administration-guide.md +++ b/docs/Running-Mastodon/Administration-guide.md @@ -35,3 +35,11 @@ You are able to set the following settings: - Site extended description You may wish to use the extended description (shown at https://yourmastodon.instance/about/more ) to display content guidelines or a user agreement (see https://mastodon.social/about/more for an example). + +## Confirming Users Manually + +The following rake task: + + RAILS_ENV=production bundle exec rails mastodon:confirm_email USER_EMAIL=alice@alice.com + +Will confirm a user manually, in case they don't have access to their confirmation email for whatever reason. diff --git a/docs/Running-Mastodon/Production-guide.md b/docs/Running-Mastodon/Production-guide.md index 785826aca..49f3e59b2 100644 --- a/docs/Running-Mastodon/Production-guide.md +++ b/docs/Running-Mastodon/Production-guide.md @@ -34,10 +34,19 @@ server { keepalive_timeout 70; sendfile on; client_max_body_size 0; - gzip off; root /home/mastodon/live/public; + gzip on; + gzip_disable "msie6"; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; location / { @@ -49,7 +58,7 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; - + proxy_set_header Proxy ""; proxy_pass_header Server; proxy_pass http://localhost:3000; @@ -67,6 +76,7 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; + proxy_set_header Proxy ""; proxy_pass http://localhost:4000; proxy_buffering off; @@ -121,7 +131,7 @@ It is recommended to use rbenv (exclusively from the `mastodon` user) to install [2]: https://github.com/rbenv/ruby-build#installation [3]: https://github.com/rbenv/ruby-build/wiki#suggested-build-environment -Then once `rbenv` is ready, run `rbenv install 2.3.1` to install the Ruby version for Mastodon. +Then once `rbenv` is ready, run `rbenv install 2.4.1` to install the Ruby version for Mastodon. ## Git diff --git a/docs/Using-Mastodon/Apps.md b/docs/Using-Mastodon/Apps.md index b5e1fa36b..ce3f2f1fc 100644 --- a/docs/Using-Mastodon/Apps.md +++ b/docs/Using-Mastodon/Apps.md @@ -14,5 +14,6 @@ Some people have started working on apps for the Mastodon API. Here is a list of |Tooter|Chrome|<https://github.com/ineffyble/tooter>|[@effy@mastodon.social](https://mastodon.social/users/effy)| |tootstream|CLI|<https://github.com/magicalraccoon/tootstream>|[@Raccoon@mastodon.social](https://mastodon.social/users/Raccoon)| |HackerNewsBot|CLI|<https://github.com/raymestalez/mastodon-hnbot>|[@rayalez@hackertribe.io](https://hackertribe.io/users/rayalez)| +|Mastodon.tools|Wordpress, web browser, social network|<https://github.com/davidlibeau/mastodon-tools>|[@David@mastodon.xyz](https://mastodon.xyz/users/David)| If you have a project like this, let me know so I can add it to the list! diff --git a/docs/Using-Mastodon/List-of-Mastodon-instances.md b/docs/Using-Mastodon/List-of-Mastodon-instances.md index db35edb1a..49b2c2012 100644 --- a/docs/Using-Mastodon/List-of-Mastodon-instances.md +++ b/docs/Using-Mastodon/List-of-Mastodon-instances.md @@ -76,7 +76,7 @@ There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz) | [mastodon.fun](https://mastodon.fun/)|Mastodon for everyone ! |Yes|Yes| | [oulipo.social](https://oulipo.social/)|An Oulipo Mastodon in which that fifth symbol in Latin script is taboo|Yes|No| | [indigo.zone](https://indigo.zone)|Open Registrations, General Purpose|Yes|No| +| [mastodon.cloud](https://mastodon.cloud)|An open Mastodon instance with people from all around the world|Yes|Yes| | [mst3k.interlinked.me](https://mst3k.interlinked.me)|Open registrations, general purpose|Yes|Yes| - We are no longer maintaining this list as instances are popping up too quickly for using GitHub to be a tenable system for tracking them. Please standby while we work on another solution diff --git a/docs/Using-the-API/API.md b/docs/Using-the-API/API.md index 8a648b6d0..fee1fde94 100644 --- a/docs/Using-the-API/API.md +++ b/docs/Using-the-API/API.md @@ -30,7 +30,7 @@ API overview - [Instance](#instance) - [Mention](#mention) - [Notification](#notification) - - [Relationships](#relationships) + - [Relationship](#relationship) - [Results](#results) - [Status](#status) - [Tag](#tag) @@ -85,6 +85,17 @@ Returns an [Account](#account). Returns the authenticated user's [Account](#account). +#### Updating the current user: + + PATCH /api/v1/accounts/update_credentials + +Form data: + +- `display_name`: The name to display in the user's profile +- `note`: A new biography for the user +- `avatar`: A base64 encoded image to display as the user's avatar (e.g. `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUoAAADrCAYAAAA...`) +- `header`: A base64 encoded image to display as the user's header image (e.g. `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUoAAADrCAYAAAA...`) + #### Getting an account's followers: GET /api/v1/accounts/:id/followers @@ -351,15 +362,15 @@ Returns an empty object. #### Reblogging/unreblogging a status: - POST /api/vi/statuses/:id/reblog - POST /api/vi/statuses/:id/unreblog + POST /api/v1/statuses/:id/reblog + POST /api/v1/statuses/:id/unreblog Returns the target [Status](#status). #### Favouriting/unfavouriting a status: - POST /api/vi/statuses/:id/favourite - POST /api/vi/statuses/:id/unfavourite + POST /api/v1/statuses/:id/favourite + POST /api/v1/statuses/:id/unfavourite Returns the target [Status](#status). @@ -456,7 +467,7 @@ ___ | `acct` | Equals `username` for local users, includes `@domain` for remote ones | | `id` | Account ID | -### Notifications +### Notification | Attribute | Description | | ------------------------ | ----------- | @@ -464,9 +475,9 @@ ___ | `type` | One of: "mention", "reblog", "favourite", "follow" | | `created_at` | The time the notification was created | | `account` | The [Account](#account) sending the notification to the user | -| `status` | The [Status](#status) associated with the notification, if applicible | +| `status` | The [Status](#status) associated with the notification, if applicable | -### Relationships +### Relationship | Attribute | Description | | ------------------------ | ----------- | @@ -516,7 +527,7 @@ ___ | `tags` | An array of [Tags](#tag) | | `application` | [Application](#application) from which the status was posted | -### Tags +### Tag | Attribute | Description | | ------------------------ | ----------- | diff --git a/docs/Using-the-API/OAuth-details.md b/docs/Using-the-API/OAuth-details.md index d0b5abd40..e88a25682 100644 --- a/docs/Using-the-API/OAuth-details.md +++ b/docs/Using-the-API/OAuth-details.md @@ -9,4 +9,4 @@ The API is divided up into access scopes: - `write`: Post statuses and upload media for statuses - `follow`: Follow, unfollow, block, unblock -Multiple scopes can be requested during the authorization phase with the `scope` query param (space-separate the scopes). +Multiple scopes can be requested during the authorization phase with the `scope` query param (space-separate the scopes). If you do not specify a `scope` in your authorization request, the resulting access token will default to `read` access. diff --git a/docs/Using-the-API/Testing-with-cURL.md b/docs/Using-the-API/Testing-with-cURL.md index dc5f2022d..a373ec2bb 100644 --- a/docs/Using-the-API/Testing-with-cURL.md +++ b/docs/Using-the-API/Testing-with-cURL.md @@ -3,7 +3,7 @@ Testing the API with cURL Mastodon builds around the idea of being a server first, rather than a client itself. Similarly to how a XMPP chat server communicates with others and with its own clients, Mastodon takes care of federation to other networks, like other Mastodon or GNU Social instances. So Mastodon provides a REST API, and a 3rd-party app system for using it via OAuth2. -You can get a client ID and client secret required for OAuth [via an API end-point](API.md#oauth-apps). +You can get a client ID and client secret required for OAuth [via an API end-point](API.md#apps). From these two, you will need to acquire an access token. It is possible to do using your account's e-mail and password like this: @@ -13,6 +13,6 @@ The `/oauth/token` path will attempt to login with the given credentials, and th Use that token in any API requests by setting a header like this: - curl --header "Authorization: Bearer ACCESS_TOKEN_HERE" -sS https://mastodon.social/api/statuses/home + curl --header "Authorization: Bearer ACCESS_TOKEN_HERE" -sS https://mastodon.social/api/v1/timelines/home Please note that the password-based approach is not recommended especially if you're dealing with other user's accounts and not just your own. Usually you would use the authorization grant approach where you redirect the user to a web page on the original site where they can login and authorize the application and are then redirected back to your application with an access code. diff --git a/docs/Using-the-API/Tips-for-app-developers.md b/docs/Using-the-API/Tips-for-app-developers.md index 561f1e273..d60b472e5 100644 --- a/docs/Using-the-API/Tips-for-app-developers.md +++ b/docs/Using-the-API/Tips-for-app-developers.md @@ -13,4 +13,4 @@ Make sure that you make it possible to see the `acct` of any user in your app (s ## Formatting -The API delivers already formatted HTML to your app. This isn't ideal since not all apps are based on HTML, but this is not fixable as its part of the way OStatus federation works. Most importantly, you get some information on linked entities alongside the HTML of the status body. For example, you get a list of mentioned users, and a list of media attachments, and a list of hashtags. It is possible to convert the HTML to whatever you need in your app by parsing the HTML tags and matching their `href`s to the linked entities. If a match cannot be found, the link must stay a clickable link. +The API delivers already formatted HTML to your app. This isn't ideal since not all apps are based on HTML, but this is not fixable as it's part of the way OStatus federation works. Most importantly, you get some information on linked entities alongside the HTML of the status body. For example, you get a list of mentioned users, and a list of media attachments, and a list of hashtags. It is possible to convert the HTML to whatever you need in your app by parsing the HTML tags and matching their `href`s to the linked entities. If a match cannot be found, the link must stay a clickable link. diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 79dcb722a..a8fb58b7f 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -10,6 +10,18 @@ namespace :mastodon do puts "Congrats! #{user.account.username} is now an admin. \\o/\nNavigate to #{admin_settings_url} to get started" end + desc 'Manually confirms a user with associated user email address stored in USER_EMAIL environment variable.' + task confirm_email: :environment do + email = ENV.fetch('USER_EMAIL') + user = User.where(email: email).first + if user + user.update(confirmed_at: Time.now.utc) + puts "User #{email} confirmed." + else + abort "User #{email} not found." + end + end + namespace :media do desc 'Removes media attachments that have not been assigned to any status for longer than a day' task clear: :environment do @@ -80,5 +92,17 @@ namespace :mastodon do Rails.logger.debug 'Done!' end + + desc 'Generate static versions of GIF avatars/headers' + task add_static_avatars: :environment do + Rails.logger.debug 'Generating static avatars/headers for GIF ones...' + + Account.unscoped.where(avatar_content_type: 'image/gif').or(Account.unscoped.where(header_content_type: 'image/gif')).find_each do |account| + account.avatar.reprocess! + account.header.reprocess! + end + + Rails.logger.debug 'Done!' + end end end diff --git a/package.json b/package.json index 14c8abe79..fee78dd69 100644 --- a/package.json +++ b/package.json @@ -72,5 +72,10 @@ "webpack": "^2.2.1", "websocket.js": "^0.1.7", "ws": "^2.1.0" + }, + "devDependencies": { + "babel-eslint": "^7.2.1", + "eslint": "^3.19.0", + "eslint-plugin-react": "^6.10.3" } } diff --git a/scalingo.json b/scalingo.json index d60f1529c..4afaa6b4e 100644 --- a/scalingo.json +++ b/scalingo.json @@ -71,6 +71,18 @@ "description": "Address to send emails from", "required": false }, + "SMTP_AUTH_METHOD": { + "description": "Authentication method to use with SMTP server. Default is 'plain'.", + "required": false + }, + "SMTP_OPENSSL_VERIFY_MODE": { + "description": "SMTP server certificate verification mode. Defaults is 'peer'.", + "required": false + }, + "SMTP_ENABLE_STARTTLS_AUTO": { + "description": "Enable STARTTLS if SMTP server supports it? Default is true.", + "required": false + }, "BUILDPACK_URL": { "description": "Internal scalingo configuration", "required": true, diff --git a/spec/controllers/about_controller_spec.rb b/spec/controllers/about_controller_spec.rb index 4282649e1..f49de9622 100644 --- a/spec/controllers/about_controller_spec.rb +++ b/spec/controllers/about_controller_spec.rb @@ -3,9 +3,16 @@ require 'rails_helper' RSpec.describe AboutController, type: :controller do render_views - describe 'GET #index' do + describe 'GET #show' do it 'returns http success' do - get :index + get :show + expect(response).to have_http_status(:success) + end + end + + describe 'GET #more' do + it 'returns http success' do + get :more expect(response).to have_http_status(:success) end end diff --git a/spec/controllers/admin/reports_controller_spec.rb b/spec/controllers/admin/reports_controller_spec.rb new file mode 100644 index 000000000..622ea87c1 --- /dev/null +++ b/spec/controllers/admin/reports_controller_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +RSpec.describe Admin::ReportsController, type: :controller do + describe 'GET #index' do + before do + sign_in Fabricate(:user, admin: true), scope: :user + end + + it 'returns http success' do + get :index + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/admin/settings_controller_spec.rb b/spec/controllers/admin/settings_controller_spec.rb new file mode 100644 index 000000000..c126b645b --- /dev/null +++ b/spec/controllers/admin/settings_controller_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +RSpec.describe Admin::SettingsController, type: :controller do + describe 'GET #index' do + before do + sign_in Fabricate(:user, admin: true), scope: :user + end + + it 'returns http success' do + get :index + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index 5d36b0159..ed49779b4 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -24,6 +24,45 @@ RSpec.describe Api::V1::AccountsController, type: :controller do end end + describe 'PATCH #update_credentials' do + describe 'with valid data' do + before do + avatar = File.read(Rails.root.join('app', 'assets', 'images', 'logo.png')) + header = File.read(Rails.root.join('app', 'assets', 'images', 'mastodon-getting-started.png')) + + patch :update_credentials, params: { + display_name: "Alice Isn't Dead", + note: "Hi!\n\nToot toot!", + avatar: "data:image/png;base64,#{Base64.encode64(avatar)}", + header: "data:image/png;base64,#{Base64.encode64(header)}", + } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'updates account info' do + user.account.reload + + expect(user.account.display_name).to eq("Alice Isn't Dead") + expect(user.account.note).to eq("Hi!\n\nToot toot!") + expect(user.account.avatar).to exist + expect(user.account.header).to exist + end + end + + describe 'with invalid data' do + before do + patch :update_credentials, params: { note: 'This is too long. ' * 10 } + end + + it 'returns http unprocessable entity' do + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + describe 'GET #statuses' do it 'returns http success' do get :statuses, params: { id: user.account.id } diff --git a/spec/controllers/api/v1/notifications_controller_spec.rb b/spec/controllers/api/v1/notifications_controller_spec.rb index e5f7eec73..c390d4f01 100644 --- a/spec/controllers/api/v1/notifications_controller_spec.rb +++ b/spec/controllers/api/v1/notifications_controller_spec.rb @@ -5,15 +5,71 @@ RSpec.describe Api::V1::NotificationsController, type: :controller do let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:token) { double acceptable?: true, resource_owner_id: user.id } + let(:other) { Fabricate(:user, account: Fabricate(:account, username: 'bob')) } before do allow(controller).to receive(:doorkeeper_token) { token } end describe 'GET #index' do - it 'returns http success' do - get :index - expect(response).to have_http_status(:success) + before do + status = PostStatusService.new.call(user.account, 'Test') + @reblog = ReblogService.new.call(other.account, status) + @mention = PostStatusService.new.call(other.account, 'Hello @alice') + @favourite = FavouriteService.new.call(other.account, status) + @follow = FollowService.new.call(other.account, 'alice') + end + + describe 'with no options' do + before do + get :index + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'includes reblog' do + expect(assigns(:notifications).map(&:activity_id)).to include(@reblog.id) + end + + it 'includes mention' do + expect(assigns(:notifications).map(&:activity_id)).to include(@mention.mentions.first.id) + end + + it 'includes favourite' do + expect(assigns(:notifications).map(&:activity_id)).to include(@favourite.id) + end + + it 'includes follow' do + expect(assigns(:notifications).map(&:activity_id)).to include(@follow.id) + end + end + + describe 'with excluded mentions' do + before do + get :index, params: { exclude_types: ['mention'] } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'includes reblog' do + expect(assigns(:notifications).map(&:activity_id)).to include(@reblog.id) + end + + it 'excludes mention' do + expect(assigns(:notifications).map(&:activity_id)).to_not include(@mention.mentions.first.id) + end + + it 'includes favourite' do + expect(assigns(:notifications).map(&:activity_id)).to include(@favourite.id) + end + + it 'includes follow' do + expect(assigns(:notifications).map(&:activity_id)).to include(@follow.id) + end end end end diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb index 27ad6cbde..6b26e6693 100644 --- a/spec/controllers/auth/registrations_controller_spec.rb +++ b/spec/controllers/auth/registrations_controller_spec.rb @@ -5,6 +5,7 @@ RSpec.describe Auth::RegistrationsController, type: :controller do describe 'GET #new' do before do + Setting.open_registrations = true request.env["devise.mapping"] = Devise.mappings[:user] end @@ -16,6 +17,7 @@ RSpec.describe Auth::RegistrationsController, type: :controller do describe 'POST #create' do before do + Setting.open_registrations = true request.env["devise.mapping"] = Devise.mappings[:user] post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678' } } end diff --git a/spec/controllers/xrd_controller_spec.rb b/spec/controllers/xrd_controller_spec.rb index e687cf9e0..b56c68f5c 100644 --- a/spec/controllers/xrd_controller_spec.rb +++ b/spec/controllers/xrd_controller_spec.rb @@ -14,7 +14,7 @@ RSpec.describe XrdController, type: :controller do let(:alice) { Fabricate(:account, username: 'alice') } it 'returns http success when account can be found' do - get :webfinger, params: { resource: "acct:#{alice.username}@#{Rails.configuration.x.local_domain}" } + get :webfinger, params: { resource: alice.to_webfinger_s } expect(response).to have_http_status(:success) end diff --git a/spec/fixtures/files/avatar.gif b/spec/fixtures/files/avatar.gif new file mode 100644 index 000000000..d929801e5 --- /dev/null +++ b/spec/fixtures/files/avatar.gif Binary files differdiff --git a/spec/helpers/about_helper_spec.rb b/spec/helpers/about_helper_spec.rb deleted file mode 100644 index 6efc9f5bd..000000000 --- a/spec/helpers/about_helper_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe AboutHelper, type: :helper do - -end diff --git a/spec/helpers/accounts_helper_spec.rb b/spec/helpers/accounts_helper_spec.rb deleted file mode 100644 index 3aea1f909..000000000 --- a/spec/helpers/accounts_helper_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe AccountsHelper, type: :helper do - -end diff --git a/spec/helpers/admin/domain_blocks_helper_spec.rb b/spec/helpers/admin/domain_blocks_helper_spec.rb deleted file mode 100644 index cc7ead84e..000000000 --- a/spec/helpers/admin/domain_blocks_helper_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe Admin::DomainBlocksHelper, type: :helper do - -end diff --git a/spec/helpers/admin/pubsubhubbub_helper_spec.rb b/spec/helpers/admin/pubsubhubbub_helper_spec.rb deleted file mode 100644 index 673236a7e..000000000 --- a/spec/helpers/admin/pubsubhubbub_helper_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe Admin::PubsubhubbubHelper, type: :helper do - -end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index c2063c995..a2eeb443c 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -1,5 +1,19 @@ require 'rails_helper' -RSpec.describe ApplicationHelper, type: :helper do +describe ApplicationHelper do + describe 'active_nav_class' do + it 'returns active when on the current page' do + allow(helper).to receive(:current_page?).and_return(true) + result = helper.active_nav_class("/test") + expect(result).to eq "active" + end + + it 'returns empty string when not on current page' do + allow(helper).to receive(:current_page?).and_return(false) + + result = helper.active_nav_class("/test") + expect(result).to eq "" + end + end end diff --git a/spec/helpers/authorize_follow_helper_spec.rb b/spec/helpers/authorize_follow_helper_spec.rb deleted file mode 100644 index ba5b0a70b..000000000 --- a/spec/helpers/authorize_follow_helper_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe AuthorizeFollowHelper, type: :helper do - -end diff --git a/spec/helpers/stream_entries_helper_spec.rb b/spec/helpers/stream_entries_helper_spec.rb index 6227f9280..221e1e32d 100644 --- a/spec/helpers/stream_entries_helper_spec.rb +++ b/spec/helpers/stream_entries_helper_spec.rb @@ -2,7 +2,17 @@ require 'rails_helper' RSpec.describe StreamEntriesHelper, type: :helper do describe '#display_name' do - pending + it 'uses the display name when it exists' do + account = Account.new(display_name: "Display", username: "Username") + + expect(helper.display_name(account)).to eq "Display" + end + + it 'uses the username when display name is nil' do + account = Account.new(display_name: nil, username: "Username") + + expect(helper.display_name(account)).to eq "Username" + end end describe '#avatar_for_status_url' do diff --git a/spec/helpers/tags_helper_spec.rb b/spec/helpers/tags_helper_spec.rb deleted file mode 100644 index f661e44ac..000000000 --- a/spec/helpers/tags_helper_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe TagsHelper, type: :helper do - -end diff --git a/spec/helpers/xrd_helper_spec.rb b/spec/helpers/xrd_helper_spec.rb deleted file mode 100644 index 0bc71b657..000000000 --- a/spec/helpers/xrd_helper_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe XrdHelper, type: :helper do - -end diff --git a/spec/javascript/components/avatar.test.jsx b/spec/javascript/components/avatar.test.jsx index 852e13a89..7131bbec7 100644 --- a/spec/javascript/components/avatar.test.jsx +++ b/spec/javascript/components/avatar.test.jsx @@ -6,16 +6,10 @@ import Avatar from '../../../app/assets/javascripts/components/components/avatar describe('<Avatar />', () => { const src = '/path/to/image.jpg'; const size = 100; - const wrapper = render(<Avatar src={src} size={size} />); + const wrapper = render(<Avatar src={src} animate size={size} />); - it('renders an img element with the given src', () => { - expect(wrapper.find('img')).to.have.attr('src', `${src}`); - }); - - it('renders an img element of the given size', () => { - ['width', 'height'].map((attr) => { - expect(wrapper.find('img')).to.have.attr(attr, `${size}`); - }); + it('renders a div element with the given src as background', () => { + expect(wrapper.find('div')).to.have.style('background-image', `url(${src})`); }); it('renders a div element of the given size', () => { diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 93a45459d..fb367ab7a 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -54,6 +54,30 @@ RSpec.describe Account, type: :model do end end + describe 'Local domain user methods' do + around do |example| + before = Rails.configuration.x.local_domain + example.run + Rails.configuration.x.local_domain = before + end + + describe '#to_webfinger_s' do + it 'returns a webfinger string for the account' do + Rails.configuration.x.local_domain = 'example.com' + + expect(subject.to_webfinger_s).to eq 'acct:alice@example.com' + end + end + + describe '#local_username_and_domain' do + it 'returns the username and local domain for the account' do + Rails.configuration.x.local_domain = 'example.com' + + expect(subject.local_username_and_domain).to eq 'alice@example.com' + end + end + end + describe '#acct' do it 'returns username for local users' do expect(subject.acct).to eql 'alice' @@ -170,6 +194,61 @@ RSpec.describe Account, type: :model do end end + describe '.search_for' do + before do + @match = Fabricate( + :account, + display_name: "Display Name", + username: "username", + domain: "example.com" + ) + _missing = Fabricate( + :account, + display_name: "Missing", + username: "missing", + domain: "missing.com" + ) + end + + it 'finds accounts with matching display_name' do + results = Account.search_for("display") + expect(results).to eq [@match] + end + + it 'finds accounts with matching username' do + results = Account.search_for("username") + expect(results).to eq [@match] + end + + it 'finds accounts with matching domain' do + results = Account.search_for("example") + expect(results).to eq [@match] + end + + it 'ranks multiple matches higher' do + account = Fabricate( + :account, + username: "username", + display_name: "username" + ) + results = Account.search_for("username") + expect(results).to eq [account, @match] + end + end + + describe '.advanced_search_for' do + it 'ranks followed accounts higher' do + account = Fabricate(:account) + match = Fabricate(:account, username: "Matching") + followed_match = Fabricate(:account, username: "Matcher") + Fabricate(:follow, account: account, target_account: followed_match) + + results = Account.advanced_search_for("match", account) + expect(results).to eq [followed_match, match] + expect(results.first.rank).to be > results.last.rank + end + end + describe '.find_local' do before do Fabricate(:account, username: 'Alice') @@ -342,4 +421,24 @@ RSpec.describe Account, type: :model do end end end + + describe 'static avatars' do + describe 'when GIF' do + it 'creates a png static style' do + subject.avatar = attachment_fixture('avatar.gif') + subject.save + + expect(subject.avatar_static_url).to_not eq subject.avatar_original_url + end + end + + describe 'when non-GIF' do + it 'does not create extra static style' do + subject.avatar = attachment_fixture('attachment.jpg') + subject.save + + expect(subject.avatar_static_url).to eq subject.avatar_original_url + end + end + end end diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index 360bbc16d..7a5b8ec89 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -12,4 +12,15 @@ RSpec.describe Tag, type: :model do expect(subject.match('https://en.wikipedia.org/wiki/Ghostbusters_(song)#Lawsuit')).to be_nil end end + + describe '.search_for' do + it 'finds tag records with matching names' do + tag = Fabricate(:tag, name: "match") + _miss_tag = Fabricate(:tag, name: "miss") + + results = Tag.search_for("match") + + expect(results).to eq [tag] + end + end end diff --git a/spec/presenters/instance_presenter_spec.rb b/spec/presenters/instance_presenter_spec.rb new file mode 100644 index 000000000..0f318d9c3 --- /dev/null +++ b/spec/presenters/instance_presenter_spec.rb @@ -0,0 +1,74 @@ +require 'rails_helper' + +describe InstancePresenter do + let(:instance_presenter) { InstancePresenter.new } + + it "delegates site_description to Setting" do + Setting.site_description = "Site desc" + + expect(instance_presenter.site_description).to eq "Site desc" + end + + it "delegates site_extended_description to Setting" do + Setting.site_extended_description = "Extended desc" + + expect(instance_presenter.site_extended_description).to eq "Extended desc" + end + + it "delegates open_registrations to Setting" do + Setting.open_registrations = false + + expect(instance_presenter.open_registrations).to eq false + end + + it "delegates closed_registrations_message to Setting" do + Setting.closed_registrations_message = "Closed message" + + expect(instance_presenter.closed_registrations_message).to eq "Closed message" + end + + it "delegates contact_email to Setting" do + Setting.contact_email = "admin@example.com" + + expect(instance_presenter.contact_email).to eq "admin@example.com" + end + + describe "contact_account" do + it "returns the account for the site contact username" do + Setting.site_contact_username = "aaa" + account = Fabricate(:account, username: "aaa") + + expect(instance_presenter.contact_account).to eq(account) + end + end + + describe "user_count" do + it "returns the number of site users" do + cache = double + allow(Rails).to receive(:cache).and_return(cache) + allow(cache).to receive(:fetch).with("user_count").and_return(123) + + expect(instance_presenter.user_count).to eq(123) + end + end + + describe "status_count" do + it "returns the number of local statuses" do + cache = double + allow(Rails).to receive(:cache).and_return(cache) + allow(cache).to receive(:fetch).with("local_status_count").and_return(234) + + expect(instance_presenter.status_count).to eq(234) + end + end + + describe "domain_count" do + it "returns the number of known domains" do + cache = double + allow(Rails).to receive(:cache).and_return(cache) + allow(cache).to receive(:fetch).with("distinct_domain_count").and_return(345) + + expect(instance_presenter.domain_count).to eq(345) + end + end +end diff --git a/spec/requests/catch_all_route_request_spec.rb b/spec/requests/catch_all_route_request_spec.rb new file mode 100644 index 000000000..22ce1cf59 --- /dev/null +++ b/spec/requests/catch_all_route_request_spec.rb @@ -0,0 +1,21 @@ +require "rails_helper" + +describe "The catch all route" do + describe "with a simple value" do + it "returns a 404 page as html" do + get "/test" + + expect(response.status).to eq 404 + expect(response.content_type).to eq "text/html" + end + end + + describe "with an implied format" do + it "returns a 404 page as html" do + get "/test.test" + + expect(response.status).to eq 404 + expect(response.content_type).to eq "text/html" + end + end +end diff --git a/streaming/index.js b/streaming/index.js index 7edf6203f..a1e7eaca7 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -87,21 +87,24 @@ const setRequestId = (req, res, next) => { const accountFromToken = (token, req, next) => { pgPool.connect((err, client, done) => { if (err) { - return next(err) + next(err) + return } client.query('SELECT oauth_access_tokens.resource_owner_id, users.account_id FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 LIMIT 1', [token], (err, result) => { done() if (err) { - return next(err) + next(err) + return } if (result.rows.length === 0) { err = new Error('Invalid access token') err.statusCode = 401 - return next(err) + next(err) + return } req.accountId = result.rows[0].account_id @@ -113,7 +116,8 @@ const accountFromToken = (token, req, next) => { const authenticationMiddleware = (req, res, next) => { if (req.method === 'OPTIONS') { - return next() + next() + return } const authorization = req.get('Authorization') @@ -122,7 +126,8 @@ const authenticationMiddleware = (req, res, next) => { const err = new Error('Missing access token') err.statusCode = 401 - return next(err) + next(err) + return } const token = authorization.replace(/^Bearer /, '') diff --git a/yarn.lock b/yarn.lock index 6a3a36270..b83924ad9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -140,6 +140,12 @@ acorn-globals@^3.1.0: dependencies: acorn "^4.0.4" +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + dependencies: + acorn "^3.0.4" + acorn@^1.0.3: version "1.2.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-1.2.2.tgz#c8ce27de0acc76d896d2b1fad3df588d9e82f014" @@ -148,7 +154,7 @@ acorn@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-2.7.0.tgz#ab6e7d9d886aaca8b085bc3312b79a198433f0e7" -acorn@^3.0.0: +acorn@^3.0.0, acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" @@ -156,6 +162,10 @@ acorn@^4.0.3, acorn@^4.0.4: version "4.0.11" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.11.tgz#edcda3bd937e7556410d42ed5860f67399c794c0" +acorn@^5.0.1: + version "5.0.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.0.3.tgz#c460df08491463f028ccb82eab3730bf01087b3d" + airbnb-js-shims@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/airbnb-js-shims/-/airbnb-js-shims-1.0.1.tgz#7d5a7d772c8c6fdeb624ea3cef62506091b180b5" @@ -169,7 +179,7 @@ airbnb-js-shims@^1.0.1: string.prototype.padend "^3.0.0" string.prototype.padstart "^3.0.0" -ajv-keywords@^1.1.1: +ajv-keywords@^1.0.0, ajv-keywords@^1.1.1: version "1.5.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" @@ -196,6 +206,10 @@ amdefine@>=0.0.4: version "1.0.0" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.0.tgz#fd17474700cb5cc9c2b709f0be9d23ce3c198c33" +ansi-escapes@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + ansi-html@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" @@ -284,10 +298,27 @@ array-reduce@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + array-unique@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" +array.prototype.find@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.0.4.tgz#556a5c5362c08648323ddaeb9de9d14bc1864c90" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.7.0" + arrify@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -424,7 +455,7 @@ babel-code-frame@^6.11.0: esutils "^2.0.2" js-tokens "^2.0.0" -babel-code-frame@^6.22.0: +babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" dependencies: @@ -480,6 +511,15 @@ babel-core@^6.11.4: slash "^1.0.0" source-map "^0.5.0" +babel-eslint@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.1.tgz#079422eb73ba811e3ca0865ce87af29327f8c52f" + dependencies: + babel-code-frame "^6.22.0" + babel-traverse "^6.23.1" + babel-types "^6.23.0" + babylon "^6.16.1" + babel-generator@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.22.0.tgz#d642bf4961911a8adc7c692b0c9297f325cda805" @@ -1302,6 +1342,10 @@ babylon@^6.15.0: version "6.15.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e" +babylon@^6.16.1: + version "6.16.1" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3" + babylon@~5.8.3: version "5.8.38" resolved "https://registry.yarnpkg.com/babylon/-/babylon-5.8.38.tgz#ec9b120b11bf6ccd4173a18bf217e60b79859ffd" @@ -1586,6 +1630,16 @@ cached-path-relative@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.1.tgz#d09c4b52800aa4c078e2dd81a869aac90d2e54e7" +caller-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + dependencies: + callsites "^0.2.0" + +callsites@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + camelcase-keys@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" @@ -1639,7 +1693,7 @@ chai@^3.5.0: deep-eql "^0.1.3" type-detect "^1.0.0" -chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: +chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: @@ -1695,6 +1749,10 @@ cipher-base@^1.0.0, cipher-base@^1.0.1: dependencies: inherits "^2.0.1" +circular-json@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" + clap@^1.0.9: version "1.1.1" resolved "https://registry.yarnpkg.com/clap/-/clap-1.1.1.tgz#a8a93e0bfb7581ac199c4f001a5525a724ce696d" @@ -1705,6 +1763,16 @@ classnames@^2.1.2, classnames@^2.2.3, classnames@~2.2: version "2.2.5" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" +cli-cursor@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + dependencies: + restore-cursor "^1.0.1" + +cli-width@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" + cliui@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" @@ -1824,7 +1892,7 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@^1.4.7, concat-stream@~1.5.0, concat-stream@~1.5.1: +concat-stream@^1.4.7, concat-stream@^1.5.2, concat-stream@~1.5.0, concat-stream@~1.5.1: version "1.5.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" dependencies: @@ -2085,6 +2153,12 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" +d@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" + dependencies: + es5-ext "^0.10.9" + d@^0.1.1, d@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309" @@ -2140,6 +2214,18 @@ defined@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" +del@^2.0.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -2197,6 +2283,13 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" +doctrine@^1.2.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + doctrine@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63" @@ -2377,6 +2470,15 @@ es-abstract@^1.3.2, es-abstract@^1.4.3, es-abstract@^1.5.0, es-abstract@^1.5.1: is-callable "^1.1.3" is-regex "^1.0.3" +es-abstract@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.7.0.tgz#dfade774e01bfcd97f96180298c449c8623fb94c" + dependencies: + es-to-primitive "^1.1.1" + function-bind "^1.1.0" + is-callable "^1.1.3" + is-regex "^1.0.3" + es-to-primitive@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" @@ -2385,6 +2487,13 @@ es-to-primitive@^1.1.1: is-date-object "^1.0.1" is-symbol "^1.0.1" +es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14: + version "0.10.15" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.15.tgz#c330a5934c1ee21284a7c081a86e5fd937c91ea6" + dependencies: + es6-iterator "2" + es6-symbol "~3.1" + es5-ext@^0.10.7, es5-ext@~0.10.11, es5-ext@~0.10.2: version "0.10.12" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.12.tgz#aa84641d4db76b62abba5e45fd805ecbab140047" @@ -2404,10 +2513,39 @@ es6-iterator@2: es5-ext "^0.10.7" es6-symbol "3" +es6-iterator@^2.0.1, es6-iterator@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-symbol "^3.1" + +es6-map@^0.1.3: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-set "~0.1.5" + es6-symbol "~3.1.1" + event-emitter "~0.3.5" + es6-promise@^3.2.1: version "3.3.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" +es6-set@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-symbol "3.1.1" + event-emitter "~0.3.5" + es6-shim@^0.35.1: version "0.35.1" resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.1.tgz#a23524009005b031ab4a352ac196dfdfd1144ab7" @@ -2419,6 +2557,22 @@ es6-symbol@3, es6-symbol@^3.0.2, es6-symbol@~3.1: d "~0.1.1" es5-ext "~0.10.11" +es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + dependencies: + d "1" + es5-ext "~0.10.14" + +es6-weak-map@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-iterator "^2.0.1" + es6-symbol "^3.1.1" + escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -2438,6 +2592,72 @@ escodegen@^1.6.1: optionalDependencies: source-map "~0.2.0" +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-plugin-react@^6.10.3: + version "6.10.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-6.10.3.tgz#c5435beb06774e12c7db2f6abaddcbf900cd3f78" + dependencies: + array.prototype.find "^2.0.1" + doctrine "^1.2.2" + has "^1.0.1" + jsx-ast-utils "^1.3.4" + object.assign "^4.0.4" + +eslint@^3.19.0: + version "3.19.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc" + dependencies: + babel-code-frame "^6.16.0" + chalk "^1.1.3" + concat-stream "^1.5.2" + debug "^2.1.1" + doctrine "^2.0.0" + escope "^3.6.0" + espree "^3.4.0" + esquery "^1.0.0" + estraverse "^4.2.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + glob "^7.0.3" + globals "^9.14.0" + ignore "^3.2.0" + imurmurhash "^0.1.4" + inquirer "^0.12.0" + is-my-json-valid "^2.10.0" + is-resolvable "^1.0.0" + js-yaml "^3.5.1" + json-stable-stringify "^1.0.0" + levn "^0.3.0" + lodash "^4.0.0" + mkdirp "^0.5.0" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.1" + pluralize "^1.2.1" + progress "^1.1.8" + require-uncached "^1.0.2" + shelljs "^0.7.5" + strip-bom "^3.0.0" + strip-json-comments "~2.0.1" + table "^3.7.8" + text-table "~0.2.0" + user-home "^2.0.0" + +espree@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.4.1.tgz#28a83ab4aaed71ed8fe0f5efe61b76a05c13c4d2" + dependencies: + acorn "^5.0.1" + acorn-jsx "^3.0.0" + esprima@^2.6.0, esprima@^2.7.1: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" @@ -2446,10 +2666,31 @@ esprima@~3.1.0: version "3.1.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" +esquery@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa" + dependencies: + estraverse "^4.0.0" + +esrecurse@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220" + dependencies: + estraverse "~4.1.0" + object-assign "^4.0.1" + estraverse@^1.9.1: version "1.9.3" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" +estraverse@^4.0.0, estraverse@^4.1.1, estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +estraverse@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2" + esutils@^2.0.0, esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" @@ -2458,6 +2699,13 @@ etag@~1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" +event-emitter@~0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + dependencies: + d "1" + es5-ext "~0.10.14" + events@^1.0.0, events@^1.1.1, events@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" @@ -2478,6 +2726,10 @@ exenv@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.0.tgz#3835f127abf075bfe082d0aed4484057c78e3c89" +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + expand-brackets@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" @@ -2559,6 +2811,20 @@ fbjs@^0.8.1, fbjs@^0.8.4: promise "^7.1.1" ua-parser-js "^0.7.9" +figures@^1.3.5: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + file-loader@^0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.9.0.tgz#1d2daddd424ce6d1b07cfe3f79731bed3617ab42" @@ -2604,6 +2870,15 @@ find-up@^1.0.0: path-exists "^2.0.0" pinkie-promise "^2.0.0" +flat-cache@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96" + dependencies: + circular-json "^0.3.1" + del "^2.0.2" + graceful-fs "^4.1.2" + write "^0.2.1" + flatten@1.0.2, flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" @@ -2815,10 +3090,21 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.0, glob@~7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -globals@^9.0.0: +globals@^9.0.0, globals@^9.14.0: version "9.14.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034" +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + globule@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/globule/-/globule-1.1.0.tgz#c49352e4dc183d85893ee825385eb994bb6df45f" @@ -2986,6 +3272,10 @@ ieee754@^1.1.4: version "1.1.8" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" +ignore@^3.2.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.7.tgz#4810ca5f1d8eca5595213a34b94f2eb4ed926bbd" + immutable@^3.7.6, immutable@^3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.1.tgz#200807f11ab0f72710ea485542de088075f68cd2" @@ -3037,6 +3327,24 @@ inline-source-map@~0.6.0: dependencies: source-map "~0.5.3" +inquirer@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" + dependencies: + ansi-escapes "^1.1.0" + ansi-regex "^2.0.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + figures "^1.3.5" + lodash "^4.3.0" + readline2 "^1.0.1" + run-async "^0.1.0" + rx-lite "^3.1.2" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + insert-module-globals@^7.0.0: version "7.0.1" resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.0.1.tgz#c03bf4e01cb086d5b5e5ace8ad0afe7889d638c3" @@ -3162,13 +3470,17 @@ is-fullwidth-code-point@^1.0.0: dependencies: number-is-nan "^1.0.0" +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + is-glob@^2.0.0, is-glob@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" dependencies: is-extglob "^1.0.0" -is-my-json-valid@^2.12.4: +is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: version "2.15.0" resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz#936edda3ca3c211fd98f3b2d3e08da43f7b2915b" dependencies: @@ -3187,6 +3499,22 @@ is-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" + dependencies: + path-is-inside "^1.0.1" + is-plain-obj@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" @@ -3217,6 +3545,12 @@ is-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" +is-resolvable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62" + dependencies: + tryit "^1.0.1" + is-stream@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -3298,7 +3632,7 @@ js-tokens@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" -js-yaml@^3.4.3, js-yaml@~3.6.1: +js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@~3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.6.1.tgz#6e5fe67d8b205ce4d22fad05b7781e8dadcc4b30" dependencies: @@ -3349,7 +3683,7 @@ json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" -json-stable-stringify@^1.0.1: +json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" dependencies: @@ -3397,6 +3731,12 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.3.6" +jsx-ast-utils@^1.3.4: + version "1.4.0" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.0.tgz#5afe38868f56bc8cc7aeaef0100ba8c75bd12591" + dependencies: + object-assign "^4.1.0" + keycode@^2.1.1: version "2.1.7" resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.7.tgz#7b9255919f6cff562b09a064d222dca70b020f5c" @@ -3435,7 +3775,7 @@ lcid@^1.0.0: dependencies: invert-kv "^1.0.0" -levn@~0.3.0: +levn@^0.3.0, levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" dependencies: @@ -3634,7 +3974,7 @@ lodash.tail@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" -lodash@4.x.x, lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.1: +lodash@4.x.x, lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.1, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -3865,10 +4205,18 @@ ms@0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" +mute-stream@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" + nan@^2.3.0, nan@^2.3.2, nan@~2.5.0: version "2.5.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2" +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" @@ -4156,6 +4504,10 @@ once@~1.3.0, once@~1.3.3: dependencies: wrappy "1" +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + optimist@~0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" @@ -4163,7 +4515,7 @@ optimist@~0.6.0: minimist "~0.0.1" wordwrap "~0.0.2" -optionator@^0.8.1: +optionator@^0.8.1, optionator@^0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" dependencies: @@ -4284,6 +4636,10 @@ path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" +path-is-inside@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + path-platform@~0.11.15: version "0.11.15" resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" @@ -4373,6 +4729,10 @@ pkg-dir@^1.0.0: dependencies: find-up "^1.0.0" +pluralize@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" + podda@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/podda/-/podda-1.2.2.tgz#15b0edbd334ade145813343f5ecf9c10a71cf500" @@ -4718,6 +5078,10 @@ process@^0.11.0, process@~0.11.0: version "0.11.9" resolved "https://registry.yarnpkg.com/process/-/process-0.11.9.tgz#7bd5ad21aa6253e7da8682264f1e11d11c0318c1" +progress@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + promise@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/promise/-/promise-7.1.1.tgz#489654c692616b8aa55b0724fa809bb7db49c5bf" @@ -5140,6 +5504,14 @@ readdirp@^2.0.0: readable-stream "^2.0.2" set-immediate-shim "^1.0.1" +readline2@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + mute-stream "0.0.5" + recast@^0.11.5: version "0.11.22" resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.22.tgz#dedeb18fb001a2bbc6ac34475fda53dfe3d47dfa" @@ -5341,6 +5713,13 @@ require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" +require-uncached@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + requires-port@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -5349,17 +5728,28 @@ reselect@^2.5.4: version "2.5.4" resolved "https://registry.yarnpkg.com/reselect/-/reselect-2.5.4.tgz#b7d23fdf00b83fa7ad0279546f8dbbbd765c7047" +resolve-from@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + resolve@1.1.7, resolve@^1.1.3, resolve@^1.1.4, resolve@^1.1.6: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + right-align@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" dependencies: align-text "^0.1.1" -rimraf@2, rimraf@~2.5.0, rimraf@~2.5.1: +rimraf@2, rimraf@^2.2.8, rimraf@~2.5.0, rimraf@~2.5.1: version "2.5.4" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" dependencies: @@ -5373,6 +5763,16 @@ ripemd160@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-1.0.1.tgz#93a4bbd4942bc574b69a8fa57c71de10ecca7d6e" +run-async@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" + dependencies: + once "^1.3.0" + +rx-lite@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" + samsam@1.1.2, samsam@~1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" @@ -5523,6 +5923,14 @@ shelljs@^0.7.4: interpret "^1.0.0" rechoir "^0.6.2" +shelljs@^0.7.5: + version "0.7.7" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.7.tgz#b2f5c77ef97148f4b4f6e22682e10bba8667cff1" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + signal-exit@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.1.tgz#5a4c884992b63a7acd9badb7894c3ee9cfccad81" @@ -5548,6 +5956,10 @@ slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + slide@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" @@ -5692,6 +6104,13 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +string-width@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^3.0.0" + string.prototype.padend@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0" @@ -5735,6 +6154,10 @@ strip-bom@^2.0.0: dependencies: is-utf8 "^0.2.0" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + strip-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" @@ -5745,6 +6168,10 @@ strip-json-comments@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + style-loader@0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.13.1.tgz#468280efbc0473023cd3a6cd56e33b5a1d7fc3a9" @@ -5809,6 +6236,17 @@ syntax-error@^1.1.1: dependencies: acorn "^2.7.0" +table@^3.7.8: + version "3.8.3" + resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" + dependencies: + ajv "^4.7.0" + ajv-keywords "^1.0.0" + chalk "^1.1.1" + lodash "^4.0.0" + slice-ansi "0.0.4" + string-width "^2.0.0" + tapable@^0.1.8, tapable@~0.1.8: version "0.1.10" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4" @@ -5856,6 +6294,10 @@ tar@^2.0.0, tar@~2.2.0, tar@~2.2.1: fstream "^1.0.2" inherits "2" +text-table@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + through2@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.1.tgz#384e75314d49f32de12eebb8136b8eb6b5d59da9" @@ -5863,7 +6305,7 @@ through2@^2.0.0: readable-stream "~2.0.0" xtend "~4.0.0" -through@2, "through@>=2.2.7 <3": +through@2, "through@>=2.2.7 <3", through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -5909,6 +6351,10 @@ trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" +tryit@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" + tty-browserify@0.0.0, tty-browserify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" @@ -6022,6 +6468,12 @@ user-home@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" +user-home@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" + dependencies: + os-homedir "^1.0.0" + utf-8-validate@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-3.0.1.tgz#5d2b8656b4ddcfded47217b647a98941b63cf213" @@ -6280,6 +6732,12 @@ write-file-atomic@^1.1.2: imurmurhash "^0.1.4" slide "^1.1.5" +write@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + dependencies: + mkdirp "^0.5.1" + ws@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ws/-/ws-2.1.0.tgz#b24eaed9609f8632dd51e3f7698619a90fddcc92" |