Skip to content

Commit 9b0d337

Browse files
authored
Merge pull request #34 from afbase/ux-issues
UX Issues with profile privacy, unfollows, mutes
2 parents 3fcc4ac + a85599f commit 9b0d337

File tree

4 files changed

+86
-22
lines changed

4 files changed

+86
-22
lines changed

patches/@atproto+api+0.14.21.patch renamed to patches/@atproto+api+0.16.7.patch

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
diff --git a/node_modules/@atproto/api/dist/moderation/decision.js b/node_modules/@atproto/api/dist/moderation/decision.js
2-
index aaac177..d27c0be 100644
2+
index 6dd9d4d..59df896 100644
33
--- a/node_modules/@atproto/api/dist/moderation/decision.js
44
+++ b/node_modules/@atproto/api/dist/moderation/decision.js
5-
@@ -67,6 +67,8 @@ class ModerationDecision {
5+
@@ -67,6 +67,10 @@ class ModerationDecision {
66
ui(context) {
77
const ui = new ui_1.ModerationUI();
88
for (const cause of this.causes) {
9-
+ if (cause?.label?.val === '!no-unauthenticated') continue;
9+
+ // Skip !no-unauthenticated label only if user is authenticated
10+
+ // If userDid is present in opts, user is logged in
11+
+ if (cause?.label?.val === '!no-unauthenticated' && this.opts?.userDid) continue;
1012
+
1113
if (cause.type === 'blocking' ||
1214
cause.type === 'blocked-by' ||

src/state/queries/microcosm-fallback.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,26 +118,31 @@ export async function fetchConstellationCounts(
118118
*
119119
* IMPORTANT: This determines whether fallback should be triggered.
120120
* We should NOT trigger fallback for intentional blocking/privacy errors.
121+
*
122+
* SECURITY: We distinguish between:
123+
* - AppView failures (suspended users, server errors) → trigger fallback
124+
* - Access denials (privacy settings, blocking) → respect AppView decision
121125
*/
122126
export function isAppViewError(error: any): boolean {
123127
if (!error) return false
124128

125129
const msg = error.message?.toLowerCase() || ''
126130

127-
// Do NOT trigger fallback for intentional blocking
128-
// "Requester has blocked actor" means the user intentionally blocked someone
129-
// This is NOT an AppView outage - it's privacy enforcement
131+
// Do NOT trigger fallback for intentional blocking/privacy enforcement
132+
// These are NOT AppView outages - they are access control decisions
130133
if (msg.includes('blocked actor')) return false
131134
if (msg.includes('requester has blocked')) return false
132135
if (msg.includes('blocking')) return false
136+
if (msg.includes('not available')) return false // Privacy: logged-out visibility
137+
if (msg.includes('logged out')) return false // Privacy: requires authentication
138+
if (msg.includes('requires auth')) return false // Privacy: authentication required
139+
if (msg.includes('unauthorized')) return false // Privacy: access denied
133140

134-
// Check HTTP status codes
135-
if (error.status === 400 || error.status === 404) return true
136-
137-
// Check error messages for actual AppView issues
138-
if (msg.includes('not found')) return true
141+
// Trigger fallback for actual AppView failures (suspended users, etc.)
142+
// Note: We removed blanket 400/404 handling to avoid bypassing access controls
139143
if (msg.includes('suspended')) return true
140-
if (msg.includes('could not locate')) return true
144+
if (msg.includes('could not locate record')) return true
145+
if (msg.includes('profile not found')) return true
141146

142147
return false
143148
}

src/state/queries/profile.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,6 @@ import {
99
type ComAtprotoRepoUploadBlob,
1010
type Un$Typed,
1111
} from '@atproto/api'
12-
import {
13-
keepPreviousData,
14-
prefetchQueryWithFallback,
15-
type QueryClient,
16-
useMutation,
17-
useQuery,
18-
useQueryClient,
19-
} from './useQueryWithFallback'
2012

2113
import {uploadBlob} from '#/lib/api'
2214
import {until} from '#/lib/async/until'
@@ -42,6 +34,14 @@ import {
4234
import {RQKEY_ROOT as RQKEY_LIST_CONVOS} from './messages/list-conversations'
4335
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
4436
import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
37+
import {
38+
keepPreviousData,
39+
prefetchQueryWithFallback,
40+
type QueryClient,
41+
useMutation,
42+
useQuery,
43+
useQueryClient,
44+
} from './useQueryWithFallback'
4545

4646
export * from '#/state/queries/unstable-profile-cache'
4747
/**
@@ -354,11 +354,21 @@ function useProfileUnfollowMutation(
354354
logContext: LogEvents['profile:unfollow']['logContext'],
355355
) {
356356
const agent = useAgent()
357+
const queryClient = useQueryClient()
357358
return useMutation<void, Error, {did: string; followUri: string}>({
358359
mutationFn: async ({followUri}) => {
359360
logEvent('profile:unfollow', {logContext})
360361
return await agent.deleteFollow(followUri)
361362
},
363+
onSuccess(_, {did}) {
364+
// Invalidate profile and feed caches to reflect unfollow
365+
resetProfilePostsQueries(queryClient, did, 1000)
366+
367+
// Add delay to handle AppView indexing lag
368+
setTimeout(() => {
369+
queryClient.invalidateQueries({queryKey: RQKEY(did)})
370+
}, 500)
371+
},
362372
})
363373
}
364374

@@ -418,8 +428,21 @@ function useProfileMuteMutation() {
418428
mutationFn: async ({did}) => {
419429
await agent.mute(did)
420430
},
421-
onSuccess() {
431+
onSuccess(_, {did}) {
432+
// Invalidate mute list cache
422433
queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()})
434+
435+
// Invalidate profile and feed caches to reflect mute
436+
resetProfilePostsQueries(queryClient, did, 1000)
437+
queryClient.invalidateQueries({queryKey: RQKEY(did)})
438+
439+
// Invalidate feeds that might contain muted user's posts
440+
queryClient.invalidateQueries({queryKey: ['post-feed']})
441+
442+
// Add delay to handle AppView indexing lag
443+
setTimeout(() => {
444+
queryClient.invalidateQueries({queryKey: RQKEY(did)})
445+
}, 500)
423446
},
424447
})
425448
}
@@ -431,8 +454,21 @@ function useProfileUnmuteMutation() {
431454
mutationFn: async ({did}) => {
432455
await agent.unmute(did)
433456
},
434-
onSuccess() {
457+
onSuccess(_, {did}) {
458+
// Invalidate mute list cache
435459
queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()})
460+
461+
// Invalidate profile and feed caches to reflect unmute
462+
resetProfilePostsQueries(queryClient, did, 1000)
463+
queryClient.invalidateQueries({queryKey: RQKEY(did)})
464+
465+
// Invalidate feeds that might contain unmuted user's posts
466+
queryClient.invalidateQueries({queryKey: ['post-feed']})
467+
468+
// Add delay to handle AppView indexing lag
469+
setTimeout(() => {
470+
queryClient.invalidateQueries({queryKey: RQKEY(did)})
471+
}, 500)
436472
},
437473
})
438474
}

src/state/queries/useQueryWithFallback.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
type UseQueryResult,
1212
} from '@tanstack/react-query'
1313

14+
import {useSession} from '#/state/session'
1415
import {
1516
buildSyntheticFeedPage,
1617
buildSyntheticPostView,
@@ -109,6 +110,7 @@ export function useQuery<TData = unknown, TError = Error>(
109110
} = options
110111

111112
const queryClient = useQueryClient()
113+
const {hasSession} = useSession()
112114

113115
// Wrap the original queryFn with fallback logic
114116
const wrappedQueryFn: typeof queryFn = async context => {
@@ -123,6 +125,15 @@ export function useQuery<TData = unknown, TError = Error>(
123125
throw error
124126
}
125127

128+
// SECURITY: Do NOT trigger fallback for logged-out users
129+
// This prevents bypassing AppView access controls like logged-out visibility settings
130+
if (!hasSession) {
131+
console.log(
132+
'[Fallback] Skipping fallback for logged-out user (respecting access controls)',
133+
)
134+
throw error
135+
}
136+
126137
console.log('[Fallback] Attempting PDS + Microcosm fallback:', {
127138
fallbackType,
128139
fallbackIdentifier,
@@ -210,6 +221,7 @@ export function useInfiniteQuery<
210221
} = options
211222

212223
const queryClient = useQueryClient()
224+
const {hasSession} = useSession()
213225

214226
// Wrap the original queryFn with fallback logic
215227
const wrappedQueryFn = async (
@@ -226,6 +238,15 @@ export function useInfiniteQuery<
226238
throw error
227239
}
228240

241+
// SECURITY: Do NOT trigger fallback for logged-out users
242+
// This prevents bypassing AppView access controls like logged-out visibility settings
243+
if (!hasSession) {
244+
console.log(
245+
'[Fallback] Skipping fallback for logged-out user (respecting access controls)',
246+
)
247+
throw error
248+
}
249+
229250
const pageParam = 'pageParam' in context ? context.pageParam : undefined
230251

231252
console.log(

0 commit comments

Comments
 (0)