Compare commits

..

2 Commits

Author SHA1 Message Date
Surinna Curtis
4aa8d9d149 Remove workaround for fixed bug in SettingToggle
SettingToggle was toggling itself in response to keydown of space, and then the keyup was doing it again
2017-09-02 08:24:55 -05:00
Surinna Curtis
6ba67f92c9 UploadArea should only preventDefault for Escape
This will make accessibility for some things less effortful, since we won't have to define a prior event handler to do whatever should be happening by default.
2017-09-02 08:24:49 -05:00
5367 changed files with 10563 additions and 27856 deletions

View File

@@ -1,6 +1,5 @@
# Service dependencies # Service dependencies
# You may set REDIS_URL instead for more advanced options # 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_HOST=redis
REDIS_PORT=6379 REDIS_PORT=6379
# You may set DATABASE_URL instead for more advanced options # You may set DATABASE_URL instead for more advanced options
@@ -27,7 +26,7 @@ LOCAL_HTTPS=true
# ALTERNATE_DOMAINS=example1.com,example2.com # ALTERNATE_DOMAINS=example1.com,example2.com
# Application secrets # Application secrets
# Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose) # Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
PAPERCLIP_SECRET= PAPERCLIP_SECRET=
SECRET_KEY_BASE= SECRET_KEY_BASE=
OTP_SECRET= OTP_SECRET=
@@ -37,7 +36,7 @@ OTP_SECRET=
# You should only generate this once per instance. If you later decide to change it, all push subscription will # You should only generate this once per instance. If you later decide to change it, all push subscription will
# be invalidated, requiring the users to access the website again to resubscribe. # be invalidated, requiring the users to access the website again to resubscribe.
# #
# Generate with `RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose) # Generate with `rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose)
# #
# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html # For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
VAPID_PRIVATE_KEY= VAPID_PRIVATE_KEY=
@@ -99,23 +98,6 @@ SMTP_FROM_ADDRESS=notifications@example.com
# S3_ENDPOINT= # S3_ENDPOINT=
# S3_SIGNATURE_VERSION= # S3_SIGNATURE_VERSION=
# Swift (optional)
# SWIFT_ENABLED=true
# SWIFT_USERNAME=
# For Keystone V3, the value for SWIFT_TENANT should be the project name
# SWIFT_TENANT=
# SWIFT_PASSWORD=
# Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid
# issues with token rate-limiting during high load.
# SWIFT_AUTH_URL=
# SWIFT_CONTAINER=
# SWIFT_OBJECT_URL=
# SWIFT_REGION=
# Defaults to 'default'
# SWIFT_DOMAIN_NAME=
# Defaults to 60 seconds. Set to 0 to disable
# SWIFT_CACHE_TTL=
# Optional alias for S3 if you want to use Cloudfront or Cloudflare in front # Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
# S3_CLOUDFRONT_HOST= # S3_CLOUDFRONT_HOST=

View File

@@ -5,14 +5,12 @@ env:
browser: true browser: true
node: true node: true
es6: true es6: true
jest: true
parser: babel-eslint parser: babel-eslint
plugins: plugins:
- react - react
- jsx-a11y - jsx-a11y
- import
parserOptions: parserOptions:
sourceType: module sourceType: module
@@ -23,14 +21,8 @@ parserOptions:
modules: true modules: true
spread: true spread: true
settings:
import/extensions:
- .js
import/ignore:
- node_modules
- \\.(css|scss|json)$
rules: rules:
brace-style: warn brace-style: warn
comma-dangle: comma-dangle:
- error - error
@@ -133,17 +125,3 @@ rules:
jsx-a11y/role-supports-aria-props: off jsx-a11y/role-supports-aria-props: off
jsx-a11y/scope: warn jsx-a11y/scope: warn
jsx-a11y/tabindex-no-positive: 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

View File

@@ -1 +1 @@
2.4.2 2.4.1

View File

@@ -26,16 +26,18 @@ addons:
postgresql: 9.4 postgresql: 9.4
apt: apt:
sources: sources:
- ubuntu-toolchain-r-test
- trusty-media - trusty-media
packages: packages:
- ffmpeg - ffmpeg
- g++-6
- libprotobuf-dev - libprotobuf-dev
- protobuf-compiler - protobuf-compiler
- libicu-dev - libicu-dev
rvm: rvm:
- 2.3.4 - 2.3.4
- 2.4.2 - 2.4.1
services: services:
- redis-server - redis-server
@@ -53,5 +55,5 @@ before_script:
script: script:
- travis_retry bundle exec parallel_test spec/ --group-by filesize --type rspec - travis_retry bundle exec parallel_test spec/ --group-by filesize --type rspec
- yarn test - npm test
- bundle exec i18n-tasks check-normalized && bundle exec i18n-tasks unused - bundle exec i18n-tasks unused

View File

@@ -1,46 +0,0 @@
# 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

View File

@@ -1,5 +1,4 @@
ffmpeg ffmpeg
libicu[0-9][0-9]
libicu-dev libicu-dev
libidn11 libidn11
libidn11-dev libidn11-dev

View File

