diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index c832f0f80..1e4cdc555 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -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 diff --git a/app/controllers/api/v1/lists/subscribes_controller.rb b/app/controllers/api/v1/lists/subscribes_controller.rb new file mode 100644 index 000000000..c6486ed6f --- /dev/null +++ b/app/controllers/api/v1/lists/subscribes_controller.rb @@ -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 diff --git a/app/controllers/settings/account_subscribes_controller.rb b/app/controllers/settings/account_subscribes_controller.rb index e21badd4e..9f56923aa 100644 --- a/app/controllers/settings/account_subscribes_controller.rb +++ b/app/controllers/settings/account_subscribes_controller.rb @@ -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 diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index a5a6c0324..8734b198e 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -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) diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index 3d29916f4..eebc8304d 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -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)); diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js index cd3cebb14..0e2c42302 100644 --- a/app/javascript/mastodon/components/account.js +++ b/app/javascript/mastodon/components/account.js @@ -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 = ; } } 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 = ; @@ -116,10 +129,10 @@ class Account extends ImmutablePureComponent { } else { let following_buttons, subscribing_buttons; if (!account.get('moved') || subscribing ) { - subscribing_buttons = ; + subscribing_buttons = ; } if (!account.get('moved') || following) { - following_buttons = ; + following_buttons = ; } buttons = {subscribing_buttons}{following_buttons} } diff --git a/app/javascript/mastodon/components/account_action_bar.js b/app/javascript/mastodon/components/account_action_bar.js index 3719d2b49..462c3f34f 100644 --- a/app/javascript/mastodon/components/account_action_bar.js +++ b/app/javascript/mastodon/components/account_action_bar.js @@ -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 = ; + if (!account.get('moved') || subscribing) { + subscribing_buttons = ; } - if (show_follow_button_on_timeline && (!account.get('moved') || following)) { + if (!account.get('moved') || following) { if (requested) { - following_buttons = ; + following_buttons = ; } else { - following_buttons = ; + following_buttons = ; } } buttons = {subscribing_buttons}{following_buttons} diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js index 7ec39198a..c9bc290ac 100644 --- a/app/javascript/mastodon/components/icon_button.js +++ b/app/javascript/mastodon/components/icon_button.js @@ -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, diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 2d07e7e5a..b7c85482d 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -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; diff --git a/app/javascript/mastodon/containers/account_container.js b/app/javascript/mastodon/containers/account_container.js index ac69b9150..1df63a045 100644 --- a/app/javascript/mastodon/containers/account_container.js +++ b/app/javascript/mastodon/containers/account_container.js @@ -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: @{account.get('acct')} }} />, @@ -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'))); diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index 7302496c8..ba7d90f7e 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -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: @{account.get('acct')} }} />, @@ -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)); diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 825f36214..652882690 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -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 = ; + subscribing_buttons = ; } if(!account.get('moved') || following) { - following_buttons = ; + following_buttons = ; } buttons = {subscribing_buttons}{following_buttons} } diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js index 44f8361f3..e5e42fc9d 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.js +++ b/app/javascript/mastodon/features/account_timeline/components/header.js @@ -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 = () => { diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js index d6b96eec3..689d74ff9 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js @@ -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: @{account.get('acct')} }} />, @@ -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'))); diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js index 4c4145ae4..629ccdc79 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.js +++ b/app/javascript/mastodon/features/directory/components/account_card.js @@ -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: @{account.get('acct')} }} />, @@ -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} /> ); } diff --git a/app/javascript/mastodon/features/list_adder/components/account.js b/app/javascript/mastodon/features/list_adder/components/account.js index 1369aac07..22f340371 100644 --- a/app/javascript/mastodon/features/list_adder/components/account.js +++ b/app/javascript/mastodon/features/list_adder/components/account.js @@ -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: @{account.get('acct')} }} />, + 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 = ; + } else { + buttons = ; + } + } + } + return (
@@ -35,6 +81,10 @@ class Account extends ImmutablePureComponent {
+ +
+ {buttons} +
); diff --git a/app/javascript/mastodon/features/list_adder/components/home.js b/app/javascript/mastodon/features/list_adder/components/home.js new file mode 100644 index 000000000..b0ce75d84 --- /dev/null +++ b/app/javascript/mastodon/features/list_adder/components/home.js @@ -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 = ; + } + if (added) { + button = ; + } else if (disabled) { + button = ; + } else { + button = ''; + } + + return ( +
+
+
+ + {intl.formatMessage(messages.title)} +
+ +
+ {subscribing_buttons} + {button} +
+
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/list_adder/components/list.js b/app/javascript/mastodon/features/list_adder/components/list.js index 60c8958a7..041e8e202 100644 --- a/app/javascript/mastodon/features/list_adder/components/list.js +++ b/app/javascript/mastodon/features/list_adder/components/list.js @@ -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 = ; + } if (added) { - button = ; - } else { + button = ; + } else if (disabled) { button = ; + } else { + button = ''; } return ( @@ -59,6 +86,7 @@ class List extends ImmutablePureComponent {
+ {subscribing_buttons} {button}
diff --git a/app/javascript/mastodon/features/list_adder/index.js b/app/javascript/mastodon/features/list_adder/index.js index cb8a15e8c..61c2ecab3 100644 --- a/app/javascript/mastodon/features/list_adder/index.js +++ b/app/javascript/mastodon/features/list_adder/index.js @@ -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 (
- +
-
- {listIds.map(ListId => )} + + {listIds.map(ListId => )}
); diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index 377cccda5..52ce79084 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -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 { diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 085efb6f4..158448f32 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -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; diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 5305b236d..d1cfb3243 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -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", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index b571c5f95..c2ce10b4a 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -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": "返信表示", diff --git a/app/javascript/mastodon/reducers/accounts_counters.js b/app/javascript/mastodon/reducers/accounts_counters.js index 9a89544ef..e939ac21e 100644 --- a/app/javascript/mastodon/reducers/accounts_counters.js +++ b/app/javascript/mastodon/reducers/accounts_counters.js @@ -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; } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index d5175134e..3688666eb 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -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 { diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss index baacf46b9..c70a89c7d 100644 --- a/app/javascript/styles/mastodon/rtl.scss +++ b/app/javascript/styles/mastodon/rtl.scss @@ -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 { diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 1d3572012..ddaf4f257 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -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) diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 0a4a76ce1..eae2d23aa 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -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 diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 6650abcac..b0e6aa12e 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -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 diff --git a/app/models/list.rb b/app/models/list.rb index cdc6ebdb3..7cfc1d169 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -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| diff --git a/app/models/user.rb b/app/models/user.rb index 0f9d1d625..b5f3d1665 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index a8c750f02..df57868b4 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -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 diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb index 0584f4a6f..49c09e64f 100644 --- a/app/serializers/rest/relationship_serializer.rb +++ b/app/serializers/rest/relationship_serializer.rb @@ -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 diff --git a/app/services/account_subscribe_service.rb b/app/services/account_subscribe_service.rb index d70f62789..7d5675a86 100644 --- a/app/services/account_subscribe_service.rb +++ b/app/services/account_subscribe_service.rb @@ -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 diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 6b23c1878..d8e3f4109 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -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? diff --git a/app/services/unsubscribe_account_service.rb b/app/services/unsubscribe_account_service.rb index 534f5d366..0be351325 100644 --- a/app/services/unsubscribe_account_service.rb +++ b/app/services/unsubscribe_account_service.rb @@ -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 diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index 356272e5f..54c01a449 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -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 diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index ac22c1586..632203f9a 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -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 diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 258e46e4e..08d8cf4bf 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -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: サイトテーマ diff --git a/config/routes.rb b/config/routes.rb index f25a08671..24feab6d2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/config/settings.yml b/config/settings.yml index 84212c513..4f1b47971 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -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 diff --git a/db/migrate/20200312020757_add_index_url_to_statuses.rb b/db/migrate/20200312020757_add_index_url_to_statuses.rb new file mode 100644 index 000000000..40b2e68cf --- /dev/null +++ b/db/migrate/20200312020757_add_index_url_to_statuses.rb @@ -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