diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 56229fd05c..6ee784b921 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -47,7 +47,7 @@ class AccountsController < ApplicationController
   end
 
   def default_statuses
-    @account.statuses.where(visibility: [:public, :unlisted])
+    @account.statuses.where(visibility: [:public, :unlisted, :public_unlisted])
   end
 
   def only_media_scope
diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb
index 8e0f9de2ee..e65dbb7d38 100644
--- a/app/controllers/activitypub/replies_controller.rb
+++ b/app/controllers/activitypub/replies_controller.rb
@@ -32,7 +32,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])
+    @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted, :public_unlisted])
     @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 4b545f9826..988304a84d 100644
--- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
@@ -25,7 +25,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
   end
 
   def paginated_statuses
-    Status.where(reblog_of_id: @status.id).where(visibility: [:public, :unlisted]).paginate_by_max_id(
+    Status.where(reblog_of_id: @status.id).where(visibility: [:public, :unlisted, :public_unlisted]).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 08020a65a2..7a2b9b9ed0 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -122,6 +122,8 @@ module ApplicationHelper
       fa_icon('globe', title: I18n.t('statuses.visibilities.public'))
     elsif status.unlisted_visibility?
       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.private_visibility? || status.limited_visibility?
       fa_icon('lock', title: I18n.t('statuses.visibilities.private'))
     elsif status.direct_visibility?
@@ -199,7 +201,7 @@ module ApplicationHelper
       text: [params[:title], params[:text], params[:url]].compact.join(' '),
     }
 
