Add show followed_by to WebUI

This commit is contained in:
noellabo 2020-03-21 13:25:30 +09:00
parent f40706037b
commit 2b7d1d9941
42 changed files with 632 additions and 111 deletions

View file

@ -39,7 +39,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def subscribe
AccountSubscribeService.new.call(current_user.account, @account)
AccountSubscribeService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs), list_id: params[:list_id])
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end
@ -59,7 +59,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def unsubscribe
UnsubscribeAccountService.new.call(current_user.account, @account)
UnsubscribeAccountService.new.call(current_user.account, @account, params[:list_id])
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end

View file

@ -0,0 +1,97 @@
# frozen_string_literal: true
class Api::V1::Lists::SubscribesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show]
before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:show]
before_action :require_user!
before_action :set_list
after_action :insert_pagination_headers, only: :show
def show
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
end
def create
ApplicationRecord.transaction do
list_accounts.each do |account|
@list.subscribes << account
end
end
render_empty
end
def destroy
AccountSubscribe.where(list: @list, target_account_id: account_ids).destroy_all
render_empty
end
private
def set_list
@list = List.where(account: current_account).find(params[:list_id])
end
def load_accounts
if unlimited?
@list.subscribes.includes(:account_stat).all
else
@list.subscribes.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
end
end
def list_accounts
Account.find(account_ids)
end
def account_ids
Array(resource_params[:account_ids])
end
def resource_params
params.permit(account_ids: [])
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
return if unlimited?
if records_continue?
api_v1_list_subscribes_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
return if unlimited?
unless @accounts.empty?
api_v1_list_subscribes_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
@accounts.last.id
end
def pagination_since_id
@accounts.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
def unlimited?
params[:limit] == '0'
end
end

View file

@ -38,7 +38,7 @@ class Settings::AccountSubscribesController < Settings::BaseController
end
def destroy
UnsubscribeAccountService.new.call(current_account, @account_subscribing.target_account)
UnsubscribeAccountService.new.call(current_account, @account_subscribing.target_account, @account_subscribing.list_id)
redirect_to settings_account_subscribes_path
end

View file

@ -58,6 +58,8 @@ class Settings::PreferencesController < Settings::BaseController
:setting_crop_images,
:setting_show_follow_button_on_timeline,
:setting_show_subscribe_button_on_timeline,
:setting_show_followed_by,
:setting_follow_button_to_list_adder,
:setting_show_target,
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag),
interactions: %i(must_be_follower must_be_following must_be_following_dm)

View file

