Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
117
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: preferences regression

also revert 7753fdafc171ed2f4d28023df24205c698b186d6 and 5d662082605f109391bebeb649a2f958aeefb120

+72 -260
+72 -249
src/state/queries/preferences/index.ts
··· 1 - import {useEffect, useMemo} from 'react' 1 + import {useCallback} from 'react' 2 2 import { 3 3 type AppBskyActorDefs, 4 4 type BskyFeedViewPreference, 5 - type BskyPreferences, 6 5 type LabelPreference, 7 6 } from '@atproto/api' 8 - import {TID} from '@atproto/common-web' 9 7 import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' 10 8 11 9 import {PROD_DEFAULT_FEED} from '#/lib/constants' ··· 22 20 type UsePreferencesQueryResponse, 23 21 } from '#/state/queries/preferences/types' 24 22 import {createQueryKey} from '#/state/queries/util' 25 - import {useBlankPrefAuthedAgent as useAgent} from '#/state/session' 23 + import {useAgent} from '#/state/session' 26 24 import {pdsAgent} from '#/state/session/agent' 27 25 import {saveLabelers} from '#/state/session/agent-config' 28 26 import {useAgeAssurance} from '#/ageAssurance' ··· 39 37 {persistedVersion: 1}, 40 38 ) 41 39 42 - /** 43 - * Some screens interpret missing prefs as "use defaults", which causes a 44 - * visible flicker when the preferences query briefly has no data. Retain the 45 - * last successful snapshot per account so those consumers stay stable. 46 - */ 47 - const lastKnownPreferencesByDid = new Map<string, UsePreferencesQueryResponse>() 48 - 49 - function normalizePreferences( 50 - res: BskyPreferences, 51 - ): UsePreferencesQueryResponse { 52 - return { 53 - ...res, 54 - savedFeeds: res.savedFeeds.filter(f => f.type !== 'unknown'), 55 - /** 56 - * Special preference, only used for following feed, previously 57 - * called `home` 58 - */ 59 - feedViewPrefs: { 60 - ...DEFAULT_HOME_FEED_PREFS, 61 - ...(res.feedViewPrefs.home || {}), 62 - }, 63 - threadViewPrefs: { 64 - ...DEFAULT_THREAD_VIEW_PREFS, 65 - ...(res.threadViewPrefs ?? {}), 66 - }, 67 - userAge: res.birthDate ? getAge(res.birthDate) : undefined, 68 - } 69 - } 70 - 71 - function ensureBirthDate( 72 - preferences: UsePreferencesQueryResponse, 73 - ): UsePreferencesQueryResponse { 74 - if (!preferences.birthDate || preferences.birthDate instanceof Date) { 75 - return preferences 76 - } 77 - return { 78 - ...preferences, 79 - birthDate: new Date(preferences.birthDate), 80 - } 81 - } 82 - 83 - function applyAgeAssurancePreferences( 84 - data: UsePreferencesQueryResponse, 85 - aa: ReturnType<typeof useAgeAssurance>, 86 - ) { 87 - /** 88 - * Prefs are all downstream of age assurance now. For logged-out 89 - * users, we override moderation prefs based on AA state. 90 - */ 91 - if (aa.state.access !== aa.Access.Full) { 92 - return { 93 - ...data, 94 - moderationPrefs: makeAgeRestrictedModerationPrefs(data.moderationPrefs), 95 - } 96 - } 97 - return data 98 - } 99 - 100 - type PreferencesMutationContext = { 101 - previousPreferences: UsePreferencesQueryResponse | undefined 102 - } 103 - 104 - function updateCachedPreferences( 105 - queryClient: ReturnType<typeof useQueryClient>, 106 - updater: (data: UsePreferencesQueryResponse) => UsePreferencesQueryResponse, 107 - ) { 108 - queryClient.setQueryData<UsePreferencesQueryResponse | undefined>( 109 - preferencesQueryKey, 110 - previous => (previous ? updater(previous) : previous), 111 - ) 112 - } 113 - 114 - async function mutateCachedPreferences( 115 - queryClient: ReturnType<typeof useQueryClient>, 116 - updater: (data: UsePreferencesQueryResponse) => UsePreferencesQueryResponse, 117 - ): Promise<PreferencesMutationContext> { 118 - await queryClient.cancelQueries({queryKey: preferencesQueryKey}) 119 - const previousPreferences = 120 - queryClient.getQueryData<UsePreferencesQueryResponse>(preferencesQueryKey) 121 - updateCachedPreferences(queryClient, updater) 122 - return {previousPreferences} 123 - } 124 - 125 - function restoreCachedPreferences( 126 - queryClient: ReturnType<typeof useQueryClient>, 127 - context: PreferencesMutationContext | undefined, 128 - ) { 129 - if (!context) return 130 - queryClient.setQueryData(preferencesQueryKey, context.previousPreferences) 131 - } 132 - 133 - function refetchPreferences(queryClient: ReturnType<typeof useQueryClient>) { 134 - void queryClient.invalidateQueries({ 135 - queryKey: preferencesQueryKey, 136 - }) 137 - } 138 - 139 40 export function usePreferencesQuery() { 140 41 const agent = useAgent() 141 42 const aa = useAgeAssurance() ··· 153 54 const res = await pdsAgent(agent).getPreferences() 154 55 155 56 // save to local storage to ensure there are labels on initial requests 156 - void saveLabelers( 57 + saveLabelers( 157 58 agent.did, 158 - res.moderationPrefs.labelers.map((l: {did: string}) => l.did), 59 + res.moderationPrefs.labelers.map(l => l.did), 159 60 ) 160 61 161 - return normalizePreferences(res) 62 + const preferences: UsePreferencesQueryResponse = { 63 + ...res, 64 + savedFeeds: res.savedFeeds.filter(f => f.type !== 'unknown'), 65 + /** 66 + * Special preference, only used for following feed, previously 67 + * called `home` 68 + */ 69 + feedViewPrefs: { 70 + ...DEFAULT_HOME_FEED_PREFS, 71 + ...(res.feedViewPrefs.home || {}), 72 + }, 73 + threadViewPrefs: { 74 + ...DEFAULT_THREAD_VIEW_PREFS, 75 + ...(res.threadViewPrefs ?? {}), 76 + }, 77 + userAge: res.birthDate ? getAge(res.birthDate) : undefined, 78 + } 79 + return preferences 162 80 } 163 81 }, 82 + select: useCallback( 83 + (data: UsePreferencesQueryResponse) => { 84 + /** 85 + * Prefs are all downstream of age assurance now. For logged-out 86 + * users, we override moderation prefs based on AA state. 87 + */ 88 + if (aa.state.access !== aa.Access.Full) { 89 + data = { 90 + ...data, 91 + moderationPrefs: makeAgeRestrictedModerationPrefs( 92 + data.moderationPrefs, 93 + ), 94 + } 95 + } 96 + return data 97 + }, 98 + [aa], 99 + ), 164 100 }) 165 101 166 - useEffect(() => { 167 - if (agent.did && query.data) { 168 - lastKnownPreferencesByDid.set(agent.did, ensureBirthDate(query.data)) 169 - } 170 - }, [agent.did, query.data]) 171 - 172 - const stableData = useMemo(() => { 173 - const data = 174 - query.data ?? 175 - (agent.did ? lastKnownPreferencesByDid.get(agent.did) : undefined) 176 - if (!data) { 177 - return data 178 - } 179 - return applyAgeAssurancePreferences(ensureBirthDate(data), aa) 180 - }, [aa, agent.did, query.data]) 181 - 182 - if (!stableData) { 183 - return query 102 + if (query.data?.birthDate) { 103 + /** 104 + * The persisted query cache stores dates as strings, but our code expects a `Date`. 105 + */ 106 + query.data.birthDate = new Date(query.data.birthDate) 184 107 } 185 108 186 - return { 187 - ...query, 188 - data: stableData, 189 - error: null, 190 - isError: false, 191 - isLoading: false, 192 - isPending: false, 193 - isSuccess: true, 194 - status: 'success', 195 - } 109 + return query 196 110 } 197 111 198 112 export function useClearPreferencesMutation() { ··· 315 229 const queryClient = useQueryClient() 316 230 const agent = useAgent() 317 231 318 - return useMutation< 319 - void, 320 - unknown, 321 - AppBskyActorDefs.SavedFeed[], 322 - PreferencesMutationContext 323 - >({ 324 - onMutate: savedFeeds => 325 - mutateCachedPreferences(queryClient, data => ({ 326 - ...data, 327 - savedFeeds, 328 - })), 329 - onError: (_error, _savedFeeds, context) => { 330 - restoreCachedPreferences(queryClient, context) 331 - }, 332 - onSettled: () => { 333 - refetchPreferences(queryClient) 334 - }, 232 + return useMutation<void, unknown, AppBskyActorDefs.SavedFeed[]>({ 335 233 mutationFn: async savedFeeds => { 336 234 await agent.overwriteSavedFeeds(savedFeeds) 235 + // triggers a refetch 236 + await queryClient.invalidateQueries({ 237 + queryKey: preferencesQueryKey, 238 + }) 337 239 }, 338 240 }) 339 241 } ··· 345 247 return useMutation< 346 248 void, 347 249 unknown, 348 - Pick<AppBskyActorDefs.SavedFeed, 'type' | 'value' | 'pinned'>[], 349 - PreferencesMutationContext 250 + Pick<AppBskyActorDefs.SavedFeed, 'type' | 'value' | 'pinned'>[] 350 251 >({ 351 - onMutate: savedFeeds => 352 - mutateCachedPreferences(queryClient, data => ({ 353 - ...data, 354 - savedFeeds: data.savedFeeds.concat( 355 - savedFeeds.map(savedFeed => ({ 356 - ...savedFeed, 357 - id: TID.nextStr(), 358 - })), 359 - ), 360 - })), 361 - onError: (_error, _savedFeeds, context) => { 362 - restoreCachedPreferences(queryClient, context) 363 - }, 364 - onSettled: () => { 365 - refetchPreferences(queryClient) 366 - }, 367 252 mutationFn: async savedFeeds => { 368 253 await agent.addSavedFeeds(savedFeeds) 254 + // triggers a refetch 255 + await queryClient.invalidateQueries({ 256 + queryKey: preferencesQueryKey, 257 + }) 369 258 }, 370 259 }) 371 260 } ··· 374 263 const queryClient = useQueryClient() 375 264 const agent = useAgent() 376 265 377 - return useMutation< 378 - void, 379 - unknown, 380 - Pick<AppBskyActorDefs.SavedFeed, 'id'>, 381 - PreferencesMutationContext 382 - >({ 383 - onMutate: savedFeed => 384 - mutateCachedPreferences(queryClient, data => ({ 385 - ...data, 386 - savedFeeds: data.savedFeeds.filter(feed => feed.id !== savedFeed.id), 387 - })), 388 - onError: (_error, _savedFeed, context) => { 389 - restoreCachedPreferences(queryClient, context) 390 - }, 391 - onSettled: () => { 392 - refetchPreferences(queryClient) 393 - }, 266 + return useMutation<void, unknown, Pick<AppBskyActorDefs.SavedFeed, 'id'>>({ 394 267 mutationFn: async savedFeed => { 395 268 await agent.removeSavedFeeds([savedFeed.id]) 269 + // triggers a refetch 270 + await queryClient.invalidateQueries({ 271 + queryKey: preferencesQueryKey, 272 + }) 396 273 }, 397 274 }) 398 275 } ··· 401 278 const queryClient = useQueryClient() 402 279 const agent = useAgent() 403 280 404 - return useMutation< 405 - void, 406 - unknown, 407 - { 408 - forYouFeedConfig: AppBskyActorDefs.SavedFeed | undefined 409 - discoverFeedConfig: AppBskyActorDefs.SavedFeed | undefined 410 - }, 411 - PreferencesMutationContext 412 - >({ 413 - onMutate: ({forYouFeedConfig, discoverFeedConfig}) => 414 - mutateCachedPreferences(queryClient, data => { 415 - let savedFeeds = data.savedFeeds 416 - 417 - if (forYouFeedConfig) { 418 - savedFeeds = savedFeeds.filter( 419 - feed => feed.id !== forYouFeedConfig.id, 420 - ) 421 - } 422 - 423 - if (!discoverFeedConfig) { 424 - savedFeeds = savedFeeds.concat({ 425 - type: 'feed', 426 - value: PROD_DEFAULT_FEED('whats-hot'), 427 - pinned: true, 428 - id: TID.nextStr(), 429 - }) 430 - } else { 431 - savedFeeds = savedFeeds.map(feed => 432 - feed.id === discoverFeedConfig.id ? {...feed, pinned: true} : feed, 433 - ) 434 - } 435 - 436 - return { 437 - ...data, 438 - savedFeeds, 439 - } 440 - }), 441 - onError: (_error, _variables, context) => { 442 - restoreCachedPreferences(queryClient, context) 443 - }, 444 - onSettled: () => { 445 - refetchPreferences(queryClient) 446 - }, 281 + return useMutation({ 447 282 mutationFn: async ({ 448 283 forYouFeedConfig, 449 284 discoverFeedConfig, ··· 470 305 }, 471 306 ]) 472 307 } 308 + // triggers a refetch 309 + await queryClient.invalidateQueries({ 310 + queryKey: preferencesQueryKey, 311 + }) 473 312 }, 474 313 }) 475 314 } ··· 478 317 const queryClient = useQueryClient() 479 318 const agent = useAgent() 480 319 481 - return useMutation< 482 - void, 483 - unknown, 484 - AppBskyActorDefs.SavedFeed[], 485 - PreferencesMutationContext 486 - >({ 487 - onMutate: feeds => 488 - mutateCachedPreferences(queryClient, data => { 489 - const nextById = new Map(feeds.map(feed => [feed.id, feed])) 490 - return { 491 - ...data, 492 - savedFeeds: data.savedFeeds.map( 493 - feed => nextById.get(feed.id) ?? feed, 494 - ), 495 - } 496 - }), 497 - onError: (_error, _feeds, context) => { 498 - restoreCachedPreferences(queryClient, context) 499 - }, 500 - onSettled: () => { 501 - refetchPreferences(queryClient) 502 - }, 320 + return useMutation<void, unknown, AppBskyActorDefs.SavedFeed[]>({ 503 321 mutationFn: async feeds => { 504 322 await agent.updateSavedFeeds(feeds) 323 + 324 + // triggers a refetch 325 + await queryClient.invalidateQueries({ 326 + queryKey: preferencesQueryKey, 327 + }) 505 328 }, 506 329 }) 507 330 }
-11
src/state/session/index.tsx
··· 481 481 } 482 482 return agent 483 483 } 484 - 485 - export function useBlankPrefAuthedAgent(): Agent { 486 - const agent = useContext(AgentContext) 487 - if (!agent) { 488 - throw Error('useAgent() must be below <SessionProvider>.') 489 - } 490 - 491 - return useMemo(() => { 492 - return (agent as BskyAppAgent | OauthBskyAppAgent).cloneWithoutProxy() 493 - }, [agent]) 494 - }