APP-2774: Add algorithmic visibility setting (#11417)

This commit is contained in:
Spence Pope
2026-08-16 17:13:17 -04:00
committed by GitHub
parent 1f5c698165
commit 0ba971b1f6
7 changed files with 191 additions and 15 deletions
@@ -0,0 +1,21 @@
{
"lexicon": 1,
"id": "app.bsky.actor.contentVisibilityDeclaration",
"defs": {
"main": {
"type": "record",
"description": "A declaration of an account's preferences for appearing in content discovery surfaces.",
"key": "literal:self",
"record": {
"type": "object",
"required": ["hideFromAlgorithmicRecommendations"],
"properties": {
"hideFromAlgorithmicRecommendations": {
"type": "boolean",
"description": "Whether the account requests that its posts be hidden from algorithmic recommendations. Consumers must treat a missing record as false."
}
}
}
}
}
}
+1
View File
@@ -25,6 +25,7 @@ export enum Features {
FollowSortEnable = 'follow_sort:enable',
OnboardingInterestsRequiredEnable = 'onboarding:interests:required:enable',
CanonicalPostNumberingEnable = 'canonical_post_numbering:enable',
ContentVisibilitySettingsEnable = 'content_visibility_settings:enable',
// values
TrendingDiscoverValues = 'trending_discover:values',
+2
View File
@@ -1075,6 +1075,8 @@ export type Events = {
'bot:label:toggle': {state: 'add' | 'remove'}
'bot:badge:click': {}
'contentVisibility:algorithmicRecommendations:change': {hide: boolean}
'live:create': {duration: number}
'live:edit': {}
'live:remove': {}
@@ -13,10 +13,13 @@ import * as Admonition from '#/components/Admonition'
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlashIcon} from '#/components/icons/EyeSlash'
import {Key_Stroke2_Corner2_Rounded as KeyIcon} from '#/components/icons/Key'
import {MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlassIcon} from '#/components/icons/MagnifyingGlass'
import {ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon} from '#/components/icons/Shield'
import * as Layout from '#/components/Layout'
import {InlineLinkText} from '#/components/Link'
import {useAnalytics} from '#/analytics'
import {type app} from '#/lexicons'
import {AlgoVisibilityOptOut} from './components/AlgoVisibilityOptOut'
import {Email2FAToggle} from './components/Email2FAToggle'
import {PwiOptOut} from './components/PwiOptOut'
import {ItemTextWithSubtitle} from './NotificationSettings/components/ItemTextWithSubtitle'
@@ -28,6 +31,10 @@ type Props = NativeStackScreenProps<
export function PrivacyAndSecuritySettingsScreen({}: Props) {
const {_} = useLingui()
const t = useTheme()
const ax = useAnalytics()
const isContentVisibilityEnabled = ax.features.enabled(
ax.features.ContentVisibilitySettingsEnable,
)
const {data: appPasswords} = useAppPasswordsQuery()
const {currentAccount} = useSession()
const {
@@ -101,13 +108,16 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) {
/>
</SettingsList.LinkItem>
<SettingsList.Divider />
<SettingsList.Group>
{isContentVisibilityEnabled && (
<SettingsList.Item style={[a.align_start]}>
<SettingsList.ItemIcon icon={MagnifyingGlassIcon} />
<AlgoVisibilityOptOut />
</SettingsList.Item>
)}
<SettingsList.Item style={[a.align_start]}>
<SettingsList.ItemIcon icon={EyeSlashIcon} />
<SettingsList.ItemText>
<Trans>Logged-out visibility</Trans>
</SettingsList.ItemText>
<PwiOptOut />
</SettingsList.Group>
</SettingsList.Item>
<SettingsList.Item>
<Admonition.Outer type="tip" style={[a.flex_1]}>
<Admonition.Row>
@@ -115,11 +125,10 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) {
<Admonition.Content>
<Admonition.Text>
<Trans>
Note: Bluesky is an open and public network. This setting
only limits the visibility of your content on the Bluesky
app and website, and other apps may not respect this
setting. Your content may still be shown to logged-out
users by other apps and websites.
Note: Bluesky is part of the Atmosphere, an open public
network. These settings ask other apps and websites to
limit your visibility, but they can choose not to. Your
public content may still appear elsewhere.
</Trans>
</Admonition.Text>
<Admonition.Text>
@@ -0,0 +1,51 @@
import {View} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro'
import {
useContentVisibilityMutation,
useContentVisibilityQuery,
} from '#/state/queries/content-visibility'
import {atoms as a, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import {Text} from '#/components/Typography'
export function AlgoVisibilityOptOut() {
const t = useTheme()
const {t: l} = useLingui()
const {data, isPending: isQueryPending} = useContentVisibilityQuery()
const updateContentVisibility = useContentVisibilityMutation()
const isOptedOut = data?.hideFromAlgorithmicRecommendations ?? false
const canToggle = !isQueryPending && !updateContentVisibility.isPending
const onToggleOptOut = (hide: boolean) => {
updateContentVisibility.mutate(hide)
}
return (
<View style={[a.flex_1, a.gap_sm]}>
<Toggle.Item
name="algorithmic_visibility"
disabled={!canToggle}
value={isOptedOut}
onChange={onToggleOptOut}
label={l`Ask apps to hide my posts from algorithmic recommendations`}
style={[a.w_full]}>
<Toggle.LabelText style={[a.flex_1]}>
<Trans>
Ask apps to hide my posts from algorithmic recommendations
</Trans>
</Toggle.LabelText>
<Toggle.Platform />
</Toggle.Item>
<Text style={[a.leading_snug, t.atoms.text_contrast_high]}>
<Trans>
Bluesky will not show your posts in the Discover feed (except to your
followers) and will ask other apps not to show your posts in their own
algorithmic recommendations.
</Trans>
</Text>
</View>
)
}
@@ -86,12 +86,12 @@ export function PwiOptOut() {
value={isOptedOut}
onChange={onToggleOptOut}
label={_(
msg`Discourage apps from showing my account to logged-out users`,
msg`Ask apps and sites not to show my account to logged-out users`,
)}
style={[a.w_full]}>
<Toggle.LabelText style={[a.flex_1]}>
<Trans>
Discourage apps from showing my account to logged-out users
Ask apps and sites not to show my account to logged-out users
</Trans>
</Toggle.LabelText>
<Toggle.Platform />
@@ -99,9 +99,9 @@ export function PwiOptOut() {
<Text style={[a.leading_snug, t.atoms.text_contrast_high]}>
<Trans>
Bluesky will not show your profile and posts to logged-out users.
Other apps may not honor this request. This does not make your account
private.
Bluesky will not show your account to logged-out users and will ask
other apps to do the same. Other apps may not honor this request. It
doesn't make your account private.
</Trans>
</Text>
</View>
+92
View File
@@ -0,0 +1,92 @@
import {t} from '@lingui/core/macro'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {isRecordNotFoundError} from '#/lib/xrpc-error'
import {createQueryKey} from '#/state/queries/util'
import {usePdsClient, useSession} from '#/state/session'
import * as Toast from '#/components/Toast'
import {useAnalytics} from '#/analytics'
import {app} from '#/lexicons'
export const contentVisibilityQueryKey = (did: string) =>
createQueryKey('content-visibility', {did})
export function useContentVisibilityQuery() {
const client = usePdsClient()
const {currentAccount} = useSession()
const did = currentAccount?.did
return useQuery({
queryKey: contentVisibilityQueryKey(did ?? ''),
queryFn: async () => {
try {
const response = await client.get(
app.bsky.actor.contentVisibilityDeclaration,
{
repo: did!,
rkey: 'self',
},
)
return app.bsky.actor.contentVisibilityDeclaration.$parse(
response.value,
)
} catch (error) {
if (isRecordNotFoundError(error)) {
return app.bsky.actor.contentVisibilityDeclaration.$build({
hideFromAlgorithmicRecommendations: false,
})
}
throw error
}
},
enabled: !!did,
})
}
export function useContentVisibilityMutation() {
const ax = useAnalytics()
const client = usePdsClient()
const {currentAccount} = useSession()
const queryClient = useQueryClient()
const did = currentAccount?.did
const queryKey = contentVisibilityQueryKey(did ?? '')
return useMutation({
mutationFn: async (hideFromAlgorithmicRecommendations: boolean) => {
if (!did) throw new Error('Not signed in')
const record = app.bsky.actor.contentVisibilityDeclaration.$build({
hideFromAlgorithmicRecommendations,
})
await client.put(app.bsky.actor.contentVisibilityDeclaration, record, {
repo: did,
rkey: 'self',
})
return record
},
onMutate: async hideFromAlgorithmicRecommendations => {
await queryClient.cancelQueries({queryKey})
const previous =
queryClient.getQueryData<app.bsky.actor.contentVisibilityDeclaration.Main>(
queryKey,
)
queryClient.setQueryData(
queryKey,
app.bsky.actor.contentVisibilityDeclaration.$build({
hideFromAlgorithmicRecommendations,
}),
)
return {previous}
},
onError: (_error, _variables, context) => {
queryClient.setQueryData(queryKey, context?.previous)
Toast.show(t`Failed to update content visibility`)
},
onSuccess: (_record, hide) => {
ax.metric('contentVisibility:algorithmicRecommendations:change', {hide})
},
onSettled: () => {
void queryClient.invalidateQueries({queryKey})
},
})
}