diff --git a/README.md b/README.md index af62a0f3d..bac7944df 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,6 @@ These are all available as options in the Runes sub-page of the app's settings. - Change PLC directory - Change Constellation instance - Set custom AppView DID - - Do not declare any default app labelers - Settings sync - Sync settings between devices - Include OpenRouter API key in synced settings diff --git a/src/lib/api/feed/custom.ts b/src/lib/api/feed/custom.ts index d6bea2c79..cb073f78d 100644 --- a/src/lib/api/feed/custom.ts +++ b/src/lib/api/feed/custom.ts @@ -1,11 +1,12 @@ import { type AppBskyFeedDefs, type AppBskyFeedGetFeed as GetCustomFeed, - BskyAgent, + type BskyAgent, jsonStringToLex, } from '@atproto/api' import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' +import {getActiveAppLabelers} from '#/lib/moderation' import { getAppLanguageAsContentLanguage, getContentLanguages, @@ -114,7 +115,7 @@ async function loggedOutFetch({ * @see https://github.com/bluesky-social/atproto/blob/60df3fc652b00cdff71dd9235d98a7a4bb828f05/packages/api/src/agent.ts#L120 */ const labelersHeader = { - 'atproto-accept-labelers': BskyAgent.appLabelers + 'atproto-accept-labelers': getActiveAppLabelers() .map(l => `${l};redact`) .join(', '), } diff --git a/src/lib/moderation.ts b/src/lib/moderation.ts index 9b0ac0ea5..b859a8da8 100644 --- a/src/lib/moderation.ts +++ b/src/lib/moderation.ts @@ -12,6 +12,7 @@ import { import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' +import {isIgnoredAppLabeler} from '#/state/preferences/ignored-app-labelers' import {type AppModerationCause} from '#/components/Pills' export const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn'] as const @@ -96,6 +97,10 @@ export function isAppLabeler( return BskyAgent.appLabelers.includes(labeler.creator.did) } +export function getActiveAppLabelers() { + return BskyAgent.appLabelers.filter(did => !isIgnoredAppLabeler(did)) +} + export function isLabelerSubscribed( labeler: | string @@ -104,6 +109,9 @@ export function isLabelerSubscribed( modOpts: ModerationOpts, ) { labeler = typeof labeler === 'string' ? labeler : labeler.creator.did + if (isIgnoredAppLabeler(labeler)) { + return false + } if (isAppLabeler(labeler)) { return true } diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx index 5be6f76a3..cb491575b 100644 --- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx @@ -14,10 +14,11 @@ import {Plural, Trans} from '@lingui/react/macro' // eslint-disable-next-line @typescript-eslint/no-unused-vars import {MAX_LABELERS} from '#/lib/constants' import {useHaptics} from '#/lib/haptics' -import {isAppLabeler} from '#/lib/moderation' +import {isAppLabeler, isLabelerSubscribed} from '#/lib/moderation' import {useProfileShadow} from '#/state/cache/profile-shadow' import {type Shadow} from '#/state/cache/types' import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' +import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useLabelerSubscriptionMutation} from '#/state/queries/labeler' import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like' import {usePreferencesQuery} from '#/state/queries/preferences' @@ -248,6 +249,7 @@ export function HeaderLabelerButtons({ const playHaptic = useHaptics() const editProfileControl = useDialogControl() const {data: preferences} = usePreferencesQuery() + const moderationOpts = useModerationOpts() const { mutateAsync: toggleSubscription, variables, @@ -255,7 +257,11 @@ export function HeaderLabelerButtons({ } = useLabelerSubscriptionMutation() const isSubscribed = variables?.subscribe ?? - preferences?.moderationPrefs.labelers.find(l => l.did === profile.did) + (moderationOpts + ? !!isLabelerSubscribed(profile.did, moderationOpts) + : !!preferences?.moderationPrefs.labelers.find( + l => l.did === profile.did, + )) const cantSubscribePrompt = Prompt.usePromptControl() @@ -304,7 +310,7 @@ export function HeaderLabelerButtons({ - ) : !isAppLabeler(profile.did) && !minimal ? ( + ) : !minimal ? ( // hidden in the minimal header, because it's not shadowed so the two buttons // can get out of sync. if you want to reenable, you'll need to add shadowing // to the subscribed state -sfn diff --git a/src/screens/Settings/RunesSettings/InfrastructureSettings.tsx b/src/screens/Settings/RunesSettings/InfrastructureSettings.tsx index 209284dbf..afbbe476f 100644 --- a/src/screens/Settings/RunesSettings/InfrastructureSettings.tsx +++ b/src/screens/Settings/RunesSettings/InfrastructureSettings.tsx @@ -18,10 +18,6 @@ import { useImageCdnHost, useSetImageCdnHost, } from '#/state/preferences/image-cdn-host' -import { - useNoAppLabelers, - useSetNoAppLabelers, -} from '#/state/preferences/no-app-labelers' import { usePlcDirectory, useSetPlcDirectory, @@ -42,7 +38,6 @@ import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import * as Toggle from '#/components/forms/Toggle' import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' -import {RaisingHand4Finger_Stroke2_Corner0_Rounded as RaisingHandIcon} from '#/components/icons/RaisingHand' import {Star_Stroke2_Corner0_Rounded as StarIcon} from '#/components/icons/Star' import {InlineLinkText} from '#/components/Link' import {Text} from '#/components/Typography' @@ -67,9 +62,6 @@ export function RunesInfrastructureSettingsScreen() { const [customAppViewDid] = useCustomAppViewDid() const setCustomAppViewDidControl = Dialog.useDialogControl() - - const noAppLabelers = useNoAppLabelers() - const setNoAppLabelers = useSetNoAppLabelers() const restartPromptControl = Dialog.useDialogControl() return ( @@ -220,44 +212,6 @@ export function RunesInfrastructureSettingsScreen() { /> - - - { - setNoAppLabelers(value) - restartPromptControl.open() - }}> - - - - Do not declare any default app labelers - - - - - - - - - Some AppViews will default to using an app labeler if you have no - labelers, so consider subscribing to at least one labeler if you - have issues. - - - - - - - App labelers are mandatory top-level labelers that can perform - "takedowns". This setting does not influence geolocation-based - labelers. - - - - ({ diff --git a/src/state/preferences/ignored-app-labelers.tsx b/src/state/preferences/ignored-app-labelers.tsx new file mode 100644 index 000000000..036f95d9e --- /dev/null +++ b/src/state/preferences/ignored-app-labelers.tsx @@ -0,0 +1,78 @@ +import { + createContext, + type PropsWithChildren, + useCallback, + useContext, + useEffect, + useState, +} from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = persisted.Schema['ignoredAppLabelers'] +type SetContext = (v: persisted.Schema['ignoredAppLabelers']) => void + +const stateContext = createContext( + persisted.defaults.ignoredAppLabelers, +) +const setContext = createContext( + (_: persisted.Schema['ignoredAppLabelers']) => {}, +) + +export function Provider({children}: PropsWithChildren<{}>) { + const [state, setState] = useState(persisted.get('ignoredAppLabelers')) + + const setStateWrapped = useCallback( + (ignoredAppLabelers: persisted.Schema['ignoredAppLabelers']) => { + setState(ignoredAppLabelers) + persisted.write('ignoredAppLabelers', ignoredAppLabelers) + }, + [setState], + ) + + useEffect(() => { + return persisted.onUpdate('ignoredAppLabelers', nextIgnoredAppLabelers => { + setState(nextIgnoredAppLabelers) + }) + }, [setStateWrapped]) + + return ( + + + {children} + + + ) +} + +export function useIgnoredAppLabelers() { + return useContext(stateContext) ?? persisted.defaults.ignoredAppLabelers! +} + +export function useSetIgnoredAppLabelers() { + return useContext(setContext) +} + +export function getIgnoredAppLabelers() { + return ( + persisted.get('ignoredAppLabelers') ?? + persisted.defaults.ignoredAppLabelers! + ) +} + +export function isIgnoredAppLabeler(did: string) { + return getIgnoredAppLabelers().includes(did) +} + +export function addIgnoredAppLabeler(did: string) { + const ignored = getIgnoredAppLabelers() + if (ignored.includes(did)) return + persisted.write('ignoredAppLabelers', [...ignored, did]) +} + +export function removeIgnoredAppLabeler(did: string) { + persisted.write( + 'ignoredAppLabelers', + getIgnoredAppLabelers().filter(d => d !== did), + ) +} diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx index 1f37ff5d2..14887250c 100644 --- a/src/state/preferences/index.tsx +++ b/src/state/preferences/index.tsx @@ -39,12 +39,12 @@ import {Provider as HideScaryFollowButtonsProvider} from './hide-scary-follow-bu import {Provider as HideSimilarAccountsRecommProvider} from './hide-similar-accounts-recommendations' import {Provider as HideUnreplyablePostsProvider} from './hide-unreplyable-posts' import {Provider as HighQualityImagesProvider} from './high-quality-images' +import {Provider as IgnoredAppLabelersProvider} from './ignored-app-labelers' import {Provider as ImageCdnHostProvider} from './image-cdn-host' import {Provider as InAppBrowserProvider} from './in-app-browser' import {Provider as KawaiiProvider} from './kawaii' import {Provider as LanguagesProvider} from './languages' import {Provider as LargeAltBadgeProvider} from './large-alt-badge' -import {Provider as NoAppLabelersProvider} from './no-app-labelers' import {Provider as NoDiscoverProvider} from './no-discover-fallback' import {Provider as OmitViaFieldProvider} from './omit-via-field' import {Provider as OpenRouterProvider} from './openrouter' @@ -147,7 +147,7 @@ export function Provider({children}: PropsWithChildren<{}>) { - + @@ -191,37 +191,37 @@ export function Provider({children}: PropsWithChildren<{}>) { - - - - - - - - - - - - - - { - children - } - - - - - - - - - - - - - - - + + + + + + + + + + + + + + { + children + } + + + + + + + + + + + + + + + @@ -263,7 +263,7 @@ export function Provider({children}: PropsWithChildren<{}>) { - + diff --git a/src/state/preferences/moderation-opts.tsx b/src/state/preferences/moderation-opts.tsx index 5f520e13c..7bbb98efc 100644 --- a/src/state/preferences/moderation-opts.tsx +++ b/src/state/preferences/moderation-opts.tsx @@ -1,8 +1,10 @@ import {createContext, useContext, useMemo} from 'react' import {type PropsWithChildren} from 'react' -import {BskyAgent, type ModerationOpts} from '@atproto/api' +import {type ModerationOpts} from '@atproto/api' +import {getActiveAppLabelers} from '#/lib/moderation' import {useHiddenPosts, useLabelDefinitions} from '#/state/preferences' +import {useIgnoredAppLabelers} from '#/state/preferences/ignored-app-labelers' import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation' import {useSession} from '#/state/session' import {usePreferencesQuery} from '../queries/preferences' @@ -28,6 +30,7 @@ export function Provider({children}: PropsWithChildren<{}>) { const prefs = usePreferencesQuery() const {labelDefs} = useLabelDefinitions() const hiddenPosts = useHiddenPosts() // TODO move this into pds-stored prefs + const ignoredAppLabelers = useIgnoredAppLabelers() const userDid = currentAccount?.did const moderationPrefs = prefs.data?.moderationPrefs @@ -43,8 +46,10 @@ export function Provider({children}: PropsWithChildren<{}>) { prefs: { ...moderationPrefs, labelers: moderationPrefs.labelers.length - ? moderationPrefs.labelers - : BskyAgent.appLabelers.map(did => ({ + ? moderationPrefs.labelers.filter( + l => !ignoredAppLabelers.includes(l.did), + ) + : getActiveAppLabelers().map(did => ({ did, labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES, })), @@ -52,7 +57,14 @@ export function Provider({children}: PropsWithChildren<{}>) { }, labelDefs, } - }, [override, userDid, labelDefs, moderationPrefs, hiddenPosts]) + }, [ + override, + userDid, + labelDefs, + moderationPrefs, + hiddenPosts, + ignoredAppLabelers, + ]) return ( diff --git a/src/state/preferences/no-app-labelers.tsx b/src/state/preferences/no-app-labelers.tsx deleted file mode 100644 index 2cddf1955..000000000 --- a/src/state/preferences/no-app-labelers.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { - createContext, - type PropsWithChildren, - useCallback, - useContext, - useEffect, - useState, -} from 'react' - -import * as persisted from '#/state/persisted' - -type StateContext = persisted.Schema['noAppLabelers'] -type SetContext = (v: persisted.Schema['noAppLabelers']) => void - -const stateContext = createContext( - persisted.defaults.noAppLabelers, -) -const setContext = createContext( - (_: persisted.Schema['noAppLabelers']) => {}, -) - -export function Provider({children}: PropsWithChildren<{}>) { - const [state, setState] = useState(persisted.get('noAppLabelers')) - - const setStateWrapped = useCallback( - (noAppLabelers: persisted.Schema['noAppLabelers']) => { - setState(noAppLabelers) - persisted.write('noAppLabelers', noAppLabelers) - }, - [setState], - ) - - useEffect(() => { - return persisted.onUpdate('noAppLabelers', nextNoAppLabelers => { - setState(nextNoAppLabelers) - }) - }, [setStateWrapped]) - - return ( - - - {children} - - - ) -} - -export function useNoAppLabelers() { - return useContext(stateContext) -} - -export function useSetNoAppLabelers() { - return useContext(setContext) -} - -export function getNoAppLabelers() { - return persisted.get('noAppLabelers') || persisted.defaults.noAppLabelers! -} diff --git a/src/state/preferences/settings-sync.tsx b/src/state/preferences/settings-sync.tsx index d219e7791..c8c38d9d4 100644 --- a/src/state/preferences/settings-sync.tsx +++ b/src/state/preferences/settings-sync.tsx @@ -39,7 +39,7 @@ export const SYNCED_PREFS_KEYS = [ 'goLinksEnabled', 'constellationEnabled', 'directFetchRecords', - 'noAppLabelers', + 'ignoredAppLabelers', 'noDiscoverFallback', 'repostCarouselEnabled', 'alsoLikedFeedEnabled', diff --git a/src/state/queries/labeler.ts b/src/state/queries/labeler.ts index 2e3d9682d..096b892bf 100644 --- a/src/state/queries/labeler.ts +++ b/src/state/queries/labeler.ts @@ -3,6 +3,11 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {z} from 'zod' import {MAX_LABELERS} from '#/lib/constants' +import {isAppLabeler} from '#/lib/moderation' +import { + addIgnoredAppLabeler, + removeIgnoredAppLabeler, +} from '#/state/preferences/ignored-app-labelers' import {GCTIME, STALE} from '#/state/queries' import { preferencesQueryKey, @@ -141,13 +146,24 @@ export function useLabelerSubscriptionMutation() { } if (subscribe) { - const labelerCount = labelerDids.length - invalidLabelers.length - if (labelerCount >= MAX_LABELERS) { - throw new Error('MAX_LABELERS') + if (isAppLabeler(did)) { + removeIgnoredAppLabeler(did) + } else { + const labelerCount = labelerDids.length - invalidLabelers.length + if (labelerCount >= MAX_LABELERS) { + throw new Error('MAX_LABELERS') + } + await pdsAgent(agent).addLabeler(did) } - await pdsAgent(agent).addLabeler(did) } else { - await pdsAgent(agent).removeLabeler(did) + if (isAppLabeler(did)) { + addIgnoredAppLabeler(did) + await pdsAgent(agent) + .removeLabeler(did) + .catch(() => {}) + } else { + await pdsAgent(agent).removeLabeler(did) + } } }, async onSuccess() { diff --git a/src/state/queries/preferences/moderation.ts b/src/state/queries/preferences/moderation.ts index 1c0f5a3fe..1ac372f0f 100644 --- a/src/state/queries/preferences/moderation.ts +++ b/src/state/queries/preferences/moderation.ts @@ -1,10 +1,11 @@ import {useMemo} from 'react' import { - BskyAgent, DEFAULT_LABEL_SETTINGS, interpretLabelValueDefinitions, } from '@atproto/api' +import {getActiveAppLabelers} from '#/lib/moderation' +import {useIgnoredAppLabelers} from '#/state/preferences/ignored-app-labelers' import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities' import {useLabelersDetailedInfoQuery} from '../labeler' import {usePreferencesQuery} from './index' @@ -23,13 +24,14 @@ export function useMyLabelersQuery({ excludeNonConfigurableLabelers?: boolean } = {}) { const prefs = usePreferencesQuery() + const ignoredAppLabelers = useIgnoredAppLabelers() let dids = Array.from( new Set( - BskyAgent.appLabelers.concat( + getActiveAppLabelers().concat( prefs.data?.moderationPrefs.labelers.map(l => l.did) || [], ), ), - ) + ).filter(did => !ignoredAppLabelers.includes(did)) if (excludeNonConfigurableLabelers) { dids = dids.filter(did => !isNonConfigurableModerationAuthority(did)) } @@ -43,7 +45,7 @@ export function useMyLabelersQuery({ data: labelers.data, refetch: labelers.refetch, } - }, [labelers, isLoading, error]) + }, [labelers, isLoading, error, ignoredAppLabelers]) } export function useLabelDefinitionsQuery() { diff --git a/src/state/session/moderation.ts b/src/state/session/moderation.ts index 6b67384bb..e93c3410f 100644 --- a/src/state/session/moderation.ts +++ b/src/state/session/moderation.ts @@ -2,7 +2,6 @@ import {type Agent} from '@atproto/api' import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api' import {IS_TEST_USER} from '#/lib/constants' -import {getNoAppLabelers} from '../preferences/no-app-labelers' import {configureAdditionalModerationAuthorities} from './additional-moderation-authorities' import {readLabelers} from './agent-config' import {type SessionAccount} from './types' @@ -41,7 +40,7 @@ export async function configureModerationForAccount( function switchToBskyAppLabeler() { BskyAgent.configure({ - appLabelers: getNoAppLabelers() ? [] : [BSKY_LABELER_DID], + appLabelers: [BSKY_LABELER_DID], }) }