diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx
index 759435afe..8d28b051f 100644
--- a/app/assets/javascripts/components/actions/accounts.jsx
+++ b/app/assets/javascripts/components/actions/accounts.jsx
@@ -51,6 +51,22 @@ export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST';
export const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS';
export const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL';
+export const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST';
+export const FOLLOW_REQUESTS_FETCH_SUCCESS = 'FOLLOW_REQUESTS_FETCH_SUCCESS';
+export const FOLLOW_REQUESTS_FETCH_FAIL = 'FOLLOW_REQUESTS_FETCH_FAIL';
+
+export const FOLLOW_REQUESTS_EXPAND_REQUEST = 'FOLLOW_REQUESTS_EXPAND_REQUEST';
+export const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS';
+export const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL';
+
+export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST';
+export const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS';
+export const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
+
+export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
+export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
+export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
+
export function setAccountSelf(account) {
return {
type: ACCOUNT_SET_SELF,
@@ -509,3 +525,140 @@ export function fetchRelationshipsFail(error) {
error
};
};
+
+export function fetchFollowRequests() {
+ return (dispatch, getState) => {
+ dispatch(fetchFollowRequestsRequest());
+
+ api(getState).get('/api/v1/follow_requests').then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null))
+ }).catch(error => dispatch(fetchFollowRequestsFail(error)));
+ };
+};
+
+export function fetchFollowRequestsRequest() {
+ return {
+ type: FOLLOW_REQUESTS_FETCH_REQUEST
+ };
+};
+
+export function fetchFollowRequestsSuccess(accounts, next) {
+ return {
+ type: FOLLOW_REQUESTS_FETCH_SUCCESS,
+ accounts,
+ next
+ };
+};
+
+export function fetchFollowRequestsFail(error) {
+ return {
+ type: FOLLOW_REQUESTS_FETCH_FAIL,
+ error
+ };
+};
+
+export function expandFollowRequests() {
+ return (dispatch, getState) => {
+ const url = getState().getIn(['user_lists', 'follow_requests', 'next']);
+
+ if (url === null) {
+ return;
+ }
+
+ dispatch(expandFollowRequestsRequest());
+
+ api(getState).get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null))
+ }).catch(error => dispatch(expandFollowRequestsFail(error)));
+ };
+};
+
+export function expandFollowRequestsRequest() {
+ return {
+ type: FOLLOW_REQUESTS_EXPAND_REQUEST
+ };
+};
+
+export function expandFollowRequestsSuccess(accounts, next) {
+ return {
+ type: FOLLOW_REQUESTS_EXPAND_SUCCESS,
+ accounts,
+ next
+ };
+};
+
+export function expandFollowRequestsFail(error) {
+ return {
+ type: FOLLOW_REQUESTS_EXPAND_FAIL,
+ error
+ };
+};
+
+export function authorizeFollowRequest(id) {
+ return (dispatch, getState) => {
+ dispatch(authorizeFollowRequestRequest(id));
+
+ api(getState)
+ .post(`/api/v1/follow_requests/${id}/authorize`)
+ .then(response => dispatch(authorizeFollowRequestSuccess(id)))
+ .catch(error => dispatch(authorizeFollowRequestFail(id, error)));
+ };
+};
+
+export function authorizeFollowRequestRequest(id) {
+ return {
+ type: FOLLOW_REQUEST_AUTHORIZE_REQUEST,
+ id
+ };
+};
+
+export function authorizeFollowRequestSuccess(id) {
+ return {
+ type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
+ id
+ };
+};
+
+export function authorizeFollowRequestFail(id, error) {
+ return {
+ type: FOLLOW_REQUEST_AUTHORIZE_FAIL,
+ id,
+ error
+ };
+};
+
+
+export function rejectFollowRequest(id) {
+ return (dispatch, getState) => {
+ dispatch(rejectFollowRequestRequest(id));
+
+ api(getState)
+ .post(`/api/v1/follow_requests/${id}/reject`)
+ .then(response => dispatch(rejectFollowRequestSuccess(id)))
+ .catch(error => dispatch(rejectFollowRequestFail(id, error)));
+ };
+};
+
+export function rejectFollowRequestRequest(id) {
+ return {
+ type: FOLLOW_REQUEST_REJECT_REQUEST,
+ id
+ };
+};
+
+export function rejectFollowRequestSuccess(id) {
+ return {
+ type: FOLLOW_REQUEST_REJECT_SUCCESS,
+ id
+ };
+};
+
+export function rejectFollowRequestFail(id, error) {
+ return {
+ type: FOLLOW_REQUEST_REJECT_FAIL,
+ id,
+ error
+ };
+};
diff --git a/app/assets/javascripts/components/components/status_list.jsx b/app/assets/javascripts/components/components/status_list.jsx
index b48d94405..e0a73435f 100644
--- a/app/assets/javascripts/components/components/status_list.jsx
+++ b/app/assets/javascripts/components/components/status_list.jsx
@@ -27,11 +27,11 @@ const StatusList = React.createClass({
this._oldScrollPosition = scrollHeight - scrollTop;
- if (scrollTop === scrollHeight - clientHeight) {
+ if (scrollTop === scrollHeight - clientHeight && this.props.onScrollToBottom) {
this.props.onScrollToBottom();
- } else if (scrollTop < 100) {
+ } else if (scrollTop < 100 && this.props.onScrollToTop) {
this.props.onScrollToTop();
- } else {
+ } else if (this.props.onScroll) {
this.props.onScroll();
}
},
diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx
index 69fe2d07f..670455376 100644
--- a/app/assets/javascripts/components/containers/mastodon.jsx
+++ b/app/assets/javascripts/components/containers/mastodon.jsx
@@ -34,6 +34,7 @@ import Reblogs from '../features/reblogs';
import Favourites from '../features/favourites';
import HashtagTimeline from '../features/hashtag_timeline';
import Notifications from '../features/notifications';
+import FollowRequests from '../features/follow_requests';
import { IntlProvider, addLocaleData } from 'react-intl';
import en from 'react-intl/locale-data/en';
import de from 'react-intl/locale-data/de';
@@ -125,6 +126,8 @@ const Mastodon = React.createClass({
+
+
diff --git a/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx
new file mode 100644
index 000000000..c16488aea
--- /dev/null
+++ b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx
@@ -0,0 +1,61 @@
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Permalink from '../../../components/permalink';
+import Avatar from '../../../components/avatar';
+import DisplayName from '../../../components/display_name';
+import emojify from '../../../emoji';
+import IconButton from '../../../components/icon_button';
+import { defineMessages, injectIntl } from 'react-intl';
+
+const messages = defineMessages({
+ authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
+ reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }
+});
+
+const outerStyle = {
+ padding: '14px 10px'
+};
+
+const panelStyle = {
+ background: '#2f3441',
+ display: 'flex',
+ flexDirection: 'row',
+ borderTop: '1px solid #363c4b',
+ borderBottom: '1px solid #363c4b',
+ padding: '10px 0'
+};
+
+const btnStyle = {
+ flex: '1 1 auto',
+ textAlign: 'center'
+};
+
+const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
+ const content = { __html: emojify(account.get('note')) };
+
+ return (
+
+ )
+};
+
+AccountAuthorize.propTypes = {
+ account: ImmutablePropTypes.map.isRequired,
+ onAuthorize: React.PropTypes.func.isRequired,
+ onReject: React.PropTypes.func.isRequired,
+ intl: React.PropTypes.object.isRequired
+};
+
+export default injectIntl(AccountAuthorize);
diff --git a/app/assets/javascripts/components/features/follow_requests/containers/account_authorize_container.jsx b/app/assets/javascripts/components/features/follow_requests/containers/account_authorize_container.jsx
new file mode 100644
index 000000000..da1e5eaa1
--- /dev/null
+++ b/app/assets/javascripts/components/features/follow_requests/containers/account_authorize_container.jsx
@@ -0,0 +1,26 @@
+import { connect } from 'react-redux';
+import { makeGetAccount } from '../../../selectors';
+import AccountAuthorize from '../components/account_authorize';
+import { authorizeFollowRequest, rejectFollowRequest } from '../../../actions/accounts';
+
+const makeMapStateToProps = () => {
+ const getAccount = makeGetAccount();
+
+ const mapStateToProps = (state, props) => ({
+ account: getAccount(state, props.id)
+ });
+
+ return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { id }) => ({
+ onAuthorize (account) {
+ dispatch(authorizeFollowRequest(id));
+ },
+
+ onReject (account) {
+ dispatch(rejectFollowRequest(id));
+ }
+});
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(AccountAuthorize);
diff --git a/app/assets/javascripts/components/features/follow_requests/index.jsx b/app/assets/javascripts/components/features/follow_requests/index.jsx
new file mode 100644
index 000000000..461370999
--- /dev/null
+++ b/app/assets/javascripts/components/features/follow_requests/index.jsx
@@ -0,0 +1,66 @@
+import { connect } from 'react-redux';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import LoadingIndicator from '../../components/loading_indicator';
+import { ScrollContainer } from 'react-router-scroll';
+import Column from '../ui/components/column';
+import AccountAuthorizeContainer from './containers/account_authorize_container';
+import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts';
+import { defineMessages, injectIntl } from 'react-intl';
+
+const messages = defineMessages({
+ heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' }
+});
+
+const mapStateToProps = state => ({
+ accountIds: state.getIn(['user_lists', 'follow_requests', 'items'])
+});
+
+const FollowRequests = React.createClass({
+ propTypes: {
+ params: React.PropTypes.object.isRequired,
+ dispatch: React.PropTypes.func.isRequired,
+ accountIds: ImmutablePropTypes.list,
+ intl: React.PropTypes.object.isRequired
+ },
+
+ mixins: [PureRenderMixin],
+
+ componentWillMount () {
+ this.props.dispatch(fetchFollowRequests());
+ },
+
+ handleScroll (e) {
+ const { scrollTop, scrollHeight, clientHeight } = e.target;
+
+ if (scrollTop === scrollHeight - clientHeight) {
+ this.props.dispatch(expandFollowRequests());
+ }
+ },
+
+ render () {
+ const { intl, accountIds } = this.props;
+
+ if (!accountIds) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {accountIds.map(id =>
+
+ )}
+
+
+
+ );
+ }
+});
+
+export default connect(mapStateToProps)(injectIntl(FollowRequests));
diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx
index bff75f86f..7c2491954 100644
--- a/app/assets/javascripts/components/features/getting_started/index.jsx
+++ b/app/assets/javascripts/components/features/getting_started/index.jsx
@@ -7,7 +7,8 @@ import { connect } from 'react-redux';
const messages = defineMessages({
heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Public timeline' },
- settings: { id: 'navigation_bar.settings', defaultMessage: 'Settings' }
+ settings: { id: 'navigation_bar.settings', defaultMessage: 'Settings' },
+ follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }
});
const mapStateToProps = state => ({
@@ -32,6 +33,7 @@ const GettingStarted = ({ intl, me }) => {
+
@@ -43,4 +45,9 @@ const GettingStarted = ({ intl, me }) => {
);
};
+GettingStarted.propTypes = {
+ intl: React.PropTypes.object.isRequired,
+ me: React.PropTypes.number
+};
+
export default connect(mapStateToProps)(injectIntl(GettingStarted));
diff --git a/app/assets/javascripts/components/reducers/accounts.jsx b/app/assets/javascripts/components/reducers/accounts.jsx
index 982e63073..7f2f89d0a 100644
--- a/app/assets/javascripts/components/reducers/accounts.jsx
+++ b/app/assets/javascripts/components/reducers/accounts.jsx
@@ -6,7 +6,8 @@ import {
FOLLOWING_FETCH_SUCCESS,
FOLLOWING_EXPAND_SUCCESS,
ACCOUNT_TIMELINE_FETCH_SUCCESS,
- ACCOUNT_TIMELINE_EXPAND_SUCCESS
+ ACCOUNT_TIMELINE_EXPAND_SUCCESS,
+ FOLLOW_REQUESTS_FETCH_SUCCESS
} from '../actions/accounts';
import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose';
import {
@@ -78,6 +79,7 @@ export default function accounts(state = initialState, action) {
case FAVOURITES_FETCH_SUCCESS:
case COMPOSE_SUGGESTIONS_READY:
case SEARCH_SUGGESTIONS_READY:
+ case FOLLOW_REQUESTS_FETCH_SUCCESS:
return normalizeAccounts(state, action.accounts);
case NOTIFICATIONS_REFRESH_SUCCESS:
case NOTIFICATIONS_EXPAND_SUCCESS:
diff --git a/app/assets/javascripts/components/reducers/user_lists.jsx b/app/assets/javascripts/components/reducers/user_lists.jsx
index 3608e4209..36093663f 100644
--- a/app/assets/javascripts/components/reducers/user_lists.jsx
+++ b/app/assets/javascripts/components/reducers/user_lists.jsx
@@ -2,7 +2,10 @@ import {
FOLLOWERS_FETCH_SUCCESS,
FOLLOWERS_EXPAND_SUCCESS,
FOLLOWING_FETCH_SUCCESS,
- FOLLOWING_EXPAND_SUCCESS
+ FOLLOWING_EXPAND_SUCCESS,
+ FOLLOW_REQUESTS_FETCH_SUCCESS,
+ FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
+ FOLLOW_REQUEST_REJECT_SUCCESS
} from '../actions/accounts';
import {
REBLOGS_FETCH_SUCCESS,
@@ -14,7 +17,8 @@ const initialState = Immutable.Map({
followers: Immutable.Map(),
following: Immutable.Map(),
reblogged_by: Immutable.Map(),
- favourited_by: Immutable.Map()
+ favourited_by: Immutable.Map(),
+ follow_requests: Immutable.Map()
});
const normalizeList = (state, type, id, accounts, next) => {
@@ -44,6 +48,11 @@ export default function userLists(state = initialState, action) {
return state.setIn(['reblogged_by', action.id], Immutable.List(action.accounts.map(item => item.id)));
case FAVOURITES_FETCH_SUCCESS:
return state.setIn(['favourited_by', action.id], Immutable.List(action.accounts.map(item => item.id)));
+ case FOLLOW_REQUESTS_FETCH_SUCCESS:
+ return state.setIn(['follow_requests', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
+ case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
+ case FOLLOW_REQUEST_REJECT_SUCCESS:
+ return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.id));
default:
return state;
}
diff --git a/app/controllers/follow_requests_controller.rb b/app/controllers/follow_requests_controller.rb
deleted file mode 100644
index d4368f773..000000000
--- a/app/controllers/follow_requests_controller.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-class FollowRequestsController < ApplicationController
- layout 'auth'
-
- before_action :authenticate_user!
- before_action :set_follow_request, except: :index
-
- def index
- @follow_requests = FollowRequest.where(target_account: current_account)
- end
-
- def authorize
- @follow_request.authorize!
- redirect_to follow_requests_path
- end
-
- def reject
- @follow_request.reject!
- redirect_to follow_requests_path
- end
-
- private
-
- def set_follow_request
- @follow_request = FollowRequest.find(params[:id])
- end
-end
diff --git a/app/helpers/follow_requests_helper.rb b/app/helpers/follow_requests_helper.rb
deleted file mode 100644
index cfd350e53..000000000
--- a/app/helpers/follow_requests_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module FollowRequestsHelper
-end
diff --git a/app/views/follow_requests/index.html.haml b/app/views/follow_requests/index.html.haml
deleted file mode 100644
index 8c83488de..000000000
--- a/app/views/follow_requests/index.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-- content_for :page_title do
- = t('follow_requests.title')
-
-- if @follow_requests.empty?
- %p.nothing-here= t('accounts.nothing_here')
-- else
- %table.table
- %tbody
- - @follow_requests.each do |follow_request|
- %tr
- %td= link_to follow_request.account.acct, web_path("accounts/#{follow_request.account.id}")
- %td{ style: 'text-align: right' }
- = table_link_to 'check-circle', t('follow_requests.authorize'), authorize_follow_request_path(follow_request), method: :post
- = table_link_to 'times-circle', t('follow_requests.reject'), reject_follow_request_path(follow_request), method: :post
-
-.form-footer= render "settings/shared/links"
diff --git a/app/views/settings/shared/_links.html.haml b/app/views/settings/shared/_links.html.haml
index b6a0b1fc1..44f097950 100644
--- a/app/views/settings/shared/_links.html.haml
+++ b/app/views/settings/shared/_links.html.haml
@@ -1,8 +1,6 @@
%ul.no-list
- if controller_name != 'profiles'
%li= link_to t('settings.edit_profile'), settings_profile_path
- - if controller_name != 'follow_requests'
- %li= link_to t('follow_requests.title'), follow_requests_path
- if controller_name != 'preferences'
%li= link_to t('settings.preferences'), settings_preferences_path
- if controller_name != 'registrations'
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 4cf958517..e5916dd76 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -40,10 +40,6 @@ en:
x_minutes: "%{count}m"
x_months: "%{count}mo"
x_seconds: "%{count}s"
- follow_requests:
- authorize: Authorize
- reject: Reject
- title: Follow requests
generic:
changes_saved_msg: Changes successfully saved!
powered_by: powered by %{link}
diff --git a/config/routes.rb b/config/routes.rb
index 2e9b2a87c..985d6583d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -48,13 +48,6 @@ Rails.application.routes.draw do
resources :media, only: [:show]
resources :tags, only: [:show]
- resources :follow_requests do
- member do
- post :authorize
- post :reject
- end
- end
-
namespace :admin do
resources :pubsubhubbub, only: [:index]
resources :domain_blocks, only: [:index, :create]
diff --git a/spec/controllers/follow_requests_controller_spec.rb b/spec/controllers/follow_requests_controller_spec.rb
deleted file mode 100644
index 72f5fd9b9..000000000
--- a/spec/controllers/follow_requests_controller_spec.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe FollowRequestsController, type: :controller do
- render_views
-
- before do
- sign_in Fabricate(:user), scope: :user
- end
-
- describe 'GET #index' do
- it 'returns http success' do
- get :index
- expect(response).to have_http_status(:success)
- end
- end
-end
diff --git a/spec/helpers/follow_requests_helper_spec.rb b/spec/helpers/follow_requests_helper_spec.rb
deleted file mode 100644
index e031cf402..000000000
--- a/spec/helpers/follow_requests_helper_spec.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe FollowRequestsHelper, type: :helper do
-
-end