Add repost-only mutes (#11223)

Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
This commit is contained in:
Eric Bailey
2026-08-06 13:53:02 -05:00
committed by GitHub
parent 1cbe7a39e6
commit 724013df1b
14 changed files with 259 additions and 64 deletions
+11 -1
View File
@@ -41,6 +41,7 @@ export type {Shadow} from './types'
export interface ProfileShadow {
followingUri: string | undefined
muted: boolean | undefined
mutedOnlyReposts: boolean | undefined
blockingUri: string | undefined
verification: AppBskyActorDefs.VerificationState
status: AppBskyActorDefs.StatusView | undefined
@@ -243,6 +244,11 @@ export function isProfileShadowApplied<
if ('muted' in shadow) {
if (profile.viewer?.muted !== shadow.muted) return false
}
if ('mutedOnlyReposts' in shadow) {
if (profile.viewer?.mutedOnlyReposts !== shadow.mutedOnlyReposts) {
return false
}
}
if ('blockingUri' in shadow) {
if (profile.viewer?.blocking !== shadow.blockingUri) return false
}
@@ -274,13 +280,17 @@ export function mergeShadow<TProfileView extends bsky.profile.AnyProfileView>(
? shadow.followingUri
: profile.viewer?.following,
muted: 'muted' in shadow ? shadow.muted : profile.viewer?.muted,
mutedOnlyReposts:
'mutedOnlyReposts' in shadow
? shadow.mutedOnlyReposts
: profile.viewer?.mutedOnlyReposts,
blocking:
'blockingUri' in shadow ? shadow.blockingUri : profile.viewer?.blocking,
activitySubscription:
'activitySubscription' in shadow
? shadow.activitySubscription
: profile.viewer?.activitySubscription,
},
} satisfies AppBskyActorDefs.ViewerState,
verification:
'verification' in shadow ? shadow.verification : profile.verification,
status:
+78 -1
View File
@@ -449,9 +449,13 @@ export function useProfileMuteMutationQueue(
})
const queueMute = useCallback(() => {
// optimistically update
/*
* Optimistically update. A full mute replaces any stored repost-only
* scope on the server, so clear it here too.
*/
updateProfileShadow(queryClient, did, {
muted: true,
mutedOnlyReposts: false,
})
return queueToggle(true)
}, [queryClient, did, queueToggle])
@@ -460,6 +464,7 @@ export function useProfileMuteMutationQueue(
// optimistically update
updateProfileShadow(queryClient, did, {
muted: false,
mutedOnlyReposts: false,
})
return queueToggle(false)
}, [queryClient, did, queueToggle])
@@ -467,6 +472,65 @@ export function useProfileMuteMutationQueue(
return [queueMute, queueUnmute] as const
}
/**
* Toggles a repost-only mute: muting hides just the account's reposts,
* unmuting removes the mute entirely. Not applicable when the account is
* fully muted (viewer.muted).
*/
export function useProfileMuteRepostsMutationQueue(
profile: Shadow<bsky.profile.AnyProfileView>,
) {
const ax = useAnalytics()
const queryClient = useQueryClient()
const did = profile.did
const initialMutedOnlyReposts = !!profile.viewer?.mutedOnlyReposts
const muteRepostsMutation = useProfileMuteRepostsMutation()
const unmuteMutation = useProfileUnmuteMutation()
const queueToggle = useToggleMutationQueue({
initialState: initialMutedOnlyReposts,
runMutation: async (_prevMutedOnlyReposts, shouldMute) => {
if (shouldMute) {
await muteRepostsMutation.mutateAsync({
did,
})
ax.metric('profile:muteReposts', {})
return true
} else {
await unmuteMutation.mutateAsync({
did,
})
ax.metric('profile:unmuteReposts', {})
return false
}
},
onSuccess(finalMutedOnlyReposts) {
// finalize
updateProfileShadow(queryClient, did, {
mutedOnlyReposts: finalMutedOnlyReposts,
})
},
})
const queueMuteReposts = useCallback(() => {
// optimistically update
updateProfileShadow(queryClient, did, {
mutedOnlyReposts: true,
})
return queueToggle(true)
}, [queryClient, did, queueToggle])
const queueUnmuteReposts = useCallback(() => {
// optimistically update
updateProfileShadow(queryClient, did, {
mutedOnlyReposts: false,
})
return queueToggle(false)
}, [queryClient, did, queueToggle])
return [queueMuteReposts, queueUnmuteReposts] as const
}
function useProfileMuteMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
@@ -480,6 +544,19 @@ function useProfileMuteMutation() {
})
}
function useProfileMuteRepostsMutation() {
const queryClient = useQueryClient()
const agent = useAgent()
return useMutation<void, Error, {did: string}>({
mutationFn: async ({did}) => {
await agent.mute(did, {onlyReposts: true})
},
onSuccess() {
void queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()})
},
})
}
function useProfileUnmuteMutation() {
const queryClient = useQueryClient()
const agent = useAgent()