Add account subscribe support to WebUI

This commit is contained in:
noellabo 2019-12-12 20:02:35 +09:00
parent 3d6eaf638d
commit 9fdfc2d8d5
50 changed files with 658 additions and 85 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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');

View file

@ -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);
}
@ -83,10 +90,11 @@ class Account extends ImmutablePureComponent {
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
}
} else if (account.get('id') !== me && account.get('relationship', null) !== null) {
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']);
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']);
if (requested) {
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
@ -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>
}
}

View file

@ -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')));

View file

@ -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>

View file

@ -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}

View file

@ -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')));

View file

@ -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,22 +205,38 @@ 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 = (
<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 = <Fragment>{subscribing_buttons}{following_buttons}</Fragment>
}
}

View 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>
);
}
}

View file

@ -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} />

View file

@ -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');
}

View file

@ -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');

View file

@ -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",

View file

@ -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": "会話を表示",

View file

@ -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));

View file

@ -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,
}));

View file

@ -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:

View file

@ -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);

View file

@ -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:

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -16,6 +16,8 @@ module AccountCounters
:following_count=,
:followers_count,
:followers_count=,
:subscribing_count,
:subscribing_count=,
:last_status_at,
to: :account_stat

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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] },

View file

@ -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

View file

@ -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?

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -178,6 +178,7 @@ ja:
setting_theme: サイトテーマ
setting_trends: 本日のトレンドタグを表示する
setting_unfollow_modal: フォローを解除する前に確認ダイアログを表示する
setting_unsubscribe_modal: 購読を解除する前に確認ダイアログを表示する
setting_use_blurhash: 非表示のメディアを色付きのぼかしで表示する
setting_use_pending_items: 手動更新モード
severity: 重大性

View file

@ -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]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -32,10 +32,11 @@ module Mastodon
case type
when 'accounts'
processed, = parallelize_with_progress(Account.local.includes(:account_stat)) do |account|
account_stat = account.account_stat
account_stat.following_count = account.active_relationships.count
account_stat.followers_count = account.passive_relationships.count
account_stat.statuses_count = account.statuses.where.not(visibility: :direct).count
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?
end

View file

@ -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

View file

@ -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' }