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],
})
}