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