diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f49964bd9..8ae4bb882 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,7 @@ --- name: Bug Report about: If something isn't working as expected - +labels: bug --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 3890729e2..ff92c0316 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,6 @@ --- name: Feature Request about: I have a suggestion - --- diff --git a/.ruby-version b/.ruby-version index 338a5b5d8..37c2961c2 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.6.6 +2.7.2 diff --git a/Dockerfile b/Dockerfile index c52f89fdc..57d061fd0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,7 @@ RUN apt update && \ cd .. && rm -rf jemalloc-$JE_VER $JE_VER.tar.gz # Install Ruby -ENV RUBY_VER="2.6.6" +ENV RUBY_VER="2.7.2" ENV CPPFLAGS="-I/opt/jemalloc/include" ENV LDFLAGS="-L/opt/jemalloc/lib/" RUN apt update && \ diff --git a/Gemfile b/Gemfile index 33e467732..950bc59bb 100644 --- a/Gemfile +++ b/Gemfile @@ -11,16 +11,13 @@ gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.0' gem 'rack', '~> 2.2.3' -gem 'thwait', '~> 0.2.0' -gem 'e2mmap', '~> 0.1.0' - gem 'hamlit-rails', '~> 0.2' gem 'pg', '~> 1.2' gem 'makara', '~> 0.4' gem 'pghero', '~> 2.7' gem 'dotenv-rails', '~> 2.7' -gem 'aws-sdk-s3', '~> 1.83', require: false +gem 'aws-sdk-s3', '~> 1.85', require: false gem 'fog-core', '<= 2.1.0' gem 'fog-openstack', '~> 0.3', require: false gem 'paperclip', '~> 6.0' @@ -30,7 +27,7 @@ gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.7' -gem 'bootsnap', '~> 1.4', require: false +gem 'bootsnap', '~> 1.5', require: false gem 'browser' gem 'charlock_holmes', '~> 0.7.7' gem 'iso-639' @@ -44,7 +41,7 @@ group :pam_authentication, optional: true do end gem 'net-ldap', '~> 0.16' -gem 'omniauth-cas', '~> 1.1' +gem 'omniauth-cas', '~> 2.0' gem 'omniauth-saml', '~> 1.10' gem 'omniauth', '~> 1.9' @@ -71,7 +68,7 @@ gem 'nsa', '~> 0.2' gem 'oj', '~> 3.10' gem 'ox', '~> 2.13' gem 'parslet' -gem 'parallel', '~> 1.19' +gem 'parallel', '~> 1.20' gem 'posix-spawn' gem 'pundit', '~> 2.1' gem 'premailer-rails' @@ -125,21 +122,21 @@ group :test do gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.1' gem 'simplecov', '~> 0.19', require: false - gem 'webmock', '~> 3.9' - gem 'parallel_tests', '~> 3.3' + gem 'webmock', '~> 3.10' + gem 'parallel_tests', '~> 3.4' gem 'rspec_junit_formatter', '~> 0.4' end group :development do gem 'active_record_query_trace', '~> 1.8' gem 'annotate', '~> 3.1' - gem 'better_errors', '~> 2.8' + gem 'better_errors', '~> 2.9' gem 'binding_of_caller', '~> 0.7' gem 'bullet', '~> 6.1' gem 'letter_opener', '~> 1.7' gem 'letter_opener_web', '~> 1.4' gem 'memory_profiler' - gem 'rubocop', '~> 0.93', require: false + gem 'rubocop', '~> 1.3', require: false gem 'rubocop-rails', '~> 2.8', require: false gem 'brakeman', '~> 4.10', require: false gem 'bundler-audit', '~> 0.7', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 0a02fecc9..01e7925a4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -79,8 +79,8 @@ GEM cocaine (~> 0.5.3) awrence (1.1.1) aws-eventstream (1.1.0) - aws-partitions (1.388.0) - aws-sdk-core (3.109.1) + aws-partitions (1.397.0) + aws-sdk-core (3.109.3) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) @@ -88,14 +88,14 @@ GEM aws-sdk-kms (1.39.0) aws-sdk-core (~> 3, >= 3.109.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.83.1) + aws-sdk-s3 (1.85.0) aws-sdk-core (~> 3, >= 3.109.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) aws-sigv4 (1.2.2) aws-eventstream (~> 1, >= 1.0.2) bcrypt (3.1.16) - better_errors (2.8.3) + better_errors (2.9.1) coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) @@ -104,10 +104,10 @@ GEM debug_inspector (>= 0.0.1) blurhash (0.1.4) ffi (~> 1.10.0) - bootsnap (1.4.9) + bootsnap (1.5.1) msgpack (~> 1.0) brakeman (4.10.0) - browser (5.1.0) + browser (4.2.0) builder (3.2.4) bullet (6.1.0) activesupport (>= 3.0.0) @@ -147,7 +147,7 @@ GEM activesupport (>= 4.0) elasticsearch (>= 2.0.0) elasticsearch-dsl - chunky_png (1.3.14) + chunky_png (1.3.12) cld3 (3.3.0) ffi (>= 1.1.0, < 1.12.0) climate_control (0.2.0) @@ -207,13 +207,12 @@ GEM erubi (1.9.0) et-orbi (1.2.4) tzinfo - excon (0.78.0) + excon (0.76.0) fabrication (2.21.1) faker (2.14.0) i18n (>= 1.6, < 2) - faraday (1.1.0) + faraday (1.0.1) multipart-post (>= 1.2, < 3) - ruby2_keywords fast_blank (1.0.0) fastimage (2.2.0) ffi (1.10.0) @@ -233,9 +232,9 @@ GEM fog-json (>= 1.0) ipaddress (>= 0.8) formatador (0.2.5) - fugit (1.4.0) + fugit (1.3.9) et-orbi (~> 1.1, >= 1.1.8) - raabro (~> 1.4) + raabro (~> 1.3) fuubar (2.5.0) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) @@ -290,7 +289,7 @@ GEM jmespath (1.4.0) json (2.3.1) json-canonicalization (0.2.0) - json-ld (3.1.4) + json-ld (3.1.5) htmlentities (~> 4.3) json-canonicalization (~> 0.2) link_header (~> 0.0, >= 0.0.8) @@ -368,11 +367,11 @@ GEM concurrent-ruby (~> 1.0, >= 1.0.2) sidekiq (>= 3.5) statsd-ruby (~> 1.4, >= 1.4.0) - oj (3.10.15) + oj (3.10.16) omniauth (1.9.1) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) - omniauth-cas (1.1.1) + omniauth-cas (2.0.0) addressable (~> 2.3) nokogiri (~> 1.5) omniauth (~> 1.2) @@ -383,7 +382,7 @@ GEM openssl-signature_algorithm (0.4.0) orm_adapter (0.5.0) ox (2.13.4) - paperclip (6.1.0) + paperclip (6.0.0) activemodel (>= 4.2.0) activesupport (>= 4.2.0) mime-types @@ -392,8 +391,8 @@ GEM paperclip-av-transcoder (0.6.4) av (~> 0.9.0) paperclip (>= 2.5.2) - parallel (1.19.2) - parallel_tests (3.3.0) + parallel (1.20.1) + parallel_tests (3.4.0) parallel parser (2.7.2.0) ast (~> 2.4.1) @@ -429,7 +428,7 @@ GEM nio4r (~> 2.0) pundit (2.1.0) activesupport (>= 3.0.0) - raabro (1.4.0) + raabro (1.3.3) rack (2.2.3) rack-attack (6.3.1) rack (>= 1.0, < 3) @@ -464,7 +463,7 @@ GEM rails-i18n (5.1.3) i18n (>= 0.7, < 2) railties (>= 5.0, < 6) - rails-settings-cached (0.7.2) + rails-settings-cached (0.6.6) rails (>= 4.2.0) railties (5.2.4.4) actionpack (= 5.2.4.4) @@ -474,12 +473,12 @@ GEM thor (>= 0.19.0, < 2.0) rainbow (3.0.0) rake (13.0.1) - rdf (3.1.6) + rdf (3.1.7) hamster (~> 3.0) link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.4.0) rdf (~> 3.1) - redis (4.2.2) + redis (4.2.5) redis-actionpack (5.2.0) actionpack (>= 5, < 7) redis-rack (>= 2.1.0, < 3) @@ -511,14 +510,14 @@ GEM chunky_png (~> 1.0) rqrcode_core (~> 0.1) rqrcode_core (0.1.2) - rspec-core (3.10.0) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.0) + rspec-core (3.9.3) + rspec-support (~> 3.9.3) + rspec-expectations (3.9.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.0) + rspec-support (~> 3.9.0) + rspec-mocks (3.9.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) + rspec-support (~> 3.9.0) rspec-rails (4.0.1) actionpack (>= 4.2) activesupport (>= 4.2) @@ -530,19 +529,19 @@ GEM rspec-sidekiq (3.1.0) rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) - rspec-support (3.10.0) + rspec-support (3.9.3) rspec_junit_formatter (0.4.1) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (0.93.1) + rubocop (1.3.1) parallel (~> 1.10) parser (>= 2.7.1.5) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8) rexml - rubocop-ast (>= 0.6.0) + rubocop-ast (>= 1.1.1) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 2.0) - rubocop-ast (1.1.0) + rubocop-ast (1.1.1) parser (>= 2.7.1.5) rubocop-rails (2.8.1) activesupport (>= 4.2.0) @@ -551,7 +550,6 @@ GEM ruby-progressbar (1.10.1) ruby-saml (1.11.0) nokogiri (>= 1.5.10) - ruby2_keywords (0.0.2) rufus-scheduler (3.6.0) fugit (~> 1.1, >= 1.1.6) safety_net_attestation (0.4.0) @@ -651,7 +649,7 @@ GEM safety_net_attestation (~> 0.4.0) securecompare (~> 1.0) tpm-key_attestation (~> 0.9.0) - webmock (3.9.3) + webmock (3.10.0) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -660,7 +658,7 @@ GEM rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) - webpush (1.0.0) + webpush (0.3.8) hkdf (~> 0.2) jwt (~> 2.0) websocket-driver (0.7.3) @@ -679,11 +677,11 @@ DEPENDENCIES active_record_query_trace (~> 1.8) addressable (~> 2.7) annotate (~> 3.1) - aws-sdk-s3 (~> 1.83) - better_errors (~> 2.8) + aws-sdk-s3 (~> 1.85) + better_errors (~> 2.9) binding_of_caller (~> 0.7) blurhash (~> 0.1) - bootsnap (~> 1.4) + bootsnap (~> 1.5) brakeman (~> 4.10) browser bullet (~> 6.1) @@ -706,7 +704,6 @@ DEPENDENCIES discard (~> 1.2) doorkeeper (~> 5.4) dotenv-rails (~> 2.7) - e2mmap (~> 0.1.0) ed25519 (~> 1.2) fabrication (~> 2.21) faker (~> 2.14) @@ -743,13 +740,13 @@ DEPENDENCIES nsa (~> 0.2) oj (~> 3.10) omniauth (~> 1.9) - omniauth-cas (~> 1.1) + omniauth-cas (~> 2.0) omniauth-saml (~> 1.10) ox (~> 2.13) paperclip (~> 6.0) paperclip-av-transcoder (~> 0.6) - parallel (~> 1.19) - parallel_tests (~> 3.3) + parallel (~> 1.20) + parallel_tests (~> 3.4) parslet pg (~> 1.2) pghero (~> 2.7) @@ -777,7 +774,7 @@ DEPENDENCIES rspec-rails (~> 4.0) rspec-sidekiq (~> 3.1) rspec_junit_formatter (~> 0.4) - rubocop (~> 0.93) + rubocop (~> 1.3) rubocop-rails (~> 2.8) ruby-progressbar (~> 1.10) sanitize (~> 5.2) @@ -795,12 +792,11 @@ DEPENDENCIES streamio-ffmpeg (~> 3.0) strong_migrations (~> 0.7) thor (~> 1.0) - thwait (~> 0.2.0) tty-prompt (~> 0.22) twitter-text (~> 1.14) tzinfo-data (~> 1.2020) webauthn (~> 3.0.0.alpha1) - webmock (~> 3.9) + webmock (~> 3.10) webpacker (~> 5.2) webpush xorcist (~> 1.1) diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 6d711afd0..ccb5ef8e8 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -102,6 +102,10 @@ class AccountsController < ApplicationController params[:username] end + def skip_temporary_suspension_response? + request.format == :json + end + def rss_url if tag_requested? short_account_tag_url(@account, params[:tag], format: 'rss') diff --git a/app/controllers/activitypub/base_controller.rb b/app/controllers/activitypub/base_controller.rb index 0c2591e97..4cbc3ab8f 100644 --- a/app/controllers/activitypub/base_controller.rb +++ b/app/controllers/activitypub/base_controller.rb @@ -8,4 +8,8 @@ class ActivityPub::BaseController < Api::BaseController def set_cache_headers response.headers['Vary'] = 'Signature' if authorized_fetch_mode? end + + def skip_temporary_suspension_response? + false + end end diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index fdb60d590..d3044f180 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -33,6 +33,10 @@ class ActivityPub::InboxesController < ActivityPub::BaseController params[:account_username].present? end + def skip_temporary_suspension_response? + true + end + def body return @body if defined?(@body) diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index 43bf4e657..fde6c861f 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -31,7 +31,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController end def set_replies - @replies = only_other_accounts? ? Status.where.not(account_id: @account.id) : @account.statuses + @replies = only_other_accounts? ? Status.where.not(account_id: @account.id).joins(:account).merge(Account.without_suspended) : @account.statuses @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted]) @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) end diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index b9b75727d..1dd7430e0 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -53,6 +53,13 @@ module Admin redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.destroyed_msg', username: @account.acct) end + def unsensitive + authorize @account, :unsensitive? + @account.unsensitize! + log_action :unsensitive, @account + redirect_to admin_account_path(@account.id) + end + def unsilence authorize @account, :unsilence? @account.unsilence! diff --git a/app/controllers/admin/announcements_controller.rb b/app/controllers/admin/announcements_controller.rb index 494fd13d0..351b9a991 100644 --- a/app/controllers/admin/announcements_controller.rb +++ b/app/controllers/admin/announcements_controller.rb @@ -71,7 +71,7 @@ class Admin::AnnouncementsController < Admin::BaseController private def set_announcements - @announcements = AnnouncementFilter.new(filter_params).results.page(params[:page]) + @announcements = AnnouncementFilter.new(filter_params).results.reverse_chronological.page(params[:page]) end def set_announcement diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index e962c4e97..fe199e689 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -103,7 +103,7 @@ class Api::BaseController < ApplicationController elsif !current_user.functional? render json: { error: 'Your login is currently disabled' }, status: 403 else - set_user_activity + update_user_sign_in end end diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb index 3af572f25..63cc521ed 100644 --- a/app/controllers/api/v1/admin/accounts_controller.rb +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -22,6 +22,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController active pending disabled + sensitized silenced suspended username @@ -68,6 +69,13 @@ class Api::V1::Admin::AccountsController < Api::BaseController render json: @account, serializer: REST::Admin::AccountSerializer end + def unsensitive + authorize @account, :unsensitive? + @account.unsensitize! + log_action :unsensitive, @account + render json: @account, serializer: REST::Admin::AccountSerializer + end + def unsilence authorize @account, :unsilence? @account.unsilence! diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb index 7afa822ed..2e21ce6a0 100644 --- a/app/controllers/api/v1/statuses/favourites_controller.rb +++ b/app/controllers/api/v1/statuses/favourites_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:favourites' } before_action :require_user! - before_action :set_status + before_action :set_status, only: [:create] def create FavouriteService.new.call(current_account, @status) @@ -13,8 +13,19 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController end def destroy - UnfavouriteWorker.perform_async(current_account.id, @status.id) + fav = current_account.favourites.find_by(status_id: params[:status_id]) + + if fav + @status = fav.status + UnfavouriteWorker.perform_async(current_account.id, @status.id) + else + @status = Status.find(params[:status_id]) + authorize @status, :show? + end + render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false }) + rescue Mastodon::NotPermittedError + not_found end private diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index c1ea702ad..13d158c67 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -7,6 +7,7 @@ class Auth::SessionsController < Devise::SessionsController skip_before_action :require_no_authentication, only: [:create] skip_before_action :require_functional! + skip_before_action :update_user_sign_in include TwoFactorAuthenticationConcern include SignInTokenAuthenticationConcern @@ -24,6 +25,7 @@ class Auth::SessionsController < Devise::SessionsController def create super do |resource| + resource.update_sign_in!(request, new_sign_in: true) remember_me(resource) flash.delete(:notice) end @@ -57,7 +59,7 @@ class Auth::SessionsController < Devise::SessionsController def find_user if session[:attempt_user_id] - User.find(session[:attempt_user_id]) + User.find_by(id: session[:attempt_user_id]) else user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication @@ -90,6 +92,7 @@ class Auth::SessionsController < Devise::SessionsController def require_no_authentication super + # Delete flash message that isn't entirely useful and may be confusing in # most cases because /web doesn't display/clear flash messages. flash.delete(:alert) if flash[:alert] == I18n.t('devise.failure.already_authenticated') @@ -107,13 +110,30 @@ class Auth::SessionsController < Devise::SessionsController def home_paths(resource) paths = [about_path] + if single_user_mode? && resource.is_a?(User) paths << short_account_path(username: resource.account) end + paths end def continue_after? truthy_param?(:continue) end + + def restart_session + clear_attempt_from_session + redirect_to new_user_session_path, alert: I18n.t('devise.failure.timeout') + end + + def set_attempt_session(user) + session[:attempt_user_id] = user.id + session[:attempt_user_updated_at] = user.updated_at.to_s + end + + def clear_attempt_from_session + session.delete(:attempt_user_id) + session.delete(:attempt_user_updated_at) + end end diff --git a/app/controllers/concerns/account_owned_concern.rb b/app/controllers/concerns/account_owned_concern.rb index 460f71f65..62e379846 100644 --- a/app/controllers/concerns/account_owned_concern.rb +++ b/app/controllers/concerns/account_owned_concern.rb @@ -29,6 +29,24 @@ module AccountOwnedConcern end def check_account_suspension - expires_in(3.minutes, public: true) && gone if @account.suspended? + if @account.suspended_permanently? + permanent_suspension_response + elsif @account.suspended? && !skip_temporary_suspension_response? + temporary_suspension_response + end + end + + def skip_temporary_suspension_response? + false + end + + def permanent_suspension_response + expires_in(3.minutes, public: true) + gone + end + + def temporary_suspension_response + expires_in(3.minutes, public: true) + forbidden end end diff --git a/app/controllers/concerns/sign_in_token_authentication_concern.rb b/app/controllers/concerns/sign_in_token_authentication_concern.rb index 91f813acc..3c95a4afd 100644 --- a/app/controllers/concerns/sign_in_token_authentication_concern.rb +++ b/app/controllers/concerns/sign_in_token_authentication_concern.rb @@ -18,7 +18,9 @@ module SignInTokenAuthenticationConcern def authenticate_with_sign_in_token user = self.resource = find_user - if user_params[:sign_in_token_attempt].present? && session[:attempt_user_id] + if user.present? && session[:attempt_user_id].present? && session[:attempt_user_updated_at] != user.updated_at.to_s + restart_session + elsif user_params.key?(:sign_in_token_attempt) && session[:attempt_user_id] authenticate_with_sign_in_token_attempt(user) elsif user.present? && user.external_or_valid_password?(user_params[:password]) prompt_for_sign_in_token(user) @@ -27,7 +29,7 @@ module SignInTokenAuthenticationConcern def authenticate_with_sign_in_token_attempt(user) if valid_sign_in_token_attempt?(user) - session.delete(:attempt_user_id) + clear_attempt_from_session remember_me(user) sign_in(user) else @@ -42,10 +44,10 @@ module SignInTokenAuthenticationConcern UserMailer.sign_in_token(user, request.remote_ip, request.user_agent, Time.now.utc.to_s).deliver_later! end - set_locale do - session[:attempt_user_id] = user.id - @body_classes = 'lighter' - render :sign_in_token - end + set_attempt_session(user) + + @body_classes = 'lighter' + + set_locale { render :sign_in_token } end end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index f69c62ec2..fc3978fbb 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -76,6 +76,7 @@ module SignatureVerification raise SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window? verify_signature_strength! + verify_body_digest! account = account_from_key_id(signature_params['keyId']) @@ -126,10 +127,19 @@ module SignatureVerification def verify_signature_strength! raise SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)') raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(Request::REQUEST_TARGET) || signed_headers.include?('digest') - raise SignatureVerificationError, 'Mastodon requires the Host header to be signed' unless signed_headers.include?('host') + raise SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host') raise SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest') end + def verify_body_digest! + return unless signed_headers.include?('digest') + + digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] } + sha256 = digests.assoc('sha-256') + raise SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil? + raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}" if body_digest != sha256[1] + end + def verify_signature(account, signature, compare_signed_string) if account.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), signature, compare_signed_string) @signed_request_account = account @@ -153,8 +163,6 @@ module SignatureVerification raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank? "(expires): #{signature_params['expires']}" - elsif signed_header == 'digest' - "digest: #{body_digest}" else "#{signed_header}: #{request.headers[to_header_name(signed_header)]}" end @@ -187,7 +195,7 @@ module SignatureVerification end def body_digest - "SHA-256=#{Digest::SHA256.base64digest(request_body)}" + @body_digest ||= Digest::SHA256.base64digest(request_body) end def to_header_name(name) diff --git a/app/controllers/concerns/two_factor_authentication_concern.rb b/app/controllers/concerns/two_factor_authentication_concern.rb index 8a2a86a02..4d4ccf49c 100644 --- a/app/controllers/concerns/two_factor_authentication_concern.rb +++ b/app/controllers/concerns/two_factor_authentication_concern.rb @@ -37,9 +37,11 @@ module TwoFactorAuthenticationConcern def authenticate_with_two_factor user = self.resource = find_user - if user.webauthn_enabled? && user_params[:credential].present? && session[:attempt_user_id] + if user.present? && session[:attempt_user_id].present? && session[:attempt_user_updated_at] != user.updated_at.to_s + restart_session + elsif user.webauthn_enabled? && user_params.key?(:credential) && session[:attempt_user_id] authenticate_with_two_factor_via_webauthn(user) - elsif user_params[:otp_attempt].present? && session[:attempt_user_id] + elsif user_params.key?(:otp_attempt) && session[:attempt_user_id] authenticate_with_two_factor_via_otp(user) elsif user.present? && user.external_or_valid_password?(user_params[:password]) prompt_for_two_factor(user) @@ -50,7 +52,7 @@ module TwoFactorAuthenticationConcern webauthn_credential = WebAuthn::Credential.from_get(user_params[:credential]) if valid_webauthn_credential?(user, webauthn_credential) - session.delete(:attempt_user_id) + clear_attempt_from_session remember_me(user) sign_in(user) render json: { redirect_path: root_path }, status: :ok @@ -61,7 +63,7 @@ module TwoFactorAuthenticationConcern def authenticate_with_two_factor_via_otp(user) if valid_otp_attempt?(user) - session.delete(:attempt_user_id) + clear_attempt_from_session remember_me(user) sign_in(user) else @@ -71,16 +73,18 @@ module TwoFactorAuthenticationConcern end def prompt_for_two_factor(user) - set_locale do - session[:attempt_user_id] = user.id - @body_classes = 'lighter' - @webauthn_enabled = user.webauthn_enabled? - @scheme_type = if user.webauthn_enabled? && user_params[:otp_attempt].blank? - 'webauthn' - else - 'totp' - end - render :two_factor + set_attempt_session(user) + + @body_classes = 'lighter' + @webauthn_enabled = user.webauthn_enabled? + @scheme_type = begin + if user.webauthn_enabled? && user_params[:otp_attempt].blank? + 'webauthn' + else + 'totp' + end end + + set_locale { render :two_factor } end end diff --git a/app/controllers/concerns/user_tracking_concern.rb b/app/controllers/concerns/user_tracking_concern.rb index be10705fc..efda37fae 100644 --- a/app/controllers/concerns/user_tracking_concern.rb +++ b/app/controllers/concerns/user_tracking_concern.rb @@ -6,14 +6,13 @@ module UserTrackingConcern UPDATE_SIGN_IN_HOURS = 24 included do - before_action :set_user_activity + before_action :update_user_sign_in end private - def set_user_activity - return unless user_needs_sign_in_update? - current_user.update_tracked_fields!(request) + def update_user_sign_in + current_user.update_sign_in!(request) if user_needs_sign_in_update? end def user_needs_sign_in_update? diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index ab0749963..ff4df2adf 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -52,6 +52,14 @@ class FollowerAccountsController < ApplicationController account_followers_url(@account, page: page) unless page.nil? end + def next_page_url + page_url(follows.next_page) if follows.respond_to?(:next_page) + end + + def prev_page_url + page_url(follows.prev_page) if follows.respond_to?(:prev_page) + end + def collection_presenter if page_requested? ActivityPub::CollectionPresenter.new( @@ -60,8 +68,8 @@ class FollowerAccountsController < ApplicationController 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) + next: next_page_url, + prev: prev_page_url ) else ActivityPub::CollectionPresenter.new( diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 918bdac0a..6bb95c454 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -52,6 +52,14 @@ class FollowingAccountsController < ApplicationController account_following_index_url(@account, page: page) unless page.nil? end + def next_page_url + page_url(follows.next_page) if follows.respond_to?(:next_page) + end + + def prev_page_url + page_url(follows.prev_page) if follows.respond_to?(:prev_page) + end + def collection_presenter if page_requested? ActivityPub::CollectionPresenter.new( @@ -60,8 +68,8 @@ class FollowingAccountsController < ApplicationController 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) + next: next_page_url, + prev: prev_page_url ) else ActivityPub::CollectionPresenter.new( diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index 0835758f2..96cce55e9 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -5,6 +5,7 @@ class RelationshipsController < ApplicationController before_action :authenticate_user! before_action :set_accounts, only: :show + before_action :set_relationships, only: :show before_action :set_body_classes helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship? @@ -28,6 +29,10 @@ class RelationshipsController < ApplicationController @accounts = RelationshipFilter.new(current_account, filter_params).results.page(params[:page]).per(40) end + def set_relationships + @relationships = AccountRelationshipsPresenter.new(@accounts.pluck(:id), current_user.account_id) + end + def form_account_batch_params params.require(:form_account_batch).permit(:action, account_ids: []) end @@ -49,7 +54,9 @@ class RelationshipsController < ApplicationController end def action_from_button - if params[:unfollow] + if params[:follow] + 'follow' + elsif params[:unfollow] 'unfollow' elsif params[:remove_from_followers] 'remove_from_followers' diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb index f96c83b80..7b8f8d207 100644 --- a/app/controllers/settings/deletes_controller.rb +++ b/app/controllers/settings/deletes_controller.rb @@ -42,7 +42,7 @@ class Settings::DeletesController < Settings::BaseController end def destroy_account! - current_account.suspend! + current_account.suspend!(origin: :local) AccountDeletionWorker.perform_async(current_user.account_id) sign_out end diff --git a/app/controllers/settings/exports/bookmarks_controller.rb b/app/controllers/settings/exports/bookmarks_controller.rb new file mode 100644 index 000000000..c12e2f147 --- /dev/null +++ b/app/controllers/settings/exports/bookmarks_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Settings + module Exports + class BookmarksController < BaseController + include ExportControllerConcern + + def index + send_export_file + end + + private + + def export_data + @export.to_bookmarks_csv + end + end + end +end diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb index 9de9db6ba..0227f722a 100644 --- a/app/controllers/well_known/webfinger_controller.rb +++ b/app/controllers/well_known/webfinger_controller.rb @@ -35,7 +35,7 @@ module WellKnown end def check_account_suspension - expires_in(3.minutes, public: true) && gone if @account.suspended? + expires_in(3.minutes, public: true) && gone if @account.suspended_permanently? end def bad_request diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 5512a9543..8914b015c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -89,6 +89,16 @@ module ApplicationHelper end end + def interrelationships_icon(relationships, account_id) + if relationships.following[account_id] && relationships.followed_by[account_id] + fa_icon('exchange', title: I18n.t('relationships.mutual'), class: 'fa-fw active passive') + elsif relationships.following[account_id] + fa_icon(locale_direction == 'ltr' ? 'arrow-right' : 'arrow-left', title: I18n.t('relationships.following'), class: 'fa-fw active') + elsif relationships.followed_by[account_id] + fa_icon(locale_direction == 'ltr' ? 'arrow-left' : 'arrow-right', title: I18n.t('relationships.followers'), class: 'fa-fw passive') + end + end + def custom_emoji_tag(custom_emoji, animate = true) if animate image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:") diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 87718dc05..5b39497b6 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -40,6 +40,7 @@ module SettingsHelper kk: 'Қазақша', kn: 'ಕನ್ನಡ', ko: '한국어', + ku: 'سۆرانی', lt: 'Lietuvių', lv: 'Latviešu', mk: 'Македонски', @@ -56,6 +57,8 @@ module SettingsHelper pt: 'Português', ro: 'Română', ru: 'Русский', + sa: 'संस्कृतम्', + sc: 'Sardu', sk: 'Slovenčina', sl: 'Slovenščina', sq: 'Shqip', @@ -69,6 +72,7 @@ module SettingsHelper uk: 'Українська', ur: 'اُردُو', vi: 'Tiếng Việt', + zgh: 'ⵜⴰⵎⴰⵣⵉⵖⵜ', 'zh-CN': '简体中文', 'zh-HK': '繁體中文(香港)', 'zh-TW': '繁體中文(臺灣)', diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index a51597cf3..daed9048f 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -4,8 +4,12 @@ module StatusesHelper EMBEDDED_CONTROLLER = 'statuses' EMBEDDED_ACTION = 'embed' - def link_to_more(url) - link_to t('statuses.show_more'), url, class: 'load-more load-gap' + def link_to_newer(url) + link_to t('statuses.show_newer'), url, class: 'load-more load-gap' + end + + def link_to_older(url) + link_to t('statuses.show_older'), url, class: 'load-more load-gap' end def nothing_here(extra_classes = '') @@ -117,6 +121,14 @@ module StatusesHelper end end + def sensitized?(status, account) + if !account.nil? && account.id == status.account_id + status.sensitive + else + status.account.sensitized? || status.sensitive + end + end + private def simplified_text(text) diff --git a/app/javascript/mastodon/actions/app.js b/app/javascript/mastodon/actions/app.js index 414968f7d..c817c8708 100644 --- a/app/javascript/mastodon/actions/app.js +++ b/app/javascript/mastodon/actions/app.js @@ -8,3 +8,10 @@ export const focusApp = () => ({ export const unfocusApp = () => ({ type: APP_UNFOCUS, }); + +export const APP_LAYOUT_CHANGE = 'APP_LAYOUT_CHANGE'; + +export const changeLayout = layout => ({ + type: APP_LAYOUT_CHANGE, + layout, +}); diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 3c5868851..6ab8bf1f9 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -151,9 +151,7 @@ export function submitCompose(routerHistory) { 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), }, }).then(function (response) { - if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) { - routerHistory.push('/timelines/direct'); - } else if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) { + if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) { routerHistory.goBack(); } diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index c4fa66428..93e18fba9 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -37,8 +37,9 @@ export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; -export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; -export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; +export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; +export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; +export const NOTIFICATIONS_DISMISS_BROWSER_PERMISSION = 'NOTIFICATIONS_DISMISS_BROWSER_PERMISSION'; defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, @@ -256,7 +257,7 @@ export function setupBrowserNotifications() { if ('Notification' in window && 'permissions' in navigator) { navigator.permissions.query({ name: 'notifications' }).then((status) => { status.onchange = () => dispatch(setBrowserPermission(Notification.permission)); - }); + }).catch(console.warn); } }; } @@ -283,3 +284,7 @@ export function setBrowserPermission (value) { value, }; } + +export const dismissBrowserPermission = () => ({ + type: NOTIFICATIONS_DISMISS_BROWSER_PERMISSION, +}); diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 15e90c578..ac4125887 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -137,7 +137,14 @@ export function fetchContext(id) { dispatch(fetchContextRequest(id)); api(getState).get(`/api/v1/statuses/${id}/context`).then(response => { - dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants))); + /** + * TODO + * the goal here is to get a way to have all the status of a conversation somewhere + */ + const statusConcat = response.data.ancestors.concat(response.data.descendants); + console.log('statusConcat', statusConcat); + + dispatch(importFetchedStatuses(statusConcat)); dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants)); }).catch(error => { diff --git a/app/javascript/mastodon/components/column.js b/app/javascript/mastodon/components/column.js index 55e3bfd5e..239824a4f 100644 --- a/app/javascript/mastodon/components/column.js +++ b/app/javascript/mastodon/components/column.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import detectPassiveEvents from 'detect-passive-events'; +import { supportsPassiveEvents } from 'detect-passive-events'; import { scrollTop } from '../scroll'; export default class Column extends React.PureComponent { @@ -35,9 +35,9 @@ export default class Column extends React.PureComponent { componentDidMount () { if (this.props.bindToDocument) { - document.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + document.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); } else { - this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); } } diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index 4290927ca..52a17c7cd 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -5,9 +5,9 @@ import IconButton from './icon_button'; import Overlay from 'react-overlays/lib/Overlay'; import Motion from '../features/ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; -import detectPassiveEvents from 'detect-passive-events'; +import { supportsPassiveEvents } from 'detect-passive-events'; -const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; let id = 0; class DropdownMenu extends React.PureComponent { diff --git a/app/javascript/mastodon/components/modal_root.js b/app/javascript/mastodon/components/modal_root.js index 6297b5e29..fe573ffda 100644 --- a/app/javascript/mastodon/components/modal_root.js +++ b/app/javascript/mastodon/components/modal_root.js @@ -9,11 +9,7 @@ export default class ModalRoot extends React.PureComponent { onClose: PropTypes.func.isRequired, }; - state = { - revealed: !!this.props.children, - }; - - activeElement = this.state.revealed ? document.activeElement : null; + activeElement = this.props.children ? document.activeElement : null; handleKeyUp = (e) => { if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) @@ -53,8 +49,6 @@ export default class ModalRoot extends React.PureComponent { this.activeElement = document.activeElement; this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true)); - } else if (!nextProps.children) { - this.setState({ revealed: false }); } } @@ -72,11 +66,6 @@ export default class ModalRoot extends React.PureComponent { console.error(error); }); } - if (this.props.children) { - requestAnimationFrame(() => { - this.setState({ revealed: true }); - }); - } } componentWillUnmount () { @@ -94,7 +83,6 @@ export default class ModalRoot extends React.PureComponent { render () { const { children, onClose } = this.props; - const { revealed } = this.state; const visible = !!children; if (!visible) { @@ -104,7 +92,7 @@ export default class ModalRoot extends React.PureComponent { } return ( -
+
{children}
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index be4f0bcca..8f288bdf9 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -97,7 +97,10 @@ class Status extends ImmutablePureComponent { cachedMediaWidth: PropTypes.number, scrollKey: PropTypes.string, deployPictureInPicture: PropTypes.func, - usingPiP: PropTypes.bool, + pictureInPicture: PropTypes.shape({ + inUse: PropTypes.bool, + available: PropTypes.bool, + }), }; // Avoid checking props that are functions (and whose equality will always @@ -108,7 +111,7 @@ class Status extends ImmutablePureComponent { 'muted', 'hidden', 'unread', - 'usingPiP', + 'pictureInPicture', ]; state = { @@ -277,7 +280,7 @@ class Status extends ImmutablePureComponent { let media = null; let statusAvatar, prepend, rebloggedByText; - const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey, usingPiP } = this.props; + const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey, pictureInPicture } = this.props; let { status, account, ...other } = this.props; @@ -348,7 +351,7 @@ class Status extends ImmutablePureComponent { status = status.get('reblog'); } - if (usingPiP) { + if (pictureInPicture.inUse) { media = ; } else if (status.get('media_attachments').size > 0) { if (this.props.muted) { @@ -375,7 +378,7 @@ class Status extends ImmutablePureComponent { width={this.props.cachedMediaWidth} height={110} cacheWidth={this.props.cacheMediaWidth} - deployPictureInPicture={this.handleDeployPictureInPicture} + deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined} /> )} @@ -388,6 +391,7 @@ class Status extends ImmutablePureComponent { {Component => ( diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index 7bfd66d3e..fe81981a7 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -57,7 +57,11 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, props) => ({ status: getStatus(state, props), - usingPiP: state.get('picture_in_picture').statusId === props.id, + + pictureInPicture: { + inUse: state.getIn(['meta', 'layout']) !== 'mobile' && state.get('picture_in_picture').statusId === props.id, + available: state.getIn(['meta', 'layout']) !== 'mobile', + }, }); return mapStateToProps; diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.js b/app/javascript/mastodon/features/account_gallery/components/media_item.js index c9a7af7f7..ba7ec46a3 100644 --- a/app/javascript/mastodon/features/account_gallery/components/media_item.js +++ b/app/javascript/mastodon/features/account_gallery/components/media_item.js @@ -122,7 +122,7 @@ export default class MediaItem extends ImmutablePureComponent {
{content} - {label} + {label && {label}}
); } diff --git a/app/javascript/mastodon/features/audio/index.js b/app/javascript/mastodon/features/audio/index.js index 434148e8e..c47f55dd1 100644 --- a/app/javascript/mastodon/features/audio/index.js +++ b/app/javascript/mastodon/features/audio/index.js @@ -386,13 +386,59 @@ class Audio extends React.PureComponent { return this.props.foregroundColor || '#ffffff'; } + seekBy (time) { + const currentTime = this.audio.currentTime + time; + + if (!isNaN(currentTime)) { + this.setState({ currentTime }, () => { + this.audio.currentTime = currentTime; + }); + } + } + + handleAudioKeyDown = e => { + // On the audio element or the seek bar, we can safely use the space bar + // for playback control because there are no buttons to press + + if (e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + this.togglePlay(); + } + } + + handleKeyDown = e => { + switch(e.key) { + case 'k': + e.preventDefault(); + e.stopPropagation(); + this.togglePlay(); + break; + case 'm': + e.preventDefault(); + e.stopPropagation(); + this.toggleMute(); + break; + case 'j': + e.preventDefault(); + e.stopPropagation(); + this.seekBy(-10); + break; + case 'l': + e.preventDefault(); + e.stopPropagation(); + this.seekBy(10); + break; + } + } + render () { const { src, intl, alt, editable, autoPlay } = this.props; const { paused, muted, volume, currentTime, duration, buffer, dragging } = this.state; const progress = Math.min((currentTime / duration) * 100, 100); return ( -
+
diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index bac136e5e..dc4f48060 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -5,7 +5,7 @@ import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components' import Overlay from 'react-overlays/lib/Overlay'; import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import detectPassiveEvents from 'detect-passive-events'; +import { supportsPassiveEvents } from 'detect-passive-events'; import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji'; import { assetHost } from 'mastodon/utils/config'; @@ -29,7 +29,7 @@ const messages = defineMessages({ let EmojiPicker, Emoji; // load asynchronously const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`; -const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; class ModifierPickerMenu extends React.PureComponent { diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js index 5223025fb..309f46290 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -5,7 +5,7 @@ import IconButton from '../../../components/icon_button'; import Overlay from 'react-overlays/lib/Overlay'; import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; -import detectPassiveEvents from 'detect-passive-events'; +import { supportsPassiveEvents } from 'detect-passive-events'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; @@ -21,7 +21,7 @@ const messages = defineMessages({ change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, }); -const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; class PrivacyDropdownMenu extends React.PureComponent { diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.js b/app/javascript/mastodon/features/getting_started/components/announcements.js index d53bd8055..5bc3abac6 100644 --- a/app/javascript/mastodon/features/getting_started/components/announcements.js +++ b/app/javascript/mastodon/features/getting_started/components/announcements.js @@ -396,7 +396,7 @@ class Announcements extends ImmutablePureComponent { _markAnnouncementAsRead () { const { dismissAnnouncement, announcements } = this.props; const { index } = this.state; - const announcement = announcements.get(index); + const announcement = announcements.get(announcements.size - 1 - index); if (!announcement.get('read')) dismissAnnouncement(announcement.get('id')); } diff --git a/app/javascript/mastodon/features/notifications/components/notifications_permission_banner.js b/app/javascript/mastodon/features/notifications/components/notifications_permission_banner.js index 766c9bb5b..6daf75814 100644 --- a/app/javascript/mastodon/features/notifications/components/notifications_permission_banner.js +++ b/app/javascript/mastodon/features/notifications/components/notifications_permission_banner.js @@ -1,25 +1,42 @@ import React from 'react'; import Icon from 'mastodon/components/icon'; import Button from 'mastodon/components/button'; -import { requestBrowserPermission } from 'mastodon/actions/notifications'; +import IconButton from 'mastodon/components/icon_button'; +import { requestBrowserPermission, dismissBrowserPermission } from 'mastodon/actions/notifications'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -export default @connect(() => {}) +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, +}); + +export default @connect() +@injectIntl class NotificationsPermissionBanner extends React.PureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, }; handleClick = () => { this.props.dispatch(requestBrowserPermission()); } + handleClose = () => { + this.props.dispatch(dismissBrowserPermission()); + } + render () { + const { intl } = this.props; + return (
+
+ +
+

}} />

diff --git a/app/javascript/mastodon/features/picture_in_picture/components/header.js b/app/javascript/mastodon/features/picture_in_picture/components/header.js index 4cb6de1a4..7dd199b75 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/header.js +++ b/app/javascript/mastodon/features/picture_in_picture/components/header.js @@ -7,12 +7,18 @@ import IconButton from 'mastodon/components/icon_button'; import { Link } from 'react-router-dom'; import Avatar from 'mastodon/components/avatar'; import DisplayName from 'mastodon/components/display_name'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, +}); const mapStateToProps = (state, { accountId }) => ({ account: state.getIn(['accounts', accountId]), }); export default @connect(mapStateToProps) +@injectIntl class Header extends ImmutablePureComponent { static propTypes = { @@ -20,10 +26,11 @@ class Header extends ImmutablePureComponent { statusId: PropTypes.string.isRequired, account: ImmutablePropTypes.map.isRequired, onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, }; render () { - const { account, statusId, onClose } = this.props; + const { account, statusId, onClose, intl } = this.props; return (
@@ -32,7 +39,7 @@ class Header extends ImmutablePureComponent { - +
); } diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index da495733e..e4be10879 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -6,7 +6,7 @@ import DisplayName from '../../../components/display_name'; import StatusContent from '../../../components/status_content'; import MediaGallery from '../../../components/media_gallery'; import { Link } from 'react-router-dom'; -import { injectIntl, defineMessages, FormattedDate } from 'react-intl'; +import { defineMessages, FormattedDate, injectIntl } from 'react-intl'; import Card from './card'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Video from '../../video'; @@ -16,16 +16,15 @@ import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; import AnimatedNumber from 'mastodon/components/animated_number'; import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder'; -import IconButton from '../../../components/icon_button'; const messages = defineMessages({ - public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, + public_short : { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, - direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, + private_short : { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, + direct_short : { id: 'privacy.direct.short', defaultMessage: 'Direct' }, }); -export default @injectIntl +export default @injectIntl class DetailedStatus extends ImmutablePureComponent { static contextTypes = { @@ -33,16 +32,16 @@ class DetailedStatus extends ImmutablePureComponent { }; static propTypes = { - status: ImmutablePropTypes.map, - onOpenMedia: PropTypes.func.isRequired, - onOpenVideo: PropTypes.func.isRequired, - onToggleHidden: PropTypes.func.isRequired, - measureHeight: PropTypes.bool, - onHeightChange: PropTypes.func, - domain: PropTypes.string.isRequired, - compact: PropTypes.bool, - showMedia: PropTypes.bool, - usingPiP: PropTypes.bool, + status : ImmutablePropTypes.map, + onOpenMedia : PropTypes.func.isRequired, + onOpenVideo : PropTypes.func.isRequired, + onToggleHidden : PropTypes.func.isRequired, + measureHeight : PropTypes.bool, + onHeightChange : PropTypes.func, + domain : PropTypes.string.isRequired, + compact : PropTypes.bool, + showMedia : PropTypes.bool, + usingPiP : PropTypes.bool, onToggleMediaVisibility: PropTypes.func, }; @@ -57,17 +56,17 @@ class DetailedStatus extends ImmutablePureComponent { } e.stopPropagation(); - } + }; handleOpenVideo = (media, options) => { this.props.onOpenVideo(media, options); - } + }; handleExpandedToggle = () => { this.props.onToggleHidden(this.props.status); - } + }; - _measureHeight (heightJustChanged) { + _measureHeight(heightJustChanged) { if (this.props.measureHeight && this.node) { scheduleIdleTask(() => this.node && this.setState({ height: Math.ceil(this.node.scrollHeight) + 1 })); @@ -80,9 +79,9 @@ class DetailedStatus extends ImmutablePureComponent { setRef = c => { this.node = c; this._measureHeight(); - } + }; - componentDidUpdate (prevProps, prevState) { + componentDidUpdate(prevProps, prevState) { this._measureHeight(prevState.height !== this.state.height); } @@ -98,10 +97,13 @@ class DetailedStatus extends ImmutablePureComponent { } window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); - } + }; - render () { + + render() { const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; + console.log('status in render ', status); + const outerStyle = { boxSizing: 'border-box' }; const { intl, compact, usingPiP } = this.props; @@ -109,7 +111,7 @@ class DetailedStatus extends ImmutablePureComponent { return null; } - let media = ''; + let media = ''; let applicationLink = ''; let reblogLink = ''; let reblogIcon = 'retweet'; @@ -143,6 +145,7 @@ class DetailedStatus extends ImmutablePureComponent { media = (
+
); } diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index ecc0b8f0b..36a84fcbf 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -31,7 +31,7 @@ import Icon from 'mastodon/components/icon'; import ComposePanel from './compose_panel'; import NavigationPanel from './navigation_panel'; -import detectPassiveEvents from 'detect-passive-events'; +import { supportsPassiveEvents } from 'detect-passive-events'; import { scrollRight } from '../../../scroll'; const componentMap = { @@ -80,7 +80,7 @@ class ColumnsArea extends ImmutablePureComponent { componentDidMount() { if (!this.props.singleColumn) { - this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); } this.lastIndex = getIndex(this.context.router.history.location.pathname); @@ -97,7 +97,7 @@ class ColumnsArea extends ImmutablePureComponent { componentDidUpdate(prevProps) { if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) { - this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); } this.lastIndex = getIndex(this.context.router.history.location.pathname); this.setState({ shouldAnimate: true }); diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js index e19f277d8..578375a7f 100644 --- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js +++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js @@ -18,7 +18,9 @@ import { length } from 'stringz'; import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components'; import GIFV from 'mastodon/components/gifv'; import { me } from 'mastodon/initial_state'; +// eslint-disable-next-line import/no-extraneous-dependencies import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js'; +// eslint-disable-next-line import/extensions import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js'; import { assetHost } from 'mastodon/utils/config'; @@ -386,6 +388,7 @@ class FocalPointModal extends ImmutablePureComponent { {media.get('type') === 'video' && (
diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index 54ec51fcf..d18f26b4e 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -38,23 +38,39 @@ class MediaModal extends ImmutablePureComponent { state = { index: null, navigationHidden: false, + zoomButtonHidden: false, }; handleSwipe = (index) => { this.setState({ index: index % this.props.media.size }); } + handleTransitionEnd = () => { + this.setState({ + zoomButtonHidden: false, + }); + } + handleNextClick = () => { - this.setState({ index: (this.getIndex() + 1) % this.props.media.size }); + this.setState({ + index: (this.getIndex() + 1) % this.props.media.size, + zoomButtonHidden: true, + }); } handlePrevClick = () => { - this.setState({ index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size }); + this.setState({ + index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size, + zoomButtonHidden: true, + }); } handleChangeIndex = (e) => { const index = Number(e.currentTarget.getAttribute('data-index')); - this.setState({ index: index % this.props.media.size }); + this.setState({ + index: index % this.props.media.size, + zoomButtonHidden: true, + }); } handleKeyDown = (e) => { @@ -149,6 +165,7 @@ class MediaModal extends ImmutablePureComponent { alt={image.get('description')} key={image.get('url')} onClick={this.toggleNavigation} + zoomButtonHidden={this.state.zoomButtonHidden} /> ); } else if (image.get('type') === 'video') { @@ -212,6 +229,7 @@ class MediaModal extends ImmutablePureComponent { style={swipeableViewsStyle} containerStyle={containerStyle} onChangeIndex={this.handleSwipe} + onTransitionEnd={this.handleTransitionEnd} index={index} disabled={disableSwiping} > diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.js index 9a07e7c4d..cce6756c3 100644 --- a/app/javascript/mastodon/features/ui/components/video_modal.js +++ b/app/javascript/mastodon/features/ui/components/video_modal.js @@ -64,6 +64,7 @@ export default class VideoModal extends ImmutablePureComponent {