diff --git a/package.json b/package.json index 01955b7905..d4526287d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.105.0", + "version": "1.106.0", "private": true, "engines": { "node": ">=20" diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index c28adad8b3..6d1cf73f7e 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -3,11 +3,11 @@ import {View} from 'react-native' import {Image} from 'expo-image' import {LinearGradient} from 'expo-linear-gradient' import { - AppBskyActorDefs, + type AppBskyActorDefs, AppBskyEmbedVideo, - AppBskyFeedDefs, + type AppBskyFeedDefs, AppBskyFeedPost, - ModerationDecision, + type ModerationDecision, } from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -15,7 +15,7 @@ import {useLingui} from '@lingui/react' import {sanitizeHandle} from '#/lib/strings/handles' import {formatCount} from '#/view/com/util/numeric/format' import {UserAvatar} from '#/view/com/util/UserAvatar' -import {VideoFeedSourceContext} from '#/screens/VideoFeed/types' +import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types' import {atoms as a, useTheme} from '#/alf' import {BLUE_HUE} from '#/alf/util/colorGeneration' import {select} from '#/alf/util/themeSelector' @@ -390,6 +390,7 @@ export function CompactVideoPostCard({ if (!AppBskyEmbedVideo.isView(embed)) return null const likeCount = post?.likeCount ?? 0 + const showLikeCount = false const {thumbnail} = embed const black = getBlackColor(t) @@ -475,47 +476,51 @@ export function CompactVideoPostCard({ /> - + + style={[a.relative, a.rounded_full, {width: 24, height: 24}]}> - - + {showLikeCount && ( - {likeCount > 0 && ( - - - - {formatCount(i18n, likeCount)} - - - )} + style={[ + a.absolute, + a.inset_0, + a.pt_2xl, + { + top: 'auto', + }, + ]}> + + + + {likeCount > 0 && ( + + + + {formatCount(i18n, likeCount)} + + + )} + - + )} diff --git a/src/components/dialogs/EmailDialog/data/useAccountEmailState.ts b/src/components/dialogs/EmailDialog/data/useAccountEmailState.ts index 377411107a..f25369f8db 100644 --- a/src/components/dialogs/EmailDialog/data/useAccountEmailState.ts +++ b/src/components/dialogs/EmailDialog/data/useAccountEmailState.ts @@ -1,7 +1,7 @@ -import {useCallback, useEffect, useState} from 'react' -import {useQuery, useQueryClient} from '@tanstack/react-query' +import {useEffect, useMemo, useState} from 'react' +import {useQuery} from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAgent, useSessionApi} from '#/state/session' import {emitEmailVerified} from '#/components/dialogs/EmailDialog/events' export type AccountEmailState = { @@ -11,57 +11,36 @@ export type AccountEmailState = { export const accountEmailStateQueryKey = ['accountEmailState'] as const -export function useInvalidateAccountEmailState() { - const qc = useQueryClient() - - return useCallback(() => { - return qc.invalidateQueries({ - queryKey: accountEmailStateQueryKey, - }) - }, [qc]) -} - -export function useUpdateAccountEmailStateQueryCache() { - const qc = useQueryClient() - - return useCallback( - (data: AccountEmailState) => { - return qc.setQueriesData( - { - queryKey: accountEmailStateQueryKey, - }, - data, - ) - }, - [qc], - ) -} - export function useAccountEmailState() { const agent = useAgent() + const {partialRefreshSession} = useSessionApi() const [prevIsEmailVerified, setPrevEmailIsVerified] = useState( !!agent.session?.emailConfirmed, ) - const fallbackData: AccountEmailState = { - isEmailVerified: !!agent.session?.emailConfirmed, - email2FAEnabled: !!agent.session?.emailAuthFactor, - } - const query = useQuery({ + const state: AccountEmailState = useMemo( + () => ({ + isEmailVerified: !!agent.session?.emailConfirmed, + email2FAEnabled: !!agent.session?.emailAuthFactor, + }), + [agent.session], + ) + + /** + * Only here to refetch on focus, when necessary + */ + useQuery({ enabled: !!agent.session, - refetchOnWindowFocus: true, + /** + * Only refetch if the email verification s incomplete. + */ + refetchOnWindowFocus: !prevIsEmailVerified, queryKey: accountEmailStateQueryKey, queryFn: async () => { - // will also trigger updates to `#/state/session` data - const {data} = await agent.resumeSession(agent.session!) - return { - isEmailVerified: !!data.emailConfirmed, - email2FAEnabled: !!data.emailAuthFactor, - } + await partialRefreshSession() + return null }, }) - const state = query.data ?? fallbackData - /* * This will emit `n` times for each instance of this hook. So the listeners * all use `once` to prevent multiple handlers firing. diff --git a/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts b/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts index 73f824fcc6..475a8cbfb6 100644 --- a/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts +++ b/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts @@ -1,13 +1,10 @@ import {useMutation} from '@tanstack/react-query' import {useAgent, useSession} from '#/state/session' -import {useUpdateAccountEmailStateQueryCache} from '#/components/dialogs/EmailDialog/data/useAccountEmailState' export function useConfirmEmail() { const agent = useAgent() const {currentAccount} = useSession() - const updateAccountEmailStateQueryCache = - useUpdateAccountEmailStateQueryCache() return useMutation({ mutationFn: async ({token}: {token: string}) => { @@ -19,11 +16,8 @@ export function useConfirmEmail() { email: currentAccount.email, token: token.trim(), }) - const {data} = await agent.resumeSession(agent.session!) - updateAccountEmailStateQueryCache({ - isEmailVerified: !!data.emailConfirmed, - email2FAEnabled: !!data.emailAuthFactor, - }) + // will update session state at root of app + await agent.resumeSession(agent.session!) }, }) } diff --git a/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts b/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts index 39f5fd2d9e..358bf86544 100644 --- a/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts +++ b/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts @@ -1,13 +1,10 @@ import {useMutation} from '@tanstack/react-query' import {useAgent, useSession} from '#/state/session' -import {useUpdateAccountEmailStateQueryCache} from '#/components/dialogs/EmailDialog/data/useAccountEmailState' export function useManageEmail2FA() { const agent = useAgent() const {currentAccount} = useSession() - const updateAccountEmailStateQueryCache = - useUpdateAccountEmailStateQueryCache() return useMutation({ mutationFn: async ({ @@ -25,11 +22,8 @@ export function useManageEmail2FA() { emailAuthFactor: enabled, token, }) - const {data} = await agent.resumeSession(agent.session!) - updateAccountEmailStateQueryCache({ - isEmailVerified: !!data.emailConfirmed, - email2FAEnabled: !!data.emailAuthFactor, - }) + // will update session state at root of app + await agent.resumeSession(agent.session!) }, }) } diff --git a/src/lib/api/feed/home.ts b/src/lib/api/feed/home.ts index e6bc45bea0..7a0d72d915 100644 --- a/src/lib/api/feed/home.ts +++ b/src/lib/api/feed/home.ts @@ -1,9 +1,9 @@ -import {AppBskyFeedDefs, BskyAgent} from '@atproto/api' +import {type AppBskyFeedDefs, type BskyAgent} from '@atproto/api' import {PROD_DEFAULT_FEED} from '#/lib/constants' import {CustomFeedAPI} from './custom' import {FollowingFeedAPI} from './following' -import {FeedAPI, FeedAPIResponse} from './types' +import {type FeedAPI, type FeedAPIResponse} from './types' // HACK // the feed API does not include any facilities for passing down @@ -93,7 +93,7 @@ export class HomeFeedAPI implements FeedAPI { } } - if (this.usingDiscover) { + if (this.usingDiscover && !__DEV__) { const res = await this.discover.fetch({cursor, limit}) returnCursor = res.cursor posts = posts.concat(res.feed) diff --git a/src/lib/custom-animations/GestureActionView.tsx b/src/lib/custom-animations/GestureActionView.tsx index ba6952a81d..e7fba570bc 100644 --- a/src/lib/custom-animations/GestureActionView.tsx +++ b/src/lib/custom-animations/GestureActionView.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {ColorValue, Dimensions, StyleSheet, View} from 'react-native' +import {type ColorValue, Dimensions, StyleSheet, View} from 'react-native' import {Gesture, GestureDetector} from 'react-native-gesture-handler' import Animated, { clamp, @@ -114,11 +114,16 @@ export function GestureActionView({ }, ) + // NOTE(haileyok): + // Absurdly high value so it doesn't interfere with the pan gestures above (i.e., scroll) + // reanimated doesn't offer great support for disabling y/x axes :/ + const effectivelyDisabledOffset = 200 const panGesture = Gesture.Pan() - .activeOffsetX([-10, 10]) - // Absurdly high value so it doesn't interfere with the pan gestures above (i.e., scroll) - // reanimated doesn't offer great support for disabling y/x axes :/ - .activeOffsetY([-200, 200]) + .activeOffsetX([ + actions.leftFirst ? -10 : -effectivelyDisabledOffset, + actions.rightFirst ? 10 : effectivelyDisabledOffset, + ]) + .activeOffsetY([-effectivelyDisabledOffset, effectivelyDisabledOffset]) .onStart(() => { 'worklet' isActive.set(true) diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index efd7d605a0..3b1106480d 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -1,6 +1,5 @@ export type Gate = // Keep this alphabetic please. - | 'age_assurance' | 'alt_share_icon' | 'debug_show_feedcontext' | 'debug_subscriptions' diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index f509f2980a..f2d3ffca9b 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -8,6 +8,7 @@ import {logger} from '#/logger' import {type MetricEvents} from '#/logger/metrics' import {isWeb} from '#/platform/detection' import * as persisted from '#/state/persisted' +import packageDotJson from '../../../package.json' import {useSession} from '../../state/session' import {timeout} from '../async/timeout' import {useNonReactiveCallback} from '../hooks/useNonReactiveCallback' @@ -25,6 +26,7 @@ type StatsigUser = { // This is the place where we can add our own stuff. // Fields here have to be non-optional to be visible in the UI. platform: 'ios' | 'android' | 'web' + appVersion: string bundleIdentifier: string bundleDate: number refSrc: string @@ -210,6 +212,7 @@ function toStatsigUser(did: string | undefined): StatsigUser { refSrc, refUrl, platform: Platform.OS as 'ios' | 'android' | 'web', + appVersion: packageDotJson.version, bundleIdentifier: BUNDLE_IDENTIFIER, bundleDate: BUNDLE_DATE, appLanguage: languagePrefs.appLanguage, diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 3013c54b63..098665dbe7 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -671,7 +671,7 @@ msgstr "" msgid "Add another account" msgstr "" -#: src/view/com/composer/Composer.tsx:773 +#: src/view/com/composer/Composer.tsx:776 msgid "Add another post" msgstr "" @@ -702,7 +702,7 @@ msgstr "" msgid "Add muted words and tags" msgstr "" -#: src/view/com/composer/Composer.tsx:1335 +#: src/view/com/composer/Composer.tsx:1340 msgid "Add new post" msgstr "" @@ -1139,11 +1139,11 @@ msgstr "" msgid "Are you sure you want to remove this from your feeds?" msgstr "" -#: src/view/com/composer/Composer.tsx:724 +#: src/view/com/composer/Composer.tsx:725 msgid "Are you sure you'd like to discard this draft?" msgstr "" -#: src/view/com/composer/Composer.tsx:905 +#: src/view/com/composer/Composer.tsx:910 msgid "Are you sure you'd like to discard this post?" msgstr "" @@ -1522,8 +1522,8 @@ msgstr "" #: src/screens/Settings/Settings.tsx:283 #: src/screens/Takendown.tsx:99 #: src/screens/Takendown.tsx:102 -#: src/view/com/composer/Composer.tsx:960 -#: src/view/com/composer/Composer.tsx:971 +#: src/view/com/composer/Composer.tsx:965 +#: src/view/com/composer/Composer.tsx:976 #: src/view/com/composer/photos/EditImageDialog.web.tsx:43 #: src/view/com/composer/photos/EditImageDialog.web.tsx:52 #: src/view/com/modals/ChangePassword.tsx:279 @@ -1887,7 +1887,7 @@ msgstr "" msgid "Closes password update alert" msgstr "" -#: src/view/com/composer/Composer.tsx:968 +#: src/view/com/composer/Composer.tsx:973 msgid "Closes post composer and discards post draft" msgstr "" @@ -1944,7 +1944,7 @@ msgstr "" msgid "Compose new post" msgstr "" -#: src/view/com/composer/Composer.tsx:869 +#: src/view/com/composer/Composer.tsx:874 msgid "Compose posts up to {0, plural, other {# characters}} in length" msgstr "" @@ -1952,7 +1952,7 @@ msgstr "" msgid "Compose reply" msgstr "" -#: src/view/com/composer/Composer.tsx:1729 +#: src/view/com/composer/Composer.tsx:1734 msgid "Compressing video..." msgstr "" @@ -2477,7 +2477,7 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:682 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:684 -#: src/view/com/composer/Composer.tsx:879 +#: src/view/com/composer/Composer.tsx:884 msgid "Delete post" msgstr "" @@ -2601,8 +2601,8 @@ msgid "Disabled" msgstr "" #: src/screens/Profile/Header/EditProfileDialog.tsx:88 -#: src/view/com/composer/Composer.tsx:726 -#: src/view/com/composer/Composer.tsx:912 +#: src/view/com/composer/Composer.tsx:727 +#: src/view/com/composer/Composer.tsx:917 msgid "Discard" msgstr "" @@ -2610,11 +2610,11 @@ msgstr "" msgid "Discard changes?" msgstr "" -#: src/view/com/composer/Composer.tsx:723 +#: src/view/com/composer/Composer.tsx:724 msgid "Discard draft?" msgstr "" -#: src/view/com/composer/Composer.tsx:904 +#: src/view/com/composer/Composer.tsx:909 msgid "Discard post?" msgstr "" @@ -2640,7 +2640,7 @@ msgstr "" msgid "Dismiss" msgstr "" -#: src/view/com/composer/Composer.tsx:1653 +#: src/view/com/composer/Composer.tsx:1658 msgid "Dismiss error" msgstr "" @@ -3098,7 +3098,7 @@ msgstr "" msgid "Entertainment" msgstr "" -#: src/view/com/composer/Composer.tsx:1738 +#: src/view/com/composer/Composer.tsx:1743 #: src/view/com/util/error/ErrorScreen.tsx:42 msgid "Error" msgstr "" @@ -3682,12 +3682,6 @@ msgstr "" msgid "Followers you know" msgstr "" -#: src/view/screens/Feeds.tsx:603 -#: src/view/screens/SavedFeeds.tsx:420 -msgctxt "feed-name" -msgid "Following" -msgstr "" - #. User is following this account, click to unfollow #: src/components/ProfileCard.tsx:484 #: src/components/ProfileHoverCard/index.web.tsx:493 @@ -3698,6 +3692,12 @@ msgstr "" msgid "Following" msgstr "" +#: src/view/screens/Feeds.tsx:603 +#: src/view/screens/SavedFeeds.tsx:420 +msgctxt "feed-name" +msgid "Following" +msgstr "" + #: src/components/ProfileCard.tsx:447 #: src/screens/Profile/Header/ProfileHeaderStandard.tsx:89 msgid "Following {0}" @@ -4039,7 +4039,7 @@ msgid "Here is your app password!" msgstr "" #: src/components/VideoPostCard.tsx:178 -#: src/components/VideoPostCard.tsx:454 +#: src/components/VideoPostCard.tsx:455 msgid "Hidden" msgstr "" @@ -4425,7 +4425,7 @@ msgstr "" msgid "It's just you right now! Add more people to your starter pack by searching above." msgstr "" -#: src/view/com/composer/Composer.tsx:1672 +#: src/view/com/composer/Composer.tsx:1677 msgid "Job ID: {0}" msgstr "" @@ -5761,7 +5761,7 @@ msgid "Open drawer menu" msgstr "" #: src/screens/Messages/components/MessageInput.web.tsx:181 -#: src/view/com/composer/Composer.tsx:1320 +#: src/view/com/composer/Composer.tsx:1325 msgid "Open emoji picker" msgstr "" @@ -5864,7 +5864,7 @@ msgstr "" msgid "Opens device photo gallery" msgstr "" -#: src/view/com/composer/Composer.tsx:1321 +#: src/view/com/composer/Composer.tsx:1326 msgid "Opens emoji picker" msgstr "" @@ -6275,12 +6275,12 @@ msgctxt "description" msgid "Post" msgstr "" -#: src/view/com/composer/Composer.tsx:1031 +#: src/view/com/composer/Composer.tsx:1036 msgctxt "action" msgid "Post" msgstr "" -#: src/view/com/composer/Composer.tsx:1029 +#: src/view/com/composer/Composer.tsx:1034 msgctxt "action" msgid "Post All" msgstr "" @@ -6453,7 +6453,7 @@ msgstr "" msgid "Privacy Policy" msgstr "" -#: src/view/com/composer/Composer.tsx:1735 +#: src/view/com/composer/Composer.tsx:1740 msgid "Processing video..." msgstr "" @@ -6492,22 +6492,22 @@ msgid "Public, sharable lists which can be used to drive feeds." msgstr "" #. Accessibility label for button to publish a single post -#: src/view/com/composer/Composer.tsx:1011 +#: src/view/com/composer/Composer.tsx:1016 msgid "Publish post" msgstr "" #. Accessibility label for button to publish multiple posts in a thread -#: src/view/com/composer/Composer.tsx:1004 +#: src/view/com/composer/Composer.tsx:1009 msgid "Publish posts" msgstr "" #. Accessibility label for button to publish multiple replies in a thread -#: src/view/com/composer/Composer.tsx:989 +#: src/view/com/composer/Composer.tsx:994 msgid "Publish replies" msgstr "" #. Accessibility label for button to publish a single reply -#: src/view/com/composer/Composer.tsx:996 +#: src/view/com/composer/Composer.tsx:1001 msgid "Publish reply" msgstr "" @@ -6850,7 +6850,7 @@ msgstr "" msgid "Replies to this post are disabled." msgstr "" -#: src/view/com/composer/Composer.tsx:1027 +#: src/view/com/composer/Composer.tsx:1032 msgctxt "action" msgid "Reply" msgstr "" @@ -9185,7 +9185,7 @@ msgstr "" msgid "Unsubscribed from list" msgstr "" -#: src/view/com/composer/Composer.tsx:811 +#: src/view/com/composer/Composer.tsx:814 msgid "Unsupported video type" msgstr "" @@ -9275,7 +9275,7 @@ msgstr "" msgid "Uploading link thumbnail..." msgstr "" -#: src/view/com/composer/Composer.tsx:1732 +#: src/view/com/composer/Composer.tsx:1737 msgid "Uploading video..." msgstr "" @@ -9521,7 +9521,7 @@ msgstr "" msgid "Video settings" msgstr "" -#: src/view/com/composer/Composer.tsx:1742 +#: src/view/com/composer/Composer.tsx:1747 msgid "Video uploaded" msgstr "" @@ -9616,7 +9616,7 @@ msgstr "" msgid "View users who like this feed" msgstr "" -#: src/components/VideoPostCard.tsx:398 +#: src/components/VideoPostCard.tsx:399 msgid "View video" msgstr "" @@ -9834,7 +9834,7 @@ msgstr "" #: src/view/com/auth/SplashScreen.tsx:38 #: src/view/com/auth/SplashScreen.web.tsx:99 -#: src/view/com/composer/Composer.tsx:774 +#: src/view/com/composer/Composer.tsx:777 msgid "What's up?" msgstr "" @@ -9916,11 +9916,11 @@ msgstr "" msgid "Write a message" msgstr "" -#: src/view/com/composer/Composer.tsx:867 +#: src/view/com/composer/Composer.tsx:872 msgid "Write post" msgstr "" -#: src/view/com/composer/Composer.tsx:772 +#: src/view/com/composer/Composer.tsx:775 #: src/view/com/post-thread/PostThreadComposePrompt.tsx:90 msgid "Write your reply" msgstr "" diff --git a/src/screens/Messages/ChatList.tsx b/src/screens/Messages/ChatList.tsx index e13f0617b8..c37a025cc9 100644 --- a/src/screens/Messages/ChatList.tsx +++ b/src/screens/Messages/ChatList.tsx @@ -164,12 +164,18 @@ export function MessagesScreenInner({navigation, route}: Props) { // filter out convos that are actively being left .filter(convo => !leftConvos.includes(convo.id)) + const hasInboxRequests = inboxPreviewConvos?.length > 0 + return [ - { - type: 'INBOX', - count: inboxPreviewConvos.length, - profiles: inboxPreviewConvos.slice(0, 3), - }, + ...(hasInboxRequests + ? [ + { + type: 'INBOX' as const, + count: inboxPreviewConvos.length, + profiles: inboxPreviewConvos.slice(0, 3), + }, + ] + : []), ...conversations.map( convo => ({type: 'CONVERSATION', conversation: convo}) as const, ), @@ -223,16 +229,24 @@ export function MessagesScreenInner({navigation, route}: Props) { return listenSoftReset(onSoftReset) }, [onSoftReset, isScreenFocused]) - // Will always have 1 item - the inbox button - if (conversations.length < 2) { + // NOTE(APiligrim) + // Show empty state only if there are no conversations at all + const actualConversations = conversations.filter( + item => item.type === 'CONVERSATION', + ) + const hasInboxRequests = inboxPreviewConvos?.length > 0 + + if (actualConversations.length === 0) { return (
- + {hasInboxRequests && ( + + )} {isLoading ? ( ) : ( diff --git a/src/state/ageAssurance/index.tsx b/src/state/ageAssurance/index.tsx index 3451b1139d..6cdd8d9299 100644 --- a/src/state/ageAssurance/index.tsx +++ b/src/state/ageAssurance/index.tsx @@ -4,7 +4,6 @@ import {useQuery} from '@tanstack/react-query' import {networkRetry} from '#/lib/async/retry' import {useGetAndRegisterPushToken} from '#/lib/notifications/notifications' -import {useGate} from '#/lib/statsig/statsig' import {isNetworkError} from '#/lib/strings/errors' import { type AgeAssuranceAPIContextType, @@ -41,7 +40,6 @@ const AgeAssuranceAPIContext = createContext({ * performance. */ export function Provider({children}: {children: React.ReactNode}) { - const gate = useGate() const agent = useAgent() const {geolocation} = useGeolocation() const isAgeAssuranceEnabled = useIsAgeAssuranceEnabled() @@ -78,12 +76,10 @@ export function Provider({children}: {children: React.ReactNode}) { account: agent.session?.did, }) - if (gate('age_assurance')) { - await getAndRegisterPushToken({ - isAgeRestricted: - !!geolocation?.isAgeRestrictedGeo && data.status !== 'assured', - }) - } + await getAndRegisterPushToken({ + isAgeRestricted: + !!geolocation?.isAgeRestrictedGeo && data.status !== 'assured', + }) return data } catch (e) { diff --git a/src/state/ageAssurance/useAgeAssurance.ts b/src/state/ageAssurance/useAgeAssurance.ts index 0215cc88dc..0613848687 100644 --- a/src/state/ageAssurance/useAgeAssurance.ts +++ b/src/state/ageAssurance/useAgeAssurance.ts @@ -28,7 +28,8 @@ export function useAgeAssurance(): AgeAssurance { return useMemo(() => { const isReady = aa.isReady && preferencesLoaded - const isDeclaredUnderage = (declaredAge || 0) < 18 + const isDeclaredUnderage = + declaredAge !== undefined ? declaredAge < 18 : false const state: AgeAssurance = { isReady, status: aa.status, diff --git a/src/state/ageAssurance/useIsAgeAssuranceEnabled.ts b/src/state/ageAssurance/useIsAgeAssuranceEnabled.ts index 06fe46d236..b020e3c573 100644 --- a/src/state/ageAssurance/useIsAgeAssuranceEnabled.ts +++ b/src/state/ageAssurance/useIsAgeAssuranceEnabled.ts @@ -1,14 +1,11 @@ import {useMemo} from 'react' -import {useGate} from '#/lib/statsig/statsig' import {useGeolocation} from '#/state/geolocation' export function useIsAgeAssuranceEnabled() { - const gate = useGate() const {geolocation} = useGeolocation() return useMemo(() => { - const enabled = gate('age_assurance') - return enabled && !!geolocation?.isAgeRestrictedGeo - }, [geolocation, gate]) + return !!geolocation?.isAgeRestrictedGeo + }, [geolocation]) } diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 92bd7babee..32026323f5 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -48,6 +48,7 @@ const ApiContext = React.createContext({ logoutEveryAccount: async () => {}, resumeSession: async () => {}, removeAccount: () => {}, + partialRefreshSession: async () => {}, }) export function Provider({children}: React.PropsWithChildren<{}>) { @@ -139,7 +140,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) const logoutCurrentAccount = React.useCallback< - SessionApiContext['logoutEveryAccount'] + SessionApiContext['logoutCurrentAccount'] >( logContext => { addSessionDebugLog({type: 'method:start', method: 'logout'}) @@ -215,6 +216,23 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [onAgentSessionChange, cancelPendingTask, shouldUseOauth], ) + const partialRefreshSession = React.useCallback< + SessionApiContext['partialRefreshSession'] + >(async () => { + const agent = state.currentAgentState.agent as BskyAppAgent + const signal = cancelPendingTask() + const {data} = await agent.com.atproto.server.getSession() + if (signal.aborted) return + dispatch({ + type: 'partial-refresh-session', + accountDid: agent.session!.did, + patch: { + emailConfirmed: data.emailConfirmed, + emailAuthFactor: data.emailAuthFactor, + }, + }) + }, [state, cancelPendingTask]) + const removeAccount = React.useCallback( account => { addSessionDebugLog({ @@ -301,6 +319,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { logoutEveryAccount, resumeSession, removeAccount, + partialRefreshSession, }), [ createAccount, @@ -309,6 +328,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { logoutEveryAccount, resumeSession, removeAccount, + partialRefreshSession, ], ) diff --git a/src/state/session/reducer.ts b/src/state/session/reducer.ts index d638f4d535..5a3dca8d16 100644 --- a/src/state/session/reducer.ts +++ b/src/state/session/reducer.ts @@ -1,4 +1,4 @@ -import {type AtpSessionEvent} from '@atproto/api' +import {type AtpSessionEvent, type BskyAgent} from '@atproto/api' import {createPublicAgent} from './agent' import {wrapSessionReducerForLogging} from './logging' @@ -52,6 +52,11 @@ export type Action = syncedAccounts: SessionAccount[] syncedCurrentDid: string | undefined } + | { + type: 'partial-refresh-session' + accountDid: string + patch: Pick + } function createPublicAgentState(): AgentState { return { @@ -180,6 +185,39 @@ let reducer = (state: State, action: Action): State => { needsPersist: false, // Synced from another tab. Don't persist to avoid cycles. } } + case 'partial-refresh-session': { + const {accountDid, patch} = action + const agent = state.currentAgentState.agent as BskyAgent + + /* + * Only mutating values that are safe. Be very careful with this. + */ + if (agent.session) { + agent.session.emailConfirmed = + patch.emailConfirmed ?? agent.session.emailConfirmed + agent.session.emailAuthFactor = + patch.emailAuthFactor ?? agent.session.emailAuthFactor + } + + return { + ...state, + currentAgentState: { + ...state.currentAgentState, + agent, + }, + accounts: state.accounts.map(a => { + if (a.did === accountDid) { + return { + ...a, + emailConfirmed: patch.emailConfirmed ?? a.emailConfirmed, + emailAuthFactor: patch.emailAuthFactor ?? a.emailAuthFactor, + } + } + return a + }), + needsPersist: true, + } + } } } reducer = wrapSessionReducerForLogging(reducer) diff --git a/src/state/session/types.ts b/src/state/session/types.ts index 8dedf18ba2..e2a7bc6679 100644 --- a/src/state/session/types.ts +++ b/src/state/session/types.ts @@ -43,4 +43,12 @@ export type SessionApiContext = { ) => void resumeSession: (account: SessionAccount) => Promise removeAccount: (account: SessionAccount) => void + /** + * Calls `getSession` and updates select fields on the current account and + * `BskyAgent`. This is an alternative to `resumeSession`, which updates + * current account/agent using the `persistSessionHandler`, but is more load + * bearing. This patches in updates without causing any side effects via + * `persistSessionHandler`. + */ + partialRefreshSession: () => Promise }