Merge remote-tracking branch 'origin/main' into app-2670
This commit is contained in:
+22
-43
@@ -9,13 +9,13 @@ import {
|
||||
import Animated, {
|
||||
Easing,
|
||||
interpolate,
|
||||
runOnJS,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import Svg, {Path, type SvgProps} from 'react-native-svg'
|
||||
import {scheduleOnRN} from 'react-native-worklets'
|
||||
import {Image} from 'expo-image'
|
||||
import * as SplashScreen from 'expo-splash-screen'
|
||||
|
||||
@@ -72,21 +72,26 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
const isDarkMode = colorScheme === 'dark'
|
||||
|
||||
const logoAnimation = useAnimatedStyle(() => {
|
||||
const introScale = interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp')
|
||||
const outroScale =
|
||||
reduceMotion === true
|
||||
? 1
|
||||
: interpolate(outroLogo.get(), [0, 0.08, 1], [1, 0.8, 500], 'clamp')
|
||||
|
||||
const introOpacity = interpolate(intro.get(), [0, 1], [0, 1], 'clamp')
|
||||
const outroOpacity = interpolate(
|
||||
outroAppOpacity.get(),
|
||||
[0, 0.1, 0.2, 1],
|
||||
[1, 1, 0, 0],
|
||||
'clamp',
|
||||
)
|
||||
|
||||
return {
|
||||
opacity: introOpacity * outroOpacity,
|
||||
transform: [
|
||||
{
|
||||
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
|
||||
},
|
||||
{
|
||||
scale: interpolate(
|
||||
outroLogo.get(),
|
||||
[0, 0.08, 1],
|
||||
[1, 0.8, 500],
|
||||
'clamp',
|
||||
),
|
||||
},
|
||||
{translateY: -(insets.top / 2)},
|
||||
{scale: 0.1 * outroScale * introScale},
|
||||
],
|
||||
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
|
||||
}
|
||||
})
|
||||
const bottomLogoAnimation = useAnimatedStyle(() => {
|
||||
@@ -94,27 +99,6 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
|
||||
}
|
||||
})
|
||||
const reducedLogoAnimation = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
|
||||
},
|
||||
],
|
||||
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
|
||||
}
|
||||
})
|
||||
|
||||
const logoWrapperAnimation = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: interpolate(
|
||||
outroAppOpacity.get(),
|
||||
[0, 0.1, 0.2, 1],
|
||||
[1, 1, 0, 0],
|
||||
'clamp',
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
const appAnimation = useAnimatedStyle(() => {
|
||||
return {
|
||||
@@ -126,7 +110,7 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
opacity: interpolate(
|
||||
outroAppOpacity.get(),
|
||||
[0, 0.1, 0.2, 1],
|
||||
[0, 0, 1, 1],
|
||||
[0.02, 0.02, 1, 1], // first two values cant be 0 for the iOS blur/glass effects to work, the values obtained by trial and error
|
||||
'clamp',
|
||||
),
|
||||
}
|
||||
@@ -152,7 +136,7 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
1,
|
||||
{duration: 1200, easing: Easing.in(Easing.cubic)},
|
||||
() => {
|
||||
runOnJS(onFinish)()
|
||||
scheduleOnRN(onFinish)
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -180,8 +164,6 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion)
|
||||
}, [])
|
||||
|
||||
const logoAnimations =
|
||||
reduceMotion === true ? reducedLogoAnimation : logoAnimation
|
||||
// special off-spec color for dark mode
|
||||
const logoBg = isDarkMode ? '#0F1824' : '#fff'
|
||||
|
||||
@@ -224,17 +206,14 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
<Animated.View
|
||||
style={[
|
||||
StyleSheet.absoluteFillObject,
|
||||
logoWrapperAnimation,
|
||||
logoAnimation,
|
||||
{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px
|
||||
},
|
||||
]}>
|
||||
<Animated.View style={[logoAnimations]}>
|
||||
<Logo fill={logoBg} />
|
||||
</Animated.View>
|
||||
<Logo fill={logoBg} />
|
||||
</Animated.View>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -4,13 +4,30 @@ import {
|
||||
} from '@atproto/api'
|
||||
|
||||
import {AgeAssuranceAccess} from '#/ageAssurance/types'
|
||||
import {ANDROID_API_LEVEL, IOS_MAJOR_VERSION, IS_ANDROID, IS_IOS} from '#/env'
|
||||
import {
|
||||
ANDROID_API_LEVEL,
|
||||
IOS_MAJOR_VERSION,
|
||||
IS_ANDROID,
|
||||
IS_IOS,
|
||||
IS_WEB,
|
||||
} from '#/env'
|
||||
|
||||
/**
|
||||
* Minimum age required to access the app at all.
|
||||
*/
|
||||
export const MIN_ACCESS_AGE = 13
|
||||
|
||||
/**
|
||||
* The identifier for the current platform, matching the `knownValues` of the
|
||||
* `platforms` property on `app.bsky.ageassurance.defs#configRegion`. Used to
|
||||
* filter out region configs that don't apply to this platform.
|
||||
*/
|
||||
export const AGE_ASSURANCE_PLATFORM: 'web' | 'ios' | 'android' = IS_WEB
|
||||
? 'web'
|
||||
: IS_IOS
|
||||
? 'ios'
|
||||
: 'android'
|
||||
|
||||
/**
|
||||
* Whether the current device can provide the native on-device age signals we
|
||||
* use for age assurance (via `expo-age-range`). We gate on OS version because
|
||||
|
||||
@@ -59,9 +59,10 @@ export const config: AppBskyAgeassuranceDefs.Config = {
|
||||
],
|
||||
},
|
||||
{
|
||||
// On-device verification region. KWS is included as a fallback for
|
||||
// platforms without the native age API (e.g. web) or when the device
|
||||
// On-device verification region, native-only (web users in TX are not
|
||||
// age assured). KWS is included as a fallback for when the device
|
||||
// result is insufficient.
|
||||
platforms: ['ios', 'android'],
|
||||
countryCode: 'US',
|
||||
regionCode: 'TX',
|
||||
minAccessAge: 18,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import {getAgeAssuranceRegionConfig} from '@atproto/api'
|
||||
|
||||
import {getAgeAssuranceRegionConfigForGeolocation} from '#/ageAssurance/util'
|
||||
|
||||
jest.mock('#/ageAssurance/data')
|
||||
jest.mock('@atproto/api', () => ({
|
||||
...jest.requireActual('@atproto/api'),
|
||||
getAgeAssuranceRegionConfig: jest.fn(),
|
||||
}))
|
||||
|
||||
/*
|
||||
* Platform-based region filtering itself is implemented and tested in
|
||||
* `@atproto/api` (see `getAgeAssuranceRegionConfig`). What we own - and test
|
||||
* here - is that region resolution passes the current platform through. The
|
||||
* jest preset is `jest-expo/ios`, so `AGE_ASSURANCE_PLATFORM` resolves to
|
||||
* `ios` in these tests.
|
||||
*/
|
||||
describe('getAgeAssuranceRegionConfigForGeolocation', () => {
|
||||
it('passes the current platform to the SDK region matcher', () => {
|
||||
const config = {regions: []}
|
||||
getAgeAssuranceRegionConfigForGeolocation(config, {
|
||||
countryCode: 'US',
|
||||
regionCode: 'TX',
|
||||
})
|
||||
expect(getAgeAssuranceRegionConfig).toHaveBeenCalledWith(config, {
|
||||
countryCode: 'US',
|
||||
regionCode: 'TX',
|
||||
platform: 'ios',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -11,6 +11,7 @@ import {getAge} from '#/lib/strings/time'
|
||||
import {regionName} from '#/locale/helpers'
|
||||
import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/const'
|
||||
import {
|
||||
AGE_ASSURANCE_PLATFORM,
|
||||
DEVICE_SIGNALS_SUPPORTED,
|
||||
FALLBACK_REGION_CONFIG,
|
||||
MIN_ACCESS_AGE,
|
||||
@@ -29,6 +30,10 @@ import {USRegionNameToRegionCode} from '#/geolocation/util'
|
||||
* Resolves a geolocation to its matched age assurance region config, or
|
||||
* undefined when the geolocation matches no AA region.
|
||||
*
|
||||
* Regions scoped to other platforms via `platforms` are passed over entirely,
|
||||
* as if they weren't in the config - a later region matching the same
|
||||
* geolocation can still apply.
|
||||
*
|
||||
* This is the single source of truth for geolocation -> region resolution.
|
||||
* Device signals are written and read back under a key derived from the
|
||||
* matched region (see `createRegionKey`), so every site that resolves a region
|
||||
@@ -42,6 +47,7 @@ export function getAgeAssuranceRegionConfigForGeolocation(
|
||||
return getAgeAssuranceRegionConfig(config, {
|
||||
countryCode: geolocation.countryCode ?? '',
|
||||
regionCode: geolocation.regionCode,
|
||||
platform: AGE_ASSURANCE_PLATFORM,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
type TextProps as RNTextProps,
|
||||
type TextStyle,
|
||||
} from 'react-native'
|
||||
import {UITextView} from 'react-native-uitextview'
|
||||
import {UITextView} from '@bsky.app/react-native-uitextview'
|
||||
import createEmojiRegex from 'emoji-regex'
|
||||
|
||||
import {type Alf, applyFonts, atoms, flatten} from '#/alf'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {MMKV} from '@bsky.app/react-native-mmkv'
|
||||
import {MMKV} from 'react-native-mmkv'
|
||||
import {setPolyfills} from '@growthbook/growthbook'
|
||||
import {GrowthBook} from '@growthbook/growthbook-react'
|
||||
import {type I18n} from '@lingui/core'
|
||||
|
||||
@@ -21,6 +21,7 @@ export enum Features {
|
||||
CustomLogoJapanEnable = 'custom_logo:japan:enable',
|
||||
VideoMultipartUploadEnable = 'video:multipart_upload:enable',
|
||||
SearchStarterPacksV2Enable = 'search_starter_packs_v2:enable',
|
||||
FollowSortEnable = 'follow_sort:enable',
|
||||
|
||||
AATest = 'aa-test',
|
||||
}
|
||||
|
||||
@@ -106,6 +106,10 @@ export type Events = {
|
||||
}
|
||||
'signup:captchaSuccess': {}
|
||||
'signup:captchaFailure': {}
|
||||
'signup:captchaBackPress': {}
|
||||
'signup:createAccountFailure': {
|
||||
reason: string
|
||||
}
|
||||
'signup:fieldError': {
|
||||
field: string
|
||||
errorCount: number
|
||||
@@ -478,25 +482,30 @@ export type Events = {
|
||||
'profile:followers:view': {
|
||||
contextProfileDid: string
|
||||
isOwnProfile: boolean
|
||||
sort?: 'latest' | 'top'
|
||||
}
|
||||
'profile:followers:paginate': {
|
||||
contextProfileDid: string
|
||||
itemCount: number
|
||||
page: number
|
||||
sort?: 'latest' | 'top'
|
||||
}
|
||||
'profile:following:view': {
|
||||
contextProfileDid: string
|
||||
isOwnProfile: boolean
|
||||
sort?: 'latest' | 'top'
|
||||
}
|
||||
'profile:following:paginate': {
|
||||
contextProfileDid: string
|
||||
itemCount: number
|
||||
page: number
|
||||
sort?: 'latest' | 'top'
|
||||
}
|
||||
'profileCard:seen': {
|
||||
contextProfileDid?: string
|
||||
profileDid: string
|
||||
position?: number
|
||||
sort?: 'latest' | 'top'
|
||||
}
|
||||
'profile:mute': {}
|
||||
'profile:unmute': {}
|
||||
@@ -744,6 +753,10 @@ export type Events = {
|
||||
'trendingTopics:hide': {
|
||||
context: 'settings' | 'sidebar' | 'interstitial' | 'explore:trending'
|
||||
}
|
||||
'trendingTopic:seen': {
|
||||
context: 'sidebar' | 'interstitial' | 'explore'
|
||||
recId?: string
|
||||
}
|
||||
'trendingTopic:click': {
|
||||
context: 'sidebar' | 'interstitial' | 'explore'
|
||||
recId?: string
|
||||
|
||||
@@ -30,7 +30,6 @@ import {KeyboardEvents} from 'react-native-keyboard-controller'
|
||||
import Animated, {
|
||||
clamp,
|
||||
interpolate,
|
||||
runOnJS,
|
||||
type SharedValue,
|
||||
useAnimatedReaction,
|
||||
useAnimatedStyle,
|
||||
@@ -44,6 +43,7 @@ import {
|
||||
useSafeAreaInsets,
|
||||
} from 'react-native-safe-area-context'
|
||||
import {captureRef} from 'react-native-view-shot'
|
||||
import {scheduleOnRN} from 'react-native-worklets'
|
||||
import {Image, type ImageErrorEventData} from 'expo-image'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -89,14 +89,12 @@ const SPRING_IN: WithSpringConfig = {
|
||||
mass: 0.75,
|
||||
damping: 300,
|
||||
stiffness: 1200,
|
||||
restDisplacementThreshold: 0.01,
|
||||
}
|
||||
|
||||
const SPRING_OUT: WithSpringConfig = {
|
||||
mass: IS_IOS ? 1.25 : 0.75,
|
||||
damping: 150,
|
||||
stiffness: 1000,
|
||||
restDisplacementThreshold: 0.01,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,7 +166,7 @@ export function Root({children}: {children: React.ReactNode}) {
|
||||
// note: return location has to be reset on open,
|
||||
// rather than on close, otherwise there's a flicker
|
||||
// where the reanimated update is faster than the react render
|
||||
runOnJS(onCompletedClose)()
|
||||
scheduleOnRN(onCompletedClose)
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -333,7 +331,7 @@ export function Trigger({
|
||||
() => hoveredItemSV.get(),
|
||||
(hovered, prev) => {
|
||||
if (hovered !== prev) {
|
||||
runOnJS(setHoveredMenuItem)(hovered)
|
||||
scheduleOnRN(setHoveredMenuItem, hovered)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -345,7 +343,7 @@ export function Trigger({
|
||||
.averageTouches(true)
|
||||
.onStart(() => {
|
||||
'worklet'
|
||||
runOnJS(open)('full')
|
||||
scheduleOnRN(open, 'full')
|
||||
})
|
||||
.onUpdate(evt => {
|
||||
'worklet'
|
||||
@@ -359,7 +357,7 @@ export function Trigger({
|
||||
// as the menu may have slid into place beneath their finger
|
||||
const item = hoveredItemSV.get()
|
||||
if (item) {
|
||||
runOnJS(onTouchUpMenuItem)(item)
|
||||
scheduleOnRN(onTouchUpMenuItem, item)
|
||||
}
|
||||
})
|
||||
}, [open, hoverablesSV, onTouchUpMenuItem, hoveredItemSV, translationSV])
|
||||
|
||||
@@ -21,11 +21,11 @@ import {
|
||||
} from 'react-native'
|
||||
import {useReanimatedKeyboardAnimation} from 'react-native-keyboard-controller'
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
type ScrollEvent,
|
||||
useAnimatedStyle,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {scheduleOnRN} from 'react-native-worklets'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -197,24 +197,8 @@ export function Outer({
|
||||
/**
|
||||
* @deprecated use `Dialog.ScrollableInner` instead
|
||||
*/
|
||||
export function Inner({children, style, header}: DialogInnerProps) {
|
||||
const insets = useSafeAreaInsets()
|
||||
return (
|
||||
<>
|
||||
{header}
|
||||
<View
|
||||
style={[
|
||||
a.pt_2xl,
|
||||
a.px_xl,
|
||||
IS_LIQUID_GLASS
|
||||
? a.pb_2xl
|
||||
: {paddingBottom: insets.bottom + insets.top},
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
export function Inner(props: DialogInnerProps) {
|
||||
return <ScrollableInner {...props} />
|
||||
}
|
||||
|
||||
export const ScrollableInner = forwardRef<ScrollView, DialogInnerProps>(
|
||||
@@ -315,9 +299,9 @@ export const InnerFlatList = forwardRef<
|
||||
}
|
||||
const {contentOffset} = e
|
||||
if (contentOffset.y > 0 && !disableDrag) {
|
||||
runOnJS(setDisableDrag)(true)
|
||||
scheduleOnRN(setDisableDrag, true)
|
||||
} else if (contentOffset.y <= 1 && disableDrag) {
|
||||
runOnJS(setDisableDrag)(false)
|
||||
scheduleOnRN(setDisableDrag, false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,9 @@ export function Outer({
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `Dialog.ScrollableInner` instead
|
||||
*/
|
||||
export function Inner({
|
||||
children,
|
||||
style,
|
||||
|
||||
@@ -3,7 +3,7 @@ import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
||||
import Animated, {
|
||||
type AnimatedRef,
|
||||
measure,
|
||||
runOnJS,
|
||||
Reanimated3DefaultSpringConfig,
|
||||
scrollTo,
|
||||
type SharedValue,
|
||||
useAnimatedRef,
|
||||
@@ -13,6 +13,7 @@ import Animated, {
|
||||
withSpring,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {scheduleOnRN} from 'react-native-worklets'
|
||||
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
@@ -25,7 +26,7 @@ import {IS_IOS} from '#/env'
|
||||
*
|
||||
* All positioning is driven by a `slots` map (key → index) and translateY
|
||||
* (no discrete `top` changes). On drag end the new slot assignment is
|
||||
* computed on the UI thread first, then React state is updated via runOnJS.
|
||||
* computed on the UI thread first, then React state is updated via scheduleOnRN.
|
||||
*
|
||||
* See SortableList.web.tsx for the web implementation using pointer events.
|
||||
*/
|
||||
@@ -41,7 +42,7 @@ interface SortableListProps<T> {
|
||||
itemHeight: number
|
||||
/** Ref to the parent Animated.ScrollView for auto-scroll. */
|
||||
scrollRef?: AnimatedRef<Animated.ScrollView>
|
||||
/** Scroll offset shared value from useScrollViewOffset. */
|
||||
/** Scroll offset shared value from useScrollOffset. */
|
||||
scrollOffset?: SharedValue<number>
|
||||
}
|
||||
|
||||
@@ -237,8 +238,8 @@ function SortableItem<T>({
|
||||
itemKey: string
|
||||
itemCount: number
|
||||
itemHeight: number
|
||||
state: Animated.SharedValue<DragState>
|
||||
dragY: Animated.SharedValue<number>
|
||||
state: SharedValue<DragState>
|
||||
dragY: SharedValue<number>
|
||||
scrollCompensation: SharedValue<number>
|
||||
isGestureActive: SharedValue<boolean>
|
||||
measureDone: SharedValue<boolean>
|
||||
@@ -264,9 +265,9 @@ function SortableItem<T>({
|
||||
measureDone.set(false)
|
||||
lastHapticSlot.set(mySlot)
|
||||
if (onDragStart) {
|
||||
runOnJS(onDragStart)()
|
||||
scheduleOnRN(onDragStart)
|
||||
}
|
||||
runOnJS(playHaptic)()
|
||||
scheduleOnRN(playHaptic)
|
||||
})
|
||||
.onChange(e => {
|
||||
'worklet'
|
||||
@@ -284,7 +285,7 @@ function SortableItem<T>({
|
||||
const clampedSlot = Math.max(0, Math.min(currentSlot, itemCount - 1))
|
||||
if (IS_IOS && clampedSlot !== lastHapticSlot.get()) {
|
||||
lastHapticSlot.set(clampedSlot)
|
||||
runOnJS(playHaptic)('Light')
|
||||
scheduleOnRN(playHaptic, 'Light')
|
||||
}
|
||||
})
|
||||
.onEnd(() => {
|
||||
@@ -325,13 +326,13 @@ function SortableItem<T>({
|
||||
dragStartSlot: -1,
|
||||
})
|
||||
dragY.set(0)
|
||||
runOnJS(onCommitReorder)(sorted)
|
||||
scheduleOnRN(onCommitReorder, sorted)
|
||||
} else {
|
||||
const s = state.get()
|
||||
state.set({...s, activeKey: '', dragStartSlot: -1})
|
||||
dragY.set(0)
|
||||
if (onDragEnd) {
|
||||
runOnJS(onDragEnd)()
|
||||
scheduleOnRN(onDragEnd)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,7 +347,7 @@ function SortableItem<T>({
|
||||
const s = state.get()
|
||||
state.set({...s, activeKey: '', dragStartSlot: -1})
|
||||
if (onDragEnd) {
|
||||
runOnJS(onDragEnd)()
|
||||
scheduleOnRN(onDragEnd)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -370,18 +371,18 @@ function SortableItem<T>({
|
||||
return {
|
||||
transform: [
|
||||
{translateY: s.dragStartSlot * itemHeight + dragY.get()},
|
||||
{scale: withSpring(1.03)},
|
||||
{scale: withSpring(1.03, Reanimated3DefaultSpringConfig)},
|
||||
],
|
||||
zIndex: 999,
|
||||
...(IS_IOS
|
||||
? {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {width: 0, height: 1},
|
||||
shadowOpacity: withSpring(0.08),
|
||||
shadowRadius: withSpring(4),
|
||||
shadowOpacity: withSpring(0.08, Reanimated3DefaultSpringConfig),
|
||||
shadowRadius: withSpring(4, Reanimated3DefaultSpringConfig),
|
||||
}
|
||||
: {
|
||||
elevation: withSpring(3),
|
||||
elevation: withSpring(3, Reanimated3DefaultSpringConfig),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -391,11 +392,11 @@ function SortableItem<T>({
|
||||
const inactive = {
|
||||
...(IS_IOS
|
||||
? {
|
||||
shadowOpacity: withSpring(0),
|
||||
shadowRadius: withSpring(0),
|
||||
shadowOpacity: withSpring(0, Reanimated3DefaultSpringConfig),
|
||||
shadowRadius: withSpring(0, Reanimated3DefaultSpringConfig),
|
||||
}
|
||||
: {
|
||||
elevation: withSpring(0),
|
||||
elevation: withSpring(0, Reanimated3DefaultSpringConfig),
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -425,7 +426,7 @@ function SortableItem<T>({
|
||||
return {
|
||||
transform: [
|
||||
{translateY: withTiming(baseY + offset, {duration: 200})},
|
||||
{scale: withSpring(1)},
|
||||
{scale: withSpring(1, Reanimated3DefaultSpringConfig)},
|
||||
],
|
||||
zIndex: 0,
|
||||
...inactive,
|
||||
|
||||
@@ -22,7 +22,7 @@ interface SortableListProps<T> {
|
||||
itemHeight: number
|
||||
/** Ref to the parent Animated.ScrollView for auto-scroll. Ignored on web. */
|
||||
scrollRef?: AnimatedRef<Animated.ScrollView>
|
||||
/** Scroll offset shared value from useScrollViewOffset. Ignored on web. */
|
||||
/** Scroll offset shared value from useScrollOffset. Ignored on web. */
|
||||
scrollOffset?: SharedValue<number>
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
atoms as a,
|
||||
native,
|
||||
useBreakpoints,
|
||||
useGutters,
|
||||
useTheme,
|
||||
type ViewStyleProp,
|
||||
web,
|
||||
@@ -60,10 +61,10 @@ function CardOuter({
|
||||
a.flex_1,
|
||||
a.w_full,
|
||||
a.p_md,
|
||||
a.rounded_lg,
|
||||
a.rounded_xl,
|
||||
a.border,
|
||||
t.atoms.bg,
|
||||
t.atoms.shadow_sm,
|
||||
t.atoms.shadow_md,
|
||||
t.atoms.border_contrast_low,
|
||||
!gtMobile && {
|
||||
width: MOBILE_CARD_WIDTH,
|
||||
@@ -204,6 +205,7 @@ export function ProfileGrid({
|
||||
const {t: l} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const gutters = useGutters([0, 'base'])
|
||||
const followDialogControl = useDialogControl()
|
||||
|
||||
const isLoading = isSuggestionsLoading || !moderationOpts
|
||||
@@ -414,7 +416,7 @@ export function ProfileGrid({
|
||||
moderationOpts={moderationOpts}
|
||||
logContext="FeedInterstitial"
|
||||
withIcon={false}
|
||||
style={[a.rounded_sm]}
|
||||
style={[a.rounded_full]}
|
||||
onFollow={() => {
|
||||
ax.metric('suggestedUser:follow', {
|
||||
logContext,
|
||||
@@ -458,40 +460,43 @@ export function ProfileGrid({
|
||||
pointerEvents={IS_IOS ? 'auto' : 'box-none'}>
|
||||
<View
|
||||
style={[
|
||||
a.px_lg,
|
||||
gutters,
|
||||
a.pt_md,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_between,
|
||||
]}
|
||||
pointerEvents={IS_IOS ? 'auto' : 'box-none'}>
|
||||
<Text style={[a.text_sm, a.font_semi_bold, t.atoms.text]}>
|
||||
<Trans>Suggested for you</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={l`See more suggested profiles`}
|
||||
onPress={() => {
|
||||
followDialogControl.open()
|
||||
ax.metric('suggestedUser:seeMore', {
|
||||
logContext,
|
||||
recId,
|
||||
})
|
||||
}}>
|
||||
{({hovered}) => (
|
||||
<Text
|
||||
style={[
|
||||
a.text_sm,
|
||||
t.atoms.text_link,
|
||||
hovered &&
|
||||
web({
|
||||
textDecorationLine: 'underline',
|
||||
textDecorationColor: t.atoms.text_link.color,
|
||||
}),
|
||||
]}>
|
||||
<Trans>See more</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</Button>
|
||||
<View style={[a.w_full, a.pl_xs, a.flex_row, a.align_center]}>
|
||||
<Text style={[a.flex_1, a.text_md, a.font_semi_bold]}>
|
||||
<Trans>Suggested for you</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={l`See more suggested profiles`}
|
||||
onPress={() => {
|
||||
followDialogControl.open()
|
||||
ax.metric('suggestedUser:seeMore', {
|
||||
logContext,
|
||||
recId,
|
||||
})
|
||||
}}>
|
||||
{({hovered, pressed}) => (
|
||||
<Text
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.font_medium,
|
||||
{
|
||||
color:
|
||||
hovered || pressed
|
||||
? t.palette.contrast_800
|
||||
: t.palette.contrast_500,
|
||||
},
|
||||
]}>
|
||||
<Trans>See more</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
<FollowDialogWithoutGuide control={followDialogControl} />
|
||||
<LayoutAnimationConfig skipExiting skipEntering>
|
||||
@@ -540,7 +545,7 @@ function SeeMoreSuggestedProfilesCard({onPress}: {onPress: () => void}) {
|
||||
a.justify_center,
|
||||
a.gap_sm,
|
||||
a.p_md,
|
||||
a.rounded_lg,
|
||||
a.rounded_xl,
|
||||
{width: FINAL_CARD_WIDTH},
|
||||
]}>
|
||||
<ButtonIcon icon={ArrowRight} size="lg" />
|
||||
|
||||
@@ -15,6 +15,8 @@ export const IS_GLASS_AVAILABLE =
|
||||
* Liquid Glass View that uses `expo-glass-effect`
|
||||
*
|
||||
* If unavailable, falls back to a regular `View`. Use `fallbackStyle` to customize the fallback appearance.
|
||||
* Note: Setting opacity to 0 on Expo GlassView or any of its parent views causes the glass effect to not render at all. https://docs.expo.dev/versions/v56.0.0/sdk/glass-effect/#known-issues
|
||||
* If animating the opacity of a parent view, start from a non-zero opacity to avoid this issue.
|
||||
*/
|
||||
export const GlassView = IS_GLASS_AVAILABLE ? InnerGlassView : FallbackView
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {DraggableScrollView} from '#/view/com/pager/DraggableScrollView'
|
||||
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||
import {atoms as a, tokens, useTheme, web} from '#/alf'
|
||||
import {transparentifyColor} from '#/alf/util/colorGeneration'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
@@ -200,42 +201,44 @@ export function InterestTabs({
|
||||
|
||||
return (
|
||||
<View style={[a.relative, a.flex_row]}>
|
||||
<DraggableScrollView
|
||||
ref={listRef}
|
||||
contentContainerStyle={[
|
||||
a.gap_sm,
|
||||
{paddingHorizontal: gutterWidth},
|
||||
contentContainerStyle,
|
||||
]}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
decelerationRate="fast"
|
||||
snapToOffsets={
|
||||
tabOffsets.filter(o => !!o).length === interests.length
|
||||
? tabOffsets.map(o => o.x - tokens.space.xl)
|
||||
: undefined
|
||||
}
|
||||
onLayout={evt => setTotalWidth(evt.nativeEvent.layout.width)}
|
||||
onContentSizeChange={width => setContentWidth(width)}
|
||||
onScroll={evt => {
|
||||
const newScrollX = evt.nativeEvent.contentOffset.x
|
||||
setScrollX(newScrollX)
|
||||
}}
|
||||
scrollEventThrottle={16}>
|
||||
{interests.map((interest, i) => {
|
||||
const active = interest === selectedInterest && !disabled
|
||||
return (
|
||||
<TabComponent
|
||||
key={interest}
|
||||
onSelectTab={handleSelectTab}
|
||||
active={active}
|
||||
index={i}
|
||||
interest={interest}
|
||||
interestsDisplayName={interestsDisplayNames[interest]}
|
||||
onLayout={handleTabLayout}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</DraggableScrollView>
|
||||
<BlockDrawerGesture>
|
||||
<DraggableScrollView
|
||||
ref={listRef}
|
||||
contentContainerStyle={[
|
||||
a.gap_sm,
|
||||
{paddingHorizontal: gutterWidth},
|
||||
contentContainerStyle,
|
||||
]}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
decelerationRate="fast"
|
||||
snapToOffsets={
|
||||
tabOffsets.filter(o => !!o).length === interests.length
|
||||
? tabOffsets.map(o => o.x - tokens.space.xl)
|
||||
: undefined
|
||||
}
|
||||
onLayout={evt => setTotalWidth(evt.nativeEvent.layout.width)}
|
||||
onContentSizeChange={width => setContentWidth(width)}
|
||||
onScroll={evt => {
|
||||
const newScrollX = evt.nativeEvent.contentOffset.x
|
||||
setScrollX(newScrollX)
|
||||
}}
|
||||
scrollEventThrottle={16}>
|
||||
{interests.map((interest, i) => {
|
||||
const active = interest === selectedInterest && !disabled
|
||||
return (
|
||||
<TabComponent
|
||||
key={interest}
|
||||
onSelectTab={handleSelectTab}
|
||||
active={active}
|
||||
index={i}
|
||||
interest={interest}
|
||||
interestsDisplayName={interestsDisplayNames[interest]}
|
||||
onLayout={handleTabLayout}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</DraggableScrollView>
|
||||
</BlockDrawerGesture>
|
||||
{IS_WEB && canScrollLeft && (
|
||||
<View
|
||||
style={[
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {forwardRef, memo, useContext, useMemo} from 'react'
|
||||
import {memo, useContext, useMemo} from 'react'
|
||||
import {
|
||||
type StyleProp,
|
||||
View,
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import Animated, {
|
||||
type AnimatedRef,
|
||||
type AnimatedScrollViewProps,
|
||||
useAnimatedStyle,
|
||||
} from 'react-native-reanimated'
|
||||
@@ -73,67 +74,64 @@ export type ContentProps = AnimatedScrollViewProps & {
|
||||
style?: StyleProp<ViewStyle>
|
||||
contentContainerStyle?: StyleProp<ViewStyle>
|
||||
ignoreTabletLayoutOffset?: boolean
|
||||
ref?: AnimatedRef<Animated.ScrollView>
|
||||
}
|
||||
|
||||
/**
|
||||
* Default scroll view for simple pages
|
||||
*/
|
||||
export const Content = memo(
|
||||
forwardRef<Animated.ScrollView, ContentProps>(function Content(
|
||||
{
|
||||
children,
|
||||
style,
|
||||
contentContainerStyle,
|
||||
ignoreTabletLayoutOffset,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const t = useTheme()
|
||||
const {footerHeight} = useShellLayout()
|
||||
const {isWithinSplitView} = useIsWithinSplitView()
|
||||
export const Content = memo(function Content({
|
||||
children,
|
||||
style,
|
||||
contentContainerStyle,
|
||||
ignoreTabletLayoutOffset,
|
||||
ref,
|
||||
...props
|
||||
}: ContentProps) {
|
||||
const t = useTheme()
|
||||
const {footerHeight} = useShellLayout()
|
||||
const {isWithinSplitView} = useIsWithinSplitView()
|
||||
|
||||
// note - if we ever make the footer transparent in any way,
|
||||
// we'll need to change this to use contentInsets/scrollIndicatorInsets
|
||||
// on iOS and contentContainerStyle padding on Android -sfn
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
marginBottom: footerHeight.get(),
|
||||
}
|
||||
})
|
||||
// note - if we ever make the footer transparent in any way,
|
||||
// we'll need to change this to use contentInsets/scrollIndicatorInsets
|
||||
// on iOS and contentContainerStyle padding on Android -sfn
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
marginBottom: footerHeight.get(),
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Animated.ScrollView
|
||||
ref={ref}
|
||||
id="content"
|
||||
automaticallyAdjustsScrollIndicatorInsets={false}
|
||||
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
|
||||
style={[
|
||||
a.w_full,
|
||||
animatedStyle,
|
||||
isWithinSplitView &&
|
||||
web({
|
||||
flex: 1,
|
||||
overflowY: 'scroll',
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: `${t.palette.contrast_100} transparent`,
|
||||
}),
|
||||
style,
|
||||
]}
|
||||
contentContainerStyle={[contentContainerStyle]}
|
||||
{...props}>
|
||||
{IS_WEB ? (
|
||||
<Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}>
|
||||
{/* @ts-expect-error web only -esb */}
|
||||
{children}
|
||||
</Center>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Animated.ScrollView>
|
||||
)
|
||||
}),
|
||||
)
|
||||
return (
|
||||
<Animated.ScrollView
|
||||
ref={ref}
|
||||
id="content"
|
||||
automaticallyAdjustsScrollIndicatorInsets={false}
|
||||
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
|
||||
style={[
|
||||
a.w_full,
|
||||
animatedStyle,
|
||||
isWithinSplitView &&
|
||||
web({
|
||||
flex: 1,
|
||||
overflowY: 'scroll',
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: `${t.palette.contrast_100} transparent`,
|
||||
}),
|
||||
style,
|
||||
]}
|
||||
contentContainerStyle={[contentContainerStyle]}
|
||||
{...props}>
|
||||
{IS_WEB ? (
|
||||
<Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}>
|
||||
{/* @ts-expect-error web only -esb */}
|
||||
{children}
|
||||
</Center>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Animated.ScrollView>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Utility component to center content within the screen
|
||||
|
||||
@@ -2,12 +2,12 @@ import {useRef, useState} from 'react'
|
||||
import {Modal, Pressable, StyleSheet, View} from 'react-native'
|
||||
import Animated, {
|
||||
interpolate,
|
||||
runOnJS,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withSpring,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {scheduleOnRN} from 'react-native-worklets'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
@@ -52,7 +52,7 @@ export function ImageMenu({onPressShare, onPressSave}: Props) {
|
||||
progress.set(
|
||||
withTiming(0, TIMING_OUT, finished => {
|
||||
if (finished) {
|
||||
runOnJS(setIsMounted)(false)
|
||||
scheduleOnRN(setIsMounted, false)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from 'react-native-gesture-handler'
|
||||
import Animated, {
|
||||
type AnimatableValue,
|
||||
runOnJS,
|
||||
Reanimated3DefaultSpringConfig,
|
||||
type SharedValue,
|
||||
useAnimatedReaction,
|
||||
useAnimatedRef,
|
||||
@@ -15,6 +15,7 @@ import Animated, {
|
||||
useSharedValue,
|
||||
withSpring,
|
||||
} from 'react-native-reanimated'
|
||||
import {scheduleOnRN} from 'react-native-worklets'
|
||||
import {Image} from 'expo-image'
|
||||
|
||||
import {
|
||||
@@ -94,7 +95,7 @@ const ImageItem = ({
|
||||
},
|
||||
(nextIsScaled, prevIsScaled) => {
|
||||
if (nextIsScaled !== prevIsScaled) {
|
||||
runOnJS(handleZoom)(nextIsScaled)
|
||||
scheduleOnRN(handleZoom, nextIsScaled)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -244,7 +245,7 @@ const ImageItem = ({
|
||||
|
||||
const singleTap = Gesture.Tap().onEnd(() => {
|
||||
'worklet'
|
||||
runOnJS(onTap)()
|
||||
scheduleOnRN(onTap)
|
||||
})
|
||||
|
||||
const doubleTap = Gesture.Tap()
|
||||
@@ -358,9 +359,9 @@ const ImageItem = ({
|
||||
},
|
||||
(show, prevShow) => {
|
||||
if (!prevShow && show) {
|
||||
runOnJS(setShowLoader)(true)
|
||||
scheduleOnRN(setShowLoader, true)
|
||||
} else if (prevShow && !show) {
|
||||
runOnJS(setShowLoader)(false)
|
||||
scheduleOnRN(setShowLoader, false)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -463,7 +464,10 @@ function clampTranslation(
|
||||
|
||||
function withClampedSpring<T extends AnimatableValue>(value: T): T {
|
||||
'worklet'
|
||||
return withSpring(value, {overshootClamping: true})
|
||||
return withSpring(value, {
|
||||
...Reanimated3DefaultSpringConfig,
|
||||
overshootClamping: true,
|
||||
})
|
||||
}
|
||||
|
||||
export default memo(ImageItem)
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
type PanGesture,
|
||||
} from 'react-native-gesture-handler'
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
type SharedValue,
|
||||
useAnimatedProps,
|
||||
useAnimatedReaction,
|
||||
@@ -24,6 +23,7 @@ import Animated, {
|
||||
useSharedValue,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaFrame} from 'react-native-safe-area-context'
|
||||
import {scheduleOnRN} from 'react-native-worklets'
|
||||
import {Image} from 'expo-image'
|
||||
|
||||
import {
|
||||
@@ -84,7 +84,7 @@ const ImageItem = ({
|
||||
'worklet'
|
||||
const nextIsScaled = e.zoomScale > 1
|
||||
if (scaled !== nextIsScaled) {
|
||||
runOnJS(handleZoom)(nextIsScaled)
|
||||
scheduleOnRN(handleZoom, nextIsScaled)
|
||||
}
|
||||
},
|
||||
onBeginDrag() {
|
||||
@@ -118,7 +118,7 @@ const ImageItem = ({
|
||||
|
||||
const singleTap = Gesture.Tap().onEnd(() => {
|
||||
'worklet'
|
||||
runOnJS(onTap)()
|
||||
scheduleOnRN(onTap)
|
||||
})
|
||||
|
||||
const doubleTap = Gesture.Tap()
|
||||
@@ -142,7 +142,7 @@ const ImageItem = ({
|
||||
screenSize,
|
||||
)
|
||||
}
|
||||
runOnJS(zoomTo)(nextZoomRect)
|
||||
scheduleOnRN(zoomTo, nextZoomRect)
|
||||
})
|
||||
|
||||
const composedGesture = Gesture.Exclusive(
|
||||
@@ -170,8 +170,6 @@ const ImageItem = ({
|
||||
width: screenSize.width,
|
||||
maxHeight: screenSize.height,
|
||||
alignSelf: 'center',
|
||||
aspectRatio: imageAspect ?? 1 /* force onLoad */,
|
||||
opacity: imageAspect === undefined ? 0 : 1,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -180,11 +178,19 @@ const ImageItem = ({
|
||||
return {
|
||||
transform: cropContentTransform,
|
||||
width: '100%',
|
||||
aspectRatio: imageAspect ?? 1 /* force onLoad */,
|
||||
opacity: imageAspect === undefined ? 0 : 1,
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
* When the aspect ratio is unknown until onLoad fires, these layout props
|
||||
* change after mount. They must be applied via a React render rather than
|
||||
* useAnimatedStyle
|
||||
*/
|
||||
const imageLayoutStyle = {
|
||||
aspectRatio: imageAspect ?? 1 /* force onLoad */,
|
||||
opacity: imageAspect === undefined ? 0 : 1,
|
||||
}
|
||||
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
const [hasLoaded, setHasLoaded] = useState(false)
|
||||
useAnimatedReaction(
|
||||
@@ -193,9 +199,9 @@ const ImageItem = ({
|
||||
},
|
||||
(show, prevShow) => {
|
||||
if (!prevShow && show) {
|
||||
runOnJS(setShowLoader)(true)
|
||||
scheduleOnRN(setShowLoader, true)
|
||||
} else if (prevShow && !show) {
|
||||
runOnJS(setShowLoader)(false)
|
||||
scheduleOnRN(setShowLoader, false)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -225,8 +231,8 @@ const ImageItem = ({
|
||||
{showLoader && (
|
||||
<ActivityIndicator size="small" color="#FFF" style={styles.loading} />
|
||||
)}
|
||||
<Animated.View style={imageCropStyle}>
|
||||
<Animated.View style={imageStyle}>
|
||||
<Animated.View style={[imageCropStyle, imageLayoutStyle]}>
|
||||
<Animated.View style={[imageStyle, imageLayoutStyle]}>
|
||||
<Image
|
||||
contentFit="contain"
|
||||
source={{uri: imageSrc.uri}}
|
||||
|
||||
@@ -20,8 +20,6 @@ import Animated, {
|
||||
measure,
|
||||
type MeasuredDimensions,
|
||||
ReduceMotion,
|
||||
runOnJS,
|
||||
runOnUI,
|
||||
type SharedValue,
|
||||
useAnimatedReaction,
|
||||
useAnimatedRef,
|
||||
@@ -32,6 +30,7 @@ import Animated, {
|
||||
withSpring,
|
||||
type WithSpringConfig,
|
||||
} from 'react-native-reanimated'
|
||||
import {scheduleOnRN, scheduleOnUI} from 'react-native-worklets'
|
||||
import {Image} from 'expo-image'
|
||||
import * as ScreenOrientation from 'expo-screen-orientation'
|
||||
|
||||
@@ -60,13 +59,11 @@ const SLOW_SPRING: WithSpringConfig = {
|
||||
mass: IS_IOS ? 1.25 : 0.75,
|
||||
damping: 300,
|
||||
stiffness: 800,
|
||||
restDisplacementThreshold: 0.001,
|
||||
}
|
||||
const FAST_SPRING: WithSpringConfig = {
|
||||
mass: IS_IOS ? 1.25 : 0.75,
|
||||
damping: 150,
|
||||
stiffness: 900,
|
||||
restDisplacementThreshold: 0.001,
|
||||
}
|
||||
|
||||
function canAnimate(lightbox: Lightbox): boolean {
|
||||
@@ -138,10 +135,10 @@ export default function ImageViewRoot({
|
||||
|
||||
const onFullyClosed = useCallback(() => {
|
||||
setActiveLightbox(null)
|
||||
runOnUI(() => {
|
||||
scheduleOnUI(() => {
|
||||
'worklet'
|
||||
thumbRects.set({})
|
||||
})()
|
||||
})
|
||||
requestIdleCallback(() => {
|
||||
void Image.clearMemoryCache()
|
||||
})
|
||||
@@ -151,7 +148,7 @@ export default function ImageViewRoot({
|
||||
() => openProgress.get() === 0,
|
||||
(isGone, wasGone) => {
|
||||
if (isGone && !wasGone) {
|
||||
runOnJS(onFullyClosed)()
|
||||
scheduleOnRN(onFullyClosed)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -162,10 +159,10 @@ export default function ImageViewRoot({
|
||||
() => openProgress.get() === 1,
|
||||
(isOpen, wasOpen) => {
|
||||
if (isOpen && !wasOpen) {
|
||||
runOnJS(ScreenOrientation.unlockAsync)()
|
||||
scheduleOnRN(ScreenOrientation.unlockAsync)
|
||||
} else if (!isOpen && wasOpen) {
|
||||
// default is PORTRAIT_UP - set via config plugin in app.config.js -sfn
|
||||
runOnJS(ScreenOrientation.lockAsync)(PORTRAIT_UP)
|
||||
scheduleOnRN(ScreenOrientation.lockAsync, PORTRAIT_UP)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -173,7 +170,7 @@ export default function ImageViewRoot({
|
||||
const onFlyAway = useCallback(() => {
|
||||
'worklet'
|
||||
openProgress.set(0)
|
||||
runOnJS(onRequestClose)()
|
||||
scheduleOnRN(onRequestClose)
|
||||
}, [onRequestClose, openProgress])
|
||||
|
||||
return (
|
||||
@@ -322,7 +319,7 @@ function ImageView({
|
||||
const handleRequestClose = useCallback(() => {
|
||||
const activeRef = images[imageIndex]?.thumbRef
|
||||
if (isAnimated && activeRef) {
|
||||
runOnUI(() => {
|
||||
scheduleOnUI(() => {
|
||||
'worklet'
|
||||
const rect = measure(activeRef)
|
||||
thumbRects.modify(rects => {
|
||||
@@ -330,8 +327,8 @@ function ImageView({
|
||||
rects[imageIndex] = rect
|
||||
return rects
|
||||
})
|
||||
runOnJS(onRequestClose)()
|
||||
})()
|
||||
scheduleOnRN(onRequestClose)
|
||||
})
|
||||
} else {
|
||||
onRequestClose()
|
||||
}
|
||||
@@ -532,7 +529,7 @@ function LightboxImage({
|
||||
const dismissTranslateY =
|
||||
isActive && openProgressValue === 1 ? dismissSwipeTranslateY.get() : 0
|
||||
|
||||
if (openProgressValue === 0 && isFlyingAway.get()) {
|
||||
if (openProgressValue === 0) {
|
||||
return {
|
||||
isHidden: true,
|
||||
isResting: false,
|
||||
@@ -609,6 +606,7 @@ function LightboxImage({
|
||||
return withSpring(0, {
|
||||
stiffness: 700,
|
||||
damping: 50,
|
||||
mass: 1,
|
||||
reduceMotion: ReduceMotion.Never,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import {createContext, useContext, useEffect, useMemo, useState} from 'react'
|
||||
import {
|
||||
measure,
|
||||
type MeasuredDimensions,
|
||||
runOnJS,
|
||||
runOnUI,
|
||||
} from 'react-native-reanimated'
|
||||
import {measure, type MeasuredDimensions} from 'react-native-reanimated'
|
||||
import {scheduleOnRN, scheduleOnUI} from 'react-native-worklets'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
@@ -73,7 +69,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
if (thumbRef) {
|
||||
// Measure the tapped image on the UI thread, then open with
|
||||
// the rect baked in so it's available from the first render.
|
||||
// Only the rect (plain data) goes through runOnJS — AnimatedRef
|
||||
// Only the rect (plain data) goes through scheduleOnRN — AnimatedRef
|
||||
// objects can't survive serialization across threads.
|
||||
const openWithRect = (rect: MeasuredDimensions | null) => {
|
||||
doOpen({
|
||||
@@ -83,11 +79,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
),
|
||||
})
|
||||
}
|
||||
runOnUI(() => {
|
||||
scheduleOnUI(() => {
|
||||
'worklet'
|
||||
const rect = measure(thumbRef)
|
||||
runOnJS(openWithRect)(rect)
|
||||
})()
|
||||
scheduleOnRN(openWithRect, rect)
|
||||
})
|
||||
} else {
|
||||
doOpen(lightbox)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import {type Component} from 'react'
|
||||
import {type TransformsStyle} from 'react-native'
|
||||
import {
|
||||
type AnimatedRef,
|
||||
@@ -29,7 +28,7 @@ export type ImageSource = {
|
||||
thumbUri: string
|
||||
thumbDimensions: Dimensions | null
|
||||
thumbRect: MeasuredDimensions | null
|
||||
thumbRef?: AnimatedRef<Component> | null
|
||||
thumbRef?: AnimatedRef | null
|
||||
thumbBorderRadius?: number
|
||||
alt?: string
|
||||
type: 'image' | 'circle-avi' | 'rect-avi'
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as Admonition from '#/components/Admonition'
|
||||
import {ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
/**
|
||||
* Warns that the running bundle came from a channel this build doesn't normally
|
||||
* receive updates from, e.g. a pull request deployment applied from the dev
|
||||
* settings. Renders nothing on a standard channel, and never on web.
|
||||
*/
|
||||
export function OTAChannelNotice({style}: {style?: StyleProp<ViewStyle>}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const {
|
||||
currentChannel,
|
||||
defaultChannel,
|
||||
isCurrentlyRunningNonStandardChannel,
|
||||
restoreDefaultChannel,
|
||||
pending,
|
||||
} = useApplyPullRequestOTAUpdate()
|
||||
|
||||
if (!isCurrentlyRunningNonStandardChannel) return null
|
||||
|
||||
return (
|
||||
<Admonition.Outer
|
||||
type="warning"
|
||||
style={[t.atoms.bg_contrast_25, a.gap_sm, style]}>
|
||||
<Admonition.Row>
|
||||
<Admonition.Icon />
|
||||
<Admonition.Content style={[a.gap_2xs]}>
|
||||
<Text style={[a.text_sm, a.font_bold, a.leading_snug]}>
|
||||
<Trans>Non-standard OTA channel</Trans>
|
||||
</Text>
|
||||
<Admonition.Text>
|
||||
<Trans>
|
||||
This app is running a deployment of{' '}
|
||||
<Text style={[a.text_sm, a.font_bold, a.leading_snug]}>
|
||||
{currentChannel}
|
||||
</Text>
|
||||
. Restore the {defaultChannel} deployment to get back to a
|
||||
standard build.
|
||||
</Trans>
|
||||
</Admonition.Text>
|
||||
</Admonition.Content>
|
||||
</Admonition.Row>
|
||||
<View style={[a.flex_row]}>
|
||||
<Admonition.Button
|
||||
color="secondary_inverted"
|
||||
label={l`Restore the ${defaultChannel} deployment`}
|
||||
disabled={pending}
|
||||
onPress={() => void restoreDefaultChannel()}>
|
||||
<ButtonText>
|
||||
<Trans>Restore default</Trans>
|
||||
</ButtonText>
|
||||
{pending && <ButtonIcon icon={Loader} />}
|
||||
</Admonition.Button>
|
||||
</View>
|
||||
</Admonition.Outer>
|
||||
)
|
||||
}
|
||||
@@ -177,6 +177,7 @@ export function LabelBase({
|
||||
text,
|
||||
a.font_semi_bold,
|
||||
a.leading_tight,
|
||||
a.flex_shrink,
|
||||
t.atoms.text_contrast_medium,
|
||||
{paddingRight: 3},
|
||||
]}>
|
||||
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
} from 'react-native'
|
||||
import Animated, {
|
||||
measure,
|
||||
runOnJS,
|
||||
useAnimatedRef,
|
||||
useFrameCallback,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {WebView} from 'react-native-webview'
|
||||
import {scheduleOnRN} from 'react-native-worklets'
|
||||
import {Image} from 'expo-image'
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
@@ -164,7 +164,7 @@ export function ExternalPlayer({
|
||||
const isVisible = top <= realWinHeight - insets.bottom && bot >= insets.top
|
||||
|
||||
if (!isVisible) {
|
||||
runOnJS(setIsPlayerActive)(false)
|
||||
scheduleOnRN(setIsPlayerActive, false)
|
||||
}
|
||||
}, false) // False here disables autostarting the callback
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export function ImageEmbed({
|
||||
|
||||
// Captured from AutoSizedImage so the peek-commit handler can reuse the same
|
||||
// ref + dims that a tap would — keeps the lightbox's return animation intact.
|
||||
const singleContainerRef = useRef<AnimatedRef<React.Component> | null>(null)
|
||||
const singleContainerRef = useRef<AnimatedRef | null>(null)
|
||||
const singleDimsRef = useRef<Dimensions | null>(null)
|
||||
|
||||
if (images.length > 0) {
|
||||
@@ -71,7 +71,7 @@ export function ImageEmbed({
|
||||
}))
|
||||
const onPress = (
|
||||
index: number,
|
||||
refs: AnimatedRef<React.Component>[],
|
||||
refs: AnimatedRef[],
|
||||
fetchedDims: (Dimensions | null)[],
|
||||
) => {
|
||||
if (postContext) {
|
||||
|
||||
@@ -601,7 +601,7 @@ export function FollowButtonPlaceholder({style}: ViewStyleProp) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
a.rounded_full,
|
||||
t.atoms.bg_contrast_50,
|
||||
a.w_full,
|
||||
{
|
||||
|
||||
@@ -5,9 +5,7 @@ import {
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
|
||||
import {useSession} from '#/state/session'
|
||||
@@ -29,7 +27,7 @@ const TOTAL_AVATARS = 10
|
||||
|
||||
export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
const {rightNavVisible} = useLayoutBreakpoints()
|
||||
const {currentAccount} = useSession()
|
||||
@@ -79,7 +77,7 @@ export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
|
||||
size="tiny"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
label={_(msg`Dismiss getting started guide`)}
|
||||
label={l`Dismiss getting started guide`}
|
||||
onPress={endProgressGuide}
|
||||
style={[a.bg_transparent, {marginTop: -6, marginRight: -6}]}>
|
||||
<ButtonIcon icon={Times} size="xs" />
|
||||
@@ -107,14 +105,14 @@ export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
|
||||
<ProgressGuideTask
|
||||
current={guide.numLikes + 1}
|
||||
total={10 + 1}
|
||||
title={_(msg`Like 10 posts`)}
|
||||
subtitle={_(msg`Teach our algorithm what you like`)}
|
||||
title={l`Like 10 posts`}
|
||||
subtitle={l`Teach our algorithm what you like`}
|
||||
/>
|
||||
<ProgressGuideTask
|
||||
current={guide.numFollows + 1}
|
||||
total={7 + 1}
|
||||
title={_(msg`Follow 7 accounts`)}
|
||||
subtitle={_(msg`Bluesky is better with friends!`)}
|
||||
title={l`Follow 7 accounts`}
|
||||
subtitle={l`Bluesky is better with friends!`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {View} from 'react-native'
|
||||
import * as Progress from 'react-native-progress'
|
||||
import {Circle as ProgressCircle} from 'react-native-progress'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {AnimatedCheck} from '../anim/AnimatedCheck'
|
||||
@@ -25,7 +25,7 @@ export function ProgressGuideTask({
|
||||
{current === total ? (
|
||||
<AnimatedCheck playOnMount fill={t.palette.primary_500} width={20} />
|
||||
) : (
|
||||
<Progress.Circle
|
||||
<ProgressCircle
|
||||
progress={current / total}
|
||||
color={t.palette.primary_400}
|
||||
size={20}
|
||||
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
import {Pressable, useWindowDimensions, View} from 'react-native'
|
||||
import Animated, {
|
||||
Easing,
|
||||
runOnJS,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {scheduleOnRN} from 'react-native-worklets'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -69,7 +69,7 @@ export const ProgressGuideToast = forwardRef<
|
||||
duration: 400,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
},
|
||||
() => runOnJS(setIsntOpen)(),
|
||||
() => scheduleOnRN(setIsntOpen),
|
||||
),
|
||||
)
|
||||
}, [setIsOpen, opacity])
|
||||
@@ -88,7 +88,7 @@ export const ProgressGuideToast = forwardRef<
|
||||
duration: 100,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
},
|
||||
() => runOnJS(playCheckmark)(),
|
||||
() => scheduleOnRN(playCheckmark),
|
||||
),
|
||||
)
|
||||
translateY.set(0)
|
||||
@@ -119,7 +119,8 @@ export const ProgressGuideToast = forwardRef<
|
||||
left = right = (winDim.width - 380) / 2
|
||||
}
|
||||
return {
|
||||
position: IS_WEB ? 'fixed' : 'absolute',
|
||||
// position: fixed is web only
|
||||
position: (IS_WEB ? 'fixed' : 'absolute') as 'absolute',
|
||||
top: 0,
|
||||
left,
|
||||
right,
|
||||
@@ -134,12 +135,7 @@ export const ProgressGuideToast = forwardRef<
|
||||
return (
|
||||
isOpen && (
|
||||
<Portal>
|
||||
<Animated.View
|
||||
style={[
|
||||
// @ts-ignore position: fixed is web only
|
||||
containerStyle,
|
||||
animatedStyle,
|
||||
]}>
|
||||
<Animated.View style={[containerStyle, animatedStyle]}>
|
||||
<Pressable
|
||||
style={[
|
||||
t.atoms.bg,
|
||||
|
||||
@@ -425,9 +425,10 @@ function Bubble({
|
||||
},
|
||||
]}
|
||||
onLayout={e => {
|
||||
setBubbleMeasurements({
|
||||
width: e.nativeEvent.layout.width,
|
||||
height: e.nativeEvent.layout.height,
|
||||
const {width, height} = e.nativeEvent.layout
|
||||
setBubbleMeasurements(prev => {
|
||||
if (prev?.width === width && prev.height === height) return prev
|
||||
return {width, height}
|
||||
})
|
||||
}}>
|
||||
{children}
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import {useMemo} from 'react'
|
||||
import {useEffect, useMemo} from 'react'
|
||||
import {type AppBskyUnspeccedDefs, type AtUri} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {PressableScale} from '#/lib/custom-animations/PressableScale'
|
||||
import {useCallOnce} from '#/lib/once'
|
||||
// import {makeProfileLink} from '#/lib/routes/links'
|
||||
// import {feedUriToHref} from '#/lib/strings/url-helpers'
|
||||
import {native} from '#/alf'
|
||||
import {Link as InternalLink, type LinkProps} from '#/components/Link'
|
||||
import {type Metrics, useAnalytics} from '#/analytics'
|
||||
|
||||
export function TrendingTopicLink({
|
||||
topic: raw,
|
||||
metricContext,
|
||||
recId,
|
||||
children,
|
||||
...rest
|
||||
}: {
|
||||
topic: AppBskyUnspeccedDefs.TrendView
|
||||
metricContext: Metrics['trendingTopic:seen']['context']
|
||||
recId?: string
|
||||
} & Omit<LinkProps, 'to' | 'label'>) {
|
||||
const topic = useTopic(raw)
|
||||
useTrendingTopicSeen(metricContext, recId)
|
||||
|
||||
return (
|
||||
<InternalLink
|
||||
@@ -29,6 +35,20 @@ export function TrendingTopicLink({
|
||||
)
|
||||
}
|
||||
|
||||
export function useTrendingTopicSeen(
|
||||
context: Metrics['trendingTopic:seen']['context'],
|
||||
recId?: string,
|
||||
) {
|
||||
const ax = useAnalytics()
|
||||
const trackSeen = useCallOnce(() => {
|
||||
ax.metric('trendingTopic:seen', {context, recId})
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
trackSeen()
|
||||
}, [trackSeen])
|
||||
}
|
||||
|
||||
type ParsedTrendingTopic =
|
||||
| {
|
||||
type: 'topic' | 'tag' | 'starter-pack' | 'unknown'
|
||||
@@ -48,14 +68,14 @@ type ParsedTrendingTopic =
|
||||
export function useTopic(
|
||||
raw: AppBskyUnspeccedDefs.TrendView,
|
||||
): ParsedTrendingTopic {
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
return useMemo(() => {
|
||||
const {topic: displayName, link} = raw
|
||||
|
||||
if (link.startsWith('/search')) {
|
||||
return {
|
||||
type: 'topic',
|
||||
label: _(msg`Browse posts about ${displayName}`),
|
||||
label: l`Browse posts about ${displayName}`,
|
||||
displayName,
|
||||
uri: undefined,
|
||||
url: link,
|
||||
@@ -63,7 +83,7 @@ export function useTopic(
|
||||
} else if (link.startsWith('/hashtag')) {
|
||||
return {
|
||||
type: 'tag',
|
||||
label: _(msg`Browse posts tagged with ${displayName}`),
|
||||
label: l`Browse posts tagged with ${displayName}`,
|
||||
displayName,
|
||||
// displayName: displayName.replace(/^#/, ''),
|
||||
uri: undefined,
|
||||
@@ -72,7 +92,7 @@ export function useTopic(
|
||||
} else if (link.startsWith('/starter-pack')) {
|
||||
return {
|
||||
type: 'starter-pack',
|
||||
label: _(msg`Browse starter pack ${displayName}`),
|
||||
label: l`Browse starter pack ${displayName}`,
|
||||
displayName,
|
||||
uri: undefined,
|
||||
url: link,
|
||||
@@ -109,10 +129,10 @@ export function useTopic(
|
||||
|
||||
return {
|
||||
type: 'unknown',
|
||||
label: _(msg`Browse topic ${displayName}`),
|
||||
label: l`Browse topic ${displayName}`,
|
||||
displayName,
|
||||
uri: undefined,
|
||||
url: link,
|
||||
}
|
||||
}, [_, raw])
|
||||
}, [l, raw])
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {UITextView} from 'react-native-uitextview'
|
||||
import {UITextView} from '@bsky.app/react-native-uitextview'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {atoms as a, type TextStyleProp, useAlf, useTheme, web} from '#/alf'
|
||||
|
||||
@@ -9,8 +9,7 @@ import {
|
||||
AppBskyFeedPost,
|
||||
type ModerationDecision,
|
||||
} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {formatCount} from '#/view/com/util/numeric/format'
|
||||
@@ -52,7 +51,7 @@ export function VideoPostCard({
|
||||
onInteract?: () => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_, i18n} = useLingui()
|
||||
const {t: l, i18n} = useLingui()
|
||||
const embed = post.embed
|
||||
const {
|
||||
state: pressed,
|
||||
@@ -118,8 +117,8 @@ export function VideoPostCard({
|
||||
|
||||
return (
|
||||
<Link
|
||||
accessibilityHint={_(msg`Views video in immersive mode`)}
|
||||
label={_(msg`Video from ${author.handle}: ${text}`)}
|
||||
accessibilityHint={l`Views video in immersive mode`}
|
||||
label={l`Video from ${author.handle}: ${text}`}
|
||||
to={{
|
||||
screen: 'VideoFeed',
|
||||
params: {
|
||||
@@ -174,9 +173,7 @@ export function VideoPostCard({
|
||||
/>
|
||||
<View style={[a.align_center, a.gap_xs]}>
|
||||
<Eye size="lg" fill="white" />
|
||||
<Text style={[a.text_sm, {color: 'white'}]}>
|
||||
{_(msg`Hidden`)}
|
||||
</Text>
|
||||
<Text style={[a.text_sm, {color: 'white'}]}>{l`Hidden`}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -367,7 +364,7 @@ export function CompactVideoPostCard({
|
||||
onInteract?: () => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_, i18n} = useLingui()
|
||||
const {t: l, i18n} = useLingui()
|
||||
const embed = post.embed
|
||||
const {
|
||||
state: pressed,
|
||||
@@ -398,7 +395,7 @@ export function CompactVideoPostCard({
|
||||
|
||||
return (
|
||||
<Link
|
||||
label={_(msg`View video`)}
|
||||
label={l`View video`}
|
||||
to={{
|
||||
screen: 'VideoFeed',
|
||||
params: {
|
||||
@@ -413,7 +410,8 @@ export function CompactVideoPostCard({
|
||||
onPressOut={onPressOut}
|
||||
style={[
|
||||
a.flex_col,
|
||||
t.atoms.shadow_sm,
|
||||
a.rounded_xl,
|
||||
t.atoms.shadow_md,
|
||||
{
|
||||
alignItems: undefined,
|
||||
justifyContent: undefined,
|
||||
@@ -424,7 +422,7 @@ export function CompactVideoPostCard({
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.rounded_lg,
|
||||
a.rounded_xl,
|
||||
a.overflow_hidden,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
@@ -458,9 +456,7 @@ export function CompactVideoPostCard({
|
||||
/>
|
||||
<View style={[a.align_center, a.gap_xs]}>
|
||||
<Eye size="lg" fill="white" />
|
||||
<Text style={[a.text_sm, {color: 'white'}]}>
|
||||
{_(msg`Hidden`)}
|
||||
</Text>
|
||||
<Text style={[a.text_sm, {color: 'white'}]}>{l`Hidden`}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -469,7 +465,7 @@ export function CompactVideoPostCard({
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.rounded_lg,
|
||||
a.rounded_xl,
|
||||
a.overflow_hidden,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
@@ -485,10 +481,17 @@ export function CompactVideoPostCard({
|
||||
/>
|
||||
<MediaInsetBorder />
|
||||
|
||||
<View style={[a.absolute, a.inset_0, t.atoms.shadow_sm]}>
|
||||
<View style={[a.absolute, a.inset_0, t.atoms.shadow_md]}>
|
||||
<View style={[a.absolute, a.inset_0, a.p_sm, {bottom: 'auto'}]}>
|
||||
<View
|
||||
style={[a.relative, a.rounded_full, {width: 24, height: 24}]}>
|
||||
style={[
|
||||
a.relative,
|
||||
a.rounded_full,
|
||||
{
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
]}>
|
||||
<UserAvatar
|
||||
type="user"
|
||||
size={24}
|
||||
@@ -547,10 +550,10 @@ export function CompactVideoPostCardPlaceholder() {
|
||||
const black = getBlackColor(t)
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1, t.atoms.shadow_sm]}>
|
||||
<View style={[a.flex_1, t.atoms.shadow_md]}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_lg,
|
||||
a.rounded_xl,
|
||||
a.overflow_hidden,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {ImageBackground} from 'expo-image'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {FocusGuards, FocusScope} from 'radix-ui/internal'
|
||||
|
||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||
@@ -22,7 +20,7 @@ interface WelcomeModalProps {
|
||||
}
|
||||
|
||||
export function WelcomeModal({control}: WelcomeModalProps) {
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const ax = useAnalytics()
|
||||
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
@@ -152,7 +150,7 @@ export function WelcomeModal({control}: WelcomeModalProps) {
|
||||
<View>
|
||||
<Button
|
||||
onPress={onPressCreateAccount}
|
||||
label={_(msg`Create account`)}
|
||||
label={l`Create account`}
|
||||
size="large"
|
||||
color="primary"
|
||||
style={{
|
||||
@@ -165,7 +163,7 @@ export function WelcomeModal({control}: WelcomeModalProps) {
|
||||
</Button>
|
||||
<Button
|
||||
onPress={onPressExplore}
|
||||
label={_(msg`Explore the app`)}
|
||||
label={l`Explore the app`}
|
||||
size="large"
|
||||
color="primary"
|
||||
variant="ghost"
|
||||
@@ -188,10 +186,11 @@ export function WelcomeModal({control}: WelcomeModalProps) {
|
||||
]}>
|
||||
<Trans>Already have an account?</Trans>{' '}
|
||||
<Pressable
|
||||
onPress={onPressSignIn}
|
||||
onPointerEnter={() => setSignInLinkHovered(true)}
|
||||
onPointerLeave={() => setSignInLinkHovered(false)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Sign in`)}
|
||||
accessibilityLabel={l`Sign in`}
|
||||
accessibilityHint="">
|
||||
<Text
|
||||
style={[
|
||||
@@ -201,8 +200,7 @@ export function WelcomeModal({control}: WelcomeModalProps) {
|
||||
fontSize: undefined,
|
||||
},
|
||||
signInLinkHovered && a.underline,
|
||||
]}
|
||||
onPress={onPressSignIn}>
|
||||
]}>
|
||||
<Trans>Sign in</Trans>
|
||||
</Text>
|
||||
</Pressable>
|
||||
@@ -211,7 +209,7 @@ export function WelcomeModal({control}: WelcomeModalProps) {
|
||||
</View>
|
||||
</View>
|
||||
<Button
|
||||
label={_(msg`Close welcome modal`)}
|
||||
label={l`Close welcome modal`}
|
||||
style={[
|
||||
a.absolute,
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import Animated, {
|
||||
type AnimatedStyle,
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
interpolateColor,
|
||||
@@ -662,7 +663,7 @@ function BlockedPlaceholder({
|
||||
style,
|
||||
}: {
|
||||
profile: Shadow<ChatBskyActorDefs.ProfileViewBasic>
|
||||
style?: StyleProp<ViewStyle>
|
||||
style?: AnimatedStyle<ViewStyle>
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {useCallback} from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {scheduleOnRN} from 'react-native-worklets'
|
||||
|
||||
import {
|
||||
ScaleAndFadeIn,
|
||||
@@ -41,7 +41,7 @@ export function NewMessagesPill({
|
||||
}, [scale])
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
runOnJS(playHaptic)()
|
||||
scheduleOnRN(playHaptic)
|
||||
onPressInner?.()
|
||||
}, [onPressInner, playHaptic])
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ import {Gesture, type GestureType} from 'react-native-gesture-handler'
|
||||
import Animated, {
|
||||
clamp,
|
||||
interpolate,
|
||||
runOnJS,
|
||||
useAnimatedStyle,
|
||||
useReducedMotion,
|
||||
useSharedValue,
|
||||
withSequence,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {scheduleOnRN} from 'react-native-worklets'
|
||||
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {atoms as a, tokens, useTheme} from '#/alf'
|
||||
@@ -111,7 +111,7 @@ export function SwipeToReply({
|
||||
if (pastThreshold && !hit.get()) {
|
||||
hit.set(true)
|
||||
runPop()
|
||||
runOnJS(playHaptic)('Medium')
|
||||
scheduleOnRN(playHaptic, 'Medium')
|
||||
} else if (!pastThreshold && hit.get()) {
|
||||
hit.set(false)
|
||||
}
|
||||
@@ -120,7 +120,7 @@ export function SwipeToReply({
|
||||
'worklet'
|
||||
// Only a clean end (finger lifted past threshold) triggers the reply.
|
||||
if (hit.get()) {
|
||||
runOnJS(onReply)()
|
||||
scheduleOnRN(onReply)
|
||||
}
|
||||
})
|
||||
.onFinalize(() => {
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
import {HITSLOP_20} from '#/lib/constants'
|
||||
import {mergeRefs} from '#/lib/merge-refs'
|
||||
import {
|
||||
android,
|
||||
applyFonts,
|
||||
atoms as a,
|
||||
platform,
|
||||
@@ -96,6 +95,8 @@ export function Root({children, isInvalid = false, style}: RootProps) {
|
||||
a.relative,
|
||||
a.w_full,
|
||||
a.px_md,
|
||||
// Contain the input's z-index so it cannot paint over nearby overlays.
|
||||
{zIndex: 0},
|
||||
style,
|
||||
]}
|
||||
{...web({
|
||||
@@ -223,10 +224,6 @@ export function createInput(Component: typeof TextInput) {
|
||||
paddingTop: 13,
|
||||
paddingBottom: 13,
|
||||
},
|
||||
android({
|
||||
paddingTop: 8,
|
||||
paddingBottom: 9,
|
||||
}),
|
||||
/*
|
||||
* Margins are needed here to avoid autofill background overlapping the
|
||||
* top and bottom borders - esb
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import {useMemo} from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import {type AppBskyUnspeccedDefs, moderateProfile} from '@atproto/api'
|
||||
import {plural} from '@lingui/core/macro'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useTrendingSettings} from '#/state/preferences/trending'
|
||||
import {
|
||||
useTrendingSettings,
|
||||
useTrendingSettingsApi,
|
||||
} from '#/state/preferences/trending'
|
||||
import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery'
|
||||
import {useTrendingConfig} from '#/state/service-config'
|
||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
@@ -18,11 +19,14 @@ import {
|
||||
useTheme,
|
||||
type ViewStyleProp,
|
||||
} from '#/alf'
|
||||
import {alpha} from '#/alf/utils'
|
||||
import {AvatarStack} from '#/components/AvatarStack'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid'
|
||||
import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending'
|
||||
import {Link} from '#/components/Link'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {SubtleHover} from '#/components/SubtleHover'
|
||||
import {useTrendingTopicSeen} from '#/components/TrendingTopics'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
|
||||
@@ -41,6 +45,8 @@ function Inner() {
|
||||
const {t: l} = useLingui()
|
||||
const gutters = useGutters([0, 'base'])
|
||||
const ax = useAnalytics()
|
||||
const trendingPrompt = Prompt.usePromptControl()
|
||||
const {setTrendingDisabled} = useTrendingSettingsApi()
|
||||
const {
|
||||
data: trending,
|
||||
error,
|
||||
@@ -49,113 +55,123 @@ function Inner() {
|
||||
} = useGetTrendsQuery({limit: TOPIC_COUNT})
|
||||
const noTopics = !isLoading && !error && !trending?.trends?.length
|
||||
|
||||
const shadowColor = alpha(t.palette.primary_100, 0.5)
|
||||
|
||||
const gradient = {
|
||||
values: [
|
||||
[0, t.atoms.bg.backgroundColor],
|
||||
[0.1, t.palette.primary_25],
|
||||
[0.9, t.palette.primary_25],
|
||||
[1, t.atoms.bg.backgroundColor],
|
||||
],
|
||||
hover_value: t.palette.white,
|
||||
}
|
||||
|
||||
if (error || noTopics) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
gutters,
|
||||
a.pt_lg,
|
||||
a.pb_xl,
|
||||
a.gap_sm,
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
<LinearGradient
|
||||
colors={gradient.values.map(c => c[1]) as [string, string, ...string[]]}
|
||||
locations={
|
||||
gradient.values.map(c => c[0]) as [number, number, ...number[]]
|
||||
}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
/>
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.z_20,
|
||||
a.px_xs,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_between,
|
||||
a.gap_sm,
|
||||
gutters,
|
||||
a.pt_xs,
|
||||
a.pb_lg,
|
||||
a.gap_xs,
|
||||
a.border_t,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<View style={[a.flex_row, a.align_center, a.justify_between, a.gap_xs]}>
|
||||
<TrendingIcon width={18} />
|
||||
<Text
|
||||
style={[a.text_md, a.font_medium, a.leading_snug]}
|
||||
numberOfLines={1}>
|
||||
<Trans>Trending</Trans>
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.z_20,
|
||||
a.pl_xs,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_between,
|
||||
a.gap_sm,
|
||||
]}>
|
||||
<View
|
||||
style={[a.flex_row, a.align_center, a.justify_between, a.gap_xs]}>
|
||||
<TrendingIcon width={18} fill={t.atoms.text.color} />
|
||||
<Text style={[a.text_md, a.font_medium]} numberOfLines={1}>
|
||||
<Trans>Trending</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<Link label={l`See more trending topics`} to="/search">
|
||||
{({hovered, pressed}) => (
|
||||
<Text
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.font_medium,
|
||||
{
|
||||
color:
|
||||
hovered || pressed
|
||||
? t.palette.contrast_800
|
||||
: t.palette.contrast_500,
|
||||
},
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
<Trans>See more</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</Link>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
label={l`Trending options`}
|
||||
onPress={() => trendingPrompt.open()}
|
||||
style={[a.bg_transparent]}>
|
||||
<ButtonIcon icon={EllipsisIcon} size="md" />
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
<Link label={l`See more trending topics`} to="/search">
|
||||
<Text
|
||||
<View style={[a.relative, a.z_10, a.rounded_xl, t.atoms.shadow_md]}>
|
||||
<View
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.font_medium,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_high,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
<Trans>See more</Trans>
|
||||
</Text>
|
||||
</Link>
|
||||
a.overflow_hidden,
|
||||
a.border,
|
||||
a.rounded_xl,
|
||||
t.atoms.bg,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
{isLoading || isRefetching
|
||||
? Array.from({length: TOPIC_COUNT}).map((_, i) => (
|
||||
<TrendingTopicRowSkeleton key={i} rank={i + 1} />
|
||||
))
|
||||
: trending?.trends?.map((trend, index) => (
|
||||
<TrendRow
|
||||
key={trend.link}
|
||||
trend={trend}
|
||||
rank={index + 1}
|
||||
recId={trending.recId}
|
||||
onPress={() => {
|
||||
ax.metric('trendingTopic:click', {
|
||||
context: 'interstitial',
|
||||
recId: trending.recId,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.z_10,
|
||||
a.border,
|
||||
a.rounded_xl,
|
||||
t.atoms.bg,
|
||||
{
|
||||
borderColor: t.palette.primary_100,
|
||||
boxShadow: `0 0 16px 0 ${shadowColor}`,
|
||||
elevation: 8,
|
||||
shadowColor: shadowColor,
|
||||
shadowOffset: {width: 0, height: 0},
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 16,
|
||||
},
|
||||
]}>
|
||||
{isLoading || isRefetching
|
||||
? Array.from({length: TOPIC_COUNT}).map((_, i) => (
|
||||
<TrendingTopicRowSkeleton key={i} rank={i + 1} />
|
||||
))
|
||||
: trending?.trends?.map((trend, index) => (
|
||||
<TrendRow
|
||||
key={trend.link}
|
||||
trend={trend}
|
||||
rank={index + 1}
|
||||
onPress={() => {
|
||||
ax.metric('trendingTopic:click', {context: 'interstitial'})
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<Prompt.Basic
|
||||
control={trendingPrompt}
|
||||
title={l`Hide trending topics?`}
|
||||
description={l`You can update this later from your settings.`}
|
||||
confirmButtonCta={l`Hide`}
|
||||
onConfirm={() => {
|
||||
ax.metric('trendingTopics:hide', {context: 'interstitial'})
|
||||
setTrendingDisabled(true)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function TrendRow({
|
||||
trend,
|
||||
rank,
|
||||
recId,
|
||||
onPress,
|
||||
}: ViewStyleProp & {
|
||||
trend: AppBskyUnspeccedDefs.TrendView
|
||||
rank: number
|
||||
recId?: string
|
||||
children?: React.ReactNode
|
||||
onPress?: () => void
|
||||
}) {
|
||||
@@ -163,6 +179,8 @@ function TrendRow({
|
||||
const {t: l, i18n} = useLingui()
|
||||
|
||||
const actors = useModerateTrendingActors(trend.actors)
|
||||
const formattedPostCount = formatCount(i18n, trend.postCount)
|
||||
useTrendingTopicSeen('interstitial', recId)
|
||||
|
||||
return (
|
||||
<Link
|
||||
@@ -170,12 +188,7 @@ function TrendRow({
|
||||
label={l`Browse topic ${trend.displayName}`}
|
||||
to={trend.link}
|
||||
onPress={onPress}
|
||||
style={[
|
||||
rank < TOPIC_COUNT && a.border_b,
|
||||
{
|
||||
borderColor: t.palette.primary_100,
|
||||
},
|
||||
]}
|
||||
style={[rank < TOPIC_COUNT && a.border_b, t.atoms.border_contrast_low]}
|
||||
PressableComponent={Pressable}>
|
||||
{({hovered, pressed}) => (
|
||||
<>
|
||||
@@ -215,14 +228,14 @@ function TrendRow({
|
||||
<Text
|
||||
style={[a.text_sm, t.atoms.text_contrast_medium]}
|
||||
numberOfLines={1}>
|
||||
{trend.postCount >= 1000 ? (
|
||||
<Trans comment="Over 1,000 posts">1K+ posts</Trans>
|
||||
) : (
|
||||
<Trans comment="'{postCount} {posts}', e.g., '1.2K posts'">
|
||||
{formatCount(i18n, trend.postCount)}{' '}
|
||||
{plural(trend.postCount, {one: 'post', other: 'posts'})}
|
||||
</Trans>
|
||||
)}
|
||||
<Trans comment="'{postCount} {posts}', e.g., '1.2K posts'">
|
||||
{formattedPostCount}{' '}
|
||||
<Plural
|
||||
value={{postCount: trend.postCount}}
|
||||
one="post"
|
||||
other="posts"
|
||||
/>
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {useCallback} from 'react'
|
||||
import {ScrollView, View} from 'react-native'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {
|
||||
useTrendingSettings,
|
||||
@@ -30,7 +29,7 @@ export function TrendingInterstitial() {
|
||||
|
||||
export function Inner() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const ax = useAnalytics()
|
||||
const gutters = useGutters([0, 'base', 0, 'base'])
|
||||
const trendingPrompt = Prompt.usePromptControl()
|
||||
@@ -103,6 +102,8 @@ export function Inner() {
|
||||
<TrendingTopicLink
|
||||
key={topic.link}
|
||||
topic={topic}
|
||||
metricContext="interstitial"
|
||||
recId={trending.recId}
|
||||
onPress={() => {
|
||||
ax.metric('trendingTopic:click', {
|
||||
context: 'interstitial',
|
||||
@@ -122,7 +123,7 @@ export function Inner() {
|
||||
</TrendingTopicLink>
|
||||
))}
|
||||
<Button
|
||||
label={_(msg`Hide trending topics`)}
|
||||
label={l`Hide trending topics`}
|
||||
size="tiny"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
@@ -138,9 +139,9 @@ export function Inner() {
|
||||
|
||||
<Prompt.Basic
|
||||
control={trendingPrompt}
|
||||
title={_(msg`Hide trending topics?`)}
|
||||
description={_(msg`You can update this later from your settings.`)}
|
||||
confirmButtonCta={_(msg`Hide`)}
|
||||
title={l`Hide trending topics?`}
|
||||
description={l`You can update this later from your settings.`}
|
||||
confirmButtonCta={l`Hide`}
|
||||
onConfirm={onConfirmHide}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {useCallback, useEffect, useMemo} from 'react'
|
||||
import {ScrollView, View} from 'react-native'
|
||||
import {AppBskyEmbedVideo, AtUri} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {VIDEO_FEED_URI} from '#/lib/constants'
|
||||
@@ -14,7 +12,7 @@ import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid'
|
||||
import {Link} from '#/components/Link'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -35,7 +33,7 @@ const FEED_PARAMS: {
|
||||
|
||||
export function TrendingVideos() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const ax = useAnalytics()
|
||||
const gutters = useGutters([0, 'base'])
|
||||
const {data, isLoading, error} = usePostFeedQuery(FEED_DESC, FEED_PARAMS)
|
||||
@@ -48,7 +46,7 @@ export function TrendingVideos() {
|
||||
.getQueryCache()
|
||||
.find({queryKey: RQKEY(FEED_DESC, FEED_PARAMS)})
|
||||
if (query && query.getObserversCount() <= 1) {
|
||||
query.fetch()
|
||||
void query.fetch()
|
||||
}
|
||||
}
|
||||
}, [queryClient])
|
||||
@@ -83,20 +81,22 @@ export function TrendingVideos() {
|
||||
a.align_center,
|
||||
a.justify_between,
|
||||
]}>
|
||||
<Text style={[a.text_sm, a.font_semi_bold, a.leading_snug]}>
|
||||
<Trans>Trending Videos</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={_(msg`Dismiss this section`)}
|
||||
size="tiny"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
shape="square"
|
||||
onPress={() => trendingPrompt.open()}>
|
||||
<ButtonIcon icon={X} size="sm" />
|
||||
</Button>
|
||||
<View style={[a.pl_xs, a.flex_row, a.align_center]}>
|
||||
<Text style={[a.flex_1, a.text_md, a.font_semi_bold]}>
|
||||
<Trans>Trending videos</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={l`Dismiss this section`}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
style={[a.bg_transparent]}
|
||||
onPress={() => trendingPrompt.open()}>
|
||||
<ButtonIcon icon={EllipsisIcon} size="md" />
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<BlockDrawerGesture>
|
||||
<ScrollView
|
||||
horizontal
|
||||
@@ -131,12 +131,11 @@ export function TrendingVideos() {
|
||||
</View>
|
||||
</ScrollView>
|
||||
</BlockDrawerGesture>
|
||||
|
||||
<Prompt.Basic
|
||||
control={trendingPrompt}
|
||||
title={_(msg`Hide trending videos?`)}
|
||||
description={_(msg`You can update this later from your settings.`)}
|
||||
confirmButtonCta={_(msg`Hide`)}
|
||||
title={l`Hide trending videos?`}
|
||||
description={l`You can update this later from your settings.`}
|
||||
confirmButtonCta={l`Hide`}
|
||||
onConfirm={onConfirmHide}
|
||||
/>
|
||||
</View>
|
||||
@@ -186,7 +185,7 @@ function VideoCards({
|
||||
|
||||
function ViewMoreCard() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
|
||||
const href = useMemo(() => {
|
||||
const urip = new AtUri(VIDEO_FEED_URI)
|
||||
@@ -197,16 +196,16 @@ function ViewMoreCard() {
|
||||
<View style={[{width: CARD_WIDTH * 2}]}>
|
||||
<Link
|
||||
to={href}
|
||||
label={_(msg`View more`)}
|
||||
label={l`View more`}
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.flex_1,
|
||||
a.rounded_lg,
|
||||
a.rounded_xl,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
t.atoms.bg,
|
||||
t.atoms.shadow_sm,
|
||||
t.atoms.shadow_md,
|
||||
]}>
|
||||
{({pressed}) => (
|
||||
<View
|
||||
@@ -225,7 +224,7 @@ function ViewMoreCard() {
|
||||
color="primary"
|
||||
size="small"
|
||||
shape="round"
|
||||
label={_(msg`View more trending videos`)}>
|
||||
label={l`View more trending videos`}>
|
||||
<ButtonIcon icon={ChevronRight} />
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {beforeEach, describe, expect, it, jest} from '@jest/globals'
|
||||
|
||||
jest.mock('@bsky.app/react-native-mmkv', () => ({
|
||||
jest.mock('react-native-mmkv', () => ({
|
||||
MMKV: class MMKVMock {
|
||||
_store = new Map<string, string>()
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import Animated, {
|
||||
clamp,
|
||||
interpolate,
|
||||
interpolateColor,
|
||||
runOnJS,
|
||||
useAnimatedReaction,
|
||||
useAnimatedStyle,
|
||||
useDerivedValue,
|
||||
@@ -14,6 +13,7 @@ import Animated, {
|
||||
withSequence,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {scheduleOnRN} from 'react-native-worklets'
|
||||
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {type GestureActions} from './GestureActionView.shared'
|
||||
@@ -74,17 +74,17 @@ export function GestureActionView({
|
||||
() => transX,
|
||||
() => {
|
||||
if (transX.get() === 0) {
|
||||
runOnJS(setActiveAction)(null)
|
||||
scheduleOnRN(setActiveAction, null)
|
||||
} else if (transX.get() < 0) {
|
||||
if (
|
||||
actions.leftSecond &&
|
||||
transX.get() <= -actions.leftSecond.threshold
|
||||
) {
|
||||
if (activeAction !== 'leftSecond') {
|
||||
runOnJS(setActiveAction)('leftSecond')
|
||||
scheduleOnRN(setActiveAction, 'leftSecond')
|
||||
}
|
||||
} else if (activeAction !== 'leftFirst') {
|
||||
runOnJS(setActiveAction)('leftFirst')
|
||||
scheduleOnRN(setActiveAction, 'leftFirst')
|
||||
}
|
||||
} else if (transX.get() > 0) {
|
||||
if (
|
||||
@@ -92,10 +92,10 @@ export function GestureActionView({
|
||||
transX.get() > actions.rightSecond.threshold
|
||||
) {
|
||||
if (activeAction !== 'rightSecond') {
|
||||
runOnJS(setActiveAction)('rightSecond')
|
||||
scheduleOnRN(setActiveAction, 'rightSecond')
|
||||
}
|
||||
} else if (activeAction !== 'rightFirst') {
|
||||
runOnJS(setActiveAction)('rightFirst')
|
||||
scheduleOnRN(setActiveAction, 'rightFirst')
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -127,7 +127,7 @@ export function GestureActionView({
|
||||
!hitSecond.get()
|
||||
) {
|
||||
runPopAnimation()
|
||||
runOnJS(haptic)()
|
||||
scheduleOnRN(haptic)
|
||||
hitSecond.set(true)
|
||||
} else if (
|
||||
hitSecond.get() &&
|
||||
@@ -144,7 +144,7 @@ export function GestureActionView({
|
||||
!hitFirst.get()
|
||||
) {
|
||||
runPopAnimation()
|
||||
runOnJS(haptic)()
|
||||
scheduleOnRN(haptic)
|
||||
hitFirst.set(true)
|
||||
} else if (
|
||||
hitFirst.get() &&
|
||||
@@ -161,7 +161,7 @@ export function GestureActionView({
|
||||
!hitSecond.get()
|
||||
) {
|
||||
runPopAnimation()
|
||||
runOnJS(haptic)()
|
||||
scheduleOnRN(haptic)
|
||||
hitSecond.set(true)
|
||||
} else if (
|
||||
hitSecond.get() &&
|
||||
@@ -178,7 +178,7 @@ export function GestureActionView({
|
||||
!hitFirst.get()
|
||||
) {
|
||||
runPopAnimation()
|
||||
runOnJS(haptic)()
|
||||
scheduleOnRN(haptic)
|
||||
hitFirst.set(true)
|
||||
} else if (
|
||||
hitFirst.get() &&
|
||||
@@ -193,15 +193,15 @@ export function GestureActionView({
|
||||
'worklet'
|
||||
if (e.translationX < 0) {
|
||||
if (hitSecond.get() && actions.leftSecond) {
|
||||
runOnJS(actions.leftSecond.action)()
|
||||
scheduleOnRN(actions.leftSecond.action)
|
||||
} else if (hitFirst.get() && actions.leftFirst) {
|
||||
runOnJS(actions.leftFirst.action)()
|
||||
scheduleOnRN(actions.leftFirst.action)
|
||||
}
|
||||
} else if (e.translationX > 0) {
|
||||
if (hitSecond.get() && actions.rightSecond) {
|
||||
runOnJS(actions.rightSecond.action)()
|
||||
scheduleOnRN(actions.rightSecond.action)
|
||||
} else if (hitSecond.get() && actions.rightFirst) {
|
||||
runOnJS(actions.rightFirst.action)()
|
||||
scheduleOnRN(actions.rightFirst.action)
|
||||
}
|
||||
}
|
||||
transX.set(() => withTiming(0, {duration: 200}))
|
||||
|
||||
@@ -107,6 +107,7 @@ export function AnimatedLikeIcon({
|
||||
zIndex: -1,
|
||||
pointerEvents: 'none',
|
||||
borderRadius: size / 2,
|
||||
opacity: 0,
|
||||
}}
|
||||
/>
|
||||
<Animated.View
|
||||
@@ -121,6 +122,7 @@ export function AnimatedLikeIcon({
|
||||
zIndex: -1,
|
||||
pointerEvents: 'none',
|
||||
borderRadius: size / 2,
|
||||
opacity: 0,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -12,10 +12,22 @@ import {
|
||||
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {IS_ANDROID, IS_IOS, IS_TESTFLIGHT} from '#/env'
|
||||
import {IS_IOS, IS_TESTFLIGHT} from '#/env'
|
||||
|
||||
const MINIMUM_MINIMIZE_TIME = 15 * 60e3
|
||||
|
||||
/**
|
||||
* The channel this native build is expected to receive updates from. Anything
|
||||
* else is only reachable through the dev tooling in settings.
|
||||
*/
|
||||
const DEFAULT_CHANNEL = IS_TESTFLIGHT ? 'testflight' : 'production'
|
||||
|
||||
/**
|
||||
* Channels that our native builds are configured with, see `eas.json`. An
|
||||
* update running on any other channel was applied manually.
|
||||
*/
|
||||
const STANDARD_CHANNELS = ['production', 'testflight', 'development']
|
||||
|
||||
async function setExtraParams() {
|
||||
await setExtraParamAsync(
|
||||
IS_IOS ? 'ios-build-number' : 'android-build-number',
|
||||
@@ -23,10 +35,7 @@ async function setExtraParams() {
|
||||
// This just ensures it gets passed as a string
|
||||
`${nativeBuildVersion}`,
|
||||
)
|
||||
await setExtraParamAsync(
|
||||
'channel',
|
||||
IS_TESTFLIGHT ? 'testflight' : 'production',
|
||||
)
|
||||
await setExtraParamAsync('channel', DEFAULT_CHANNEL)
|
||||
}
|
||||
|
||||
async function setExtraParamsPullRequest(channel: string) {
|
||||
@@ -71,6 +80,14 @@ export function useApplyPullRequestOTAUpdate() {
|
||||
const currentChannel = currentlyRunning?.channel
|
||||
const isCurrentlyRunningPullRequestDeployment =
|
||||
currentChannel?.startsWith('pull-request')
|
||||
/*
|
||||
* Covers pull request deployments as well as any other channel we manually
|
||||
* applied an update from. Note that `channel` is null when updates are
|
||||
* disabled (e.g. in dev), in which case there's nothing to restore.
|
||||
*/
|
||||
const isCurrentlyRunningNonStandardChannel = Boolean(
|
||||
currentChannel && !STANDARD_CHANNELS.includes(currentChannel),
|
||||
)
|
||||
|
||||
const tryApplyUpdate = async (channel: string) => {
|
||||
setPending(true)
|
||||
@@ -104,19 +121,42 @@ export function useApplyPullRequestOTAUpdate() {
|
||||
setPending(false)
|
||||
}
|
||||
|
||||
const revertToEmbedded = async () => {
|
||||
/**
|
||||
* Pulls the newest update from the channel this build ships with and relaunches
|
||||
* into it, undoing a manually applied deployment.
|
||||
*/
|
||||
const restoreDefaultChannel = async () => {
|
||||
setPending(true)
|
||||
try {
|
||||
await updateTestflight()
|
||||
await setExtraParams()
|
||||
const res = await checkForUpdateAsync()
|
||||
if (res.isAvailable) {
|
||||
await fetchUpdateAsync()
|
||||
await reloadAsync()
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Nothing to Restore',
|
||||
`No deployment of ${DEFAULT_CHANNEL} is currently available for your native build. Reinstall the app to get back to a standard build.`,
|
||||
)
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error('Internal OTA Update Error', {error: `${e}`})
|
||||
Alert.alert(
|
||||
'Restore Failed',
|
||||
`Could not restore the ${DEFAULT_CHANNEL} deployment: ${e}`,
|
||||
)
|
||||
} finally {
|
||||
setPending(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tryApplyUpdate,
|
||||
revertToEmbedded,
|
||||
restoreDefaultChannel,
|
||||
isCurrentlyRunningPullRequestDeployment,
|
||||
isCurrentlyRunningNonStandardChannel,
|
||||
currentChannel,
|
||||
defaultChannel: DEFAULT_CHANNEL,
|
||||
pending,
|
||||
}
|
||||
}
|
||||
@@ -192,13 +232,6 @@ export function useOTAUpdates() {
|
||||
return
|
||||
}
|
||||
|
||||
// TEMP: disable wake-from-background OTA loading on Android.
|
||||
// This is causing a crash when the thread view is open due to
|
||||
// `maintainVisibleContentPosition`. See repro repo for more details:
|
||||
// https://github.com/mozzius/ota-crash-repro
|
||||
// Old Arch only - re-enable once we're on the New Archictecture! -sfn
|
||||
if (IS_ANDROID) return
|
||||
|
||||
const subscription = AppState.addEventListener(
|
||||
'change',
|
||||
async nextAppState => {
|
||||
|
||||
@@ -2,9 +2,11 @@ export function useOTAUpdates() {}
|
||||
export function useApplyPullRequestOTAUpdate() {
|
||||
return {
|
||||
tryApplyUpdate: async (_channel: string) => {},
|
||||
revertToEmbedded: () => {},
|
||||
restoreDefaultChannel: async () => {},
|
||||
isCurrentlyRunningPullRequestDeployment: false,
|
||||
isCurrentlyRunningNonStandardChannel: false,
|
||||
currentChannel: 'web-build',
|
||||
defaultChannel: 'web-build',
|
||||
pending: false,
|
||||
}
|
||||
}
|
||||
|
||||
+922
-896
File diff suppressed because it is too large
Load Diff
+933
-907
File diff suppressed because it is too large
Load Diff
+922
-896
File diff suppressed because it is too large
Load Diff
+933
-907
File diff suppressed because it is too large
Load Diff
+933
-907
File diff suppressed because it is too large
Load Diff
+933
-907
File diff suppressed because it is too large
Load Diff
+986
-960
File diff suppressed because it is too large
Load Diff
+938
-912
File diff suppressed because it is too large
Load Diff
+1022
-996
File diff suppressed because it is too large
Load Diff
+1005
-979
File diff suppressed because it is too large
Load Diff
+946
-920
File diff suppressed because it is too large
Load Diff
+921
-895
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+339
-344
File diff suppressed because one or more lines are too long
+932
-906
File diff suppressed because it is too large
Load Diff
+923
-897
File diff suppressed because it is too large
Load Diff
+939
-913
File diff suppressed because it is too large
Load Diff
+921
-895
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+922
-896
File diff suppressed because it is too large
Load Diff
+922
-896
File diff suppressed because it is too large
Load Diff
+921
-895
File diff suppressed because it is too large
Load Diff
+923
-897
File diff suppressed because it is too large
Load Diff
+922
-896
File diff suppressed because it is too large
Load Diff
+933
-907
File diff suppressed because it is too large
Load Diff
+921
-895
File diff suppressed because it is too large
Load Diff
+959
-933
File diff suppressed because it is too large
Load Diff
+922
-896
File diff suppressed because it is too large
Load Diff
+922
-896
File diff suppressed because it is too large
Load Diff
+927
-901
File diff suppressed because it is too large
Load Diff
+922
-896
File diff suppressed because it is too large
Load Diff
+933
-907
File diff suppressed because it is too large
Load Diff
+921
-895
File diff suppressed because it is too large
Load Diff
+925
-899
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+933
-907
File diff suppressed because it is too large
Load Diff
+933
-907
File diff suppressed because it is too large
Load Diff
+921
-895
File diff suppressed because it is too large
Load Diff
+921
-895
File diff suppressed because it is too large
Load Diff
+922
-896
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+925
-899
File diff suppressed because it is too large
Load Diff
+923
-897
File diff suppressed because it is too large
Load Diff
+932
-906
File diff suppressed because it is too large
Load Diff
+921
-895
File diff suppressed because it is too large
Load Diff
+924
-898
File diff suppressed because it is too large
Load Diff
+921
-895
File diff suppressed because it is too large
Load Diff
+922
-896
File diff suppressed because it is too large
Load Diff
+1057
-1031
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user