about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2019-03-16 11:23:22 +0100
committerGitHub <noreply@github.com>2019-03-16 11:23:22 +0100
commit1c113fd72df18999de1d6f09fa3790dd1f715506 (patch)
tree6a2683eb7d63cca3e568f6f2786f77e606320bb1 /app
parent8da5b8e6695e3bcca8eb4bca754faef83a6656eb (diff)
Add relationship manager UI (#10268)
Diffstat (limited to 'app')
-rw-r--r--app/controllers/relationships_controller.rb98
-rw-r--r--app/controllers/settings/follower_domains_controller.rb28
-rw-r--r--app/helpers/admin/filter_helper.rb3
-rw-r--r--app/javascript/styles/mastodon/tables.scss19
-rw-r--r--app/models/form/account_batch.rb60
-rw-r--r--app/views/relationships/_account.html.haml20
-rw-r--r--app/views/relationships/show.html.haml43
-rw-r--r--app/views/settings/follower_domains/show.html.haml34
8 files changed, 242 insertions, 63 deletions
diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb
new file mode 100644
index 000000000..e6dd65e44
--- /dev/null
+++ b/app/controllers/relationships_controller.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+class RelationshipsController < ApplicationController
+  layout 'admin'
+
+  before_action :authenticate_user!
+  before_action :set_accounts, only: :show
+  before_action :set_body_classes
+
+  helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship?
+
+  def show
+    @form = Form::AccountBatch.new
+  end
+
+  def update
+    @form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
+    @form.save
+  rescue ActionController::ParameterMissing
+    # Do nothing
+  ensure
+    redirect_to relationships_path(current_params)
+  end
+
+  private
+
+  def set_accounts
+    @accounts = relationships_scope.page(params[:page]).per(40)
+  end
+
+  def relationships_scope
+    scope = begin
+      if following_relationship?
+        current_account.following.includes(:account_stat)
+      else
+        current_account.followers.includes(:account_stat)
+      end
+    end
+
+    scope.merge!(Follow.recent)
+    scope.merge!(mutual_relationship_scope) if mutual_relationship?
+    scope.merge!(abandoned_account_scope)   if params[:status] == 'abandoned'
+    scope.merge!(active_account_scope)      if params[:status] == 'active'
+    scope.merge!(by_domain_scope)           if params[:by_domain].present?
+
+    scope
+  end
+
+  def mutual_relationship_scope
+    Account.where(id: current_account.following)
+  end
+
+  def abandoned_account_scope
+    Account.where.not(moved_to_account_id: nil)
+  end
+
+  def active_account_scope
+    Account.where(moved_to_account_id: nil)
+  end
+
+  def by_domain_scope
+    Account.where(domain: params[:by_domain])
+  end
+
+  def form_account_batch_params
+    params.require(:form_account_batch).permit(:action, account_ids: [])
+  end
+
+  def following_relationship?
+    params[:relationship].blank? || params[:relationship] == 'following'
+  end
+
+  def mutual_relationship?
+    params[:relationship] == 'mutual'
+  end
+
+  def followed_by_relationship?
+    params[:relationship] == 'followed_by'
+  end
+
+  def current_params
+    params.slice(:page, :status, :relationship, :by_domain).permit(:page, :status, :relationship, :by_domain)
+  end
+
+  def action_from_button
+    if params[:unfollow]
+      'unfollow'
+    elsif params[:remove_from_followers]
+      'remove_from_followers'
+    elsif params[:block_domains]
+      'block_domains'
+    end
+  end
+
+  def set_body_classes
+    @body_classes = 'admin'
+  end
+end
diff --git a/app/controllers/settings/follower_domains_controller.rb b/app/controllers/settings/follower_domains_controller.rb
deleted file mode 100644
index ce8ec985d..000000000
--- a/app/controllers/settings/follower_domains_controller.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-class Settings::FollowerDomainsController < Settings::BaseController
-  layout 'admin'
-
-  before_action :authenticate_user!
-
-  def show
-    @account = current_account
-    @domains = current_account.followers.reorder(Arel.sql('MIN(follows.id) DESC')).group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10)
-  end
-
-  def update
-    domains = bulk_params[:select] || []
-
-    AfterAccountDomainBlockWorker.push_bulk(domains) do |domain|
-      [current_account.id, domain]
-    end
-
-    redirect_to settings_follower_domains_path, notice: I18n.t('followers.success', count: domains.size)
-  end
-
-  private
-
-  def bulk_params
-    params.permit(select: [])
-  end
-end
diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb
index 8f78bf5f8..09a356296 100644
--- a/app/helpers/admin/filter_helper.rb
+++ b/app/helpers/admin/filter_helper.rb
@@ -7,8 +7,9 @@ module Admin::FilterHelper
   CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
   TAGS_FILTERS         = %i(hidden).freeze
   INSTANCES_FILTERS    = %i(limited by_domain).freeze
+  FOLLOWERS_FILTERS    = %i(relationship status by_domain).freeze
 
-  FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS
+  FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS + FOLLOWERS_FILTERS
 
   def filter_link_to(text, link_to_params, link_class_params = link_to_params)
     new_url = filtered_url_for(link_to_params)
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index 9e8785679..d3a0ea03d 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -140,6 +140,15 @@ a.table-action-link {
       input {
         margin-top: 8px;
       }
+
+      &--aligned {
+        display: flex;
+        align-items: center;
+
+        input {
+          margin-top: 0;
+        }
+      }
     }
 
     &__actions,
@@ -183,6 +192,10 @@ a.table-action-link {
     &__content {
       padding-top: 12px;
       padding-bottom: 16px;
+
+      &--unpadded {
+        padding: 0;
+      }
     }
   }
 
@@ -197,4 +210,10 @@ a.table-action-link {
       font-weight: 700;
     }
   }
+
+  .nothing-here {
+    border: 1px solid darken($ui-base-color, 8%);
+    border-top: 0;
+    box-shadow: none;
+  }
 }
diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb
new file mode 100644
index 000000000..60eaaf0e2
--- /dev/null
+++ b/app/models/form/account_batch.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+class Form::AccountBatch
+  include ActiveModel::Model
+
+  attr_accessor :account_ids, :action, :current_account
+
+  def save
+    case action
+    when 'unfollow'
+      unfollow!
+    when 'remove_from_followers'
+      remove_from_followers!
+    when 'block_domains'
+      block_domains!
+    end
+  end
+
+  private
+
+  def unfollow!
+    accounts.find_each do |target_account|
+      UnfollowService.new.call(current_account, target_account)
+    end
+  end
+
+  def remove_from_followers!
+    current_account.passive_relationships.where(account_id: account_ids).find_each do |follow|
+      reject_follow!(follow)
+    end
+  end
+
+  def block_domains!
+    AfterAccountDomainBlockWorker.push_bulk(account_domains) do |domain|
+      [current_account.id, domain]
+    end
+  end
+
+  def account_domains
+    accounts.pluck(Arel.sql('distinct domain')).compact
+  end
+
+  def accounts
+    Account.where(id: account_ids)
+  end
+
+  def reject_follow!(follow)
+    follow.destroy
+
+    return unless follow.account.activitypub?
+
+    json = ActiveModelSerializers::SerializableResource.new(
+      follow,
+      serializer: ActivityPub::RejectFollowSerializer,
+      adapter: ActivityPub::Adapter
+    ).to_json
+
+    ActivityPub::DeliveryWorker.perform_async(json, current_account.id, follow.account.inbox_url)
+  end
+end
diff --git a/app/views/relationships/_account.html.haml b/app/views/relationships/_account.html.haml
new file mode 100644
index 000000000..6c22deb51
--- /dev/null
+++ b/app/views/relationships/_account.html.haml
@@ -0,0 +1,20 @@
+.batch-table__row
+  %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
+    = f.check_box :account_ids, { multiple: true, include_hidden: false }, account.id
+  .batch-table__row__content.batch-table__row__content--unpadded
+    %table.accounts-table
+      %tbody
+        %tr
+          %td= account_link_to account
+          %td.accounts-table__count.optional
+            = number_to_human account.statuses_count, strip_insignificant_zeros: true
+            %small= t('accounts.posts', count: account.statuses_count).downcase
+          %td.accounts-table__count.optional
+            = number_to_human account.followers_count, strip_insignificant_zeros: true
+            %small= t('accounts.followers', count: account.followers_count).downcase
+          %td.accounts-table__count
+            - if account.last_status_at.present?
+              %time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at
+            - else
+              \-
+            %small= t('accounts.last_active')
diff --git a/app/views/relationships/show.html.haml b/app/views/relationships/show.html.haml
new file mode 100644
index 000000000..33a43f1a8
--- /dev/null
+++ b/app/views/relationships/show.html.haml
@@ -0,0 +1,43 @@
+- content_for :page_title do
+  = t('settings.relationships')
+
+- content_for :header_tags do
+  = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous'
+
+.filters
+  .filter-subset
+    %strong= t 'relationships.relationship'
+    %ul
+      %li= filter_link_to t('accounts.following', count: current_account.following_count), relationship: nil
+      %li= filter_link_to t('accounts.followers', count: current_account.followers_count), relationship: 'followed_by'
+      %li= filter_link_to t('relationships.mutual'), relationship: 'mutual'
+
+  .filter-subset
+    %strong= t 'relationships.status'
+    %ul
+      %li= filter_link_to t('generic.all'), status: nil
+      %li= filter_link_to t('relationships.active'), status: 'active'
+      %li= filter_link_to t('relationships.abandoned'), status: 'abandoned'
+
+= form_for(@form, url: relationships_path, method: :patch) do |f|
+  = hidden_field_tag :page, params[:page] || 1
+  = hidden_field_tag :relationship, params[:relationship]
+  = hidden_field_tag :status, params[:status]
+
+  .batch-table
+    .batch-table__toolbar
+      %label.batch-table__toolbar__select.batch-checkbox-all
+        = check_box_tag :batch_checkbox_all, nil, false
+      .batch-table__toolbar__actions
+        = f.button safe_join([fa_icon('user-times'), t('relationships.remove_selected_follows')]), name: :unfollow, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } unless followed_by_relationship?
+
+        = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_followers')]), name: :remove_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } unless following_relationship?
+
+        = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :block_domains, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship?
+    .batch-table__body
+      - if @accounts.empty?
+        = nothing_here 'nothing-here--under-tabs'
+      - else
+        = render partial: 'account', collection: @accounts, locals: { f: f }
+
+= paginate @accounts
diff --git a/app/views/settings/follower_domains/show.html.haml b/app/views/settings/follower_domains/show.html.haml
deleted file mode 100644
index f1687d4d2..000000000
--- a/app/views/settings/follower_domains/show.html.haml
+++ /dev/null
@@ -1,34 +0,0 @@
-- content_for :page_title do
-  = t('settings.followers')
-
-= form_tag settings_follower_domains_path, method: :patch, class: 'table-form' do
-  - unless @account.locked?
-    .warning
-      %strong
-        = fa_icon('warning')
-        = t('followers.unlocked_warning_title')
-      = t('followers.unlocked_warning_html', lock_link: link_to(t('followers.lock_link'), settings_profile_url))
-
-  %p= t('followers.explanation_html')
-  %p= t('followers.true_privacy_html')
-
-  .table-wrapper
-    %table.table
-      %thead
-        %tr
-          %th
-          %th= t('followers.domain')
-          %th= t('followers.followers_count')
-      %tbody
-        - @domains.each do |domain|
-          %tr
-            %td
-              = check_box_tag 'select[]', domain.domain, false, disabled: !@account.locked? unless domain.domain.nil?
-            %td
-              %samp= domain.domain.presence || Rails.configuration.x.local_domain
-            %td= number_with_delimiter domain.accounts_from_domain
-
-  .action-pagination
-    .actions
-      = button_tag t('followers.purge'), type: :submit, class: 'button', disabled: !@account.locked?
-    = paginate @domains