accept merge from upstream

This commit is contained in:
Baptiste Lemoine 2020-03-08 17:32:42 +01:00
commit 158c51edc1
122 changed files with 898 additions and 827 deletions

View File

@ -6,6 +6,7 @@ aliases:
- image: circleci/ruby:2.7-buster-node - image: circleci/ruby:2.7-buster-node
environment: &ruby_environment environment: &ruby_environment
BUNDLE_APP_CONFIG: ./.bundle/ BUNDLE_APP_CONFIG: ./.bundle/
BUNDLE_PATH: ./vendor/bundle/
DB_HOST: localhost DB_HOST: localhost
DB_USER: root DB_USER: root
RAILS_ENV: test RAILS_ENV: test

View File

@ -49,7 +49,7 @@ gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.9' gem 'omniauth', '~> 1.9'
gem 'discard', '~> 1.1' gem 'discard', '~> 1.1'
gem 'doorkeeper', '~> 5.2' gem 'doorkeeper', '~> 5.3'
gem 'fast_blank', '~> 1.0' gem 'fast_blank', '~> 1.0'
gem 'fastimage' gem 'fastimage'
gem 'goldfinger', '~> 2.1' gem 'goldfinger', '~> 2.1'
@ -92,7 +92,7 @@ gem 'simple-navigation', '~> 4.1'
gem 'simple_form', '~> 5.0' gem 'simple_form', '~> 5.0'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.2.0' gem 'stoplight', '~> 2.2.0'
gem 'strong_migrations', '~> 0.5' gem 'strong_migrations', '~> 0.6'
gem 'tty-command', '~> 0.9', require: false gem 'tty-command', '~> 0.9', require: false
gem 'tty-prompt', '~> 0.20', require: false gem 'tty-prompt', '~> 0.20', require: false
gem 'twitter-text', '~> 1.14' gem 'twitter-text', '~> 1.14'

View File

@ -195,21 +195,21 @@ GEM
docile (1.3.2) docile (1.3.2)
domain_name (0.5.20190701) domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.2.3) doorkeeper (5.3.1)
railties (>= 5) railties (>= 5)
dotenv (2.7.5) dotenv (2.7.5)
dotenv-rails (2.7.5) dotenv-rails (2.7.5)
dotenv (= 2.7.5) dotenv (= 2.7.5)
railties (>= 3.2, < 6.1) railties (>= 3.2, < 6.1)
e2mmap (0.1.0) e2mmap (0.1.0)
elasticsearch (7.3.0) elasticsearch (7.5.0)
elasticsearch-api (= 7.3.0) elasticsearch-api (= 7.5.0)
elasticsearch-transport (= 7.3.0) elasticsearch-transport (= 7.5.0)
elasticsearch-api (7.3.0) elasticsearch-api (7.5.0)
multi_json multi_json
elasticsearch-dsl (0.1.8) elasticsearch-dsl (0.1.8)
elasticsearch-transport (7.3.0) elasticsearch-transport (7.5.0)
faraday faraday (>= 0.14, < 1)
multi_json multi_json
encryptor (3.0.0) encryptor (3.0.0)
equatable (0.6.1) equatable (0.6.1)
@ -220,7 +220,7 @@ GEM
fabrication (2.21.0) fabrication (2.21.0)
faker (2.10.1) faker (2.10.1)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
faraday (1.0.0) faraday (0.17.3)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
fast_blank (1.0.0) fast_blank (1.0.0)
fastimage (2.1.7) fastimage (2.1.7)
@ -311,7 +311,7 @@ GEM
multi_json (~> 1.14) multi_json (~> 1.14)
rack (~> 2.0) rack (~> 2.0)
rdf (~> 3.1) rdf (~> 3.1)
json-ld-preloaded (3.1.0) json-ld-preloaded (3.1.1)
json-ld (~> 3.1) json-ld (~> 3.1)
rdf (~> 3.1) rdf (~> 3.1)
jsonapi-renderer (0.2.2) jsonapi-renderer (0.2.2)
@ -383,7 +383,7 @@ GEM
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5) sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0) statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.10.1) oj (3.10.3)
omniauth (1.9.0) omniauth (1.9.0)
hashie (>= 3.4.6, < 3.7.0) hashie (>= 3.4.6, < 3.7.0)
rack (>= 1.6.2, < 3) rack (>= 1.6.2, < 3)
@ -435,7 +435,7 @@ GEM
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (4.0.3) public_suffix (4.0.3)
puma (4.3.1) puma (4.3.3)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.1.0) pundit (2.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -602,7 +602,7 @@ GEM
stoplight (2.2.0) stoplight (2.2.0)
streamio-ffmpeg (3.0.2) streamio-ffmpeg (3.0.2)
multi_json (~> 1.8) multi_json (~> 1.8)
strong_migrations (0.5.1) strong_migrations (0.6.2)
activerecord (>= 5) activerecord (>= 5)
temple (0.8.2) temple (0.8.2)
terminal-table (1.8.0) terminal-table (1.8.0)
@ -689,7 +689,7 @@ DEPENDENCIES
devise-two-factor (~> 3.1) devise-two-factor (~> 3.1)
devise_pam_authenticatable2 (~> 9.2) devise_pam_authenticatable2 (~> 9.2)
discard (~> 1.1) discard (~> 1.1)
doorkeeper (~> 5.2) doorkeeper (~> 5.3)
dotenv-rails (~> 2.7) dotenv-rails (~> 2.7)
e2mmap (~> 0.1.0) e2mmap (~> 0.1.0)
fabrication (~> 2.21) fabrication (~> 2.21)
@ -777,7 +777,7 @@ DEPENDENCIES
stackprof stackprof
stoplight (~> 2.2.0) stoplight (~> 2.2.0)
streamio-ffmpeg (~> 3.0) streamio-ffmpeg (~> 3.0)
strong_migrations (~> 0.5) strong_migrations (~> 0.6)
thor (~> 0.20) thor (~> 0.20)
thwait (~> 0.1.0) thwait (~> 0.1.0)
tty-command (~> 0.9) tty-command (~> 0.9)

View File

@ -6,7 +6,7 @@ class AccountFollowController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
def create 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) redirect_to account_path(@account)
end end
end end

View File

@ -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

View File

@ -44,6 +44,10 @@ class Api::BaseController < ApplicationController
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503 render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end end
rescue_from Mastodon::RateLimitExceededError do
render json: { error: I18n.t('errors.429') }, status: 429
end
rescue_from ActionController::ParameterMissing do |e| rescue_from ActionController::ParameterMissing do |e|
render json: { error: e.to_s }, status: 400 render json: { error: e.to_s }, status: 400
end end

View File

@ -5,8 +5,6 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
before_action :set_account before_action :set_account
after_action :insert_pagination_headers after_action :insert_pagination_headers
respond_to :json
def index def index
@accounts = load_accounts @accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer render json: @accounts, each_serializer: REST::AccountSerializer

View File

@ -5,8 +5,6 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
before_action :set_account before_action :set_account
after_action :insert_pagination_headers after_action :insert_pagination_headers
respond_to :json
def index def index
@accounts = load_accounts @accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer render json: @accounts, each_serializer: REST::AccountSerializer

View File

@ -4,8 +4,6 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController
before_action :require_user! before_action :require_user!
before_action :set_account before_action :set_account
respond_to :json
def index def index
@proofs = @account.identity_proofs.active @proofs = @account.identity_proofs.active
render json: @proofs, each_serializer: REST::IdentityProofSerializer render json: @proofs, each_serializer: REST::IdentityProofSerializer

View File

@ -5,8 +5,6 @@ class Api::V1::Accounts::ListsController < Api::BaseController
before_action :require_user! before_action :require_user!
before_action :set_account before_action :set_account
respond_to :json
def index def index
@lists = @account.lists.where(account: current_account) @lists = @account.lists.where(account: current_account)
render json: @lists, each_serializer: REST::ListSerializer render json: @lists, each_serializer: REST::ListSerializer

View File

@ -7,8 +7,6 @@ class Api::V1::Accounts::PinsController < Api::BaseController
before_action :require_user! before_action :require_user!
before_action :set_account before_action :set_account
respond_to :json
def create def create
AccountPin.create!(account: current_account, target_account: @account) AccountPin.create!(account: current_account, target_account: @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter

View File

@ -4,8 +4,6 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:follows' } before_action -> { doorkeeper_authorize! :read, :'read:follows' }
before_action :require_user! before_action :require_user!
respond_to :json
def index def index
accounts = Account.where(id: account_ids).select('id') accounts = Account.where(id: account_ids).select('id')
# .where doesn't guarantee that our results are in the same order # .where doesn't guarantee that our results are in the same order

View File

@ -4,8 +4,6 @@ class Api::V1::Accounts::SearchController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' } before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
before_action :require_user! before_action :require_user!
respond_to :json
def show def show
@accounts = account_search @accounts = account_search
render json: @accounts, each_serializer: REST::AccountSerializer render json: @accounts, each_serializer: REST::AccountSerializer

View File

@ -6,8 +6,6 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) } after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) }
respond_to :json
def index def index
@statuses = load_statuses @statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)

View File

@ -14,7 +14,7 @@ class Api::V1::AccountsController < Api::BaseController
skip_before_action :require_authenticated_user!, only: :create skip_before_action :require_authenticated_user!, only: :create
respond_to :json override_rate_limit_headers :follow, family: :follows
def show def show
render json: @account, serializer: REST::AccountSerializer render json: @account, serializer: REST::AccountSerializer
@ -31,7 +31,7 @@ class Api::V1::AccountsController < Api::BaseController
end end
def follow 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 } } options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }

View File

@ -3,8 +3,6 @@
class Api::V1::Apps::CredentialsController < Api::BaseController class Api::V1::Apps::CredentialsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read } before_action -> { doorkeeper_authorize! :read }
respond_to :json
def show def show
render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key) render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key)
end end

View File

@ -5,8 +5,6 @@ class Api::V1::BlocksController < Api::BaseController
before_action :require_user! before_action :require_user!
after_action :insert_pagination_headers after_action :insert_pagination_headers
respond_to :json
def index def index
@accounts = load_accounts @accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer render json: @accounts, each_serializer: REST::AccountSerializer

View File

@ -5,8 +5,6 @@ class Api::V1::BookmarksController < Api::BaseController
before_action :require_user! before_action :require_user!
after_action :insert_pagination_headers after_action :insert_pagination_headers
respond_to :json
def index def index
@statuses = load_statuses @statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)

View File

@ -9,8 +9,6 @@ class Api::V1::ConversationsController < Api::BaseController
before_action :set_conversation, except: :index before_action :set_conversation, except: :index
after_action :insert_pagination_headers, only: :index after_action :insert_pagination_headers, only: :index
respond_to :json
def index def index
@conversations = paginated_conversations @conversations = paginated_conversations
render json: @conversations, each_serializer: REST::ConversationSerializer render json: @conversations, each_serializer: REST::ConversationSerializer

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::CustomEmojisController < Api::BaseController class Api::V1::CustomEmojisController < Api::BaseController
respond_to :json
skip_before_action :set_cache_headers skip_before_action :set_cache_headers
def index def index

View File

@ -8,8 +8,6 @@ class Api::V1::DomainBlocksController < Api::BaseController
before_action :require_user! before_action :require_user!
after_action :insert_pagination_headers, only: :show after_action :insert_pagination_headers, only: :show
respond_to :json
def show def show
@blocks = load_domain_blocks @blocks = load_domain_blocks
render json: @blocks.map(&:domain) render json: @blocks.map(&:domain)

View File