-    permit_visibilities = %w(public unlisted private direct)
+    permit_visibilities = %w(public unlisted public_unlisted 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 d1e3fddafe..2b009da391 100644
--- a/app/helpers/statuses_helper.rb
+++ b/app/helpers/statuses_helper.rb
@@ -98,6 +98,8 @@ module StatusesHelper
       fa_icon 'globe fw'
     when 'unlisted'
       fa_icon 'unlock fw'
+    when 'public_unlisted'
+      fa_icon 'cloud 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 de1e9390c9..f1bea03f1a 100644
--- a/app/javascript/mastodon/components/status.jsx
+++ b/app/javascript/mastodon/components/status.jsx
@@ -55,6 +55,7 @@ export const defaultMediaVisibility = (status) => {
 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' },
   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}' },
@@ -503,6 +504,7 @@ class Status extends ImmutablePureComponent {
     const visibilityIconInfo = {
       '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) },
       '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 282892809c..03b27e381d 100644
--- a/app/javascript/mastodon/components/status_action_bar.jsx
+++ b/app/javascript/mastodon/components/status_action_bar.jsx
@@ -248,8 +248,8 @@ class StatusActionBar extends ImmutablePureComponent {
     const { signedIn, permissions } = this.context.identity;
 
     const anonymousAccess    = !signedIn;
-    const publicStatus       = ['public', 'unlisted'].includes(status.get('visibility'));
-    const pinnableStatus     = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
+    const publicStatus       = ['public', 'unlisted', 'public_unlisted'].includes(status.get('visibility'));
+    const pinnableStatus     = ['public', 'unlisted', 'public_unlisted', 'private'].includes(status.get('visibility'));
     const mutingConversation = status.get('muted');
     const account            = status.get('account');
     const writtenByMe        = status.getIn(['account', 'id']) === me;
diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx
index ece54621f5..49a4277e07 100644
--- a/app/javascript/mastodon/components/status_content.jsx
+++ b/app/javascript/mastodon/components/status_content.jsx
@@ -220,7 +220,7 @@ class StatusContent extends React.PureComponent {
 
     const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
     const renderReadMore = this.props.onClick && status.get('collapsed');
-    const renderTranslate = translationEnabled && this.context.identity.signedIn && this.props.onTranslate && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && status.get('language') !== null && intl.locale !== status.get('language');
+    const renderTranslate = translationEnabled && this.context.identity.signedIn && this.props.onTranslate && ['public', 'unlisted', 'public_unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && status.get('language') !== null && intl.locale !== status.get('language');
 
     const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') };
     const spoilerContent = { __html: status.get('spoilerHtml') };
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx
index e641d59f4d..d5da8141c6 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.jsx
+++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx
@@ -217,7 +217,7 @@ class ComposeForm extends ImmutablePureComponent {
     } else if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
       publishText = <span className='compose-form__publish-private'><Icon id='lock' /> {intl.formatMessage(messages.publish)}</span>;
     } else {
-      publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
+      publishText = (this.props.privacy !== 'unlisted' && this.props.privacy !== 'public_unlisted') ? 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 ffd1094cd3..51ed372dc3 100644
--- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx
+++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx
@@ -12,6 +12,8 @@ const messages = defineMessages({
   public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all' },
   unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
   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, but dont visible in GTL' },
   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' },
@@ -218,6 +220,7 @@ 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: '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) },
     ];
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 0ee6d06c7f..10e18e504f 100644
--- a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx
+++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx
@@ -154,7 +154,7 @@ class Footer extends ImmutablePureComponent {
   render () {
     const { status, intl, withOpenButton } = this.props;
 
-    const publicStatus  = ['public', 'unlisted'].includes(status.get('visibility'));
+    const publicStatus  = ['public', 'unlisted', 'public_unlisted'].includes(status.get('visibility'));
     const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === '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 5366da90be..ba44d4a0ed 100644
--- a/app/javascript/mastodon/features/report/components/status_check_box.jsx
+++ b/app/javascript/mastodon/features/report/components/status_check_box.jsx
@@ -13,6 +13,7 @@ import Icon from 'mastodon/components/icon';
 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' },
   private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
   direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
 });
@@ -43,6 +44,7 @@ class StatusCheckBox extends React.PureComponent {
     const visibilityIconInfo = {
       '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) },
       '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 dc87e9751f..bc91f8e032 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.jsx
+++ b/app/javascript/mastodon/features/status/components/action_bar.jsx
@@ -190,8 +190,8 @@ class ActionBar extends React.PureComponent {
     const { status, relationship, intl } = this.props;
     const { signedIn, permissions } = this.context.identity;
 
-    const publicStatus       = ['public', 'unlisted'].includes(status.get('visibility'));
-    const pinnableStatus     = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
+    const publicStatus       = ['public', 'unlisted', 'public_unlisted'].includes(status.get('visibility'));
+    const pinnableStatus     = ['public', 'unlisted', 'public_unlisted', 'private'].includes(status.get('visibility'));
     const mutingConversation = status.get('muted');
     const account            = status.get('account');
     const writtenByMe        = status.getIn(['account', 'id']) === me;
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx
index e864ced944..2e5e0d45d2 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.jsx
+++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx
@@ -22,6 +22,7 @@ import EditedTimestamp from 'mastodon/components/edited_timestamp';
 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' },
   private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
   direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
 });
@@ -206,6 +207,7 @@ class DetailedStatus extends ImmutablePureComponent {
     const visibilityIconInfo = {
       '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) },
       '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 d6a6cea31e..f9b36907c6 100644
--- a/app/javascript/mastodon/features/ui/components/boost_modal.jsx
+++ b/app/javascript/mastodon/features/ui/components/boost_modal.jsx
@@ -20,6 +20,7 @@ const messages = defineMessages({
   reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
   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' },
   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 {
     const visibilityIconInfo = {
       '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) },
       'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
       'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) },
     };
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 842b7af518..9cd81a7bf0 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -218,7 +218,7 @@ const insertEmoji = (state, position, emojiData, needsSpace) => {
 };
 
 const privacyPreference = (a, b) => {
-  const order = ['public', 'unlisted', 'private', 'direct'];
+  const order = ['public', 'public_unlisted', 'unlisted', 'private', 'direct'];
   return order[Math.max(order.indexOf(a), order.indexOf(b), 0)];
 };
 
diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb
index 87ea178887..4358172ae4 100644
--- a/app/lib/activitypub/tag_manager.rb
+++ b/app/lib/activitypub/tag_manager.rb
@@ -84,7 +84,7 @@ class ActivityPub::TagManager
     case status.visibility
     when 'public'
       [COLLECTIONS[:public]]
-    when 'unlisted', 'private'
+    when 'unlisted', 'public_unlisted', 'private'
       [account_followers_url(status.account)]
     when 'direct', 'limited'
       if status.account.silenced?
@@ -120,7 +120,7 @@ class ActivityPub::TagManager
     case status.visibility
     when 'public'
       cc << account_followers_url(status.account)
-    when 'unlisted'
+    when 'unlisted', 'public_unlisted'
       cc << COLLECTIONS[:public]
     end
 
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 8d7540e0f7..b8ed4dd04c 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, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
+    query        = from_account.statuses.where(visibility: [:public, :unlisted, :public_unlisted, :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, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
+    query        = from_account.statuses.where(visibility: [:public, :unlisted, :public_unlisted, :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, :private]).includes(:preloadable_poll, :media_attachments, :account, reblog: :account).limit(limit)
+      statuses = target_account.statuses.where(visibility: [:public, :unlisted, :public_unlisted, :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/lib/status_reach_finder.rb b/app/lib/status_reach_finder.rb
index 36fb0e80fb..cbcc0e6b61 100644
--- a/app/lib/status_reach_finder.rb
+++ b/app/lib/status_reach_finder.rb
@@ -87,7 +87,7 @@ class StatusReachFinder
   end
 
   def distributable?
-    @status.public_visibility? || @status.unlisted_visibility?
+    @status.public_visibility? || @status.unlisted_visibility? || @status.public_unlisted_visibility?
   end
 
   def unsafe?
diff --git a/app/models/account_statuses_filter.rb b/app/models/account_statuses_filter.rb
index 211f414787..068ab329e9 100644
--- a/app/models/account_statuses_filter.rb
+++ b/app/models/account_statuses_filter.rb
@@ -35,7 +35,7 @@ class AccountStatusesFilter
     if suspended?
       Status.none
     elsif anonymous?
-      account.statuses.where(visibility: %i(public unlisted))
+      account.statuses.where(visibility: %i(public unlisted public_unlisted))
     elsif author?
       account.statuses.all # NOTE: #merge! does not work without the #all
     elsif blocked?
@@ -48,7 +48,7 @@ class AccountStatusesFilter
   def filtered_scope
     scope = account.statuses.left_outer_joins(:mentions)
 
-    scope.merge!(scope.where(visibility: follower? ? %i(public unlisted private) : %i(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 private) : %i(public unlisted public_unlisted)).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 4d439e9a1c..8f8b538137 100644
--- a/app/models/admin/status_filter.rb
+++ b/app/models/admin/status_filter.rb
@@ -14,7 +14,7 @@ class Admin::StatusFilter
   end
 
   def results
-    scope = @account.statuses.where(visibility: [:public, :unlisted])
+    scope = @account.statuses.where(visibility: [:public, :unlisted, :public_unlisted])
 
     params.each do |key, value|
       next if %w(page report_id).include?(key.to_s)
diff --git a/app/models/announcement.rb b/app/models/announcement.rb
index 339f5ae70c..5d30d03fef 100644
--- a/app/models/announcement.rb
+++ b/app/models/announcement.rb
@@ -57,7 +57,7 @@ class Announcement < ApplicationRecord
     @statuses ||= if status_ids.nil?
                     []
                   else
-                    Status.where(id: status_ids, visibility: [:public, :unlisted])
+                    Status.where(id: status_ids, visibility: [:public, :unlisted, :public_unlisted])
                   end
   end
 
diff --git a/app/models/concerns/status_threading_concern.rb b/app/models/concerns/status_threading_concern.rb
index 8b628beea2..15118c10f7 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]).reorder(id: :asc).limit(limit)
+    account.statuses.where(in_reply_to_id: id, visibility: [:public, :unlisted, :public_unlisted]).reorder(id: :asc).limit(limit)
   end
 
   private
diff --git a/app/models/featured_tag.rb b/app/models/featured_tag.rb
index 587dcf9912..1bc47af7f2 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)).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)).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)).tagged_with(tag).count
-    self.last_status_at = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).select(:created_at).first&.created_at
+    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
   end
 
   def validate_featured_tags_limit
diff --git a/app/models/public_feed.rb b/app/models/public_feed.rb
index 1cfd9a500c..51a9058a84 100644
--- a/app/models/public_feed.rb
+++ b/app/models/public_feed.rb
@@ -25,6 +25,7 @@ class PublicFeed
     scope.merge!(without_reblogs_scope) unless with_reblogs?
     scope.merge!(local_only_scope) if local_only?
     scope.merge!(remote_only_scope) if remote_only?
+    scope.merge!(global_timeline_only_scope) if global_timeline?
     scope.merge!(account_filters_scope) if account?
     scope.merge!(media_only_scope) if media_only?
     scope.merge!(language_scope) if account&.chosen_languages.present?
@@ -52,6 +53,10 @@ class PublicFeed
     options[:remote]
   end
 
+  def global_timeline?
+    !options[:remote] && !options[:local]
+  end
+
   def account?
     account.present?
   end
@@ -72,6 +77,10 @@ class PublicFeed
     Status.remote
   end
 
+  def global_timeline_only_scope
+    Status.with_global_timeline_visibility.joins(:account).merge(Account.without_suspended.without_silenced)
+  end
+
   def without_replies_scope
     Status.without_replies
   end
diff --git a/app/models/status.rb b/app/models/status.rb
index 3bccb5faa6..fe93de6efb 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -51,7 +51,7 @@ class Status < ApplicationRecord
 
   update_index('statuses', :proper)
 
-  enum visibility: { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4 }, _suffix: :visibility
+  enum visibility: { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4, public_unlisted: 10 }, _suffix: :visibility
 
   belongs_to :application, class_name: 'Doorkeeper::Application', optional: true
 
@@ -99,7 +99,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) }
+  scope :with_public_visibility, -> { where(visibility: [:public, :public_unlisted]) }
+  scope :with_global_timeline_visibility, -> { where(visibility: [:public]) }
   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 }) }
@@ -232,7 +233,7 @@ class Status < ApplicationRecord
   end
 
   def distributable?
-    public_visibility? || unlisted_visibility?
+    public_visibility? || unlisted_visibility? || public_unlisted_visibility?
   end
 
   alias sign? distributable?
diff --git a/app/policies/admin/status_policy.rb b/app/policies/admin/status_policy.rb
index ffaa30f13d..181c16653f 100644
--- a/app/policies/admin/status_policy.rb
+++ b/app/policies/admin/status_policy.rb
@@ -12,7 +12,7 @@ class Admin::StatusPolicy < ApplicationPolicy
   end
 
   def show?
-    role.can?(:manage_reports, :manage_users) && (record.public_visibility? || record.unlisted_visibility? || record.reported?)
+    role.can?(:manage_reports, :manage_users) && (record.public_visibility? || record.unlisted_visibility? || record.public_unlisted_visibility? || record.reported?)
   end
 
   def destroy?
diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb
index 6088dbcffb..22d5e3d95d 100644
--- a/app/presenters/status_relationships_presenter.rb
+++ b/app/presenters/status_relationships_presenter.rb
@@ -17,7 +17,7 @@ class StatusRelationshipsPresenter
       statuses            = statuses.compact
       status_ids          = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq.compact
       conversation_ids    = statuses.filter_map(&:conversation_id).uniq
-      pinnable_status_ids = statuses.map(&:proper).filter_map { |s| s.id if s.account_id == current_account_id && %w(public unlisted private).include?(s.visibility) }
+      pinnable_status_ids = statuses.map(&:proper).filter_map { |s| s.id if s.account_id == current_account_id && %w(public unlisted public_unlisted private).include?(s.visibility) }
 
       @filters_map         = build_filters_map(statuses, current_account_id).merge(options[:filters_map] || {})
       @reblogs_map         = Status.reblogs_map(status_ids, current_account_id).merge(options[:reblogs_map] || {})
diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb
index 12e76fa285..382c8bb460 100644
--- a/app/serializers/rest/status_serializer.rb
+++ b/app/serializers/rest/status_serializer.rb
@@ -137,7 +137,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
     current_user? &&
       current_user.account_id == object.account_id &&
       !object.reblog? &&
-      %w(public unlisted private).include?(object.visibility)
+      %w(public unlisted public_unlisted private).include?(object.visibility)
   end
 
   def source_requested?
diff --git a/app/services/concerns/account_limitable.rb b/app/services/concerns/account_limitable.rb
index 8a55d857c9..9788eda91e 100644
--- a/app/services/concerns/account_limitable.rb
+++ b/app/services/concerns/account_limitable.rb
@@ -3,7 +3,7 @@
 module AccountLimitable
   def scope_status(status)
     case status.visibility.to_sym
-    when :public, :unlisted
+    when :public, :unlisted, :public_unlisted
       #scope_local.merge(scope_list_following_account(status.account))
       scope_local
     when :private
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index 2554756a5d..5544155ce4 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -41,7 +41,7 @@ class FanOutOnWriteService < BaseService
     notify_about_update! if update?
 
     case @status.visibility.to_sym
-    when :public, :unlisted, :private
+    when :public, :unlisted, :public_unlisted, :private
       deliver_to_all_followers!
       deliver_to_lists!
     when :limited
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index ea27f374e7..d1e750c9c4 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -65,7 +65,7 @@ class PostStatusService < BaseService
     @sensitive    = (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @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 && @account.silenced?
+    @visibility   = :unlisted if (@visibility&.to_sym == :public || @visibility&.to_sym == :public_unlisted) && @account.silenced?
     @scheduled_at = @options[:scheduled_at]&.to_datetime
     @scheduled_at = nil if scheduled_in_the_past?
   rescue ArgumentError
diff --git a/app/services/report_service.rb b/app/services/report_service.rb
index 0ce525b071..b673c5a029 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 private) : %i(public unlisted)
+    visibility = has_followers ? %i(public unlisted public_unlisted 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/services/translate_status_service.rb b/app/services/translate_status_service.rb
index 539a0d9db5..ebe64a6028 100644
--- a/app/services/translate_status_service.rb
+++ b/app/services/translate_status_service.rb
@@ -6,7 +6,7 @@ class TranslateStatusService < BaseService
   include FormattingHelper
 
   def call(status, target_language)
-    raise Mastodon::NotPermittedError unless status.public_visibility? || status.unlisted_visibility?
+    raise Mastodon::NotPermittedError unless status.public_visibility? || status.unlisted_visibility? || status.public_unlisted_visibility?
 
     @status = status
     @content = status_content_format(@status)
diff --git a/app/workers/feed_any_json_worker.rb b/app/workers/feed_any_json_worker.rb
index cf9bdca17d..2178dce11c 100644
--- a/app/workers/feed_any_json_worker.rb
+++ b/app/workers/feed_any_json_worker.rb
@@ -16,8 +16,8 @@ class FeedAnyJsonWorker
         redis.publish("timeline:#{account.id}", payload_json) if redis.exists?("subscribed:timeline:#{account.id}")
       end
 
-      if status.visibility.to_sym != :public && status.visibility.to_sym != :unlisted && status.account_id != my_account_id &&
-         redis.exists?("subscribed:timeline:#{status.account_id}")
+      if status.visibility.to_sym != :public && status.visibility.to_sym != :unlisted && status.visibility.to_sum != :public_unlisted && status.account_id != my_account_id &&
+          redis.exists?("subscribed:timeline:#{status.account_id}")
         redis.publish("timeline:#{status.account_id}", payload_json)
       end
     end