Merge branch 'main' into pnbx/fix-consume

This commit is contained in:
Hailey
2025-01-15 15:24:14 -08:00
7 changed files with 302 additions and 61 deletions
+3
View File
@@ -961,6 +961,9 @@ export const atoms = {
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
transitionDuration: '100ms',
}),
transition_delay_50ms: web({
transitionDelay: '50ms',
}),
/**
* {@link Layout.SCROLLBAR_OFFSET}
+30 -16
View File
@@ -3,10 +3,12 @@ import {
AccessibilityProps,
GestureResponderEvent,
MouseEvent,
NativeSyntheticEvent,
Pressable,
PressableProps,
StyleProp,
StyleSheet,
TargetedEvent,
TextProps,
TextStyle,
View,
@@ -76,6 +78,8 @@ export type ButtonProps = Pick<
| 'onHoverOut'
| 'onPressIn'
| 'onPressOut'
| 'onFocus'
| 'onBlur'
> &
AccessibilityProps &
VariantProps & {
@@ -116,6 +120,12 @@ export const Button = React.forwardRef<View, ButtonProps>(
style,
hoverStyle: hoverStyleProp,
PressableComponent = Pressable,
onPressIn: onPressInOuter,
onPressOut: onPressOutOuter,
onHoverIn: onHoverInOuter,
onHoverOut: onHoverOutOuter,
onFocus: onFocusOuter,
onBlur: onBlurOuter,
...rest
},
ref,
@@ -127,7 +137,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
focused: false,
})
const onPressInOuter = rest.onPressIn
const onPressIn = React.useCallback(
(e: GestureResponderEvent) => {
setState(s => ({
@@ -138,7 +147,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
},
[setState, onPressInOuter],
)
const onPressOutOuter = rest.onPressOut
const onPressOut = React.useCallback(
(e: GestureResponderEvent) => {
setState(s => ({
@@ -149,7 +157,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
},
[setState, onPressOutOuter],
)
const onHoverInOuter = rest.onHoverIn
const onHoverIn = React.useCallback(
(e: MouseEvent) => {
setState(s => ({
@@ -160,7 +167,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
},
[setState, onHoverInOuter],
)
const onHoverOutOuter = rest.onHoverOut
const onHoverOut = React.useCallback(
(e: MouseEvent) => {
setState(s => ({
@@ -171,18 +177,26 @@ export const Button = React.forwardRef<View, ButtonProps>(
},
[setState, onHoverOutOuter],
)
const onFocus = React.useCallback(() => {
setState(s => ({
...s,
focused: true,
}))
}, [setState])
const onBlur = React.useCallback(() => {
setState(s => ({
...s,
focused: false,
}))
}, [setState])
const onFocus = React.useCallback(
(e: NativeSyntheticEvent<TargetedEvent>) => {
setState(s => ({
...s,
focused: true,
}))
onFocusOuter?.(e)
},
[setState, onFocusOuter],
)
const onBlur = React.useCallback(
(e: NativeSyntheticEvent<TargetedEvent>) => {
setState(s => ({
...s,
focused: false,
}))
onBlurOuter?.(e)
},
[setState, onBlurOuter],
)
const {baseStyles, hoverStyles} = React.useMemo(() => {
const baseStyles: ViewStyle[] = []
+2 -1
View File
@@ -202,7 +202,7 @@ export function Outer({
)
}
export function Item({children, label, onPress, ...rest}: ItemProps) {
export function Item({children, label, onPress, style, ...rest}: ItemProps) {
const t = useTheme()
const {control} = useMenuContext()
const {
@@ -248,6 +248,7 @@ export function Item({children, label, onPress, ...rest}: ItemProps) {
? t.atoms.bg_contrast_25
: t.atoms.bg_contrast_50,
],
style,
])}
{...web({
onMouseEnter,
+40 -2
View File
@@ -1,4 +1,4 @@
import React, {forwardRef, useCallback, useContext} from 'react'
import React, {Children, forwardRef, useCallback, useContext} from 'react'
import {View} from 'react-native'
import {DrawerGestureContext} from 'react-native-drawer-layout'
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
@@ -17,6 +17,7 @@ import Animated, {
} from 'react-native-reanimated'
import {useFocusEffect} from '@react-navigation/native'
import {isAndroid} from '#/platform/detection'
import {useSetDrawerSwipeDisabled} from '#/state/shell'
import {atoms as a, native} from '#/alf'
@@ -148,7 +149,11 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
style={[a.flex_1]}
initialPage={initialPage}
onPageScroll={handlePageScroll}>
{children}
{isAndroid
? Children.map(children, child => (
<CaptureSwipesAndroid>{child}</CaptureSwipesAndroid>
))
: children}
</AnimatedPagerView>
</GestureDetector>
</View>
@@ -156,6 +161,39 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
},
)
// HACK.
// This works around https://github.com/callstack/react-native-pager-view/issues/960.
// It appears that the Pressables inside the pager get confused if there's enough work
// happening on the JS thread, and mistakingly interpret a pager swipe as a tap.
// We can prevent this by stealing all horizontal movements from the tree inside.
function CaptureSwipesAndroid({children}: {children: React.ReactNode}) {
const lastTouchStart = React.useRef<{x: number; y: number} | null>(null)
return (
<View
onTouchStart={e => {
lastTouchStart.current = {
x: e.nativeEvent.pageX,
y: e.nativeEvent.pageY,
}
}}
onMoveShouldSetResponderCapture={e => {
const coords = lastTouchStart.current
if (!coords) {
return false
}
const dx = Math.abs(e.nativeEvent.pageX - coords.x)
if (dx > 0) {
// This is a horizontal movement and will result in a swipe.
// Prevent pager children from receiving this touch.
return true
}
return false
}}>
{children}
</View>
)
}
function usePagerHandlers(
handlers: {
onPageScroll: (e: PagerViewOnPageScrollEventData) => void
@@ -149,9 +149,6 @@ export function useVideoElement(ref: React.RefObject<HTMLVideoElement>) {
ref.current.addEventListener('ended', handleEnded, {
signal: abortController.signal,
})
ref.current.addEventListener('volumechange', handleVolumeChange, {
signal: abortController.signal,
})
return () => {
abortController.abort()
+224 -39
View File
@@ -1,5 +1,6 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -9,28 +10,33 @@ import {
useNavigationState,
} from '@react-navigation/native'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {getCurrentRoute, isTab} from '#/lib/routes/helpers'
import {makeProfileLink} from '#/lib/routes/links'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {isInvalidHandle} from '#/lib/strings/handles'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles'
import {emitSoftReset} from '#/state/events'
import {useHomeBadge} from '#/state/home-badge'
import {useFetchHandle} from '#/state/queries/handle'
import {useUnreadMessageCount} from '#/state/queries/messages/list-conversations'
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {useProfilesQuery} from '#/state/queries/profile'
import {SessionAccount, useSession, useSessionApi} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {Link} from '#/view/com/util/Link'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {PressableWithHover} from '#/view/com/util/PressableWithHover'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {NavSignupCard} from '#/view/shell/NavSignupCard'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {DialogControlProps} from '#/components/Dialog'
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft'
import {
Bell_Filled_Corner0_Rounded as BellFilled,
Bell_Stroke2_Corner0_Rounded as Bell,
@@ -39,6 +45,7 @@ import {
BulletList_Filled_Corner0_Rounded as ListFilled,
BulletList_Stroke2_Corner0_Rounded as List,
} from '#/components/icons/BulletList'
import {DotGrid_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid'
import {EditBig_Stroke2_Corner0_Rounded as EditBig} from '#/components/icons/EditBig'
import {
Hashtag_Filled_Corner0_Rounded as HashtagFilled,
@@ -54,6 +61,7 @@ import {
Message_Stroke2_Corner0_Rounded as Message,
Message_Stroke2_Corner0_Rounded_Filled as MessageFilled,
} from '#/components/icons/Message'
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
import {
SettingsGear2_Filled_Corner0_Rounded as SettingsFilled,
SettingsGear2_Stroke2_Corner0_Rounded as Settings,
@@ -62,44 +70,231 @@ import {
UserCircle_Filled_Corner0_Rounded as UserCircleFilled,
UserCircle_Stroke2_Corner0_Rounded as UserCircle,
} from '#/components/icons/UserCircle'
import * as Menu from '#/components/Menu'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army'
import {router} from '../../../routes'
const NAV_ICON_WIDTH = 28
function ProfileCard() {
const {currentAccount} = useSession()
const {isLoading, data: profile} = useProfileQuery({did: currentAccount!.did})
const {isDesktop} = useWebMediaQueries()
const {currentAccount, accounts} = useSession()
const {logoutEveryAccount} = useSessionApi()
const {isLoading, data} = useProfilesQuery({
handles: accounts.map(acc => acc.did),
})
const profiles = data?.profiles
const signOutPromptControl = Prompt.usePromptControl()
const {gtTablet} = useBreakpoints()
const {_} = useLingui()
const t = useTheme()
const size = 48
return !isLoading && profile ? (
<Link
href={makeProfileLink({
did: currentAccount!.did,
handle: currentAccount!.handle,
})}
style={[styles.profileCard, !isDesktop && styles.profileCardTablet]}
title={_(msg`My Profile`)}
asAnchor>
<UserAvatar
avatar={profile.avatar}
size={size}
type={profile?.associated?.labeler ? 'labeler' : 'user'}
/>
</Link>
) : (
<View style={[styles.profileCard, !isDesktop && styles.profileCardTablet]}>
<LoadingPlaceholder
width={size}
height={size}
style={{borderRadius: size}}
const profile = profiles?.find(p => p.did === currentAccount!.did)
const otherAccounts = accounts
.filter(acc => acc.did !== currentAccount!.did)
.map(account => ({
account,
profile: profiles?.find(p => p.did === account.did),
}))
return (
<View style={[a.my_md, gtTablet && [a.w_full, a.align_start]]}>
{!isLoading && profile ? (
<Menu.Root>
<Menu.Trigger label={_(msg`Switch accounts`)}>
{({props, state, control}) => {
const active = state.hovered || state.focused || control.isOpen
return (
<Button
label={props.accessibilityLabel}
{...props}
style={[
a.w_full,
a.transition_color,
active ? t.atoms.bg_contrast_25 : a.transition_delay_50ms,
a.rounded_full,
a.justify_between,
a.align_center,
a.flex_row,
{gap: 6},
gtTablet && [a.pl_lg, a.pr_md],
]}>
<View
style={[
!PlatformInfo.getIsReducedMotionEnabled() && [
a.transition_transform,
{transitionDuration: '250ms'},
!active && a.transition_delay_50ms,
],
a.relative,
a.z_10,
active && {
transform: [
{scale: gtTablet ? 2 / 3 : 0.8},
{translateX: gtTablet ? -22 : 0},
],
},
]}>
<UserAvatar
avatar={profile.avatar}
size={size}
type={profile?.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
{gtTablet && (
<>
<View
style={[
a.flex_1,
a.transition_opacity,
!active && a.transition_delay_50ms,
{
marginLeft: tokens.space.xl * -1,
opacity: active ? 1 : 0,
},
]}>
<Text
style={[a.font_heavy, a.text_sm, a.leading_snug]}
numberOfLines={1}>
{sanitizeDisplayName(
profile.displayName || profile.handle,
)}
</Text>
<Text
style={[
a.text_xs,
a.leading_snug,
t.atoms.text_contrast_medium,
]}
numberOfLines={1}>
{sanitizeHandle(profile.handle, '@')}
</Text>
</View>
<EllipsisIcon
aria-hidden={true}
style={[
t.atoms.text_contrast_medium,
a.transition_opacity,
{opacity: active ? 1 : 0},
]}
size="sm"
/>
</>
)}
</Button>
)
}}
</Menu.Trigger>
<SwitchMenuItems
accounts={otherAccounts}
signOutPromptControl={signOutPromptControl}
/>
</Menu.Root>
) : (
<LoadingPlaceholder
width={size}
height={size}
style={[{borderRadius: size}, gtTablet && a.ml_lg]}
/>
)}
<Prompt.Basic
control={signOutPromptControl}
title={_(msg`Sign out?`)}
description={_(msg`You will be signed out of all your accounts.`)}
onConfirm={() => logoutEveryAccount('Settings')}
confirmButtonCta={_(msg`Sign out`)}
cancelButtonCta={_(msg`Cancel`)}
confirmButtonColor="negative"
/>
</View>
)
}
function SwitchMenuItems({
accounts,
signOutPromptControl,
}: {
accounts:
| {
account: SessionAccount
profile?: AppBskyActorDefs.ProfileView
}[]
| undefined
signOutPromptControl: DialogControlProps
}) {
const {_} = useLingui()
const {onPressSwitchAccount, pendingDid} = useAccountSwitcher()
const {setShowLoggedOut} = useLoggedOutViewControls()
const closeEverything = useCloseAllActiveElements()
const onAddAnotherAccount = () => {
setShowLoggedOut(true)
closeEverything()
}
return (
<Menu.Outer>
{accounts && accounts.length > 0 && (
<>
<Menu.Group>
<Menu.LabelText>
<Trans>Switch account</Trans>
</Menu.LabelText>
{accounts.map(other => (
<Menu.Item
disabled={!!pendingDid}
style={[{minWidth: 150}]}
key={other.account.did}
label={_(
msg`Switch to ${sanitizeHandle(
other.profile?.handle ?? other.account.handle,
'@',
)}`,
)}
onPress={() =>
onPressSwitchAccount(other.account, 'SwitchAccount')
}>
<View style={[{marginLeft: tokens.space._2xs * -1}]}>
<UserAvatar
avatar={other.profile?.avatar}
size={20}
type={
other.profile?.associated?.labeler ? 'labeler' : 'user'
}
/>
</View>
<Menu.ItemText>
{sanitizeHandle(
other.profile?.handle ?? other.account.handle,
'@',
)}
</Menu.ItemText>
</Menu.Item>
))}
</Menu.Group>
<Menu.Divider />
</>
)}
<Menu.Item
label={_(msg`Add another account`)}
onPress={onAddAnotherAccount}>
<Menu.ItemIcon icon={PlusIcon} />
<Menu.ItemText>
<Trans>Add another account</Trans>
</Menu.ItemText>
</Menu.Item>
<Menu.Item label={_(msg`Sign out`)} onPress={signOutPromptControl.open}>
<Menu.ItemIcon icon={LeaveIcon} />
<Menu.ItemText>
<Trans>Sign out</Trans>
</Menu.ItemText>
</Menu.Item>
</Menu.Outer>
)
}
interface NavItemProps {
count?: string
hasNew?: boolean
@@ -539,16 +734,6 @@ const styles = StyleSheet.create({
alignItems: 'center',
transform: [],
},
profileCard: {
marginVertical: 10,
width: 90,
paddingLeft: 12,
},
profileCardTablet: {
width: 70,
},
backBtn: {
position: 'absolute',
top: 12,
+3
View File
@@ -28,6 +28,9 @@ module.exports = async function (env, argv) {
]
if (env.mode === 'development') {
config.plugins.push(new ReactRefreshWebpackPlugin())
} else {
// Support static CDN for chunks
config.output.publicPath = 'auto'
}
if (GENERATE_STATS || OPEN_ANALYZER) {