@ -5,8 +5,6 @@ class Api::V1::EndorsementsController < Api::BaseController
before_action :require_user! before_action :require_user!
after_action :insert_pagination_headers after_action :insert_pagination_headers
respond_to :json
def index def index
@accounts = load_accounts @accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer render json: @accounts, each_serializer: REST::AccountSerializer

View File

@ -5,8 +5,6 @@ class Api::V1::FavouritesController < Api::BaseController
before_action :require_user! before_action :require_user!
after_action :insert_pagination_headers after_action :insert_pagination_headers
respond_to :json
def index def index
@statuses = load_statuses @statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)

View File

@ -2,12 +2,9 @@
class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
before_action :require_user! before_action :require_user!
before_action :set_most_used_tags, only: :index before_action :set_most_used_tags, only: :index
respond_to :json
def index def index
render json: @most_used_tags, each_serializer: REST::TagSerializer render json: @most_used_tags, each_serializer: REST::TagSerializer
end end

View File

@ -7,8 +7,6 @@ class Api::V1::FiltersController < Api::BaseController
before_action :set_filters, only: :index before_action :set_filters, only: :index
before_action :set_filter, only: [:show, :update, :destroy] before_action :set_filter, only: [:show, :update, :destroy]
respond_to :json
def index def index
render json: @filters, each_serializer: REST::FilterSerializer render json: @filters, each_serializer: REST::FilterSerializer
end end

View File

@ -6,8 +6,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController
skip_before_action :set_cache_headers skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode? skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
respond_to :json
def show def show
expires_in 1.day, public: true expires_in 1.day, public: true
render_with_cache json: :activity, expires_in: 1.day render_with_cache json: :activity, expires_in: 1.day

View File

@ -6,8 +6,6 @@ class Api::V1::Instances::PeersController < Api::BaseController
skip_before_action :set_cache_headers skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode? skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
respond_to :json
def index def index
expires_in 1.day, public: true expires_in 1.day, public: true
render_with_cache(expires_in: 1.day) { Account.remote.domains } render_with_cache(expires_in: 1.day) { Account.remote.domains }

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::InstancesController < Api::BaseController class Api::V1::InstancesController < Api::BaseController
respond_to :json
skip_before_action :set_cache_headers skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode? skip_before_action :require_authenticated_user!, unless: :whitelist_mode?

View File

@ -4,8 +4,6 @@ class Api::V1::MediaController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:media' } before_action -> { doorkeeper_authorize! :write, :'write:media' }
before_action :require_user! before_action :require_user!
respond_to :json
def create def create
@media = current_account.media_attachments.create!(media_params) @media = current_account.media_attachments.create!(media_params)
render json: @media, serializer: REST::MediaAttachmentSerializer render json: @media, serializer: REST::MediaAttachmentSerializer

View File

@ -5,8 +5,6 @@ class Api::V1::MutesController < Api::BaseController
before_action :require_user! before_action :require_user!
after_action :insert_pagination_headers after_action :insert_pagination_headers
respond_to :json
def index def index
@accounts = load_accounts @accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer render json: @accounts, each_serializer: REST::AccountSerializer

View File

@ -6,8 +6,6 @@ class Api::V1::NotificationsController < Api::BaseController
before_action :require_user! before_action :require_user!
after_action :insert_pagination_headers, only: :index after_action :insert_pagination_headers, only: :index
respond_to :json
DEFAULT_NOTIFICATIONS_LIMIT = 15 DEFAULT_NOTIFICATIONS_LIMIT = 15
def index def index

View File

@ -7,8 +7,6 @@ class Api::V1::Polls::VotesController < Api::BaseController
before_action :require_user! before_action :require_user!
before_action :set_poll before_action :set_poll
respond_to :json
def create def create
VoteService.new.call(current_account, @poll, vote_params[:choices]) VoteService.new.call(current_account, @poll, vote_params[:choices])
render json: @poll, serializer: REST::PollSerializer render json: @poll, serializer: REST::PollSerializer

View File

@ -7,8 +7,6 @@ class Api::V1::PollsController < Api::BaseController
before_action :set_poll before_action :set_poll
before_action :refresh_poll before_action :refresh_poll
respond_to :json
def show def show
render json: @poll, serializer: REST::PollSerializer, include_results: true render json: @poll, serializer: REST::PollSerializer, include_results: true
end end

View File

@ -4,8 +4,6 @@ class Api::V1::PreferencesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' } before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
before_action :require_user! before_action :require_user!
respond_to :json
def index def index
render json: current_account, serializer: REST::PreferencesSerializer render json: current_account, serializer: REST::PreferencesSerializer
end end

View File

@ -4,8 +4,6 @@ class Api::V1::ReportsController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:reports' }, only: [:create] before_action -> { doorkeeper_authorize! :write, :'write:reports' }, only: [:create]
before_action :require_user! before_action :require_user!
respond_to :json
def create def create
@report = ReportService.new.call( @report = ReportService.new.call(
current_account, current_account,

View File

@ -7,8 +7,6 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController
before_action :require_user! before_action :require_user!
before_action :set_status before_action :set_status
respond_to :json
def create def create
current_account.bookmarks.find_or_create_by!(account: current_account, status: @status) current_account.bookmarks.find_or_create_by!(account: current_account, status: @status)
render json: @status, serializer: REST::StatusSerializer render json: @status, serializer: REST::StatusSerializer

View File

@ -7,8 +7,6 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
before_action :set_status before_action :set_status
after_action :insert_pagination_headers after_action :insert_pagination_headers
respond_to :json
def index def index
@accounts = load_accounts @accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer render json: @accounts, each_serializer: REST::AccountSerializer

View File

@ -7,8 +7,6 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
before_action :require_user! before_action :require_user!
before_action :set_status before_action :set_status
respond_to :json
def create def create
FavouriteService.new.call(current_account, @status) FavouriteService.new.call(current_account, @status)
render json: @status, serializer: REST::StatusSerializer render json: @status, serializer: REST::StatusSerializer

View File

@ -8,8 +8,6 @@ class Api::V1::Statuses::MutesController < Api::BaseController
before_action :set_status before_action :set_status
before_action :set_conversation before_action :set_conversation
respond_to :json
def create def create
current_account.mute_conversation!(@conversation) current_account.mute_conversation!(@conversation)
@mutes_map = { @conversation.id => true } @mutes_map = { @conversation.id => true }

View File

@ -7,8 +7,6 @@ class Api::V1::Statuses::PinsController < Api::BaseController
before_action :require_user! before_action :require_user!
before_action :set_status before_action :set_status
respond_to :json
def create def create
StatusPin.create!(account: current_account, status: @status) StatusPin.create!(account: current_account, status: @status)
distribute_add_activity! distribute_add_activity!

View File

@ -7,8 +7,6 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
before_action :set_status before_action :set_status
after_action :insert_pagination_headers after_action :insert_pagination_headers
respond_to :json
def index def index
@accounts = load_accounts @accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer render json: @accounts, each_serializer: REST::AccountSerializer

View File

@ -7,10 +7,11 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
before_action :require_user! before_action :require_user!
before_action :set_reblog before_action :set_reblog
respond_to :json override_rate_limit_headers :create, family: :statuses
def create def create
@status = ReblogService.new.call(current_account, @reblog, reblog_params) @status = ReblogService.new.call(current_account, @reblog, reblog_params)
render json: @status, serializer: REST::StatusSerializer render json: @status, serializer: REST::StatusSerializer
end end

View File

@ -8,7 +8,7 @@ class Api::V1::StatusesController < Api::BaseController
before_action :require_user!, except: [:show, :context] before_action :require_user!, except: [:show, :context]
before_action :set_status, only: [: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 # This API was originally unlimited, pagination cannot be introduced without
# breaking backwards-compatibility. Arbitrarily high number to cover most # breaking backwards-compatibility. Arbitrarily high number to cover most
@ -44,7 +44,8 @@ class Api::V1::StatusesController < Api::BaseController
scheduled_at: status_params[:scheduled_at], scheduled_at: status_params[:scheduled_at],
application: doorkeeper_token.application, application: doorkeeper_token.application,
poll: status_params[:poll], poll: status_params[:poll],
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 render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
end end

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::StreamingController < Api::BaseController class Api::V1::StreamingController < Api::BaseController
respond_to :json
def index def index
if Rails.configuration.x.streaming_api_base_url != request.host if Rails.configuration.x.streaming_api_base_url != request.host
redirect_to streaming_api_url, status: 301 redirect_to streaming_api_url, status: 301

View File

@ -7,8 +7,6 @@ class Api::V1::SuggestionsController < Api::BaseController
before_action :require_user! before_action :require_user!
before_action :set_accounts before_action :set_accounts
respond_to :json
def index def index
render json: @accounts, each_serializer: REST::AccountSerializer render json: @accounts, each_serializer: REST::AccountSerializer
end end

View File

@ -5,8 +5,6 @@ class Api::V1::Timelines::HomeController < Api::BaseController
before_action :require_user!, only: [:show] before_action :require_user!, only: [:show]
after_action :insert_pagination_headers, unless: -> { @statuses.empty? } after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
respond_to :json
def show def show
@statuses = load_statuses @statuses = load_statuses

View File

@ -4,8 +4,6 @@ class Api::V1::Timelines::PublicController < Api::BaseController
before_action :require_user!, only: [:show], if: :require_auth? before_action :require_user!, only: [:show], if: :require_auth?
after_action :insert_pagination_headers, unless: -> { @statuses.empty? } after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
respond_to :json
def show def show
@statuses = load_statuses @statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)

View File

@ -4,8 +4,6 @@ class Api::V1::Timelines::TagController < Api::BaseController
before_action :load_tag before_action :load_tag
after_action :insert_pagination_headers, unless: -> { @statuses.empty? } after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
respond_to :json
def show def show
@statuses = load_statuses @statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)

View File

@ -3,8 +3,6 @@
class Api::V1::TrendsController < Api::BaseController class Api::V1::TrendsController < Api::BaseController
before_action :set_tags before_action :set_tags
respond_to :json
def index def index
render json: @tags, each_serializer: REST::TagSerializer render json: @tags, each_serializer: REST::TagSerializer
end end

View File

@ -8,8 +8,6 @@ class Api::V2::SearchController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:search' } before_action -> { doorkeeper_authorize! :read, :'read:search' }
before_action :require_user! before_action :require_user!
respond_to :json
def index def index
@search = Search.new(search_results) @search = Search.new(search_results)
render json: @search, serializer: REST::SearchSerializer render json: @search, serializer: REST::SearchSerializer

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::Web::EmbedsController < Api::Web::BaseController class Api::Web::EmbedsController < Api::Web::BaseController
respond_to :json
before_action :require_user! before_action :require_user!
def create def create

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::Web::PushSubscriptionsController < Api::Web::BaseController class Api::Web::PushSubscriptionsController < Api::Web::BaseController
respond_to :json
before_action :require_user! before_action :require_user!
def create def create

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::Web::SettingsController < Api::Web::BaseController class Api::Web::SettingsController < Api::Web::BaseController
respond_to :json
before_action :require_user! before_action :require_user!
def update def update

View File

@ -29,6 +29,7 @@ class ApplicationController < ActionController::Base
rescue_from Mastodon::NotPermittedError, with: :forbidden rescue_from Mastodon::NotPermittedError, with: :forbidden
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
rescue_from Mastodon::RaceConditionError, with: :service_unavailable 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 :store_current_location, except: :raise_not_found, unless: :devise_controller?
before_action :require_functional!, if: :user_signed_in? before_action :require_functional!, if: :user_signed_in?
@ -111,6 +112,10 @@ class ApplicationController < ActionController::Base
respond_with_error(503) respond_with_error(503)
end end
def too_many_requests
respond_with_error(429)
end
def single_user_mode? def single_user_mode?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists? @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists?
end end

View File

@ -20,7 +20,7 @@ class AuthorizeInteractionsController < ApplicationController
end end
def create 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 render :success
else else
render :error render :error

View File

@ -3,6 +3,20 @@
module RateLimitHeaders module RateLimitHeaders
extend ActiveSupport::Concern 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 included do
before_action :set_rate_limit_headers, if: :rate_limited_request? before_action :set_rate_limit_headers, if: :rate_limited_request?
end end
@ -44,7 +58,7 @@ module RateLimitHeaders
end end
def api_throttle_data 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] request.env['rack.attack.throttle_data'][most_limited_type]
end end

