Add follow request banner on account header (#20785)
* Add requested_by to relationship maps * Display whether an account has requested to follow you on their profilepull/41/head
parent
7a3c6bb888
commit
70415714f1
|
@ -0,0 +1,37 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import Icon from 'mastodon/components/icon';
|
||||||
|
|
||||||
|
export default class FollowRequestNote extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { account, onAuthorize, onReject } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='follow-request-banner'>
|
||||||
|
<div className='follow-request-banner__message'>
|
||||||
|
<FormattedMessage id='account.requested_follow' defaultMessage='{name} has requested to follow you' values={{ name: <bdi><strong dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi> }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='follow-request-banner__action'>
|
||||||
|
<button type='button' className='button button-tertiary button--confirmation' onClick={onAuthorize}>
|
||||||
|
<Icon id='check' fixedWidth />
|
||||||
|
<FormattedMessage id='follow_request.authorize' defaultMessage='Authorize' />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type='button' className='button button-tertiary button--destructive' onClick={onReject}>
|
||||||
|
<Icon id='times' fixedWidth />
|
||||||
|
<FormattedMessage id='follow_request.reject' defaultMessage='Reject' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import ShortNumber from 'mastodon/components/short_number';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||||
import AccountNoteContainer from '../containers/account_note_container';
|
import AccountNoteContainer from '../containers/account_note_container';
|
||||||
|
import FollowRequestNoteContainer from '../containers/follow_request_note_container';
|
||||||
import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions';
|
import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
|
@ -311,6 +312,8 @@ class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
<div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||||
|
{!(suspended || hidden || account.get('moved')) && account.getIn(['relationship', 'requested_by']) && <FollowRequestNoteContainer account={account} />}
|
||||||
|
|
||||||
<div className='account__header__image'>
|
<div className='account__header__image'>
|
||||||
<div className='account__header__info'>
|
<div className='account__header__info'>
|
||||||
{!suspended && info}
|
{!suspended && info}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import FollowRequestNote from '../components/follow_request_note';
|
||||||
|
import { authorizeFollowRequest, rejectFollowRequest } from 'mastodon/actions/accounts';
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, { account }) => ({
|
||||||
|
onAuthorize () {
|
||||||
|
dispatch(authorizeFollowRequest(account.get('id')));
|
||||||
|
},
|
||||||
|
|
||||||
|
onReject () {
|
||||||
|
dispatch(rejectFollowRequest(account.get('id')));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(FollowRequestNote);
|
|
@ -1,3 +1,6 @@
|
||||||
|
import {
|
||||||
|
NOTIFICATIONS_UPDATE,
|
||||||
|
} from '../actions/notifications';
|
||||||
import {
|
import {
|
||||||
ACCOUNT_FOLLOW_SUCCESS,
|
ACCOUNT_FOLLOW_SUCCESS,
|
||||||
ACCOUNT_FOLLOW_REQUEST,
|
ACCOUNT_FOLLOW_REQUEST,
|
||||||
|
@ -12,6 +15,8 @@ import {
|
||||||
ACCOUNT_PIN_SUCCESS,
|
ACCOUNT_PIN_SUCCESS,
|
||||||
ACCOUNT_UNPIN_SUCCESS,
|
ACCOUNT_UNPIN_SUCCESS,
|
||||||
RELATIONSHIPS_FETCH_SUCCESS,
|
RELATIONSHIPS_FETCH_SUCCESS,
|
||||||
|
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
|
||||||
|
FOLLOW_REQUEST_REJECT_SUCCESS,
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
import {
|
import {
|
||||||
DOMAIN_BLOCK_SUCCESS,
|
DOMAIN_BLOCK_SUCCESS,
|
||||||
|
@ -44,6 +49,12 @@ const initialState = ImmutableMap();
|
||||||
|
|
||||||
export default function relationships(state = initialState, action) {
|
export default function relationships(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
|
case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
|
||||||
|
return state.setIn([action.id, 'followed_by'], true).setIn([action.id, 'requested_by'], false);
|
||||||
|
case FOLLOW_REQUEST_REJECT_SUCCESS:
|
||||||
|
return state.setIn([action.id, 'followed_by'], false).setIn([action.id, 'requested_by'], false);
|
||||||
|
case NOTIFICATIONS_UPDATE:
|
||||||
|
return action.notification.type === 'follow_request' ? state.setIn([action.notification.account.id, 'requested_by'], true) : state;
|
||||||
case ACCOUNT_FOLLOW_REQUEST:
|
case ACCOUNT_FOLLOW_REQUEST:
|
||||||
return state.getIn([action.id, 'following']) ? state : state.setIn([action.id, action.locked ? 'requested' : 'following'], true);
|
return state.getIn([action.id, 'following']) ? state : state.setIn([action.id, action.locked ? 'requested' : 'following'], true);
|
||||||
case ACCOUNT_FOLLOW_FAIL:
|
case ACCOUNT_FOLLOW_FAIL:
|
||||||
|
|
|
@ -166,6 +166,30 @@
|
||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.button--confirmation {
|
||||||
|
color: $valid-value-color;
|
||||||
|
border-color: $valid-value-color;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
background: $valid-value-color;
|
||||||
|
color: $primary-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.button--destructive {
|
||||||
|
color: $error-value-color;
|
||||||
|
border-color: $error-value-color;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
background: $error-value-color;
|
||||||
|
color: $primary-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.button--block {
|
&.button--block {
|
||||||
|
@ -6722,7 +6746,8 @@ noscript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.moved-account-banner {
|
.moved-account-banner,
|
||||||
|
.follow-request-banner {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: lighten($ui-base-color, 4%);
|
background: lighten($ui-base-color, 4%);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -6745,6 +6770,7 @@ noscript {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailed-status__display-name {
|
.detailed-status__display-name {
|
||||||
|
@ -6752,6 +6778,10 @@ noscript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.follow-request-banner .button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.column-inline-form {
|
.column-inline-form {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -44,6 +44,10 @@ module AccountInteractions
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def requested_by_map(target_account_ids, account_id)
|
||||||
|
follow_mapping(FollowRequest.where(account_id: target_account_ids, target_account_id: account_id), :account_id)
|
||||||
|
end
|
||||||
|
|
||||||
def endorsed_map(target_account_ids, account_id)
|
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)
|
follow_mapping(AccountPin.where(account_id: account_id, target_account_id: target_account_ids), :target_account_id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
class AccountRelationshipsPresenter
|
class AccountRelationshipsPresenter
|
||||||
attr_reader :following, :followed_by, :blocking, :blocked_by,
|
attr_reader :following, :followed_by, :blocking, :blocked_by,
|
||||||
:muting, :requested, :domain_blocking,
|
:muting, :requested, :requested_by, :domain_blocking,
|
||||||
:endorsed, :account_note
|
:endorsed, :account_note
|
||||||
|
|
||||||
def initialize(account_ids, current_account_id, **options)
|
def initialize(account_ids, current_account_id, **options)
|
||||||
|
@ -15,6 +15,7 @@ class AccountRelationshipsPresenter
|
||||||
@blocked_by = cached[:blocked_by].merge(Account.blocked_by_map(@uncached_account_ids, @current_account_id))
|
@blocked_by = cached[:blocked_by].merge(Account.blocked_by_map(@uncached_account_ids, @current_account_id))
|
||||||
@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))
|
||||||
|
@requested_by = cached[:requested_by].merge(Account.requested_by_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))
|
@endorsed = cached[:endorsed].merge(Account.endorsed_map(@uncached_account_ids, @current_account_id))
|
||||||
@account_note = cached[:account_note].merge(Account.account_note_map(@uncached_account_ids, @current_account_id))
|
@account_note = cached[:account_note].merge(Account.account_note_map(@uncached_account_ids, @current_account_id))
|
||||||
|
@ -27,6 +28,7 @@ class AccountRelationshipsPresenter
|
||||||
@blocked_by.merge!(options[:blocked_by_map] || {})
|
@blocked_by.merge!(options[:blocked_by_map] || {})
|
||||||
@muting.merge!(options[:muting_map] || {})
|
@muting.merge!(options[:muting_map] || {})
|
||||||
@requested.merge!(options[:requested_map] || {})
|
@requested.merge!(options[:requested_map] || {})
|
||||||
|
@requested_by.merge!(options[:requested_by_map] || {})
|
||||||
@domain_blocking.merge!(options[:domain_blocking_map] || {})
|
@domain_blocking.merge!(options[:domain_blocking_map] || {})
|
||||||
@endorsed.merge!(options[:endorsed_map] || {})
|
@endorsed.merge!(options[:endorsed_map] || {})
|
||||||
@account_note.merge!(options[:account_note_map] || {})
|
@account_note.merge!(options[:account_note_map] || {})
|
||||||
|
@ -44,6 +46,7 @@ class AccountRelationshipsPresenter
|
||||||
blocked_by: {},
|
blocked_by: {},
|
||||||
muting: {},
|
muting: {},
|
||||||
requested: {},
|
requested: {},
|
||||||
|
requested_by: {},
|
||||||
domain_blocking: {},
|
domain_blocking: {},
|
||||||
endorsed: {},
|
endorsed: {},
|
||||||
account_note: {},
|
account_note: {},
|
||||||
|
@ -73,6 +76,7 @@ class AccountRelationshipsPresenter
|
||||||
blocked_by: { account_id => blocked_by[account_id] },
|
blocked_by: { account_id => blocked_by[account_id] },
|
||||||
muting: { account_id => muting[account_id] },
|
muting: { account_id => muting[account_id] },
|
||||||
requested: { account_id => requested[account_id] },
|
requested: { account_id => requested[account_id] },
|
||||||
|
requested_by: { account_id => requested_by[account_id] },
|
||||||
domain_blocking: { account_id => domain_blocking[account_id] },
|
domain_blocking: { account_id => domain_blocking[account_id] },
|
||||||
endorsed: { account_id => endorsed[account_id] },
|
endorsed: { account_id => endorsed[account_id] },
|
||||||
account_note: { account_id => account_note[account_id] },
|
account_note: { account_id => account_note[account_id] },
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
class REST::RelationshipSerializer < ActiveModel::Serializer
|
class REST::RelationshipSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :following, :showing_reblogs, :notifying, :languages, :followed_by,
|
attributes :id, :following, :showing_reblogs, :notifying, :languages, :followed_by,
|
||||||
:blocking, :blocked_by, :muting, :muting_notifications, :requested,
|
:blocking, :blocked_by, :muting, :muting_notifications,
|
||||||
:domain_blocking, :endorsed, :note
|
:requested, :requested_by, :domain_blocking, :endorsed, :note
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.id.to_s
|
object.id.to_s
|
||||||
|
@ -54,6 +54,10 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
|
||||||
instance_options[:relationships].requested[object.id] ? true : false
|
instance_options[:relationships].requested[object.id] ? true : false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def requested_by
|
||||||
|
instance_options[:relationships].requested_by[object.id] ? true : false
|
||||||
|
end
|
||||||
|
|
||||||
def domain_blocking
|
def domain_blocking
|
||||||
instance_options[:relationships].domain_blocking[object.id] || false
|
instance_options[:relationships].domain_blocking[object.id] || false
|
||||||
end
|
end
|
||||||
|
|
|
@ -658,6 +658,12 @@ RSpec.describe Account, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.requested_by_map' do
|
||||||
|
it 'returns an hash' do
|
||||||
|
expect(Account.requested_by_map([], 1)).to be_a Hash
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'MENTION_RE' do
|
describe 'MENTION_RE' do
|
||||||
subject { Account::MENTION_RE }
|
subject { Account::MENTION_RE }
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ RSpec.describe AccountRelationshipsPresenter do
|
||||||
allow(Account).to receive(:blocking_map).with(account_ids, current_account_id).and_return(default_map)
|
allow(Account).to receive(:blocking_map).with(account_ids, current_account_id).and_return(default_map)
|
||||||
allow(Account).to receive(:muting_map).with(account_ids, current_account_id).and_return(default_map)
|
allow(Account).to receive(:muting_map).with(account_ids, current_account_id).and_return(default_map)
|
||||||
allow(Account).to receive(:requested_map).with(account_ids, current_account_id).and_return(default_map)
|
allow(Account).to receive(:requested_map).with(account_ids, current_account_id).and_return(default_map)
|
||||||
|
allow(Account).to receive(:requested_by_map).with(account_ids, current_account_id).and_return(default_map)
|
||||||
allow(Account).to receive(:domain_blocking_map).with(account_ids, current_account_id).and_return(default_map)
|
allow(Account).to receive(:domain_blocking_map).with(account_ids, current_account_id).and_return(default_map)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -71,6 +72,14 @@ RSpec.describe AccountRelationshipsPresenter do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'options[:requested_by_map] is set' do
|
||||||
|
let(:options) { { requested_by_map: { 6 => true } } }
|
||||||
|
|
||||||
|
it 'sets @requested merged with default_map and options[:requested_by_map]' do
|
||||||
|
expect(presenter.requested_by).to eq default_map.merge(options[:requested_by_map])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'options[:domain_blocking_map] is set' do
|
context 'options[:domain_blocking_map] is set' do
|
||||||
let(:options) { { domain_blocking_map: { 7 => true } } }
|
let(:options) { { domain_blocking_map: { 7 => true } } }
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue