diff --git a/.env.production.sample b/.env.production.sample
index e022a84059c3f2..91fcce6ac4568b 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -1,5 +1,6 @@
# Service dependencies
# You may set REDIS_URL instead for more advanced options
+# You may also set REDIS_NAMESPACE to share Redis between multiple Mastodon servers
REDIS_HOST=redis
REDIS_PORT=6379
# You may set DATABASE_URL instead for more advanced options
diff --git a/.eslintrc.yml b/.eslintrc.yml
index 1c60cbdb3e5c93..7c6da9d57a0326 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -5,12 +5,14 @@ env:
browser: true
node: true
es6: true
+ jest: true
parser: babel-eslint
plugins:
- react
- jsx-a11y
+- import
parserOptions:
sourceType: module
@@ -21,8 +23,14 @@ parserOptions:
modules: true
spread: true
-rules:
+settings:
+ import/extensions:
+ - .js
+ import/ignore:
+ - node_modules
+ - \\.(css|scss|json)$
+rules:
brace-style: warn
comma-dangle:
- error
@@ -125,3 +133,17 @@ rules:
jsx-a11y/role-supports-aria-props: off
jsx-a11y/scope: warn
jsx-a11y/tabindex-no-positive: warn
+
+ import/extensions:
+ - error
+ - always
+ - js: never
+ import/newline-after-import: error
+ import/no-extraneous-dependencies:
+ - error
+ - devDependencies:
+ - "config/webpack/**"
+ - "app/javascript/mastodon/test_setup.js"
+ - "app/javascript/**/__tests__/**"
+ import/no-unresolved: error
+ import/no-webpack-loader-syntax: error
diff --git a/.ruby-version b/.ruby-version
index 005119baaa0653..8e8299dcc06835 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.4.1
+2.4.2
diff --git a/.travis.yml b/.travis.yml
index 2f8a728fe635d3..27a907b4c1f391 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -26,17 +26,15 @@ addons:
postgresql: 9.4
apt:
sources:
- - ubuntu-toolchain-r-test
- trusty-media
packages:
- ffmpeg
- - g++-6
- libprotobuf-dev
- protobuf-compiler
- libicu-dev
rvm:
- - 2.4.1
+ - 2.4.2
services:
- redis-server
@@ -54,5 +52,5 @@ before_script:
script:
- travis_retry bundle exec parallel_test spec/ --group-by filesize --type rspec
- - npm test
- - bundle exec i18n-tasks unused
+ - yarn test
+ - bundle exec i18n-tasks check-normalized && bundle exec i18n-tasks unused
diff --git a/.yarnclean b/.yarnclean
new file mode 100644
index 00000000000000..f2de52869c823b
--- /dev/null
+++ b/.yarnclean
@@ -0,0 +1,46 @@
+# test directories
+__tests__
+test
+tests
+powered-test
+
+# asset directories
+docs
+doc
+website
+images
+# assets
+
+# examples
+example
+examples
+
+# code coverage directories
+coverage
+.nyc_output
+
+# build scripts
+Makefile
+Gulpfile.js
+Gruntfile.js
+
+# configs
+.tern-project
+.gitattributes
+.editorconfig
+.*ignore
+.eslintrc
+.jshintrc
+.flowconfig
+.documentup.json
+.yarn-metadata.json
+.*.yml
+*.yml
+
+# misc
+*.gz
+*.md
+
+# for specific ignore
+!.svgo.yml
+
diff --git a/CODEOWNERS b/CODEOWNERS
index 42fc73ded75718..32919bd50c5fc3 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -8,8 +8,25 @@
# /config/locales/*.fr.yml @żelipapą
# /config/locales/fr.yml @żelipapą
+# Polish
/app/javascript/mastodon/locales/pl.json @m4sk1n
/app/views/user_mailer/*.pl.html.erb @m4sk1n
/app/views/user_mailer/*.pl.text.erb @m4sk1n
/config/locales/*.pl.yml @m4sk1n
/config/locales/pl.yml @m4sk1n
+
+# French
+/app/javascript/mastodon/locales/fr.json @aldarone
+/app/javascript/mastodon/locales/whitelist_fr.json @aldarone
+/app/views/user_mailer/*.fr.html.erb @aldarone
+/app/views/user_mailer/*.fr.text.erb @aldarone
+/config/locales/*.fr.yml @aldarone
+/config/locales/fr.yml @aldarone
+
+# Dutch
+/app/javascript/mastodon/locales/nl.json @jeroenpraat
+/app/javascript/mastodon/locales/whitelist_nl.json @jeroenpraat
+/app/views/user_mailer/*.nl.html.erb @jeroenpraat
+/app/views/user_mailer/*.nl.text.erb @jeroenpraat
+/config/locales/*.nl.yml @jeroenpraat
+/config/locales/nl.yml @jeroenpraat
diff --git a/Dockerfile b/Dockerfile
index 15138065b688fd..c3b38fa8b1a7f3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM ruby:2.4.1-alpine3.6
+FROM ruby:2.4.2-alpine3.6
LABEL maintainer="https://github.com/tootsuite/mastodon" \
description="A GNU Social-compatible microblogging server"
@@ -7,6 +7,8 @@ ENV UID=991 GID=991 \
RAILS_SERVE_STATIC_FILES=true \
RAILS_ENV=production NODE_ENV=production
+ARG YARN_VERSION=1.1.0
+ARG YARN_DOWNLOAD_SHA256=171c1f9ee93c488c0d774ac6e9c72649047c3f896277d88d0f805266519430f3
ARG LIBICONV_VERSION=1.15
ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178
@@ -19,6 +21,7 @@ RUN apk -U upgrade \
build-base \
icu-dev \
libidn-dev \
+ libressl \
libtool \
postgresql-dev \
protobuf-dev \
@@ -32,16 +35,21 @@ RUN apk -U upgrade \
imagemagick \
libidn \
libpq \
- nodejs-npm \
nodejs \
+ nodejs-npm \
protobuf \
su-exec \
tini \
- yarn \
&& update-ca-certificates \
+ && mkdir -p /tmp/src /opt \
+ && wget -O yarn.tar.gz "https://github.com/yarnpkg/yarn/releases/download/v$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz" \
+ && echo "$YARN_DOWNLOAD_SHA256 *yarn.tar.gz" | sha256sum -c - \
+ && tar -xzf yarn.tar.gz -C /tmp/src \
+ && rm yarn.tar.gz \
+ && mv /tmp/src/yarn-v$YARN_VERSION /opt/yarn \
+ && ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
&& wget -O libiconv.tar.gz "http://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \
&& echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \
- && mkdir -p /tmp/src \
&& tar -xzf libiconv.tar.gz -C /tmp/src \
&& rm libiconv.tar.gz \
&& cd /tmp/src/libiconv-$LIBICONV_VERSION \
@@ -52,11 +60,12 @@ RUN apk -U upgrade \
&& cd /mastodon \
&& rm -rf /tmp/* /var/cache/apk/*
-COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
+COPY Gemfile Gemfile.lock package.json yarn.lock .yarnclean /mastodon/
RUN bundle config build.nokogiri --with-iconv-lib=/usr/local/lib --with-iconv-include=/usr/local/include \
&& bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \
- && yarn --ignore-optional --pure-lockfile
+ && yarn --pure-lockfile \
+ && yarn cache clean
COPY . /mastodon
diff --git a/Gemfile b/Gemfile
index 514d690013e978..c6099e5030ed17 100644
--- a/Gemfile
+++ b/Gemfile
@@ -42,6 +42,7 @@ gem 'kaminari', '~> 1.0'
gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.1'
gem 'nokogiri', '~> 1.7'
+gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.0'
gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.5'
@@ -64,10 +65,10 @@ gem 'sidekiq-bulk', '~>0.1.1'
gem 'simple-navigation', '~> 4.0'
gem 'simple_form', '~> 3.4'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
-gem 'statsd-instrument', '~> 2.1'
+gem 'strong_migrations'
gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2017'
-gem 'webpacker', '~> 2.0'
+gem 'webpacker', '~> 3.0'
gem 'webpush'
gem 'airbrake', '~> 5.0'
@@ -103,8 +104,8 @@ group :development do
gem 'letter_opener', '~> 1.4'
gem 'letter_opener_web', '~> 1.3'
gem 'rubocop', require: false
- gem 'brakeman', '~> 3.6', require: false
- gem 'bundler-audit', '~> 0.5', require: false
+ gem 'brakeman', '~> 4.0', require: false
+ gem 'bundler-audit', '~> 0.6', require: false
gem 'scss_lint', '~> 0.53', require: false
gem 'capistrano', '~> 3.8'
@@ -119,7 +120,7 @@ group :production do
end
# for friends
-gem 'omniauth-niconico'
+gem 'omniauth-niconico', '0.0.1'
group :production do
gem 'exception_notification', '~> 4.2.2'
gem 'slack-notifier', '~> 2.3.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index e838c5d567d984..f94c765f883946 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -60,33 +60,33 @@ GEM
encryptor (~> 3.0.0)
av (0.9.0)
cocaine (~> 0.5.3)
- aws-sdk (2.10.21)
- aws-sdk-resources (= 2.10.21)
- aws-sdk-core (2.10.21)
+ aws-sdk (2.10.46)
+ aws-sdk-resources (= 2.10.46)
+ aws-sdk-core (2.10.46)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
- aws-sdk-resources (2.10.21)
- aws-sdk-core (= 2.10.21)
- aws-sigv4 (1.0.1)
+ aws-sdk-resources (2.10.46)
+ aws-sdk-core (= 2.10.46)
+ aws-sigv4 (1.0.2)
bcrypt (3.1.11)
- better_errors (2.1.1)
+ better_errors (2.3.0)
coderay (>= 1.0.0)
- erubis (>= 2.6.6)
+ erubi (>= 1.0.0)
rack (>= 0.9.0)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
- bootsnap (1.1.2)
+ bootsnap (1.1.3)
msgpack (~> 1.0)
- brakeman (3.7.2)
- browser (2.4.0)
+ brakeman (4.0.1)
+ browser (2.5.1)
builder (3.2.3)
- bullet (5.5.1)
+ bullet (5.6.1)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0)
bundler-audit (0.6.0)
bundler (~> 1.2)
thor (~> 0.18)
- capistrano (3.8.2)
+ capistrano (3.9.1)
airbrussh (>= 1.0.0)
i18n
rake (>= 10.0.0)
@@ -102,9 +102,9 @@ GEM
sshkit (~> 1.3)
capistrano-yarn (2.0.2)
capistrano (~> 3.0)
- capybara (2.14.4)
+ capybara (2.15.1)
addressable
- mime-types (>= 1.16)
+ mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
@@ -118,7 +118,7 @@ GEM
climate_control (0.2.0)
cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0)
- coderay (1.1.1)
+ coderay (1.1.2)
colorize (0.8.1)
concurrent-ruby (1.0.5)
connection_pool (2.2.1)
@@ -154,16 +154,15 @@ GEM
thread_safe
encryptor (3.0.0)
erubi (1.6.1)
- erubis (2.7.0)
et-orbi (1.0.5)
tzinfo
exception_notification (4.2.2)
actionmailer (>= 4.0, < 6)
activesupport (>= 4.0, < 6)
- excon (0.58.0)
+ excon (0.59.0)
execjs (2.7.0)
- fabrication (2.16.2)
- faker (1.7.3)
+ fabrication (2.16.3)
+ faker (1.8.4)
i18n (~> 0.5)
faraday (0.12.2)
multipart-post (>= 1.2, < 3)
@@ -202,7 +201,7 @@ GEM
railties (>= 4.0.1)
hamster (3.0.0)
concurrent-ruby (~> 1.0)
- hashdiff (0.3.5)
+ hashdiff (0.3.7)
hashie (3.5.6)
highline (1.7.8)
hiredis (0.6.1)
@@ -222,11 +221,11 @@ GEM
colorize
rack
i18n (0.8.6)
- i18n-tasks (0.9.16)
+ i18n-tasks (0.9.18)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
easy_translate (>= 0.5.0)
- erubis
+ erubi
highline (>= 1.7.3)
i18n
parser (>= 2.2.3.0)
@@ -240,7 +239,7 @@ GEM
json-ld (2.1.5)
multi_json (~> 1.12)
rdf (~> 2.2)
- json-ld-preloaded (2.2.1)
+ json-ld-preloaded (2.2.2)
json-ld (~> 2.1, >= 2.1.5)
multi_json (~> 1.11)
rdf (~> 2.2)
@@ -267,10 +266,11 @@ GEM
letter_opener (~> 1.0)
railties (>= 3.2)
link_header (0.0.8)
- lograge (0.5.1)
+ lograge (0.6.0)
actionpack (>= 4, < 5.2)
activesupport (>= 4, < 5.2)
railties (>= 4, < 5.2)
+ request_store (~> 1.0)
loofah (2.0.3)
nokogiri (>= 1.5.9)
mail (2.6.6)
@@ -285,28 +285,34 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mimemagic (0.3.2)
+ mini_mime (0.1.4)
mini_portile2 (2.2.0)
minitest (5.10.3)
msgpack (1.1.0)
- multi_json (1.12.1)
+ multi_json (1.12.2)
multi_xml (0.6.0)
multipart-post (2.0.0)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
- net-ssh (4.1.0)
+ net-ssh (4.2.0)
nio4r (2.1.0)
nokogiri (1.8.0)
mini_portile2 (~> 2.2.0)
nokogumbo (1.4.13)
nokogiri
+ nsa (0.2.4)
+ activesupport (>= 4.2, < 6)
+ concurrent-ruby (~> 1.0.0)
+ sidekiq (>= 3.5.0)
+ statsd-ruby (~> 1.2.0)
oauth2 (1.4.0)
faraday (>= 0.8, < 0.13)
jwt (~> 1.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
- oj (3.3.4)
- omniauth (1.6.1)
+ oj (3.3.5)
+ omniauth (1.7.1)
hashie (>= 3.4.6, < 3.6.0)
rack (>= 1.6.2, < 3)
omniauth-niconico (0.0.1)
@@ -314,14 +320,14 @@ GEM
omniauth-oauth2 (1.4.0)
oauth2 (~> 1.0)
omniauth (~> 1.2)
- openssl (2.0.4)
+ openssl (2.0.5)
orm_adapter (0.5.0)
ostatus2 (2.0.1)
addressable (~> 2.4)
http (~> 2.0)
nokogiri (~> 1.6)
openssl (~> 2.0)
- ox (2.5.0)
+ ox (2.6.0)
paperclip (5.1.0)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
@@ -331,15 +337,15 @@ GEM
paperclip-av-transcoder (0.6.4)
av (~> 0.9.0)
paperclip (>= 2.5.2)
- parallel (1.11.2)
- parallel_tests (2.14.2)
+ parallel (1.12.0)
+ parallel_tests (2.15.0)
parallel
parser (2.4.0.0)
ast (~> 2.2)
pg (0.21.0)
pghero (1.7.0)
activerecord
- pkg-config (1.2.4)
+ pkg-config (1.2.7)
powerpack (0.1.1)
pry (0.10.4)
coderay (~> 1.1.0)
@@ -359,6 +365,8 @@ GEM
rack-cors (0.4.1)
rack-protection (2.0.0)
rack
+ rack-proxy (0.6.2)
+ rack
rack-test (0.7.0)
rack (>= 1.0, < 3)
rack-timeout (0.4.2)
@@ -396,8 +404,8 @@ GEM
thor (>= 0.18.1, < 2.0)
rainbow (2.2.2)
rake
- rake (12.0.0)
- rdf (2.2.8)
+ rake (12.1.0)
+ rdf (2.2.9)
hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.2)
@@ -421,6 +429,7 @@ GEM
redis-store (>= 1.2, < 2)
redis-store (1.3.0)
redis (>= 2.2)
+ request_store (1.3.2)
responders (2.4.0)
actionpack (>= 4.2.0, < 5.3)
railties (>= 4.2.0, < 5.3)
@@ -435,7 +444,7 @@ GEM
rspec-mocks (3.6.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0)
- rspec-rails (3.6.0)
+ rspec-rails (3.6.1)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
@@ -447,15 +456,15 @@ GEM
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.6.0)
- rubocop (0.49.1)
+ rubocop (0.50.0)
parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1)
- rainbow (>= 1.99.1, < 3.0)
+ rainbow (>= 2.2.2, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-oembed (0.12.0)
- ruby-progressbar (1.8.1)
+ ruby-progressbar (1.8.3)
rufus-scheduler (3.4.2)
et-orbi (~> 1.0)
safe_yaml (1.0.4)
@@ -463,7 +472,7 @@ GEM
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1)
- sass (3.4.24)
+ sass (3.4.25)
scss_lint (0.54.0)
rake (>= 0.9, < 13)
sass (~> 3.4.20)
@@ -475,12 +484,12 @@ GEM
sidekiq-bulk (0.1.1)
activesupport
sidekiq
- sidekiq-scheduler (2.1.8)
+ sidekiq-scheduler (2.1.9)
redis (~> 3)
rufus-scheduler (~> 3.2)
sidekiq (>= 3)
tilt (>= 1.4.0)
- sidekiq-unique-jobs (5.0.9)
+ sidekiq-unique-jobs (5.0.10)
sidekiq (>= 4.0, <= 6.0)
thor (~> 0)
simple-navigation (4.0.5)
@@ -488,11 +497,11 @@ GEM
simple_form (3.5.0)
actionpack (> 4, < 5.2)
activemodel (> 4, < 5.2)
- simplecov (0.14.1)
+ simplecov (0.15.1)
docile (~> 1.1.0)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
- simplecov-html (0.10.1)
+ simplecov-html (0.10.2)
slack-notifier (2.3.1)
slop (3.6.0)
sprockets (3.7.1)
@@ -502,10 +511,12 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
- sshkit (1.13.1)
+ sshkit (1.14.0)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
- statsd-instrument (2.1.4)
+ statsd-ruby (1.2.1)
+ strong_migrations (0.1.9)
+ activerecord (>= 3.2.0)
temple (0.8.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
@@ -528,13 +539,13 @@ GEM
uniform_notifier (1.10.0)
warden (1.2.7)
rack (>= 1.0)
- webmock (3.0.1)
+ webmock (3.1.0)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
- webpacker (2.0)
+ webpacker (3.0.1)
activesupport (>= 4.2)
- multi_json (~> 1.2)
+ rack-proxy (>= 0.6.1)
railties (>= 4.2)
webpush (0.3.2)
hkdf (~> 0.2)
@@ -558,10 +569,10 @@ DEPENDENCIES
better_errors (~> 2.1)
binding_of_caller (~> 0.7)
bootsnap
- brakeman (~> 3.6)
+ brakeman (~> 4.0)
browser
bullet (~> 5.5)
- bundler-audit (~> 0.5)
+ bundler-audit (~> 0.6)
capistrano (~> 3.8)
capistrano-rails (~> 1.2)
capistrano-rbenv (~> 2.1)
@@ -600,8 +611,9 @@ DEPENDENCIES
microformats (~> 4.0)
mime-types (~> 3.1)
nokogiri (~> 1.7)
+ nsa (~> 0.2)
oj (~> 3.0)
- omniauth-niconico
+ omniauth-niconico (= 0.0.1)
ostatus2 (~> 2.0)
ox (~> 2.5)
paperclip (~> 5.1)
@@ -642,16 +654,16 @@ DEPENDENCIES
simplecov (~> 0.14)
slack-notifier (~> 2.3.1)
sprockets-rails (~> 3.2)
- statsd-instrument (~> 2.1)
+ strong_migrations
twitter-text (~> 1.14)
tzinfo-data (~> 1.2017)
uglifier (~> 3.2)
webmock (~> 3.0)
- webpacker (~> 2.0)
+ webpacker (~> 3.0)
webpush
RUBY VERSION
- ruby 2.4.1p111
+ ruby 2.4.2p198
BUNDLED WITH
1.15.4
diff --git a/Procfile.dev b/Procfile.dev
index 9084b4263ce831..e75a491c7b4219 100644
--- a/Procfile.dev
+++ b/Procfile.dev
@@ -1,4 +1,4 @@
web: PORT=3000 bundle exec puma -C config/puma.rb
sidekiq: PORT=3000 bundle exec sidekiq
stream: PORT=4000 yarn run start
-webpack: ./bin/webpack-dev-server --host 0.0.0.0
+webpack: ./bin/webpack-dev-server --listen-host 0.0.0.0
diff --git a/README.md b/README.md
index 7b3fe447ee2863..a0bde5b9e6b515 100644
--- a/README.md
+++ b/README.md
@@ -26,46 +26,62 @@ friends.nicoへようこそ!
[travis]: https://travis-ci.org/tootsuite/mastodon
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon
-Mastodon is a free, open-source social network server. A decentralized solution 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.
+Mastodon is a **free, open-source social network server** based on **open web protocols** like ActivityPub and OStatus. The social focus of the project is a viable decentralized alternative to commercial social media silos that returns the control of the content distribution channels to the people. The technical focus of the project is a good user interface, a clean REST API for 3rd party apps and robust anti-abuse tools.
-An alternative implementation of the GNU social project. Based on [ActivityStreams](https://en.wikipedia.org/wiki/Activity_Streams_(format)), [Webfinger](https://en.wikipedia.org/wiki/WebFinger), [WebSub](https://en.wikipedia.org/wiki/WebSub) and [Salmon](https://en.wikipedia.org/wiki/Salmon_(protocol)).
-
-Click on the screenshot to watch a demo of the UI:
+Click on the screenshot below to watch a demo of the UI:
[![Screenshot](https://i.imgur.com/pG3Nnz3.jpg)][youtube_demo]
[youtube_demo]: https://www.youtube.com/watch?v=YO1jQ8_rAMU
-The project focus is a clean REST API and a good user interface. Ruby on Rails is used for the back-end, while React.js and Redux are used for the dynamic front-end. A static front-end for public resources (profiles and statuses) is also provided.
+**Ruby on Rails** is used for the back-end, while **React.js** and Redux are used for the dynamic front-end. A static front-end for public resources (profiles and statuses) is also provided.
If you would like, you can [support the development of this project on Patreon][patreon]. Alternatively, you can donate to this BTC address: `17j2g7vpgHhLuXhN4bueZFCvdxxieyRVWd`
[patreon]: https://www.patreon.com/user?u=619786
+---
+
## Resources
-- [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](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)
+- [Use this tool to find Twitter friends on Mastodon](https://bridge.joinmastodon.org)
+- [API overview](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)
+- [List of Mastodon instances](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md)
- [List of apps](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md)
+- [List of sponsors](https://joinmastodon.org/sponsors)
## Features
-- **Fully interoperable with GNU social and any OStatus platform**
- Whatever implements Atom feeds, ActivityStreams, Salmon, WebSub and Webfinger is part of the network
-- **Real-time timeline updates**
- See the updates of people you're following appear in real-time in the UI via WebSockets
-- **Federated thread resolving**
- If someone you follow replies to a user unknown to the server, the server fetches the full thread so you can view it without leaving the UI
-- **Media attachments like images and WebM**
- Upload and view images and WebM videos attached to the updates
-- **OAuth2 and a straightforward REST API**
- Mastodon acts as an OAuth2 provider so 3rd party apps can use the API, which is RESTful and simple
-- **Background processing for long-running tasks**
- Mastodon tries to be as fast and responsive as possible, so all long-running tasks that can be delegated to background processing, are
-- **Deployable via Docker**
- You don't need to mess with dependencies and configuration if you want to try Mastodon, if you have Docker and Docker Compose the deployment is extremely easy
+**No vendor lock-in: Fully interoperable with any conforming platform**
+
+It doesn't have to be Mastodon, whatever implements ActivityPub or OStatus is part of the social network!
+
+**Real-time timeline updates**
+
+See the updates of people you're following appear in real-time in the UI via WebSockets. There's a firehose view as well!
+
+**Federated thread resolving**
+
+If someone you follow replies to a user unknown to the server, the server fetches the full thread so you can view it without leaving the UI
+
+**Media attachments like images and short videos**
+
+Upload and view images and WebM/MP4 videos attached to the updates. Videos with no audio track are treated like GIFs; normal videos are looped - like vines!
+
+**OAuth2 and a straightforward REST API**
+
+Mastodon acts as an OAuth2 provider so 3rd party apps can use the API
+
+**Fast response times**
+
+Mastodon tries to be as fast and responsive as possible, so all long-running tasks are delegated to background processing
+
+**Deployable via Docker**
+
+You don't need to mess with dependencies and configuration if you want to try Mastodon, if you have Docker and Docker Compose the deployment is extremely easy
+
+---
## Development
@@ -81,9 +97,8 @@ You can open issues for bugs you've found or features you think are missing. You
**IRC channel**: #mastodon on irc.freenode.net
-## Extra credits
+---
-- The [Emoji One](https://github.com/Ranks/emojione) pack has been used for the emojis
-- The error page image courtesy of [Dopatwo](https://www.youtube.com/user/dopatwo)
+## Extra credits
-![Mastodon error image](https://mastodon.social/oops.png)
+The elephant friend illustrations are created by [Dopatwo](https://mastodon.social/@dopatwo)
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 26ab6636b54758..75915b33712f62 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -26,7 +26,10 @@ def show
end
format.json do
- render json: @account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
+ render json: @account,
+ serializer: ActivityPub::ActorSerializer,
+ adapter: ActivityPub::Adapter,
+ content_type: 'application/activity+json'
end
end
end
diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb
index b37910b364d771..76553a162a24b0 100644
--- a/app/controllers/activitypub/inboxes_controller.rb
+++ b/app/controllers/activitypub/inboxes_controller.rb
@@ -9,9 +9,9 @@ def create
if signed_request_account
upgrade_account
process_payload
- head 201
- else
head 202
+ else
+ [signature_verification_failure_reason, 401]
end
end
@@ -32,6 +32,7 @@ def upgrade_account
end
Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed?
+ DeliveryFailureTracker.track_inverse_success!(signed_request_account)
end
def process_payload
diff --git a/app/controllers/admin/account_moderation_notes_controller.rb b/app/controllers/admin/account_moderation_notes_controller.rb
new file mode 100644
index 00000000000000..414a875d04be4c
--- /dev/null
+++ b/app/controllers/admin/account_moderation_notes_controller.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class Admin::AccountModerationNotesController < Admin::BaseController
+ def create
+ @account_moderation_note = current_account.account_moderation_notes.new(resource_params)
+ if @account_moderation_note.save
+ @target_account = @account_moderation_note.target_account
+ redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.created_msg')
+ else
+ @account = @account_moderation_note.target_account
+ @moderation_notes = @account.targeted_moderation_notes.latest
+ render template: 'admin/accounts/show'
+ end
+ end
+
+ def destroy
+ @account_moderation_note = AccountModerationNote.find(params[:id])
+ @target_account = @account_moderation_note.target_account
+ @account_moderation_note.destroy
+ redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
+ end
+
+ private
+
+ def resource_params
+ params.require(:account_moderation_note).permit(
+ :content,
+ :target_account_id
+ )
+ end
+end
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index 54c659e1b9f137..ffa4dc850f0cd5 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -9,7 +9,10 @@ def index
@accounts = filtered_accounts.page(params[:page])
end
- def show; end
+ def show
+ @account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
+ @moderation_notes = @account.targeted_moderation_notes.latest
+ end
def subscribe
Pubsubhubbub::SubscribeWorker.perform_async(@account.id)
diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb
new file mode 100644
index 00000000000000..5cce5bce4667d7
--- /dev/null
+++ b/app/controllers/admin/custom_emojis_controller.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module Admin
+ class CustomEmojisController < BaseController
+ before_action :set_custom_emoji, except: [:index, :new, :create]
+
+ def index
+ @custom_emojis = filtered_custom_emojis.page(params[:page])
+ end
+
+ def new
+ @custom_emoji = CustomEmoji.new
+ end
+
+ def create
+ @custom_emoji = CustomEmoji.new(resource_params)
+
+ if @custom_emoji.save
+ redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg')
+ else
+ render :new
+ end
+ end
+
+ def destroy
+ @custom_emoji.destroy
+ redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
+ end
+
+ def copy
+ emoji = CustomEmoji.new(domain: nil, shortcode: @custom_emoji.shortcode, image: @custom_emoji.image)
+
+ if emoji.save
+ flash[:notice] = I18n.t('admin.custom_emojis.copied_msg')
+ else
+ flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
+ end
+
+ redirect_to admin_custom_emojis_path(params[:page])
+ end
+
+ def enable
+ @custom_emoji.update!(disabled: false)
+ redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg')
+ end
+
+ def disable
+ @custom_emoji.update!(disabled: true)
+ redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg')
+ end
+
+ private
+
+ def set_custom_emoji
+ @custom_emoji = CustomEmoji.find(params[:id])
+ end
+
+ def resource_params
+ params.require(:custom_emoji).permit(:shortcode, :image)
+ end
+
+ def filtered_custom_emojis
+ CustomEmojiFilter.new(filter_params).results
+ end
+
+ def filter_params
+ params.permit(
+ :local,
+ :remote
+ )
+ end
+ end
+end
diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb
new file mode 100644
index 00000000000000..09275d5dc822d6
--- /dev/null
+++ b/app/controllers/admin/email_domain_blocks_controller.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Admin
+ class EmailDomainBlocksController < BaseController
+ before_action :set_email_domain_block, only: [:show, :destroy]
+
+ def index
+ @email_domain_blocks = EmailDomainBlock.page(params[:page])
+ end
+
+ def new
+ @email_domain_block = EmailDomainBlock.new
+ end
+
+ def create
+ @email_domain_block = EmailDomainBlock.new(resource_params)
+
+ if @email_domain_block.save
+ redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg')
+ else
+ render :new
+ end
+ end
+
+ def destroy
+ @email_domain_block.destroy
+ redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg')
+ end
+
+ private
+
+ def set_email_domain_block
+ @email_domain_block = EmailDomainBlock.find(params[:id])
+ end
+
+ def resource_params
+ params.require(:email_domain_block).permit(:domain)
+ end
+ end
+end
diff --git a/app/controllers/api/salmon_controller.rb b/app/controllers/api/salmon_controller.rb
index e9e700b18de5c7..143e9d3cdc6b8b 100644
--- a/app/controllers/api/salmon_controller.rb
+++ b/app/controllers/api/salmon_controller.rb
@@ -7,9 +7,11 @@ class Api::SalmonController < Api::BaseController
def update
if verify_payload?
process_salmon
- head 201
- else
head 202
+ elsif payload.present?
+ [signature_verification_failure_reason, 401]
+ else
+ head 400
end
end
diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb
index a88cf2021acf96..91a942d7530fb5 100644
--- a/app/controllers/api/v1/accounts/relationships_controller.rb
+++ b/app/controllers/api/v1/accounts/relationships_controller.rb
@@ -7,7 +7,10 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
respond_to :json
def index
- @accounts = Account.where(id: account_ids).select('id')
+ accounts = Account.where(id: account_ids).select('id')
+ # .where doesn't guarantee that our results are in the same order
+ # we requested them, so return the "right" order to the requestor.
+ @accounts = accounts.index_by(&:id).values_at(*account_ids)
render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
end
diff --git a/app/controllers/api/v1/apps/credentials_controller.rb b/app/controllers/api/v1/apps/credentials_controller.rb
new file mode 100644
index 00000000000000..e469c7d210459e
--- /dev/null
+++ b/app/controllers/api/v1/apps/credentials_controller.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class Api::V1::Apps::CredentialsController < Api::BaseController
+ before_action -> { doorkeeper_authorize! :read }
+
+ respond_to :json
+
+ def show
+ render json: doorkeeper_token.application, serializer: REST::StatusSerializer::ApplicationSerializer
+ end
+end
diff --git a/app/controllers/api/v1/apps_controller.rb b/app/controllers/api/v1/apps_controller.rb
index 44a27b20a2208b..e9f7a7291c15d9 100644
--- a/app/controllers/api/v1/apps_controller.rb
+++ b/app/controllers/api/v1/apps_controller.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class Api::V1::AppsController < Api::BaseController
- respond_to :json
-
def create
@app = Doorkeeper::Application.create!(application_options)
render json: @app, serializer: REST::ApplicationSerializer
diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb
index a412e434145805..3a6690766cf01a 100644
--- a/app/controllers/api/v1/blocks_controller.rb
+++ b/app/controllers/api/v1/blocks_controller.rb
@@ -15,19 +15,17 @@ def index
private
def load_accounts
- default_accounts.merge(paginated_blocks).to_a
- end
-
- def default_accounts
- Account.includes(:blocked_by).references(:blocked_by)
+ paginated_blocks.map(&:target_account)
end
def paginated_blocks
- Block.where(account: current_account).paginate_by_max_id(
- limit_param(DEFAULT_ACCOUNTS_LIMIT),
- params[:max_id],
- params[:since_id]
- )
+ @paginated_blocks ||= Block.eager_load(:target_account)
+ .where(account: current_account)
+ .paginate_by_max_id(
+ limit_param(DEFAULT_ACCOUNTS_LIMIT),
+ params[:max_id],
+ params[:since_id]
+ )
end
def insert_pagination_headers
@@ -41,21 +39,21 @@ def next_path
end
def prev_path
- unless @accounts.empty?
+ unless paginated_blocks.empty?
api_v1_blocks_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
- @accounts.last.blocked_by_ids.last
+ paginated_blocks.last.id
end
def pagination_since_id
- @accounts.first.blocked_by_ids.first
+ paginated_blocks.first.id
end
def records_continue?
- @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
+ paginated_blocks.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
def pagination_params(core_params)
diff --git a/app/controllers/api/v1/custom_emojis_controller.rb b/app/controllers/api/v1/custom_emojis_controller.rb
new file mode 100644
index 00000000000000..f8cd64455a009b
--- /dev/null
+++ b/app/controllers/api/v1/custom_emojis_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Api::V1::CustomEmojisController < Api::BaseController
+ respond_to :json
+
+ def index
+ render json: CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer
+ end
+end
diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb
index 8a1992fca4160b..9f330f0dfe962d 100644
--- a/app/controllers/api/v1/media_controller.rb
+++ b/app/controllers/api/v1/media_controller.rb
@@ -10,7 +10,7 @@ class Api::V1::MediaController < Api::BaseController
respond_to :json
def create
- @media = current_account.media_attachments.create!(file: media_params[:file])
+ @media = current_account.media_attachments.create!(media_params)
render json: @media, serializer: REST::MediaAttachmentSerializer
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
render json: file_type_error, status: 422
@@ -18,10 +18,16 @@ def create
render json: processing_error, status: 500
end
+ def update
+ @media = current_account.media_attachments.where(status_id: nil).find(params[:id])
+ @media.update!(media_params)
+ render json: @media, serializer: REST::MediaAttachmentSerializer
+ end
+
private
def media_params
- params.permit(:file)
+ params.permit(:file, :description)
end
def file_type_error
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 0b40fb05b730f3..d5eca6ffb60125 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base
helper_method :current_account
helper_method :current_session
+ helper_method :current_theme
helper_method :single_user_mode?
rescue_from ActionController::RoutingError, with: :not_found
@@ -77,6 +78,11 @@ def current_session
@current_session ||= SessionActivation.find_by(session_id: cookies.signed['_session_id'])
end
+ def current_theme
+ return Setting.default_settings['theme'] unless Themes.instance.names.include? current_user&.setting_theme
+ current_user.setting_theme
+ end
+
def cache_collection(raw, klass)
return raw unless klass.respond_to?(:with_includes)
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index 60ace04d7b1e4b..223db96ff24059 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -6,6 +6,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :check_enabled_registrations, only: [:new, :create]
before_action :configure_sign_up_params, only: [:create]
before_action :set_sessions, only: [:edit, :update]
+ before_action :set_instance_presenter, only: [:new, :create, :update]
def destroy
not_found
@@ -39,6 +40,10 @@ def check_enabled_registrations
private
+ def set_instance_presenter
+ @instance_presenter = InstancePresenter.new
+ end
+
def determine_layout
%w(edit update).include?(action_name) ? 'admin' : 'auth'
end
diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb
index bc3bd2f4bcd156..463a183e432a53 100644
--- a/app/controllers/auth/sessions_controller.rb
+++ b/app/controllers/auth/sessions_controller.rb
@@ -8,6 +8,7 @@ class Auth::SessionsController < Devise::SessionsController
skip_before_action :require_no_authentication, only: [:create]
skip_before_action :check_suspension, only: [:destroy]
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
+ before_action :set_instance_presenter, only: [:new]
def create
super do |resource|
@@ -84,6 +85,10 @@ def prompt_for_two_factor(user)
private
+ def set_instance_presenter
+ @instance_presenter = InstancePresenter.new
+ end
+
def home_paths(resource)
paths = [about_path]
if single_user_mode? && resource.is_a?(User)
diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb
index 4211283ed737a4..2baafb5bf5d0a5 100644
--- a/app/controllers/concerns/signature_verification.rb
+++ b/app/controllers/concerns/signature_verification.rb
@@ -9,10 +9,15 @@ def signed_request?
request.headers['Signature'].present?
end
+ def signature_verification_failure_reason
+ return @signature_verification_failure_reason if defined?(@signature_verification_failure_reason)
+ end
+
def signed_request_account
return @signed_request_account if defined?(@signed_request_account)
unless signed_request?
+ @signature_verification_failure_reason = 'Request not signed'
@signed_request_account = nil
return
end
@@ -27,6 +32,7 @@ def signed_request_account
end
if incompatible_signature?(signature_params)
+ @signature_verification_failure_reason = 'Incompatible request signature'
@signed_request_account = nil
return
end
@@ -34,6 +40,7 @@ def signed_request_account
account = account_from_key_id(signature_params['keyId'])
if account.nil?
+ @signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
@signed_request_account = nil
return
end
@@ -44,7 +51,18 @@ def signed_request_account
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
@signed_request_account = account
@signed_request_account
+ elsif account.possibly_stale?
+ account = account.refresh!
+
+ if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
+ @signed_request_account = account
+ @signed_request_account
+ else
+ @signed_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
+ @signed_request_account = nil
+ end
else
+ @signed_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
@signed_request_account = nil
end
end
@@ -99,7 +117,7 @@ def account_from_key_id(key_id)
ResolveRemoteAccountService.new.call(key_id.gsub(/\Aacct:/, ''))
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
- account ||= ActivityPub::FetchRemoteKeyService.new.call(key_id)
+ account ||= ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false)
account
end
end
diff --git a/app/controllers/concerns/user_tracking_concern.rb b/app/controllers/concerns/user_tracking_concern.rb
index 8a63af95d0aa3d..8663c3086b2dfa 100644
--- a/app/controllers/concerns/user_tracking_concern.rb
+++ b/app/controllers/concerns/user_tracking_concern.rb
@@ -7,12 +7,14 @@ module UserTrackingConcern
UPDATE_SIGN_IN_HOURS = 24
included do
- before_action :set_user_activity, if: %i(user_signed_in? user_needs_sign_in_update?)
+ before_action :set_user_activity
end
private
def set_user_activity
+ return unless user_needs_sign_in_update?
+
# Mark as signed-in today
current_user.update_tracked_fields!(request)
@@ -21,7 +23,7 @@ def set_user_activity
end
def user_needs_sign_in_update?
- current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_HOURS.hours.ago
+ user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_HOURS.hours.ago)
end
def user_needs_feed_update?
diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb
new file mode 100644
index 00000000000000..a82b9340bf83ae
--- /dev/null
+++ b/app/controllers/emojis_controller.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class EmojisController < ApplicationController
+ before_action :set_emoji
+
+ def show
+ respond_to do |format|
+ format.json do
+ render json: @emoji,
+ serializer: ActivityPub::EmojiSerializer,
+ adapter: ActivityPub::Adapter,
+ content_type: 'application/activity+json'
+ end
+ end
+ end
+
+ private
+
+ def set_emoji
+ @emoji = CustomEmoji.local.find(params[:id])
+ end
+end
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index 0e194989790006..399e79665e728b 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -10,19 +10,39 @@ def index
format.html
format.json do
- render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
+ render json: collection_presenter,
+ serializer: ActivityPub::CollectionSerializer,
+ adapter: ActivityPub::Adapter,
+ content_type: 'application/activity+json'
end
end
end
private
+ def page_url(page)
+ account_followers_url(@account, page: page) unless page.nil?
+ end
+
def collection_presenter
- ActivityPub::CollectionPresenter.new(
- id: account_followers_url(@account),
+ page = ActivityPub::CollectionPresenter.new(
+ id: account_followers_url(@account, page: params.fetch(:page, 1)),
type: :ordered,
size: @account.followers_count,
- items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) }
+ items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) },
+ part_of: account_followers_url(@account),
+ next: page_url(@follows.next_page),
+ prev: page_url(@follows.prev_page)
)
+ if params[:page].present?
+ page
+ else
+ ActivityPub::CollectionPresenter.new(
+ id: account_followers_url(@account),
+ type: :ordered,
+ size: @account.followers_count,
+ first: page
+ )
+ end
end
end
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index d4593093ff643b..1e73d4bd4087a0 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -10,19 +10,39 @@ def index
format.html
format.json do
- render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
+ render json: collection_presenter,
+ serializer: ActivityPub::CollectionSerializer,
+ adapter: ActivityPub::Adapter,
+ content_type: 'application/activity+json'
end
end
end
private
+ def page_url(page)
+ account_following_index_url(@account, page: page) unless page.nil?
+ end
+
def collection_presenter
- ActivityPub::CollectionPresenter.new(
- id: account_following_index_url(@account),
+ page = ActivityPub::CollectionPresenter.new(
+ id: account_following_index_url(@account, page: params.fetch(:page, 1)),
type: :ordered,
size: @account.following_count,
- items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) }
+ items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) },
+ part_of: account_following_index_url(@account),
+ next: page_url(@follows.next_page),
+ prev: page_url(@follows.prev_page)
)
+ if params[:page].present?
+ page
+ else
+ ActivityPub::CollectionPresenter.new(
+ id: account_following_index_url(@account),
+ type: :ordered,
+ size: @account.following_count,
+ first: page
+ )
+ end
end
end
diff --git a/app/controllers/manifests_controller.rb b/app/controllers/manifests_controller.rb
index 832e1eb6f67e79..ac267c229459b2 100644
--- a/app/controllers/manifests_controller.rb
+++ b/app/controllers/manifests_controller.rb
@@ -1,11 +1,7 @@
# frozen_string_literal: true
class ManifestsController < ApplicationController
- before_action :set_instance_presenter
-
- def show; end
-
- def set_instance_presenter
- @instance_presenter = InstancePresenter.new
+ def show
+ render json: InstancePresenter.new, serializer: ManifestSerializer
end
end
diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb
index 6a83cf9dc06eb5..155670837eb740 100644
--- a/app/controllers/media_proxy_controller.rb
+++ b/app/controllers/media_proxy_controller.rb
@@ -18,7 +18,7 @@ def show
def redownload!
@media_attachment.file_remote_url = @media_attachment.remote_url
- @media_attachment.touch(:created_at)
+ @media_attachment.created_at = Time.now.utc
@media_attachment.save!
end
diff --git a/app/controllers/settings/follower_domains_controller.rb b/app/controllers/settings/follower_domains_controller.rb
index 90b48887facd74..9968504e5f74e4 100644
--- a/app/controllers/settings/follower_domains_controller.rb
+++ b/app/controllers/settings/follower_domains_controller.rb
@@ -9,7 +9,7 @@ class Settings::FollowerDomainsController < ApplicationController
def show
@account = current_account
- @domains = current_account.followers.reorder(nil).group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10)
+ @domains = current_account.followers.reorder('MIN(follows.id) DESC').group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10)
end
def update
diff --git a/app/controllers/settings/notifications_controller.rb b/app/controllers/settings/notifications_controller.rb
new file mode 100644
index 00000000000000..09839f16eaa1cb
--- /dev/null
+++ b/app/controllers/settings/notifications_controller.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class Settings::NotificationsController < ApplicationController
+ layout 'admin'
+
+ before_action :authenticate_user!
+
+ def show; end
+
+ def update
+ user_settings.update(user_settings_params.to_h)
+
+ if current_user.save
+ redirect_to settings_notifications_path, notice: I18n.t('generic.changes_saved_msg')
+ else
+ render :show
+ end
+ end
+
+ private
+
+ def user_settings
+ UserSettingsDecorator.new(current_user)
+ end
+
+ def user_settings_params
+ params.require(:user).permit(
+ notification_emails: %i(follow follow_request reblog favourite mention digest),
+ interactions: %i(must_be_follower must_be_following)
+ )
+ end
+end
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index f107f2b165f844..0690267151c958 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -39,8 +39,10 @@ def user_settings_params
:setting_boost_modal,
:setting_delete_modal,
:setting_auto_play_gif,
+ :setting_reduce_motion,
:setting_system_font_ui,
:setting_noindex,
+ :setting_theme,
notification_emails: %i(follow follow_request reblog favourite mention digest),
interactions: %i(must_be_follower must_be_following)
)
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index 65206ea969e549..e8a360fb5752eb 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -21,13 +21,19 @@ def show
end
format.json do
- render json: @status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
+ render json: @status,
+ serializer: ActivityPub::NoteSerializer,
+ adapter: ActivityPub::Adapter,
+ content_type: 'application/activity+json'
end
end
end
def activity
- render json: @status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
+ render json: @status,
+ serializer: ActivityPub::ActivitySerializer,
+ adapter: ActivityPub::Adapter,
+ content_type: 'application/activity+json'
end
def embed
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index 3001b2ee314c52..9f3090e37be363 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -1,24 +1,40 @@
# frozen_string_literal: true
class TagsController < ApplicationController
- layout 'public'
+ before_action :set_body_classes
+ before_action :set_instance_presenter
def show
- @tag = Tag.find_by!(name: params[:id].downcase)
- @statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id])
- @statuses = cache_collection(@statuses, Status)
+ @tag = Tag.find_by!(name: params[:id].downcase)
respond_to do |format|
- format.html
+ format.html do
+ serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
+ @initial_state_json = serializable_resource.to_json
+ end
format.json do
- render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
+ @statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id])
+ @statuses = cache_collection(@statuses, Status)
+
+ render json: collection_presenter,
+ serializer: ActivityPub::CollectionSerializer,
+ adapter: ActivityPub::Adapter,
+ content_type: 'application/activity+json'
end
end
end
private
+ def set_body_classes
+ @body_classes = 'tag-body'
+ end
+
+ def set_instance_presenter
+ @instance_presenter = InstancePresenter.new
+ end
+
def collection_presenter
ActivityPub::CollectionPresenter.new(
id: tag_url(@tag),
@@ -27,4 +43,11 @@ def collection_presenter
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
)
end
+
+ def initial_state_params
+ {
+ settings: {},
+ token: current_session&.token,
+ }
+ end
end
diff --git a/app/helpers/admin/account_moderation_notes_helper.rb b/app/helpers/admin/account_moderation_notes_helper.rb
new file mode 100644
index 00000000000000..b17c522643d234
--- /dev/null
+++ b/app/helpers/admin/account_moderation_notes_helper.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+module Admin::AccountModerationNotesHelper
+end
diff --git a/app/helpers/emoji_helper.rb b/app/helpers/emoji_helper.rb
deleted file mode 100644
index 848c03fce721aa..00000000000000
--- a/app/helpers/emoji_helper.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-module EmojiHelper
- def emojify(text)
- return text if text.blank?
-
- text.gsub(emoji_pattern) do |match|
- emoji = Emoji.instance.unicode($1) # rubocop:disable Style/PerlBackrefs
-
- if emoji
- emoji
- else
- match
- end
- end
- end
-
- def emoji_pattern
- @emoji_pattern ||=
- /(?<=[^[:alnum:]:]|\n|^)
- (#{Emoji.instance.names.map { |name| Regexp.escape(name) }.join('|')})
- (?=[^[:alnum:]:]|$)/x
- end
-end
diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb
index d82a073320d3b4..c23a2e095278d4 100644
--- a/app/helpers/jsonld_helper.rb
+++ b/app/helpers/jsonld_helper.rb
@@ -22,7 +22,18 @@ def canonicalize(json)
graph.dump(:normalize)
end
- def fetch_resource(uri)
+ def fetch_resource(uri, id)
+ unless id
+ json = fetch_resource_without_id_validation(uri)
+ return unless json
+ uri = json['id']
+ end
+
+ json = fetch_resource_without_id_validation(uri)
+ json.present? && json['id'] == uri ? json : nil
+ end
+
+ def fetch_resource_without_id_validation(uri)
response = build_request(uri).perform
return if response.code != 200
body_to_json(response.to_s)
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index 14776b35400c7a..abce858123425e 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -27,6 +27,7 @@ module SettingsHelper
pt: 'Português',
'pt-BR': 'Português do Brasil',
ru: 'Русский',
+ sv: 'Svenska',
th: 'ภาษาไทย',
tr: 'Türkçe',
uk: 'Українська',
diff --git a/app/javascript/__mock__/fileMock.js b/app/javascript/__mock__/fileMock.js
new file mode 100644
index 00000000000000..f053ebf7976e37
--- /dev/null
+++ b/app/javascript/__mock__/fileMock.js
@@ -0,0 +1 @@
+module.exports = {};
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index 03e3d3d9f94f1f..73d6baaceaa61b 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -122,7 +122,7 @@ export function unfollowAccount(id) {
dispatch(unfollowAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => {
- dispatch(unfollowAccountSuccess(response.data));
+ dispatch(unfollowAccountSuccess(response.data, getState().get('statuses')));
}).catch(error => {
dispatch(unfollowAccountFail(error));
});
@@ -157,10 +157,11 @@ export function unfollowAccountRequest(id) {
};
};
-export function unfollowAccountSuccess(relationship) {
+export function unfollowAccountSuccess(relationship, statuses) {
return {
type: ACCOUNT_UNFOLLOW_SUCCESS,
relationship,
+ statuses,
};
};
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index c47840911c45c8..7d9aec6e505fd2 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -1,4 +1,7 @@
import api from '../api';
+import { throttle } from 'lodash';
+import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
+import { useEmoji } from './emojis';
import {
updateTimeline,
@@ -14,6 +17,7 @@ export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
+export const COMPOSE_RESET = 'COMPOSE_RESET';
export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
@@ -41,6 +45,10 @@ export const COMPOSE_PROFILE_EMOJI_SUGGESTIONS_CLEAR = 'COMPOSE_PROFILE_EMOJI_SU
export const COMPOSE_PROFILE_EMOJI_SUGGESTIONS_READY = 'COMPOSE_PROFILE_EMOJI_SUGGESTIONS_READY';
export const COMPOSE_PROFILE_EMOJI_SUGGESTION_SELECT = 'COMPOSE_PROFILE_EMOJI_SUGGESTION_SELECT';
+export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST';
+export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
+export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
+
export function changeCompose(text) {
return {
type: COMPOSE_CHANGE,
@@ -67,6 +75,12 @@ export function cancelReplyCompose() {
};
};
+export function resetCompose() {
+ return {
+ type: COMPOSE_RESET,
+ };
+};
+
export function mentionCompose(account, router) {
return (dispatch, getState) => {
dispatch({
@@ -181,6 +195,40 @@ export function uploadCompose(files) {
};
};
+export function changeUploadCompose(id, description) {
+ return (dispatch, getState) => {
+ dispatch(changeUploadComposeRequest());
+
+ api(getState).put(`/api/v1/media/${id}`, { description }).then(response => {
+ dispatch(changeUploadComposeSuccess(response.data));
+ }).catch(error => {
+ dispatch(changeUploadComposeFail(id, error));
+ });
+ };
+};
+
+export function changeUploadComposeRequest() {
+ return {
+ type: COMPOSE_UPLOAD_CHANGE_REQUEST,
+ skipLoading: true,
+ };
+};
+export function changeUploadComposeSuccess(media) {
+ return {
+ type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
+ media: media,
+ skipLoading: true,
+ };
+};
+
+export function changeUploadComposeFail(error) {
+ return {
+ type: COMPOSE_UPLOAD_CHANGE_FAIL,
+ error: error,
+ skipLoading: true,
+ };
+};
+
export function uploadComposeRequest() {
return {
type: COMPOSE_UPLOAD_REQUEST,
@@ -225,21 +273,46 @@ export function clearComposeSuggestions() {
};
};
+const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => {
+ api(getState).get('/api/v1/accounts/search', {
+ params: {
+ q: token.slice(1),
+ resolve: false,
+ limit: 4,
+ },
+ }).then(response => {
+ dispatch(readyComposeSuggestionsAccounts(token, response.data));
+ });
+}, 200, { leading: true, trailing: true });
+
+const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => {
+ const results = emojiSearch(token.replace(':', ''), { maxResults: 5 });
+ dispatch(readyComposeSuggestionsEmojis(token, results));
+};
+
export function fetchComposeSuggestions(token) {
return (dispatch, getState) => {
- api(getState).get('/api/v1/accounts/search', {
- params: {
- q: token,
- resolve: false,
- limit: 4,
- },
- }).then(response => {
- dispatch(readyComposeSuggestions(token, response.data));
- });
+ if (token[0] === ':') {
+ if (token[1] === '@') {
+ fetchComposeSuggestionsAccounts(dispatch, getState, token.slice(1));
+ } else {
+ fetchComposeSuggestionsEmojis(dispatch, getState, token);
+ }
+ } else {
+ fetchComposeSuggestionsAccounts(dispatch, getState, token);
+ }
+ };
+};
+
+export function readyComposeSuggestionsEmojis(token, emojis) {
+ return {
+ type: COMPOSE_SUGGESTIONS_READY,
+ token,
+ emojis,
};
};
-export function readyComposeSuggestions(token, accounts) {
+export function readyComposeSuggestionsAccounts(token, accounts) {
return {
type: COMPOSE_SUGGESTIONS_READY,
token,
@@ -247,13 +320,26 @@ export function readyComposeSuggestions(token, accounts) {
};
};
-export function selectComposeSuggestion(position, token, accountId) {
+export function selectComposeSuggestion(position, token, suggestion) {
return (dispatch, getState) => {
- const completion = getState().getIn(['accounts', accountId, 'acct']);
+ let completion, startPosition;
+
+ if (typeof suggestion === 'object' && suggestion.id) {
+ completion = suggestion.native || suggestion.colons;
+ startPosition = position - 1;
+
+ dispatch(useEmoji(suggestion));
+ } else {
+ completion = getState().getIn(['accounts', suggestion, 'acct']);
+ if (token[1] === '@') {
+ completion = `@${completion}:`;
+ }
+ startPosition = position;
+ }
dispatch({
type: COMPOSE_SUGGESTION_SELECT,
- position,
+ position: startPosition,
token,
completion,
});
diff --git a/app/javascript/mastodon/actions/emojis.js b/app/javascript/mastodon/actions/emojis.js
new file mode 100644
index 00000000000000..7cd9d4b7b359f1
--- /dev/null
+++ b/app/javascript/mastodon/actions/emojis.js
@@ -0,0 +1,14 @@
+import { saveSettings } from './settings';
+
+export const EMOJI_USE = 'EMOJI_USE';
+
+export function useEmoji(emoji) {
+ return dispatch => {
+ dispatch({
+ type: EMOJI_USE,
+ emoji,
+ });
+
+ dispatch(saveSettings());
+ };
+};
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index c7d2481227386d..b24ac8b733858b 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -31,6 +31,7 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
const unescapeHTML = (html) => {
const wrapper = document.createElement('div');
+ html = html.replace(/
|
|\n/, ' ');
wrapper.innerHTML = html;
return wrapper.textContent;
};
diff --git a/app/javascript/mastodon/actions/settings.js b/app/javascript/mastodon/actions/settings.js
index f9d304c96a37bd..79adca18c6ea90 100644
--- a/app/javascript/mastodon/actions/settings.js
+++ b/app/javascript/mastodon/actions/settings.js
@@ -1,6 +1,8 @@
import axios from 'axios';
+import { debounce } from 'lodash';
export const SETTING_CHANGE = 'SETTING_CHANGE';
+export const SETTING_SAVE = 'SETTING_SAVE';
export function changeSetting(key, value) {
return dispatch => {
@@ -14,10 +16,16 @@ export function changeSetting(key, value) {
};
};
+const debouncedSave = debounce((dispatch, getState) => {
+ if (getState().getIn(['settings', 'saved'])) {
+ return;
+ }
+
+ const data = getState().get('settings').filter((_, key) => key !== 'saved').toJS();
+
+ axios.put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE }));
+}, 5000, { trailing: true });
+
export function saveSettings() {
- return (_, getState) => {
- axios.put('/api/web/settings', {
- data: getState().get('settings').toJS(),
- });
- };
+ return (dispatch, getState) => debouncedSave(dispatch, getState);
};
diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js
index 0597d265ec40b5..a1db0fdd51c60c 100644
--- a/app/javascript/mastodon/actions/store.js
+++ b/app/javascript/mastodon/actions/store.js
@@ -5,8 +5,7 @@ export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
const convertState = rawState =>
fromJS(rawState, (k, v) =>
- Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x =>
- Number.isNaN(x * 1) ? x : x * 1));
+ Iterable.isIndexed(v) ? v.toList() : v.toMap());
export function hydrateStore(rawState) {
const state = convertState(rawState);
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index b6f9a34e1cc549..bb59800bab7c8f 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -18,6 +18,8 @@ export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
+export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE';
+
export function refreshTimelineSuccess(timeline, statuses, skipLoading, next) {
return {
type: TIMELINE_REFRESH_SUCCESS,
@@ -31,6 +33,16 @@ export function refreshTimelineSuccess(timeline, statuses, skipLoading, next) {
export function updateTimeline(timeline, status) {
return (dispatch, getState) => {
const references = status.reblog ? getState().get('statuses').filter((item, itemId) => (itemId === status.reblog.id || item.get('reblog') === status.reblog.id)).map((_, itemId) => itemId) : [];
+ const parents = [];
+
+ if (status.in_reply_to_id) {
+ let parent = getState().getIn(['statuses', status.in_reply_to_id]);
+
+ while (parent && parent.get('in_reply_to_id')) {
+ parents.push(parent.get('id'));
+ parent = getState().getIn(['statuses', parent.get('in_reply_to_id')]);
+ }
+ }
dispatch(checkHighlightNotification(status));
@@ -40,6 +52,14 @@ export function updateTimeline(timeline, status) {
status,
references,
});
+
+ if (parents.length > 0) {
+ dispatch({
+ type: TIMELINE_CONTEXT_UPDATE,
+ status,
+ references: parents,
+ });
+ }
};
};
diff --git a/app/javascript/mastodon/base_polyfills.js b/app/javascript/mastodon/base_polyfills.js
index 266a0020cc85fb..7856b26f9d8860 100644
--- a/app/javascript/mastodon/base_polyfills.js
+++ b/app/javascript/mastodon/base_polyfills.js
@@ -1,5 +1,5 @@
import 'intl';
-import 'intl/locale-data/jsonp/en.js';
+import 'intl/locale-data/jsonp/en';
import 'es6-symbol/implement';
import includes from 'array-includes';
import assign from 'object-assign';
diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap
new file mode 100644
index 00000000000000..76ab3374ae8488
--- /dev/null
+++ b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.js.snap
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`
children
; + const component = renderer.create(); + const tree = component.toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('renders the props.text instead of children', () => { + const text = 'foo'; + const children =children
; + const component = renderer.create(); + const tree = component.toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('renders class="button--block" if props.block given', () => { + const component = renderer.create(); + const tree = component.toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('adds class "button-secondary" if props.secondary given', () => { + const component = renderer.create(); + const tree = component.toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/app/javascript/mastodon/components/__tests__/display_name-test.js b/app/javascript/mastodon/components/__tests__/display_name-test.js new file mode 100644 index 00000000000000..0d040c4cd8cc04 --- /dev/null +++ b/app/javascript/mastodon/components/__tests__/display_name-test.js @@ -0,0 +1,18 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { fromJS } from 'immutable'; +import DisplayName from '../display_name'; + +describe('Foo
', + }); + const component = renderer.create(