about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2018-08-09 09:56:53 +0200
committerGitHub <noreply@github.com>2018-08-09 09:56:53 +0200
commitf2404de871f0bdfda5c9aeeeb4c6c4d10a8da8ab (patch)
treef45577d1fe26ea56f7323b940f506f8875d7344e
parent80176a3814abad7f5c9023f97b5d4d82b73c089d (diff)
Public profile endorsements (accounts picked by profile owner) (#8146)
-rw-r--r--app/controllers/accounts_controller.rb5
-rw-r--r--app/controllers/api/v1/accounts/pins_controller.rb32
-rw-r--r--app/helpers/home_helper.rb32
-rw-r--r--app/javascript/mastodon/actions/accounts.js74
-rw-r--r--app/javascript/mastodon/features/account/components/action_bar.js6
-rw-r--r--app/javascript/mastodon/features/account_timeline/components/header.js6
-rw-r--r--app/javascript/mastodon/features/account_timeline/containers/header_container.js10
-rw-r--r--app/javascript/mastodon/reducers/relationships.js4
-rw-r--r--app/javascript/styles/mastodon/widgets.scss32
-rw-r--r--app/models/account.rb4
-rw-r--r--app/models/account_pin.rb26
-rw-r--r--app/models/concerns/account_interactions.rb8
-rw-r--r--app/presenters/account_relationships_presenter.rb7
-rw-r--r--app/serializers/rest/relationship_serializer.rb7
-rw-r--r--app/views/accounts/show.html.haml8
-rw-r--r--config/locales/en.yml3
-rw-r--r--config/routes.rb3
-rw-r--r--db/migrate/20180808175627_create_account_pins.rb12
-rw-r--r--db/schema.rb16
-rw-r--r--spec/fabricators/account_pin_fabricator.rb4
-rw-r--r--spec/models/account_pin_spec.rb5
21 files changed, 298 insertions, 6 deletions
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 86e3b6e47..e5a7301ee 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -10,8 +10,9 @@ class AccountsController < ApplicationController
   def show
     respond_to do |format|
       format.html do
-        @body_classes    = 'with-modals'
-        @pinned_statuses = []
+        @body_classes      = 'with-modals'
+        @pinned_statuses   = []
+        @endorsed_accounts = @account.endorsed_accounts.to_a.sample(4)
 
         if current_account && @account.blocking?(current_account)
           @statuses = []
diff --git a/app/controllers/api/v1/accounts/pins_controller.rb b/app/controllers/api/v1/accounts/pins_controller.rb
new file mode 100644
index 000000000..0a0239c42
--- /dev/null
+++ b/app/controllers/api/v1/accounts/pins_controller.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class Api::V1::Accounts::PinsController < Api::BaseController
+  include Authorization
+
+  before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
+  before_action :require_user!
+  before_action :set_account
+
+  respond_to :json
+
+  def create
+    AccountPin.create!(account: current_account, target_account: @account)
+    render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
+  end
+
+  def destroy
+    pin = AccountPin.find_by(account: current_account, target_account: @account)
+    pin&.destroy!
+    render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
+  end
+
+  private
+
+  def set_account
+    @account = Account.find(params[:account_id])
+  end
+
+  def relationships_presenter
+    AccountRelationshipsPresenter.new([@account.id], current_user.account_id)
+  end
+end
diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb
index d3c6b13a6..8449f6c8a 100644
--- a/app/helpers/home_helper.rb
+++ b/app/helpers/home_helper.rb
@@ -6,4 +6,36 @@ module HomeHelper
       locale: I18n.locale,
     }
   end
+
+  def account_link_to(account, button = '')
+    content_tag(:div, class: 'account') do
+      content_tag(:div, class: 'account__wrapper') do
+        section = if account.nil?
+                    content_tag(:div, class: 'account__display-name') do
+                      content_tag(:div, class: 'account__avatar-wrapper') do
+                        content_tag(:div, '', class: 'account__avatar', style: "background-image: url(#{full_asset_url('avatars/original/missing.png', skip_pipeline: true)})")
+                      end +
+                        content_tag(:span, class: 'display-name') do
+                          content_tag(:strong, t('about.contact_missing')) +
+                            content_tag(:span, t('about.contact_unavailable'), class: 'display-name__account')
+                        end
+                    end
+                  else
+                    link_to(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: "background-image: url(#{account.avatar.url})")
+                      end +
+                        content_tag(:span, class: 'display-name') do
+                          content_tag(:bdi) do
+                            content_tag(:strong, display_name(account, custom_emojify: true), class: 'display-name__html emojify')
+                          end +
+                            content_tag(:span, "@#{account.acct}", class: 'display-name__account')
+                        end
+                    end
+                  end
+
+        section + button
+      end
+    end
+  end
 end
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index c9e4afcfc..cbae62a0f 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -30,6 +30,14 @@ export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST';
 export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS';
 export const ACCOUNT_UNMUTE_FAIL    = 'ACCOUNT_UNMUTE_FAIL';
 
