+ {!(suspended || hidden || account.get('moved')) && account.getIn(['relationship', 'requested_by']) &&
{!suspended && info}
diff --git a/app/javascript/mastodon/features/account/containers/follow_request_note_container.js b/app/javascript/mastodon/features/account/containers/follow_request_note_container.js
new file mode 100644
index 00000000000..c33c3de591e
--- /dev/null
+++ b/app/javascript/mastodon/features/account/containers/follow_request_note_container.js
@@ -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);
diff --git a/app/javascript/mastodon/reducers/relationships.js b/app/javascript/mastodon/reducers/relationships.js
index 53949258a32..850ece35163 100644
--- a/app/javascript/mastodon/reducers/relationships.js
+++ b/app/javascript/mastodon/reducers/relationships.js
@@ -1,3 +1,6 @@
+import {
+ NOTIFICATIONS_UPDATE,
+} from '../actions/notifications';
import {
ACCOUNT_FOLLOW_SUCCESS,
ACCOUNT_FOLLOW_REQUEST,
@@ -12,6 +15,8 @@ import {
ACCOUNT_PIN_SUCCESS,
ACCOUNT_UNPIN_SUCCESS,
RELATIONSHIPS_FETCH_SUCCESS,
+ FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
+ FOLLOW_REQUEST_REJECT_SUCCESS,
} from '../actions/accounts';
import {
DOMAIN_BLOCK_SUCCESS,
@@ -44,6 +49,12 @@ const initialState = ImmutableMap();
export default function relationships(state = initialState, action) {
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:
return state.getIn([action.id, 'following']) ? state : state.setIn([action.id, action.locked ? 'requested' : 'following'], true);
case ACCOUNT_FOLLOW_FAIL:
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 15fc6aa6900..6a22f60095e 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -166,6 +166,30 @@
&:disabled {
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 {
@@ -6722,7 +6746,8 @@ noscript {
}
}
-.moved-account-banner {
+.moved-account-banner,
+.follow-request-banner {
padding: 20px;
background: lighten($ui-base-color, 4%);
display: flex;
@@ -6745,6 +6770,7 @@ noscript {
justify-content: space-between;
align-items: center;
gap: 15px;
+ width: 100%;
}
.detailed-status__display-name {
@@ -6752,6 +6778,10 @@ noscript {
}
}
+.follow-request-banner .button {
+ width: 100%;
+}
+
.column-inline-form {
padding: 15px;
display: flex;
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index 15c49f2fecb..de8bf338f2d 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -44,6 +44,10 @@ module AccountInteractions
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)
follow_mapping(AccountPin.where(account_id: account_id, target_account_id: target_account_ids), :target_account_id)
end
diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb
index d662380f69f..ab8bac41290 100644
--- a/app/presenters/account_relationships_presenter.rb
+++ b/app/presenters/account_relationships_presenter.rb
@@ -2,7 +2,7 @@
class AccountRelationshipsPresenter
attr_reader :following, :followed_by, :blocking, :blocked_by,
- :muting, :requested, :domain_blocking,
+ :muting, :requested, :requested_by, :domain_blocking,
:endorsed, :account_note
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))
@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_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))
@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))
@@ -27,6 +28,7 @@ class AccountRelationshipsPresenter
@blocked_by.merge!(options[:blocked_by_map] || {})
@muting.merge!(options[:muting_map] || {})
@requested.merge!(options[:requested_map] || {})
+ @requested_by.merge!(options[:requested_by_map] || {})
@domain_blocking.merge!(options[:domain_blocking_map] || {})
@endorsed.merge!(options[:endorsed_map] || {})
@account_note.merge!(options[:account_note_map] || {})
@@ -44,6 +46,7 @@ class AccountRelationshipsPresenter
blocked_by: {},
muting: {},
requested: {},
+ requested_by: {},
domain_blocking: {},
endorsed: {},
account_note: {},
@@ -73,6 +76,7 @@ class AccountRelationshipsPresenter
blocked_by: { account_id => blocked_by[account_id] },
muting: { account_id => muting[account_id] },
requested: { account_id => requested[account_id] },
+ requested_by: { account_id => requested_by[account_id] },
domain_blocking: { account_id => domain_blocking[account_id] },
endorsed: { account_id => endorsed[account_id] },
account_note: { account_id => account_note[account_id] },
diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb
index 31fc60eb25a..b533874012b 100644
--- a/app/serializers/rest/relationship_serializer.rb
+++ b/app/serializers/rest/relationship_serializer.rb
@@ -2,8 +2,8 @@
class REST::RelationshipSerializer < ActiveModel::Serializer
attributes :id, :following, :showing_reblogs, :notifying, :languages, :followed_by,
- :blocking, :blocked_by, :muting, :muting_notifications, :requested,
- :domain_blocking, :endorsed, :note
+ :blocking, :blocked_by, :muting, :muting_notifications,
+ :requested, :requested_by, :domain_blocking, :endorsed, :note
def id
object.id.to_s
@@ -54,6 +54,10 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
instance_options[:relationships].requested[object.id] ? true : false
end
+ def requested_by
+ instance_options[:relationships].requested_by[object.id] ? true : false
+ end
+
def domain_blocking
instance_options[:relationships].domain_blocking[object.id] || false
end
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index c9d782cee9a..6cd769dc84b 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -658,6 +658,12 @@ RSpec.describe Account, type: :model do
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
subject { Account::MENTION_RE }
diff --git a/spec/presenters/account_relationships_presenter_spec.rb b/spec/presenters/account_relationships_presenter_spec.rb
index edfbbb35493..8a485d2b9a9 100644
--- a/spec/presenters/account_relationships_presenter_spec.rb
+++ b/spec/presenters/account_relationships_presenter_spec.rb
@@ -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(: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_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)
end
@@ -71,6 +72,14 @@ RSpec.describe AccountRelationshipsPresenter do
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
let(:options) { { domain_blocking_map: { 7 => true } } }