Public profile endorsements (accounts picked by profile owner) (#8146)
This commit is contained in:
		@@ -10,8 +10,9 @@ class AccountsController < ApplicationController
 | 
				
			|||||||
  def show
 | 
					  def show
 | 
				
			||||||
    respond_to do |format|
 | 
					    respond_to do |format|
 | 
				
			||||||
      format.html do
 | 
					      format.html do
 | 
				
			||||||
        @body_classes    = 'with-modals'
 | 
					        @body_classes      = 'with-modals'
 | 
				
			||||||
        @pinned_statuses = []
 | 
					        @pinned_statuses   = []
 | 
				
			||||||
 | 
					        @endorsed_accounts = @account.endorsed_accounts.to_a.sample(4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if current_account && @account.blocking?(current_account)
 | 
					        if current_account && @account.blocking?(current_account)
 | 
				
			||||||
          @statuses = []
 | 
					          @statuses = []
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										32
									
								
								app/controllers/api/v1/accounts/pins_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/controllers/api/v1/accounts/pins_controller.rb
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
@@ -6,4 +6,36 @@ module HomeHelper
 | 
				
			|||||||
      locale: I18n.locale,
 | 
					      locale: I18n.locale,
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  end
 | 
					  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
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,6 +30,14 @@ export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST';
 | 
				
			|||||||
export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS';
 | 
					export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS';
 | 
				
			||||||
export const ACCOUNT_UNMUTE_FAIL    = 'ACCOUNT_UNMUTE_FAIL';
 | 
					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_REQUEST = 'FOLLOWERS_FETCH_REQUEST';
 | 
				
			||||||
export const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS';
 | 
					export const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS';
 | 
				
			||||||
export const FOLLOWERS_FETCH_FAIL    = 'FOLLOWERS_FETCH_FAIL';
 | 
					export const FOLLOWERS_FETCH_FAIL    = 'FOLLOWERS_FETCH_FAIL';
 | 
				
			||||||
@@ -694,3 +702,69 @@ export function rejectFollowRequestFail(id, error) {
 | 
				
			|||||||
    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,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,6 +32,8 @@ const messages = defineMessages({
 | 
				
			|||||||
  blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
 | 
					  blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
 | 
				
			||||||
  domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
 | 
					  domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
 | 
				
			||||||
  mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
 | 
					  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
 | 
					@injectIntl
 | 
				
			||||||
@@ -48,6 +50,7 @@ export default class ActionBar extends React.PureComponent {
 | 
				
			|||||||
    onMute: PropTypes.func.isRequired,
 | 
					    onMute: PropTypes.func.isRequired,
 | 
				
			||||||
    onBlockDomain: PropTypes.func.isRequired,
 | 
					    onBlockDomain: PropTypes.func.isRequired,
 | 
				
			||||||
    onUnblockDomain: PropTypes.func.isRequired,
 | 
					    onUnblockDomain: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    onEndorseToggle: PropTypes.func.isRequired,
 | 
				
			||||||
    intl: PropTypes.object.isRequired,
 | 
					    intl: PropTypes.object.isRequired,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -93,6 +96,9 @@ export default class ActionBar extends React.PureComponent {
 | 
				
			|||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
 | 
					          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'])) {
 | 
					      if (account.getIn(['relationship', 'muting'])) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,6 +22,7 @@ export default class Header extends ImmutablePureComponent {
 | 
				
			|||||||
    onMute: PropTypes.func.isRequired,
 | 
					    onMute: PropTypes.func.isRequired,
 | 
				
			||||||
    onBlockDomain: PropTypes.func.isRequired,
 | 
					    onBlockDomain: PropTypes.func.isRequired,
 | 
				
			||||||
    onUnblockDomain: PropTypes.func.isRequired,
 | 
					    onUnblockDomain: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    onEndorseToggle: PropTypes.func.isRequired,
 | 
				
			||||||
    hideTabs: PropTypes.bool,
 | 
					    hideTabs: PropTypes.bool,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -73,6 +74,10 @@ export default class Header extends ImmutablePureComponent {
 | 
				
			|||||||
    this.props.onUnblockDomain(domain);
 | 
					    this.props.onUnblockDomain(domain);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleEndorseToggle = () => {
 | 
				
			||||||
 | 
					    this.props.onEndorseToggle(this.props.account);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { account, hideTabs } = this.props;
 | 
					    const { account, hideTabs } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -100,6 +105,7 @@ export default class Header extends ImmutablePureComponent {
 | 
				
			|||||||
          onMute={this.handleMute}
 | 
					          onMute={this.handleMute}
 | 
				
			||||||
          onBlockDomain={this.handleBlockDomain}
 | 
					          onBlockDomain={this.handleBlockDomain}
 | 
				
			||||||
          onUnblockDomain={this.handleUnblockDomain}
 | 
					          onUnblockDomain={this.handleUnblockDomain}
 | 
				
			||||||
 | 
					          onEndorseToggle={this.handleEndorseToggle}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {!hideTabs && (
 | 
					        {!hideTabs && (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,8 @@ import {
 | 
				
			|||||||
  blockAccount,
 | 
					  blockAccount,
 | 
				
			||||||
  unblockAccount,
 | 
					  unblockAccount,
 | 
				
			||||||
  unmuteAccount,
 | 
					  unmuteAccount,
 | 
				
			||||||
 | 
					  pinAccount,
 | 
				
			||||||
 | 
					  unpinAccount,
 | 
				
			||||||
} from '../../../actions/accounts';
 | 
					} from '../../../actions/accounts';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  mentionCompose,
 | 
					  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) {
 | 
					  onReport (account) {
 | 
				
			||||||
    dispatch(initReport(account));
 | 
					    dispatch(initReport(account));
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,8 @@ import {
 | 
				
			|||||||
  ACCOUNT_UNBLOCK_SUCCESS,
 | 
					  ACCOUNT_UNBLOCK_SUCCESS,
 | 
				
			||||||
  ACCOUNT_MUTE_SUCCESS,
 | 
					  ACCOUNT_MUTE_SUCCESS,
 | 
				
			||||||
  ACCOUNT_UNMUTE_SUCCESS,
 | 
					  ACCOUNT_UNMUTE_SUCCESS,
 | 
				
			||||||
 | 
					  ACCOUNT_PIN_SUCCESS,
 | 
				
			||||||
 | 
					  ACCOUNT_UNPIN_SUCCESS,
 | 
				
			||||||
  RELATIONSHIPS_FETCH_SUCCESS,
 | 
					  RELATIONSHIPS_FETCH_SUCCESS,
 | 
				
			||||||
} from '../actions/accounts';
 | 
					} from '../actions/accounts';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@@ -41,6 +43,8 @@ export default function relationships(state = initialState, action) {
 | 
				
			|||||||
  case ACCOUNT_UNBLOCK_SUCCESS:
 | 
					  case ACCOUNT_UNBLOCK_SUCCESS:
 | 
				
			||||||
  case ACCOUNT_MUTE_SUCCESS:
 | 
					  case ACCOUNT_MUTE_SUCCESS:
 | 
				
			||||||
  case ACCOUNT_UNMUTE_SUCCESS:
 | 
					  case ACCOUNT_UNMUTE_SUCCESS:
 | 
				
			||||||
 | 
					  case ACCOUNT_PIN_SUCCESS:
 | 
				
			||||||
 | 
					  case ACCOUNT_UNPIN_SUCCESS:
 | 
				
			||||||
    return normalizeRelationship(state, action.relationship);
 | 
					    return normalizeRelationship(state, action.relationship);
 | 
				
			||||||
  case RELATIONSHIPS_FETCH_SUCCESS:
 | 
					  case RELATIONSHIPS_FETCH_SUCCESS:
 | 
				
			||||||
    return normalizeRelationships(state, action.relationships);
 | 
					    return normalizeRelationships(state, action.relationships);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 {
 | 
					.moved-account-widget {
 | 
				
			||||||
  padding: 15px;
 | 
					  padding: 15px;
 | 
				
			||||||
  padding-bottom: 20px;
 | 
					  padding-bottom: 20px;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -89,6 +89,10 @@ class Account < ApplicationRecord
 | 
				
			|||||||
  has_many :status_pins, inverse_of: :account, dependent: :destroy
 | 
					  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
 | 
					  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
 | 
					  # Media
 | 
				
			||||||
  has_many :media_attachments, dependent: :destroy
 | 
					  has_many :media_attachments, dependent: :destroy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										26
									
								
								app/models/account_pin.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								app/models/account_pin.rb
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
@@ -40,6 +40,10 @@ module AccountInteractions
 | 
				
			|||||||
      end
 | 
					      end
 | 
				
			||||||
    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)
 | 
					    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
 | 
					      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)
 | 
					      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?
 | 
					    status_pins.where(status: status).exists?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def endorsed?(account)
 | 
				
			||||||
 | 
					    account_pins.where(target_account: account).exists?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def followers_for_local_distribution
 | 
					  def followers_for_local_distribution
 | 
				
			||||||
    followers.local
 | 
					    followers.local
 | 
				
			||||||
             .joins(:user)
 | 
					             .joins(:user)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class AccountRelationshipsPresenter
 | 
					class AccountRelationshipsPresenter
 | 
				
			||||||
  attr_reader :following, :followed_by, :blocking,
 | 
					  attr_reader :following, :followed_by, :blocking,
 | 
				
			||||||
              :muting, :requested, :domain_blocking
 | 
					              :muting, :requested, :domain_blocking,
 | 
				
			||||||
 | 
					              :endorsed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def initialize(account_ids, current_account_id, **options)
 | 
					  def initialize(account_ids, current_account_id, **options)
 | 
				
			||||||
    @account_ids        = account_ids.map { |a| a.is_a?(Account) ? a.id : a }
 | 
					    @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))
 | 
					    @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))
 | 
					    @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))
 | 
					    @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!
 | 
					    cache_uncached!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -23,6 +25,7 @@ class AccountRelationshipsPresenter
 | 
				
			|||||||
    @muting.merge!(options[:muting_map] || {})
 | 
					    @muting.merge!(options[:muting_map] || {})
 | 
				
			||||||
    @requested.merge!(options[:requested_map] || {})
 | 
					    @requested.merge!(options[:requested_map] || {})
 | 
				
			||||||
    @domain_blocking.merge!(options[:domain_blocking_map] || {})
 | 
					    @domain_blocking.merge!(options[:domain_blocking_map] || {})
 | 
				
			||||||
 | 
					    @endorsed.merge!(options[:endorsed_map] || {})
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
@@ -37,6 +40,7 @@ class AccountRelationshipsPresenter
 | 
				
			|||||||
      muting: {},
 | 
					      muting: {},
 | 
				
			||||||
      requested: {},
 | 
					      requested: {},
 | 
				
			||||||
      domain_blocking: {},
 | 
					      domain_blocking: {},
 | 
				
			||||||
 | 
					      endorsed: {},
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @uncached_account_ids = []
 | 
					    @uncached_account_ids = []
 | 
				
			||||||
@@ -63,6 +67,7 @@ class AccountRelationshipsPresenter
 | 
				
			|||||||
        muting:          { account_id => muting[account_id] },
 | 
					        muting:          { account_id => muting[account_id] },
 | 
				
			||||||
        requested:       { account_id => requested[account_id] },
 | 
					        requested:       { account_id => requested[account_id] },
 | 
				
			||||||
        domain_blocking: { account_id => domain_blocking[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)
 | 
					      Rails.cache.write("relationship:#{@current_account_id}:#{account_id}", maps_for_account, expires_in: 1.day)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class REST::RelationshipSerializer < ActiveModel::Serializer
 | 
					class REST::RelationshipSerializer < ActiveModel::Serializer
 | 
				
			||||||
  attributes :id, :following, :showing_reblogs, :followed_by, :blocking,
 | 
					  attributes :id, :following, :showing_reblogs, :followed_by, :blocking,
 | 
				
			||||||
             :muting, :muting_notifications, :requested, :domain_blocking
 | 
					             :muting, :muting_notifications, :requested, :domain_blocking,
 | 
				
			||||||
 | 
					             :endorsed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def id
 | 
					  def id
 | 
				
			||||||
    object.id.to_s
 | 
					    object.id.to_s
 | 
				
			||||||
@@ -41,4 +42,8 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
 | 
				
			|||||||
  def domain_blocking
 | 
					  def domain_blocking
 | 
				
			||||||
    instance_options[:relationships].domain_blocking[object.id] || false
 | 
					    instance_options[:relationships].domain_blocking[object.id] || false
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def endorsed
 | 
				
			||||||
 | 
					    instance_options[:relationships].endorsed[object.id] || false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,4 +55,12 @@
 | 
				
			|||||||
      = render 'moved', account: @account
 | 
					      = render 'moved', account: @account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    = render 'bio', 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'
 | 
					    = render 'application/sidebar'
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,6 +39,7 @@ en:
 | 
				
			|||||||
    user_count_before: Home to
 | 
					    user_count_before: Home to
 | 
				
			||||||
    what_is_mastodon: What is Mastodon?
 | 
					    what_is_mastodon: What is Mastodon?
 | 
				
			||||||
  accounts:
 | 
					  accounts:
 | 
				
			||||||
 | 
					    choices_html: "%{name}'s choices:"
 | 
				
			||||||
    follow: Follow
 | 
					    follow: Follow
 | 
				
			||||||
    followers: Followers
 | 
					    followers: Followers
 | 
				
			||||||
    following: Following
 | 
					    following: Following
 | 
				
			||||||
@@ -49,6 +50,8 @@ en:
 | 
				
			|||||||
    nothing_here: There is nothing here!
 | 
					    nothing_here: There is nothing here!
 | 
				
			||||||
    people_followed_by: People whom %{name} follows
 | 
					    people_followed_by: People whom %{name} follows
 | 
				
			||||||
    people_who_follow: People who follow %{name}
 | 
					    people_who_follow: People who follow %{name}
 | 
				
			||||||
 | 
					    pin_errors:
 | 
				
			||||||
 | 
					      following: You must be already following the person you want to endorse
 | 
				
			||||||
    posts: Toots
 | 
					    posts: Toots
 | 
				
			||||||
    posts_with_replies: Toots and replies
 | 
					    posts_with_replies: Toots and replies
 | 
				
			||||||
    reserved_username: The username is reserved
 | 
					    reserved_username: The username is reserved
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -309,6 +309,9 @@ Rails.application.routes.draw do
 | 
				
			|||||||
          post :mute
 | 
					          post :mute
 | 
				
			||||||
          post :unmute
 | 
					          post :unmute
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        resource :pin, only: :create, controller: 'accounts/pins'
 | 
				
			||||||
 | 
					        post :unpin, to: 'accounts/pins#destroy'
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      resources :lists, only: [:index, :create, :show, :update, :destroy] do
 | 
					      resources :lists, only: [:index, :create, :show, :update, :destroy] do
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								db/migrate/20180808175627_create_account_pins.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								db/migrate/20180808175627_create_account_pins.rb
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
							
								
								
									
										16
									
								
								db/schema.rb
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								db/schema.rb
									
									
									
									
									
								
							@@ -10,7 +10,7 @@
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
# It's strongly recommended that you check this file into your version control system.
 | 
					# 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
 | 
					  # These are extensions that must be enabled in order to support this database
 | 
				
			||||||
  enable_extension "plpgsql"
 | 
					  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"
 | 
					    t.index ["target_account_id"], name: "index_account_moderation_notes_on_target_account_id"
 | 
				
			||||||
  end
 | 
					  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|
 | 
					  create_table "accounts", force: :cascade do |t|
 | 
				
			||||||
    t.string "username", default: "", null: false
 | 
					    t.string "username", default: "", null: false
 | 
				
			||||||
    t.string "domain"
 | 
					    t.string "domain"
 | 
				
			||||||
@@ -149,9 +159,9 @@ ActiveRecord::Schema.define(version: 2018_07_11_152640) do
 | 
				
			|||||||
    t.text "phrase", default: "", null: false
 | 
					    t.text "phrase", default: "", null: false
 | 
				
			||||||
    t.string "context", default: [], null: false, array: true
 | 
					    t.string "context", default: [], null: false, array: true
 | 
				
			||||||
    t.boolean "irreversible", default: false, null: false
 | 
					    t.boolean "irreversible", default: false, null: false
 | 
				
			||||||
    t.boolean "whole_word", default: true, null: false
 | 
					 | 
				
			||||||
    t.datetime "created_at", null: false
 | 
					    t.datetime "created_at", null: false
 | 
				
			||||||
    t.datetime "updated_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"
 | 
					    t.index ["account_id"], name: "index_custom_filters_on_account_id"
 | 
				
			||||||
  end
 | 
					  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_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade
 | 
				
			||||||
  add_foreign_key "account_moderation_notes", "accounts"
 | 
					  add_foreign_key "account_moderation_notes", "accounts"
 | 
				
			||||||
  add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id"
 | 
					  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 "accounts", "accounts", column: "moved_to_account_id", on_delete: :nullify
 | 
				
			||||||
  add_foreign_key "admin_action_logs", "accounts", on_delete: :cascade
 | 
					  add_foreign_key "admin_action_logs", "accounts", on_delete: :cascade
 | 
				
			||||||
  add_foreign_key "backups", "users", on_delete: :nullify
 | 
					  add_foreign_key "backups", "users", on_delete: :nullify
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								spec/fabricators/account_pin_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								spec/fabricators/account_pin_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					Fabricator(:account_pin) do
 | 
				
			||||||
 | 
					  account        nil
 | 
				
			||||||
 | 
					  target_account nil
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										5
									
								
								spec/models/account_pin_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								spec/models/account_pin_spec.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					require 'rails_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RSpec.describe AccountPin, type: :model do
 | 
				
			||||||
 | 
					  pending "add some examples to (or delete) #{__FILE__}"
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
		Reference in New Issue
	
	Block a user