about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.circleci/config.yml12
-rw-r--r--Vagrantfile2
-rw-r--r--app/controllers/admin/action_logs_controller.rb14
-rw-r--r--app/helpers/admin/action_logs_helper.rb75
-rw-r--r--app/helpers/admin/filter_helper.rb1
-rw-r--r--app/javascript/core/admin.js4
-rw-r--r--app/javascript/mastodon/features/follow_requests/index.js17
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json6
-rw-r--r--app/javascript/mastodon/locales/en.json1
-rw-r--r--app/javascript/styles/mastodon/admin.scss68
-rw-r--r--app/javascript/styles/mastodon/components.scss8
-rw-r--r--app/models/admin/action_log_filter.rb81
-rw-r--r--app/views/admin/accounts/show.html.haml2
-rw-r--r--app/views/admin/action_logs/_action_log.html.haml6
-rw-r--r--app/views/admin/action_logs/index.html.haml24
-rw-r--r--config/initializers/kaminari_config.rb2
-rw-r--r--config/locales/en.yml39
-rw-r--r--spec/helpers/admin/action_log_helper_spec.rb238
18 files changed, 215 insertions, 385 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml
index a343fc654..dd943e327 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -139,9 +139,10 @@ jobs:
     docker:
       - image: circleci/ruby:2.7-buster-node
         environment: *ruby_environment
-      - image: circleci/postgres:10.6-alpine
+      - image: circleci/postgres:12.2
         environment:
           POSTGRES_USER: root
+          POSTGRES_HOST_AUTH_METHOD: trust
       - image: circleci/redis:5-alpine
     steps:
       - *attach_workspace
@@ -158,9 +159,10 @@ jobs:
     docker:
       - image: circleci/ruby:2.7-buster-node
         environment: *ruby_environment
-      - image: circleci/postgres:10.6-alpine
+      - image: circleci/postgres:12.2
         environment:
           POSTGRES_USER: root
+          POSTGRES_HOST_AUTH_METHOD: trust
       - image: circleci/redis:5-alpine
     <<: *test_steps
 
@@ -169,9 +171,10 @@ jobs:
     docker:
       - image: circleci/ruby:2.6-buster-node
         environment: *ruby_environment
-      - image: circleci/postgres:10.6-alpine
+      - image: circleci/postgres:12.2
         environment:
           POSTGRES_USER: root
+          POSTGRES_HOST_AUTH_METHOD: trust
       - image: circleci/redis:5-alpine
     <<: *test_steps
 
@@ -180,9 +183,10 @@ jobs:
     docker:
       - image: circleci/ruby:2.5-buster-node
         environment: *ruby_environment
-      - image: circleci/postgres:10.6-alpine
+      - image: circleci/postgres:12.2
         environment:
           POSTGRES_USER: root
+          POSTGRES_HOST_AUTH_METHOD: trust
       - image: circleci/redis:5-alpine
     <<: *test_steps
 
diff --git a/Vagrantfile b/Vagrantfile
index e00ed1ca6..143fa27fd 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -91,7 +91,7 @@ VAGRANTFILE_API_VERSION = "2"
 
 Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
 
-  config.vm.box = "ubuntu/xenial64"
+  config.vm.box = "ubuntu/bionic64"
 
   config.vm.provider :virtualbox do |vb|
     vb.name = "mastodon"
diff --git a/app/controllers/admin/action_logs_controller.rb b/app/controllers/admin/action_logs_controller.rb
index e273dfeae..2d77620df 100644
--- a/app/controllers/admin/action_logs_controller.rb
+++ b/app/controllers/admin/action_logs_controller.rb
@@ -2,8 +2,18 @@
 
 module Admin
   class ActionLogsController < BaseController
-    def index
-      @action_logs = Admin::ActionLog.page(params[:page])
+    before_action :set_action_logs
+
+    def index; end
+
+    private
+
+    def set_action_logs
+      @action_logs = Admin::ActionLogFilter.new(filter_params).results.page(params[:page])
+    end
+
+    def filter_params
+      params.slice(:page, *Admin::ActionLogFilter::KEYS).permit(:page, *Admin::ActionLogFilter::KEYS)
     end
   end
 end
diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb
index 6bc75aa56..88d6e4580 100644
--- a/app/helpers/admin/action_logs_helper.rb
+++ b/app/helpers/admin/action_logs_helper.rb
@@ -9,79 +9,8 @@ module Admin::ActionLogsHelper
     end
   end
 
-  def relevant_log_changes(log)
-    if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)
-      log.recorded_changes.slice('domain')
-    elsif log.target_type == 'CustomEmoji' && log.action == :update
-      log.recorded_changes.slice('domain', 'visible_in_picker')
-    elsif log.target_type == 'User' && [:promote, :demote].include?(log.action)
-      log.recorded_changes.slice('moderator', 'admin')
-    elsif log.target_type == 'User' && [:change_email].include?(log.action)
-      log.recorded_changes.slice('email', 'unconfirmed_email')
-    elsif log.target_type == 'DomainBlock'
-      log.recorded_changes.slice('severity', 'reject_media')
-    elsif log.target_type == 'Status' && log.action == :update
-      log.recorded_changes.slice('sensitive')
-    elsif log.target_type == 'Announcement' && log.action == :update
-      log.recorded_changes.slice('text', 'starts_at', 'ends_at', 'all_day')
-    end
-  end
-
-  def log_extra_attributes(hash)
-    safe_join(hash.to_a.map { |key, value| safe_join([content_tag(:span, key, class: 'diff-key'), '=', log_change(value)]) }, ' ')
-  end
-
-  def log_change(val)
-    return content_tag(:span, val, class: 'diff-neutral') unless val.is_a?(Array)
-    safe_join([content_tag(:span, val.first, class: 'diff-old'), content_tag(:span, val.last, class: 'diff-new')], '→')
-  end
-
-  def icon_for_log(log)
-    case log.target_type
-    when 'Account', 'User'
-      'user'
-    when 'CustomEmoji'
-      'file'
-    when 'Report'
-      'flag'
-    when 'DomainBlock'
-      'lock'
-    when 'DomainAllow'
-      'plus-circle'
-    when 'EmailDomainBlock'
-      'envelope'
-    when 'Status'
-      'pencil'
-    when 'AccountWarning'
-      'warning'
-    when 'Announcement'
-      'bullhorn'
-    end
-  end
-
-  def class_for_log_icon(log)
-    case log.action
-    when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve
-      'positive'
-    when :create
-      opposite_verbs?(log) ? 'negative' : 'positive'
-    when :update, :reset_password, :disable_2fa, :memorialize, :change_email
-      'neutral'
-    when :demote, :silence, :disable, :suspend, :remove_avatar, :remove_header, :reopen
-      'negative'
-    when :destroy
-      opposite_verbs?(log) ? 'positive' : 'negative'
-    else
-      ''
-    end
-  end
-
   private
 
-  def opposite_verbs?(log)
-    %w(DomainBlock EmailDomainBlock AccountWarning).include?(log.target_type)
-  end
-
   def linkable_log_target(record)
     case record.class.name
     when 'Account'
@@ -99,7 +28,7 @@ module Admin::ActionLogsHelper
     when 'AccountWarning'
       link_to record.target_account.acct, admin_account_path(record.target_account_id)
     when 'Announcement'
-      link_to "##{record.id}", edit_admin_announcement_path(record.id)
+      link_to truncate(record.text), edit_admin_announcement_path(record.id)
     end
   end
 
@@ -118,7 +47,7 @@ module Admin::ActionLogsHelper
         I18n.t('admin.action_logs.deleted_status')
       end
     when 'Announcement'
-      "##{attributes['id']}"
+      truncate(attributes['text'])
     end
   end
 end
diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb
index 6ab92939d..ba0ca9638 100644
--- a/app/helpers/admin/filter_helper.rb
+++ b/app/helpers/admin/filter_helper.rb
@@ -10,6 +10,7 @@ module Admin::FilterHelper
     InviteFilter::KEYS,
     RelationshipFilter::KEYS,
     AnnouncementFilter::KEYS,
+    Admin::ActionLogFilter::KEYS,
   ].flatten.freeze
 
   def filter_link_to(text, link_to_params, link_class_params = link_to_params)
diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js
index 09da9efd3..f2334c254 100644
--- a/app/javascript/core/admin.js
+++ b/app/javascript/core/admin.js
@@ -32,6 +32,10 @@ delegate(document, '.media-spoiler-hide-button', 'click', () => {
   });
 });
 
+delegate(document, '.filter-subset--with-select select', 'change', ({ target }) => {
+  target.form.submit();
+});
+
 const onDomainBlockSeverityChange = (target) => {
   const rejectMediaDiv   = document.querySelector('.input.with_label.domain_block_reject_media');
   const rejectReportsDiv = document.querySelector('.input.with_label.domain_block_reject_reports');
diff --git a/app/javascript/mastodon/features/follow_requests/index.js b/app/javascript/mastodon/features/follow_requests/index.js
index bef56fab5..7078e4e6c 100644
--- a/app/javascript/mastodon/features/follow_requests/index.js
+++ b/app/javascript/mastodon/features/follow_requests/index.js
@@ -11,6 +11,7 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim';
 import AccountAuthorizeContainer from './containers/account_authorize_container';
 import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts';
 import ScrollableList from '../../components/scrollable_list';
+import { me } from '../../initial_state';
 
 const messages = defineMessages({
   heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' },
@@ -19,6 +20,8 @@ const messages = defineMessages({
 const mapStateToProps = state => ({
   accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
   hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']),
+  locked: !!state.getIn(['accounts', me, 'locked']),
+  domain: state.getIn(['meta', 'domain']),
 });
 
 export default @connect(mapStateToProps)
@@ -31,6 +34,8 @@ class FollowRequests extends ImmutablePureComponent {
     shouldUpdateScroll: PropTypes.func,
     hasMore: PropTypes.bool,
     accountIds: ImmutablePropTypes.list,
+    locked: PropTypes.bool,
+    domain: PropTypes.string,
     intl: PropTypes.object.isRequired,
     multiColumn: PropTypes.bool,
   };
@@ -44,7 +49,7 @@ class FollowRequests extends ImmutablePureComponent {
   }, 300, { leading: true });
 
   render () {
-    const { intl, shouldUpdateScroll, accountIds, hasMore, multiColumn } = this.props;
+    const { intl, shouldUpdateScroll, accountIds, hasMore, multiColumn, locked, domain } = this.props;
 
     if (!accountIds) {
       return (
@@ -55,6 +60,15 @@ class FollowRequests extends ImmutablePureComponent {
     }
 
     const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />;
+    const unlockedPrependMessage = locked ? null : (
+      <div className='follow_requests-unlocked_explanation'>
+        <FormattedMessage
+          id='follow_requests.unlocked_explanation'
+          defaultMessage='Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.'
+          values={{ domain: domain }}
+        />
+      </div>
+    );
 
     return (
       <Column bindToDocument={!multiColumn} icon='user-plus' heading={intl.formatMessage(messages.heading)}>
@@ -66,6 +80,7 @@ class FollowRequests extends ImmutablePureComponent {
           shouldUpdateScroll={shouldUpdateScroll}
           emptyMessage={emptyMessage}
           bindToDocument={!multiColumn}
+          prepend={unlockedPrependMessage}
         >
           {accountIds.map(id =>
             <AccountAuthorizeContainer key={id} id={id} />,
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 225b7ade2..993347273 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -1532,6 +1532,10 @@
       {
         "defaultMessage": "You don't have any follow requests yet. When you receive one, it will show up here.",
         "id": "empty_column.follow_requests"
+      },
+      {
+        "defaultMessage": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
+        "id": "follow_requests.unlocked_explanation"
       }
     ],
     "path": "app/javascript/mastodon/features/follow_requests/index.json"
@@ -2961,4 +2965,4 @@
     ],
     "path": "app/javascript/mastodon/features/video/index.json"
   }
-]
\ No newline at end of file
+]
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 703bbcaac..c7153b7b1 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -168,6 +168,7 @@
   "errors.unexpected_crash.report_issue": "Report issue",
   "follow_request.authorize": "Authorize",
   "follow_request.reject": "Reject",
+  "follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
   "getting_started.developers": "Developers",
   "getting_started.directory": "Profile directory",
   "getting_started.documentation": "Documentation",
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index fb136d1a3..7bff2daa1 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -418,6 +418,11 @@ body,
       }
     }
 
+    &--with-select strong {
+      display: block;
+      margin-bottom: 10px;
+    }
+
     a {
       display: inline-block;
       color: $darker-text-color;
@@ -583,19 +588,22 @@ body,
 }
 
 .log-entry {
-  margin-bottom: 20px;
   line-height: 20px;
+  padding: 15px 0;
+  background: $ui-base-color;
+  border-bottom: 1px solid lighten($ui-base-color, 4%);
+
+  &:last-child {
+    border-bottom: 0;
+  }
 
   &__header {
     display: flex;
     justify-content: flex-start;
     align-items: center;
-    padding: 10px;
-    background: $ui-base-color;
     color: $darker-text-color;
-    border-radius: 4px 4px 0 0;
     font-size: 14px;
-    position: relative;
+    padding: 0 10px;
   }
 
   &__avatar {
@@ -622,44 +630,6 @@ body,
     color: $dark-text-color;
   }
 
-  &__extras {
-    background: lighten($ui-base-color, 6%);
-    border-radius: 0 0 4px 4px;
-    padding: 10px;
-    color: $darker-text-color;
-    font-family: $font-monospace, monospace;
-    font-size: 12px;
-    word-wrap: break-word;
-    min-height: 20px;
-  }
-
-  &__icon {
-    font-size: 28px;
-    margin-right: 10px;
-    color: $dark-text-color;
-  }
-
-  &__icon__overlay {
-    position: absolute;
-    top: 10px;
-    right: 10px;
-    width: 10px;
-    height: 10px;
-    border-radius: 50%;
-
-    &.positive {
-      background: $success-green;
-    }
-
-    &.negative {
-      background: lighten($error-red, 12%);
-    }
-
-    &.neutral {
-      background: $ui-highlight-color;
-    }
-  }
-
   a,
   .username,
   .target {
@@ -667,18 +637,6 @@ body,
     text-decoration: none;
     font-weight: 500;
   }
-
-  .diff-old {
-    color: lighten($error-red, 12%);
-  }
-
-  .diff-neutral {
-    color: $secondary-text-color;
-  }
-
-  .diff-new {
-    color: $success-green;
-  }
 }
 
 a.name-tag,
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index aea2ae6cb..dd82b0824 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -3800,7 +3800,8 @@ a.status-card.compact:hover {
 }
 
 .empty-column-indicator,
-.error-column {
+.error-column,
+.follow_requests-unlocked_explanation {
   color: $dark-text-color;
   background: $ui-base-color;
   text-align: center;
@@ -3831,6 +3832,11 @@ a.status-card.compact:hover {
   }
 }
 
+.follow_requests-unlocked_explanation {
+  background: darken($ui-base-color, 4%);
+  contain: initial;
+}
+
 .error-column {
   flex-direction: column;
 }
diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb
new file mode 100644
index 000000000..0ba7e1609
--- /dev/null
+++ b/app/models/admin/action_log_filter.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+class Admin::ActionLogFilter
+  KEYS = %i(
+    action_type
+    account_id
+    target_account_id
+  ).freeze
+
+  ACTION_TYPE_MAP = {
+    assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze,
+    change_email_user: { target_type: 'User', action: 'change_email' }.freeze,
+    confirm_user: { target_type: 'User', action: 'confirm' }.freeze,
+    create_account_warning: { target_type: 'AccountWarning', action: 'create' }.freeze,
+    create_announcement: { target_type: 'Announcement', action: 'create' }.freeze,
+    create_custom_emoji: { target_type: 'CustomEmoji', action: 'create' }.freeze,
+    create_domain_allow: { target_type: 'DomainAllow', action: 'create' }.freeze,
+    create_domain_block: { target_type: 'DomainBlock', action: 'create' }.freeze,
+    create_email_domain_block: { target_type: 'EmailDomainBlock', action: 'create' }.freeze,
+    demote_user: { target_type: 'User', action: 'demote' }.freeze,
+    destroy_announcement: { target_type: 'Announcement', action: 'destroy' }.freeze,
+    destroy_custom_emoji: { target_type: 'CustomEmoji', action: 'destroy' }.freeze,
+    destroy_domain_allow: { target_type: 'DomainAllow', action: 'destroy' }.freeze,
+    destroy_domain_block: { target_type: 'DomainBlock', action: 'destroy' }.freeze,
+    destroy_email_domain_block: { target_type: 'EmailDomainBlock', action: 'destroy' }.freeze,
+    destroy_status: { target_type: 'Status', action: 'destroy' }.freeze,
+    disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze,
+    disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze,
+    disable_user: { target_type: 'User', action: 'disable' }.freeze,
+    enable_custom_emoji: { target_type: 'CustomEmoji', action: 'enable' }.freeze,
+    enable_user: { target_type: 'User', action: 'enable' }.freeze,
+    memorialize_account: { target_type: 'Account', action: 'memorialize' }.freeze,
+    promote_user: { target_type: 'User', action: 'promote' }.freeze,
+    remove_avatar_user: { target_type: 'User', action: 'remove_avatar' }.freeze,
+    reopen_report: { target_type: 'Report', action: 'reopen' }.freeze,
+    reset_password_user: { target_type: 'User', action: 'reset_password' }.freeze,
+    resolve_report: { target_type: 'Report', action: 'resolve' }.freeze,
+    silence_account: { target_type: 'Account', action: 'silence' }.freeze,
+    suspend_account: { target_type: 'Account', action: 'suspend' }.freeze,
+    unassigned_report: { target_type: 'Report', action: 'unassigned' }.freeze,
+    unsilence_account: { target_type: 'Account', action: 'unsilence' }.freeze,
+    unsuspend_account: { target_type: 'Account', action: 'unsuspend' }.freeze,
+    update_announcement: { target_type: 'Announcement', action: 'update' }.freeze,
+    update_custom_emoji: { target_type: 'CustomEmoji', action: 'update' }.freeze,
+    update_status: { target_type: 'Status', action: 'update' }.freeze,
+  }.freeze
+
+  attr_reader :params
+
+  def initialize(params)
+    @params = params
+  end
+
+  def results
+    scope = Admin::ActionLog.includes(:target)
+
+    params.each do |key, value|
+      next if key.to_s == 'page'
+
+      scope.merge!(scope_for(key.to_s, value.to_s.strip)) if value.present?
+    end
+
+    scope
+  end
+
+  private
+
+  def scope_for(key, value)
+    case key
+    when 'action_type'
+      Admin::ActionLog.where(ACTION_TYPE_MAP[value.to_sym])
+    when 'account_id'
+      Admin::ActionLog.where(account_id: value)
+    when 'target_account_id'
+      account = Account.find(value)
+      Admin::ActionLog.where(target: [account, account.user].compact)
+    else
+      raise "Unknown filter: #{key}"
+    end
+  end
+end
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index 744d17d1f..965fd6fb6 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -53,7 +53,7 @@
       .dashboard__counters__num= number_with_delimiter @account.targeted_reports.count
       .dashboard__counters__label= t '.targeted_reports'
   %div
-    %div
+    = link_to admin_action_logs_path(target_account_id: @account.id) do
       .dashboard__counters__text
         - if @account.local? && @account.user.nil?
           %span.neutral= t('admin.accounts.deleted')
diff --git a/app/views/admin/action_logs/_action_log.html.haml b/app/views/admin/action_logs/_action_log.html.haml
index a545e189e..59905f341 100644
--- a/app/views/admin/action_logs/_action_log.html.haml
+++ b/app/views/admin/action_logs/_action_log.html.haml
@@ -7,9 +7,3 @@
         = t("admin.action_logs.actions.#{action_log.action}_#{action_log.target_type.underscore}", name: content_tag(:span, action_log.account.username, class: 'username'), target: content_tag(:span, log_target(action_log), class: 'target')).html_safe
       .log-entry__timestamp
         %time.formatted{ datetime: action_log.created_at.iso8601 }
-    .spacer
-    .log-entry__icon
-      = fa_icon icon_for_log(action_log)
-      .log-entry__icon__overlay{ class: class_for_log_icon(action_log) }
-  .log-entry__extras
-    = log_extra_attributes relevant_log_changes(action_log)
diff --git a/app/views/admin/action_logs/index.html.haml b/app/views/admin/action_logs/index.html.haml
index a4d3871a9..99f756762 100644
--- a/app/views/admin/action_logs/index.html.haml
+++ b/app/views/admin/action_logs/index.html.haml
@@ -1,6 +1,28 @@
 - content_for :page_title do
   = t('admin.action_logs.title')
 
-= render @action_logs
+- content_for :header_tags do
+  = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous'
+
+= form_tag admin_action_logs_url, method: 'GET', class: 'simple_form' do
+  = hidden_field_tag :target_account_id, params[:target_account_id] if params[:target_account_id].present?
+
+  .filters
+    .filter-subset.filter-subset--with-select
+      %strong= t('admin.action_logs.filter_by_user')
+      .input.select.optional
+        = select_tag :account_id, options_from_collection_for_select(Account.joins(:user).merge(User.staff), :id, :username, params[:account_id]), prompt: I18n.t('admin.accounts.moderation.all')
+
+    .filter-subset.filter-subset--with-select
+      %strong= t('admin.action_logs.filter_by_action')
+      .input.select.optional
+        = select_tag :action_type, options_for_select(Admin::ActionLogFilter::ACTION_TYPE_MAP.keys.map { |key| [I18n.t("admin.action_logs.action_types.#{key}"), key]}, params[:action_type]), prompt: I18n.t('admin.accounts.moderation.all')
+
+- if @action_logs.empty?
+  %div.muted-hint.center-text
+    = t 'admin.action_logs.empty'
+- else
+  .announcements-list
+    = render @action_logs
 
 = paginate @action_logs
diff --git a/config/initializers/kaminari_config.rb b/config/initializers/kaminari_config.rb
index aa1517256..4fec4320c 100644
--- a/config/initializers/kaminari_config.rb
+++ b/config/initializers/kaminari_config.rb
@@ -2,6 +2,6 @@
 
 Kaminari.configure do |config|
   config.default_per_page = 40
-  config.window = 1
+  config.window = 2
   config.outer_window = 1
 end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 4d4308560..5a4302606 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -195,6 +195,42 @@ en:
       web: Web
       whitelisted: Whitelisted
     action_logs:
+      action_types:
+        assigned_to_self_report: Assign Report
+        change_email_user: Change E-mail for User
+        confirm_user: Confirm User
+        create_account_warning: Create Warning
+        create_announcement: Create Announcement
+        create_custom_emoji: Create Custom Emoji
+        create_domain_allow: Create Domain Allow
+        create_domain_block: Create Domain Block
+        create_email_domain_block: Create E-mail Domain Block
+        demote_user: Demote User
+        destroy_announcement: Delete Announcement
+        destroy_custom_emoji: Delete Custom Emoji
+        destroy_domain_allow: Delete Domain Allow
+        destroy_domain_block: Delete Domain Block
+        destroy_email_domain_block: Delete e-mail domain block
+        destroy_status: Delete Status
+        disable_2fa_user: Disable 2FA
+        disable_custom_emoji: Disable Custom Emoji
+        disable_user: Disable User
+        enable_custom_emoji: Enable Custom Emoji
+        enable_user: Enable User
+        memorialize_account: Memorialize Account
+        promote_user: Promote User
+        remove_avatar_user: Remove Avatar
+        reopen_report: Reopen Report
+        reset_password_user: Reset Password
+        resolve_report: Resolve Report
+        silence_account: Silence Account
+        suspend_account: Suspend Account
+        unassigned_report: Unassign Report
+        unsilence_account: Unsilence Account
+        unsuspend_account: Unsuspend Account
+        update_announcement: Update Announcement
+        update_custom_emoji: Update Custom Emoji
+        update_status: Update Status
       actions:
         assigned_to_self_report: "%{name} assigned report %{target} to themselves"
         change_email_user: "%{name} changed the e-mail address of user %{target}"
@@ -232,6 +268,9 @@ en:
         update_custom_emoji: "%{name} updated emoji %{target}"
         update_status: "%{name} updated status by %{target}"
       deleted_status: "(deleted status)"
+      empty: No logs found.
+      filter_by_action: Filter by action
+      filter_by_user: Filter by user
       title: Audit log
     announcements:
       destroyed_msg: Announcement successfully deleted!
diff --git a/spec/helpers/admin/action_log_helper_spec.rb b/spec/helpers/admin/action_log_helper_spec.rb
index d7af6b939..60f5ecdcc 100644
--- a/spec/helpers/admin/action_log_helper_spec.rb
+++ b/spec/helpers/admin/action_log_helper_spec.rb
@@ -31,242 +31,4 @@ RSpec.describe Admin::ActionLogsHelper, type: :helper do
       end
     end
   end
-
-  describe '#relevant_log_changes' do
-    let(:log) { double(target_type: target_type, action: log_action, recorded_changes: recorded_changes) }
-    let(:recorded_changes) { double }
-
-    after do
-      hoge.relevant_log_changes(log)
-    end
-
-    context "log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)" do
-      let(:target_type) { 'CustomEmoji' }
-      let(:log_action)  { :enable }
-
-      it "calls log.recorded_changes.slice('domain')" do
-        expect(recorded_changes).to receive(:slice).with('domain')
-      end
-    end
-
-    context "log.target_type == 'CustomEmoji' && log.action == :update" do
-      let(:target_type) { 'CustomEmoji' }
-      let(:log_action)  { :update }
-
-      it "calls log.recorded_changes.slice('domain', 'visible_in_picker')" do
-        expect(recorded_changes).to receive(:slice).with('domain', 'visible_in_picker')
-      end
-    end
-
-    context "log.target_type == 'User' && [:promote, :demote].include?(log.action)" do
-      let(:target_type) { 'User' }
-      let(:log_action)  { :promote }
-
-      it "calls log.recorded_changes.slice('moderator', 'admin')" do
-        expect(recorded_changes).to receive(:slice).with('moderator', 'admin')
-      end
-    end
-
-    context "log.target_type == 'User' && [:change_email].include?(log.action)" do
-      let(:target_type) { 'User' }
-      let(:log_action)  { :change_email }
-
-      it "calls log.recorded_changes.slice('email', 'unconfirmed_email')" do
-        expect(recorded_changes).to receive(:slice).with('email', 'unconfirmed_email')
-      end
-    end
-
-    context "log.target_type == 'DomainBlock'" do
-      let(:target_type) { 'DomainBlock' }
-      let(:log_action)  { nil }
-
-      it "calls log.recorded_changes.slice('severity', 'reject_media')" do
-        expect(recorded_changes).to receive(:slice).with('severity', 'reject_media')
-      end
-    end
-
-    context "log.target_type == 'Status' && log.action == :update" do
-      let(:target_type) { 'Status' }
-      let(:log_action)  { :update }
-
-      it "log.recorded_changes.slice('sensitive')" do
-        expect(recorded_changes).to receive(:slice).with('sensitive')
-      end
-    end
-  end
-
-  describe '#log_extra_attributes' do
-    after do
-      hoge.log_extra_attributes(hoge: 'hoge')
-    end
-
-    it "calls content_tag(:span, key, class: 'diff-key')" do
-      allow(hoge).to receive(:log_change).with(anything)
-      expect(hoge).to receive(:content_tag).with(:span, :hoge, class: 'diff-key')
-    end
-
-    it 'calls safe_join twice' do
-      expect(hoge).to receive(:safe_join).with(
-        ['<span class="diff-key">hoge</span>',
-         '=',
-         '<span class="diff-neutral">hoge</span>']
-      )
-
-      expect(hoge).to receive(:safe_join).with([nil], ' ')
-    end
-  end
-
-  describe '#log_change' do
-    after do
-      hoge.log_change(val)
-    end
-
-    context '!val.is_a?(Array)' do
-      let(:val) { 'hoge' }
-
-      it "calls content_tag(:span, val, class: 'diff-neutral')" do
-        expect(hoge).to receive(:content_tag).with(:span, val, class: 'diff-neutral')
-      end
-    end
-
-    context 'val.is_a?(Array)' do
-      let(:val) { %w(foo bar) }
-
-      it 'calls #content_tag twice and #safe_join' do
-        expect(hoge).to receive(:content_tag).with(:span, 'foo', class: 'diff-old')
-        expect(hoge).to receive(:content_tag).with(:span, 'bar', class: 'diff-new')
-        expect(hoge).to receive(:safe_join).with([nil, nil], '→')
-      end
-    end
-  end
-
-  describe '#icon_for_log' do
-    subject   { hoge.icon_for_log(log) }
-
-    context "log.target_type == 'Account'" do
-      let(:log) { double(target_type: 'Account') }
-
-      it 'returns "user"' do
-        expect(subject).to be 'user'
-      end
-    end
-
-    context "log.target_type == 'User'" do
-      let(:log) { double(target_type: 'User') }
-
-      it 'returns "user"' do
-        expect(subject).to be 'user'
-      end
-    end
-
-    context "log.target_type == 'CustomEmoji'" do
-      let(:log) { double(target_type: 'CustomEmoji') }
-
-      it 'returns "file"' do
-        expect(subject).to be 'file'
-      end
-    end
-
-    context "log.target_type == 'Report'" do
-      let(:log) { double(target_type: 'Report') }
-
-      it 'returns "flag"' do
-        expect(subject).to be 'flag'
-      end
-    end
-
-    context "log.target_type == 'DomainBlock'" do
-      let(:log) { double(target_type: 'DomainBlock') }
-
-      it 'returns "lock"' do
-        expect(subject).to be 'lock'
-      end
-    end
-
-    context "log.target_type == 'EmailDomainBlock'" do
-      let(:log) { double(target_type: 'EmailDomainBlock') }
-
-      it 'returns "envelope"' do
-        expect(subject).to be 'envelope'
-      end
-    end
-
-    context "log.target_type == 'Status'" do
-      let(:log) { double(target_type: 'Status') }
-
-      it 'returns "pencil"' do
-        expect(subject).to be 'pencil'
-      end
-    end
-  end
-
-  describe '#class_for_log_icon' do
-    subject   { hoge.class_for_log_icon(log) }
-
-    %i(enable unsuspend unsilence confirm promote resolve).each do |action|
-      context "log.action == #{action}" do
-        let(:log) { double(action: action) }
-
-        it 'returns "positive"' do
-          expect(subject).to be 'positive'
-        end
-      end
-    end
-
-    context 'log.action == :create' do
-      context 'opposite_verbs?(log)' do
-        let(:log) { double(action: :create, target_type: 'DomainBlock') }
-
-        it 'returns "negative"' do
-          expect(subject).to be 'negative'
-        end
-      end
-
-      context '!opposite_verbs?(log)' do
-        let(:log) { double(action: :create, target_type: '') }
-
-        it 'returns "positive"' do
-          expect(subject).to be 'positive'
-        end
-      end
-    end
-
-    %i(update reset_password disable_2fa memorialize change_email).each do |action|
-      context "log.action == #{action}" do
-        let(:log) { double(action: action) }
-
-        it 'returns "neutral"' do
-          expect(subject).to be 'neutral'
-        end
-      end
-    end
-
-    %i(demote silence disable suspend remove_avatar remove_header reopen).each do |action|
-      context "log.action == #{action}" do
-        let(:log) { double(action: action) }
-
-        it 'returns "negative"' do
-          expect(subject).to be 'negative'
-        end
-      end
-    end
-
-    context 'log.action == :destroy' do
-      context 'opposite_verbs?(log)' do
-        let(:log) { double(action: :destroy, target_type: 'DomainBlock') }
-
-        it 'returns "positive"' do
-          expect(subject).to be 'positive'
-        end
-      end
-
-      context '!opposite_verbs?(log)' do
-        let(:log) { double(action: :destroy, target_type: '') }
-
-        it 'returns "negative"' do
-          expect(subject).to be 'negative'
-        end
-      end
-    end
-  end
 end