mirror of https://github.com/mastodon/mastodon
commit
de154dbd5d
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
Binary file not shown.
Before Width: | Height: | Size: 313 KiB After Width: | Height: | Size: 244 KiB |
@ -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')} to={`/accounts/${account.get('id')}`} 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));
|
@ -0,0 +1,150 @@
|
|||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import Toggle from 'react-toggle';
|
||||||
|
import { Motion, spring } from 'react-motion';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
const outerStyle = {
|
||||||
|
background: '#373b4a',
|
||||||
|
padding: '15px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconStyle = {
|
||||||
|
fontSize: '16px',
|
||||||
|
padding: '15px',
|
||||||
|
position: 'absolute',
|
||||||
|
right: '0',
|
||||||
|
top: '-48px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelStyle = {
|
||||||
|
display: 'block',
|
||||||
|
lineHeight: '24px',
|
||||||
|
verticalAlign: 'middle'
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelSpanStyle = {
|
||||||
|
display: 'inline-block',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
marginBottom: '14px',
|
||||||
|
marginLeft: '8px',
|
||||||
|
color: '#9baec8'
|
||||||
|
};
|
||||||
|
|
||||||
|
const sectionStyle = {
|
||||||
|
cursor: 'default',
|
||||||
|
display: 'block',
|
||||||
|
fontWeight: '500',
|
||||||
|
color: '#9baec8',
|
||||||
|
marginBottom: '10px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const rowStyle = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const ColumnSettings = React.createClass({
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
|
onChange: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState () {
|
||||||
|
return {
|
||||||
|
collapsed: true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
|
handleToggleCollapsed () {
|
||||||
|
this.setState({ collapsed: !this.state.collapsed });
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChange (key, e) {
|
||||||
|
this.props.onChange(key, e.target.checked);
|
||||||
|
},
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { settings } = this.props;
|
||||||
|
const { collapsed } = this.state;
|
||||||
|
|
||||||
|
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
||||||
|
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<div style={{...iconStyle, color: collapsed ? '#9baec8' : '#fff', background: collapsed ? '#2f3441' : '#373b4a' }} onClick={this.handleToggleCollapsed}><i className='fa fa-sliders' /></div>
|
||||||
|
|
||||||
|
<Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : 458) }}>
|
||||||
|
{({ opacity, height }) =>
|
||||||
|
<div style={{ overflow: 'hidden', height: `${height}px`, opacity: opacity / 100 }}>
|
||||||
|
<div style={outerStyle}>
|
||||||
|
<span style={sectionStyle}><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
|
||||||
|
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<label style={labelStyle}>
|
||||||
|
<Toggle checked={settings.getIn(['alerts', 'follow'])} onChange={this.handleChange.bind(this, ['alerts', 'follow'])} />
|
||||||
|
<span style={labelSpanStyle}>{alertStr}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label style={labelStyle}>
|
||||||
|
<Toggle checked={settings.getIn(['shows', 'follow'])} onChange={this.handleChange.bind(this, ['shows', 'follow'])} />
|
||||||
|
<span style={labelSpanStyle}>{showStr}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span style={sectionStyle}><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
|
||||||
|
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<label style={labelStyle}>
|
||||||
|
<Toggle checked={settings.getIn(['alerts', 'favourite'])} onChange={this.handleChange.bind(this, ['alerts', 'favourite'])} />
|
||||||
|
<span style={labelSpanStyle}>{alertStr}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label style={labelStyle}>
|
||||||
|
<Toggle checked={settings.getIn(['shows', 'favourite'])} onChange={this.handleChange.bind(this, ['shows', 'favourite'])} />
|
||||||
|
<span style={labelSpanStyle}>{showStr}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span style={sectionStyle}><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
|
||||||
|
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<label style={labelStyle}>
|
||||||
|
<Toggle checked={settings.getIn(['alerts', 'mention'])} onChange={this.handleChange.bind(this, ['alerts', 'mention'])} />
|
||||||
|
<span style={labelSpanStyle}>{alertStr}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label style={labelStyle}>
|
||||||
|
<Toggle checked={settings.getIn(['shows', 'mention'])} onChange={this.handleChange.bind(this, ['shows', 'mention'])} />
|
||||||
|
<span style={labelSpanStyle}>{showStr}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span style={sectionStyle}><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
|
||||||
|
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<label style={labelStyle}>
|
||||||
|
<Toggle checked={settings.getIn(['alerts', 'reblog'])} onChange={this.handleChange.bind(this, ['alerts', 'reblog'])} />
|
||||||
|
<span style={labelSpanStyle}>{alertStr}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label style={labelStyle}>
|
||||||
|
<Toggle checked={settings.getIn(['shows', 'reblog'])} onChange={this.handleChange.bind(this, ['shows', 'reblog'])} />
|
||||||
|
<span style={labelSpanStyle}>{showStr}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</Motion>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ColumnSettings;
|
@ -0,0 +1,17 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ColumnSettings from '../components/column_settings';
|
||||||
|
import { changeNotificationsSetting } from '../../../actions/notifications';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
settings: state.getIn(['notifications', 'settings'])
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
||||||
|
onChange (key, checked) {
|
||||||
|
dispatch(changeNotificationsSetting(key, checked));
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
|
@ -0,0 +1,46 @@
|
|||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
const outerStyle = {
|
||||||
|
position: 'absolute',
|
||||||
|
right: '0',
|
||||||
|
top: '-48px',
|
||||||
|
padding: '15px',
|
||||||
|
fontSize: '16px',
|
||||||
|
background: '#2f3441',
|
||||||
|
flex: '0 0 auto',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: '#2b90d9'
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconStyle = {
|
||||||
|
display: 'inline-block',
|
||||||
|
marginRight: '5px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const ColumnBackButton = React.createClass({
|
||||||
|
|
||||||
|
contextTypes: {
|
||||||
|
router: React.PropTypes.object
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
|
handleClick () {
|
||||||
|
this.context.router.push('/');
|
||||||
|
},
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<div style={outerStyle} onClick={this.handleClick} className='column-back-button'>
|
||||||
|
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
||||||
|
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ColumnBackButton;
|
@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::BlocksController < ApiController
|
||||||
|
before_action -> { doorkeeper_authorize! :follow }
|
||||||
|
before_action :require_user!
|
||||||
|
|
||||||
|
respond_to :json
|
||||||
|
|
||||||
|
def index
|
||||||
|
results = Block.where(account: current_account).paginate_by_max_id(DEFAULT_ACCOUNTS_LIMIT, params[:max_id], params[:since_id])
|
||||||
|
accounts = Account.where(id: results.map(&:target_account_id)).map { |a| [a.id, a] }.to_h
|
||||||
|
@accounts = results.map { |f| accounts[f.target_account_id] }
|
||||||
|
|
||||||
|
set_account_counters_maps(@accounts)
|
||||||
|
|
||||||
|
next_path = api_v1_blocks_url(max_id: results.last.id) if results.size == DEFAULT_ACCOUNTS_LIMIT
|
||||||
|
prev_path = api_v1_blocks_url(since_id: results.first.id) unless results.empty?
|
||||||
|
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::FavouritesController < ApiController
|
||||||
|
before_action -> { doorkeeper_authorize! :read }
|
||||||
|
before_action :require_user!
|
||||||
|
|
||||||
|
respond_to :json
|
||||||
|
|
||||||
|
def index
|
||||||
|
results = Favourite.where(account: current_account).paginate_by_max_id(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id])
|
||||||
|
@statuses = cache_collection(Status.where(id: results.map(&:status_id)), Status)
|
||||||
|
|
||||||
|
set_maps(@statuses)
|
||||||
|
set_counters_maps(@statuses)
|
||||||
|
|
||||||
|
next_path = api_v1_favourites_url(max_id: results.last.id) if results.size == DEFAULT_ACCOUNTS_LIMIT
|
||||||
|
prev_path = api_v1_favourites_url(since_id: results.first.id) unless results.empty?
|
||||||
|
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::FollowRequestsController < ApiController
|
||||||
|
before_action -> { doorkeeper_authorize! :follow }
|
||||||
|
before_action :require_user!
|
||||||
|
|
||||||
|
def index
|
||||||
|
results = FollowRequest.where(target_account: current_account).paginate_by_max_id(DEFAULT_ACCOUNTS_LIMIT, params[:max_id], params[:since_id])
|
||||||
|
accounts = Account.where(id: results.map(&:account_id)).map { |a| [a.id, a] }.to_h
|
||||||
|
@accounts = results.map { |f| accounts[f.account_id] }
|
||||||
|
|
||||||
|
set_account_counters_maps(@accounts)
|
||||||
|
|
||||||
|
next_path = api_v1_follow_requests_url(max_id: results.last.id) if results.size == DEFAULT_ACCOUNTS_LIMIT
|
||||||
|
prev_path = api_v1_follow_requests_url(since_id: results.first.id) unless results.empty?
|
||||||
|
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorize
|
||||||
|
FollowRequest.find_by!(account_id: params[:id], target_account: current_account).authorize!
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
def reject
|
||||||
|
FollowRequest.find_by!(account_id: params[:id], target_account: current_account).reject!
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,45 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AuthorizeFollowController < ApplicationController
|
||||||
|
layout 'public'
|
||||||
|
|
||||||
|
before_action :authenticate_user!
|
||||||
|
|
||||||
|
def new
|
||||||
|
uri = Addressable::URI.parse(acct_param)
|
||||||
|
|
||||||
|
if uri.path && %w(http https).include?(uri.scheme)
|
||||||
|
set_account_from_url
|
||||||
|
else
|
||||||
|
set_account_from_acct
|
||||||
|
end
|
||||||
|
|
||||||
|
render :error if @account.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@account = FollowService.new.call(current_account, acct_param).try(:target_account)
|
||||||
|
|
||||||
|
if @account.nil?
|
||||||
|
render :error
|
||||||
|
else
|
||||||
|
redirect_to web_url("accounts/#{@account.id}")
|
||||||
|
end
|
||||||
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermitted
|
||||||
|
render :error
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_account_from_url
|
||||||
|
@account = FetchRemoteAccountService.new.call(acct_param)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_account_from_acct
|
||||||
|
@account = FollowRemoteAccountService.new.call(acct_param)
|
||||||
|
end
|
||||||
|
|
||||||
|
def acct_param
|
||||||
|
params[:acct].gsub(/\Aacct:/, '')
|
||||||
|
end
|
||||||
|
end
|
@ -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
|
|
@ -0,0 +1,47 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RemoteFollowController < ApplicationController
|
||||||
|
layout 'public'
|
||||||
|
|
||||||
|
before_action :set_account
|
||||||
|
before_action :check_account_suspension
|
||||||
|
|
||||||
|
def new
|
||||||
|
@remote_follow = RemoteFollow.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@remote_follow = RemoteFollow.new(resource_params)
|
||||||
|
|
||||||
|
if @remote_follow.valid?
|
||||||
|
resource = Goldfinger.finger("acct:#{@remote_follow.acct}")
|
||||||
|
redirect_url_link = resource&.link('http://ostatus.org/schema/1.0/subscribe')
|
||||||
|
|
||||||
|
if redirect_url_link.nil? || redirect_url_link.template.nil?
|
||||||
|
@remote_follow.errors.add(:acct, I18n.t('remote_follow.missing_resource'))
|
||||||
|
render(:new) && return
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: "#{@account.username}@#{Rails.configuration.x.local_domain}").to_s
|
||||||
|
else
|
||||||
|
render :new
|
||||||
|
end
|
||||||
|
rescue Goldfinger::Error
|
||||||
|
@remote_follow.errors.add(:acct, I18n.t('remote_follow.missing_resource'))
|
||||||
|
render :new
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.require(:remote_follow).permit(:acct)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = Account.find_local!(params[:account_username])
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_account_suspension
|
||||||
|
head 410 if @account.suspended?
|
||||||
|
end
|
||||||
|
end
|
@ -1,2 +0,0 @@
|
|||||||
module Api::OembedHelper
|
|
||||||
end
|
|
@ -0,0 +1,4 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module AuthorizeFollowHelper
|
||||||
|
end
|
@ -1,2 +0,0 @@
|
|||||||
module FollowRequestsHelper
|
|
||||||
end
|
|
@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RemoteFollow
|
||||||
|
include ActiveModel::Validations
|
||||||
|
|
||||||
|
attr_accessor :acct
|
||||||
|
|
||||||
|
validates :acct, presence: true
|
||||||
|
|
||||||
|
def initialize(attrs = {})
|
||||||
|
@acct = attrs[:acct]
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,2 @@
|
|||||||
|
collection @accounts
|
||||||
|
extends 'api/v1/accounts/show'
|
@ -0,0 +1,2 @@
|
|||||||
|
collection @statuses
|
||||||
|
extends 'api/v1/statuses/show'
|
@ -0,0 +1,2 @@
|
|||||||
|
collection @accounts
|
||||||
|
extends 'api/v1/accounts/show'
|
@ -0,0 +1,11 @@
|
|||||||
|
.account-card
|
||||||
|
.detailed-status__display-name
|
||||||
|
%div
|
||||||
|
= image_tag account.avatar.url(:original), alt: '', width: 48, height: 48, class: 'avatar'
|
||||||
|
|
||||||
|
%span.display-name
|
||||||
|
%strong= display_name(account)
|
||||||
|
%span= "@#{account.acct}"
|
||||||
|
|
||||||
|
- unless account.note.blank?
|
||||||
|
.account__header__content= Formatter.instance.simplified_format(account)
|
@ -0,0 +1,3 @@
|
|||||||
|
.form-container
|
||||||
|
.flash-message#error_explanation
|
||||||
|
= t('authorize_follow.error')
|
@ -0,0 +1,12 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('authorize_follow.title', acct: @account.acct)
|
||||||
|
|
||||||
|
.form-container
|
||||||
|
.follow-prompt
|
||||||
|
%h2= t('authorize_follow.prompt_html', self: current_account.username)
|
||||||
|
|
||||||
|
= render partial: 'card', locals: { account: @account }
|
||||||
|
|
||||||
|
= form_tag authorize_follow_path, method: :post, class: 'simple_form' do
|
||||||
|
= hidden_field_tag :acct, @account.acct
|
||||||
|
= button_tag t('authorize_follow.follow'), type: :submit
|
@ -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"
|
|
@ -0,0 +1,5 @@
|
|||||||
|
<%= display_name(@me) %>,
|
||||||
|
|
||||||
|
<%= t('notification_mailer.follow_request.body', name: @account.acct) %>
|
||||||
|
|
||||||
|
<%= web_url("follow_requests") %>
|
@ -1,2 +1,3 @@
|
|||||||
.flash-message#error_explanation
|
.form-container
|
||||||
= @pre_auth.error_response.body[:error_description]
|
.flash-message#error_explanation
|
||||||
|
= @pre_auth.error_response.body[:error_description]
|
||||||
|
@ -1,25 +1,26 @@
|
|||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('doorkeeper.authorizations.new.title')
|
= t('doorkeeper.authorizations.new.title')
|
||||||
|
|
||||||
.oauth-prompt
|
.form-container
|
||||||
%h2= t('doorkeeper.authorizations.new.prompt', client_name: @pre_auth.client.name)
|
.oauth-prompt
|
||||||
|
%h2= t('doorkeeper.authorizations.new.prompt', client_name: @pre_auth.client.name)
|
||||||
|
|
||||||
%p
|
%p
|
||||||
= t('doorkeeper.authorizations.new.able_to')
|
= t('doorkeeper.authorizations.new.able_to')
|
||||||
= @pre_auth.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.map { |s| "<strong>#{s}</strong>"}.to_sentence.html_safe
|
= @pre_auth.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.map { |s| "<strong>#{s}</strong>"}.to_sentence.html_safe
|
||||||
|
|
||||||
= form_tag oauth_authorization_path, method: :post, class: 'simple_form' do
|
= form_tag oauth_authorization_path, method: :post, class: 'simple_form' do
|
||||||
= hidden_field_tag :client_id, @pre_auth.client.uid
|
= hidden_field_tag :client_id, @pre_auth.client.uid
|
||||||
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
|
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
|
||||||
= hidden_field_tag :state, @pre_auth.state
|
= hidden_field_tag :state, @pre_auth.state
|
||||||
= hidden_field_tag :response_type, @pre_auth.response_type
|
= hidden_field_tag :response_type, @pre_auth.response_type
|
||||||
= hidden_field_tag :scope, @pre_auth.scope
|
= hidden_field_tag :scope, @pre_auth.scope
|
||||||
= button_tag t('doorkeeper.authorizations.buttons.authorize'), type: :submit
|
= button_tag t('doorkeeper.authorizations.buttons.authorize'), type: :submit
|
||||||
|
|
||||||
= form_tag oauth_authorization_path, method: :delete, class: 'simple_form' do
|
= form_tag oauth_authorization_path, method: :delete, class: 'simple_form' do
|
||||||
= hidden_field_tag :client_id, @pre_auth.client.uid
|
= hidden_field_tag :client_id, @pre_auth.client.uid
|
||||||
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
|
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
|
||||||
= hidden_field_tag :state, @pre_auth.state
|
= hidden_field_tag :state, @pre_auth.state
|
||||||
= hidden_field_tag :response_type, @pre_auth.response_type
|
= hidden_field_tag :response_type, @pre_auth.response_type
|
||||||
= hidden_field_tag :scope, @pre_auth.scope
|
= hidden_field_tag :scope, @pre_auth.scope
|
||||||
= button_tag t('doorkeeper.authorizations.buttons.deny'), type: :submit, class: 'negative'
|
= button_tag t('doorkeeper.authorizations.buttons.deny'), type: :submit, class: 'negative'
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
.flash-message
|
.form-container
|
||||||
%code= params[:code]
|
.flash-message
|
||||||
|
%code= params[:code]
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
.form-container
|
||||||
|
.follow-prompt
|
||||||
|
%h2= t('remote_follow.prompt')
|
||||||
|
|
||||||
|
= render partial: 'authorize_follow/card', locals: { account: @account }
|
||||||
|
|
||||||
|
= simple_form_for @remote_follow, as: :remote_follow, url: account_remote_follow_path(@account) do |f|
|
||||||
|
= render 'shared/error_messages', object: @remote_follow
|
||||||
|
|
||||||
|
= f.input :acct, placeholder: t('remote_follow.acct')
|
||||||
|
|
||||||
|
.actions
|
||||||
|
= f.button :button, t('remote_follow.proceed'), type: :submit
|
@ -0,0 +1,19 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Api::V1::BlocksController, type: :controller do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
|
||||||
|
let(:token) { double acceptable?: true, resource_owner_id: user.id }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(controller).to receive(:doorkeeper_token) { token }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #index' do
|
||||||
|
it 'returns http success' do
|
||||||
|
get :index
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,19 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Api::V1::FavouritesController, type: :controller do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
|
||||||
|
let(:token) { double acceptable?: true, resource_owner_id: user.id }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(controller).to receive(:doorkeeper_token) { token }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #index' do
|
||||||
|
it 'returns http success' do
|
||||||
|
get :index
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,52 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Api::V1::FollowRequestsController, type: :controller do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice', locked: true)) }
|
||||||
|
let(:token) { double acceptable?: true, resource_owner_id: user.id }
|
||||||
|
let(:follower) { Fabricate(:account, username: 'bob') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
FollowService.new.call(follower, user.account.acct)
|
||||||
|
allow(controller).to receive(:doorkeeper_token) { token }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET #index' do
|
||||||
|
before do
|
||||||
|
get :index
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST #authorize' do
|
||||||
|
before do
|
||||||
|
post :authorize, params: { id: follower.id }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows follower to follow' do
|
||||||
|
expect(follower.following?(user.account)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST #reject' do
|
||||||
|
before do
|
||||||
|
post :reject, params: { id: follower.id }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes follow request' do
|
||||||
|
expect(FollowRequest.where(target_account: user.account, account: follower).count).to eq 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,6 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe AuthorizeFollowController, type: :controller do
|
||||||
|
describe 'GET #new'
|
||||||
|
describe 'POST #create'
|
||||||
|
end
|
@ -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
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue