Merge branch 'main' into hailey/oauth-yeag

This commit is contained in:
Hailey
2025-07-24 00:01:05 -07:00
17 changed files with 223 additions and 170 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.105.0",
"version": "1.106.0",
"private": true,
"engines": {
"node": ">=20"
+38 -33
View File
@@ -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({
/>
<MediaInsetBorder />
<View style={[a.absolute, a.inset_0]}>
<View style={[a.absolute, a.inset_0, t.atoms.shadow_sm]}>
<View style={[a.absolute, a.inset_0, a.p_sm, {bottom: 'auto'}]}>
<View
style={[a.relative, a.rounded_full, {width: 20, height: 20}]}>
style={[a.relative, a.rounded_full, {width: 24, height: 24}]}>
<UserAvatar
type="user"
size={20}
size={24}
avatar={post.author.avatar}
/>
<MediaInsetBorder />
</View>
</View>
<View
style={[
a.absolute,
a.inset_0,
a.pt_2xl,
{
top: 'auto',
},
]}>
<LinearGradient
colors={[black, 'rgba(0, 0, 0, 0)']}
locations={[0.02, 1]}
start={{x: 0, y: 1}}
end={{x: 0, y: 0}}
style={[a.absolute, a.inset_0, {opacity: 0.9}]}
/>
{showLikeCount && (
<View
style={[a.relative, a.z_10, a.p_sm, a.flex_row, a.gap_md]}>
{likeCount > 0 && (
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Heart size="sm" fill="white" />
<Text style={[a.text_sm, a.font_bold, {color: 'white'}]}>
{formatCount(i18n, likeCount)}
</Text>
</View>
)}
style={[
a.absolute,
a.inset_0,
a.pt_2xl,
{
top: 'auto',
},
]}>
<LinearGradient
colors={[black, 'rgba(0, 0, 0, 0)']}
locations={[0.02, 1]}
start={{x: 0, y: 1}}
end={{x: 0, y: 0}}
style={[a.absolute, a.inset_0, {opacity: 0.9}]}
/>
<View
style={[a.relative, a.z_10, a.p_sm, a.flex_row, a.gap_md]}>
{likeCount > 0 && (
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
<Heart size="sm" fill="white" />
<Text
style={[a.text_sm, a.font_bold, {color: 'white'}]}>
{formatCount(i18n, likeCount)}
</Text>
</View>
)}
</View>
</View>
</View>
)}
</View>
</View>
</Hider.Content>
@@ -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<AccountEmailState>({
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.
@@ -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!)
},
})
}
@@ -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!)
},
})
}
+3 -3
View File
@@ -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)
@@ -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)
-1
View File
@@ -1,6 +1,5 @@
export type Gate =
// Keep this alphabetic please.
| 'age_assurance'
| 'alt_share_icon'
| 'debug_show_feedcontext'
| 'debug_subscriptions'
+3
View File
@@ -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,
+41 -41
View File
@@ -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 ""
+25 -11
View File
@@ -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 (
<Layout.Screen>
<Header newChatControl={newChatControl} />
<Layout.Center>
<InboxPreview
count={inboxPreviewConvos.length}
profiles={inboxPreviewConvos}
/>
{hasInboxRequests && (
<InboxPreview
count={inboxPreviewConvos.length}
profiles={inboxPreviewConvos}
/>
)}
{isLoading ? (
<ChatListLoadingPlaceholder />
) : (
+4 -8
View File
@@ -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<AgeAssuranceAPIContextType>({
* 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) {
+2 -1
View File
@@ -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,
@@ -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])
}
+21 -1
View File
@@ -48,6 +48,7 @@ const ApiContext = React.createContext<SessionApiContext>({
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<SessionApiContext['removeAccount']>(
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,
],
)
+39 -1
View File
@@ -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<SessionAccount, 'emailConfirmed' | 'emailAuthFactor'>
}
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)
+8
View File
@@ -43,4 +43,12 @@ export type SessionApiContext = {
) => void
resumeSession: (account: SessionAccount) => Promise<void>
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<void>
}