+export const ACCOUNT_PIN_REQUEST = 'ACCOUNT_PIN_REQUEST';
+export const ACCOUNT_PIN_SUCCESS = 'ACCOUNT_PIN_SUCCESS';
+export const ACCOUNT_PIN_FAIL    = 'ACCOUNT_PIN_FAIL';
+
+export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST';
+export const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS';
+export const ACCOUNT_UNPIN_FAIL    = 'ACCOUNT_UNPIN_FAIL';
+
 export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST';
 export const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS';
 export const FOLLOWERS_FETCH_FAIL    = 'FOLLOWERS_FETCH_FAIL';
@@ -694,3 +702,69 @@ export function rejectFollowRequestFail(id, error) {
     error,
   };
 };
+
+export function pinAccount(id) {
+  return (dispatch, getState) => {
+    dispatch(pinAccountRequest(id));
+
+    api(getState).post(`/api/v1/accounts/${id}/pin`).then(response => {
+      dispatch(pinAccountSuccess(response.data));
+    }).catch(error => {
+      dispatch(pinAccountFail(error));
+    });
+  };
+};
+
+export function unpinAccount(id) {
+  return (dispatch, getState) => {
+    dispatch(unpinAccountRequest(id));
+
+    api(getState).post(`/api/v1/accounts/${id}/unpin`).then(response => {
+      dispatch(unpinAccountSuccess(response.data));
+    }).catch(error => {
+      dispatch(unpinAccountFail(error));
+    });
+  };
+};
+
+export function pinAccountRequest(id) {
+  return {
+    type: ACCOUNT_PIN_REQUEST,
+    id,
+  };
+};
+
+export function pinAccountSuccess(relationship) {
+  return {
+    type: ACCOUNT_PIN_SUCCESS,
+    relationship,
+  };
+};
+
+export function pinAccountFail(error) {
+  return {
+    type: ACCOUNT_PIN_FAIL,
+    error,
+  };
+};
+
+export function unpinAccountRequest(id) {
+  return {
+    type: ACCOUNT_UNPIN_REQUEST,
+    id,
+  };
+};
+
+export function unpinAccountSuccess(relationship) {
+  return {
+    type: ACCOUNT_UNPIN_SUCCESS,
+    relationship,
+  };
+};
+
+export function unpinAccountFail(error) {
+  return {
+    type: ACCOUNT_UNPIN_FAIL,
+    error,
+  };
+};
diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js
index e3f2d0f55..43b4811e1 100644
--- a/app/javascript/mastodon/features/account/components/action_bar.js
+++ b/app/javascript/mastodon/features/account/components/action_bar.js
@@ -32,6 +32,8 @@ const messages = defineMessages({
   blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
   domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
   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' },
 });
 
 @injectIntl
@@ -48,6 +50,7 @@ export default class ActionBar extends React.PureComponent {
     onMute: PropTypes.func.isRequired,
     onBlockDomain: PropTypes.func.isRequired,
     onUnblockDomain: PropTypes.func.isRequired,
+    onEndorseToggle: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
   };
 
@@ -93,6 +96,9 @@ export default class ActionBar extends React.PureComponent {
         } else {
           menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
         }
