diff --git a/src/view/com/lightbox/Lightbox.web.tsx b/src/view/com/lightbox/Lightbox.web.tsx index 97811da7f..ab50fbcf0 100644 --- a/src/view/com/lightbox/Lightbox.web.tsx +++ b/src/view/com/lightbox/Lightbox.web.tsx @@ -76,6 +76,7 @@ function LightboxInner({ const onKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key === 'Escape') { + e.preventDefault() onClose() } else if (e.key === 'ArrowLeft') { onPressLeft() -- 2.51.2 From ea7f984a405c287f3fb50c151a78a6836e101347 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 19 Aug 2025 11:33:46 -0500 Subject: [PATCH 02/22] Fix isPartOfLastBranchFromDepth assignment (#8856) --- src/state/queries/usePostThread/traversal.ts | 11 +++++++++-- src/state/queries/usePostThread/types.ts | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) 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 /** -- 2.51.2 From bf6e01551e496e6737dc627f8587cd545d236776 Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Wed, 20 Aug 2025 02:38:26 +0000 Subject: [PATCH 03/22] Nightly source-language update --- src/locale/locales/en/messages.po | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 725d03495..94bb386ba 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -1905,7 +1905,7 @@ msgstr "" msgid "Close image" msgstr "" -#: src/view/com/lightbox/Lightbox.web.tsx:109 +#: src/view/com/lightbox/Lightbox.web.tsx:110 msgid "Close image viewer" msgstr "" @@ -3207,11 +3207,11 @@ msgstr "" msgid "Exits image cropping process" msgstr "" -#: src/view/com/lightbox/Lightbox.web.tsx:110 +#: src/view/com/lightbox/Lightbox.web.tsx:111 msgid "Exits image view" msgstr "" -#: src/view/com/lightbox/Lightbox.web.tsx:184 +#: src/view/com/lightbox/Lightbox.web.tsx:185 msgid "Expand alt text" msgstr "" @@ -4234,7 +4234,7 @@ msgstr "" msgid "I understand" msgstr "" -#: src/view/com/lightbox/Lightbox.web.tsx:186 +#: src/view/com/lightbox/Lightbox.web.tsx:187 msgid "If alt text is long, toggles alt text expanded state" msgstr "" @@ -5444,7 +5444,7 @@ msgstr "" msgid "Next" msgstr "" -#: src/view/com/lightbox/Lightbox.web.tsx:169 +#: src/view/com/lightbox/Lightbox.web.tsx:170 msgid "Next image" msgstr "" @@ -6451,7 +6451,7 @@ msgstr "" msgid "Press to view followers of this account that you also follow" msgstr "" -#: src/view/com/lightbox/Lightbox.web.tsx:150 +#: src/view/com/lightbox/Lightbox.web.tsx:151 msgid "Previous image" msgstr "" -- 2.51.2 From 9835d0fb7b3750d08b5535d7243420c48eb348fe Mon Sep 17 00:00:00 2001 From: surfdude29 <149612116+surfdude29@users.noreply.github.com> Date: Wed, 20 Aug 2025 20:10:34 +0200 Subject: [PATCH 04/22] tweak plural formatting (#8871) --- src/screens/PostThread/components/ThreadItemReadMore.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/screens/PostThread/components/ThreadItemReadMore.tsx b/src/screens/PostThread/components/ThreadItemReadMore.tsx index 22ae63395..66ec11cb7 100644 --- a/src/screens/PostThread/components/ThreadItemReadMore.tsx +++ b/src/screens/PostThread/components/ThreadItemReadMore.tsx @@ -90,10 +90,10 @@ export const ThreadItemReadMore = memo(function ThreadItemReadMore({ interacted && a.underline, ]}> - Read {item.moreReplies} more{' '} + Read{' '} -- 2.51.2 From ef7c222e1b3828a029834ec0229b7a23272ddcef Mon Sep 17 00:00:00 2001 From: cirx Date: Thu, 21 Aug 2025 03:11:02 +0900 Subject: [PATCH 05/22] fix typo (#8869) --- src/screens/Moderation/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx index e829abd47..983919c64 100644 --- a/src/screens/Moderation/index.tsx +++ b/src/screens/Moderation/index.tsx @@ -207,7 +207,7 @@ export function ModerationScreenInner({ Your declared age is under 18. Some settings below may be - disabled. If this was a mistake, you may edit your bithdate in + disabled. If this was a mistake, you may edit your birthdate in your{' '} Date: Wed, 20 Aug 2025 15:21:52 -0500 Subject: [PATCH 06/22] [APP-1398] Fix post shadow/like shadow state when replying (#8866) * Add optimisticReplyCount to post shadow * Add special util * Fix subtle bug in query cache --- src/state/cache/post-shadow.ts | 15 ++++++++++ src/state/queries/usePostThread/queryCache.ts | 29 ++++++++++++++++--- 2 files changed, 40 insertions(+), 4 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 > = 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 | 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/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) -- 2.51.2 From d900d0b7a79f2edfbd3865c2484694a0de61a35c Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Thu, 21 Aug 2025 02:38:38 +0000 Subject: [PATCH 07/22] Nightly source-language update --- src/locale/locales/en/messages.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 94bb386ba..f8031f13c 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -6651,7 +6651,7 @@ msgid "Reactivate your account" msgstr "" #: src/screens/PostThread/components/ThreadItemReadMore.tsx:92 -msgid "Read {0} more {1, plural, one {reply} other {replies}}" +msgid "Read {0, plural, one {# more reply} other {# more replies}}" msgstr "" #: src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx:158 @@ -10541,7 +10541,7 @@ msgid "Your current handle <0>{0} will automatically remain reserved for you msgstr "" #: src/screens/Moderation/index.tsx:208 -msgid "Your declared age is under 18. Some settings below may be disabled. If this was a mistake, you may edit your bithdate in your <0>account settings." +msgid "Your declared age is under 18. Some settings below may be disabled. If this was a mistake, you may edit your birthdate in your <0>account settings." msgstr "" #: src/components/ageAssurance/AgeAssuranceInitDialog.tsx:253 -- 2.51.2 From eabcd9150d3513988f5b3c47b95a601d5f1bf738 Mon Sep 17 00:00:00 2001 From: Caidan Date: Thu, 21 Aug 2025 11:56:17 -0700 Subject: [PATCH 08/22] [APP-1357] profile header follow recommendations (#8784) --- src/components/FeedInterstitials.tsx | 286 ++++++++------- src/components/ProfileCard.tsx | 18 + .../custom-animations/AccordionAnimation.tsx | 77 ++++ src/lib/statsig/gates.ts | 1 + .../Profile/Header/ProfileHeaderStandard.tsx | 329 +++++++++--------- src/screens/Profile/Header/Shell.tsx | 2 +- .../Profile/Header/SuggestedFollows.tsx | 45 +++ src/state/queries/suggested-follows.ts | 15 +- 8 files changed, 461 insertions(+), 312 deletions(-) create mode 100644 src/lib/custom-animations/AccordionAnimation.tsx create mode 100644 src/screens/Profile/Header/SuggestedFollows.tsx diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 18da12b22..07ad2d501 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -1,6 +1,5 @@ import React from 'react' -import {View} from 'react-native' -import {ScrollView} from 'react-native-gesture-handler' +import {ScrollView, View} from 'react-native' import {type AppBskyFeedDefs, AtUri} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -9,6 +8,7 @@ import {useNavigation} from '@react-navigation/native' import {type NavigationProp} from '#/lib/routes/types' import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' +import {isIOS} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useGetPopularFeedsQuery} from '#/state/queries/feed' import {type FeedDescriptor} from '#/state/queries/post-feed' @@ -25,7 +25,7 @@ import { type ViewStyleProp, web, } from '#/alf' -import {Button, ButtonText} from '#/components/Button' +import {Button} from '#/components/Button' import * as FeedCard from '#/components/FeedCard' import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' @@ -46,11 +46,13 @@ function CardOuter({ return ( + @@ -78,24 +77,15 @@ export function SuggestedFollowPlaceholder() { - + ) } export function SuggestedFeedsCardPlaceholder() { - const t = useTheme() return ( - + @@ -253,129 +243,133 @@ export function ProfileGrid({ profiles: bsky.profile.AnyProfileView[] recId?: number error: Error | null - viewContext: 'profile' | 'feed' + viewContext: 'profile' | 'profileHeader' | 'feed' }) { const t = useTheme() const {_} = useLingui() const moderationOpts = useModerationOpts() const {gtMobile} = useBreakpoints() + const isLoading = isSuggestionsLoading || !moderationOpts - const maxLength = gtMobile ? 3 : 6 + const isProfileHeaderContext = viewContext === 'profileHeader' + const isFeedContext = viewContext === 'feed' - const content = isLoading ? ( - Array(maxLength) - .fill(0) - .map((_, i) => ( - - - - )) - ) : error || !profiles.length ? null : ( - <> - {profiles.slice(0, maxLength).map((profile, index) => ( - { - logEvent('suggestedUser:press', { - logContext: - viewContext === 'feed' + const maxLength = gtMobile ? 3 : isProfileHeaderContext ? 12 : 6 + const minLength = gtMobile ? 3 : 4 + + const content = isLoading + ? Array(maxLength) + .fill(0) + .map((_, i) => ( + + + + )) + : error || !profiles.length + ? null + : profiles.slice(0, maxLength).map((profile, index) => ( + { + logEvent('suggestedUser:press', { + logContext: isFeedContext ? 'InterstitialDiscover' : 'InterstitialProfile', - recId, - position: index, - }) - }} - style={[ - a.flex_1, - gtMobile && - web([ - a.flex_0, - a.flex_grow, - {width: `calc(30% - ${a.gap_md.gap / 2}px)`}, - ]), - ]}> - {({hovered, pressed}) => ( - - - - - - + {({hovered, pressed}) => ( + + + + - + + + + - - - { - logEvent('suggestedUser:follow', { - logContext: - viewContext === 'feed' + + { + logEvent('suggestedUser:follow', { + logContext: isFeedContext ? 'InterstitialDiscover' : 'InterstitialProfile', - location: 'Card', - recId, - position: index, - }) - }} - /> - - - )} - - ))} - - ) + location: 'Card', + recId, + position: index, + }) + }} + /> + + + )} + + )) - if (error || (!isLoading && profiles.length < 4)) { + if (error || (!isLoading && profiles.length < minLength)) { logger.debug(`Not enough profiles to show suggested follows`) return null } return ( + style={[ + !isProfileHeaderContext && a.border_t, + t.atoms.border_contrast_low, + t.atoms.bg_contrast_25, + ]} + pointerEvents={isIOS ? 'auto' : 'box-none'}> + ]} + pointerEvents={isIOS ? 'auto' : 'box-none'}> - {viewContext === 'profile' ? ( - Similar accounts - ) : ( + {isFeedContext ? ( Suggested for you + ) : ( + Similar accounts )} - - See more - + {!isProfileHeaderContext && ( + + See more + + )} {gtMobile ? ( @@ -406,19 +403,16 @@ export function ProfileGrid({ ) : ( - - - - {content} - - - - - + + {content} + + {!isProfileHeaderContext && } + )} @@ -427,7 +421,6 @@ export function ProfileGrid({ function SeeMoreSuggestedProfilesCard() { const navigation = useNavigation() - const t = useTheme() const {_} = useLingui() return ( @@ -437,7 +430,7 @@ function SeeMoreSuggestedProfilesCard() { onPress={() => { navigation.navigate('SearchTab') }}> - + @@ -491,10 +484,7 @@ export function SuggestedFeeds() { }}> {({hovered, pressed}) => ( + style={[(hovered || pressed) && t.atoms.border_contrast_high]}> @@ -568,7 +558,7 @@ export function SuggestedFeeds() { navigation.navigate('SearchTab') }} style={[a.flex_col]}> - + diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index f12d922fd..5c99474a2 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -561,6 +561,24 @@ export function FollowButtonInner({ ) } +export function FollowButtonPlaceholder({style}: ViewStyleProp) { + const t = useTheme() + + return ( + + ) +} + export function Labels({ profile, moderationOpts, diff --git a/src/lib/custom-animations/AccordionAnimation.tsx b/src/lib/custom-animations/AccordionAnimation.tsx new file mode 100644 index 000000000..146735aa6 --- /dev/null +++ b/src/lib/custom-animations/AccordionAnimation.tsx @@ -0,0 +1,77 @@ +import { + type LayoutChangeEvent, + type StyleProp, + View, + type ViewStyle, +} from 'react-native' +import Animated, { + Easing, + FadeInUp, + FadeOutUp, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated' + +import {isIOS, isWeb} from '#/platform/detection' + +type AccordionAnimationProps = React.PropsWithChildren<{ + isExpanded: boolean + duration?: number + style?: StyleProp +}> + +function WebAccordion({ + isExpanded, + duration = 300, + style, + children, +}: AccordionAnimationProps) { + const heightValue = useSharedValue(0) + + const animatedStyle = useAnimatedStyle(() => { + const targetHeight = isExpanded ? heightValue.get() : 0 + return { + height: withTiming(targetHeight, { + duration, + easing: Easing.out(Easing.cubic), + }), + overflow: 'hidden', + } + }) + + const onLayout = (e: LayoutChangeEvent) => { + if (heightValue.get() === 0) { + heightValue.set(e.nativeEvent.layout.height) + } + } + + return ( + + {children} + + ) +} + +function MobileAccordion({ + isExpanded, + duration = 200, + style, + children, +}: AccordionAnimationProps) { + if (!isExpanded) return null + + return ( + + {children} + + ) +} + +export function AccordionAnimation(props: AccordionAnimationProps) { + return isWeb ? : +} diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 66134a462..8ec86c971 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -8,6 +8,7 @@ export type Gate = | 'handle_suggestions' | 'old_postonboarding' | 'onboarding_add_video_feed' + | 'post_follow_profile_suggested_accounts' | 'post_threads_v2_unspecced' | 'remove_show_latest_button' | 'test_gate_1' diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx index 2f61ba4df..1df35d5e0 100644 --- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx +++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx @@ -1,4 +1,4 @@ -import React, {memo, useMemo} from 'react' +import {memo, useCallback, useMemo, useState} from 'react' import {View} from 'react-native' import { type AppBskyActorDefs, @@ -40,6 +40,7 @@ import {EditProfileDialog} from './EditProfileDialog' import {ProfileHeaderHandle} from './Handle' import {ProfileHeaderMetrics} from './Metrics' import {ProfileHeaderShell} from './Shell' +import {AnimatedProfileHeaderSuggestedFollows} from './SuggestedFollows' interface Props { profile: AppBskyActorDefs.ProfileViewDetailed @@ -73,6 +74,7 @@ let ProfileHeaderStandard = ({ const [_queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) const unblockPromptControl = Prompt.usePromptControl() const requireAuth = useRequireAuth() + const [showSuggestedFollows, setShowSuggestedFollows] = useState(false) const isBlockedUser = profile.viewer?.blocking || profile.viewer?.blockedBy || @@ -81,6 +83,7 @@ let ProfileHeaderStandard = ({ const editProfileControl = useDialogControl() const onPressFollow = () => { + setShowSuggestedFollows(true) requireAuth(async () => { try { await queueFollow() @@ -102,6 +105,7 @@ let ProfileHeaderStandard = ({ } const onPressUnfollow = () => { + setShowSuggestedFollows(false) requireAuth(async () => { try { await queueUnfollow() @@ -122,7 +126,7 @@ let ProfileHeaderStandard = ({ }) } - const unblockAccount = React.useCallback(async () => { + const unblockAccount = useCallback(async () => { try { await queueUnblock() Toast.show(_(msg({message: 'Account unblocked', context: 'toast'}))) @@ -155,174 +159,185 @@ let ProfileHeaderStandard = ({ }, [profile]) return ( - - + <> + - {isMe ? ( - <> - - - - ) : profile.viewer?.blocking ? ( - profile.viewer?.blockingByList ? null : ( - - ) - ) : !profile.viewer?.blockedBy ? ( - <> - {hasSession && subscriptionsAllowed && ( - + {isMe ? ( + <> + + - )} - {hasSession && } - - + ) + ) : !profile.viewer?.blockedBy ? ( + <> + {hasSession && subscriptionsAllowed && ( + )} - - {profile.viewer?.following ? ( - Following - ) : profile.viewer?.followedBy ? ( - Follow Back - ) : ( - Follow + {hasSession && } + + - - ) : null} - - - - - - {sanitizeDisplayName( - profile.displayName || sanitizeHandle(profile.handle), - moderation.ui('displayName'), - )} - + {profile.viewer?.following ? ( + Following + ) : profile.viewer?.followedBy ? ( + Follow Back + ) : ( + Follow + )} + + + + ) : null} + + + + + - - - + {sanitizeDisplayName( + profile.displayName || sanitizeHandle(profile.handle), + moderation.ui('displayName'), + )} + + + + + + - - - {!isPlaceholderProfile && !isBlockedUser && ( - - - {descriptionRT && !moderation.ui('profileView').blur ? ( - - - - ) : undefined} - - {!isMe && - !isBlockedUser && - shouldShowKnownFollowers(profile.viewer?.knownFollowers) && ( - - + + {descriptionRT && !moderation.ui('profileView').blur ? ( + + - )} - - )} - - + + + )} + + )} + + + + + + - + ) } + ProfileHeaderStandard = memo(ProfileHeaderStandard) export {ProfileHeaderStandard} diff --git a/src/screens/Profile/Header/Shell.tsx b/src/screens/Profile/Header/Shell.tsx index 167be0aa8..cff0a707c 100644 --- a/src/screens/Profile/Header/Shell.tsx +++ b/src/screens/Profile/Header/Shell.tsx @@ -211,7 +211,7 @@ let ProfileHeaderShell = ({ {!isPlaceholderProfile && ( {isMe ? ( diff --git a/src/screens/Profile/Header/SuggestedFollows.tsx b/src/screens/Profile/Header/SuggestedFollows.tsx new file mode 100644 index 000000000..d005d888e --- /dev/null +++ b/src/screens/Profile/Header/SuggestedFollows.tsx @@ -0,0 +1,45 @@ +import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation' +import {useGate} from '#/lib/statsig/statsig' +import {isAndroid} from '#/platform/detection' +import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows' +import {ProfileGrid} from '#/components/FeedInterstitials' + +export function ProfileHeaderSuggestedFollows({actorDid}: {actorDid: string}) { + const {isLoading, data, error} = useSuggestedFollowsByActorQuery({ + did: actorDid, + }) + + return ( + + ) +} + +export function AnimatedProfileHeaderSuggestedFollows({ + isExpanded, + actorDid, +}: { + isExpanded: boolean + actorDid: string +}) { + const gate = useGate() + if (!gate('post_follow_profile_suggested_accounts')) return null + + /* NOTE (caidanw): + * Android does not work well with this feature yet. + * This issue stems from Android not allowing dragging on clickable elements in the profile header. + * Blocking the ability to scroll on Android is too much of a trade-off for now. + **/ + if (isAndroid) return null + + return ( + + + + ) +} 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({ -- 2.51.2 From b1ad229771103f9721e26a0a71b3890ec9d874d6 Mon Sep 17 00:00:00 2001 From: Caidan Williams Date: Wed, 20 Aug 2025 11:23:19 -0700 Subject: [PATCH 09/22] fix: show 'Follow back' label if user is followed by profile Updated ProfileCard.FollowButton label to display 'Follow back' when the current user is followed by the profile being viewed. --- src/components/ProfileCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index 5c99474a2..d3a2b8f1f 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -515,7 +515,7 @@ export function FollowButtonInner({ ) const followLabel = _( msg({ - message: 'Follow', + message: profile.viewer?.followedBy ? 'Follow back' : 'Follow', comment: 'User is not following this account, click to follow', }), ) -- 2.51.2 From c097f68a7d4116408bb848593346444a8260a16b Mon Sep 17 00:00:00 2001 From: Caidan Williams Date: Thu, 21 Aug 2025 12:53:35 -0700 Subject: [PATCH 10/22] refactor: extract ViewMoreCard from VideoCards component Moved the 'View more' card logic into a separate ViewMoreCard component for better separation of concerns and readability. Updated imports to use named React hooks instead of React namespace. To modernise this old component per the new guidelines. --- .../interstitials/TrendingVideos.tsx | 102 ++++++++++-------- 1 file changed, 55 insertions(+), 47 deletions(-) diff --git a/src/components/interstitials/TrendingVideos.tsx b/src/components/interstitials/TrendingVideos.tsx index 4d59e2fb5..4275f7fc8 100644 --- a/src/components/interstitials/TrendingVideos.tsx +++ b/src/components/interstitials/TrendingVideos.tsx @@ -1,4 +1,4 @@ -import React, {useEffect} from 'react' +import {useCallback, useEffect, useMemo} from 'react' import {ScrollView, View} from 'react-native' import {AppBskyEmbedVideo, AtUri} from '@atproto/api' import {msg, Trans} from '@lingui/macro' @@ -55,7 +55,7 @@ export function TrendingVideos() { const {setTrendingVideoDisabled} = useTrendingSettingsApi() const trendingPrompt = Prompt.usePromptControl() - const onConfirmHide = React.useCallback(() => { + const onConfirmHide = useCallback(() => { setTrendingVideoDisabled(true) logEvent('trendingVideos:hide', {context: 'interstitial:discover'}) }, [setTrendingVideoDisabled]) @@ -147,9 +147,7 @@ function VideoCards({ }: { data: Exclude['data'], undefined> }) { - const t = useTheme() - const {_} = useLingui() - const items = React.useMemo(() => { + const items = useMemo(() => { return data.pages .flatMap(page => page.slices) .map(slice => slice.items[0]) @@ -157,10 +155,6 @@ function VideoCards({ .filter(item => AppBskyEmbedVideo.isView(item.post.embed)) .slice(0, 8) }, [data]) - const href = React.useMemo(() => { - const urip = new AtUri(VIDEO_FEED_URI) - return makeCustomFeedLink(urip.host, urip.rkey, undefined, 'discover') - }, []) return ( <> @@ -183,50 +177,64 @@ function VideoCards({ ))} - - - {({pressed}) => ( + + + ) +} + +function ViewMoreCard() { + const t = useTheme() + const {_} = useLingui() + + const href = useMemo(() => { + const urip = new AtUri(VIDEO_FEED_URI) + return makeCustomFeedLink(urip.host, urip.rkey, undefined, 'discover') + }, []) + + return ( + + + {({pressed}) => ( + + + View more + - - View more - - - - + - )} - - - + + )} + + ) } -- 2.51.2 From 8ab49da6da0f6e253aa782808afa0a8940ccee54 Mon Sep 17 00:00:00 2001 From: Caidan Williams Date: Thu, 21 Aug 2025 13:23:53 -0700 Subject: [PATCH 11/22] refactor: replace View with Button in ViewMoreCard for consistent colors across themes Updated ViewMoreCard to use a Button component instead of a styled View for the 'View more' action. This improves accessibility and consistency with other interactive elements in TrendingVideos. --- .../interstitials/TrendingVideos.tsx | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/components/interstitials/TrendingVideos.tsx b/src/components/interstitials/TrendingVideos.tsx index 4275f7fc8..6be64335a 100644 --- a/src/components/interstitials/TrendingVideos.tsx +++ b/src/components/interstitials/TrendingVideos.tsx @@ -219,19 +219,13 @@ function ViewMoreCard() { View more - + )} -- 2.51.2 From 994003e6037e994dde3f96593ea6074ef5d68a61 Mon Sep 17 00:00:00 2001 From: Caidan Williams Date: Thu, 21 Aug 2025 13:53:44 -0700 Subject: [PATCH 12/22] fix: capitalization of "Follow back" to "Follow Back" --- src/components/ProfileCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index d3a2b8f1f..ea7333c2e 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -515,7 +515,7 @@ export function FollowButtonInner({ ) const followLabel = _( msg({ - message: profile.viewer?.followedBy ? 'Follow back' : 'Follow', + message: profile.viewer?.followedBy ? 'Follow Back' : 'Follow', comment: 'User is not following this account, click to follow', }), ) -- 2.51.2 From 7a1adfe67ccc90abc5ab594e8fd90e8cd0a03af0 Mon Sep 17 00:00:00 2001 From: Caidan Williams Date: Thu, 21 Aug 2025 15:55:01 -0700 Subject: [PATCH 13/22] refactor: all uses of "Follow Back" to "Follow back" --- src/components/ProfileCard.tsx | 2 +- src/screens/Profile/Header/ProfileHeaderStandard.tsx | 2 +- src/view/com/post-thread/PostThreadFollowBtn.tsx | 4 ++-- src/view/com/profile/FollowButton.tsx | 10 +++++----- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index ea7333c2e..d3a2b8f1f 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -515,7 +515,7 @@ export function FollowButtonInner({ ) const followLabel = _( msg({ - message: profile.viewer?.followedBy ? 'Follow Back' : 'Follow', + message: profile.viewer?.followedBy ? 'Follow back' : 'Follow', comment: 'User is not following this account, click to follow', }), ) diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx index 1df35d5e0..32111dd3b 100644 --- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx +++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx @@ -247,7 +247,7 @@ let ProfileHeaderStandard = ({ {profile.viewer?.following ? ( Following ) : profile.viewer?.followedBy ? ( - Follow Back + Follow back ) : ( Follow )} diff --git a/src/view/com/post-thread/PostThreadFollowBtn.tsx b/src/view/com/post-thread/PostThreadFollowBtn.tsx index 145e919f9..fc9296cad 100644 --- a/src/view/com/post-thread/PostThreadFollowBtn.tsx +++ b/src/view/com/post-thread/PostThreadFollowBtn.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {AppBskyActorDefs} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' @@ -126,7 +126,7 @@ function PostThreadFollowBtnLoaded({ {!isFollowing ? ( isFollowedBy ? ( - Follow Back + Follow back ) : ( Follow ) diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx index 656ed914a..ff9c1cd7b 100644 --- a/src/view/com/profile/FollowButton.tsx +++ b/src/view/com/profile/FollowButton.tsx @@ -1,11 +1,11 @@ -import {StyleProp, TextStyle, View} from 'react-native' +import {type StyleProp, type TextStyle, View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {Shadow} from '#/state/cache/types' +import {type Shadow} from '#/state/cache/types' import {useProfileFollowMutationQueue} from '#/state/queries/profile' -import * as bsky from '#/types/bsky' -import {Button, ButtonType} from '../util/forms/Button' +import type * as bsky from '#/types/bsky' +import {Button, type ButtonType} from '../util/forms/Button' import * as Toast from '../util/Toast' export function FollowButton({ @@ -78,7 +78,7 @@ export function FollowButton({ type={unfollowedType} labelStyle={labelStyle} onPress={onPressFollow} - label={_(msg({message: 'Follow Back', context: 'action'}))} + label={_(msg({message: 'Follow back', context: 'action'}))} /> ) } -- 2.51.2 From 0baa5198e067b08f281a2c0ee7916c6345f01c29 Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Fri, 22 Aug 2025 02:38:34 +0000 Subject: [PATCH 14/22] Nightly source-language update --- src/locale/locales/en/messages.po | 68 ++++++++++++++++--------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index f8031f13c..c08ea29e8 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -602,7 +602,7 @@ msgstr "" msgid "Account removed from quick access" msgstr "" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:128 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:132 #: src/view/com/profile/ProfileMenu.tsx:148 msgctxt "toast" msgid "Account unblocked" @@ -1280,7 +1280,7 @@ msgid "Birthday" msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:753 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:320 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:328 #: src/view/com/profile/ProfileMenu.tsx:473 msgid "Block" msgstr "" @@ -1449,20 +1449,20 @@ msgstr "" msgid "Books" msgstr "" -#: src/components/FeedInterstitials.tsx:435 +#: src/components/FeedInterstitials.tsx:428 msgid "Browse more accounts on the Explore page" msgstr "" -#: src/components/FeedInterstitials.tsx:566 +#: src/components/FeedInterstitials.tsx:556 msgid "Browse more feeds on the Explore page" msgstr "" -#: src/components/FeedInterstitials.tsx:547 -#: src/components/FeedInterstitials.tsx:550 +#: src/components/FeedInterstitials.tsx:537 +#: src/components/FeedInterstitials.tsx:540 msgid "Browse more suggestions" msgstr "" -#: src/components/FeedInterstitials.tsx:575 +#: src/components/FeedInterstitials.tsx:565 msgid "Browse more suggestions on the Explore page" msgstr "" @@ -2906,12 +2906,12 @@ msgstr "" #: src/screens/Profile/Header/EditProfileDialog.tsx:276 #: src/screens/Profile/Header/EditProfileDialog.tsx:282 #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:183 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:185 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:190 msgid "Edit profile" msgstr "" #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:186 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:188 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:193 msgid "Edit Profile" msgstr "" @@ -3630,7 +3630,7 @@ msgstr "" #: src/components/ProfileCard.tsx:517 #: src/components/ProfileHoverCard/index.web.tsx:496 #: src/components/ProfileHoverCard/index.web.tsx:507 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:245 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:252 #: src/screens/VideoFeed/index.tsx:851 #: src/view/com/post-thread/PostThreadFollowBtn.tsx:131 msgid "Follow" @@ -3641,7 +3641,7 @@ msgctxt "action" msgid "Follow" msgstr "" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:230 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:237 #: src/view/com/post-thread/PostThreadFollowBtn.tsx:113 msgid "Follow {0}" msgstr "" @@ -3669,7 +3669,7 @@ msgstr "" msgid "Follow all" msgstr "" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:243 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:250 #: src/view/com/post-thread/PostThreadFollowBtn.tsx:129 msgid "Follow Back" msgstr "" @@ -3708,7 +3708,7 @@ msgstr "" #: src/components/ProfileCard.tsx:511 #: src/components/ProfileHoverCard/index.web.tsx:495 #: src/components/ProfileHoverCard/index.web.tsx:506 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:241 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:248 #: src/screens/VideoFeed/index.tsx:849 #: src/view/com/post-thread/PostThreadFollowBtn.tsx:134 msgid "Following" @@ -3721,7 +3721,7 @@ msgid "Following" msgstr "" #: src/components/ProfileCard.tsx:474 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:89 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:92 msgid "Following {0}" msgstr "" @@ -5479,7 +5479,7 @@ msgid "No likes yet" msgstr "" #: src/components/ProfileCard.tsx:496 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:110 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:114 msgid "No longer following {0}" msgstr "" @@ -7432,15 +7432,15 @@ msgstr "" msgid "See jobs at Bluesky" msgstr "" -#: src/components/FeedInterstitials.tsx:397 +#: src/components/FeedInterstitials.tsx:393 msgid "See more" msgstr "" -#: src/components/FeedInterstitials.tsx:444 +#: src/components/FeedInterstitials.tsx:437 msgid "See more accounts you might like" msgstr "" -#: src/components/FeedInterstitials.tsx:395 +#: src/components/FeedInterstitials.tsx:391 msgid "See more suggested profiles on the Explore page" msgstr "" @@ -8023,7 +8023,7 @@ msgstr "" msgid "Signed in as @{0}" msgstr "" -#: src/components/FeedInterstitials.tsx:389 +#: src/components/FeedInterstitials.tsx:386 msgid "Similar accounts" msgstr "" @@ -8053,7 +8053,7 @@ msgstr "" msgid "Some of your verifications are invalid." msgstr "" -#: src/components/FeedInterstitials.tsx:529 +#: src/components/FeedInterstitials.tsx:519 msgid "Some other feeds you might like" msgstr "" @@ -8285,7 +8285,7 @@ msgstr "" msgid "Suggested Accounts" msgstr "" -#: src/components/FeedInterstitials.tsx:391 +#: src/components/FeedInterstitials.tsx:384 msgid "Suggested for you" msgstr "" @@ -8475,7 +8475,7 @@ msgstr "" msgid "That's everything!" msgstr "" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:316 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:324 #: src/view/com/profile/ProfileMenu.tsx:461 msgid "The account will be able to interact with you after unblocking." msgstr "" @@ -8645,9 +8645,9 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:361 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:374 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:384 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:98 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:119 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:132 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:101 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:123 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:136 #: src/view/com/post-thread/PostThreadFollowBtn.tsx:90 #: src/view/com/post-thread/PostThreadFollowBtn.tsx:101 #: src/view/com/profile/ProfileMenu.tsx:128 @@ -9095,14 +9095,14 @@ msgstr "" #: src/components/dms/MessagesListBlockedFooter.tsx:104 #: src/components/dms/MessagesListBlockedFooter.tsx:112 #: src/components/dms/MessagesListBlockedFooter.tsx:119 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:203 -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:320 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:208 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:328 #: src/view/com/profile/ProfileMenu.tsx:473 #: src/view/screens/ProfileList.tsx:723 msgid "Unblock" msgstr "" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:208 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:213 msgctxt "action" msgid "Unblock" msgstr "" @@ -9114,7 +9114,7 @@ msgstr "" msgid "Unblock account" msgstr "" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:314 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:322 #: src/view/com/profile/ProfileMenu.tsx:455 msgid "Unblock Account?" msgstr "" @@ -9138,7 +9138,7 @@ msgctxt "action" msgid "Unfollow" msgstr "" -#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:229 +#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:236 msgid "Unfollow {0}" msgstr "" @@ -9674,13 +9674,17 @@ msgstr "" msgid "View information about these labels" msgstr "" -#: src/components/interstitials/TrendingVideos.tsx:189 -#: src/components/interstitials/TrendingVideos.tsx:211 +#: src/components/interstitials/TrendingVideos.tsx:198 +#: src/components/interstitials/TrendingVideos.tsx:220 #: src/screens/Search/modules/ExploreTrendingVideos.tsx:194 #: src/screens/Search/modules/ExploreTrendingVideos.tsx:213 msgid "View more" msgstr "" +#: src/components/interstitials/TrendingVideos.tsx:226 +msgid "View more trending videos" +msgstr "" + #: src/components/ProfileHoverCard/index.web.tsx:466 #: src/components/ProfileHoverCard/index.web.tsx:486 #: src/components/ProfileHoverCard/index.web.tsx:513 -- 2.51.2 From f038ac70da530416161e870fbbbd42159cea2bbe Mon Sep 17 00:00:00 2001 From: surfdude29 <149612116+surfdude29@users.noreply.github.com> Date: Fri, 22 Aug 2025 21:00:00 +0200 Subject: [PATCH 15/22] rearrange translation logic (#8880) --- src/components/ProfileCard.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index d3a2b8f1f..095b62167 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -513,12 +513,19 @@ export function FollowButtonInner({ comment: 'User is following this account, click to unfollow', }), ) - const followLabel = _( - msg({ - message: profile.viewer?.followedBy ? 'Follow back' : 'Follow', - comment: 'User is not following this account, click to follow', - }), - ) + const followLabel = profile.viewer?.followedBy + ? _( + msg({ + message: 'Follow back', + comment: 'User is not following this account, click to follow back', + }), + ) + : _( + msg({ + message: 'Follow', + comment: 'User is not following this account, click to follow', + }), + ) if (!profile.viewer) return null if ( -- 2.51.2 From 912ab1bd9b771cf14c830203332f3620e661a752 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 22 Aug 2025 14:55:16 -0500 Subject: [PATCH 16/22] [LEG-246] Geo overlay (#8881) * Add AgeBlockedGeo * Add MaxMind usage text * Add geo overlay --------- Co-authored-by: rafael --- bskyweb/cmd/bskyweb/server.go | 4 +- src/components/BlockedGeoOverlay.tsx | 109 +++++++++++++++++++++++++++ src/components/Link.tsx | 88 ++++++++++++++++++++- src/components/icons/Logo.tsx | 37 +++++++++ src/logger/metrics.ts | 5 ++ src/state/geolocation.tsx | 2 + src/storage/schema.ts | 1 + src/view/shell/index.tsx | 18 +++-- src/view/shell/index.web.tsx | 27 ++++--- 9 files changed, 269 insertions(+), 22 deletions(-) create mode 100644 src/components/BlockedGeoOverlay.tsx diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 4208eea2d..89cd112cd 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -606,10 +606,10 @@ type IPCCRequest struct { type IPCCResponse struct { CC string `json:"countryCode"` AgeRestrictedGeo bool `json:"isAgeRestrictedGeo,omitempty"` + AgeBlockedGeo bool `json:"isAgeBlockedGeo,omitempty"` } -// IP address data is powered by IPinfo -// https://ipinfo.io +// This product includes GeoLite2 Data created by MaxMind, available from https://www.maxmind.com. func (srv *Server) WebIpCC(c echo.Context) error { realIP := c.RealIP() addr, err := netip.ParseAddr(realIP) diff --git a/src/components/BlockedGeoOverlay.tsx b/src/components/BlockedGeoOverlay.tsx new file mode 100644 index 000000000..ae5790da9 --- /dev/null +++ b/src/components/BlockedGeoOverlay.tsx @@ -0,0 +1,109 @@ +import {useEffect} from 'react' +import {ScrollView, View} from 'react-native' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {logger} from '#/logger' +import {isWeb} from '#/platform/detection' +import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import {Full as Logo, Mark} from '#/components/icons/Logo' +import {SimpleInlineLinkText as InlineLinkText} from '#/components/Link' +import {Text} from '#/components/Typography' + +export function BlockedGeoOverlay() { + const t = useTheme() + const {_} = useLingui() + const {gtPhone} = useBreakpoints() + const insets = useSafeAreaInsets() + + useEffect(() => { + // just counting overall hits here + logger.metric(`blockedGeoOverlay:shown`, {}) + }, []) + + const textStyles = [a.text_md, a.leading_normal] + const links = { + blog: { + to: `https://bsky.social/about/blog/08-22-2025-mississippi-hb1126`, + label: _(msg`Read our blog post`), + overridePresentation: false, + disableMismatchWarning: true, + style: textStyles, + }, + } + + const blocks = [ + _(msg`Unfortunately, Bluesky is unavailable in Mississippi right now.`), + _( + msg`A new Mississippi law requires us to implement age verification for all users before they can access Bluesky. We think this law creates challenges that go beyond its child safety goals, and creates significant barriers that limit free speech and disproportionately harm smaller platforms and emerging technologies.`, + ), + _( + msg`As a small team, we cannot justify building the expensive infrastructure this requirement demands while legal challenges to this law are pending.`, + ), + _( + msg`For now, we have made the difficult decision to block access to Bluesky in the state of Mississippi.`, + ), + <> + To learn more, read our{' '} + blog post. + , + ] + + return ( + + + + + + + Announcement + + + + + + {blocks.map((block, index) => ( + + {block} + + ))} + + + + + + ) +} diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 6954be6a8..421a7fe9d 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -1,5 +1,5 @@ import React, {useMemo} from 'react' -import {type GestureResponderEvent} from 'react-native' +import {type GestureResponderEvent, Linking} from 'react-native' import {sanitizeUrl} from '@braintree/sanitize-url' import { type LinkProps as RNLinkProps, @@ -13,6 +13,7 @@ import {type AllNavigatorParams, type RouteParams} from '#/lib/routes/types' import {shareUrl} from '#/lib/sharing' import { convertBskyAppUrlIfNeeded, + createProxiedUrl, isBskyDownloadUrl, isExternalUrl, linkRequiresWarning, @@ -407,6 +408,91 @@ export function InlineLinkText({ ) } +/** + * A barebones version of `InlineLinkText`, for use outside a + * `react-navigation` context. + */ +export function SimpleInlineLinkText({ + children, + to, + style, + download, + selectable, + label, + disableUnderline, + shouldProxy, + ...rest +}: Omit< + InlineLinkProps, + | 'to' + | 'action' + | 'disableMismatchWarning' + | 'overridePresentation' + | 'onPress' + | 'onLongPress' + | 'shareOnLongPress' +> & { + to: string +}) { + const t = useTheme() + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() + const flattenedStyle = flatten(style) || {} + const isExternal = isExternalUrl(to) + + let href = to + if (shouldProxy) { + href = createProxiedUrl(href) + } + + const onPress = () => { + Linking.openURL(href) + } + + return ( + + {children} + + ) +} + export function WebOnlyInlineLinkText({ children, to, diff --git a/src/components/icons/Logo.tsx b/src/components/icons/Logo.tsx index 6f16d8a44..75c5cb420 100644 --- a/src/components/icons/Logo.tsx +++ b/src/components/icons/Logo.tsx @@ -1,5 +1,42 @@ +import Svg, {Path} from 'react-native-svg' + +import {type Props, useCommonSVGProps} from './common' import {createSinglePathSVG} from './TEMPLATE' export const Mark = createSinglePathSVG({ path: 'M6.335 4.212c2.293 1.76 4.76 5.327 5.665 7.241.906-1.914 3.372-5.482 5.665-7.241C19.319 2.942 22 1.96 22 5.086c0 .624-.35 5.244-.556 5.994-.713 2.608-3.315 3.273-5.629 2.87 4.045.704 5.074 3.035 2.852 5.366-4.22 4.426-6.066-1.111-6.54-2.53-.086-.26-.126-.382-.127-.278 0-.104-.041.018-.128.278-.473 1.419-2.318 6.956-6.539 2.53-2.222-2.331-1.193-4.662 2.852-5.366-2.314.403-4.916-.262-5.63-2.87C2.35 10.33 2 5.71 2 5.086c0-3.126 2.68-2.144 4.335-.874Z', }) + +export function Full( + props: Omit & { + markFill?: Props['fill'] + textFill?: Props['fill'] + }, +) { + const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) + const ratio = 123 / 555 + + return ( + + {gradient} + + + + ) +} diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index 0c9ea1ef6..4a09d8593 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -475,4 +475,9 @@ export type MetricEvents = { 'ageAssurance:redirectDialogFail': {} 'ageAssurance:appealDialogOpen': {} 'ageAssurance:appealDialogSubmit': {} + + /* + * Specifically for the `BlockedGeoOverlay` + */ + 'blockedGeoOverlay:shown': {} } diff --git a/src/state/geolocation.tsx b/src/state/geolocation.tsx index 4581996a0..a69161324 100644 --- a/src/state/geolocation.tsx +++ b/src/state/geolocation.tsx @@ -25,6 +25,7 @@ const onGeolocationUpdate = ( */ export const DEFAULT_GEOLOCATION: Device['geolocation'] = { countryCode: undefined, + isAgeBlockedGeo: undefined, isAgeRestrictedGeo: false, } @@ -40,6 +41,7 @@ async function getGeolocation(): Promise { if (json.countryCode) { return { countryCode: json.countryCode, + isAgeBlockedGeo: json.isAgeBlockedGeo ?? false, isAgeRestrictedGeo: json.isAgeRestrictedGeo ?? false, } } else { diff --git a/src/storage/schema.ts b/src/storage/schema.ts index 421264ac1..a3f2336cf 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -10,6 +10,7 @@ export type Device = { geolocation?: { countryCode: string | undefined isAgeRestrictedGeo: boolean | undefined + isAgeBlockedGeo: boolean | undefined } trendingBetaEnabled: boolean devMode: boolean diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 04fccc44c..8b4c65b8f 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -13,6 +13,7 @@ import {useNotificationsRegistration} from '#/lib/notifications/notifications' import {isStateAtTabRoot} from '#/lib/routes/helpers' import {isAndroid, isIOS} from '#/platform/detection' import {useDialogFullyExpandedCountContext} from '#/state/dialogs' +import {useGeolocation} from '#/state/geolocation' import {useSession} from '#/state/session' import { useIsDrawerOpen, @@ -26,6 +27,7 @@ import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {atoms as a, select, useTheme} from '#/alf' import {setSystemUITheme} from '#/alf/util/systemUI' import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog' +import {BlockedGeoOverlay} from '#/components/BlockedGeoOverlay' import {EmailDialog} from '#/components/dialogs/EmailDialog' import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsent' import {LinkWarningDialog} from '#/components/dialogs/LinkWarning' @@ -180,9 +182,11 @@ function ShellInner() { ) } -export const Shell: React.FC = function ShellImpl() { - const fullyExpandedCount = useDialogFullyExpandedCountContext() +export function Shell() { const t = useTheme() + const {geolocation} = useGeolocation() + const fullyExpandedCount = useDialogFullyExpandedCountContext() + useIntentHandler() useEffect(() => { @@ -200,9 +204,13 @@ export const Shell: React.FC = function ShellImpl() { navigationBar: t.name !== 'light' ? 'light' : 'dark', }} /> - - - + {geolocation?.isAgeBlockedGeo ? ( + + ) : ( + + + + )} ) } diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 3c2bc58ab..f942ab49e 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -5,11 +5,10 @@ import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {RemoveScrollBar} from 'react-remove-scroll-bar' -import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' import {useIntentHandler} from '#/lib/hooks/useIntentHandler' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {type NavigationProp} from '#/lib/routes/types' -import {colors} from '#/lib/styles' +import {useGeolocation} from '#/state/geolocation' import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell' import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut' import {useCloseAllActiveElements} from '#/state/util' @@ -18,6 +17,7 @@ import {ModalsContainer} from '#/view/com/modals/Modal' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {atoms as a, select, useTheme} from '#/alf' import {AgeAssuranceRedirectDialog} from '#/components/ageAssurance/AgeAssuranceRedirectDialog' +import {BlockedGeoOverlay} from '#/components/BlockedGeoOverlay' import {EmailDialog} from '#/components/dialogs/EmailDialog' import {LinkWarningDialog} from '#/components/dialogs/LinkWarning' import {MutedWordsDialog} from '#/components/dialogs/MutedWords' @@ -130,24 +130,23 @@ function ShellInner() { ) } -export const Shell: React.FC = function ShellImpl() { - const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark) +export function Shell() { + const t = useTheme() + const {geolocation} = useGeolocation() return ( - - - - + + {geolocation?.isAgeBlockedGeo ? ( + + ) : ( + + + + )} ) } const styles = StyleSheet.create({ - bgLight: { - backgroundColor: colors.white, - }, - bgDark: { - backgroundColor: colors.black, // TODO - }, drawerMask: { ...a.fixed, width: '100%', -- 2.51.2 From 184df9cbfef582180c5765d8158bbbb249558f8a Mon Sep 17 00:00:00 2001 From: Chenyu Huang Date: Fri, 22 Aug 2025 13:28:32 -0700 Subject: [PATCH 17/22] wrapp failed to send feed interactions inside isNetworkError --- src/state/feed-feedback.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 -- 2.51.2 From 5120c037d1be6310d1d7aa7552435b4ef3d01807 Mon Sep 17 00:00:00 2001 From: Caidan Williams Date: Fri, 22 Aug 2025 14:39:56 -0700 Subject: [PATCH 18/22] feat: add canonical URL to home page for SEO optimization --- bskyweb/templates/home.html | 1 + 1 file changed, 1 insertion(+) diff --git a/bskyweb/templates/home.html b/bskyweb/templates/home.html index 5d3fbad85..9e5c670a2 100644 --- a/bskyweb/templates/home.html +++ b/bskyweb/templates/home.html @@ -12,6 +12,7 @@ + -- 2.51.2 From 560c503aa61d0c03b52549c31610845cd1ecd36c Mon Sep 17 00:00:00 2001 From: Caidan Williams Date: Fri, 22 Aug 2025 14:40:57 -0700 Subject: [PATCH 19/22] feat: add canonical URL to post template for better SEO --- bskyweb/templates/post.html | 1 + 1 file changed, 1 insertion(+) diff --git a/bskyweb/templates/post.html b/bskyweb/templates/post.html index f6f03c7ee..963af2065 100644 --- a/bskyweb/templates/post.html +++ b/bskyweb/templates/post.html @@ -14,6 +14,7 @@ {%- if requestURI %} + {% endif -%} {%- if postView.Author.DisplayName %} -- 2.51.2 From d8c904d1d92c9685d42ac537f6a07a610b98eb43 Mon Sep 17 00:00:00 2001 From: Caidan Williams Date: Fri, 22 Aug 2025 14:42:05 -0700 Subject: [PATCH 20/22] feat: add canonical URL to profile template for better SEO --- bskyweb/templates/profile.html | 1 + 1 file changed, 1 insertion(+) diff --git a/bskyweb/templates/profile.html b/bskyweb/templates/profile.html index ab84dd157..af4180dc1 100644 --- a/bskyweb/templates/profile.html +++ b/bskyweb/templates/profile.html @@ -15,6 +15,7 @@ {%- if requestURI %} + {% endif -%} {%- if profileView.DisplayName %} -- 2.51.2 From 7b9bc52720c090bc7bdf0d4636d0227a13abfedd Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Sat, 23 Aug 2025 02:36:29 +0000 Subject: [PATCH 21/22] Nightly source-language update --- src/locale/locales/en/messages.po | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index c08ea29e8..d17dca0a0 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -524,6 +524,10 @@ msgstr "" msgid "A new form of verification" msgstr "" +#: src/components/BlockedGeoOverlay.tsx:39 +msgid "A new Mississippi law requires us to implement age verification for all users before they can access Bluesky. We think this law creates challenges that go beyond its child safety goals, and creates significant barriers that limit free speech and disproportionately harm smaller platforms and emerging technologies." +msgstr "" + #: src/components/dialogs/nuxs/ActivitySubscriptions.tsx:113 msgid "A screenshot of a profile page with a bell icon next to the follow button, indicating the new activity notifications feature." msgstr "" @@ -1012,6 +1016,7 @@ msgstr "" msgid "Animated GIF" msgstr "" +#: src/components/BlockedGeoOverlay.tsx:92 #: src/components/PolicyUpdateOverlay/Badge.tsx:33 msgid "Announcement" msgstr "" @@ -1190,6 +1195,10 @@ msgstr "" msgid "Artistic or non-erotic nudity." msgstr "" +#: src/components/BlockedGeoOverlay.tsx:42 +msgid "As a small team, we cannot justify building the expensive infrastructure this requirement demands while legal challenges to this law are pending." +msgstr "" + #: src/components/PostControls/PostMenu/PostMenuItems.tsx:491 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:493 msgid "Assign topic for algo" @@ -3627,7 +3636,7 @@ msgid "Flexible" msgstr "" #. User is not following this account, click to follow -#: src/components/ProfileCard.tsx:517 +#: src/components/ProfileCard.tsx:524 #: src/components/ProfileHoverCard/index.web.tsx:496 #: src/components/ProfileHoverCard/index.web.tsx:507 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:252 @@ -3669,14 +3678,16 @@ msgstr "" msgid "Follow all" msgstr "" +#. User is not following this account, click to follow back +#: src/components/ProfileCard.tsx:518 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:250 #: src/view/com/post-thread/PostThreadFollowBtn.tsx:129 -msgid "Follow Back" +msgid "Follow back" msgstr "" #: src/view/com/profile/FollowButton.tsx:81 msgctxt "action" -msgid "Follow Back" +msgid "Follow back" msgstr "" #: src/components/KnownFollowers.tsx:238 @@ -3760,6 +3771,10 @@ msgstr "" msgid "Food" msgstr "" +#: src/components/BlockedGeoOverlay.tsx:45 +msgid "For now, we have made the difficult decision to block access to Bluesky in the state of Mississippi." +msgstr "" + #: src/view/com/modals/DeleteAccount.tsx:125 msgid "For security reasons, we'll need to send a confirmation code to your email address." msgstr "" @@ -6671,6 +6686,7 @@ msgstr "" msgid "Read more replies" msgstr "" +#: src/components/BlockedGeoOverlay.tsx:29 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:112 msgid "Read our blog post" msgstr "" @@ -9151,6 +9167,10 @@ msgstr "" msgid "Unfollows the user" msgstr "" +#: src/components/BlockedGeoOverlay.tsx:37 +msgid "Unfortunately, Bluesky is unavailable in Mississippi right now." +msgstr "" + #: src/components/moderation/ReportDialog/index.tsx:372 msgid "Unfortunately, none of your subscribed labelers supports this report type." msgstr "" -- 2.51.2 From 3b7b93a411a6096fa5e5665f105118dc29973d94 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Sat, 23 Aug 2025 14:45:57 -0500 Subject: [PATCH 22/22] Debug (#8888) * Swap URL * Add debug --- src/logger/metrics.ts | 2 ++ src/state/geolocation.tsx | 54 ++++++++++++++++++++++++++++++++++----- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index 4a09d8593..e51905f84 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -480,4 +480,6 @@ export type MetricEvents = { * Specifically for the `BlockedGeoOverlay` */ 'blockedGeoOverlay:shown': {} + + 'geo:debug': {} } diff --git a/src/state/geolocation.tsx b/src/state/geolocation.tsx index a69161324..6229c6f2c 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://bapp-config.bsky.workers.dev/config` + const events = new EventEmitter() const EVENT = 'geolocation-updated' const emitGeolocationUpdate = (geolocation: Device['geolocation']) => { @@ -29,8 +32,18 @@ export const DEFAULT_GEOLOCATION: Device['geolocation'] = { isAgeRestrictedGeo: false, } -async function getGeolocation(): Promise { - 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 { + const res = await fetch(url) if (!res.ok) { throw new Error(`geolocation: lookup failed ${res.status}`) @@ -43,12 +56,39 @@ async function getGeolocation(): Promise { 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. */ @@ -81,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`) @@ -101,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`)