diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 00819041d..b698dfda9 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -84,7 +84,16 @@ class Api::V1::StatusesController < Api::BaseController end def set_circle - @circle = status_params[:circle_id].blank? ? nil : current_account.owned_circles.find(status_params[:circle_id]) + @circle = begin + if status_params[:visibility] == 'mutual' + status_params[:visibility] = 'limited' + current_account + elsif status_params[:circle_id].blank? + nil + else + current_account.owned_circles.find(status_params[:circle_id]) + end + end rescue ActiveRecord::RecordNotFound render json: { error: I18n.t('statuses.errors.circle_not_found') }, status: 404 end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 71aac1f60..8e30d12a8 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -178,7 +178,7 @@ module ApplicationHelper text: [params[:title], params[:text], params[:url]].compact.join(' '), } - permit_visibilities = %w(public unlisted private direct) + permit_visibilities = %w(public unlisted private mutual direct) default_privacy = current_account&.user&.setting_default_privacy permit_visibilities.shift(permit_visibilities.index(default_privacy) + 1) if default_privacy.present? state_params[:visibility] = params[:visibility] if permit_visibilities.include? params[:visibility] diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index d1c2073ac..5c548f015 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -82,6 +82,7 @@ const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, + mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual-followers-only' }, limited_short: { id: 'privacy.limited.short', defaultMessage: 'Circle' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, }); @@ -540,6 +541,7 @@ class Status extends ImmutablePureComponent { 'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) }, 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) }, 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) }, + 'mutual': { icon: 'exchange', text: intl.formatMessage(messages.mutual_short) }, 'limited': { icon: 'user-circle', text: intl.formatMessage(messages.limited_short) }, 'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) }, }; diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js index f9249f39d..99fdb363b 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -16,6 +16,8 @@ const messages = defineMessages({ unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but not in public timelines' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' }, + mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutuals-followers-only' }, + mutual_long: { id: 'privacy.mutual.long', defaultMessage: 'Visible for mutual followers only (Supported servers only)' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' }, limited_short: { id: 'privacy.limited.short', defaultMessage: 'Circle' }, @@ -239,6 +241,7 @@ class PrivacyDropdown extends React.PureComponent { { icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) }, { icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) }, { icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) }, + { icon: 'exchange', value: 'mutual', text: formatMessage(messages.mutual_short), meta: formatMessage(messages.mutual_long) }, { icon: 'user-circle', value: 'limited', text: formatMessage(messages.limited_short), meta: formatMessage(messages.limited_long) }, ]; diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index 9eb507db6..c0e8d8150 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -22,6 +22,7 @@ const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, + mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual-followers-only' }, limited_short: { id: 'privacy.limited.short', defaultMessage: 'Circle' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, }); @@ -338,6 +339,7 @@ class DetailedStatus extends ImmutablePureComponent { 'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) }, 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) }, 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) }, + 'mutual': { icon: 'exchange', text: intl.formatMessage(messages.mutual_short) }, 'limited': { icon: 'user-circle', text: intl.formatMessage(messages.limited_short) }, 'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) }, }; diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.js b/app/javascript/mastodon/features/ui/components/boost_modal.js index 4060844f5..b25fe6560 100644 --- a/app/javascript/mastodon/features/ui/components/boost_modal.js +++ b/app/javascript/mastodon/features/ui/components/boost_modal.js @@ -21,6 +21,7 @@ const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, + mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual-followers-only' }, limited_short: { id: 'privacy.limited.short', defaultMessage: 'Circle' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, }); @@ -94,6 +95,7 @@ class BoostModal extends ImmutablePureComponent { 'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) }, 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) }, 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) }, + 'mutual': { icon: 'exchange', text: intl.formatMessage(messages.mutual_short) }, 'limited': { icon: 'user-circle', text: intl.formatMessage(messages.limited_short) }, 'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) }, }; diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index f1e558a20..8daf8d507 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -388,6 +388,8 @@ "privacy.direct.short": "Direct", "privacy.limited.long": "Visible for circle users only", "privacy.limited.short": "Circle", + "privacy.mutual.long": "Visible for mutual followers only (Supported servers only)", + "privacy.mutual.short": "Mutual-followers-only", "privacy.private.long": "Visible for followers only", "privacy.private.short": "Followers-only", "privacy.public.long": "Visible for all, shown in public timelines", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 9bafc79e7..1f2586866 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -388,6 +388,8 @@ "privacy.direct.short": "ダイレクト", "privacy.limited.long": "サークルで指定したユーザーのみ閲覧可", "privacy.limited.short": "サークル", + "privacy.mutual.long": "相互フォローのみ閲覧可(対応サーバのみ)", + "privacy.mutual.short": "相互フォロー限定", "privacy.private.long": "フォロワーのみ閲覧可", "privacy.private.short": "フォロワー限定", "privacy.public.long": "誰でも閲覧可、公開TLに表示", diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 4c6dbc5cb..c8ba41189 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -219,7 +219,7 @@ const insertEmoji = (state, position, emojiData, needsSpace) => { }; const privacyPreference = (a, b) => { - const order = ['public', 'unlisted', 'private', 'limited', 'direct']; + const order = ['public', 'unlisted', 'private', 'mutual', 'limited', 'direct']; return order[Math.max(order.indexOf(a), order.indexOf(b), 0)]; }; diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index cf5ab11d2..d8139fc9a 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -341,6 +341,10 @@ module AccountInteractions end end + def mutuals + followers.merge(Account.where(id: following)) + end + private def remove_potential_friendship(other_account, mutual = false) diff --git a/app/models/status.rb b/app/models/status.rb index 8bf3e64fa..842b86bf5 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -51,7 +51,7 @@ class Status < ApplicationRecord update_index('statuses#status', :proper) - enum visibility: [:public, :unlisted, :private, :mutual, :direct, :limited], _suffix: :visibility + enum visibility: [:public, :unlisted, :private, :direct, :limited, :mutual], _suffix: :visibility enum expires_action: [:delete, :hint], _prefix: :expires belongs_to :application, class_name: 'Doorkeeper::Application', optional: true diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index 50351a0e1..5b6279637 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -44,7 +44,7 @@ class ProcessMentionsService < BaseService end if circle.present? - circle.accounts.find_each do |target_account| + (circle.class.name == 'Account' ? circle.mutuals : circle.accounts).find_each do |target_account| status.mentions.create(silent: true, account: target_account) end elsif status.limited_visibility? && status.thread&.limited_visibility? diff --git a/config/locales/en.yml b/config/locales/en.yml index aaf2f3448..3a2d4bcbd 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1388,6 +1388,8 @@ en: direct_long: Only show to mentioned users limited: Circle limited_long: Only show to circle users + mutual: Mutual + mutual_long: Only show to mutual followers private: Followers-only private_long: Only show to followers public: Public diff --git a/config/locales/ja.yml b/config/locales/ja.yml index dde462557..95029ec4d 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1325,6 +1325,8 @@ ja: direct_long: 送信した相手のみ閲覧可 limited: サークル limited_long: サークルで指定したユーザーのみ閲覧可 + mutual: 相互フォロー限定 + mutual_long: 相互フォロー相手にのみ表示されます private: フォロワー限定 private_long: フォロワーにのみ表示されます public: 公開