diff options
Diffstat (limited to 'app')
122 files changed, 452 insertions, 248 deletions
diff --git a/app/controllers/account_follow_controller.rb b/app/controllers/account_follow_controller.rb index 185a355f8..33394074d 100644 --- a/app/controllers/account_follow_controller.rb +++ b/app/controllers/account_follow_controller.rb @@ -6,7 +6,7 @@ class AccountFollowController < ApplicationController before_action :authenticate_user! def create - FollowService.new.call(current_user.account, @account.acct) + FollowService.new.call(current_user.account, @account, with_rate_limit: true) redirect_to account_path(@account) end end diff --git a/app/controllers/admin/site_uploads_controller.rb b/app/controllers/admin/site_uploads_controller.rb new file mode 100644 index 000000000..cacecedb0 --- /dev/null +++ b/app/controllers/admin/site_uploads_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Admin + class SiteUploadsController < BaseController + before_action :set_site_upload + + def destroy + authorize :settings, :destroy? + + @site_upload.destroy! + + redirect_to edit_admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg') + end + + private + + def set_site_upload + @site_upload = SiteUpload.find(params[:id]) + end + end +end diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 68bf425f4..153ade253 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -44,6 +44,10 @@ class Api::BaseController < ApplicationController render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503 end + rescue_from Mastodon::RateLimitExceededError do + render json: { error: I18n.t('errors.429') }, status: 429 + end + rescue_from ActionController::ParameterMissing do |e| render json: { error: e.to_s }, status: 400 end diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index e360b8a92..850702cca 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -5,8 +5,6 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController before_action :set_account after_action :insert_pagination_headers - respond_to :json - def index @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index a405b365f..830dcd8a1 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -5,8 +5,6 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController before_action :set_account after_action :insert_pagination_headers - respond_to :json - def index @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer diff --git a/app/controllers/api/v1/accounts/identity_proofs_controller.rb b/app/controllers/api/v1/accounts/identity_proofs_controller.rb index bea51ae11..8dad6fee9 100644 --- a/app/controllers/api/v1/accounts/identity_proofs_controller.rb +++ b/app/controllers/api/v1/accounts/identity_proofs_controller.rb @@ -4,8 +4,6 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController before_action :require_user! before_action :set_account - respond_to :json - def index @proofs = @account.identity_proofs.active render json: @proofs, each_serializer: REST::IdentityProofSerializer diff --git a/app/controllers/api/v1/accounts/lists_controller.rb b/app/controllers/api/v1/accounts/lists_controller.rb index 72392453c..ccb751f8f 100644 --- a/app/controllers/api/v1/accounts/lists_controller.rb +++ b/app/controllers/api/v1/accounts/lists_controller.rb @@ -5,8 +5,6 @@ class Api::V1::Accounts::ListsController < Api::BaseController before_action :require_user! before_action :set_account - respond_to :json - def index @lists = @account.lists.where(account: current_account) render json: @lists, each_serializer: REST::ListSerializer diff --git a/app/controllers/api/v1/accounts/pins_controller.rb b/app/controllers/api/v1/accounts/pins_controller.rb index 0a0239c42..3915b5669 100644 --- a/app/controllers/api/v1/accounts/pins_controller.rb +++ b/app/controllers/api/v1/accounts/pins_controller.rb @@ -7,8 +7,6 @@ class Api::V1::Accounts::PinsController < Api::BaseController before_action :require_user! before_action :set_account - respond_to :json - def create AccountPin.create!(account: current_account, target_account: @account) render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index ab8a0461f..1d3992a28 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -4,8 +4,6 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController before_action -> { doorkeeper_authorize! :read, :'read:follows' } before_action :require_user! - respond_to :json - def index accounts = Account.where(id: account_ids).select('id') # .where doesn't guarantee that our results are in the same order diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb index 4217b527a..3061fcb7e 100644 --- a/app/controllers/api/v1/accounts/search_controller.rb +++ b/app/controllers/api/v1/accounts/search_controller.rb @@ -4,8 +4,6 @@ class Api::V1::Accounts::SearchController < Api::BaseController before_action -> { doorkeeper_authorize! :read, :'read:accounts' } before_action :require_user! - respond_to :json - def show @accounts = account_search render json: @accounts, each_serializer: REST::AccountSerializer diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 333db9618..114ee0a82 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -6,8 +6,6 @@ class Api::V1::Accounts::StatusesController < Api::BaseController after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) } - respond_to :json - def index @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index d68d2715f..0080faf33 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -14,7 +14,7 @@ class Api::V1::AccountsController < Api::BaseController skip_before_action :require_authenticated_user!, only: :create - respond_to :json + override_rate_limit_headers :follow, family: :follows def show render json: @account, serializer: REST::AccountSerializer @@ -31,7 +31,7 @@ class Api::V1::AccountsController < Api::BaseController end def follow - FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs)) + FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs), with_rate_limit: true) options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } } diff --git a/app/controllers/api/v1/apps/credentials_controller.rb b/app/controllers/api/v1/apps/credentials_controller.rb index 8b63d0490..0475b2d4a 100644 --- a/app/controllers/api/v1/apps/credentials_controller.rb +++ b/app/controllers/api/v1/apps/credentials_controller.rb @@ -3,8 +3,6 @@ class Api::V1::Apps::CredentialsController < Api::BaseController before_action -> { doorkeeper_authorize! :read } - respond_to :json - def show render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key) end diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb index 4cff04cad..a2baeef90 100644 --- a/app/controllers/api/v1/blocks_controller.rb +++ b/app/controllers/api/v1/blocks_controller.rb @@ -5,8 +5,6 @@ class Api::V1::BlocksController < Api::BaseController before_action :require_user! after_action :insert_pagination_headers - respond_to :json - def index @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer diff --git a/app/controllers/api/v1/bookmarks_controller.rb b/app/controllers/api/v1/bookmarks_controller.rb index e1b244e76..c15212f0a 100644 --- a/app/controllers/api/v1/bookmarks_controller.rb +++ b/app/controllers/api/v1/bookmarks_controller.rb @@ -5,8 +5,6 @@ class Api::V1::BookmarksController < Api::BaseController before_action :require_user! after_action :insert_pagination_headers - respond_to :json - def index @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb index b19f27ebf..bc8013379 100644 --- a/app/controllers/api/v1/conversations_controller.rb +++ b/app/controllers/api/v1/conversations_controller.rb @@ -9,8 +9,6 @@ class Api::V1::ConversationsController < Api::BaseController before_action :set_conversation, except: :index after_action :insert_pagination_headers, only: :index - respond_to :json - def index @conversations = paginated_conversations render json: @conversations, each_serializer: REST::ConversationSerializer diff --git a/app/controllers/api/v1/custom_emojis_controller.rb b/app/controllers/api/v1/custom_emojis_controller.rb index 4e6d5d7c6..08b3474cc 100644 --- a/app/controllers/api/v1/custom_emojis_controller.rb +++ b/app/controllers/api/v1/custom_emojis_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::CustomEmojisController < Api::BaseController - respond_to :json - skip_before_action :set_cache_headers def index diff --git a/app/controllers/api/v1/domain_blocks_controller.rb b/app/controllers/api/v1/domain_blocks_controller.rb index af9e7a20f..5bb02d834 100644 --- a/app/controllers/api/v1/domain_blocks_controller.rb +++ b/app/controllers/api/v1/domain_blocks_controller.rb @@ -8,8 +8,6 @@ class Api::V1::DomainBlocksController < Api::BaseController before_action :require_user! after_action :insert_pagination_headers, only: :show - respond_to :json - def show @blocks = load_domain_blocks render json: @blocks.map(&:domain) diff --git a/app/controllers/api/v1/endorsements_controller.rb b/app/controllers/api/v1/endorsements_controller.rb index 2770c7aef..c87dbc4ce 100644 --- a/app/controllers/api/v1/endorsements_controller.rb +++ b/app/controllers/api/v1/endorsements_controller.rb @@ -5,8 +5,6 @@ class Api::V1::EndorsementsController < Api::BaseController before_action :require_user! after_action :insert_pagination_headers - respond_to :json - def index @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb index db827f9d4..3e242905d 100644 --- a/app/controllers/api/v1/favourites_controller.rb +++ b/app/controllers/api/v1/favourites_controller.rb @@ -5,8 +5,6 @@ class Api::V1::FavouritesController < Api::BaseController before_action :require_user! after_action :insert_pagination_headers - respond_to :json - def index @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) diff --git a/app/controllers/api/v1/featured_tags/suggestions_controller.rb b/app/controllers/api/v1/featured_tags/suggestions_controller.rb index fb27ef88b..8c1b81a0f 100644 --- a/app/controllers/api/v1/featured_tags/suggestions_controller.rb +++ b/app/controllers/api/v1/featured_tags/suggestions_controller.rb @@ -2,12 +2,9 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index - before_action :require_user! before_action :set_most_used_tags, only: :index - respond_to :json - def index render json: @most_used_tags, each_serializer: REST::TagSerializer end diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb index e5ebaff4d..b0ace3af0 100644 --- a/app/controllers/api/v1/filters_controller.rb +++ b/app/controllers/api/v1/filters_controller.rb @@ -7,8 +7,6 @@ class Api::V1::FiltersController < Api::BaseController before_action :set_filters, only: :index before_action :set_filter, only: [:show, :update, :destroy] - respond_to :json - def index render json: @filters, each_serializer: REST::FilterSerializer end diff --git a/app/controllers/api/v1/instances/activity_controller.rb b/app/controllers/api/v1/instances/activity_controller.rb index b30e8464c..4f6b4bcbf 100644 --- a/app/controllers/api/v1/instances/activity_controller.rb +++ b/app/controllers/api/v1/instances/activity_controller.rb @@ -6,8 +6,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController skip_before_action :set_cache_headers skip_before_action :require_authenticated_user!, unless: :whitelist_mode? - respond_to :json - def show expires_in 1.day, public: true render_with_cache json: :activity, expires_in: 1.day diff --git a/app/controllers/api/v1/instances/peers_controller.rb b/app/controllers/api/v1/instances/peers_controller.rb index cc00d8a6b..9fa440935 100644 --- a/app/controllers/api/v1/instances/peers_controller.rb +++ b/app/controllers/api/v1/instances/peers_controller.rb @@ -6,8 +6,6 @@ class Api::V1::Instances::PeersController < Api::BaseController skip_before_action :set_cache_headers skip_before_action :require_authenticated_user!, unless: :whitelist_mode? - respond_to :json - def index expires_in 1.day, public: true render_with_cache(expires_in: 1.day) { Account.remote.domains } diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index c323b60b4..5b5058a7b 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::InstancesController < Api::BaseController - respond_to :json - skip_before_action :set_cache_headers skip_before_action :require_authenticated_user!, unless: :whitelist_mode? diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index 81825db15..d87d7b946 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -4,8 +4,6 @@ class Api::V1::MediaController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:media' } before_action :require_user! - respond_to :json - def create @media = current_account.media_attachments.create!(media_params) render json: @media, serializer: REST::MediaAttachmentSerializer diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb index 3b3a39943..5dc047b43 100644 --- a/app/controllers/api/v1/mutes_controller.rb +++ b/app/controllers/api/v1/mutes_controller.rb @@ -5,8 +5,6 @@ class Api::V1::MutesController < Api::BaseController before_action :require_user! after_action :insert_pagination_headers - respond_to :json - def index @data = @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index c91753ae7..9dce9b807 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -6,8 +6,6 @@ class Api::V1::NotificationsController < Api::BaseController before_action :require_user! after_action :insert_pagination_headers, only: :index - respond_to :json - DEFAULT_NOTIFICATIONS_LIMIT = 15 def index diff --git a/app/controllers/api/v1/polls/votes_controller.rb b/app/controllers/api/v1/polls/votes_controller.rb index 3fa0b6a76..e1d26106a 100644 --- a/app/controllers/api/v1/polls/votes_controller.rb +++ b/app/controllers/api/v1/polls/votes_controller.rb @@ -7,8 +7,6 @@ class Api::V1::Polls::VotesController < Api::BaseController before_action :require_user! before_action :set_poll - respond_to :json - def create VoteService.new.call(current_account, @poll, vote_params[:choices]) render json: @poll, serializer: REST::PollSerializer diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb index 031e6d42d..744baf7bb 100644 --- a/app/controllers/api/v1/polls_controller.rb +++ b/app/controllers/api/v1/polls_controller.rb @@ -7,8 +7,6 @@ class Api::V1::PollsController < Api::BaseController before_action :set_poll before_action :refresh_poll - respond_to :json - def show render json: @poll, serializer: REST::PollSerializer, include_results: true end diff --git a/app/controllers/api/v1/preferences_controller.rb b/app/controllers/api/v1/preferences_controller.rb index 077d39f5d..1640a8224 100644 --- a/app/controllers/api/v1/preferences_controller.rb +++ b/app/controllers/api/v1/preferences_controller.rb @@ -4,8 +4,6 @@ class Api::V1::PreferencesController < Api::BaseController before_action -> { doorkeeper_authorize! :read, :'read:accounts' } before_action :require_user! - respond_to :json - def index render json: current_account, serializer: REST::PreferencesSerializer end diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index 1b0b4b05b..66c40f6f4 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -4,8 +4,6 @@ class Api::V1::ReportsController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:reports' }, only: [:create] before_action :require_user! - respond_to :json - def create @report = ReportService.new.call( current_account, diff --git a/app/controllers/api/v1/statuses/bookmarks_controller.rb b/app/controllers/api/v1/statuses/bookmarks_controller.rb index a7f1eed00..3954af3c9 100644 --- a/app/controllers/api/v1/statuses/bookmarks_controller.rb +++ b/app/controllers/api/v1/statuses/bookmarks_controller.rb @@ -7,8 +7,6 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController before_action :require_user! before_action :set_status - respond_to :json - def create current_account.bookmarks.find_or_create_by!(account: current_account, status: @status) render json: @status, serializer: REST::StatusSerializer diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index 05f4acc33..8229786d6 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -7,8 +7,6 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController before_action :set_status after_action :insert_pagination_headers - respond_to :json - def index @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb index f18ace996..7afa822ed 100644 --- a/app/controllers/api/v1/statuses/favourites_controller.rb +++ b/app/controllers/api/v1/statuses/favourites_controller.rb @@ -7,8 +7,6 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController before_action :require_user! before_action :set_status - respond_to :json - def create FavouriteService.new.call(current_account, @status) render json: @status, serializer: REST::StatusSerializer diff --git a/app/controllers/api/v1/statuses/mutes_controller.rb b/app/controllers/api/v1/statuses/mutes_controller.rb index b02469b4f..43c7a525a 100644 --- a/app/controllers/api/v1/statuses/mutes_controller.rb +++ b/app/controllers/api/v1/statuses/mutes_controller.rb @@ -8,8 +8,6 @@ class Api::V1::Statuses::MutesController < Api::BaseController before_action :set_status before_action :set_conversation - respond_to :json - def create current_account.mute_conversation!(@conversation) @mutes_map = { @conversation.id => true } diff --git a/app/controllers/api/v1/statuses/pins_controller.rb b/app/controllers/api/v1/statuses/pins_controller.rb index 4118a8ce4..51b1621b6 100644 --- a/app/controllers/api/v1/statuses/pins_controller.rb +++ b/app/controllers/api/v1/statuses/pins_controller.rb @@ -7,8 +7,6 @@ class Api::V1::Statuses::PinsController < Api::BaseController before_action :require_user! before_action :set_status - respond_to :json - def create StatusPin.create!(account: current_account, status: @status) distribute_add_activity! diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index fa60e7d84..6c9e49d90 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -7,8 +7,6 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController before_action :set_status after_action :insert_pagination_headers - respond_to :json - def index @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index 67106ccbe..7fa774a4d 100644 --- a/app/controllers/api/v1/statuses/reblogs_controller.rb +++ b/app/controllers/api/v1/statuses/reblogs_controller.rb @@ -7,10 +7,11 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController before_action :require_user! before_action :set_reblog - respond_to :json + override_rate_limit_headers :create, family: :statuses def create @status = ReblogService.new.call(current_account, @reblog, reblog_params) + render json: @status, serializer: REST::StatusSerializer end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 486004f9c..544e8e3c9 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -8,7 +8,7 @@ class Api::V1::StatusesController < Api::BaseController before_action :require_user!, except: [:show, :context] before_action :set_status, only: [:show, :context] - respond_to :json + override_rate_limit_headers :create, family: :statuses # This API was originally unlimited, pagination cannot be introduced without # breaking backwards-compatibility. Arbitrarily high number to cover most @@ -45,7 +45,8 @@ class Api::V1::StatusesController < Api::BaseController application: doorkeeper_token.application, poll: status_params[:poll], content_type: status_params[:content_type], - idempotency: request.headers['Idempotency-Key']) + idempotency: request.headers['Idempotency-Key'], + with_rate_limit: true) render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer end diff --git a/app/controllers/api/v1/streaming_controller.rb b/app/controllers/api/v1/streaming_controller.rb index ebb17608c..7cd60615a 100644 --- a/app/controllers/api/v1/streaming_controller.rb +++ b/app/controllers/api/v1/streaming_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::V1::StreamingController < Api::BaseController - respond_to :json - def index if Rails.configuration.x.streaming_api_base_url != request.host redirect_to streaming_api_url, status: 301 diff --git a/app/controllers/api/v1/suggestions_controller.rb b/app/controllers/api/v1/suggestions_controller.rb index 9da2b60ae..52054160d 100644 --- a/app/controllers/api/v1/suggestions_controller.rb +++ b/app/controllers/api/v1/suggestions_controller.rb @@ -7,8 +7,6 @@ class Api::V1::SuggestionsController < Api::BaseController before_action :require_user! before_action :set_accounts - respond_to :json - def index render json: @accounts, each_serializer: REST::AccountSerializer end diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb index ff5ede138..ae6dbcb8b 100644 --- a/app/controllers/api/v1/timelines/home_controller.rb +++ b/app/controllers/api/v1/timelines/home_controller.rb @@ -5,8 +5,6 @@ class Api::V1::Timelines::HomeController < Api::BaseController before_action :require_user!, only: [:show] after_action :insert_pagination_headers, unless: -> { @statuses.empty? } - respond_to :json - def show @statuses = load_statuses diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index ccc10f966..581befef1 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -4,8 +4,6 @@ class Api::V1::Timelines::PublicController < Api::BaseController before_action :require_user!, only: [:show], if: :require_auth? after_action :insert_pagination_headers, unless: -> { @statuses.empty? } - respond_to :json - def show @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index 9adc4ad29..2d6ad5a80 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -4,8 +4,6 @@ class Api::V1::Timelines::TagController < Api::BaseController before_action :load_tag after_action :insert_pagination_headers, unless: -> { @statuses.empty? } - respond_to :json - def show @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) diff --git a/app/controllers/api/v1/trends_controller.rb b/app/controllers/api/v1/trends_controller.rb index bcea9857e..c875e9041 100644 --- a/app/controllers/api/v1/trends_controller.rb +++ b/app/controllers/api/v1/trends_controller.rb @@ -3,8 +3,6 @@ class Api::V1::TrendsController < Api::BaseController before_action :set_tags - respond_to :json - def index render json: @tags, each_serializer: REST::TagSerializer end diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index 76decdb25..ddcf92200 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -8,8 +8,6 @@ class Api::V2::SearchController < Api::BaseController before_action -> { doorkeeper_authorize! :read, :'read:search' } before_action :require_user! - respond_to :json - def index @search = Search.new(search_results) render json: @search, serializer: REST::SearchSerializer diff --git a/app/controllers/api/web/embeds_controller.rb b/app/controllers/api/web/embeds_controller.rb index 4aa31695c..741ba910f 100644 --- a/app/controllers/api/web/embeds_controller.rb +++ b/app/controllers/api/web/embeds_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::Web::EmbedsController < Api::Web::BaseController - respond_to :json - before_action :require_user! def create diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb index f388b17e5..7916b82fa 100644 --- a/app/controllers/api/web/push_subscriptions_controller.rb +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::Web::PushSubscriptionsController < Api::Web::BaseController - respond_to :json - before_action :require_user! def create diff --git a/app/controllers/api/web/settings_controller.rb b/app/controllers/api/web/settings_controller.rb index e3178bf48..3d65e46ed 100644 --- a/app/controllers/api/web/settings_controller.rb +++ b/app/controllers/api/web/settings_controller.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class Api::Web::SettingsController < Api::Web::BaseController - respond_to :json - before_action :require_user! def update diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c882d40ab..63d9f91fb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -30,6 +30,7 @@ class ApplicationController < ActionController::Base rescue_from Mastodon::NotPermittedError, with: :forbidden rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error rescue_from Mastodon::RaceConditionError, with: :service_unavailable + rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? before_action :require_functional!, if: :user_signed_in? @@ -181,6 +182,10 @@ class ApplicationController < ActionController::Base respond_with_error(503) end + def too_many_requests + respond_with_error(429) + end + def single_user_mode? @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists? end diff --git a/app/controllers/authorize_interactions_controller.rb b/app/controllers/authorize_interactions_controller.rb index 20b3fa94b..f0bcac75b 100644 --- a/app/controllers/authorize_interactions_controller.rb +++ b/app/controllers/authorize_interactions_controller.rb @@ -21,7 +21,7 @@ class AuthorizeInteractionsController < ApplicationController end def create - if @resource.is_a?(Account) && FollowService.new.call(current_account, @resource) + if @resource.is_a?(Account) && FollowService.new.call(current_account, @resource, with_rate_limit: true) render :success else render :error diff --git a/app/controllers/concerns/rate_limit_headers.rb b/app/controllers/concerns/rate_limit_headers.rb index b79c558d8..86fe58a71 100644 --- a/app/controllers/concerns/rate_limit_headers.rb +++ b/app/controllers/concerns/rate_limit_headers.rb @@ -3,6 +3,20 @@ module RateLimitHeaders extend ActiveSupport::Concern + class_methods do + def override_rate_limit_headers(method_name, options = {}) + around_action(only: method_name, if: :current_account) do |_controller, block| + begin + block.call + ensure + rate_limiter = RateLimiter.new(current_account, options) + rate_limit_headers = rate_limiter.to_headers + response.headers.merge!(rate_limit_headers) unless response.headers['X-RateLimit-Remaining'].present? && rate_limit_headers['X-RateLimit-Remaining'].to_i > response.headers['X-RateLimit-Remaining'].to_i + end + end + end + end + included do before_action :set_rate_limit_headers, if: :rate_limited_request? end @@ -44,7 +58,7 @@ module RateLimitHeaders end def api_throttle_data - most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_, v| v[:limit] } + most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_, v| v[:limit] - v[:count] } request.env['rack.attack.throttle_data'][most_limited_type] end diff --git a/app/helpers/admin/settings_helper.rb b/app/helpers/admin/settings_helper.rb new file mode 100644 index 000000000..baf14ab25 --- /dev/null +++ b/app/helpers/admin/settings_helper.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Admin::SettingsHelper + def site_upload_delete_hint(hint, var) + upload = SiteUpload.find_by(var: var.to_s) + return hint unless upload + + link = link_to t('admin.site_uploads.delete'), admin_site_upload_path(upload), data: { method: :delete } + safe_join([hint, link], '<br/>'.html_safe) + end +end diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index d4a824e2c..4af36e998 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -106,7 +106,7 @@ export function fetchAccount(id) { dispatch, getState, db.transaction('accounts', 'read').objectStore('accounts').index('id'), - id + id, ).then(() => db.close(), error => { db.close(); throw error; diff --git a/app/javascript/mastodon/components/intersection_observer_article.js b/app/javascript/mastodon/components/intersection_observer_article.js index e453730ba..124b34b02 100644 --- a/app/javascript/mastodon/components/intersection_observer_article.js +++ b/app/javascript/mastodon/components/intersection_observer_article.js @@ -44,7 +44,7 @@ export default class IntersectionObserverArticle extends React.Component { intersectionObserverWrapper.observe( id, this.node, - this.handleIntersection + this.handleIntersection, ); this.componentMounted = true; diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index cfe164a50..283d7e0a5 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -10,7 +10,7 @@ import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_s import { decode } from 'blurhash'; const messages = defineMessages({ - toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }, + toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Hide media' }, }); class Item extends React.PureComponent { diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index e2c8d43c9..bebbbcb5a 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -36,8 +36,8 @@ const messages = defineMessages({ admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, - blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, - unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, + blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' }, + unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, }); diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 8bd7f2db5..35cc3952f 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -29,8 +29,8 @@ const messages = defineMessages({ report: { id: 'account.report', defaultMessage: 'Report @{name}' }, share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' }, media: { id: 'account.media', defaultMessage: 'Media' }, - blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, - unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, + blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' }, + unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' }, showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' }, diff --git a/app/javascript/mastodon/features/audio/index.js b/app/javascript/mastodon/features/audio/index.js index fda5a074f..95c9c7751 100644 --- a/app/javascript/mastodon/features/audio/index.js +++ b/app/javascript/mastodon/features/audio/index.js @@ -214,8 +214,8 @@ class Audio extends React.PureComponent { <div className='video-player__controls active'> <div className='video-player__buttons-bar'> <div className='video-player__buttons left'> - <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> - <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> + <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> + <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> @@ -236,7 +236,7 @@ class Audio extends React.PureComponent { </div> <div className='video-player__buttons right'> - <button type='button' aria-label={intl.formatMessage(messages.download)}> + <button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)}> <a className='video-player__download__icon' href={this.props.src} download> <Icon id={'download'} fixedWidth /> </a> diff --git a/app/javascript/mastodon/features/blocks/index.js b/app/javascript/mastodon/features/blocks/index.js index 051431ed2..870c0de09 100644 --- a/app/javascript/mastodon/features/blocks/index.js +++ b/app/javascript/mastodon/features/blocks/index.js @@ -68,7 +68,7 @@ class Blocks extends ImmutablePureComponent { bindToDocument={!multiColumn} > {accountIds.map(id => - <AccountContainer key={id} id={id} /> + <AccountContainer key={id} id={id} />, )} </ScrollableList> </Column> diff --git a/app/javascript/mastodon/features/compose/components/poll_form.js b/app/javascript/mastodon/features/compose/components/poll_form.js index cac3776bb..cc740def2 100644 --- a/app/javascript/mastodon/features/compose/components/poll_form.js +++ b/app/javascript/mastodon/features/compose/components/poll_form.js @@ -155,6 +155,7 @@ class PollForm extends ImmutablePureComponent { <div className='poll__footer'> <button disabled={options.size >= 5} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button> + {/* eslint-disable-next-line jsx-a11y/no-onchange */} <select value={expiresIn} onChange={this.handleSelectDuration}> <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option> <option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option> diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js index 7cbfe463a..de030b7a2 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -11,13 +11,13 @@ import Icon from 'mastodon/components/icon'; const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, - public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' }, + public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all, shown in public timelines' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' }, + unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but not in public timelines' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, - private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' }, + private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, - direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' }, + direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' }, change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, }); diff --git a/app/javascript/mastodon/features/domain_blocks/index.js b/app/javascript/mastodon/features/domain_blocks/index.js index 482245c86..06533d5ac 100644 --- a/app/javascript/mastodon/features/domain_blocks/index.js +++ b/app/javascript/mastodon/features/domain_blocks/index.js @@ -69,7 +69,7 @@ class Blocks extends ImmutablePureComponent { bindToDocument={!multiColumn} > {domains.map(domain => - <DomainContainer key={domain} domain={domain} /> + <DomainContainer key={domain} domain={domain} />, )} </ScrollableList> </Column> diff --git a/app/javascript/mastodon/features/favourites/index.js b/app/javascript/mastodon/features/favourites/index.js index 249e6a044..75cb00c0e 100644 --- a/app/javascript/mastodon/features/favourites/index.js +++ b/app/javascript/mastodon/features/favourites/index.js @@ -79,7 +79,7 @@ class Favourites extends ImmutablePureComponent { bindToDocument={!multiColumn} > {accountIds.map(id => - <AccountContainer key={id} id={id} withNote={false} /> + <AccountContainer key={id} id={id} withNote={false} />, )} </ScrollableList> </Column> diff --git a/app/javascript/mastodon/features/follow_requests/index.js b/app/javascript/mastodon/features/follow_requests/index.js index 57ef44145..bef56fab5 100644 --- a/app/javascript/mastodon/features/follow_requests/index.js +++ b/app/javascript/mastodon/features/follow_requests/index.js @@ -68,7 +68,7 @@ class FollowRequests extends ImmutablePureComponent { bindToDocument={!multiColumn} > {accountIds.map(id => - <AccountAuthorizeContainer key={id} id={id} /> + <AccountAuthorizeContainer key={id} id={id} />, )} </ScrollableList> </Column> diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js index 9e635d250..f8723e055 100644 --- a/app/javascript/mastodon/features/followers/index.js +++ b/app/javascript/mastodon/features/followers/index.js @@ -93,7 +93,7 @@ class Followers extends ImmutablePureComponent { bindToDocument={!multiColumn} > {blockedBy ? [] : accountIds.map(id => - <AccountContainer key={id} id={id} withNote={false} /> + <AccountContainer key={id} id={id} withNote={false} />, )} </ScrollableList> </Column> diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js index 284ae2c11..5112bfa9d 100644 --- a/app/javascript/mastodon/features/following/index.js +++ b/app/javascript/mastodon/features/following/index.js @@ -93,7 +93,7 @@ class Following extends ImmutablePureComponent { bindToDocument={!multiColumn} > {blockedBy ? [] : accountIds.map(id => - <AccountContainer key={id} id={id} withNote={false} /> + <AccountContainer key={id} id={id} withNote={false} />, )} </ScrollableList> </Column> diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.js b/app/javascript/mastodon/features/getting_started/components/announcements.js index 91cf6215e..fb4a6bf0d 100644 --- a/app/javascript/mastodon/features/getting_started/components/announcements.js +++ b/app/javascript/mastodon/features/getting_started/components/announcements.js @@ -95,6 +95,10 @@ class Content extends ImmutablePureComponent { } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); } else { + let status = this.props.announcement.get('statuses').find(item => link.href === item.get('url')); + if (status) { + link.addEventListener('click', this.onStatusClick.bind(this, status), false); + } link.setAttribute('title', link.href); link.classList.add('unhandled-link'); } @@ -120,6 +124,13 @@ class Content extends ImmutablePureComponent { } } + onStatusClick = (status, e) => { + if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.context.router.history.push(`/statuses/${status.get('id')}`); + } + } + handleEmojiMouseEnter = ({ target }) => { target.src = target.getAttribute('data-original'); } diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index adbc147d1..d9838e1c7 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -106,20 +106,20 @@ class GettingStarted extends ImmutablePureComponent { if (profile_directory) { navItems.push( - <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' /> + <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />, ); height += 48; } navItems.push( - <ColumnSubheading key={i++} text={intl.formatMessage(messages.personal)} /> + <ColumnSubheading key={i++} text={intl.formatMessage(messages.personal)} />, ); height += 34; } else if (profile_directory) { navItems.push( - <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' /> + <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />, ); height += 48; @@ -129,7 +129,7 @@ class GettingStarted extends ImmutablePureComponent { <ColumnLink key={i++} icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />, <ColumnLink key={i++} icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />, <ColumnLink key={i++} icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />, - <ColumnLink key={i++} icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' /> + <ColumnLink key={i++} icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />, ); height += 48*4; diff --git a/app/javascript/mastodon/features/lists/index.js b/app/javascript/mastodon/features/lists/index.js index 7f7f5009c..ca1fa1f5e 100644 --- a/app/javascript/mastodon/features/lists/index.js +++ b/app/javascript/mastodon/features/lists/index.js @@ -74,7 +74,7 @@ class Lists extends ImmutablePureComponent { bindToDocument={!multiColumn} > {lists.map(list => - <ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} /> + <ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />, )} </ScrollableList> </Column> diff --git a/app/javascript/mastodon/features/mutes/index.js b/app/javascript/mastodon/features/mutes/index.js index 91dd268c1..3f58a62d2 100644 --- a/app/javascript/mastodon/features/mutes/index.js +++ b/app/javascript/mastodon/features/mutes/index.js @@ -68,7 +68,7 @@ class Mutes extends ImmutablePureComponent { bindToDocument={!multiColumn} > {accountIds.map(id => - <AccountContainer key={id} id={id} /> + <AccountContainer key={id} id={id} />, )} </ScrollableList> </Column> diff --git a/app/javascript/mastodon/features/reblogs/index.js b/app/javascript/mastodon/features/reblogs/index.js index 9179e51db..4becb5fb7 100644 --- a/app/javascript/mastodon/features/reblogs/index.js +++ b/app/javascript/mastodon/features/reblogs/index.js @@ -79,7 +79,7 @@ class Reblogs extends ImmutablePureComponent { bindToDocument={!multiColumn} > {accountIds.map(id => - <AccountContainer key={id} id={id} withNote={false} /> + <AccountContainer key={id} id={id} withNote={false} />, )} </ScrollableList> </Column> diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index 959774da4..ba62d7b10 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -32,8 +32,8 @@ const messages = defineMessages({ admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, - blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, - unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, + blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' }, + unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, }); diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js index 2993fe29a..b8344a667 100644 --- a/app/javascript/mastodon/features/status/components/card.js +++ b/app/javascript/mastodon/features/status/components/card.js @@ -98,7 +98,7 @@ export default class Card extends React.PureComponent { }, }, ]), - 0 + 0, ); }; diff --git a/app/javascript/mastodon/features/ui/components/__tests__/column-test.js b/app/javascript/mastodon/features/ui/components/__tests__/column-test.js index 11cc1b6e8..89cb2458d 100644 --- a/app/javascript/mastodon/features/ui/components/__tests__/column-test.js +++ b/app/javascript/mastodon/features/ui/components/__tests__/column-test.js @@ -19,7 +19,7 @@ describe('<Column />', () => { const wrapper = mount( <Column heading='notifications'> <div className='scrollable' /> - </Column> + </Column>, ); wrapper.find(ColumnHeader).find('button').simulate('click'); expect(global.requestAnimationFrame.mock.calls.length).toEqual(1); diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index 8ac9c8db7..42ded9d21 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -492,8 +492,8 @@ class Video extends React.PureComponent { <div className='video-player__buttons-bar'> <div className='video-player__buttons left'> - <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> - <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> + <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> + <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> @@ -517,11 +517,11 @@ class Video extends React.PureComponent { </div> <div className='video-player__buttons right'> - {(!onCloseVideo && !editable && !fullscreen) && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>} - {(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>} - {onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>} - <button type='button' aria-label={intl.formatMessage(messages.download)}><a className='video-player__download__icon' href={this.props.src} download><Icon id={'download'} fixedWidth /></a></button> - <button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button> + {(!onCloseVideo && !editable && !fullscreen) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>} + {(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>} + {onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>} + <button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)}><a className='video-player__download__icon' href={this.props.src} download><Icon id={'download'} fixedWidth /></a></button> + <button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button> </div> </div> </div> diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index 5429f358f..c1a7bb533 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -254,7 +254,7 @@ "lists.subheading": "Your lists", "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Зареждане...", - "media_gallery.toggle_visible": "Toggle visibility", + "media_gallery.toggle_visible": "Hide media", "missing_indicator.label": "Not found", "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json index 96092457f..b6bfa6648 100644 --- a/app/javascript/mastodon/locales/br.json +++ b/app/javascript/mastodon/locales/br.json @@ -254,7 +254,7 @@ "lists.subheading": "Your lists", "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", - "media_gallery.toggle_visible": "Toggle visibility", + "media_gallery.toggle_visible": "Hide media", "missing_indicator.label": "Not found", "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 18692bc44..93cfa2431 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -142,7 +142,7 @@ { "descriptors": [ { - "defaultMessage": "Unhide {domain}", + "defaultMessage": "Unblock domain {domain}", "id": "account.unblock_domain" } ], @@ -217,7 +217,7 @@ { "descriptors": [ { - "defaultMessage": "Toggle visibility", + "defaultMessage": "Hide media", "id": "media_gallery.toggle_visible" }, { @@ -451,11 +451,11 @@ "id": "status.copy" }, { - "defaultMessage": "Hide everything from {domain}", + "defaultMessage": "Block domain {domain}", "id": "account.block_domain" }, { - "defaultMessage": "Unhide {domain}", + "defaultMessage": "Unblock domain {domain}", "id": "account.unblock_domain" }, { @@ -697,11 +697,11 @@ "id": "account.media" }, { - "defaultMessage": "Hide everything from {domain}", + "defaultMessage": "Block domain {domain}", "id": "account.block_domain" }, { - "defaultMessage": "Unhide {domain}", + "defaultMessage": "Unblock domain {domain}", "id": "account.unblock_domain" }, { @@ -1073,7 +1073,7 @@ "id": "privacy.public.short" }, { - "defaultMessage": "Post to public timelines", + "defaultMessage": "Visible for all, shown in public timelines", "id": "privacy.public.long" }, { @@ -1081,7 +1081,7 @@ "id": "privacy.unlisted.short" }, { - "defaultMessage": "Do not show in public timelines", + "defaultMessage": "Visible for all, but not in public timelines", "id": "privacy.unlisted.long" }, { @@ -1089,7 +1089,7 @@ "id": "privacy.private.short" }, { - "defaultMessage": "Post to followers only", + "defaultMessage": "Visible for followers only", "id": "privacy.private.long" }, { @@ -1097,7 +1097,7 @@ "id": "privacy.direct.short" }, { - "defaultMessage": "Post to mentioned users only", + "defaultMessage": "Visible for mentioned users only", "id": "privacy.direct.long" }, { @@ -1470,7 +1470,7 @@ "id": "column.domain_blocks" }, { - "defaultMessage": "Unhide {domain}", + "defaultMessage": "Unblock domain {domain}", "id": "account.unblock_domain" }, { @@ -2384,11 +2384,11 @@ "id": "status.copy" }, { - "defaultMessage": "Hide everything from {domain}", + "defaultMessage": "Block domain {domain}", "id": "account.block_domain" }, { - "defaultMessage": "Unhide {domain}", + "defaultMessage": "Unblock domain {domain}", "id": "account.unblock_domain" }, { @@ -2957,4 +2957,4 @@ ], "path": "app/javascript/mastodon/features/video/index.json" } -] \ No newline at end of file +] diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index e25199905..3ec951903 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -3,7 +3,7 @@ "account.badges.bot": "Bot", "account.badges.group": "Group", "account.block": "Block @{name}", - "account.block_domain": "Hide everything from {domain}", + "account.block_domain": "Block domain {domain}", "account.blocked": "Blocked", "account.cancel_follow_request": "Cancel follow request", "account.direct": "Direct message @{name}", @@ -34,7 +34,7 @@ "account.share": "Share @{name}'s profile", "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Unblock @{name}", - "account.unblock_domain": "Unhide {domain}", + "account.unblock_domain": "Unblock domain {domain}", "account.unendorse": "Don't feature on profile", "account.unfollow": "Unfollow", "account.unmute": "Unmute @{name}", @@ -258,7 +258,7 @@ "lists.subheading": "Your lists", "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", - "media_gallery.toggle_visible": "Toggle visibility", + "media_gallery.toggle_visible": "Hide media", "missing_indicator.label": "Not found", "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", @@ -324,13 +324,13 @@ "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "Adjust status privacy", - "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.long": "Visible for mentioned users only", "privacy.direct.short": "Direct", - "privacy.private.long": "Post to followers only", + "privacy.private.long": "Visible for followers only", "privacy.private.short": "Followers-only", - "privacy.public.long": "Post to public timelines", + "privacy.public.long": "Visible for all, shown in public timelines", "privacy.public.short": "Public", - "privacy.unlisted.long": "Do not post to public timelines", + "privacy.unlisted.long": "Visible for all, but not in public timelines", "privacy.unlisted.short": "Unlisted", "refresh": "Refresh", "regeneration_indicator.label": "Loading…", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 0e9c9d6d1..b69ec2b95 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -254,7 +254,7 @@ "lists.subheading": "Your lists", "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", - "media_gallery.toggle_visible": "Toggle visibility", + "media_gallery.toggle_visible": "Hide media", "missing_indicator.label": "Not found", "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", diff --git a/app/javascript/mastodon/locales/hi.json b/app/javascript/mastodon/locales/hi.json index d4659bc2e..ee9d701b0 100644 --- a/app/javascript/mastodon/locales/hi.json +++ b/app/javascript/mastodon/locales/hi.json @@ -254,7 +254,7 @@ "lists.subheading": "Your lists", "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "लोड हो रहा है...", - "media_gallery.toggle_visible": "Toggle visibility", + "media_gallery.toggle_visible": "Hide media", "missing_indicator.label": "नहीं मिला", "missing_indicator.sublabel": "यह संसाधन नहीं मिल सका।", "mute_modal.hide_notifications": "Hide notifications from this user?", diff --git a/app/javascript/mastodon/locales/kn.json b/app/javascript/mastodon/locales/kn.json index 5385de456..eafb7ede7 100644 --- a/app/javascript/mastodon/locales/kn.json +++ b/app/javascript/mastodon/locales/kn.json @@ -254,7 +254,7 @@ "lists.subheading": "Your lists", "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", - "media_gallery.toggle_visible": "Toggle visibility", + "media_gallery.toggle_visible": "Hide media", "missing_indicator.label": "Not found", "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index 5385de456..eafb7ede7 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -254,7 +254,7 @@ "lists.subheading": "Your lists", "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", - "media_gallery.toggle_visible": "Toggle visibility", + "media_gallery.toggle_visible": "Hide media", "missing_indicator.label": "Not found", "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index 1b5b7cf5e..82ec3b8e8 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -254,7 +254,7 @@ "lists.subheading": "Your lists", "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", - "media_gallery.toggle_visible": "Toggle visibility", + "media_gallery.toggle_visible": "Hide media", "missing_indicator.label": "Not found", "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", diff --git a/app/javascript/mastodon/locales/mk.json b/app/javascript/mastodon/locales/mk.json index 4a1f736cf..ee8b13ace 100644 --- a/app/javascript/mastodon/locales/mk.json +++ b/app/javascript/mastodon/locales/mk.json @@ -254,7 +254,7 @@ "lists.subheading": "Your lists", "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", - "media_gallery.toggle_visible": "Toggle visibility", + "media_gallery.toggle_visible": "Hide media", "missing_indicator.label": "Not found", "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", diff --git a/app/javascript/mastodon/locales/ml.json b/app/javascript/mastodon/locales/ml.json index 58bb9b723..b3a06d6ed 100644 --- a/app/javascript/mastodon/locales/ml.json +++ b/app/javascript/mastodon/locales/ml.json @@ -254,7 +254,7 @@ "lists.subheading": "Your lists", "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", - "media_gallery.toggle_visible": "Toggle visibility", + "media_gallery.toggle_visible": "Hide media", "missing_indicator.label": "Not found", "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", diff --git a/app/javascript/mastodon/locales/mr.json b/app/javascript/mastodon/locales/mr.json index 8c0fed9b0..be5d9a396 100644 --- a/app/javascript/mastodon/locales/mr.json +++ b/app/javascript/mastodon/locales/mr.json @@ -254,7 +254,7 @@ "lists.subheading": "Your lists", "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", - "media_gallery.toggle_visible": "Toggle visibility", + "media_gallery.toggle_visible": "Hide media", "missing_indicator.label": "Not found", "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json index 0d23e0d2c..19f3ca257 100644 --- a/app/javascript/mastodon/locales/ms.json +++ b/app/javascript/mastodon/locales/ms.json @@ -254,7 +254,7 @@ "lists.subheading": "Your lists", "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", - "media_gallery.toggle_visible": "Toggle visibility", + "media_gallery.toggle_visible": "Hide media", "missing_indicator.label": "Not found", "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", diff --git a/app/javascript/mastodon/locales/ur.json b/app/javascript/mastodon/locales/ur.json index 664eebfbf..d625a88bf 100644 --- a/app/javascript/mastodon/locales/ur.json +++ b/app/javascript/mastodon/locales/ur.json @@ -254,7 +254,7 @@ "lists.subheading": "Your lists", "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", - "media_gallery.toggle_visible": "Toggle visibility", + "media_gallery.toggle_visible": "Hide media", "missing_indicator.label": "Not found", "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 60e901e39..ed1ba0272 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -72,11 +72,11 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece mutable.update(usePendingItems ? 'pendingItems' : 'items', list => { const lastIndex = 1 + list.findLastIndex( - item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')) + item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')), ); const firstIndex = 1 + list.take(lastIndex).findLastIndex( - item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0 + item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0, ); return list.take(firstIndex).concat(items, list.skip(lastIndex)); diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index 0d7222e10..63b76773d 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -54,7 +54,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is return oldIds.take(firstIndex + 1).concat( isPartial && oldIds.get(firstIndex) !== null ? newIds.unshift(null) : newIds, - oldIds.skip(lastIndex) + oldIds.skip(lastIndex), ); }); } @@ -166,7 +166,7 @@ export default function timelines(state = initialState, action) { return state.update( action.timeline, initialTimeline, - map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) + map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items), ); default: return state; diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index 6a48f3b3f..673268c5a 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -117,7 +117,7 @@ export const makeGetStatus = () => { map.set('account', accountBase); map.set('filtered', filtered); }); - } + }, ); }; diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js index 1ab0dc0fa..958e5fc12 100644 --- a/app/javascript/mastodon/service_worker/web_push_notifications.js +++ b/app/javascript/mastodon/service_worker/web_push_notifications.js @@ -117,7 +117,7 @@ const handlePush = (event) => { badge: '/badge.png', data: { access_token, preferred_locale, url: '/web/notifications' }, }); - }) + }), ); }; diff --git a/app/javascript/mastodon/store/configureStore.js b/app/javascript/mastodon/store/configureStore.js index 7e7472841..e18af842f 100644 --- a/app/javascript/mastodon/store/configureStore.js +++ b/app/javascript/mastodon/store/configureStore.js @@ -10,6 +10,6 @@ export default function configureStore() { thunk, loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }), errorsMiddleware(), - soundsMiddleware() + soundsMiddleware(), ), window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f)); }; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index aa885e241..143384d9e 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -388,8 +388,8 @@ .emoji-picker-dropdown { position: absolute; - top: 5px; - right: 5px; + top: 0; + right: 0; } .compose-form__autosuggest-wrapper { @@ -870,6 +870,7 @@ .announcements__item__content { word-wrap: break-word; + overflow-y: auto; .emojione { width: 20px; @@ -4060,10 +4061,7 @@ a.status-card.compact:hover { .emoji-button { display: block; - font-size: 24px; - line-height: 24px; - margin-left: 2px; - width: 24px; + padding: 5px 5px 2px 2px; outline: 0; cursor: pointer; @@ -4079,7 +4077,6 @@ a.status-card.compact:hover { margin: 0; width: 22px; height: 22px; - margin-top: 2px; } &:hover, @@ -5058,12 +5055,6 @@ a.status-card.compact:hover { } .media-gallery__gifv { - &.autoplay { - .media-gallery__gifv__label { - display: none; - } - } - &:hover { .media-gallery__gifv__label { opacity: 1; @@ -6682,17 +6673,21 @@ noscript { box-sizing: border-box; width: 100%; padding: 15px; - padding-right: 15px + 18px; position: relative; font-size: 15px; line-height: 20px; word-wrap: break-word; font-weight: 400; + max-height: 50vh; + overflow: hidden; + display: flex; + flex-direction: column; &__range { display: block; font-weight: 500; margin-bottom: 10px; + padding-right: 18px; } &__unread { diff --git a/app/lib/exceptions.rb b/app/lib/exceptions.rb index 01346bfe5..3362576b0 100644 --- a/app/lib/exceptions.rb +++ b/app/lib/exceptions.rb @@ -8,6 +8,7 @@ module Mastodon class LengthValidationError < ValidationError; end class DimensionsValidationError < ValidationError; end class RaceConditionError < Error; end + class RateLimitExceededError < Error; end class UnexpectedResponseError < Error def initialize(response = nil) diff --git a/app/lib/rate_limiter.rb b/app/lib/rate_limiter.rb new file mode 100644 index 000000000..68dae9add --- /dev/null +++ b/app/lib/rate_limiter.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +class RateLimiter + include Redisable + + FAMILIES = { + follows: { + limit: 400, + period: 24.hours.freeze, + }.freeze, + + statuses: { + limit: 300, + period: 3.hours.freeze, + }.freeze, + + media: { + limit: 30, + period: 30.minutes.freeze, + }.freeze, + }.freeze + + def initialize(by, options = {}) + @by = by + @family = options[:family] + @limit = FAMILIES[@family][:limit] + @period = FAMILIES[@family][:period].to_i + end + + def record! + count = redis.get(key) + + if count.nil? + redis.set(key, 0) + redis.expire(key, (@period - (last_epoch_time % @period) + 1).to_i) + end + + raise Mastodon::RateLimitExceededError if count.present? && count.to_i >= @limit + + redis.incr(key) + end + + def rollback! + redis.decr(key) + end + + def to_headers(now = Time.now.utc) + { + 'X-RateLimit-Limit' => @limit.to_s, + 'X-RateLimit-Remaining' => (@limit - (redis.get(key) || 0).to_i).to_s, + 'X-RateLimit-Reset' => (now + (@period - now.to_i % @period)).iso8601(6), + } + end + + private + + def key + @key ||= "rate_limit:#{@by.id}:#{@family}:#{(last_epoch_time / @period).to_i}" + end + + def last_epoch_time + @last_epoch_time ||= Time.now.to_i + end +end diff --git a/app/models/account.rb b/app/models/account.rb index 0fcf897c9..73421ee5a 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -106,6 +106,7 @@ class Account < ApplicationRecord scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) } scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) } scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) } + scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) } scope :popular, -> { order('account_stats.followers_count desc') } scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) } scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) } diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb index c7bf07787..7b6012e0f 100644 --- a/app/models/account_filter.rb +++ b/app/models/account_filter.rb @@ -14,6 +14,7 @@ class AccountFilter email ip staff + order ).freeze attr_reader :params @@ -24,7 +25,7 @@ class AccountFilter end def results - scope = Account.recent.includes(:user) + scope = Account.includes(:user).reorder(nil) params.each do |key, value| scope.merge!(scope_for(key, value.to_s.strip)) if value.present? @@ -38,6 +39,7 @@ class AccountFilter def set_defaults! params['local'] = '1' if params['remote'].blank? params['active'] = '1' if params['suspended'].blank? && params['silenced'].blank? && params['pending'].blank? + params['order'] = 'recent' if params['order'].blank? end def scope_for(key, value) @@ -51,9 +53,9 @@ class AccountFilter when 'active' Account.without_suspended when 'pending' - accounts_with_users.merge User.pending + accounts_with_users.merge(User.pending) when 'disabled' - accounts_with_users.merge User.disabled + accounts_with_users.merge(User.disabled) when 'silenced' Account.silenced when 'suspended' @@ -63,16 +65,31 @@ class AccountFilter when 'display_name' Account.matches_display_name(value) when 'email' - accounts_with_users.merge User.matches_email(value) + accounts_with_users.merge(User.matches_email(value)) when 'ip' valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value)) : Account.none when 'staff' - accounts_with_users.merge User.staff + accounts_with_users.merge(User.staff) + when 'order' + order_scope(value) else raise "Unknown filter: #{key}" end end + def order_scope(value) + case value + when 'active' + params['remote'] ? Account.joins(:account_stat).by_recent_status : Account.joins(:user).by_recent_sign_in + when 'recent' + Account.recent + when 'alphabetic' + Account.alphabetic + else + raise "Unknown order: #{value}" + end + end + def accounts_with_users Account.joins(:user) end diff --git a/app/models/announcement.rb b/app/models/announcement.rb index d99502f44..f8ac4e09d 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -48,6 +48,10 @@ class Announcement < ApplicationRecord @mentions ||= Account.from_text(text) end + def statuses + @statuses ||= Status.from_text(text) + end + def tags @tags ||= Tag.find_or_create_by_names(Extractor.extract_hashtags(text)) end diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 14bcf7bb1..32fcb5397 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -87,10 +87,10 @@ module AccountInteractions has_many :announcement_mutes, dependent: :destroy end - def follow!(other_account, reblogs: nil, uri: nil) + def follow!(other_account, reblogs: nil, uri: nil, rate_limit: false) reblogs = true if reblogs.nil? - rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri) + rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit) .find_or_create_by!(target_account: other_account) rel.update!(show_reblogs: reblogs) @@ -99,6 +99,18 @@ module AccountInteractions rel end + def request_follow!(other_account, reblogs: nil, uri: nil, rate_limit: false) + reblogs = true if reblogs.nil? + + rel = follow_requests.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit) + .find_or_create_by!(target_account: other_account) + + rel.update!(show_reblogs: reblogs) + remove_potential_friendship(other_account) + + rel + end + def block!(other_account, uri: nil) remove_potential_friendship(other_account) block_relationships.create_with(uri: uri) diff --git a/app/models/concerns/rate_limitable.rb b/app/models/concerns/rate_limitable.rb new file mode 100644 index 000000000..ad1b5e44e --- /dev/null +++ b/app/models/concerns/rate_limitable.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module RateLimitable + extend ActiveSupport::Concern + + def rate_limit=(value) + @rate_limit = value + end + + def rate_limit? + @rate_limit + end + + def rate_limiter(by, options = {}) + return @rate_limiter if defined?(@rate_limiter) + + @rate_limiter = RateLimiter.new(by, options) + end + + class_methods do + def rate_limit(options = {}) + after_create do + by = public_send(options[:by]) + + if rate_limit? && by&.local? + rate_limiter(by, options).record! + @rate_limit_recorded = true + end + end + + after_rollback do + rate_limiter(public_send(options[:by]), options).rollback! if @rate_limit_recorded + end + end + end +end diff --git a/app/models/follow.rb b/app/models/follow.rb index 87fa11425..f3e48a2ed 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -15,6 +15,9 @@ class Follow < ApplicationRecord include Paginable include RelationshipCacheable + include RateLimitable + + rate_limit by: :account, family: :follows belongs_to :account belongs_to :target_account, class_name: 'Account' diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index 96ac7eaa5..3325e264c 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -15,6 +15,9 @@ class FollowRequest < ApplicationRecord include Paginable include RelationshipCacheable + include RateLimitable + + rate_limit by: :account, family: :follows belongs_to :account belongs_to :target_account, class_name: 'Account' diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 6a0b892f6..2813d9200 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -170,6 +170,7 @@ class MediaAttachment < ApplicationRecord def variant?(other_file_name) return true if file_file_name == other_file_name + return false if file_file_name.nil? formats = file.styles.values.map(&:format).compact diff --git a/app/models/status.rb b/app/models/status.rb index f4284f771..b2d3c3f3b 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -35,6 +35,9 @@ class Status < ApplicationRecord include Paginable include Cacheable include StatusThreadingConcern + include RateLimitable + + rate_limit by: :account, family: :statuses self.discard_column = :deleted_at @@ -416,6 +419,21 @@ class Status < ApplicationRecord end end + def from_text(text) + return [] if text.blank? + + text.scan(FetchLinkCardService::URL_PATTERN).map(&:first).uniq.map do |url| + status = begin + if TagManager.instance.local_url?(url) + ActivityPub::TagManager.instance.uri_to_resource(url, Status) + else + Status.find_by(uri: url) || Status.find_by(url: url) + end + end + status&.distributable? ? status : nil + end.compact + end + private def timeline_scope(local_only = false) diff --git a/app/policies/settings_policy.rb b/app/policies/settings_policy.rb index 2dcb79f51..874f97bab 100644 --- a/app/policies/settings_policy.rb +++ b/app/policies/settings_policy.rb @@ -8,4 +8,8 @@ class SettingsPolicy < ApplicationPolicy def show? admin? end + + def destroy? + admin? + end end diff --git a/app/serializers/rest/announcement_serializer.rb b/app/serializers/rest/announcement_serializer.rb index f27feb669..9343b97d2 100644 --- a/app/serializers/rest/announcement_serializer.rb +++ b/app/serializers/rest/announcement_serializer.rb @@ -7,6 +7,7 @@ class REST::AnnouncementSerializer < ActiveModel::Serializer attribute :read, if: :current_user? has_many :mentions + has_many :statuses has_many :tags, serializer: REST::StatusSerializer::TagSerializer has_many :emojis, serializer: REST::CustomEmojiSerializer has_many :reactions, serializer: REST::ReactionSerializer @@ -46,4 +47,16 @@ class REST::AnnouncementSerializer < ActiveModel::Serializer object.pretty_acct end end + + class StatusSerializer < ActiveModel::Serializer + attributes :id, :url + + def id + object.id.to_s + end + + def url + ActivityPub::TagManager.instance.url_for(object) + end + end end diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb index d217dabb3..493813aab 100644 --- a/app/services/account_search_service.rb +++ b/app/services/account_search_service.rb @@ -171,7 +171,7 @@ class AccountSearchService < BaseService end def username_complete? - query.include?('@') && "@#{query}" =~ Account::MENTION_RE + query.include?('@') && "@#{query}" =~ /\A#{Account::MENTION_RE}\Z/ end def likely_acct? diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 4d19002c4..311ae7fa6 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -7,54 +7,68 @@ class FollowService < BaseService # Follow a remote user, notify remote user about the follow # @param [Account] source_account From which to follow # @param [String, Account] uri User URI to follow in the form of username@domain (or account record) - # @param [true, false, nil] reblogs Whether or not to show reblogs, defaults to true - def call(source_account, target_account, reblogs: nil, bypass_locked: false) - reblogs = true if reblogs.nil? - target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true) - - raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended? - raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) || target_account.moved? || (!target_account.local? && target_account.ostatus?) || source_account.domain_blocking?(target_account.domain) - - if source_account.following?(target_account) - # We're already following this account, but we'll call follow! again to - # make sure the reblogs status is set correctly. - return source_account.follow!(target_account, reblogs: reblogs) - elsif source_account.requested?(target_account) - # This isn't managed by a method in AccountInteractions, so we modify it - # ourselves if necessary. - req = source_account.follow_requests.find_by(target_account: target_account) - req.update!(show_reblogs: reblogs) - return req + # @param [Hash] options + # @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true + # @option [Boolean] :bypass_locked + # @option [Boolean] :with_rate_limit + def call(source_account, target_account, options = {}) + @source_account = source_account + @target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true) + @options = { reblogs: true, bypass_locked: false, with_rate_limit: false }.merge(options) + + raise ActiveRecord::RecordNotFound if following_not_possible? + raise Mastodon::NotPermittedError if following_not_allowed? + + if @source_account.following?(@target_account) + return change_follow_options! + elsif @source_account.requested?(@target_account) + return change_follow_request_options! end ActivityTracker.increment('activity:interactions') - if (target_account.locked? && !bypass_locked) || source_account.silenced? || target_account.activitypub? - request_follow(source_account, target_account, reblogs: reblogs) - elsif target_account.local? - direct_follow(source_account, target_account, reblogs: reblogs) + if (@target_account.locked? && !@options[:bypass_locked]) || @source_account.silenced? || @target_account.activitypub? + request_follow! + elsif @target_account.local? + direct_follow! end end private - def request_follow(source_account, target_account, reblogs: true) - follow_request = FollowRequest.create!(account: source_account, target_account: target_account, show_reblogs: reblogs) + def following_not_possible? + @target_account.nil? || @target_account.id == @source_account.id || @target_account.suspended? + end + + def following_not_allowed? + @target_account.blocking?(@source_account) || @source_account.blocking?(@target_account) || @target_account.moved? || (!@target_account.local? && @target_account.ostatus?) || @source_account.domain_blocking?(@target_account.domain) + end + + def change_follow_options! + @source_account.follow!(@target_account, reblogs: @options[:reblogs]) + end + + def change_follow_request_options! + @source_account.request_follow!(@target_account, reblogs: @options[:reblogs]) + end + + def request_follow! + follow_request = @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], rate_limit: @options[:with_rate_limit]) - if target_account.local? - LocalNotificationWorker.perform_async(target_account.id, follow_request.id, follow_request.class.name) - elsif target_account.activitypub? - ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), source_account.id, target_account.inbox_url) + if @target_account.local? + LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name) + elsif @target_account.activitypub? + ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url) end follow_request end - def direct_follow(source_account, target_account, reblogs: true) - follow = source_account.follow!(target_account, reblogs: reblogs) + def direct_follow! + follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], rate_limit: @options[:with_rate_limit]) - LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name) - MergeWorker.perform_async(target_account.id, source_account.id) + LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name) + MergeWorker.perform_async(@target_account.id, @source_account.id) follow end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 936e6ac55..5d3b8d725 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -19,6 +19,7 @@ class PostStatusService < BaseService # @option [Enumerable] :media_ids Optional array of media IDs to attach # @option [Doorkeeper::Application] :application # @option [String] :idempotency Optional idempotency key + # @option [Boolean] :with_rate_limit # @return [Status] def call(account, options = {}) @account = account @@ -58,7 +59,7 @@ class PostStatusService < BaseService end end @visibility = @options[:visibility] || @account.user&.setting_default_privacy - @visibility = :unlisted if @visibility == :public && @account.silenced? + @visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced? @scheduled_at = @options[:scheduled_at]&.to_datetime @scheduled_at = nil if scheduled_in_the_past? rescue ArgumentError @@ -170,6 +171,7 @@ class PostStatusService < BaseService language: language_from_option(@options[:language]) || @account.user&.setting_default_language&.presence || LanguageDetector.instance.detect(@text, @account), application: @options[:application], content_type: @options[:content_type] || @account.user&.setting_default_content_type, + rate_limit: @options[:with_rate_limit], }.compact end @@ -189,10 +191,11 @@ class PostStatusService < BaseService def scheduled_options @options.tap do |options_hash| - options_hash[:in_reply_to_id] = options_hash.delete(:thread)&.id - options_hash[:application_id] = options_hash.delete(:application)&.id - options_hash[:scheduled_at] = nil - options_hash[:idempotency] = nil + options_hash[:in_reply_to_id] = options_hash.delete(:thread)&.id + options_hash[:application_id] = options_hash.delete(:application)&.id + options_hash[:scheduled_at] = nil + options_hash[:idempotency] = nil + options_hash[:with_rate_limit] = false end end end diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index 0b12f143c..0a46509f8 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -8,6 +8,8 @@ class ReblogService < BaseService # @param [Account] account Account to reblog from # @param [Status] reblogged_status Status to be reblogged # @param [Hash] options + # @option [String] :visibility + # @option [Boolean] :with_rate_limit # @return [Status] def call(account, reblogged_status, options = {}) reblogged_status = reblogged_status.reblog if reblogged_status.reblog? @@ -18,9 +20,15 @@ class ReblogService < BaseService return reblog unless reblog.nil? - visibility = options[:visibility] || account.user&.setting_default_privacy - visibility = reblogged_status.visibility if reblogged_status.hidden? - reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility) + visibility = begin + if reblogged_status.hidden? + reblogged_status.visibility + else + options[:visibility] || account.user&.setting_default_privacy + end + end + + reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility, rate_limit: options[:with_rate_limit]) DistributionWorker.perform_async(reblog.id) ActivityPub::DistributionWorker.perform_async(reblog.id) unless reblogged_status.local_only? @@ -45,7 +53,9 @@ class ReblogService < BaseService def bump_potential_friendship(account, reblog) ActivityTracker.increment('activity:interactions') + return if account.following?(reblog.reblog.account_id) + PotentialFriendshipTracker.record(account.id, reblog.reblog.account_id, :reblog) end diff --git a/app/views/admin/accounts/_account.html.haml b/app/views/admin/accounts/_account.html.haml index b057d3e42..44b10af6e 100644 --- a/app/views/admin/accounts/_account.html.haml +++ b/app/views/admin/accounts/_account.html.haml @@ -11,6 +11,8 @@ %td - if account.user_current_sign_in_at %time.time-ago{ datetime: account.user_current_sign_in_at.iso8601, title: l(account.user_current_sign_in_at) }= l account.user_current_sign_in_at + - elsif account.last_status_at.present? + %time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at - else \- %td diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index 3a85324c9..7592161c9 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -19,6 +19,12 @@ %ul %li= filter_link_to t('admin.accounts.moderation.all'), staff: nil %li= filter_link_to t('admin.accounts.roles.staff'), staff: '1' + .filter-subset + %strong= t 'generic.order_by' + %ul + %li= filter_link_to t('relationships.most_recent'), order: nil + %li= filter_link_to t('admin.accounts.username'), order: 'alphabetic' + %li= filter_link_to t('relationships.last_active'), order: 'active' = form_tag admin_accounts_url, method: 'GET', class: 'simple_form' do .fields-group diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index 63b352361..bff706389 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -1,7 +1,10 @@ - content_for :page_title do = t('admin.settings.title') -= simple_form_for @admin_settings, url: admin_settings_path, html: { method: :patch } do |f| + - content_for :heading_actions do + = button_tag t('generic.save_changes'), class: 'button', form: 'edit_admin' + += simple_form_for @admin_settings, url: admin_settings_path, html: { method: :patch, id: 'edit_admin' } do |f| = render 'shared/error_messages', object: @admin_settings .fields-group @@ -27,13 +30,13 @@ .fields-row .fields-row__column.fields-row__column-6.fields-group - = f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: t('admin.settings.thumbnail.desc_html') + = f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: site_upload_delete_hint(t('admin.settings.thumbnail.desc_html'), :thumbnail) .fields-row__column.fields-row__column-6.fields-group - = f.input :hero, as: :file, wrapper: :with_block_label, label: t('admin.settings.hero.title'), hint: t('admin.settings.hero.desc_html') + = f.input :hero, as: :file, wrapper: :with_block_label, label: t('admin.settings.hero.title'), hint: site_upload_delete_hint(t('admin.settings.hero.desc_html'), :hero) .fields-row .fields-row__column.fields-row__column-6.fields-group - = f.input :mascot, as: :file, wrapper: :with_block_label, label: t('admin.settings.mascot.title'), hint: t('admin.settings.mascot.desc_html') + = f.input :mascot, as: :file, wrapper: :with_block_label, label: t('admin.settings.mascot.title'), hint: site_upload_delete_hint(t('admin.settings.mascot.desc_html'), :mascot) %hr.spacer/ diff --git a/app/views/errors/429.html.haml b/app/views/errors/429.html.haml new file mode 100644 index 000000000..2df4f4175 --- /dev/null +++ b/app/views/errors/429.html.haml @@ -0,0 +1,5 @@ +- content_for :page_title do + = t('errors.429') + +- content_for :content do + = t('errors.429') diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index f460cebba..5453177fd 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -1,7 +1,10 @@ - content_for :page_title do = t('settings.appearance') -= simple_form_for current_user, url: settings_preferences_appearance_path, html: { method: :put } do |f| +- content_for :heading_actions do + = button_tag t('generic.save_changes'), class: 'button', form: 'edit_user' + += simple_form_for current_user, url: settings_preferences_appearance_path, html: { method: :put, id: 'edit_user' } do |f| .fields-group = f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }, selected: I18n.locale, hint: false diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml index a496be21b..d7cc1ed5d 100644 --- a/app/views/settings/preferences/notifications/show.html.haml +++ b/app/views/settings/preferences/notifications/show.html.haml @@ -1,7 +1,10 @@ - content_for :page_title do = t('settings.notifications') -= simple_form_for current_user, url: settings_preferences_notifications_path, html: { method: :put } do |f| +- content_for :heading_actions do + = button_tag t('generic.save_changes'), class: 'button', form: 'edit_notification' + += simple_form_for current_user, url: settings_preferences_notifications_path, html: { method: :put, id: 'edit_notification' } do |f| = render 'shared/error_messages', object: current_user %h4= t 'notifications.email_events' @@ -32,6 +35,3 @@ = ff.input :must_be_follower, as: :boolean, wrapper: :with_label = ff.input :must_be_following, as: :boolean, wrapper: :with_label = ff.input :must_be_following_dm, as: :boolean, wrapper: :with_label - - .actions - = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index 9bdcb368d..3b5c7016d 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -1,7 +1,10 @@ - content_for :page_title do = t('settings.preferences') -= simple_form_for current_user, url: settings_preferences_other_path, html: { method: :put } do |f| +- content_for :heading_actions do + = button_tag t('generic.save_changes'), class: 'button', form: 'edit_preferences' + += simple_form_for current_user, url: settings_preferences_other_path, html: { method: :put, id: 'edit_preferences' } do |f| = render 'shared/error_messages', object: current_user .fields-group diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index f5d928233..7413be1db 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -1,7 +1,10 @@ - content_for :page_title do = t('settings.edit_profile') -= simple_form_for @account, url: settings_profile_path, html: { method: :put } do |f| +- content_for :heading_actions do + = button_tag t('generic.save_changes'), class: 'button', form: 'edit_profile' + += simple_form_for @account, url: settings_profile_path, html: { method: :put, id: 'edit_profile' } do |f| = render 'shared/error_messages', object: @account .fields-row |