Remove invalid labelers when subscribing/unsubscribing (#4771)

* Remove invalid labelers when subscribing/unsubscribing

* Let the async lock cook

* Use link to associate, leave copy as is
zio/stable
Eric Bailey 2024-07-12 14:55:34 -05:00 committed by GitHub
parent 7c1c24ef1b
commit 3627a249ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 74 additions and 27 deletions

View File

@ -133,3 +133,5 @@ export const GIF_SEARCH = (params: string) =>
`${GIF_SERVICE}/tenor/v2/search?${params}` `${GIF_SERVICE}/tenor/v2/search?${params}`
export const GIF_FEATURED = (params: string) => export const GIF_FEATURED = (params: string) =>
`${GIF_SERVICE}/tenor/v2/featured?${params}` `${GIF_SERVICE}/tenor/v2/featured?${params}`
export const MAX_LABELERS = 20

View File

@ -10,6 +10,8 @@ import {
import {msg, Plural, plural, Trans} from '@lingui/macro' import {msg, Plural, plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import {MAX_LABELERS} from '#/lib/constants'
import {isAppLabeler} from '#/lib/moderation' import {isAppLabeler} from '#/lib/moderation'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Shadow} from '#/state/cache/types' import {Shadow} from '#/state/cache/types'
@ -75,14 +77,14 @@ let ProfileHeaderLabeler = ({
[profile, moderationOpts], [profile, moderationOpts],
) )
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
const {mutateAsync: toggleSubscription, variables} = const {
useLabelerSubscriptionMutation() mutateAsync: toggleSubscription,
variables,
reset,
} = useLabelerSubscriptionMutation()
const isSubscribed = const isSubscribed =
variables?.subscribe ?? variables?.subscribe ??
preferences?.moderationPrefs.labelers.find(l => l.did === profile.did) preferences?.moderationPrefs.labelers.find(l => l.did === profile.did)
const canSubscribe =
isSubscribed ||
(preferences ? preferences?.moderationPrefs.labelers.length <= 20 : false)
const {mutateAsync: likeMod, isPending: isLikePending} = useLikeMutation() const {mutateAsync: likeMod, isPending: isLikePending} = useLikeMutation()
const {mutateAsync: unlikeMod, isPending: isUnlikePending} = const {mutateAsync: unlikeMod, isPending: isUnlikePending} =
useUnlikeMutation() useUnlikeMutation()
@ -130,17 +132,17 @@ let ProfileHeaderLabeler = ({
const onPressSubscribe = React.useCallback( const onPressSubscribe = React.useCallback(
() => () =>
requireAuth(async (): Promise<void> => { requireAuth(async (): Promise<void> => {
if (!canSubscribe) {
cantSubscribePrompt.open()
return
}
try { try {
await toggleSubscription({ await toggleSubscription({
did: profile.did, did: profile.did,
subscribe: !isSubscribed, subscribe: !isSubscribed,
}) })
} catch (e: any) { } catch (e: any) {
// setSubscriptionError(e.message) reset()
if (e.message === 'MAX_LABELERS') {
cantSubscribePrompt.open()
return
}
logger.error(`Failed to subscribe to labeler`, {message: e.message}) logger.error(`Failed to subscribe to labeler`, {message: e.message})
} }
}), }),
@ -149,8 +151,8 @@ let ProfileHeaderLabeler = ({
toggleSubscription, toggleSubscription,
isSubscribed, isSubscribed,
profile, profile,
canSubscribe,
cantSubscribePrompt, cantSubscribePrompt,
reset,
], ],
) )
@ -199,8 +201,7 @@ let ProfileHeaderLabeler = ({
style={[ style={[
{ {
paddingVertical: gtMobile ? 12 : 10, paddingVertical: gtMobile ? 12 : 10,
backgroundColor: backgroundColor: isSubscribed
isSubscribed || !canSubscribe
? state.hovered || state.pressed ? state.hovered || state.pressed
? t.palette.contrast_50 ? t.palette.contrast_50
: t.palette.contrast_25 : t.palette.contrast_25
@ -215,11 +216,9 @@ let ProfileHeaderLabeler = ({
<Text <Text
style={[ style={[
{ {
color: canSubscribe color: isSubscribed
? isSubscribed
? t.palette.contrast_700 ? t.palette.contrast_700
: t.palette.white : t.palette.white,
: t.palette.contrast_400,
}, },
a.font_bold, a.font_bold,
a.text_center, a.text_center,
@ -317,6 +316,9 @@ let ProfileHeaderLabeler = ({
ProfileHeaderLabeler = memo(ProfileHeaderLabeler) ProfileHeaderLabeler = memo(ProfileHeaderLabeler)
export {ProfileHeaderLabeler} export {ProfileHeaderLabeler}
/**
* Keep this in sync with the value of {@link MAX_LABELERS}
*/
function CantSubscribePrompt({ function CantSubscribePrompt({
control, control,
}: { }: {

View File

@ -2,9 +2,13 @@ import {AppBskyLabelerDefs} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {z} from 'zod' import {z} from 'zod'
import {MAX_LABELERS} from '#/lib/constants'
import {labelersDetailedInfoQueryKeyRoot} from '#/lib/react-query' import {labelersDetailedInfoQueryKeyRoot} from '#/lib/react-query'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {preferencesQueryKey} from '#/state/queries/preferences' import {
preferencesQueryKey,
usePreferencesQuery,
} from '#/state/queries/preferences'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
const labelerInfoQueryKeyRoot = 'labeler-info' const labelerInfoQueryKeyRoot = 'labeler-info'
@ -77,6 +81,7 @@ export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
export function useLabelerSubscriptionMutation() { export function useLabelerSubscriptionMutation() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const agent = useAgent()
const preferences = usePreferencesQuery()
return useMutation({ return useMutation({
async mutationFn({did, subscribe}: {did: string; subscribe: boolean}) { async mutationFn({did, subscribe}: {did: string; subscribe: boolean}) {
@ -86,14 +91,52 @@ export function useLabelerSubscriptionMutation() {
subscribe: z.boolean(), subscribe: z.boolean(),
}).parse({did, subscribe}) }).parse({did, subscribe})
/**
* If a user has invalid/takendown/deactivated labelers, we need to
* remove them. We don't have a great way to do this atm on the server,
* so we do it here.
*
* We also need to push validation into this method, since we need to
* check {@link MAX_LABELERS} _after_ we've removed invalid or takendown
* labelers.
*/
const labelerDids = (
preferences.data?.moderationPrefs?.labelers ?? []
).map(l => l.did)
const invalidLabelers: string[] = []
if (labelerDids.length) {
const profiles = await agent.getProfiles({actors: labelerDids})
if (profiles.data) {
for (const did of labelerDids) {
const exists = profiles.data.profiles.find(p => p.did === did)
if (exists) {
// profile came back but it's not a valid labeler
if (exists.associated && !exists.associated.labeler) {
invalidLabelers.push(did)
}
} else {
// no response came back, might be deactivated or takendown
invalidLabelers.push(did)
}
}
}
}
if (invalidLabelers.length) {
await Promise.all(invalidLabelers.map(did => agent.removeLabeler(did)))
}
if (subscribe) { if (subscribe) {
const labelerCount = labelerDids.length - invalidLabelers.length
if (labelerCount >= MAX_LABELERS) {
throw new Error('MAX_LABELERS')
}
await agent.addLabeler(did) await agent.addLabeler(did)
} else { } else {
await agent.removeLabeler(did) await agent.removeLabeler(did)
} }
}, },
onSuccess() { async onSuccess() {
queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: preferencesQueryKey, queryKey: preferencesQueryKey,
}) })
}, },