forked from fedi/mastodon
Replacing follow requests in the settings area with in-UI column
This commit is contained in:
parent
004382e4d0
commit
3689c119f0
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -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));
|
|
@ -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));
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -1,2 +0,0 @@
|
|||
module FollowRequestsHelper
|
||||
end
|
|
@ -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"
|
|
@ -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'
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
|
@ -1,5 +0,0 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe FollowRequestsHelper, type: :helper do
|
||||
|
||||
end
|
Loading…
Reference in a new issue