Merge tag 'v2.7.0rc1' into instance_only_statuses
This commit is contained in:
@ -31,7 +31,7 @@ class ActivityPub::CollectionsController < Api::BaseController
|
||||
when 'featured'
|
||||
@account.pinned_statuses.count
|
||||
else
|
||||
raise ActiveRecord::NotFound
|
||||
raise ActiveRecord::RecordNotFound
|
||||
end
|
||||
end
|
||||
|
||||
@ -42,7 +42,7 @@ class ActivityPub::CollectionsController < Api::BaseController
|
||||
scope.merge!(@account.pinned_statuses)
|
||||
end
|
||||
else
|
||||
raise ActiveRecord::NotFound
|
||||
raise ActiveRecord::RecordNotFound
|
||||
end
|
||||
end
|
||||
|
||||
|
36
app/controllers/admin/account_actions_controller.rb
Normal file
36
app/controllers/admin/account_actions_controller.rb
Normal file
@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class AccountActionsController < BaseController
|
||||
before_action :set_account
|
||||
|
||||
def new
|
||||
@account_action = Admin::AccountAction.new(type: params[:type], report_id: params[:report_id], send_email_notification: true)
|
||||
@warning_presets = AccountWarningPreset.all
|
||||
end
|
||||
|
||||
def create
|
||||
account_action = Admin::AccountAction.new(resource_params)
|
||||
account_action.target_account = @account
|
||||
account_action.current_account = current_account
|
||||
|
||||
account_action.save!
|
||||
|
||||
if account_action.with_report?
|
||||
redirect_to admin_reports_path
|
||||
else
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:admin_account_action).permit(:type, :report_id, :warning_preset_id, :text, :send_email_notification)
|
||||
end
|
||||
end
|
||||
end
|
@ -14,6 +14,7 @@ module Admin
|
||||
else
|
||||
@account = @account_moderation_note.target_account
|
||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||
@warnings = @account.targeted_account_warnings.latest.custom
|
||||
|
||||
render template: 'admin/accounts/show'
|
||||
end
|
||||
|
@ -2,9 +2,9 @@
|
||||
|
||||
module Admin
|
||||
class AccountsController < BaseController
|
||||
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :enable, :disable, :memorialize]
|
||||
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize]
|
||||
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
|
||||
before_action :require_local_account!, only: [:enable, :disable, :memorialize]
|
||||
before_action :require_local_account!, only: [:enable, :memorialize]
|
||||
|
||||
def index
|
||||
authorize :account, :index?
|
||||
@ -13,8 +13,10 @@ module Admin
|
||||
|
||||
def show
|
||||
authorize @account, :show?
|
||||
|
||||
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||
@warnings = @account.targeted_account_warnings.latest.custom
|
||||
end
|
||||
|
||||
def subscribe
|
||||
@ -43,19 +45,25 @@ module Admin
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
||||
def disable
|
||||
authorize @account.user, :disable?
|
||||
@account.user.disable!
|
||||
log_action :disable, @account.user
|
||||
def unsilence
|
||||
authorize @account, :unsilence?
|
||||
@account.unsilence!
|
||||
log_action :unsilence, @account
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
||||
def unsuspend
|
||||
authorize @account, :unsuspend?
|
||||
@account.unsuspend!
|
||||
log_action :unsuspend, @account
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
||||
def redownload
|
||||
authorize @account, :redownload?
|
||||
|
||||
@account.reset_avatar!
|
||||
@account.reset_header!
|
||||
@account.save!
|
||||
@account.update!(last_webfingered_at: nil)
|
||||
ResolveAccountService.new.call(@account)
|
||||
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
@ -71,6 +79,17 @@ module Admin
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
||||
def remove_header
|
||||
authorize @account, :remove_header?
|
||||
|
||||
@account.header = nil
|
||||
@account.save!
|
||||
|
||||
log_action :remove_header, @account.user
|
||||
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@ -94,8 +113,8 @@ module Admin
|
||||
:local,
|
||||
:remote,
|
||||
:by_domain,
|
||||
:active,
|
||||
:silenced,
|
||||
:alphabetic,
|
||||
:suspended,
|
||||
:username,
|
||||
:display_name,
|
||||
|
@ -15,5 +15,9 @@ module Admin
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
|
||||
def set_user
|
||||
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -25,10 +25,6 @@ module Admin
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
def check_confirmation
|
||||
if @user.confirmed?
|
||||
flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed')
|
||||
|
@ -28,6 +28,7 @@ module Admin
|
||||
@pam_enabled = ENV['PAM_ENABLED'] == 'true'
|
||||
@hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
|
||||
@trending_hashtags = TrendingTags.get(7)
|
||||
@profile_directory = Setting.profile_directory
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -4,14 +4,9 @@ module Admin
|
||||
class DomainBlocksController < BaseController
|
||||
before_action :set_domain_block, only: [:show, :destroy]
|
||||
|
||||
def index
|
||||
authorize :domain_block, :index?
|
||||
@domain_blocks = DomainBlock.page(params[:page])
|
||||
end
|
||||
|
||||
def new
|
||||
authorize :domain_block, :create?
|
||||
@domain_block = DomainBlock.new
|
||||
@domain_block = DomainBlock.new(domain: params[:_domain])
|
||||
end
|
||||
|
||||
def create
|
||||
@ -22,7 +17,7 @@ module Admin
|
||||
if @domain_block.save
|
||||
DomainBlockWorker.perform_async(@domain_block.id)
|
||||
log_action :create, @domain_block
|
||||
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg')
|
||||
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
|
||||
else
|
||||
render :new
|
||||
end
|
||||
@ -36,7 +31,7 @@ module Admin
|
||||
authorize @domain_block, :destroy?
|
||||
UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
|
||||
log_action :destroy, @domain_block
|
||||
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
|
||||
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.destroyed_msg')
|
||||
end
|
||||
|
||||
private
|
||||
|
18
app/controllers/admin/followers_controller.rb
Normal file
18
app/controllers/admin/followers_controller.rb
Normal file
@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class FollowersController < BaseController
|
||||
before_action :set_account
|
||||
|
||||
PER_PAGE = 40
|
||||
|
||||
def index
|
||||
authorize :account, :index?
|
||||
@followers = @account.followers.local.recent.page(params[:page]).per(PER_PAGE)
|
||||
end
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
end
|
||||
end
|
@ -4,14 +4,21 @@ module Admin
|
||||
class InstancesController < BaseController
|
||||
def index
|
||||
authorize :instance, :index?
|
||||
|
||||
@instances = ordered_instances
|
||||
end
|
||||
|
||||
def resubscribe
|
||||
authorize :instance, :resubscribe?
|
||||
params.require(:by_domain)
|
||||
Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id))
|
||||
redirect_to admin_instances_path
|
||||
def show
|
||||
authorize :instance, :show?
|
||||
|
||||
@instance = Instance.new(Account.by_domain_accounts.find_by(domain: params[:id]) || DomainBlock.find_by!(domain: params[:id]))
|
||||
@following_count = Follow.where(account: Account.where(domain: params[:id])).count
|
||||
@followers_count = Follow.where(target_account: Account.where(domain: params[:id])).count
|
||||
@reports_count = Report.where(target_account: Account.where(domain: params[:id])).count
|
||||
@blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count
|
||||
@available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url)
|
||||
@media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
|
||||
@domain_block = DomainBlock.find_by(domain: params[:id])
|
||||
end
|
||||
|
||||
private
|
||||
@ -27,17 +34,11 @@ module Admin
|
||||
helper_method :paginated_instances
|
||||
|
||||
def ordered_instances
|
||||
paginated_instances.map { |account| Instance.new(account) }
|
||||
end
|
||||
|
||||
def subscribeable_accounts
|
||||
Account.with_followers.remote.where(domain: params[:by_domain])
|
||||
paginated_instances.map { |resource| Instance.new(resource) }
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.permit(
|
||||
:domain_name
|
||||
)
|
||||
params.permit(:limited)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -13,75 +13,42 @@ module Admin
|
||||
authorize @report, :show?
|
||||
|
||||
@report_note = @report.notes.new
|
||||
@report_notes = (@report.notes.latest + @report.history).sort_by(&:created_at)
|
||||
@report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at)
|
||||
@form = Form::StatusBatch.new
|
||||
end
|
||||
|
||||
def update
|
||||
def assign_to_self
|
||||
authorize @report, :update?
|
||||
process_report
|
||||
@report.update!(assigned_account_id: current_account.id)
|
||||
log_action :assigned_to_self, @report
|
||||
redirect_to admin_report_path(@report)
|
||||
end
|
||||
|
||||
if @report.action_taken?
|
||||
redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
|
||||
else
|
||||
redirect_to admin_report_path(@report)
|
||||
end
|
||||
def unassign
|
||||
authorize @report, :update?
|
||||
@report.update!(assigned_account_id: nil)
|
||||
log_action :unassigned, @report
|
||||
redirect_to admin_report_path(@report)
|
||||
end
|
||||
|
||||
def reopen
|
||||
authorize @report, :update?
|
||||
@report.unresolve!
|
||||
log_action :reopen, @report
|
||||
redirect_to admin_report_path(@report)
|
||||
end
|
||||
|
||||
def resolve
|
||||
authorize @report, :update?
|
||||
@report.resolve!(current_account)
|
||||
log_action :resolve, @report
|
||||
redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_report
|
||||
case params[:outcome].to_s
|
||||
when 'assign_to_self'
|
||||
@report.update!(assigned_account_id: current_account.id)
|
||||
log_action :assigned_to_self, @report
|
||||
when 'unassign'
|
||||
@report.update!(assigned_account_id: nil)
|
||||
log_action :unassigned, @report
|
||||
when 'reopen'
|
||||
@report.unresolve!
|
||||
log_action :reopen, @report
|
||||
when 'resolve'
|
||||
@report.resolve!(current_account)
|
||||
log_action :resolve, @report
|
||||
when 'disable'
|
||||
@report.resolve!(current_account)
|
||||
@report.target_account.user.disable!
|
||||
|
||||
log_action :resolve, @report
|
||||
log_action :disable, @report.target_account.user
|
||||
|
||||
resolve_all_target_account_reports
|
||||
when 'silence'
|
||||
@report.resolve!(current_account)
|
||||
@report.target_account.update!(silenced: true)
|
||||
|
||||
log_action :resolve, @report
|
||||
log_action :silence, @report.target_account
|
||||
|
||||
resolve_all_target_account_reports
|
||||
else
|
||||
raise ActiveRecord::RecordNotFound
|
||||
end
|
||||
|
||||
@report.reload
|
||||
end
|
||||
|
||||
def resolve_all_target_account_reports
|
||||
unresolved_reports_for_target_account.update_all(action_taken: true, action_taken_by_account_id: current_account.id)
|
||||
end
|
||||
|
||||
def unresolved_reports_for_target_account
|
||||
Report.where(
|
||||
target_account: @report.target_account
|
||||
).unresolved
|
||||
end
|
||||
|
||||
def filtered_reports
|
||||
ReportFilter.new(filter_params).results.order(id: :desc).includes(
|
||||
:account,
|
||||
:target_account
|
||||
)
|
||||
ReportFilter.new(filter_params).results.order(id: :desc).includes(:account, :target_account)
|
||||
end
|
||||
|
||||
def filter_params
|
||||
|
@ -10,11 +10,5 @@ module Admin
|
||||
log_action :reset_password, @user
|
||||
redirect_to admin_accounts_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -17,11 +17,5 @@ module Admin
|
||||
log_action :demote, @user
|
||||
redirect_to admin_account_path(@user.account_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -26,6 +26,7 @@ module Admin
|
||||
show_known_fediverse_at_about_page
|
||||
preview_sensitive_media
|
||||
custom_css
|
||||
profile_directory
|
||||
).freeze
|
||||
|
||||
BOOLEAN_SETTINGS = %w(
|
||||
@ -37,6 +38,7 @@ module Admin
|
||||
peers_api_enabled
|
||||
show_known_fediverse_at_about_page
|
||||
preview_sensitive_media
|
||||
profile_directory
|
||||
).freeze
|
||||
|
||||
UPLOAD_SETTINGS = %w(
|
||||
|
@ -1,27 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class SilencesController < BaseController
|
||||
before_action :set_account
|
||||
|
||||
def create
|
||||
authorize @account, :silence?
|
||||
@account.update!(silenced: true)
|
||||
log_action :silence, @account
|
||||
redirect_to admin_accounts_path
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @account, :unsilence?
|
||||
@account.update!(silenced: false)
|
||||
log_action :unsilence, @account
|
||||
redirect_to admin_accounts_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
end
|
||||
end
|
@ -22,6 +22,15 @@ module Admin
|
||||
@form = Form::StatusBatch.new
|
||||
end
|
||||
|
||||
def show
|
||||
authorize :status, :index?
|
||||
|
||||
@statuses = @account.statuses.where(id: params[:id])
|
||||
authorize @statuses.first, :show?
|
||||
|
||||
@form = Form::StatusBatch.new
|
||||
end
|
||||
|
||||
def create
|
||||
authorize :status, :update?
|
||||
|
||||
|
@ -1,60 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class SuspensionsController < BaseController
|
||||
before_action :set_account
|
||||
|
||||
def new
|
||||
@suspension = Form::AdminSuspensionConfirmation.new(report_id: params[:report_id])
|
||||
end
|
||||
|
||||
def create
|
||||
authorize @account, :suspend?
|
||||
|
||||
@suspension = Form::AdminSuspensionConfirmation.new(suspension_params)
|
||||
|
||||
if suspension_params[:acct] == @account.acct
|
||||
resolve_report! if suspension_params[:report_id].present?
|
||||
perform_suspend!
|
||||
mark_reports_resolved!
|
||||
redirect_to admin_accounts_path
|
||||
else
|
||||
flash.now[:alert] = I18n.t('admin.suspensions.bad_acct_msg')
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @account, :unsuspend?
|
||||
@account.unsuspend!
|
||||
log_action :unsuspend, @account
|
||||
redirect_to admin_accounts_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
|
||||
def suspension_params
|
||||
params.require(:form_admin_suspension_confirmation).permit(:acct, :report_id)
|
||||
end
|
||||
|
||||
def resolve_report!
|
||||
report = Report.find(suspension_params[:report_id])
|
||||
report.resolve!(current_account)
|
||||
log_action :resolve, report
|
||||
end
|
||||
|
||||
def perform_suspend!
|
||||
@account.suspend!
|
||||
Admin::SuspensionWorker.perform_async(@account.id)
|
||||
log_action :suspend, @account
|
||||
end
|
||||
|
||||
def mark_reports_resolved!
|
||||
Report.where(target_account: @account).unresolved.update_all(action_taken: true, action_taken_by_account_id: current_account.id)
|
||||
end
|
||||
end
|
||||
end
|
44
app/controllers/admin/tags_controller.rb
Normal file
44
app/controllers/admin/tags_controller.rb
Normal file
@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class TagsController < BaseController
|
||||
before_action :set_tags, only: :index
|
||||
before_action :set_tag, except: :index
|
||||
before_action :set_filter_params
|
||||
|
||||
def index
|
||||
authorize :tag, :index?
|
||||
end
|
||||
|
||||
def hide
|
||||
authorize @tag, :hide?
|
||||
@tag.account_tag_stat.update!(hidden: true)
|
||||
redirect_to admin_tags_path(@filter_params)
|
||||
end
|
||||
|
||||
def unhide
|
||||
authorize @tag, :unhide?
|
||||
@tag.account_tag_stat.update!(hidden: false)
|
||||
redirect_to admin_tags_path(@filter_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tags
|
||||
@tags = Tag.discoverable
|
||||
@tags.merge!(Tag.hidden) if filter_params[:hidden]
|
||||
end
|
||||
|
||||
def set_tag
|
||||
@tag = Tag.find(params[:id])
|
||||
end
|
||||
|
||||
def set_filter_params
|
||||
@filter_params = filter_params.to_hash.symbolize_keys
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.permit(:hidden)
|
||||
end
|
||||
end
|
||||
end
|
@ -2,7 +2,7 @@
|
||||
|
||||
module Admin
|
||||
class TwoFactorAuthenticationsController < BaseController
|
||||
before_action :set_user
|
||||
before_action :set_target_user
|
||||
|
||||
def destroy
|
||||
authorize @user, :disable_2fa?
|
||||
@ -13,7 +13,7 @@ module Admin
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
def set_target_user
|
||||
@user = User.find(params[:user_id])
|
||||
end
|
||||
end
|
||||
|
58
app/controllers/admin/warning_presets_controller.rb
Normal file
58
app/controllers/admin/warning_presets_controller.rb
Normal file
@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class WarningPresetsController < BaseController
|
||||
before_action :set_warning_preset, except: [:index, :create]
|
||||
|
||||
def index
|
||||
authorize :account_warning_preset, :index?
|
||||
|
||||
@warning_presets = AccountWarningPreset.all
|
||||
@warning_preset = AccountWarningPreset.new
|
||||
end
|
||||
|
||||
def create
|
||||
authorize :account_warning_preset, :create?
|
||||
|
||||
@warning_preset = AccountWarningPreset.new(warning_preset_params)
|
||||
|
||||
if @warning_preset.save
|
||||
redirect_to admin_warning_presets_path
|
||||
else
|
||||
@warning_presets = AccountWarningPreset.all
|
||||
render :index
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize @warning_preset, :update?
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @warning_preset, :update?
|
||||
|
||||
if @warning_preset.update(warning_preset_params)
|
||||
redirect_to admin_warning_presets_path
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @warning_preset, :destroy?
|
||||
|
||||
@warning_preset.destroy!
|
||||
redirect_to admin_warning_presets_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_warning_preset
|
||||
@warning_preset = AccountWarningPreset.find(params[:id])
|
||||
end
|
||||
|
||||
def warning_preset_params
|
||||
params.require(:account_warning_preset).permit(:text)
|
||||
end
|
||||
end
|
||||
end
|
@ -68,12 +68,14 @@ class Api::BaseController < ApplicationController
|
||||
end
|
||||
|
||||
def require_user!
|
||||
if current_user && !current_user.disabled?
|
||||
set_user_activity
|
||||
elsif current_user
|
||||
render json: { error: 'Your login is currently disabled' }, status: 403
|
||||
else
|
||||
if !current_user
|
||||
render json: { error: 'This method requires an authenticated user' }, status: 422
|
||||
elsif current_user.disabled?
|
||||
render json: { error: 'Your login is currently disabled' }, status: 403
|
||||
elsif !current_user.confirmed?
|
||||
render json: { error: 'Email confirmation is not completed' }, status: 403
|
||||
else
|
||||
set_user_activity
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -21,7 +21,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
|
||||
private
|
||||
|
||||
def account_params
|
||||
params.permit(:display_name, :note, :avatar, :header, :locked, :bot, fields_attributes: [:name, :value])
|
||||
params.permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value])
|
||||
end
|
||||
|
||||
def user_settings_params
|
||||
|
@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
Account.includes(:active_relationships).references(:active_relationships)
|
||||
Account.includes(:active_relationships, :account_stat).references(:active_relationships)
|
||||
end
|
||||
|
||||
def paginated_follows
|
||||
|
@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
Account.includes(:passive_relationships).references(:passive_relationships)
|
||||
Account.includes(:passive_relationships, :account_stat).references(:passive_relationships)
|
||||
end
|
||||
|
||||
def paginated_follows
|
||||
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::StatusesController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
|
||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
|
||||
before_action :set_account
|
||||
after_action :insert_pagination_headers
|
||||
|
||||
@ -28,13 +28,11 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
||||
|
||||
def account_statuses
|
||||
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
|
||||
statuses = statuses.paginate_by_id(
|
||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||
params_slice(:max_id, :since_id, :min_id)
|
||||
)
|
||||
statuses = statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||
|
||||
statuses.merge!(only_media_scope) if truthy_param?(:only_media)
|
||||
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
|
||||
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
|
||||
|
||||
statuses
|
||||
end
|
||||
@ -65,6 +63,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
||||
Status.without_replies
|
||||
end
|
||||
|
||||
def no_reblogs_scope
|
||||
Status.without_reblogs
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
|
||||
end
|
||||
|
@ -1,14 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::AccountsController < Api::BaseController
|
||||
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute]
|
||||
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :block, :unblock, :mute, :unmute]
|
||||
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow]
|
||||
before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute]
|
||||
before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
|
||||
|
||||
before_action :require_user!, except: [:show]
|
||||
before_action :set_account
|
||||
before_action :require_user!, except: [:show, :create]
|
||||
before_action :set_account, except: [:create]
|
||||
before_action :check_account_suspension, only: [:show]
|
||||
before_action :check_enabled_registrations, only: [:create]
|
||||
|
||||
respond_to :json
|
||||
|
||||
@ -16,6 +18,16 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
render json: @account, serializer: REST::AccountSerializer
|
||||
end
|
||||
|
||||
def create
|
||||
token = AppSignUpService.new.call(doorkeeper_token.application, account_params)
|
||||
response = Doorkeeper::OAuth::TokenResponse.new(token)
|
||||
|
||||
headers.merge!(response.headers)
|
||||
|
||||
self.response_body = Oj.dump(response.body)
|
||||
self.status = response.status
|
||||
end
|
||||
|
||||
def follow
|
||||
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs))
|
||||
|
||||
@ -62,4 +74,12 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
def check_account_suspension
|
||||
gone if @account.suspended?
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.permit(:username, :email, :password, :agreement, :locale)
|
||||
end
|
||||
|
||||
def check_enabled_registrations
|
||||
forbidden if single_user_mode? || !Setting.open_registrations
|
||||
end
|
||||
end
|
||||
|
@ -19,7 +19,7 @@ class Api::V1::BlocksController < Api::BaseController
|
||||
end
|
||||
|
||||
def paginated_blocks
|
||||
@paginated_blocks ||= Block.eager_load(:target_account)
|
||||
@paginated_blocks ||= Block.eager_load(target_account: :account_stat)
|
||||
.where(account: current_account)
|
||||
.paginate_by_max_id(
|
||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||
|
@ -4,6 +4,8 @@ class Api::V1::CustomEmojisController < Api::BaseController
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
render json: CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer
|
||||
render_cached_json('api:v1:custom_emojis', expires_in: 1.minute) do
|
||||
ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -27,7 +27,7 @@ class Api::V1::EndorsementsController < Api::BaseController
|
||||
end
|
||||
|
||||
def endorsed_accounts
|
||||
current_account.endorsed_accounts
|
||||
current_account.endorsed_accounts.includes(:account_stat)
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
|
@ -33,7 +33,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
Account.includes(:follow_requests).references(:follow_requests)
|
||||
Account.includes(:follow_requests, :account_stat).references(:follow_requests)
|
||||
end
|
||||
|
||||
def paginated_follow_requests
|
||||
|
@ -37,9 +37,9 @@ class Api::V1::Lists::AccountsController < Api::BaseController
|
||||
|
||||
def load_accounts
|
||||
if unlimited?
|
||||
@list.accounts.all
|
||||
@list.accounts.includes(:account_stat).all
|
||||
else
|
||||
@list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
|
||||
@list.accounts.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
|
||||
end
|
||||
end
|
||||
|
||||
|
77
app/controllers/api/v1/scheduled_statuses_controller.rb
Normal file
77
app/controllers/api/v1/scheduled_statuses_controller.rb
Normal file
@ -0,0 +1,77 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::ScheduledStatusesController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, except: [:update, :destroy]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:update, :destroy]
|
||||
|
||||
before_action :set_statuses, only: :index
|
||||
before_action :set_status, except: :index
|
||||
|
||||
after_action :insert_pagination_headers, only: :index
|
||||
|
||||
def index
|
||||
render json: @statuses, each_serializer: REST::ScheduledStatusSerializer
|
||||
end
|
||||
|
||||
def show
|
||||
render json: @status, serializer: REST::ScheduledStatusSerializer
|
||||
end
|
||||
|
||||
def update
|
||||
@status.update!(scheduled_status_params)
|
||||
render json: @status, serializer: REST::ScheduledStatusSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
@status.destroy!
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_statuses
|
||||
@statuses = current_account.scheduled_statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||
end
|
||||
|
||||
def set_status
|
||||
@status = current_account.scheduled_statuses.find(params[:id])
|
||||
end
|
||||
|
||||
def scheduled_status_params
|
||||
params.permit(:scheduled_at)
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(:limit).permit(:limit).merge(core_params)
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
||||
def next_path
|
||||
if records_continue?
|
||||
api_v1_scheduled_statuses_url pagination_params(max_id: pagination_max_id)
|
||||
end
|
||||
end
|
||||
|
||||
def prev_path
|
||||
unless @statuses.empty?
|
||||
api_v1_scheduled_statuses_url pagination_params(min_id: pagination_since_id)
|
||||
end
|
||||
end
|
||||
|
||||
def records_continue?
|
||||
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@statuses.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@statuses.first.id
|
||||
end
|
||||
end
|
@ -22,7 +22,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
|
||||
|
||||
def default_accounts
|
||||
Account
|
||||
.includes(:favourites)
|
||||
.includes(:favourites, :account_stat)
|
||||
.references(:favourites)
|
||||
.where(favourites: { status_id: @status.id })
|
||||
end
|
||||
|
@ -21,7 +21,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
Account.includes(:statuses).references(:statuses)
|
||||
Account.includes(:statuses, :account_stat).references(:statuses)
|
||||
end
|
||||
|
||||
def paginated_statuses
|
||||
|
@ -45,17 +45,18 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
|
||||
def create
|
||||
@status = PostStatusService.new.call(current_user.account,
|
||||
status_params[:status],
|
||||
status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]),
|
||||
text: status_params[:status],
|
||||
thread: status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]),
|
||||
media_ids: status_params[:media_ids],
|
||||
sensitive: status_params[:sensitive],
|
||||
spoiler_text: status_params[:spoiler_text],
|
||||
visibility: status_params[:visibility],
|
||||
scheduled_at: status_params[:scheduled_at],
|
||||
application: doorkeeper_token.application,
|
||||
idempotency: request.headers['Idempotency-Key'],
|
||||
local_only: status_params[:local_only])
|
||||
|
||||
render json: @status, serializer: REST::StatusSerializer
|
||||
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
@ -78,7 +79,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
end
|
||||
|
||||
def status_params
|
||||
params.permit(:status, :in_reply_to_id, :sensitive, :spoiler_text, :visibility, :local_only, media_ids: [])
|
||||
params.permit(:status, :in_reply_to_id, :sensitive, :spoiler_text, :visibility, :scheduled_at, :local_only, media_ids: [])
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
|
@ -45,7 +45,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
|
||||
end
|
||||
|
||||
def tag_timeline_statuses
|
||||
Status.as_tag_timeline(@tag, current_account, truthy_param?(:local))
|
||||
HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, truthy_param?(:local))
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
|
@ -10,6 +10,7 @@ class Api::Web::EmbedsController < Api::Web::BaseController
|
||||
render json: status, serializer: OEmbedSerializer, width: 400
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
oembed = FetchOEmbedService.new.call(params[:url])
|
||||
oembed[:html] = Formatter.instance.sanitize(oembed[:html], Sanitize::Config::MASTODON_OEMBED) if oembed[:html].present?
|
||||
|
||||
if oembed
|
||||
render json: oembed
|
||||
|
@ -6,9 +6,9 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
||||
before_action :set_body_classes
|
||||
before_action :set_user, only: [:finish_signup]
|
||||
|
||||
# GET/PATCH /users/:id/finish_signup
|
||||
def finish_signup
|
||||
return unless request.patch? && params[:user]
|
||||
|
||||
if @user.update(user_params)
|
||||
@user.skip_reconfirmation!
|
||||
bypass_sign_in(@user)
|
||||
@ -31,4 +31,12 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
||||
def user_params
|
||||
params.require(:user).permit(:email)
|
||||
end
|
||||
|
||||
def after_confirmation_path_for(_resource_name, user)
|
||||
if user.created_by_application && truthy_param?(:redirect_to_app)
|
||||
user.created_by_application.redirect_uri
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -26,6 +26,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||
|
||||
resource.locale = I18n.locale
|
||||
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
|
||||
resource.agreement = true
|
||||
|
||||
resource.build_account if resource.account.nil?
|
||||
end
|
||||
|
@ -1,21 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module RemoteAccountControllerConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
layout 'public'
|
||||
before_action :set_account
|
||||
before_action :check_account_suspension
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find_remote!(params[:acct])
|
||||
end
|
||||
|
||||
def check_account_suspension
|
||||
gone if @account.suspended?
|
||||
end
|
||||
end
|
@ -43,7 +43,13 @@ module SignatureVerification
|
||||
return
|
||||
end
|
||||
|
||||
account = account_from_key_id(signature_params['keyId'])
|
||||
account_stoplight = Stoplight("source:#{request.ip}") { account_from_key_id(signature_params['keyId']) }
|
||||
.with_fallback { nil }
|
||||
.with_threshold(1)
|
||||
.with_cool_off_time(5.minutes.seconds)
|
||||
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) }
|
||||
|
||||
account = account_stoplight.run
|
||||
|
||||
if account.nil?
|
||||
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
|
||||
@ -54,23 +60,26 @@ module SignatureVerification
|
||||
signature = Base64.decode64(signature_params['signature'])
|
||||
compare_signed_string = build_signed_string(signature_params['headers'])
|
||||
|
||||
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
|
||||
@signed_request_account = account
|
||||
@signed_request_account
|
||||
elsif account.possibly_stale?
|
||||
account = account.refresh!
|
||||
return account unless verify_signature(account, signature, compare_signed_string).nil?
|
||||
|
||||
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
|
||||
@signed_request_account = account
|
||||
@signed_request_account
|
||||
else
|
||||
@signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
|
||||
@signed_request_account = nil
|
||||
end
|
||||
else
|
||||
@signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
|
||||
account_stoplight = Stoplight("source:#{request.ip}") { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
|
||||
.with_fallback { nil }
|
||||
.with_threshold(1)
|
||||
.with_cool_off_time(5.minutes.seconds)
|
||||
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) }
|
||||
|
||||
account = account_stoplight.run
|
||||
|
||||
if account.nil?
|
||||
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
|
||||
@signed_request_account = nil
|
||||
return
|
||||
end
|
||||
|
||||
return account unless verify_signature(account, signature, compare_signed_string).nil?
|
||||
|
||||
@signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
|
||||
@signed_request_account = nil
|
||||
end
|
||||
|
||||
def request_body
|
||||
@ -79,6 +88,15 @@ module SignatureVerification
|
||||
|
||||
private
|
||||
|
||||
def verify_signature(account, signature, compare_signed_string)
|
||||
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
|
||||
@signed_request_account = account
|
||||
@signed_request_account
|
||||
end
|
||||
rescue OpenSSL::PKey::RSAError
|
||||
nil
|
||||
end
|
||||
|
||||
def build_signed_string(signed_headers)
|
||||
signed_headers = 'date' if signed_headers.blank?
|
||||
|
||||
@ -125,4 +143,9 @@ module SignatureVerification
|
||||
account
|
||||
end
|
||||
end
|
||||
|
||||
def account_refresh_key(account)
|
||||
return if account.local? || !account.activitypub?
|
||||
ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true)
|
||||
end
|
||||
end
|
||||
|
43
app/controllers/directories_controller.rb
Normal file
43
app/controllers/directories_controller.rb
Normal file
@ -0,0 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DirectoriesController < ApplicationController
|
||||
layout 'public'
|
||||
|
||||
before_action :check_enabled
|
||||
before_action :set_instance_presenter
|
||||
before_action :set_tag, only: :show
|
||||
before_action :set_tags
|
||||
before_action :set_accounts
|
||||
|
||||
def index
|
||||
render :index
|
||||
end
|
||||
|
||||
def show
|
||||
render :index
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_enabled
|
||||
return not_found unless Setting.profile_directory
|
||||
end
|
||||
|
||||
def set_tag
|
||||
@tag = Tag.discoverable.find_by!(name: params[:id].downcase)
|
||||
end
|
||||
|
||||
def set_tags
|
||||
@tags = Tag.discoverable.limit(30)
|
||||
end
|
||||
|
||||
def set_accounts
|
||||
@accounts = Account.discoverable.page(params[:page]).per(40).tap do |query|
|
||||
query.merge!(Account.tagged_with(@tag.id)) if @tag
|
||||
end
|
||||
end
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
end
|
@ -6,12 +6,17 @@ class MediaController < ApplicationController
|
||||
before_action :set_media_attachment
|
||||
before_action :verify_permitted_status!
|
||||
|
||||
content_security_policy only: :player do |p|
|
||||
p.frame_ancestors(false)
|
||||
end
|
||||
|
||||
def show
|
||||
redirect_to @media_attachment.file.url(:original)
|
||||
end
|
||||
|
||||
def player
|
||||
@body_classes = 'player'
|
||||
response.headers['X-Frame-Options'] = 'ALLOWALL'
|
||||
raise ActiveRecord::RecordNotFound unless @media_attachment.video? || @media_attachment.gifv?
|
||||
end
|
||||
|
||||
|
@ -5,6 +5,7 @@ class RemoteInteractionController < ApplicationController
|
||||
|
||||
layout 'modal'
|
||||
|
||||
before_action :set_interaction_type
|
||||
before_action :set_status
|
||||
before_action :set_body_classes
|
||||
|
||||
@ -45,4 +46,8 @@ class RemoteInteractionController < ApplicationController
|
||||
@body_classes = 'modal-layout'
|
||||
@hide_header = true
|
||||
end
|
||||
|
||||
def set_interaction_type
|
||||
@interaction_type = %w(reply reblog favourite).include?(params[:type]) ? params[:type] : 'reply'
|
||||
end
|
||||
end
|
||||
|
@ -1,12 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::ApplicationsController < ApplicationController
|
||||
class Settings::ApplicationsController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_application, only: [:show, :update, :destroy, :regenerate]
|
||||
before_action :prepare_scopes, only: [:create, :update]
|
||||
before_action :set_body_classes
|
||||
|
||||
def index
|
||||
@applications = current_user.applications.order(id: :desc).page(params[:page])
|
||||
@ -70,8 +69,4 @@ class Settings::ApplicationsController < ApplicationController
|
||||
scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil)
|
||||
params[:doorkeeper_application][:scopes] = scopes.join(' ') if scopes.is_a? Array
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
end
|
||||
|
11
app/controllers/settings/base_controller.rb
Normal file
11
app/controllers/settings/base_controller.rb
Normal file
@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::BaseController < ApplicationController
|
||||
before_action :set_body_classes
|
||||
|
||||
private
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
end
|
@ -1,11 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::DeletesController < ApplicationController
|
||||
class Settings::DeletesController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :check_enabled_deletion
|
||||
before_action :authenticate_user!
|
||||
before_action :set_body_classes
|
||||
|
||||
def show
|
||||
@confirmation = Form::DeleteConfirmation.new
|
||||
@ -30,8 +29,4 @@ class Settings::DeletesController < ApplicationController
|
||||
def delete_params
|
||||
params.require(:form_delete_confirmation).permit(:password)
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
end
|
||||
|
@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Settings
|
||||
module Exports
|
||||
class BlockedDomainsController < ApplicationController
|
||||
include ExportControllerConcern
|
||||
|
||||
def index
|
||||
send_export_file
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def export_data
|
||||
@export.to_blocked_domains_csv
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
19
app/controllers/settings/exports/lists_controller.rb
Normal file
19
app/controllers/settings/exports/lists_controller.rb
Normal file
@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Settings
|
||||
module Exports
|
||||
class ListsController < ApplicationController
|
||||
include ExportControllerConcern
|
||||
|
||||
def index
|
||||
send_export_file
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def export_data
|
||||
@export.to_lists_csv
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,12 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::ExportsController < ApplicationController
|
||||
class Settings::ExportsController < Settings::BaseController
|
||||
include Authorization
|
||||
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_body_classes
|
||||
|
||||
def show
|
||||
@export = Export.new(current_account)
|
||||
@ -21,10 +20,4 @@ class Settings::ExportsController < ApplicationController
|
||||
|
||||
redirect_to settings_export_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
end
|
||||
|
@ -1,10 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::FollowerDomainsController < ApplicationController
|
||||
class Settings::FollowerDomainsController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_body_classes
|
||||
|
||||
def show
|
||||
@account = current_account
|
||||
@ -26,8 +25,4 @@ class Settings::FollowerDomainsController < ApplicationController
|
||||
def bulk_params
|
||||
params.permit(select: [])
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
end
|
||||
|
@ -1,11 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::ImportsController < ApplicationController
|
||||
class Settings::ImportsController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_account
|
||||
before_action :set_body_classes
|
||||
|
||||
def show
|
||||
@import = Import.new
|
||||
@ -32,8 +31,4 @@ class Settings::ImportsController < ApplicationController
|
||||
def import_params
|
||||
params.require(:import).permit(:data, :type)
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
end
|
||||
|
@ -1,10 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::MigrationsController < ApplicationController
|
||||
class Settings::MigrationsController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_body_classes
|
||||
|
||||
def show
|
||||
@migration = Form::Migration.new(account: current_account.moved_to_account)
|
||||
@ -32,8 +31,4 @@ class Settings::MigrationsController < ApplicationController
|
||||
current_account.moved_to_account_id != @migration.account&.id &&
|
||||
current_account.id != @migration.account&.id
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
end
|
||||
|
@ -1,10 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::NotificationsController < ApplicationController
|
||||
class Settings::NotificationsController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_body_classes
|
||||
|
||||
def show; end
|
||||
|
||||
@ -30,8 +29,4 @@ class Settings::NotificationsController < ApplicationController
|
||||
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
||||
)
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
end
|
||||
|
@ -1,10 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::PreferencesController < ApplicationController
|
||||
class Settings::PreferencesController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_body_classes
|
||||
|
||||
def show; end
|
||||
|
||||
@ -49,12 +48,9 @@ class Settings::PreferencesController < ApplicationController
|
||||
:setting_noindex,
|
||||
:setting_theme,
|
||||
:setting_hide_network,
|
||||
:setting_aggregate_reblogs,
|
||||
notification_emails: %i(follow follow_request reblog favourite mention digest report),
|
||||
interactions: %i(must_be_follower must_be_following)
|
||||
)
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
end
|
||||
|
@ -1,13 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::ProfilesController < ApplicationController
|
||||
class Settings::ProfilesController < Settings::BaseController
|
||||
include ObfuscateFilename
|
||||
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_account
|
||||
before_action :set_body_classes
|
||||
|
||||
obfuscate_filename [:account, :avatar]
|
||||
obfuscate_filename [:account, :header]
|
||||
@ -29,14 +28,10 @@ class Settings::ProfilesController < ApplicationController
|
||||
private
|
||||
|
||||
def account_params
|
||||
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, fields_attributes: [:name, :value])
|
||||
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value])
|
||||
end
|
||||
|
||||
def set_account
|
||||
@account = current_user.account
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
end
|
||||
|
@ -1,8 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Settings::SessionsController < ApplicationController
|
||||
class Settings::SessionsController < Settings::BaseController
|
||||
before_action :set_session, only: :destroy
|
||||
before_action :set_body_classes
|
||||
|
||||
def destroy
|
||||
@session.destroy!
|
||||
@ -15,8 +14,4 @@ class Settings::SessionsController < ApplicationController
|
||||
def set_session
|
||||
@session = current_user.session_activations.find(params[:id])
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
end
|
||||
|
@ -2,12 +2,11 @@
|
||||
|
||||
module Settings
|
||||
module TwoFactorAuthentication
|
||||
class ConfirmationsController < ApplicationController
|
||||
class ConfirmationsController < BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :ensure_otp_secret
|
||||
before_action :set_body_classes
|
||||
|
||||
def new
|
||||
prepare_two_factor_form
|
||||
@ -44,10 +43,6 @@ module Settings
|
||||
def ensure_otp_secret
|
||||
redirect_to settings_two_factor_authentication_path unless current_user.otp_secret
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -2,11 +2,10 @@
|
||||
|
||||
module Settings
|
||||
module TwoFactorAuthentication
|
||||
class RecoveryCodesController < ApplicationController
|
||||
class RecoveryCodesController < BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_body_classes
|
||||
|
||||
def create
|
||||
@recovery_codes = current_user.generate_otp_backup_codes!
|
||||
@ -14,12 +13,6 @@ module Settings
|
||||
flash[:notice] = I18n.t('two_factor_authentication.recovery_codes_regenerated')
|
||||
render :index
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,12 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Settings
|
||||
class TwoFactorAuthenticationsController < ApplicationController
|
||||
class TwoFactorAuthenticationsController < BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :verify_otp_required, only: [:create]
|
||||
before_action :set_body_classes
|
||||
|
||||
def show
|
||||
@confirmation = Form::TwoFactorConfirmation.new
|
||||
@ -44,9 +43,5 @@ module Settings
|
||||
current_user.validate_and_consume_otp!(confirmation_params[:code]) ||
|
||||
current_user.invalidate_otp_backup_code!(confirmation_params[:code])
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -65,12 +65,13 @@ class StatusesController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def create_descendant_thread(depth, statuses)
|
||||
def create_descendant_thread(starting_depth, statuses)
|
||||
depth = starting_depth + statuses.size
|
||||
if depth < DESCENDANTS_DEPTH_LIMIT
|
||||
{ statuses: statuses }
|
||||
{ statuses: statuses, starting_depth: starting_depth }
|
||||
else
|
||||
next_status = statuses.pop
|
||||
{ statuses: statuses, next_status: next_status }
|
||||
{ statuses: statuses, starting_depth: starting_depth, next_status: next_status }
|
||||
end
|
||||
end
|
||||
|
||||
@ -101,16 +102,19 @@ class StatusesController < ApplicationController
|
||||
@descendant_threads = []
|
||||
|
||||
if descendants.present?
|
||||
statuses = [descendants.first]
|
||||
depth = 1
|
||||
statuses = [descendants.first]
|
||||
starting_depth = 0
|
||||
|
||||
descendants.drop(1).each_with_index do |descendant, index|
|
||||
if descendants[index].id == descendant.in_reply_to_id
|
||||
depth += 1
|
||||
statuses << descendant
|
||||
else
|
||||
@descendant_threads << create_descendant_thread(depth, statuses)
|
||||
@descendant_threads << create_descendant_thread(starting_depth, statuses)
|
||||
|
||||
# The thread is broken, assume it's a reply to the root status
|
||||
starting_depth = 0
|
||||
|
||||
# ... unless we can find its ancestor in one of the already-processed threads
|
||||
@descendant_threads.reverse_each do |descendant_thread|
|
||||
statuses = descendant_thread[:statuses]
|
||||
|
||||
@ -119,18 +123,16 @@ class StatusesController < ApplicationController
|
||||
end
|
||||
|
||||
if index.present?
|
||||
depth += index - statuses.size
|
||||
starting_depth = descendant_thread[:starting_depth] + index + 1
|
||||
break
|
||||
end
|
||||
|
||||
depth -= statuses.size
|
||||
end
|
||||
|
||||
statuses = [descendant]
|
||||
end
|
||||
end
|
||||
|
||||
@descendant_threads << create_descendant_thread(depth, statuses)
|
||||
@descendant_threads << create_descendant_thread(starting_depth, statuses)
|
||||
end
|
||||
|
||||
@max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT
|
||||
|
@ -16,14 +16,15 @@ class TagsController < ApplicationController
|
||||
end
|
||||
|
||||
format.rss do
|
||||
@statuses = Status.as_tag_timeline(@tag).limit(PAGE_SIZE)
|
||||
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none)).limit(PAGE_SIZE)
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
|
||||
render xml: RSS::TagSerializer.render(@tag, @statuses)
|
||||
end
|
||||
|
||||
format.json do
|
||||
@statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id])
|
||||
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local])
|
||||
.paginate_by_max_id(PAGE_SIZE, params[:max_id])
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
|
||||
render json: collection_presenter,
|
||||
@ -46,7 +47,7 @@ class TagsController < ApplicationController
|
||||
|
||||
def collection_presenter
|
||||
ActivityPub::CollectionPresenter.new(
|
||||
id: tag_url(@tag),
|
||||
id: tag_url(@tag, params.slice(:any, :all, :none)),
|
||||
type: :ordered,
|
||||
size: @tag.statuses.count,
|
||||
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
|
||||
|
@ -24,7 +24,7 @@ module Admin::AccountModerationNotesHelper
|
||||
|
||||
def name_tag_classes(account, inline = false)
|
||||
classes = [inline ? 'inline-name-tag' : 'name-tag']
|
||||
classes << 'suspended' if account.suspended?
|
||||
classes << 'suspended' if account.suspended? || (account.local? && account.user.nil?)
|
||||
classes.join(' ')
|
||||
end
|
||||
end
|
||||
|
@ -23,6 +23,8 @@ module Admin::ActionLogsHelper
|
||||
link_to record.domain, "https://#{record.domain}"
|
||||
when 'Status'
|
||||
link_to record.account.acct, TagManager.instance.url_for(record)
|
||||
when 'AccountWarning'
|
||||
link_to record.target_account.acct, admin_account_path(record.target_account_id)
|
||||
end
|
||||
end
|
||||
|
||||
@ -34,6 +36,7 @@ module Admin::ActionLogsHelper
|
||||
link_to attributes['domain'], "https://#{attributes['domain']}"
|
||||
when 'Status'
|
||||
tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))
|
||||
|
||||
if tmp_status.account
|
||||
link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id)
|
||||
else
|
||||
@ -81,6 +84,8 @@ module Admin::ActionLogsHelper
|
||||
'envelope'
|
||||
when 'Status'
|
||||
'pencil'
|
||||
when 'AccountWarning'
|
||||
'warning'
|
||||
end
|
||||
end
|
||||
|
||||
@ -92,7 +97,7 @@ module Admin::ActionLogsHelper
|
||||
opposite_verbs?(log) ? 'negative' : 'positive'
|
||||
when :update, :reset_password, :disable_2fa, :memorialize, :change_email
|
||||
'neutral'
|
||||
when :demote, :silence, :disable, :suspend, :remove_avatar, :reopen
|
||||
when :demote, :silence, :disable, :suspend, :remove_avatar, :remove_header, :reopen
|
||||
'negative'
|
||||
when :destroy
|
||||
opposite_verbs?(log) ? 'positive' : 'negative'
|
||||
@ -104,6 +109,6 @@ module Admin::ActionLogsHelper
|
||||
private
|
||||
|
||||
def opposite_verbs?(log)
|
||||
%w(DomainBlock EmailDomainBlock).include?(log.target_type)
|
||||
%w(DomainBlock EmailDomainBlock AccountWarning).include?(log.target_type)
|
||||
end
|
||||
end
|
||||
|
@ -1,12 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin::FilterHelper
|
||||
ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended alphabetic username display_name email ip staff).freeze
|
||||
ACCOUNT_FILTERS = %i(local remote by_domain active silenced suspended username display_name email ip staff).freeze
|
||||
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
|
||||
INVITE_FILTER = %i(available expired).freeze
|
||||
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
|
||||
TAGS_FILTERS = %i(hidden).freeze
|
||||
INSTANCES_FILTERS = %i(limited).freeze
|
||||
|
||||
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS
|
||||
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS
|
||||
|
||||
def filter_link_to(text, link_to_params, link_class_params = link_to_params)
|
||||
new_url = filtered_url_for(link_to_params)
|
||||
|
@ -23,7 +23,7 @@ module HomeHelper
|
||||
else
|
||||
link_to(path || TagManager.instance.url_for(account), class: 'account__display-name') do
|
||||
content_tag(:div, class: 'account__avatar-wrapper') do
|
||||
content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{account.avatar.url})")
|
||||
content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url)})")
|
||||
end +
|
||||
content_tag(:span, class: 'display-name') do
|
||||
content_tag(:bdi) do
|
||||
|
@ -1,4 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module MailerHelper
|
||||
end
|
@ -30,6 +30,7 @@ module SettingsHelper
|
||||
ja: '日本語',
|
||||
ka: 'ქართული',
|
||||
ko: '한국어',
|
||||
ml: 'മലയാളം',
|
||||
nl: 'Nederlands',
|
||||
no: 'Norsk',
|
||||
oc: 'Occitan',
|
||||
|
@ -34,12 +34,14 @@ module StreamEntriesHelper
|
||||
end
|
||||
end
|
||||
|
||||
def account_badge(account)
|
||||
def account_badge(account, all: false)
|
||||
if account.bot?
|
||||
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
|
||||
elsif Setting.show_staff_badge && account.user_staff?
|
||||
elsif (Setting.show_staff_badge && account.user_staff?) || all
|
||||
content_tag(:div, class: 'roles') do
|
||||
if account.user_admin?
|
||||
if all && !account.user_staff?
|
||||
content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role')
|
||||
elsif account.user_admin?
|
||||
content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
|
||||
elsif account.user_moderator?
|
||||
content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
|
||||
|
4
app/javascript/images/icon_flag.svg
Normal file
4
app/javascript/images/icon_flag.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg fill="#FFFFFF" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 197 B |
BIN
app/javascript/images/mailer/icon_warning.png
Normal file
BIN
app/javascript/images/mailer/icon_warning.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 371 B |
1
app/javascript/images/screen_federation.svg
Normal file
1
app/javascript/images/screen_federation.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 37 KiB |
1
app/javascript/images/screen_hello.svg
Normal file
1
app/javascript/images/screen_hello.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.8 KiB |
1
app/javascript/images/screen_interactions.svg
Normal file
1
app/javascript/images/screen_interactions.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 27 KiB |
@ -132,6 +132,12 @@ export function submitCompose(routerHistory) {
|
||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||
},
|
||||
}).then(function (response) {
|
||||
if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) {
|
||||
routerHistory.push('/timelines/direct');
|
||||
} else if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) {
|
||||
routerHistory.goBack();
|
||||
}
|
||||
|
||||
dispatch(insertIntoTagHistory(response.data.tags, status));
|
||||
dispatch(submitComposeSuccess({ ...response.data }));
|
||||
|
||||
@ -144,9 +150,7 @@ export function submitCompose(routerHistory) {
|
||||
}
|
||||
};
|
||||
|
||||
if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) {
|
||||
routerHistory.push('/timelines/direct');
|
||||
} else if (response.data.visibility !== 'direct') {
|
||||
if (response.data.visibility !== 'direct') {
|
||||
insertIfOnline('home');
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => {
|
||||
const params = { max_id: maxId };
|
||||
|
||||
if (!maxId) {
|
||||
params.since_id = getState().getIn(['conversations', 0, 'last_status']);
|
||||
params.since_id = getState().getIn(['conversations', 'items', 0, 'last_status']);
|
||||
}
|
||||
|
||||
api(getState).get('/api/v1/conversations', { params })
|
||||
|
@ -19,6 +19,7 @@ export function fetchCustomEmojis() {
|
||||
export function fetchCustomEmojisRequest() {
|
||||
return {
|
||||
type: CUSTOM_EMOJIS_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
@ -26,6 +27,7 @@ export function fetchCustomEmojisSuccess(custom_emojis) {
|
||||
return {
|
||||
type: CUSTOM_EMOJIS_FETCH_SUCCESS,
|
||||
custom_emojis,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
@ -33,5 +35,6 @@ export function fetchCustomEmojisFail(error) {
|
||||
return {
|
||||
type: CUSTOM_EMOJIS_FETCH_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
@ -30,6 +30,7 @@ export function fetchFavouritedStatuses() {
|
||||
export function fetchFavouritedStatusesRequest() {
|
||||
return {
|
||||
type: FAVOURITED_STATUSES_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
@ -38,6 +39,7 @@ export function fetchFavouritedStatusesSuccess(statuses, next) {
|
||||
type: FAVOURITED_STATUSES_FETCH_SUCCESS,
|
||||
statuses,
|
||||
next,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
@ -45,6 +47,7 @@ export function fetchFavouritedStatusesFail(error) {
|
||||
return {
|
||||
type: FAVOURITED_STATUSES_FETCH_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -42,6 +42,13 @@ export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST';
|
||||
export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS';
|
||||
export const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL';
|
||||
|
||||
export const LIST_ADDER_RESET = 'LIST_ADDER_RESET';
|
||||
export const LIST_ADDER_SETUP = 'LIST_ADDER_SETUP';
|
||||
|
||||
export const LIST_ADDER_LISTS_FETCH_REQUEST = 'LIST_ADDER_LISTS_FETCH_REQUEST';
|
||||
export const LIST_ADDER_LISTS_FETCH_SUCCESS = 'LIST_ADDER_LISTS_FETCH_SUCCESS';
|
||||
export const LIST_ADDER_LISTS_FETCH_FAIL = 'LIST_ADDER_LISTS_FETCH_FAIL';
|
||||
|
||||
export const fetchList = id => (dispatch, getState) => {
|
||||
if (getState().getIn(['lists', id])) {
|
||||
return;
|
||||
@ -316,3 +323,50 @@ export const removeFromListFail = (listId, accountId, error) => ({
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const resetListAdder = () => ({
|
||||
type: LIST_ADDER_RESET,
|
||||
});
|
||||
|
||||
export const setupListAdder = accountId => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: LIST_ADDER_SETUP,
|
||||
account: getState().getIn(['accounts', accountId]),
|
||||
});
|
||||
dispatch(fetchLists());
|
||||
dispatch(fetchAccountLists(accountId));
|
||||
};
|
||||
|
||||
export const fetchAccountLists = accountId => (dispatch, getState) => {
|
||||
dispatch(fetchAccountListsRequest(accountId));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/${accountId}/lists`)
|
||||
.then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data)))
|
||||
.catch(err => dispatch(fetchAccountListsFail(accountId, err)));
|
||||
};
|
||||
|
||||
export const fetchAccountListsRequest = id => ({
|
||||
type:LIST_ADDER_LISTS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchAccountListsSuccess = (id, lists) => ({
|
||||
type: LIST_ADDER_LISTS_FETCH_SUCCESS,
|
||||
id,
|
||||
lists,
|
||||
});
|
||||
|
||||
export const fetchAccountListsFail = (id, err) => ({
|
||||
type: LIST_ADDER_LISTS_FETCH_FAIL,
|
||||
id,
|
||||
err,
|
||||
});
|
||||
|
||||
export const addToListAdder = listId => (dispatch, getState) => {
|
||||
dispatch(addToList(listId, getState().getIn(['listAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
export const removeFromListAdder = listId => (dispatch, getState) => {
|
||||
dispatch(removeFromList(listId, getState().getIn(['listAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
importFetchedStatuses,
|
||||
} from './importer';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { unescapeHTML } from '../utils/html';
|
||||
import { getFilters, regexFromFilters } from '../selectors';
|
||||
|
||||
@ -18,6 +19,8 @@ export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
|
||||
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
|
||||
export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
|
||||
|
||||
export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
|
||||
|
||||
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
|
||||
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
||||
|
||||
@ -88,11 +91,18 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||
|
||||
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
||||
|
||||
const excludeTypesFromFilter = filter => {
|
||||
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention']);
|
||||
return allTypes.filterNot(item => item === filter).toJS();
|
||||
};
|
||||
|
||||
const noOp = () => {};
|
||||
|
||||
export function expandNotifications({ maxId } = {}, done = noOp) {
|
||||
return (dispatch, getState) => {
|
||||
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
|
||||
const notifications = getState().get('notifications');
|
||||
const isLoadingMore = !!maxId;
|
||||
|
||||
if (notifications.get('isLoading')) {
|
||||
done();
|
||||
@ -101,14 +111,16 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
|
||||
|
||||
const params = {
|
||||
max_id: maxId,
|
||||
exclude_types: excludeTypesFromSettings(getState()),
|
||||
exclude_types: activeFilter === 'all'
|
||||
? excludeTypesFromSettings(getState())
|
||||
: excludeTypesFromFilter(activeFilter),
|
||||
};
|
||||
|
||||
if (!maxId && notifications.get('items').size > 0) {
|
||||
params.since_id = notifications.getIn(['items', 0]);
|
||||
params.since_id = notifications.getIn(['items', 0, 'id']);
|
||||
}
|
||||
|
||||
dispatch(expandNotificationsRequest());
|
||||
dispatch(expandNotificationsRequest(isLoadingMore));
|
||||
|
||||
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
@ -116,34 +128,37 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
|
||||
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
|
||||
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
||||
|
||||
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
|
||||
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore));
|
||||
fetchRelatedRelationships(dispatch, response.data);
|
||||
done();
|
||||
}).catch(error => {
|
||||
dispatch(expandNotificationsFail(error));
|
||||
dispatch(expandNotificationsFail(error, isLoadingMore));
|
||||
done();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function expandNotificationsRequest() {
|
||||
export function expandNotificationsRequest(isLoadingMore) {
|
||||
return {
|
||||
type: NOTIFICATIONS_EXPAND_REQUEST,
|
||||
skipLoading: !isLoadingMore,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandNotificationsSuccess(notifications, next) {
|
||||
export function expandNotificationsSuccess(notifications, next, isLoadingMore) {
|
||||
return {
|
||||
type: NOTIFICATIONS_EXPAND_SUCCESS,
|
||||
notifications,
|
||||
next,
|
||||
skipLoading: !isLoadingMore,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandNotificationsFail(error) {
|
||||
export function expandNotificationsFail(error, isLoadingMore) {
|
||||
return {
|
||||
type: NOTIFICATIONS_EXPAND_FAIL,
|
||||
error,
|
||||
skipLoading: !isLoadingMore,
|
||||
};
|
||||
};
|
||||
|
||||
@ -163,3 +178,14 @@ export function scrollTopNotifications(top) {
|
||||
top,
|
||||
};
|
||||
};
|
||||
|
||||
export function setFilter (filterType) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: NOTIFICATIONS_FILTER_SET,
|
||||
path: ['notifications', 'quickFilter', 'active'],
|
||||
value: filterType,
|
||||
});
|
||||
dispatch(expandNotifications());
|
||||
};
|
||||
};
|
||||
|
@ -1,14 +1,8 @@
|
||||
import { openModal } from './modal';
|
||||
import { changeSetting, saveSettings } from './settings';
|
||||
|
||||
export function showOnboardingOnce() {
|
||||
return (dispatch, getState) => {
|
||||
const alreadySeen = getState().getIn(['settings', 'onboarded']);
|
||||
export const INTRODUCTION_VERSION = 20181216044202;
|
||||
|
||||
if (!alreadySeen) {
|
||||
dispatch(openModal('ONBOARDING'));
|
||||
dispatch(changeSetting(['onboarded'], true));
|
||||
dispatch(saveSettings());
|
||||
}
|
||||
};
|
||||
export const closeOnboarding = () => dispatch => {
|
||||
dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION));
|
||||
dispatch(saveSettings());
|
||||
};
|
||||
|
@ -12,7 +12,7 @@ import { getLocale } from '../locales';
|
||||
|
||||
const { messages } = getLocale();
|
||||
|
||||
export function connectTimelineStream (timelineId, path, pollingRefresh = null) {
|
||||
export function connectTimelineStream (timelineId, path, pollingRefresh = null, accept = null) {
|
||||
|
||||
return connectStream (path, pollingRefresh, (dispatch, getState) => {
|
||||
const locale = getState().getIn(['meta', 'locale']);
|
||||
@ -24,7 +24,7 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
|
||||
onReceive (data) {
|
||||
switch(data.event) {
|
||||
case 'update':
|
||||
dispatch(updateTimeline(timelineId, JSON.parse(data.payload)));
|
||||
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), accept));
|
||||
break;
|
||||
case 'delete':
|
||||
dispatch(deleteFromTimelines(data.payload));
|
||||
@ -51,6 +51,6 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
|
||||
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
|
||||
export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
|
||||
export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`);
|
||||
export const connectHashtagStream = tag => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
|
||||
export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
|
||||
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
|
||||
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
|
||||
|
@ -4,6 +4,7 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
|
||||
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
|
||||
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
|
||||
export const TIMELINE_CLEAR = 'TIMELINE_CLEAR';
|
||||
|
||||
export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
|
||||
export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
|
||||
@ -13,9 +14,11 @@ export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
|
||||
|
||||
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
|
||||
|
||||
export function updateTimeline(timeline, status) {
|
||||
return (dispatch, getState) => {
|
||||
const references = status.reblog ? getState().get('statuses').filter((item, itemId) => (itemId === status.reblog.id || item.get('reblog') === status.reblog.id)).map((_, itemId) => itemId) : [];
|
||||
export function updateTimeline(timeline, status, accept) {
|
||||
return dispatch => {
|
||||
if (typeof accept === 'function' && !accept(status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(importFetchedStatus(status));
|
||||
|
||||
@ -23,7 +26,6 @@ export function updateTimeline(timeline, status) {
|
||||
type: TIMELINE_UPDATE,
|
||||
timeline,
|
||||
status,
|
||||
references,
|
||||
});
|
||||
};
|
||||
};
|
||||
@ -44,11 +46,24 @@ export function deleteFromTimelines(id) {
|
||||
};
|
||||
};
|
||||
|
||||
export function clearTimeline(timeline) {
|
||||
return (dispatch) => {
|
||||
dispatch({ type: TIMELINE_CLEAR, timeline });
|
||||
};
|
||||
};
|
||||
|
||||
const noOp = () => {};
|
||||
|
||||
const parseTags = (tags = {}, mode) => {
|
||||
return (tags[mode] || []).map((tag) => {
|
||||
return tag.value;
|
||||
});
|
||||
};
|
||||
|
||||
export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
||||
return (dispatch, getState) => {
|
||||
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
|
||||
const isLoadingMore = !!params.max_id;
|
||||
|
||||
if (timeline.get('isLoading')) {
|
||||
done();
|
||||
@ -59,15 +74,17 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
||||
params.since_id = timeline.getIn(['items', 0]);
|
||||
}
|
||||
|
||||
dispatch(expandTimelineRequest(timelineId));
|
||||
const isLoadingRecent = !!params.since_id;
|
||||
|
||||
dispatch(expandTimelineRequest(timelineId, isLoadingMore));
|
||||
|
||||
api(getState).get(path, { params }).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206));
|
||||
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore));
|
||||
done();
|
||||
}).catch(error => {
|
||||
dispatch(expandTimelineFail(timelineId, error));
|
||||
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
||||
done();
|
||||
});
|
||||
};
|
||||
@ -79,31 +96,42 @@ export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done =
|
||||
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
|
||||
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
|
||||
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true });
|
||||
export const expandHashtagTimeline = (hashtag, { maxId } = {}, done = noOp) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }, done);
|
||||
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
||||
export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
|
||||
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||
max_id: maxId,
|
||||
any: parseTags(tags, 'any'),
|
||||
all: parseTags(tags, 'all'),
|
||||
none: parseTags(tags, 'none'),
|
||||
}, done);
|
||||
};
|
||||
|
||||
export function expandTimelineRequest(timeline) {
|
||||
export function expandTimelineRequest(timeline, isLoadingMore) {
|
||||
return {
|
||||
type: TIMELINE_EXPAND_REQUEST,
|
||||
timeline,
|
||||
skipLoading: !isLoadingMore,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandTimelineSuccess(timeline, statuses, next, partial) {
|
||||
export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore) {
|
||||
return {
|
||||
type: TIMELINE_EXPAND_SUCCESS,
|
||||
timeline,
|
||||
statuses,
|
||||
next,
|
||||
partial,
|
||||
isLoadingRecent,
|
||||
skipLoading: !isLoadingMore,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandTimelineFail(timeline, error) {
|
||||
export function expandTimelineFail(timeline, error, isLoadingMore) {
|
||||
return {
|
||||
type: TIMELINE_EXPAND_FAIL,
|
||||
timeline,
|
||||
error,
|
||||
skipLoading: !isLoadingMore,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import LinkHeader from 'http-link-header';
|
||||
import ready from './ready';
|
||||
import LinkHeader from './link_header';
|
||||
|
||||
export const getLinks = response => {
|
||||
const value = response.headers.link;
|
||||
|
@ -68,10 +68,10 @@ class Account extends ImmutablePureComponent {
|
||||
|
||||
if (hidden) {
|
||||
return (
|
||||
<div>
|
||||
<Fragment>
|
||||
{account.get('display_name')}
|
||||
{account.get('username')}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -37,6 +37,14 @@ class ColumnHeader extends React.PureComponent {
|
||||
animating: false,
|
||||
};
|
||||
|
||||
historyBack = () => {
|
||||
if (window.history && window.history.length === 1) {
|
||||
this.context.router.history.push('/');
|
||||
} else {
|
||||
this.context.router.history.goBack();
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleClick = (e) => {
|
||||
e.stopPropagation();
|
||||
this.setState({ collapsed: !this.state.collapsed, animating: true });
|
||||
@ -55,16 +63,22 @@ class ColumnHeader extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleBackClick = () => {
|
||||
if (window.history && window.history.length === 1) this.context.router.history.push('/');
|
||||
else this.context.router.history.goBack();
|
||||
this.historyBack();
|
||||
}
|
||||
|
||||
handleTransitionEnd = () => {
|
||||
this.setState({ animating: false });
|
||||
}
|
||||
|
||||
handlePin = () => {
|
||||
if (!this.props.pinned) {
|
||||
this.historyBack();
|
||||
}
|
||||
this.props.onPin();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { title, icon, active, children, pinned, onPin, multiColumn, extraButton, showBackButton, intl: { formatMessage } } = this.props;
|
||||
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage } } = this.props;
|
||||
const { collapsed, animating } = this.state;
|
||||
|
||||
const wrapperClassName = classNames('column-header__wrapper', {
|
||||
@ -95,7 +109,7 @@ class ColumnHeader extends React.PureComponent {
|
||||
}
|
||||
|
||||
if (multiColumn && pinned) {
|
||||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={onPin}><i className='fa fa fa-times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
|
||||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
|
||||
|
||||
moveButtons = (
|
||||
<div key='move-buttons' className='column-header__setting-arrows'>
|
||||
@ -104,7 +118,7 @@ class ColumnHeader extends React.PureComponent {
|
||||
</div>
|
||||
);
|
||||
} else if (multiColumn) {
|
||||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={onPin}><i className='fa fa fa-plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
|
||||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
|
||||
}
|
||||
|
||||
if (!pinned && (multiColumn || showBackButton)) {
|
||||
|
@ -51,6 +51,10 @@ class Item extends React.PureComponent {
|
||||
const { index, onClick } = this.props;
|
||||
|
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
if (this.hoverToPlay()) {
|
||||
e.target.pause();
|
||||
e.target.currentTime = 0;
|
||||
}
|
||||
e.preventDefault();
|
||||
onClick(index);
|
||||
}
|
||||
|
@ -33,13 +33,15 @@ export default class ModalRoot extends React.PureComponent {
|
||||
} else if (!nextProps.children) {
|
||||
this.setState({ revealed: false });
|
||||
}
|
||||
if (!nextProps.children && !!this.props.children) {
|
||||
this.activeElement.focus();
|
||||
this.activeElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (!this.props.children && !!prevProps.children) {
|
||||
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
|
||||
this.activeElement.focus();
|
||||
this.activeElement = null;
|
||||
}
|
||||
if (this.props.children) {
|
||||
requestAnimationFrame(() => {
|
||||
|
@ -8,6 +8,9 @@ import { throttle } from 'lodash';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import classNames from 'classnames';
|
||||
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
|
||||
import LoadingIndicator from './loading_indicator';
|
||||
|
||||
const MOUSE_IDLE_DELAY = 300;
|
||||
|
||||
export default class ScrollableList extends PureComponent {
|
||||
|
||||
@ -23,10 +26,10 @@ export default class ScrollableList extends PureComponent {
|
||||
trackScroll: PropTypes.bool,
|
||||
shouldUpdateScroll: PropTypes.func,
|
||||
isLoading: PropTypes.bool,
|
||||
showLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
prepend: PropTypes.node,
|
||||
alwaysPrepend: PropTypes.bool,
|
||||
alwaysShowScrollbar: PropTypes.bool,
|
||||
emptyMessage: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
@ -46,7 +49,7 @@ export default class ScrollableList extends PureComponent {
|
||||
const { scrollTop, scrollHeight, clientHeight } = this.node;
|
||||
const offset = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
if (400 > offset && this.props.onLoadMore && !this.props.isLoading) {
|
||||
if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
|
||||
this.props.onLoadMore();
|
||||
}
|
||||
|
||||
@ -55,14 +58,72 @@ export default class ScrollableList extends PureComponent {
|
||||
} else if (this.props.onScroll) {
|
||||
this.props.onScroll();
|
||||
}
|
||||
|
||||
if (!this.lastScrollWasSynthetic) {
|
||||
// If the last scroll wasn't caused by setScrollTop(), assume it was
|
||||
// intentional and cancel any pending scroll reset on mouse idle
|
||||
this.scrollToTopOnMouseIdle = false;
|
||||
}
|
||||
this.lastScrollWasSynthetic = false;
|
||||
}
|
||||
}, 150, {
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
mouseIdleTimer = null;
|
||||
mouseMovedRecently = false;
|
||||
lastScrollWasSynthetic = false;
|
||||
scrollToTopOnMouseIdle = false;
|
||||
|
||||
setScrollTop = newScrollTop => {
|
||||
if (this.node.scrollTop !== newScrollTop) {
|
||||
this.lastScrollWasSynthetic = true;
|
||||
this.node.scrollTop = newScrollTop;
|
||||
}
|
||||
};
|
||||
|
||||
clearMouseIdleTimer = () => {
|
||||
if (this.mouseIdleTimer === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(this.mouseIdleTimer);
|
||||
this.mouseIdleTimer = null;
|
||||
};
|
||||
|
||||
handleMouseMove = throttle(() => {
|
||||
// As long as the mouse keeps moving, clear and restart the idle timer.
|
||||
this.clearMouseIdleTimer();
|
||||
this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);
|
||||
|
||||
if (!this.mouseMovedRecently && this.node.scrollTop === 0) {
|
||||
// Only set if we just started moving and are scrolled to the top.
|
||||
this.scrollToTopOnMouseIdle = true;
|
||||
}
|
||||
|
||||
// Save setting this flag for last, so we can do the comparison above.
|
||||
this.mouseMovedRecently = true;
|
||||
}, MOUSE_IDLE_DELAY / 2);
|
||||
|
||||
handleWheel = throttle(() => {
|
||||
this.scrollToTopOnMouseIdle = false;
|
||||
}, 150, {
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
handleMouseIdle = () => {
|
||||
if (this.scrollToTopOnMouseIdle) {
|
||||
this.setScrollTop(0);
|
||||
}
|
||||
|
||||
this.mouseMovedRecently = false;
|
||||
this.scrollToTopOnMouseIdle = false;
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.attachScrollListener();
|
||||
this.attachIntersectionObserver();
|
||||
|
||||
attachFullscreenListener(this.onFullScreenChange);
|
||||
|
||||
// Handle initial scroll posiiton
|
||||
@ -73,7 +134,8 @@ export default class ScrollableList extends PureComponent {
|
||||
const someItemInserted = React.Children.count(prevProps.children) > 0 &&
|
||||
React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
|
||||
this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
|
||||
if (someItemInserted && this.node.scrollTop > 0) {
|
||||
|
||||
if (someItemInserted && (this.node.scrollTop > 0 || this.mouseMovedRecently)) {
|
||||
return this.node.scrollHeight - this.node.scrollTop;
|
||||
} else {
|
||||
return null;
|
||||
@ -84,15 +146,12 @@ export default class ScrollableList extends PureComponent {
|
||||
// Reset the scroll position when a new child comes in in order not to
|
||||
// jerk the scrollbar around if you're already scrolled down the page.
|
||||
if (snapshot !== null) {
|
||||
const newScrollTop = this.node.scrollHeight - snapshot;
|
||||
|
||||
if (this.node.scrollTop !== newScrollTop) {
|
||||
this.node.scrollTop = newScrollTop;
|
||||
}
|
||||
this.setScrollTop(this.node.scrollHeight - snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.clearMouseIdleTimer();
|
||||
this.detachScrollListener();
|
||||
this.detachIntersectionObserver();
|
||||
detachFullscreenListener(this.onFullScreenChange);
|
||||
@ -115,20 +174,24 @@ export default class ScrollableList extends PureComponent {
|
||||
|
||||
attachScrollListener () {
|
||||
this.node.addEventListener('scroll', this.handleScroll);
|
||||
this.node.addEventListener('wheel', this.handleWheel);
|
||||
}
|
||||
|
||||
detachScrollListener () {
|
||||
this.node.removeEventListener('scroll', this.handleScroll);
|
||||
this.node.removeEventListener('wheel', this.handleWheel);
|
||||
}
|
||||
|
||||
getFirstChildKey (props) {
|
||||
const { children } = props;
|
||||
let firstChild = children;
|
||||
let firstChild = children;
|
||||
|
||||
if (children instanceof ImmutableList) {
|
||||
firstChild = children.get(0);
|
||||
} else if (Array.isArray(children)) {
|
||||
firstChild = children[0];
|
||||
}
|
||||
|
||||
return firstChild && firstChild.key;
|
||||
}
|
||||
|
||||
@ -136,22 +199,34 @@ export default class ScrollableList extends PureComponent {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
handleLoadMore = (e) => {
|
||||
handleLoadMore = e => {
|
||||
e.preventDefault();
|
||||
this.props.onLoadMore();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, alwaysPrepend, alwaysShowScrollbar, emptyMessage, onLoadMore } = this.props;
|
||||
const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props;
|
||||
const { fullscreen } = this.state;
|
||||
const childrenCount = React.Children.count(children);
|
||||
|
||||
const loadMore = (hasMore && childrenCount > 0 && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
|
||||
const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
|
||||
let scrollableArea = null;
|
||||
|
||||
if (isLoading || childrenCount > 0 || !emptyMessage) {
|
||||
if (showLoading) {
|
||||
scrollableArea = (
|
||||
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
|
||||
<div className='scrollable scrollable--flex' ref={this.setRef}>
|
||||
<div role='feed' className='item-list'>
|
||||
{prepend}
|
||||
</div>
|
||||
|
||||
<div className='scrollable__append'>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
|
||||
scrollableArea = (
|
||||
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
||||
<div role='feed' className='item-list'>
|
||||
{prepend}
|
||||
|
||||
@ -173,10 +248,8 @@ export default class ScrollableList extends PureComponent {
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const scrollable = alwaysShowScrollbar;
|
||||
|
||||
scrollableArea = (
|
||||
<div className={classNames({ scrollable, fullscreen })} ref={this.setRef} style={{ flex: '1 1 auto', display: 'flex', flexDirection: 'column' }}>
|
||||
<div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef}>
|
||||
{alwaysPrepend && prepend}
|
||||
|
||||
<div className='empty-column-indicator'>
|
||||
|
@ -5,7 +5,7 @@ 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 { me } from '../initial_state';
|
||||
import { me, isStaff } from '../initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||
@ -31,6 +31,8 @@ const messages = defineMessages({
|
||||
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' },
|
||||
});
|
||||
|
||||
const obfuscatedCount = count => {
|
||||
@ -150,6 +152,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
|
||||
let menu = [];
|
||||
let reblogIcon = 'retweet';
|
||||
let replyIcon;
|
||||
let replyTitle;
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
||||
@ -183,6 +186,11 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
|
||||
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
|
||||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||
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')}` });
|
||||
}
|
||||
}
|
||||
|
||||
if (status.get('visibility') === 'direct') {
|
||||
@ -192,8 +200,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
if (status.get('in_reply_to_id', null) === null) {
|
||||
replyIcon = 'reply';
|
||||
replyTitle = intl.formatMessage(messages.reply);
|
||||
} else {
|
||||
replyIcon = 'reply-all';
|
||||
replyTitle = intl.formatMessage(messages.replyAll);
|
||||
}
|
||||
|
||||
@ -203,7 +213,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
|
||||
return (
|
||||
<div className='status__action-bar'>
|
||||
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon='reply' onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
|
||||
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} 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={anonymousAccess || !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' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
||||
{shareButton}
|
||||
|
@ -25,7 +25,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||
prepend: PropTypes.node,
|
||||
emptyMessage: PropTypes.node,
|
||||
alwaysPrepend: PropTypes.bool,
|
||||
timelineId: PropTypes.string.isRequired,
|
||||
timelineId: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -55,7 +55,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
handleLoadOlder = debounce(() => {
|
||||
this.props.onLoadMore(this.props.statusIds.last());
|
||||
this.props.onLoadMore(this.props.statusIds.size > 0 ? this.props.statusIds.last() : undefined);
|
||||
}, 300, { leading: true })
|
||||
|
||||
_selectChild (index) {
|
||||
@ -124,7 +124,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} shouldUpdateScroll={shouldUpdateScroll} ref={this.setRef}>
|
||||
<ScrollableList {...other} showLoading={isLoading && statusIds.size === 0} onLoadMore={onLoadMore && this.handleLoadOlder} shouldUpdateScroll={shouldUpdateScroll} ref={this.setRef}>
|
||||
{scrollableContent}
|
||||
</ScrollableList>
|
||||
);
|
||||
|
@ -10,8 +10,7 @@ const messages = defineMessages({
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const mapStateToProps = (state, { }) => ({
|
||||
});
|
||||
const mapStateToProps = () => ({});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Provider, connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import configureStore from '../store/configureStore';
|
||||
import { showOnboardingOnce } from '../actions/onboarding';
|
||||
import { INTRODUCTION_VERSION } from '../actions/onboarding';
|
||||
import { BrowserRouter, Route } from 'react-router-dom';
|
||||
import { ScrollContext } from 'react-router-scroll-4';
|
||||
import UI from '../features/ui';
|
||||
import Introduction from '../features/introduction';
|
||||
import { fetchCustomEmojis } from '../actions/custom_emojis';
|
||||
import { hydrateStore } from '../actions/store';
|
||||
import { connectUserStream } from '../actions/streaming';
|
||||
@ -18,11 +19,39 @@ addLocaleData(localeData);
|
||||
|
||||
export const store = configureStore();
|
||||
const hydrateAction = hydrateStore(initialState);
|
||||
store.dispatch(hydrateAction);
|
||||
|
||||
// load custom emojis
|
||||
store.dispatch(hydrateAction);
|
||||
store.dispatch(fetchCustomEmojis());
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
showIntroduction: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
|
||||
});
|
||||
|
||||
@connect(mapStateToProps)
|
||||
class MastodonMount extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
showIntroduction: PropTypes.bool,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { showIntroduction } = this.props;
|
||||
|
||||
if (showIntroduction) {
|
||||
return <Introduction />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BrowserRouter basename='/web'>
|
||||
<ScrollContext>
|
||||
<Route path='/' component={UI} />
|
||||
</ScrollContext>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class Mastodon extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
@ -31,14 +60,6 @@ export default class Mastodon extends React.PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this.disconnect = store.dispatch(connectUserStream());
|
||||
|
||||
// Desktop notifications
|
||||
// Ask after 1 minute
|
||||
if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
|
||||
window.setTimeout(() => Notification.requestPermission(), 60 * 1000);
|
||||
}
|
||||
|
||||
store.dispatch(showOnboardingOnce());
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
@ -54,11 +75,7 @@ export default class Mastodon extends React.PureComponent {
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<Provider store={store}>
|
||||
<BrowserRouter basename='/web'>
|
||||
<ScrollContext>
|
||||
<Route path='/' component={UI} />
|
||||
</ScrollContext>
|
||||
</BrowserRouter>
|
||||
<MastodonMount />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { me } from '../../../initial_state';
|
||||
import { me, isStaff } from '../../../initial_state';
|
||||
import { shortNumberFormat } from '../../../utils/numbers';
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -34,6 +34,8 @@ const messages = defineMessages({
|
||||
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||
endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
|
||||
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
|
||||
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
|
||||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
@ -51,6 +53,7 @@ class ActionBar extends React.PureComponent {
|
||||
onBlockDomain: PropTypes.func.isRequired,
|
||||
onUnblockDomain: PropTypes.func.isRequired,
|
||||
onEndorseToggle: PropTypes.func.isRequired,
|
||||
onAddToList: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
@ -105,6 +108,7 @@ class ActionBar extends React.PureComponent {
|
||||
}
|
||||
|
||||
menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
|
||||
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
@ -148,6 +152,11 @@ class ActionBar extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
if (account.get('id') !== me && isStaff) {
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` });
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{extraInfo}
|
||||
|
@ -16,6 +16,7 @@ const messages = defineMessages({
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
|
||||
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
|
||||
});
|
||||
|
||||
const dateFormatOptions = {
|
||||
@ -148,7 +149,7 @@ class Header extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
if (account.get('locked')) {
|
||||
lockedIcon = <i className='fa fa-lock' />;
|
||||
lockedIcon = <i className='fa fa-lock' title={intl.formatMessage(messages.account_locked)} />;
|
||||
}
|
||||
|
||||
const content = { __html: account.get('note_emojified') };
|
||||
@ -157,7 +158,7 @@ class Header extends ImmutablePureComponent {
|
||||
const badge = account.get('bot') ? (<div className='roles'><div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div></div>) : null;
|
||||
|
||||
return (
|
||||
<div className={classNames('account__header', { inactive: !!account.get('moved') })} style={{ backgroundImage: `url(${account.get('header')})` }}>
|
||||
<div className={classNames('account__header', { inactive: !!account.get('moved') })} style={{ backgroundImage: `url(${autoPlayGif ? account.get('header') : account.get('header_static')})` }}>
|
||||
<div>
|
||||
<Avatar account={account} />
|
||||
|
||||
|
@ -36,7 +36,7 @@ class LoadMoreMedia extends ImmutablePureComponent {
|
||||
return (
|
||||
<LoadMore
|
||||
disabled={this.props.disabled}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
onClick={this.handleLoadMore}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -68,7 +68,7 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
|
||||
handleScrollToBottom = () => {
|
||||
if (this.props.hasMore) {
|
||||
this.handleLoadMore(this.props.medias.last().getIn(['status', 'id']));
|
||||
this.handleLoadMore(this.props.medias.size > 0 ? this.props.medias.last().getIn(['status', 'id']) : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,8 +103,8 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoading && medias.size > 0 && hasMore) {
|
||||
loadOlder = <LoadMore onClick={this.handleLoadOlder} />;
|
||||
if (hasMore && !(isLoading && medias.size === 0)) {
|
||||
loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -112,14 +112,15 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
<ColumnBackButton />
|
||||
|
||||
<ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={shouldUpdateScroll}>
|
||||
<div className='scrollable' onScroll={this.handleScroll}>
|
||||
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
|
||||
<HeaderContainer accountId={this.props.params.accountId} />
|
||||
|
||||
<div className='account-gallery__container'>
|
||||
<div role='feed' className='account-gallery__container'>
|
||||
{medias.map((media, index) => media === null ? (
|
||||
<LoadMoreMedia
|
||||
key={'more:' + medias.getIn(index + 1, 'id')}
|
||||
maxId={index > 0 ? medias.getIn(index - 1, 'id') : null}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
/>
|
||||
) : (
|
||||
<MediaItem
|
||||
@ -129,6 +130,12 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
))}
|
||||
{loadOlder}
|
||||
</div>
|
||||
|
||||
{isLoading && medias.size === 0 && (
|
||||
<div className='scrollable__append'>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
</Column>
|
||||
|
@ -23,6 +23,7 @@ export default class Header extends ImmutablePureComponent {
|
||||
onBlockDomain: PropTypes.func.isRequired,
|
||||
onUnblockDomain: PropTypes.func.isRequired,
|
||||
onEndorseToggle: PropTypes.func.isRequired,
|
||||
onAddToList: PropTypes.func.isRequired,
|
||||
hideTabs: PropTypes.bool,
|
||||
};
|
||||
|
||||
@ -78,6 +79,10 @@ export default class Header extends ImmutablePureComponent {
|
||||
this.props.onEndorseToggle(this.props.account);
|
||||
}
|
||||
|
||||
handleAddToList = () => {
|
||||
this.props.onAddToList(this.props.account);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { account, hideTabs } = this.props;
|
||||
|
||||
@ -106,6 +111,7 @@ export default class Header extends ImmutablePureComponent {
|
||||
onBlockDomain={this.handleBlockDomain}
|
||||
onUnblockDomain={this.handleUnblockDomain}
|
||||
onEndorseToggle={this.handleEndorseToggle}
|
||||
onAddToList={this.handleAddToList}
|
||||
/>
|
||||
|
||||
{!hideTabs && (
|
||||
|
@ -116,6 +116,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
dispatch(unblockDomain(domain));
|
||||
},
|
||||
|
||||
onAddToList(account){
|
||||
dispatch(openModal('LIST_ADDER', {
|
||||
accountId: account.get('id'),
|
||||
}));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
|
||||
|
@ -11,6 +11,7 @@ import HeaderContainer from './containers/header_container';
|
||||
import ColumnBackButton from '../../components/column_back_button';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => {
|
||||
const path = withReplies ? `${accountId}:with_replies` : accountId;
|
||||
@ -78,6 +79,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||
|
||||
<StatusList
|
||||
prepend={<HeaderContainer accountId={this.props.params.accountId} />}
|
||||
alwaysPrepend
|
||||
scrollKey='account_timeline'
|
||||
statusIds={statusIds}
|
||||
featuredStatusIds={featuredStatusIds}
|
||||
@ -85,6 +87,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
shouldUpdateScroll={shouldUpdateScroll}
|
||||
emptyMessage={<FormattedMessage id='empty_column.account_timeline' defaultMessage='No toots here!' />}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
|
@ -48,6 +48,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
caretPosition: PropTypes.number,
|
||||
preselectDate: PropTypes.instanceOf(Date),
|
||||
is_submitting: PropTypes.bool,
|
||||
is_changing_upload: PropTypes.bool,
|
||||
is_uploading: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
@ -83,10 +84,10 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
// Submit disabled:
|
||||
const { is_submitting, is_uploading, anyMedia } = this.props;
|
||||
const { is_submitting, is_changing_upload, is_uploading, anyMedia } = this.props;
|
||||
const fulltext = [this.props.spoiler_text, countableText(this.props.text)].join('');
|
||||
|
||||
if (is_submitting || is_uploading || length(fulltext) > 500 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
|
||||
if (is_submitting || is_uploading || is_changing_upload || length(fulltext) > 500 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -162,7 +163,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
const { intl, onPaste, showSearch, anyMedia } = this.props;
|
||||
const disabled = this.props.is_submitting;
|
||||
const text = [this.props.spoiler_text, countableText(this.props.text)].join('');
|
||||
const disabledButton = disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
|
||||
const disabledButton = disabled || this.props.is_uploading || this.props.is_changing_upload || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
|
||||
let publishText = '';
|
||||
|
||||
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user