mirror of
https://framagit.org/tykayn/mastodon.git
synced 2023-08-25 08:33:12 +02:00
accept merge from upstream
This commit is contained in:
commit
158c51edc1
@ -6,6 +6,7 @@ aliases:
|
||||
- image: circleci/ruby:2.7-buster-node
|
||||
environment: &ruby_environment
|
||||
BUNDLE_APP_CONFIG: ./.bundle/
|
||||
BUNDLE_PATH: ./vendor/bundle/
|
||||
DB_HOST: localhost
|
||||
DB_USER: root
|
||||
RAILS_ENV: test
|
||||
|
4
Gemfile
4
Gemfile
@ -49,7 +49,7 @@ gem 'omniauth-saml', '~> 1.10'
|
||||
gem 'omniauth', '~> 1.9'
|
||||
|
||||
gem 'discard', '~> 1.1'
|
||||
gem 'doorkeeper', '~> 5.2'
|
||||
gem 'doorkeeper', '~> 5.3'
|
||||
gem 'fast_blank', '~> 1.0'
|
||||
gem 'fastimage'
|
||||
gem 'goldfinger', '~> 2.1'
|
||||
@ -92,7 +92,7 @@ gem 'simple-navigation', '~> 4.1'
|
||||
gem 'simple_form', '~> 5.0'
|
||||
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
|
||||
gem 'stoplight', '~> 2.2.0'
|
||||
gem 'strong_migrations', '~> 0.5'
|
||||
gem 'strong_migrations', '~> 0.6'
|
||||
gem 'tty-command', '~> 0.9', require: false
|
||||
gem 'tty-prompt', '~> 0.20', require: false
|
||||
gem 'twitter-text', '~> 1.14'
|
||||
|
28
Gemfile.lock
28
Gemfile.lock
@ -195,21 +195,21 @@ GEM
|
||||
docile (1.3.2)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (5.2.3)
|
||||
doorkeeper (5.3.1)
|
||||
railties (>= 5)
|
||||
dotenv (2.7.5)
|
||||
dotenv-rails (2.7.5)
|
||||
dotenv (= 2.7.5)
|
||||
railties (>= 3.2, < 6.1)
|
||||
e2mmap (0.1.0)
|
||||
elasticsearch (7.3.0)
|
||||
elasticsearch-api (= 7.3.0)
|
||||
elasticsearch-transport (= 7.3.0)
|
||||
elasticsearch-api (7.3.0)
|
||||
elasticsearch (7.5.0)
|
||||
elasticsearch-api (= 7.5.0)
|
||||
elasticsearch-transport (= 7.5.0)
|
||||
elasticsearch-api (7.5.0)
|
||||
multi_json
|
||||
elasticsearch-dsl (0.1.8)
|
||||
elasticsearch-transport (7.3.0)
|
||||
faraday
|
||||
elasticsearch-transport (7.5.0)
|
||||
faraday (>= 0.14, < 1)
|
||||
multi_json
|
||||
encryptor (3.0.0)
|
||||
equatable (0.6.1)
|
||||
@ -220,7 +220,7 @@ GEM
|
||||
fabrication (2.21.0)
|
||||
faker (2.10.1)
|
||||
i18n (>= 1.6, < 2)
|
||||
faraday (1.0.0)
|
||||
faraday (0.17.3)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
fast_blank (1.0.0)
|
||||
fastimage (2.1.7)
|
||||
@ -311,7 +311,7 @@ GEM
|
||||
multi_json (~> 1.14)
|
||||
rack (~> 2.0)
|
||||
rdf (~> 3.1)
|
||||
json-ld-preloaded (3.1.0)
|
||||
json-ld-preloaded (3.1.1)
|
||||
json-ld (~> 3.1)
|
||||
rdf (~> 3.1)
|
||||
jsonapi-renderer (0.2.2)
|
||||
@ -383,7 +383,7 @@ GEM
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sidekiq (>= 3.5)
|
||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||
oj (3.10.1)
|
||||
oj (3.10.3)
|
||||
omniauth (1.9.0)
|
||||
hashie (>= 3.4.6, < 3.7.0)
|
||||
rack (>= 1.6.2, < 3)
|
||||
@ -435,7 +435,7 @@ GEM
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.3)
|
||||
puma (4.3.1)
|
||||
puma (4.3.3)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
@ -602,7 +602,7 @@ GEM
|
||||
stoplight (2.2.0)
|
||||
streamio-ffmpeg (3.0.2)
|
||||
multi_json (~> 1.8)
|
||||
strong_migrations (0.5.1)
|
||||
strong_migrations (0.6.2)
|
||||
activerecord (>= 5)
|
||||
temple (0.8.2)
|
||||
terminal-table (1.8.0)
|
||||
@ -689,7 +689,7 @@ DEPENDENCIES
|
||||
devise-two-factor (~> 3.1)
|
||||
devise_pam_authenticatable2 (~> 9.2)
|
||||
discard (~> 1.1)
|
||||
doorkeeper (~> 5.2)
|
||||
doorkeeper (~> 5.3)
|
||||
dotenv-rails (~> 2.7)
|
||||
e2mmap (~> 0.1.0)
|
||||
fabrication (~> 2.21)
|
||||
@ -777,7 +777,7 @@ DEPENDENCIES
|
||||
stackprof
|
||||
stoplight (~> 2.2.0)
|
||||
streamio-ffmpeg (~> 3.0)
|
||||
strong_migrations (~> 0.5)
|
||||
strong_migrations (~> 0.6)
|
||||
thor (~> 0.20)
|
||||
thwait (~> 0.1.0)
|
||||
tty-command (~> 0.9)
|
||||
|
@ -6,7 +6,7 @@ class AccountFollowController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def create
|
||||
FollowService.new.call(current_user.account, @account.acct)
|
||||
FollowService.new.call(current_user.account, @account, with_rate_limit: true)
|
||||
redirect_to account_path(@account)
|
||||
end
|
||||
end
|
||||
|
21
app/controllers/admin/site_uploads_controller.rb
Normal file
21
app/controllers/admin/site_uploads_controller.rb
Normal 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
|
@ -44,6 +44,10 @@ class Api::BaseController < ApplicationController
|
||||
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
|
||||
end
|
||||
|
||||
rescue_from Mastodon::RateLimitExceededError do
|
||||
render json: { error: I18n.t('errors.429') }, status: 429
|
||||
end
|
||||
|
||||
rescue_from ActionController::ParameterMissing do |e|
|
||||
render json: { error: e.to_s }, status: 400
|
||||
end
|
||||
|
@ -5,8 +5,6 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
|
||||
before_action :set_account
|
||||
after_action :insert_pagination_headers
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@accounts = load_accounts
|
||||
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||
|
@ -5,8 +5,6 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
|
||||
before_action :set_account
|
||||
after_action :insert_pagination_headers
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@accounts = load_accounts
|
||||
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||
|
@ -4,8 +4,6 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController
|
||||
before_action :require_user!
|
||||
before_action :set_account
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@proofs = @account.identity_proofs.active
|
||||
render json: @proofs, each_serializer: REST::IdentityProofSerializer
|
||||
|
@ -5,8 +5,6 @@ class Api::V1::Accounts::ListsController < Api::BaseController
|
||||
before_action :require_user!
|
||||
before_action :set_account
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@lists = @account.lists.where(account: current_account)
|
||||
render json: @lists, each_serializer: REST::ListSerializer
|
||||
|
@ -7,8 +7,6 @@ class Api::V1::Accounts::PinsController < Api::BaseController
|
||||
before_action :require_user!
|
||||
before_action :set_account
|
||||
|
||||
respond_to :json
|
||||
|
||||
def create
|
||||
AccountPin.create!(account: current_account, target_account: @account)
|
||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
|
||||
|
@ -4,8 +4,6 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:follows' }
|
||||
before_action :require_user!
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
accounts = Account.where(id: account_ids).select('id')
|
||||
# .where doesn't guarantee that our results are in the same order
|
||||
|
@ -4,8 +4,6 @@ class Api::V1::Accounts::SearchController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
|
||||
before_action :require_user!
|
||||
|
||||
respond_to :json
|
||||
|
||||
def show
|
||||
@accounts = account_search
|
||||
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||
|
@ -6,8 +6,6 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
||||
|
||||
after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) }
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@statuses = load_statuses
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||
|
@ -14,7 +14,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
|
||||
skip_before_action :require_authenticated_user!, only: :create
|
||||
|
||||
respond_to :json
|
||||
override_rate_limit_headers :follow, family: :follows
|
||||
|
||||
def show
|
||||
render json: @account, serializer: REST::AccountSerializer
|
||||
@ -31,7 +31,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
end
|
||||
|
||||
def follow
|
||||
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs))
|
||||
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs), with_rate_limit: true)
|
||||
|
||||
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
|
||||
|
||||
|
@ -3,8 +3,6 @@
|
||||
class Api::V1::Apps::CredentialsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read }
|
||||
|
||||
respond_to :json
|
||||
|
||||
def show
|
||||
render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key)
|
||||
end
|
||||
|
@ -5,8 +5,6 @@ class Api::V1::BlocksController < Api::BaseController
|
||||
before_action :require_user!
|
||||
after_action :insert_pagination_headers
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@accounts = load_accounts
|
||||
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||
|
@ -5,8 +5,6 @@ class Api::V1::BookmarksController < Api::BaseController
|
||||
before_action :require_user!
|
||||
after_action :insert_pagination_headers
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@statuses = load_statuses
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||
|
@ -9,8 +9,6 @@ class Api::V1::ConversationsController < Api::BaseController
|
||||
before_action :set_conversation, except: :index
|
||||
after_action :insert_pagination_headers, only: :index
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@conversations = paginated_conversations
|
||||
render json: @conversations, each_serializer: REST::ConversationSerializer
|
||||
|
@ -1,8 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::CustomEmojisController < Api::BaseController
|
||||
respond_to :json
|
||||
|
||||
skip_before_action :set_cache_headers
|
||||
|
||||
def index
|
||||
|
@ -8,8 +8,6 @@ class Api::V1::DomainBlocksController < Api::BaseController
|
||||
before_action :require_user!
|
||||
after_action :insert_pagination_headers, only: :show
|
||||
|
||||
respond_to :json
|
||||
|
||||
def show
|
||||
@blocks = load_domain_blocks
|
||||
render json: @blocks.map(&:domain)
|
||||
|
@ -5,8 +5,6 @@ class Api::V1::EndorsementsController < Api::BaseController
|
||||
before_action :require_user!
|
||||
after_action :insert_pagination_headers
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@accounts = load_accounts
|
||||
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||
|
@ -5,8 +5,6 @@ class Api::V1::FavouritesController < Api::BaseController
|
||||
before_action :require_user!
|
||||
after_action :insert_pagination_headers
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@statuses = load_statuses
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||
|
@ -2,12 +2,9 @@
|
||||
|
||||
class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
|
||||
|
||||
before_action :require_user!
|
||||
before_action :set_most_used_tags, only: :index
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
render json: @most_used_tags, each_serializer: REST::TagSerializer
|
||||
end
|
||||
|
@ -7,8 +7,6 @@ class Api::V1::FiltersController < Api::BaseController
|
||||
before_action :set_filters, only: :index
|
||||
before_action :set_filter, only: [:show, :update, :destroy]
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
render json: @filters, each_serializer: REST::FilterSerializer
|
||||
end
|
||||
|
@ -6,8 +6,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController
|
||||
skip_before_action :set_cache_headers
|
||||
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
|
||||
|
||||
respond_to :json
|
||||
|
||||
def show
|
||||
expires_in 1.day, public: true
|
||||
render_with_cache json: :activity, expires_in: 1.day
|
||||
|
@ -6,8 +6,6 @@ class Api::V1::Instances::PeersController < Api::BaseController
|
||||
skip_before_action :set_cache_headers
|
||||
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
expires_in 1.day, public: true
|
||||
render_with_cache(expires_in: 1.day) { Account.remote.domains }
|
||||
|
@ -1,8 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::InstancesController < Api::BaseController
|
||||
respond_to :json
|
||||
|
||||
skip_before_action :set_cache_headers
|
||||
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
|
||||
|
||||
|
@ -4,8 +4,6 @@ class Api::V1::MediaController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:media' }
|
||||
before_action :require_user!
|
||||
|
||||
respond_to :json
|
||||
|
||||
def create
|
||||
@media = current_account.media_attachments.create!(media_params)
|
||||
render json: @media, serializer: REST::MediaAttachmentSerializer
|
||||
|
@ -5,8 +5,6 @@ class Api::V1::MutesController < Api::BaseController
|
||||
before_action :require_user!
|
||||
after_action :insert_pagination_headers
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@accounts = load_accounts
|
||||
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||
|
@ -6,8 +6,6 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||
before_action :require_user!
|
||||
after_action :insert_pagination_headers, only: :index
|
||||
|
||||
respond_to :json
|
||||
|
||||
DEFAULT_NOTIFICATIONS_LIMIT = 15
|
||||
|
||||
def index
|
||||
|
@ -7,8 +7,6 @@ class Api::V1::Polls::VotesController < Api::BaseController
|
||||
before_action :require_user!
|
||||
before_action :set_poll
|
||||
|
||||
respond_to :json
|
||||
|
||||
def create
|
||||
VoteService.new.call(current_account, @poll, vote_params[:choices])
|
||||
render json: @poll, serializer: REST::PollSerializer
|
||||
|
@ -7,8 +7,6 @@ class Api::V1::PollsController < Api::BaseController
|
||||
before_action :set_poll
|
||||
before_action :refresh_poll
|
||||
|
||||
respond_to :json
|
||||
|
||||
def show
|
||||
render json: @poll, serializer: REST::PollSerializer, include_results: true
|
||||
end
|
||||
|
@ -4,8 +4,6 @@ class Api::V1::PreferencesController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
|
||||
before_action :require_user!
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
render json: current_account, serializer: REST::PreferencesSerializer
|
||||
end
|
||||
|
@ -4,8 +4,6 @@ class Api::V1::ReportsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:reports' }, only: [:create]
|
||||
before_action :require_user!
|
||||
|
||||
respond_to :json
|
||||
|
||||
def create
|
||||
@report = ReportService.new.call(
|
||||
current_account,
|
||||
|
@ -7,8 +7,6 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController
|
||||
before_action :require_user!
|
||||
before_action :set_status
|
||||
|
||||
respond_to :json
|
||||
|
||||
def create
|
||||
current_account.bookmarks.find_or_create_by!(account: current_account, status: @status)
|
||||
render json: @status, serializer: REST::StatusSerializer
|
||||
|
@ -7,8 +7,6 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
|
||||
before_action :set_status
|
||||
after_action :insert_pagination_headers
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@accounts = load_accounts
|
||||
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||
|
@ -7,8 +7,6 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
|
||||
before_action :require_user!
|
||||
before_action :set_status
|
||||
|
||||
respond_to :json
|
||||
|
||||
def create
|
||||
FavouriteService.new.call(current_account, @status)
|
||||
render json: @status, serializer: REST::StatusSerializer
|
||||
|
@ -8,8 +8,6 @@ class Api::V1::Statuses::MutesController < Api::BaseController
|
||||
before_action :set_status
|
||||
before_action :set_conversation
|
||||
|
||||
respond_to :json
|
||||
|
||||
def create
|
||||
current_account.mute_conversation!(@conversation)
|
||||
@mutes_map = { @conversation.id => true }
|
||||
|
@ -7,8 +7,6 @@ class Api::V1::Statuses::PinsController < Api::BaseController
|
||||
before_action :require_user!
|
||||
before_action :set_status
|
||||
|
||||
respond_to :json
|
||||
|
||||
def create
|
||||
StatusPin.create!(account: current_account, status: @status)
|
||||
distribute_add_activity!
|
||||
|
@ -7,8 +7,6 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
|
||||
before_action :set_status
|
||||
after_action :insert_pagination_headers
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@accounts = load_accounts
|
||||
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||
|
@ -7,10 +7,11 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
|
||||
before_action :require_user!
|
||||
before_action :set_reblog
|
||||
|
||||
respond_to :json
|
||||
override_rate_limit_headers :create, family: :statuses
|
||||
|
||||
def create
|
||||
@status = ReblogService.new.call(current_account, @reblog, reblog_params)
|
||||
|
||||
render json: @status, serializer: REST::StatusSerializer
|
||||
end
|
||||
|
||||
|
@ -8,7 +8,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
before_action :require_user!, except: [:show, :context]
|
||||
before_action :set_status, only: [:show, :context]
|
||||
|
||||
respond_to :json
|
||||
override_rate_limit_headers :create, family: :statuses
|
||||
|
||||
# This API was originally unlimited, pagination cannot be introduced without
|
||||
# breaking backwards-compatibility. Arbitrarily high number to cover most
|
||||
@ -44,7 +44,8 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
scheduled_at: status_params[:scheduled_at],
|
||||
application: doorkeeper_token.application,
|
||||
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
|
||||
end
|
||||
|
@ -1,8 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::StreamingController < Api::BaseController
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
if Rails.configuration.x.streaming_api_base_url != request.host
|
||||
redirect_to streaming_api_url, status: 301
|
||||
|
@ -7,8 +7,6 @@ class Api::V1::SuggestionsController < Api::BaseController
|
||||
before_action :require_user!
|
||||
before_action :set_accounts
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||
end
|
||||
|
@ -5,8 +5,6 @@ class Api::V1::Timelines::HomeController < Api::BaseController
|
||||
before_action :require_user!, only: [:show]
|
||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||
|
||||
respond_to :json
|
||||
|
||||
def show
|
||||
@statuses = load_statuses
|
||||
|
||||
|
@ -4,8 +4,6 @@ class Api::V1::Timelines::PublicController < Api::BaseController
|
||||
before_action :require_user!, only: [:show], if: :require_auth?
|
||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||
|
||||
respond_to :json
|
||||
|
||||
def show
|
||||
@statuses = load_statuses
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||
|
@ -4,8 +4,6 @@ class Api::V1::Timelines::TagController < Api::BaseController
|
||||
before_action :load_tag
|
||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||
|
||||
respond_to :json
|
||||
|
||||
def show
|
||||
@statuses = load_statuses
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
|
||||
|
@ -3,8 +3,6 @@
|
||||
class Api::V1::TrendsController < Api::BaseController
|
||||
before_action :set_tags
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
render json: @tags, each_serializer: REST::TagSerializer
|
||||
end
|
||||
|
@ -8,8 +8,6 @@ class Api::V2::SearchController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:search' }
|
||||
before_action :require_user!
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@search = Search.new(search_results)
|
||||
render json: @search, serializer: REST::SearchSerializer
|
||||
|
@ -1,8 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::Web::EmbedsController < Api::Web::BaseController
|
||||
respond_to :json
|
||||
|
||||
before_action :require_user!
|
||||
|
||||
def create
|
||||
|
@ -1,8 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
||||
respond_to :json
|
||||
|
||||
before_action :require_user!
|
||||
|
||||
def create
|
||||
|
@ -1,8 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::Web::SettingsController < Api::Web::BaseController
|
||||
respond_to :json
|
||||
|
||||
before_action :require_user!
|
||||
|
||||
def update
|
||||
|
@ -29,6 +29,7 @@ class ApplicationController < ActionController::Base
|
||||
rescue_from Mastodon::NotPermittedError, with: :forbidden
|
||||
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
|
||||
rescue_from Mastodon::RaceConditionError, with: :service_unavailable
|
||||
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
|
||||
|
||||
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
|
||||
before_action :require_functional!, if: :user_signed_in?
|
||||
@ -111,6 +112,10 @@ class ApplicationController < ActionController::Base
|
||||
respond_with_error(503)
|
||||
end
|
||||
|
||||
def too_many_requests
|
||||
respond_with_error(429)
|
||||
end
|
||||
|
||||
def single_user_mode?
|
||||
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists?
|
||||
end
|
||||
|
@ -20,7 +20,7 @@ class AuthorizeInteractionsController < ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
if @resource.is_a?(Account) && FollowService.new.call(current_account, @resource)
|
||||
if @resource.is_a?(Account) && FollowService.new.call(current_account, @resource, with_rate_limit: true)
|
||||
render :success
|
||||
else
|
||||
render :error
|
||||
|
@ -3,6 +3,20 @@
|
||||
module RateLimitHeaders
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def override_rate_limit_headers(method_name, options = {})
|
||||
around_action(only: method_name, if: :current_account) do |_controller, block|
|
||||
begin
|
||||
block.call
|
||||
ensure
|
||||
rate_limiter = RateLimiter.new(current_account, options)
|
||||
rate_limit_headers = rate_limiter.to_headers
|
||||
response.headers.merge!(rate_limit_headers) unless response.headers['X-RateLimit-Remaining'].present? && rate_limit_headers['X-RateLimit-Remaining'].to_i > response.headers['X-RateLimit-Remaining'].to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
included do
|
||||
before_action :set_rate_limit_headers, if: :rate_limited_request?
|
||||
end
|
||||
@ -44,7 +58,7 @@ module RateLimitHeaders
|
||||
end
|
||||
|
||||
def api_throttle_data
|
||||
most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_, v| v[:limit] }
|
||||
most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_, v| v[:limit] - v[:count] }
|
||||
request.env['rack.attack.throttle_data'][most_limited_type]
|
||||
end
|
||||
|
||||
|
11
app/helpers/admin/settings_helper.rb
Normal file
11
app/helpers/admin/settings_helper.rb
Normal 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
|
@ -10,7 +10,7 @@ import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_s
|
||||
import { decode } from 'blurhash';
|
||||
|
||||
const messages = defineMessages({
|
||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
|
||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Hide media' },
|
||||
});
|
||||
|
||||
class Item extends React.PureComponent {
|
||||
|
@ -6,54 +6,49 @@ import IconButton from './icon_button';
|
||||
import DropdownMenuContainer from '../containers/dropdown_menu_container';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { isStaff, me } from '../initial_state';
|
||||
import { me, isStaff } from '../initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
delete : { id: 'status.delete', defaultMessage: 'Delete' },
|
||||
redraft : { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
||||
direct : { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||
mention : { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||
mute : { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||
block : { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||
reply : { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
share : { id: 'status.share', defaultMessage: 'Share' },
|
||||
more : { id: 'status.more', defaultMessage: 'More' },
|
||||
replyAll : { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
|
||||
reblog : { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||
reblog_private : { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' },
|
||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
||||
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
share: { id: 'status.share', defaultMessage: 'Share' },
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
|
||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' },
|
||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||
cannot_reblog : { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||
favourite : { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||
bookmark : { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||
removeBookmark : { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
|
||||
open : { id: 'status.open', defaultMessage: 'Expand this status' },
|
||||
report : { id: 'status.report', defaultMessage: 'Report @{name}' },
|
||||
muteConversation : { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||
unmuteConversation : { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||
pin : { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
||||
unpin : { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
|
||||
embed : { id: 'status.embed', defaultMessage: 'Embed' },
|
||||
admin_account : { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||
admin_status : { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
|
||||
copy : { id: 'status.copy', defaultMessage: 'Copy link to status' },
|
||||
blockDomain : { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
|
||||
unblockDomain : { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
|
||||
unmute : { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||
unblock : { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||
removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
|
||||
open: { id: 'status.open', defaultMessage: 'Expand this status' },
|
||||
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
|
||||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
||||
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
|
||||
embed: { id: 'status.embed', defaultMessage: 'Embed' },
|
||||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
|
||||
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
|
||||
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
|
||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
});
|
||||
const ObfuscateCountReplies = false;
|
||||
const obfuscatedCount = count => {
|
||||
if (ObfuscateCountReplies) {
|
||||
|
||||
if (count < 0) {
|
||||
return 0;
|
||||
} else if (count <= 1) {
|
||||
return count;
|
||||
} else {
|
||||
return '1+';
|
||||
}
|
||||
} else {
|
||||
const obfuscatedCount = count => {
|
||||
if (count < 0) {
|
||||
return 0;
|
||||
} else if (count <= 1) {
|
||||
return count;
|
||||
} else {
|
||||
return '1+';
|
||||
}
|
||||
};
|
||||
|
||||
@ -70,28 +65,27 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
status : ImmutablePropTypes.map.isRequired,
|
||||
relationship : ImmutablePropTypes.map,
|
||||
onReply : PropTypes.func,
|
||||
onFavourite : PropTypes.func,
|
||||
onReblog : PropTypes.func,
|
||||
onDelete : PropTypes.func,
|
||||
onDirect : PropTypes.func,
|
||||
onMention : PropTypes.func,
|
||||
onMute : PropTypes.func,
|
||||
onUnmute : PropTypes.func,
|
||||
onBlock : PropTypes.func,
|
||||
onUnblock : PropTypes.func,
|
||||
onBlockDomain : PropTypes.func,
|
||||
onUnblockDomain : PropTypes.func,
|
||||
onReport : PropTypes.func,
|
||||
onEmbed : PropTypes.func,
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
relationship: ImmutablePropTypes.map,
|
||||
onReply: PropTypes.func,
|
||||
onFavourite: PropTypes.func,
|
||||
onReblog: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onDirect: PropTypes.func,
|
||||
onMention: PropTypes.func,
|
||||
onMute: PropTypes.func,
|
||||
onUnmute: PropTypes.func,
|
||||
onBlock: PropTypes.func,
|
||||
onUnblock: PropTypes.func,
|
||||
onBlockDomain: PropTypes.func,
|
||||
onUnblockDomain: PropTypes.func,
|
||||
onReport: PropTypes.func,
|
||||
onEmbed: PropTypes.func,
|
||||
onMuteConversation: PropTypes.func,
|
||||
onPin : PropTypes.func,
|
||||
onBookmark : PropTypes.func,
|
||||
withDismiss : PropTypes.bool,
|
||||
showReplyFormUnder: PropTypes.bool,
|
||||
intl : PropTypes.object.isRequired,
|
||||
onPin: PropTypes.func,
|
||||
onBookmark: PropTypes.func,
|
||||
withDismiss: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
// Avoid checking props that are functions (and whose equality will always
|
||||
@ -100,7 +94,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
'status',
|
||||
'relationship',
|
||||
'withDismiss',
|
||||
];
|
||||
]
|
||||
|
||||
handleReplyClick = () => {
|
||||
if (me) {
|
||||
@ -108,20 +102,16 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
} else {
|
||||
this._openInteractionDialog('reply');
|
||||
}
|
||||
};
|
||||
handleReplyUnderClick = () => {
|
||||
console.info('hop show');
|
||||
this.handleReplyClick();
|
||||
};
|
||||
}
|
||||
|
||||
handleShareClick = () => {
|
||||
navigator.share({
|
||||
text: this.props.status.get('search_index'),
|
||||
url : this.props.status.get('url'),
|
||||
url: this.props.status.get('url'),
|
||||
}).catch((e) => {
|
||||
if (e.name !== 'AbortError') console.error(e);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
handleFavouriteClick = () => {
|
||||
if (me) {
|
||||
@ -129,7 +119,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
} else {
|
||||
this._openInteractionDialog('favourite');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleReblogClick = e => {
|
||||
if (me) {
|
||||
@ -137,35 +127,35 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
} else {
|
||||
this._openInteractionDialog('reblog');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_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');
|
||||
};
|
||||
}
|
||||
|
||||
handleBookmarkClick = () => {
|
||||
this.props.onBookmark(this.props.status);
|
||||
};
|
||||
}
|
||||
|
||||
handleDeleteClick = () => {
|
||||
this.props.onDelete(this.props.status, this.context.router.history);
|
||||
};
|
||||
}
|
||||
|
||||
handleRedraftClick = () => {
|
||||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
||||
};
|
||||
}
|
||||
|
||||
handlePinClick = () => {
|
||||
this.props.onPin(this.props.status);
|
||||
};
|
||||
}
|
||||
|
||||
handleMentionClick = () => {
|
||||
this.props.onMention(this.props.status.get('account'), this.context.router.history);
|
||||
};
|
||||
}
|
||||
|
||||
handleDirectClick = () => {
|
||||
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
|
||||
};
|
||||
}
|
||||
|
||||
handleMuteClick = () => {
|
||||
const { status, relationship, onMute, onUnmute } = this.props;
|
||||
@ -176,7 +166,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
} else {
|
||||
onMute(account);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleBlockClick = () => {
|
||||
const { status, relationship, onBlock, onUnblock } = this.props;
|
||||
@ -187,43 +177,43 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
} else {
|
||||
onBlock(status);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleBlockDomain = () => {
|
||||
const { status, onBlockDomain } = this.props;
|
||||
const account = status.get('account');
|
||||
|
||||
onBlockDomain(account.get('acct').split('@')[1]);
|
||||
};
|
||||
}
|
||||
|
||||
handleUnblockDomain = () => {
|
||||
const { status, onUnblockDomain } = this.props;
|
||||
const account = status.get('account');
|
||||
|
||||
onUnblockDomain(account.get('acct').split('@')[1]);
|
||||
};
|
||||
}
|
||||
|
||||
handleOpen = () => {
|
||||
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
|
||||
};
|
||||
}
|
||||
|
||||
handleEmbed = () => {
|
||||
this.props.onEmbed(this.props.status);
|
||||
};
|
||||
}
|
||||
|
||||
handleReport = () => {
|
||||
this.props.onReport(this.props.status);
|
||||
};
|
||||
}
|
||||
|
||||
handleConversationMuteClick = () => {
|
||||
this.props.onMuteConversation(this.props.status);
|
||||
};
|
||||
}
|
||||
|
||||
handleCopy = () => {
|
||||
const url = this.props.status.get('url');
|
||||
const url = this.props.status.get('url');
|
||||
const textarea = document.createElement('textarea');
|
||||
|
||||
textarea.textContent = url;
|
||||
textarea.textContent = url;
|
||||
textarea.style.position = 'fixed';
|
||||
|
||||
document.body.appendChild(textarea);
|
||||
@ -236,16 +226,15 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
render () {
|
||||
const { status, relationship, intl, withDismiss } = this.props;
|
||||
|
||||
console.log('this.props', this.props);
|
||||
const mutingConversation = status.get('muted');
|
||||
const anonymousAccess = !me;
|
||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||
const account = status.get('account');
|
||||
const anonymousAccess = !me;
|
||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||
const account = status.get('account');
|
||||
|
||||
let menu = [];
|
||||
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(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark),
|
||||
action: this.handleBookmarkClick,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
|
||||
menu.push(null);
|
||||
|
||||
if (status.getIn(['account', 'id']) === me || withDismiss) {
|
||||
menu.push({
|
||||
text : intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation),
|
||||
action: this.handleConversationMuteClick,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
if (status.getIn(['account', 'id']) === me) {
|
||||
if (publicStatus) {
|
||||
menu.push({
|
||||
text : intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin),
|
||||
action: this.handlePinClick,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||
} else {
|
||||
if (status.get('visibility') === 'private') {
|
||||
menu.push({
|
||||
text : intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private),
|
||||
action: this.handleReblogClick,
|
||||
});
|
||||
menu.push({ 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.redraft), action: this.handleRedraftClick });
|
||||
} else {
|
||||
menu.push({
|
||||
text : intl.formatMessage(messages.mention, { name: account.get('username') }),
|
||||
action: this.handleMentionClick,
|
||||
});
|
||||
menu.push({
|
||||
text : intl.formatMessage(messages.direct, { name: account.get('username') }),
|
||||
action: this.handleDirectClick,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
|
||||
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
|
||||
menu.push(null);
|
||||
|
||||
if (relationship && relationship.get('muting')) {
|
||||
menu.push({
|
||||
text : intl.formatMessage(messages.unmute, { name: account.get('username') }),
|
||||
action: this.handleMuteClick,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
|
||||
} else {
|
||||
menu.push({
|
||||
text : intl.formatMessage(messages.mute, { name: account.get('username') }),
|
||||
action: this.handleMuteClick,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick });
|
||||
}
|
||||
|
||||
if (relationship && relationship.get('blocking')) {
|
||||
menu.push({
|
||||
text : intl.formatMessage(messages.unblock, { name: account.get('username') }),
|
||||
action: this.handleBlockClick,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
|
||||
} else {
|
||||
menu.push({
|
||||
text : intl.formatMessage(messages.block, { name: account.get('username') }),
|
||||
action: this.handleBlockClick,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick });
|
||||
}
|
||||
|
||||
menu.push({
|
||||
text : intl.formatMessage(messages.report, { name: account.get('username') }),
|
||||
action: this.handleReport,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport });
|
||||
|
||||
if (account.get('acct') !== account.get('username')) {
|
||||
const domain = account.get('acct').split('@')[1];
|
||||
@ -344,14 +300,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
|
||||
if (isStaff) {
|
||||
menu.push(null);
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.admin_account, { name: account.get('username') }),
|
||||
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')}`,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), 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);
|
||||
}
|
||||
|
||||
const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && (
|
||||
<IconButton
|
||||
className='status__action-bar-button'
|
||||
title={intl.formatMessage(messages.share)}
|
||||
icon='share-alt'
|
||||
onClick={this.handleShareClick}
|
||||
/>
|
||||
const shareButton = ('share' in navigator) && publicStatus && (
|
||||
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='status__action-bar'>
|
||||
<div className='status__action-bar__counter'>
|
||||
|
||||
<IconButton
|
||||
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}
|
||||
/>
|
||||
<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 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}
|
||||
|
||||
<div className='status__action-bar-dropdown'>
|
||||
<DropdownMenuContainer
|
||||
disabled={anonymousAccess}
|
||||
status={status}
|
||||
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 >
|
||||
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -29,8 +29,8 @@ const messages = defineMessages({
|
||||
report: { id: 'account.report', defaultMessage: 'Report @{name}' },
|
||||
share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
|
||||
media: { id: 'account.media', defaultMessage: 'Media' },
|
||||
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
|
||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
|
||||
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
|
||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
||||
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
|
||||
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
|
||||
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
|
||||
|
@ -214,8 +214,8 @@ class Audio extends React.PureComponent {
|
||||
<div className='video-player__controls active'>
|
||||
<div className='video-player__buttons-bar'>
|
||||
<div className='video-player__buttons left'>
|
||||
<button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
|
||||
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
||||
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
|
||||
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
||||
|
||||
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
|
||||
|
||||
@ -236,7 +236,7 @@ class Audio extends React.PureComponent {
|
||||
</div>
|
||||
|
||||
<div className='video-player__buttons right'>
|
||||
<button type='button' aria-label={intl.formatMessage(messages.download)}>
|
||||
<button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)}>
|
||||
<a className='video-player__download__icon' href={this.props.src} download>
|
||||
<Icon id={'download'} fixedWidth />
|
||||
</a>
|
||||
|
@ -155,6 +155,7 @@ class PollForm extends ImmutablePureComponent {
|
||||
<div className='poll__footer'>
|
||||
<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}>
|
||||
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
|
||||
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
|
||||
|
@ -11,13 +11,13 @@ import Icon from 'mastodon/components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' },
|
||||
public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all, shown in public timelines' },
|
||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' },
|
||||
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but not in public timelines' },
|
||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
|
||||
private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' },
|
||||
private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' },
|
||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
||||
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' },
|
||||
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
|
||||
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
|
||||
});
|
||||
|
||||
|
@ -5,7 +5,11 @@ import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { debounce } from 'lodash';
|
||||
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 AccountContainer from '../../containers/account_container';
|
||||
import Column from '../ui/components/column';
|
||||
@ -15,34 +19,34 @@ import ScrollableList from '../../components/scrollable_list';
|
||||
import MissingIndicator from 'mastodon/components/missing_indicator';
|
||||
|
||||
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']),
|
||||
hasMore : !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']),
|
||||
blockedBy : state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
|
||||
hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']),
|
||||
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Followers extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params : PropTypes.object.isRequired,
|
||||
dispatch : PropTypes.func.isRequired,
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
shouldUpdateScroll: PropTypes.func,
|
||||
accountIds : ImmutablePropTypes.list,
|
||||
hasMore : PropTypes.bool,
|
||||
blockedBy : PropTypes.bool,
|
||||
isAccount : PropTypes.bool,
|
||||
multiColumn : PropTypes.bool,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
blockedBy: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentWillMount () {
|
||||
if (!this.props.accountIds) {
|
||||
this.props.dispatch(fetchAccount(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) {
|
||||
this.props.dispatch(fetchAccount(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));
|
||||
}, 300, { leading: true });
|
||||
|
||||
render() {
|
||||
render () {
|
||||
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn } = this.props;
|
||||
|
||||
if (!isAccount) {
|
||||
return (
|
||||
<Column >
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column >
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
<Column >
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column >
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = blockedBy ? (<FormattedMessage
|
||||
id='empty_column.account_unavailable'
|
||||
defaultMessage='Profile unavailable'
|
||||
/>) : (<FormattedMessage
|
||||
id='account.followers.empty'
|
||||
defaultMessage='No one follows this user yet.'
|
||||
/>);
|
||||
const emptyMessage = blockedBy ? <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' /> : <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />;
|
||||
|
||||
return (
|
||||
<Column >
|
||||
<Column>
|
||||
<ColumnBackButton multiColumn={multiColumn} />
|
||||
|
||||
<ScrollableList
|
||||
@ -89,23 +87,16 @@ class Followers extends ImmutablePureComponent {
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
shouldUpdateScroll={shouldUpdateScroll}
|
||||
prepend={<HeaderContainer
|
||||
accountId={this.props.params.accountId}
|
||||
hideTabs
|
||||
/>}
|
||||
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
|
||||
alwaysPrepend
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{blockedBy ? [] : accountIds.map(id =>
|
||||
(<AccountContainer
|
||||
key={id}
|
||||
id={id}
|
||||
withNote={false}
|
||||
/>),
|
||||
<AccountContainer key={id} id={id} withNote={false} />,
|
||||
)}
|
||||
</ScrollableList >
|
||||
</Column >
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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] === '#')) {
|
||||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
||||
} else {
|
||||
let status = this.props.announcement.get('statuses').find(item => link.href === item.get('url'));
|
||||
if (status) {
|
||||
link.addEventListener('click', this.onStatusClick.bind(this, status), false);
|
||||
}
|
||||
link.setAttribute('title', link.href);
|
||||
link.classList.add('unhandled-link');
|
||||
}
|
||||
@ -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 }) => {
|
||||
target.src = target.getAttribute('data-original');
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
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 Column from '../ui/components/column';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
@ -25,15 +25,15 @@ export default @connect(mapStateToProps)
|
||||
class Reblogs extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params : PropTypes.object.isRequired,
|
||||
dispatch : PropTypes.func.isRequired,
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
shouldUpdateScroll: PropTypes.func,
|
||||
accountIds : ImmutablePropTypes.list,
|
||||
multiColumn : PropTypes.bool,
|
||||
intl : PropTypes.object.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
multiColumn: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentWillMount () {
|
||||
if (!this.props.accountIds) {
|
||||
this.props.dispatch(fetchReblogs(this.props.params.statusId));
|
||||
}
|
||||
@ -47,23 +47,20 @@ class Reblogs extends ImmutablePureComponent {
|
||||
|
||||
handleRefresh = () => {
|
||||
this.props.dispatch(fetchReblogs(this.props.params.statusId));
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
render () {
|
||||
const { intl, shouldUpdateScroll, accountIds, multiColumn } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
<Column >
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column >
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = (<FormattedMessage
|
||||
id='status.reblogs.empty'
|
||||
defaultMessage='No one has boosted this toot yet. When someone does, they will show up here.'
|
||||
/>);
|
||||
const emptyMessage = <FormattedMessage id='status.reblogs.empty' defaultMessage='No one has boosted this toot yet. When someone does, they will show up here.' />;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn}>
|
||||
@ -71,12 +68,7 @@ class Reblogs extends ImmutablePureComponent {
|
||||
showBackButton
|
||||
multiColumn={multiColumn}
|
||||
extraButton={(
|
||||
<button
|
||||
className='column-header__button'
|
||||
title={intl.formatMessage(messages.refresh)}
|
||||
aria-label={intl.formatMessage(messages.refresh)}
|
||||
onClick={this.handleRefresh}
|
||||
><Icon id='refresh' /></button >
|
||||
<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}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
(<AccountContainer
|
||||
key={id}
|
||||
id={id}
|
||||
withNote={false}
|
||||
/>),
|
||||
<AccountContainer key={id} id={id} withNote={false} />,
|
||||
)}
|
||||
</ScrollableList >
|
||||
</Column >
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -5,37 +5,37 @@ import IconButton from '../../../components/icon_button';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { isStaff, me } from '../../../initial_state';
|
||||
import { me, isStaff } from '../../../initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
delete : { id: 'status.delete', defaultMessage: 'Delete' },
|
||||
redraft : { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
||||
direct : { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||
mention : { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||
reply : { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
reblog : { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||
reblog_private : { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' },
|
||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
||||
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' },
|
||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||
cannot_reblog : { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||
favourite : { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||
bookmark : { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||
more : { id: 'status.more', defaultMessage: 'More' },
|
||||
mute : { id: 'status.mute', defaultMessage: 'Mute @{name}' },
|
||||
muteConversation : { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||
unmuteConversation : { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||
block : { id: 'status.block', defaultMessage: 'Block @{name}' },
|
||||
report : { id: 'status.report', defaultMessage: 'Report @{name}' },
|
||||
share : { id: 'status.share', defaultMessage: 'Share' },
|
||||
pin : { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
||||
unpin : { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
|
||||
embed : { id: 'status.embed', defaultMessage: 'Embed' },
|
||||
admin_account : { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||
admin_status : { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
|
||||
copy : { id: 'status.copy', defaultMessage: 'Copy link to status' },
|
||||
blockDomain : { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
|
||||
unblockDomain : { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
|
||||
unmute : { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||
unblock : { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' },
|
||||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||
block: { id: 'status.block', defaultMessage: 'Block @{name}' },
|
||||
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
|
||||
share: { id: 'status.share', defaultMessage: 'Share' },
|
||||
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
||||
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
|
||||
embed: { id: 'status.embed', defaultMessage: 'Embed' },
|
||||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
|
||||
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
|
||||
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
|
||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { status }) => ({
|
||||
@ -51,59 +51,59 @@ class ActionBar extends React.PureComponent {
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
status : ImmutablePropTypes.map.isRequired,
|
||||
relationship : ImmutablePropTypes.map,
|
||||
onReply : PropTypes.func.isRequired,
|
||||
onReblog : PropTypes.func.isRequired,
|
||||
onFavourite : PropTypes.func.isRequired,
|
||||
onBookmark : PropTypes.func.isRequired,
|
||||
onDelete : PropTypes.func.isRequired,
|
||||
onDirect : PropTypes.func.isRequired,
|
||||
onMention : PropTypes.func.isRequired,
|
||||
onMute : PropTypes.func,
|
||||
onUnmute : PropTypes.func,
|
||||
onBlock : PropTypes.func,
|
||||
onUnblock : PropTypes.func,
|
||||
onBlockDomain : PropTypes.func,
|
||||
onUnblockDomain : PropTypes.func,
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
relationship: ImmutablePropTypes.map,
|
||||
onReply: PropTypes.func.isRequired,
|
||||
onReblog: PropTypes.func.isRequired,
|
||||
onFavourite: PropTypes.func.isRequired,
|
||||
onBookmark: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onDirect: PropTypes.func.isRequired,
|
||||
onMention: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func,
|
||||
onUnmute: PropTypes.func,
|
||||
onBlock: PropTypes.func,
|
||||
onUnblock: PropTypes.func,
|
||||
onBlockDomain: PropTypes.func,
|
||||
onUnblockDomain: PropTypes.func,
|
||||
onMuteConversation: PropTypes.func,
|
||||
onReport : PropTypes.func,
|
||||
onPin : PropTypes.func,
|
||||
onEmbed : PropTypes.func,
|
||||
intl : PropTypes.object.isRequired,
|
||||
onReport: PropTypes.func,
|
||||
onPin: PropTypes.func,
|
||||
onEmbed: PropTypes.func,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleReplyClick = () => {
|
||||
this.props.onReply(this.props.status);
|
||||
};
|
||||
}
|
||||
|
||||
handleReblogClick = (e) => {
|
||||
this.props.onReblog(this.props.status, e);
|
||||
};
|
||||
}
|
||||
|
||||
handleFavouriteClick = () => {
|
||||
this.props.onFavourite(this.props.status);
|
||||
};
|
||||
}
|
||||
|
||||
handleBookmarkClick = (e) => {
|
||||
this.props.onBookmark(this.props.status, e);
|
||||
};
|
||||
}
|
||||
|
||||
handleDeleteClick = () => {
|
||||
this.props.onDelete(this.props.status, this.context.router.history);
|
||||
};
|
||||
}
|
||||
|
||||
handleRedraftClick = () => {
|
||||
this.props.onDelete(this.props.status, this.context.router.history, true);
|
||||
};
|
||||
}
|
||||
|
||||
handleDirectClick = () => {
|
||||
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
|
||||
};
|
||||
}
|
||||
|
||||
handleMentionClick = () => {
|
||||
this.props.onMention(this.props.status.get('account'), this.context.router.history);
|
||||
};
|
||||
}
|
||||
|
||||
handleMuteClick = () => {
|
||||
const { status, relationship, onMute, onUnmute } = this.props;
|
||||
@ -114,7 +114,7 @@ class ActionBar extends React.PureComponent {
|
||||
} else {
|
||||
onMute(account);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleBlockClick = () => {
|
||||
const { status, relationship, onBlock, onUnblock } = this.props;
|
||||
@ -125,50 +125,50 @@ class ActionBar extends React.PureComponent {
|
||||
} else {
|
||||
onBlock(status);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleBlockDomain = () => {
|
||||
const { status, onBlockDomain } = this.props;
|
||||
const account = status.get('account');
|
||||
|
||||
onBlockDomain(account.get('acct').split('@')[1]);
|
||||
};
|
||||
}
|
||||
|
||||
handleUnblockDomain = () => {
|
||||
const { status, onUnblockDomain } = this.props;
|
||||
const account = status.get('account');
|
||||
|
||||
onUnblockDomain(account.get('acct').split('@')[1]);
|
||||
};
|
||||
}
|
||||
|
||||
handleConversationMuteClick = () => {
|
||||
this.props.onMuteConversation(this.props.status);
|
||||
};
|
||||
}
|
||||
|
||||
handleReport = () => {
|
||||
this.props.onReport(this.props.status);
|
||||
};
|
||||
}
|
||||
|
||||
handlePinClick = () => {
|
||||
this.props.onPin(this.props.status);
|
||||
};
|
||||
}
|
||||
|
||||
handleShare = () => {
|
||||
navigator.share({
|
||||
text: this.props.status.get('search_index'),
|
||||
url : this.props.status.get('url'),
|
||||
url: this.props.status.get('url'),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
handleEmbed = () => {
|
||||
this.props.onEmbed(this.props.status);
|
||||
};
|
||||
}
|
||||
|
||||
handleCopy = () => {
|
||||
const url = this.props.status.get('url');
|
||||
const url = this.props.status.get('url');
|
||||
const textarea = document.createElement('textarea');
|
||||
|
||||
textarea.textContent = url;
|
||||
textarea.textContent = url;
|
||||
textarea.style.position = 'fixed';
|
||||
|
||||
document.body.appendChild(textarea);
|
||||
@ -181,14 +181,14 @@ class ActionBar extends React.PureComponent {
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
render () {
|
||||
const { status, relationship, intl } = this.props;
|
||||
|
||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||
const mutingConversation = status.get('muted');
|
||||
const account = status.get('account');
|
||||
const account = status.get('account');
|
||||
|
||||
let menu = [];
|
||||
|
||||
@ -200,66 +200,36 @@ class ActionBar extends React.PureComponent {
|
||||
|
||||
if (me === status.getIn(['account', 'id'])) {
|
||||
if (publicStatus) {
|
||||
menu.push({
|
||||
text : intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin),
|
||||
action: this.handlePinClick,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||
} else {
|
||||
if (status.get('visibility') === 'private') {
|
||||
menu.push({
|
||||
text : intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private),
|
||||
action: this.handleReblogClick,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick });
|
||||
}
|
||||
}
|
||||
|
||||
menu.push(null);
|
||||
menu.push({
|
||||
text : intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation),
|
||||
action: this.handleConversationMuteClick,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
||||
} else {
|
||||
menu.push({
|
||||
text : intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }),
|
||||
action: this.handleMentionClick,
|
||||
});
|
||||
menu.push({
|
||||
text : intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }),
|
||||
action: this.handleDirectClick,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
||||
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
|
||||
menu.push(null);
|
||||
|
||||
if (relationship && relationship.get('muting')) {
|
||||
menu.push({
|
||||
text : intl.formatMessage(messages.unmute, { name: account.get('username') }),
|
||||
action: this.handleMuteClick,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
|
||||
} else {
|
||||
menu.push({
|
||||
text : intl.formatMessage(messages.mute, { name: account.get('username') }),
|
||||
action: this.handleMuteClick,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick });
|
||||
}
|
||||
|
||||
if (relationship && relationship.get('blocking')) {
|
||||
menu.push({
|
||||
text : intl.formatMessage(messages.unblock, { name: account.get('username') }),
|
||||
action: this.handleBlockClick,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
|
||||
} else {
|
||||
menu.push({
|
||||
text : intl.formatMessage(messages.block, { name: account.get('username') }),
|
||||
action: this.handleBlockClick,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick });
|
||||
}
|
||||
|
||||
menu.push({
|
||||
text : intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }),
|
||||
action: this.handleReport,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||
|
||||
if (account.get('acct') !== account.get('username')) {
|
||||
const domain = account.get('acct').split('@')[1];
|
||||
@ -275,23 +245,13 @@ class ActionBar extends React.PureComponent {
|
||||
|
||||
if (isStaff) {
|
||||
menu.push(null);
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }),
|
||||
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')}`,
|
||||
});
|
||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), 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' && (
|
||||
<div className='detailed-status__button'><IconButton
|
||||
title={intl.formatMessage(messages.share)}
|
||||
icon='share-alt'
|
||||
onClick={this.handleShare}
|
||||
/></div >
|
||||
const shareButton = ('share' in navigator) && publicStatus && (
|
||||
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShare} /></div>
|
||||
);
|
||||
|
||||
let replyIcon;
|
||||
@ -305,50 +265,18 @@ class ActionBar extends React.PureComponent {
|
||||
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
|
||||
else if (status.get('visibility') === 'private') reblogIcon = 'lock';
|
||||
|
||||
let reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private');
|
||||
|
||||
return (
|
||||
<div className='detailed-status__action-bar'>
|
||||
<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 >
|
||||
<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 >
|
||||
<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>
|
||||
<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}
|
||||
<div className='detailed-status__button'><IconButton
|
||||
className='bookmark-icon'
|
||||
active={status.get('bookmarked')}
|
||||
title={intl.formatMessage(messages.bookmark)}
|
||||
icon='bookmark'
|
||||
onClick={this.handleBookmarkClick}
|
||||
/></div >
|
||||
<div className='detailed-status__button'><IconButton 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'>
|
||||
<DropdownMenuContainer
|
||||
size={18}
|
||||
icon='ellipsis-h'
|
||||
status={status}
|
||||
items={menu}
|
||||
direction='left'
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
</div >
|
||||
</div >
|
||||
<DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
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 { throttle } from 'lodash';
|
||||
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 Icon from 'mastodon/components/icon';
|
||||
import { decode } from 'blurhash';
|
||||
@ -112,41 +112,28 @@ class Video extends React.PureComponent {
|
||||
};
|
||||
|
||||
state = {
|
||||
currentTime : 0,
|
||||
duration : 0,
|
||||
volume : 0.5,
|
||||
paused : true,
|
||||
dragging : false,
|
||||
currentTime: 0,
|
||||
duration: 0,
|
||||
volume: 0.5,
|
||||
paused: true,
|
||||
dragging: false,
|
||||
containerWidth: this.props.width,
|
||||
fullscreen : false,
|
||||
hovered : false,
|
||||
muted : false,
|
||||
revealed : this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
|
||||
fullscreen: false,
|
||||
hovered: false,
|
||||
muted: false,
|
||||
revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
|
||||
};
|
||||
|
||||
// Hard-coded in components.scss
|
||||
// Any way to get ::before values programatically?
|
||||
volWidth = 50;
|
||||
volWidth = 50;
|
||||
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 => {
|
||||
const offset = v * this.volWidth + this.volOffset;
|
||||
|
||||
return (offset > 110) ? 110 : offset;
|
||||
};
|
||||
}
|
||||
|
||||
setPlayerRef = c => {
|
||||
this.player = c;
|
||||
@ -158,7 +145,7 @@ class Video extends React.PureComponent {
|
||||
containerWidth: c.offsetWidth,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setVideoRef = c => {
|
||||
this.video = c;
|
||||
@ -166,36 +153,36 @@ class Video extends React.PureComponent {
|
||||
if (this.video) {
|
||||
this.setState({ volume: this.video.volume, muted: this.video.muted });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setSeekRef = c => {
|
||||
this.seek = c;
|
||||
};
|
||||
}
|
||||
|
||||
setVolumeRef = c => {
|
||||
this.volume = c;
|
||||
};
|
||||
|
||||
handleClickRoot = e => e.stopPropagation();
|
||||
}
|
||||
|
||||
setCanvasRef = c => {
|
||||
this.canvas = c;
|
||||
};
|
||||
}
|
||||
|
||||
handleClickRoot = e => e.stopPropagation();
|
||||
|
||||
handlePlay = () => {
|
||||
this.setState({ paused: false });
|
||||
};
|
||||
}
|
||||
|
||||
handlePause = () => {
|
||||
this.setState({ paused: true });
|
||||
};
|
||||
}
|
||||
|
||||
handleTimeUpdate = () => {
|
||||
this.setState({
|
||||
currentTime: Math.floor(this.video.currentTime),
|
||||
duration : Math.floor(this.video.duration),
|
||||
duration: Math.floor(this.video.duration),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
handleVolumeMouseDown = e => {
|
||||
document.addEventListener('mousemove', this.handleMouseVolSlide, true);
|
||||
@ -207,7 +194,14 @@ class Video extends React.PureComponent {
|
||||
|
||||
e.preventDefault();
|
||||
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 => {
|
||||
const rect = this.volume.getBoundingClientRect();
|
||||
@ -218,7 +212,7 @@ class Video extends React.PureComponent {
|
||||
|
||||
if(x > 1) {
|
||||
slideamt = 1;
|
||||
} else if (x < 0) {
|
||||
} else if(x < 0) {
|
||||
slideamt = 0;
|
||||
}
|
||||
|
||||
@ -227,13 +221,6 @@ class Video extends React.PureComponent {
|
||||
}
|
||||
}, 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 => {
|
||||
document.addEventListener('mousemove', this.handleMouseMove, true);
|
||||
document.addEventListener('mouseup', this.handleMouseUp, true);
|
||||
@ -246,7 +233,17 @@ class Video extends React.PureComponent {
|
||||
|
||||
e.preventDefault();
|
||||
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 => {
|
||||
const { x } = getPointerPosition(this.seek, e);
|
||||
@ -258,25 +255,23 @@ class Video extends React.PureComponent {
|
||||
}
|
||||
}, 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 = () => {
|
||||
if (this.state.paused) {
|
||||
this.setState({ paused: false }, () => this.video.play());
|
||||
} else {
|
||||
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('webkitfullscreenchange', this.handleFullscreenChange, true);
|
||||
document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
|
||||
@ -321,32 +316,37 @@ class Video extends React.PureComponent {
|
||||
const pixels = decode(hash, 32, 32);
|
||||
|
||||
if (pixels) {
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
const imageData = new ImageData(pixels, 32, 32);
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
toggleFullscreen = () => {
|
||||
if (isFullscreen()) {
|
||||
exitFullscreen();
|
||||
} else {
|
||||
requestFullscreen(this.player);
|
||||
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 })
|
||||
|
||||
handleFullscreenChange = () => {
|
||||
this.setState({ fullscreen: isFullscreen() });
|
||||
};
|
||||
}
|
||||
|
||||
handleMouseEnter = () => {
|
||||
this.setState({ hovered: true });
|
||||
};
|
||||
}
|
||||
|
||||
handleMouseLeave = () => {
|
||||
this.setState({ hovered: false });
|
||||
};
|
||||
}
|
||||
|
||||
toggleMute = () => {
|
||||
const muted = !this.video.muted;
|
||||
@ -354,7 +354,7 @@ class Video extends React.PureComponent {
|
||||
this.setState({ muted }, () => {
|
||||
this.video.muted = muted;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
toggleReveal = () => {
|
||||
if (this.props.onToggleVisibility) {
|
||||
@ -362,31 +362,31 @@ class Video extends React.PureComponent {
|
||||
} else {
|
||||
this.setState({ revealed: !this.state.revealed });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleLoadedData = () => {
|
||||
if (this.props.startTime) {
|
||||
this.video.currentTime = this.props.startTime;
|
||||
this.video.play();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleProgress = () => {
|
||||
if (this.video.buffered.length > 0) {
|
||||
this.setState({ buffer: this.video.buffered.end(0) / this.video.duration * 100 });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleVolumeChange = () => {
|
||||
this.setState({ volume: this.video.volume, muted: this.video.muted });
|
||||
};
|
||||
}
|
||||
|
||||
handleOpenVideo = () => {
|
||||
const { src, preview, width, height, alt } = this.props;
|
||||
|
||||
const media = fromJS({
|
||||
type : 'video',
|
||||
url : src,
|
||||
type: 'video',
|
||||
url: src,
|
||||
preview_url: preview,
|
||||
description: alt,
|
||||
width,
|
||||
@ -395,12 +395,12 @@ class Video extends React.PureComponent {
|
||||
|
||||
this.video.pause();
|
||||
this.props.onOpenVideo(media, this.video.currentTime);
|
||||
};
|
||||
}
|
||||
|
||||
handleCloseVideo = () => {
|
||||
this.video.pause();
|
||||
this.props.onCloseVideo();
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
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 left'>
|
||||
<button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
|
||||
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
||||
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
|
||||
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
||||
|
||||
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
|
||||
|
||||
@ -517,25 +517,11 @@ class Video extends React.PureComponent {
|
||||
</div>
|
||||
|
||||
<div className='video-player__buttons right'>
|
||||
{(!onCloseVideo && !editable && !fullscreen) && <button
|
||||
type='button'
|
||||
aria-label={intl.formatMessage(messages.hide)}
|
||||
onClick={this.toggleReveal}
|
||||
><Icon
|
||||
id='eye-slash'
|
||||
fixedWidth
|
||||
/></button >}
|
||||
{(!fullscreen && onOpenVideo) && <button
|
||||
type='button'
|
||||
aria-label={intl.formatMessage(messages.expand)}
|
||||
onClick={this.handleOpenVideo}
|
||||
><Icon
|
||||
id='expand'
|
||||
fixedWidth
|
||||
/></button >}
|
||||
{onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
|
||||
<button type='button' aria-label={intl.formatMessage(messages.download)}><a className='video-player__download__icon' href={this.props.src} download><Icon id={'download'} fixedWidth /></a></button>
|
||||
<button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
|
||||
{(!onCloseVideo && !editable && !fullscreen) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
|
||||
{(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
|
||||
{onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
|
||||
<button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)}><a className='video-player__download__icon' href={this.props.src} download><Icon id={'download'} fixedWidth /></a></button>
|
||||
<button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -254,7 +254,7 @@
|
||||
"lists.subheading": "Your lists",
|
||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||
"loading_indicator.label": "Зареждане...",
|
||||
"media_gallery.toggle_visible": "Toggle visibility",
|
||||
"media_gallery.toggle_visible": "Hide media",
|
||||
"missing_indicator.label": "Not found",
|
||||
"missing_indicator.sublabel": "This resource could not be found",
|
||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||
|
@ -254,7 +254,7 @@
|
||||
"lists.subheading": "Your lists",
|
||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||
"loading_indicator.label": "Loading...",
|
||||
"media_gallery.toggle_visible": "Toggle visibility",
|
||||
"media_gallery.toggle_visible": "Hide media",
|
||||
"missing_indicator.label": "Not found",
|
||||
"missing_indicator.sublabel": "This resource could not be found",
|
||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||
|
@ -142,7 +142,7 @@
|
||||
{
|
||||
"descriptors": [
|
||||
{
|
||||
"defaultMessage": "Unhide {domain}",
|
||||
"defaultMessage": "Unblock domain {domain}",
|
||||
"id": "account.unblock_domain"
|
||||
}
|
||||
],
|
||||
@ -217,7 +217,7 @@
|
||||
{
|
||||
"descriptors": [
|
||||
{
|
||||
"defaultMessage": "Toggle visibility",
|
||||
"defaultMessage": "Hide media",
|
||||
"id": "media_gallery.toggle_visible"
|
||||
},
|
||||
{
|
||||
@ -451,11 +451,11 @@
|
||||
"id": "status.copy"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Hide everything from {domain}",
|
||||
"defaultMessage": "Block domain {domain}",
|
||||
"id": "account.block_domain"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Unhide {domain}",
|
||||
"defaultMessage": "Unblock domain {domain}",
|
||||
"id": "account.unblock_domain"
|
||||
},
|
||||
{
|
||||
@ -697,11 +697,11 @@
|
||||
"id": "account.media"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Hide everything from {domain}",
|
||||
"defaultMessage": "Block domain {domain}",
|
||||
"id": "account.block_domain"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Unhide {domain}",
|
||||
"defaultMessage": "Unblock domain {domain}",
|
||||
"id": "account.unblock_domain"
|
||||
},
|
||||
{
|
||||
@ -1073,7 +1073,7 @@
|
||||
"id": "privacy.public.short"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Post to public timelines",
|
||||
"defaultMessage": "Visible for all, shown in public timelines",
|
||||
"id": "privacy.public.long"
|
||||
},
|
||||
{
|
||||
@ -1081,7 +1081,7 @@
|
||||
"id": "privacy.unlisted.short"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Do not show in public timelines",
|
||||
"defaultMessage": "Visible for all, but not in public timelines",
|
||||
"id": "privacy.unlisted.long"
|
||||
},
|
||||
{
|
||||
@ -1089,7 +1089,7 @@
|
||||
"id": "privacy.private.short"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Post to followers only",
|
||||
"defaultMessage": "Visible for followers only",
|
||||
"id": "privacy.private.long"
|
||||
},
|
||||
{
|
||||
@ -1097,7 +1097,7 @@
|
||||
"id": "privacy.direct.short"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Post to mentioned users only",
|
||||
"defaultMessage": "Visible for mentioned users only",
|
||||
"id": "privacy.direct.long"
|
||||
},
|
||||
{
|
||||
@ -1470,7 +1470,7 @@
|
||||
"id": "column.domain_blocks"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Unhide {domain}",
|
||||
"defaultMessage": "Unblock domain {domain}",
|
||||
"id": "account.unblock_domain"
|
||||
},
|
||||
{
|
||||
@ -2384,11 +2384,11 @@
|
||||
"id": "status.copy"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Hide everything from {domain}",
|
||||
"defaultMessage": "Block domain {domain}",
|
||||
"id": "account.block_domain"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Unhide {domain}",
|
||||
"defaultMessage": "Unblock domain {domain}",
|
||||
"id": "account.unblock_domain"
|
||||
},
|
||||
{
|
||||
@ -2944,4 +2944,4 @@
|
||||
],
|
||||
"path": "app/javascript/mastodon/features/video/index.json"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
@ -3,7 +3,7 @@
|
||||
"account.badges.bot": "Bot",
|
||||
"account.badges.group": "Group",
|
||||
"account.block": "Block @{name}",
|
||||
"account.block_domain": "Hide everything from {domain}",
|
||||
"account.block_domain": "Block domain {domain}",
|
||||
"account.blocked": "Blocked",
|
||||
"account.cancel_follow_request": "Cancel follow request",
|
||||
"account.direct": "Direct message @{name}",
|
||||
@ -34,7 +34,7 @@
|
||||
"account.share": "Share @{name}'s profile",
|
||||
"account.show_reblogs": "Show boosts from @{name}",
|
||||
"account.unblock": "Unblock @{name}",
|
||||
"account.unblock_domain": "Unhide {domain}",
|
||||
"account.unblock_domain": "Unblock domain {domain}",
|
||||
"account.unendorse": "Don't feature on profile",
|
||||
"account.unfollow": "Unfollow",
|
||||
"account.unmute": "Unmute @{name}",
|
||||
@ -254,7 +254,7 @@
|
||||
"lists.subheading": "Your lists",
|
||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||
"loading_indicator.label": "Loading...",
|
||||
"media_gallery.toggle_visible": "Toggle visibility",
|
||||
"media_gallery.toggle_visible": "Hide media",
|
||||
"missing_indicator.label": "Not found",
|
||||
"missing_indicator.sublabel": "This resource could not be found",
|
||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||
@ -319,13 +319,13 @@
|
||||
"poll_button.add_poll": "Add a poll",
|
||||
"poll_button.remove_poll": "Remove poll",
|
||||
"privacy.change": "Adjust status privacy",
|
||||
"privacy.direct.long": "Post to mentioned users only",
|
||||
"privacy.direct.long": "Visible for mentioned users only",
|
||||
"privacy.direct.short": "Direct",
|
||||
"privacy.private.long": "Post to followers only",
|
||||
"privacy.private.long": "Visible for followers only",
|
||||
"privacy.private.short": "Followers-only",
|
||||
"privacy.public.long": "Post to public timelines",
|
||||
"privacy.public.long": "Visible for all, shown in public timelines",
|
||||
"privacy.public.short": "Public",
|
||||
"privacy.unlisted.long": "Do not post to public timelines",
|
||||
"privacy.unlisted.long": "Visible for all, but not in public timelines",
|
||||
"privacy.unlisted.short": "Unlisted",
|
||||
"refresh": "Refresh",
|
||||
"regeneration_indicator.label": "Loading…",
|
||||
|
@ -254,7 +254,7 @@
|
||||
"lists.subheading": "Your lists",
|
||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||
"loading_indicator.label": "Loading...",
|
||||
"media_gallery.toggle_visible": "Toggle visibility",
|
||||
"media_gallery.toggle_visible": "Hide media",
|
||||
"missing_indicator.label": "Not found",
|
||||
"missing_indicator.sublabel": "This resource could not be found",
|
||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||
|
@ -254,7 +254,7 @@
|
||||
"lists.subheading": "Your lists",
|
||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||
"loading_indicator.label": "लोड हो रहा है...",
|
||||
"media_gallery.toggle_visible": "Toggle visibility",
|
||||
"media_gallery.toggle_visible": "Hide media",
|
||||
"missing_indicator.label": "नहीं मिला",
|
||||
"missing_indicator.sublabel": "यह संसाधन नहीं मिल सका।",
|
||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||
|
@ -254,7 +254,7 @@
|
||||
"lists.subheading": "Your lists",
|
||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||
"loading_indicator.label": "Loading...",
|
||||
"media_gallery.toggle_visible": "Toggle visibility",
|
||||
"media_gallery.toggle_visible": "Hide media",
|
||||
"missing_indicator.label": "Not found",
|
||||
"missing_indicator.sublabel": "This resource could not be found",
|
||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||
|
@ -254,7 +254,7 @@
|
||||
"lists.subheading": "Your lists",
|
||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||
"loading_indicator.label": "Loading...",
|
||||
"media_gallery.toggle_visible": "Toggle visibility",
|
||||
"media_gallery.toggle_visible": "Hide media",
|
||||
"missing_indicator.label": "Not found",
|
||||
"missing_indicator.sublabel": "This resource could not be found",
|
||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||
|
@ -254,7 +254,7 @@
|
||||
"lists.subheading": "Your lists",
|
||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||
"loading_indicator.label": "Loading...",
|
||||
"media_gallery.toggle_visible": "Toggle visibility",
|
||||
"media_gallery.toggle_visible": "Hide media",
|
||||
"missing_indicator.label": "Not found",
|
||||
"missing_indicator.sublabel": "This resource could not be found",
|
||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||
|
@ -254,7 +254,7 @@
|
||||
"lists.subheading": "Your lists",
|
||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||
"loading_indicator.label": "Loading...",
|
||||
"media_gallery.toggle_visible": "Toggle visibility",
|
||||
"media_gallery.toggle_visible": "Hide media",
|
||||
"missing_indicator.label": "Not found",
|
||||
"missing_indicator.sublabel": "This resource could not be found",
|
||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||
|
@ -254,7 +254,7 @@
|
||||
"lists.subheading": "Your lists",
|
||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||
"loading_indicator.label": "Loading...",
|
||||
"media_gallery.toggle_visible": "Toggle visibility",
|
||||
"media_gallery.toggle_visible": "Hide media",
|
||||
"missing_indicator.label": "Not found",
|
||||
"missing_indicator.sublabel": "This resource could not be found",
|
||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||
|
@ -254,7 +254,7 @@
|
||||
"lists.subheading": "Your lists",
|
||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||
"loading_indicator.label": "Loading...",
|
||||
"media_gallery.toggle_visible": "Toggle visibility",
|
||||
"media_gallery.toggle_visible": "Hide media",
|
||||
"missing_indicator.label": "Not found",
|
||||
"missing_indicator.sublabel": "This resource could not be found",
|
||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||
|
@ -254,7 +254,7 @@
|
||||
"lists.subheading": "Your lists",
|
||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||
"loading_indicator.label": "Loading...",
|
||||
"media_gallery.toggle_visible": "Toggle visibility",
|
||||
"media_gallery.toggle_visible": "Hide media",
|
||||
"missing_indicator.label": "Not found",
|
||||
"missing_indicator.sublabel": "This resource could not be found",
|
||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||
|
@ -254,7 +254,7 @@
|
||||
"lists.subheading": "Your lists",
|
||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||
"loading_indicator.label": "Loading...",
|
||||
"media_gallery.toggle_visible": "Toggle visibility",
|
||||
"media_gallery.toggle_visible": "Hide media",
|
||||
"missing_indicator.label": "Not found",
|
||||
"missing_indicator.sublabel": "This resource could not be found",
|
||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||
|
@ -388,8 +388,8 @@
|
||||
|
||||
.emoji-picker-dropdown {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.compose-form__autosuggest-wrapper {
|
||||
@ -870,6 +870,7 @@
|
||||
|
||||
.announcements__item__content {
|
||||
word-wrap: break-word;
|
||||
overflow-y: auto;
|
||||
|
||||
.emojione {
|
||||
width: 20px;
|
||||
@ -4070,10 +4071,7 @@ a.status-card.compact:hover {
|
||||
|
||||
.emoji-button {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
margin-left: 2px;
|
||||
width: 24px;
|
||||
padding: 5px 5px 2px 2px;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
|
||||
@ -4089,7 +4087,6 @@ a.status-card.compact:hover {
|
||||
margin: 0;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
@ -5068,12 +5065,6 @@ a.status-card.compact:hover {
|
||||
}
|
||||
|
||||
.media-gallery__gifv {
|
||||
&.autoplay {
|
||||
.media-gallery__gifv__label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.media-gallery__gifv__label {
|
||||
opacity: 1;
|
||||
@ -6699,17 +6690,21 @@ noscript {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
padding-right: 15px + 18px;
|
||||
position: relative;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
word-wrap: break-word;
|
||||
font-weight: 400;
|
||||
max-height: 50vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__range {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
&__unread {
|
||||
|
@ -8,6 +8,7 @@ module Mastodon
|
||||
class LengthValidationError < ValidationError; end
|
||||
class DimensionsValidationError < ValidationError; end
|
||||
class RaceConditionError < Error; end
|
||||
class RateLimitExceededError < Error; end
|
||||
|
||||
class UnexpectedResponseError < Error
|
||||
def initialize(response = nil)
|
||||
|
64
app/lib/rate_limiter.rb
Normal file
64
app/lib/rate_limiter.rb
Normal 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
|
@ -102,6 +102,7 @@ class Account < ApplicationRecord
|
||||
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
|
||||
scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
|
||||
scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) }
|
||||
scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) }
|
||||
scope :popular, -> { order('account_stats.followers_count desc') }
|
||||
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
|
||||
scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
|
||||
@ -478,7 +479,16 @@ class Account < ApplicationRecord
|
||||
def from_text(text)
|
||||
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
|
||||
|
||||
private
|
||||
|
@ -14,6 +14,7 @@ class AccountFilter
|
||||
email
|
||||
ip
|
||||
staff
|
||||
order
|
||||
).freeze
|
||||
|
||||
attr_reader :params
|
||||
@ -24,7 +25,7 @@ class AccountFilter
|
||||
end
|
||||
|
||||
def results
|
||||
scope = Account.recent.includes(:user)
|
||||
scope = Account.includes(:user).reorder(nil)
|
||||
|
||||
params.each do |key, value|
|
||||
scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
|
||||
@ -38,6 +39,7 @@ class AccountFilter
|
||||
def set_defaults!
|
||||
params['local'] = '1' if params['remote'].blank?
|
||||
params['active'] = '1' if params['suspended'].blank? && params['silenced'].blank? && params['pending'].blank?
|
||||
params['order'] = 'recent' if params['order'].blank?
|
||||
end
|
||||
|
||||
def scope_for(key, value)
|
||||
@ -51,9 +53,9 @@ class AccountFilter
|
||||
when 'active'
|
||||
Account.without_suspended
|
||||
when 'pending'
|
||||
accounts_with_users.merge User.pending
|
||||
accounts_with_users.merge(User.pending)
|
||||
when 'disabled'
|
||||
accounts_with_users.merge User.disabled
|
||||
accounts_with_users.merge(User.disabled)
|
||||
when 'silenced'
|
||||
Account.silenced
|
||||
when 'suspended'
|
||||
@ -63,16 +65,31 @@ class AccountFilter
|
||||
when 'display_name'
|
||||
Account.matches_display_name(value)
|
||||
when 'email'
|
||||
accounts_with_users.merge User.matches_email(value)
|
||||
accounts_with_users.merge(User.matches_email(value))
|
||||
when 'ip'
|
||||
valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value)) : Account.none
|
||||
when 'staff'
|
||||
accounts_with_users.merge User.staff
|
||||
accounts_with_users.merge(User.staff)
|
||||
when 'order'
|
||||
order_scope(value)
|
||||
else
|
||||
raise "Unknown filter: #{key}"
|
||||
end
|
||||
end
|
||||
|
||||
def order_scope(value)
|
||||
case value
|
||||
when 'active'
|
||||
params['remote'] ? Account.joins(:account_stat).by_recent_status : Account.joins(:user).by_recent_sign_in
|
||||
when 'recent'
|
||||
Account.recent
|
||||
when 'alphabetic'
|
||||
Account.alphabetic
|
||||
else
|
||||
raise "Unknown order: #{value}"
|
||||
end
|
||||
end
|
||||
|
||||
def accounts_with_users
|
||||
Account.joins(:user)
|
||||
end
|
||||
|
@ -48,6 +48,10 @@ class Announcement < ApplicationRecord
|
||||
@mentions ||= Account.from_text(text)
|
||||
end
|
||||
|
||||
def statuses
|
||||
@statuses ||= Status.from_text(text)
|
||||
end
|
||||
|
||||
def tags
|
||||
@tags ||= Tag.find_or_create_by_names(Extractor.extract_hashtags(text))
|
||||
end
|
||||
|
@ -87,10 +87,10 @@ module AccountInteractions
|
||||
has_many :announcement_mutes, dependent: :destroy
|
||||
end
|
||||
|
||||
def follow!(other_account, reblogs: nil, uri: nil)
|
||||
def follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
|
||||
reblogs = true if reblogs.nil?
|
||||
|
||||
rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri)
|
||||
rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
|
||||
.find_or_create_by!(target_account: other_account)
|
||||
|
||||
rel.update!(show_reblogs: reblogs)
|
||||
@ -99,6 +99,18 @@ module AccountInteractions
|
||||
rel
|
||||
end
|
||||
|
||||
def request_follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
|
||||
reblogs = true if reblogs.nil?
|
||||
|
||||
rel = follow_requests.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
|
||||
.find_or_create_by!(target_account: other_account)
|
||||
|
||||
rel.update!(show_reblogs: reblogs)
|
||||
remove_potential_friendship(other_account)
|
||||
|
||||
rel
|
||||
end
|
||||
|
||||
def block!(other_account, uri: nil)
|
||||
remove_potential_friendship(other_account)
|
||||
block_relationships.create_with(uri: uri)
|
||||
|
36
app/models/concerns/rate_limitable.rb
Normal file
36
app/models/concerns/rate_limitable.rb
Normal 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
|
@ -15,6 +15,9 @@
|
||||
class Follow < ApplicationRecord
|
||||
include Paginable
|
||||
include RelationshipCacheable
|
||||
include RateLimitable
|
||||
|
||||
rate_limit by: :account, family: :follows
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :target_account, class_name: 'Account'
|
||||
|
@ -15,6 +15,9 @@
|
||||
class FollowRequest < ApplicationRecord
|
||||
include Paginable
|
||||
include RelationshipCacheable
|
||||
include RateLimitable
|
||||
|
||||
rate_limit by: :account, family: :follows
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :target_account, class_name: 'Account'
|
||||
|
@ -170,6 +170,7 @@ class MediaAttachment < ApplicationRecord
|
||||
|
||||
def variant?(other_file_name)
|
||||
return true if file_file_name == other_file_name
|
||||
return false if file_file_name.nil?
|
||||
|
||||
formats = file.styles.values.map(&:format).compact
|
||||
|
||||
|
@ -32,6 +32,9 @@ class Status < ApplicationRecord
|
||||
include Paginable
|
||||
include Cacheable
|
||||
include StatusThreadingConcern
|
||||
include RateLimitable
|
||||
|
||||
rate_limit by: :account, family: :statuses
|
||||
|
||||
self.discard_column = :deleted_at
|
||||
|
||||
@ -366,6 +369,21 @@ class Status < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def from_text(text)
|
||||
return [] if text.blank?
|
||||
|
||||
text.scan(FetchLinkCardService::URL_PATTERN).map(&:first).uniq.map do |url|
|
||||
status = begin
|
||||
if TagManager.instance.local_url?(url)
|
||||
ActivityPub::TagManager.instance.uri_to_resource(url, Status)
|
||||
else
|
||||
Status.find_by(uri: url) || Status.find_by(url: url)
|
||||
end
|
||||
end
|
||||
status&.distributable? ? status : nil
|
||||
end.compact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def timeline_scope(local_only = false)
|
||||
|
@ -8,4 +8,8 @@ class SettingsPolicy < ApplicationPolicy
|
||||
def show?
|
||||
admin?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
admin?
|
||||
end
|
||||
end
|
||||
|
@ -7,6 +7,7 @@ class REST::AnnouncementSerializer < ActiveModel::Serializer
|
||||
attribute :read, if: :current_user?
|
||||
|
||||
has_many :mentions
|
||||
has_many :statuses
|
||||
has_many :tags, serializer: REST::StatusSerializer::TagSerializer
|
||||
has_many :emojis, serializer: REST::CustomEmojiSerializer
|
||||
has_many :reactions, serializer: REST::ReactionSerializer
|
||||
@ -46,4 +47,16 @@ class REST::AnnouncementSerializer < ActiveModel::Serializer
|
||||
object.pretty_acct
|
||||
end
|
||||
end
|
||||
|
||||
class StatusSerializer < ActiveModel::Serializer
|
||||
attributes :id, :url
|
||||
|
||||
def id
|
||||
object.id.to_s
|
||||
end
|
||||
|
||||
def url
|
||||
ActivityPub::TagManager.instance.url_for(object)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -171,7 +171,7 @@ class AccountSearchService < BaseService
|
||||
end
|
||||
|
||||
def username_complete?
|
||||
query.include?('@') && "@#{query}" =~ Account::MENTION_RE
|
||||
query.include?('@') && "@#{query}" =~ /\A#{Account::MENTION_RE}\Z/
|
||||
end
|
||||
|
||||
def likely_acct?
|
||||
|
@ -7,54 +7,68 @@ class FollowService < BaseService
|
||||
# Follow a remote user, notify remote user about the follow
|
||||
# @param [Account] source_account From which to follow
|
||||
# @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
|
||||
# @param [true, false, nil] reblogs Whether or not to show reblogs, defaults to true
|
||||
def call(source_account, target_account, reblogs: nil, bypass_locked: false)
|
||||
reblogs = true if reblogs.nil?
|
||||
target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
|
||||
# @param [Hash] options
|
||||
# @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true
|
||||
# @option [Boolean] :bypass_locked
|
||||
# @option [Boolean] :with_rate_limit
|
||||
def call(source_account, target_account, options = {})
|
||||
@source_account = source_account
|
||||
@target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
|
||||
@options = { reblogs: true, bypass_locked: false, with_rate_limit: false }.merge(options)
|
||||
|
||||
raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
|
||||
raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) || target_account.moved? || (!target_account.local? && target_account.ostatus?) || source_account.domain_blocking?(target_account.domain)
|
||||
raise ActiveRecord::RecordNotFound if following_not_possible?
|
||||
raise Mastodon::NotPermittedError if following_not_allowed?
|
||||
|
||||
if source_account.following?(target_account)
|
||||
# We're already following this account, but we'll call follow! again to
|
||||
# make sure the reblogs status is set correctly.
|
||||
return source_account.follow!(target_account, reblogs: reblogs)
|
||||
elsif source_account.requested?(target_account)
|
||||
# This isn't managed by a method in AccountInteractions, so we modify it
|
||||
# ourselves if necessary.
|
||||
req = source_account.follow_requests.find_by(target_account: target_account)
|
||||
req.update!(show_reblogs: reblogs)
|
||||
return req
|
||||
if @source_account.following?(@target_account)
|
||||
return change_follow_options!
|
||||
elsif @source_account.requested?(@target_account)
|
||||
return change_follow_request_options!
|
||||
end
|
||||
|
||||
ActivityTracker.increment('activity:interactions')
|
||||
|
||||
if (target_account.locked? && !bypass_locked) || source_account.silenced? || target_account.activitypub?
|
||||
request_follow(source_account, target_account, reblogs: reblogs)
|
||||
elsif target_account.local?
|
||||
direct_follow(source_account, target_account, reblogs: reblogs)
|
||||
if (@target_account.locked? && !@options[:bypass_locked]) || @source_account.silenced? || @target_account.activitypub?
|
||||
request_follow!
|
||||
elsif @target_account.local?
|
||||
direct_follow!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def request_follow(source_account, target_account, reblogs: true)
|
||||
follow_request = FollowRequest.create!(account: source_account, target_account: target_account, show_reblogs: reblogs)
|
||||
def following_not_possible?
|
||||
@target_account.nil? || @target_account.id == @source_account.id || @target_account.suspended?
|
||||
end
|
||||
|
||||
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)
|
||||
def following_not_allowed?
|
||||
@target_account.blocking?(@source_account) || @source_account.blocking?(@target_account) || @target_account.moved? || (!@target_account.local? && @target_account.ostatus?) || @source_account.domain_blocking?(@target_account.domain)
|
||||
end
|
||||
|
||||
def change_follow_options!
|
||||
@source_account.follow!(@target_account, reblogs: @options[:reblogs])
|
||||
end
|
||||
|
||||
def change_follow_request_options!
|
||||
@source_account.request_follow!(@target_account, reblogs: @options[:reblogs])
|
||||
end
|
||||
|
||||
def request_follow!
|
||||
follow_request = @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], rate_limit: @options[:with_rate_limit])
|
||||
|
||||
if @target_account.local?
|
||||
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name)
|
||||
elsif @target_account.activitypub?
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url)
|
||||
end
|
||||
|
||||
follow_request
|
||||
end
|
||||
|
||||
def direct_follow(source_account, target_account, reblogs: true)
|
||||
follow = source_account.follow!(target_account, reblogs: reblogs)
|
||||
def direct_follow!
|
||||
follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], rate_limit: @options[:with_rate_limit])
|
||||
|
||||
LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name)
|
||||
MergeWorker.perform_async(target_account.id, source_account.id)
|
||||
LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name)
|
||||
MergeWorker.perform_async(@target_account.id, @source_account.id)
|
||||
|
||||
follow
|
||||
end
|
||||
|
@ -19,6 +19,7 @@ class PostStatusService < BaseService
|
||||
# @option [Enumerable] :media_ids Optional array of media IDs to attach
|
||||
# @option [Doorkeeper::Application] :application
|
||||
# @option [String] :idempotency Optional idempotency key
|
||||
# @option [Boolean] :with_rate_limit
|
||||
# @return [Status]
|
||||
def call(account, options = {})
|
||||
@account = account
|
||||
@ -49,7 +50,7 @@ class PostStatusService < BaseService
|
||||
def preprocess_attributes!
|
||||
@text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present?
|
||||
@visibility = @options[:visibility] || @account.user&.setting_default_privacy
|
||||
@visibility = :unlisted if @visibility == :public && @account.silenced?
|
||||
@visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced?
|
||||
@scheduled_at = @options[:scheduled_at]&.to_datetime
|
||||
@scheduled_at = nil if scheduled_in_the_past?
|
||||
rescue ArgumentError
|
||||
@ -160,6 +161,7 @@ class PostStatusService < BaseService
|
||||
visibility: @visibility,
|
||||
language: language_from_option(@options[:language]) || @account.user&.setting_default_language&.presence || LanguageDetector.instance.detect(@text, @account),
|
||||
application: @options[:application],
|
||||
rate_limit: @options[:with_rate_limit],
|
||||
}.compact
|
||||
end
|
||||
|
||||
@ -179,10 +181,11 @@ class PostStatusService < BaseService
|
||||
|
||||
def scheduled_options
|
||||
@options.tap do |options_hash|
|
||||
options_hash[:in_reply_to_id] = options_hash.delete(:thread)&.id
|
||||
options_hash[:application_id] = options_hash.delete(:application)&.id
|
||||
options_hash[:scheduled_at] = nil
|
||||
options_hash[:idempotency] = nil
|
||||
options_hash[:in_reply_to_id] = options_hash.delete(:thread)&.id
|
||||
options_hash[:application_id] = options_hash.delete(:application)&.id
|
||||
options_hash[:scheduled_at] = nil
|
||||
options_hash[:idempotency] = nil
|
||||
options_hash[:with_rate_limit] = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -8,6 +8,8 @@ class ReblogService < BaseService
|
||||
# @param [Account] account Account to reblog from
|
||||
# @param [Status] reblogged_status Status to be reblogged
|
||||
# @param [Hash] options
|
||||
# @option [String] :visibility
|
||||
# @option [Boolean] :with_rate_limit
|
||||
# @return [Status]
|
||||
def call(account, reblogged_status, options = {})
|
||||
reblogged_status = reblogged_status.reblog if reblogged_status.reblog?
|
||||
@ -18,9 +20,15 @@ class ReblogService < BaseService
|
||||
|
||||
return reblog unless reblog.nil?
|
||||
|
||||
visibility = options[:visibility] || account.user&.setting_default_privacy
|
||||
visibility = reblogged_status.visibility if reblogged_status.hidden?
|
||||
reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility)
|
||||
visibility = begin
|
||||
if reblogged_status.hidden?
|
||||
reblogged_status.visibility
|
||||
else
|
||||
options[:visibility] || account.user&.setting_default_privacy
|
||||
end
|
||||
end
|
||||
|
||||
reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility, rate_limit: options[:with_rate_limit])
|
||||
|
||||
DistributionWorker.perform_async(reblog.id)
|
||||
ActivityPub::DistributionWorker.perform_async(reblog.id)
|
||||
@ -45,7 +53,9 @@ class ReblogService < BaseService
|
||||
|
||||
def bump_potential_friendship(account, reblog)
|
||||
ActivityTracker.increment('activity:interactions')
|
||||
|
||||
return if account.following?(reblog.reblog.account_id)
|
||||
|
||||
PotentialFriendshipTracker.record(account.id, reblog.reblog.account_id, :reblog)
|
||||
end
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user