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