Add account subscribe support to WebUI
This commit is contained in:
parent
3d6eaf638d
commit
9fdfc2d8d5
50 changed files with 658 additions and 85 deletions
|
@ -37,6 +37,7 @@ class AccountsIndex < Chewy::Index
|
|||
|
||||
field :following_count, type: 'long', value: ->(account) { account.following.local.count }
|
||||
field :followers_count, type: 'long', value: ->(account) { account.followers.local.count }
|
||||
field :subscribing_count, type: 'long', value: ->(account) { account.subscribing.local.count }
|
||||
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::AccountSubscribesController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:follows' }, only: [:index, :show]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:follows' }, except: [:index, :show]
|
||||
|
||||
before_action :require_user!
|
||||
before_action :set_account_subscribe, except: [:index, :create]
|
||||
|
||||
def index
|
||||
@account_subscribes = AccountSubscribe.where(account: current_account).all
|
||||
render json: @account_subscribes, each_serializer: REST::AccountSubscribeSerializer
|
||||
end
|
||||
|
||||
def show
|
||||
render json: @account_subscribe, serializer: REST::AccountSubscribeSerializer
|
||||
end
|
||||
|
||||
def create
|
||||
@account_subscribe = AccountSubscribe.create!(account_subscribe_params.merge(account: current_account))
|
||||
render json: @account_subscribe, serializer: REST::AccountSubscribeSerializer
|
||||
end
|
||||
|
||||
def update
|
||||
@account_subscribe.update!(account_subscribe_params)
|
||||
render json: @account_subscribe, serializer: REST::AccountSubscribeSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
@account_subscribe.destroy!
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account_subscribe
|
||||
@account_subscribe = AccountSubscribe.where(account: current_account).find(params[:id])
|
||||
end
|
||||
|
||||
def account_subscribe_params
|
||||
params.permit(:acct)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,64 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::SubscribingAccountsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
|
||||
before_action :require_user!
|
||||
after_action :insert_pagination_headers
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@accounts = load_accounts
|
||||
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_accounts
|
||||
default_accounts.merge(paginated_subscribings).to_a
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
Account.includes(:passive_subscribes, :account_stat).references(:passive_subscribes)
|
||||
end
|
||||
|
||||
def paginated_subscribings
|
||||
AccountSubscribe.where(account_id: current_user.account_id).paginate_by_max_id(
|
||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||
params[:max_id],
|
||||
params[:since_id]
|
||||
)
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
||||
def next_path
|
||||
if records_continue?
|
||||
api_v1_accounts_subscribing_index_url pagination_params(max_id: pagination_max_id)
|
||||
end
|
||||
end
|
||||
|
||||
def prev_path
|
||||
unless @accounts.empty?
|
||||
api_v1_accounts_subscribing_index_url pagination_params(since_id: pagination_since_id)
|
||||
end
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@accounts.last.passive_subscribes.first.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@accounts.first.passive_subscribes.first.id
|
||||
end
|
||||
|
||||
def records_continue?
|
||||
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(:limit).permit(:limit).merge(core_params)
|
||||
end
|
||||
end
|
|
@ -1,8 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::AccountsController < Api::BaseController
|
||||
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :block, :unblock, :mute, :unmute]
|
||||
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow]
|
||||
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :subscribe, :unsubscribe, :block, :unblock, :mute, :unmute]
|
||||
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow, :subscribe, :unsubscribe]
|
||||
before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute]
|
||||
before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
|
||||
|
@ -38,6 +38,11 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(**options)
|
||||
end
|
||||
|
||||
def subscribe
|
||||
AccountSubscribeService.new.call(current_user.account, @account)
|
||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
end
|
||||
|
||||
def block
|
||||
BlockService.new.call(current_user.account, @account)
|
||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
|
@ -53,6 +58,11 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
end
|
||||
|
||||
def unsubscribe
|
||||
UnsubscribeAccountService.new.call(current_user.account, @account)
|
||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
end
|
||||
|
||||
def unblock
|
||||
UnblockService.new.call(current_user.account, @account)
|
||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
|
|
|
@ -37,6 +37,7 @@ class Settings::PreferencesController < Settings::BaseController
|
|||
:setting_default_sensitive,
|
||||
:setting_default_language,
|
||||
:setting_unfollow_modal,
|
||||
:setting_unsubscribe_modal,
|
||||
:setting_boost_modal,
|
||||
:setting_delete_modal,
|
||||
:setting_auto_play_gif,
|
||||
|
|
|
@ -13,6 +13,14 @@ export const ACCOUNT_UNFOLLOW_REQUEST = 'ACCOUNT_UNFOLLOW_REQUEST';
|
|||
export const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS';
|
||||
export const ACCOUNT_UNFOLLOW_FAIL = 'ACCOUNT_UNFOLLOW_FAIL';
|
||||
|
||||
export const ACCOUNT_SUBSCRIBE_REQUEST = 'ACCOUNT_SUBSCRIBE_REQUEST';
|
||||
export const ACCOUNT_SUBSCRIBE_SUCCESS = 'ACCOUNT_SUBSCRIBE_SUCCESS';
|
||||
export const ACCOUNT_SUBSCRIBE_FAIL = 'ACCOUNT_SUBSCRIBE_FAIL';
|
||||
|
||||
export const ACCOUNT_UNSUBSCRIBE_REQUEST = 'ACCOUNT_UNSUBSCRIBE_REQUEST';
|
||||
export const ACCOUNT_UNSUBSCRIBE_SUCCESS = 'ACCOUNT_UNSUBSCRIBE_SUCCESS';
|
||||
export const ACCOUNT_UNSUBSCRIBE_FAIL = 'ACCOUNT_UNSUBSCRIBE_FAIL';
|
||||
|
||||
export const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST';
|
||||
export const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS';
|
||||
export const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL';
|
||||
|
@ -53,6 +61,14 @@ export const FOLLOWING_EXPAND_REQUEST = 'FOLLOWING_EXPAND_REQUEST';
|
|||
export const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS';
|
||||
export const FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL';
|
||||
|
||||
export const SUBSCRIBING_FETCH_REQUEST = 'SUBSCRIBING_FETCH_REQUEST';
|
||||
export const SUBSCRIBING_FETCH_SUCCESS = 'SUBSCRIBING_FETCH_SUCCESS';
|
||||
export const SUBSCRIBING_FETCH_FAIL = 'SUBSCRIBING_FETCH_FAIL';
|
||||
|
||||
export const SUBSCRIBING_EXPAND_REQUEST = 'SUBSCRIBING_EXPAND_REQUEST';
|
||||
export const SUBSCRIBING_EXPAND_SUCCESS = 'SUBSCRIBING_EXPAND_SUCCESS';
|
||||
export const SUBSCRIBING_EXPAND_FAIL = 'SUBSCRIBING_EXPAND_FAIL';
|
||||
|
||||
export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST';
|
||||
export const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS';
|
||||
export const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL';
|
||||
|
@ -188,6 +204,85 @@ export function unfollowAccountFail(error) {
|
|||
};
|
||||
};
|
||||
|
||||
export function subscribeAccount(id, reblogs = true) {
|
||||
return (dispatch, getState) => {
|
||||
const alreadySubscribe = getState().getIn(['relationships', id, 'subscribing']);
|
||||
const locked = getState().getIn(['accounts', id, 'locked'], false);
|
||||
|
||||
dispatch(subscribeAccountRequest(id, locked));
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/subscribe`).then(response => {
|
||||
dispatch(subscribeAccountSuccess(response.data, alreadySubscribe));
|
||||
}).catch(error => {
|
||||
dispatch(subscribeAccountFail(error, locked));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function unsubscribeAccount(id) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(unsubscribeAccountRequest(id));
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/unsubscribe`).then(response => {
|
||||
dispatch(unsubscribeAccountSuccess(response.data, getState().get('statuses')));
|
||||
}).catch(error => {
|
||||
dispatch(unsubscribeAccountFail(error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function subscribeAccountRequest(id, locked) {
|
||||
return {
|
||||
type: ACCOUNT_SUBSCRIBE_REQUEST,
|
||||
id,
|
||||
locked,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
export function subscribeAccountSuccess(relationship, alreadySubscribe) {
|
||||
return {
|
||||
type: ACCOUNT_SUBSCRIBE_SUCCESS,
|
||||
relationship,
|
||||
alreadySubscribe,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
export function subscribeAccountFail(error, locked) {
|
||||
return {
|
||||
type: ACCOUNT_SUBSCRIBE_FAIL,
|
||||
error,
|
||||
locked,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
export function unsubscribeAccountRequest(id) {
|
||||
return {
|
||||
type: ACCOUNT_UNSUBSCRIBE_REQUEST,
|
||||
id,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
export function unsubscribeAccountSuccess(relationship, statuses) {
|
||||
return {
|
||||
type: ACCOUNT_UNSUBSCRIBE_SUCCESS,
|
||||
relationship,
|
||||
statuses,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
export function unsubscribeAccountFail(error) {
|
||||
return {
|
||||
type: ACCOUNT_UNSUBSCRIBE_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
};
|
||||
};
|
||||
|
||||
export function blockAccount(id) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(blockAccountRequest(id));
|
||||
|
@ -500,6 +595,92 @@ export function expandFollowingFail(id, error) {
|
|||
};
|
||||
};
|
||||
|
||||
export function fetchSubscribing(id) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(fetchSubscribeRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/subscribing`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchSubscribeSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(fetchSubscribeFail(id, error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchSubscribeRequest(id) {
|
||||
return {
|
||||
type: SUBSCRIBING_FETCH_REQUEST,
|
||||
id,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchSubscribeSuccess(id, accounts, next) {
|
||||
return {
|
||||
type: SUBSCRIBING_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchSubscribeFail(id, error) {
|
||||
return {
|
||||
type: SUBSCRIBING_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandSubscribing(id) {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['user_lists', 'subscribing', id, 'next']);
|
||||
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandSubscribeRequest(id));
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandSubscribeSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(expandSubscribeFail(id, error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function expandSubscribeRequest(id) {
|
||||
return {
|
||||
type: SUBSCRIBING_EXPAND_REQUEST,
|
||||
id,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandSubscribeSuccess(id, accounts, next) {
|
||||
return {
|
||||
type: SUBSCRIBING_EXPAND_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandSubscribeFail(id, error) {
|
||||
return {
|
||||
type: SUBSCRIBING_EXPAND_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchRelationships(accountIds) {
|
||||
return (dispatch, getState) => {
|
||||
const loadedRelationships = getState().get('relationships');
|
||||
|
|
|
@ -13,6 +13,8 @@ import RelativeTimestamp from './relative_timestamp';
|
|||
const messages = defineMessages({
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe' },
|
||||
subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe' },
|
||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||
|
@ -26,6 +28,7 @@ class Account extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onSubscribe: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
onMuteNotifications: PropTypes.func.isRequired,
|
||||
|
@ -40,6 +43,10 @@ class Account extends ImmutablePureComponent {
|
|||
this.props.onFollow(this.props.account);
|
||||
}
|
||||
|
||||
handleSubscribe = () => {
|
||||
this.props.onSubscribe(this.props.account);
|
||||
}
|
||||
|
||||
handleBlock = () => {
|
||||
this.props.onBlock(this.props.account);
|
||||
}
|
||||
|
@ -84,6 +91,7 @@ class Account extends ImmutablePureComponent {
|
|||
}
|
||||
} else if (account.get('id') !== me && account.get('relationship', null) !== null) {
|
||||
const following = account.getIn(['relationship', 'following']);
|
||||
const subscribing = account.getIn(['relationship', 'subscribing']);
|
||||
const requested = account.getIn(['relationship', 'requested']);
|
||||
const blocking = account.getIn(['relationship', 'blocking']);
|
||||
const muting = account.getIn(['relationship', 'muting']);
|
||||
|
@ -105,8 +113,15 @@ class Account extends ImmutablePureComponent {
|
|||
{hidingNotificationsButton}
|
||||
</Fragment>
|
||||
);
|
||||
} else if (!account.get('moved') || following) {
|
||||
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
||||
} else {
|
||||
let following_buttons, subscribing_buttons;
|
||||
if (!account.get('moved') || subscribing ) {
|
||||
subscribing_buttons = <IconButton icon='rss-square' title={intl.formatMessage(subscribing ? messages.unsubscribe : messages.subscribe)} onClick={this.handleSubscribe} active={subscribing} />;
|
||||
}
|
||||
if (!account.get('moved') || following) {
|
||||
following_buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
||||
}
|
||||
buttons = <span>{subscribing_buttons}{following_buttons}</span>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ import Account from '../components/account';
|
|||
import {
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
subscribeAccount,
|
||||
unsubscribeAccount,
|
||||
blockAccount,
|
||||
unblockAccount,
|
||||
muteAccount,
|
||||
|
@ -13,10 +15,11 @@ import {
|
|||
} from '../actions/accounts';
|
||||
import { openModal } from '../actions/modal';
|
||||
import { initMuteModal } from '../actions/mutes';
|
||||
import { unfollowModal } from '../initial_state';
|
||||
import { unfollowModal, unsubscribeModal } from '../initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||
unsubscribeConfirm: { id: 'confirmations.unsubscribe.confirm', defaultMessage: 'Unsubscribe' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
|
@ -47,6 +50,22 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
}
|
||||
},
|
||||
|
||||
onSubscribe (account) {
|
||||
if (account.getIn(['relationship', 'subscribing'])) {
|
||||
if (unsubscribeModal) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.unsubscribe.message' defaultMessage='Are you sure you want to unsubscribe {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.unsubscribeConfirm),
|
||||
onConfirm: () => dispatch(unsubscribeAccount(account.get('id'))),
|
||||
}));
|
||||
} else {
|
||||
dispatch(unsubscribeAccount(account.get('id')));
|
||||
}
|
||||
} else {
|
||||
dispatch(subscribeAccount(account.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onBlock (account) {
|
||||
if (account.getIn(['relationship', 'blocking'])) {
|
||||
dispatch(unblockAccount(account.get('id')));
|
||||
|
|
|
@ -18,6 +18,8 @@ import AccountNoteContainer from '../containers/account_note_container';
|
|||
const messages = defineMessages({
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe' },
|
||||
subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe' },
|
||||
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' },
|
||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
|
@ -68,6 +70,7 @@ class Header extends ImmutablePureComponent {
|
|||
account: ImmutablePropTypes.map,
|
||||
identity_props: ImmutablePropTypes.list,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onSubscribe: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMention: PropTypes.func.isRequired,
|
||||
onDirect: PropTypes.func.isRequired,
|
||||
|
@ -260,6 +263,22 @@ class Header extends ImmutablePureComponent {
|
|||
badge = null;
|
||||
}
|
||||
|
||||
const following = account.getIn(['relationship', 'following']);
|
||||
const subscribing = account.getIn(['relationship', 'subscribing']);
|
||||
const blockd_by = account.getIn(['relationship', 'blocked_by']);
|
||||
let buttons;
|
||||
|
||||
if(me !== account.get('id') && !blockd_by) {
|
||||
let following_buttons, subscribing_buttons;
|
||||
if(!account.get('moved') || subscribing) {
|
||||
subscribing_buttons = <IconButton icon='rss-square' title={intl.formatMessage(subscribing ? messages.unsubscribe : messages.subscribe)} onClick={this.props.onSubscribe} active={subscribing} />;
|
||||
}
|
||||
if(!account.get('moved') || following) {
|
||||
following_buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} active={following} />;
|
||||
}
|
||||
buttons = <span>{subscribing_buttons}{following_buttons}</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||
<div className='account__header__image'>
|
||||
|
@ -293,6 +312,9 @@ class Header extends ImmutablePureComponent {
|
|||
<span dangerouslySetInnerHTML={displayNameHtml} /> {badge}
|
||||
<small>@{acct} {lockedIcon}</small>
|
||||
</h1>
|
||||
<div className='account__header__tabs__name__relationship account__relationship'>
|
||||
{buttons}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='account__header__extra'>
|
||||
|
@ -352,6 +374,12 @@ class Header extends ImmutablePureComponent {
|
|||
renderer={counterRenderer('followers')}
|
||||
/>
|
||||
</NavLink>
|
||||
|
||||
{ (me === account.get('id')) && (
|
||||
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/subscribing`} title={intl.formatNumber(account.get('subscribing_count'))}>
|
||||
<strong>{shortNumberFormat(account.get('subscribing_count'))}</strong> <FormattedMessage id='account.subscribes' defaultMessage='Subscribes' />
|
||||
</NavLink>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -13,6 +13,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
account: ImmutablePropTypes.map,
|
||||
identity_proofs: ImmutablePropTypes.list,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onSubscribe: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMention: PropTypes.func.isRequired,
|
||||
onDirect: PropTypes.func.isRequired,
|
||||
|
@ -35,6 +36,10 @@ export default class Header extends ImmutablePureComponent {
|
|||
this.props.onFollow(this.props.account);
|
||||
}
|
||||
|
||||
handleSubscribe = () => {
|
||||
this.props.onSubscribe(this.props.account);
|
||||
}
|
||||
|
||||
handleBlock = () => {
|
||||
this.props.onBlock(this.props.account);
|
||||
}
|
||||
|
@ -106,6 +111,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
account={account}
|
||||
identity_proofs={identity_proofs}
|
||||
onFollow={this.handleFollow}
|
||||
onSubscribe={this.handleSubscribe}
|
||||
onBlock={this.handleBlock}
|
||||
onMention={this.handleMention}
|
||||
onDirect={this.handleDirect}
|
||||
|
|
|
@ -5,6 +5,8 @@ import Header from '../components/header';
|
|||
import {
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
subscribeAccount,
|
||||
unsubscribeAccount,
|
||||
unblockAccount,
|
||||
unmuteAccount,
|
||||
pinAccount,
|
||||
|
@ -20,11 +22,12 @@ import { initReport } from '../../../actions/reports';
|
|||
import { openModal } from '../../../actions/modal';
|
||||
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { unfollowModal } from '../../../initial_state';
|
||||
import { unfollowModal, unsubscribeModal } from '../../../initial_state';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||
unsubscribeConfirm: { id: 'confirmations.unsubscribe.confirm', defaultMessage: 'Unsubscribe' },
|
||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
||||
});
|
||||
|
||||
|
@ -58,6 +61,22 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
}
|
||||
},
|
||||
|
||||
onSubscribe (account) {
|
||||
if (account.getIn(['relationship', 'subscribing'])) {
|
||||
if (unsubscribeModal) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.unsubscribe.message' defaultMessage='Are you sure you want to unsubscribe {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.unsubscribeConfirm),
|
||||
onConfirm: () => dispatch(unsubscribeAccount(account.get('id'))),
|
||||
}));
|
||||
} else {
|
||||
dispatch(unsubscribeAccount(account.get('id')));
|
||||
}
|
||||
} else {
|
||||
dispatch(subscribeAccount(account.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onBlock (account) {
|
||||
if (account.getIn(['relationship', 'blocking'])) {
|
||||
dispatch(unblockAccount(account.get('id')));
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -10,14 +10,16 @@ import Permalink from 'mastodon/components/permalink';
|
|||
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
|
||||
import { autoPlayGif, me, unfollowModal, unsubscribeModal } from 'mastodon/initial_state';
|
||||
import ShortNumber from 'mastodon/components/short_number';
|
||||
import {
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
subscribeAccount,
|
||||
unsubscribeAccount,
|
||||
blockAccount,
|
||||
unblockAccount,
|
||||
unmuteAccount,
|
||||
unmuteAccount
|
||||
} from 'mastodon/actions/accounts';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { initMuteModal } from 'mastodon/actions/mutes';
|
||||
|
@ -25,6 +27,8 @@ import { initMuteModal } from 'mastodon/actions/mutes';
|
|||
const messages = defineMessages({
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe' },
|
||||
subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe' },
|
||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||
|
@ -72,6 +76,22 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
}
|
||||
},
|
||||
|
||||
onSubscribe(account) {
|
||||
if (account.getIn(['relationship', 'subscribing'])) {
|
||||
if (unsubscribeModal) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: <FormattedMessage id='confirmations.unsubscribe.message' defaultMessage='Are you sure you want to unsubscribe {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.unsubscribeConfirm),
|
||||
onConfirm: () => dispatch(unsubscribeAccount(account.get('id'))),
|
||||
}));
|
||||
} else {
|
||||
dispatch(unsubscribeAccount(account.get('id')));
|
||||
}
|
||||
} else {
|
||||
dispatch(subscribeAccount(account.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onBlock(account) {
|
||||
if (account.getIn(['relationship', 'blocking'])) {
|
||||
dispatch(unblockAccount(account.get('id')));
|
||||
|
@ -98,6 +118,7 @@ class AccountCard extends ImmutablePureComponent {
|
|||
account: ImmutablePropTypes.map.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onSubscribe: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
};
|
||||
|
@ -132,6 +153,10 @@ class AccountCard extends ImmutablePureComponent {
|
|||
this.props.onFollow(this.props.account);
|
||||
};
|
||||
|
||||
handleSubscribe = () => {
|
||||
this.props.onSubscribe(this.props.account);
|
||||
}
|
||||
|
||||
handleBlock = () => {
|
||||
this.props.onBlock(this.props.account);
|
||||
};
|
||||
|
@ -150,6 +175,7 @@ class AccountCard extends ImmutablePureComponent {
|
|||
account.get('relationship', null) !== null
|
||||
) {
|
||||
const following = account.getIn(['relationship', 'following']);
|
||||
const subscribing = account.getIn(['relationship', 'subscribing']);
|
||||
const requested = account.getIn(['relationship', 'requested']);
|
||||
const blocking = account.getIn(['relationship', 'blocking']);
|
||||
const muting = account.getIn(['relationship', 'muting']);
|
||||
|
@ -179,23 +205,39 @@ class AccountCard extends ImmutablePureComponent {
|
|||
active
|
||||
icon='volume-up'
|
||||
title={intl.formatMessage(messages.unmute, {
|
||||
name: account.get('username'),
|
||||
name: account.get('username')
|
||||
})}
|
||||
onClick={this.handleMute}
|
||||
/>
|
||||
);
|
||||
} else if (!account.get('moved') || following) {
|
||||
buttons = (
|
||||
} else {
|
||||
let following_buttons, subscribing_buttons;
|
||||
if(!account.get('moved') || subscribing) {
|
||||
subscribing_buttons = (
|
||||
<IconButton
|
||||
icon='rss-square'
|
||||
title={intl.formatMessage(
|
||||
subscribing ? messages.unsubscribe : messages.subscribe
|
||||
)}
|
||||
onClick={this.handleSubscribe}
|
||||
active={subscribing}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if(!account.get('moved') || following) {
|
||||
following_buttons = (
|
||||
<IconButton
|
||||
icon={following ? 'user-times' : 'user-plus'}
|
||||
title={intl.formatMessage(
|
||||
following ? messages.unfollow : messages.follow,
|
||||
following ? messages.unfollow : messages.follow
|
||||
)}
|
||||
onClick={this.handleFollow}
|
||||
active={following}
|
||||
/>
|
||||
);
|
||||
}
|
||||
buttons = <Fragment>{subscribing_buttons}{following_buttons}</Fragment>
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
103
app/javascript/mastodon/features/subscribing/index.js
Normal file
103
app/javascript/mastodon/features/subscribing/index.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { debounce } from 'lodash';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import {
|
||||
fetchAccount,
|
||||
fetchSubscribing,
|
||||
expandSubscribing,
|
||||
} from '../../actions/accounts';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
import Column from '../ui/components/column';
|
||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
||||
import ColumnBackButton from '../../components/column_back_button';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import MissingIndicator from 'mastodon/components/missing_indicator';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
||||
accountIds: state.getIn(['user_lists', 'subscribing', props.params.accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'subscribing', props.params.accountId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'subscribing', props.params.accountId, 'isLoading'], true),
|
||||
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Subscribing extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
blockedBy: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
if (!this.props.accountIds) {
|
||||
this.props.dispatch(fetchAccount(this.props.params.accountId));
|
||||
this.props.dispatch(fetchSubscribing(this.props.params.accountId));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
|
||||
this.props.dispatch(fetchAccount(nextProps.params.accountId));
|
||||
this.props.dispatch(fetchSubscribing(nextProps.params.accountId));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandSubscribing());
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading } = this.props;
|
||||
|
||||
if (!isAccount) {
|
||||
return (
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = blockedBy ? <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' /> : <FormattedMessage id='account.subscribes.empty' defaultMessage="This user doesn't subscribe anyone yet." />;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton multiColumn={multiColumn} />
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='subscribing'
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
|
||||
alwaysPrepend
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{blockedBy ? [] : accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} withNote={false} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -35,6 +35,7 @@ import {
|
|||
HomeTimeline,
|
||||
Followers,
|
||||
Following,
|
||||
Subscribing,
|
||||
Reblogs,
|
||||
Favourites,
|
||||
DirectTimeline,
|
||||
|
@ -179,6 +180,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
|
||||
<WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} />
|
||||
<WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} />
|
||||
<WrappedRoute path='/accounts/:accountId/subscribing' component={Subscribing} content={children} />
|
||||
<WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} />
|
||||
|
||||
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
|
||||
|
|
|
@ -74,6 +74,10 @@ export function Following () {
|
|||
return import(/* webpackChunkName: "features/following" */'../../following');
|
||||
}
|
||||
|
||||
export function Subscribing () {
|
||||
return import(/* webpackChunkName: "features/subscribing" */'../../subscribing');
|
||||
}
|
||||
|
||||
export function Reblogs () {
|
||||
return import(/* webpackChunkName: "features/reblogs" */'../../reblogs');
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ export const autoPlayGif = getMeta('auto_play_gif');
|
|||
export const displayMedia = getMeta('display_media');
|
||||
export const expandSpoilers = getMeta('expand_spoilers');
|
||||
export const unfollowModal = getMeta('unfollow_modal');
|
||||
export const unsubscribeModal = getMeta('unsubscribe_modal');
|
||||
export const boostModal = getMeta('boost_modal');
|
||||
export const deleteModal = getMeta('delete_modal');
|
||||
export const me = getMeta('me');
|
||||
|
|
|
@ -41,10 +41,14 @@
|
|||
"account.share": "Share @{name}'s profile",
|
||||
"account.show_reblogs": "Show boosts from @{name}",
|
||||
"account.statuses_counter": "{count, plural, one {{counter} Post} other {{counter} Posts}}",
|
||||
"account.subscribe": "Subscribe",
|
||||
"account.subscribes": "Subscribes",
|
||||
"account.subscribes.empty": "This user doesn't subscribe anyone yet.",
|
||||
"account.unblock": "Unblock @{name}",
|
||||
"account.unblock_domain": "Unblock domain {domain}",
|
||||
"account.unendorse": "Don't feature on profile",
|
||||
"account.unfollow": "Unfollow",
|
||||
"account.unsubscribe": "Unsubscribe",
|
||||
"account.unmute": "Unmute @{name}",
|
||||
"account.unmute_notifications": "Unmute notifications from @{name}",
|
||||
"account_note.placeholder": "Click to add note",
|
||||
|
@ -129,6 +133,8 @@
|
|||
"confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
|
||||
"confirmations.unfollow.confirm": "Unfollow",
|
||||
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||
"confirmations.unsubscribe.confirm": "Unsubscribe",
|
||||
"confirmations.unsubscribe.message": "Are you sure you want to unsubscribe {name}?",
|
||||
"conversation.delete": "Delete conversation",
|
||||
"conversation.mark_as_read": "Mark as read",
|
||||
"conversation.open": "View conversation",
|
||||
|
|
|
@ -41,10 +41,14 @@
|
|||
"account.share": "@{name}さんのプロフィールを共有する",
|
||||
"account.show_reblogs": "@{name}さんからのブーストを表示",
|
||||
"account.statuses_counter": "{counter} 投稿",
|
||||
"account.subscribe": "購読",
|
||||
"account.subscribes": "購読",
|
||||
"account.subscribes.empty": "まだ誰も購読していません。",
|
||||
"account.unblock": "@{name}さんのブロックを解除",
|
||||
"account.unblock_domain": "{domain}のブロックを解除",
|
||||
"account.unendorse": "プロフィールから外す",
|
||||
"account.unfollow": "フォロー解除",
|
||||
"account.unsubscribe": "購読解除",
|
||||
"account.unmute": "@{name}さんのミュートを解除",
|
||||
"account.unmute_notifications": "@{name}さんからの通知を受け取るようにする",
|
||||
"account_note.placeholder": "クリックしてメモを追加",
|
||||
|
@ -129,6 +133,8 @@
|
|||
"confirmations.reply.message": "今返信すると現在作成中のメッセージが上書きされます。本当に実行しますか?",
|
||||
"confirmations.unfollow.confirm": "フォロー解除",
|
||||
"confirmations.unfollow.message": "本当に{name}さんのフォローを解除しますか?",
|
||||
"confirmations.unsubscribe.confirm": "購読解除",
|
||||
"confirmations.unsubscribe.message": "本当に{name}さんの購読を解除しますか?",
|
||||
"conversation.delete": "会話を削除",
|
||||
"conversation.mark_as_read": "既読にする",
|
||||
"conversation.open": "会話を表示",
|
||||
|
|
|
@ -8,6 +8,7 @@ const normalizeAccount = (state, account) => {
|
|||
|
||||
delete account.followers_count;
|
||||
delete account.following_count;
|
||||
delete account.subscribing_count;
|
||||
delete account.statuses_count;
|
||||
|
||||
return state.set(account.id, fromJS(account));
|
||||
|
|
|
@ -8,6 +8,7 @@ import { Map as ImmutableMap, fromJS } from 'immutable';
|
|||
const normalizeAccount = (state, account) => state.set(account.id, fromJS({
|
||||
followers_count: account.followers_count,
|
||||
following_count: account.following_count,
|
||||
subscribing_count: account.subscribing_count,
|
||||
statuses_count: account.statuses_count,
|
||||
}));
|
||||
|
||||
|
|
|
@ -5,6 +5,12 @@ import {
|
|||
ACCOUNT_UNFOLLOW_SUCCESS,
|
||||
ACCOUNT_UNFOLLOW_REQUEST,
|
||||
ACCOUNT_UNFOLLOW_FAIL,
|
||||
ACCOUNT_SUBSCRIBE_SUCCESS,
|
||||
ACCOUNT_SUBSCRIBE_REQUEST,
|
||||
ACCOUNT_SUBSCRIBE_FAIL,
|
||||
ACCOUNT_UNSUBSCRIBE_SUCCESS,
|
||||
ACCOUNT_UNSUBSCRIBE_REQUEST,
|
||||
ACCOUNT_UNSUBSCRIBE_FAIL,
|
||||
ACCOUNT_BLOCK_SUCCESS,
|
||||
ACCOUNT_UNBLOCK_SUCCESS,
|
||||
ACCOUNT_MUTE_SUCCESS,
|
||||
|
@ -52,8 +58,18 @@ export default function relationships(state = initialState, action) {
|
|||
return state.setIn([action.id, 'following'], false);
|
||||
case ACCOUNT_UNFOLLOW_FAIL:
|
||||
return state.setIn([action.id, 'following'], true);
|
||||
case ACCOUNT_SUBSCRIBE_REQUEST:
|
||||
return state.setIn([action.id, 'subscribing'], true);
|
||||
case ACCOUNT_SUBSCRIBE_FAIL:
|
||||
return state.setIn([action.id, 'subscribing'], false);
|
||||
case ACCOUNT_UNSUBSCRIBE_REQUEST:
|
||||
return state.setIn([action.id, 'subscribing'], false);
|
||||
case ACCOUNT_UNSUBSCRIBE_FAIL:
|
||||
return state.setIn([action.id, 'subscribing'], true);
|
||||
case ACCOUNT_FOLLOW_SUCCESS:
|
||||
case ACCOUNT_UNFOLLOW_SUCCESS:
|
||||
case ACCOUNT_SUBSCRIBE_SUCCESS:
|
||||
case ACCOUNT_UNSUBSCRIBE_SUCCESS:
|
||||
case ACCOUNT_BLOCK_SUCCESS:
|
||||
case ACCOUNT_UNBLOCK_SUCCESS:
|
||||
case ACCOUNT_MUTE_SUCCESS:
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
ACCOUNT_BLOCK_SUCCESS,
|
||||
ACCOUNT_MUTE_SUCCESS,
|
||||
ACCOUNT_UNFOLLOW_SUCCESS,
|
||||
ACCOUNT_UNSUBSCRIBE_SUCCESS,
|
||||
} from '../actions/accounts';
|
||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
import compareId from '../compare_id';
|
||||
|
@ -158,6 +159,7 @@ export default function timelines(state = initialState, action) {
|
|||
case ACCOUNT_MUTE_SUCCESS:
|
||||
return filterTimelines(state, action.relationship, action.statuses);
|
||||
case ACCOUNT_UNFOLLOW_SUCCESS:
|
||||
case ACCOUNT_UNSUBSCRIBE_SUCCESS:
|
||||
return filterTimeline('home', state, action.relationship, action.statuses);
|
||||
case TIMELINE_SCROLL_TOP:
|
||||
return updateTop(state, action.timeline, action.top);
|
||||
|
|
|
@ -15,6 +15,12 @@ import {
|
|||
FOLLOWING_EXPAND_SUCCESS,
|
||||
FOLLOWING_EXPAND_FAIL,
|
||||
FOLLOW_REQUESTS_FETCH_REQUEST,
|
||||
SUBSCRIBING_FETCH_REQUEST,
|
||||
SUBSCRIBING_FETCH_SUCCESS,
|
||||
SUBSCRIBING_FETCH_FAIL,
|
||||
SUBSCRIBING_EXPAND_REQUEST,
|
||||
SUBSCRIBING_EXPAND_SUCCESS,
|
||||
SUBSCRIBING_EXPAND_FAIL,
|
||||
FOLLOW_REQUESTS_FETCH_SUCCESS,
|
||||
FOLLOW_REQUESTS_FETCH_FAIL,
|
||||
FOLLOW_REQUESTS_EXPAND_REQUEST,
|
||||
|
@ -62,6 +68,7 @@ const initialListState = ImmutableMap({
|
|||
const initialState = ImmutableMap({
|
||||
followers: initialListState,
|
||||
following: initialListState,
|
||||
subscribing: initialListState,
|
||||
reblogged_by: initialListState,
|
||||
favourited_by: initialListState,
|
||||
follow_requests: initialListState,
|
||||
|
@ -111,6 +118,16 @@ export default function userLists(state = initialState, action) {
|
|||
case FOLLOWING_FETCH_FAIL:
|
||||
case FOLLOWING_EXPAND_FAIL:
|
||||
return state.setIn(['following', action.id, 'isLoading'], false);
|
||||
case SUBSCRIBING_FETCH_SUCCESS:
|
||||
return normalizeList(state, 'subscribing', action.id, action.accounts, action.next);
|
||||
case SUBSCRIBING_EXPAND_SUCCESS:
|
||||
return appendToList(state, 'subscribing', action.id, action.accounts, action.next);
|
||||
case SUBSCRIBING_FETCH_REQUEST:
|
||||
case SUBSCRIBING_EXPAND_REQUEST:
|
||||
return state.setIn(['subscribing', action.id, 'isLoading'], true);
|
||||
case SUBSCRIBING_FETCH_FAIL:
|
||||
case SUBSCRIBING_EXPAND_FAIL:
|
||||
return state.setIn(['subscribing', action.id, 'isLoading'], false);
|
||||
case REBLOGS_FETCH_SUCCESS:
|
||||
return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
|
||||
case FAVOURITES_FETCH_SUCCESS:
|
||||
|
|
|
@ -5991,7 +5991,7 @@ a.status-card.compact:hover {
|
|||
}
|
||||
|
||||
&__relationship {
|
||||
width: 23px;
|
||||
width: 46px;
|
||||
min-height: 1px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
@ -6761,6 +6761,7 @@ noscript {
|
|||
|
||||
&__name {
|
||||
padding: 5px 10px;
|
||||
display: flex;
|
||||
|
||||
.account-role {
|
||||
vertical-align: top;
|
||||
|
@ -6779,6 +6780,7 @@ noscript {
|
|||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1 1 auto;
|
||||
|
||||
small {
|
||||
display: block;
|
||||
|
@ -6789,6 +6791,9 @@ noscript {
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
&__relationship {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
|
|
|
@ -21,6 +21,7 @@ class UserSettingsDecorator
|
|||
user.settings['default_sensitive'] = default_sensitive_preference if change?('setting_default_sensitive')
|
||||
user.settings['default_language'] = default_language_preference if change?('setting_default_language')
|
||||
user.settings['unfollow_modal'] = unfollow_modal_preference if change?('setting_unfollow_modal')
|
||||
user.settings['unsubscribe_modal'] = unsubscribe_modal_preference if change?('setting_unsubscribe_modal')
|
||||
user.settings['boost_modal'] = boost_modal_preference if change?('setting_boost_modal')
|
||||
user.settings['delete_modal'] = delete_modal_preference if change?('setting_delete_modal')
|
||||
user.settings['auto_play_gif'] = auto_play_gif_preference if change?('setting_auto_play_gif')
|
||||
|
@ -61,6 +62,10 @@ class UserSettingsDecorator
|
|||
boolean_cast_setting 'setting_unfollow_modal'
|
||||
end
|
||||
|
||||
def unsubscribe_modal_preference
|
||||
boolean_cast_setting 'setting_unsubscribe_modal'
|
||||
end
|
||||
|
||||
def boost_modal_preference
|
||||
boolean_cast_setting 'setting_boost_modal'
|
||||
end
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
# statuses_count :bigint(8) default(0), not null
|
||||
# following_count :bigint(8) default(0), not null
|
||||
# followers_count :bigint(8) default(0), not null
|
||||
# subscribing_count :bigint(8) default(0), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# last_status_at :datetime
|
||||
|
|
|
@ -11,6 +11,9 @@
|
|||
#
|
||||
|
||||
class AccountSubscribe < ApplicationRecord
|
||||
include Paginable
|
||||
include RelationshipCacheable
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :target_account, class_name: 'Account'
|
||||
belongs_to :list, optional: true
|
||||
|
@ -20,4 +23,17 @@ class AccountSubscribe < ApplicationRecord
|
|||
scope :recent, -> { reorder(id: :desc) }
|
||||
scope :subscribed_lists, ->(account) { AccountSubscribe.where(target_account_id: account.id).where.not(list_id: nil).select(:list_id).uniq }
|
||||
|
||||
after_create :increment_cache_counters
|
||||
after_destroy :decrement_cache_counters
|
||||
|
||||
private
|
||||
|
||||
def increment_cache_counters
|
||||
account&.increment_count!(:subscribing_count)
|
||||
end
|
||||
|
||||
def decrement_cache_counters
|
||||
account&.decrement_count!(:subscribing_count)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -16,6 +16,8 @@ module AccountCounters
|
|||
:following_count=,
|
||||
:followers_count,
|
||||
:followers_count=,
|
||||
:subscribing_count,
|
||||
:subscribing_count=,
|
||||
:last_status_at,
|
||||
to: :account_stat
|
||||
|
||||
|
|
|
@ -17,6 +17,10 @@ module AccountInteractions
|
|||
follow_mapping(Follow.where(account_id: target_account_ids, target_account_id: account_id), :account_id)
|
||||
end
|
||||
|
||||
def subscribing_map(target_account_ids, account_id)
|
||||
follow_mapping(AccountSubscribe.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
|
||||
end
|
||||
|
||||
def blocking_map(target_account_ids, account_id)
|
||||
follow_mapping(Block.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
|
||||
end
|
||||
|
@ -191,7 +195,11 @@ module AccountInteractions
|
|||
end
|
||||
|
||||
def subscribe!(other_account)
|
||||
active_subscribes.find_or_create_by!(target_account: other_account)
|
||||
rel = active_subscribes.find_or_create_by!(target_account: other_account)
|
||||
|
||||
remove_potential_friendship(other_account)
|
||||
|
||||
rel
|
||||
end
|
||||
|
||||
def following?(other_account)
|
||||
|
|
|
@ -122,6 +122,7 @@ module StatusThreadingConcern
|
|||
blocked_by: Account.blocked_by_map(account_ids, account.id),
|
||||
muting: Account.muting_map(account_ids, account.id),
|
||||
following: Account.following_map(account_ids, account.id),
|
||||
subscribing: Account.subscribing_map(account_ids, account.id),
|
||||
domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id),
|
||||
}
|
||||
end
|
||||
|
|
|
@ -71,6 +71,10 @@ class Export
|
|||
account.following_count
|
||||
end
|
||||
|
||||
def total_subscribes
|
||||
account.subscribing_count
|
||||
end
|
||||
|
||||
def total_lists
|
||||
account.owned_lists.count
|
||||
end
|
||||
|
|
|
@ -121,7 +121,7 @@ class User < ApplicationRecord
|
|||
|
||||
has_many :session_activations, dependent: :destroy
|
||||
|
||||
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal,
|
||||
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :unsubscribe_modal, :boost_modal, :delete_modal,
|
||||
:reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network,
|
||||
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
|
||||
:advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AccountRelationshipsPresenter
|
||||
attr_reader :following, :followed_by, :blocking, :blocked_by,
|
||||
attr_reader :following, :followed_by, :subscribing, :blocking, :blocked_by,
|
||||
:muting, :requested, :domain_blocking,
|
||||
:endorsed, :account_note
|
||||
|
||||
|
@ -11,6 +11,7 @@ class AccountRelationshipsPresenter
|
|||
|
||||
@following = cached[:following].merge(Account.following_map(@uncached_account_ids, @current_account_id))
|
||||
@followed_by = cached[:followed_by].merge(Account.followed_by_map(@uncached_account_ids, @current_account_id))
|
||||
@subscribing = cached[:subscribing].merge(Account.subscribing_map(@uncached_account_ids, @current_account_id))
|
||||
@blocking = cached[:blocking].merge(Account.blocking_map(@uncached_account_ids, @current_account_id))
|
||||
@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))
|
||||
|
@ -23,6 +24,7 @@ class AccountRelationshipsPresenter
|
|||
|
||||
@following.merge!(options[:following_map] || {})
|
||||
@followed_by.merge!(options[:followed_by_map] || {})
|
||||
@subscribing.merge!(options[:subscribing_map] || {})
|
||||
@blocking.merge!(options[:blocking_map] || {})
|
||||
@blocked_by.merge!(options[:blocked_by_map] || {})
|
||||
@muting.merge!(options[:muting_map] || {})
|
||||
|
@ -40,6 +42,7 @@ class AccountRelationshipsPresenter
|
|||
@cached = {
|
||||
following: {},
|
||||
followed_by: {},
|
||||
subscribing: {},
|
||||
blocking: {},
|
||||
blocked_by: {},
|
||||
muting: {},
|
||||
|
@ -69,6 +72,7 @@ class AccountRelationshipsPresenter
|
|||
maps_for_account = {
|
||||
following: { account_id => following[account_id] },
|
||||
followed_by: { account_id => followed_by[account_id] },
|
||||
subscribing: { account_id => subscribing[account_id] },
|
||||
blocking: { account_id => blocking[account_id] },
|
||||
blocked_by: { account_id => blocked_by[account_id] },
|
||||
muting: { account_id => muting[account_id] },
|
||||
|
|
|
@ -28,6 +28,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||
if object.current_account
|
||||
store[:me] = object.current_account.id.to_s
|
||||
store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal
|
||||
store[:unsubscribe_modal] = object.current_account.user.setting_unsubscribe_modal
|
||||
store[:boost_modal] = object.current_account.user.setting_boost_modal
|
||||
store[:delete_modal] = object.current_account.user.setting_delete_modal
|
||||
store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif
|
||||
|
|
|
@ -5,7 +5,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
|||
|
||||
attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :group, :created_at,
|
||||
:note, :url, :avatar, :avatar_static, :header, :header_static,
|
||||
:followers_count, :following_count, :statuses_count, :last_status_at
|
||||
:followers_count, :following_count, :subscribing_count, :statuses_count, :last_status_at
|
||||
|
||||
has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::RelationshipSerializer < ActiveModel::Serializer
|
||||
attributes :id, :following, :showing_reblogs, :notifying, :followed_by,
|
||||
attributes :id, :following, :showing_reblogs, :notifying, :followed_by, :subscribing,
|
||||
:blocking, :blocked_by, :muting, :muting_notifications, :requested,
|
||||
:domain_blocking, :endorsed, :note
|
||||
|
||||
|
@ -29,6 +29,10 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
|
|||
instance_options[:relationships].followed_by[object.id] || false
|
||||
end
|
||||
|
||||
def subscribing
|
||||
instance_options[:relationships].subscribing[object.id] ? true : false
|
||||
end
|
||||
|
||||
def blocking
|
||||
instance_options[:relationships].blocking[object.id] || false
|
||||
end
|
||||
|
|
|
@ -6,7 +6,8 @@ class AccountSubscribeService < BaseService
|
|||
# @param [String, Account] uri User URI to subscribe in the form of username@domain (or account record)
|
||||
def call(source_account, target_account)
|
||||
begin
|
||||
target_account = ResolveAccountService.new.call(target_account, skip_webfinger: false)
|
||||
target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
|
||||
target_account ||= ResolveAccountService.new.call(target_account, skip_webfinger: false)
|
||||
rescue
|
||||
target_account = nil
|
||||
end
|
||||
|
@ -14,9 +15,7 @@ class AccountSubscribeService < BaseService
|
|||
raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
|
||||
raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) || (!target_account.local? && target_account.ostatus?) || source_account.domain_blocking?(target_account.domain)
|
||||
|
||||
if source_account.following?(target_account)
|
||||
return
|
||||
elsif source_account.subscribing?(target_account)
|
||||
if source_account.subscribing?(target_account)
|
||||
return
|
||||
end
|
||||
|
||||
|
|
|
@ -234,6 +234,7 @@ class DeleteAccountService < BaseService
|
|||
@account.statuses_count = 0
|
||||
@account.followers_count = 0
|
||||
@account.following_count = 0
|
||||
@account.subscribing_count = 0
|
||||
@account.moved_to_account = nil
|
||||
@account.also_known_as = []
|
||||
@account.trust_level = :untrusted
|
||||
|
|
|
@ -79,7 +79,6 @@ class FollowService < BaseService
|
|||
def direct_follow!
|
||||
follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit])
|
||||
|
||||
UnsubscribeAccountService.new.call(@source_account, @target_account) if @source_account.subscribing?(@target_account)
|
||||
LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name, :follow)
|
||||
MergeWorker.perform_async(@target_account.id, @source_account.id)
|
||||
|
||||
|
|
|
@ -119,6 +119,7 @@ class SearchService < BaseService
|
|||
blocked_by: Account.blocked_by_map(account_ids, account.id),
|
||||
muting: Account.muting_map(account_ids, account.id),
|
||||
following: Account.following_map(account_ids, account.id),
|
||||
subscribing: Account.subscribing_map(account_ids, account.id),
|
||||
domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id),
|
||||
}
|
||||
end
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
|
||||
.fields-group
|
||||
= f.input :setting_unfollow_modal, as: :boolean, wrapper: :with_label
|
||||
= f.input :setting_unsubscribe_modal, as: :boolean, wrapper: :with_label
|
||||
= f.input :setting_boost_modal, as: :boolean, wrapper: :with_label
|
||||
= f.input :setting_delete_modal, as: :boolean, wrapper: :with_label
|
||||
|
||||
|
|
|
@ -178,6 +178,7 @@ en:
|
|||
setting_theme: Site theme
|
||||
setting_trends: Show today's trends
|
||||
setting_unfollow_modal: Show confirmation dialog before unfollowing someone
|
||||
setting_unsubscribe_modal: Show confirmation dialog before unsubscribing someone
|
||||
setting_use_blurhash: Show colorful gradients for hidden media
|
||||
setting_use_pending_items: Slow mode
|
||||
severity: Severity
|
||||
|
|
|
@ -178,6 +178,7 @@ ja:
|
|||
setting_theme: サイトテーマ
|
||||
setting_trends: 本日のトレンドタグを表示する
|
||||
setting_unfollow_modal: フォローを解除する前に確認ダイアログを表示する
|
||||
setting_unsubscribe_modal: 購読を解除する前に確認ダイアログを表示する
|
||||
setting_use_blurhash: 非表示のメディアを色付きのぼかしで表示する
|
||||
setting_use_pending_items: 手動更新モード
|
||||
severity: 重大性
|
||||
|
|
|
@ -451,6 +451,7 @@ Rails.application.routes.draw do
|
|||
resource :search, only: :show, controller: :search
|
||||
resource :lookup, only: :show, controller: :lookup
|
||||
resources :relationships, only: :index
|
||||
resources :subscribing, only: :index, controller: 'subscribing_accounts'
|
||||
end
|
||||
|
||||
resources :accounts, only: [:create, :show] do
|
||||
|
@ -464,6 +465,8 @@ Rails.application.routes.draw do
|
|||
member do
|
||||
post :follow
|
||||
post :unfollow
|
||||
post :subscribe
|
||||
post :unsubscribe
|
||||
post :block
|
||||
post :unblock
|
||||
post :mute
|
||||
|
@ -486,7 +489,6 @@ Rails.application.routes.draw do
|
|||
resources :featured_tags, only: [:index, :create, :destroy]
|
||||
resources :favourite_tags, only: [:index, :create, :show, :update, :destroy]
|
||||
resources :follow_tags, only: [:index, :create, :show, :update, :destroy]
|
||||
resources :account_subscribes, only: [:index, :create, :show, :update, :destroy]
|
||||
resources :domain_subscribes, only: [:index, :create, :show, :update, :destroy]
|
||||
resources :keyword_subscribes, only: [:index, :create, :show, :update, :destroy]
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ defaults: &defaults
|
|||
default_sensitive: false
|
||||
hide_network: false
|
||||
unfollow_modal: false
|
||||
unsubscribe_modal: false
|
||||
boost_modal: false
|
||||
delete_modal: true
|
||||
auto_play_gif: false
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class AddSubscribingCountToAccountStat < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :account_stats, :subscribing_count, :bigint, null: false, default: 0
|
||||
end
|
||||
end
|
|
@ -111,6 +111,7 @@ ActiveRecord::Schema.define(version: 2021_08_08_071221) do
|
|||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.datetime "last_status_at"
|
||||
t.bigint "subscribing_count", default: 0, null: false
|
||||
t.index ["account_id"], name: "index_account_stats_on_account_id", unique: true
|
||||
end
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ module Mastodon
|
|||
account_stat = account.account_stat
|
||||
account_stat.following_count = account.active_relationships.count
|
||||
account_stat.followers_count = account.passive_relationships.count
|
||||
account_stat.subscribing_count = account.active_subscribes.count
|
||||
account_stat.statuses_count = account.statuses.where.not(visibility: :direct).count
|
||||
|
||||
account_stat.save if account_stat.changed?
|
||||
|
|
|
@ -6,7 +6,7 @@ RSpec.describe Settings::ScopedSettings do
|
|||
let(:object) { Fabricate(:user) }
|
||||
let(:scoped_setting) { described_class.new(object) }
|
||||
let(:val) { 'whatever' }
|
||||
let(:methods) { %i(auto_play_gif default_sensitive unfollow_modal boost_modal delete_modal reduce_motion system_font_ui noindex theme) }
|
||||
let(:methods) { %i(auto_play_gif default_sensitive unfollow_modal unsubscribe_modal boost_modal delete_modal reduce_motion system_font_ui noindex theme) }
|
||||
|
||||
describe '.initialize' do
|
||||
it 'sets @object' do
|
||||
|
|
|
@ -42,6 +42,13 @@ describe UserSettingsDecorator do
|
|||
expect(user.settings['unfollow_modal']).to eq false
|
||||
end
|
||||
|
||||
it 'updates the user settings value for unsubscribe modal' do
|
||||
values = { 'setting_unsubscribe_modal' => '0' }
|
||||
|
||||
settings.update(values)
|
||||
expect(user.settings['unsubscribe_modal']).to eq false
|
||||
end
|
||||
|
||||
it 'updates the user settings value for boost modal' do
|
||||
values = { 'setting_boost_modal' => '1' }
|
||||
|
||||
|
|
Loading…
Reference in a new issue