diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index ec00f0c0668014ff04905d598958262527d5f207..c210d336a3817673ae78f34a5cf7f6abb5266ffb 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -59,6 +59,9 @@ class Settings::PreferencesController < Settings::BaseController :setting_use_pending_items, :setting_trends, :setting_crop_images, + :setting_show_follow_button_on_timeline, + :setting_show_subscribe_button_on_timeline, + :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/bookmarks.js b/app/javascript/mastodon/actions/bookmarks.js index 544ed2ff2244553b9e39e1ef8133a7fc3681a973..b3e0a2ab4b992517d13c5345610a21b11a5fb21b 100644 --- a/app/javascript/mastodon/actions/bookmarks.js +++ b/app/javascript/mastodon/actions/bookmarks.js @@ -1,5 +1,7 @@ +import { fetchRelationships } from './accounts'; import api, { getLinks } from '../api'; import { importFetchedStatuses } from './importer'; +import { uniq } from '../utils/uniq'; export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST'; export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS'; @@ -20,6 +22,7 @@ export function fetchBookmarkedStatuses() { api(getState).get('/api/v1/bookmarks').then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); + dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id)))); dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null)); }).catch(error => { dispatch(fetchBookmarkedStatusesFail(error)); @@ -61,6 +64,7 @@ export function expandBookmarkedStatuses() { api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); + dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id)))); dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null)); }).catch(error => { dispatch(expandBookmarkedStatusesFail(error)); diff --git a/app/javascript/mastodon/actions/favourites.js b/app/javascript/mastodon/actions/favourites.js index 9448b1efe7eae93a64212d91cadde348cb7f086f..9b28ac4c4647ba0c8489be19013e72ecff5c9464 100644 --- a/app/javascript/mastodon/actions/favourites.js +++ b/app/javascript/mastodon/actions/favourites.js @@ -1,5 +1,7 @@ +import { fetchRelationships } from './accounts'; import api, { getLinks } from '../api'; import { importFetchedStatuses } from './importer'; +import { uniq } from '../utils/uniq'; export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST'; export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS'; @@ -20,6 +22,7 @@ export function fetchFavouritedStatuses() { api(getState).get('/api/v1/favourites').then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); + dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id)))); dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null)); }).catch(error => { dispatch(fetchFavouritedStatusesFail(error)); @@ -64,6 +67,7 @@ export function expandFavouritedStatuses() { api(getState).get(url).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); + dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id)))); dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null)); }).catch(error => { dispatch(expandFavouritedStatusesFail(error)); diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 798f9b37ea5182a6022f08d3b75ff68d21e77d54..c6ac071246840d717eb540802e39aa6fc295f334 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -38,7 +38,7 @@ defineMessages({ }); const fetchRelatedRelationships = (dispatch, notifications) => { - const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id); + const accountIds = notifications.map(item => item.account.id); if (accountIds.length > 0) { dispatch(fetchRelationships(accountIds)); diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index ee54a082da9efb3c6ca2666e695f928913bca065..baf106207a347d7fb50df16fad621f1e23ece44f 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -1,8 +1,10 @@ +import { fetchRelationships } from './accounts'; import { importFetchedStatus, importFetchedStatuses } from './importer'; import api, { getLinks } from 'mastodon/api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import compareId from 'mastodon/compare_id'; import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; +import { uniq } from '../utils/uniq'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; @@ -29,6 +31,7 @@ export function updateTimeline(timeline, status, accept) { } dispatch(importFetchedStatus(status)); + dispatch(fetchRelationships([status.reblog ? status.reblog.account.id : status.account.id])); dispatch({ type: TIMELINE_UPDATE, @@ -97,6 +100,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); + dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id)))); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); done(); }).catch(error => { diff --git a/app/javascript/mastodon/components/account_action_bar.js b/app/javascript/mastodon/components/account_action_bar.js new file mode 100644 index 0000000000000000000000000000000000000000..3719d2b499da0ddb8605a170a857b72d90536b29 --- /dev/null +++ b/app/javascript/mastodon/components/account_action_bar.js @@ -0,0 +1,73 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +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'; + +const messages = defineMessages({ + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe' }, + subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, +}); + +export default @injectIntl +class AccountActionBar extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + onFollow: PropTypes.func.isRequired, + onSubscribe: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + updateOnProps = [ + 'account', + ] + + handleFollow = () => { + this.props.onFollow(this.props.account); + } + + handleSubscribe = () => { + this.props.onSubscribe(this.props.account); + } + + render () { + const { account, intl } = this.props; + + if (!account || (!show_follow_button_on_timeline && !show_subscribe_button_on_timeline)) { + return <div />; + } + + 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']); + + 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 (show_follow_button_on_timeline && (!account.get('moved') || following)) { + if (requested) { + following_buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />; + } else { + following_buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />; + } + } + buttons = <span>{subscribing_buttons}{following_buttons}</span> + } + + return ( + <div className='account__action-bar'> + {buttons} + </div> + ); + } + +} diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 2177f0cfabe0b5c189d7eee3f24e66f059c0cc5e..6f763b46e6e5dcdc385e58b0f5339363cdffbf30 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -8,6 +8,7 @@ import RelativeTimestamp from './relative_timestamp'; import DisplayName from './display_name'; import StatusContent from './status_content'; import StatusActionBar from './status_action_bar'; +import AccountActionBar from './account_action_bar'; import AttachmentList from './attachment_list'; import Card from '../features/status/components/card'; import { injectIntl, FormattedMessage } from 'react-intl'; @@ -78,6 +79,8 @@ class Status extends ImmutablePureComponent { onToggleHidden: PropTypes.func, onToggleCollapsed: PropTypes.func, onQuoteToggleHidden: PropTypes.func, + onFollow: PropTypes.func.isRequired, + onSubscribe: PropTypes.func.isRequired, muted: PropTypes.bool, hidden: PropTypes.bool, unread: PropTypes.bool, @@ -274,6 +277,14 @@ class Status extends ImmutablePureComponent { return status.get('quote'); } + handleFollow = () => { + this.props.onFollow(this._properStatus().get('account')); + } + + handleSubscribe = () => { + this.props.onSubscribe(this._properStatus().get('account')); + } + render () { let media = null; let statusAvatar, prepend, rebloggedByText, unlistedQuoteText; @@ -487,6 +498,7 @@ class Status extends ImmutablePureComponent { {prepend} <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}> + <AccountActionBar account={status.get('account')} {...other} /> <div className='status__expand' onClick={this.handleExpandClick} role='presentation' /> <div className='status__info'> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a> diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index adb7a4e8b4b81b4cecb529515d2259bea04f228b..57a8b7d3da1ecde37bec898994f6d81e427e5030 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -29,6 +29,10 @@ import { revealQuote, } from '../actions/statuses'; import { + followAccount, + unfollowAccount, + subscribeAccount, + unsubscribeAccount, unmuteAccount, unblockAccount, } from '../actions/accounts'; @@ -36,12 +40,13 @@ import { blockDomain, unblockDomain, } from '../actions/domain_blocks'; + import { initMuteModal } from '../actions/mutes'; import { initBlockModal } from '../actions/blocks'; import { initReport } from '../actions/reports'; import { openModal } from '../actions/modal'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { boostModal, deleteModal } from '../initial_state'; +import { boostModal, deleteModal, unfollowModal, unsubscribeModal } from '../initial_state'; import { showAlertForError } from '../actions/alerts'; const messages = defineMessages({ @@ -52,6 +57,8 @@ const messages = defineMessages({ replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, + unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, + unsubscribeConfirm: { id: 'confirmations.unsubscribe.confirm', defaultMessage: 'Unsubscribe' }, }); const makeMapStateToProps = () => { @@ -222,6 +229,37 @@ 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'))); + } + }, + + onSubscribe (account) { + if (account.getIn(['relationship', 'subscribing'])) { + if (unsubscribeModal) { + dispatch(openModal('CONFIRM', { + message: <FormattedMessage id='confirmations.unsubscribe.message' defaultMessage='Are you sure you want to unsubscribe {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, + confirm: intl.formatMessage(messages.unsubscribeConfirm), + onConfirm: () => dispatch(unsubscribeAccount(account.get('id'))), + })); + } else { + dispatch(unsubscribeAccount(account.get('id'))); + } + } else { + dispatch(subscribeAccount(account.get('id'))); + } + }, }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index e724b7959221210184c946d2571f2b70d58d9a8d..2c55c7ba00e96f7bc3d2cd4e503c0f32d2aee037 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -26,5 +26,8 @@ export const usePendingItems = getMeta('use_pending_items'); export const showTrends = getMeta('trends'); export const title = getMeta('title'); export const cropImages = getMeta('crop_images'); +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_target = getMeta('show_target'); export default initialState; diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index 6f1ce9602a4986beee09fa21bbc5c5ad60f8771f..01d914b923ce27eef0aaf8790d233109d9075923 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -89,15 +89,28 @@ export const makeGetStatus = () => { (state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]), + (state, { id }) => state.getIn(['relationships', state.getIn(['statuses', id, 'account'])]), + (state, { id }) => state.getIn(['relationships', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]), + (state, { id }) => state.getIn(['accounts', state.getIn(['accounts', state.getIn(['statuses', id, 'account']), 'moved'])]), + (state, { id }) => state.getIn(['accounts', state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account']), 'moved'])]), getFiltersRegex, ], - (statusBase, statusReblog, accountBase, accountReblog, filtersRegex) => { + (statusBase, statusReblog, accountBase, accountReblog, relationship, reblogRelationship, moved, reblogMoved, filtersRegex) => { if (!statusBase) { return null; } + accountBase = accountBase.withMutations(map => { + map.set('relationship', relationship); + map.set('moved', moved); + }); + if (statusReblog) { + accountReblog = accountReblog.withMutations(map => { + map.set('relationship', reblogRelationship); + map.set('moved', reblogMoved); + }); statusReblog = statusReblog.set('account', accountReblog); } else { statusReblog = null; diff --git a/app/javascript/mastodon/utils/uniq.js b/app/javascript/mastodon/utils/uniq.js new file mode 100644 index 0000000000000000000000000000000000000000..00f1804a19e07d60eb7540798f7962d57bb31c51 --- /dev/null +++ b/app/javascript/mastodon/utils/uniq.js @@ -0,0 +1,3 @@ +export const uniq = array => { + return array.filter((x, i, self) => self.indexOf(x) === i) +}; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 427dad4e3d359505dc895bc7af14ef4042d385ad..b4aec09f023f919204eb52b34b9458dc852eccdb 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1421,76 +1421,6 @@ a .account__avatar { } } -.account__action-bar { - border-top: 1px solid lighten($ui-base-color, 8%); - border-bottom: 1px solid lighten($ui-base-color, 8%); - line-height: 36px; - overflow: hidden; - flex: 0 0 auto; - display: flex; -} - -.account__action-bar-dropdown { - padding: 10px; - - .icon-button { - vertical-align: middle; - } - - .dropdown--active { - .dropdown__content.dropdown__right { - left: 6px; - right: initial; - } - - &::after { - bottom: initial; - margin-left: 11px; - margin-top: -7px; - right: initial; - } - } -} - -.account__action-bar-links { - display: flex; - flex: 1 1 auto; - line-height: 18px; - text-align: center; -} - -.account__action-bar__tab { - text-decoration: none; - overflow: hidden; - flex: 0 1 100%; - border-right: 1px solid lighten($ui-base-color, 8%); - padding: 10px 0; - border-bottom: 4px solid transparent; - - &.active { - border-bottom: 4px solid $ui-highlight-color; - } - - & > span { - display: block; - font-size: 12px; - color: $darker-text-color; - } - - strong { - display: block; - font-size: 15px; - font-weight: 500; - color: $primary-text-color; - - @each $lang in $cjk-langs { - &:lang(#{$lang}) { - font-weight: 700; - } - } - } -} - .account-authorize { padding: 14px 10px; @@ -1577,6 +1507,15 @@ a.account__display-name { margin-right: 10px; } +.account__action-bar { + position: absolute; + height: 24px; + width: 48px; + top: 60px; + left: 10px; + z-index: 1; +} + .status__avatar { height: 48px; left: 10px; @@ -2331,6 +2270,11 @@ a.account__display-name { margin-right: 15px; } } + + .account__action-bar { + top: 67px; + left: 15px; + } } } diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 68b388c8e89b0e662aac438c1e2714b4f24e086e..a031378a3b9876f4096822bc628fccb88f2371c3 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -15,30 +15,33 @@ class UserSettingsDecorator private def process_update - user.settings['notification_emails'] = merged_notification_emails if change?('notification_emails') - user.settings['interactions'] = merged_interactions if change?('interactions') - user.settings['default_privacy'] = default_privacy_preference if change?('setting_default_privacy') - user.settings['default_sensitive'] = default_sensitive_preference if change?('setting_default_sensitive') - user.settings['default_language'] = default_language_preference if change?('setting_default_language') - user.settings['unfollow_modal'] = unfollow_modal_preference if change?('setting_unfollow_modal') - user.settings['unsubscribe_modal'] = unsubscribe_modal_preference if change?('setting_unsubscribe_modal') - user.settings['boost_modal'] = boost_modal_preference if change?('setting_boost_modal') - user.settings['delete_modal'] = delete_modal_preference if change?('setting_delete_modal') - user.settings['auto_play_gif'] = auto_play_gif_preference if change?('setting_auto_play_gif') - user.settings['display_media'] = display_media_preference if change?('setting_display_media') - user.settings['expand_spoilers'] = expand_spoilers_preference if change?('setting_expand_spoilers') - user.settings['reduce_motion'] = reduce_motion_preference if change?('setting_reduce_motion') - user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui') - user.settings['noindex'] = noindex_preference if change?('setting_noindex') - user.settings['theme'] = theme_preference if change?('setting_theme') - user.settings['hide_network'] = hide_network_preference if change?('setting_hide_network') - user.settings['aggregate_reblogs'] = aggregate_reblogs_preference if change?('setting_aggregate_reblogs') - user.settings['show_application'] = show_application_preference if change?('setting_show_application') - user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout') - user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash') - user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items') - user.settings['trends'] = trends_preference if change?('setting_trends') - user.settings['crop_images'] = crop_images_preference if change?('setting_crop_images') + user.settings['notification_emails'] = merged_notification_emails if change?('notification_emails') + user.settings['interactions'] = merged_interactions if change?('interactions') + user.settings['default_privacy'] = default_privacy_preference if change?('setting_default_privacy') + user.settings['default_sensitive'] = default_sensitive_preference if change?('setting_default_sensitive') + user.settings['default_language'] = default_language_preference if change?('setting_default_language') + user.settings['unfollow_modal'] = unfollow_modal_preference if change?('setting_unfollow_modal') + user.settings['unsubscribe_modal'] = unsubscribe_modal_preference if change?('setting_unsubscribe_modal') + user.settings['boost_modal'] = boost_modal_preference if change?('setting_boost_modal') + user.settings['delete_modal'] = delete_modal_preference if change?('setting_delete_modal') + user.settings['auto_play_gif'] = auto_play_gif_preference if change?('setting_auto_play_gif') + user.settings['display_media'] = display_media_preference if change?('setting_display_media') + user.settings['expand_spoilers'] = expand_spoilers_preference if change?('setting_expand_spoilers') + user.settings['reduce_motion'] = reduce_motion_preference if change?('setting_reduce_motion') + user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui') + user.settings['noindex'] = noindex_preference if change?('setting_noindex') + user.settings['theme'] = theme_preference if change?('setting_theme') + user.settings['hide_network'] = hide_network_preference if change?('setting_hide_network') + user.settings['aggregate_reblogs'] = aggregate_reblogs_preference if change?('setting_aggregate_reblogs') + user.settings['show_application'] = show_application_preference if change?('setting_show_application') + user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout') + user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash') + user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items') + user.settings['trends'] = trends_preference if change?('setting_trends') + 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_target'] = show_target_preference if change?('setting_show_target') end def merged_notification_emails @@ -137,6 +140,18 @@ class UserSettingsDecorator boolean_cast_setting 'setting_crop_images' end + def show_follow_button_on_timeline_preference + boolean_cast_setting 'setting_show_follow_button_on_timeline' + end + + def show_subscribe_button_on_timeline_preference + boolean_cast_setting 'setting_show_subscribe_button_on_timeline' + end + + def show_target_preference + boolean_cast_setting 'setting_show_target' + end + def boolean_cast_setting(key) ActiveModel::Type::Boolean.new.cast(settings[key]) end diff --git a/app/models/user.rb b/app/models/user.rb index 01723e97d0e9c975071d1017599b48661263c427..e2851860e4bd9755fa7970edeef0859b15e8c020 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -110,6 +110,7 @@ class User < ApplicationRecord :reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network, :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, :advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images, + :show_follow_button_on_timeline, :show_subscribe_button_on_timeline, :show_target, to: :settings, prefix: :setting, allow_nil: false attr_reader :invite_code diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index dde09716172295c715494b63c11362f07ac321ca..d43f4ca30a73f13590f39dd4a68f86d10093c16b 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -25,22 +25,25 @@ class InitialStateSerializer < ActiveModel::Serializer } if object.current_account - store[:me] = object.current_account.id.to_s - store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal - store[:unsubscribe_modal] = object.current_account.user.setting_unsubscribe_modal - store[:boost_modal] = object.current_account.user.setting_boost_modal - store[:delete_modal] = object.current_account.user.setting_delete_modal - store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif - store[:display_media] = object.current_account.user.setting_display_media - store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers - store[:reduce_motion] = object.current_account.user.setting_reduce_motion - store[:advanced_layout] = object.current_account.user.setting_advanced_layout - store[:use_blurhash] = object.current_account.user.setting_use_blurhash - store[:use_pending_items] = object.current_account.user.setting_use_pending_items - store[:is_staff] = object.current_account.user.staff? - store[:trends] = Setting.trends && object.current_account.user.setting_trends - store[:crop_images] = object.current_account.user.setting_crop_images - else + store[:me] = object.current_account.id.to_s + store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal + store[:unsubscribe_modal] = object.current_account.user.setting_unsubscribe_modal + store[:boost_modal] = object.current_account.user.setting_boost_modal + store[:delete_modal] = object.current_account.user.setting_delete_modal + store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif + store[:display_media] = object.current_account.user.setting_display_media + store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers + store[:reduce_motion] = object.current_account.user.setting_reduce_motion + store[:advanced_layout] = object.current_account.user.setting_advanced_layout + store[:use_blurhash] = object.current_account.user.setting_use_blurhash + store[:use_pending_items] = object.current_account.user.setting_use_pending_items + store[:is_staff] = object.current_account.user.staff? + store[:trends] = Setting.trends && object.current_account.user.setting_trends + 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_target] = object.current_account.user.setting_show_target + else store[:auto_play_gif] = Setting.auto_play_gif store[:display_media] = Setting.display_media store[:reduce_motion] = Setting.reduce_motion diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index db7d806bc05445eed383840606b674be059d0b2a..962fd6802f0f7184566bb8340c979f2f4ad5fbe5 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -28,6 +28,17 @@ .fields-group = f.input :setting_show_application, as: :boolean, wrapper: :with_label, recommended: true + %h4= t 'preferences.fedibird_features' + + .fields-group + = f.input :setting_show_follow_button_on_timeline, as: :boolean, wrapper: :with_label + + .fields-group + = f.input :setting_show_subscribe_button_on_timeline, as: :boolean, wrapper: :with_label + + -# .fields-group + -# = f.input :setting_show_target, as: :boolean, wrapper: :with_label + %h4= t 'preferences.public_timelines' .fields-group diff --git a/config/locales/en.yml b/config/locales/en.yml index 7ab08c21bffa7a0048d6031a259b2db7e2f51f9c..2b13f40d83a1fcb93850faefb7878d41c8136a96 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -997,6 +997,7 @@ en: too_few_options: must have more than one item too_many_options: can't contain more than %{max} items preferences: + fedibird_features: Fedibird features other: Other posting_defaults: Posting defaults public_timelines: Public timelines diff --git a/config/locales/ja.yml b/config/locales/ja.yml index f34f60a759c6294de382c2abc71b4e1cd871c4c8..c7355f2408b6fd83574f56c7094c40105504539b 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -966,6 +966,7 @@ ja: too_few_options: ã¯è¤‡æ•°å¿…è¦ã§ã™ too_many_options: ã¯%{max}個ã¾ã§ã§ã™ preferences: + fedibird_features: Fedibirdã®æ©Ÿèƒ½ other: ãã®ä»– posting_defaults: ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã®æŠ•ç¨¿è¨å®š public_timelines: 公開タイムライン diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 0bbb8d755caa7df8f43d3f3265db95b1af52ce27..6531812589b6b3922eb61d17352d62cdbfe131cc 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -41,6 +41,9 @@ en: setting_hide_network: Who you follow and who follows you will not be shown on your profile setting_noindex: Affects your public profile and status pages setting_show_application: The application you use to toot will be displayed in the detailed view of your toots + 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_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 username: Your username will be unique on %{domain} @@ -140,6 +143,9 @@ en: setting_noindex: Opt-out of search engine indexing setting_reduce_motion: Reduce motion in animations setting_show_application: Disclose application used to send toots + setting_show_follow_button_on_timeline: Show follow button on timeline + setting_show_subscribe_button_on_timeline: Show subscribe button on timeline + setting_show_target: Enable targeting features setting_system_font_ui: Use system's default font setting_theme: Site theme setting_trends: Show today's trends diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 1a29511a99bb3d275d1bcf81a73d0cabd11ad04d..f4222a97e820537fb30b1bdd977004d0932f0046 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -41,6 +41,9 @@ ja: setting_hide_network: フォãƒãƒ¼ã¨ãƒ•ã‚©ãƒãƒ¯ãƒ¼ã®æƒ…å ±ãŒãƒ—ãƒãƒ•ィールページã§è¦‹ã‚‰ã‚Œãªã„よã†ã«ã—ã¾ã™ setting_noindex: 公開プãƒãƒ•ィールãŠã‚ˆã³å„投稿ページã«å½±éŸ¿ã—ã¾ã™ setting_show_application: トゥートã™ã‚‹ã®ã«ä½¿ç”¨ã—ãŸã‚¢ãƒ—リãŒãƒˆã‚¥ãƒ¼ãƒˆã®è©³ç´°ãƒ“ューã«è¡¨ç¤ºã•れるよã†ã«ãªã‚Šã¾ã™ + setting_show_follow_button_on_timeline: フォãƒãƒ¼çŠ¶æ…‹ã‚’ç¢ºèªã—易ããªã‚Šã€ç´ æ—©ãフォãƒãƒ¼ãƒªã‚¹ãƒˆã‚’構築ã§ãã¾ã™ + setting_show_subscribe_button_on_timeline: è³¼èªçŠ¶æ…‹ã‚’ç¢ºèªã—易ããªã‚Šã€ç´ æ—©ãè³¼èªãƒªã‚¹ãƒˆã‚’構築ã§ãã¾ã™ + setting_show_target: 投稿対象ã¨ã€ãƒ•ã‚©ãƒãƒ¼ãƒ»è³¼èªã®å¯¾è±¡ã‚’切り替ãˆã‚‹æ©Ÿèƒ½ã‚’有効ã«ã—ã¾ã™ setting_use_blurhash: ã¼ã‹ã—ã¯ãƒ¡ãƒ‡ã‚£ã‚¢ã®è‰²ã‚’å…ƒã«ç”Ÿæˆã•れã¾ã™ãŒã€ç´°éƒ¨ã¯è¦‹ãˆã«ãããªã£ã¦ã„ã¾ã™ setting_use_pending_items: æ–°ç€ãŒã‚ã£ã¦ã‚‚タイムラインを自動的ã«ã‚¹ã‚¯ãƒãƒ¼ãƒ«ã—ãªã„よã†ã«ã—ã¾ã™ username: ã‚ãªãŸã®ãƒ¦ãƒ¼ã‚¶ãƒ¼å㯠%{domain} ã®ä¸ã§é‡è¤‡ã—ã¦ã„ãªã„å¿…è¦ãŒã‚りã¾ã™ @@ -140,6 +143,9 @@ ja: setting_noindex: 検索エンジンã«ã‚ˆã‚‹ã‚¤ãƒ³ãƒ‡ãƒƒã‚¯ã‚¹ã‚’æ‹’å¦ã™ã‚‹ setting_reduce_motion: アニメーションã®å‹•ãを減ら㙠setting_show_application: é€ä¿¡ã—ãŸã‚¢ãƒ—リを開示ã™ã‚‹ + setting_show_follow_button_on_timeline: タイムライン上ã«ãƒ•ã‚©ãƒãƒ¼ãƒœã‚¿ãƒ³ã‚’表示ã™ã‚‹ + setting_show_subscribe_button_on_timeline: タイムライン上ã«è³¼èªãƒœã‚¿ãƒ³ã‚’表示ã™ã‚‹ + setting_show_target: ターゲット機能を有効ã«ã™ã‚‹ setting_system_font_ui: システムã®ãƒ‡ãƒ•ォルトフォントを使ㆠsetting_theme: サイトテーマ setting_trends: 本日ã®ãƒˆãƒ¬ãƒ³ãƒ‰ã‚¿ã‚°ã‚’表示ã™ã‚‹ diff --git a/config/settings.yml b/config/settings.yml index f31e7c90cbba516a9bafc28f054b6ff89cbdfcfc..2b4fa5fe3e9e582ef3e9ef3283fb514dbd0f4c32 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -38,6 +38,9 @@ defaults: &defaults trends: true trendable_by_default: false crop_images: true + show_follow_button_on_timeline: false + show_subscribe_button_on_timeline: false + show_target: false notification_emails: follow: false reblog: false