@ -1,5 +1,6 @@
import api, { getLinks } from '../api';
import { importFetchedAccount, importFetchedAccounts } from './importer';
import { Map as ImmutableMap } from 'immutable';
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
@ -125,7 +126,7 @@ export function fetchAccountFail(id, error) {
};
};
export function followAccount(id, options = { reblogs: true }) {
export function followAccount(id, options = { reblogs: true, delivery: true }) {
return (dispatch, getState) => {
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
const locked = getState().getIn(['accounts', id, 'locked'], false);
@ -204,14 +205,14 @@ export function unfollowAccountFail(error) {
};
};
export function subscribeAccount(id, reblogs = true) {
export function subscribeAccount(id, reblogs = true, list_id = null) {
return (dispatch, getState) => {
const alreadySubscribe = getState().getIn(['relationships', id, 'subscribing']);
const alreadySubscribe = (list_id ? getState().getIn(['relationships', id, 'subscribing', list_id], new Map) : getState().getIn(['relationships', id, 'subscribing'], new Map)).size > 0;
const locked = getState().getIn(['accounts', id, 'locked'], false);
dispatch(subscribeAccountRequest(id, locked));
api(getState).post(`/api/v1/accounts/${id}/subscribe`).then(response => {
api(getState).post(`/api/v1/accounts/${id}/subscribe`, { reblogs, list_id }).then(response => {
dispatch(subscribeAccountSuccess(response.data, alreadySubscribe));
}).catch(error => {
dispatch(subscribeAccountFail(error, locked));
@ -219,11 +220,11 @@ export function subscribeAccount(id, reblogs = true) {
};
};
export function unsubscribeAccount(id) {
export function unsubscribeAccount(id, list_id = null) {
return (dispatch, getState) => {
dispatch(unsubscribeAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unsubscribe`).then(response => {
api(getState).post(`/api/v1/accounts/${id}/unsubscribe`, { list_id }).then(response => {
dispatch(unsubscribeAccountSuccess(response.data, getState().get('statuses')));
}).catch(error => {
dispatch(unsubscribeAccountFail(error));

View file

@ -7,8 +7,9 @@ import Permalink from './permalink';
import IconButton from './icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from '../initial_state';
import { me, show_followed_by, follow_button_to_list_adder } from '../initial_state';
import RelativeTimestamp from './relative_timestamp';
import { Map as ImmutableMap } from 'immutable';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
@ -29,6 +30,7 @@ class Account extends ImmutablePureComponent {
account: ImmutablePropTypes.map.isRequired,
onFollow: PropTypes.func.isRequired,
onSubscribe: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
onMuteNotifications: PropTypes.func.isRequired,
@ -39,12 +41,20 @@ class Account extends ImmutablePureComponent {
onActionClick: PropTypes.func,
};
handleFollow = () => {
this.props.onFollow(this.props.account);
handleFollow = (e) => {
if ((e && e.shiftKey) || !follow_button_to_list_adder) {
this.props.onFollow(this.props.account);
} else {
this.props.onAddToList(this.props.account);
}
}
handleSubscribe = () => {
this.props.onSubscribe(this.props.account);
handleSubscribe = (e) => {
if ((e && e.shiftKey) || !follow_button_to_list_adder) {
this.props.onSubscribe(this.props.account);
} else {
this.props.onAddToList(this.props.account);
}
}
handleBlock = () => {
@ -90,11 +100,14 @@ 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 subscribing = account.getIn(['relationship', 'subscribing']);
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 delivery = account.getIn(['relationship', 'delivery_following']);
const followed_by = account.getIn(['relationship', 'followed_by']) && show_followed_by;
const subscribing = account.getIn(['relationship', 'subscribing'], new Map).size > 0;
const subscribing_home = account.getIn(['relationship', 'subscribing', '-1'], new Map).size > 0;
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)} />;
@ -116,10 +129,10 @@ class Account extends ImmutablePureComponent {
} 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} />;
subscribing_buttons = <IconButton icon='rss-square' title={intl.formatMessage(subscribing ? messages.unsubscribe : messages.subscribe)} onClick={this.handleSubscribe} active={subscribing} no_delivery={subscribing && !subscribing_home} />;
}
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} />;
following_buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} passive={followed_by} no_delivery={following && !delivery} />;
}
buttons = <span>{subscribing_buttons}{following_buttons}</span>
}

View file

@ -4,7 +4,13 @@ import PropTypes from 'prop-types';
import IconButton from './icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me, show_follow_button_on_timeline, show_subscribe_button_on_timeline } from '../initial_state';
import {
me,
show_follow_button_on_timeline,
show_subscribe_button_on_timeline,
show_followed_by,
follow_button_to_list_adder,
} from '../initial_state';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
@ -21,6 +27,7 @@ class AccountActionBar extends ImmutablePureComponent {
account: ImmutablePropTypes.map.isRequired,
onFollow: PropTypes.func.isRequired,
onSubscribe: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
@ -28,12 +35,20 @@ class AccountActionBar extends ImmutablePureComponent {
'account',
]
handleFollow = () => {
this.props.onFollow(this.props.account);
handleFollow = (e) => {
if ((e && e.shiftKey) || !follow_button_to_list_adder) {
this.props.onFollow(this.props.account);
} else {
this.props.onAddToList(this.props.account);
}
}
handleSubscribe = () => {
this.props.onSubscribe(this.props.account);
handleSubscribe = (e) => {
if ((e && e.shiftKey) || !follow_button_to_list_adder) {
this.props.onSubscribe(this.props.account);
} else {
this.props.onAddToList(this.props.account);
}
}
render () {
@ -46,18 +61,21 @@ class AccountActionBar extends ImmutablePureComponent {
let buttons, following_buttons, subscribing_buttons;
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 following = account.getIn(['relationship', 'following']);
const delivery = account.getIn(['relationship', 'delivery_following']);
const followed_by = account.getIn(['relationship', 'followed_by']) && show_followed_by;
const subscribing = account.getIn(['relationship', 'subscribing'], new Map).size > 0;
const subscribing_home = account.getIn(['relationship', 'subscribing', '-1'], new Map).size > 0;
const requested = account.getIn(['relationship', 'requested']);
if (show_subscribe_button_on_timeline && (!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') || subscribing) {
subscribing_buttons = <IconButton icon='rss-square' title={intl.formatMessage(subscribing ? messages.unsubscribe : messages.subscribe)} onClick={this.handleSubscribe} active={subscribing} no_delivery={subscribing && !subscribing_home} />;
}
if (show_follow_button_on_timeline && (!account.get('moved') || following)) {
if (!account.get('moved') || following) {
if (requested) {
following_buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
following_buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} active={followed_by} />;
} else {
following_buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
following_buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} passive={followed_by} no_delivery={following && !delivery} />;
}
}
buttons = <span>{subscribing_buttons}{following_buttons}</span>

View file

@ -16,6 +16,8 @@ export default class IconButton extends React.PureComponent {
onKeyPress: PropTypes.func,
size: PropTypes.number,
active: PropTypes.bool,
passive: PropTypes.bool,
no_delivery: PropTypes.bool,
pressed: PropTypes.bool,
expanded: PropTypes.bool,
style: PropTypes.object,
@ -32,6 +34,8 @@ export default class IconButton extends React.PureComponent {
static defaultProps = {
size: 18,
active: false,
passive: false,
no_delivery: false,
disabled: false,
animate: false,
overlay: false,
@ -92,11 +96,13 @@ export default class IconButton extends React.PureComponent {
const {
active,
className,
no_delivery,
disabled,
expanded,
icon,
inverted,
overlay,
passive,
pressed,
tabIndex,
title,
@ -111,6 +117,8 @@ export default class IconButton extends React.PureComponent {
const classes = classNames(className, 'icon-button', {
active,
passive,
no_delivery,
disabled,
inverted,
activate,

View file

@ -113,8 +113,7 @@ class Status extends ImmutablePureComponent {
onToggleHidden: PropTypes.func,
onToggleCollapsed: PropTypes.func,
onQuoteToggleHidden: PropTypes.func,
onFollow: PropTypes.func.isRequired,
onSubscribe: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
muted: PropTypes.bool,
hidden: PropTypes.bool,
unread: PropTypes.bool,
@ -349,14 +348,6 @@ class Status extends ImmutablePureComponent {
this.node = c;
}
handleFollow = () => {
this.props.onFollow(this._properStatus().get('account'));
}
handleSubscribe = () => {
this.props.onSubscribe(this._properStatus().get('account'));
}
render () {
let media = null;
let statusAvatar, prepend, rebloggedByText;

View file

@ -16,6 +16,7 @@ import {
import { openModal } from '../actions/modal';
import { initMuteModal } from '../actions/mutes';
import { unfollowModal, unsubscribeModal } from '../initial_state';
import { Map as ImmutableMap } from 'immutable';
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
@ -51,7 +52,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
},
onSubscribe (account) {
if (account.getIn(['relationship', 'subscribing'])) {
if (account.getIn(['relationship', 'subscribing', '-1'], new Map).size > 0) {
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> }} />,
@ -66,6 +67,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onAddToList (account){
dispatch(openModal('LIST_ADDER', {
accountId: account.get('id'),
}));
},
onBlock (account) {
if (account.getIn(['relationship', 'blocking'])) {
dispatch(unblockAccount(account.get('id')));

View file

@ -50,6 +50,7 @@ import { deployPictureInPicture } from '../actions/picture_in_picture';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { boostModal, deleteModal, unfollowModal, unsubscribeModal } from '../initial_state';
import { showAlertForError } from '../actions/alerts';
import { Map as ImmutableMap } from 'immutable';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
@ -268,7 +269,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
},
onSubscribe (account) {
if (account.getIn(['relationship', 'subscribing'])) {
if (account.getIn(['relationship', 'subscribing', '-1'], new Map).size > 0) {
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> }} />,
@ -282,6 +283,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(subscribeAccount(account.get('id')));
}
},
onAddToList (account){
dispatch(openModal('LIST_ADDER', {
accountId: account.get('id'),
}));
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));

View file

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Button from 'mastodon/components/button';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif, me, isStaff } from 'mastodon/initial_state';
import { autoPlayGif, me, isStaff, show_followed_by, follow_button_to_list_adder } from 'mastodon/initial_state';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import IconButton from 'mastodon/components/icon_button';
@ -14,6 +14,7 @@ import ShortNumber from 'mastodon/components/short_number';
import { NavLink } from 'react-router-dom';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import AccountNoteContainer from '../containers/account_note_container';
import { Map as ImmutableMap } from 'immutable';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@ -71,6 +72,7 @@ class Header extends ImmutablePureComponent {
identity_props: ImmutablePropTypes.list,
onFollow: PropTypes.func.isRequired,
onSubscribe: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
@ -125,6 +127,26 @@ class Header extends ImmutablePureComponent {
}
}
handleFollow = (e) => {
if ((e && e.shiftKey) || !follow_button_to_list_adder) {
this.props.onFollow(this.props.account);
} else {
this.props.onAddToList(this.props.account);
}
}
handleSubscribe = (e) => {
if ((e && e.shiftKey) || !follow_button_to_list_adder) {
this.props.onSubscribe(this.props.account);
} else {
this.props.onAddToList(this.props.account);
}
}
setRef = (c) => {
this.node = c;
}
render () {
const { account, intl, domain, identity_proofs } = this.props;
@ -212,9 +234,10 @@ class Header extends ImmutablePureComponent {
}
menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
menu.push(null);
}
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
menu.push(null);
if (account.getIn(['relationship', 'muting'])) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
@ -263,18 +286,21 @@ 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']);
const following = account.getIn(['relationship', 'following']);
const delivery = account.getIn(['relationship', 'delivery_following']);
const followed_by = account.getIn(['relationship', 'followed_by']) && show_followed_by;
const subscribing = account.getIn(['relationship', 'subscribing'], new Map).size > 0;
const subscribing_home = account.getIn(['relationship', 'subscribing', '-1'], new Map).size > 0;
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} />;
subscribing_buttons = <IconButton icon='rss-square' title={intl.formatMessage(subscribing ? messages.unsubscribe : messages.subscribe)} onClick={this.handleSubscribe} active={subscribing} no_delivery={subscribing && !subscribing_home} />;
}
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} />;
following_buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} passive={followed_by} no_delivery={following && !delivery} />;
}
buttons = <span>{subscribing_buttons}{following_buttons}</span>
}

View file

@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import InnerHeader from '../../account/components/header';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { follow_button_to_list_adder } from 'mastodon/initial_state';
import MovedNote from './moved_note';
import { FormattedMessage } from 'react-intl';
import { NavLink } from 'react-router-dom';
@ -14,6 +15,7 @@ export default class Header extends ImmutablePureComponent {
identity_proofs: ImmutablePropTypes.list,
onFollow: PropTypes.func.isRequired,
onSubscribe: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
@ -32,12 +34,20 @@ export default class Header extends ImmutablePureComponent {
router: PropTypes.object,
};
handleFollow = () => {
this.props.onFollow(this.props.account);
handleFollow = (e) => {
if ((e && e.shiftKey) || !follow_button_to_list_adder) {
this.props.onFollow(this.props.account);
} else {
this.props.onAddToList(this.props.account);
}
}
handleSubscribe = () => {
this.props.onSubscribe(this.props.account);
handleSubscribe = (e) => {
if ((e && e.shiftKey) || !follow_button_to_list_adder) {
this.props.onSubscribe(this.props.account);
} else {
this.props.onAddToList(this.props.account);
}
}
handleBlock = () => {

View file

@ -62,7 +62,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
},
onSubscribe (account) {
if (account.getIn(['relationship', 'subscribing'])) {
if (account.getIn(['relationship', 'subscribing', '-1'], new Map).size > 0) {
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> }} />,
@ -77,6 +77,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onAddToList (account){
dispatch(openModal('LIST_ADDER', {
accountId: account.get('id'),
}));
},
onBlock (account) {
if (account.getIn(['relationship', 'blocking'])) {
dispatch(unblockAccount(account.get('id')));

View file

@ -10,7 +10,7 @@ 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, unsubscribeModal } from 'mastodon/initial_state';
import { autoPlayGif, me, unfollowModal, unsubscribeModal, show_followed_by } from 'mastodon/initial_state';
import ShortNumber from 'mastodon/components/short_number';
import {
followAccount,
@ -23,6 +23,7 @@ import {
} from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal';
import { initMuteModal } from 'mastodon/actions/mutes';
import { Map as ImmutableMap } from 'immutable';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
@ -36,6 +37,10 @@ const messages = defineMessages({
id: 'confirmations.unfollow.confirm',
defaultMessage: 'Unfollow',
},
unsubscribeConfirm: {
id: 'confirmations.unsubscribe.confirm',
defaultMessage: 'Unsubscribe'
},
});
const makeMapStateToProps = () => {
@ -77,7 +82,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
},
onSubscribe(account) {
if (account.getIn(['relationship', 'subscribing'])) {
if (account.getIn(['relationship', 'subscribing', '-1'], new Map).size > 0) {
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> }} />,
@ -92,6 +97,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onAddToList(account){
dispatch(openModal('LIST_ADDER', {
accountId: account.get('id'),
}));
},
onBlock(account) {
if (account.getIn(['relationship', 'blocking'])) {
dispatch(unblockAccount(account.get('id')));
@ -119,6 +130,7 @@ class AccountCard extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
onFollow: PropTypes.func.isRequired,
onSubscribe: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
};
@ -149,13 +161,21 @@ class AccountCard extends ImmutablePureComponent {
}
}
handleFollow = () => {
this.props.onFollow(this.props.account);
handleFollow = (e) => {
if ((e && e.shiftKey) || !follow_button_to_list_adder) {
this.props.onFollow(this.props.account);
} else {
this.props.onAddToList(this.props.account);
}
};
handleSubscribe = () => {
this.props.onSubscribe(this.props.account);
}
handleSubscribe = (e) => {
if ((e && e.shiftKey) || !follow_button_to_list_adder) {
this.props.onSubscribe(this.props.account);
} else {
this.props.onAddToList(this.props.account);
}
};
handleBlock = () => {
this.props.onBlock(this.props.account);
@ -174,11 +194,14 @@ class AccountCard extends ImmutablePureComponent {
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']);
const following = account.getIn(['relationship', 'following']);
const delivery = account.getIn(['relationship', 'delivery_following']);
const followed_by = account.getIn(['relationship', 'followed_by']) && show_followed_by;
const subscribing = account.getIn(['relationship', 'subscribing'], new Map).size > 0;
const subscribing_home = account.getIn(['relationship', 'subscribing', '-1'], new Map).size > 0;
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']);
if (requested) {
buttons = (
@ -221,6 +244,7 @@ class AccountCard extends ImmutablePureComponent {
)}
onClick={this.handleSubscribe}
active={subscribing}
no_delivery={subscribing && !subscribing_home}
/>
);
}
@ -233,6 +257,8 @@ class AccountCard extends ImmutablePureComponent {
)}
onClick={this.handleFollow}
active={following}
passive={followed_by}
no_delivery={following && !delivery}
/>
);
}

View file

@ -1,33 +1,79 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { makeGetAccount } from '../../../selectors';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Avatar from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
import { injectIntl } from 'react-intl';
import IconButton from '../../../components/icon_button';
import { unfollowAccount, followAccount } from '../../../actions/accounts';
import { me, show_followed_by, unfollowModal } from '../../../initial_state';
import { openModal } from '../../../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { Map as ImmutableMap } from 'immutable';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
});
const mapStateToProps = (state, { accountId }) => ({
account: getAccount(state, accountId),
});
const MapStateToProps = (state) => ({
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
if (unfollowModal) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.unfollowConfirm),
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
}));
} else {
dispatch(unfollowAccount(account.get('id')));
}
} else {
dispatch(followAccount(account.get('id')));
}
},
});
export default @connect(makeMapStateToProps)
export default @connect(MapStateToProps, mapDispatchToProps)
@injectIntl
class Account extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
onFollow: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleFollow = () => {
this.props.onFollow(this.props.account);
}
render () {
const { account } = this.props;
const { account, intl } = this.props;
let buttons;
if (account.get('id') !== me && account.get('relationship', null) !== null) {
const following = account.getIn(['relationship', 'following']);
const delivery = account.getIn(['relationship', 'delivery_following']);
const followed_by = account.getIn(['relationship', 'followed_by']) && show_followed_by;
const requested = account.getIn(['relationship', 'requested']);
if (!account.get('moved') || following) {
if (requested) {
buttons = <IconButton icon='hourglass' title={intl.formatMessage(messages.requested)} active={followed_by} onClick={this.handleFollow} />;
} else {
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} passive={followed_by} no_delivery={following && !delivery} />;
}
}
}
return (
<div className='account'>
<div className='account__wrapper'>
@ -35,6 +81,10 @@ class Account extends ImmutablePureComponent {
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<DisplayName account={account} />
</div>
<div className='account__relationship'>
{buttons}
</div>
</div>
</div>
);

View file

@ -0,0 +1,108 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import IconButton from '../../../components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import { followAccount, unsubscribeAccount, subscribeAccount } from '../../../actions/accounts';
import Icon from 'mastodon/components/icon';
const messages = defineMessages({
title: { id: 'column.home', defaultMessage: 'Home' },
remove: { id: 'home.account.remove', defaultMessage: 'Remove from home' },
add: { id: 'home.account.add', defaultMessage: 'Add to home' },
unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe' },
subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe' },
unsubscribeConfirm: { id: 'confirmations.unsubscribe.confirm', defaultMessage: 'Unsubscribe' },
});
const MapStateToProps = (state, { account }) => ({
added: account.getIn(['relationship', 'delivery_following'], false),
});
const mapDispatchToProps = (dispatch) => ({
onRemove (account) {
dispatch(followAccount(account.get('id'), { delivery: false }));
},
onAdd (account) {
dispatch(followAccount(account.get('id'), { delivery: true }));
},
onSubscribe (account) {
if (account.getIn(['relationship', 'subscribing', '-1'], new Map).size > 0) {
dispatch(unsubscribeAccount(account.get('id')));
} else {
dispatch(subscribeAccount(account.get('id')));
}
},
});
export default @connect(MapStateToProps, mapDispatchToProps)
@injectIntl
class Home extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
intl: PropTypes.object.isRequired,
onRemove: PropTypes.func.isRequired,
onAdd: PropTypes.func.isRequired,
onSubscribe: PropTypes.func.isRequired,
added: PropTypes.bool,
disabled: PropTypes.bool,
};
static defaultProps = {
added: false,
disabled: true,
};
handleRemove = () => {
this.props.onRemove(this.props.account);
}
handleAdd = () => {
this.props.onAdd(this.props.account);
}
handleSubscribe = () => {
this.props.onSubscribe(this.props.account);
}
render () {
const { account, intl, added, disabled } = this.props;
const subscribing_home = account.getIn(['relationship', 'subscribing', '-1'], new Map).size > 0;
let button, subscribing_buttons;
if (!account.get('moved') || subscribing_home) {
subscribing_buttons = <IconButton icon='rss-square' title={intl.formatMessage(subscribing_home ? messages.unsubscribe : messages.subscribe)} onClick={this.handleSubscribe} active={subscribing_home} />;
}
if (added) {
button = <IconButton icon='times' title={intl.formatMessage(messages.remove)} onClick={this.handleRemove} active />;
} else if (disabled) {
button = <IconButton icon='plus' title={intl.formatMessage(messages.add)} onClick={this.handleAdd} />;
} else {
button = '';
}
return (
<div className='list'>
<div className='list__wrapper'>
<div className='list__display-name'>
<Icon id='home' className='column-link__icon' fixedWidth />
{intl.formatMessage(messages.title)}
</div>
<div className='account__relationship'>
{subscribing_buttons}
{button}
</div>
</div>
</div>
);
}
}

View file

@ -5,12 +5,16 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import IconButton from '../../../components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import { unsubscribeAccount, subscribeAccount } from '../../../actions/accounts';
import { removeFromListAdder, addToListAdder } from '../../../actions/lists';
import Icon from 'mastodon/components/icon';
const messages = defineMessages({
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe' },
subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe' },
unsubscribeConfirm: { id: 'confirmations.unsubscribe.confirm', defaultMessage: 'Unsubscribe' },
});
const MapStateToProps = (state, { listId, added }) => ({
@ -21,6 +25,14 @@ const MapStateToProps = (state, { listId, added }) => ({
const mapDispatchToProps = (dispatch, { listId }) => ({
onRemove: () => dispatch(removeFromListAdder(listId)),
onAdd: () => dispatch(addToListAdder(listId)),
onSubscribe (account) {
if (account.getIn(['relationship', 'subscribing', listId], new Map).size > 0) {
dispatch(unsubscribeAccount(account.get('id'), listId));
} else {
dispatch(subscribeAccount(account.get('id'), true, listId));
}
},
});
export default @connect(MapStateToProps, mapDispatchToProps)
@ -28,26 +40,41 @@ export default @connect(MapStateToProps, mapDispatchToProps)
class List extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
list: ImmutablePropTypes.map.isRequired,
intl: PropTypes.object.isRequired,
onRemove: PropTypes.func.isRequired,
onAdd: PropTypes.func.isRequired,
onSubscribe: PropTypes.func.isRequired,
added: PropTypes.bool,
disabled: PropTypes.bool,
};
static defaultProps = {
added: false,
disabled: true,
};
handleSubscribe = () => {
this.props.onSubscribe(this.props.account);
}
render () {
const { list, intl, onRemove, onAdd, added } = this.props;
const { account, list, intl, onRemove, onAdd, added, disabled } = this.props;
let button;
const subscribing = account.getIn(['relationship', 'subscribing', list.get('id')], new Map).size > 0;
let button, 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 (added) {
button = <IconButton icon='times' title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
} else {
button = <IconButton icon='times' title={intl.formatMessage(messages.remove)} onClick={onRemove} active />;
} else if (disabled) {
button = <IconButton icon='plus' title={intl.formatMessage(messages.add)} onClick={onAdd} />;
} else {
button = '';
}
return (
@ -59,6 +86,7 @@ class List extends ImmutablePureComponent {
</div>
<div className='account__relationship'>
{subscribing_buttons}
{button}
</div>
</div>

View file

@ -6,6 +6,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl } from 'react-intl';
import { setupListAdder, resetListAdder } from '../../actions/lists';
import { createSelector } from 'reselect';
import { makeGetAccount } from '../../selectors';
import Home from './components/home';
import List from './components/list';
import Account from './components/account';
import NewListForm from '../lists/components/new_list_form';
@ -19,7 +21,10 @@ const getOrderedLists = createSelector([state => state.get('lists')], lists => {
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
});
const mapStateToProps = state => ({
const getAccount = makeGetAccount();
const mapStateToProps = (state, { accountId }) => ({
account: getAccount(state, accountId),
listIds: getOrderedLists(state).map(list=>list.get('id')),
});
@ -33,7 +38,7 @@ export default @connect(mapStateToProps, mapDispatchToProps)
class ListAdder extends ImmutablePureComponent {
static propTypes = {
accountId: PropTypes.string.isRequired,
account: ImmutablePropTypes.map.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
onInitialize: PropTypes.func.isRequired,
@ -42,8 +47,8 @@ class ListAdder extends ImmutablePureComponent {
};
componentDidMount () {
const { onInitialize, accountId } = this.props;
onInitialize(accountId);
const { onInitialize, account } = this.props;
onInitialize(account.get('id'));
}
componentWillUnmount () {
@ -52,19 +57,21 @@ class ListAdder extends ImmutablePureComponent {
}
render () {
const { accountId, listIds } = this.props;
const { account, listIds, intl } = this.props;
const following = account.getIn(['relationship', 'following']);
return (
<div className='modal-root__modal list-adder'>
<div className='list-adder__account'>
<Account accountId={accountId} />
<Account account={account} intl={intl} />
</div>
<NewListForm />
<div className='list-adder__lists'>
{listIds.map(ListId => <List key={ListId} listId={ListId} />)}
<Home account={account} disabled={following} intl={intl} />
{listIds.map(ListId => <List key={ListId} account={account} listId={ListId} disabled={following} intl={intl} />)}
</div>
</div>
);

View file

@ -34,7 +34,7 @@ const MODAL_COMPONENTS = {
'EMBED': EmbedModal,
'LIST_EDITOR': ListEditor,
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
'LIST_ADDER':ListAdder,
'LIST_ADDER': ListAdder,
};
export default class ModalRoot extends React.PureComponent {

View file

@ -30,6 +30,8 @@ export const cropImages = getMeta('crop_images');
export const disableSwiping = getMeta('disable_swiping');
export const show_follow_button_on_timeline = getMeta('show_follow_button_on_timeline');
export const show_subscribe_button_on_timeline = getMeta('show_subscribe_button_on_timeline');
export const show_followed_by = getMeta('show_followed_by');
export const follow_button_to_list_adder = getMeta('follow_button_to_list_adder');
export const show_target = getMeta('show_target');
export default initialState;

View file

@ -209,6 +209,8 @@
"hashtag.column_settings.tag_mode.any": "Any of these",
"hashtag.column_settings.tag_mode.none": "None of these",
"hashtag.column_settings.tag_toggle": "Include additional tags for this column",
"home.account.add": "Add to home",
"home.account.remove": "Remove from home",
"home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",

View file

@ -209,6 +209,8 @@
"hashtag.column_settings.tag_mode.any": "いずれかを含む",
"hashtag.column_settings.tag_mode.none": "これらを除く",
"hashtag.column_settings.tag_toggle": "このカラムに追加のタグを含める",
"home.account.add": "ホームに追加",
"home.account.remove": "ホームから外す",
"home.column_settings.basic": "基本設定",
"home.column_settings.show_reblogs": "ブースト表示",
"home.column_settings.show_replies": "返信表示",

View file

@ -1,6 +1,8 @@
import {
ACCOUNT_FOLLOW_SUCCESS,
ACCOUNT_UNFOLLOW_SUCCESS,
ACCOUNT_SUBSCRIBE_SUCCESS,
ACCOUNT_UNSUBSCRIBE_SUCCESS,
} from '../actions/accounts';
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer';
import { Map as ImmutableMap, fromJS } from 'immutable';
@ -33,6 +35,11 @@ export default function accountsCounters(state = initialState, action) {
state.updateIn([action.relationship.id, 'followers_count'], num => num + 1);
case ACCOUNT_UNFOLLOW_SUCCESS:
return state.updateIn([action.relationship.id, 'followers_count'], num => Math.max(0, num - 1));
case ACCOUNT_SUBSCRIBE_SUCCESS:
return action.alreadySubscribe ? state :
state.updateIn([action.relationship.id, 'subscribing_count'], num => num + 1);
case ACCOUNT_UNSUBSCRIBE_SUCCESS:
return state.updateIn([action.relationship.id, 'subscribing_count'], num => Math.max(0, num - 1));
default:
return state;
}

View file

@ -202,6 +202,24 @@
color: $highlight-text-color;
}
&.passive {
color: $passive-text-color;
}
&.active.passive {
color: $active-passive-text-color;
}
&.active.no_delivery {
color: rgba($highlight-text-color, 33%);
-webkit-text-stroke: 1px $highlight-text-color;
}
&.active.passive.no_delivery {
color: rgba($active-passive-text-color, 33%);
-webkit-text-stroke: 1px $active-passive-text-color;
}
&::-moz-focus-inner {
border: 0;
}
@ -1630,6 +1648,13 @@ a.account__display-name {
top: 60px;
left: 10px;
z-index: 1;
&.account__action-bar_home,
&.account__action-bar_list {
left: unset;
right: 10px;
top: 8px;
}
}
.status__avatar {
@ -6564,6 +6589,7 @@ noscript {
.list__wrapper {
display: flex;
position: relative;
}
.list__display-name {

View file

@ -185,6 +185,16 @@ body.rtl {
left: 0;
}
.account__action-bar {
right: 10px;
&.account__action-bar_home,
&.account__action-bar_list {
left: 10px;
right: unset;
}
}
.status__relative-time,
.status__visibility-icon,
.activity-stream .status.light .status__header .status__meta {

View file

@ -236,7 +236,7 @@ class FeedManager
add_to_feed(:home, account.id, status, aggregate)
end
account.following.includes(:account_stat).find_each do |target_account|
account.delivery_following.includes(:account_stat).find_each do |target_account|
if redis.zcard(timeline_key) >= limit
oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i
last_status_score = Mastodon::Snowflake.id_at(account.last_status_at)

View file

@ -42,6 +42,8 @@ class UserSettingsDecorator
user.settings['crop_images'] = crop_images_preference if change?('setting_crop_images')
user.settings['show_follow_button_on_timeline'] = show_follow_button_on_timeline_preference if change?('setting_show_follow_button_on_timeline')
user.settings['show_subscribe_button_on_timeline'] = show_subscribe_button_on_timeline_preference if change?('setting_show_subscribe_button_on_timeline')
user.settings['show_followed_by'] = show_followed_by_preference if change?('setting_show_followed_by')
user.settings['follow_button_to_list_adder'] = follow_button_to_list_adder_preference if change?('setting_follow_button_to_list_adder')
user.settings['show_target'] = show_target_preference if change?('setting_show_target')
end
@ -153,6 +155,14 @@ class UserSettingsDecorator
boolean_cast_setting 'setting_show_subscribe_button_on_timeline'
end
def show_followed_by_preference
boolean_cast_setting 'setting_show_followed_by'
end
def follow_button_to_list_adder_preference
boolean_cast_setting 'setting_follow_button_to_list_adder'
end
def show_target_preference
boolean_cast_setting 'setting_show_target'
end

View file

@ -24,7 +24,13 @@ module AccountInteractions
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)
AccountSubscribe.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |subscribe, mapping|
mapping[subscribe.target_account_id] = (mapping[subscribe.target_account_id] || {}).merge({
subscribe.list_id || -1 => {
reblogs: subscribe.show_reblogs?,
}
})
end
end
def blocking_map(target_account_ids, account_id)
@ -135,7 +141,7 @@ module AccountInteractions
rel
end
def request_follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false, bypass_limit: false)
def request_follow!(other_account, reblogs: nil, notify: nil, delivery: nil, uri: nil, rate_limit: false, bypass_limit: false)
rel = follow_requests.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, delivery: delivery.nil? ? true : delivery, uri: uri, rate_limit: rate_limit, bypass_follow_limit: bypass_limit)
.find_or_create_by!(target_account: other_account)
@ -205,9 +211,11 @@ module AccountInteractions
block&.destroy
end
def subscribe!(other_account, show_reblogs = true, list_id = nil)
rel = active_subscribes.find_or_create_by!(target_account: other_account, show_reblogs: show_reblogs, list_id: list_id)
def subscribe!(other_account, reblogs = true, list_id = nil)
rel = active_subscribes.create_with(show_reblogs: reblogs)
.find_or_create_by!(target_account: other_account, list_id: list_id)
rel.update!(show_reblogs: reblogs)
remove_potential_friendship(other_account)
rel
@ -278,7 +286,7 @@ module AccountInteractions
end
def subscribing?(other_account, list_id = nil)
active_subscribes.where(target_account: other_account, list_id: list_id).exists?
active_subscribes.where(target_account: other_account, list_id: list_id).exists?
end
def followers_for_local_distribution

View file

@ -23,6 +23,15 @@ class List < ApplicationRecord
has_many :list_accounts, inverse_of: :list, dependent: :destroy
has_many :accounts, through: :list_accounts
has_many :account_subscribes, inverse_of: :list, dependent: :destroy
has_many :subscribes, through: :account_subscribes, source: :target_account
has_many :follow_tags, inverse_of: :list, dependent: :destroy
has_many :tags, through: :follow_tags, source: :tag
has_many :domain_subscribes, inverse_of: :list, dependent: :destroy
has_many :keyword_subscribes, inverse_of: :list, dependent: :destroy
validates :title, presence: true
validates_each :account_id, on: :create do |record, _attr, value|

View file

@ -127,6 +127,8 @@ class User < ApplicationRecord
:advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images,
:disable_swiping,
:show_follow_button_on_timeline, :show_subscribe_button_on_timeline, :show_target,
:show_follow_button_on_timeline, :show_subscribe_button_on_timeline, :show_followed_by, :show_target,
:follow_button_to_list_adder,
to: :settings, prefix: :setting, allow_nil: false
attr_reader :invite_code, :sign_in_token_attempt

View file

@ -44,6 +44,8 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:crop_images] = object.current_account.user.setting_crop_images
store[:show_follow_button_on_timeline] = object.current_account.user.setting_show_follow_button_on_timeline
store[:show_subscribe_button_on_timeline] = object.current_account.user.setting_show_subscribe_button_on_timeline
store[:show_followed_by] = object.current_account.user.setting_show_followed_by
store[:follow_button_to_list_adder] = object.current_account.user.setting_follow_button_to_list_adder
store[:show_target] = object.current_account.user.setting_show_target
else
store[:auto_play_gif] = Setting.auto_play_gif

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class REST::RelationshipSerializer < ActiveModel::Serializer
attributes :id, :following, :showing_reblogs, :notifying, :followed_by, :subscribing,
attributes :id, :following, :delivery_following, :showing_reblogs, :notifying, :followed_by, :subscribing,
:blocking, :blocked_by, :muting, :muting_notifications, :requested,
:domain_blocking, :endorsed, :note
@ -13,6 +13,12 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
instance_options[:relationships].following[object.id] ? true : false
end
def delivery_following
(instance_options[:relationships].following[object.id] || {})[:delivery] ||
(instance_options[:relationships].requested[object.id] || {})[:delivery] ||
false
end
def showing_reblogs
(instance_options[:relationships].following[object.id] || {})[:reblogs] ||
(instance_options[:relationships].requested[object.id] || {})[:reblogs] ||
@ -30,7 +36,7 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
end
def subscribing
instance_options[:relationships].subscribing[object.id] ? true : false
instance_options[:relationships].subscribing[object.id] || {}
end
def blocking

View file

@ -4,7 +4,9 @@ class AccountSubscribeService < BaseService
# Subscribe a remote user
# @param [Account] source_account From which to subscribe
# @param [String, Account] uri User URI to subscribe in the form of username@domain (or account record)
def call(source_account, target_acct, show_reblogs = true, list_id = nil)
def call(source_account, target_acct, options = {})
@options = { show_reblogs: true, list_id: nil }.merge(options)
if target_acct.class.name == 'Account'
target_account = target_acct
else
@ -19,14 +21,14 @@ 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.subscribing?(target_account, list_id)
if source_account.subscribing?(target_account, @options[:list_id])
return
end
ActivityTracker.increment('activity:interactions')
subscribe = source_account.subscribe!(target_account, show_reblogs, list_id)
MergeWorker.perform_async(target_account.id, source_account.id, true) if list_id.nil?
subscribe = source_account.subscribe!(target_account, @options[:show_reblogs], @options[:list_id])
MergeWorker.perform_async(target_account.id, source_account.id, true) if @options[:list_id].nil?
subscribe
end
end

View file

@ -14,10 +14,11 @@ class FollowService < BaseService
# @option [Boolean] :bypass_locked
# @option [Boolean] :bypass_limit Allow following past the total follow number
# @option [Boolean] :with_rate_limit
# @option [Boolean] :delivery
def call(source_account, target_account, options = {})
@source_account = source_account
@target_account = target_account
@options = { bypass_locked: false, bypass_limit: false, with_rate_limit: false }.merge(options)
@options = { bypass_locked: false, delivery: true, bypass_limit: false, with_rate_limit: false }.merge(options)
raise ActiveRecord::RecordNotFound if following_not_possible?
raise Mastodon::NotPermittedError if following_not_allowed?

View file

@ -4,13 +4,13 @@ class UnsubscribeAccountService < BaseService
# UnsubscribeAccount
# @param [Account] source_account Where to unsubscribe from
# @param [Account] target_account Which to unsubscribe
def call(source_account, target_account)
subscribe = AccountSubscribe.find_by(account: source_account, target_account: target_account)
def call(source_account, target_account, list_id = nil)
subscribe = AccountSubscribe.find_by(account: source_account, target_account: target_account, list_id: list_id)
return unless subscribe
subscribe.destroy!
UnmergeWorker.perform_async(target_account.id, source_account.id)
UnmergeWorker.perform_async(target_account.id, source_account.id) if list_id.nil?
subscribe
end
end

View file

@ -39,6 +39,12 @@
.fields-group
= f.input :setting_show_subscribe_button_on_timeline, as: :boolean, wrapper: :with_label
.fields-group
= f.input :setting_show_followed_by, as: :boolean, wrapper: :with_label
.fields-group
= f.input :setting_follow_button_to_list_adder, as: :boolean, wrapper: :with_label
-# .fields-group
-# = f.input :setting_show_target, as: :boolean, wrapper: :with_label

View file

@ -51,11 +51,13 @@ en:
setting_display_media_default: Hide media marked as sensitive
setting_display_media_hide_all: Always hide media
setting_display_media_show_all: Always show media
setting_follow_button_to_list_adder: Change the behavior of the Follow / Subscribe button, open a dialog where you can select a list to follow / subscribe, or opt out of receiving at home
setting_hide_network: Who you follow and who follows you will be hidden on your profile
setting_noindex: Affects your public profile and post pages
setting_show_application: The application you use to post will be displayed in the detailed view of your posts
setting_show_follow_button_on_timeline: You can easily check the follow status and build a follow list quickly
setting_show_subscribe_button_on_timeline: You can easily check the status of your subscriptions and quickly build a subscription list
setting_show_followed_by: "The color of the follow button changes according to the follow status (gray: no follow relationship, yellow: followed, blue: following, green: mutual follow)"
setting_show_target: Enable the function to switch between posting target and follow / subscribe target
setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details
setting_use_pending_items: Hide timeline updates behind a click instead of automatically scrolling the feed
@ -176,12 +178,14 @@ en:
setting_display_media_hide_all: Hide all
setting_display_media_show_all: Show all
setting_expand_spoilers: Always expand posts marked with content warnings
setting_follow_button_to_list_adder: Open list add dialog with follow button
setting_hide_network: Hide your social graph
setting_noindex: Opt-out of search engine indexing
setting_reduce_motion: Reduce motion in animations
setting_show_application: Disclose application used to send posts
setting_show_follow_button_on_timeline: Show follow button on timeline
setting_show_subscribe_button_on_timeline: Show subscribe button on timeline
setting_show_followed_by: Reflect the following status on the follow button
setting_show_target: Enable targeting features
setting_system_font_ui: Use system's default font
setting_theme: Site theme

View file

@ -51,11 +51,13 @@ ja:
setting_display_media_default: 閲覧注意としてマークされたメディアは隠す
setting_display_media_hide_all: メディアを常に隠す
setting_display_media_show_all: メディアを常に表示する
setting_follow_button_to_list_adder: フォロー・購読ボタンの動作を変更し、フォロー・購読するリストを選択したり、ホームで受け取らないよう設定するダイアログを開きます
setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします
setting_noindex: 公開プロフィールおよび各投稿ページに影響します
setting_show_application: 投稿するのに使用したアプリが投稿の詳細ビューに表示されるようになります
setting_show_follow_button_on_timeline: フォロー状態を確認し易くなり、素早くフォローリストを構築できます
setting_show_subscribe_button_on_timeline: 購読状態を確認し易くなり、素早く購読リストを構築できます
setting_show_followed_by: フォロー状態に応じてフォローボタンの色が変わります(灰色:フォロー関係なし、黄色:フォローされている、青色:フォローしている、緑色:相互フォロー)
setting_show_target: 投稿対象と、フォロー・購読の対象を切り替える機能を有効にします
setting_use_blurhash: ぼかしはメディアの色を元に生成されますが、細部は見えにくくなっています
setting_use_pending_items: 新着があってもタイムラインを自動的にスクロールしないようにします
@ -176,12 +178,14 @@ ja:
setting_display_media_hide_all: 非表示
setting_display_media_show_all: 表示
setting_expand_spoilers: 閲覧注意としてマークされた投稿を常に展開する
setting_follow_button_to_list_adder: フォローボタンでリスト追加ダイアログを開く
setting_hide_network: 繋がりを隠す
setting_noindex: 検索エンジンによるインデックスを拒否する
setting_reduce_motion: アニメーションの動きを減らす
setting_show_application: 送信したアプリを開示する
setting_show_follow_button_on_timeline: タイムライン上にフォローボタンを表示する
setting_show_subscribe_button_on_timeline: タイムライン上に購読ボタンを表示する
setting_show_followed_by: 被フォロー状態をフォローボタンに反映する
setting_show_target: ターゲット機能を有効にする
setting_system_font_ui: システムのデフォルトフォントを使う
setting_theme: サイトテーマ

View file

@ -480,6 +480,7 @@ Rails.application.routes.draw do
resources :lists, only: [:index, :create, :show, :update, :destroy] do
resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts'
resource :subscribes, only: [:show, :create, :destroy], controller: 'lists/subscribes'
end
namespace :featured_tags do

View file

@ -41,6 +41,8 @@ defaults: &defaults
crop_images: true
show_follow_button_on_timeline: false
show_subscribe_button_on_timeline: false
show_followed_by: false
follow_button_to_list_adder: false
show_target: false
notification_emails:
follow: false

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddIndexUrlToStatuses < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def change
add_index :statuses, :url, algorithm: :concurrently
end
end