diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..5f8702cf --- /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 fbb28470..97bba5e3 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 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..6d540c41 --- /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 2bf1c1cc..005119ba 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.3.1 +2.4.1 diff --git a/.travis.yml b/.travis.yml index b1b0c2bc..a9824ccf 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 57a8f34e..a05525b3 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 63e90c5a..9e51928e 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' @@ -32,6 +32,7 @@ gem 'htmlentities' gem 'http' gem 'http_accept_language' gem 'httplog' +gem 'kaminari' gem 'link_header' gem 'nokogiri' gem 'oj' @@ -52,7 +53,6 @@ gem 'simple_form' gem 'statsd-instrument' gem 'twitter-text' gem 'tzinfo-data' -gem 'will_paginate' gem 'react-rails' gem 'browserify-rails' @@ -71,6 +71,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 2ebfdf9a..5edc75dd 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) @@ -186,10 +184,10 @@ GEM 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) @@ -197,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) @@ -231,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) @@ -251,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) @@ -288,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) @@ -306,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) @@ -349,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) @@ -357,27 +366,27 @@ 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) @@ -385,20 +394,20 @@ GEM sidekiq-skylight (0.2.0) sidekiq (>= 3.3.0) skylight (>= 0.5.2) - 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) simplecov-html (0.10.0) - skylight (1.1.0) + skylight (1.2.0) activesupport (>= 3.0.0) slop (3.6.0) sprockets (3.7.1) @@ -408,43 +417,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 @@ -483,6 +488,7 @@ DEPENDENCIES httplog i18n-tasks (~> 0.9.6) jquery-rails + kaminari letter_opener letter_opener_web link_header @@ -502,6 +508,7 @@ DEPENDENCIES rack-cors rack-timeout rails (~> 5.0.2) + rails-controller-testing rails-settings-cached rails_12factor react-rails @@ -525,10 +532,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.5 + 1.14.6 diff --git a/README.md b/README.md index fa944a90..37065f9d 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,11 @@ If you would like, you can [support the development of this project on Patreon][ ## Resources -- [List of Mastodon instances](docs/Using-Mastodon/List-of-Mastodon-instances.md) +- [List of Mastodon instances](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md) - [Use this tool to find Twitter friends on Mastodon](https://mastodon-bridge.herokuapp.com) -- [API overview](docs/Using-the-API/API.md) -- [Frequently Asked Questions](docs/Using-Mastodon/FAQ.md) -- [List of apps](docs/Using-Mastodon/Apps.md) +- [API overview](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md) +- [Frequently Asked Questions](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md) +- [List of apps](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md) ## Features @@ -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 @@ -117,25 +117,25 @@ Which will re-create the updated containers, leaving databases and data as is. D ## Deployment without Docker -Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](docs/Running-Mastodon/Production-guide.md) for examples, configuration and instructions. +Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md) for examples, configuration and instructions. ## Deployment on Scalingo [![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/tootsuite/mastodon#master) -[You can view a guide for deployment on Scalingo here.](docs/Running-Mastodon/Scalingo-guide.md) +[You can view a guide for deployment on Scalingo here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Scalingo-guide.md) ## Deployment on Heroku (experimental) [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) -Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.com) app. [You can view a guide for deployment on Heroku here.](docs/Running-Mastodon/Heroku-guide.md) +Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.com) app. [You can view a guide for deployment on Heroku here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Heroku-guide.md) ## Development with Vagrant A quick way to get a development environment up and running is with Vagrant. You will need recent versions of [Vagrant](https://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) installed. -[You can find the guide for setting up a Vagrant development environment here.](docs/Running-Mastodon/Vagrant-guide.md) +[You can find the guide for setting up a Vagrant development environment here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Vagrant-guide.md) ## Contributing diff --git a/Vagrantfile b/Vagrantfile index cd7f7447..90f60464 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 29c1f9f9..6c4294c7 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/images/background-photo.jpeg b/app/assets/images/background-photo.jpeg index d7937fd4..03341b8e 100644 Binary files a/app/assets/images/background-photo.jpeg and b/app/assets/images/background-photo.jpeg differ diff --git a/app/assets/javascripts/components/actions/notifications.jsx b/app/assets/javascripts/components/actions/notifications.jsx index 980b7d63..b09ca085 100644 --- a/app/assets/javascripts/components/actions/notifications.jsx +++ b/app/assets/javascripts/components/actions/notifications.jsx @@ -50,6 +50,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) { }; }; +const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); + export function refreshNotifications() { return (dispatch, getState) => { dispatch(refreshNotificationsRequest()); @@ -61,6 +63,8 @@ export function refreshNotifications() { params.since_id = ids.first().get('id'); } + params.exclude_types = excludeTypesFromSettings(getState()); + api(getState).get('/api/v1/notifications', { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); @@ -105,11 +109,11 @@ export function expandNotifications() { dispatch(expandNotificationsRequest()); - api(getState).get(url, { - params: { - limit: 5 - } - }).then(response => { + const params = {}; + + params.exclude_types = excludeTypesFromSettings(getState()); + + 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/api.jsx b/app/assets/javascripts/components/api.jsx index 93cfc804..185729ce 100644 --- a/app/assets/javascripts/components/api.jsx +++ b/app/assets/javascripts/components/api.jsx @@ -1,5 +1,5 @@ import axios from 'axios'; -import LinkHeader from 'http-link-header'; +import LinkHeader from './link_header'; export const getLinks = response => { const value = response.headers.link; diff --git a/app/assets/javascripts/components/components/account.jsx b/app/assets/javascripts/components/components/account.jsx index 7a1c9f5c..8ce9b192 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({
-
+
diff --git a/app/assets/javascripts/components/components/avatar.jsx b/app/assets/javascripts/components/components/avatar.jsx index 0237a190..673b1a24 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 ( -
- -
- ); + 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 ( -
- - -
+
); } diff --git a/app/assets/javascripts/components/components/icon_button.jsx b/app/assets/javascripts/components/components/icon_button.jsx index a08b1159..33835f9a 100644 --- a/app/assets/javascripts/components/components/icon_button.jsx +++ b/app/assets/javascripts/components/components/icon_button.jsx @@ -31,7 +31,7 @@ const IconButton = React.createClass({ e.preventDefault(); if (!this.props.disabled) { - this.props.onClick(); + this.props.onClick(e); } }, diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx index 110d26c6..c4d5f829 100644 --- a/app/assets/javascripts/components/components/status.jsx +++ b/app/assets/javascripts/components/components/status.jsx @@ -27,6 +27,7 @@ const Status = React.createClass({ onOpenMedia: React.PropTypes.func, onBlock: React.PropTypes.func, me: React.PropTypes.number, + boostModal: React.PropTypes.bool, muted: React.PropTypes.bool }, @@ -90,7 +91,7 @@ const Status = React.createClass({
- +
diff --git a/app/assets/javascripts/components/components/status_action_bar.jsx b/app/assets/javascripts/components/components/status_action_bar.jsx index 4ebb76ea..02424e77 100644 --- a/app/assets/javascripts/components/components/status_action_bar.jsx +++ b/app/assets/javascripts/components/components/status_action_bar.jsx @@ -46,8 +46,8 @@ const StatusActionBar = React.createClass({ this.props.onFavourite(this.props.status); }, - handleReblogClick () { - this.props.onReblog(this.props.status); + handleReblogClick (e) { + this.props.onReblog(this.props.status, e); }, handleDeleteClick () { diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx index 6c25afde..9cf03bb3 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({ ); - } else { + } else if (this.props.onClick) { return (
); + } else { + return ( +
+ ); } }, diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index 00f20074..d48bb2ba 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -48,6 +48,9 @@ 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 ja from 'react-intl/locale-data/ja'; + import getMessagesForLocale from '../locales'; import { hydrateStore } from '../actions/store'; import createStream from '../stream'; @@ -60,7 +63,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, ...ja]); + const Mastodon = React.createClass({ diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx index fd3fbe4c..f92c1cdf 100644 --- a/app/assets/javascripts/components/containers/status_container.jsx +++ b/app/assets/javascripts/components/containers/status_container.jsx @@ -26,7 +26,8 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, props) => ({ status: getStatus(state, props.id), - me: state.getIn(['meta', 'me']) + me: state.getIn(['meta', 'me']), + boostModal: state.getIn(['meta', 'boost_modal']) }); return mapStateToProps; @@ -38,11 +39,19 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(replyCompose(status, router)); }, - onReblog (status) { + onModalReblog (status) { + dispatch(reblog(status)); + }, + + onReblog (status, e) { if (status.get('reblogged')) { dispatch(unreblog(status)); } else { - dispatch(reblog(status)); + if (e.altKey || !this.boostModal) { + this.onModalReblog(status); + } else { + dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); + } } }, 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 5591b45c..2d21f343 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 }) => (
-
+
); 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 b016d3f2..cb4b62f6 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 1920b29b..36e97df4 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 @@ -46,8 +46,8 @@ const EmojiPickerDropdown = React.createClass({ 🙂 - - + + ); 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 076ac7cb..1a748a23 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 (
- + 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 1766655c..1939eba6 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 }) => {
-
+
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 71877fb2..debbfd01 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 ( -
+
); diff --git a/app/assets/javascripts/components/features/notifications/components/notification.jsx b/app/assets/javascripts/components/features/notifications/components/notification.jsx index 0de4df52..fdebe4bb 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 ( -
+
@@ -41,7 +41,7 @@ const Notification = React.createClass({ renderFavourite (notification, link) { return ( -
+
@@ -57,7 +57,7 @@ const Notification = React.createClass({ renderReblog (notification, link) { return ( -
+
@@ -76,17 +76,17 @@ 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 = ; + const link = ; switch(notification.get('type')) { - case 'follow': - return this.renderFollow(account, link); - case 'mention': - return this.renderMention(notification); - case 'favourite': - return this.renderFavourite(notification, link); - case 'reblog': - return this.renderReblog(notification, link); + case 'follow': + return this.renderFollow(account, link); + case 'mention': + return this.renderMention(notification); + case 'favourite': + return this.renderFavourite(notification, link); + case 'reblog': + return this.renderReblog(notification, link); } } diff --git a/app/assets/javascripts/components/features/status/components/action_bar.jsx b/app/assets/javascripts/components/features/status/components/action_bar.jsx index 2aebcd70..fdcb8b98 100644 --- a/app/assets/javascripts/components/features/status/components/action_bar.jsx +++ b/app/assets/javascripts/components/features/status/components/action_bar.jsx @@ -37,8 +37,8 @@ const ActionBar = React.createClass({ this.props.onReply(this.props.status); }, - handleReblogClick () { - this.props.onReblog(this.props.status); + handleReblogClick (e) { + this.props.onReblog(this.props.status, e); }, handleFavouriteClick () { 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 caa46ff3..2da57252 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 (
-
+
diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx index f98fe1b0..91302bc3 100644 --- a/app/assets/javascripts/components/features/status/index.jsx +++ b/app/assets/javascripts/components/features/status/index.jsx @@ -38,7 +38,8 @@ const makeMapStateToProps = () => { status: getStatus(state, Number(props.params.statusId)), ancestorsIds: state.getIn(['timelines', 'ancestors', Number(props.params.statusId)]), descendantsIds: state.getIn(['timelines', 'descendants', Number(props.params.statusId)]), - me: state.getIn(['meta', 'me']) + me: state.getIn(['meta', 'me']), + boostModal: state.getIn(['meta', 'boost_modal']) }); return mapStateToProps; @@ -55,7 +56,8 @@ const Status = React.createClass({ status: ImmutablePropTypes.map, ancestorsIds: ImmutablePropTypes.list, descendantsIds: ImmutablePropTypes.list, - me: React.PropTypes.number + me: React.PropTypes.number, + boostModal: React.PropTypes.bool }, mixins: [PureRenderMixin], @@ -82,11 +84,19 @@ const Status = React.createClass({ this.props.dispatch(replyCompose(status, this.context.router)); }, - handleReblogClick (status) { + handleModalReblog (status) { + this.props.dispatch(reblog(status)); + }, + + handleReblogClick (status, e) { if (status.get('reblogged')) { this.props.dispatch(unreblog(status)); } else { - this.props.dispatch(reblog(status)); + if (e.altKey || !this.props.boostModal) { + this.handleModalReblog(status); + } else { + this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog })); + } } }, diff --git a/app/assets/javascripts/components/features/ui/components/boost_modal.jsx b/app/assets/javascripts/components/features/ui/components/boost_modal.jsx new file mode 100644 index 00000000..023abc6a --- /dev/null +++ b/app/assets/javascripts/components/features/ui/components/boost_modal.jsx @@ -0,0 +1,77 @@ +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import IconButton from '../../../components/icon_button'; +import Button from '../../../components/button'; +import StatusContent from '../../../components/status_content'; +import Avatar from '../../../components/avatar'; +import RelativeTimestamp from '../../../components/relative_timestamp'; +import DisplayName from '../../../components/display_name'; + +const messages = defineMessages({ + reblog: { id: 'status.reblog', defaultMessage: 'Boost' } +}); + +const BoostModal = React.createClass({ + contextTypes: { + router: React.PropTypes.object + }, + + propTypes: { + status: ImmutablePropTypes.map.isRequired, + onReblog: React.PropTypes.func.isRequired, + onClose: React.PropTypes.func.isRequired, + intl: React.PropTypes.object.isRequired + }, + + mixins: [PureRenderMixin], + + handleReblog() { + this.props.onReblog(this.props.status); + this.props.onClose(); + }, + + handleAccountClick (e) { + if (e.button === 0) { + e.preventDefault(); + this.props.onClose(); + this.context.router.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); + } + }, + + render () { + const { status, intl, onClose } = this.props; + + return ( +
+
+
+
+
+ +
+ + +
+ +
+ + +
+
+ + +
+
+ +
+
Alt + }} />
+
+
+ ); + } + +}); + +export default injectIntl(BoostModal); diff --git a/app/assets/javascripts/components/features/ui/components/column.jsx b/app/assets/javascripts/components/features/ui/components/column.jsx index 2b7e11bf..057b1a9a 100644 --- a/app/assets/javascripts/components/features/ui/components/column.jsx +++ b/app/assets/javascripts/components/features/ui/components/column.jsx @@ -41,8 +41,11 @@ const Column = React.createClass({ mixins: [PureRenderMixin], handleHeaderClick () { - let node = ReactDOM.findDOMNode(this); - this._interruptScrollAnimation = scrollTop(node.querySelector('.scrollable')); + const scrollable = ReactDOM.findDOMNode(this).querySelector('.scrollable'); + if (!scrollable) { + return; + } + this._interruptScrollAnimation = scrollTop(scrollable); }, handleWheel () { diff --git a/app/assets/javascripts/components/features/ui/components/modal_root.jsx b/app/assets/javascripts/components/features/ui/components/modal_root.jsx index d2ae5e14..e7ac02dd 100644 --- a/app/assets/javascripts/components/features/ui/components/modal_root.jsx +++ b/app/assets/javascripts/components/features/ui/components/modal_root.jsx @@ -1,9 +1,11 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; import MediaModal from './media_modal'; +import BoostModal from './boost_modal'; import { TransitionMotion, spring } from 'react-motion'; const MODAL_COMPONENTS = { - 'MEDIA': MediaModal + 'MEDIA': MediaModal, + 'BOOST': BoostModal }; const ModalRoot = React.createClass({ diff --git a/app/assets/javascripts/components/link_header.jsx b/app/assets/javascripts/components/link_header.jsx new file mode 100644 index 00000000..9a9ff7e7 --- /dev/null +++ b/app/assets/javascripts/components/link_header.jsx @@ -0,0 +1,33 @@ +import Link from 'http-link-header'; +import querystring from 'querystring'; + +Link.parseAttrs = (link, parts) => { + let match = null + let attr = '' + let value = '' + let attrs = '' + + let uriAttrs = /<(.*)>;\s*(.*)/gi.exec(parts) + + if(uriAttrs) { + attrs = uriAttrs[2] + link = Link.parseParams(link, uriAttrs[1]) + } + + while(match = Link.attrPattern.exec(attrs)) { + attr = match[1].toLowerCase() + value = match[4] || match[3] || match[2] + + if( /\*$/.test(attr)) { + Link.setAttr(link, attr, Link.parseExtendedValue(value)) + } else if(/%/.test(value)) { + Link.setAttr(link, attr, querystring.decode(value)) + } else { + Link.setAttr(link, attr, value) + } + } + + return link +}; + +export default Link; diff --git a/app/assets/javascripts/components/locales/fr.jsx b/app/assets/javascripts/components/locales/fr.jsx index 568422ff..9dff8f2b 100644 --- a/app/assets/javascripts/components/locales/fr.jsx +++ b/app/assets/javascripts/components/locales/fr.jsx @@ -12,10 +12,12 @@ const fr = { "status.sensitive_toggle": "Cliquer pour dévoiler", "status.show_more": "Déplier", "status.show_less": "Replier", - "status.open": "Déplier ce status", + "status.open": "Déplier ce statut", "status.report": "Signaler @{name}", "status.load_more": "Charger plus", + "status.media_hidden": "Média caché", "video_player.toggle_sound": "Mettre/Couper le son", + "video_player.toggle_visible": "Afficher/Cacher la vidéo", "account.mention": "Mentionner", "account.edit_profile": "Modifier le profil", "account.unblock": "Débloquer", @@ -42,16 +44,25 @@ const fr = { "column.notifications": "Notifications", "column.blocks": "Utilisateurs bloqués", "column.favourites": "Favoris", + "column.follow_requests": "Demandes de suivi", "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateurs⋅trices pour débuter la conversation.", + "empty_column.public": "Il n'y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateurs d'autres instances pour remplir le fil public.", + "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d'autres utilisateurs.", + "empty_column.home.public_timeline": "le fil public", + "empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !", + "empty_column.hashtag": "Il n'y a encore aucun contenu relatif à ce hashtag", "tabs_bar.compose": "Composer", "tabs_bar.home": "Accueil", "tabs_bar.mentions": "Mentions", "tabs_bar.public": "Fil public global", "tabs_bar.notifications": "Notifications", + "tabs_bar.local_timeline": "Fil public local", + "tabs_bar.federated_timeline": "Fil public global", "compose_form.placeholder": "Qu’avez-vous en tête ?", "compose_form.publish": "Pouet", "compose_form.sensitive": "Marquer le média comme délicat", "compose_form.spoiler": "Masquer le texte derrière un avertissement", + "compose_form.spoiler_placeholder": "Avertissement", "compose_form.private": "Rendre privé", "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodons. Si {domains} {domainsCount, plural, one {n'est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n'y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d'une autre manière à d'autres personnes imprévues", "compose_form.unlisted": "Ne pas afficher dans les fils publics", @@ -64,23 +75,31 @@ const fr = { "navigation_bar.favourites": "Favoris", "navigation_bar.info": "Plus d'informations", "navigation_bar.logout": "Déconnexion", + "navigation_bar.follow_requests": "Demandes de suivi", "reply_indicator.cancel": "Annuler", - "search.placeholder": "Chercher", + "search.placeholder": "Rechercher", "search.account": "Compte", "search.hashtag": "Mot-clé", "search_results.total": "{count} {count, plural, one {résultat} other {résultats}}", + "search.status_by": "Statuts de {name}", "upload_button.label": "Joindre un média", "upload_form.undo": "Annuler", + "upload_progress.label": "Envoi en cours…", + "upload_area.title": "Glissez et déposez pour envoyer", "notification.follow": "{name} vous suit.", "notification.favourite": "{name} a ajouté à ses favoris :", "notification.reblog": "{name} a partagé votre statut :", "notification.mention": "{name} vous a mentionné⋅e :", "notifications.column_settings.alert": "Notifications locales", "notifications.column_settings.show": "Afficher dans la colonne", + "notifications.column_settings.sound": "Émettre un son", "notifications.column_settings.follow": "Nouveaux abonnés :", "notifications.column_settings.favourite": "Favoris :", "notifications.column_settings.mention": "Mentions :", "notifications.column_settings.reblog": "Partages :", + "notifications.clear": "Nettoyer", + "notifications.clear_confirmation": "Voulez-vous vraiment nettoyer toutes vos notifications ?", + "notifications.settings": "Paramètres de la colonne", "privacy.public.short": "Public", "privacy.public.long": "Afficher dans les fils publics", "privacy.unlisted.short": "Non-listé", @@ -90,6 +109,20 @@ const fr = { "privacy.direct.short": "Direct", "privacy.direct.long": "N’afficher que pour les personnes mentionné⋅e⋅s", "privacy.change": "Ajuster la confidentialité du message", + "media_gallery.toggle_visible": "Modifier la visibilité", + "missing_indicator.label": "Non trouvé", + "follow_request.authorize": "Autoriser", + "follow_request.reject": "Rejeter", + "home.settings": "Paramètres de la colonne", + "home.column_settings.basic": "Basique", + "home.column_settings.show_reblogs": "Afficher les partages", + "home.column_settings.show_replies": "Afficher les réponses", + "home.column_settings.advanced": "Avancé", + "home.column_settings.filter_regex": "Filtrer avec une expression rationnelle", + "report.heading": "Nouveau signalement", + "report.placeholder": "Commentaires additionnels", + "report.submit": "Envoyer", + "report.target": "Signalement" }; export default fr; diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx index 1e7b8b54..da85240b 100644 --- a/app/assets/javascripts/components/locales/index.jsx +++ b/app/assets/javascripts/components/locales/index.jsx @@ -7,6 +7,9 @@ import pt from './pt'; import uk from './uk'; import fi from './fi'; import eo from './eo'; +import ru from './ru'; +import ja from './ja'; + const locales = { en, @@ -17,7 +20,10 @@ const locales = { pt, uk, fi, - eo + eo, + ru, + ja + }; export default function getMessagesForLocale (locale) { diff --git a/app/assets/javascripts/components/locales/ja.jsx b/app/assets/javascripts/components/locales/ja.jsx new file mode 100644 index 00000000..ac451203 --- /dev/null +++ b/app/assets/javascripts/components/locales/ja.jsx @@ -0,0 +1,72 @@ +const ja = { + "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": "見るにはクリック", + "status.show_more": "もっと見る", + "status.show_less": "隠す", + "status.open": "Expand this status", + "status.report": "@{name}さんを報告", + "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": "ドメインとユーザー名を知っているなら検索フォームに入力すればフォローできます。", + "getting_started.about_shortcuts": "対象のアカウントがあなたと同じドメインのユーザーならばユーザー名のみで検索できます。これは返信のときも一緒です。", + "getting_started.open_source_notice": "Mastodon はオープンソースのソフトウェアです。誰でもGitHub({github})から開発に参加したり、問題を報告したりできます。 {apps}", + "column.home": "ホーム", + "column.community": "ローカルタイムライン", + "column.public": "連邦タイムライン", + "column.notifications": "通知", + "tabs_bar.compose": "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": "あなたの非公開トゥートは返信先のユーザー(at {domains})に公開されます。{domainsCount, plural, one {that server} other {those servers}}を信頼しますか?投稿のプライバシー保護はMastodonサーバー内でのみ有効です。 もし{domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}ならばあなたの投稿のプライバシーは保護されず、ブーストされたり予期しないユーザーに見られる可能性があります。", + "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 ja; diff --git a/app/assets/javascripts/components/locales/ru.jsx b/app/assets/javascripts/components/locales/ru.jsx new file mode 100644 index 00000000..e109005a --- /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 b3ae3350..407e917b 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; @@ -148,7 +149,7 @@ order: 1; } - @media screen and (max-width: 360px) { + @media screen and (max-width: 480px) { .details { display: block; } @@ -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/boost.scss b/app/assets/stylesheets/boost.scss index a2e6421f..6688f90f 100644 --- a/app/assets/stylesheets/boost.scss +++ b/app/assets/stylesheets/boost.scss @@ -2,6 +2,6 @@ @return '%23' + str-slice('#{$colour}', 2, -1) } -button i.fa-retweet { +button.icon-button i.fa-retweet { background-image: url("data:image/svg+xml;utf8,"); } diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index 9aead00b..8e4a667e 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; } @@ -325,6 +349,43 @@ a.status__content__spoiler-link { .status__display-name { color: lighten($color1, 26%); } + + &.light { + .status__relative-time { + color: $color3; + } + + .status__display-name { + color: $color1; + } + + .display-name { + strong { + color: $color1; + } + + span { + color: $color3; + } + } + + .status__content { + color: $color1; + + a { + color: $color4; + } + + a.status__content__spoiler-link { + color: $color5; + background: $color3; + + &:hover { + background: lighten($color3, 8%); + } + } + } + } } .status-check-box { @@ -643,6 +704,12 @@ a.status__content__spoiler-link { left: 8px; } + &.light { + &:before { + border-color: transparent transparent $color5 transparent; + } + } + & > ul { list-style: none; background: $color2; @@ -660,7 +727,7 @@ a.status__content__spoiler-link { } & > .emoji-dialog { - left: -249px; + left: -210px; } } @@ -714,15 +781,7 @@ a.status__content__spoiler-link { @media screen and (min-width: 360px) { .columns-area { - margin: 0; - } - - .column:first-child, .drawer:first-child { - margin-left: 0; - } - - .column:last-child, .drawer:last-child { - margin-right: 0; + padding: 10px; } } @@ -730,9 +789,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 { @@ -764,6 +826,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; @@ -823,38 +937,6 @@ a.status__content__spoiler-link { } } -.column, .drawer { - margin: 10px; - margin-left: 5px; - margin-right: 5px; - flex: 0 0 auto; - overflow: hidden; -} - -.column:first-child, .drawer:first-child { - margin-left: 10px; -} - -.column:last-child, .drawer:last-child { - margin-right: 10px; -} - -@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%); @@ -865,17 +947,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 { @@ -889,37 +972,13 @@ a.status__content__spoiler-link { } span { + margin-left: 5px; display: none; } } -@media screen and (min-width: 360px) { - .columns-area { - margin: 10px; - } - - .tabs-bar { - margin: 10px; - margin-bottom: 0; - } - - .search { - margin-bottom: 10px; - } -} - -@media screen and (min-width: 1024px) { - .columns-area { - margin: 0; - } -} - @media screen and (min-width: 600px) { .tabs-bar__link { - .fa { - margin-right: 5px; - } - span { display: inline; } @@ -1199,7 +1258,7 @@ a.status__content__spoiler-link { @import 'boost'; -button i.fa-retweet { +button.icon-button i.fa-retweet { height: 19px; width: 22px; background-position: 0 0; @@ -1211,7 +1270,7 @@ button i.fa-retweet { } } -button.active i.fa-retweet { +button.icon-button.active i.fa-retweet { transition-duration: 0.9s; background-position: 0 100%; } @@ -1381,12 +1440,15 @@ 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; @@ -1412,22 +1474,23 @@ button.active i.fa-retweet { } .emoji-dialog { - width: 280px; - height: 220px; - background: $color2; + width: 245px; + height: 270px; + background: $color5; box-sizing: border-box; - border-radius: 2px; + border-radius: 4px; overflow: hidden; position: relative; - box-shadow: 0 0 15px rgba($color8, 0.4); + box-shadow: 0 0 8px rgba($color8, 0.2); .emojione { margin: 0; + width: 100%; + height: auto; } .emoji-dialog-header { padding: 0 10px; - background-color: $color3; ul { padding: 0; @@ -1438,18 +1501,29 @@ button.active i.fa-retweet { li { display: inline-block; box-sizing: border-box; - height: 42px; - padding: 9px 5px; + padding: 10px 5px; cursor: pointer; + border-bottom: 2px solid transparent; + + .emoji { + width: 18px; + height: 18px; + } img, svg { - width: 22px; - height: 22px; + width: 18px; + height: 18px; filter: grayscale(100%); } + &:hover { + img, svg { + filter: grayscale(0); + } + } + &.active { - background: lighten($color3, 6%); + border-bottom-color: $color4; img, svg { filter: grayscale(0); @@ -1473,7 +1547,7 @@ button.active i.fa-retweet { .emoji-category-header { box-sizing: border-box; overflow-y: hidden; - padding: 8px 16px 0; + padding: 10px 8px 10px 16px; display: table; > * { @@ -1483,10 +1557,10 @@ button.active i.fa-retweet { } .emoji-category-title { - font-size: 14px; - font-family: sans-serif; - font-weight: normal; - color: $color1; + font-size: 12px; + text-transform: uppercase; + font-weight: 500; + color: darken($color2, 18%); cursor: default; } @@ -1526,7 +1600,7 @@ button.active i.fa-retweet { width: 7px; height: 7px; border-radius: 10px; - border: 2px solid $color1; + border: 2px solid $color5; top: 2px; left: 2px; } @@ -1534,14 +1608,20 @@ button.active i.fa-retweet { } .emoji-search-wrapper { - padding: 6px 16px; + padding: 10px; + border-bottom: 1px solid lighten($color2, 4%); } .emoji-search { - font-size: 12px; - padding: 6px 4px; + font-size: 14px; + font-weight: 400; + padding: 7px 9px; + font-family: inherit; + display: block; width: 100%; - border: 1px solid #ddd; + background: rgba($color2, 0.3); + color: darken($color2, 18%); + border: 1px solid $color2; border-radius: 4px; } @@ -1554,11 +1634,21 @@ button.active i.fa-retweet { } .emoji-search-wrapper + .emoji-categories-wrapper { - top: 83px; + top: 93px; } - .emoji-row .emoji:hover { - background: lighten($color2, 3%); + .emoji-row .emoji { + img, svg { + transition: transform 60ms ease-in-out; + } + + &:hover { + background: lighten($color2, 3%); + + img, svg { + transform: translateZ(0) scale(1.2); + } + } } .emoji { @@ -1915,3 +2005,41 @@ button.active i.fa-retweet { max-height: 80vh; } } + +.boost-modal { + background: lighten($color2, 8%); + color: $color1; + border-radius: 8px; + overflow: hidden; + max-width: 90vw; + width: 480px; + position: relative; + flex-direction: column; +} + +.boost-modal__container { + padding: 10px; + + .status { + user-select: text; + border-bottom: 0; + } +} + +.boost-modal__action-bar { + display: flex; + background: $color2; + padding: 10px; + line-height: 36px; + + & > div { + flex: 1 1 auto; + text-align: right; + color: lighten($color1, 33%); + padding-right: 10px; + } + + .button { + flex: 0 0 auto; + } +} diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 619c04be..d4f15761 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 df2c7beb..71cb8edd 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 +module Admin + class AccountsController < BaseController + before_action :set_account, except: :index - layout 'admin' + def index + @accounts = Account.alphabetic.page(params[:page]) - 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 - @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 show; end + def suspend + Admin::SuspensionWorker.perform_async(@account.id) + redirect_to admin_accounts_path + 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 unsuspend - @account.update(suspended: false) - redirect_to admin_accounts_path - end + def silence + @account.update(silenced: true) + 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 - def unsilence - @account.update(silenced: false) - redirect_to admin_accounts_path - end + private - private + def set_account + @account = Account.find(params[:id]) + end - def set_account - @account = Account.find(params[:id]) - end - - def account_params - params.require(:account).permit(:silenced, :suspended) + 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 00000000..11fe326b --- /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 1f443284..a8b56c08 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! +module Admin + class DomainBlocksController < BaseController + def index + @blocks = DomainBlock.page(params[:page]) + end - layout 'admin' + def new + @domain_block = DomainBlock.new + end - def index - @blocks = DomainBlock.paginate(page: params[:page], per_page: 40) - end + def create + @domain_block = DomainBlock.new(resource_params) - def new - @domain_block = DomainBlock.new - end + 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 - def create - @domain_block = DomainBlock.new(resource_params) + private - 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 + def resource_params + params.require(:domain_block).permit(:domain, :severity) end end - - private - - def resource_params - params.require(:domain_block).permit(:domain, :severity) - end end diff --git a/app/controllers/admin/pubsubhubbub_controller.rb b/app/controllers/admin/pubsubhubbub_controller.rb index b9e840ff..31c80a17 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 2b3b1809..3c308231 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] +module Admin + class ReportsController < BaseController + before_action :set_report, except: [:index] - layout 'admin' + 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 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 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 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 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 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 - def remove - RemovalWorker.perform_async(params[:status_id]) - redirect_to admin_report_path(@report) - end + private - private - - def set_report - @report = Report.find(params[:id]) + 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 7615c781..6cca5c3e 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' - - 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] - - # Special cases - value = value == 'true' if @setting.var == 'open_registrations' - - if @setting.value != value - @setting.value = value - @setting.save +module Admin + class SettingsController < BaseController + def index + @settings = Setting.all_as_records end - respond_to do |format| - format.html { redirect_to admin_settings_path } - format.json { respond_with_bip(@setting) } + 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' + + if @setting.value != value + @setting.value = value + @setting.save + end + + respond_to do |format| + format.html { redirect_to admin_settings_path } + format.json { respond_with_bip(@setting) } + end 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/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 71c05433..3cff2998 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 db16f82e..57604f1d 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/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb index 1e3f786e..22e37683 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/blocked_accounts_controller.rb b/app/controllers/settings/exports/blocked_accounts_controller.rb new file mode 100644 index 00000000..0bf8848b --- /dev/null +++ b/app/controllers/settings/exports/blocked_accounts_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Settings + module Exports + class BlockedAccountsController < ApplicationController + before_action :authenticate_user! + + def index + export_data = Export.new(current_account.blocking).to_csv + + respond_to do |format| + format.csv { send_data export_data, filename: 'blocking.csv' } + end + end + end + end +end diff --git a/app/controllers/settings/exports/following_accounts_controller.rb b/app/controllers/settings/exports/following_accounts_controller.rb new file mode 100644 index 00000000..a7f4344c --- /dev/null +++ b/app/controllers/settings/exports/following_accounts_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Settings + module Exports + class FollowingAccountsController < ApplicationController + before_action :authenticate_user! + + def index + export_data = Export.new(current_account.following).to_csv + + respond_to do |format| + format.csv { send_data export_data, filename: 'following.csv' } + end + end + end + end +end diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index 4fcec532..e060f03d 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -1,46 +1,13 @@ # frozen_string_literal: true -require 'csv' - class Settings::ExportsController < ApplicationController layout 'admin' before_action :authenticate_user! - before_action :set_account def show @total_storage = current_account.media_attachments.sum(:file_file_size) @total_follows = current_account.following.count @total_blocks = current_account.blocking.count end - - def download_following_list - @accounts = current_account.following - - respond_to do |format| - format.csv { render text: accounts_list_to_csv(@accounts) } - end - end - - def download_blocking_list - @accounts = current_account.blocking - - respond_to do |format| - format.csv { render text: accounts_list_to_csv(@accounts) } - end - end - - private - - def set_account - @account = current_user.account - end - - 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)] - end - end - end end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 60400e46..c758e4ef 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -23,8 +23,9 @@ class Settings::PreferencesController < ApplicationController } current_user.settings['default_privacy'] = user_params[:setting_default_privacy] + current_user.settings['boost_modal'] = user_params[:setting_boost_modal] == '1' - if current_user.update(user_params.except(:notification_emails, :interactions, :setting_default_privacy)) + if current_user.update(user_params.except(:notification_emails, :interactions, :setting_default_privacy, :setting_boost_modal)) redirect_to settings_preferences_path, notice: I18n.t('generic.changes_saved_msg') else render action: :show @@ -34,6 +35,6 @@ class Settings::PreferencesController < ApplicationController private def user_params - params.require(:user).permit(:locale, :setting_default_privacy, notification_emails: [:follow, :follow_request, :reblog, :favourite, :mention, :digest], interactions: [:must_be_follower, :must_be_following]) + params.require(:user).permit(:locale, :setting_default_privacy, :setting_boost_modal, notification_emails: [:follow, :follow_request, :reblog, :favourite, :mention, :digest], interactions: [:must_be_follower, :must_be_following]) end end diff --git a/app/controllers/xrd_controller.rb b/app/controllers/xrd_controller.rb index 6db87cef..5964172e 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/accounts_helper.rb b/app/helpers/accounts_helper.rb deleted file mode 100644 index af23a78d..00000000 --- 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/atom_builder_helper.rb b/app/helpers/atom_builder_helper.rb deleted file mode 100644 index b750eeb0..00000000 --- a/app/helpers/atom_builder_helper.rb +++ /dev/null @@ -1,285 +0,0 @@ -# frozen_string_literal: true - -module AtomBuilderHelper - def stream_updated_at - if @account.stream_entries.last - (@account.updated_at > @account.stream_entries.last.created_at ? @account.updated_at : @account.stream_entries.last.created_at) - else - @account.updated_at - end - end - - def entry(xml, is_root = false, &block) - if is_root - root_tag(xml, :entry, &block) - else - xml.entry(&block) - end - end - - def feed(xml, &block) - root_tag(xml, :feed, &block) - end - - def unique_id(xml, date, id, type) - xml.id_ TagManager.instance.unique_tag(date, id, type) - end - - def simple_id(xml, id) - xml.id_ id - end - - def published_at(xml, date) - xml.published date.iso8601 - end - - def updated_at(xml, date) - xml.updated date.iso8601 - end - - def verb(xml, verb) - xml['activity'].send('verb', TagManager::VERBS[verb]) - end - - def content(xml, content, warning = nil) - xml.summary(warning) unless warning.blank? - xml.content({ type: 'html' }, content) unless content.blank? - end - - def title(xml, title) - xml.title strip_tags(title || '').truncate(80) - end - - def author(xml, &block) - xml.author(&block) - end - - def category(xml, term) - xml.category(term: term) - end - - def target(xml, &block) - xml['activity'].object(&block) - end - - def object_type(xml, type) - xml['activity'].send('object-type', TagManager::TYPES[type]) - end - - def uri(xml, uri) - xml.uri uri - end - - def name(xml, name) - xml.name name - end - - def summary(xml, summary) - xml.summary(summary) unless summary.blank? - end - - def subtitle(xml, subtitle) - xml.subtitle(subtitle) unless subtitle.blank? - end - - def link_alternate(xml, url) - xml.link(rel: 'alternate', type: 'text/html', href: url) - end - - def link_self(xml, url) - xml.link(rel: 'self', type: 'application/atom+xml', href: url) - end - - def link_next(xml, url) - xml.link(rel: 'next', type: 'application/atom+xml', href: url) - end - - def link_hub(xml, url) - xml.link(rel: 'hub', href: url) - end - - def link_salmon(xml, url) - xml.link(rel: 'salmon', href: url) - end - - def portable_contact(xml, account) - xml['poco'].preferredUsername account.username - xml['poco'].displayName(account.display_name) unless account.display_name.blank? - xml['poco'].note(Formatter.instance.simplified_format(account)) unless account.note.blank? - end - - def in_reply_to(xml, uri, url) - xml['thr'].send('in-reply-to', ref: uri, href: url, type: 'text/html') - end - - def link_mention(xml, account) - xml.link(:rel => 'mentioned', :href => TagManager.instance.uri_for(account), 'ostatus:object-type' => TagManager::TYPES[:person]) - end - - def link_enclosure(xml, media) - xml.link(rel: 'enclosure', href: full_asset_url(media.file.url(:original, false)), type: media.file_content_type, length: media.file_file_size) - end - - def link_avatar(xml, account) - single_link_avatar(xml, account, :original, 120) - end - - def link_header(xml, account) - xml.link('rel' => 'header', 'type' => account.header_content_type, 'media:width' => 700, 'media:height' => 335, 'href' => full_asset_url(account.header.url(:original))) - end - - def logo(xml, url) - xml.logo url - end - - def email(xml, email) - xml.email email - end - - def conditionally_formatted(activity) - if activity.is_a?(Status) - Formatter.instance.format(activity.reblog? ? activity.reblog : activity) - elsif activity.nil? - nil - else - activity.content - end - end - - def link_visibility(xml, item) - return unless item.respond_to?(:visibility) && item.public_visibility? - xml.link(:rel => 'mentioned', :href => TagManager::COLLECTIONS[:public], 'ostatus:object-type' => TagManager::TYPES[:collection]) - end - - def privacy_scope(xml, level) - xml['mastodon'].scope(level) - end - - def include_author(xml, account) - simple_id xml, TagManager.instance.uri_for(account) - 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 - summary xml, account.note - link_alternate xml, TagManager.instance.url_for(account) - link_avatar xml, account - link_header xml, account - portable_contact xml, account - privacy_scope xml, account.locked? ? :private : :public - end - - def rich_content(xml, activity) - if activity.is_a?(Status) - content xml, conditionally_formatted(activity), activity.spoiler_text - else - content xml, conditionally_formatted(activity) - end - end - - def include_target(xml, target) - simple_id xml, TagManager.instance.uri_for(target) - - if target.object_type == :person - include_author xml, target - else - object_type xml, target.object_type - verb xml, target.verb - title xml, target.title - link_alternate xml, TagManager.instance.url_for(target) - end - - # Statuses have content and author - return unless target.is_a?(Status) - - rich_content xml, target - verb xml, target.verb - published_at xml, target.created_at - updated_at xml, target.updated_at - - author(xml) do - include_author xml, target.account - end - - if target.reply? - in_reply_to xml, TagManager.instance.uri_for(target.thread), TagManager.instance.url_for(target.thread) - end - - link_visibility xml, target - - target.mentions.each do |mention| - link_mention xml, mention.account - end - - target.media_attachments.each do |media| - link_enclosure xml, media - end - - target.tags.each do |tag| - category xml, tag.name - end - - category(xml, 'nsfw') if target.sensitive? - privacy_scope(xml, target.visibility) - end - - def include_entry(xml, stream_entry) - unique_id xml, stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type - published_at xml, stream_entry.created_at - updated_at xml, stream_entry.updated_at - title xml, stream_entry.title - rich_content xml, stream_entry.activity - verb xml, stream_entry.verb - link_self xml, account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom') - link_alternate xml, account_stream_entry_url(stream_entry.account, stream_entry) - object_type xml, stream_entry.object_type - - # Comments need thread element - if stream_entry.threaded? - in_reply_to xml, TagManager.instance.uri_for(stream_entry.thread), TagManager.instance.url_for(stream_entry.thread) - end - - if stream_entry.targeted? - target(xml) do - include_target(xml, stream_entry.target) - end - end - - link_visibility xml, stream_entry.activity - - stream_entry.mentions.each do |mentioned| - link_mention xml, mentioned - end - - return unless stream_entry.activity.is_a?(Status) - - stream_entry.activity.media_attachments.each do |media| - link_enclosure xml, media - end - - stream_entry.activity.tags.each do |tag| - category xml, tag.name - end - - category(xml, 'nsfw') if stream_entry.activity.sensitive? - privacy_scope(xml, stream_entry.activity.visibility) - end - - private - - def root_tag(xml, tag, &block) - xml.send(tag, { - 'xmlns' => TagManager::XMLNS, - 'xmlns:thr' => TagManager::THR_XMLNS, - 'xmlns:activity' => TagManager::AS_XMLNS, - 'xmlns:poco' => TagManager::POCO_XMLNS, - 'xmlns:media' => TagManager::MEDIA_XMLNS, - 'xmlns:ostatus' => TagManager::OS_XMLNS, - 'xmlns:mastodon' => TagManager::MTDN_XMLNS, - }, &block) - end - - def single_link_avatar(xml, account, size, px) - xml.link('rel' => 'avatar', 'type' => account.avatar_content_type, 'media:width' => px, 'media:height' => px, 'href' => full_asset_url(account.avatar.url(size))) - end -end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 74dc0e11..8a94df5f 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -5,13 +5,16 @@ 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: 'Русский', + ja: '日本語', + }.freeze def human_locale(locale) diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb index 38e63ed8..d5cc004b 100644 --- a/app/helpers/stream_entries_helper.rb +++ b/app/helpers/stream_entries_helper.rb @@ -9,10 +9,6 @@ module StreamEntriesHelper "@#{account.acct}#{@external_links && account.local? ? "@#{Rails.configuration.x.local_domain}" : ''}" end - def avatar_for_status_url(status) - status.reblog? ? status.reblog.account.avatar.url(:original) : status.account.avatar.url(:original) - end - def entry_classes(status, is_predecessor, is_successor, include_threads) classes = ['entry'] classes << 'entry-reblog u-repost-of h-cite' if status.reblog? @@ -22,18 +18,6 @@ module StreamEntriesHelper classes.join(' ') end - def relative_time(date) - date < 5.days.ago ? date.strftime('%d.%m.%Y') : "#{time_ago_in_words(date)} ago" - end - - def reblogged_by_me_class(status) - user_signed_in? && @reblogged.key?(status.id) ? 'reblogged' : '' - end - - def favourited_by_me_class(status) - user_signed_in? && @favourited.key?(status.id) ? 'favourited' : '' - end - def rtl?(text) return false if text.empty? diff --git a/app/lib/atom_serializer.rb b/app/lib/atom_serializer.rb index 845d38c9..68d2fce6 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))) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 58d9fb1f..339a5c78 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 da7ad202..c3f331ff 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 c59c7600..8ceda7f9 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) @@ -284,6 +308,18 @@ class Account < ApplicationRecord 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/export.rb b/app/models/export.rb new file mode 100644 index 00000000..cd1a58eb --- /dev/null +++ b/app/models/export.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +require 'csv' + +class Export + attr_reader :accounts + + def initialize(accounts) + @accounts = accounts + end + + def to_csv + CSV.generate do |csv| + accounts.each do |account| + csv << [(account.local? ? account.local_username_and_domain : account.acct)] + end + end + end +end diff --git a/app/models/notification.rb b/app/models/notification.rb index b7b47486..302d4382 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/user.rb b/app/models/user.rb index bf2916d9..d2aa5d80 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -26,4 +26,8 @@ class User < ApplicationRecord def setting_default_privacy settings.default_privacy || (account.locked? ? 'private' : 'public') end + + def setting_boost_modal + settings.boost_modal + end end diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 6653255f..99e8c875 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -5,5 +5,4 @@ class BaseService include ActionView::Helpers::SanitizeHelper include RoutingHelper - include AtomBuilderHelper end diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml index 8a0d00da..0c066962 100644 --- a/app/views/about/show.html.haml +++ b/app/views/about/show.html.haml @@ -35,7 +35,7 @@ .info = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' · - = link_to t('about.other_instances'), 'https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md' + = link_to t('about.other_instances'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md' · = link_to t('about.about_this'), about_more_path @@ -79,8 +79,8 @@ .info = link_to t('about.terms'), terms_path · - = link_to t('about.apps'), 'https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/Apps.md' + = link_to t('about.apps'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' · = link_to t('about.source_code'), 'https://github.com/tootsuite/mastodon' · - = link_to t('about.other_instances'), 'https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md' + = link_to t('about.other_instances'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md' diff --git a/app/views/accounts/followers.html.haml b/app/views/accounts/followers.html.haml index 49349102..fa5071f3 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 370cd6c4..987dcba1 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 e9089772..3b0d69dc 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 f8ed4ef9..4d636601 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 eb7894b8..fe6ff683 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 cb11a502..2b8e36e6 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 839259dc..9c5c7893 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 32df0457..8826aa22 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/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl index 71949ab0..caee2bfc 100644 --- a/app/views/home/initial_state.json.rabl +++ b/app/views/home/initial_state.json.rabl @@ -5,6 +5,7 @@ node(:meta) do access_token: @token, locale: I18n.locale, me: current_account.id, + boost_modal: current_account.user.setting_boost_modal, } end diff --git a/app/views/kaminari/_next_page.html.haml b/app/views/kaminari/_next_page.html.haml new file mode 100644 index 00000000..30a3643d --- /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 00000000..b1da236d --- /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 00000000..1089e356 --- /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/settings/exports/show.html.haml b/app/views/settings/exports/show.html.haml index 0a0ff863..432a61b4 100644 --- a/app/views/settings/exports/show.html.haml +++ b/app/views/settings/exports/show.html.haml @@ -10,8 +10,8 @@ %tr %th= t('exports.follows') %td= @total_follows - %td= table_link_to 'download', t('exports.csv'), follows_settings_export_path(format: :csv) + %td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv) %tr %th= t('exports.blocks') %td= @total_blocks - %td= table_link_to 'download', t('exports.csv'), blocks_settings_export_path(format: :csv) + %td= table_link_to 'download', t('exports.csv'), settings_exports_blocks_path(format: :csv) diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index 64cf32c3..e819429b 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -22,5 +22,8 @@ = ff.input :must_be_follower, as: :boolean, wrapper: :with_label = ff.input :must_be_following, as: :boolean, wrapper: :with_label + .fields-group + = f.input :setting_boost_modal, as: :boolean, wrapper: :with_label + .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/shared/_landing_strip.html.haml b/app/views/shared/_landing_strip.html.haml index bb081e54..3536c5ca 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 434c5c8d..1333d4d8 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 32a50e15..c894cdb2 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.ja.html.erb b/app/views/user_mailer/confirmation_instructions.ja.html.erb new file mode 100644 index 00000000..bbb44b2c --- /dev/null +++ b/app/views/user_mailer/confirmation_instructions.ja.html.erb @@ -0,0 +1,5 @@ +

ようこそ<%= @resource.email %>さん

+ +

以下のリンクをクリックしてMastodonアカウントのメールアドレスを確認してください

+ +

<%= link_to 'メールアドレスの確認', confirmation_url(@resource, confirmation_token: @token) %>

diff --git a/app/views/user_mailer/confirmation_instructions.ja.text.erb b/app/views/user_mailer/confirmation_instructions.ja.text.erb new file mode 100644 index 00000000..ad8abee2 --- /dev/null +++ b/app/views/user_mailer/confirmation_instructions.ja.text.erb @@ -0,0 +1,5 @@ +ようこそ<%= @resource.email %>さん + +以下のリンクをクリックしてMastodonアカウントのメールアドレスを確認してください + +<%= confirmation_url(@resource, confirmation_token: @token) %> diff --git a/app/views/user_mailer/password_change.ja.html.erb b/app/views/user_mailer/password_change.ja.html.erb new file mode 100644 index 00000000..3aa83f18 --- /dev/null +++ b/app/views/user_mailer/password_change.ja.html.erb @@ -0,0 +1,3 @@ +

こんにちは<%= @resource.email %>さん

+ +

Mastodonアカウントのパスワードが変更されました。

diff --git a/app/views/user_mailer/password_change.ja.text.erb b/app/views/user_mailer/password_change.ja.text.erb new file mode 100644 index 00000000..aa29b9b2 --- /dev/null +++ b/app/views/user_mailer/password_change.ja.text.erb @@ -0,0 +1,3 @@ +こんにちは<%= @resource.email %>さん + +Mastodonアカウントのパスワードが変更されました。 diff --git a/app/views/user_mailer/reset_password_instructions.ja.html.erb b/app/views/user_mailer/reset_password_instructions.ja.html.erb new file mode 100644 index 00000000..156758ef --- /dev/null +++ b/app/views/user_mailer/reset_password_instructions.ja.html.erb @@ -0,0 +1,8 @@ +

こんにちは<%= @resource.email %>さん

+ +

Mastodonアカウントのパスワードの変更がリクエストされました。以下のリンクをクリックして操作を完了できます。

+ +

<%= link_to 'パスワードを変更', edit_password_url(@resource, reset_password_token: @token) %>

+ +

このメールに見に覚えのない場合は無視してください。

+

上記のリンクにアクセスし、変更をしない限りパスワードは変更されません。

diff --git a/app/views/user_mailer/reset_password_instructions.ja.text.erb b/app/views/user_mailer/reset_password_instructions.ja.text.erb new file mode 100644 index 00000000..5fb0eba0 --- /dev/null +++ b/app/views/user_mailer/reset_password_instructions.ja.text.erb @@ -0,0 +1,8 @@ +こんにちは<%= @resource.email %>さん + +Mastodonアカウントのパスワードの変更がリクエストされました。以下のリンクをクリックして操作を完了できます。 + +<%= edit_password_url(@resource, reset_password_token: @token) %> + +このメールに見に覚えのない場合は無視してください。 +上記のリンクにアクセスし、変更をしない限りパスワードは変更されません。 diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb index d5a33cad..60529c0e 100644 --- a/app/workers/import_worker.rb +++ b/app/workers/import_worker.rb @@ -4,32 +4,41 @@ require 'csv' class ImportWorker include Sidekiq::Worker - sidekiq_options queue: 'pull', retry: false - def perform(import_id) - import = Import.find(import_id) + attr_reader :import - case import.type + def perform(import_id) + @import = Import.find(import_id) + + case @import.type when 'blocking' - process_blocks(import) + process_blocks when 'following' - process_follows(import) + process_follows end - import.destroy + @import.destroy end private - def process_blocks(import) - from_account = import.account + def from_account + @import.account + end - CSV.foreach(import.data.path) do |row| - next if row.size != 1 + def import_contents + Paperclip.io_adapters.for(@import.data).read + end + def import_rows + CSV.new(import_contents).reject(&:blank?) + end + + def process_blocks + import_rows.each do |row| begin - target_account = FollowRemoteAccountService.new.call(row[0]) + target_account = FollowRemoteAccountService.new.call(row.first) next if target_account.nil? BlockService.new.call(from_account, target_account) rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError @@ -38,14 +47,10 @@ class ImportWorker end end - def process_follows(import) - from_account = import.account - - CSV.foreach(import.data.path) do |row| - next if row.size != 1 - + def process_follows + import_rows.each do |row| begin - FollowService.new.call(from_account, row[0]) + FollowService.new.call(from_account, row.first) rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError next end diff --git a/config/application.rb b/config/application.rb index 9a5c0d0d..a3991639 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, :ja] + config.i18n.default_locale = :en # config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') diff --git a/config/deploy.rb b/config/deploy.rb index cbfb8ec9..e348c8e9 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -1,11 +1,11 @@ -lock '3.7.2' +lock '3.8.0' set :application, 'mastodon' set :repo_url, 'https://github.com/tootsuite/mastodon.git' -set :branch, 'master' +set :branch, 'skylight' set :rbenv_type, :user set :rbenv_ruby, File.read('.ruby-version').strip set :migration_role, :app append :linked_files, '.env.production' -append :linked_dirs, 'vendor/bundle', 'node_modules', 'public/system' +append :linked_dirs, 'vendor/bundle', 'node_modules', 'public/system', 'tmp/cache' diff --git a/config/environments/production.rb b/config/environments/production.rb index d299e4f4..37a10fd4 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -40,7 +40,7 @@ Rails.application.configure do # By default, use the lowest log level to ensure availability of diagnostic information # when problems arise. - config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'debug').to_sym + config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info').to_sym # Prepend all log lines with the following tags. config.log_tags = [:request_id] @@ -64,7 +64,7 @@ Rails.application.configure do password: ENV.fetch('REDIS_PASSWORD') { false }, db: 0, namespace: 'cache', - expires_in: 20.minutes, + expires_in: 10.minutes, } # Enable serving of images, stylesheets, and JavaScripts from an asset server. @@ -94,12 +94,14 @@ Rails.application.configure do # E-mails config.action_mailer.smtp_settings = { - :port => ENV['SMTP_PORT'], - :address => ENV['SMTP_SERVER'], - :user_name => ENV['SMTP_LOGIN'], - :password => ENV['SMTP_PASSWORD'], - :domain => ENV['SMTP_DOMAIN'] || config.x.local_domain, - :authentication => :plain, + :port => ENV['SMTP_PORT'], + :address => ENV['SMTP_SERVER'], + :user_name => ENV['SMTP_LOGIN'], + :password => ENV['SMTP_PASSWORD'], + :domain => ENV['SMTP_DOMAIN'] || config.x.local_domain, + :authentication => ENV['SMTP_AUTH_METHOD'] || :plain, + :openssl_verify_mode => ENV['SMTP_OPENSSL_VERIFY_MODE'], + :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 4304bbd1..7ae143f9 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 ede6640b..3c23e7b2 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 37f113d5..5cfc16a8 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 00000000..bd455f38 --- /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 00000000..e69de29b diff --git a/config/locales/de.yml b/config/locales/de.yml index ed54bb69..75ac4e1b 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.en.yml b/config/locales/devise.en.yml index 32ac92cf..442e70d5 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -29,7 +29,7 @@ en: success: Successfully authenticated from %{kind} account. passwords: no_token: You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided. - send_instructions: You will receive an email with instructions on how to reset your password in a few minutes. + send_instructions: If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes. send_paranoid_instructions: If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes. updated: Your password has been changed successfully. You are now signed in. updated_not_active: Your password has been changed successfully. diff --git a/config/locales/devise.fi.yml b/config/locales/devise.fi.yml index 79fe8123..bf4f530d 100644 --- a/config/locales/devise.fi.yml +++ b/config/locales/devise.fi.yml @@ -29,7 +29,7 @@ fi: success: Onnistuneesti varmennettu %{kind} tilillä. passwords: no_token: Et pääse tälle sivulle ilman salasanan vaihto sähköpostia. Jos tulet tämmöisestä postista, varmista että sinulla on täydellinen URL. - send_instructions: Saat sähköpostitse ohjeet salasanan palautukseen muutaman minuutin kuluessa. + send_instructions: Jos sähköpostisi on meidän tietokannassa, saat pian ohjeet salasanan palautukseen. send_paranoid_instructions: Jos sähköpostisi on meidän tietokannassa, saat pian ohjeet salasanan palautukseen. updated: Salasanasi vaihdettu onnistuneesti. Olet nyt kirjautunut sisään. updated_not_active: Salasanasi vaihdettu onnistuneesti. diff --git a/config/locales/devise.fr.yml b/config/locales/devise.fr.yml index 3b46b01e..a986113e 100644 --- a/config/locales/devise.fr.yml +++ b/config/locales/devise.fr.yml @@ -12,7 +12,7 @@ fr: last_attempt: Vous avez droit à une tentative avant que votre compte ne soit verrouillé. locked: Votre compte est verrouillé. not_found_in_database: Email ou mot de passe invalide. - timeout: Votre session est expirée. Veuillez vous reconnecter pour continuer. + timeout: Votre session a expiré. Veuillez vous reconnecter pour continuer. unauthenticated: Vous devez vous connecter ou vous inscrire pour continuer. unconfirmed: Vous devez valider votre compte pour continuer. mailer: @@ -21,23 +21,23 @@ fr: password_change: subject: Votre mot de passe a été modifié avec succés. reset_password_instructions: - subject: Instructions pour changer le mot de passe + subject: Instructions pour modifier le mot de passe unlock_instructions: subject: Instructions pour déverrouiller le compte omniauth_callbacks: failure: 'Nous n''avons pas pu vous authentifier via %{kind} : ''%{reason}''.' success: Authentifié avec succès via %{kind}. passwords: - no_token: Vous ne pouvez accéder à cette page sans passer par un e-mail de réinitialisation de mot de passe. Si vous êtes passé par un e-mail de ce type, assurez-vous d'utiliser l'URL complète. + no_token: Vous ne pouvez accéder à cette page sans passer par un e-mail de réinitialisation de mot de passe. Si vous êtes passé⋅e par un e-mail de ce type, assurez-vous d'utiliser l'URL complète. send_instructions: Vous allez recevoir les instructions de réinitialisation du mot de passe dans quelques instants send_paranoid_instructions: Si votre e-mail existe dans notre base de données, vous allez recevoir un lien de réinitialisation par e-mail - updated: Votre mot de passe a été édité avec succès, vous êtes maintenant connecté - updated_not_active: Votre mot de passe a été changé avec succès. + updated: Votre mot de passe a été modifié avec succès, vous êtes maintenant connecté⋅e + updated_not_active: Votre mot de passe a été modifié avec succès. registrations: destroyed: Votre compte a été supprimé avec succès. Nous espérons vous revoir bientôt. - signed_up: Bienvenue, vous êtes connecté. - signed_up_but_inactive: Vous êtes bien enregistré. Vous ne pouvez cependant pas vous connecter car votre compte n'est pas encore activé. - signed_up_but_locked: Vous êtes bien enregistré. Vous ne pouvez cependant pas vous connecter car votre compte est verrouillé. + signed_up: Bienvenue, vous êtes connecté⋅e. + signed_up_but_inactive: Vous êtes bien enregistré⋅e. Vous ne pouvez cependant pas vous connecter car votre compte n'est pas encore activé. + signed_up_but_locked: Vous êtes bien enregistré⋅e. Vous ne pouvez cependant pas vous connecter car votre compte est verrouillé. signed_up_but_unconfirmed: Un message contenant un lien de confirmation a été envoyé à votre adresse email. Ouvrez ce lien pour activer votre compte. update_needs_confirmation: Votre compte a bien été mis à jour mais nous devons vérifier votre nouvelle adresse email. Merci de vérifier vos emails et de cliquer sur le lien de confirmation pour finaliser la validation de votre nouvelle adresse. updated: Votre compte a été modifié avec succès. @@ -48,14 +48,14 @@ fr: unlocks: send_instructions: Vous allez recevoir les instructions nécessaires au déverrouillage de votre compte dans quelques instants send_paranoid_instructions: Si votre compte existe, vous allez bientôt recevoir un email contenant les instructions pour le déverrouiller. - unlocked: Votre compte a été déverrouillé avec succès, vous êtes maintenant connecté. + unlocked: Votre compte a été déverrouillé avec succès, vous êtes maintenant connecté⋅e. errors: messages: - already_confirmed: a déjà été validé(e), veuillez essayer de vous connecter + already_confirmed: a déjà été validé⋅e, veuillez essayer de vous connecter confirmation_period_expired: à confirmer dans les %{period}, merci de faire une nouvelle demande expired: a expiré, merci d'en faire une nouvelle demande - not_found: n'a pas été trouvé(e) - not_locked: n'était pas verrouillé(e) + not_found: n'a pas été trouvé⋅e + 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) : ' + one: '1 erreur a empêché ce(tte) %{resource} d’être sauvegardé⋅e :' + other: '%{count} erreurs ont empêché %{resource} d’être sauvegardé⋅e :' diff --git a/config/locales/devise.ja.yml b/config/locales/devise.ja.yml new file mode 100644 index 00000000..4aeb09cd --- /dev/null +++ b/config/locales/devise.ja.yml @@ -0,0 +1,61 @@ +--- +ja: + devise: + confirmations: + confirmed: メールアドレスの確認が正常に完了しました。 + send_instructions: まもなくメールアドレスの確認の方法が記載されたメールが送信されます。 + send_paranoid_instructions: もしあなたのメールアドレスが登録されていれば、まもなくメールアドレスの確認の方法が記載されたメールが送信されます。 + failure: + already_authenticated: 既にログイン済みです。 + inactive: あなたのアカウントはまだアクティベートされていません。 + invalid: '%{authentication_keys}かパスワードが誤っています' + last_attempt: あと1回失敗するとアカウントがロックされます。 + locked: アカウントはロックされました。 + not_found_in_database: '%{authentication_keys}かパスワードが誤っています' + timeout: セッションの有効期限が切れました。続行するには再度ログインしてください。 + unauthenticated: 続行するにはログインするか、アカウントを作成してください。 + unconfirmed: 続行するにはメールアドレスを確認する必要があります。 + mailer: + confirmation_instructions: + subject: 'Mastodon: メールアドレスの確認' + password_change: + subject: 'Mastodon: パスワードが変更されました' + reset_password_instructions: + subject: 'Mastodon: パスワード再発行' + unlock_instructions: + subject: 'Mastodon: アカウントのロックの解除' + omniauth_callbacks: + failure: '%{reason}によって%{kind}からのアクセスを認証できませんでした。' + success: '%{kind}からのアクセスは正常に認証されました。' + passwords: + no_token: パスワード再発行のメール以外からこのページにアクセスすることはできません。 パスワード再発行のメールからアクセスしたのにもかかわらずこのメッセージが表示される場合は、アクセスしたURLが間違っていないか確認してください。 + send_instructions: パスワード再発行の方法が記載されたメールが間もなく送信されます。 + send_paranoid_instructions: メールアドレスが登録済みであれば、パスワードをリセットするリンクが記載されたメールがあなたのメールアドレスに送信されます。 + updated: パスワードは正常に更新されました。なお、既にログイン済みです。 + updated_not_active: パスワードは正常に更新されました。 + registrations: + destroyed: アカウントの作成はキャンセルされました。またのご利用をお待ちしています。 + signed_up: アカウントの作成が完了しました。Mastodonへようこそ! + signed_up_but_inactive: アカウントの作成が完了しました。しかし、アカウントが有効化されていないためログインできませんでした。 + signed_up_but_locked: アカウントの作成が完了しました。しかし、アカウントがロックされているためログインできませんでした。 + signed_up_but_unconfirmed: メールアドレスの確認用のリンクが入力したメールアドレスに送信されました。メール内のリンクをクリックしてアカウントを有効化してください。 + update_needs_confirmation: アカウント情報の更新に成功しました。しかし、メールアドレスの確認が必要です。送信されたメール内のリンクをクリックしてメールアドレスを確認してください。 + updated: アカウント情報の更新に成功しました。 + sessions: + already_signed_out: ログアウトしました。 + signed_in: ログインしました。 + signed_out: ログアウトしました。 + unlocks: + send_instructions: まもなくアカウントのロックを解除するための方法を記載したメールが送信されます。 + send_paranoid_instructions: もしアカウントが存在すれば、まもなくアカウントのロックを解除するための方法を記載したメールが送信されます。 + unlocked: アカウントロックは正常に解除されました。続行するにはログインしてください。 + errors: + messages: + already_confirmed: は確認されました。ログインを試してください。 + confirmation_period_expired: '%{period}以内に確認が必要です。再度試してください。' + expired: は期限切れです。再度試してください。 + not_found: 見つかりません + not_locked: ロックされていません + not_saved: + one: 'エラーが発生したため、%{resource}の保存に失敗しました。' + other: "%{count}個のエラーが発生したため、保存に失敗しました。 %{resource}" diff --git a/config/locales/devise.ru.yml b/config/locales/devise.ru.yml new file mode 100644 index 00000000..f829f9d8 --- /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 cfc9083d..edfc7133 100644 --- a/config/locales/doorkeeper.fr.yml +++ b/config/locales/doorkeeper.fr.yml @@ -1,5 +1,12 @@ --- fr: + activemodel: + errors: + models: + remote_follow: + attributes: + acct: + blank: Le nom d'utilisateur ne doit pas être vide activerecord: attributes: doorkeeper/application: @@ -14,6 +21,23 @@ fr: invalid_uri: doit être une URL valide. relative_uri: doit être une URL absolue. secured_uri: doit être une URL HTTP/SSL. + account: + attributes: + username: + blank: Identifiant vide + user: + attributes: + email: + taken: Email pris + invalid: Email invalide + blank: Email vide + password: + blank: Mot de passe vide + too_short: Mot de passe trop court + password_confirmation: + confirmation: Le mot de passe ne correspond pas + messages: + record_invalid: Données invalides doorkeeper: applications: buttons: diff --git a/config/locales/doorkeeper.ja.yml b/config/locales/doorkeeper.ja.yml new file mode 100644 index 00000000..7c6a1405 --- /dev/null +++ b/config/locales/doorkeeper.ja.yml @@ -0,0 +1,113 @@ +--- +ja: + 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: URIはHTTPS/SSLである必要があります。 + doorkeeper: + applications: + buttons: + authorize: 承認 + cancel: キャンセル + destroy: 削除 + edit: 編集 + submit: 送信 + confirmations: + destroy: 本当に削除しますか? + edit: + title: アプリケーションの編集 + form: + error: フォームにエラーが無いか確認してください。 + help: + native_redirect_uri: ローカルテストに %{native_redirect_uri} を使用 + redirect_uri: 一行に一つのURLを入力してください + scopes: アクセス権は半角スペースで区切ることができます。 空白のままにするとデフォルトを使用します。 + index: + callback_url: コールバックURL + name: 名前 + new: 新規アプリケーション + title: あなたのアプリケーション + new: + title: 新規アプリケーション + show: + actions: アクション + application_id: アクションId + callback_urls: コールバックurl + 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/doorkeeper.ru.yml b/config/locales/doorkeeper.ru.yml new file mode 100644 index 00000000..8862936d --- /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 118798ba..6c473899 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 3644b37b..e82e4249 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 19f2c71b..42245d67 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 56aa9df4..db8194ff 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -11,12 +11,12 @@ fi: domain_count_before: Yhdistyneenä features: api: Avoin API ohjelmille ja palveluille - blocks: Rikkaat esto ja hiljennys työkalut + blocks: Rikkaat esto- ja hiljennystyökalut characters: 500 kirjainta per viesti chronology: Aikajana on kronologisessa järjestyksessä - ethics: 'Eettinen suunnittelu: ei mainoksia, no seurantaa' + ethics: 'Eettinen suunnittelu: ei mainoksia, ei seurantaa' gifv: GIFV settejä ja lyhyitä videoita - privacy: Julkaisu kohtainen yksityisyys asetus + privacy: Julkaisukohtainen yksityisyysasetus public: Julkiset aikajanat features_headline: Mikä erottaa Mastodonin muista get_started: Aloita käyttö @@ -39,23 +39,23 @@ fi: remote_follow: Etäseuranta unfollow: Lopeta seuraaminen application_mailer: - settings: 'Muokkaa sähköposti asetuksia: %{link}' - signature: Mastodon ilmoituksia palvelimelta %{instance} + settings: 'Muokkaa sähköpostiasetuksia: %{link}' + signature: Mastodon-ilmoituksia palvelimelta %{instance} view: 'Katso:' applications: invalid_url: Annettu URL on väärä auth: change_password: Tunnukset - didnt_get_confirmation: Etkö saanut varmennus ohjeita? + didnt_get_confirmation: Etkö saanut varmennusohjeita? forgot_password: Unohditko salasanasi? login: Kirjaudu sisään logout: Kirjaudu ulos register: Rekisteröidy - resend_confirmation: Lähetä varmennus ohjeet uudestaan - reset_password: Palauta Salasana + resend_confirmation: Lähetä varmennusohjeet uudestaan + reset_password: Palauta salasana set_new_password: Aseta uusi salasana authorize_follow: - error: Valitettavasti tapahtui virhe etätilin haussa + error: Valitettavasti tapahtui virhe etätilin haussa. follow: Seuraa prompt_html: 'Sinä (%{self}) olet pyytänyt lupaa seurata:' title: Seuraa %{acct} @@ -79,12 +79,12 @@ fi: follows: Seurattavat storage: Mediasi generic: - changes_saved_msg: Muutokset onnistuneesti tallenettu! + changes_saved_msg: Muutokset onnistuneesti tallennettu! powered_by: powered by %{link} save_changes: Tallenna muutokset validation_errors: - one: Jokin ei ole viellä oikein! Katso virhe alapuolelta - other: Jokin ei ole viellä oikein! Katso %{count} virhettä alapuolelta + one: Jokin ei ole viellä oikein! Katso virhe alapuolelta. + other: Jokin ei ole viellä oikein! Katso %{count} virhettä alapuolelta. imports: preface: Voit tuoda tiettyä dataa kaikista ihmisistä joita seuraat tai estät tilillesi tälle palvelimelle tiedostoista, jotka on luotu toisella palvelimella success: Datasi on onnistuneesti ladattu ja käsitellään pian @@ -151,14 +151,12 @@ fi: formats: default: "%b %d, %Y, %H:%M" two_factor_auth: - description_html: Jos otat käyttöön kaksivaiheisen tunnistuksen, kirjautumiseen vaaditaan puhelin, joka voi generoida tokeneita kirjautumista varten. + description_html: Jos otat käyttöön kaksivaiheisen tunnistuksen, kirjautumiseen vaaditaan puhelin, joka voi luoda tokeneita kirjautumista varten. disable: Poista käytöstä enable: Ota käyttöön - instructions_html: "Skannaa tämä QR-koodi Google Authenticator tai samanlaiseen sovellukseen puhelimellasi. Tästä hetkestä lähtien ohjelma generoi koodin, mikä sinun tarvitsee syöttää sisäänkirjautuessa." + instructions_html: "Skannaa tämä QR-koodi Google Authenticator- tai vastaavaan sovellukseen puhelimellasi. Tästä hetkestä lähtien ohjelma luo koodin, mikä sinun tarvitsee syöttää sisäänkirjautuessa." plaintext_secret_html: 'Plain-text secret: %{secret}' warning: Jos et juuri nyt voi konfiguroida authenticator-applikaatiota juuri nyt, sinun pitäisi klikata "Poista käytöstä" tai et voi kirjautua sisään. users: invalid_email: Virheellinen sähköposti - invalid_otp_token: Virheellinen kaksivaihe tunnistus koodi - will_paginate: - page_gap: "…" + invalid_otp_token: Virheellinen kaksivaihetunnistuskoodi diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 9a9c1b6d..92cf4394 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -167,5 +167,3 @@ fr: users: 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 915d02c1..96b73d43 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/ja.yml b/config/locales/ja.yml new file mode 100644 index 00000000..d30fd348 --- /dev/null +++ b/config/locales/ja.yml @@ -0,0 +1,173 @@ +--- +ja: + about: + about_mastodon: Mastodon は無料でオープンソースのソーシャルネットワークです。 従来のプラットフォームとは違う分散型で、これはあなたの会話が一つの会社によって独占されるのを防ぎます。自分の信頼できるサーバーを選びます— どのサーバーを選んでも、誰とでも会話することができます。 だれでも自分の Mastodon サーバーを作ることができ、シームレスにソーシャルネットワークに参加できます。 + about_this: このサーバーについて + apps: アプリ + business_email: 'ビジネスメールアドレス:' + closed_registrations: 現在このサーバーでの新規登録は受け付けていません。 + contact: 連絡先 + description_headline: '%{domain}とは?' + domain_count_after: 個のサーバー + domain_count_before: 接続中 + features: + api: アプリやその他サービスにAPIを公開 + blocks: ブロックやミュートの種類は豊富 + characters: 1投稿は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: 'メール設定の変更: %{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: 'あなた (%{self}) は以下のアカウントのフォローをリクエストしました:' + 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: powered by %{link} + save_changes: 変更を保存 + validation_errors: + one: エラーが発生しました。以下のエラーを確認してください + other: エラーが発生しました。以下の%{count}個のエラーを確認してください + imports: + preface: このサーバーのあなたのアカウントにフォロー、ブロック、などの他のサーバーからエクスポートされたファイルの情報をインポートできます。 + success: ファイルは正常にアップロードされ、現在処理中です。しばらくしてから確認してください + types: + blocking: ブロック中のアカウントリスト + following: フォロー中のアカウントリスト + upload: アップロード + landing_strip_html: %{name}さんはサーバー%{domain}のユーザーです。アカウントさえ持っていればフォローしたり会話したりできます。もしお持ちでないならこちらからサインアップできます。 + media_attachments: + validations: + images_and_video: 既に画像が追加されている場合動画を追加することはできません。 + too_many: 追加できるファイルは4つまでです。 + notification_mailer: + digest: + body: '%{instance}での最後のログインからの出来事:' + 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: フォローしたい人の ユーザー名@ドメイン を入力してください + missing_resource: リダイレクト先が見つかりませんでした + proceed: フォローする + prompt: 'フォローしようとしています:' + settings: + authorized_apps: 認証済みアプリ + back: 戻る + edit_profile: プロフィール編集 + export: データのエクスポート + import: データのインポート + preferences: ユーザー設定 + settings: 設定 + two_factor_auth: 二段階認証 + statuses: + open_in_web: Webで開く + over_character_limit: '%{max}文字までしか入力できません' + show_more: もっと見る + visibilities: + private: フォロワーだけに見せる + public: 公開 + unlisted: 公開されますが、公開タイムラインには載りません + stream_entries: + click_to_show: 見るにはクリック + reblogged: ブーストされました + sensitive_content: 不適切なコンテンツの可能性があります + time: + formats: + default: "%Y年%m月%d日 %H:%M" + two_factor_auth: + code_hint: 確認するには認証アプリで表示されたコードを入力してください + description_html: 二段階認証を有効にするとログイン時、電話でコードを受け取る必要があります。 + disable: 無効 + enable: 有効 + enabled_success: 二段階認証が有効になりました + instructions_html: "Google Authenticatorか、もしくはほかのTOTPアプリでこのQRコードをスキャンしてください。これ以降、ログインするときはそのアプリで生成されるコードが必要になります。" + manual_instructions: 'QRコードがスキャンできず、手動での登録を希望の場合はこのシークレットコードを利用してください。:' + setup: 初期設定 + warning: 現在認証アプリを設定できない場合、無効に設定して、有効にしないでください。 + wrong_code: コードが間違っています。サーバー上の時間とデバイス上の時間が一致していることを確認してください。 + users: + invalid_email: メールアドレスが無効です + invalid_otp_token: 二段階認証コードが間違っています + will_paginate: + page_gap: "…" diff --git a/config/locales/no.yml b/config/locales/no.yml index b9a752d5..9aa966d2 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 ad7d05e3..f2c7458f 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 00000000..fab17862 --- /dev/null +++ b/config/locales/ru.yml @@ -0,0 +1,163 @@ +--- +ru: + about: + about_mastodon: Mastodon - это свободная социальная сеть с открытым исходным кодом. Как децентрализованная альтернатива коммерческим платформам, Mastodon предотвращает риск монополизации Вашего общения одной компанией. Выберите сервер, которому Вы доверяете — что бы Вы ни выбрали, Вы сможете общаться со всеми остальными. Любой может запустить свой собственный узел Mastodon и участвовать в социальной сети совершенно бесшовно. + 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: 'Вы (%{self}) запросили подписку:' + 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: %{name} - пользователь на %{domain}. Вы можете подписаться на него/нее и общаться с ним/ней, если у Вас есть аккаунт на любом узле общей сети. Если у Вас его нет, вы можете зарегистрироваться здесь. + 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: При включении двухфакторной аутентификации, вход потребует от Вас использования Вашего телефона, который сгенерирует входные токены. + disable: Отключить + enable: Включить + instructions_html: "Отсканируйте этот QR-код с помощью Google Authenticator или другого подобного приложения на Вашем телефоне. С этого момента приложение будет генерировать токены, которые будет необходимо ввести для входа." + plaintext_secret_html: 'Секрет открытым текстом: %{secret}' + warning: Если сейчас у Вас не получается настроить аутентификатор, нажмите "отключить", иначе Вы не сможете войти! + users: + invalid_email: Введенный e-mail неверен + invalid_otp_token: Введен неверный код diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index dfc67fdf..6b6657a9 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -29,6 +29,7 @@ en: setting_default_privacy: Post privacy type: Import type username: Username + setting_boost_modal: Show confirmation dialog before boosting interactions: must_be_follower: Block notifications from non-followers must_be_following: Block notifications from people you don't follow diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml new file mode 100644 index 00000000..565ec74e --- /dev/null +++ b/config/locales/simple_form.ja.yml @@ -0,0 +1,46 @@ +--- +ja: + simple_form: + hints: + defaults: + avatar: PNGやGIF、JPGは2MBまでです。120x120pxまで縮小されます。 + display_name: 名前は30文字まで設定することができます。 + header: PNGやGIF、JPGは2MBまでです。 700x335pxまで縮小されます。 + locked: フォロワーを手動で承認する必要があります。デフォルトでは投稿範囲はフォロワーまでです。 + note: プロフィールは30文字まで設定することができます。 + imports: + data: CSVファイルからデータをインポートしました。 + labels: + defaults: + avatar: アカウント + confirm_new_password: 新しいパスワード(確認用) + confirm_password: 新しいパスワード + current_password: 現在のパスワード + data: データ + display_name: 表示名 + email: メールアドレス + header: ヘッダー + locale: 言語 + locked: 非公開アカウントにする + new_password: パスワード + note: プロフィール + otp_attempt: 二段階認証コード + password: パスワード + setting_default_privacy: 投稿範囲 + type: インポートするファイルの種類 + username: ユーザー名 + interactions: + must_be_follower: フォロワー以外からの通知をブロック + must_be_following: フォローしていないユーザーからの通知をブロック + notification_emails: + digest: タイムラインからピックアップしてメールで通知する + favourite: いいねされた時メールで通知する + follow: フォローされた時メールで通知する + follow_request: フォローリクエストを受けた時メールで通知する + mention: 返信された時メールで通知する + reblog: あなたのトゥートがブーストされた時メールで通知する + 'no': 'いいえ' + required: + mark: "*" + text: 必須 + 'yes': 'はい' diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml new file mode 100644 index 00000000..6f4873bf --- /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 27e8135d..f7176e86 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 78c4d46e..48028d00 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 9adcdb86..69f8887b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -53,11 +53,10 @@ Rails.application.routes.draw do resource :preferences, only: [:show, :update] resource :import, only: [:show, :create] - resource :export, only: [:show] do - collection do - get :follows, to: 'exports#download_following_list' - get :blocks, to: 'exports#download_blocking_list' - end + resource :export, only: [:show] + namespace :exports, constraints: { format: :csv } do + resources :follows, only: :index, controller: :following_accounts + resources :blocks, only: :index, controller: :blocked_accounts end resource :two_factor_auth, only: [:show, :new, :create] do diff --git a/config/settings.yml b/config/settings.yml index 038a84c5..d364120d 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -14,6 +14,7 @@ defaults: &defaults site_contact_email: '' open_registrations: true closed_registrations_message: '' + boost_modal: true notification_emails: follow: false reblog: false diff --git a/docker-compose.yml b/docker-compose.yml index d6ba66dd..910bf8cf 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/Contributing-to-Mastodon/Sponsors.md b/docs/Contributing-to-Mastodon/Sponsors.md index 131f5fab..8fc22b6b 100644 --- a/docs/Contributing-to-Mastodon/Sponsors.md +++ b/docs/Contributing-to-Mastodon/Sponsors.md @@ -1,46 +1 @@ -Sponsors -======== - -These people make the development of Mastodon possible through [Patreon](https://www.patreon.com/user?u=619786): - -**Extra special Patrons** - -- [World'sTallestLadder](https://mastodon.social/users/carcinoGeneticist) -- [Jimmy Tidey](https://mastodon.social/users/jimmytidey) -- [Kurtis Rainbolt-Greene](https://mastodon.social/users/krainboltgreene) -- [Kit Redgrave](https://socially.constructed.space/users/KitRedgrave) -- [Zeipher](https://mastodon.social/users/Zeipher) -- [Effy Elden](https://mastodon.social/users/effy) -- [Zoë Quinn](https://mastodon.social/users/zoequinn) - -**Thank you to the following people** - -- [Harris Bomberguy](https://mastodon.social/users/Hbomberguy) -- [Edward Saperia](https://nwspk.com) -- [Yoz Grahame](http://yoz.com/) -- [Jenn Kaplan](https://gay.crime.team/users/jkap) -- [Natalie Weizenbaum](https://mastodon.social/users/nex3) -- [Matteo De Micheli](http://matteodem.ch/) -- [BirdMachine](https://mastodon.social/users/BirdMachine) -- [Jessica Hayley](https://mastodon.social/users/jayhay) -- [Niels Roesen Abildgaard](http://hypesystem.dk/) -- [Zatnosk](https://github.com/Zatnosk) -- [Spex Bluefox](https://mastodon.social/users/Spex) -- [J. C. Holder](http://jcholder.com/) -- [glocal](https://mastodon.social/users/glocal) -- [jk](https://mastodon.social/users/jk) -- [C418](https://mastodon.social/users/C418) -- [halcy](https://icosahedron.website/users/halcy) -- [Extropic](https://gnusocial.no/extropic) -- [Pat Monaghan](http://iwrite.software/) -- TBD -- TBD -- TBD -- TBD -- TBD -- TBD -- TBD -- TBD -- TBD -- TBD -- TBD +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Contributing-to-Mastodon/Sponsors.md) diff --git a/docs/Contributing-to-Mastodon/Translating.md b/docs/Contributing-to-Mastodon/Translating.md index 7319375e..1671645e 100644 --- a/docs/Contributing-to-Mastodon/Translating.md +++ b/docs/Contributing-to-Mastodon/Translating.md @@ -1,48 +1 @@ -Translating -=========== - -If you want to localise Mastodon into your language, here is how. - -There are two parts to Mastodon, the server and the web client. The translations for the web client are in `app/assets/javascripts/components/locales`. For the server-side, the translations live in `config/locales` and are divided into different files. Here are all the files you’ll need to translate: - -| Original file (English) | Location | Description | -|---|---|---| -| [`en.jsx`](/app/assets/javascripts/components/locales/en.jsx) | `app/assets/javascripts/components/locales/en.jsx` | Strings for the web client | -| [`en.yml`](/config/locales/en.yml) | `config/locales/en.yml` | Strings for general use | -| [`simple_form.en.yml`](/config/locales/simple_form.en.yml) | `config/locales/simple_form.en.yml` | Strings for the settings area | -| [`devise.en.yml`](/config/locales/devise.en.yml) | `config/locales/devise.en.yml` | Generic strings for Devise | -| [`doorkeeper.en.yml`](/config/locales/doorkeeper.en.yml) | `config/locales/doorkeeper.en.yml` | Generic strings for Doorkeeper | - -## Translating - -If you use Github, first clone the Mastodon repository to your account. - -1. Duplicate the files in their folder and replace `en` in the filenames by your language’s standard two-letters code ([ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)). - For instance `simple_form.en.yml` becomes `simple_form.es.yml` in the Spanish translation. -2. Also replace the language code in the first lines of all the files, and the last line of the `.jsx` file. -3. Translate the right-side values from English to your language. Keep the indentation and punctuation. - -Since Devise and Doorkeeper are popular libraries, there may already be translation files for your language available on the Internet. - -## Declaring the language - -The locales are mentioned in several other files. To activate your translation, add your language code to the different lists present in these files: - -| File | Location | Comment | -|---|---|---| -| [`index.jsx`](/app/assets/javascripts/components/locales/index.jsx) | `app/assets/javascripts/components/locales/index.jsx` | 2 lines to add | -|[`mastodon.jsx`](/app/assets/javascripts/components/containers/mastodon.jsx) | `app/assets/javascripts/components/containers/mastodon.jsx` | 1 line to add + 1 list to complete | -| [`settings_helper.rb`](/app/helpers/settings_helper.rb) | `app/helpers/settings_helper.rb` | 1 line to add + your language’s name | -| [`application.rb`](/config/application.rb) | `config/application.rb` | 1 list to complete | - -## Sending the translation - -You can then push the files to git and submit a pull request. - -## Testing the translation - -Once the pull request is accepted, wait for the code to be deployed on a Mastodon instance. Log-in with your account there, and change the locale in the settings. Browse and use the website. See if everything makes sense in context and if anything seems out of place or breaks the layout. Invite other Mastodon users speaking your language to try it and give feedback. Make changes accordingly and update the translation. - -## Updating the translation - -Keep an eye on the original English files in `app/assets/javascripts/components/locales` and `config/locales`. When they are updated, pass on the changes to your language files. For new strings, add the new lines to the same position and translate them. Once you’re finished with the updates, you can submit a new pull request. +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Contributing-to-Mastodon/Translating.md) diff --git a/docs/Extensions.md b/docs/Extensions.md index cf79379e..be4c6eab 100644 --- a/docs/Extensions.md +++ b/docs/Extensions.md @@ -1,50 +1 @@ -Protocol extensions -=================== - -Some functionality in Mastodon required some additions to the protocols to enable seamless federation of those features: - -### Federation of blocks/unblocks - -ActivityStreams was lacking verbs for block/unblock. Mastodon creates Salmon slaps for block and unblock events, which are not part of a user's public feed, but are nevertheless delivered to the target user. The intent of these Salmon slaps is not to notify the target user, but to notify the target user's server, so that it can perform any number of UX-related tasks such as removing the target user as a follower of the blocker, and/or displaying a message to the target user such as "You can't follow this person because you've been blocked" - -The Salmon slaps have the exact same structure as standard follow/unfollow slaps, the verbs are namespaced: - -- `http://mastodon.social/schema/1.0/block` -- `http://mastodon.social/schema/1.0/unblock` - -### Federation of sensitive material - -Statuses can be marked as containing sensitive (or not safe for work) media. This is symbolized by a `` on the Atom entry - -### Federation of privacy features -#### Locked accounts and status privacy levels - -Accounts and statuses have an access "scope": - -Accounts can be "private" or "public". The former requires a follow request to be approved before a follow relationship can be established, the latter can be followed directly. - -Statuses can be "private", "unlisted" or "public". Private must only be shown to the followers of the account or people mentioned in the status; public can be displayed publicly. Unlisted statuses may be displayed publicly but preferably outside of any spotlights e.g. "whole known network" or "public" timelines. - -Namespace of the scope element is `http://mastodon.social/schema/1.0`. Example: - -```xml - - - - - private - - - private - -``` - -#### Follow requests - -Mastodon uses the following Salmon slaps to signal a follow request, a follow request authorization and a follow request rejection: - -- `http://activitystrea.ms/schema/1.0/request-friend` -- `http://activitystrea.ms/schema/1.0/authorize` -- `http://activitystrea.ms/schema/1.0/reject` - -The activity object of the request-friend slap is the account in question. The activity object of the authorize and reject slaps is the original request-friend activity. Request-friend slap is sent to the locked account, when the end-user of that account decides, the authorize/reject decision slap is sent back to the requester. +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Extensions.md) diff --git a/docs/README.md b/docs/README.md index abf6fcc4..63bcf5a2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,36 +1 @@ -Index -===== - -**Mastodon** is a free, open-source GNU social-compatible 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. - -### Using Mastodon -- [Frequently Asked Questions](Using-Mastodon/FAQ.md) -- [List of Mastodon instances](Using-Mastodon/List-of-Mastodon-instances.md) -- [Apps](Using-Mastodon/Apps.md) -- [User Guide](Using-Mastodon/User-guide.md) - -### Using the API -- [API documentation](Using-the-API/API.md) -- [Streaming API documentation](Using-the-API/Streaming-API.md) -- [Testing the API with cURL](Using-the-API/Testing-with-cURL.md) -- [OAuth details](Using-the-API/OAuth-details.md) -- [Tips for app developers](Using-the-API/Tips-for-app-developers.md) -- [Push notifications](Using-the-API/Push-notifications.md) - -### Running Mastodon -- [Production guide](Running-Mastodon/Production-guide.md) -- [Alternative: Running on Heroku](Running-Mastodon/Heroku-guide.md) -- [Development guide](Running-Mastodon/Development-guide.md) -- [Alternative: Development with Vagrant](Running-Mastodon/Vagrant-guide.md) -- [Administration guide](Running-Mastodon/Administration-guide.md) -- [Tuning Mastodon](Running-Mastodon/Tuning.md) - -### Contributing to Mastodon -- [Sponsors](Contributing-to-Mastodon/Sponsors.md) -- [Translate Mastodon in your language](Contributing-to-Mastodon/Translating.md) -- [Report bugs and submit ideas](https://github.com/tootsuite/mastodon/issues) - -### Protocols - -- [List of used specs and RFCs for the federation](Specs-and-RFCs-used.md) -- [Extensions of the above protocols](Extensions.md) +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/README.md) diff --git a/docs/Running-Mastodon/Administration-guide.md b/docs/Running-Mastodon/Administration-guide.md index 8bcfe7c9..e7571be3 100644 --- a/docs/Running-Mastodon/Administration-guide.md +++ b/docs/Running-Mastodon/Administration-guide.md @@ -1,45 +1 @@ -Administration guide -==================== - -So, you have a working Mastodon instance... now what? - -## Turning into an admin - -The following rake task: - - RAILS_ENV=production bundle exec rails mastodon:make_admin USERNAME=alice - -Would turn the local user "alice" into an admin. - -## Administration web interface - -A user that is designated as `admin = TRUE` in the database is able to access a suite of administration tools: - -* View, edit, silence, or suspend users - https://yourmastodon.instance/admin/accounts -* View PubSubHubbub subscriptions - https://yourmastodon.instance/admin/pubsubhubbub -* View domain blocks - https://yourmastodon.instance/admin/domain_blocks -* Sidekiq dashboard - https://yourmastodon.instance/sidekiq -* PGHero dashboard for PostgreSQL - https://yourmastodon.instance/pghero -* Edit site settings - https://yourmastodon.instance/admin/settings - -## Site settings - -Your site settings are stored in the `settings` database table, and editable through the admin interface at https://yourmastodon.instance/admin/settings. - -You are able to set the following settings: - -- Site title -- Contact username -- Contact email -- Site description -- 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. +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Administration-guide.md) diff --git a/docs/Running-Mastodon/Development-guide.md b/docs/Running-Mastodon/Development-guide.md index 27efa346..10ed6408 100644 --- a/docs/Running-Mastodon/Development-guide.md +++ b/docs/Running-Mastodon/Development-guide.md @@ -1,50 +1 @@ -Development guide -================= - -**Don't use Docker to do development**. It's a quick way to get Mastodon running in production, it's **really really inconvenient for development**. Normally in Rails development environment you get hot reloading of backend code and on-the-fly compilation of assets like JS and CSS, but you lose those benefits by compiling a Docker image. If you want to contribute to Mastodon, it is worth it to simply set up a proper development environment. - -In fact, all you need is described in the [production guide](Production-guide.md), **with the following exceptions**. You **don't** need: - -- Nginx -- SystemD -- An `.env.production` file. If you need to set any environment variables, you can use an `.env` file -- To prefix any commands with `RAILS_ENV=production` since the default environment is "development" anyway -- Any cronjobs - -The command to install project dependencies does not require any flags, i.e. simply - - bundle install - -By default the development environment wants to connect to a `mastodon_development` database on localhost using your user/ident to login to Postgres (i.e. not a md5 password) - -You can run Mastodon with: - - rails s - -And open `http://localhost:3000` in your browser. Background jobs run inline (aka synchronously) in the development environment, so you don't need to run a Sidekiq process. - -By default, your development environment will have an admin account created for you to use - the email address will be `admin@YOURDOMAIN` (e.g. admin@localhost:3000) and the password will be `mastodonadmin`. - -You can run tests with: - - rspec - -You can check localization status with: - - i18n-tasks health - -You can check code quality with: - - rubocop - -## Development tips - -You can use a localhost->world tunneling service like ngrok if you want to test federation, **however** that should not be your primary mode of operation. If you want to have a permanently federating server, set up a proper instance on a VPS with a domain name, and simply keep it up to date with your own fork of the project while doing development on localhost. - -Ngrok and similar services give you a random domain on each start up. This is good enough to test how the code you're working on handles real-world situations. But as soon as your domain changes, for everybody else concerned you're a different instance than before. - -Generally, federation bits are tricky to work on for exactly this reason - it's hard to test. And when you are testing with a disposable instance you are polluting the databases of the real servers you're testing against, usually not a big deal but can be annoying. The way I have handled this so far was thus: I have used ngrok for one session, and recorded the exchanges from its web interface to create fixtures and test suites. From then on I've been working with those rather than live servers. - -I advise to study the existing code and the RFCs before trying to implement any federation-related changes. It's not *that* difficult, but I think "here be dragons" applies because it's easy to break. - -If your development environment is running remotely (e.g. on a VPS or virtual machine), setting the `REMOTE_DEV` environment variable will swap your instance from using "letter opener" (which launches a local browser) to "letter opener web" (which collects emails and displays them at /letter_opener ). \ No newline at end of file +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Development-guide.md) diff --git a/docs/Running-Mastodon/Heroku-guide.md b/docs/Running-Mastodon/Heroku-guide.md index 4978a20a..aa5abc1f 100644 --- a/docs/Running-Mastodon/Heroku-guide.md +++ b/docs/Running-Mastodon/Heroku-guide.md @@ -1,86 +1 @@ -Heroku guide -============ - -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?button-url=https://github.com/tootsuite/mastodon&template=https://github.com/tootsuite/mastodon) - -Mastodon can be run on a free [Heroku](https://heroku.com) app. It should be -noted this has limited testing and could have unpredictable results. - -## Basic setup - -Click the button above to start creating a Heroku app with the Mastodon repo as -the source. This tells Heroku to use the `app.json` file which does things like -prompt for config variables, set up the right buildpacks, run a postdeploy task, -and add the appropriate addons. - -If you don't use the deploy button and app.json approach, you will need to do -some of that manually. - -## Domain names and SSL - -You can add your domain name to the Heroku app's setting, and then also use -Heroku's (free) auto renewal program for Lets Encrypt certificates, by -requesting a cert from the settings screen. You'll have to point your hostname -DNS at Heroku using the values heroku gives you on this screen, using whatever -method is appropriate for your DNS setup. - -You should set the Heroku config vars of `LOCAL_DOMAIN` to your hostname, and -`LOCAL_HTTPS` to "true" as well. - -## Email - -Consider using [Mailgun](https://mailgun.com) or similar, who offer free plans -that should suit your interests. Look in `production.rb` to see which config -variables need to be set on Heroku for outgoing email to work. - -## File storage - -You will want Amazon S3 for file storage. The only exception is for development -purposes, where you may not care if files are not saved. Follow a guide online -for creating a free Amazon S3 bucket and Access Key, then enter the details. - -If you deploy from the web, the format for all the S3 bits use Paperclip conventions: - -S3 Bucket is just the name of the bucket, e.g. `bucketname` not the full ARN. - -S3 Region is the AWS code for the region e.g. `ap-northeast-1` not the name of the city displayed on the AWS Dashboard. - -To protect the privacy of the users of the your instance, you should have permissons on the your S3 bucket set to no-read and no-write for the public and non-application-specific AWS users, with only one authorized IAM user or group set up to be able to upload or display content. This is an example of an IAM policy used for the S3 bucket used Mastadon instance hentai.loan: - - { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "s3:ListAllMyBuckets" - ], - "Resource": [ - "arn:aws:s3:::*" - ] - }, - { - "Effect": "Allow", - "Action": [ - "s3:*" - ], - "Resource": [ - "arn:aws:s3:::hentailoan”, - "arn:aws:s3:::hentailoan/*" - ] - } - ] - } - - -## Deployment - -You can deploy from the Heroku web interface or from the command line. Run: - - `heroku run rails db:migrate` - -after you first deploy to set up the first database. - -To make yourself an admin, you may need to use the `heroku` CLI application after creating an account online: - - `heroku rake mastodon:make_admin USERNAME=yourUsername` +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Heroku-guide.md) diff --git a/docs/Running-Mastodon/Production-guide.md b/docs/Running-Mastodon/Production-guide.md index 785826ac..08649e9c 100644 --- a/docs/Running-Mastodon/Production-guide.md +++ b/docs/Running-Mastodon/Production-guide.md @@ -1,251 +1 @@ -Production guide -================ - -## Nginx - -Regardless of whether you go with the Docker approach or not, here is an example Nginx server configuration: - -```nginx -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} - -server { - listen 80; - listen [::]:80; - server_name example.com; - return 301 https://$host$request_uri; -} - -server { - listen 443 ssl; - server_name example.com; - - ssl_protocols TLSv1.2; - ssl_ciphers EECDH+AESGCM:EECDH+AES; - ssl_ecdh_curve prime256v1; - ssl_prefer_server_ciphers on; - ssl_session_cache shared:SSL:10m; - - ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; - - keepalive_timeout 70; - sendfile on; - client_max_body_size 0; - gzip off; - - root /home/mastodon/live/public; - - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; - - location / { - try_files $uri @proxy; - } - - location @proxy { - proxy_set_header Host $host; - 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_pass_header Server; - - proxy_pass http://localhost:3000; - proxy_buffering off; - proxy_redirect off; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - - tcp_nodelay on; - } - - location /api/v1/streaming { - proxy_set_header Host $host; - 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_pass http://localhost:4000; - proxy_buffering off; - proxy_redirect off; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - - tcp_nodelay on; - } - - error_page 500 501 502 503 504 /500.html; -} -``` - -## Running in production without Docker - -It is recommended to create a special user for mastodon on the server (you could call the user `mastodon`), though remember to disable outside login for it. You should only be able to get into that user through `sudo su - mastodon`. - -## General dependencies - - sudo apt-get install imagemagick ffmpeg libpq-dev libxml2-dev libxslt1-dev nodejs file git curl - curl -sL https://deb.nodesource.com/setup_4.x | sudo bash - - - sudo apt-get install nodejs - - sudo npm install -g yarn - -## Redis - - sudo apt-get install redis-server redis-tools - -## Postgres - - sudo apt-get install postgresql postgresql-contrib - -Setup a user and database for Mastodon: - - sudo su - postgres - psql - -In the prompt: - - CREATE USER mastodon CREATEDB; - \q - -## Rbenv - -It is recommended to use rbenv (exclusively from the `mastodon` user) to install the desired Ruby version. Follow the guides to [install rbenv][1] and [rbenv-build][2] (I recommend checking the [prerequisites][3] for your system on the rbenv-build project and installing them beforehand, obviously outside the unprivileged `mastodon` user) - -[1]: https://github.com/rbenv/rbenv#installation -[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. - -## Git - -You need the `git-core` package installed on your system. If it is so, from the `mastodon` user: - - cd ~ - git clone https://github.com/tootsuite/mastodon.git live - cd live - -Then you can proceed to install project dependencies: - - gem install bundler - bundle install --deployment --without development test - yarn install - -## Configuration - -Then you have to configure your instance: - - cp .env.production.sample .env.production - nano .env.production - -Fill in the important data, like host/port of the redis database, host/port/username/password of the postgres database, your domain name, SMTP details (e.g. from Mailgun or equivalent transactional e-mail service, many have free tiers), whether you intend to use SSL, etc. If you need to generate secrets, you can use: - - rake secret - -To get a random string. If you are setting up on one single server (most likely), then `REDIS_HOST` is localhost and `DB_HOST` is `/var/run/postgresql`, `DB_USER` is `mastodon` and `DB_NAME` is `mastodon_production` while `DB_PASS` is empty because this setup will use the ident authentication method (system user "mastodon" maps to postgres user "mastodon"). - -## Setup - -And setup the database for the first time, this will create the tables and basic data: - - RAILS_ENV=production bundle exec rails db:setup - -Finally, pre-compile all CSS and JavaScript files: - - RAILS_ENV=production bundle exec rails assets:precompile - -## Systemd - -Example systemd configuration for the web workers, to be placed in `/etc/systemd/system/mastodon-web.service`: - -```systemd -[Unit] -Description=mastodon-web -After=network.target - -[Service] -Type=simple -User=mastodon -WorkingDirectory=/home/mastodon/live -Environment="RAILS_ENV=production" -Environment="PORT=3000" -ExecStart=/home/mastodon/.rbenv/shims/bundle exec puma -C config/puma.rb -TimeoutSec=15 -Restart=always - -[Install] -WantedBy=multi-user.target -``` - -Example systemd configuration for the background workers, to be placed in `/etc/systemd/system/mastodon-sidekiq.service`: - -```systemd -[Unit] -Description=mastodon-sidekiq -After=network.target - -[Service] -Type=simple -User=mastodon -WorkingDirectory=/home/mastodon/live -Environment="RAILS_ENV=production" -Environment="DB_POOL=5" -ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -c 5 -q default -q mailers -q pull -q push -TimeoutSec=15 -Restart=always - -[Install] -WantedBy=multi-user.target -``` - -Example systemd configuration file for the streaming API, to be placed in `/etc/systemd/system/mastodon-streaming.service`: - -```systemd -[Unit] -Description=mastodon-streaming -After=network.target - -[Service] -Type=simple -User=mastodon -WorkingDirectory=/home/mastodon/live -Environment="NODE_ENV=production" -Environment="PORT=4000" -ExecStart=/usr/bin/npm run start -TimeoutSec=15 -Restart=always - -[Install] -WantedBy=multi-user.target -``` - -This allows you to `sudo systemctl enable /etc/systemd/system/mastodon-*.service` and `sudo systemctl start mastodon-web.service mastodon-sidekiq.service mastodon-streaming.service` to get things going. - -## Cronjobs - -I recommend creating a couple cronjobs for the following tasks: - -- `RAILS_ENV=production bundle exec rake mastodon:media:clear` -- `RAILS_ENV=production bundle exec rake mastodon:push:refresh` -- `RAILS_ENV=production bundle exec rake mastodon:feeds:clear` - -You may want to run `which bundle` first and copypaste that full path instead of simply `bundle` in the above commands because cronjobs usually don't have all the paths set. The time and intervals of when to run these jobs are up to you, but once every day should be enough for all. - -You can edit the cronjob file for the `mastodon` user by running `sudo crontab -e -u mastodon` (outside of the mastodon user). - -## Things to look out for when upgrading Mastodon - -You can upgrade Mastodon with a `git pull` from the repository directory. You may need to run: - -- `RAILS_ENV=production bundle exec rails db:migrate` -- `RAILS_ENV=production bundle exec rails assets:precompile` - -Depending on which files changed, e.g. if anything in the `/db/` or `/app/assets` directory changed, respectively. Also, Mastodon runs in memory, so you need to restart it before you see any changes. If you're using systemd, that would be: - - sudo systemctl restart mastodon-*.service +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md) diff --git a/docs/Running-Mastodon/Scalingo-guide.md b/docs/Running-Mastodon/Scalingo-guide.md index 9329f753..8c986f75 100644 --- a/docs/Running-Mastodon/Scalingo-guide.md +++ b/docs/Running-Mastodon/Scalingo-guide.md @@ -1,13 +1 @@ -Scalingo guide -============== - -[![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/tootsuite/mastodon#master) - -1. Click the above button. -2. Fill in the options requested. - * You can use a .scalingo.io domain, which will be simple to set up, or you can use a custom domain. - * You will want Amazon S3 for file storage. The only exception is for development purposes, where you may not care if files are not saved. Follow a guide online for creating a free Amazon S3 bucket and Access Key, then enter the details. - * If you want your Mastodon to be able to send emails, configure SMTP settings here (or later). Consider using [Mailgun](https://mailgun.com) or similar, who offer free plans that should suit your interests. -3. Deploy! The app should be set up, with a working web interface and database. You can change settings and manage versions from the Scalingo dashboard. - -To make yourself an admin, you can use the `scalingo` CLI: `scalingo run -e USERNAME=yourusername rails mastodon:make_admin`. +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Scalingo-guide.md) diff --git a/docs/Running-Mastodon/Tuning.md b/docs/Running-Mastodon/Tuning.md index c4acb992..41fa93ef 100644 --- a/docs/Running-Mastodon/Tuning.md +++ b/docs/Running-Mastodon/Tuning.md @@ -1,104 +1 @@ -Tuning Mastodon -=============== - -Mastodon has three types of processes: - -- web -- streaming API -- background processing - -By default, the web type spawns two worker processes with 5 threads each, the streaming API is a single thread/process with 10 database pool connections, and background processing spawns one process with 5 threads. - -### Web - -The web process serves short-lived HTTP requests for most of the application. The following environment variables control it: - -- `WEB_CONCURRENCY` controls the number of worker processes -- `MAX_THREADS` controls the number of threads per process - -The default is 2 workers with 5 threads each. Threads share the memory of their parent process. Different processes allocate their own memory each. Threads in Ruby are not native threads, so it's more or less: threads equal concurrency, processes equal parallelism. A larger number of threads maxes out your CPU first, a larger number of processes maxes out your RAM first. - -These values affect how many HTTP requests can be served at the same time. When not enough threads are available, requests are queued until they can be answered. - -For a single-user instance, 1 process with 5 threads should be more than enough. - -### Streaming API - -The streaming API handles long-lived HTTP and WebSockets connections, through which clients receive real-time updates. It is a single-threaded process. By default it has a database connection pool of 10, which means 10 different database queries can run *at the same time*. The database is not heavily used in the streaming API, only for initial authentication of the request, and for some special receiver-specific filter queries when receiving new messages. At the time of writing this value cannot be reconfigured, but mostly doesn't need to. - -If you need to scale up the streaming API, spawn more separate processes on different ports (e.g. 4000, 4001, 4003, etc) and load-balance between them with nginx. - -### Background processing - -Many tasks in Mastodon are delegated to background processing to ensure the HTTP requests are fast, and to prevent HTTP request aborts from affecting the execution of those tasks. Sidekiq is a single process, with a configurable numbero of threads. By default, it is 5. That means, 5 different jobs can be executed at the same time. Others will be queued until they can be processed. - -While the amount of threads in the web process affects the responsiveness of the Mastodon instance to the end-user, the amount of threads allocated to background processing affects how quickly posts can be delivered from the author to anyone else, how soon e-mails are sent out, etc. - -The amount of threads is not controlled by an environment variable in this case, but a command line argument in the invocation of Sidekiq: - - bundle exec sidekiq -c 15 -q default -q mailers -q push - -Would start the sidekiq process with 15 threads. Please mind that each threads needs to be able to connect to the database, which means that the database pool needs to be large enough to support all the threads. The database pool size is controlled with the `DB_POOL` environment variable, and defaults to the value of `MAX_THREADS` (therefore, is 5 by default). - -You might notice that the above command specifies three queues to be processed: - -- "default" contains most tasks such as delivering messages to followers and processing incoming notifications from other instances -- "mailers" contains tasks that send e-mails -- "push" contains tasks that deliver messages to other instances - -If you wish, you could start three different processes for each queue, to ensure that even when there is a lot of tasks of one type, important tasks of other types still get executed in a timely manner. - -___ - -### How to set environment variables -#### With systemd - -In the `.service` file: - -```systemd -... -Environment="WEB_CONCURRENCY=1" -Environment="MAX_THREADS=5" -ExecStart="..." -... -``` - -Don't forget to `sudo systemctl daemon-reload` before restarting the services so that the changes would take effect! - -#### With docker-compose - -Edit `docker-compose.yml`: - -```yml -... - web: - restart: always - build: . - env_file: .env.production - environment: - - WEB_CONCURRENCY=1 - - MAX_THREADS=5 -... -``` - -Re-create the containers with `docker-compose up -d` for the changes to take effect. - -You can also scale the number of containers per "service" (where service is "web", "sidekiq" and "streaming"): - - docker-compose scale web=1 sidekiq=2 streaming=3 - -Realistically the `docker-compose.yml` file needs to be modified a bit further for the above to work, because by default it wants to bind the web container to host port 3000 and streaming container to host port 4000, of either of which there is only one on the host system. However, if you change: - -```yml -ports: - - "3000:3000" -``` - -to simply: - -```yml -ports: - - "3000" -``` - -for each service respectively, Docker will allocate random host ports of the services, allowing multiple containers to run alongside each other. But it will be on you to look up which host ports those are (e.g. with `docker ps`), and they will be different on each container restart. +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Tuning-guide.md) diff --git a/docs/Running-Mastodon/Vagrant-guide.md b/docs/Running-Mastodon/Vagrant-guide.md index 83a89240..c5823b09 100644 --- a/docs/Running-Mastodon/Vagrant-guide.md +++ b/docs/Running-Mastodon/Vagrant-guide.md @@ -1,66 +1 @@ -Vagrant guide -============= - -A quick way to get a development environment up and running is with Vagrant. You will need recent versions of [Vagrant](https://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) installed. - -## Basic setup - -Install the latest versions of Vagrant and VirtualBox for your operating systems, and then run: - - vagrant plugin install vagrant-hostsupdater - -This is optional, but will update your 'hosts' file when you start the virtual machine, allowing you to access the site at http://mastodon.dev (instead of http://localhost:3000). - -To create and provision a new virtual machine for Mastodon development: - - git clone git@github.com:tootsuite/mastodon.git - cd mastodon - vagrant up - -**Note:** On Linux hosts, you will need to [enable NFS support](https://www.vagrantup.com/docs/synced-folders/nfs.html). - -Running `vagrant up` for the first time will run provisioning, which will: - -- Download the Ubuntu 14.04 base image, if there isn't already a copy on your machine -- Create a new VirtualBox virtual machine from that image -- Run the provisioning script (located inside the Vagrantfile), which installs the system packages, Ruby gems, and JS modules required for Mastodon -- Run the startup script - -## Starting the server - -The Vagrant box will automatically start after provisioning. It can be started in future with `vagrant up` from the mastodon directory. - -Once the Ubuntu virtual machine has booted, it will run the startup script, which loads the environment variables from `.env.vagrant` and then runs `rails s -d -b 0.0.0.0`. This will start a Rails server. You can then access your development site at http://mastodon.dev (or at http://localhost:3000 if you haven't installed vagrants-hostupdater). By default, your development environment will have an admin account created for you to use - the email address will be `admin@mastodon.dev` and the password will be `mastodonadmin`. - -To stop the server, simply run `vagrant halt`. - -## Using the server - -You should now have a working Mastodon instance, although it will not federate, as it is not publicly accessible. Should you need temporary federation for development and testing, see the Ngrok information in the [Development Guide](Development-guide.md). - -By default, your instance's ActionMailer will use "Letter Opener Web" for email. This means that any email that would normally be sent, will instead be stored, and accessible at http://mastodon.dev/letter_opener - you can use this to verify a registered user account. - -## Making changes/developing - -You are able to set environment variables, which are used for Mastodon configuration, by editing the `.env.vagrant` file. Any changes you make will take effect after a Vagrant restart. - -Vagrant has mounted your mastodon folder inside the virtual machine. This means that any change to the files in the folder(e.g. the Rails controllers or the React components in /app) should immediately take effect on the live server. This allows you to make and test changes, and create new commits, without ever needing to access the virtual machine. - -Should you need to access the virtual machine (for example, to manually restart the Rails process without restarting the box), run `vagrant ssh` from the mastodon folder. You will now be logged in as the `vagrant` user on the VirtualBox Ubuntu VM. You will want to `cd /vagrant` to see the app folder. - -## Debugging - -You can find the Rails server logs in in the `log` folder, which will often have the information you need. - -If your Mastodon instance or Vagrant box are really not behaving, you can re-run the provisioning process. Stop the box with `vagrant halt`, and then run `vagrant destroy` - this will delete the virtual machine. You may then run `vagrant up` to create a new box, and re-run provisioning. - -## Testing - -To run the `rspec` tests and `rubocop` style checker, you may either: - -* Install the relevant gems locally, or -* SSH into the virtual machine, `cd /vagrant`, and then run the commands - -## Support/help - -If you are confused, or having any issues with the above, the Mastodon IRC channel ( irc.freenode.net #mastodon ) is a good place to find assistance. +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Vagrant-guide.md) diff --git a/docs/Specs-and-RFCs-used.md b/docs/Specs-and-RFCs-used.md index 9bb1bb62..89a4dd31 100644 --- a/docs/Specs-and-RFCs-used.md +++ b/docs/Specs-and-RFCs-used.md @@ -1,12 +1 @@ -Specs and RFCs used -=================== - -* [OStatus](https://www.w3.org/community/ostatus/wiki/images/9/93/OStatus_1.0_Draft_2.pdf) -* [Salmon](http://www.salmon-protocol.org/salmon-protocol-summary) -* [Portable Contacts](https://web.archive.org/web/20160305010620/http://portablecontacts.net/draft-spec.html) -* [Atom](https://tools.ietf.org/html/rfc4287) -* [Atom ActivityStreams](http://activitystrea.ms/specs/atom/1.0/) -* [Atom Threading](https://tools.ietf.org/html/rfc4685) -* [PubSubHubbub](https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html) -* [Webfinger](https://tools.ietf.org/html/rfc7033) -* [Link-based Resource Descriptor Discovery](https://tools.ietf.org/html/rfc6415) +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Specs-and-RFCs-used.md) diff --git a/docs/Using-Mastodon/2FA.md b/docs/Using-Mastodon/2FA.md index 663563a2..d5c6985b 100644 --- a/docs/Using-Mastodon/2FA.md +++ b/docs/Using-Mastodon/2FA.md @@ -1,44 +1 @@ -# 2-Factor Authentication - -2-Factor Authentication is a security mechanism that requires you to enter a computer generated code from your phone every time you log into Mastodon. - -We highly recommend that you set up 2-factor authentication as it prevents malicious users from logging into your account if they obtain your password. - -## Warning - -If you lose access to your 2-factor authentication (such as by losing your phone or performing a factory reset) and you do cannot log in, you will not be able to access your account and will need to contact an instance admin to remove 2-factor authentication from your account. - -## Setup - -1. Open your [settings page](https://mastodon.social/settings/two_factor_auth) and navigate to the Two-factor Authentication page -2. Press the big blue "Enable" button that appears on the right ![screenshot](screenshots/2fa/enable.png) -3. Follow instructions below to install an authenticator for your smartphone - -## Android - -__Recommended Application:__ [Google -Authenticator](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2) - -4. Download the above application on your phone -5. Open the "Authenticator" app -6. Press the + button in the bottom right-hand corner -7. Press "Scan a barcode" -8. Line up the black and white QR code with the target box that appears on your camera -9. Now, whenever you log in to Mastodon, open the Authenticator app and enter the 6 digit code that appears above the "mastodon.social (email address)" text - -## iPhone - -__Recommended Application:__ iPhone: [Authenticator by Matt -Ruben](https://itunes.apple.com/us/app/authenticator/id766157276?mt=8) - -4. Download the above application on your phone -5. Open the "Authenticator" app -6. Press the + button in the bottom right-hand corner -7. Authenticator should prompt you for access to your camera- hit "OK" -8. Line up the black and white QR code with the target box that appears on your camera -9. Now, whenever you log in to Mastodon, open the Authenticator app and enter the 6 digit code that appears above the "mastodon.social (email address)" text - -# Disabling 2-factor Authentication - -1. Go to [the 2-factor authentication settings page](https://mastodon.social/settings/two_factor_auth) -2. Press the big blue "Disable" button underneath your QR code ![disable button screenshot](screenshots/2fa/disable.png) +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/2FA.md) diff --git a/docs/Using-Mastodon/Apps.md b/docs/Using-Mastodon/Apps.md index b5e1fa36..c2ced445 100644 --- a/docs/Using-Mastodon/Apps.md +++ b/docs/Using-Mastodon/Apps.md @@ -1,18 +1 @@ -List of apps -============ - -Some people have started working on apps for the Mastodon API. Here is a list of them: - -|App|Platform|Link|Developer(s)| -|---|--------|----|------------| -|[Tusky](https://play.google.com/store/apps/details?id=com.keylesspalace.tusky)|Android||[@Vavassor@mastodon.social](https://mastodon.social/users/Vavassor)| -|mastodroid|Android||[@charlag@mastodon.social](https://mastodon.social/users/charlag)| -|TootyFruity|Android||[@eggplant@mastodon.social](https://mastodon.social/users/eggplant)| -|11t|iOS/Android||[@jeroensmeets@mastodon.social](https://mastodon.social/users/jeroensmeets)| -|[Amaroq](https://itunes.apple.com/us/app/amarok-for-mastodon/id1214116200?ls=1&mt=8)|iOS||[@eurasierboy@mastodon.social](https://mastodon.social/users/eurasierboy)| -|Albatross|iOS||[@goldie_ice@mastodon.social](https://mastodon.social/users/goldie_ice)| -|Tooter|Chrome||[@effy@mastodon.social](https://mastodon.social/users/effy)| -|tootstream|CLI||[@Raccoon@mastodon.social](https://mastodon.social/users/Raccoon)| -|HackerNewsBot|CLI||[@rayalez@hackertribe.io](https://hackertribe.io/users/rayalez)| - -If you have a project like this, let me know so I can add it to the list! +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md) diff --git a/docs/Using-Mastodon/FAQ.md b/docs/Using-Mastodon/FAQ.md index 3b03a8ee..d50e63b9 100644 --- a/docs/Using-Mastodon/FAQ.md +++ b/docs/Using-Mastodon/FAQ.md @@ -1,44 +1 @@ -Frequently Asked Questions -========================== - -#### What is a Mastodon? - -A prehistoric animal, predecessor of the mammoth. - -#### Why the name Mastodon? - -There's a progressive metal band with the same name that I'm a fan of that brought the animal to my attention. I thought it's a pretty cool name/animal. - -#### How exactly is it decentralized? - -There are different ways in which something can be decentralized; in this case, Mastodon is the "federated" kind. Think e-mail, not BitTorrent. There are different servers (instances), users have an account on one of them, but can interact and follow each other regardless of where their account is. - -#### Technically, how does the federation work? - -We are using the OStatus suite of protocols: - -1. Webfinger for user-on-domain lookup -2. Atom feeds with ActivityStreams, Portable Contacts, Threads extensions for the actual content -3. PubSubHubbub for subscribing to Atom feeds -4. Salmon for delivering certain items from the Atom feeds to interested parties such as the mentioned user, author of the status being replied to, person being followed, etc - -#### What is mastodon.social? - -The "flagship" instance of Mastodon, aka the server I run myself with the latest code. It's not supposed to be the only instance in the end. - -#### What else is part of the federated network? - -Let's call it the "fediverse". It has existed for a longer while, populated by GNU social servers, Friendica, Hubzilla, Diaspora etc. Not every one of those servers is fully compatible with every other. Mastodon strives to be fully standards-compliant and compatibility with GNU social is higher in priority than the others. - -#### I tried logging into a GNU social client app with Mastodon and it didn't work, why? - -While Mastodon is compatible with GNU social in terms of server to server communication, the client to server API (aka how you access Mastodon) is different. Therefore, client apps that were made for specifically GNU social will not work with Mastodon. The reason for this is half technical, half ideological. - -Because Mastodon has been created from a blank slate, it is much simpler to have the API mirror internal structures as closely as possible, rather than build an emulation layer. Secondly, the GNU social client API is actually a half-way implementation of the legacy Twitter API - that's the reason why it works with some older Twitter client apps. However, many of those apps are not maintained anymore, the GNU social API does not actually keep up with the real Twitter API and never fully implemented all its features; at the same time, the Twitter API was never meant for a federated service and so obscures some of the functionality. - - -#### How is Mastodon funded? - -Development of Mastodon and hosting of mastodon.social is funded through my [Patreon (also BTC/PayPal donations)](https://www.patreon.com/user?u=619786). Beyond that, I am not interested in VC funding, monetizing, advertising, or anything of that sort. I could offer setup/maintenance services on demand. - -The software is free and open source and communities should host their own servers if they can, that way the costs are more or less distributed. Obviously it'd be hard for me to pay the bills if literally everyone decided to use the mastodon.social instance only. +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md) diff --git a/docs/Using-Mastodon/List-of-Mastodon-instances.md b/docs/Using-Mastodon/List-of-Mastodon-instances.md index db35edb1..0e2d0864 100644 --- a/docs/Using-Mastodon/List-of-Mastodon-instances.md +++ b/docs/Using-Mastodon/List-of-Mastodon-instances.md @@ -1,82 +1 @@ -List of Known Mastodon instances -========================== - -There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz) showing realtime information about instances. - -| Name | Theme/Notes, if applicable | Open Registrations | IPv6 | -| -------------|-------------|---|---| -| [mastodon.social](https://mastodon.social) |Flagship, quick updates|No|No| -| [securitymastod.one](https://securitymastod.one/) |Information security enthusiasts and pros|Yes|Yes| -| [mastodon.nuzgo.net](https://mastodon.nuzgo.net/) |Mastodon instance hosted in Paris |Yes|Yes| -| [mastodon.cx](https://mastodon.cx/) |Alternative Mastodon instance hosted in France|Yes|Yes| -| [mastodon.network](https://mastodon.network) |N/A|Yes|Yes| -| [awoo.space](https://awoo.space) |Intentionally moderated, only federates with mastodon.social|Yes|No| -| [animalliberation.social](https://animalliberation.social) |Animal Rights|Yes|No| -| [socially.constructed.space](https://socially.constructed.space) |Single user|No|No| -| [epiktistes.com](https://epiktistes.com) |N/A|Yes|No| -| [fern.surgeplay.com](https://fern.surgeplay.com) |Federates everywhere, Minecraft-focused|Yes|No -| [gay.crime.team](https://gay.crime.team) |the place for doin' gay crime online (please don't actually do crime here)|No|No| -| [icosahedron.website](https://icosahedron.website/) |Icosahedron-themed (well, visually), open registration.|Yes|No| -| [memetastic.space](https://memetastic.space) |Memes|Yes|No| -| [masto.razrnet.fr](https://masto.razrnet.fr) |Instance Française pour tout le monde ! Développeurs, gamers, etc...|Yes|No| -| [social.diskseven.com](https://social.diskseven.com) |Single user|No|Yes| -| [social.gestaltzerfall.net](https://social.gestaltzerfall.net) |Single user|No|No| -| [mastodon.xyz](https://mastodon.xyz) |N/A|Yes|Yes| -| [mastodon.land](https://mastodon.land) |N/A|Yes|Yes| -| [mastodon.partipirate.org](https://mastodon.partipirate.org) |French Pirate Party Instance - Politics and stuff|Yes|No| -| [social.targaryen.house](https://social.targaryen.house) |Federates everywhere, quick updates.|Yes|Yes| -| [masto.themimitoof.fr](https://masto.themimitoof.fr) |N/A|Yes|Yes| -| [mstdn.io](https://mstdn.io) |N/A|Yes|Yes| -| [social.imirhil.fr](https://social.imirhil.fr) |N/A|No|Yes| -| [social.wxcafe.net](https://social.wxcafe.net) |Open registrations, queer people, activists, safe as much as possible |Yes|Yes| -| [octodon.social](https://octodon.social) |Open registrations, federates everywhere, cutest instance yet|Yes|Yes| -| [mastodon.club](https://mastodon.club)|Open Registration, Open Federation, Mostly Canadians|Yes|No| -| [mastodon.irish](https://mastodon.irish)|Open Registration|Yes|No| -| [hostux.social](https://hostux.social) |N/A|Yes|Yes| -| [social.alex73630.xyz](https://social.alex73630.xyz) |Francophones|Yes|Yes| -| [oc.todon.fr](https://oc.todon.fr) |Modérée et principalement francophone, pas de tolérances pour misogynie/LGBTphobies/validisme/etc.|Yes|Yes| -| [maly.io](https://maly.io) |N/A|Yes|No| -| [social.lou.lt](https://social.lou.lt) |Francophones|Yes|No| -| [mastodon.ninetailed.uk](https://mastodon.ninetailed.uk) |Open registrations, furry-friendly, UK-based|Yes|No| -| [soc.louiz.org](https://soc.louiz.org) |"Coucou"|Yes|No| -| [7nw.eu](https://7nw.eu) |N/A|Yes|No| -| [mastodon.gougere.fr](https://mastodon.gougere.fr)|N/A|Yes|No| -| [aleph.land](https://aleph.land)|N/A|Yes|No| -| [share.elouworld.org](https://share.elouworld.org)|N/A|No|No| -| [social.lkw.tf](https://social.lkw.tf)|N/A|No|No| -| [manowar.social](https://manowar.social)|N/A|No|No| -| [social.ballpointcarrot.net](https://social.ballpointcarrot.net)|N/A|No|No| -| [social.nasqueron.org](https://social.nasqueron.org) |Dreamers, open source developers, free culture|Yes|Yes| -| [status.dissidence.ovh](https://status.dissidence.ovh)|N/A|Yes|Yes| -| [mastodon.cc](https://mastodon.cc)|Art|Yes|No| -| [mastodon.technology](https://mastodon.technology)|Open registrations, federates everywhere, for tech folks|Yes|No| -| [mastodon.systemlab.fr](https://mastodon.systemlab.fr/)|Le mastodon Français, informatique, jeux-vidéos, gaming et hébergement.|Yes| -| [mastodon.top](https://mastodon.top) |N/A|Yes|Yes| -| [niu.moe](https://niu.moe/)|:dolls: The most cutest node ever, FR/EN, anime and computer :balloon:|Yes|Yes| -| [im-in.space](https://im-in.space/)|SPAAAAACE! Probably with a lot of French people. (Invite-only, might randomly open registrations)|No|Yes| -| [social.bytestemplar.com](https://social.bytestemplar.com)|N/A|Yes|No| -| [digitalhumanities.club](http://www.digitalhumanities.club)|[Digital humanities](http://whatisdigitalhumanities.com) community; invitations will open once code of conduct drafted.|No|No -| [design.vu](https://design.vu)|— what's your design view‽|Yes|No| -| [masto.raildecake.fr](https://masto.raildecake.fr)|Hebergé chez un FAI associatif dans le sud de la france, grillons & pins en options|Yes|No| -| [good-dragon.com](https://good-dragon.com/)|Quick updates, Relaxed Moderation, Federates Everywhere, Furries|Yes|No| -| [rich.gop](https://rich.gop/)|Federates everywhere, Open registration, Privacy respected|Yes|Yes| -| [social.nowa.re](https://social.nowa.re)|Open Registration|Yes|No| -| [mastodon.ml](http://mastodon.ml) |A chill place to hangout and chat about anime, programming and movies.|Yes|Yes| -| [off-the-clock.us](https://off-the-clock.us/)|The work day is over.|Yes|No| -| [infinimatix.net](https://infinimatix.net)|Informatics|Yes|Yes| -| [social.0day.agency](https://social.0day.agency)|Infosec, Hacking, Fun (only protonmail)|Yes|Yes| -| [kagrumez.lerk.io](https://kagrumez.lerk.io)|Open registration. German end english.|Yes|No| -| [meow.social](https://meow.social)|A furry fandom focused instance|Yes|No| -| [neumastodon.com](https://neumastodon.com/)|Northeastern University Mastodon |Yes|No| -| [dancingbanana.party](https://dancingbanana.party)|La banane qui danse.|Yes|No| -| [mastodon.brussels](https://mastodon.brussels/)|Le mastodon pour les belges, si vous aimez la bonne ambiance venez nous rejoindre !|Yes|Yes| -| [mastodon.llamasweet.tech](https://mastodon.llamasweet.tech/)|Mastodon about Android developement|Yes|No| -| [manx.social](https://manx.social/)|Instance for the Isle of Man|Yes|Yes| -| [mastodon.host](https://mastodon.host/)|Lightly moderated, federates everywhere and has a follow bot ( Huge federated timeline )|Yes|No| -| [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| -| [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 +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md) diff --git a/docs/Using-Mastodon/User-guide.md b/docs/Using-Mastodon/User-guide.md index e456de29..7ef5a117 100644 --- a/docs/Using-Mastodon/User-guide.md +++ b/docs/Using-Mastodon/User-guide.md @@ -1,206 +1 @@ -Mastodon User's Guide -===================== - -* [Intro](User-guide.md#intro) - * [Decentralization and Federation](User-guide.md#decentralization-and-federation) -* [Getting Started](User-guide.md#getting-started) - * [Setting Up Your Profile](User-guide.md#setting-up-your-profile) - * [E-Mail Notifications](User-guide.md#e-mail-notifications) - * [Text Posts](User-guide.md#text-posts) - * [Content Warnings](User-guide.md#content-warnings) - * [Hashtags](User-guide.md#hashtags) - * [Boosts and Favourites](User-guide.md#boosts-and-favourites) - * [Posting Images](User-guide.md#posting-images) - * [Following Other Users](User-guide.md#following-other-users) - * [Notifications](User-guide.md#notifications) - * [Mobile Apps](User-guide.md#mobile-apps) - * [The Federated Timeline](User-guide.md#the-federated-timeline) - * [The Local Timeline](User-guide.md#the-local-timeline) - * [Searching](User-guide.md#searching) -* [Privacy, Safety and Security](User-guide.md#privacy-safety-and-security) - * [Two-Factor Authentication](User-guide.md#two-factor-authentication) - * [Account Privacy](User-guide.md#account-privacy) - * [Toot Privacy](User-guide.md#toot-privacy) - * [Blocking](User-guide.md#blocking) - * [Reporting Toots or Users](User-guide.md#reporting-toots-or-users) - -## Intro - -Mastodon is a social network application based on the OStatus protocol. It behaves a lot like other social networks, especially Twitter, with one key difference - it is open-source and anyone can start their own server (also called an "*instance*"), and users of any instance can interact freely with those of other instances (called "*federation*"). Thus, it is possible for small communities to set up their own servers to use amongst themselves while also allowing interaction with other communities. - -#### Decentralization and Federation - -Mastodon is a system decentralized through a concept called "*federation*" - rather than depending on a single person or organization to run its infrastructure, anyone can download and run the software and run their own server. Federation means different Mastodon servers can interact with each other seamlessly, similar to e.g. e-mail. - -As such, anyone can download Mastodon and e.g. run it for a small community of people, but any user registered on that instance can follow, send, and read posts from other Mastodon instances (as well as servers running other OStatus-compatible services, such as GNU Social and postActiv). This means that not only is users' data not inherently owned by a company with an interest in selling it to advertisers, but also that if any given server shuts down its users can set up a new one or migrate to another instance, rather than the entire service being lost. - -Within each Mastodon instance, usernames just appear as `@username`, similar to other services such as Twitter. Users from other instances appear, and can be searched for and followed, as `@user@servername.ext` - so e.g. `@gargron` on the `mastodon.social` instance can be followed from other instances as `@gargron@mastodon.social`). - -Posts from users on external instances are "*federated*" into the local one, i.e. if `user1@mastodon1` follows `user2@gnusocial2`, any posts `user2@gnusocial2` makes appear in both `user1@mastodon1`'s Home feed and the public timeline on the `mastodon1` server. Mastodon server administrators have some control over this and can exclude users' posts from appearing on the public timeline; post privacy settings from users on Mastodon instances also affect this, see below in the [Toot Privacy](User-guide.md#toot-privacy) section. - -## Getting Started - -#### Setting Up Your Profile - -You can customise your Mastodon profile in a number of ways - you can set a custom "display" name, a profile "avatar" picture, a background image for your profile page header, and a short "bio" that summarises you or your account. - -![Preferences icon](screenshots/preferences.png) To edit your profile, click the Preferences icon in the Compose column and select "Edit Profile" on the left-hand menu on the Preferences page. Your display name is limited to 30 characters, your bio to 160. Avatars and header pictures can be uploaded as png, gif or jpg images and cannot be larger than 2MB. They will be resized to standard sizes - 120x120 pixels for avatars, 700x335 pixels for header pictures. - -#### E-Mail Notifications - -![Preferences icon](screenshots/preferences.png) Mastodon can notify you of activity via e-mail if you so choose. To adjust your settings for receiving e-mail notifications, click the Preferences icon in the Compose column and select the "Preferences" page from the left-hand menu. Here you will find a number of checkboxes to enable or disable e-mail notifications for various types of activity. - -#### Text Posts - -The most basic way to interact with Mastodon is to make a text post, also called a *Toot*. In order to toot, simply enter the message you want to post into the "What is on your mind?" text box in the Compose column and click "TOOT". There is a limit of up to 500 characters per toot; if you really do need more than this you can reply to your own toots so they will appear like a conversation. - -If you want to reply to another user's toot, you can click the "Reply" icon on it. This will add their username to your input box along with a preview of the message you're replying to, and the user will receive a notification of your response. - -Similarly, in order to start a conversation with another user, just mention their user name in your toot. When you type the @ symbol followed directly (without a space) by any character in a message, Mastodon will automatically start suggesting users that match the username you're typing. Like with replies, mentioning a user like this will send them a notification. If the post starts with a mention, it will be treated as a reply and will only appear in the Home timelines of users who follow both you and the user you are mentioning. It will still be visible on your profile depending on privacy settings. - -##### Content Warnings - -When you want to post something that you don't want to be immediately visible - for example, spoilers for that film that's just come out, or some personal thoughts that mention potentially upsetting topics, you can "hide" it behind a Content Warning. - -To do this, click the ![CW icon](screenshots/compose-cw.png) "CW" switch under the Compose box. This will add another text box labeled "Content warning"; you should enter a short summary of what the "body" of your post contains here while your actual post goes into the "What is on your mind?" box as normal. - -![animation showing how to enable content warnings](screenshots/content-warning.gif) - -This will cause the body of your post to be hidden behind a "Show More" button in the timeline, with only the content warning and any mentioned users visible by default: - -![animation showing content warnings in the timeline](screenshots/cw-toot.gif) - -**NOTE** that this will not hide images included in your post - images can be marked as "sensitive" separately to hide them from view until clicked on. To find out how to do this, see the [Posting Images](User-guide.md#posting-images) section of this user guide. - -##### Hashtags - -If you're making a post belonging to a wider subject, it might be worth adding a "hashtag" to it. This can be done simply by writing in the post a # sign followed by a phrase, e.g. #introductions (which is popular on mastodon.social for new users to introduce themselves to the community), or #politics for political discussions, etc. Clicking on a hashtag in a toot will show a timeline consisting only of public posts that include this hashtag (i.e. it's a shortcut to searching for it). This allows users to group messages of similar subjects together, forming a separate "timeline" for people interested in that subject. Hashtags can also be searched for from the search bar above the compose box. - -##### Boosts and Favourites - -You can *favourite* another user's toot by clicking the star icon underneath. This will send the user a notification that you have marked their post as a favourite; the meaning of this varies widely by context from a general "I'm listening" to signalling agreement or offering support for the ideas expressed. - -Additionally you can *boost* toots by clicking the "circular arrows" icon. Boosting a toot will show it on your profile timeline and make it appear to all your followers, even if they aren't following the user who made the original post. This is helpful if someone posts a message you think others should see, as it increases the message's reach while keeping the author information intact. - -#### Posting Images - -![Image icon](screenshots/compose-media.png) In order to post an image, simply click or tap the "image" icon in your Compose column and select a file to upload. - -If the image is "not safe for work" or has otherwise sensitive content, you can select the ![NSFW toggle](screenshots/compose-nsfw.png) "NSFW" button which appears once you have added an image. This will hide the image in your post by default, making it clickable to show the preview. This is the "visual" version of [content warnings](User-guide.md#content-warnings) and could be combined with them if there is text to accompany the image - otherwise it's fine to just mark the image as sensitive and make the body of your post the content warning. - -You can also attach video files or GIF animations to Toots. However, there is a 4MB file size limit for these files and videos must be in .webm or .mp4 format. - -#### Following Other Users - -Following another user will make all of their toots as well as other users' toots which they [boost](User-guide.md#boosts-and-favourites) appear in your Home column. This gives you a separate timeline from the [public timelines](User-guide.md#the-public-timelines) in which you can read what particular people are up to without the noise of general conversation. - -![Follow icon](screenshots/follow.png) In order to follow a user, click their name or avatar to open their profile, then click the Follow icon in the top left of their profile view. - -If their account has a padlock icon ![Padlock icon](screenshots/locked-icon.png) next to their user name, they will receive a notification of your request to follow them and they will need to approve this before you are added to their follower list (and thus see their toots). To show you that you are waiting for someone to approve your follow request, the Follow icon ![Follow icon](screenshots/follow-icon.png) on their profile will be replaced with an hourglass icon ![Pending icon](screenshots/pending-icon.png). The requirement for new followers to be approved is something you can enable for your own profile under preferences. - -Once you follow a user, the Follow icon will be highlighted in blue on their profile ![Following icon](screenshots/following-icon.png); you can unfollow them again by clicking this. - -If you know someone's user name you can also open their profile for following by entering it in the [Search box](User-guide.md#searching) in the Compose column. This also works for remote users, though depending on whether they are known to your home instance you might have to enter their full name including the domain (e.g. `gargron@mastodon.social`) into the search box before their profile will appear in the suggestions. - -Alternately, if you already have a user's profile open in a separate browser tab, most OStatus-related networks should have a "Follow" or "Subscribe" button on their profile page. This will ask you to enter the full user name to follow **from** (ie. if your account is on mastodon.social you would want to enter this as `myaccount@mastodon.social`) - -#### Notifications - -When someone follows your account or requests to follow you, mentions your user name, or boosts or favourites one of your toots, you will receive a notification for this. These will appear as desktop notifications on your computer (if your web browser supports this and you've enabled them) as well as in your "Notifications" column. - -![Notification Settings icon](screenshots/notifications-settings.png) You can filter what kind of notifications you see in the Notifications column by clicking the Notification Settings icon at the top of the column and ticking or un-ticking what you do or don't want to see notifications for. - -![Clear icon](screenshots/notifications-clear.png) If your notifications become cluttered, you can clear the column by clicking the Clear icon at the top of the column; this will wipe its contents. - -![Preferences icon](screenshots/preferences.png) You can also disable notifications from people you don't follow or who don't follow you entirely - to do this, click the Preferences icon in the Compose column, select "Preferences" on the left-hand menu and check either of the respective "Block notifications" options. - -#### Mobile Apps - -Mastodon has an open API, so anyone can develop a client or app to use Mastodon from anything. Many people have already developed mobile apps for iOS and Android. You can find a list of these [here](Apps.md). Many of these projects are also open source and welcome collaborators. - -#### The Public Timelines - -In addition to your Home timeline, there are two public timelines available. The Federated Timeline and the Local Timeline. These are both a good way to meet new people to follow or interact with. - -##### The Federated Timeline - -The Federated Timeline shows all public posts from all users "known" to your instance. This means the user is either on the same instance as you, or somebody on your instance follows that user. The Federated Timeline is a great way to engage in the broad chatter of the world. Following users on remote instances who you meet on the Federated Timeline can lead to meeting more users on more instances and further connecting your instance to more and more of the entire Mastodon and OStatus network. - -![Federated Timeline icon](screenshots/federated-timeline.png) To view the federated timeline, click the "Federated Timeline" icon in your Compose column or the respective button on the Getting Started panel. To hide the federated timeline again, simply click the "Back" link at the top of the column while you're viewing it. - -#### The Local Timeline - -The Local Timeline only shows public posts made by users on your home instance. This can be useful if your instance has particular community norms that users on other instances may not have, such as particular topics that get put under content warnings; or particular in-jokes and shared interests. To view the Local Timeline, click the ![Menu icon](screenshots/compose-menu.png) Menu icon on the Compose pane and then select "Local Timeline" on the rightmost column. - -#### Searching - -Mastodon has a search function - you can use it to search for users and [hashtags](User-guide.md#hashtags). The search does not look through the entire text of posts, only hashtags. In order to start a search, just type into the search box in the Compose column and hit *enter*; This will open the search pane. The search pane will show suggestions as you type. Selecting any of these will open the user's profile or a view of all toots on the hashtag. - -## Privacy, Safety and Security - -Mastodon has a number of advanced security, privacy and safety features over more public networks such as Twitter. Particularly the privacy controls are fairly granular; this section will explain how these features work. - -#### Two-Factor Authentication - -Two-Factor Authentication (2FA) is a mechanism that improves the security of your Mastodon account by requiring a numeric code from another device (most commonly mobile phones) linked to your Mastodon account when you log in - this means that even if someone gets hold of both your e-mail address and your password, they cannot take over your Mastodon account as they would need a physical device you own to log in. - -Mastodon's 2FA uses Google Authenticator (or compatible apps, such as Authy). You can install this for free to your [Android](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2) or [iOS](https://itunes.apple.com/gb/app/google-authenticator/id388497605) device; [this Wikipedia page](https://en.wikipedia.org/wiki/Google_Authenticator#Implementations) lists further versions of the app for other systems. - -![Preferences icon](screenshots/preferences.png) In order to enable 2FA for your Mastodon account, click the Preferences icon in the Compose column, click "Two-factor Authentication" in the left menu on the settings page and follow the instructions. Once activated, every time you log in you will need a one-time code generated by the Authenticator app on the device you've linked to your account. - -#### Account Privacy - -To allow you more control over who can see your toots, Mastodon supports "private" or "locked" accounts. If your account is set to private, you will be notified every time someone tries to follow you, and you will be able to allow or deny the follow request. Additionally, if your account is private, any new toots you compose will default to being private (see the [Toot Privacy](User-guide.md#toot-privacy) section below). - -![Preferences icon](screenshots/preferences.png) To make your account private, click the Preferences icon in the Compose pane, select "Edit Profile" and tick the "Make account private" checkbox, then click "Save Changes". - -![Screenshot of the "Private Account" setting](screenshots/private.png) - -#### Toot Privacy - -Toot privacy is handled independently of account privacy, and individually for each toot. The four tiers of visibility for toots are Public (default), Unlisted, Private, and Direct. In order to select your privacy level, click the ![Globe icon](screenshots/compose-privacy.png) globe icon. Changes to this setting are remembered between posts, i.e. if you make one private toot, each toot you make will be private until you change it back to public. You can change your default post privacy under preferences. - -**Public** is the default status of toots on most accounts. Public toots are visible to any other user on the public timelines, federate to other Mastodon and OStatus instances without restriction, and appear on your user profile page to anyone including search engine bots and visitors who aren't logged into a Mastodon account. - -**Unlisted** toots are public, except that they do not appear in the public timelines or search results. They are visible to anyone following you and appear on your profile page to the public even without a Mastodon login. Other than not appearing in the public timelines or search results, they function identically to public posts. - -**Private** toots do not appear in the public timeline nor on your profile page to anyone viewing it unless they are on your Followers list. The option is of limited use if your account is not also set to require approval of new followers (as anyone can follow you without confirmation and thus see your private toots). However the separation of this means that if you *do* set your entire account to private, you can switch this option off on a toot to make unlisted or even public toots from your otherwise private account. - -Private toots cannot be boosted. If someone you follow makes a private toot, it will appear in your timeline with a padlock icon in place of the Boost icon. **NOTE** that remote instances may not respect this. - -Private toots do not federate to other instances, unless you @mention a remote user. In this case, they will federate to their instance, and users on that instance who follow both you and the @mentioned user will see it in their Home timelines. There is no reliable way to check if an instance will actually respect post privacy. Non-Mastodon servers, such as a GNU Social server, do not support Mastodon privacy settings. A user on GNU Social who you @mention in a private post would not even be aware that the post is intended to be private and would be able to boost it, which would undo the privacy setting. There is also no way to guarantee that someone could not just modify the code on their particular Mastodon instance to not respect private post restrictions. A warning will be displayed if you're composing a private toot that will federate to another instance. You should thus think through how much you trust the user you are @mentioning and the instance they are on. - -Private posts are not encrypted. Make sure you trust your instance admin not to just read your private posts on the back-end. Do not say anything you would not want potentially intercepted. - -**Direct** posts are only visible to users you have @mentioned in them and cannot be boosted. Like with private posts, you should be mindful that the remote instance may not respect this protocol. If you are discussing a sensitive matter you should move the conversation off of Mastodon. - -To summarise: - -Toot Privacy | Visible on Profile | Visible on Public Timeline | Federates to other instances ------------- | ------------------ | -------------------------- | --------------------------- -Public | Anyone incl. anonymous viewers | Yes | Yes -Unlisted | Anyone incl. anonymous viewers | No | Yes -Private | Followers only | No | Only remote @mentions -Direct | No | No | Only remote @mentions - -#### Blocking - -You can block a user to stop them contacting you. To do this, you can click or tap the Menu icon on either a toot of theirs or their profile view and select "Block". - -**NOTE** that this will stop them from seeing your public toots while they are logged in, but they *will* be able to see your public toots by simply opening your profile in another browser that isn't logged into Mastodon (or logged into a different account that you have not blocked). - -Mentions, favourites, boosts or any other interaction with you from a blocked user will be hidden from your view. You will not see replies to a blocked person, even if the reply mentions you, nor will you see their toots if someone boosts them. - -The blocked user will not be notified of your blocking them. They will be removed from your followers. - -#### Muting - -If you do not wish to see posts from a particular user, but do not care about if they see your posts, you may choose to *mute* them. You can mute a user from the same menu on their profile page that you would block them from. You will not see posts from a muted user unless they @mention you. A muted user will have no way to know that you have them muted. - -#### Reporting Toots or Users - -If you encounter a toot or a user that is breaking the rules of your instance or that you otherwise want to draw the instance administrators' attention to (e.g. if someone is harassing another user, spamming pornography or posting illegal content), you can click the "..." menu button on the toot or the "hamburger" menu on the profile and select to report this. The rightmost column will then switch over to the following form: - -![Report form](screenshots/report.png) - -In this form, you can select any toots you would like to report to the instance administrators and fill in any comment that might be helpful in identifying or handling the issue (from "is a spammer" to "this post contains untagged pornography"). The report will be visible to server administrators once it is sent so they can take appropriate action, for example hiding the user's posts from the public timeline or banning their account. +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/User-guide.md) diff --git a/docs/Using-Mastodon/screenshots/2fa/disable.png b/docs/Using-Mastodon/screenshots/2fa/disable.png deleted file mode 100644 index f008c27b..00000000 Binary files a/docs/Using-Mastodon/screenshots/2fa/disable.png and /dev/null differ diff --git a/docs/Using-Mastodon/screenshots/2fa/enable.png b/docs/Using-Mastodon/screenshots/2fa/enable.png deleted file mode 100644 index ea82a0af..00000000 Binary files a/docs/Using-Mastodon/screenshots/2fa/enable.png and /dev/null differ diff --git a/docs/Using-Mastodon/screenshots/compose-cw.png b/docs/Using-Mastodon/screenshots/compose-cw.png deleted file mode 100644 index 584080a5..00000000 Binary files a/docs/Using-Mastodon/screenshots/compose-cw.png and /dev/null differ diff --git a/docs/Using-Mastodon/screenshots/compose-media.png b/docs/Using-Mastodon/screenshots/compose-media.png deleted file mode 100644 index 7a63c196..00000000 Binary files a/docs/Using-Mastodon/screenshots/compose-media.png and /dev/null differ diff --git a/docs/Using-Mastodon/screenshots/compose-nsfw.png b/docs/Using-Mastodon/screenshots/compose-nsfw.png deleted file mode 100644 index a4ff5ed7..00000000 Binary files a/docs/Using-Mastodon/screenshots/compose-nsfw.png and /dev/null differ diff --git a/docs/Using-Mastodon/screenshots/compose-privacy.png b/docs/Using-Mastodon/screenshots/compose-privacy.png deleted file mode 100644 index b18ed204..00000000 Binary files a/docs/Using-Mastodon/screenshots/compose-privacy.png and /dev/null differ diff --git a/docs/Using-Mastodon/screenshots/content-warning.gif b/docs/Using-Mastodon/screenshots/content-warning.gif deleted file mode 100644 index 2e472061..00000000 Binary files a/docs/Using-Mastodon/screenshots/content-warning.gif and /dev/null differ diff --git a/docs/Using-Mastodon/screenshots/cw-toot.gif b/docs/Using-Mastodon/screenshots/cw-toot.gif deleted file mode 100644 index 5329933a..00000000 Binary files a/docs/Using-Mastodon/screenshots/cw-toot.gif and /dev/null differ diff --git a/docs/Using-Mastodon/screenshots/federated-timeline.png b/docs/Using-Mastodon/screenshots/federated-timeline.png deleted file mode 100644 index d74b089f..00000000 Binary files a/docs/Using-Mastodon/screenshots/federated-timeline.png and /dev/null differ diff --git a/docs/Using-Mastodon/screenshots/follow-icon.png b/docs/Using-Mastodon/screenshots/follow-icon.png deleted file mode 100644 index ee516c2f..00000000 Binary files a/docs/Using-Mastodon/screenshots/follow-icon.png and /dev/null differ diff --git a/docs/Using-Mastodon/screenshots/following-icon.png b/docs/Using-Mastodon/screenshots/following-icon.png deleted file mode 100644 index bccdc110..00000000 Binary files a/docs/Using-Mastodon/screenshots/following-icon.png and /dev/null differ diff --git a/docs/Using-Mastodon/screenshots/locked-icon.png b/docs/Using-Mastodon/screenshots/locked-icon.png deleted file mode 100644 index d199f1f1..00000000 Binary files a/docs/Using-Mastodon/screenshots/locked-icon.png and /dev/null differ diff --git a/docs/Using-Mastodon/screenshots/notifications-clear.png b/docs/Using-Mastodon/screenshots/notifications-clear.png deleted file mode 100644 index 7d0922cc..00000000 Binary files a/docs/Using-Mastodon/screenshots/notifications-clear.png and /dev/null differ diff --git a/docs/Using-Mastodon/screenshots/notifications-settings.png b/docs/Using-Mastodon/screenshots/notifications-settings.png deleted file mode 100644 index 3a3417e7..00000000 Binary files a/docs/Using-Mastodon/screenshots/notifications-settings.png and /dev/null differ diff --git a/docs/Using-Mastodon/screenshots/pending-icon.png b/docs/Using-Mastodon/screenshots/pending-icon.png deleted file mode 100644 index 777b3c39..00000000 Binary files a/docs/Using-Mastodon/screenshots/pending-icon.png and /dev/null differ diff --git a/docs/Using-Mastodon/screenshots/preferences.png b/docs/Using-Mastodon/screenshots/preferences.png deleted file mode 100644 index 943413fe..00000000 Binary files a/docs/Using-Mastodon/screenshots/preferences.png and /dev/null differ diff --git a/docs/Using-Mastodon/screenshots/private.png b/docs/Using-Mastodon/screenshots/private.png deleted file mode 100644 index cf338aad..00000000 Binary files a/docs/Using-Mastodon/screenshots/private.png and /dev/null differ diff --git a/docs/Using-Mastodon/screenshots/report.png b/docs/Using-Mastodon/screenshots/report.png deleted file mode 100644 index 5ce401ee..00000000 Binary files a/docs/Using-Mastodon/screenshots/report.png and /dev/null differ diff --git a/docs/Using-the-API/API.md b/docs/Using-the-API/API.md index bd7915a3..6de61e19 100644 --- a/docs/Using-the-API/API.md +++ b/docs/Using-the-API/API.md @@ -1,535 +1 @@ -API overview -============ - -## Contents - -- [Available libraries](#available-libraries) -- [Notes](#notes) -- [Methods](#methods) - - [Accounts](#accounts) - - [Apps](#apps) - - [Blocks](#blocks) - - [Favourites](#favourites) - - [Follow Requests](#follow-requests) - - [Follows](#follows) - - [Instances](#instances) - - [Media](#media) - - [Mutes](#mutes) - - [Notifications](#notifications) - - [Reports](#reports) - - [Search](#search) - - [Statuses](#statuses) - - [Timelines](#timelines) -- [Entities](#entities) - - [Account](#account) - - [Application](#application) - - [Attachment](#attachment) - - [Card](#card) - - [Context](#context) - - [Error](#error) - - [Instance](#instance) - - [Mention](#mention) - - [Notification](#notification) - - [Relationship](#relationship) - - [Results](#results) - - [Status](#status) - - [Tag](#tag) - -___ - -## Available libraries - -- [For Ruby](https://github.com/tootsuite/mastodon-api) -- [For Python](https://github.com/halcy/Mastodon.py) -- [For JavaScript](https://github.com/Zatnosk/libodonjs) -- [For JavaScript (Node.js)](https://github.com/jessicahayley/node-mastodon) -- [For Elixir](https://github.com/milmazz/hunter) - -___ - -## Notes - -### Parameter types - -When an array parameter is mentioned, the Rails convention of specifying array parameters in query strings is meant. -For example, a ruby array like `foo = [1, 2, 3]` can be encoded in the params as `foo[]=1&foo[]=2&foo[]=3`. -Square brackets can be indexed but can also be empty. - -When a file parameter is mentioned, a form-encoded upload is expected. - -### Selecting ranges - -For most `GET` operations that return arrays, the query parameters `max_id` and `since_id` can be used to specify the range of IDs to return. -API methods that return collections of items can return a `Link` header containing URLs for the `next` and `prev` pages. -See the [Link header RFC](https://tools.ietf.org/html/rfc5988) for more information. - -### Errors - -If the request you make doesn't go through, Mastodon will usually respond with an [Error](#error). - -___ - -## Methods - -### Accounts - -#### Fetching an account: - - GET /api/v1/accounts/:id - -Returns an [Account](#account). - -#### Getting the current user: - - GET /api/v1/accounts/verify_credentials - -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 - -Returns an array of [Accounts](#account). - -#### Getting who account is following: - - GET /api/v1/accounts/:id/following - -Returns an array of [Accounts](#account). - -#### Getting an account's statuses: - - GET /api/v1/accounts/:id/statuses - -Query parameters: - -- `only_media` (optional): Only return statuses that have media attachments -- `exclude_replies` (optional): Skip statuses that reply to other statuses - -Returns an array of [Statuses](#status). - -#### Following/unfollowing an account: - - GET /api/v1/accounts/:id/follow - GET /api/v1/accounts/:id/unfollow - -Returns the target [Account](#account). - -#### Blocking/unblocking an account: - - GET /api/v1/accounts/:id/block - GET /api/v1/accounts/:id/unblock - -Returns the target [Account](#account). - -#### Muting/unmuting an account: - - GET /api/v1/accounts/:id/mute - GET /api/v1/accounts/:id/unmute - -Returns the target [Account](#account). - -#### Getting an account's relationships: - - GET /api/v1/accounts/relationships - -Query parameters: - -- `id` (can be array): Account IDs - -Returns an array of [Relationships](#relationships) of the current user to a list of given accounts. - -#### Searching for accounts: - - GET /api/v1/accounts/search - -Query parameters: - -- `q`: What to search for -- `limit`: Maximum number of matching accounts to return (default: `40`) - -Returns an array of matching [Accounts](#accounts). -Will lookup an account remotely if the search term is in the `username@domain` format and not yet in the database. - -### Apps - -#### Registering an application: - - POST /api/v1/apps - -Form data: - -- `client_name`: Name of your application -- `redirect_uris`: Where the user should be redirected after authorization (for no redirect, use `urn:ietf:wg:oauth:2.0:oob`) -- `scopes`: This can be a space-separated list of the following items: "read", "write" and "follow" (see [this page](OAuth-details.md) for details on what the scopes do) -- `website`: (optional) URL to the homepage of your app - -Creates a new OAuth app. -Returns `id`, `client_id` and `client_secret` which can be used with [OAuth authentication in your 3rd party app](Testing-with-cURL.md). - -These values should be requested in the app itself from the API for each new app install + mastodon domain combo, and stored in the app for future requests. - -### Blocks - -#### Fetching a user's blocks: - - GET /api/v1/blocks - -Returns an array of [Accounts](#account) blocked by the authenticated user. - -### Favourites - -#### Fetching a user's favourites: - - GET /api/v1/favourites - -Returns an array of [Statuses](#status) favourited by the authenticated user. - -### Follow Requests - -#### Fetching a list of follow requests: - - GET /api/v1/follow_requests - -Returns an array of [Accounts](#account) which have requested to follow the authenticated user. - -#### Authorizing or rejecting follow requests: - - POST /api/v1/follow_requests/authorize - POST /api/v1/follow_requests/reject - -Form data: - -- `id`: The id of the account to authorize or reject - -Returns an empty object. - -### Follows - -#### Following a remote user: - - POST /api/v1/follows - -Form data: - -- `uri`: `username@domain` of the person you want to follow - -Returns the local representation of the followed account, as an [Account](#account). - -### Instances - -#### Getting instance information: - - GET /api/v1/instance - -Returns the current [Instance](#instance). -Does not require authentication. - -### Media - -#### Uploading a media attachment: - - POST /api/v1/media - -Form data: - -- `file`: Media to be uploaded - -Returns an [Attachment](#attachment) that can be used when creating a status. - -### Mutes - -#### Fetching a user's mutes: - - GET /api/v1/mutes - -Returns an array of [Accounts](#account) muted by the authenticated user. - -### Notifications - -#### Fetching a user's notifications: - - GET /api/v1/notifications - -Returns a list of [Notifications](#notification) for the authenticated user. - -#### Getting a single notification: - - GET /api/v1/notifications/:id - -Returns the [Notification](#notification). - -#### Clearing notifications: - - POST /api/v1/notifications/clear - -Deletes all notifications from the Mastodon server for the authenticated user. -Returns an empty object. - -### Reports - -#### Fetching a user's reports: - - GET /api/v1/reports - -Returns a list of [Reports](#report) made by the authenticated user. - -#### Reporting a user: - - POST /api/v1/reports - -Form data: - -- `account_id`: The ID of the account to report -- `status_ids`: The IDs of statuses to report (can be an array) -- `comment`: A comment to associate with the report. - -Returns the finished [Report](#report). - -### Search - -#### Searching for content: - - GET /api/v1/search - -Form data: - -- `q`: The search query -- `resolve`: Whether to resolve non-local accounts - -Returns [Results](#results). -If `q` is a URL, Mastodon will attempt to fetch the provided account or status. -Otherwise, it will do a local account and hashtag search. - -### Statuses - -#### Fetching a status: - - GET /api/v1/statuses/:id - -Returns a [Status](#status). - -#### Getting status context: - - GET /api/v1/statuses/:id/context - -Returns a [Context](#context). - -#### Getting a card associated with a status: - - GET /api/v1/statuses/:id/card - -Returns a [Card](#card). - -#### Getting who reblogged/favourited a status: - - GET /api/v1/statuses/:id/reblogged_by - GET /api/v1/statuses/:id/favourited_by - -Returns an array of [Accounts](#account). - -#### Posting a new status: - - POST /api/v1/statuses - -Form data: - -- `status`: The text of the status -- `in_reply_to_id` (optional): local ID of the status you want to reply to -- `media_ids` (optional): array of media IDs to attach to the status (maximum 4) -- `sensitive` (optional): set this to mark the media of the status as NSFW -- `spoiler_text` (optional): text to be shown as a warning before the actual content -- `visibility` (optional): either "direct", "private", "unlisted" or "public" - -Returns the new [Status](#status). - -#### Deleting a status: - - DELETE /api/v1/statuses/:id - -Returns an empty object. - -#### Reblogging/unreblogging a status: - - POST /api/vi/statuses/:id/reblog - POST /api/vi/statuses/:id/unreblog - -Returns the target [Status](#status). - -#### Favouriting/unfavouriting a status: - - POST /api/vi/statuses/:id/favourite - POST /api/vi/statuses/:id/unfavourite - -Returns the target [Status](#status). - -### Timelines - -#### Retrieving a timeline: - - GET /api/v1/timelines/home - GET /api/v1/timelines/public - GET /api/v1/timelines/tag/:hashtag - -Query parameters: - -- `local` (optional; public and tag timelines only): Only return statuses originating from this instance - -Returns an array of [Statuses](#status), most recent ones first. -___ - -## Entities - -### Account - -| Attribute | Description | -| ------------------------ | ----------- | -| `id` | The ID of the account | -| `username` | The username of the account | -| `acct` | Equals `username` for local users, includes `@domain` for remote ones | -| `display_name` | The account's display name | -| `note` | Biography of user | -| `url` | URL of the user's profile page (can be remote) | -| `avatar` | URL to the avatar image | -| `header` | URL to the header image | -| `locked` | Boolean for when the account cannot be followed without waiting for approval first | -| `created_at` | The time the account was created | -| `followers_count` | The number of followers for the account | -| `following_count` | The number of accounts the given account is following | -| `statuses_count` | The number of statuses the account has made | - -### Application - -| Attribute | Description | -| ------------------------ | ----------- | -| `name` | Name of the app | -| `website` | Homepage URL of the app | - -### Attachment - -| Attribute | Description | -| ------------------------ | ----------- | -| `id` | ID of the attachment | -| `type` | One of: "image", "video", "gifv" | -| `url` | URL of the locally hosted version of the image | -| `remote_url` | For remote images, the remote URL of the original image | -| `preview_url` | URL of the preview image | -| `text_url` | Shorter URL for the image, for insertion into text (only present on local images) | - -### Card - -| Attribute | Description | -| ------------------------ | ----------- | -| `url` | The url associated with the card | -| `title` | The title of the card | -| `description` | The card description | -| `image` | The image associated with the card, if any | - -### Context - -| Attribute | Description | -| ------------------------ | ----------- | -| `ancestors` | The ancestors of the status in the conversation, as a list of [Statuses](#status) | -| `descendants` | The descendants of the status in the conversation, as a list of [Statuses](#status) | - -### Error - -| Attribute | Description | -| ------------------------ | ----------- | -| `error` | A textual description of the error | - -### Instance - -| Attribute | Description | -| ------------------------ | ----------- | -| `uri` | URI of the current instance | -| `title` | The instance's title | -| `description` | A description for the instance | -| `email` | An email address which can be used to contact the instance administrator | - -### Mention - -| Attribute | Description | -| ------------------------ | ----------- | -| `url` | URL of user's profile (can be remote) | -| `username` | The username of the account | -| `acct` | Equals `username` for local users, includes `@domain` for remote ones | -| `id` | Account ID | - -### Notification - -| Attribute | Description | -| ------------------------ | ----------- | -| `id` | The notification ID | -| `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 applicable | - -### Relationship - -| Attribute | Description | -| ------------------------ | ----------- | -| `following` | Whether the user is currently following the account | -| `followed_by` | Whether the user is currently being followed by the account | -| `blocking` | Whether the user is currently blocking the account | -| `muting` | Whether the user is currently muting the account | -| `requested` | Whether the user has requested to follow the account | - -### Report - -| Attribute | Description | -| ------------------------ | ----------- | -| `id` | The ID of the report | -| `action_taken` | The action taken in response to the report | - -### Results - -| Attribute | Description | -| ------------------------ | ----------- | -| `accounts` | An array of matched [Accounts](#account) | -| `statuses` | An array of matchhed [Statuses](#status) | -| `hashtags` | An array of matched hashtags, as strings | - -### Status - -| Attribute | Description | -| ------------------------ | ----------- | -| `id` | The ID of the status | -| `uri` | A Fediverse-unique resource ID | -| `url` | URL to the status page (can be remote) | -| `account` | The [Account](#account) which posted the status | -| `in_reply_to_id` | `null` or the ID of the status it replies to | -| `in_reply_to_account_id` | `null` or the ID of the account it replies to | -| `reblog` | `null` or the reblogged [Status](#status) | -| `content` | Body of the status; this will contain HTML (remote HTML already sanitized) | -| `created_at` | The time the status was created | -| `reblogs_count` | The number of reblogs for the status | -| `favourites_count` | The number of favourites for the status | -| `reblogged` | Whether the authenticated user has reblogged the status | -| `favourited` | Whether the authenticated user has favourited the status | -| `sensitive` | Whether media attachments should be hidden by default | -| `spoiler_text` | If not empty, warning text that should be displayed before the actual content | -| `visibility` | One of: `public`, `unlisted`, `private`, `direct` | -| `media_attachments` | An array of [Attachments](#attachment) | -| `mentions` | An array of [Mentions](#mention) | -| `tags` | An array of [Tags](#tag) | -| `application` | [Application](#application) from which the status was posted | - -### Tag - -| Attribute | Description | -| ------------------------ | ----------- | -| `name` | The hashtag, not including the preceding `#` | -| `url` | The URL of the hashtag | +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md) diff --git a/docs/Using-the-API/OAuth-details.md b/docs/Using-the-API/OAuth-details.md index d0b5abd4..6a6926bb 100644 --- a/docs/Using-the-API/OAuth-details.md +++ b/docs/Using-the-API/OAuth-details.md @@ -1,12 +1 @@ -OAuth details -============= - -We use the [Doorkeeper gem for OAuth](https://github.com/doorkeeper-gem/doorkeeper/wiki), so you can refer to their docs on specifics of the end-points. - -The API is divided up into access scopes: - -- `read`: Read data -- `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). +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-the-API/OAuth-details.md) diff --git a/docs/Using-the-API/Push-notifications.md b/docs/Using-the-API/Push-notifications.md index fc373e72..3292c0a6 100644 --- a/docs/Using-the-API/Push-notifications.md +++ b/docs/Using-the-API/Push-notifications.md @@ -1,4 +1 @@ -Push notifications -================== - -See for an example of how to create push notifications for a mobile app. It involves using the Mastodon streaming API on behalf of the app's users, as a sort of proxy. +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-the-API/Push-notifications.md) diff --git a/docs/Using-the-API/Streaming-API.md b/docs/Using-the-API/Streaming-API.md index b6d41ab0..482f901c 100644 --- a/docs/Using-the-API/Streaming-API.md +++ b/docs/Using-the-API/Streaming-API.md @@ -1,40 +1 @@ -Streaming API -============= - -Your application can use a server-sent events endpoint to receive updates in real-time. Server-sent events is an incredibly simple transport method that relies entirely on chunked-encoding transfer, i.e. the HTTP connection is kept open and receives new data periodically. - -### Endpoints: - -**GET /api/v1/streaming/user** - -Returns events that are relevant to the authorized user, i.e. home timeline and notifications - -**GET /api/v1/streaming/public** - -Returns all public statuses - -**GET /api/v1/streaming/hashtag** - -Returns all public statuses for a particular hashtag (query param `tag`) - -### Stream contents - -The stream will contain events as well as heartbeat comments. Lines that begin with a colon (`:`) can be ignored by parsers, they are simply there to keep the connection open. Events have this structure: - -``` -event: name -data: payload - -``` - -[See MDN](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) - -### Event types - -|Event|Description|What's in the payload| -|-----|-----------|---------------------| -|`update`|A new status has appeared!|Status| -|`notification`|A new notification|Notification| -|`delete`|A status has been deleted|ID of the deleted status| - -The payload is JSON-encoded. +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-the-API/Streaming-API.md) diff --git a/docs/Using-the-API/Testing-with-cURL.md b/docs/Using-the-API/Testing-with-cURL.md index dc5f2022..04c7c87b 100644 --- a/docs/Using-the-API/Testing-with-cURL.md +++ b/docs/Using-the-API/Testing-with-cURL.md @@ -1,18 +1 @@ -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). - -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: - - curl -X POST -d "client_id=CLIENT_ID_HERE&client_secret=CLIENT_SECRET_HERE&grant_type=password&username=YOUR_EMAIL&password=YOUR_PASSWORD" -Ss https://mastodon.social/oauth/token - -The `/oauth/token` path will attempt to login with the given credentials, and then retrieve the access token for the current user. If the login failed the response will be a 302 redirect to `/auth/sign_in`. Otherwise the response will be a JSON object containing the key `access_token`. - -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 - -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. +[The documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-the-API/Testing-with-cURL.md) diff --git a/docs/Using-the-API/Tips-for-app-developers.md b/docs/Using-the-API/Tips-for-app-developers.md index 561f1e27..36a28da2 100644 --- a/docs/Using-the-API/Tips-for-app-developers.md +++ b/docs/Using-the-API/Tips-for-app-developers.md @@ -1,16 +1 @@ -Tips for app developers -======================= - -## Authentication - -Make sure that you allow your users to specify the domain they want to connect to before login. Use that domain to acquire a client id/secret for OAuth2 and then proceed with normal OAuth2 also using that domain to build the URLs. - -In my opinion it is easier for people to understand what is being asked of them if you ask for a `username@domain` type input, since it looks like an e-mail address. Though the username part is not required for anything in the OAuth2 process. Once the user is logged in, you get information about the logged in user from `/api/v1/accounts/verify_credentials` - -## Usernames - -Make sure that you make it possible to see the `acct` of any user in your app (since it includes the domain part for remote users), people must be able to tell apart users from different domains with the same username. - -## 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 documentation has moved to its own repository](https://github.com/tootsuite/documentation/blob/master/Using-the-API/Tips-for-app-developers.md) diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 5dc7f156..4761b291 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -13,10 +13,13 @@ namespace :mastodon do 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) - user.update(confirmed_at: Time.now.utc) - - puts "User #{email} confirmed." + 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 @@ -72,6 +75,13 @@ namespace :mastodon do end end + namespace :users do + desc 'clear unconfirmed users' + task clear: :environment do + User.where('confirmed_at is NULL AND confirmation_sent_at <= ?', 2.days.ago).find_each(&:destroy) + end + end + namespace :maintenance do desc 'Update counter caches' task update_counter_caches: :environment do @@ -89,5 +99,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 14c8abe7..df496c55 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "escape-html": "^1.0.3", "eventsource": "^0.2.1", "express": "^4.14.1", - "http-link-header": "^0.5.0", + "http-link-header": "^0.8.0", "immutable": "^3.8.1", "intl": "^1.2.5", "jsdom": "^9.11.0", @@ -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 d60f1529..4afaa6b4 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/admin/reports_controller_spec.rb b/spec/controllers/admin/reports_controller_spec.rb new file mode 100644 index 00000000..622ea87c --- /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 00000000..c126b645 --- /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/notifications_controller_spec.rb b/spec/controllers/api/v1/notifications_controller_spec.rb index e5f7eec7..c390d4f0 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/settings/exports/blocked_accounts_controller_spec.rb b/spec/controllers/settings/exports/blocked_accounts_controller_spec.rb new file mode 100644 index 00000000..574d4d87 --- /dev/null +++ b/spec/controllers/settings/exports/blocked_accounts_controller_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +describe Settings::Exports::BlockedAccountsController do + before do + sign_in Fabricate(:user), scope: :user + end + + describe 'GET #index' do + it 'returns a csv of the blocking accounts' do + get :index, format: :csv + + expect(response).to have_http_status(:success) + expect(response.content_type).to eq 'text/csv' + expect(response.headers['Content-Disposition']).to eq 'attachment; filename="blocking.csv"' + end + end +end diff --git a/spec/controllers/settings/exports/following_accounts_controller_spec.rb b/spec/controllers/settings/exports/following_accounts_controller_spec.rb new file mode 100644 index 00000000..bf768052 --- /dev/null +++ b/spec/controllers/settings/exports/following_accounts_controller_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +describe Settings::Exports::FollowingAccountsController do + before do + sign_in Fabricate(:user), scope: :user + end + + describe 'GET #index' do + it 'returns a csv of the following accounts' do + get :index, format: :csv + + expect(response).to have_http_status(:success) + expect(response.content_type).to eq 'text/csv' + expect(response.headers['Content-Disposition']).to eq 'attachment; filename="following.csv"' + end + end +end diff --git a/spec/controllers/settings/exports_controller_spec.rb b/spec/controllers/settings/exports_controller_spec.rb new file mode 100644 index 00000000..ff98f3ad --- /dev/null +++ b/spec/controllers/settings/exports_controller_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +describe Settings::ExportsController do + before do + sign_in Fabricate(:user), scope: :user + end + + describe 'GET #show' do + it 'returns http success' do + get :show + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/controllers/settings/imports_controller_spec.rb b/spec/controllers/settings/imports_controller_spec.rb new file mode 100644 index 00000000..d57350a1 --- /dev/null +++ b/spec/controllers/settings/imports_controller_spec.rb @@ -0,0 +1,43 @@ +require 'rails_helper' + +RSpec.describe Settings::ImportsController, type: :controller do + + before do + sign_in Fabricate(:user), scope: :user + end + + describe "GET #show" do + it "returns http success" do + get :show + expect(response).to have_http_status(:success) + end + end + + describe 'POST #create' do + it 'redirects to settings path with successful following import' do + service = double(call: nil) + allow(FollowRemoteAccountService).to receive(:new).and_return(service) + post :create, params: { + import: { + type: 'following', + data: fixture_file_upload('files/imports.txt') + } + } + + expect(response).to redirect_to(settings_import_path) + end + + it 'redirects to settings path with successful blocking import' do + service = double(call: nil) + allow(FollowRemoteAccountService).to receive(:new).and_return(service) + post :create, params: { + import: { + type: 'blocking', + data: fixture_file_upload('files/imports.txt') + } + } + + expect(response).to redirect_to(settings_import_path) + end + end +end diff --git a/spec/controllers/xrd_controller_spec.rb b/spec/controllers/xrd_controller_spec.rb index e687cf9e..b56c68f5 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 00000000..d929801e Binary files /dev/null and b/spec/fixtures/files/avatar.gif differ diff --git a/spec/fixtures/files/imports.txt b/spec/fixtures/files/imports.txt new file mode 100644 index 00000000..dac295c4 --- /dev/null +++ b/spec/fixtures/files/imports.txt @@ -0,0 +1,3 @@ +user@example.com + +user@test.com diff --git a/spec/helpers/accounts_helper_spec.rb b/spec/helpers/accounts_helper_spec.rb deleted file mode 100644 index 3aea1f90..00000000 --- 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/atom_builder_helper_spec.rb b/spec/helpers/atom_builder_helper_spec.rb deleted file mode 100644 index 0aca58ee..00000000 --- a/spec/helpers/atom_builder_helper_spec.rb +++ /dev/null @@ -1,187 +0,0 @@ -require 'rails_helper' - -RSpec.describe AtomBuilderHelper, type: :helper do - describe '#stream_updated_at' do - pending - end - - describe '#entry' do - it 'creates an entry' do - expect(used_in_builder { |xml| helper.entry(xml) }).to match '' - end - end - - describe '#feed' do - it 'creates a feed' do - expect(used_in_builder { |xml| helper.feed(xml) }).to match '' - end - end - - describe '#unique_id' do - it 'creates an id' do - time = Time.now - expect(used_in_builder { |xml| helper.unique_id(xml, time, 1, 'Status') }).to match "#{TagManager.instance.unique_tag(time, 1, 'Status')}" - end - end - - describe '#simple_id' do - it 'creates an id' do - expect(used_in_builder { |xml| helper.simple_id(xml, 1) }).to match '1' - end - end - - describe '#published_at' do - it 'creates a published tag' do - time = Time.now - expect(used_in_builder { |xml| helper.published_at(xml, time) }).to match "#{time.iso8601}" - end - end - - describe '#updated_at' do - it 'creates an updated tag' do - time = Time.now - expect(used_in_builder { |xml| helper.updated_at(xml, time) }).to match "#{time.iso8601}" - end - end - - describe '#verb' do - it 'creates an entry' do - expect(used_with_namespaces { |xml| helper.verb(xml, :post) }).to match 'http://activitystrea.ms/schema/1.0/post' - end - end - - describe '#content' do - it 'creates a content' do - expect(used_in_builder { |xml| helper.content(xml, 'foo') }).to match 'foo' - end - end - - describe '#title' do - it 'creates a title' do - expect(used_in_builder { |xml| helper.title(xml, 'foo') }).to match 'foo' - end - end - - describe '#author' do - it 'creates an author' do - expect(used_in_builder { |xml| helper.author(xml) }).to match '' - end - end - - describe '#target' do - it 'creates a target' do - expect(used_with_namespaces { |xml| helper.target(xml) }).to match '' - end - end - - describe '#object_type' do - it 'creates an object type' do - expect(used_with_namespaces { |xml| helper.object_type(xml, :person) }).to match 'http://activitystrea.ms/schema/1.0/person' - end - end - - describe '#uri' do - it 'creates a uri' do - expect(used_in_builder { |xml| helper.uri(xml, 1) }).to match '1' - end - end - - describe '#name' do - it 'creates a name' do - expect(used_in_builder { |xml| helper.name(xml, 1) }).to match '1' - end - end - - describe '#summary' do - it 'creates a summary' do - expect(used_in_builder { |xml| helper.summary(xml, 1) }).to match '1' - end - end - - describe '#subtitle' do - it 'creates a subtitle' do - expect(used_in_builder { |xml| helper.subtitle(xml, 1) }).to match '1' - end - end - - describe '#link_alternate' do - it 'creates a link' do - expect(used_in_builder { |xml| helper.link_alternate(xml, 1) }).to match '' - end - end - - describe '#link_self' do - it 'creates a link' do - expect(used_in_builder { |xml| helper.link_self(xml, 1) }).to match '' - end - end - - describe '#link_hub' do - it 'creates a link' do - expect(used_in_builder { |xml| helper.link_hub(xml, 1) }).to match '' - end - end - - describe '#link_salmon' do - it 'creates a link' do - expect(used_in_builder { |xml| helper.link_salmon(xml, 1) }).to match '' - end - end - - describe '#portable_contact' do - let(:account) { Fabricate(:account, username: 'alice', display_name: 'Alice in Wonderland') } - - it 'creates portable contacts entries' do - expect(used_with_namespaces { |xml| helper.portable_contact(xml, account) }).to match 'Alice in Wonderland' - end - end - - describe '#in_reply_to' do - it 'creates a thread' do - expect(used_with_namespaces { |xml| helper.in_reply_to(xml, 'uri', 'url') }).to match '' - end - end - - describe '#link_mention' do - let(:account) { Fabricate(:account, username: 'alice') } - - it 'creates a link' do - expect(used_in_builder { |xml| helper.link_mention(xml, account) }).to match '' - end - end - - describe '#include_author' do - pending - end - - describe '#include_entry' do - pending - end - - describe '#link_avatar' do - let(:account) { Fabricate(:account, username: 'alice') } - - it 'creates a link' do - expect(used_with_namespaces { |xml| helper.link_avatar(xml, account) }).to match '' - end - end - - describe '#link_enclosure' do - pending - end - - describe '#logo' do - it 'creates a logo' do - expect(used_in_builder { |xml| helper.logo(xml, 1) }).to match '1' - end - end - - def used_in_builder(&block) - builder = Nokogiri::XML::Builder.new(&block) - builder.doc.root.to_xml - end - - def used_with_namespaces(&block) - used_in_builder { |xml| helper.entry(xml, true, &block) } - end -end diff --git a/spec/helpers/stream_entries_helper_spec.rb b/spec/helpers/stream_entries_helper_spec.rb index 221e1e32..a8313391 100644 --- a/spec/helpers/stream_entries_helper_spec.rb +++ b/spec/helpers/stream_entries_helper_spec.rb @@ -15,23 +15,7 @@ RSpec.describe StreamEntriesHelper, type: :helper do end end - describe '#avatar_for_status_url' do - pending - end - describe '#entry_classes' do pending end - - describe '#relative_time' do - pending - end - - describe '#reblogged_by_me_class' do - pending - end - - describe '#favourited_by_me_class' do - pending - end end diff --git a/spec/javascript/components/avatar.test.jsx b/spec/javascript/components/avatar.test.jsx index 852e13a8..7131bbec 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('', () => { const src = '/path/to/image.jpg'; const size = 100; - const wrapper = render(); + const wrapper = render(); - 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 0c3b2b04..fb367ab7 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' @@ -397,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/export_spec.rb b/spec/models/export_spec.rb new file mode 100644 index 00000000..5cc62c26 --- /dev/null +++ b/spec/models/export_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +describe Export do + describe 'to_csv' do + it 'returns a csv of the accounts' do + one = Account.new(username: 'one', domain: 'local.host') + two = Account.new(username: 'two', domain: 'local.host') + accounts = [one, two] + + export = Export.new(accounts).to_csv + results = export.strip.split + + expect(results.size).to eq 2 + expect(results.first).to eq 'one@local.host' + end + end +end diff --git a/streaming/index.js b/streaming/index.js index 7edf6203..a1e7eaca 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 6a3a3627..7f1c48db 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" @@ -1241,7 +1281,7 @@ babel-template@^6.3.0: babylon "^6.11.0" lodash "^4.2.0" -babel-traverse@^6.16.0, babel-traverse@^6.22.0, babel-traverse@^6.23.0, babel-traverse@^6.23.1: +babel-traverse@^6.16.0, babel-traverse@^6.22.0, babel-traverse@^6.22.1, babel-traverse@^6.23.0, babel-traverse@^6.23.1: version "6.23.1" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.23.1.tgz#d3cb59010ecd06a97d81310065f966b699e14f48" dependencies: @@ -1255,30 +1295,7 @@ babel-traverse@^6.16.0, babel-traverse@^6.22.0, babel-traverse@^6.23.0, babel-tr invariant "^2.2.0" lodash "^4.2.0" -babel-traverse@^6.22.1: - version "6.22.1" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.22.1.tgz#3b95cd6b7427d6f1f757704908f2fc9748a5f59f" - dependencies: - babel-code-frame "^6.22.0" - babel-messages "^6.22.0" - babel-runtime "^6.22.0" - babel-types "^6.22.0" - babylon "^6.15.0" - debug "^2.2.0" - globals "^9.0.0" - invariant "^2.2.0" - lodash "^4.2.0" - -babel-types@^6.16.0, babel-types@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.22.0.tgz#2a447e8d0ea25d2512409e4175479fd78cc8b1db" - dependencies: - babel-runtime "^6.22.0" - esutils "^2.0.2" - lodash "^4.2.0" - to-fast-properties "^1.0.1" - -babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.9.0: +babel-types@^6.16.0, babel-types@^6.19.0, babel-types@^6.22.0, babel-types@^6.23.0, babel-types@^6.9.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.23.0.tgz#bb17179d7538bad38cd0c9e115d340f77e7e9acf" dependencies: @@ -1294,13 +1311,9 @@ babelify@^7.3.0: babel-core "^6.0.14" object-assign "^4.0.0" -babylon@^6.11.0: - version "6.11.4" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.11.4.tgz#75e1f52187efa0cde5a541a7f7fdda38f6eb5bd2" - -babylon@^6.15.0: - version "6.15.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e" +babylon@^6.11.0, babylon@^6.15.0, 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" @@ -1586,6 +1599,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 +1662,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 +1718,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 +1732,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 +1861,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,11 +2122,11 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" -d@^0.1.1, d@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309" +d@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" dependencies: - es5-ext "~0.10.2" + es5-ext "^0.10.9" dashdash@^1.12.0: version "1.14.0" @@ -2140,6 +2177,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 +2246,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" @@ -2368,7 +2424,7 @@ error-ex@^1.2.0: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.3.2, es-abstract@^1.4.3, es-abstract@^1.5.0, es-abstract@^1.5.1: +es-abstract@^1.3.2: version "1.6.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.6.1.tgz#bb8a2064120abcf928a086ea3d9043114285ec99" dependencies: @@ -2377,6 +2433,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.4.3, es-abstract@^1.5.0, es-abstract@^1.5.1, 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,9 +2450,9 @@ es-to-primitive@^1.1.1: is-date-object "^1.0.1" is-symbol "^1.0.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" +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" @@ -2396,28 +2461,58 @@ es5-shim@^4.5.9: version "4.5.9" resolved "https://registry.yarnpkg.com/es5-shim/-/es5-shim-4.5.9.tgz#2a1e2b9e583ff5fed0c20a3ee2cbf3f75230a5c0" -es6-iterator@2: - version "2.0.0" - resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.0.tgz#bd968567d61635e33c0b80727613c9cb4b096bac" +es6-iterator@2, 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 "^0.1.1" - es5-ext "^0.10.7" - es6-symbol "3" + 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" -es6-symbol@3, es6-symbol@^3.0.2, es6-symbol@~3.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.0.tgz#94481c655e7a7cad82eba832d97d5433496d7ffa" +es6-symbol@3.1.1, es6-symbol@^3.0.2, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.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 "~0.1.1" - es5-ext "~0.10.11" + 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" @@ -2438,6 +2533,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 +2607,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 +2640,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 +2667,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 +2752,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 +2811,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 +3031,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" @@ -2958,9 +3185,9 @@ http-errors@~1.5.1: setprototypeof "1.0.2" statuses ">= 1.3.1 < 2" -http-link-header@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/http-link-header/-/http-link-header-0.5.0.tgz#68598d92c55d3dac7d3e6ae405142fecf7bd3303" +http-link-header@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/http-link-header/-/http-link-header-0.8.0.tgz#a22b41a0c9b1e2d8fac1bf1b697c6bd532d5f5e4" http-signature@~1.1.0: version "1.1.1" @@ -2986,6 +3213,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 +3268,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 +3411,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 +3440,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 +3486,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 +3573,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 +3624,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 +3672,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 +3716,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 +3915,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 +4146,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 +4445,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 +4456,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 +4577,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 +4670,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 +5019,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 +5445,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 +5654,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 +5669,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 +5704,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 +5864,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 +5897,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 +6045,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 +6095,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 +6109,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 +6177,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 +6235,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 +6246,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 +6292,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 +6409,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 +6673,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"