From 20497e0c21eb4a4c2fd47ae83bdb6d41ec2264ba Mon Sep 17 00:00:00 2001 From: KMY Date: Wed, 24 May 2023 17:41:47 +0900 Subject: [PATCH] Add login visibility --- .rubocop.yml | 2 ++ app/controllers/accounts_controller.rb | 4 +++- app/controllers/activitypub/replies_controller.rb | 2 +- .../v1/statuses/reblogged_by_accounts_controller.rb | 2 +- app/helpers/application_helper.rb | 6 ++++-- app/helpers/statuses_helper.rb | 2 ++ app/javascript/mastodon/components/status.jsx | 2 ++ .../mastodon/components/status_action_bar.jsx | 7 ++++--- .../features/compose/components/compose_form.jsx | 2 +- .../compose/components/privacy_dropdown.jsx | 13 +++++++++++-- .../compose/containers/warning_container.jsx | 2 +- .../picture_in_picture/components/footer.jsx | 2 +- .../features/report/components/status_check_box.jsx | 2 ++ .../features/status/components/action_bar.jsx | 11 ++++++++--- .../features/status/components/detailed_status.jsx | 2 ++ .../mastodon/features/ui/components/boost_modal.jsx | 2 ++ app/javascript/mastodon/initial_state.js | 2 ++ app/javascript/mastodon/reducers/compose.js | 2 +- app/lib/activitypub/activity/create.rb | 4 +++- app/lib/activitypub/tag_manager.rb | 4 +++- app/lib/feed_manager.rb | 6 +++--- app/models/account_statuses_filter.rb | 3 ++- app/models/admin/status_filter.rb | 2 +- app/models/concerns/has_user_settings.rb | 4 ++++ app/models/concerns/status_threading_concern.rb | 2 +- app/models/featured_tag.rb | 6 +++--- app/models/public_feed.rb | 5 +++++ app/models/status.rb | 8 ++++---- app/models/user_settings.rb | 1 + app/policies/status_policy.rb | 6 ++++++ app/presenters/status_relationships_presenter.rb | 2 +- app/serializers/initial_state_serializer.rb | 3 ++- app/serializers/rest/status_serializer.rb | 4 ++-- app/services/concerns/account_scope.rb | 3 +-- app/services/fan_out_on_write_service.rb | 6 +++--- app/services/group_reblog_service.rb | 2 +- app/services/post_status_service.rb | 6 +++--- app/services/report_service.rb | 2 +- app/views/settings/preferences/other/show.html.haml | 3 +++ app/workers/delivery_emoji_reaction_worker.rb | 2 +- config/locales/simple_form.en.yml | 1 + config/locales/simple_form.ja.yml | 1 + 42 files changed, 106 insertions(+), 47 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index a0ea7badfa..26cf7be28b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -130,6 +130,7 @@ Metrics/ClassLength: - 'app/models/status.rb' - 'app/models/tag.rb' - 'app/models/user.rb' + - 'app/policies/status_policy.rb' - 'app/serializers/activitypub/actor_serializer.rb' - 'app/serializers/activitypub/note_serializer.rb' - 'app/serializers/rest/account_serializer.rb' @@ -155,6 +156,7 @@ Metrics/CyclomaticComplexity: Exclude: - 'app/policies/status_policy.rb' - 'app/services/activitypub/process_account_service.rb' + - 'app/services/post_status_service.rb' - lib/mastodon/*cli*.rb - db/*migrate/**/* diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index ffd4907681..83b9cf49d9 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -48,7 +48,9 @@ class AccountsController < ApplicationController end def default_statuses - @account.statuses.where(visibility: [:public, :unlisted, :public_unlisted]) + visibilities = [:public, :unlisted, :public_unlisted] + visibilities << :login unless current_account.nil? + @account.statuses.where(visibility: visibilities) end def only_media_scope diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index 5c53f58e44..f7afc9ba31 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -33,7 +33,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController def set_replies @replies = only_other_accounts? ? Status.where.not(account_id: @account.id).joins(:account).merge(Account.without_suspended) : @account.statuses - @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted, :public_unlisted]) + @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted, :public_unlisted, :login]) @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) end diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index 8464f02244..5cc71a6eab 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -26,7 +26,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController end def paginated_statuses - Status.where(reblog_of_id: @status.id).where(visibility: [:public, :unlisted, :public_unlisted]).paginate_by_max_id( + Status.where(reblog_of_id: @status.id).where(visibility: [:public, :unlisted, :public_unlisted, :login]).paginate_by_max_id( limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id] diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 300f763ff0..fcfd9c884b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -124,6 +124,8 @@ module ApplicationHelper fa_icon('unlock', title: I18n.t('statuses.visibilities.unlisted')) elsif status.public_unlisted_visibility? fa_icon('cloud', title: I18n.t('statuses.visibilities.public_unlisted')) + elsif status.login_visibility? + fa_icon('key', title: I18n.t('statuses.visibilities.login')) elsif status.private_visibility? || status.limited_visibility? fa_icon('lock', title: I18n.t('statuses.visibilities.private')) elsif status.direct_visibility? @@ -189,8 +191,8 @@ module ApplicationHelper text: [params[:title], params[:text], params[:url]].compact.join(' '), } - permit_visibilities = %w(public unlisted public_unlisted private direct) - permit_searchabilities = %w(public unlisted public_unlisted private direct) + permit_visibilities = %w(public unlisted public_unlisted login private direct) + permit_searchabilities = %w(public unlisted public_unlisted login private 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/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index ccc42e0f43..d878029d9a 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -100,6 +100,8 @@ module StatusesHelper fa_icon 'unlock fw' when 'public_unlisted' fa_icon 'cloud fw' + when 'login' + fa_icon 'key fw' when 'private' fa_icon 'lock fw' when 'direct' diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 5e084f1f6a..53808a9f9b 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -56,6 +56,7 @@ const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' }, + login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, edited: { id: 'status.edited', defaultMessage: 'Edited {date}' }, @@ -364,6 +365,7 @@ class Status extends ImmutablePureComponent { 'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) }, 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) }, 'public_unlisted': { icon: 'cloud', text: intl.formatMessage(messages.public_unlisted_short) }, + 'login': { icon: 'key', text: intl.formatMessage(messages.login_short) }, 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) }, 'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) }, }; diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index ba42a3b09a..2e2eee8ac0 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -255,8 +255,9 @@ class StatusActionBar extends ImmutablePureComponent { const { signedIn, permissions } = this.context.identity; const anonymousAccess = !signedIn; - const publicStatus = ['public', 'unlisted', 'public_unlisted'].includes(status.get('visibility_ex')); - const pinnableStatus = ['public', 'unlisted', 'public_unlisted', 'private'].includes(status.get('visibility_ex')); + const publicStatus = ['public', 'unlisted', 'public_unlisted', 'login'].includes(status.get('visibility_ex')); + const anonymousStatus = ['public', 'unlisted', 'public_unlisted'].includes(status.get('visibility_ex')); + const pinnableStatus = ['public', 'unlisted', 'public_unlisted', 'login', 'private'].includes(status.get('visibility_ex')); const mutingConversation = status.get('muted'); const account = status.get('account'); const writtenByMe = status.getIn(['account', 'id']) === me; @@ -272,7 +273,7 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); - if (publicStatus) { + if (anonymousStatus) { menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); } diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx index 0bdab7e028..1ce1e1f7f3 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.jsx +++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx @@ -239,7 +239,7 @@ class ComposeForm extends ImmutablePureComponent { } else if (this.props.privacy === 'private' || this.props.privacy === 'direct') { publishText = {intl.formatMessage(messages.publish)}; } else { - publishText = (this.props.privacy !== 'unlisted' && this.props.privacy !== 'public_unlisted') ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish); + publishText = (this.props.privacy !== 'unlisted' && this.props.privacy !== 'public_unlisted' && this.props.privacy !== 'login') ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish); } return ( diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx b/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx index 82c738d9dd..93762b2edc 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx @@ -6,6 +6,7 @@ import Overlay from 'react-overlays/Overlay'; import { supportsPassiveEvents } from 'detect-passive-events'; import classNames from 'classnames'; import { Icon } from 'mastodon/components/icon'; +import { enableLoginPrivacy } from 'mastodon/initial_state'; const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, @@ -14,6 +15,8 @@ const messages = defineMessages({ unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but opted-out of discovery features' }, public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' }, public_unlisted_long: { id: 'privacy.public_unlisted.long', defaultMessage: 'Visible for all without GTL' }, + login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' }, + login_long: { id: 'privacy.login.long', defaultMessage: 'Login user only' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' }, private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, @@ -220,9 +223,15 @@ class PrivacyDropdown extends React.PureComponent { this.options = [ { icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) }, { icon: 'cloud', value: 'public_unlisted', text: formatMessage(messages.public_unlisted_short), meta: formatMessage(messages.public_unlisted_long) }, + { icon: 'key', value: 'login', text: formatMessage(messages.login_short), meta: formatMessage(messages.login_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) }, ]; + this.selectableOptions = [...this.options]; + + if (!enableLoginPrivacy) { + this.selectableOptions = this.selectableOptions.filter((opt) => opt.value !== 'login'); + } if (!this.props.noDirect) { this.options.push( @@ -247,7 +256,7 @@ class PrivacyDropdown extends React.PureComponent { const { value, container, disabled, intl } = this.props; const { open, placement } = this.state; - const valueOption = this.options.find(item => item.value === value); + const valueOption = this.options.find(item => item.value === value) || this.options[0]; return (
@@ -273,7 +282,7 @@ class PrivacyDropdown extends React.PureComponent {
({ needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']), - hashtagWarning: ['public', 'public_unlisted'].indexOf(state.getIn(['compose', 'privacy'])) < 0 && HASHTAG_PATTERN_REGEX.test(state.getIn(['compose', 'text'])), + hashtagWarning: ['public', 'public_unlisted', 'login'].indexOf(state.getIn(['compose', 'privacy'])) < 0 && HASHTAG_PATTERN_REGEX.test(state.getIn(['compose', 'text'])), directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct', searchabilityWarning: state.getIn(['compose', 'searchability']) === 'limited', }); diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx index e6a5592d65..72aedb6349 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx @@ -152,7 +152,7 @@ class Footer extends ImmutablePureComponent { render () { const { status, intl, withOpenButton } = this.props; - const publicStatus = ['public', 'unlisted', 'public_unlisted'].includes(status.get('visibility_ex')); + const publicStatus = ['public', 'unlisted', 'public_unlisted', 'login'].includes(status.get('visibility_ex')); const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility_ex') === 'private'; let replyIcon, replyTitle; diff --git a/app/javascript/mastodon/features/report/components/status_check_box.jsx b/app/javascript/mastodon/features/report/components/status_check_box.jsx index 6ecb470c40..9ef87bf9c8 100644 --- a/app/javascript/mastodon/features/report/components/status_check_box.jsx +++ b/app/javascript/mastodon/features/report/components/status_check_box.jsx @@ -14,6 +14,7 @@ const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' }, + login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, }); @@ -44,6 +45,7 @@ class StatusCheckBox extends React.PureComponent { 'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) }, 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) }, 'public_unlisted': { icon: 'cloud', text: intl.formatMessage(messages.public_unlisted_short) }, + 'login': { icon: 'key', text: intl.formatMessage(messages.login_short) }, 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) }, 'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) }, }; diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index ae963fd5bd..e6108592df 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -196,8 +196,9 @@ class ActionBar extends React.PureComponent { const { status, relationship, intl } = this.props; const { signedIn, permissions } = this.context.identity; - const publicStatus = ['public', 'unlisted', 'public_unlisted'].includes(status.get('visibility_ex')); - const pinnableStatus = ['public', 'unlisted', 'public_unlisted', 'private'].includes(status.get('visibility_ex')); + const publicStatus = ['public', 'unlisted', 'public_unlisted', 'login'].includes(status.get('visibility_ex')); + const anonymousStatus = ['public', 'unlisted', 'public_unlisted'].includes(status.get('visibility_ex')); + const pinnableStatus = ['public', 'unlisted', 'public_unlisted', 'login', 'private'].includes(status.get('visibility_ex')); const mutingConversation = status.get('muted'); const account = status.get('account'); const writtenByMe = status.getIn(['account', 'id']) === me; @@ -211,7 +212,11 @@ class ActionBar extends React.PureComponent { } menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); - menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); + + if (anonymousStatus) { + menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); + } + menu.push(null); menu.push({ text: intl.formatMessage(messages.reblog), action: this.handleReblogForceModalClick }); menu.push(null); diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx index be5eab43b9..33177e172c 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -23,6 +23,7 @@ const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' }, + login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, searchability_public_short: { id: 'searchability.public.short', defaultMessage: 'Public' }, @@ -214,6 +215,7 @@ class DetailedStatus extends ImmutablePureComponent { 'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) }, 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) }, 'public_unlisted': { icon: 'cloud', text: intl.formatMessage(messages.public_unlisted_short) }, + 'login': { icon: 'key', text: intl.formatMessage(messages.login_short) }, 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) }, 'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) }, }; diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.jsx b/app/javascript/mastodon/features/ui/components/boost_modal.jsx index 2c1e4d401b..ad39653994 100644 --- a/app/javascript/mastodon/features/ui/components/boost_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/boost_modal.jsx @@ -21,6 +21,7 @@ const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' }, + login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, }); @@ -87,6 +88,7 @@ class BoostModal extends ImmutablePureComponent { 'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) }, 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) }, 'public_unlisted': { icon: 'cloud', text: intl.formatMessage(messages.public_unlisted_short) }, + 'login': { icon: 'key', text: intl.formatMessage(messages.login_short) }, 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) }, 'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) }, }; diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 524a4229c8..991af7eb24 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -58,6 +58,7 @@ * @property {string} display_media * @property {boolean} display_media_expand * @property {string} domain + * @property {boolean} enable_login_privacy * @property {boolean=} expand_spoilers * @property {boolean} limited_federation_mode * @property {string} locale @@ -111,6 +112,7 @@ export const disabledAccountId = getMeta('disabled_account_id'); export const displayMedia = getMeta('display_media'); export const displayMediaExpand = getMeta('display_media_expand'); export const domain = getMeta('domain'); +export const enableLoginPrivacy = getMeta('enable_login_privacy'); export const expandSpoilers = getMeta('expand_spoilers'); export const forceSingleColumn = !getMeta('advanced_layout'); export const limitedFederationMode = getMeta('limited_federation_mode'); diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index c24b899a9a..994dfa4cf0 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -238,7 +238,7 @@ const insertExpiration = (state, position, data) => { }; const privacyPreference = (a, b) => { - const order = ['public', 'public_unlisted', 'unlisted', 'private', 'direct']; + const order = ['public', 'public_unlisted', 'unlisted', 'login', 'private', 'direct']; return order[Math.max(order.indexOf(a), order.indexOf(b), 0)]; }; diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 66fb26c03e..414ada0fa0 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -514,7 +514,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity if searchability == visibility || searchability == :limited || searchability == :direct searchability - elsif [:public, :unlisted, :private].include?(searchability) && [:public, :public_unlisted, :unlisted, :private].include?(visibility) + elsif [:public, :unlisted, :private].include?(searchability) && [:public, :public_unlisted, :unlisted, :login, :private].include?(visibility) :private else :direct @@ -526,6 +526,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity :public elsif audience_cc.any? { |cc| ActivityPub::TagManager.instance.public_collection?(cc) } :unlisted + elsif audience_cc.include?('as:LoginOnly') + :login elsif audience_to.include?(@account.followers_url) :private else diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 1bbc10985a..ea319bf6ae 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -86,7 +86,7 @@ class ActivityPub::TagManager case status.visibility when 'public' [COLLECTIONS[:public]] - when 'unlisted', 'public_unlisted', 'private' + when 'unlisted', 'public_unlisted', 'login', 'private' [account_followers_url(status.account)] when 'direct', 'limited' if status.account.silenced? @@ -124,6 +124,8 @@ class ActivityPub::TagManager cc << account_followers_url(status.account) when 'unlisted', 'public_unlisted' cc << COLLECTIONS[:public] + when 'login' + cc << 'as:LoginOnly' end cc + cc_private_visibility(status) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 3b64f62631..521a048cd3 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -109,7 +109,7 @@ class FeedManager def merge_into_home(from_account, into_account) timeline_key = key(:home, into_account.id) aggregate = into_account.user&.aggregates_reblogs? - query = from_account.statuses.where(visibility: [:public, :unlisted, :public_unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4) + query = from_account.statuses.where(visibility: [:public, :unlisted, :public_unlisted, :login, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4) if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4 oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i @@ -135,7 +135,7 @@ class FeedManager def merge_into_list(from_account, list) timeline_key = key(:list, list.id) aggregate = list.account.user&.aggregates_reblogs? - query = from_account.statuses.where(visibility: [:public, :unlisted, :public_unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4) + query = from_account.statuses.where(visibility: [:public, :unlisted, :public_unlisted, :login, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4) if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4 oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i @@ -253,7 +253,7 @@ class FeedManager next if last_status_score < oldest_home_score end - statuses = target_account.statuses.where(visibility: [:public, :unlisted, :public_unlisted, :private]).includes(:preloadable_poll, :media_attachments, :account, reblog: :account).limit(limit) + statuses = target_account.statuses.where(visibility: [:public, :unlisted, :public_unlisted, :login, :private]).includes(:preloadable_poll, :media_attachments, :account, reblog: :account).limit(limit) crutches = build_crutches(account.id, statuses) statuses.each do |status| diff --git a/app/models/account_statuses_filter.rb b/app/models/account_statuses_filter.rb index d76e62ae72..03dd4a34f0 100644 --- a/app/models/account_statuses_filter.rb +++ b/app/models/account_statuses_filter.rb @@ -30,6 +30,7 @@ class AccountStatusesFilter scope.merge!(scope.where.not(visibility: :public_unlisted)) if domain_block&.reject_send_public_unlisted || (domain_block&.detect_invalid_subscription && @account.user&.setting_reject_public_unlisted_subscription) scope.merge!(scope.where.not(visibility: :unlisted)) if domain_block&.detect_invalid_subscription && @account.user&.setting_reject_unlisted_subscription scope.merge!(scope.where(spoiler_text: ['', nil])) if domain_block&.reject_send_sensitive + scope.merge!(scope.where.not(visibility: :login)) if current_account.nil? scope end @@ -51,7 +52,7 @@ class AccountStatusesFilter def filtered_scope scope = account.statuses.left_outer_joins(:mentions) - scope.merge!(scope.where(visibility: follower? ? %i(public unlisted public_unlisted private) : %i(public unlisted public_unlisted)).or(scope.where(mentions: { account_id: current_account.id })).group(Status.arel_table[:id])) + scope.merge!(scope.where(visibility: follower? ? %i(public unlisted public_unlisted login private) : %i(public unlisted public_unlisted login)).or(scope.where(mentions: { account_id: current_account.id })).group(Status.arel_table[:id])) scope.merge!(filtered_reblogs_scope) if reblogs_may_occur? scope diff --git a/app/models/admin/status_filter.rb b/app/models/admin/status_filter.rb index 7d19e293b9..1c76ba997b 100644 --- a/app/models/admin/status_filter.rb +++ b/app/models/admin/status_filter.rb @@ -16,7 +16,7 @@ class Admin::StatusFilter end def results - scope = @account.statuses.where(visibility: [:public, :unlisted, :public_unlisted]) + scope = @account.statuses.where(visibility: [:public, :unlisted, :public_unlisted, :login]) params.each do |key, value| next if IGNORED_PARAMS.include?(key.to_s) diff --git a/app/models/concerns/has_user_settings.rb b/app/models/concerns/has_user_settings.rb index 19ab0bf073..0c56e724b9 100644 --- a/app/models/concerns/has_user_settings.rb +++ b/app/models/concerns/has_user_settings.rb @@ -23,6 +23,10 @@ module HasUserSettings settings['web.auto_play'] end + def setting_enable_login_privacy + settings['web.enable_login_privacy'] + end + def setting_default_sensitive settings['default_sensitive'] end diff --git a/app/models/concerns/status_threading_concern.rb b/app/models/concerns/status_threading_concern.rb index 1665c2ba5f..38d0f393b7 100644 --- a/app/models/concerns/status_threading_concern.rb +++ b/app/models/concerns/status_threading_concern.rb @@ -12,7 +12,7 @@ module StatusThreadingConcern end def self_replies(limit) - account.statuses.where(in_reply_to_id: id, visibility: [:public, :unlisted, :public_unlisted]).reorder(id: :asc).limit(limit) + account.statuses.where(in_reply_to_id: id, visibility: [:public, :unlisted, :public_unlisted, :login]).reorder(id: :asc).limit(limit) end private diff --git a/app/models/featured_tag.rb b/app/models/featured_tag.rb index 1bc47af7f2..6aa5834aca 100644 --- a/app/models/featured_tag.rb +++ b/app/models/featured_tag.rb @@ -45,7 +45,7 @@ class FeaturedTag < ApplicationRecord end def decrement(deleted_status_id) - update(statuses_count: [0, statuses_count - 1].max, last_status_at: account.statuses.where(visibility: %i(public unlisted public_unlisted)).tagged_with(tag).where.not(id: deleted_status_id).select(:created_at).first&.created_at) + update(statuses_count: [0, statuses_count - 1].max, last_status_at: account.statuses.where(visibility: %i(public unlisted public_unlisted login)).tagged_with(tag).where.not(id: deleted_status_id).select(:created_at).first&.created_at) end private @@ -59,8 +59,8 @@ class FeaturedTag < ApplicationRecord end def reset_data - self.statuses_count = account.statuses.where(visibility: %i(public unlisted public_unlisted)).tagged_with(tag).count - self.last_status_at = account.statuses.where(visibility: %i(public unlisted public_unlisted)).tagged_with(tag).select(:created_at).first&.created_at + self.statuses_count = account.statuses.where(visibility: %i(public unlisted public_unlisted login)).tagged_with(tag).count + self.last_status_at = account.statuses.where(visibility: %i(public unlisted public_unlisted login)).tagged_with(tag).select(:created_at).first&.created_at end def validate_featured_tags_limit diff --git a/app/models/public_feed.rb b/app/models/public_feed.rb index 51a9058a84..f4d27e53d3 100644 --- a/app/models/public_feed.rb +++ b/app/models/public_feed.rb @@ -29,6 +29,7 @@ class PublicFeed scope.merge!(account_filters_scope) if account? scope.merge!(media_only_scope) if media_only? scope.merge!(language_scope) if account&.chosen_languages.present? + scope.merge!(anonymous_scope) unless account? scope.cache_ids.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id) end @@ -97,6 +98,10 @@ class PublicFeed Status.where(language: account.chosen_languages) end + def anonymous_scope + Status.where.not(visibility: :login) + end + def account_filters_scope Status.not_excluded_by_account(account).tap do |scope| scope.merge!(Status.not_domain_blocked_by_account(account)) unless local_only? diff --git a/app/models/status.rb b/app/models/status.rb index 81573fa5a1..b6f5ac1152 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -52,7 +52,7 @@ class Status < ApplicationRecord update_index('statuses', :proper) - enum visibility: { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4, public_unlisted: 10 }, _suffix: :visibility + enum visibility: { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4, public_unlisted: 10, login: 11 }, _suffix: :visibility enum searchability: { public: 0, private: 1, direct: 2, limited: 3, public_unlisted: 10 }, _suffix: :searchability belongs_to :application, class_name: 'Doorkeeper::Application', optional: true @@ -102,8 +102,8 @@ class Status < ApplicationRecord scope :with_accounts, ->(ids) { where(id: ids).includes(:account) } scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') } scope :without_reblogs, -> { where(statuses: { reblog_of_id: nil }) } - scope :with_public_visibility, -> { where(visibility: [:public, :public_unlisted]) } - scope :with_global_timeline_visibility, -> { where(visibility: [:public]) } + scope :with_public_visibility, -> { where(visibility: [:public, :public_unlisted, :login]) } + scope :with_global_timeline_visibility, -> { where(visibility: [:public, :login]) } scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) } scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced_at: nil }) } scope :including_silenced_accounts, -> { left_outer_joins(:account).where.not(accounts: { silenced_at: nil }) } @@ -526,7 +526,7 @@ class Status < ApplicationRecord def set_searchability return if searchability.nil? - self.searchability = [Status.searchabilities[searchability], Status.visibilities[visibility == 'public_unlisted' ? 'public' : visibility]].max + self.searchability = [Status.searchabilities[searchability], Status.visibilities[visibility == 'public_unlisted' || visibility == 'login' ? 'public' : visibility]].max end def set_conversation diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index 907c18b7b1..2634fc48c2 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -37,6 +37,7 @@ class UserSettings setting :use_system_font, default: false setting :disable_swiping, default: false setting :delete_modal, default: true + setting :enable_login_privacy, default: false setting :reblog_modal, default: false setting :unfollow_modal, default: true setting :reduce_motion, default: false diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index 94bc333ede..335abe9e92 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -15,6 +15,8 @@ class StatusPolicy < ApplicationPolicy if requires_mention? owned? || mention_exists? + elsif login? + owned? || !current_account.nil? elsif private? owned? || following_author? || mention_exists? else @@ -58,6 +60,10 @@ class StatusPolicy < ApplicationPolicy record.private_visibility? end + def login? + record.login_visibility? + end + def public? record.public_visibility? || record.public_unlisted_visibility? end diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb index 24e00adcb9..e5ec67f8c0 100644 --- a/app/presenters/status_relationships_presenter.rb +++ b/app/presenters/status_relationships_presenter.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class StatusRelationshipsPresenter - PINNABLE_VISIBILITIES = %w(public public_unlisted unlisted private).freeze + PINNABLE_VISIBILITIES = %w(public public_unlisted unlisted login private).freeze attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map, :bookmarks_map, :filters_map, :emoji_reactions_map diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 209b6d6af5..c2206eef05 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -42,7 +42,8 @@ class InitialStateSerializer < ActiveModel::Serializer store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif store[:display_media] = object.current_account.user.setting_display_media store[:display_media_expand] = object.current_account.user.setting_display_media_expand - store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers + store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers + store[:enable_login_privacy] = object.current_account.user.setting_enable_login_privacy 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 diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index c065c817a6..fe9334205e 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -57,7 +57,7 @@ class REST::StatusSerializer < ActiveModel::Serializer # UX differences if object.limited_visibility? 'private' - elsif object.public_unlisted_visibility? + elsif object.public_unlisted_visibility? || object.login_visibility? 'public' else object.visibility @@ -159,7 +159,7 @@ class REST::StatusSerializer < ActiveModel::Serializer current_user? && current_user.account_id == object.account_id && !object.reblog? && - %w(public unlisted public_unlisted private).include?(object.visibility) + %w(public unlisted public_unlisted login private).include?(object.visibility) end def reactions? diff --git a/app/services/concerns/account_scope.rb b/app/services/concerns/account_scope.rb index 43bef653bd..00bb9a3960 100644 --- a/app/services/concerns/account_scope.rb +++ b/app/services/concerns/account_scope.rb @@ -3,8 +3,7 @@ module AccountScope def scope_status(status) case status.visibility.to_sym - when :public, :unlisted, :public_unlisted - # scope_local.merge(scope_list_following_account(status.account)) + when :public, :unlisted, :public_unlisted, :login scope_local when :private scope_account_local_followers(status.account) diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index ac9f91990f..b8d37952e3 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -46,10 +46,10 @@ class FanOutOnWriteService < BaseService notify_about_update! if update? case @status.visibility.to_sym - when :public, :unlisted, :public_unlisted, :private + when :public, :unlisted, :public_unlisted, :login, :private deliver_to_all_followers! deliver_to_lists! - deliver_to_antennas! if [:public, :public_unlisted].include?(@status.visibility.to_sym) && !@account.dissubscribable + deliver_to_antennas! if [:public, :public_unlisted, :login].include?(@status.visibility.to_sym) && !@account.dissubscribable deliver_to_stl_antennas! when :limited deliver_to_mentioned_followers! @@ -121,7 +121,7 @@ class FanOutOnWriteService < BaseService antennas = Antenna.available_stls antennas = antennas.where(account_id: Account.without_suspended.joins(:user).select('accounts.id').where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)) - antennas = antennas.where(account: @account.followers).or(antennas.where(account: @account)).where.not(list_id: 0) if !@account.domain.nil? || [:public, :public_unlisted].exclude?(@status.visibility.to_sym) + antennas = antennas.where(account: @account.followers).or(antennas.where(account: @account)).where.not(list_id: 0) if !@account.domain.nil? || [:public, :public_unlisted, :login].exclude?(@status.visibility.to_sym) collection = AntennaCollection.new(@status, @options[:update]) diff --git a/app/services/group_reblog_service.rb b/app/services/group_reblog_service.rb index a733429b2d..4c30b0677b 100644 --- a/app/services/group_reblog_service.rb +++ b/app/services/group_reblog_service.rb @@ -9,7 +9,7 @@ class GroupReblogService < BaseService return nil if status.account.group? visibility = status.visibility.to_sym - return nil unless %i(public public_unlisted unlisted private direct).include?(visibility) + return nil unless %i(public public_unlisted unlisted login private direct).include?(visibility) accounts = status.mentions.map(&:account) | status.active_mentions.map(&:account) transcription = %i(private direct).include?(visibility) diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 43a387d1cc..9859787d90 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -72,7 +72,7 @@ class PostStatusService < BaseService end) || @options[:spoiler_text].present? @text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present? @visibility = @options[:visibility] || @account.user&.setting_default_privacy - @visibility = :unlisted if (@visibility&.to_sym == :public || @visibility&.to_sym == :public_unlisted) && @account.silenced? + @visibility = :unlisted if (@visibility&.to_sym == :public || @visibility&.to_sym == :public_unlisted || @visibility&.to_sym == :login) && @account.silenced? @visibility = :public_unlisted if @visibility&.to_sym == :public && !@options[:force_visibility] && !@options[:application]&.superapp && @account.user&.setting_public_post_to_unlisted @searchability = searchability @markdown = @options[:markdown] || false @@ -85,9 +85,9 @@ class PostStatusService < BaseService def searchability case @options[:searchability]&.to_sym when :public - case @visibility&.to_sym when :public, :public_unlisted then :public when :unlisted, :private then :private else :direct end + case @visibility&.to_sym when :public, :public_unlisted, :login then :public when :unlisted, :private then :private else :direct end when :private - case @visibility&.to_sym when :public, :public_unlisted, :unlisted, :private then :private else :direct end + case @visibility&.to_sym when :public, :public_unlisted, :login, :unlisted, :private then :private else :direct end when :direct :direct when nil diff --git a/app/services/report_service.rb b/app/services/report_service.rb index b673c5a029..30272685a3 100644 --- a/app/services/report_service.rb +++ b/app/services/report_service.rb @@ -62,7 +62,7 @@ class ReportService < BaseService # If the account making reports is remote, it is likely anonymized so we have to relax the requirements for attaching statuses. domain = @source_account.domain.to_s.downcase has_followers = @target_account.followers.where(Account.arel_table[:domain].lower.eq(domain)).exists? - visibility = has_followers ? %i(public unlisted public_unlisted private) : %i(public unlisted public_unlisted) + visibility = has_followers ? %i(public unlisted public_unlisted login private) : %i(public unlisted public_unlisted) scope = @target_account.statuses.with_discarded scope.merge!(scope.where(visibility: visibility).or(scope.where('EXISTS (SELECT 1 FROM mentions m JOIN accounts a ON m.account_id = a.id WHERE lower(a.domain) = ?)', domain))) # Allow missing posts to not drop reports that include e.g. a deleted post diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index 69e1d8f430..38bf483f95 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -33,6 +33,9 @@ .fields-group.fields-row__column.fields-row__column-6 = ff.input :default_language, collection: [nil] + filterable_languages, wrapper: :with_label, label_method: lambda { |locale| locale.nil? ? I18n.t('statuses.default_language') : native_locale_name(locale) }, required: false, include_blank: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_language') + .fields-group + = ff.input :'web.enable_login_privacy', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_enable_login_privacy'), hint: false + .fields-group = ff.input :public_post_to_unlisted, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_public_post_to_unlisted'), hint: I18n.t('simple_form.hints.defaults.setting_public_post_to_unlisted') diff --git a/app/workers/delivery_emoji_reaction_worker.rb b/app/workers/delivery_emoji_reaction_worker.rb index 7115c1e9d0..5ec9dd4fba 100644 --- a/app/workers/delivery_emoji_reaction_worker.rb +++ b/app/workers/delivery_emoji_reaction_worker.rb @@ -16,7 +16,7 @@ class DeliveryEmojiReactionWorker redis.publish("timeline:#{account.id}", payload_json) if !account.user&.setting_stop_emoji_reaction_streaming && redis.exists?("subscribed:timeline:#{account.id}") end - if [:public, :unlisted, :public_unlisted].exclude?(status.visibility.to_sym) && status.account_id != my_account_id && + if [:public, :unlisted, :public_unlisted, :login].exclude?(status.visibility.to_sym) && status.account_id != my_account_id && redis.exists?("subscribed:timeline:#{status.account_id}") && !status.account.user&.setting_stop_emoji_reaction_streaming redis.publish("timeline:#{status.account_id}", payload_json) end diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 7613ff85b0..bfae64a85c 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -220,6 +220,7 @@ en: setting_display_media_hide_all: Hide all setting_display_media_show_all: Show all setting_emoji_reaction_streaming_notify_impl2: Enable stamp notification compat with Nyastodon, Catstodon, glitch-soc + setting_enable_login_privacy: Enable login visibility setting_expand_spoilers: Always expand posts marked with content warnings setting_hide_followers_count: Hide followers count setting_hide_following_count: Hide following count diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index c7207d18d4..817ca05d6f 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -227,6 +227,7 @@ ja: setting_display_media_expand: 5個目以降のメディアも表示する (最大16) setting_display_media_hide_all: 非表示 setting_display_media_show_all: 表示 + setting_enable_login_privacy: 公開範囲「ログインユーザーのみ」をWeb UIで選択可能にする setting_emoji_reaction_streaming_notify_impl2: Nyastodon, Catstodon, glitch-soc互換のスタンプ機能を有効にする setting_expand_spoilers: 閲覧注意としてマークされた投稿を常に展開する setting_hide_followers_count: フォロワー数を隠す