diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 3f48e2ba8..a5a6c0324 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -56,6 +56,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/components/account_action_bar.js b/app/javascript/mastodon/components/account_action_bar.js new file mode 100644 index 000000000..3719d2b49 --- /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
; + } + + 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 = ; + } + if (show_follow_button_on_timeline && (!account.get('moved') || following)) { + if (requested) { + following_buttons = ; + } else { + following_buttons = ; + } + } + buttons = {subscribing_buttons}{following_buttons} + } + + return ( +
+ {buttons} +
+ ); + } + +} diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 5cb49d312..2d07e7e5a 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -9,6 +9,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, defineMessages, FormattedMessage } from 'react-intl'; @@ -112,6 +113,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, @@ -346,6 +349,14 @@ 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; @@ -658,6 +669,7 @@ class Status extends ImmutablePureComponent { {prepend}
+
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index ce055cd96..7302496c8 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,6 +40,7 @@ import { blockDomain, unblockDomain, } from '../actions/domain_blocks'; + import { initMuteModal } from '../actions/mutes'; import { initBlockModal } from '../actions/blocks'; import { initBoostModal } from '../actions/boosts'; @@ -43,7 +48,7 @@ import { initReport } from '../actions/reports'; import { openModal } from '../actions/modal'; import { deployPictureInPicture } from '../actions/picture_in_picture'; 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({ @@ -56,6 +61,8 @@ const messages = defineMessages({ quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' }, quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting 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 = () => { @@ -244,6 +251,37 @@ 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'))); + } + }, + + onSubscribe (account) { + if (account.getIn(['relationship', 'subscribing'])) { + if (unsubscribeModal) { + dispatch(openModal('CONFIRM', { + message: @{account.get('acct')} }} />, + 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 c68942118..085efb6f4 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -28,5 +28,8 @@ export const showTrends = getMeta('trends'); export const title = getMeta('title'); 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_target = getMeta('show_target'); export default initialState; diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 6eb32c862..0a4a76ce1 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -15,31 +15,34 @@ 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['disable_swiping'] = disable_swiping_preference if change?('setting_disable_swiping') - 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['disable_swiping'] = disable_swiping_preference if change?('setting_disable_swiping') + 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 @@ -142,6 +145,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 164631d16..0f9d1d625 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -126,6 +126,7 @@ class User < ApplicationRecord :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, :advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images, :disable_swiping, + :show_follow_button_on_timeline, :show_subscribe_button_on_timeline, :show_target, 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 aceca1dcc..a8c750f02 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -26,23 +26,26 @@ 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[:disable_swiping] = object.current_account.user.setting_disable_swiping - 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[:disable_swiping] = object.current_account.user.setting_disable_swiping + 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 539a70056..356272e5f 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -31,6 +31,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 9cdb9b782..2e1e3055c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1217,6 +1217,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 fa693d889..df4532807 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1162,6 +1162,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 df87eb31a..ac22c1586 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -54,6 +54,9 @@ en: 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_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} @@ -177,6 +180,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 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_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 f2b96cf39..258e46e4e 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -54,6 +54,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} の中で重複していない必要があります @@ -177,6 +180,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 7d47d4e9d..84212c513 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -39,6 +39,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