@@ -8,25 +8,8 @@
# /config/locales/*.fr.yml @żelipapą # /config/locales/*.fr.yml @żelipapą
# /config/locales/fr.yml @żelipapą # /config/locales/fr.yml @żelipapą
# Polish
/app/javascript/mastodon/locales/pl.json @m4sk1n /app/javascript/mastodon/locales/pl.json @m4sk1n
/app/views/user_mailer/*.pl.html.erb @m4sk1n /app/views/user_mailer/*.pl.html.erb @m4sk1n
/app/views/user_mailer/*.pl.text.erb @m4sk1n /app/views/user_mailer/*.pl.text.erb @m4sk1n
/config/locales/*.pl.yml @m4sk1n /config/locales/*.pl.yml @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

View File

@@ -1,46 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at eugen@zeonfederated.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@@ -1,4 +1,4 @@
FROM ruby:2.4.2-alpine3.6 FROM ruby:2.4.1-alpine3.6
LABEL maintainer="https://github.com/tootsuite/mastodon" \ LABEL maintainer="https://github.com/tootsuite/mastodon" \
description="A GNU Social-compatible microblogging server" description="A GNU Social-compatible microblogging server"
@@ -7,8 +7,6 @@ ENV UID=991 GID=991 \
RAILS_SERVE_STATIC_FILES=true \ RAILS_SERVE_STATIC_FILES=true \
RAILS_ENV=production NODE_ENV=production RAILS_ENV=production NODE_ENV=production
ARG YARN_VERSION=1.1.0
ARG YARN_DOWNLOAD_SHA256=171c1f9ee93c488c0d774ac6e9c72649047c3f896277d88d0f805266519430f3
ARG LIBICONV_VERSION=1.15 ARG LIBICONV_VERSION=1.15
ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178 ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178
@@ -21,7 +19,6 @@ RUN apk -U upgrade \
build-base \ build-base \
icu-dev \ icu-dev \
libidn-dev \ libidn-dev \
libressl \
libtool \ libtool \
postgresql-dev \ postgresql-dev \
protobuf-dev \ protobuf-dev \
@@ -35,21 +32,16 @@ RUN apk -U upgrade \
imagemagick \ imagemagick \
libidn \ libidn \
libpq \ libpq \
nodejs \
nodejs-npm \ nodejs-npm \
nodejs \
protobuf \ protobuf \
su-exec \ su-exec \
tini \ tini \
yarn \
&& update-ca-certificates \ && 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" \ && 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 - \ && echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \
&& mkdir -p /tmp/src \
&& tar -xzf libiconv.tar.gz -C /tmp/src \ && tar -xzf libiconv.tar.gz -C /tmp/src \
&& rm libiconv.tar.gz \ && rm libiconv.tar.gz \
&& cd /tmp/src/libiconv-$LIBICONV_VERSION \ && cd /tmp/src/libiconv-$LIBICONV_VERSION \
@@ -60,12 +52,11 @@ RUN apk -U upgrade \
&& cd /mastodon \ && cd /mastodon \
&& rm -rf /tmp/* /var/cache/apk/* && rm -rf /tmp/* /var/cache/apk/*
COPY Gemfile Gemfile.lock package.json yarn.lock .yarnclean /mastodon/ COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
RUN bundle config build.nokogiri --with-iconv-lib=/usr/local/lib --with-iconv-include=/usr/local/include \ 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 \ && bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \
&& yarn --pure-lockfile \ && yarn --ignore-optional --pure-lockfile
&& yarn cache clean
COPY . /mastodon COPY . /mastodon

47
Gemfile
View File

@@ -5,8 +5,8 @@ ruby '>= 2.3.0', '< 2.5.0'
gem 'pkg-config', '~> 1.2' gem 'pkg-config', '~> 1.2'
gem 'puma', '~> 3.10' gem 'puma', '~> 3.8'
gem 'rails', '~> 5.1.4' gem 'rails', '~> 5.1.0'
gem 'uglifier', '~> 3.2' gem 'uglifier', '~> 3.2'
gem 'hamlit-rails', '~> 0.2' gem 'hamlit-rails', '~> 0.2'
@@ -14,10 +14,7 @@ gem 'pg', '~> 0.20'
gem 'pghero', '~> 1.7' gem 'pghero', '~> 1.7'
gem 'dotenv-rails', '~> 2.2' gem 'dotenv-rails', '~> 2.2'
gem 'fog-aws', '~> 1.4', require: false gem 'aws-sdk', '~> 2.9'
gem 'fog-core', '~> 1.45'
gem 'fog-local', '~> 0.4', require: false
gem 'fog-openstack', '~> 0.1', require: false
gem 'paperclip', '~> 5.1' gem 'paperclip', '~> 5.1'
gem 'paperclip-av-transcoder', '~> 0.6' gem 'paperclip-av-transcoder', '~> 0.6'
@@ -26,8 +23,7 @@ gem 'addressable', '~> 2.5'
gem 'bootsnap' gem 'bootsnap'
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.5' gem 'charlock_holmes', '~> 0.7.5'
gem 'iso-639' gem 'cld3', '~> 3.1'
gem 'cld3', '~> 3.2.0'
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
gem 'devise-two-factor', '~> 3.0' gem 'devise-two-factor', '~> 3.0'
gem 'doorkeeper', '~> 4.2' gem 'doorkeeper', '~> 4.2'
@@ -40,14 +36,13 @@ gem 'http', '~> 2.2'
gem 'http_accept_language', '~> 2.1' gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 0.99' gem 'httplog', '~> 0.99'
gem 'idn-ruby', require: 'idn' gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.1' gem 'kaminari', '~> 1.0'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.1' gem 'mime-types', '~> 3.1'
gem 'nokogiri', '~> 1.8' gem 'nokogiri', '~> 1.7'
gem 'nsa', '~> 0.2' gem 'oj', '~> 3.0'
gem 'oj', '~> 3.3'
gem 'ostatus2', '~> 2.0' gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.8' gem 'ox', '~> 2.5'
gem 'pundit', '~> 1.1' gem 'pundit', '~> 1.1'
gem 'rabl', '~> 0.13' gem 'rabl', '~> 0.13'
gem 'rack-attack', '~> 5.0' gem 'rack-attack', '~> 5.0'
@@ -67,25 +62,25 @@ gem 'sidekiq-bulk', '~>0.1.1'
gem 'simple-navigation', '~> 4.0' gem 'simple-navigation', '~> 4.0'
gem 'simple_form', '~> 3.4' gem 'simple_form', '~> 3.4'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'strong_migrations' gem 'statsd-instrument', '~> 2.1'
gem 'twitter-text', '~> 1.14' gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2017' gem 'tzinfo-data', '~> 1.2017'
gem 'webpacker', '~> 3.0' gem 'webpacker', '~> 2.0'
gem 'webpush' gem 'webpush'
gem 'json-ld-preloaded', '~> 2.2.1' gem 'json-ld-preloaded', '~> 2.2.1'
gem 'rdf-normalize', '~> 0.3.1' gem 'rdf-normalize', '~> 0.3.1'
group :development, :test do group :development, :test do
gem 'fabrication', '~> 2.18' gem 'fabrication', '~> 2.16'
gem 'fuubar', '~> 2.2' gem 'fuubar', '~> 2.2'
gem 'i18n-tasks', '~> 0.9', require: false gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-rails', '~> 0.3' gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 3.7' gem 'rspec-rails', '~> 3.6'
end end
group :test do group :test do
gem 'capybara', '~> 2.15' gem 'capybara', '~> 2.14'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 1.7' gem 'faker', '~> 1.7'
gem 'microformats', '~> 4.0' gem 'microformats', '~> 4.0'
@@ -93,29 +88,29 @@ group :test do
gem 'rspec-sidekiq', '~> 3.0' gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.14', require: false gem 'simplecov', '~> 0.14', require: false
gem 'webmock', '~> 3.0' gem 'webmock', '~> 3.0'
gem 'parallel_tests', '~> 2.17' gem 'parallel_tests', '~> 2.14'
end end
group :development do group :development do
gem 'active_record_query_trace', '~> 1.5' gem 'active_record_query_trace', '~> 1.5'
gem 'annotate', '~> 2.7' gem 'annotate', '~> 2.7'
gem 'better_errors', '~> 2.4' gem 'better_errors', '~> 2.1'
gem 'binding_of_caller', '~> 0.7' gem 'binding_of_caller', '~> 0.7'
gem 'bullet', '~> 5.5' gem 'bullet', '~> 5.5'
gem 'letter_opener', '~> 1.4' gem 'letter_opener', '~> 1.4'
gem 'letter_opener_web', '~> 1.3' gem 'letter_opener_web', '~> 1.3'
gem 'rubocop', require: false gem 'rubocop', require: false
gem 'brakeman', '~> 4.0', require: false gem 'brakeman', '~> 3.6', require: false
gem 'bundler-audit', '~> 0.6', require: false gem 'bundler-audit', '~> 0.5', require: false
gem 'scss_lint', '~> 0.55', require: false gem 'scss_lint', '~> 0.53', require: false
gem 'capistrano', '~> 3.10' gem 'capistrano', '~> 3.8'
gem 'capistrano-rails', '~> 1.3' gem 'capistrano-rails', '~> 1.2'
gem 'capistrano-rbenv', '~> 2.1' gem 'capistrano-rbenv', '~> 2.1'
gem 'capistrano-yarn', '~> 2.0' gem 'capistrano-yarn', '~> 2.0'
end end
group :production do group :production do
gem 'lograge', '~> 0.7' gem 'lograge', '~> 0.5'
gem 'redis-rails', '~> 5.0' gem 'redis-rails', '~> 5.0'
end end

View File

@@ -1,25 +1,25 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (5.1.4) actioncable (5.1.3)
actionpack (= 5.1.4) actionpack (= 5.1.3)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (~> 0.6.1) websocket-driver (~> 0.6.1)
actionmailer (5.1.4) actionmailer (5.1.3)
actionpack (= 5.1.4) actionpack (= 5.1.3)
actionview (= 5.1.4) actionview (= 5.1.3)
activejob (= 5.1.4) activejob (= 5.1.3)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (5.1.4) actionpack (5.1.3)
actionview (= 5.1.4) actionview (= 5.1.3)
activesupport (= 5.1.4) activesupport (= 5.1.3)
rack (~> 2.0) rack (~> 2.0)
rack-test (>= 0.6.3) rack-test (~> 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.1.4) actionview (5.1.3)
activesupport (= 5.1.4) activesupport (= 5.1.3)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
@@ -30,16 +30,16 @@ GEM
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.2)
active_record_query_trace (1.5.4) active_record_query_trace (1.5.4)
activejob (5.1.4) activejob (5.1.3)
activesupport (= 5.1.4) activesupport (= 5.1.3)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (5.1.4) activemodel (5.1.3)
activesupport (= 5.1.4) activesupport (= 5.1.3)
activerecord (5.1.4) activerecord (5.1.3)
activemodel (= 5.1.4) activemodel (= 5.1.3)
activesupport (= 5.1.4) activesupport (= 5.1.3)
arel (~> 8.0) arel (~> 8.0)
activesupport (5.1.4) activesupport (5.1.3)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7) i18n (~> 0.7)
minitest (~> 5.1) minitest (~> 5.1)
@@ -57,43 +57,51 @@ GEM
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
av (0.9.0) av (0.9.0)
cocaine (~> 0.5.3) cocaine (~> 0.5.3)
aws-sdk (2.10.21)
aws-sdk-resources (= 2.10.21)
aws-sdk-core (2.10.21)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-resources (2.10.21)
aws-sdk-core (= 2.10.21)
aws-sigv4 (1.0.1)
bcrypt (3.1.11) bcrypt (3.1.11)
better_errors (2.4.0) better_errors (2.1.1)
coderay (>= 1.0.0) coderay (>= 1.0.0)
erubi (>= 1.0.0) erubis (>= 2.6.6)
rack (>= 0.9.0) rack (>= 0.9.0)
binding_of_caller (0.7.3) binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.1.5) bootsnap (1.1.2)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (4.0.1) brakeman (3.7.2)
browser (2.5.2) browser (2.4.0)
builder (3.2.3) builder (3.2.3)
bullet (5.6.1) bullet (5.5.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0) uniform_notifier (~> 1.10.0)
bundler-audit (0.6.0) bundler-audit (0.6.0)
bundler (~> 1.2) bundler (~> 1.2)
thor (~> 0.18) thor (~> 0.18)
capistrano (3.10.0) capistrano (3.8.2)
airbrussh (>= 1.0.0) airbrussh (>= 1.0.0)
i18n i18n
rake (>= 10.0.0) rake (>= 10.0.0)
sshkit (>= 1.9.0) sshkit (>= 1.9.0)
capistrano-bundler (1.3.0) capistrano-bundler (1.2.0)
capistrano (~> 3.1) capistrano (~> 3.1)
sshkit (~> 1.2) sshkit (~> 1.2)
capistrano-rails (1.3.0) capistrano-rails (1.3.0)
capistrano (~> 3.1) capistrano (~> 3.1)
capistrano-bundler (~> 1.1) capistrano-bundler (~> 1.1)
capistrano-rbenv (2.1.2) capistrano-rbenv (2.1.1)
capistrano (~> 3.1) capistrano (~> 3.1)
sshkit (~> 1.3) sshkit (~> 1.3)
capistrano-yarn (2.0.2) capistrano-yarn (2.0.2)
capistrano (~> 3.0) capistrano (~> 3.0)
capybara (2.15.4) capybara (2.14.4)
addressable addressable
mini_mime (>= 0.1.3) mime-types (>= 1.16)
nokogiri (>= 1.3.3) nokogiri (>= 1.3.3)
rack (>= 1.0.0) rack (>= 1.0.0)
rack-test (>= 0.5.4) rack-test (>= 0.5.4)
@@ -102,12 +110,12 @@ GEM
activesupport activesupport
charlock_holmes (0.7.5) charlock_holmes (0.7.5)
chunky_png (1.3.8) chunky_png (1.3.8)
cld3 (3.2.1) cld3 (3.1.3)
ffi (>= 1.1.0, < 1.10.0) ffi (>= 1.1.0, < 1.10.0)
climate_control (0.2.0) climate_control (0.2.0)
cocaine (0.5.8) cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
coderay (1.1.2) coderay (1.1.1)
colorize (0.8.1) colorize (0.8.1)
concurrent-ruby (1.0.5) concurrent-ruby (1.0.5)
connection_pool (2.2.1) connection_pool (2.2.1)
@@ -142,42 +150,20 @@ GEM
thread thread
thread_safe thread_safe
encryptor (3.0.0) encryptor (3.0.0)
erubi (1.7.0) erubi (1.6.1)
et-orbi (1.0.8) erubis (2.7.0)
et-orbi (1.0.5)
tzinfo tzinfo
excon (0.59.0)
execjs (2.7.0) execjs (2.7.0)
fabrication (2.18.0) fabrication (2.16.2)
faker (1.8.4) faker (1.7.3)
i18n (~> 0.5) i18n (~> 0.5)
fast_blank (1.0.0) fast_blank (1.0.0)
ffi (1.9.18) ffi (1.9.18)
fog-aws (1.4.1)
fog-core (~> 1.38)
fog-json (~> 1.0)
fog-xml (~> 0.1)
ipaddress (~> 0.8)
fog-core (1.45.0)
builder
excon (~> 0.58)
formatador (~> 0.2)
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
fog-local (0.4.0)
fog-core (~> 1.27)
fog-openstack (0.1.22)
fog-core (>= 1.40)
fog-json (>= 1.0)
ipaddress (>= 0.8)
fog-xml (0.1.3)
fog-core
nokogiri (>= 1.5.11, < 2.0.0)
formatador (0.2.5)
fuubar (2.2.0) fuubar (2.2.0)
rspec-core (~> 3.0) rspec-core (~> 3.0)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
globalid (0.4.1) globalid (0.4.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
goldfinger (2.0.1) goldfinger (2.0.1)
addressable (~> 2.5) addressable (~> 2.5)
@@ -195,7 +181,7 @@ GEM
railties (>= 4.0.1) railties (>= 4.0.1)
hamster (3.0.0) hamster (3.0.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
hashdiff (0.3.7) hashdiff (0.3.5)
highline (1.7.8) highline (1.7.8)
hiredis (0.6.1) hiredis (0.6.1)
hkdf (0.3.0) hkdf (0.3.0)
@@ -213,43 +199,41 @@ GEM
httplog (0.99.7) httplog (0.99.7)
colorize colorize
rack rack
i18n (0.9.0) i18n (0.8.6)
concurrent-ruby (~> 1.0) i18n-tasks (0.9.16)
i18n-tasks (0.9.18)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)
ast (>= 2.1.0) ast (>= 2.1.0)
easy_translate (>= 0.5.0) easy_translate (>= 0.5.0)
erubi erubis
highline (>= 1.7.3) highline (>= 1.7.3)
i18n i18n
parser (>= 2.2.3.0) parser (>= 2.2.3.0)
rainbow (~> 2.2) rainbow (~> 2.2)
terminal-table (>= 1.5.1) terminal-table (>= 1.5.1)
idn-ruby (0.1.0) idn-ruby (0.1.0)
ipaddress (0.8.3) jmespath (1.3.1)
iso-639 (0.2.8)
json (2.1.0) json (2.1.0)
json-ld (2.1.7) json-ld (2.1.5)
multi_json (~> 1.12) multi_json (~> 1.12)
rdf (~> 2.2, >= 2.2.8) rdf (~> 2.2)
json-ld-preloaded (2.2.2) json-ld-preloaded (2.2.1)
json-ld (~> 2.1, >= 2.1.5) json-ld (~> 2.1, >= 2.1.5)
multi_json (~> 1.11) multi_json (~> 1.11)
rdf (~> 2.2) rdf (~> 2.2)
jsonapi-renderer (0.1.3) jsonapi-renderer (0.1.3)
jwt (1.5.6) jwt (1.5.6)
kaminari (1.1.1) kaminari (1.0.1)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
kaminari-actionview (= 1.1.1) kaminari-actionview (= 1.0.1)
kaminari-activerecord (= 1.1.1) kaminari-activerecord (= 1.0.1)
kaminari-core (= 1.1.1) kaminari-core (= 1.0.1)
kaminari-actionview (1.1.1) kaminari-actionview (1.0.1)
actionview actionview
kaminari-core (= 1.1.1) kaminari-core (= 1.0.1)
kaminari-activerecord (1.1.1) kaminari-activerecord (1.0.1)
activerecord activerecord
kaminari-core (= 1.1.1) kaminari-core (= 1.0.1)
kaminari-core (1.1.1) kaminari-core (1.0.1)
launchy (2.4.3) launchy (2.4.3)
addressable (~> 2.3) addressable (~> 2.3)
letter_opener (1.4.1) letter_opener (1.4.1)
@@ -259,19 +243,17 @@ GEM
letter_opener (~> 1.0) letter_opener (~> 1.0)
railties (>= 3.2) railties (>= 3.2)
link_header (0.0.8) link_header (0.0.8)
lograge (0.7.1) lograge (0.5.1)
actionpack (>= 4, < 5.2) actionpack (>= 4, < 5.2)
activesupport (>= 4, < 5.2) activesupport (>= 4, < 5.2)
railties (>= 4, < 5.2) railties (>= 4, < 5.2)
request_store (~> 1.0) loofah (2.0.3)
loofah (2.1.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.6.6) mail (2.6.6)
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
mario-redis-lock (1.2.0) mario-redis-lock (1.2.0)
redis (~> 3, >= 3.0.5) redis (~> 3, >= 3.0.5)
method_source (0.9.0) method_source (0.8.2)
microformats (4.0.7) microformats (4.0.7)
json json
nokogiri nokogiri
@@ -279,33 +261,27 @@ GEM
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521) mime-types-data (3.2016.0521)
mimemagic (0.3.2) mimemagic (0.3.2)
mini_mime (0.1.4) mini_portile2 (2.2.0)
mini_portile2 (2.3.0)
minitest (5.10.3) minitest (5.10.3)
msgpack (1.1.0) msgpack (1.1.0)
multi_json (1.12.2) multi_json (1.12.1)
net-scp (1.2.1) net-scp (1.2.1)
net-ssh (>= 2.6.5) net-ssh (>= 2.6.5)
net-ssh (4.2.0) net-ssh (4.1.0)
nio4r (2.1.0) nio4r (2.1.0)
nokogiri (1.8.1) nokogiri (1.8.0)
mini_portile2 (~> 2.3.0) mini_portile2 (~> 2.2.0)
nokogumbo (1.4.13) nokogumbo (1.4.13)
nokogiri nokogiri
nsa (0.2.4) oj (3.3.4)
activesupport (>= 4.2, < 6) openssl (2.0.4)
concurrent-ruby (~> 1.0.0)
sidekiq (>= 3.5.0)
statsd-ruby (~> 1.2.0)
oj (3.3.9)
openssl (2.0.6)
orm_adapter (0.5.0) orm_adapter (0.5.0)
ostatus2 (2.0.1) ostatus2 (2.0.1)
addressable (~> 2.4) addressable (~> 2.4)
http (~> 2.0) http (~> 2.0)
nokogiri (~> 1.6) nokogiri (~> 1.6)
openssl (~> 2.0) openssl (~> 2.0)
ox (2.8.1) ox (2.5.0)
paperclip (5.1.0) paperclip (5.1.0)
activemodel (>= 4.2.0) activemodel (>= 4.2.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
@@ -315,23 +291,24 @@ GEM
paperclip-av-transcoder (0.6.4) paperclip-av-transcoder (0.6.4)
av (~> 0.9.0) av (~> 0.9.0)
paperclip (>= 2.5.2) paperclip (>= 2.5.2)
parallel (1.12.0) parallel (1.11.2)
parallel_tests (2.17.0) parallel_tests (2.14.2)
parallel parallel
parser (2.4.0.0) parser (2.4.0.0)
ast (~> 2.2) ast (~> 2.2)
pg (0.21.0) pg (0.21.0)
pghero (1.7.0) pghero (1.7.0)
activerecord activerecord
pkg-config (1.2.8) pkg-config (1.2.4)
powerpack (0.1.1) powerpack (0.1.1)
pry (0.11.2) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.9.0) method_source (~> 0.8.1)
slop (~> 3.4)
pry-rails (0.3.6) pry-rails (0.3.6)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (3.0.0) public_suffix (3.0.0)
puma (3.10.0) puma (3.9.1)
pundit (1.1.0) pundit (1.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
rabl (0.13.1) rabl (0.13.1)
@@ -342,22 +319,20 @@ GEM
rack-cors (0.4.1) rack-cors (0.4.1)
rack-protection (2.0.0) rack-protection (2.0.0)
rack rack
rack-proxy (0.6.2) rack-test (0.6.3)
rack rack (>= 1.0)
rack-test (0.7.0)
rack (>= 1.0, < 3)
rack-timeout (0.4.2) rack-timeout (0.4.2)
rails (5.1.4) rails (5.1.3)
actioncable (= 5.1.4) actioncable (= 5.1.3)
actionmailer (= 5.1.4) actionmailer (= 5.1.3)
actionpack (= 5.1.4) actionpack (= 5.1.3)
actionview (= 5.1.4) actionview (= 5.1.3)
activejob (= 5.1.4) activejob (= 5.1.3)
activemodel (= 5.1.4) activemodel (= 5.1.3)
activerecord (= 5.1.4) activerecord (= 5.1.3)
activesupport (= 5.1.4) activesupport (= 5.1.3)
bundler (>= 1.3.0) bundler (>= 1.3.0)
railties (= 5.1.4) railties (= 5.1.3)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2) rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1) actionpack (~> 5.x, >= 5.0.1)
@@ -373,75 +348,74 @@ GEM
railties (~> 5.0) railties (~> 5.0)
rails-settings-cached (0.6.6) rails-settings-cached (0.6.6)
rails (>= 4.2.0) rails (>= 4.2.0)
railties (5.1.4) railties (5.1.3)
actionpack (= 5.1.4) actionpack (= 5.1.3)
activesupport (= 5.1.4) activesupport (= 5.1.3)
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.2.2) rainbow (2.2.2)
rake rake
rake (12.2.1) rake (12.0.0)
rdf (2.2.11) rdf (2.2.8)
hamster (~> 3.0) hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.2) rdf-normalize (0.3.2)
rdf (~> 2.0) rdf (~> 2.0)
redis (3.3.5) redis (3.3.3)
redis-actionpack (5.0.2) redis-actionpack (5.0.1)
actionpack (>= 4.0, < 6) actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3) redis-rack (>= 1, < 3)
redis-store (>= 1.1.0, < 2) redis-store (>= 1.1.0, < 1.4.0)
redis-activesupport (5.0.4) redis-activesupport (5.0.3)
activesupport (>= 3, < 6) activesupport (>= 3, < 6)
redis-store (>= 1.3, < 2) redis-store (~> 1.3.0)
redis-namespace (1.5.3) redis-namespace (1.5.3)
redis (~> 3.0, >= 3.0.4) redis (~> 3.0, >= 3.0.4)
redis-rack (2.0.3) redis-rack (2.0.2)
rack (>= 1.5, < 3) rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 1.4)
redis-rails (5.0.2) redis-rails (5.0.2)
redis-actionpack (>= 5.0, < 6) redis-actionpack (>= 5.0, < 6)
redis-activesupport (>= 5.0, < 6) redis-activesupport (>= 5.0, < 6)
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 2)
redis-store (1.4.1) redis-store (1.3.0)
redis (>= 2.2, < 5) redis (>= 2.2)
request_store (1.3.2)
responders (2.4.0) responders (2.4.0)
actionpack (>= 4.2.0, < 5.3) actionpack (>= 4.2.0, < 5.3)
railties (>= 4.2.0, < 5.3) railties (>= 4.2.0, < 5.3)
rotp (2.1.2) rotp (2.1.2)
rqrcode (0.10.1) rqrcode (0.10.1)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rspec-core (3.7.0) rspec-core (3.6.0)
rspec-support (~> 3.7.0) rspec-support (~> 3.6.0)
rspec-expectations (3.7.0) rspec-expectations (3.6.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0) rspec-support (~> 3.6.0)
rspec-mocks (3.7.0) rspec-mocks (3.6.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0) rspec-support (~> 3.6.0)
rspec-rails (3.7.1) rspec-rails (3.6.0)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
railties (>= 3.0) railties (>= 3.0)
rspec-core (~> 3.7.0) rspec-core (~> 3.6.0)
rspec-expectations (~> 3.7.0) rspec-expectations (~> 3.6.0)
rspec-mocks (~> 3.7.0) rspec-mocks (~> 3.6.0)
rspec-support (~> 3.7.0) rspec-support (~> 3.6.0)
rspec-sidekiq (3.0.3) rspec-sidekiq (3.0.3)
rspec-core (~> 3.0, >= 3.0.0) rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0) sidekiq (>= 2.4.0)
rspec-support (3.7.0) rspec-support (3.6.0)
rubocop (0.51.0) rubocop (0.49.1)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0) parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1) powerpack (~> 0.1)
rainbow (>= 2.2.2, < 3.0) rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1) unicode-display_width (~> 1.0, >= 1.0.1)
ruby-oembed (0.12.0) ruby-oembed (0.12.0)
ruby-progressbar (1.9.0) ruby-progressbar (1.8.1)
rufus-scheduler (3.4.2) rufus-scheduler (3.4.2)
et-orbi (~> 1.0) et-orbi (~> 1.0)
safe_yaml (1.0.4) safe_yaml (1.0.4)
@@ -449,24 +423,24 @@ GEM
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1) nokogumbo (~> 1.4.1)
sass (3.4.25) sass (3.4.24)
scss_lint (0.55.0) scss_lint (0.54.0)
rake (>= 0.9, < 13) rake (>= 0.9, < 13)
sass (~> 3.4.20) sass (~> 3.4.20)
sidekiq (5.0.5) sidekiq (5.0.4)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0) connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0) rack-protection (>= 1.5.0)
redis (>= 3.3.4, < 5) redis (~> 3.3, >= 3.3.3)
sidekiq-bulk (0.1.1) sidekiq-bulk (0.1.1)
activesupport activesupport
sidekiq sidekiq
sidekiq-scheduler (2.1.10) sidekiq-scheduler (2.1.8)
redis (>= 3, < 5) redis (~> 3)
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 3) sidekiq (>= 3)
tilt (>= 1.4.0) tilt (>= 1.4.0)
sidekiq-unique-jobs (5.0.10) sidekiq-unique-jobs (5.0.9)
sidekiq (>= 4.0, <= 6.0) sidekiq (>= 4.0, <= 6.0)
thor (~> 0) thor (~> 0)
simple-navigation (4.0.5) simple-navigation (4.0.5)
@@ -474,24 +448,23 @@ GEM
simple_form (3.5.0) simple_form (3.5.0)
actionpack (> 4, < 5.2) actionpack (> 4, < 5.2)
activemodel (> 4, < 5.2) activemodel (> 4, < 5.2)
simplecov (0.15.1) simplecov (0.14.1)
docile (~> 1.1.0) docile (~> 1.1.0)
json (>= 1.8, < 3) json (>= 1.8, < 3)
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
simplecov-html (0.10.2) simplecov-html (0.10.1)
slop (3.6.0)
sprockets (3.7.1) sprockets (3.7.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
sprockets-rails (3.2.1) sprockets-rails (3.2.0)
actionpack (>= 4.0) actionpack (>= 4.0)
activesupport (>= 4.0) activesupport (>= 4.0)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sshkit (1.14.0) sshkit (1.13.1)
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
net-ssh (>= 2.8.0) net-ssh (>= 2.8.0)
statsd-ruby (1.2.1) statsd-instrument (2.1.4)
strong_migrations (0.1.9)
activerecord (>= 3.2.0)
temple (0.8.0) temple (0.8.0)
terminal-table (1.8.0) terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (~> 1.1, >= 1.1.1)
@@ -501,9 +474,9 @@ GEM
tilt (2.0.8) tilt (2.0.8)
twitter-text (1.14.7) twitter-text (1.14.7)
unf (~> 0.1.0) unf (~> 0.1.0)
tzinfo (1.2.4) tzinfo (1.2.3)
thread_safe (~> 0.1) thread_safe (~> 0.1)
tzinfo-data (1.2017.3) tzinfo-data (1.2017.2)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
uglifier (3.2.0) uglifier (3.2.0)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
@@ -514,13 +487,13 @@ GEM
uniform_notifier (1.10.0) uniform_notifier (1.10.0)
warden (1.2.7) warden (1.2.7)
rack (>= 1.0) rack (>= 1.0)
webmock (3.1.0) webmock (3.0.1)
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
webpacker (3.0.2) webpacker (2.0)
activesupport (>= 4.2) activesupport (>= 4.2)
rack-proxy (>= 0.6.1) multi_json (~> 1.2)
railties (>= 4.2) railties (>= 4.2)
webpush (0.3.2) webpush (0.3.2)
hkdf (~> 0.2) hkdf (~> 0.2)
@@ -539,32 +512,29 @@ DEPENDENCIES
active_record_query_trace (~> 1.5) active_record_query_trace (~> 1.5)
addressable (~> 2.5) addressable (~> 2.5)
annotate (~> 2.7) annotate (~> 2.7)
better_errors (~> 2.4) aws-sdk (~> 2.9)
better_errors (~> 2.1)
binding_of_caller (~> 0.7) binding_of_caller (~> 0.7)
bootsnap bootsnap
brakeman (~> 4.0) brakeman (~> 3.6)
browser browser
bullet (~> 5.5) bullet (~> 5.5)
bundler-audit (~> 0.6) bundler-audit (~> 0.5)
capistrano (~> 3.10) capistrano (~> 3.8)
capistrano-rails (~> 1.3) capistrano-rails (~> 1.2)
capistrano-rbenv (~> 2.1) capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0) capistrano-yarn (~> 2.0)
capybara (~> 2.15) capybara (~> 2.14)
charlock_holmes (~> 0.7.5) charlock_holmes (~> 0.7.5)
cld3 (~> 3.2.0) cld3 (~> 3.1)
climate_control (~> 0.2) climate_control (~> 0.2)
devise (~> 4.2) devise (~> 4.2)
devise-two-factor (~> 3.0) devise-two-factor (~> 3.0)
doorkeeper (~> 4.2) doorkeeper (~> 4.2)
dotenv-rails (~> 2.2) dotenv-rails (~> 2.2)
fabrication (~> 2.18) fabrication (~> 2.16)
faker (~> 1.7) faker (~> 1.7)
fast_blank (~> 1.0) fast_blank (~> 1.0)
fog-aws (~> 1.4)
fog-core (~> 1.45)
fog-local (~> 0.4)
fog-openstack (~> 0.1)
fuubar (~> 2.2) fuubar (~> 2.2)
goldfinger (~> 2.0) goldfinger (~> 2.0)
hamlit-rails (~> 0.2) hamlit-rails (~> 0.2)
@@ -575,35 +545,33 @@ DEPENDENCIES
httplog (~> 0.99) httplog (~> 0.99)
i18n-tasks (~> 0.9) i18n-tasks (~> 0.9)
idn-ruby idn-ruby
iso-639
json-ld-preloaded (~> 2.2.1) json-ld-preloaded (~> 2.2.1)
kaminari (~> 1.1) kaminari (~> 1.0)
letter_opener (~> 1.4) letter_opener (~> 1.4)
letter_opener_web (~> 1.3) letter_opener_web (~> 1.3)
link_header (~> 0.0) link_header (~> 0.0)
lograge (~> 0.7) lograge (~> 0.5)
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
microformats (~> 4.0) microformats (~> 4.0)
mime-types (~> 3.1) mime-types (~> 3.1)
nokogiri (~> 1.8) nokogiri (~> 1.7)
nsa (~> 0.2) oj (~> 3.0)
oj (~> 3.3)
ostatus2 (~> 2.0) ostatus2 (~> 2.0)
ox (~> 2.8) ox (~> 2.5)
paperclip (~> 5.1) paperclip (~> 5.1)
paperclip-av-transcoder (~> 0.6) paperclip-av-transcoder (~> 0.6)
parallel_tests (~> 2.17) parallel_tests (~> 2.14)
pg (~> 0.20) pg (~> 0.20)
pghero (~> 1.7) pghero (~> 1.7)
pkg-config (~> 1.2) pkg-config (~> 1.2)
pry-rails (~> 0.3) pry-rails (~> 0.3)
puma (~> 3.10) puma (~> 3.8)
pundit (~> 1.1) pundit (~> 1.1)
rabl (~> 0.13) rabl (~> 0.13)
rack-attack (~> 5.0) rack-attack (~> 5.0)
rack-cors (~> 0.4) rack-cors (~> 0.4)
rack-timeout (~> 0.4) rack-timeout (~> 0.4)
rails (~> 5.1.4) rails (~> 5.1.0)
rails-controller-testing (~> 1.0) rails-controller-testing (~> 1.0)
rails-i18n (~> 5.0) rails-i18n (~> 5.0)
rails-settings-cached (~> 0.6) rails-settings-cached (~> 0.6)
@@ -612,12 +580,12 @@ DEPENDENCIES
redis-namespace (~> 1.5) redis-namespace (~> 1.5)
redis-rails (~> 5.0) redis-rails (~> 5.0)
rqrcode (~> 0.10) rqrcode (~> 0.10)
rspec-rails (~> 3.7) rspec-rails (~> 3.6)
rspec-sidekiq (~> 3.0) rspec-sidekiq (~> 3.0)
rubocop rubocop
ruby-oembed (~> 0.12) ruby-oembed (~> 0.12)
sanitize (~> 4.4) sanitize (~> 4.4)
scss_lint (~> 0.55) scss_lint (~> 0.53)
sidekiq (~> 5.0) sidekiq (~> 5.0)
sidekiq-bulk (~> 0.1.1) sidekiq-bulk (~> 0.1.1)
sidekiq-scheduler (~> 2.1) sidekiq-scheduler (~> 2.1)
@@ -626,16 +594,16 @@ DEPENDENCIES
simple_form (~> 3.4) simple_form (~> 3.4)
simplecov (~> 0.14) simplecov (~> 0.14)
sprockets-rails (~> 3.2) sprockets-rails (~> 3.2)
strong_migrations statsd-instrument (~> 2.1)
twitter-text (~> 1.14) twitter-text (~> 1.14)
tzinfo-data (~> 1.2017) tzinfo-data (~> 1.2017)
uglifier (~> 3.2) uglifier (~> 3.2)
webmock (~> 3.0) webmock (~> 3.0)
webpacker (~> 3.0) webpacker (~> 2.0)
webpush webpush
RUBY VERSION RUBY VERSION
ruby 2.4.2p198 ruby 2.4.1p111
BUNDLED WITH BUNDLED WITH
1.15.4 1.15.4

View File

@@ -1,4 +1,4 @@
web: PORT=3000 bundle exec puma -C config/puma.rb web: PORT=3000 bundle exec puma -C config/puma.rb
sidekiq: PORT=3000 bundle exec sidekiq sidekiq: PORT=3000 bundle exec sidekiq
stream: PORT=4000 yarn run start stream: PORT=4000 yarn run start
webpack: ./bin/webpack-dev-server --listen-host 0.0.0.0 webpack: ./bin/webpack-dev-server --host 0.0.0.0

View File

@@ -1,69 +1,53 @@
![Mastodon](https://i.imgur.com/NhZc40l.png) ![Mastodon](https://i.imgur.com/NhZc40l.png)
======== ========
[![Build Status](https://img.shields.io/travis/tootsuite/mastodon.svg)][travis] [![Build Status](http://img.shields.io/travis/tootsuite/mastodon.svg)][travis]
[![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate] [![Code Climate](https://img.shields.io/codeclimate/github/tootsuite/mastodon.svg)][code_climate]
[travis]: https://travis-ci.org/tootsuite/mastodon [travis]: https://travis-ci.org/tootsuite/mastodon
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon [code_climate]: https://codeclimate.com/github/tootsuite/mastodon
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. 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.
Click on the screenshot below to watch a demo of the UI: 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:
[![Screenshot](https://i.imgur.com/pG3Nnz3.jpg)][youtube_demo] [![Screenshot](https://i.imgur.com/pG3Nnz3.jpg)][youtube_demo]
[youtube_demo]: https://www.youtube.com/watch?v=YO1jQ8_rAMU [youtube_demo]: https://www.youtube.com/watch?v=YO1jQ8_rAMU
**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. 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.
If you would like, you can [support the development of this project on Patreon][patreon]. Alternatively, you can donate to this BTC address: `17j2g7vpgHhLuXhN4bueZFCvdxxieyRVWd` 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 [patreon]: https://www.patreon.com/user?u=619786
---
## Resources ## Resources
- [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 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)
- [List of apps](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md) - [List of apps](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md)
- [List of sponsors](https://joinmastodon.org/sponsors)
## Features ## Features
**No vendor lock-in: Fully interoperable with any conforming platform** - **Fully interoperable with GNU social and any OStatus platform**
Whatever implements Atom feeds, ActivityStreams, Salmon, WebSub and Webfinger is part of the network
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
**Real-time timeline updates** - **Federated thread resolving**
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 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**
**Media attachments like images and short videos** Upload and view images and WebM videos attached to the updates
- **OAuth2 and a straightforward REST API**
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! 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**
**OAuth2 and a straightforward REST API** 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**
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 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 ## Development
Please follow the [development guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Development-guide.md) from the documentation repository. Please follow the [development guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Development-guide.md) from the documentation repository.
@@ -78,8 +62,9 @@ You can open issues for bugs you've found or features you think are missing. You
**IRC channel**: #mastodon on irc.freenode.net **IRC channel**: #mastodon on irc.freenode.net
---
## Extra credits ## Extra credits
The elephant friend illustrations are created by [Dopatwo](https://mastodon.social/@dopatwo) - 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)
![Mastodon error image](https://mastodon.social/oops.png)

View File

@@ -14,7 +14,7 @@ class AccountsController < ApplicationController
return return
end end
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses? @pinned_statuses = cache_collection(@account.pinned_statuses, Status) unless media_requested?
@statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id]) @statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
@next_url = next_url unless @statuses.empty? @next_url = next_url unless @statuses.empty?
@@ -22,24 +22,17 @@ class AccountsController < ApplicationController
format.atom do format.atom do
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id]) @entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? })) render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.to_a))
end end
format.json do format.json do
render json: @account, render json: @account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
serializer: ActivityPub::ActorSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
end end
end end
end end
private private
def show_pinned_statuses?
[replies_requested?, media_requested?, params[:max_id].present?, params[:since_id].present?].none?
end
def filtered_statuses def filtered_statuses
default_statuses.tap do |statuses| default_statuses.tap do |statuses|
statuses.merge!(only_media_scope) if media_requested? statuses.merge!(only_media_scope) if media_requested?

View File

@@ -9,9 +9,9 @@ class ActivityPub::InboxesController < Api::BaseController
if signed_request_account if signed_request_account
upgrade_account upgrade_account
process_payload process_payload
head 202 head 201
else else
[signature_verification_failure_reason, 401] head 202
end end
end end
@@ -26,13 +26,8 @@ class ActivityPub::InboxesController < Api::BaseController
end end
def upgrade_account def upgrade_account
if signed_request_account.ostatus? return unless signed_request_account.subscribed?
signed_request_account.update(last_webfingered_at: nil) Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id)
ResolveRemoteAccountWorker.perform_async(signed_request_account.acct)
end
Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed?
DeliveryFailureTracker.track_inverse_success!(signed_request_account)
end end
def process_payload def process_payload

View File

@@ -1,41 +0,0 @@
# frozen_string_literal: true
module Admin
class AccountModerationNotesController < BaseController
before_action :set_account_moderation_note, only: [:destroy]
def create
authorize AccountModerationNote, :create?
@account_moderation_note = current_account.account_moderation_notes.new(resource_params)
if @account_moderation_note.save
redirect_to admin_account_path(@account_moderation_note.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
authorize @account_moderation_note, :destroy?
@account_moderation_note.destroy
redirect_to admin_account_path(@account_moderation_note.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
def set_account_moderation_note
@account_moderation_note = AccountModerationNote.find(params[:id])
end
end
end

View File

@@ -2,54 +2,26 @@
module Admin module Admin
class AccountsController < BaseController class AccountsController < BaseController
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :enable, :disable, :memorialize] before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload]
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload] before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
before_action :require_local_account!, only: [:enable, :disable, :memorialize]
def index def index
authorize :account, :index?
@accounts = filtered_accounts.page(params[:page]) @accounts = filtered_accounts.page(params[:page])
end end
def show def show; end
authorize @account, :show?
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
@moderation_notes = @account.targeted_moderation_notes.latest
end
def subscribe def subscribe
authorize @account, :subscribe?
Pubsubhubbub::SubscribeWorker.perform_async(@account.id) Pubsubhubbub::SubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end
def unsubscribe def unsubscribe
authorize @account, :unsubscribe?
Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id) Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end
def memorialize
authorize @account, :memorialize?
@account.memorialize!
redirect_to admin_account_path(@account.id)
end
def enable
authorize @account.user, :enable?
@account.user.enable!
redirect_to admin_account_path(@account.id)
end
def disable
authorize @account.user, :disable?
@account.user.disable!
redirect_to admin_account_path(@account.id)
end
def redownload def redownload
authorize @account, :redownload?
@account.reset_avatar! @account.reset_avatar!
@account.reset_header! @account.reset_header!
@account.save! @account.save!
@@ -67,10 +39,6 @@ module Admin
redirect_to admin_account_path(@account.id) if @account.local? redirect_to admin_account_path(@account.id) if @account.local?
end end
def require_local_account!
redirect_to admin_account_path(@account.id) unless @account.local? && @account.user.present?
end
def filtered_accounts def filtered_accounts
AccountFilter.new(filter_params).results AccountFilter.new(filter_params).results
end end

View File

@@ -2,9 +2,7 @@
module Admin module Admin
class BaseController < ApplicationController class BaseController < ApplicationController
include Authorization before_action :require_admin!
before_action :require_staff!
layout 'admin' layout 'admin'
end end

View File

@@ -2,18 +2,15 @@
module Admin module Admin
class ConfirmationsController < BaseController class ConfirmationsController < BaseController
before_action :set_user
def create def create
authorize @user, :confirm? account_user.confirm
@user.confirm!
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
private private
def set_user def account_user
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end end
end end
end end

View File

@@ -1,92 +0,0 @@
# frozen_string_literal: true
module Admin
class CustomEmojisController < BaseController
before_action :set_custom_emoji, except: [:index, :new, :create]
def index
authorize :custom_emoji, :index?
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])
end
def new
authorize :custom_emoji, :create?
@custom_emoji = CustomEmoji.new
end
def create
authorize :custom_emoji, :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 update
authorize @custom_emoji, :update?
if @custom_emoji.update(resource_params)
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
else
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg')
end
end
def destroy
authorize @custom_emoji, :destroy?
@custom_emoji.destroy
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
end
def copy
authorize @custom_emoji, :copy?
emoji = CustomEmoji.find_or_create_by(domain: nil, shortcode: @custom_emoji.shortcode)
if emoji.update(image: @custom_emoji.image)
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(page: params[:page])
end
def enable
authorize @custom_emoji, :enable?
@custom_emoji.update!(disabled: false)
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg')
end
def disable
authorize @custom_emoji, :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, :visible_in_picker)
end
def filtered_custom_emojis
CustomEmojiFilter.new(filter_params).results
end
def filter_params
params.permit(
:local,
:remote
)
end
end
end

View File

@@ -5,18 +5,14 @@ module Admin
before_action :set_domain_block, only: [:show, :destroy] before_action :set_domain_block, only: [:show, :destroy]
def index def index
authorize :domain_block, :index?
@domain_blocks = DomainBlock.page(params[:page]) @domain_blocks = DomainBlock.page(params[:page])
end end
def new def new
authorize :domain_block, :create?
@domain_block = DomainBlock.new @domain_block = DomainBlock.new
end end
def create def create
authorize :domain_block, :create?
@domain_block = DomainBlock.new(resource_params) @domain_block = DomainBlock.new(resource_params)
if @domain_block.save if @domain_block.save
@@ -27,12 +23,9 @@ module Admin
end end
end end
def show def show; end
authorize @domain_block, :show?
end
def destroy def destroy
authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block, retroactive_unblock?) UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg') redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
end end

View File

@@ -1,45 +0,0 @@
# frozen_string_literal: true
module Admin
class EmailDomainBlocksController < BaseController
before_action :set_email_domain_block, only: [:show, :destroy]
def index
authorize :email_domain_block, :index?
@email_domain_blocks = EmailDomainBlock.page(params[:page])
end
def new
authorize :email_domain_block, :create?
@email_domain_block = EmailDomainBlock.new
end
def create
authorize :email_domain_block, :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
authorize @email_domain_block, :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

View File

@@ -3,12 +3,10 @@
module Admin module Admin
class InstancesController < BaseController class InstancesController < BaseController
def index def index
authorize :instance, :index?
@instances = ordered_instances @instances = ordered_instances
end end
def resubscribe def resubscribe
authorize :instance, :resubscribe?
params.require(:by_domain) params.require(:by_domain)
Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id)) Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id))
redirect_to admin_instances_path redirect_to admin_instances_path
@@ -16,12 +14,8 @@ module Admin
private private
def filtered_instances
InstanceFilter.new(filter_params).results
end
def paginated_instances def paginated_instances
filtered_instances.page(params[:page]) Account.remote.by_domain_accounts.page(params[:page])
end end
helper_method :paginated_instances helper_method :paginated_instances
@@ -33,11 +27,5 @@ module Admin
def subscribeable_accounts def subscribeable_accounts
Account.with_followers.remote.where(domain: params[:by_domain]) Account.with_followers.remote.where(domain: params[:by_domain])
end end
def filter_params
params.permit(
:domain_name
)
end
end end
end end

View File

@@ -2,20 +2,19 @@
module Admin module Admin
class ReportedStatusesController < BaseController class ReportedStatusesController < BaseController
include Authorization
before_action :set_report before_action :set_report
before_action :set_status, only: [:update, :destroy] before_action :set_status, only: [:update, :destroy]
def create def create
authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params) @form = Form::StatusBatch.new(form_status_batch_params)
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
end end
def update def update
authorize @status, :update?
@status.update(status_params) @status.update(status_params)
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
end end

View File

@@ -5,17 +5,14 @@ module Admin
before_action :set_report, except: [:index] before_action :set_report, except: [:index]
def index def index
authorize :report, :index?
@reports = filtered_reports.page(params[:page]) @reports = filtered_reports.page(params[:page])
end end
def show def show
authorize @report, :show?
@form = Form::StatusBatch.new @form = Form::StatusBatch.new
end end
def update def update
authorize @report, :update?
process_report process_report
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
end end

View File

@@ -2,18 +2,17 @@
module Admin module Admin
class ResetsController < BaseController class ResetsController < BaseController
before_action :set_user before_action :set_account
def create def create
authorize @user, :reset_password? @account.user.send_reset_password_instructions
@user.send_reset_password_instructions
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
private private
def set_user def set_account
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) @account = Account.find(params[:account_id])
end end
end end
end end

View File

@@ -1,25 +0,0 @@
# frozen_string_literal: true
module Admin
class RolesController < BaseController
before_action :set_user
def promote
authorize @user, :promote?
@user.promote!
redirect_to admin_account_path(@user.account_id)
end
def demote
authorize @user, :demote?
@user.demote!
redirect_to admin_account_path(@user.account_id)
end
private
def set_user
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end
end
end

View File

@@ -13,8 +13,6 @@ module Admin
closed_registrations_message closed_registrations_message
open_deletion open_deletion
timeline_preview timeline_preview
bootstrap_timeline_accounts
thumbnail
).freeze ).freeze
BOOLEAN_SETTINGS = %w( BOOLEAN_SETTINGS = %w(
@@ -23,27 +21,15 @@ module Admin
timeline_preview timeline_preview
).freeze ).freeze
UPLOAD_SETTINGS = %w(
thumbnail
).freeze
def edit def edit
authorize :settings, :show?
@admin_settings = Form::AdminSettings.new @admin_settings = Form::AdminSettings.new
end end
def update def update
authorize :settings, :update?
settings_params.each do |key, value| settings_params.each do |key, value|
if UPLOAD_SETTINGS.include?(key)
upload = SiteUpload.where(var: key).first_or_initialize(var: key)
upload.update(file: value)
else
setting = Setting.where(var: key).first_or_initialize(var: key) setting = Setting.where(var: key).first_or_initialize(var: key)
setting.update(value: value_for_update(key, value)) setting.update(value: value_for_update(key, value))
end end
end
flash[:notice] = I18n.t('generic.changes_saved_msg') flash[:notice] = I18n.t('generic.changes_saved_msg')
redirect_to edit_admin_settings_path redirect_to edit_admin_settings_path

View File

@@ -5,13 +5,11 @@ module Admin
before_action :set_account before_action :set_account
def create def create
authorize @account, :silence?
@account.update(silenced: true) @account.update(silenced: true)
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
def destroy def destroy
authorize @account, :unsilence?
@account.update(silenced: false) @account.update(silenced: false)
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end

View File

@@ -2,6 +2,8 @@
module Admin module Admin
class StatusesController < BaseController class StatusesController < BaseController
include Authorization
helper_method :current_params helper_method :current_params
before_action :set_account before_action :set_account
@@ -10,30 +12,24 @@ module Admin
PER_PAGE = 20 PER_PAGE = 20
def index def index
authorize :status, :index?
@statuses = @account.statuses @statuses = @account.statuses
if params[:media] if params[:media]
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
@statuses.merge!(Status.where(id: account_media_status_ids)) @statuses.merge!(Status.where(id: account_media_status_ids))
end end
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE) @statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
@form = Form::StatusBatch.new @form = Form::StatusBatch.new
end end
def create def create
authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params) @form = Form::StatusBatch.new(form_status_batch_params)
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_account_statuses_path(@account.id, current_params) redirect_to admin_account_statuses_path(@account.id, current_params)
end end
def update def update
authorize @status, :update?
@status.update(status_params) @status.update(status_params)
redirect_to admin_account_statuses_path(@account.id, current_params) redirect_to admin_account_statuses_path(@account.id, current_params)
end end
@@ -64,7 +60,6 @@ module Admin
def current_params def current_params
page = (params[:page] || 1).to_i page = (params[:page] || 1).to_i
{ {
media: params[:media], media: params[:media],
page: page > 1 && page, page: page > 1 && page,

View File

@@ -3,7 +3,6 @@
module Admin module Admin
class SubscriptionsController < BaseController class SubscriptionsController < BaseController
def index def index
authorize :subscription, :index?
@subscriptions = ordered_subscriptions.page(requested_page) @subscriptions = ordered_subscriptions.page(requested_page)
end end

View File

@@ -5,14 +5,12 @@ module Admin
before_action :set_account before_action :set_account
def create def create
authorize @account, :suspend?
Admin::SuspensionWorker.perform_async(@account.id) Admin::SuspensionWorker.perform_async(@account.id)
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
def destroy def destroy
authorize @account, :unsuspend? @account.update(suspended: false)
@account.unsuspend!
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end

View File

@@ -5,7 +5,6 @@ module Admin
before_action :set_user before_action :set_user
def destroy def destroy
authorize @user, :disable_2fa?
@user.disable_two_factor! @user.disable_two_factor!
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end

View File

@@ -7,11 +7,9 @@ class Api::SalmonController < Api::BaseController
def update def update
if verify_payload? if verify_payload?
process_salmon process_salmon
head 202 head 201
elsif payload.present?
[signature_verification_failure_reason, 401]
else else
head 400 head 202
end end
end end

View File

@@ -7,10 +7,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
respond_to :json respond_to :json
def index 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 render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
end end

View File

@@ -14,10 +14,7 @@ class Api::V1::AccountsController < Api::BaseController
def follow def follow
FollowService.new.call(current_user.account, @account.acct) FollowService.new.call(current_user.account, @account.acct)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
options = @account.locked? ? {} : { following_map: { @account.id => true }, requested_map: { @account.id => false } }
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
end end
def block def block
@@ -26,7 +23,7 @@ class Api::V1::AccountsController < Api::BaseController
end end
def mute def mute
MuteService.new.call(current_user.account, @account, notifications: params[:notifications]) MuteService.new.call(current_user.account, @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end end
@@ -51,7 +48,7 @@ class Api::V1::AccountsController < Api::BaseController
@account = Account.find(params[:id]) @account = Account.find(params[:id])
end end
def relationships(options = {}) def relationships
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options) AccountRelationshipsPresenter.new([@account.id], current_user.account_id)
end end
end end

View File

@@ -1,11 +0,0 @@
# 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

View File

@@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::AppsController < Api::BaseController class Api::V1::AppsController < Api::BaseController
respond_to :json
def create def create
@app = Doorkeeper::Application.create!(application_options) @app = Doorkeeper::Application.create!(application_options)
render json: @app, serializer: REST::ApplicationSerializer render json: @app, serializer: REST::ApplicationSerializer

View File

@@ -15,13 +15,15 @@ class Api::V1::BlocksController < Api::BaseController
private private
def load_accounts def load_accounts
paginated_blocks.map(&:target_account) default_accounts.merge(paginated_blocks).to_a
end
def default_accounts
Account.includes(:blocked_by).references(:blocked_by)
end end
def paginated_blocks def paginated_blocks
@paginated_blocks ||= Block.eager_load(:target_account) Block.where(account: current_account).paginate_by_max_id(
.where(account: current_account)
.paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT), limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id], params[:max_id],
params[:since_id] params[:since_id]
@@ -39,21 +41,21 @@ class Api::V1::BlocksController < Api::BaseController
end end
def prev_path def prev_path
unless paginated_blocks.empty? unless @accounts.empty?
api_v1_blocks_url pagination_params(since_id: pagination_since_id) api_v1_blocks_url pagination_params(since_id: pagination_since_id)
end end
end end
def pagination_max_id def pagination_max_id
paginated_blocks.last.id @accounts.last.blocked_by_ids.last
end end
def pagination_since_id def pagination_since_id
paginated_blocks.first.id @accounts.first.blocked_by_ids.first
end end
def records_continue? def records_continue?
paginated_blocks.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end end
def pagination_params(core_params) def pagination_params(core_params)

View File

@@ -1,9 +0,0 @@
# 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

View File

@@ -10,12 +10,6 @@ class Api::V1::FollowsController < Api::BaseController
raise ActiveRecord::RecordNotFound if follow_params[:uri].blank? raise ActiveRecord::RecordNotFound if follow_params[:uri].blank?
@account = FollowService.new.call(current_user.account, target_uri).try(:target_account) @account = FollowService.new.call(current_user.account, target_uri).try(:target_account)
if @account.nil?
username, domain = target_uri.split('@')
@account = Account.find_remote!(username, domain)
end
render json: @account, serializer: REST::AccountSerializer render json: @account, serializer: REST::AccountSerializer
end end

View File

@@ -10,7 +10,7 @@ class Api::V1::MediaController < Api::BaseController
respond_to :json respond_to :json
def create def create
@media = current_account.media_attachments.create!(media_params) @media = current_account.media_attachments.create!(file: media_params[:file])
render json: @media, serializer: REST::MediaAttachmentSerializer render json: @media, serializer: REST::MediaAttachmentSerializer
rescue Paperclip::Errors::NotIdentifiedByImageMagickError rescue Paperclip::Errors::NotIdentifiedByImageMagickError
render json: file_type_error, status: 422 render json: file_type_error, status: 422
@@ -18,16 +18,10 @@ class Api::V1::MediaController < Api::BaseController
render json: processing_error, status: 500 render json: processing_error, status: 500
end 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 private
def media_params def media_params
params.permit(:file, :description) params.permit(:file)
end end
def file_type_error def file_type_error

View File

@@ -19,7 +19,7 @@ class Api::V1::ReportsController < Api::BaseController
comment: report_params[:comment] comment: report_params[:comment]
) )
User.staff.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later } User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
render json: @report, serializer: REST::ReportSerializer render json: @report, serializer: REST::ReportSerializer
end end

View File

@@ -1,8 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::SearchController < Api::BaseController class Api::V1::SearchController < Api::BaseController
include Authorization
RESULTS_LIMIT = 5 RESULTS_LIMIT = 5
before_action -> { doorkeeper_authorize! :read } before_action -> { doorkeeper_authorize! :read }
@@ -11,24 +9,12 @@ class Api::V1::SearchController < Api::BaseController
respond_to :json respond_to :json
def index def index
@search = Search.new(search) @search = Search.new(search_results)
render json: @search, serializer: REST::SearchSerializer render json: @search, serializer: REST::SearchSerializer
end end
private private
def search
search_results.tap do |search|
search[:statuses].keep_if do |status|
begin
authorize status, :show?
rescue Mastodon::NotPermittedError
false
end
end
end
end
def search_results def search_results
SearchService.new.call( SearchService.new.call(
params[:q], params[:q],

View File

@@ -12,13 +12,11 @@ class ApplicationController < ActionController::Base
helper_method :current_account helper_method :current_account
helper_method :current_session helper_method :current_session
helper_method :current_theme
helper_method :single_user_mode? helper_method :single_user_mode?
rescue_from ActionController::RoutingError, with: :not_found rescue_from ActionController::RoutingError, with: :not_found
rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
rescue_from Mastodon::NotPermittedError, with: :forbidden
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
before_action :check_suspension, if: :user_signed_in? before_action :check_suspension, if: :user_signed_in?
@@ -41,10 +39,6 @@ class ApplicationController < ActionController::Base
redirect_to root_path unless current_user&.admin? redirect_to root_path unless current_user&.admin?
end end
def require_staff!
redirect_to root_path unless current_user&.staff?
end
def check_suspension def check_suspension
forbidden if current_user.account.suspended? forbidden if current_user.account.suspended?
end end
@@ -83,11 +77,6 @@ class ApplicationController < ActionController::Base
@current_session ||= SessionActivation.find_by(session_id: cookies.signed['_session_id']) @current_session ||= SessionActivation.find_by(session_id: cookies.signed['_session_id'])
end 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) def cache_collection(raw, klass)
return raw unless klass.respond_to?(:with_includes) return raw unless klass.respond_to?(:with_includes)

View File

@@ -2,10 +2,4 @@
class Auth::ConfirmationsController < Devise::ConfirmationsController class Auth::ConfirmationsController < Devise::ConfirmationsController
layout 'auth' layout 'auth'
def show
super do |user|
BootstrapTimelineWorker.perform_async(user.account_id) if user.errors.empty?
end
end
end end

View File

@@ -6,7 +6,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :check_enabled_registrations, only: [:new, :create] before_action :check_enabled_registrations, only: [:new, :create]
before_action :configure_sign_up_params, only: [:create] before_action :configure_sign_up_params, only: [:create]
before_action :set_sessions, only: [:edit, :update] before_action :set_sessions, only: [:edit, :update]
before_action :set_instance_presenter, only: [:new, :create, :update]
def destroy def destroy
not_found not_found
@@ -40,10 +39,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
private private
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def determine_layout def determine_layout
%w(edit update).include?(action_name) ? 'admin' : 'auth' %w(edit update).include?(action_name) ? 'admin' : 'auth'
end end

View File

@@ -8,7 +8,6 @@ class Auth::SessionsController < Devise::SessionsController
skip_before_action :require_no_authentication, only: [:create] skip_before_action :require_no_authentication, only: [:create]
skip_before_action :check_suspension, only: [:destroy] skip_before_action :check_suspension, only: [:destroy]
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create] prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
before_action :set_instance_presenter, only: [:new]
def create def create
super do |resource| super do |resource|
@@ -85,10 +84,6 @@ class Auth::SessionsController < Devise::SessionsController
private private
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def home_paths(resource) def home_paths(resource)
paths = [about_path] paths = [about_path]
if single_user_mode? && resource.is_a?(User) if single_user_mode? && resource.is_a?(User)

View File

@@ -2,7 +2,6 @@
module Authorization module Authorization
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Pundit include Pundit
def pundit_user def pundit_user

View File

@@ -9,15 +9,10 @@ module SignatureVerification
request.headers['Signature'].present? request.headers['Signature'].present?
end end
def signature_verification_failure_reason
return @signature_verification_failure_reason if defined?(@signature_verification_failure_reason)
end
def signed_request_account def signed_request_account
return @signed_request_account if defined?(@signed_request_account) return @signed_request_account if defined?(@signed_request_account)
unless signed_request? unless signed_request?
@signature_verification_failure_reason = 'Request not signed'
@signed_request_account = nil @signed_request_account = nil
return return
end end
@@ -32,7 +27,6 @@ module SignatureVerification
end end
if incompatible_signature?(signature_params) if incompatible_signature?(signature_params)
@signature_verification_failure_reason = 'Incompatible request signature'
@signed_request_account = nil @signed_request_account = nil
return return
end end
@@ -40,7 +34,6 @@ module SignatureVerification
account = account_from_key_id(signature_params['keyId']) account = account_from_key_id(signature_params['keyId'])
if account.nil? if account.nil?
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
@signed_request_account = nil @signed_request_account = nil
return return
end end
@@ -48,21 +41,10 @@ module SignatureVerification
signature = Base64.decode64(signature_params['signature']) signature = Base64.decode64(signature_params['signature'])
compare_signed_string = build_signed_string(signature_params['headers']) compare_signed_string = build_signed_string(signature_params['headers'])
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) if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
@signed_request_account = account @signed_request_account = account
@signed_request_account @signed_request_account
else 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 @signed_request_account = nil
end end
end end
@@ -117,7 +99,7 @@ module SignatureVerification
ResolveRemoteAccountService.new.call(key_id.gsub(/\Aacct:/, '')) ResolveRemoteAccountService.new.call(key_id.gsub(/\Aacct:/, ''))
elsif !ActivityPub::TagManager.instance.local_uri?(key_id) elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account) account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
account ||= ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) account ||= ActivityPub::FetchRemoteKeyService.new.call(key_id)
account account
end end
end end

View File

@@ -7,14 +7,12 @@ module UserTrackingConcern
UPDATE_SIGN_IN_HOURS = 24 UPDATE_SIGN_IN_HOURS = 24
included do included do
before_action :set_user_activity before_action :set_user_activity, if: %i(user_signed_in? user_needs_sign_in_update?)
end end
private private
def set_user_activity def set_user_activity
return unless user_needs_sign_in_update?
# Mark as signed-in today # Mark as signed-in today
current_user.update_tracked_fields!(request) current_user.update_tracked_fields!(request)
@@ -23,7 +21,7 @@ module UserTrackingConcern
end end
def user_needs_sign_in_update? def user_needs_sign_in_update?
user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_HOURS.hours.ago) current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_HOURS.hours.ago
end end
def user_needs_feed_update? def user_needs_feed_update?

View File

@@ -1,22 +0,0 @@
# 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

View File

@@ -10,39 +10,19 @@ class FollowerAccountsController < ApplicationController
format.html format.html
format.json do format.json do
render json: collection_presenter, render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
end end
end end
end end
private private
def page_url(page)
account_followers_url(@account, page: page) unless page.nil?
end
def collection_presenter def collection_presenter
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) },
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( ActivityPub::CollectionPresenter.new(
id: account_followers_url(@account), id: account_followers_url(@account),
type: :ordered, type: :ordered,
size: @account.followers_count, size: @account.followers_count,
first: page items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) }
) )
end end
end end
end

View File

@@ -10,39 +10,19 @@ class FollowingAccountsController < ApplicationController
format.html format.html
format.json do format.json do
render json: collection_presenter, render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
end end
end end
end end
private private
def page_url(page)
account_following_index_url(@account, page: page) unless page.nil?
end
def collection_presenter def collection_presenter
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) },
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( ActivityPub::CollectionPresenter.new(
id: account_following_index_url(@account), id: account_following_index_url(@account),
type: :ordered, type: :ordered,
size: @account.following_count, size: @account.following_count,
first: page items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) }
) )
end end
end end
end

View File

@@ -11,30 +11,7 @@ class HomeController < ApplicationController
private private
def authenticate_user! def authenticate_user!
return if user_signed_in? redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in?
matches = request.path.match(/\A\/web\/(statuses|accounts)\/([\d]+)\z/)
if matches
case matches[1]
when 'statuses'
status = Status.find_by(id: matches[2])
if status && (status.public_visibility? || status.unlisted_visibility?)
redirect_to(ActivityPub::TagManager.instance.url_for(status))
return
end
when 'accounts'
account = Account.find_by(id: matches[2])
if account
redirect_to(ActivityPub::TagManager.instance.url_for(account))
return
end
end
end
redirect_to(default_redirect_path)
end end
def set_initial_state_json def set_initial_state_json
@@ -51,14 +28,4 @@ class HomeController < ApplicationController
admin: Account.find_local(Setting.site_contact_username), admin: Account.find_local(Setting.site_contact_username),
} }
end end
def default_redirect_path
if request.path.start_with?('/web')
new_user_session_path
elsif single_user_mode?
short_account_path(Account.first)
else
about_path
end
end
end end

View File

@@ -1,7 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
class ManifestsController < ApplicationController class ManifestsController < ApplicationController
def show before_action :set_instance_presenter
render json: InstancePresenter.new, serializer: ManifestSerializer
def show; end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end end
end end

View File

@@ -1,40 +0,0 @@
# frozen_string_literal: true
class MediaProxyController < ApplicationController
include RoutingHelper
def show
RedisLock.acquire(lock_options) do |lock|
if lock.acquired?
@media_attachment = MediaAttachment.remote.find(params[:id])
redownload! if @media_attachment.needs_redownload? && !reject_media?
end
end
redirect_to full_asset_url(@media_attachment.file.url(version))
end
private
def redownload!
@media_attachment.file_remote_url = @media_attachment.remote_url
@media_attachment.created_at = Time.now.utc
@media_attachment.save!
end
def version
if request.path.ends_with?('/small')
:small
else
:original
end
end
def lock_options
{ redis: Redis.current, key: "media_download:#{params[:id]}" }
end
def reject_media?
DomainBlock.find_by(domain: @media_attachment.account.domain)&.reject_media?
end
end

View File

@@ -9,7 +9,7 @@ class Settings::FollowerDomainsController < ApplicationController
def show def show
@account = current_account @account = current_account
@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) @domains = current_account.followers.reorder(nil).group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10)
end end
def update def update

View File

@@ -1,32 +0,0 @@
# 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 must_be_following_dm)
)
end
end

View File

@@ -39,10 +39,8 @@ class Settings::PreferencesController < ApplicationController
:setting_boost_modal, :setting_boost_modal,
:setting_delete_modal, :setting_delete_modal,
:setting_auto_play_gif, :setting_auto_play_gif,
:setting_reduce_motion,
:setting_system_font_ui, :setting_system_font_ui,
:setting_noindex, :setting_noindex,
:setting_theme,
notification_emails: %i(follow follow_request reblog favourite mention digest), notification_emails: %i(follow follow_request reblog favourite mention digest),
interactions: %i(must_be_follower must_be_following) interactions: %i(must_be_follower must_be_following)
) )

View File

@@ -21,19 +21,13 @@ class StatusesController < ApplicationController
end end
format.json do format.json do
render json: @status, render json: @status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
serializer: ActivityPub::NoteSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
end end
end end
end end
def activity def activity
render json: @status, render json: @status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
serializer: ActivityPub::ActivitySerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
end end
def embed def embed

View File

@@ -1,40 +1,24 @@
# frozen_string_literal: true # frozen_string_literal: true
class TagsController < ApplicationController class TagsController < ApplicationController
before_action :set_body_classes layout 'public'
before_action :set_instance_presenter
def show def show
@tag = Tag.find_by!(name: params[:id].downcase) @tag = Tag.find_by!(name: params[:id].downcase)
respond_to do |format|
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
@statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id]) @statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id])
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
render json: collection_presenter, respond_to do |format|
serializer: ActivityPub::CollectionSerializer, format.html
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json' format.json do
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end
end end
end end
private private
def set_body_classes
@body_classes = 'tag-body'
end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def collection_presenter def collection_presenter
ActivityPub::CollectionPresenter.new( ActivityPub::CollectionPresenter.new(
id: tag_url(@tag), id: tag_url(@tag),
@@ -43,11 +27,4 @@ class TagsController < ApplicationController
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) } items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
) )
end end
def initial_state_params
{
settings: {},
token: current_session&.token,
}
end
end end

View File

@@ -1,4 +0,0 @@
# frozen_string_literal: true
module Admin::AccountModerationNotesHelper
end

View File

@@ -35,11 +35,6 @@ module ApplicationHelper
Rails.env.production? ? site_title : "#{site_title} (Dev)" Rails.env.production? ? site_title : "#{site_title} (Dev)"
end end
def can?(action, record)
return false if record.nil?
policy(record).public_send("#{action}?")
end
def fa_icon(icon, attributes = {}) def fa_icon(icon, attributes = {})
class_names = attributes[:class]&.split(' ') || [] class_names = attributes[:class]&.split(' ') || []
class_names << 'fa' class_names << 'fa'
@@ -47,12 +42,4 @@ module ApplicationHelper
content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
end end
def custom_emoji_tag(custom_emoji)
image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:")
end
def opengraph(property, content)
tag(:meta, content: content, property: property)
end
end end

View File

@@ -0,0 +1,24 @@
# 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

View File

@@ -9,10 +9,6 @@ module JsonLdHelper
value.is_a?(Array) ? value.first : value value.is_a?(Array) ? value.first : value
end end
def as_array(value)
value.is_a?(Array) ? value : [value]
end
def value_or_id(value) def value_or_id(value)
value.is_a?(String) || value.nil? ? value : value['id'] value.is_a?(String) || value.nil? ? value : value['id']
end end
@@ -26,18 +22,7 @@ module JsonLdHelper
graph.dump(:normalize) graph.dump(:normalize)
end end
def fetch_resource(uri, id) def fetch_resource(uri)
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 response = build_request(uri).perform
return if response.code != 200 return if response.code != 200
body_to_json(response.to_s) body_to_json(response.to_s)

View File

@@ -12,14 +12,8 @@ module RoutingHelper
end end
def full_asset_url(source, options = {}) def full_asset_url(source, options = {})
source = ActionController::Base.helpers.asset_url(source, options) unless use_storage? source = ActionController::Base.helpers.asset_url(source, options) unless Rails.configuration.x.use_s3
URI.join(root_url, source).to_s URI.join(root_url, source).to_s
end end
private
def use_storage?
Rails.configuration.x.use_s3 || Rails.configuration.x.use_swift
end
end end

View File

@@ -27,11 +27,9 @@ module SettingsHelper
pt: 'Português', pt: 'Português',
'pt-BR': 'Português do Brasil', 'pt-BR': 'Português do Brasil',
ru: 'Русский', ru: 'Русский',
sv: 'Svenska',
th: 'ภาษาไทย', th: 'ภาษาไทย',
tr: 'Türkçe', tr: 'Türkçe',
uk: 'Українська', uk: 'Українська',
zh: '中文',
'zh-CN': '简体中文', 'zh-CN': '简体中文',
'zh-HK': '繁體中文(香港)', 'zh-HK': '繁體中文(香港)',
'zh-TW': '繁體中文(臺灣)', 'zh-TW': '繁體中文(臺灣)',
@@ -41,10 +39,6 @@ module SettingsHelper
HUMAN_LOCALES[locale] HUMAN_LOCALES[locale]
end end
def filterable_languages
LanguageDetector.instance.language_names.select(&HUMAN_LOCALES.method(:key?))
end
def hash_to_object(hash) def hash_to_object(hash)
HashObject.new(hash) HashObject.new(hash)
end end

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976"><path d="M211.80734 139.0875c-3.18125 16.36625-28.4925 34.2775-57.5625 37.74875-15.15875 1.80875-30.08375 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.39125 27.9425 21.11625.7225 39.91875-5.20625 39.91875-5.20625l.8675 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23234 213.82 1.40609 165.31125.20859 116.09125c-.365-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67234 3.45375 78.20359.2425 107.86484 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.975 14.7525 32.975 65.0825 0 0 .41375 37.13375-4.59875 62.915" fill="#3088d4"/><path d="M177.50984 80.077v60.94125h-24.14375v-59.15c0-12.46875-5.24625-18.7975-15.74-18.7975-11.6025 0-17.4175 7.5075-17.4175 22.3525v32.37625H96.20734V85.42325c0-14.845-5.81625-22.3525-17.41875-22.3525-10.49375 0-15.74 6.32875-15.74 18.7975v59.15H38.90484V80.077c0-12.455 3.17125-22.3525 9.54125-29.675 6.56875-7.3225 15.17125-11.07625 25.85-11.07625 12.355 0 21.71125 4.74875 27.8975 14.2475l6.01375 10.08125 6.015-10.08125c6.185-9.49875 15.54125-14.2475 27.8975-14.2475 10.6775 0 19.28 3.75375 25.85 11.07625 6.36875 7.3225 9.54 17.22 9.54 29.675" fill="#fff"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="61.076954mm" height="65.47831mm" viewBox="0 0 216.4144 232.00976"><path d="M211.80734 139.0875c-3.18125 16.36625-28.4925 34.2775-57.5625 37.74875-15.15875 1.80875-30.08375 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.39125 27.9425 21.11625.7225 39.91875-5.20625 39.91875-5.20625l.8675 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23234 213.82 1.40609 165.31125.20859 116.09125c-.365-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67234 3.45375 78.20359.2425 107.86484 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.975 14.7525 32.975 65.0825 0 0 .41375 37.13375-4.59875 62.915" fill="#3088d4"/><path d="M177.50984 80.077v60.94125h-24.14375v-59.15c0-12.46875-5.24625-18.7975-15.74-18.7975-11.6025 0-17.4175 7.5075-17.4175 22.3525v32.37625H96.20734V85.42325c0-14.845-5.81625-22.3525-17.41875-22.3525-10.49375 0-15.74 6.32875-15.74 18.7975v59.15H38.90484V80.077c0-12.455 3.17125-22.3525 9.54125-29.675 6.56875-7.3225 15.17125-11.07625 25.85-11.07625 12.355 0 21.71125 4.74875 27.8975 14.2475l6.01375 10.08125 6.015-10.08125c6.185-9.49875 15.54125-14.2475 27.8975-14.2475 10.6775 0 19.28 3.75375 25.85 11.07625 6.36875 7.3225 9.54 17.22 9.54 29.675" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.41507 232.00976"><path d="M211.80683 139.0875c-3.1825 16.36625-28.4925 34.2775-57.5625 37.74875-15.16 1.80875-30.0825 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.3925 27.9425 21.115.7225 39.91625-5.20625 39.91625-5.20625l.86875 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23183 213.82 1.40558 165.31125.20808 116.09125c-.36375-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67058 3.45375 78.20308.2425 107.86433 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.97625 14.7525 32.97625 65.0825 0 0 .4125 37.13375-4.6 62.915" fill="#3088d4"/><path d="M65.68743 96.45938c0 9.01375-7.3075 16.32125-16.3225 16.32125-9.01375 0-16.32-7.3075-16.32-16.32125 0-9.01375 7.30625-16.3225 16.32-16.3225 9.015 0 16.3225 7.30875 16.3225 16.3225M124.52893 96.45938c0 9.01375-7.30875 16.32125-16.3225 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.3225 7.30875 16.3225 16.3225M183.36933 96.45938c0 9.01375-7.3075 16.32125-16.32125 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.32125 7.30875 16.32125 16.3225" fill="#fff"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="61.077141mm" height="65.47831mm" viewBox="0 0 216.41507 232.00976"><path d="M211.80683 139.0875c-3.1825 16.36625-28.4925 34.2775-57.5625 37.74875-15.16 1.80875-30.0825 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.3925 27.9425 21.115.7225 39.91625-5.20625 39.91625-5.20625l.86875 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23183 213.82 1.40558 165.31125.20808 116.09125c-.36375-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67058 3.45375 78.20308.2425 107.86433 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.97625 14.7525 32.97625 65.0825 0 0 .4125 37.13375-4.6 62.915" fill="#3088d4"/><path d="M65.68743 96.45938c0 9.01375-7.3075 16.32125-16.3225 16.32125-9.01375 0-16.32-7.3075-16.32-16.32125 0-9.01375 7.30625-16.3225 16.32-16.3225 9.015 0 16.3225 7.30875 16.3225 16.3225M124.52893 96.45938c0 9.01375-7.30875 16.32125-16.3225 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.3225 7.30875 16.3225 16.3225M183.36933 96.45938c0 9.01375-7.3075 16.32125-16.32125 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.32125 7.30875 16.32125 16.3225" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

View File

@@ -122,7 +122,7 @@ export function unfollowAccount(id) {
dispatch(unfollowAccountRequest(id)); dispatch(unfollowAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => { api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => {
dispatch(unfollowAccountSuccess(response.data, getState().get('statuses'))); dispatch(unfollowAccountSuccess(response.data));
}).catch(error => { }).catch(error => {
dispatch(unfollowAccountFail(error)); dispatch(unfollowAccountFail(error));
}); });
@@ -157,11 +157,10 @@ export function unfollowAccountRequest(id) {
}; };
}; };
export function unfollowAccountSuccess(relationship, statuses) { export function unfollowAccountSuccess(relationship) {
return { return {
type: ACCOUNT_UNFOLLOW_SUCCESS, type: ACCOUNT_UNFOLLOW_SUCCESS,
relationship, relationship,
statuses,
}; };
}; };
@@ -241,11 +240,11 @@ export function unblockAccountFail(error) {
}; };
export function muteAccount(id, notifications) { export function muteAccount(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(muteAccountRequest(id)); dispatch(muteAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications }).then(response => { api(getState).post(`/api/v1/accounts/${id}/mute`).then(response => {
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
dispatch(muteAccountSuccess(response.data, getState().get('statuses'))); dispatch(muteAccountSuccess(response.data, getState().get('statuses')));
}).catch(error => { }).catch(error => {

View File

@@ -1,14 +1,6 @@
import api from '../api'; import api from '../api';
import { throttle } from 'lodash';
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
import { useEmoji } from './emojis';
import { import { updateTimeline } from './timelines';
updateTimeline,
refreshHomeTimeline,
refreshCommunityTimeline,
refreshPublicTimeline,
} from './timelines';
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST'; export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
@@ -17,7 +9,6 @@ export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
export const COMPOSE_REPLY = 'COMPOSE_REPLY'; export const COMPOSE_REPLY = 'COMPOSE_REPLY';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL'; export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_MENTION = 'COMPOSE_MENTION'; export const COMPOSE_MENTION = 'COMPOSE_MENTION';
export const COMPOSE_RESET = 'COMPOSE_RESET';
export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST'; export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS'; export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS';
export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL'; export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
@@ -40,10 +31,6 @@ export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT'; export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
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) { export function changeCompose(text) {
return { return {
type: COMPOSE_CHANGE, type: COMPOSE_CHANGE,
@@ -70,12 +57,6 @@ export function cancelReplyCompose() {
}; };
}; };
export function resetCompose() {
return {
type: COMPOSE_RESET,
};
};
export function mentionCompose(account, router) { export function mentionCompose(account, router) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ dispatch({
@@ -114,20 +95,16 @@ export function submitCompose() {
dispatch(submitComposeSuccess({ ...response.data })); dispatch(submitComposeSuccess({ ...response.data }));
// To make the app more responsive, immediately get the status into the columns // To make the app more responsive, immediately get the status into the columns
dispatch(updateTimeline('home', { ...response.data }));
const insertOrRefresh = (timelineId, refreshAction) => {
if (getState().getIn(['timelines', timelineId, 'online'])) {
dispatch(updateTimeline(timelineId, { ...response.data }));
} else if (getState().getIn(['timelines', timelineId, 'loaded'])) {
dispatch(refreshAction());
}
};
insertOrRefresh('home', refreshHomeTimeline);
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertOrRefresh('community', refreshCommunityTimeline); if (getState().getIn(['timelines', 'community', 'loaded'])) {
insertOrRefresh('public', refreshPublicTimeline); dispatch(updateTimeline('community', { ...response.data }));
}
if (getState().getIn(['timelines', 'public', 'loaded'])) {
dispatch(updateTimeline('public', { ...response.data }));
}
} }
}).catch(function (error) { }).catch(function (error) {
dispatch(submitComposeFail(error)); dispatch(submitComposeFail(error));
@@ -178,40 +155,6 @@ 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() { export function uploadComposeRequest() {
return { return {
type: COMPOSE_UPLOAD_REQUEST, type: COMPOSE_UPLOAD_REQUEST,
@@ -256,42 +199,21 @@ export function clearComposeSuggestions() {
}; };
}; };
const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => { export function fetchComposeSuggestions(token) {
return (dispatch, getState) => {
api(getState).get('/api/v1/accounts/search', { api(getState).get('/api/v1/accounts/search', {
params: { params: {
q: token.slice(1), q: token,
resolve: false, resolve: false,
limit: 4, limit: 4,
}, },
}).then(response => { }).then(response => {
dispatch(readyComposeSuggestionsAccounts(token, response.data)); dispatch(readyComposeSuggestions(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) => {
if (token[0] === ':') {
fetchComposeSuggestionsEmojis(dispatch, getState, token);
} else {
fetchComposeSuggestionsAccounts(dispatch, getState, token);
}
}; };
}; };
export function readyComposeSuggestionsEmojis(token, emojis) { export function readyComposeSuggestions(token, accounts) {
return {
type: COMPOSE_SUGGESTIONS_READY,
token,
emojis,
};
};
export function readyComposeSuggestionsAccounts(token, accounts) {
return { return {
type: COMPOSE_SUGGESTIONS_READY, type: COMPOSE_SUGGESTIONS_READY,
token, token,
@@ -299,23 +221,13 @@ export function readyComposeSuggestionsAccounts(token, accounts) {
}; };
}; };
export function selectComposeSuggestion(position, token, suggestion) { export function selectComposeSuggestion(position, token, accountId) {
return (dispatch, getState) => { return (dispatch, getState) => {
let completion, startPosition; const completion = getState().getIn(['accounts', accountId, 'acct']);
if (typeof suggestion === 'object' && suggestion.id) {
completion = suggestion.native || suggestion.colons;
startPosition = position - 1;
dispatch(useEmoji(suggestion));
} else {
completion = getState().getIn(['accounts', suggestion, 'acct']);
startPosition = position;
}
dispatch({ dispatch({
type: COMPOSE_SUGGESTION_SELECT, type: COMPOSE_SUGGESTION_SELECT,
position: startPosition, position,
token, token,
completion, completion,
}); });

View File

@@ -1,14 +0,0 @@
import { saveSettings } from './settings';
export const EMOJI_USE = 'EMOJI_USE';
export function useEmoji(emoji) {
return dispatch => {
dispatch({
type: EMOJI_USE,
emoji,
});
dispatch(saveSettings());
};
};

View File

@@ -1,17 +0,0 @@
export const HEIGHT_CACHE_SET = 'HEIGHT_CACHE_SET';
export const HEIGHT_CACHE_CLEAR = 'HEIGHT_CACHE_CLEAR';
export function setHeight (key, id, height) {
return {
type: HEIGHT_CACHE_SET,
key,
id,
height,
};
};
export function clearHeight () {
return {
type: HEIGHT_CACHE_CLEAR,
};
};

View File

@@ -1,6 +1,5 @@
import api, { getLinks } from '../api'; import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts'; import { fetchRelationships } from './accounts';
import { openModal } from '../../mastodon/actions/modal';
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST'; export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS'; export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
@@ -10,9 +9,6 @@ export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS'; export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL'; export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
export function fetchMutes() { export function fetchMutes() {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(fetchMutesRequest()); dispatch(fetchMutesRequest());
@@ -84,20 +80,3 @@ export function expandMutesFail(error) {
error, error,
}; };
}; };
export function initMuteModal(account) {
return dispatch => {
dispatch({
type: MUTES_INIT_MODAL,
account,
});
dispatch(openModal('MUTE'));
};
}
export function toggleHideNotifications() {
return dispatch => {
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
};
}

View File

@@ -31,7 +31,6 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
const unescapeHTML = (html) => { const unescapeHTML = (html) => {
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
html = html.replace(/<br \/>|<br>|\n/, ' ');
wrapper.innerHTML = html; wrapper.innerHTML = html;
return wrapper.textContent; return wrapper.textContent;
}; };

View File

@@ -1,40 +0,0 @@
import api from '../api';
export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
import { me } from '../initial_state';
export function fetchPinnedStatuses() {
return (dispatch, getState) => {
dispatch(fetchPinnedStatusesRequest());
api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => {
dispatch(fetchPinnedStatusesSuccess(response.data, null));
}).catch(error => {
dispatch(fetchPinnedStatusesFail(error));
});
};
};
export function fetchPinnedStatusesRequest() {
return {
type: PINNED_STATUSES_FETCH_REQUEST,
};
};
export function fetchPinnedStatusesSuccess(statuses, next) {
return {
type: PINNED_STATUSES_FETCH_SUCCESS,
statuses,
next,
};
};
export function fetchPinnedStatusesFail(error) {
return {
type: PINNED_STATUSES_FETCH_FAIL,
error,
};
};

View File

@@ -1,8 +1,6 @@
import axios from 'axios'; import axios from 'axios';
import { debounce } from 'lodash';
export const SETTING_CHANGE = 'SETTING_CHANGE'; export const SETTING_CHANGE = 'SETTING_CHANGE';
export const SETTING_SAVE = 'SETTING_SAVE';
export function changeSetting(key, value) { export function changeSetting(key, value) {
return dispatch => { return dispatch => {
@@ -16,16 +14,10 @@ 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() { export function saveSettings() {
return (dispatch, getState) => debouncedSave(dispatch, getState); return (_, getState) => {
axios.put('/api/web/settings', {
data: getState().get('settings').toJS(),
});
};
}; };

View File

@@ -23,6 +23,9 @@ export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST';
export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS'; export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL'; export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
export const STATUS_SET_HEIGHT = 'STATUS_SET_HEIGHT';
export const STATUSES_CLEAR_HEIGHT = 'STATUSES_CLEAR_HEIGHT';
export function fetchStatusRequest(id, skipLoading) { export function fetchStatusRequest(id, skipLoading) {
return { return {
type: STATUS_FETCH_REQUEST, type: STATUS_FETCH_REQUEST,
@@ -215,3 +218,17 @@ export function unmuteStatusFail(id, error) {
error, error,
}; };
}; };
export function setStatusHeight (id, height) {
return {
type: STATUS_SET_HEIGHT,
id,
height,
};
};
export function clearStatusesHeight () {
return {
type: STATUSES_CLEAR_HEIGHT,
};
};

View File

@@ -5,7 +5,8 @@ export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
const convertState = rawState => const convertState = rawState =>
fromJS(rawState, (k, v) => fromJS(rawState, (k, v) =>
Iterable.isIndexed(v) ? v.toList() : v.toMap()); Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x =>
Number.isNaN(x * 1) ? x : x * 1));
export function hydrateStore(rawState) { export function hydrateStore(rawState) {
const state = convertState(rawState); const state = convertState(rawState);

View File

@@ -17,8 +17,6 @@ export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE';
export function refreshTimelineSuccess(timeline, statuses, skipLoading, next) { export function refreshTimelineSuccess(timeline, statuses, skipLoading, next) {
return { return {
type: TIMELINE_REFRESH_SUCCESS, type: TIMELINE_REFRESH_SUCCESS,
@@ -32,16 +30,6 @@ export function refreshTimelineSuccess(timeline, statuses, skipLoading, next) {
export function updateTimeline(timeline, status) { export function updateTimeline(timeline, status) {
return (dispatch, getState) => { 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 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({ dispatch({
type: TIMELINE_UPDATE, type: TIMELINE_UPDATE,
@@ -49,14 +37,6 @@ export function updateTimeline(timeline, status) {
status, status,
references, references,
}); });
if (parents.length > 0) {
dispatch({
type: TIMELINE_CONTEXT_UPDATE,
status,
references: parents,
});
}
}; };
}; };

View File

@@ -1,5 +1,5 @@
import 'intl'; import 'intl';
import 'intl/locale-data/jsonp/en'; import 'intl/locale-data/jsonp/en.js';
import 'es6-symbol/implement'; import 'es6-symbol/implement';
import includes from 'array-includes'; import includes from 'array-includes';
import assign from 'object-assign'; import assign from 'object-assign';

View File

@@ -1,33 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
<div
className="account__avatar"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
style={
Object {
"backgroundImage": "url(/animated/alice.gif)",
"backgroundSize": "100px 100px",
"height": "100px",
"width": "100px",
}
}
/>
`;
exports[`<Avatar /> Still renders a still avatar 1`] = `
<div
className="account__avatar"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
style={
Object {
"backgroundImage": "url(/static/alice.jpg)",
"backgroundSize": "100px 100px",
"height": "100px",
"width": "100px",
}
}
/>
`;

View File

@@ -1,24 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AvatarOverlay renders a overlay avatar 1`] = `
<div
className="account__avatar-overlay"
>
<div
className="account__avatar-overlay-base"
style={
Object {
"backgroundImage": "url(/static/alice.jpg)",
}
}
/>
<div
className="account__avatar-overlay-overlay"
style={
Object {
"backgroundImage": "url(/static/eve.jpg)",
}
}
/>
</div>
`;

View File

@@ -1,114 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] = `
<button
className="button button-secondary"
disabled={undefined}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
/>
`;
exports[`<Button /> renders a button element 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
/>
`;
exports[`<Button /> renders a disabled attribute if props.disabled given 1`] = `
<button
className="button"
disabled={true}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
/>
`;
exports[`<Button /> renders class="button--block" if props.block given 1`] = `
<button
className="button button--block"
disabled={undefined}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
/>
`;
exports[`<Button /> renders the children 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
>
<p>
children
</p>
</button>
`;
exports[`<Button /> renders the given text 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
>
foo
</button>
`;
exports[`<Button /> renders the props.text instead of children 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
>
foo
</button>
`;

View File

@@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<DisplayName /> renders display name + account name 1`] = `
<span
className="display-name"
>
<strong
className="display-name__html"
dangerouslySetInnerHTML={
Object {
"__html": "<p>Foo</p>",
}
}
/>
<span
className="display-name__account"
>
@
bar@baz
</span>
</span>
`;

View File

@@ -1,36 +0,0 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { fromJS } from 'immutable';
import Avatar from '../avatar';
describe('<Avatar />', () => {
const account = fromJS({
username: 'alice',
acct: 'alice',
display_name: 'Alice',
avatar: '/animated/alice.gif',
avatar_static: '/static/alice.jpg',
});
const size = 100;
describe('Autoplay', () => {
it('renders a animated avatar', () => {
const component = renderer.create(<Avatar account={account} animate size={size} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
describe('Still', () => {
it('renders a still avatar', () => {
const component = renderer.create(<Avatar account={account} size={size} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
// TODO add autoplay test if possible
});

View File

@@ -1,29 +0,0 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { fromJS } from 'immutable';
import AvatarOverlay from '../avatar_overlay';
describe('<AvatarOverlay', () => {
const account = fromJS({
username: 'alice',
acct: 'alice',
display_name: 'Alice',
avatar: '/animated/alice.gif',
avatar_static: '/static/alice.jpg',
});
const friend = fromJS({
username: 'eve',
acct: 'eve@blackhat.lair',
display_name: 'Evelyn',
avatar: '/animated/eve.gif',
avatar_static: '/static/eve.jpg',
});
it('renders a overlay avatar', () => {
const component = renderer.create(<AvatarOverlay account={account} friend={friend} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -1,75 +0,0 @@
import { shallow } from 'enzyme';
import React from 'react';
import renderer from 'react-test-renderer';
import Button from '../button';
describe('<Button />', () => {
it('renders a button element', () => {
const component = renderer.create(<Button />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders the given text', () => {
const text = 'foo';
const component = renderer.create(<Button text={text} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('handles click events using the given handler', () => {
const handler = jest.fn();
const button = shallow(<Button onClick={handler} />);
button.find('button').simulate('click');
expect(handler.mock.calls.length).toEqual(1);
});
it('does not handle click events if props.disabled given', () => {
const handler = jest.fn();
const button = shallow(<Button onClick={handler} disabled />);
button.find('button').simulate('click');
expect(handler.mock.calls.length).toEqual(0);
});
it('renders a disabled attribute if props.disabled given', () => {
const component = renderer.create(<Button disabled />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders the children', () => {
const children = <p>children</p>;
const component = renderer.create(<Button>{children}</Button>);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders the props.text instead of children', () => {
const text = 'foo';
const children = <p>children</p>;
const component = renderer.create(<Button text={text}>{children}</Button>);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders class="button--block" if props.block given', () => {
const component = renderer.create(<Button block />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('adds class "button-secondary" if props.secondary given', () => {
const component = renderer.create(<Button secondary />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -1,18 +0,0 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { fromJS } from 'immutable';
import DisplayName from '../display_name';
describe('<DisplayName />', () => {
it('renders display name + account name', () => {
const account = fromJS({
username: 'bar',
acct: 'bar@baz',
display_name_html: '<p>Foo</p>',
});
const component = renderer.create(<DisplayName account={account} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -7,7 +7,6 @@ import Permalink from './permalink';
import IconButton from './icon_button'; import IconButton from './icon_button';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from '../initial_state';
const messages = defineMessages({ const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' },
@@ -15,8 +14,6 @@ const messages = defineMessages({
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
}); });
@injectIntl @injectIntl
@@ -24,6 +21,7 @@ export default class Account extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired,
me: PropTypes.number.isRequired,
onFollow: PropTypes.func.isRequired, onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired,
@@ -43,16 +41,8 @@ export default class Account extends ImmutablePureComponent {
this.props.onMute(this.props.account); this.props.onMute(this.props.account);
} }
handleMuteNotifications = () => {
this.props.onMuteNotifications(this.props.account, true);
}
handleUnmuteNotifications = () => {
this.props.onMuteNotifications(this.props.account, false);
}
render () { render () {
const { account, intl, hidden } = this.props; const { account, me, intl, hidden } = this.props;
if (!account) { if (!account) {
return <div />; return <div />;
@@ -80,18 +70,7 @@ export default class Account extends ImmutablePureComponent {
} else if (blocking) { } else if (blocking) {
buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
} else if (muting) { } else if (muting) {
let hidingNotificationsButton; buttons = <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
if (muting.get('notifications')) {
hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />;
} else {
hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username') })} onClick={this.handleMuteNotifications} />;
}
buttons = (
<div>
<IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />
{hidingNotificationsButton}
</div>
);
} else { } else {
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />; buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
} }

View File

@@ -1,42 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light';
const assetHost = process.env.CDN_HOST || '';
export default class AutosuggestEmoji extends React.PureComponent {
static propTypes = {
emoji: PropTypes.object.isRequired,
};
render () {
const { emoji } = this.props;
let url;
if (emoji.custom) {
url = emoji.imageUrl;
} else {
const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
if (!mapping) {
return null;
}
url = `${assetHost}/emoji/${mapping.filename}.svg`;
}
return (
<div className='autosuggest-emoji'>
<img
className='emojione'
src={url}
alt={emoji.native || emoji.colons}
/>
{emoji.colons}
</div>
);
}
}

View File

@@ -1,12 +1,10 @@
import React from 'react'; import React from 'react';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { isRtl } from '../rtl'; import { isRtl } from '../rtl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Textarea from 'react-textarea-autosize'; import Textarea from 'react-textarea-autosize';
import classNames from 'classnames';
const textAtCursorMatchesToken = (str, caretPosition) => { const textAtCursorMatchesToken = (str, caretPosition) => {
let word; let word;
@@ -20,11 +18,11 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
word = str.slice(left, right + caretPosition); word = str.slice(left, right + caretPosition);
} }
if (!word || word.trim().length < 3 || ['@', ':'].indexOf(word[0]) === -1) { if (!word || word.trim().length < 2 || word[0] !== '@') {
return [null, null]; return [null, null];
} }
word = word.trim().toLowerCase(); word = word.trim().toLowerCase().slice(1);
if (word.length > 0) { if (word.length > 0) {
return [left + 1, word]; return [left + 1, word];
@@ -125,22 +123,12 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
this.props.onKeyDown(e); this.props.onKeyDown(e);
} }
onKeyUp = e => {
if (e.key === 'Escape' && this.state.suggestionsHidden) {
document.querySelector('.ui').parentElement.focus();
}
if (this.props.onKeyUp) {
this.props.onKeyUp(e);
}
}
onBlur = () => { onBlur = () => {
this.setState({ suggestionsHidden: true }); this.setState({ suggestionsHidden: true });
} }
onSuggestionClick = (e) => { onSuggestionClick = (e) => {
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index')); const suggestion = Number(e.currentTarget.getAttribute('data-index'));
e.preventDefault(); e.preventDefault();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
this.textarea.focus(); this.textarea.focus();
@@ -163,28 +151,9 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
} }
} }
renderSuggestion = (suggestion, i) => {
const { selectedSuggestion } = this.state;
let inner, key;
if (typeof suggestion === 'object') {
inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
} else {
inner = <AutosuggestAccountContainer id={suggestion} />;
key = suggestion;
}
return (
<div role='button' tabIndex='0' key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
{inner}
</div>
);
}
render () { render () {
const { value, suggestions, disabled, placeholder, autoFocus } = this.props; const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus } = this.props;
const { suggestionsHidden } = this.state; const { suggestionsHidden, selectedSuggestion } = this.state;
const style = { direction: 'ltr' }; const style = { direction: 'ltr' };
if (isRtl(value)) { if (isRtl(value)) {
@@ -195,7 +164,6 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
<div className='autosuggest-textarea'> <div className='autosuggest-textarea'>
<label> <label>
<span style={{ display: 'none' }}>{placeholder}</span> <span style={{ display: 'none' }}>{placeholder}</span>
<Textarea <Textarea
inputRef={this.setTextarea} inputRef={this.setTextarea}
className='autosuggest-textarea__textarea' className='autosuggest-textarea__textarea'
@@ -205,7 +173,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
value={value} value={value}
onChange={this.onChange} onChange={this.onChange}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
onKeyUp={this.onKeyUp} onKeyUp={onKeyUp}
onBlur={this.onBlur} onBlur={this.onBlur}
onPaste={this.onPaste} onPaste={this.onPaste}
style={style} style={style}
@@ -213,7 +181,18 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
</label> </label>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}> <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
{suggestions.map(this.renderSuggestion)} {suggestions.map((suggestion, i) => (
<div
role='button'
tabIndex='0'
key={suggestion}
data-index={suggestion}
className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`}
onMouseDown={this.onSuggestionClick}
>
<AutosuggestAccountContainer id={suggestion} />
</div>
))}
</div> </div>
</div> </div>
); );

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import Motion from '../features/ui/util/optional_motion'; import Motion from 'react-motion/lib/Motion';
import spring from 'react-motion/lib/spring'; import spring from 'react-motion/lib/spring';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';

View File

@@ -135,11 +135,9 @@ export default class ColumnHeader extends React.PureComponent {
return ( return (
<div className={wrapperClassName}> <div className={wrapperClassName}>
<h1 tabIndex={focusable ? 0 : null} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}> <h1 tabIndex={focusable && '0'} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
<i className={`fa fa-fw fa-${icon} column-header__icon`} /> <i className={`fa fa-fw fa-${icon} column-header__icon`} />
<span className='column-header__title'>
{title} {title}
</span>
<div className='column-header__buttons'> <div className='column-header__buttons'>
{backButton} {backButton}
@@ -147,7 +145,7 @@ export default class ColumnHeader extends React.PureComponent {
</div> </div>
</h1> </h1>
<div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}> <div className={collapsibleClassName} tabIndex={collapsed && -1} onTransitionEnd={this.handleTransitionEnd}>
<div className='column-header__collapsible-inner'> <div className='column-header__collapsible-inner'>
{(!collapsed || animating) && collapsedContent} {(!collapsed || animating) && collapsedContent}
</div> </div>

View File

@@ -1,59 +1,53 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import IconButton from './icon_button'; import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
import Overlay from 'react-overlays/lib/Overlay'; import PropTypes from 'prop-types';
import Motion from '../features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import detectPassiveEvents from 'detect-passive-events';
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; export default class DropdownMenu extends React.PureComponent {
class DropdownMenu extends React.PureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object, router: PropTypes.object,
}; };
static propTypes = { static propTypes = {
isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
icon: PropTypes.string.isRequired,
items: PropTypes.array.isRequired, items: PropTypes.array.isRequired,
onClose: PropTypes.func.isRequired, size: PropTypes.number.isRequired,
style: PropTypes.object, direction: PropTypes.string,
placement: PropTypes.string, status: ImmutablePropTypes.map,
arrowOffsetLeft: PropTypes.string, ariaLabel: PropTypes.string,
arrowOffsetTop: PropTypes.string, disabled: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
style: {}, ariaLabel: 'Menu',
placement: 'bottom', isModalOpen: false,
isUserTouching: () => false,
}; };
handleDocumentClick = e => { state = {
if (this.node && !this.node.contains(e.target)) { direction: 'left',
this.props.onClose(); expanded: false,
} };
setRef = (c) => {
this.dropdown = c;
} }
componentDidMount () { handleClick = (e) => {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
componentWillUnmount () {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
setRef = c => {
this.node = c;
}
handleClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index')); const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i]; const { action, to } = this.props.items[i];
this.props.onClose(); if (this.props.isModalOpen) {
this.props.onModalClose();
}
// Don't call e.preventDefault() when the item uses 'href' property.
// ex. "Edit profile" on the account action bar
if (typeof action === 'function') { if (typeof action === 'function') {
e.preventDefault(); e.preventDefault();
@@ -62,18 +56,46 @@ class DropdownMenu extends React.PureComponent {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(to); this.context.router.history.push(to);
} }
this.dropdown.hide();
} }
renderItem (option, i) { handleShow = () => {
if (option === null) { if (this.props.isUserTouching()) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />; this.props.onModalOpen({
status: this.props.status,
actions: this.props.items,
onClick: this.handleClick,
});
} else {
this.setState({ expanded: true });
}
} }
const { text, href = '#' } = option; handleHide = () => this.setState({ expanded: false })
handleToggle = (e) => {
if (e.key === 'Enter') {
if (this.props.isUserTouching()) {
this.handleShow();
} else {
this.setState({ expanded: !this.state.expanded });
}
} else if (e.key === 'Escape') {
this.setState({ expanded: false });
}
}
renderItem = (item, i) => {
if (item === null) {
return <li key={`sep-${i}`} className='dropdown__sep' />;
}
const { text, href = '#' } = item;
return ( return (
<li className='dropdown-menu__item' key={`${text}-${i}`}> <li className='dropdown__content-list-item' key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' autoFocus={i === 0} onClick={this.handleClick} data-index={i}> <a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' autoFocus={i === 0} onClick={this.handleClick} data-index={i} className='dropdown__content-list-link'>
{text} {text}
</a> </a>
</li> </li>
@@ -81,131 +103,44 @@ class DropdownMenu extends React.PureComponent {
} }
render () { render () {
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props; const { icon, items, size, direction, ariaLabel, disabled } = this.props;
return (
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
{({ opacity, scaleX, scaleY }) => (
<div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }} ref={this.setRef}>
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
<ul>
{items.map((option, i) => this.renderItem(option, i))}
</ul>
</div>
)}
</Motion>
);
}
}
export default class Dropdown extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
icon: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
size: PropTypes.number.isRequired,
ariaLabel: PropTypes.string,
disabled: PropTypes.bool,
status: ImmutablePropTypes.map,
isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
};
static defaultProps = {
ariaLabel: 'Menu',
};
state = {
expanded: false,
};
handleClick = () => {
if (!this.state.expanded && this.props.isUserTouching() && this.props.onModalOpen) {
const { status, items } = this.props;
this.props.onModalOpen({
status,
actions: items,
onClick: this.handleItemClick,
});
return;
}
this.setState({ expanded: !this.state.expanded });
}
handleClose = () => {
if (this.props.onModalClose) {
this.props.onModalClose();
}
this.setState({ expanded: false });
}
handleKeyDown = e => {
switch(e.key) {
case 'Enter':
this.handleClick();
break;
case 'Escape':
this.handleClose();
break;
}
}
handleItemClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i];
this.handleClose();
if (typeof action === 'function') {
e.preventDefault();
action();
} else if (to) {
e.preventDefault();
this.context.router.history.push(to);
}
}
setTargetRef = c => {
this.target = c;
}
findTarget = () => {
return this.target;
}
render () {
const { icon, items, size, ariaLabel, disabled } = this.props;
const { expanded } = this.state; const { expanded } = this.state;
const isUserTouching = this.props.isUserTouching();
const directionClass = (direction === 'left') ? 'dropdown__left' : 'dropdown__right';
const iconStyle = { fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` };
const iconClassname = `fa fa-fw fa-${icon} dropdown__icon`;
if (disabled) {
return ( return (
<div onKeyDown={this.handleKeyDown}> <div className='icon-button disabled' style={iconStyle} aria-label={ariaLabel}>
<IconButton <i className={iconClassname} aria-hidden />
icon={icon}
title={ariaLabel}
active={expanded}
disabled={disabled}
size={size}
ref={this.setTargetRef}
onClick={this.handleClick}
/>
<Overlay show={expanded} placement='bottom' target={this.findTarget}>
<DropdownMenu items={items} onClose={this.handleClose} />
</Overlay>
</div> </div>
); );
} }
const dropdownItems = expanded && (
<ul role='group' className='dropdown__content-list' onClick={this.handleHide}>
{items.map(this.renderItem)}
</ul>
);
// No need to render the actual dropdown if we use the modal. If we
// don't render anything <Dropdow /> breaks, so we just put an empty div.
const dropdownContent = !isUserTouching ? (
<DropdownContent className={directionClass} >
{dropdownItems}
</DropdownContent>
) : <div />;
return (
<Dropdown ref={this.setRef} active={isUserTouching ? false : expanded} onShow={this.handleShow} onHide={this.handleHide}>
<DropdownTrigger className='icon-button' style={iconStyle} role='button' aria-expanded={expanded} onKeyDown={this.handleToggle} tabIndex='0' aria-label={ariaLabel}>
<i className={iconClassname} aria-hidden />
</DropdownTrigger>
{dropdownContent}
</Dropdown>
);
}
} }

View File

@@ -5,7 +5,6 @@ export default class ExtendedVideoPlayer extends React.PureComponent {
static propTypes = { static propTypes = {
src: PropTypes.string.isRequired, src: PropTypes.string.isRequired,
alt: PropTypes.string,
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
time: PropTypes.number, time: PropTypes.number,
@@ -32,20 +31,15 @@ export default class ExtendedVideoPlayer extends React.PureComponent {
} }
render () { render () {
const { src, muted, controls, alt } = this.props;
return ( return (
<div className='extended-video-player'> <div className='extended-video-player'>
<video <video
ref={this.setRef} ref={this.setRef}
src={src} src={this.props.src}
autoPlay autoPlay
role='button' muted={this.props.muted}
tabIndex='0' controls={this.props.controls}
aria-label={alt} loop={!this.props.controls}
muted={muted}
controls={controls}
loop={!controls}
/> />
</div> </div>
); );

Some files were not shown because too many files have changed in this diff Show More