+
+        menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
+        menu.push(null);
       }
 
       if (account.getIn(['relationship', 'muting'])) {
diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js
index 1ae5126e6..ab29e4bdf 100644
--- a/app/javascript/mastodon/features/account_timeline/components/header.js
+++ b/app/javascript/mastodon/features/account_timeline/components/header.js
@@ -22,6 +22,7 @@ export default class Header extends ImmutablePureComponent {
     onMute: PropTypes.func.isRequired,
     onBlockDomain: PropTypes.func.isRequired,
     onUnblockDomain: PropTypes.func.isRequired,
+    onEndorseToggle: PropTypes.func.isRequired,
     hideTabs: PropTypes.bool,
   };
 
@@ -73,6 +74,10 @@ export default class Header extends ImmutablePureComponent {
     this.props.onUnblockDomain(domain);
   }
 
+  handleEndorseToggle = () => {
+    this.props.onEndorseToggle(this.props.account);
+  }
+
   render () {
     const { account, hideTabs } = this.props;
 
@@ -100,6 +105,7 @@ export default class Header extends ImmutablePureComponent {
           onMute={this.handleMute}
           onBlockDomain={this.handleBlockDomain}
           onUnblockDomain={this.handleUnblockDomain}
+          onEndorseToggle={this.handleEndorseToggle}
         />
 
         {!hideTabs && (
diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
index 7681430b7..02803893d 100644
--- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js
+++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
@@ -8,6 +8,8 @@ import {
   blockAccount,
   unblockAccount,
   unmuteAccount,
+  pinAccount,
+  unpinAccount,
 } from '../../../actions/accounts';
 import {
   mentionCompose,
@@ -82,6 +84,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     }
   },
 
+  onEndorseToggle (account) {
+    if (account.getIn(['relationship', 'endorsed'])) {
+      dispatch(unpinAccount(account.get('id')));
+    } else {
+      dispatch(pinAccount(account.get('id')));
+    }
+  },
+
   onReport (account) {
     dispatch(initReport(account));
   },
diff --git a/app/javascript/mastodon/reducers/relationships.js b/app/javascript/mastodon/reducers/relationships.js
index d1caabc1c..f46049297 100644
--- a/app/javascript/mastodon/reducers/relationships.js
+++ b/app/javascript/mastodon/reducers/relationships.js
@@ -5,6 +5,8 @@ import {
   ACCOUNT_UNBLOCK_SUCCESS,
   ACCOUNT_MUTE_SUCCESS,
   ACCOUNT_UNMUTE_SUCCESS,
+  ACCOUNT_PIN_SUCCESS,
+  ACCOUNT_UNPIN_SUCCESS,
   RELATIONSHIPS_FETCH_SUCCESS,
 } from '../actions/accounts';
 import {
@@ -41,6 +43,8 @@ export default function relationships(state = initialState, action) {
   case ACCOUNT_UNBLOCK_SUCCESS:
   case ACCOUNT_MUTE_SUCCESS:
   case ACCOUNT_UNMUTE_SUCCESS:
+  case ACCOUNT_PIN_SUCCESS:
+  case ACCOUNT_UNPIN_SUCCESS:
     return normalizeRelationship(state, action.relationship);
   case RELATIONSHIPS_FETCH_SUCCESS:
     return normalizeRelationships(state, action.relationships);
diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss
index d37a6f458..b05bbbda7 100644
--- a/app/javascript/styles/mastodon/widgets.scss
+++ b/app/javascript/styles/mastodon/widgets.scss
@@ -71,6 +71,38 @@
   }
 }
 
+.endorsements-widget {
+  margin-bottom: 10px;
+  padding-bottom: 10px;
+
+  h4 {
+    padding: 10px;
+    text-transform: uppercase;
+    font-weight: 700;
+    font-size: 13px;
+    color: $darker-text-color;
+  }
+
+  .account {
+    padding: 10px 0;
+
+    &:last-child {
+      border-bottom: 0;
+    }
+
+    .account__display-name {
+      display: flex;
+      align-items: center;
+    }
+
+    .account__avatar {
+      width: 44px;
+      height: 44px;
+      background-size: 44px 44px;
+    }
+  }
+}
+
 .moved-account-widget {
   padding: 15px;
   padding-bottom: 20px;
diff --git a/app/models/account.rb b/app/models/account.rb
index 0272b4615..c33ec4bd5 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -89,6 +89,10 @@ class Account < ApplicationRecord
   has_many :status_pins, inverse_of: :account, dependent: :destroy
   has_many :pinned_statuses, -> { reorder('status_pins.created_at DESC') }, through: :status_pins, class_name: 'Status', source: :status
 
+  # Endorsements
+  has_many :account_pins, inverse_of: :account, dependent: :destroy
+  has_many :endorsed_accounts, through: :account_pins, class_name: 'Account', source: :target_account
+
   # Media
   has_many :media_attachments, dependent: :destroy
 
diff --git a/app/models/account_pin.rb b/app/models/account_pin.rb
new file mode 100644
index 000000000..9a21c3405
--- /dev/null
+++ b/app/models/account_pin.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: account_pins
+#
+#  id                :bigint(8)        not null, primary key
+#  account_id        :bigint(8)
+#  target_account_id :bigint(8)
+#  created_at        :datetime         not null
+#  updated_at        :datetime         not null
+#
+
+class AccountPin < ApplicationRecord
+  include RelationshipCacheable
+
+  belongs_to :account
+  belongs_to :target_account, class_name: 'Account'
+
+  validate :validate_follow_relationship
+
+  private
+
+  def validate_follow_relationship
+    errors.add(:base, I18n.t('accounts.pin_errors.following')) unless account.following?(target_account)
+  end
+end
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index e14e041f6..f5f833446 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -40,6 +40,10 @@ module AccountInteractions
       end
     end
 
+    def endorsed_map(target_account_ids, account_id)
+      follow_mapping(AccountPin.where(account_id: account_id, target_account_id: target_account_ids), :target_account_id)
+    end
+
     def domain_blocking_map(target_account_ids, account_id)
       accounts_map    = Account.where(id: target_account_ids).select('id, domain').map { |a| [a.id, a.domain] }.to_h
       blocked_domains = domain_blocking_map_by_domain(accounts_map.values.compact, account_id)
@@ -190,6 +194,10 @@ module AccountInteractions
     status_pins.where(status: status).exists?
   end
 
+  def endorsed?(account)
+    account_pins.where(target_account: account).exists?
+  end
+
   def followers_for_local_distribution
     followers.local
              .joins(:user)
diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb
index b1e99b31b..e4aaa65f6 100644
--- a/app/presenters/account_relationships_presenter.rb
+++ b/app/presenters/account_relationships_presenter.rb
@@ -2,7 +2,8 @@
 
 class AccountRelationshipsPresenter
   attr_reader :following, :followed_by, :blocking,
-              :muting, :requested, :domain_blocking
+              :muting, :requested, :domain_blocking,
+              :endorsed
 
   def initialize(account_ids, current_account_id, **options)
     @account_ids        = account_ids.map { |a| a.is_a?(Account) ? a.id : a }
@@ -14,6 +15,7 @@ class AccountRelationshipsPresenter
     @muting          = cached[:muting].merge(Account.muting_map(@uncached_account_ids, @current_account_id))
     @requested       = cached[:requested].merge(Account.requested_map(@uncached_account_ids, @current_account_id))
     @domain_blocking = cached[:domain_blocking].merge(Account.domain_blocking_map(@uncached_account_ids, @current_account_id))
+    @endorsed        = cached[:endorsed].merge(Account.endorsed_map(@uncached_account_ids, @current_account_id))
 
     cache_uncached!
 
@@ -23,6 +25,7 @@ class AccountRelationshipsPresenter
     @muting.merge!(options[:muting_map] || {})
     @requested.merge!(options[:requested_map] || {})
     @domain_blocking.merge!(options[:domain_blocking_map] || {})
+    @endorsed.merge!(options[:endorsed_map] || {})
   end
 
   private
@@ -37,6 +40,7 @@ class AccountRelationshipsPresenter
       muting: {},
       requested: {},
       domain_blocking: {},
+      endorsed: {},
     }
 
     @uncached_account_ids = []
@@ -63,6 +67,7 @@ class AccountRelationshipsPresenter
         muting:          { account_id => muting[account_id] },
         requested:       { account_id => requested[account_id] },
         domain_blocking: { account_id => domain_blocking[account_id] },
+        endorsed:        { account_id => endorsed[account_id] },
       }
 
       Rails.cache.write("relationship:#{@current_account_id}:#{account_id}", maps_for_account, expires_in: 1.day)
diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb
index 45bfd4d6e..c6c722a54 100644
--- a/app/serializers/rest/relationship_serializer.rb
+++ b/app/serializers/rest/relationship_serializer.rb
@@ -2,7 +2,8 @@
 
 class REST::RelationshipSerializer < ActiveModel::Serializer
   attributes :id, :following, :showing_reblogs, :followed_by, :blocking,
-             :muting, :muting_notifications, :requested, :domain_blocking
+             :muting, :muting_notifications, :requested, :domain_blocking,
+             :endorsed
 
   def id
     object.id.to_s
@@ -41,4 +42,8 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
   def domain_blocking
     instance_options[:relationships].domain_blocking[object.id] || false
   end
+
+  def endorsed
+    instance_options[:relationships].endorsed[object.id] || false
+  end
 end
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index b30755d94..e398fc29b 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -55,4 +55,12 @@
       = render 'moved', account: @account
 
     = render 'bio', account: @account
+
+    - unless @endorsed_accounts.empty?
+      .endorsements-widget
+        %h4= t 'accounts.choices_html', name: content_tag(:bdi, display_name(@account, custom_emojify: true))
+
+        - @endorsed_accounts.each do |account|
+          = account_link_to account
+
     = render 'application/sidebar'
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 4693fe1ba..ef5b2c93b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -39,6 +39,7 @@ en:
     user_count_before: Home to
     what_is_mastodon: What is Mastodon?
   accounts:
+    choices_html: "%{name}'s choices:"
     follow: Follow
     followers: Followers
     following: Following
@@ -49,6 +50,8 @@ en:
     nothing_here: There is nothing here!
     people_followed_by: People whom %{name} follows
     people_who_follow: People who follow %{name}
+    pin_errors:
+      following: You must be already following the person you want to endorse
     posts: Toots
     posts_with_replies: Toots and replies
     reserved_username: The username is reserved
diff --git a/config/routes.rb b/config/routes.rb
index 1c97f5a82..2983011d2 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -309,6 +309,9 @@ Rails.application.routes.draw do
           post :mute
           post :unmute
         end
+
+        resource :pin, only: :create, controller: 'accounts/pins'
+        post :unpin, to: 'accounts/pins#destroy'
       end
 
       resources :lists, only: [:index, :create, :show, :update, :destroy] do
diff --git a/db/migrate/20180808175627_create_account_pins.rb b/db/migrate/20180808175627_create_account_pins.rb
new file mode 100644
index 000000000..43d8185be
--- /dev/null
+++ b/db/migrate/20180808175627_create_account_pins.rb
@@ -0,0 +1,12 @@
+class CreateAccountPins < ActiveRecord::Migration[5.2]
+  def change
+    create_table :account_pins do |t|
+      t.belongs_to :account, foreign_key: { on_delete: :cascade }
+      t.belongs_to :target_account, foreign_key: { on_delete: :cascade, to_table: :accounts }
+
+      t.timestamps
+    end
+
+    add_index :account_pins, [:account_id, :target_account_id], unique: true
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e0da669c4..46ee42714 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2018_07_11_152640) do
+ActiveRecord::Schema.define(version: 2018_08_08_175627) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -33,6 +33,16 @@ ActiveRecord::Schema.define(version: 2018_07_11_152640) do
     t.index ["target_account_id"], name: "index_account_moderation_notes_on_target_account_id"
   end
 
+  create_table "account_pins", force: :cascade do |t|
+    t.bigint "account_id"
+    t.bigint "target_account_id"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["account_id", "target_account_id"], name: "index_account_pins_on_account_id_and_target_account_id", unique: true
+    t.index ["account_id"], name: "index_account_pins_on_account_id"
+    t.index ["target_account_id"], name: "index_account_pins_on_target_account_id"
+  end
+
   create_table "accounts", force: :cascade do |t|
     t.string "username", default: "", null: false
     t.string "domain"
@@ -149,9 +159,9 @@ ActiveRecord::Schema.define(version: 2018_07_11_152640) do
     t.text "phrase", default: "", null: false
     t.string "context", default: [], null: false, array: true
     t.boolean "irreversible", default: false, null: false
-    t.boolean "whole_word", default: true, null: false
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
+    t.boolean "whole_word", default: true, null: false
     t.index ["account_id"], name: "index_custom_filters_on_account_id"
   end
 
@@ -575,6 +585,8 @@ ActiveRecord::Schema.define(version: 2018_07_11_152640) do
   add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade
   add_foreign_key "account_moderation_notes", "accounts"
   add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id"
+  add_foreign_key "account_pins", "accounts", column: "target_account_id", on_delete: :cascade
+  add_foreign_key "account_pins", "accounts", on_delete: :cascade
   add_foreign_key "accounts", "accounts", column: "moved_to_account_id", on_delete: :nullify
   add_foreign_key "admin_action_logs", "accounts", on_delete: :cascade
   add_foreign_key "backups", "users", on_delete: :nullify
diff --git a/spec/fabricators/account_pin_fabricator.rb b/spec/fabricators/account_pin_fabricator.rb
new file mode 100644
index 000000000..c0f8b8afb
--- /dev/null
+++ b/spec/fabricators/account_pin_fabricator.rb
@@ -0,0 +1,4 @@
+Fabricator(:account_pin) do
+  account        nil
+  target_account nil
+end
diff --git a/spec/models/account_pin_spec.rb b/spec/models/account_pin_spec.rb
new file mode 100644
index 000000000..4f226b127
--- /dev/null
+++ b/spec/models/account_pin_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe AccountPin, type: :model do
+  pending "add some examples to (or delete) #{__FILE__}"
+end