Merge remote-tracking branch 'origin/main' into update-img-config

* origin/main: (31 commits)
  Add group chat join links to supported embed types (#10454)
  Disable overscroll on Android to fix settle discrepancy (#10717)
  Fix labeller highlight when selected on bottom bar (#10716)
  Use correct `Plural` macro in InviteLinkDialog (#10723)
  Fix invalid `zero` plural category and add plural formatting (#10722)
  Nightly source-language update
  `expo-image` tweaks (#10710)
  Make extra sure the drawer gesture doesn't accidentally trigger on Android (#10713)
  Fix image peek in carousels for quote posts (#10688)
  Compile i18n before running lint (#10711)
  [Android] Fix horizontal swipes being cancelled when leaving bounds (#10712)
  Enable ESLint errors and suppress existing violations (#10490)
  Hide accept button for locked chats (#10696)
  Mount message dialogs once at the list level (#10435)
  Fix alt text selection in lightbox on Android (and iOS) (#10698)
  Mention default `<Text>` style in CLAUDE.md (#10705)
  Use square icons for labellers in Automation Label preview card (#10701)
  [ALF] Set text to `leading_snug` by default (#10627)
  Nightly source-language update
  Transpile `@atproto/api` package for older browser support (#10700)
  ...
This commit is contained in:
Eric Bailey
2026-06-04 10:42:24 -05:00
133 changed files with 6200 additions and 1265 deletions
+30 -19
View File
@@ -9,6 +9,7 @@ import {PressableScale} from '#/lib/custom-animations/PressableScale'
import {STALE} from '#/state/queries'
import {profilesQueryKey} from '#/state/queries/profile'
import {useAgent, useSession} from '#/state/session'
import {useSetActiveLanding} from '#/state/shell/landing'
import {
useLoggedOutView,
useLoggedOutViewControls,
@@ -16,6 +17,7 @@ import {
import {useEnableMinimalShellMode} from '#/state/shell/minimal-mode'
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {Login} from '#/screens/Login'
import {JoinRequest} from '#/screens/Messages/JoinRequest'
import {Signup} from '#/screens/Signup'
import {LandingScreen} from '#/screens/StarterPack/StarterPackLandingScreen'
import {atoms as a, native, tokens, useTheme} from '#/alf'
@@ -29,9 +31,26 @@ enum ScreenState {
S_Login,
S_CreateAccount,
S_StarterPack,
S_GroupChatJoinRequest,
}
export {ScreenState as LoggedOutScreenState}
function getInitialScreenState(requestedAccountSwitchTo?: string): ScreenState {
switch (requestedAccountSwitchTo) {
case 'new':
return ScreenState.S_CreateAccount
case 'starterpack':
return ScreenState.S_StarterPack
case 'groupchat':
return ScreenState.S_GroupChatJoinRequest
case undefined:
return ScreenState.S_LoginOrCreateAccount
default:
// DID or other string = login with that account
return ScreenState.S_Login
}
}
export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
const {_} = useLingui()
const ax = useAnalytics()
@@ -39,18 +58,11 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
const insets = useSafeAreaInsets()
useEnableMinimalShellMode()
const {requestedAccountSwitchTo} = useLoggedOutView()
const [screenState, setScreenState] = useState<ScreenState>(() => {
if (requestedAccountSwitchTo === 'new') {
return ScreenState.S_CreateAccount
} else if (requestedAccountSwitchTo === 'starterpack') {
return ScreenState.S_StarterPack
} else if (requestedAccountSwitchTo != null) {
return ScreenState.S_Login
} else {
return ScreenState.S_LoginOrCreateAccount
}
})
const initialScreenState = getInitialScreenState(requestedAccountSwitchTo)
const [screenState, setScreenState] =
useState<ScreenState>(initialScreenState)
const {clearRequestedAccount} = useLoggedOutViewControls()
const setActiveLanding = useSetActiveLanding()
const queryClient = useQueryClient()
const {accounts} = useSession()
@@ -73,7 +85,9 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
onDismiss()
}
clearRequestedAccount()
}, [clearRequestedAccount, onDismiss])
// Clear landing context when user dismisses the modal
setActiveLanding(undefined)
}, [clearRequestedAccount, onDismiss, setActiveLanding])
return (
<View
@@ -107,6 +121,8 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
{screenState === ScreenState.S_StarterPack ? (
<LandingScreen setScreenState={setScreenState} />
) : screenState === ScreenState.S_GroupChatJoinRequest ? (
<JoinRequest setScreenState={setScreenState} />
) : screenState === ScreenState.S_LoginOrCreateAccount ? (
<SplashScreen
onPressSignin={() => {
@@ -122,17 +138,12 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
{screenState === ScreenState.S_Login ? (
<Login
onPressBack={() => {
setScreenState(ScreenState.S_LoginOrCreateAccount)
clearRequestedAccount()
setScreenState(initialScreenState)
}}
/>
) : undefined}
{screenState === ScreenState.S_CreateAccount ? (
<Signup
onPressBack={() =>
setScreenState(ScreenState.S_LoginOrCreateAccount)
}
/>
<Signup onPressBack={() => setScreenState(initialScreenState)} />
) : undefined}
</ErrorBoundary>
</View>
+10 -10
View File
@@ -151,8 +151,8 @@ function ComposerReplyToImages({
<Image
source={{uri: images[0].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
useAppleWebpCodec
/>
)) ||
(images.length === 2 && (
@@ -160,14 +160,14 @@ function ComposerReplyToImages({
<Image
source={{uri: images[0].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
useAppleWebpCodec
/>
<Image
source={{uri: images[1].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
useAppleWebpCodec
/>
</View>
)) ||
@@ -176,21 +176,21 @@ function ComposerReplyToImages({
<Image
source={{uri: images[0].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
useAppleWebpCodec
/>
<View style={[a.flex_1, a.gap_2xs]}>
<Image
source={{uri: images[1].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
useAppleWebpCodec
/>
<Image
source={{uri: images[2].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
useAppleWebpCodec
/>
</View>
</View>
@@ -201,28 +201,28 @@ function ComposerReplyToImages({
<Image
source={{uri: images[0].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
useAppleWebpCodec
/>
<Image
source={{uri: images[1].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
useAppleWebpCodec
/>
</View>
<View style={[a.flex_1, a.flex_row, a.gap_2xs]}>
<Image
source={{uri: images[2].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
useAppleWebpCodec
/>
<Image
source={{uri: images[3].thumb}}
style={[a.flex_1]}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
useAppleWebpCodec
/>
</View>
</View>
+5 -1
View File
@@ -11,6 +11,7 @@ import {atoms as a, useTheme} from '#/alf'
import {Loader} from '#/components/Loader'
import {ExternalEmbed} from '#/components/Post/Embed/ExternalEmbed'
import {ModeratedFeedEmbed} from '#/components/Post/Embed/FeedEmbed'
import {JoinRequestEmbed} from '#/components/Post/Embed/JoinRequestEmbed'
import {ModeratedListEmbed} from '#/components/Post/Embed/ListEmbed'
import {StandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed'
import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils'
@@ -98,7 +99,8 @@ export const ExternalEmbedLink = ({
uri,
description:
data.view?.external?.description || data.description,
thumb: data.view?.external?.thumb || data.thumb?.source.path,
// prefer opengraph data to atproto record-derived image
thumb: data.thumb?.source.path || data.view?.external?.thumb,
}}
/>
)
@@ -114,6 +116,8 @@ export const ExternalEmbedLink = ({
hideAlt
/>
)
} else if (data.type === 'chat-invite') {
return <JoinRequestEmbed preview={data.view} />
} else if (data.kind === 'feed') {
return (
<ModeratedFeedEmbed
@@ -1,6 +1,5 @@
import {View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useLingui} from '@lingui/react/macro'
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
@@ -11,12 +10,12 @@ export function ExternalEmbedRemoveBtn({
style,
}: {onRemove: () => void} & ViewStyleProp) {
const t = useTheme()
const {_} = useLingui()
const {t: l} = useLingui()
return (
<View style={[a.absolute, {top: 8, right: 8}, a.z_50, style]}>
<Button
label={_(msg`Remove attachment`)}
label={l`Remove attachment`}
onPress={onRemove}
size="small"
variant="solid"
+3 -3
View File
@@ -470,11 +470,11 @@ export async function draftToComposerPosts(
height,
mime: 'image/jpeg',
},
} as ComposerImage
} satisfies ComposerImage
})
const images = (await Promise.all(imagePromises)).filter(
(img): img is ComposerImage => img !== null,
(img): img is NonNullable<typeof img> => img !== null,
)
if (images.length > 0) {
embed.media = {type: 'images', images}
@@ -511,7 +511,7 @@ export async function draftToComposerPosts(
tinygif: mediaObject,
preview: mediaObject,
},
} as Gif,
},
alt: gifData.alt,
}
break
+1
View File
@@ -245,6 +245,7 @@ const GalleryItem = ({
}}
accessible={true}
accessibilityIgnoresInvertColors
enforceEarlyResizing
cachePolicy="none"
autoplay={false}
contentFit="cover"
@@ -1093,6 +1093,7 @@ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) {
<MediaPreview.Embed
embed={post.embed}
style={styles.additionalPostImages}
peekable
/>
</>
)
+1 -1
View File
@@ -55,7 +55,7 @@ export function TestCtrls() {
accessibilityLabel="Text input field"
accessibilityHint="Enter proxy header"
testID="e2eProxyHeaderInput"
onChangeText={val => setProxyHeader(val as any)}
onChangeText={val => setProxyHeader(val)}
autoComplete="off"
autoCorrect={false}
autoCapitalize="none"
+22 -1
View File
@@ -13,6 +13,7 @@ import {
import {sanitizeUrl} from '@braintree/sanitize-url'
import {StackActions} from '@react-navigation/native'
import {useGroupChatJoinIntent} from '#/lib/hooks/useIntentHandler'
import {
type DebouncedNavigationProp,
useNavigationDeduped,
@@ -21,6 +22,7 @@ import {useOpenLink} from '#/lib/hooks/useOpenLink'
import {getTabState, TabState} from '#/lib/routes/helpers'
import {
convertBskyAppUrlIfNeeded,
getChatInviteCodeFromUrl,
isExternalUrl,
linkRequiresWarning,
} from '#/lib/strings/url-helpers'
@@ -81,6 +83,7 @@ export const Link = memo(function Link({
const navigation = useNavigationDeduped()
const anchorHref = asAnchor ? sanitizeUrl(href) : undefined
const openLink = useOpenLink()
const groupChatJoinIntent = useGroupChatJoinIntent()
const onPress = useCallback(
(e?: Event) => {
@@ -92,11 +95,20 @@ export const Link = memo(function Link({
sanitizeUrl(href),
navigationAction,
openLink,
groupChatJoinIntent,
e,
)
}
},
[closeModal, navigation, navigationAction, href, openLink, onBeforePress],
[
closeModal,
navigation,
navigationAction,
href,
openLink,
onBeforePress,
groupChatJoinIntent,
],
)
const accessibilityActionsWithActivate = [
@@ -196,6 +208,7 @@ export const TextLink = memo(function TextLink({
const {closeModal} = useModalControls()
const {linkWarningDialogControl} = useGlobalDialogsControlContext()
const openLink = useOpenLink()
const groupChatJoinIntent = useGroupChatJoinIntent()
if (!disableMismatchWarning && typeof text !== 'string') {
console.error('Unable to detect mismatching label')
@@ -238,6 +251,7 @@ export const TextLink = memo(function TextLink({
sanitizeUrl(href),
navigationAction,
openLink,
groupChatJoinIntent,
e,
)
},
@@ -252,6 +266,7 @@ export const TextLink = memo(function TextLink({
navigationAction,
openLink,
linkWarningDialogControl,
groupChatJoinIntent,
],
)
const hrefAttrs = useMemo(() => {
@@ -373,6 +388,7 @@ function onPressInner(
href: string,
navigationAction: 'push' | 'replace' | 'navigate' = 'push',
openLink: (href: string) => void,
groupChatJoinIntent: (code: string, uri?: string) => void,
e?: Event,
) {
let shouldHandle = false
@@ -400,6 +416,11 @@ function onPressInner(
if (shouldHandle) {
href = convertBskyAppUrlIfNeeded(href)
const chatInviteCode = getChatInviteCodeFromUrl(href)
if (chatInviteCode) {
groupChatJoinIntent(chatInviteCode, href)
return
}
if (
newTab ||
href.startsWith('http') ||
+1 -1
View File
@@ -3,9 +3,9 @@ import {
Pressable,
type PressableProps,
type StyleProp,
type View,
type ViewStyle,
} from 'react-native'
import {type View} from 'react-native'
import {addStyle} from '#/lib/styles'
import {useInteractionState} from '#/components/hooks/useInteractionState'
+1
View File
@@ -331,6 +331,7 @@ let UserAvatar = ({
}}
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
onLoad={onLoad}
useAppleWebpCodec
/>
)}
{!noBorder && <MediaInsetBorder style={borderStyle} />}
+2
View File
@@ -132,6 +132,7 @@ export function UserBanner({
source={{uri: banner}}
accessible={true}
accessibilityIgnoresInvertColors
useAppleWebpCodec
/>
) : (
<View
@@ -219,6 +220,7 @@ export function UserBanner({
blurRadius={moderation?.blur ? 100 : 0}
accessible={true}
accessibilityIgnoresInvertColors
useAppleWebpCodec
/>
) : (
<View
+55
View File
@@ -0,0 +1,55 @@
import Svg, {Path, type PathProps, type SvgProps} from 'react-native-svg'
import {useTheme} from '#/alf'
const ratio = 17 / 64
export function LogomarkWithType({
fill,
...rest
}: {fill?: PathProps['fill']} & SvgProps) {
const t = useTheme()
const size = parseInt(`${rest.width || 32}`)
return (
<Svg
fill="none"
viewBox="0 0 136 31"
{...rest}
width={size}
height={Number(size) * ratio}>
<Path
d="M7.06102 1.95468C10.7957 4.79341 14.8125 10.5493 16.2874 13.6379C17.7624 10.5493 21.7792 4.79341 25.5138 1.95468C28.2085 -0.0935907 32.5749 -1.67844 32.5749 3.36465C32.5749 4.3718 32.0045 11.8254 31.67 13.0356C30.5072 17.2426 26.2702 18.3156 22.5011 17.6661C29.0892 18.8014 30.7651 22.5617 27.1457 26.3219C19.002 34.7826 16.2874 21.7959 16.2874 21.7959C16.2874 21.7959 13.5729 34.7826 5.42914 26.3219C1.80971 22.5617 3.48561 18.8014 10.0737 17.6661C6.30464 18.3156 2.06767 17.2426 0.904857 13.0356C0.570361 11.8254 0 4.3718 0 3.36465C0 -1.67844 4.36632 -0.0935907 7.06102 1.95468Z"
fill={fill || t.atoms.text.color}
/>
<Path
d="M52.8842 14.3087C55.1269 15.1125 56.3176 16.9695 56.3176 19.0483C56.3176 22.5684 53.964 24.7027 49.4508 24.7027H40.2305V4.9679H49.1462C53.4379 4.9679 55.5146 7.15757 55.5146 10.0679C55.5146 12.0081 54.6286 13.4217 52.8842 14.3087ZM48.8693 8.04453H43.9684V13.2H48.8693C50.7798 13.2 51.8043 12.2021 51.8043 10.5391C51.8043 9.01463 50.7521 8.04453 48.8693 8.04453ZM43.9684 21.5983H49.257C51.3613 21.5983 52.4965 20.6282 52.4965 18.882C52.4965 17.0527 51.4167 16.138 49.257 16.138H43.9684V21.5983Z"
fill={fill || t.atoms.text.color}
/>
<Path
d="M61.5003 24.7027H58.0115V4.9679H61.5003V24.7027Z"
fill={fill || t.atoms.text.color}
/>
<Path
d="M73.0679 18.4108V10.4005H76.5567V24.7027H73.1787V22.6239C72.0988 24.2315 70.6036 25.0353 68.6931 25.0353C65.675 25.0353 63.7091 23.2059 63.7091 19.8799V10.4005H67.1979V19.2978C67.1979 21.0994 68.084 22.0141 69.8837 22.0141C71.5727 22.0141 73.0679 20.7668 73.0679 18.4108Z"
fill={fill || t.atoms.text.color}
/>
<Path
d="M92.4368 17.801V18.6326H81.7489C81.9981 21.0994 83.3549 22.319 85.4592 22.319C87.0652 22.319 88.145 21.626 88.7265 20.2679H92.0768C91.3292 23.1782 88.8372 25.0353 85.4315 25.0353C83.2995 25.0353 81.5828 24.3424 80.2814 22.9842C78.9801 21.626 78.3156 19.8244 78.3156 17.5516C78.3156 15.3065 78.9524 13.5049 80.2538 12.119C81.5551 10.7608 83.2441 10.0679 85.3762 10.0679C87.5359 10.0679 89.2526 10.7885 90.5262 12.2021C91.7999 13.6157 92.4368 15.5005 92.4368 17.801ZM85.3485 12.7842C83.438 12.7842 82.1089 13.8929 81.7766 16.1934H88.948C88.6434 14.1146 87.3697 12.7842 85.3485 12.7842Z"
fill={fill || t.atoms.text.color}
/>
<Path
d="M100.169 25.0907C96.0159 25.0907 93.8285 23.4554 93.6347 20.157H97.0404C97.2342 21.9309 98.1203 22.5962 100.225 22.5962C102.107 22.5962 103.049 22.0141 103.049 20.8777C103.049 19.8521 102.384 19.3532 100.252 18.9929L98.6187 18.7157C95.4898 18.1891 93.9393 16.7478 93.9393 14.3918C93.9393 11.7032 96.0713 10.0679 99.8647 10.0679C103.935 10.0679 106.039 11.6755 106.178 14.9184H102.883C102.8 13.1722 101.775 12.5625 99.8647 12.5625C98.2033 12.5625 97.3727 13.1168 97.3727 14.2255C97.3727 15.2233 98.0926 15.6668 99.7539 15.9717L101.554 16.2489C105.015 16.8864 106.51 18.1614 106.51 20.6005C106.51 23.4831 104.184 25.0907 100.169 25.0907Z"
fill={fill || t.atoms.text.color}
/>
<Path
d="M121.822 24.7027H117.835L113.682 18.0505L111.522 20.2125V24.7027H108.089V4.9679H111.522V16.332L117.337 10.4005H121.49L116.091 15.8054L121.822 24.7027Z"
fill={fill || t.atoms.text.color}
/>
<Path
d="M130.934 14.0038L132.097 10.4005H135.751L130.241 25.8668C129.66 27.4467 128.94 28.5831 128.026 29.2206C127.113 29.8581 125.811 30.163 124.095 30.163C123.513 30.163 123.015 30.1353 122.572 30.0799V27.3358H123.901C125.479 27.3358 126.254 26.3657 126.254 25.0353C126.254 24.3701 126.033 23.4 125.59 22.1527L121.436 10.4005H125.202L126.365 13.976C127.223 16.6646 127.971 19.3255 128.635 21.9587C129.245 19.6858 130.02 17.025 130.934 14.0038Z"
fill={fill || t.atoms.text.color}
/>
</Svg>
)
}
+3 -1
View File
@@ -4,6 +4,8 @@ import {Gesture, GestureDetector} from 'react-native-gesture-handler'
export function BlockDrawerGesture({children}: {children: React.ReactNode}) {
const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web
const scrollGesture = Gesture.Native().blocksExternalGesture(drawerGesture)
let scrollGesture = Gesture.Native()
.shouldCancelWhenOutside(false) // for some reason defaults to true on Android
.blocksExternalGesture(drawerGesture)
return <GestureDetector gesture={scrollGesture}>{children}</GestureDetector>
}
+3 -2
View File
@@ -144,6 +144,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
const [demoMode] = useDemoMode()
const {isActive: live} = useActorStatus(profile)
const isLabeler = profile?.associated?.labeler
return (
<>
@@ -276,9 +277,9 @@ export function BottomBar({navigation}: BottomTabBarProps) {
<View
style={[
styles.ctrlIcon,
styles.profileIcon,
isLabeler ? styles.profileIconSquare : styles.profileIcon,
isAtMyProfile && [
styles.onProfile,
isLabeler ? styles.onProfileSquare : styles.onProfile,
{
borderColor: t.atoms.text.color,
borderWidth: live ? 0 : 1,
@@ -67,9 +67,18 @@ export const styles = StyleSheet.create({
borderWidth: 1,
borderColor: 'transparent',
},
profileIconSquare: {
borderRadius: 3,
borderWidth: 1,
borderColor: 'transparent',
},
messagesIcon: {},
onProfile: {
borderWidth: 1,
borderRadius: 100,
},
onProfileSquare: {
borderWidth: 1,
borderRadius: 3,
},
})
+7 -2
View File
@@ -65,6 +65,7 @@ export function BottomBarWeb() {
const unreadMessageCount = useUnreadMessageCount()
const notificationCountStr = useUnreadNotifications()
const aa = useAgeAssurance()
const isLabeler = profile?.associated?.labeler
const showSignIn = useCallback(() => {
closeAllActiveElements()
@@ -186,9 +187,13 @@ export function BottomBarWeb() {
<View
style={[
styles.ctrlIcon,
styles.profileIcon,
isLabeler
? styles.profileIconSquare
: styles.profileIcon,
isActive && [
styles.onProfile,
isLabeler
? styles.onProfileSquare
: styles.onProfile,
{borderColor: t.atoms.text.color},
],
]}>
@@ -171,7 +171,7 @@ function NativeStackNavigator({
}
// Evicted screens get a lightweight placeholder instead of their full tree
finalDescriptors = {} as typeof descriptors
finalDescriptors = {}
for (const key in descriptors) {
if (mountSet.has(key)) {
finalDescriptors[key] = descriptors[key]
+9 -2
View File
@@ -174,8 +174,15 @@ function DrawerLayout({children}: {children: React.ReactNode}) {
// so fail the drawer gesture immediately.
.failOffsetX(-1)
// Don't rush declaring that a movement to the right
// is a drawer swipe. It could be a vertical scroll.
.activeOffsetX(5)
// is a drawer swipe. It could be a vertical scroll, or a
// slow horizontal carousel swipe. On Android a child
// `blocksExternalGesture` only holds the drawer off once the
// native scroll has activated, which on a slow swipe doesn't
// happen until movement crosses the native touch slop
// (~8-16px). Activating the drawer below that lets a slow
// carousel swipe pop the drawer open (APP-2119), so require
// more travel before claiming on Android.
.activeOffsetX(IS_ANDROID ? 20 : 5)
)
}
} else {