View File

@ -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

View File

@ -10,7 +10,7 @@ import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_s
import { decode } from 'blurhash'; import { decode } from 'blurhash';
const messages = defineMessages({ 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 { class Item extends React.PureComponent {

View File

@ -6,54 +6,49 @@ import IconButton from './icon_button';
import DropdownMenuContainer from '../containers/dropdown_menu_container'; import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { isStaff, me } from '../initial_state'; import { me, isStaff } from '../initial_state';
const messages = defineMessages({ const messages = defineMessages({
delete : { id: 'status.delete', defaultMessage: 'Delete' }, delete: { id: 'status.delete', defaultMessage: 'Delete' },
redraft : { id: 'status.redraft', defaultMessage: 'Delete & re-draft' }, redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
direct : { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention : { id: 'status.mention', defaultMessage: 'Mention @{name}' }, mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
mute : { id: 'account.mute', defaultMessage: 'Mute @{name}' }, mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
block : { id: 'account.block', defaultMessage: 'Block @{name}' }, block: { id: 'account.block', defaultMessage: 'Block @{name}' },
reply : { id: 'status.reply', defaultMessage: 'Reply' }, reply: { id: 'status.reply', defaultMessage: 'Reply' },
share : { id: 'status.share', defaultMessage: 'Share' }, share: { id: 'status.share', defaultMessage: 'Share' },
more : { id: 'status.more', defaultMessage: 'More' }, more: { id: 'status.more', defaultMessage: 'More' },
replyAll : { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
reblog : { id: 'status.reblog', defaultMessage: 'Boost' }, reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private : { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' }, reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog : { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite : { id: 'status.favourite', defaultMessage: 'Favourite' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
bookmark : { id: 'status.bookmark', defaultMessage: 'Bookmark' }, bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
removeBookmark : { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' }, removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
open : { id: 'status.open', defaultMessage: 'Expand this status' }, open: { id: 'status.open', defaultMessage: 'Expand this status' },
report : { id: 'status.report', defaultMessage: 'Report @{name}' }, report: { id: 'status.report', defaultMessage: 'Report @{name}' },
muteConversation : { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation : { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
pin : { id: 'status.pin', defaultMessage: 'Pin on profile' }, pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin : { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed : { id: 'status.embed', defaultMessage: 'Embed' }, embed: { id: 'status.embed', defaultMessage: 'Embed' },
admin_account : { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, 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' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
copy : { id: 'status.copy', defaultMessage: 'Copy link to status' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
blockDomain : { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
unblockDomain : { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
unmute : { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unblock : { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
}); });
const ObfuscateCountReplies = false;
const obfuscatedCount = count => {
if (ObfuscateCountReplies) {
if (count < 0) { const obfuscatedCount = count => {
return 0; if (count < 0) {
} else if (count <= 1) { return 0;
return count; } else if (count <= 1) {
} else {
return '1+';
}
} else {
return count; return count;
} else {
return '1+';
} }
}; };
@ -70,28 +65,27 @@ class StatusActionBar extends ImmutablePureComponent {
}; };
static propTypes = { static propTypes = {
status : ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
relationship : ImmutablePropTypes.map, relationship: ImmutablePropTypes.map,
onReply : PropTypes.func, onReply: PropTypes.func,
onFavourite : PropTypes.func, onFavourite: PropTypes.func,
onReblog : PropTypes.func, onReblog: PropTypes.func,
onDelete : PropTypes.func, onDelete: PropTypes.func,
onDirect : PropTypes.func, onDirect: PropTypes.func,
onMention : PropTypes.func, onMention: PropTypes.func,
onMute : PropTypes.func, onMute: PropTypes.func,
onUnmute : PropTypes.func, onUnmute: PropTypes.func,
onBlock : PropTypes.func, onBlock: PropTypes.func,
onUnblock : PropTypes.func, onUnblock: PropTypes.func,
onBlockDomain : PropTypes.func, onBlockDomain: PropTypes.func,
onUnblockDomain : PropTypes.func, onUnblockDomain: PropTypes.func,
onReport : PropTypes.func, onReport: PropTypes.func,
onEmbed : PropTypes.func, onEmbed: PropTypes.func,
onMuteConversation: PropTypes.func, onMuteConversation: PropTypes.func,
onPin : PropTypes.func, onPin: PropTypes.func,
onBookmark : PropTypes.func, onBookmark: PropTypes.func,
withDismiss : PropTypes.bool, withDismiss: PropTypes.bool,
showReplyFormUnder: PropTypes.bool, intl: PropTypes.object.isRequired,
intl : PropTypes.object.isRequired,
}; };
// Avoid checking props that are functions (and whose equality will always // Avoid checking props that are functions (and whose equality will always
@ -100,7 +94,7 @@ class StatusActionBar extends ImmutablePureComponent {
'status', 'status',
'relationship', 'relationship',
'withDismiss', 'withDismiss',
]; ]
handleReplyClick = () => { handleReplyClick = () => {
if (me) { if (me) {
@ -108,20 +102,16 @@ class StatusActionBar extends ImmutablePureComponent {
} else { } else {
this._openInteractionDialog('reply'); this._openInteractionDialog('reply');
} }
}; }
handleReplyUnderClick = () => {
console.info('hop show');
this.handleReplyClick();
};
handleShareClick = () => { handleShareClick = () => {
navigator.share({ navigator.share({
text: this.props.status.get('search_index'), text: this.props.status.get('search_index'),
url : this.props.status.get('url'), url: this.props.status.get('url'),
}).catch((e) => { }).catch((e) => {
if (e.name !== 'AbortError') console.error(e); if (e.name !== 'AbortError') console.error(e);
}); });
}; }
handleFavouriteClick = () => { handleFavouriteClick = () => {
if (me) { if (me) {
@ -129,7 +119,7 @@ class StatusActionBar extends ImmutablePureComponent {
} else { } else {
this._openInteractionDialog('favourite'); this._openInteractionDialog('favourite');
} }
}; }
handleReblogClick = e => { handleReblogClick = e => {
if (me) { if (me) {
@ -137,35 +127,35 @@ class StatusActionBar extends ImmutablePureComponent {
} else { } else {
this._openInteractionDialog('reblog'); this._openInteractionDialog('reblog');
} }
}; }
_openInteractionDialog = type => { _openInteractionDialog = type => {
window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
}; }
handleBookmarkClick = () => { handleBookmarkClick = () => {
this.props.onBookmark(this.props.status); this.props.onBookmark(this.props.status);
}; }
handleDeleteClick = () => { handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.context.router.history); this.props.onDelete(this.props.status, this.context.router.history);
}; }
handleRedraftClick = () => { handleRedraftClick = () => {
this.props.onDelete(this.props.status, this.context.router.history, true); this.props.onDelete(this.props.status, this.context.router.history, true);
}; }
handlePinClick = () => { handlePinClick = () => {
this.props.onPin(this.props.status); this.props.onPin(this.props.status);
}; }
handleMentionClick = () => { handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.context.router.history); this.props.onMention(this.props.status.get('account'), this.context.router.history);
}; }
handleDirectClick = () => { handleDirectClick = () => {
this.props.onDirect(this.props.status.get('account'), this.context.router.history); this.props.onDirect(this.props.status.get('account'), this.context.router.history);
}; }
handleMuteClick = () => { handleMuteClick = () => {
const { status, relationship, onMute, onUnmute } = this.props; const { status, relationship, onMute, onUnmute } = this.props;
@ -176,7 +166,7 @@ class StatusActionBar extends ImmutablePureComponent {
} else { } else {
onMute(account); onMute(account);
} }
}; }
handleBlockClick = () => { handleBlockClick = () => {
const { status, relationship, onBlock, onUnblock } = this.props; const { status, relationship, onBlock, onUnblock } = this.props;
@ -187,43 +177,43 @@ class StatusActionBar extends ImmutablePureComponent {
} else { } else {
onBlock(status); onBlock(status);
} }
}; }
handleBlockDomain = () => { handleBlockDomain = () => {
const { status, onBlockDomain } = this.props; const { status, onBlockDomain } = this.props;
const account = status.get('account'); const account = status.get('account');
onBlockDomain(account.get('acct').split('@')[1]); onBlockDomain(account.get('acct').split('@')[1]);
}; }
handleUnblockDomain = () => { handleUnblockDomain = () => {
const { status, onUnblockDomain } = this.props; const { status, onUnblockDomain } = this.props;
const account = status.get('account'); const account = status.get('account');
onUnblockDomain(account.get('acct').split('@')[1]); onUnblockDomain(account.get('acct').split('@')[1]);
}; }
handleOpen = () => { handleOpen = () => {
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
}; }
handleEmbed = () => { handleEmbed = () => {
this.props.onEmbed(this.props.status); this.props.onEmbed(this.props.status);
}; }
handleReport = () => { handleReport = () => {
this.props.onReport(this.props.status); this.props.onReport(this.props.status);
}; }
handleConversationMuteClick = () => { handleConversationMuteClick = () => {
this.props.onMuteConversation(this.props.status); this.props.onMuteConversation(this.props.status);
}; }
handleCopy = () => { handleCopy = () => {
const url = this.props.status.get('url'); const url = this.props.status.get('url');
const textarea = document.createElement('textarea'); const textarea = document.createElement('textarea');
textarea.textContent = url; textarea.textContent = url;
textarea.style.position = 'fixed'; textarea.style.position = 'fixed';
document.body.appendChild(textarea); document.body.appendChild(textarea);
@ -236,16 +226,15 @@ class StatusActionBar extends ImmutablePureComponent {
} finally { } finally {
document.body.removeChild(textarea); document.body.removeChild(textarea);
} }
}; }
render() { render () {
const { status, relationship, intl, withDismiss } = this.props; const { status, relationship, intl, withDismiss } = this.props;
console.log('this.props', this.props);
const mutingConversation = status.get('muted'); const mutingConversation = status.get('muted');
const anonymousAccess = !me; const anonymousAccess = !me;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const account = status.get('account'); const account = status.get('account');
let menu = []; let menu = [];
let reblogIcon = 'retweet'; let reblogIcon = 'retweet';
@ -259,76 +248,43 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
} }
menu.push({ menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
text : intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark),
action: this.handleBookmarkClick,
});
menu.push(null); menu.push(null);
if (status.getIn(['account', 'id']) === me || withDismiss) { if (status.getIn(['account', 'id']) === me || withDismiss) {
menu.push({ menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
text : intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation),
action: this.handleConversationMuteClick,
});
menu.push(null); menu.push(null);
} }
if (status.getIn(['account', 'id']) === me) { if (status.getIn(['account', 'id']) === me) {
if (publicStatus) { if (publicStatus) {
menu.push({ menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
text : intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin),
action: this.handlePinClick,
});
} else { } else {
if (status.get('visibility') === 'private') { if (status.get('visibility') === 'private') {
menu.push({ menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick });
text : intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private),
action: this.handleReblogClick,
});
} }
} }
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
} else { } else {
menu.push({ menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
text : intl.formatMessage(messages.mention, { name: account.get('username') }), menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
action: this.handleMentionClick,
});
menu.push({
text : intl.formatMessage(messages.direct, { name: account.get('username') }),
action: this.handleDirectClick,
});
menu.push(null); menu.push(null);
if (relationship && relationship.get('muting')) { if (relationship && relationship.get('muting')) {
menu.push({ menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
text : intl.formatMessage(messages.unmute, { name: account.get('username') }),
action: this.handleMuteClick,
});
} else { } else {
menu.push({ menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick });
text : intl.formatMessage(messages.mute, { name: account.get('username') }),
action: this.handleMuteClick,
});
} }
if (relationship && relationship.get('blocking')) { if (relationship && relationship.get('blocking')) {
menu.push({ menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
text : intl.formatMessage(messages.unblock, { name: account.get('username') }),
action: this.handleBlockClick,
});
} else { } else {
menu.push({ menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick });
text : intl.formatMessage(messages.block, { name: account.get('username') }),
action: this.handleBlockClick,
});
} }
menu.push({ menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport });
text : intl.formatMessage(messages.report, { name: account.get('username') }),
action: this.handleReport,
});
if (account.get('acct') !== account.get('username')) { if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1]; const domain = account.get('acct').split('@')[1];
@ -344,14 +300,8 @@ class StatusActionBar extends ImmutablePureComponent {
if (isStaff) { if (isStaff) {
menu.push(null); menu.push(null);
menu.push({ menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
href: `/admin/accounts/${status.getIn(['account', 'id'])}`,
});
menu.push({
text: intl.formatMessage(messages.admin_status),
href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}`,
});
} }
} }
@ -369,73 +319,21 @@ class StatusActionBar extends ImmutablePureComponent {
replyTitle = intl.formatMessage(messages.replyAll); replyTitle = intl.formatMessage(messages.replyAll);
} }
const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && ( const shareButton = ('share' in navigator) && publicStatus && (
<IconButton <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
className='status__action-bar-button'
title={intl.formatMessage(messages.share)}
icon='share-alt'
onClick={this.handleShareClick}
/>
); );
return ( return (
<div className='status__action-bar'> <div className='status__action-bar'>
<div className='status__action-bar__counter'> <div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
<IconButton className='status__action-bar-button' disabled={!publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
className='status__action-bar-button reply_under'
title={replyTitle}
icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon}
onClick={this.handleReplyClick}
/>
{/*{this.state.showReplyFormUnder && (*/}
{/* <span >*/}
{/* yes*/}
{/* </span >*/}
{/*)}*/}
{/*<span className='status__action-bar__counter__label'>{obfuscatedCount(status.get('replies_count'))}</span >*/}
<span className='status__action-bar__counter__label'>
{obfuscatedCount(status.get('replies_count'))}
</span >
</div >
<IconButton
className='status__action-bar-button'
disabled={!publicStatus}
active={status.get('reblogged')}
pressed={status.get('reblogged')}
title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)}
icon={reblogIcon}
onClick={this.handleReblogClick}
/>
<IconButton
className='status__action-bar-button star-icon'
animate
active={status.get('favourited')}
pressed={status.get('favourited')}
title={intl.formatMessage(messages.favourite)}
icon='star'
onClick={this.handleFavouriteClick}
/>
{shareButton} {shareButton}
<div className='status__action-bar-dropdown'> <div className='status__action-bar-dropdown'>
<DropdownMenuContainer <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} />
disabled={anonymousAccess} </div>
status={status} </div>
items={menu}
icon='ellipsis-h'
size={18}
direction='right'
title={intl.formatMessage(messages.more)}
/>
</div >
{this.props.showReplyFormUnder && (
<div className='reply_form'>
form de reply
</div >
)}
</div >
); );
} }

View File

@ -29,8 +29,8 @@ const messages = defineMessages({
report: { id: 'account.report', defaultMessage: 'Report @{name}' }, report: { id: 'account.report', defaultMessage: 'Report @{name}' },
share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' }, share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
media: { id: 'account.media', defaultMessage: 'Media' }, media: { id: 'account.media', defaultMessage: 'Media' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' }, hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },

View File

@ -214,8 +214,8 @@ class Audio extends React.PureComponent {
<div className='video-player__controls active'> <div className='video-player__controls active'>
<div className='video-player__buttons-bar'> <div className='video-player__buttons-bar'>
<div className='video-player__buttons left'> <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' 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' 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(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}> <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
&nbsp; &nbsp;
@ -236,7 +236,7 @@ class Audio extends React.PureComponent {
</div> </div>
<div className='video-player__buttons right'> <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> <a className='video-player__download__icon' href={this.props.src} download>
<Icon id={'download'} fixedWidth /> <Icon id={'download'} fixedWidth />
</a> </a>

View File

@ -155,6 +155,7 @@ class PollForm extends ImmutablePureComponent {
<div className='poll__footer'> <div className='poll__footer'>
<button disabled={options.size >= 4} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button> <button disabled={options.size >= 4} 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}> <select value={expiresIn} onChange={this.handleSelectDuration}>
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option> <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option> <option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>

View File

@ -11,13 +11,13 @@ import Icon from 'mastodon/components/icon';
const messages = defineMessages({ const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, 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_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_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_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' }, change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
}); });

View File

@ -5,7 +5,11 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
import { expandFollowers, fetchAccount, fetchFollowers } from '../../actions/accounts'; import {
fetchAccount,
fetchFollowers,
expandFollowers,
} from '../../actions/accounts';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
@ -15,34 +19,34 @@ import ScrollableList from '../../components/scrollable_list';
import MissingIndicator from 'mastodon/components/missing_indicator'; import MissingIndicator from 'mastodon/components/missing_indicator';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
isAccount : !!state.getIn(['accounts', props.params.accountId]), isAccount: !!state.getIn(['accounts', props.params.accountId]),
accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']), accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']),
hasMore : !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']), hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']),
blockedBy : state.getIn(['relationships', props.params.accountId, 'blocked_by'], false), blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
}); });
export default @connect(mapStateToProps) export default @connect(mapStateToProps)
class Followers extends ImmutablePureComponent { class Followers extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params : PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch : PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func, shouldUpdateScroll: PropTypes.func,
accountIds : ImmutablePropTypes.list, accountIds: ImmutablePropTypes.list,
hasMore : PropTypes.bool, hasMore: PropTypes.bool,
blockedBy : PropTypes.bool, blockedBy: PropTypes.bool,
isAccount : PropTypes.bool, isAccount: PropTypes.bool,
multiColumn : PropTypes.bool, multiColumn: PropTypes.bool,
}; };
componentWillMount() { componentWillMount () {
if (!this.props.accountIds) { if (!this.props.accountIds) {
this.props.dispatch(fetchAccount(this.props.params.accountId)); this.props.dispatch(fetchAccount(this.props.params.accountId));
this.props.dispatch(fetchFollowers(this.props.params.accountId)); this.props.dispatch(fetchFollowers(this.props.params.accountId));
} }
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
this.props.dispatch(fetchAccount(nextProps.params.accountId)); this.props.dispatch(fetchAccount(nextProps.params.accountId));
this.props.dispatch(fetchFollowers(nextProps.params.accountId)); this.props.dispatch(fetchFollowers(nextProps.params.accountId));
@ -53,35 +57,29 @@ class Followers extends ImmutablePureComponent {
this.props.dispatch(expandFollowers(this.props.params.accountId)); this.props.dispatch(expandFollowers(this.props.params.accountId));
}, 300, { leading: true }); }, 300, { leading: true });
render() { render () {
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn } = this.props; const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn } = this.props;
if (!isAccount) { if (!isAccount) {
return ( return (
<Column > <Column>
<MissingIndicator /> <MissingIndicator />
</Column > </Column>
); );
} }
if (!accountIds) { if (!accountIds) {
return ( return (
<Column > <Column>
<LoadingIndicator /> <LoadingIndicator />
</Column > </Column>
); );
} }
const emptyMessage = blockedBy ? (<FormattedMessage const emptyMessage = blockedBy ? <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' /> : <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />;
id='empty_column.account_unavailable'
defaultMessage='Profile unavailable'
/>) : (<FormattedMessage
id='account.followers.empty'
defaultMessage='No one follows this user yet.'
/>);
return ( return (
<Column > <Column>
<ColumnBackButton multiColumn={multiColumn} /> <ColumnBackButton multiColumn={multiColumn} />
<ScrollableList <ScrollableList
@ -89,23 +87,16 @@ class Followers extends ImmutablePureComponent {
hasMore={hasMore} hasMore={hasMore}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll} shouldUpdateScroll={shouldUpdateScroll}
prepend={<HeaderContainer prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
accountId={this.props.params.accountId}
hideTabs
/>}
alwaysPrepend alwaysPrepend
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
> >
{blockedBy ? [] : accountIds.map(id => {blockedBy ? [] : accountIds.map(id =>
(<AccountContainer <AccountContainer key={id} id={id} withNote={false} />,
key={id}
id={id}
withNote={false}
/>),
)} )}
</ScrollableList > </ScrollableList>
</Column > </Column>
); );
} }

View File

@ -94,6 +94,10 @@ class Content extends ImmutablePureComponent {
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { } 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); link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
} else { } 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.setAttribute('title', link.href);
link.classList.add('unhandled-link'); link.classList.add('unhandled-link');
} }
@ -119,6 +123,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 }) => { handleEmojiMouseEnter = ({ target }) => {
target.src = target.getAttribute('data-original'); target.src = target.getAttribute('data-original');
}; };

View File

@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
import { fetchReblogs } from '../../actions/interactions'; import { fetchReblogs } from '../../actions/interactions';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
@ -25,15 +25,15 @@ export default @connect(mapStateToProps)
class Reblogs extends ImmutablePureComponent { class Reblogs extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params : PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch : PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func, shouldUpdateScroll: PropTypes.func,
accountIds : ImmutablePropTypes.list, accountIds: ImmutablePropTypes.list,
multiColumn : PropTypes.bool, multiColumn: PropTypes.bool,
intl : PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
componentWillMount() { componentWillMount () {
if (!this.props.accountIds) { if (!this.props.accountIds) {
this.props.dispatch(fetchReblogs(this.props.params.statusId)); this.props.dispatch(fetchReblogs(this.props.params.statusId));
} }
@ -47,23 +47,20 @@ class Reblogs extends ImmutablePureComponent {
handleRefresh = () => { handleRefresh = () => {
this.props.dispatch(fetchReblogs(this.props.params.statusId)); this.props.dispatch(fetchReblogs(this.props.params.statusId));
}; }
render() { render () {
const { intl, shouldUpdateScroll, accountIds, multiColumn } = this.props; const { intl, shouldUpdateScroll, accountIds, multiColumn } = this.props;
if (!accountIds) { if (!accountIds) {
return ( return (
<Column > <Column>
<LoadingIndicator /> <LoadingIndicator />
</Column > </Column>
); );
} }
const emptyMessage = (<FormattedMessage const emptyMessage = <FormattedMessage id='status.reblogs.empty' defaultMessage='No one has boosted this toot yet. When someone does, they will show up here.' />;
id='status.reblogs.empty'
defaultMessage='No one has boosted this toot yet. When someone does, they will show up here.'
/>);
return ( return (
<Column bindToDocument={!multiColumn}> <Column bindToDocument={!multiColumn}>
@ -71,12 +68,7 @@ class Reblogs extends ImmutablePureComponent {
showBackButton showBackButton
multiColumn={multiColumn} multiColumn={multiColumn}
extraButton={( extraButton={(
<button <button className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button>
className='column-header__button'
title={intl.formatMessage(messages.refresh)}
aria-label={intl.formatMessage(messages.refresh)}
onClick={this.handleRefresh}
><Icon id='refresh' /></button >
)} )}
/> />
@ -87,14 +79,10 @@ class Reblogs extends ImmutablePureComponent {
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
> >
{accountIds.map(id => {accountIds.map(id =>
(<AccountContainer <AccountContainer key={id} id={id} withNote={false} />,
key={id}
id={id}
withNote={false}
/>),
)} )}
</ScrollableList > </ScrollableList>
</Column > </Column>
); );
} }

View File

@ -5,37 +5,37 @@ import IconButton from '../../../components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { isStaff, me } from '../../../initial_state'; import { me, isStaff } from '../../../initial_state';
const messages = defineMessages({ const messages = defineMessages({
delete : { id: 'status.delete', defaultMessage: 'Delete' }, delete: { id: 'status.delete', defaultMessage: 'Delete' },
redraft : { id: 'status.redraft', defaultMessage: 'Delete & re-draft' }, redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
direct : { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention : { id: 'status.mention', defaultMessage: 'Mention @{name}' }, mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
reply : { id: 'status.reply', defaultMessage: 'Reply' }, reply: { id: 'status.reply', defaultMessage: 'Reply' },
reblog : { id: 'status.reblog', defaultMessage: 'Boost' }, reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private : { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' }, reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog : { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite : { id: 'status.favourite', defaultMessage: 'Favourite' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
bookmark : { id: 'status.bookmark', defaultMessage: 'Bookmark' }, bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
more : { id: 'status.more', defaultMessage: 'More' }, more: { id: 'status.more', defaultMessage: 'More' },
mute : { id: 'status.mute', defaultMessage: 'Mute @{name}' }, mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' },
muteConversation : { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation : { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
block : { id: 'status.block', defaultMessage: 'Block @{name}' }, block: { id: 'status.block', defaultMessage: 'Block @{name}' },
report : { id: 'status.report', defaultMessage: 'Report @{name}' }, report: { id: 'status.report', defaultMessage: 'Report @{name}' },
share : { id: 'status.share', defaultMessage: 'Share' }, share: { id: 'status.share', defaultMessage: 'Share' },
pin : { id: 'status.pin', defaultMessage: 'Pin on profile' }, pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin : { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed : { id: 'status.embed', defaultMessage: 'Embed' }, embed: { id: 'status.embed', defaultMessage: 'Embed' },
admin_account : { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, 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' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
copy : { id: 'status.copy', defaultMessage: 'Copy link to status' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
blockDomain : { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
unblockDomain : { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
unmute : { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unblock : { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
}); });
const mapStateToProps = (state, { status }) => ({ const mapStateToProps = (state, { status }) => ({
@ -51,59 +51,59 @@ class ActionBar extends React.PureComponent {
}; };
static propTypes = { static propTypes = {
status : ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
relationship : ImmutablePropTypes.map, relationship: ImmutablePropTypes.map,
onReply : PropTypes.func.isRequired, onReply: PropTypes.func.isRequired,
onReblog : PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired,
onFavourite : PropTypes.func.isRequired, onFavourite: PropTypes.func.isRequired,
onBookmark : PropTypes.func.isRequired, onBookmark: PropTypes.func.isRequired,
onDelete : PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onDirect : PropTypes.func.isRequired, onDirect: PropTypes.func.isRequired,
onMention : PropTypes.func.isRequired, onMention: PropTypes.func.isRequired,
onMute : PropTypes.func, onMute: PropTypes.func,
onUnmute : PropTypes.func, onUnmute: PropTypes.func,
onBlock : PropTypes.func, onBlock: PropTypes.func,
onUnblock : PropTypes.func, onUnblock: PropTypes.func,
onBlockDomain : PropTypes.func, onBlockDomain: PropTypes.func,
onUnblockDomain : PropTypes.func, onUnblockDomain: PropTypes.func,
onMuteConversation: PropTypes.func, onMuteConversation: PropTypes.func,
onReport : PropTypes.func, onReport: PropTypes.func,
onPin : PropTypes.func, onPin: PropTypes.func,
onEmbed : PropTypes.func, onEmbed: PropTypes.func,
intl : PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
handleReplyClick = () => { handleReplyClick = () => {
this.props.onReply(this.props.status); this.props.onReply(this.props.status);
}; }
handleReblogClick = (e) => { handleReblogClick = (e) => {
this.props.onReblog(this.props.status, e); this.props.onReblog(this.props.status, e);
}; }
handleFavouriteClick = () => { handleFavouriteClick = () => {
this.props.onFavourite(this.props.status); this.props.onFavourite(this.props.status);
}; }
handleBookmarkClick = (e) => { handleBookmarkClick = (e) => {
this.props.onBookmark(this.props.status, e); this.props.onBookmark(this.props.status, e);
}; }
handleDeleteClick = () => { handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.context.router.history); this.props.onDelete(this.props.status, this.context.router.history);
}; }
handleRedraftClick = () => { handleRedraftClick = () => {
this.props.onDelete(this.props.status, this.context.router.history, true); this.props.onDelete(this.props.status, this.context.router.history, true);
}; }
handleDirectClick = () => { handleDirectClick = () => {
this.props.onDirect(this.props.status.get('account'), this.context.router.history); this.props.onDirect(this.props.status.get('account'), this.context.router.history);
}; }
handleMentionClick = () => { handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.context.router.history); this.props.onMention(this.props.status.get('account'), this.context.router.history);
}; }
handleMuteClick = () => { handleMuteClick = () => {
const { status, relationship, onMute, onUnmute } = this.props; const { status, relationship, onMute, onUnmute } = this.props;
@ -114,7 +114,7 @@ class ActionBar extends React.PureComponent {
} else { } else {
onMute(account); onMute(account);
} }
}; }
handleBlockClick = () => { handleBlockClick = () => {
const { status, relationship, onBlock, onUnblock } = this.props; const { status, relationship, onBlock, onUnblock } = this.props;
@ -125,50 +125,50 @@ class ActionBar extends React.PureComponent {
} else { } else {
onBlock(status); onBlock(status);
} }
}; }
handleBlockDomain = () => { handleBlockDomain = () => {
const { status, onBlockDomain } = this.props; const { status, onBlockDomain } = this.props;
const account = status.get('account'); const account = status.get('account');
onBlockDomain(account.get('acct').split('@')[1]); onBlockDomain(account.get('acct').split('@')[1]);
}; }
handleUnblockDomain = () => { handleUnblockDomain = () => {
const { status, onUnblockDomain } = this.props; const { status, onUnblockDomain } = this.props;
const account = status.get('account'); const account = status.get('account');
onUnblockDomain(account.get('acct').split('@')[1]); onUnblockDomain(account.get('acct').split('@')[1]);
}; }
handleConversationMuteClick = () => { handleConversationMuteClick = () => {
this.props.onMuteConversation(this.props.status); this.props.onMuteConversation(this.props.status);
}; }
handleReport = () => { handleReport = () => {
this.props.onReport(this.props.status); this.props.onReport(this.props.status);
}; }
handlePinClick = () => { handlePinClick = () => {
this.props.onPin(this.props.status); this.props.onPin(this.props.status);
}; }
handleShare = () => { handleShare = () => {
navigator.share({ navigator.share({
text: this.props.status.get('search_index'), text: this.props.status.get('search_index'),
url : this.props.status.get('url'), url: this.props.status.get('url'),
}); });
}; }
handleEmbed = () => { handleEmbed = () => {
this.props.onEmbed(this.props.status); this.props.onEmbed(this.props.status);
}; }
handleCopy = () => { handleCopy = () => {
const url = this.props.status.get('url'); const url = this.props.status.get('url');
const textarea = document.createElement('textarea'); const textarea = document.createElement('textarea');
textarea.textContent = url; textarea.textContent = url;
textarea.style.position = 'fixed'; textarea.style.position = 'fixed';
document.body.appendChild(textarea); document.body.appendChild(textarea);
@ -181,14 +181,14 @@ class ActionBar extends React.PureComponent {
} finally { } finally {
document.body.removeChild(textarea); document.body.removeChild(textarea);
} }
}; }
render() { render () {
const { status, relationship, intl } = this.props; const { status, relationship, intl } = this.props;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const mutingConversation = status.get('muted'); const mutingConversation = status.get('muted');
const account = status.get('account'); const account = status.get('account');
let menu = []; let menu = [];
@ -200,66 +200,36 @@ class ActionBar extends React.PureComponent {
if (me === status.getIn(['account', 'id'])) { if (me === status.getIn(['account', 'id'])) {
if (publicStatus) { if (publicStatus) {
menu.push({ menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
text : intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin),
action: this.handlePinClick,
});
} else { } else {
if (status.get('visibility') === 'private') { if (status.get('visibility') === 'private') {
menu.push({ menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick });
text : intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private),
action: this.handleReblogClick,
});
} }
} }
menu.push(null); menu.push(null);
menu.push({ menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
text : intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation),
action: this.handleConversationMuteClick,
});
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
} else { } else {
menu.push({ menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
text : intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
action: this.handleMentionClick,
});
menu.push({
text : intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }),
action: this.handleDirectClick,
});
menu.push(null); menu.push(null);
if (relationship && relationship.get('muting')) { if (relationship && relationship.get('muting')) {
menu.push({ menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
text : intl.formatMessage(messages.unmute, { name: account.get('username') }),
action: this.handleMuteClick,
});
} else { } else {
menu.push({ menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick });
text : intl.formatMessage(messages.mute, { name: account.get('username') }),
action: this.handleMuteClick,
});
} }
if (relationship && relationship.get('blocking')) { if (relationship && relationship.get('blocking')) {
menu.push({ menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
text : intl.formatMessage(messages.unblock, { name: account.get('username') }),
action: this.handleBlockClick,
});
} else { } else {
menu.push({ menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick });
text : intl.formatMessage(messages.block, { name: account.get('username') }),
action: this.handleBlockClick,
});
} }
menu.push({ menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
text : intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }),
action: this.handleReport,
});
if (account.get('acct') !== account.get('username')) { if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1]; const domain = account.get('acct').split('@')[1];
@ -275,23 +245,13 @@ class ActionBar extends React.PureComponent {
if (isStaff) { if (isStaff) {
menu.push(null); menu.push(null);
menu.push({ menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
href: `/admin/accounts/${status.getIn(['account', 'id'])}`,
});
menu.push({
text: intl.formatMessage(messages.admin_status),
href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}`,
});
} }
} }
const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && ( const shareButton = ('share' in navigator) && publicStatus && (
<div className='detailed-status__button'><IconButton <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShare} /></div>
title={intl.formatMessage(messages.share)}
icon='share-alt'
onClick={this.handleShare}
/></div >
); );
let replyIcon; let replyIcon;
@ -305,50 +265,18 @@ class ActionBar extends React.PureComponent {
if (status.get('visibility') === 'direct') reblogIcon = 'envelope'; if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
else if (status.get('visibility') === 'private') reblogIcon = 'lock'; else if (status.get('visibility') === 'private') reblogIcon = 'lock';
let reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private');
return ( return (
<div className='detailed-status__action-bar'> <div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
title={intl.formatMessage(messages.reply)} <div className='detailed-status__button'><IconButton disabled={!publicStatus} active={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
onClick={this.handleReplyClick}
/></div >
<div className='detailed-status__button'><IconButton
disabled={!publicStatus}
active={status.get('reblogged')}
title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)}
icon={reblogIcon}
onClick={this.handleReblogClick}
/></div >
<div className='detailed-status__button'><IconButton
className='star-icon'
animate
active={status.get('favourited')}
title={intl.formatMessage(messages.favourite)}
icon='star'
onClick={this.handleFavouriteClick}
/></div >
{shareButton} {shareButton}
<div className='detailed-status__button'><IconButton <div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
className='bookmark-icon'
active={status.get('bookmarked')}
title={intl.formatMessage(messages.bookmark)}
icon='bookmark'
onClick={this.handleBookmarkClick}
/></div >
<div className='detailed-status__action-bar-dropdown'> <div className='detailed-status__action-bar-dropdown'>
<DropdownMenuContainer <DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />
size={18} </div>
icon='ellipsis-h' </div>
status={status}
items={menu}
direction='left'
title={intl.formatMessage(messages.more)}
/>
</div >
</div >
); );
} }

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { fromJS, is } from 'immutable'; import { fromJS, is } from 'immutable';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { exitFullscreen, isFullscreen, requestFullscreen } from '../ui/util/fullscreen'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
import { displayMedia, useBlurhash } from '../../initial_state'; import { displayMedia, useBlurhash } from '../../initial_state';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import { decode } from 'blurhash'; import { decode } from 'blurhash';
@ -112,41 +112,28 @@ class Video extends React.PureComponent {
}; };
state = { state = {
currentTime : 0, currentTime: 0,
duration : 0, duration: 0,
volume : 0.5, volume: 0.5,
paused : true, paused: true,
dragging : false, dragging: false,
containerWidth: this.props.width, containerWidth: this.props.width,
fullscreen : false, fullscreen: false,
hovered : false, hovered: false,
muted : false, muted: false,
revealed : this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'), revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
}; };
// Hard-coded in components.scss // Hard-coded in components.scss
// Any way to get ::before values programatically? // Any way to get ::before values programatically?
volWidth = 50; volWidth = 50;
volOffset = 70; volOffset = 70;
handleScroll = throttle(() => {
if (!this.video) {
return;
}
const { top, height } = this.video.getBoundingClientRect();
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
if (!this.state.paused && !inView) {
this.setState({ paused: true }, () => this.video.pause());
}
}, 150, { trailing: true });
volHandleOffset = v => { volHandleOffset = v => {
const offset = v * this.volWidth + this.volOffset; const offset = v * this.volWidth + this.volOffset;
return (offset > 110) ? 110 : offset; return (offset > 110) ? 110 : offset;
}; }
setPlayerRef = c => { setPlayerRef = c => {
this.player = c; this.player = c;
@ -158,7 +145,7 @@ class Video extends React.PureComponent {
containerWidth: c.offsetWidth, containerWidth: c.offsetWidth,
}); });
} }
}; }
setVideoRef = c => { setVideoRef = c => {
this.video = c; this.video = c;
@ -166,36 +153,36 @@ class Video extends React.PureComponent {
if (this.video) { if (this.video) {
this.setState({ volume: this.video.volume, muted: this.video.muted }); this.setState({ volume: this.video.volume, muted: this.video.muted });
} }
}; }
setSeekRef = c => { setSeekRef = c => {
this.seek = c; this.seek = c;
}; }
setVolumeRef = c => { setVolumeRef = c => {
this.volume = c; this.volume = c;
}; }
handleClickRoot = e => e.stopPropagation();
setCanvasRef = c => { setCanvasRef = c => {
this.canvas = c; this.canvas = c;
}; }
handleClickRoot = e => e.stopPropagation();
handlePlay = () => { handlePlay = () => {
this.setState({ paused: false }); this.setState({ paused: false });
}; }
handlePause = () => { handlePause = () => {
this.setState({ paused: true }); this.setState({ paused: true });
}; }
handleTimeUpdate = () => { handleTimeUpdate = () => {
this.setState({ this.setState({
currentTime: Math.floor(this.video.currentTime), currentTime: Math.floor(this.video.currentTime),
duration : Math.floor(this.video.duration), duration: Math.floor(this.video.duration),
}); });
}; }
handleVolumeMouseDown = e => { handleVolumeMouseDown = e => {
document.addEventListener('mousemove', this.handleMouseVolSlide, true); document.addEventListener('mousemove', this.handleMouseVolSlide, true);
@ -207,7 +194,14 @@ class Video extends React.PureComponent {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
}; }
handleVolumeMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseVolSlide, true);
document.removeEventListener('mouseup', this.handleVolumeMouseUp, true);
document.removeEventListener('touchmove', this.handleMouseVolSlide, true);
document.removeEventListener('touchend', this.handleVolumeMouseUp, true);
}
handleMouseVolSlide = throttle(e => { handleMouseVolSlide = throttle(e => {
const rect = this.volume.getBoundingClientRect(); const rect = this.volume.getBoundingClientRect();
@ -218,7 +212,7 @@ class Video extends React.PureComponent {
if(x > 1) { if(x > 1) {
slideamt = 1; slideamt = 1;
} else if (x < 0) { } else if(x < 0) {
slideamt = 0; slideamt = 0;
} }
@ -227,13 +221,6 @@ class Video extends React.PureComponent {
} }
}, 60); }, 60);
handleVolumeMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseVolSlide, true);
document.removeEventListener('mouseup', this.handleVolumeMouseUp, true);
document.removeEventListener('touchmove', this.handleMouseVolSlide, true);
document.removeEventListener('touchend', this.handleVolumeMouseUp, true);
};
handleMouseDown = e => { handleMouseDown = e => {
document.addEventListener('mousemove', this.handleMouseMove, true); document.addEventListener('mousemove', this.handleMouseMove, true);
document.addEventListener('mouseup', this.handleMouseUp, true); document.addEventListener('mouseup', this.handleMouseUp, true);
@ -246,7 +233,17 @@ class Video extends React.PureComponent {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
}; }
handleMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseMove, true);
document.removeEventListener('mouseup', this.handleMouseUp, true);
document.removeEventListener('touchmove', this.handleMouseMove, true);
document.removeEventListener('touchend', this.handleMouseUp, true);
this.setState({ dragging: false });
this.video.play();
}
handleMouseMove = throttle(e => { handleMouseMove = throttle(e => {
const { x } = getPointerPosition(this.seek, e); const { x } = getPointerPosition(this.seek, e);
@ -258,25 +255,23 @@ class Video extends React.PureComponent {
} }
}, 60); }, 60);
handleMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseMove, true);
document.removeEventListener('mouseup', this.handleMouseUp, true);
document.removeEventListener('touchmove', this.handleMouseMove, true);
document.removeEventListener('touchend', this.handleMouseUp, true);
this.setState({ dragging: false });
this.video.play();
};
togglePlay = () => { togglePlay = () => {
if (this.state.paused) { if (this.state.paused) {
this.setState({ paused: false }, () => this.video.play()); this.setState({ paused: false }, () => this.video.play());
} else { } else {
this.setState({ paused: true }, () => this.video.pause()); this.setState({ paused: true }, () => this.video.pause());
} }
}; }
componentDidMount() { toggleFullscreen = () => {
if (isFullscreen()) {
exitFullscreen();
} else {
requestFullscreen(this.player);
}
}
componentDidMount () {
document.addEventListener('fullscreenchange', this.handleFullscreenChange, true); document.addEventListener('fullscreenchange', this.handleFullscreenChange, true);
document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true); document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true); document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
@ -321,32 +316,37 @@ class Video extends React.PureComponent {
const pixels = decode(hash, 32, 32); const pixels = decode(hash, 32, 32);
if (pixels) { if (pixels) {
const ctx = this.canvas.getContext('2d'); const ctx = this.canvas.getContext('2d');
const imageData = new ImageData(pixels, 32, 32); const imageData = new ImageData(pixels, 32, 32);
ctx.putImageData(imageData, 0, 0); ctx.putImageData(imageData, 0, 0);
} }
} }
toggleFullscreen = () => { handleScroll = throttle(() => {
if (isFullscreen()) { if (!this.video) {
exitFullscreen(); return;
} else {
requestFullscreen(this.player);
} }
};
const { top, height } = this.video.getBoundingClientRect();
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
if (!this.state.paused && !inView) {
this.setState({ paused: true }, () => this.video.pause());
}
}, 150, { trailing: true })
handleFullscreenChange = () => { handleFullscreenChange = () => {
this.setState({ fullscreen: isFullscreen() }); this.setState({ fullscreen: isFullscreen() });
}; }
handleMouseEnter = () => { handleMouseEnter = () => {
this.setState({ hovered: true }); this.setState({ hovered: true });
}; }
handleMouseLeave = () => { handleMouseLeave = () => {
this.setState({ hovered: false }); this.setState({ hovered: false });
}; }
toggleMute = () => { toggleMute = () => {
const muted = !this.video.muted; const muted = !this.video.muted;
@ -354,7 +354,7 @@ class Video extends React.PureComponent {
this.setState({ muted }, () => { this.setState({ muted }, () => {
this.video.muted = muted; this.video.muted = muted;
}); });
}; }
toggleReveal = () => { toggleReveal = () => {
if (this.props.onToggleVisibility) { if (this.props.onToggleVisibility) {
@ -362,31 +362,31 @@ class Video extends React.PureComponent {
} else { } else {
this.setState({ revealed: !this.state.revealed }); this.setState({ revealed: !this.state.revealed });
} }
}; }
handleLoadedData = () => { handleLoadedData = () => {
if (this.props.startTime) { if (this.props.startTime) {
this.video.currentTime = this.props.startTime; this.video.currentTime = this.props.startTime;
this.video.play(); this.video.play();
} }
}; }
handleProgress = () => { handleProgress = () => {
if (this.video.buffered.length > 0) { if (this.video.buffered.length > 0) {
this.setState({ buffer: this.video.buffered.end(0) / this.video.duration * 100 }); this.setState({ buffer: this.video.buffered.end(0) / this.video.duration * 100 });
} }
}; }
handleVolumeChange = () => { handleVolumeChange = () => {
this.setState({ volume: this.video.volume, muted: this.video.muted }); this.setState({ volume: this.video.volume, muted: this.video.muted });
}; }
handleOpenVideo = () => { handleOpenVideo = () => {
const { src, preview, width, height, alt } = this.props; const { src, preview, width, height, alt } = this.props;
const media = fromJS({ const media = fromJS({
type : 'video', type: 'video',
url : src, url: src,
preview_url: preview, preview_url: preview,
description: alt, description: alt,
width, width,
@ -395,12 +395,12 @@ class Video extends React.PureComponent {
this.video.pause(); this.video.pause();
this.props.onOpenVideo(media, this.video.currentTime); this.props.onOpenVideo(media, this.video.currentTime);
}; }
handleCloseVideo = () => { handleCloseVideo = () => {
this.video.pause(); this.video.pause();
this.props.onCloseVideo(); this.props.onCloseVideo();
}; }
render () { render () {
const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable } = this.props; const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable } = this.props;
@ -492,8 +492,8 @@ class Video extends React.PureComponent {
<div className='video-player__buttons-bar'> <div className='video-player__buttons-bar'>
<div className='video-player__buttons left'> <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' 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' 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(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}> <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
&nbsp; &nbsp;
@ -517,25 +517,11 @@ class Video extends React.PureComponent {
</div> </div>
<div className='video-player__buttons right'> <div className='video-player__buttons right'>
{(!onCloseVideo && !editable && !fullscreen) && <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>}
type='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>}
aria-label={intl.formatMessage(messages.hide)} {onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
onClick={this.toggleReveal} <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>
><Icon <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>
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>
</div> </div>
</div> </div>
</div> </div>

View File

@ -254,7 +254,7 @@
"lists.subheading": "Your lists", "lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Зареждане...", "loading_indicator.label": "Зареждане...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Hide media",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found", "missing_indicator.sublabel": "This resource could not be found",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Hide notifications from this user?",

View File

@ -254,7 +254,7 @@
"lists.subheading": "Your lists", "lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading...", "loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Hide media",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found", "missing_indicator.sublabel": "This resource could not be found",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Hide notifications from this user?",

View File

@ -142,7 +142,7 @@
{ {
"descriptors": [ "descriptors": [
{ {
"defaultMessage": "Unhide {domain}", "defaultMessage": "Unblock domain {domain}",
"id": "account.unblock_domain" "id": "account.unblock_domain"
} }
], ],
@ -217,7 +217,7 @@
{ {
"descriptors": [ "descriptors": [
{ {
"defaultMessage": "Toggle visibility", "defaultMessage": "Hide media",
"id": "media_gallery.toggle_visible" "id": "media_gallery.toggle_visible"
}, },
{ {
@ -451,11 +451,11 @@
"id": "status.copy" "id": "status.copy"
}, },
{ {
"defaultMessage": "Hide everything from {domain}", "defaultMessage": "Block domain {domain}",
"id": "account.block_domain" "id": "account.block_domain"
}, },
{ {
"defaultMessage": "Unhide {domain}", "defaultMessage": "Unblock domain {domain}",
"id": "account.unblock_domain" "id": "account.unblock_domain"
}, },
{ {
@ -697,11 +697,11 @@
"id": "account.media" "id": "account.media"
}, },
{ {
"defaultMessage": "Hide everything from {domain}", "defaultMessage": "Block domain {domain}",
"id": "account.block_domain" "id": "account.block_domain"
}, },
{ {
"defaultMessage": "Unhide {domain}", "defaultMessage": "Unblock domain {domain}",
"id": "account.unblock_domain" "id": "account.unblock_domain"
}, },
{ {
@ -1073,7 +1073,7 @@
"id": "privacy.public.short" "id": "privacy.public.short"
}, },
{ {
"defaultMessage": "Post to public timelines", "defaultMessage": "Visible for all, shown in public timelines",
"id": "privacy.public.long" "id": "privacy.public.long"
}, },
{ {
@ -1081,7 +1081,7 @@
"id": "privacy.unlisted.short" "id": "privacy.unlisted.short"
}, },
{ {
"defaultMessage": "Do not show in public timelines", "defaultMessage": "Visible for all, but not in public timelines",
"id": "privacy.unlisted.long" "id": "privacy.unlisted.long"
}, },
{ {
@ -1089,7 +1089,7 @@
"id": "privacy.private.short" "id": "privacy.private.short"
}, },
{ {
"defaultMessage": "Post to followers only", "defaultMessage": "Visible for followers only",
"id": "privacy.private.long" "id": "privacy.private.long"
}, },
{ {
@ -1097,7 +1097,7 @@
"id": "privacy.direct.short" "id": "privacy.direct.short"
}, },
{ {
"defaultMessage": "Post to mentioned users only", "defaultMessage": "Visible for mentioned users only",
"id": "privacy.direct.long" "id": "privacy.direct.long"
}, },
{ {
@ -1470,7 +1470,7 @@
"id": "column.domain_blocks" "id": "column.domain_blocks"
}, },
{ {
"defaultMessage": "Unhide {domain}", "defaultMessage": "Unblock domain {domain}",
"id": "account.unblock_domain" "id": "account.unblock_domain"
}, },
{ {
@ -2384,11 +2384,11 @@
"id": "status.copy" "id": "status.copy"
}, },
{ {
"defaultMessage": "Hide everything from {domain}", "defaultMessage": "Block domain {domain}",
"id": "account.block_domain" "id": "account.block_domain"
}, },
{ {
"defaultMessage": "Unhide {domain}", "defaultMessage": "Unblock domain {domain}",
"id": "account.unblock_domain" "id": "account.unblock_domain"
}, },
{ {
@ -2944,4 +2944,4 @@
], ],
"path": "app/javascript/mastodon/features/video/index.json" "path": "app/javascript/mastodon/features/video/index.json"
} }
] ]

View File

@ -3,7 +3,7 @@
"account.badges.bot": "Bot", "account.badges.bot": "Bot",
"account.badges.group": "Group", "account.badges.group": "Group",
"account.block": "Block @{name}", "account.block": "Block @{name}",
"account.block_domain": "Hide everything from {domain}", "account.block_domain": "Block domain {domain}",
"account.blocked": "Blocked", "account.blocked": "Blocked",
"account.cancel_follow_request": "Cancel follow request", "account.cancel_follow_request": "Cancel follow request",
"account.direct": "Direct message @{name}", "account.direct": "Direct message @{name}",
@ -34,7 +34,7 @@
"account.share": "Share @{name}'s profile", "account.share": "Share @{name}'s profile",
"account.show_reblogs": "Show boosts from @{name}", "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "Unblock @{name}", "account.unblock": "Unblock @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unblock domain {domain}",
"account.unendorse": "Don't feature on profile", "account.unendorse": "Don't feature on profile",
"account.unfollow": "Unfollow", "account.unfollow": "Unfollow",
"account.unmute": "Unmute @{name}", "account.unmute": "Unmute @{name}",
@ -254,7 +254,7 @@
"lists.subheading": "Your lists", "lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading...", "loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Hide media",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found", "missing_indicator.sublabel": "This resource could not be found",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Hide notifications from this user?",
@ -319,13 +319,13 @@
"poll_button.add_poll": "Add a poll", "poll_button.add_poll": "Add a poll",
"poll_button.remove_poll": "Remove poll", "poll_button.remove_poll": "Remove poll",
"privacy.change": "Adjust status privacy", "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.direct.short": "Direct",
"privacy.private.long": "Post to followers only", "privacy.private.long": "Visible for followers only",
"privacy.private.short": "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.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", "privacy.unlisted.short": "Unlisted",
"refresh": "Refresh", "refresh": "Refresh",
"regeneration_indicator.label": "Loading…", "regeneration_indicator.label": "Loading…",

View File

@ -254,7 +254,7 @@
"lists.subheading": "Your lists", "lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading...", "loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Hide media",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found", "missing_indicator.sublabel": "This resource could not be found",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Hide notifications from this user?",

View File

@ -254,7 +254,7 @@
"lists.subheading": "Your lists", "lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "लोड हो रहा है...", "loading_indicator.label": "लोड हो रहा है...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Hide media",
"missing_indicator.label": "नहीं मिला", "missing_indicator.label": "नहीं मिला",
"missing_indicator.sublabel": "यह संसाधन नहीं मिल सका।", "missing_indicator.sublabel": "यह संसाधन नहीं मिल सका।",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Hide notifications from this user?",

View File

@ -254,7 +254,7 @@
"lists.subheading": "Your lists", "lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading...", "loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Hide media",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found", "missing_indicator.sublabel": "This resource could not be found",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Hide notifications from this user?",

View File

@ -254,7 +254,7 @@
"lists.subheading": "Your lists", "lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading...", "loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Hide media",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found", "missing_indicator.sublabel": "This resource could not be found",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Hide notifications from this user?",

View File

@ -254,7 +254,7 @@
"lists.subheading": "Your lists", "lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading...", "loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Hide media",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found", "missing_indicator.sublabel": "This resource could not be found",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Hide notifications from this user?",

View File

@ -254,7 +254,7 @@
"lists.subheading": "Your lists", "lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading...", "loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Hide media",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found", "missing_indicator.sublabel": "This resource could not be found",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Hide notifications from this user?",

View File

@ -254,7 +254,7 @@
"lists.subheading": "Your lists", "lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading...", "loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Hide media",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found", "missing_indicator.sublabel": "This resource could not be found",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Hide notifications from this user?",

View File

@ -254,7 +254,7 @@
"lists.subheading": "Your lists", "lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading...", "loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Hide media",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found", "missing_indicator.sublabel": "This resource could not be found",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Hide notifications from this user?",

View File

@ -254,7 +254,7 @@
"lists.subheading": "Your lists", "lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading...", "loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Hide media",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found", "missing_indicator.sublabel": "This resource could not be found",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Hide notifications from this user?",

View File

@ -254,7 +254,7 @@
"lists.subheading": "Your lists", "lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading...", "loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Hide media",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found", "missing_indicator.sublabel": "This resource could not be found",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Hide notifications from this user?",

View File

@ -388,8 +388,8 @@
.emoji-picker-dropdown { .emoji-picker-dropdown {
position: absolute; position: absolute;
top: 5px; top: 0;
right: 5px; right: 0;
} }
.compose-form__autosuggest-wrapper { .compose-form__autosuggest-wrapper {
@ -870,6 +870,7 @@
.announcements__item__content { .announcements__item__content {
word-wrap: break-word; word-wrap: break-word;
overflow-y: auto;
.emojione { .emojione {
width: 20px; width: 20px;
@ -4070,10 +4071,7 @@ a.status-card.compact:hover {
.emoji-button { .emoji-button {
display: block; display: block;
font-size: 24px; padding: 5px 5px 2px 2px;
line-height: 24px;
margin-left: 2px;
width: 24px;
outline: 0; outline: 0;
cursor: pointer; cursor: pointer;
@ -4089,7 +4087,6 @@ a.status-card.compact:hover {
margin: 0; margin: 0;
width: 22px; width: 22px;
height: 22px; height: 22px;
margin-top: 2px;
} }
&:hover, &:hover,
@ -5068,12 +5065,6 @@ a.status-card.compact:hover {
} }
.media-gallery__gifv { .media-gallery__gifv {
&.autoplay {
.media-gallery__gifv__label {
display: none;
}
}
&:hover { &:hover {
.media-gallery__gifv__label { .media-gallery__gifv__label {
opacity: 1; opacity: 1;
@ -6699,17 +6690,21 @@ noscript {
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
padding: 15px; padding: 15px;
padding-right: 15px + 18px;
position: relative; position: relative;
font-size: 15px; font-size: 15px;
line-height: 20px; line-height: 20px;
word-wrap: break-word; word-wrap: break-word;
font-weight: 400; font-weight: 400;
max-height: 50vh;
overflow: hidden;
display: flex;
flex-direction: column;
&__range { &__range {
display: block; display: block;
font-weight: 500; font-weight: 500;
margin-bottom: 10px; margin-bottom: 10px;
padding-right: 18px;
} }
&__unread { &__unread {

View File

@ -8,6 +8,7 @@ module Mastodon
class LengthValidationError < ValidationError; end class LengthValidationError < ValidationError; end
class DimensionsValidationError < ValidationError; end class DimensionsValidationError < ValidationError; end
class RaceConditionError < Error; end class RaceConditionError < Error; end
class RateLimitExceededError < Error; end
class UnexpectedResponseError < Error class UnexpectedResponseError < Error
def initialize(response = nil) def initialize(response = nil)

64
app/lib/rate_limiter.rb Normal file
View File

@ -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

View File

@ -102,6 +102,7 @@ class Account < ApplicationRecord
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) } 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 :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_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 :popular, -> { order('account_stats.followers_count desc') }
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) } 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) } scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
@ -478,7 +479,16 @@ class Account < ApplicationRecord
def from_text(text) def from_text(text)
return [] if text.blank? return [] if text.blank?
text.scan(MENTION_RE).map { |match| match.first.split('@', 2) }.uniq.map { |(username, domain)| EntityCache.instance.mention(username, domain) } text.scan(MENTION_RE).map { |match| match.first.split('@', 2) }.uniq.map do |(username, domain)|
domain = begin
if TagManager.instance.local_domain?(domain)
nil
else
TagManager.instance.normalize_domain(domain)
end
end
EntityCache.instance.mention(username, domain)
end.compact
end end
private private

View File

@ -14,6 +14,7 @@ class AccountFilter
email email
ip ip
staff staff
order
).freeze ).freeze
attr_reader :params attr_reader :params
@ -24,7 +25,7 @@ class AccountFilter
end end
def results def results
scope = Account.recent.includes(:user) scope = Account.includes(:user).reorder(nil)
params.each do |key, value| params.each do |key, value|
scope.merge!(scope_for(key, value.to_s.strip)) if value.present? scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
@ -38,6 +39,7 @@ class AccountFilter
def set_defaults! def set_defaults!
params['local'] = '1' if params['remote'].blank? params['local'] = '1' if params['remote'].blank?
params['active'] = '1' if params['suspended'].blank? && params['silenced'].blank? && params['pending'].blank? params['active'] = '1' if params['suspended'].blank? && params['silenced'].blank? && params['pending'].blank?
params['order'] = 'recent' if params['order'].blank?
end end
def scope_for(key, value) def scope_for(key, value)
@ -51,9 +53,9 @@ class AccountFilter
when 'active' when 'active'
Account.without_suspended Account.without_suspended
when 'pending' when 'pending'
accounts_with_users.merge User.pending accounts_with_users.merge(User.pending)
when 'disabled' when 'disabled'
accounts_with_users.merge User.disabled accounts_with_users.merge(User.disabled)
when 'silenced' when 'silenced'
Account.silenced Account.silenced
when 'suspended' when 'suspended'
@ -63,16 +65,31 @@ class AccountFilter
when 'display_name' when 'display_name'
Account.matches_display_name(value) Account.matches_display_name(value)
when 'email' when 'email'
accounts_with_users.merge User.matches_email(value) accounts_with_users.merge(User.matches_email(value))
when 'ip' when 'ip'
valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value)) : Account.none valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value)) : Account.none
when 'staff' when 'staff'
accounts_with_users.merge User.staff accounts_with_users.merge(User.staff)
when 'order'
order_scope(value)
else else
raise "Unknown filter: #{key}" raise "Unknown filter: #{key}"
end end
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 def accounts_with_users
Account.joins(:user) Account.joins(:user)
end end

View File

@ -48,6 +48,10 @@ class Announcement < ApplicationRecord
@mentions ||= Account.from_text(text) @mentions ||= Account.from_text(text)
end end
def statuses
@statuses ||= Status.from_text(text)
end
def tags def tags
@tags ||= Tag.find_or_create_by_names(Extractor.extract_hashtags(text)) @tags ||= Tag.find_or_create_by_names(Extractor.extract_hashtags(text))
end end

View File

@ -87,10 +87,10 @@ module AccountInteractions
has_many :announcement_mutes, dependent: :destroy has_many :announcement_mutes, dependent: :destroy
end 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? 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) .find_or_create_by!(target_account: other_account)
rel.update!(show_reblogs: reblogs) rel.update!(show_reblogs: reblogs)
@ -99,6 +99,18 @@ module AccountInteractions
rel rel
end 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) def block!(other_account, uri: nil)
remove_potential_friendship(other_account) remove_potential_friendship(other_account)
block_relationships.create_with(uri: uri) block_relationships.create_with(uri: uri)

View File

@ -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

View File

@ -15,6 +15,9 @@
class Follow < ApplicationRecord class Follow < ApplicationRecord
include Paginable include Paginable
include RelationshipCacheable include RelationshipCacheable
include RateLimitable
rate_limit by: :account, family: :follows
belongs_to :account belongs_to :account
belongs_to :target_account, class_name: 'Account' belongs_to :target_account, class_name: 'Account'

View File

@ -15,6 +15,9 @@
class FollowRequest < ApplicationRecord class FollowRequest < ApplicationRecord
include Paginable include Paginable
include RelationshipCacheable include RelationshipCacheable
include RateLimitable
rate_limit by: :account, family: :follows
belongs_to :account belongs_to :account
belongs_to :target_account, class_name: 'Account' belongs_to :target_account, class_name: 'Account'

View File

@ -170,6 +170,7 @@ class MediaAttachment < ApplicationRecord
def variant?(other_file_name) def variant?(other_file_name)
return true if file_file_name == 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 formats = file.styles.values.map(&:format).compact

View File

@ -32,6 +32,9 @@ class Status < ApplicationRecord
include Paginable include Paginable
include Cacheable include Cacheable
include StatusThreadingConcern include StatusThreadingConcern
include RateLimitable
rate_limit by: :account, family: :statuses
self.discard_column = :deleted_at self.discard_column = :deleted_at
@ -366,6 +369,21 @@ class Status < ApplicationRecord
end end
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 private
def timeline_scope(local_only = false) def timeline_scope(local_only = false)

View File

@ -8,4 +8,8 @@ class SettingsPolicy < ApplicationPolicy
def show? def show?
admin? admin?
end end
def destroy?
admin?
end
end end

View File

@ -7,6 +7,7 @@ class REST::AnnouncementSerializer < ActiveModel::Serializer
attribute :read, if: :current_user? attribute :read, if: :current_user?
has_many :mentions has_many :mentions
has_many :statuses
has_many :tags, serializer: REST::StatusSerializer::TagSerializer has_many :tags, serializer: REST::StatusSerializer::TagSerializer
has_many :emojis, serializer: REST::CustomEmojiSerializer has_many :emojis, serializer: REST::CustomEmojiSerializer
has_many :reactions, serializer: REST::ReactionSerializer has_many :reactions, serializer: REST::ReactionSerializer
@ -46,4 +47,16 @@ class REST::AnnouncementSerializer < ActiveModel::Serializer
object.pretty_acct object.pretty_acct
end end
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 end

View File

@ -171,7 +171,7 @@ class AccountSearchService < BaseService
end end
def username_complete? def username_complete?
query.include?('@') && "@#{query}" =~ Account::MENTION_RE query.include?('@') && "@#{query}" =~ /\A#{Account::MENTION_RE}\Z/
end end
def likely_acct? def likely_acct?

View File

@ -7,54 +7,68 @@ class FollowService < BaseService
# Follow a remote user, notify remote user about the follow # Follow a remote user, notify remote user about the follow
# @param [Account] source_account From which to 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 [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 # @param [Hash] options
def call(source_account, target_account, reblogs: nil, bypass_locked: false) # @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true
reblogs = true if reblogs.nil? # @option [Boolean] :bypass_locked
target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true) # @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 target_account.nil? || target_account.id == source_account.id || target_account.suspended? raise ActiveRecord::RecordNotFound if following_not_possible?
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) raise Mastodon::NotPermittedError if following_not_allowed?
if source_account.following?(target_account) if @source_account.following?(@target_account)
# We're already following this account, but we'll call follow! again to return change_follow_options!
# make sure the reblogs status is set correctly. elsif @source_account.requested?(@target_account)
return source_account.follow!(target_account, reblogs: reblogs) return change_follow_request_options!
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
end end
ActivityTracker.increment('activity:interactions') ActivityTracker.increment('activity:interactions')
if (target_account.locked? && !bypass_locked) || source_account.silenced? || target_account.activitypub? if (@target_account.locked? && !@options[:bypass_locked]) || @source_account.silenced? || @target_account.activitypub?
request_follow(source_account, target_account, reblogs: reblogs) request_follow!
elsif target_account.local? elsif @target_account.local?
direct_follow(source_account, target_account, reblogs: reblogs) direct_follow!
end end
end end
private private
def request_follow(source_account, target_account, reblogs: true) def following_not_possible?
follow_request = FollowRequest.create!(account: source_account, target_account: target_account, show_reblogs: reblogs) @target_account.nil? || @target_account.id == @source_account.id || @target_account.suspended?
end
if target_account.local? def following_not_allowed?
LocalNotificationWorker.perform_async(target_account.id, follow_request.id, follow_request.class.name) @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)
elsif target_account.activitypub? end
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), source_account.id, target_account.inbox_url)
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)
end end
follow_request follow_request
end end
def direct_follow(source_account, target_account, reblogs: true) def direct_follow!
follow = source_account.follow!(target_account, reblogs: reblogs) 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) LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name)
MergeWorker.perform_async(target_account.id, source_account.id) MergeWorker.perform_async(@target_account.id, @source_account.id)
follow follow
end end

View File

@ -19,6 +19,7 @@ class PostStatusService < BaseService
# @option [Enumerable] :media_ids Optional array of media IDs to attach # @option [Enumerable] :media_ids Optional array of media IDs to attach
# @option [Doorkeeper::Application] :application # @option [Doorkeeper::Application] :application
# @option [String] :idempotency Optional idempotency key # @option [String] :idempotency Optional idempotency key
# @option [Boolean] :with_rate_limit
# @return [Status] # @return [Status]
def call(account, options = {}) def call(account, options = {})
@account = account @account = account
@ -49,7 +50,7 @@ class PostStatusService < BaseService
def preprocess_attributes! def preprocess_attributes!
@text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present? @text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present?
@visibility = @options[:visibility] || @account.user&.setting_default_privacy @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 = @options[:scheduled_at]&.to_datetime
@scheduled_at = nil if scheduled_in_the_past? @scheduled_at = nil if scheduled_in_the_past?
rescue ArgumentError rescue ArgumentError
@ -160,6 +161,7 @@ class PostStatusService < BaseService
visibility: @visibility, visibility: @visibility,
language: language_from_option(@options[:language]) || @account.user&.setting_default_language&.presence || LanguageDetector.instance.detect(@text, @account), language: language_from_option(@options[:language]) || @account.user&.setting_default_language&.presence || LanguageDetector.instance.detect(@text, @account),
application: @options[:application], application: @options[:application],
rate_limit: @options[:with_rate_limit],
}.compact }.compact
end end
@ -179,10 +181,11 @@ class PostStatusService < BaseService
def scheduled_options def scheduled_options
@options.tap do |options_hash| @options.tap do |options_hash|
options_hash[:in_reply_to_id] = options_hash.delete(:thread)&.id options_hash[:in_reply_to_id] = options_hash.delete(:thread)&.id
options_hash[:application_id] = options_hash.delete(:application)&.id options_hash[:application_id] = options_hash.delete(:application)&.id
options_hash[:scheduled_at] = nil options_hash[:scheduled_at] = nil
options_hash[:idempotency] = nil options_hash[:idempotency] = nil
options_hash[:with_rate_limit] = false
end end
end end
end end

View File

@ -8,6 +8,8 @@ class ReblogService < BaseService
# @param [Account] account Account to reblog from # @param [Account] account Account to reblog from
# @param [Status] reblogged_status Status to be reblogged # @param [Status] reblogged_status Status to be reblogged
# @param [Hash] options # @param [Hash] options
# @option [String] :visibility
# @option [Boolean] :with_rate_limit
# @return [Status] # @return [Status]
def call(account, reblogged_status, options = {}) def call(account, reblogged_status, options = {})
reblogged_status = reblogged_status.reblog if reblogged_status.reblog? reblogged_status = reblogged_status.reblog if reblogged_status.reblog?
@ -18,9 +20,15 @@ class ReblogService < BaseService
return reblog unless reblog.nil? return reblog unless reblog.nil?
visibility = options[:visibility] || account.user&.setting_default_privacy visibility = begin
visibility = reblogged_status.visibility if reblogged_status.hidden? if reblogged_status.hidden?
reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility) 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) DistributionWorker.perform_async(reblog.id)
ActivityPub::DistributionWorker.perform_async(reblog.id) ActivityPub::DistributionWorker.perform_async(reblog.id)
@ -45,7 +53,9 @@ class ReblogService < BaseService
def bump_potential_friendship(account, reblog) def bump_potential_friendship(account, reblog)
ActivityTracker.increment('activity:interactions') ActivityTracker.increment('activity:interactions')
return if account.following?(reblog.reblog.account_id) return if account.following?(reblog.reblog.account_id)
PotentialFriendshipTracker.record(account.id, reblog.reblog.account_id, :reblog) PotentialFriendshipTracker.record(account.id, reblog.reblog.account_id, :reblog)
end end

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