diff --git a/.env.production.sample b/.env.production.sample index 3e054db8446..91fcce6ac45 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -1,5 +1,6 @@ # Service dependencies # You may set REDIS_URL instead for more advanced options +# You may also set REDIS_NAMESPACE to share Redis between multiple Mastodon servers REDIS_HOST=redis REDIS_PORT=6379 # You may set DATABASE_URL instead for more advanced options @@ -101,11 +102,19 @@ SMTP_FROM_ADDRESS=notifications@example.com # 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 # S3_CLOUDFRONT_HOST= diff --git a/.gitignore b/.gitignore index 38ebc934f28..2f5f1e71ad8 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ public/system public/assets public/packs public/packs-test +public/500.html .env .env.production node_modules/ diff --git a/.ruby-version b/.ruby-version index 005119baaa0..8e8299dcc06 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.4.1 +2.4.2 diff --git a/.travis.yml b/.travis.yml index d5b51fcb0c0..52ff15c0120 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,18 +26,16 @@ addons: postgresql: 9.4 apt: sources: - - ubuntu-toolchain-r-test - trusty-media packages: - ffmpeg - - g++-6 - libprotobuf-dev - protobuf-compiler - libicu-dev rvm: - 2.3.4 - - 2.4.1 + - 2.4.2 services: - redis-server diff --git a/Aptfile b/Aptfile index 48dff1a770c..5dac836079e 100644 --- a/Aptfile +++ b/Aptfile @@ -1,4 +1,5 @@ ffmpeg +libicu[0-9][0-9] libicu-dev libidn11 libidn11-dev diff --git a/Dockerfile b/Dockerfile index 15138065b68..431ef5bbeb3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.4.1-alpine3.6 +FROM ruby:2.4.2-alpine3.6 LABEL maintainer="https://github.com/tootsuite/mastodon" \ description="A GNU Social-compatible microblogging server" @@ -7,6 +7,8 @@ ENV UID=991 GID=991 \ RAILS_SERVE_STATIC_FILES=true \ RAILS_ENV=production NODE_ENV=production +ARG YARN_VERSION=1.1.0 +ARG YARN_DOWNLOAD_SHA256=171c1f9ee93c488c0d774ac6e9c72649047c3f896277d88d0f805266519430f3 ARG LIBICONV_VERSION=1.15 ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178 @@ -19,6 +21,7 @@ RUN apk -U upgrade \ build-base \ icu-dev \ libidn-dev \ + libressl \ libtool \ postgresql-dev \ protobuf-dev \ @@ -32,16 +35,21 @@ RUN apk -U upgrade \ imagemagick \ libidn \ libpq \ - nodejs-npm \ nodejs \ + nodejs-npm \ protobuf \ su-exec \ tini \ - yarn \ && update-ca-certificates \ + && mkdir -p /tmp/src /opt \ + && wget -O yarn.tar.gz "https://github.com/yarnpkg/yarn/releases/download/v$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz" \ + && echo "$YARN_DOWNLOAD_SHA256 *yarn.tar.gz" | sha256sum -c - \ + && tar -xzf yarn.tar.gz -C /tmp/src \ + && rm yarn.tar.gz \ + && mv /tmp/src/yarn-v$YARN_VERSION /opt/yarn \ + && ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \ && wget -O libiconv.tar.gz "http://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \ && echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \ - && mkdir -p /tmp/src \ && tar -xzf libiconv.tar.gz -C /tmp/src \ && rm libiconv.tar.gz \ && cd /tmp/src/libiconv-$LIBICONV_VERSION \ @@ -56,7 +64,7 @@ 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 \ && bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \ - && yarn --ignore-optional --pure-lockfile + && yarn --pure-lockfile COPY . /mastodon diff --git a/Gemfile b/Gemfile index 637bf53eebe..09b3b8effd7 100644 --- a/Gemfile +++ b/Gemfile @@ -5,8 +5,8 @@ ruby '>= 2.3.0', '< 2.5.0' gem 'pkg-config', '~> 1.2' -gem 'puma', '~> 3.8' -gem 'rails', '~> 5.1.0' +gem 'puma', '~> 3.10' +gem 'rails', '~> 5.1.4' gem 'uglifier', '~> 3.2' gem 'hamlit-rails', '~> 0.2' @@ -25,7 +25,7 @@ gem 'bootsnap' gem 'browser' gem 'charlock_holmes', '~> 0.7.5' gem 'iso-639' -gem 'cld3', '~> 3.1' +gem 'cld3', '~> 3.2.0' gem 'devise', '~> 4.2' gem 'devise-two-factor', '~> 3.0' gem 'doorkeeper', '~> 4.2' @@ -67,7 +67,7 @@ gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'statsd-instrument', '~> 2.1' gem 'twitter-text', '~> 1.14' gem 'tzinfo-data', '~> 1.2017' -gem 'webpacker', '~> 2.0' +gem 'webpacker', '~> 3.0' gem 'webpush' gem 'json-ld-preloaded', '~> 2.2.1' @@ -102,9 +102,10 @@ group :development do gem 'letter_opener', '~> 1.4' gem 'letter_opener_web', '~> 1.3' gem 'rubocop', require: false - gem 'brakeman', '~> 3.6', require: false - gem 'bundler-audit', '~> 0.5', require: false + gem 'brakeman', '~> 4.0', require: false + gem 'bundler-audit', '~> 0.6', require: false gem 'scss_lint', '~> 0.53', require: false + gem 'strong_migrations' gem 'capistrano', '~> 3.8' gem 'capistrano-rails', '~> 1.2' diff --git a/Gemfile.lock b/Gemfile.lock index ddb97dd940a..73419fd28a4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,25 +1,25 @@ GEM remote: https://rubygems.org/ specs: - actioncable (5.1.3) - actionpack (= 5.1.3) + actioncable (5.1.4) + actionpack (= 5.1.4) nio4r (~> 2.0) websocket-driver (~> 0.6.1) - actionmailer (5.1.3) - actionpack (= 5.1.3) - actionview (= 5.1.3) - activejob (= 5.1.3) + actionmailer (5.1.4) + actionpack (= 5.1.4) + actionview (= 5.1.4) + activejob (= 5.1.4) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.1.3) - actionview (= 5.1.3) - activesupport (= 5.1.3) + actionpack (5.1.4) + actionview (= 5.1.4) + activesupport (= 5.1.4) rack (~> 2.0) - rack-test (~> 0.6.3) + rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.1.3) - activesupport (= 5.1.3) + actionview (5.1.4) + activesupport (= 5.1.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -30,16 +30,16 @@ GEM case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.2) active_record_query_trace (1.5.4) - activejob (5.1.3) - activesupport (= 5.1.3) + activejob (5.1.4) + activesupport (= 5.1.4) globalid (>= 0.3.6) - activemodel (5.1.3) - activesupport (= 5.1.3) - activerecord (5.1.3) - activemodel (= 5.1.3) - activesupport (= 5.1.3) + activemodel (5.1.4) + activesupport (= 5.1.4) + activerecord (5.1.4) + activemodel (= 5.1.4) + activesupport (= 5.1.4) arel (~> 8.0) - activesupport (5.1.3) + activesupport (5.1.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (~> 0.7) minitest (~> 5.1) @@ -57,33 +57,33 @@ GEM encryptor (~> 3.0.0) av (0.9.0) cocaine (~> 0.5.3) - aws-sdk (2.10.21) - aws-sdk-resources (= 2.10.21) - aws-sdk-core (2.10.21) + aws-sdk (2.10.46) + aws-sdk-resources (= 2.10.46) + aws-sdk-core (2.10.46) aws-sigv4 (~> 1.0) jmespath (~> 1.0) - aws-sdk-resources (2.10.21) - aws-sdk-core (= 2.10.21) - aws-sigv4 (1.0.1) + aws-sdk-resources (2.10.46) + aws-sdk-core (= 2.10.46) + aws-sigv4 (1.0.2) bcrypt (3.1.11) - better_errors (2.1.1) + better_errors (2.3.0) coderay (>= 1.0.0) - erubis (>= 2.6.6) + erubi (>= 1.0.0) rack (>= 0.9.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - bootsnap (1.1.2) + bootsnap (1.1.3) msgpack (~> 1.0) - brakeman (3.7.2) - browser (2.4.0) + brakeman (4.0.1) + browser (2.5.1) builder (3.2.3) - bullet (5.5.1) + bullet (5.6.1) activesupport (>= 3.0.0) uniform_notifier (~> 1.10.0) bundler-audit (0.6.0) bundler (~> 1.2) thor (~> 0.18) - capistrano (3.8.2) + capistrano (3.9.1) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) @@ -99,9 +99,9 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (2.14.4) + capybara (2.15.1) addressable - mime-types (>= 1.16) + mini_mime (>= 0.1.3) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) @@ -110,12 +110,12 @@ GEM activesupport charlock_holmes (0.7.5) chunky_png (1.3.8) - cld3 (3.1.3) + cld3 (3.2.0) ffi (>= 1.1.0, < 1.10.0) climate_control (0.2.0) cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) - coderay (1.1.1) + coderay (1.1.2) colorize (0.8.1) concurrent-ruby (1.0.5) connection_pool (2.2.1) @@ -151,13 +151,12 @@ GEM thread_safe encryptor (3.0.0) erubi (1.6.1) - erubis (2.7.0) et-orbi (1.0.5) tzinfo - excon (0.58.0) + excon (0.59.0) execjs (2.7.0) - fabrication (2.16.2) - faker (1.7.3) + fabrication (2.16.3) + faker (1.8.4) i18n (~> 0.5) fast_blank (1.0.0) ffi (1.9.18) @@ -194,7 +193,7 @@ GEM railties (>= 4.0.1) hamster (3.0.0) concurrent-ruby (~> 1.0) - hashdiff (0.3.5) + hashdiff (0.3.6) highline (1.7.8) hiredis (0.6.1) hkdf (0.3.0) @@ -213,11 +212,11 @@ GEM colorize rack i18n (0.8.6) - i18n-tasks (0.9.16) + i18n-tasks (0.9.18) activesupport (>= 4.0.2) ast (>= 2.1.0) easy_translate (>= 0.5.0) - erubis + erubi highline (>= 1.7.3) i18n parser (>= 2.2.3.0) @@ -231,7 +230,7 @@ GEM json-ld (2.1.5) multi_json (~> 1.12) rdf (~> 2.2) - json-ld-preloaded (2.2.1) + json-ld-preloaded (2.2.2) json-ld (~> 2.1, >= 2.1.5) multi_json (~> 1.11) rdf (~> 2.2) @@ -258,10 +257,11 @@ GEM letter_opener (~> 1.0) railties (>= 3.2) link_header (0.0.8) - lograge (0.5.1) + lograge (0.6.0) actionpack (>= 4, < 5.2) activesupport (>= 4, < 5.2) railties (>= 4, < 5.2) + request_store (~> 1.0) loofah (2.0.3) nokogiri (>= 1.5.9) mail (2.6.6) @@ -276,27 +276,28 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) mimemagic (0.3.2) + mini_mime (0.1.4) mini_portile2 (2.2.0) minitest (5.10.3) msgpack (1.1.0) - multi_json (1.12.1) + multi_json (1.12.2) net-scp (1.2.1) net-ssh (>= 2.6.5) - net-ssh (4.1.0) + net-ssh (4.2.0) nio4r (2.1.0) nokogiri (1.8.0) mini_portile2 (~> 2.2.0) nokogumbo (1.4.13) nokogiri - oj (3.3.4) - openssl (2.0.4) + oj (3.3.5) + openssl (2.0.5) orm_adapter (0.5.0) ostatus2 (2.0.1) addressable (~> 2.4) http (~> 2.0) nokogiri (~> 1.6) openssl (~> 2.0) - ox (2.5.0) + ox (2.6.0) paperclip (5.1.0) activemodel (>= 4.2.0) activesupport (>= 4.2.0) @@ -306,15 +307,15 @@ GEM paperclip-av-transcoder (0.6.4) av (~> 0.9.0) paperclip (>= 2.5.2) - parallel (1.11.2) - parallel_tests (2.14.2) + parallel (1.12.0) + parallel_tests (2.15.0) parallel parser (2.4.0.0) ast (~> 2.2) pg (0.21.0) pghero (1.7.0) activerecord - pkg-config (1.2.4) + pkg-config (1.2.7) powerpack (0.1.1) pry (0.10.4) coderay (~> 1.1.0) @@ -323,7 +324,7 @@ GEM pry-rails (0.3.6) pry (>= 0.10.4) public_suffix (3.0.0) - puma (3.9.1) + puma (3.10.0) pundit (1.1.0) activesupport (>= 3.0.0) rabl (0.13.1) @@ -334,20 +335,22 @@ GEM rack-cors (0.4.1) rack-protection (2.0.0) rack - rack-test (0.6.3) - rack (>= 1.0) + rack-proxy (0.6.2) + rack + rack-test (0.7.0) + rack (>= 1.0, < 3) rack-timeout (0.4.2) - rails (5.1.3) - actioncable (= 5.1.3) - actionmailer (= 5.1.3) - actionpack (= 5.1.3) - actionview (= 5.1.3) - activejob (= 5.1.3) - activemodel (= 5.1.3) - activerecord (= 5.1.3) - activesupport (= 5.1.3) + rails (5.1.4) + actioncable (= 5.1.4) + actionmailer (= 5.1.4) + actionpack (= 5.1.4) + actionview (= 5.1.4) + activejob (= 5.1.4) + activemodel (= 5.1.4) + activerecord (= 5.1.4) + activesupport (= 5.1.4) bundler (>= 1.3.0) - railties (= 5.1.3) + railties (= 5.1.4) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.2) actionpack (~> 5.x, >= 5.0.1) @@ -363,16 +366,16 @@ GEM railties (~> 5.0) rails-settings-cached (0.6.6) rails (>= 4.2.0) - railties (5.1.3) - actionpack (= 5.1.3) - activesupport (= 5.1.3) + railties (5.1.4) + actionpack (= 5.1.4) + activesupport (= 5.1.4) method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.2.2) rake - rake (12.0.0) - rdf (2.2.8) + rake (12.1.0) + rdf (2.2.9) hamster (~> 3.0) link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.3.2) @@ -396,6 +399,7 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.3.0) redis (>= 2.2) + request_store (1.3.2) responders (2.4.0) actionpack (>= 4.2.0, < 5.3) railties (>= 4.2.0, < 5.3) @@ -410,7 +414,7 @@ GEM rspec-mocks (3.6.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.6.0) - rspec-rails (3.6.0) + rspec-rails (3.6.1) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) @@ -422,15 +426,15 @@ GEM rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) rspec-support (3.6.0) - rubocop (0.49.1) + rubocop (0.50.0) parallel (~> 1.10) parser (>= 2.3.3.1, < 3.0) powerpack (~> 0.1) - rainbow (>= 1.99.1, < 3.0) + rainbow (>= 2.2.2, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) ruby-oembed (0.12.0) - ruby-progressbar (1.8.1) + ruby-progressbar (1.8.3) rufus-scheduler (3.4.2) et-orbi (~> 1.0) safe_yaml (1.0.4) @@ -438,7 +442,7 @@ GEM crass (~> 1.0.2) nokogiri (>= 1.4.4) nokogumbo (~> 1.4.1) - sass (3.4.24) + sass (3.4.25) scss_lint (0.54.0) rake (>= 0.9, < 13) sass (~> 3.4.20) @@ -450,12 +454,12 @@ GEM sidekiq-bulk (0.1.1) activesupport sidekiq - sidekiq-scheduler (2.1.8) + sidekiq-scheduler (2.1.9) redis (~> 3) rufus-scheduler (~> 3.2) sidekiq (>= 3) tilt (>= 1.4.0) - sidekiq-unique-jobs (5.0.9) + sidekiq-unique-jobs (5.0.10) sidekiq (>= 4.0, <= 6.0) thor (~> 0) simple-navigation (4.0.5) @@ -463,23 +467,25 @@ GEM simple_form (3.5.0) actionpack (> 4, < 5.2) activemodel (> 4, < 5.2) - simplecov (0.14.1) + simplecov (0.15.1) docile (~> 1.1.0) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) - simplecov-html (0.10.1) + simplecov-html (0.10.2) slop (3.6.0) sprockets (3.7.1) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.2.0) + sprockets-rails (3.2.1) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sshkit (1.13.1) + sshkit (1.14.0) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) statsd-instrument (2.1.4) + strong_migrations (0.1.9) + activerecord (>= 3.2.0) temple (0.8.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) @@ -506,9 +512,9 @@ GEM addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff - webpacker (2.0) + webpacker (3.0.1) activesupport (>= 4.2) - multi_json (~> 1.2) + rack-proxy (>= 0.6.1) railties (>= 4.2) webpush (0.3.2) hkdf (~> 0.2) @@ -531,17 +537,17 @@ DEPENDENCIES better_errors (~> 2.1) binding_of_caller (~> 0.7) bootsnap - brakeman (~> 3.6) + brakeman (~> 4.0) browser bullet (~> 5.5) - bundler-audit (~> 0.5) + bundler-audit (~> 0.6) capistrano (~> 3.8) capistrano-rails (~> 1.2) capistrano-rbenv (~> 2.1) capistrano-yarn (~> 2.0) capybara (~> 2.14) charlock_holmes (~> 0.7.5) - cld3 (~> 3.1) + cld3 (~> 3.2.0) climate_control (~> 0.2) devise (~> 4.2) devise-two-factor (~> 3.0) @@ -582,13 +588,13 @@ DEPENDENCIES pghero (~> 1.7) pkg-config (~> 1.2) pry-rails (~> 0.3) - puma (~> 3.8) + puma (~> 3.10) pundit (~> 1.1) rabl (~> 0.13) rack-attack (~> 5.0) rack-cors (~> 0.4) rack-timeout (~> 0.4) - rails (~> 5.1.0) + rails (~> 5.1.4) rails-controller-testing (~> 1.0) rails-i18n (~> 5.0) rails-settings-cached (~> 0.6) @@ -612,15 +618,16 @@ DEPENDENCIES simplecov (~> 0.14) sprockets-rails (~> 3.2) statsd-instrument (~> 2.1) + strong_migrations twitter-text (~> 1.14) tzinfo-data (~> 1.2017) uglifier (~> 3.2) webmock (~> 3.0) - webpacker (~> 2.0) + webpacker (~> 3.0) webpush RUBY VERSION - ruby 2.4.1p111 + ruby 2.4.2p198 BUNDLED WITH 1.15.4 diff --git a/Procfile.dev b/Procfile.dev index 9084b4263ce..e75a491c7b4 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,4 +1,4 @@ web: PORT=3000 bundle exec puma -C config/puma.rb sidekiq: PORT=3000 bundle exec sidekiq stream: PORT=4000 yarn run start -webpack: ./bin/webpack-dev-server --host 0.0.0.0 +webpack: ./bin/webpack-dev-server --listen-host 0.0.0.0 diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb new file mode 100644 index 00000000000..d70514d9a97 --- /dev/null +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Admin + class CustomEmojisController < BaseController + def index + @custom_emojis = CustomEmoji.local + end + + def new + @custom_emoji = CustomEmoji.new + end + + def create + @custom_emoji = CustomEmoji.new(resource_params) + + if @custom_emoji.save + redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg') + else + render :new + end + end + + def destroy + CustomEmoji.find(params[:id]).destroy + redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg') + end + + private + + def resource_params + params.require(:custom_emoji).permit(:shortcode, :image) + end + end +end diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index 3296e08db89..22f02e5d089 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -14,8 +14,12 @@ module Admin private + def filtered_instances + InstanceFilter.new(filter_params).results + end + def paginated_instances - Account.remote.by_domain_accounts.page(params[:page]) + filtered_instances.page(params[:page]) end helper_method :paginated_instances @@ -27,5 +31,11 @@ module Admin def subscribeable_accounts Account.with_followers.remote.where(domain: params[:by_domain]) end + + def filter_params + params.permit( + :domain_name + ) + end end end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index c5e6fe4e50e..a2f86b8a9ee 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -14,6 +14,7 @@ module Admin open_deletion timeline_preview bootstrap_timeline_accounts + thumbnail ).freeze BOOLEAN_SETTINGS = %w( @@ -22,14 +23,23 @@ module Admin timeline_preview ).freeze + UPLOAD_SETTINGS = %w( + thumbnail + ).freeze + def edit @admin_settings = Form::AdminSettings.new end def update settings_params.each do |key, value| - setting = Setting.where(var: key).first_or_initialize(var: key) - setting.update(value: value_for_update(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.update(value: value_for_update(key, value)) + end end flash[:notice] = I18n.t('generic.changes_saved_msg') diff --git a/app/controllers/api/v1/custom_emojis_controller.rb b/app/controllers/api/v1/custom_emojis_controller.rb new file mode 100644 index 00000000000..4dd77fb5503 --- /dev/null +++ b/app/controllers/api/v1/custom_emojis_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Api::V1::CustomEmojisController < Api::BaseController + respond_to :json + + def index + render json: CustomEmoji.local, each_serializer: REST::CustomEmojiSerializer + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0b40fb05b73..d5eca6ffb60 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base helper_method :current_account helper_method :current_session + helper_method :current_theme helper_method :single_user_mode? rescue_from ActionController::RoutingError, with: :not_found @@ -77,6 +78,11 @@ class ApplicationController < ActionController::Base @current_session ||= SessionActivation.find_by(session_id: cookies.signed['_session_id']) end + def current_theme + return Setting.default_settings['theme'] unless Themes.instance.names.include? current_user&.setting_theme + current_user.setting_theme + end + def cache_collection(raw, klass) return raw unless klass.respond_to?(:with_includes) diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 0e194989790..8eb4d28225e 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -17,12 +17,29 @@ class FollowerAccountsController < ApplicationController private + def page_url(page) + account_followers_url(@account, page: page) unless page.nil? + end + def collection_presenter - ActivityPub::CollectionPresenter.new( - id: account_followers_url(@account), + page = ActivityPub::CollectionPresenter.new( + id: account_followers_url(@account, page: params.fetch(:page, 1)), type: :ordered, size: @account.followers_count, - items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) } + items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) }, + part_of: account_followers_url(@account), + next: page_url(@follows.next_page), + prev: page_url(@follows.prev_page) ) + if params[:page].present? + page + else + ActivityPub::CollectionPresenter.new( + id: account_followers_url(@account), + type: :ordered, + size: @account.followers_count, + first: page + ) + end end end diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index d4593093ff6..1ca6f0fe7d5 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -17,12 +17,29 @@ class FollowingAccountsController < ApplicationController private + def page_url(page) + account_following_index_url(@account, page: page) unless page.nil? + end + def collection_presenter - ActivityPub::CollectionPresenter.new( - id: account_following_index_url(@account), + page = ActivityPub::CollectionPresenter.new( + id: account_following_index_url(@account, page: params.fetch(:page, 1)), type: :ordered, size: @account.following_count, - items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) } + items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) }, + part_of: account_following_index_url(@account), + next: page_url(@follows.next_page), + prev: page_url(@follows.prev_page) ) + if params[:page].present? + page + else + ActivityPub::CollectionPresenter.new( + id: account_following_index_url(@account), + type: :ordered, + size: @account.following_count, + first: page + ) + end end end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index fbfb5473ee8..ad7f09f3486 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -12,7 +12,30 @@ class HomeController < ApplicationController private def authenticate_user! - redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in? + return if 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 def set_initial_state_json @@ -29,4 +52,14 @@ class HomeController < ApplicationController admin: Account.find_local(Setting.site_contact_username), } 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 diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb new file mode 100644 index 00000000000..155670837eb --- /dev/null +++ b/app/controllers/media_proxy_controller.rb @@ -0,0 +1,40 @@ +# 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 diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index f107f2b165f..207c7b3240b 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -41,6 +41,7 @@ class Settings::PreferencesController < ApplicationController :setting_auto_play_gif, :setting_system_font_ui, :setting_noindex, + :setting_theme, notification_emails: %i(follow follow_request reblog favourite mention digest), interactions: %i(must_be_follower must_be_following) ) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 61d4442c12b..6d625e7db9c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -42,4 +42,8 @@ module ApplicationHelper content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) end + + def opengraph(property, content) + tag(:meta, content: content, property: property) + end end diff --git a/app/helpers/emoji_helper.rb b/app/helpers/emoji_helper.rb deleted file mode 100644 index 848c03fce72..00000000000 --- a/app/helpers/emoji_helper.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module EmojiHelper - def emojify(text) - return text if text.blank? - - text.gsub(emoji_pattern) do |match| - emoji = Emoji.instance.unicode($1) # rubocop:disable Style/PerlBackrefs - - if emoji - emoji - else - match - end - end - end - - def emoji_pattern - @emoji_pattern ||= - /(?<=[^[:alnum:]:]|\n|^) - (#{Emoji.instance.names.map { |name| Regexp.escape(name) }.join('|')}) - (?=[^[:alnum:]:]|$)/x - end -end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 369a4568099..14776b35400 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -41,7 +41,7 @@ module SettingsHelper end def filterable_languages - I18n.available_locales.map { |locale| locale.to_s.split('-').first.to_sym }.uniq + LanguageDetector.instance.language_names.select(&HUMAN_LOCALES.method(:key?)) end def hash_to_object(hash) diff --git a/app/javascript/glitch/components/account/header.js b/app/javascript/glitch/components/account/header.js index a1197c4be84..bc2ce30f623 100644 --- a/app/javascript/glitch/components/account/header.js +++ b/app/javascript/glitch/components/account/header.js @@ -44,7 +44,6 @@ Imports: import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import escapeTextContentForBrowser from 'escape-html'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -89,7 +88,7 @@ export default class AccountHeader extends ImmutablePureComponent { static propTypes = { account : ImmutablePropTypes.map, - me : PropTypes.number.isRequired, + me : PropTypes.string.isRequired, onFollow : PropTypes.func.isRequired, intl : PropTypes.object.isRequired, }; @@ -117,7 +116,7 @@ then we set the `displayName` to just be the `username` of the account. return null; } - let displayName = account.get('display_name'); + let displayName = account.get('display_name_html'); let info = ''; let actionBtn = ''; let following = false; @@ -167,16 +166,11 @@ appropriate icon. } /* - -`displayNameHTML` processes the `displayName` and prepares it for -insertion into the document. Meanwhile, we extract the `text` and + we extract the `text` and `metadata` from our account's `note` using `processBio()`. */ - const displayNameHTML = { - __html : emojify(escapeTextContentForBrowser(displayName)), - }; const { text, metadata } = processBio(account.get('note')); /* @@ -198,7 +192,7 @@ Here, we render our component using all the things we've defined above. diff --git a/app/javascript/glitch/components/local_settings/navigation/item/style.scss b/app/javascript/glitch/components/local_settings/navigation/item/style.scss index 505c869126a..33d7d374415 100644 --- a/app/javascript/glitch/components/local_settings/navigation/item/style.scss +++ b/app/javascript/glitch/components/local_settings/navigation/item/style.scss @@ -1,4 +1,4 @@ -@import 'variables'; +@import 'styles/variables'; .glitch.local-settings__navigation__item { display: block; diff --git a/app/javascript/glitch/components/local_settings/navigation/style.scss b/app/javascript/glitch/components/local_settings/navigation/style.scss index 1cc39e3e95e..a610a12121f 100644 --- a/app/javascript/glitch/components/local_settings/navigation/style.scss +++ b/app/javascript/glitch/components/local_settings/navigation/style.scss @@ -1,4 +1,4 @@ -@import 'variables'; +@import 'styles/variables'; .glitch.local-settings__navigation { background: $primary-text-color; diff --git a/app/javascript/glitch/components/local_settings/page/item/style.scss b/app/javascript/glitch/components/local_settings/page/item/style.scss index e614030c02d..da1941b998c 100644 --- a/app/javascript/glitch/components/local_settings/page/item/style.scss +++ b/app/javascript/glitch/components/local_settings/page/item/style.scss @@ -1,4 +1,4 @@ -@import 'variables'; +@import 'styles/variables'; .glitch.local-settings__page__item { select { diff --git a/app/javascript/glitch/components/local_settings/page/style.scss b/app/javascript/glitch/components/local_settings/page/style.scss index 7269056c3d6..53c95ea4026 100644 --- a/app/javascript/glitch/components/local_settings/page/style.scss +++ b/app/javascript/glitch/components/local_settings/page/style.scss @@ -1,4 +1,4 @@ -@import 'variables'; +@import 'styles/variables'; .glitch.local-settings__page { display: block; diff --git a/app/javascript/glitch/components/local_settings/style.scss b/app/javascript/glitch/components/local_settings/style.scss index 6f7fcbaa4ea..54fec47bd91 100644 --- a/app/javascript/glitch/components/local_settings/style.scss +++ b/app/javascript/glitch/components/local_settings/style.scss @@ -1,4 +1,4 @@ -@import 'variables'; +@import 'styles/variables'; .glitch.local-settings { position: relative; diff --git a/app/javascript/glitch/components/notification/follow.js b/app/javascript/glitch/components/notification/follow.js index f471307b932..e2c21bf3505 100644 --- a/app/javascript/glitch/components/notification/follow.js +++ b/app/javascript/glitch/components/notification/follow.js @@ -11,11 +11,9 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; -import escapeTextContentForBrowser from 'escape-html'; import ImmutablePureComponent from 'react-immutable-pure-component'; // Mastodon imports. -import emojify from '../../../mastodon/emoji'; import Permalink from '../../../mastodon/components/permalink'; import AccountContainer from '../../../mastodon/containers/account_container'; @@ -30,7 +28,7 @@ import NotificationOverlayContainer from '../notification/overlay/container'; export default class NotificationFollow extends ImmutablePureComponent { static propTypes = { - id : PropTypes.number.isRequired, + id : PropTypes.string.isRequired, account : ImmutablePropTypes.map.isRequired, notification : ImmutablePropTypes.map.isRequired, }; @@ -39,15 +37,14 @@ export default class NotificationFollow extends ImmutablePureComponent { const { account, notification } = this.props; // Links to the display name. - const displayName = account.get('display_name') || account.get('username'); - const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; + const displayName = account.get('display_name_html') || account.get('username'); const link = ( ); diff --git a/app/javascript/glitch/components/status/action_bar.js b/app/javascript/glitch/components/status/action_bar.js index d4d26c62c8f..f4450d31bfb 100644 --- a/app/javascript/glitch/components/status/action_bar.js +++ b/app/javascript/glitch/components/status/action_bar.js @@ -50,7 +50,7 @@ export default class StatusActionBar extends ImmutablePureComponent { onEmbed: PropTypes.func, onMuteConversation: PropTypes.func, onPin: PropTypes.func, - me: PropTypes.number, + me: PropTypes.string, withDismiss: PropTypes.bool, intl: PropTypes.object.isRequired, }; diff --git a/app/javascript/glitch/components/status/content.js b/app/javascript/glitch/components/status/content.js index 2aad55070b7..06015619b14 100644 --- a/app/javascript/glitch/components/status/content.js +++ b/app/javascript/glitch/components/status/content.js @@ -1,13 +1,11 @@ // Package imports // import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import escapeTextContentForBrowser from 'escape-html'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import classnames from 'classnames'; // Mastodon imports // -import emojify from '../../../mastodon/emoji'; import { isRtl } from '../../../mastodon/rtl'; import Permalink from '../../../mastodon/components/permalink'; @@ -32,7 +30,7 @@ export default class StatusContent extends React.PureComponent { const node = this.node; const links = node.querySelectorAll('a'); - for (var i = 0; i < links.length; ++i) { + for (let i = 0; i < links.length; ++i) { let link = links[i]; let mention = this.props.status.get('mentions').find(item => link.href === item.get('url')); @@ -131,12 +129,8 @@ export default class StatusContent extends React.PureComponent { this.state.hidden ); - const content = { __html: emojify(status.get('content')) }; - const spoilerContent = { - __html: emojify(escapeTextContentForBrowser( - status.get('spoiler_text', '') - )), - }; + const content = { __html: status.get('contentHtml') }; + const spoilerContent = { __html: status.get('spoilerHtml') }; const directionStyle = { direction: 'ltr' }; const classNames = classnames('status__content', { 'status__content--with-action': parseClick && !disabled, diff --git a/app/javascript/glitch/components/status/index.js b/app/javascript/glitch/components/status/index.js index 4a2a0e1d42f..9e758793c62 100644 --- a/app/javascript/glitch/components/status/index.js +++ b/app/javascript/glitch/components/status/index.js @@ -155,12 +155,12 @@ export default class Status extends ImmutablePureComponent { }; static propTypes = { - id : PropTypes.number, + id : PropTypes.string, status : ImmutablePropTypes.map, account : ImmutablePropTypes.map, settings : ImmutablePropTypes.map, notification : ImmutablePropTypes.map, - me : PropTypes.number, + me : PropTypes.string, onFavourite : PropTypes.func, onReblog : PropTypes.func, onModalReblog : PropTypes.func, diff --git a/app/javascript/glitch/components/status/prepend.js b/app/javascript/glitch/components/status/prepend.js index 6213e4c8d41..8c0aed0f417 100644 --- a/app/javascript/glitch/components/status/prepend.js +++ b/app/javascript/glitch/components/status/prepend.js @@ -22,12 +22,8 @@ Imports: import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import escapeTextContentForBrowser from 'escape-html'; import { FormattedMessage } from 'react-intl'; -// Mastodon imports // -import emojify from '../../../mastodon/emoji'; - /* * * * */ /* @@ -99,9 +95,7 @@ generate the message. > diff --git a/app/javascript/images/logo.svg b/app/javascript/images/logo.svg index 4b72b3ac8bf..034a9c22177 100644 --- a/app/javascript/images/logo.svg +++ b/app/javascript/images/logo.svg @@ -1 +1 @@ - + diff --git a/app/javascript/images/logo_alt.svg b/app/javascript/images/logo_alt.svg index e88ca741851..102d4c787e1 100644 --- a/app/javascript/images/logo_alt.svg +++ b/app/javascript/images/logo_alt.svg @@ -1 +1 @@ - + diff --git a/app/javascript/images/logo_full.svg b/app/javascript/images/logo_full.svg index 8b1328e8c8d..c33883342d7 100644 --- a/app/javascript/images/logo_full.svg +++ b/app/javascript/images/logo_full.svg @@ -1 +1 @@ - + diff --git a/app/javascript/images/mastodon_small.jpg b/app/javascript/images/mastodon_small.jpg deleted file mode 100644 index 9c88ce3f7bd..00000000000 Binary files a/app/javascript/images/mastodon_small.jpg and /dev/null differ diff --git a/app/javascript/images/preview.jpg b/app/javascript/images/preview.jpg new file mode 100644 index 00000000000..ec285674844 Binary files /dev/null and b/app/javascript/images/preview.jpg differ diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index c86184046e5..20cb09f582f 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -1,5 +1,5 @@ import api from '../api'; -import emojione from 'emojione'; +import { emojiIndex } from 'emoji-mart'; import { updateTimeline, @@ -23,7 +23,6 @@ export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO'; export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR'; export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY'; -export const COMPOSE_SUGGESTIONS_READY_TXT = 'COMPOSE_SUGGESTIONS_READY_TXT'; export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT'; export const COMPOSE_MOUNT = 'COMPOSE_MOUNT'; @@ -213,59 +212,35 @@ export function clearComposeSuggestions() { }; }; -let allShortcodes = null; // cached list of all shortcodes for suggestions - export function fetchComposeSuggestions(token) { - let leading = token[0]; - - if (leading === '@') { - // handle search - return (dispatch, getState) => { - api(getState).get('/api/v1/accounts/search', { - params: { - q: token.slice(1), // remove the '@' - resolve: false, - limit: 4, - }, - }).then(response => { - dispatch(readyComposeSuggestions(token, response.data)); - }); - }; - } else if (leading === ':') { - // shortcode - if (!allShortcodes) { - allShortcodes = Object.keys(emojione.emojioneList); - // TODO when we have custom emojons merged, add them to this shortcode list + return (dispatch, getState) => { + if (token[0] === ':') { + const results = emojiIndex.search(token.replace(':', ''), { maxResults: 3 }); + dispatch(readyComposeSuggestionsEmojis(token, results)); + return; } - return (dispatch) => { - const innertxt = token.slice(1); - if (innertxt.length > 1) { // prevent searching single letter, causes lag - dispatch(readyComposeSuggestionsTxt(token, allShortcodes.filter((sc) => { - return sc.indexOf(innertxt) !== -1; - }).sort((a, b) => { - if (a.indexOf(token) === 0 && b.indexOf(token) === 0) return a.localeCompare(b); - if (a.indexOf(token) === 0) return -1; - if (b.indexOf(token) === 0) return 1; - return a.localeCompare(b); - }))); - } - }; - } else { - // hashtag - return (dispatch, getState) => { - api(getState).get('/api/v1/search', { - params: { - q: token, - resolve: true, - }, - }).then(response => { - dispatch(readyComposeSuggestionsTxt(token, response.data.hashtags.map((ht) => `#${ht}`))); - }); - }; - } + + api(getState).get('/api/v1/accounts/search', { + params: { + q: token.slice(1), + resolve: false, + limit: 4, + }, + }).then(response => { + dispatch(readyComposeSuggestionsAccounts(token, response.data)); + }); + }; }; -export function readyComposeSuggestions(token, accounts) { +export function readyComposeSuggestionsEmojis(token, emojis) { + return { + type: COMPOSE_SUGGESTIONS_READY, + token, + emojis, + }; +}; + +export function readyComposeSuggestionsAccounts(token, accounts) { return { type: COMPOSE_SUGGESTIONS_READY, token, @@ -273,23 +248,21 @@ export function readyComposeSuggestions(token, accounts) { }; }; -export function readyComposeSuggestionsTxt(token, items) { - return { - type: COMPOSE_SUGGESTIONS_READY_TXT, - token, - items, - }; -}; - -export function selectComposeSuggestion(position, token, accountId) { +export function selectComposeSuggestion(position, token, suggestion) { return (dispatch, getState) => { - const completion = (typeof accountId === 'string') ? - accountId.slice(1) : // text suggestion: discard the leading : or # - the replacing code replaces only what follows - getState().getIn(['accounts', accountId, 'acct']); + let completion, startPosition; + + if (typeof suggestion === 'object' && suggestion.id) { + completion = suggestion.native || suggestion.colons; + startPosition = position - 1; + } else { + completion = getState().getIn(['accounts', suggestion, 'acct']); + startPosition = position; + } dispatch({ type: COMPOSE_SUGGESTION_SELECT, - position, + position: startPosition, token, completion, }); diff --git a/app/javascript/mastodon/actions/height_cache.js b/app/javascript/mastodon/actions/height_cache.js new file mode 100644 index 00000000000..4c752993feb --- /dev/null +++ b/app/javascript/mastodon/actions/height_cache.js @@ -0,0 +1,17 @@ +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, + }; +}; diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 0b5e72c1713..2204e0b14a6 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -23,9 +23,6 @@ export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST'; export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS'; 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) { return { type: STATUS_FETCH_REQUEST, @@ -218,17 +215,3 @@ export function unmuteStatusFail(id, error) { error, }; }; - -export function setStatusHeight (id, height) { - return { - type: STATUS_SET_HEIGHT, - id, - height, - }; -}; - -export function clearStatusesHeight () { - return { - type: STATUSES_CLEAR_HEIGHT, - }; -}; diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js index 0597d265ec4..a1db0fdd51c 100644 --- a/app/javascript/mastodon/actions/store.js +++ b/app/javascript/mastodon/actions/store.js @@ -5,8 +5,7 @@ export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; const convertState = rawState => fromJS(rawState, (k, v) => - Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x => - Number.isNaN(x * 1) ? x : x * 1)); + Iterable.isIndexed(v) ? v.toList() : v.toMap()); export function hydrateStore(rawState) { const state = convertState(rawState); diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js index 762cea4f734..7cdb8c672df 100644 --- a/app/javascript/mastodon/components/account.js +++ b/app/javascript/mastodon/components/account.js @@ -23,7 +23,7 @@ export default class Account extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, - me: PropTypes.number.isRequired, + me: PropTypes.string.isRequired, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/components/autosuggest_emoji.js b/app/javascript/mastodon/components/autosuggest_emoji.js new file mode 100644 index 00000000000..e2866e8e471 --- /dev/null +++ b/app/javascript/mastodon/components/autosuggest_emoji.js @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { unicodeMapping } from '../emojione_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 [ filename ] = unicodeMapping[emoji.native]; + url = `${assetHost}/emoji/${filename}.svg`; + } + + return ( +
+ {emoji.native + + {emoji.colons} +
+ ); + } + +} diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js index 1c7ffe1968d..6f725885dee 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.js +++ b/app/javascript/mastodon/components/autosuggest_textarea.js @@ -1,11 +1,12 @@ import React from 'react'; import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; -import AutosuggestShortcode from '../features/compose/components/autosuggest_shortcode'; +import AutosuggestEmoji from './autosuggest_emoji'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { isRtl } from '../rtl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Textarea from 'react-textarea-autosize'; +import classNames from 'classnames'; const textAtCursorMatchesToken = (str, caretPosition) => { let word; @@ -19,12 +20,11 @@ const textAtCursorMatchesToken = (str, caretPosition) => { word = str.slice(left, right + caretPosition); } - if (!word || word.trim().length < 2 || ['@', ':', '#'].indexOf(word[0]) === -1) { + if (!word || word.trim().length < 3 || ['@', ':'].indexOf(word[0]) === -1) { return [null, null]; } word = word.trim().toLowerCase(); - // was: .slice(1); - we leave the leading char there, handler can decide what to do based on it if (word.length > 0) { return [left + 1, word]; @@ -43,7 +43,6 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { onSuggestionSelected: PropTypes.func.isRequired, onSuggestionsClearRequested: PropTypes.func.isRequired, onSuggestionsFetchRequested: PropTypes.func.isRequired, - onLocalSuggestionsFetchRequested: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, onKeyUp: PropTypes.func, onKeyDown: PropTypes.func, @@ -67,13 +66,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { if (token !== null && this.state.lastToken !== token) { this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); - if (token[0] === ':') { - // faster debounce for shortcodes. - // hashtags have long debounce because they're fetched from server. - this.props.onLocalSuggestionsFetchRequested(token); - } else { - this.props.onSuggestionsFetchRequested(token); - } + this.props.onSuggestionsFetchRequested(token); } else if (token === null) { this.setState({ lastToken: null }); this.props.onSuggestionsClearRequested(); @@ -137,9 +130,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } onSuggestionClick = (e) => { - // leave suggestion string unchanged if it's a hash / shortcode suggestion. convert account number to int. - const suggestionStr = e.currentTarget.getAttribute('data-index'); - const suggestion = [':', '#'].indexOf(suggestionStr[0]) !== -1 ? suggestionStr : Number(suggestionStr); + const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index')); e.preventDefault(); this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); this.textarea.focus(); @@ -162,36 +153,39 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } } - componentDidUpdate () { - if (this.refs.selected) { - if (this.refs.selected.scrollIntoViewIfNeeded) - this.refs.selected.scrollIntoViewIfNeeded(); - else - this.refs.selected.scrollIntoView({ behavior: 'auto', block: 'nearest' }); + renderSuggestion = (suggestion, i) => { + const { selectedSuggestion } = this.state; + let inner, key; + + if (typeof suggestion === 'object') { + inner = ; + key = suggestion.id; + } else { + inner = ; + key = suggestion; } + + return ( +
+ {inner} +
+ ); } render () { const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus } = this.props; - const { suggestionsHidden, selectedSuggestion } = this.state; + const { suggestionsHidden } = this.state; const style = { direction: 'ltr' }; if (isRtl(value)) { style.direction = 'rtl'; } - let makeItem = (suggestion) => { - if (suggestion[0] === ':') return ; - if (suggestion[0] === '#') return suggestion; // hashtag - - // else - accounts are always returned as IDs with no prefix - return ; - }; - return (