diff options
17 files changed, 334 insertions, 87 deletions
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({ <Route path='followers' component={Followers} /> <Route path='following' component={Following} /> </Route> + + <Route path='follow_requests' component={FollowRequests} /> </Route> </Router> </Provider> 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 ( + <div> + <div style={outerStyle}> + <Permalink href={account.get('url')} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}> + <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={48} /></div> + <DisplayName account={account} /> + </Permalink> + + <div style={{ color: '#616b86', fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} /> + </div> + + <div style={panelStyle}> + <div style={btnStyle}><IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /></div> + <div style={btnStyle}><IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /></div> + </div> + </div> + ) +}; + +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 ( + <Column> + <LoadingIndicator /> + </Column> + ); + } + + return ( + <Column icon='users' heading={intl.formatMessage(messages.heading)}> + <ScrollContainer scrollKey='follow_requests'> + <div className='scrollable' onScroll={this.handleScroll}> + {accountIds.map(id => + <AccountAuthorizeContainer key={id} id={id} /> + )} + </div> + </ScrollContainer> + </Column> + ); + } +}); + +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 }) => { <div style={hamburgerStyle}><i className='fa fa-bars' /></div> <ColumnLink icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' /> <ColumnLink icon='cog' text={intl.formatMessage(messages.settings)} href='/settings/profile' /> + <ColumnLink icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' /> </div> <div className='static-content'> @@ -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 |