diff options
Diffstat (limited to 'src/state')
-rw-r--r-- | src/state/cache/post-shadow.ts | 15 | ||||
-rw-r--r-- | src/state/feed-feedback.tsx | 5 | ||||
-rw-r--r-- | src/state/geolocation.tsx | 56 | ||||
-rw-r--r-- | src/state/queries/suggested-follows.ts | 15 | ||||
-rw-r--r-- | src/state/queries/usePostThread/queryCache.ts | 29 | ||||
-rw-r--r-- | src/state/queries/usePostThread/traversal.ts | 11 | ||||
-rw-r--r-- | src/state/queries/usePostThread/types.ts | 4 |
7 files changed, 114 insertions, 21 deletions
diff --git a/src/state/cache/post-shadow.ts b/src/state/cache/post-shadow.ts index d7f1eb8b9..8cc3dca1a 100644 --- a/src/state/cache/post-shadow.ts +++ b/src/state/cache/post-shadow.ts @@ -24,6 +24,7 @@ export interface PostShadow { isDeleted: boolean embed: AppBskyEmbedRecord.View | AppBskyEmbedRecordWithMedia.View | undefined pinned: boolean + optimisticReplyCount: number | undefined } export const POST_TOMBSTONE = Symbol('PostTombstone') @@ -34,6 +35,14 @@ const shadows: WeakMap< Partial<PostShadow> > = new WeakMap() +/** + * Use with caution! This function returns the raw shadow data for a post. + * Prefer using `usePostShadow`. + */ +export function dangerousGetPostShadow(post: AppBskyFeedDefs.PostView) { + return shadows.get(post) +} + export function usePostShadow( post: AppBskyFeedDefs.PostView, ): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE { @@ -95,6 +104,11 @@ function mergeShadow( repostCount = Math.max(0, repostCount) } + let replyCount = post.replyCount ?? 0 + if ('optimisticReplyCount' in shadow) { + replyCount = shadow.optimisticReplyCount ?? replyCount + } + let embed: typeof post.embed if ('embed' in shadow) { if ( @@ -112,6 +126,7 @@ function mergeShadow( embed: embed || post.embed, likeCount: likeCount, repostCount: repostCount, + replyCount: replyCount, viewer: { ...(post.viewer || {}), like: 'likeUri' in shadow ? shadow.likeUri : post.viewer?.like, diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index ee381259d..8b235f492 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -11,6 +11,7 @@ import {type AppBskyFeedDefs} from '@atproto/api' import throttle from 'lodash.throttle' import {FEEDBACK_FEEDS, STAGING_FEEDS} from '#/lib/constants' +import {isNetworkError} from '#/lib/hooks/useCleanError' import {logEvent} from '#/lib/statsig/statsig' import {Logger} from '#/logger' import { @@ -83,7 +84,9 @@ export function useFeedFeedback( }, ) .catch((e: any) => { - logger.warn('Failed to send feed interactions', {error: e}) + if (!isNetworkError(e)) { + logger.warn('Failed to send feed interactions', {error: e}) + } }) // Send to Statsig diff --git a/src/state/geolocation.tsx b/src/state/geolocation.tsx index 4581996a0..c4d8cb946 100644 --- a/src/state/geolocation.tsx +++ b/src/state/geolocation.tsx @@ -5,6 +5,9 @@ import {networkRetry} from '#/lib/async/retry' import {logger} from '#/logger' import {type Device, device} from '#/storage' +const IPCC_URL = `https://bsky.app/ipcc` +const BAPP_CONFIG_URL = `https://ip.bsky.app/config` + const events = new EventEmitter() const EVENT = 'geolocation-updated' const emitGeolocationUpdate = (geolocation: Device['geolocation']) => { @@ -25,11 +28,22 @@ const onGeolocationUpdate = ( */ export const DEFAULT_GEOLOCATION: Device['geolocation'] = { countryCode: undefined, + isAgeBlockedGeo: undefined, isAgeRestrictedGeo: false, } -async function getGeolocation(): Promise<Device['geolocation']> { - const res = await fetch(`https://bsky.app/ipcc`) +function sanitizeGeolocation( + geolocation: Device['geolocation'], +): Device['geolocation'] { + return { + countryCode: geolocation?.countryCode ?? undefined, + isAgeBlockedGeo: geolocation?.isAgeBlockedGeo ?? false, + isAgeRestrictedGeo: geolocation?.isAgeRestrictedGeo ?? false, + } +} + +async function getGeolocation(url: string): Promise<Device['geolocation']> { + const res = await fetch(url) if (!res.ok) { throw new Error(`geolocation: lookup failed ${res.status}`) @@ -40,13 +54,41 @@ async function getGeolocation(): Promise<Device['geolocation']> { if (json.countryCode) { return { countryCode: json.countryCode, + isAgeBlockedGeo: json.isAgeBlockedGeo ?? false, isAgeRestrictedGeo: json.isAgeRestrictedGeo ?? false, + // @ts-ignore + regionCode: json.regionCode ?? undefined, } } else { return undefined } } +async function compareWithIPCC(bapp: Device['geolocation']) { + try { + const ipcc = await getGeolocation(IPCC_URL) + + if (!ipcc || !bapp) return + + logger.metric( + 'geo:debug', + { + bappCountryCode: bapp.countryCode, + // @ts-ignore + bappRegionCode: bapp.regionCode, + bappIsAgeBlockedGeo: bapp.isAgeBlockedGeo, + bappIsAgeRestrictedGeo: bapp.isAgeRestrictedGeo, + ipccCountryCode: ipcc.countryCode, + ipccIsAgeBlockedGeo: ipcc.isAgeBlockedGeo, + ipccIsAgeRestrictedGeo: ipcc.isAgeRestrictedGeo, + }, + { + statsig: false, + }, + ) + } catch {} +} + /** * Local promise used within this file only. */ @@ -79,11 +121,12 @@ export function beginResolveGeolocation() { try { // Try once, fail fast - const geolocation = await getGeolocation() + const geolocation = await getGeolocation(BAPP_CONFIG_URL) if (geolocation) { - device.set(['geolocation'], geolocation) + device.set(['geolocation'], sanitizeGeolocation(geolocation)) emitGeolocationUpdate(geolocation) logger.debug(`geolocation: success`, {geolocation}) + compareWithIPCC(geolocation) } else { // endpoint should throw on all failures, this is insurance throw new Error(`geolocation: nothing returned from initial request`) @@ -99,13 +142,14 @@ export function beginResolveGeolocation() { device.set(['geolocation'], DEFAULT_GEOLOCATION) // retry 3 times, but don't await, proceed with default - networkRetry(3, getGeolocation) + networkRetry(3, () => getGeolocation(BAPP_CONFIG_URL)) .then(geolocation => { if (geolocation) { - device.set(['geolocation'], geolocation) + device.set(['geolocation'], sanitizeGeolocation(geolocation)) emitGeolocationUpdate(geolocation) logger.debug(`geolocation: success`, {geolocation}) success = true + compareWithIPCC(geolocation) } else { // endpoint should throw on all failures, this is insurance throw new Error(`geolocation: nothing returned from retries`) diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts index 0a2343150..c7a6e5f75 100644 --- a/src/state/queries/suggested-follows.ts +++ b/src/state/queries/suggested-follows.ts @@ -1,13 +1,13 @@ import { - AppBskyActorDefs, - AppBskyActorGetSuggestions, - AppBskyGraphGetSuggestedFollowsByActor, + type AppBskyActorDefs, + type AppBskyActorGetSuggestions, + type AppBskyGraphGetSuggestedFollowsByActor, moderateProfile, } from '@atproto/api' import { - InfiniteData, - QueryClient, - QueryKey, + type InfiniteData, + type QueryClient, + type QueryKey, useInfiniteQuery, useQuery, } from '@tanstack/react-query' @@ -106,12 +106,15 @@ export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) { export function useSuggestedFollowsByActorQuery({ did, enabled, + staleTime = STALE.MINUTES.FIVE, }: { did: string enabled?: boolean + staleTime?: number }) { const agent = useAgent() return useQuery({ + staleTime, queryKey: suggestedFollowsByActorQueryKey(did), queryFn: async () => { const res = await agent.app.bsky.graph.getSuggestedFollowsByActor({ diff --git a/src/state/queries/usePostThread/queryCache.ts b/src/state/queries/usePostThread/queryCache.ts index 826932349..5e27ebb87 100644 --- a/src/state/queries/usePostThread/queryCache.ts +++ b/src/state/queries/usePostThread/queryCache.ts @@ -9,6 +9,10 @@ import { } from '@atproto/api' import {type QueryClient} from '@tanstack/react-query' +import { + dangerousGetPostShadow, + updatePostShadow, +} from '#/state/cache/post-shadow' import {findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData} from '#/state/queries/explore-feed-previews' import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed' import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed' @@ -85,10 +89,27 @@ export function createCacheMutator({ /* * Update parent data */ - parent.value.post = { - ...parent.value.post, - replyCount: (parent.value.post.replyCount || 0) + 1, - } + const shadow = dangerousGetPostShadow(parent.value.post) + const prevOptimisticCount = shadow?.optimisticReplyCount + const prevReplyCount = parent.value.post.replyCount + // prefer optimistic count, if we already have some + const currentReplyCount = + (prevOptimisticCount ?? prevReplyCount ?? 0) + 1 + + /* + * We must update the value in the query cache in order for thread + * traversal to properly compute required metadata. + */ + parent.value.post.replyCount = currentReplyCount + + /** + * Additionally, we need to update the post shadow to keep track of + * these new values, since mutating the post object above does not + * cause a re-render. + */ + updatePostShadow(queryClient, parent.value.post.uri, { + optimisticReplyCount: currentReplyCount, + }) const opDid = getRootPostAtUri(parent.value.post)?.host const nextPreexistingItem = thread.at(i + 1) diff --git a/src/state/queries/usePostThread/traversal.ts b/src/state/queries/usePostThread/traversal.ts index 2809d32e9..2e7693fab 100644 --- a/src/state/queries/usePostThread/traversal.ts +++ b/src/state/queries/usePostThread/traversal.ts @@ -307,9 +307,16 @@ export function sortAndAnnotateThreadItems( metadata.isPartOfLastBranchFromDepth = metadata.depth /** - * If the parent is part of the last branch of the sub-tree, so is the child. + * If the parent is part of the last branch of the sub-tree, so + * is the child. However, if the child is also a last sibling, + * then we need to start tracking `isPartOfLastBranchFromDepth` + * from this point onwards, always updating it to the depth of + * the last sibling as we go down. */ - if (metadata.parentMetadata.isPartOfLastBranchFromDepth) { + if ( + !metadata.isLastSibling && + metadata.parentMetadata.isPartOfLastBranchFromDepth + ) { metadata.isPartOfLastBranchFromDepth = metadata.parentMetadata.isPartOfLastBranchFromDepth } diff --git a/src/state/queries/usePostThread/types.ts b/src/state/queries/usePostThread/types.ts index 2f370b0ab..5df7c2e42 100644 --- a/src/state/queries/usePostThread/types.ts +++ b/src/state/queries/usePostThread/types.ts @@ -151,8 +151,8 @@ export type TraversalMetadata = { */ isLastChild: boolean /** - * Indicates if the post is the left/lower-most branch of the reply tree. - * Value corresponds to the depth at which this branch started. + * Indicates if the post is the left-most AND lower-most branch of the reply + * tree. Value corresponds to the depth at which this branch started. */ isPartOfLastBranchFromDepth?: number /** |