Merge remote-tracking branch 'origin/main' into next/embeds
* origin/main: new arch (#8295) Nightly source-language update Loosen post source constraints (#8478) hide keyboard when backgrounding (#8450)
This commit is contained in:
@@ -1,16 +1,21 @@
|
||||
import {InteractionManager, View} from 'react-native'
|
||||
import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
|
||||
import {
|
||||
type AnimatedRef,
|
||||
measure,
|
||||
type MeasuredDimensions,
|
||||
runOnJS,
|
||||
runOnUI,
|
||||
} from 'react-native-reanimated'
|
||||
import {Image} from 'expo-image'
|
||||
|
||||
import {HandleRef, measureHandle} from '#/lib/hooks/useHandleRef'
|
||||
import {useLightboxControls} from '#/state/lightbox'
|
||||
import {Dimensions} from '#/view/com/lightbox/ImageViewing/@types'
|
||||
import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types'
|
||||
import {AutoSizedImage} from '#/view/com/util/images/AutoSizedImage'
|
||||
import {ImageLayoutGrid} from '#/view/com/util/images/ImageLayoutGrid'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||
import {EmbedType} from '#/types/bsky/post'
|
||||
import {CommonProps} from './types'
|
||||
import {type EmbedType} from '#/types/bsky/post'
|
||||
import {type CommonProps} from './types'
|
||||
|
||||
export function ImageEmbed({
|
||||
embed,
|
||||
@@ -45,13 +50,15 @@ export function ImageEmbed({
|
||||
}
|
||||
const onPress = (
|
||||
index: number,
|
||||
refs: HandleRef[],
|
||||
refs: AnimatedRef<any>[],
|
||||
fetchedDims: (Dimensions | null)[],
|
||||
) => {
|
||||
const handles = refs.map(r => r.current)
|
||||
runOnUI(() => {
|
||||
'worklet'
|
||||
const rects = handles.map(measureHandle)
|
||||
const rects: (MeasuredDimensions | null)[] = []
|
||||
for (const r of refs) {
|
||||
rects.push(measure(r))
|
||||
}
|
||||
runOnJS(_openLightbox)(index, rects, fetchedDims)
|
||||
})()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
AccessibilityProps,
|
||||
type AccessibilityProps,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
TextInputProps,
|
||||
TextStyle,
|
||||
type TextInputProps,
|
||||
type TextStyle,
|
||||
View,
|
||||
ViewStyle,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
|
||||
import {HITSLOP_20} from '#/lib/constants'
|
||||
@@ -16,13 +16,13 @@ import {
|
||||
applyFonts,
|
||||
atoms as a,
|
||||
ios,
|
||||
TextStyleProp,
|
||||
type TextStyleProp,
|
||||
useAlf,
|
||||
useTheme,
|
||||
web,
|
||||
} from '#/alf'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const Context = React.createContext<{
|
||||
@@ -196,7 +196,7 @@ export function createInput(Component: typeof TextInput) {
|
||||
minWidth: 0,
|
||||
},
|
||||
ios({paddingTop: 12, paddingBottom: 13}),
|
||||
android(a.py_sm),
|
||||
android(a.py_md),
|
||||
// fix for autofill styles covering border
|
||||
web({
|
||||
paddingTop: 10,
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import {useState} from 'react'
|
||||
import {AnimatedRef, measure, MeasuredDimensions} from 'react-native-reanimated'
|
||||
|
||||
export type HandleRef = {
|
||||
(node: any): void
|
||||
current: null | number
|
||||
}
|
||||
|
||||
// This is a lighterweight alternative to `useAnimatedRef()` for imperative UI thread actions.
|
||||
// Render it like <View ref={ref} />, then pass `ref.current` to `measureHandle()` and such.
|
||||
export function useHandleRef(): HandleRef {
|
||||
return useState(() => {
|
||||
const ref = (node: any) => {
|
||||
if (node) {
|
||||
ref.current =
|
||||
node._nativeTag ??
|
||||
node.__nativeTag ??
|
||||
node.canonical?.nativeTag ??
|
||||
null
|
||||
} else {
|
||||
ref.current = null
|
||||
}
|
||||
}
|
||||
ref.current = null
|
||||
return ref
|
||||
})[0] as HandleRef
|
||||
}
|
||||
|
||||
// When using this version, you need to read ref.current on the JS thread, and pass it to UI.
|
||||
export function measureHandle(
|
||||
current: number | null,
|
||||
): MeasuredDimensions | null {
|
||||
'worklet'
|
||||
if (current !== null) {
|
||||
return measure((() => current) as AnimatedRef<any>)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
+37
-50
@@ -1,8 +1,9 @@
|
||||
import {Image as RNImage, Share as RNShare} from 'react-native'
|
||||
import {Image as RNImage} from 'react-native'
|
||||
import uuid from 'react-native-uuid'
|
||||
import {
|
||||
cacheDirectory,
|
||||
copyAsync,
|
||||
createDownloadResumable,
|
||||
deleteAsync,
|
||||
EncodingType,
|
||||
getInfoAsync,
|
||||
@@ -14,7 +15,6 @@ import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
|
||||
import * as MediaLibrary from 'expo-media-library'
|
||||
import * as Sharing from 'expo-sharing'
|
||||
import {Buffer} from 'buffer'
|
||||
import RNFetchBlob from 'rn-fetch-blob'
|
||||
|
||||
import {POST_IMG_MAX} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
@@ -68,28 +68,13 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
|
||||
return
|
||||
}
|
||||
|
||||
let downloadRes
|
||||
const path = createPath(appendExt)
|
||||
|
||||
try {
|
||||
const downloadResPromise = RNFetchBlob.config({
|
||||
fileCache: true,
|
||||
appendExt,
|
||||
}).fetch('GET', opts.uri)
|
||||
const to1 = setTimeout(() => downloadResPromise.cancel(), opts.timeout)
|
||||
downloadRes = await downloadResPromise
|
||||
clearTimeout(to1)
|
||||
|
||||
const status = downloadRes.info().status
|
||||
if (status !== 200) {
|
||||
return
|
||||
}
|
||||
|
||||
const localUri = normalizePath(downloadRes.path(), true)
|
||||
return await doResize(localUri, opts)
|
||||
await downloadImage(opts.uri, path, opts.timeout)
|
||||
return await doResize(path, opts)
|
||||
} finally {
|
||||
// TODO Whenever we remove `rn-fetch-blob`, we will need to replace this `flush()` with a `deleteAsync()` -hailey
|
||||
if (downloadRes) {
|
||||
downloadRes.flush()
|
||||
}
|
||||
safeDeleteAsync(path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,32 +83,16 @@ export async function shareImageModal({uri}: {uri: string}) {
|
||||
// TODO might need to give an error to the user in this case -prf
|
||||
return
|
||||
}
|
||||
const downloadResponse = await RNFetchBlob.config({
|
||||
fileCache: true,
|
||||
}).fetch('GET', uri)
|
||||
|
||||
// NOTE
|
||||
// assuming PNG
|
||||
// we're currently relying on the fact our CDN only serves pngs
|
||||
// -prf
|
||||
|
||||
let imagePath = downloadResponse.path()
|
||||
imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true)
|
||||
|
||||
// NOTE
|
||||
// for some reason expo-sharing refuses to work on iOS
|
||||
// ...and visa versa
|
||||
// -prf
|
||||
if (isIOS) {
|
||||
await RNShare.share({url: imagePath})
|
||||
} else {
|
||||
await Sharing.shareAsync(imagePath, {
|
||||
mimeType: 'image/png',
|
||||
UTI: 'image/png',
|
||||
})
|
||||
}
|
||||
|
||||
safeDeleteAsync(imagePath)
|
||||
const imageUri = await downloadImage(uri, createPath('png'), 5e3)
|
||||
const imagePath = await moveToPermanentPath(imageUri, '.png')
|
||||
safeDeleteAsync(imageUri)
|
||||
await Sharing.shareAsync(imagePath, {
|
||||
mimeType: 'image/png',
|
||||
UTI: 'image/png',
|
||||
})
|
||||
}
|
||||
|
||||
const ALBUM_NAME = 'Bluesky'
|
||||
@@ -134,11 +103,8 @@ export async function saveImageToMediaLibrary({uri}: {uri: string}) {
|
||||
// assuming PNG
|
||||
// we're currently relying on the fact our CDN only serves pngs
|
||||
// -prf
|
||||
const downloadResponse = await RNFetchBlob.config({
|
||||
fileCache: true,
|
||||
}).fetch('GET', uri)
|
||||
let imagePath = downloadResponse.path()
|
||||
imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true)
|
||||
const imageUri = await downloadImage(uri, createPath('png'), 5e3)
|
||||
const imagePath = await moveToPermanentPath(imageUri, '.png')
|
||||
|
||||
// save
|
||||
try {
|
||||
@@ -403,3 +369,24 @@ export function getResizedDimensions(originalDims: {
|
||||
height: Math.round(originalDims.height * ratio),
|
||||
}
|
||||
}
|
||||
|
||||
function createPath(ext: string) {
|
||||
// cacheDirectory will never be null on native, so the null check here is not necessary except for typescript.
|
||||
// we use a web-only function for downloadAndResize on web
|
||||
return `${cacheDirectory ?? ''}/${uuid.v4()}.${ext}`
|
||||
}
|
||||
|
||||
async function downloadImage(uri: string, path: string, timeout: number) {
|
||||
const dlResumable = createDownloadResumable(uri, path, {cache: true})
|
||||
|
||||
const to1 = setTimeout(() => dlResumable.cancelAsync(), timeout)
|
||||
|
||||
const dlRes = await dlResumable.downloadAsync()
|
||||
clearTimeout(to1)
|
||||
|
||||
if (!dlRes?.uri) {
|
||||
throw new Error('Failed to download image - dlRes is undefined')
|
||||
}
|
||||
|
||||
return normalizePath(dlRes.uri)
|
||||
}
|
||||
|
||||
+253
-155
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
||||
import React, {memo, useEffect} from 'react'
|
||||
import {memo, useCallback, useEffect, useMemo} from 'react'
|
||||
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
|
||||
import {
|
||||
import Animated, {
|
||||
measure,
|
||||
type MeasuredDimensions,
|
||||
runOnJS,
|
||||
runOnUI,
|
||||
useAnimatedRef,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {type AppBskyActorDefs, type ModerationDecision} from '@atproto/api'
|
||||
@@ -14,7 +16,6 @@ import {useNavigation} from '@react-navigation/native'
|
||||
import {useActorStatus} from '#/lib/actor-status'
|
||||
import {BACK_HITSLOP} from '#/lib/constants'
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {logger} from '#/logger'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
@@ -59,9 +60,9 @@ let ProfileHeaderShell = ({
|
||||
const playHaptic = useHaptics()
|
||||
const liveStatusControl = useDialogControl()
|
||||
|
||||
const aviRef = useHandleRef()
|
||||
const aviRef = useAnimatedRef()
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
const onPressBack = useCallback(() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
@@ -69,7 +70,7 @@ let ProfileHeaderShell = ({
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
const _openLightbox = React.useCallback(
|
||||
const _openLightbox = useCallback(
|
||||
(uri: string, thumbRect: MeasuredDimensions | null) => {
|
||||
openLightbox({
|
||||
images: [
|
||||
@@ -92,7 +93,7 @@ let ProfileHeaderShell = ({
|
||||
[openLightbox],
|
||||
)
|
||||
|
||||
const isMe = React.useMemo(
|
||||
const isMe = useMemo(
|
||||
() => currentAccount?.did === profile.did,
|
||||
[currentAccount, profile],
|
||||
)
|
||||
@@ -109,7 +110,7 @@ let ProfileHeaderShell = ({
|
||||
}
|
||||
}, [live.isActive, profile.did])
|
||||
|
||||
const onPressAvi = React.useCallback(() => {
|
||||
const onPressAvi = useCallback(() => {
|
||||
if (live.isActive) {
|
||||
playHaptic('Light')
|
||||
logger.metric(
|
||||
@@ -122,10 +123,9 @@ let ProfileHeaderShell = ({
|
||||
const modui = moderation.ui('avatar')
|
||||
const avatar = profile.avatar
|
||||
if (avatar && !(modui.blur && modui.noOverride)) {
|
||||
const aviHandle = aviRef.current
|
||||
runOnUI(() => {
|
||||
'worklet'
|
||||
const rect = measureHandle(aviHandle)
|
||||
const rect = measure(aviRef)
|
||||
runOnJS(_openLightbox)(avatar, rect)
|
||||
})()
|
||||
}
|
||||
@@ -223,7 +223,7 @@ let ProfileHeaderShell = ({
|
||||
styles.avi,
|
||||
profile.associated?.labeler && styles.aviLabeler,
|
||||
]}>
|
||||
<View ref={aviRef} collapsable={false}>
|
||||
<Animated.View ref={aviRef} collapsable={false}>
|
||||
<UserAvatar
|
||||
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
||||
size={live.isActive ? 88 : 90}
|
||||
@@ -231,7 +231,7 @@ let ProfileHeaderShell = ({
|
||||
moderation={moderation.ui('avatar')}
|
||||
/>
|
||||
{live.isActive && <LiveIndicator size="large" />}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</GrowableAvatar>
|
||||
|
||||
@@ -34,7 +34,7 @@ const consumedSources = new Map<string, PostSource>()
|
||||
* Used for FeedFeedback and other ephemeral non-critical systems.
|
||||
*/
|
||||
export function setUnstablePostSource(key: string, source: PostSource) {
|
||||
assertValid(
|
||||
assertValidDevOnly(
|
||||
key,
|
||||
`setUnstablePostSource key should be a URI containing a handle, received ${key} — use buildPostSourceKey`,
|
||||
)
|
||||
@@ -51,9 +51,10 @@ export function setUnstablePostSource(key: string, source: PostSource) {
|
||||
export function useUnstablePostSource(key: string) {
|
||||
const id = useId()
|
||||
const [source] = useState(() => {
|
||||
assertValid(
|
||||
assertValidDevOnly(
|
||||
key,
|
||||
`consumeUnstablePostSource key should be a URI containing a handle, received ${key} — use buildPostSourceKey`,
|
||||
`consumeUnstablePostSource key should be a URI containing a handle, received ${key} — be sure to use buildPostSourceKey when setting the source`,
|
||||
true,
|
||||
)
|
||||
const source = consumedSources.get(id) || transientSources.get(key)
|
||||
if (source) {
|
||||
@@ -87,11 +88,15 @@ export function buildPostSourceKey(key: string, handle: string) {
|
||||
/**
|
||||
* Just a lil dev helper
|
||||
*/
|
||||
function assertValid(key: string, message: string) {
|
||||
function assertValidDevOnly(key: string, message: string, beChill = false) {
|
||||
if (__DEV__) {
|
||||
const urip = new AtUri(key)
|
||||
if (urip.host.startsWith('did:')) {
|
||||
throw new Error(message)
|
||||
if (beChill) {
|
||||
logger.warn(message)
|
||||
} else {
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
type SupportedMimeTypes,
|
||||
} from '#/lib/constants'
|
||||
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
||||
import {useAppState} from '#/lib/hooks/useAppState'
|
||||
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
@@ -823,6 +824,8 @@ let ComposerPost = React.memo(function ComposerPost({
|
||||
[post.id, onSelectVideo, onImageAdd, _],
|
||||
)
|
||||
|
||||
useHideKeyboardOnBackground()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -1527,6 +1530,18 @@ function isEmptyPost(post: PostDraft) {
|
||||
)
|
||||
}
|
||||
|
||||
function useHideKeyboardOnBackground() {
|
||||
const appState = useAppState()
|
||||
|
||||
useEffect(() => {
|
||||
if (isIOS) {
|
||||
if (appState === 'inactive') {
|
||||
Keyboard.dismiss()
|
||||
}
|
||||
}
|
||||
}, [appState])
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
topbarInner: {
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
|
||||
import PasteInput, {
|
||||
type PastedFile,
|
||||
type PasteInputRef,
|
||||
type PasteInputRef, // @ts-expect-error no types when installing from github
|
||||
} from '@mattermost/react-native-paste-input'
|
||||
|
||||
import {POST_IMG_MAX} from '#/lib/constants'
|
||||
|
||||
+115
-107
@@ -1,16 +1,22 @@
|
||||
import React, {forwardRef, useCallback, useContext} from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {DrawerGestureContext} from 'react-native-drawer-layout'
|
||||
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
||||
import PagerView, {
|
||||
PagerViewOnPageScrollEventData,
|
||||
PagerViewOnPageSelectedEvent,
|
||||
PagerViewOnPageSelectedEventData,
|
||||
PageScrollStateChangedNativeEventData,
|
||||
type PagerViewOnPageScrollEventData,
|
||||
type PagerViewOnPageSelectedEvent,
|
||||
type PagerViewOnPageSelectedEventData,
|
||||
type PageScrollStateChangedNativeEventData,
|
||||
} from 'react-native-pager-view'
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
SharedValue,
|
||||
type SharedValue,
|
||||
useEvent,
|
||||
useHandler,
|
||||
useSharedValue,
|
||||
@@ -36,8 +42,12 @@ export interface RenderTabBarFnProps {
|
||||
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
||||
|
||||
interface Props {
|
||||
ref?: React.Ref<PagerRef>
|
||||
initialPage?: number
|
||||
renderTabBar: RenderTabBarFn
|
||||
// tab pressed, yet to scroll to page
|
||||
onTabPressed?: (index: number) => void
|
||||
// scroll settled
|
||||
onPageSelected?: (index: number) => void
|
||||
onPageScrollStateChanged?: (
|
||||
scrollState: 'idle' | 'dragging' | 'settling',
|
||||
@@ -47,114 +57,112 @@ interface Props {
|
||||
|
||||
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
|
||||
|
||||
export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
||||
function PagerImpl(
|
||||
export function Pager({
|
||||
ref,
|
||||
children,
|
||||
initialPage = 0,
|
||||
renderTabBar,
|
||||
onPageSelected: parentOnPageSelected,
|
||||
onTabPressed: parentOnTabPressed,
|
||||
onPageScrollStateChanged: parentOnPageScrollStateChanged,
|
||||
testID,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const [selectedPage, setSelectedPage] = useState(initialPage)
|
||||
const pagerView = useRef<PagerView>(null)
|
||||
|
||||
const [isIdle, setIsIdle] = useState(true)
|
||||
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const canSwipeDrawer = selectedPage === 0 && isIdle
|
||||
setDrawerSwipeDisabled(!canSwipeDrawer)
|
||||
return () => {
|
||||
setDrawerSwipeDisabled(false)
|
||||
}
|
||||
}, [setDrawerSwipeDisabled, selectedPage, isIdle]),
|
||||
)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
setPage: (index: number) => {
|
||||
pagerView.current?.setPage(index)
|
||||
},
|
||||
}))
|
||||
|
||||
const onPageSelectedJSThread = useCallback(
|
||||
(nextPosition: number) => {
|
||||
setSelectedPage(nextPosition)
|
||||
parentOnPageSelected?.(nextPosition)
|
||||
},
|
||||
[setSelectedPage, parentOnPageSelected],
|
||||
)
|
||||
|
||||
const onTabBarSelect = useCallback(
|
||||
(index: number) => {
|
||||
parentOnTabPressed?.(index)
|
||||
pagerView.current?.setPage(index)
|
||||
},
|
||||
[pagerView, parentOnTabPressed],
|
||||
)
|
||||
|
||||
const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle')
|
||||
const dragProgress = useSharedValue(selectedPage)
|
||||
const didInit = useSharedValue(false)
|
||||
const handlePageScroll = usePagerHandlers(
|
||||
{
|
||||
children,
|
||||
initialPage = 0,
|
||||
renderTabBar,
|
||||
onPageScrollStateChanged: parentOnPageScrollStateChanged,
|
||||
onPageSelected: parentOnPageSelected,
|
||||
testID,
|
||||
}: React.PropsWithChildren<Props>,
|
||||
ref,
|
||||
) {
|
||||
const [selectedPage, setSelectedPage] = React.useState(initialPage)
|
||||
const pagerView = React.useRef<PagerView>(null)
|
||||
|
||||
const [isIdle, setIsIdle] = React.useState(true)
|
||||
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const canSwipeDrawer = selectedPage === 0 && isIdle
|
||||
setDrawerSwipeDisabled(!canSwipeDrawer)
|
||||
return () => {
|
||||
setDrawerSwipeDisabled(false)
|
||||
onPageScroll(e: PagerViewOnPageScrollEventData) {
|
||||
'worklet'
|
||||
if (didInit.get() === false) {
|
||||
// On iOS, there's a spurious scroll event with 0 position
|
||||
// even if a different page was supplied as the initial page.
|
||||
// Ignore it and wait for the first confirmed selection instead.
|
||||
return
|
||||
}
|
||||
}, [setDrawerSwipeDisabled, selectedPage, isIdle]),
|
||||
)
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
setPage: (index: number) => {
|
||||
pagerView.current?.setPage(index)
|
||||
dragProgress.set(e.offset + e.position)
|
||||
},
|
||||
}))
|
||||
|
||||
const onPageSelectedJSThread = React.useCallback(
|
||||
(nextPosition: number) => {
|
||||
setSelectedPage(nextPosition)
|
||||
parentOnPageSelected?.(nextPosition)
|
||||
onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) {
|
||||
'worklet'
|
||||
runOnJS(setIsIdle)(e.pageScrollState === 'idle')
|
||||
if (dragState.get() === 'idle' && e.pageScrollState === 'settling') {
|
||||
// This is a programmatic scroll on Android.
|
||||
// Stay "idle" to match iOS and avoid confusing downstream code.
|
||||
return
|
||||
}
|
||||
dragState.set(e.pageScrollState)
|
||||
parentOnPageScrollStateChanged?.(e.pageScrollState)
|
||||
},
|
||||
[setSelectedPage, parentOnPageSelected],
|
||||
)
|
||||
|
||||
const onTabBarSelect = React.useCallback(
|
||||
(index: number) => {
|
||||
pagerView.current?.setPage(index)
|
||||
onPageSelected(e: PagerViewOnPageSelectedEventData) {
|
||||
'worklet'
|
||||
didInit.set(true)
|
||||
runOnJS(onPageSelectedJSThread)(e.position)
|
||||
},
|
||||
[pagerView],
|
||||
)
|
||||
},
|
||||
[parentOnPageScrollStateChanged],
|
||||
)
|
||||
|
||||
const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle')
|
||||
const dragProgress = useSharedValue(selectedPage)
|
||||
const didInit = useSharedValue(false)
|
||||
const handlePageScroll = usePagerHandlers(
|
||||
{
|
||||
onPageScroll(e: PagerViewOnPageScrollEventData) {
|
||||
'worklet'
|
||||
if (didInit.get() === false) {
|
||||
// On iOS, there's a spurious scroll event with 0 position
|
||||
// even if a different page was supplied as the initial page.
|
||||
// Ignore it and wait for the first confirmed selection instead.
|
||||
return
|
||||
}
|
||||
dragProgress.set(e.offset + e.position)
|
||||
},
|
||||
onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) {
|
||||
'worklet'
|
||||
runOnJS(setIsIdle)(e.pageScrollState === 'idle')
|
||||
if (dragState.get() === 'idle' && e.pageScrollState === 'settling') {
|
||||
// This is a programmatic scroll on Android.
|
||||
// Stay "idle" to match iOS and avoid confusing downstream code.
|
||||
return
|
||||
}
|
||||
dragState.set(e.pageScrollState)
|
||||
parentOnPageScrollStateChanged?.(e.pageScrollState)
|
||||
},
|
||||
onPageSelected(e: PagerViewOnPageSelectedEventData) {
|
||||
'worklet'
|
||||
didInit.set(true)
|
||||
runOnJS(onPageSelectedJSThread)(e.position)
|
||||
},
|
||||
},
|
||||
[parentOnPageScrollStateChanged],
|
||||
)
|
||||
const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web
|
||||
const nativeGesture =
|
||||
Gesture.Native().requireExternalGestureToFail(drawerGesture)
|
||||
|
||||
const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web
|
||||
const nativeGesture =
|
||||
Gesture.Native().requireExternalGestureToFail(drawerGesture)
|
||||
|
||||
return (
|
||||
<View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
|
||||
{renderTabBar({
|
||||
selectedPage,
|
||||
onSelect: onTabBarSelect,
|
||||
dragProgress,
|
||||
dragState,
|
||||
})}
|
||||
<GestureDetector gesture={nativeGesture}>
|
||||
<AnimatedPagerView
|
||||
ref={pagerView}
|
||||
style={[a.flex_1]}
|
||||
initialPage={initialPage}
|
||||
onPageScroll={handlePageScroll}>
|
||||
{children}
|
||||
</AnimatedPagerView>
|
||||
</GestureDetector>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
return (
|
||||
<View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
|
||||
{renderTabBar({
|
||||
selectedPage,
|
||||
onSelect: onTabBarSelect,
|
||||
dragProgress,
|
||||
dragState,
|
||||
})}
|
||||
<GestureDetector gesture={nativeGesture}>
|
||||
<AnimatedPagerView
|
||||
ref={pagerView}
|
||||
style={[a.flex_1]}
|
||||
initialPage={initialPage}
|
||||
onPageScroll={handlePageScroll}>
|
||||
{children}
|
||||
</AnimatedPagerView>
|
||||
</GestureDetector>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function usePagerHandlers(
|
||||
handlers: {
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Children,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {flushSync} from 'react-dom'
|
||||
|
||||
import {s} from '#/lib/styles'
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
export interface PagerRef {
|
||||
setPage: (index: number) => void
|
||||
}
|
||||
|
||||
export interface RenderTabBarFnProps {
|
||||
selectedPage: number
|
||||
@@ -12,30 +23,30 @@ export interface RenderTabBarFnProps {
|
||||
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
||||
|
||||
interface Props {
|
||||
ref?: React.Ref<PagerRef>
|
||||
initialPage?: number
|
||||
renderTabBar: RenderTabBarFn
|
||||
onPageSelected?: (index: number) => void
|
||||
}
|
||||
export const Pager = React.forwardRef(function PagerImpl(
|
||||
{
|
||||
children,
|
||||
initialPage = 0,
|
||||
renderTabBar,
|
||||
onPageSelected,
|
||||
}: React.PropsWithChildren<Props>,
|
||||
ref,
|
||||
) {
|
||||
const [selectedPage, setSelectedPage] = React.useState(initialPage)
|
||||
const scrollYs = React.useRef<Array<number | null>>([])
|
||||
const anchorRef = React.useRef(null)
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
export function Pager({
|
||||
ref,
|
||||
children,
|
||||
initialPage = 0,
|
||||
renderTabBar,
|
||||
onPageSelected,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const [selectedPage, setSelectedPage] = useState(initialPage)
|
||||
const scrollYs = useRef<Array<number | null>>([])
|
||||
const anchorRef = useRef(null)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
setPage: (index: number) => {
|
||||
onTabBarSelect(index)
|
||||
},
|
||||
}))
|
||||
|
||||
const onTabBarSelect = React.useCallback(
|
||||
const onTabBarSelect = useCallback(
|
||||
(index: number) => {
|
||||
const scrollY = window.scrollY
|
||||
// We want to determine if the tabbar is already "sticking" at the top (in which
|
||||
@@ -75,11 +86,13 @@ export const Pager = React.forwardRef(function PagerImpl(
|
||||
tabBarAnchor: <View ref={anchorRef} />,
|
||||
onSelect: e => onTabBarSelect(e),
|
||||
})}
|
||||
{React.Children.map(children, (child, i) => (
|
||||
<View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}>
|
||||
{Children.map(children, (child, i) => (
|
||||
<View
|
||||
style={selectedPage === i ? a.flex_1 : a.hidden}
|
||||
key={`page-${i}`}>
|
||||
{child}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import * as React from 'react'
|
||||
import {memo, useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {
|
||||
LayoutChangeEvent,
|
||||
NativeScrollEvent,
|
||||
ScrollView,
|
||||
type LayoutChangeEvent,
|
||||
type NativeScrollEvent,
|
||||
type ScrollView,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import Animated, {
|
||||
AnimatedRef,
|
||||
runOnJS,
|
||||
type AnimatedRef,
|
||||
runOnUI,
|
||||
scrollTo,
|
||||
SharedValue,
|
||||
type SharedValue,
|
||||
useAnimatedRef,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
@@ -20,9 +19,13 @@ import Animated, {
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {ScrollProvider} from '#/lib/ScrollContext'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager'
|
||||
import {
|
||||
Pager,
|
||||
type PagerRef,
|
||||
type RenderTabBarFnProps,
|
||||
} from '#/view/com/pager/Pager'
|
||||
import {useTheme} from '#/alf'
|
||||
import {ListMethods} from '../util/List'
|
||||
import {type ListMethods} from '../util/List'
|
||||
import {PagerHeaderProvider} from './PagerHeaderContext'
|
||||
import {TabBar} from './TabBar'
|
||||
|
||||
@@ -33,6 +36,7 @@ export interface PagerWithHeaderChildParams {
|
||||
}
|
||||
|
||||
export interface PagerWithHeaderProps {
|
||||
ref?: React.Ref<PagerRef>
|
||||
testID?: string
|
||||
children:
|
||||
| (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
|
||||
@@ -49,97 +53,94 @@ export interface PagerWithHeaderProps {
|
||||
onCurrentPageSelected?: (index: number) => void
|
||||
allowHeaderOverScroll?: boolean
|
||||
}
|
||||
export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
||||
function PageWithHeaderImpl(
|
||||
{
|
||||
children,
|
||||
testID,
|
||||
export function PagerWithHeader({
|
||||
ref,
|
||||
children,
|
||||
testID,
|
||||
items,
|
||||
isHeaderReady,
|
||||
renderHeader,
|
||||
initialPage,
|
||||
onPageSelected,
|
||||
onCurrentPageSelected,
|
||||
allowHeaderOverScroll,
|
||||
}: PagerWithHeaderProps) {
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
const [tabBarHeight, setTabBarHeight] = useState(0)
|
||||
const [headerOnlyHeight, setHeaderOnlyHeight] = useState(0)
|
||||
const scrollY = useSharedValue(0)
|
||||
const headerHeight = headerOnlyHeight + tabBarHeight
|
||||
|
||||
// capture the header bar sizing
|
||||
const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => {
|
||||
const height = evt.nativeEvent.layout.height
|
||||
if (height > 0) {
|
||||
// The rounding is necessary to prevent jumps on iOS
|
||||
setTabBarHeight(Math.round(height * 2) / 2)
|
||||
}
|
||||
})
|
||||
const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => {
|
||||
if (height > 0) {
|
||||
// The rounding is necessary to prevent jumps on iOS
|
||||
setHeaderOnlyHeight(Math.round(height * 2) / 2)
|
||||
}
|
||||
})
|
||||
|
||||
const renderTabBar = useCallback(
|
||||
(props: RenderTabBarFnProps) => {
|
||||
return (
|
||||
<PagerHeaderProvider scrollY={scrollY} headerHeight={headerOnlyHeight}>
|
||||
<PagerTabBar
|
||||
headerOnlyHeight={headerOnlyHeight}
|
||||
items={items}
|
||||
isHeaderReady={isHeaderReady}
|
||||
renderHeader={renderHeader}
|
||||
currentPage={currentPage}
|
||||
onCurrentPageSelected={onCurrentPageSelected}
|
||||
onTabBarLayout={onTabBarLayout}
|
||||
onHeaderOnlyLayout={onHeaderOnlyLayout}
|
||||
onSelect={props.onSelect}
|
||||
scrollY={scrollY}
|
||||
testID={testID}
|
||||
allowHeaderOverScroll={allowHeaderOverScroll}
|
||||
dragProgress={props.dragProgress}
|
||||
dragState={props.dragState}
|
||||
/>
|
||||
</PagerHeaderProvider>
|
||||
)
|
||||
},
|
||||
[
|
||||
headerOnlyHeight,
|
||||
items,
|
||||
isHeaderReady,
|
||||
renderHeader,
|
||||
initialPage,
|
||||
onPageSelected,
|
||||
currentPage,
|
||||
onCurrentPageSelected,
|
||||
onTabBarLayout,
|
||||
onHeaderOnlyLayout,
|
||||
scrollY,
|
||||
testID,
|
||||
allowHeaderOverScroll,
|
||||
}: PagerWithHeaderProps,
|
||||
ref,
|
||||
) {
|
||||
const [currentPage, setCurrentPage] = React.useState(0)
|
||||
const [tabBarHeight, setTabBarHeight] = React.useState(0)
|
||||
const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
|
||||
const scrollY = useSharedValue(0)
|
||||
const headerHeight = headerOnlyHeight + tabBarHeight
|
||||
],
|
||||
)
|
||||
|
||||
// capture the header bar sizing
|
||||
const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => {
|
||||
const height = evt.nativeEvent.layout.height
|
||||
if (height > 0) {
|
||||
// The rounding is necessary to prevent jumps on iOS
|
||||
setTabBarHeight(Math.round(height * 2) / 2)
|
||||
}
|
||||
})
|
||||
const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => {
|
||||
if (height > 0) {
|
||||
// The rounding is necessary to prevent jumps on iOS
|
||||
setHeaderOnlyHeight(Math.round(height * 2) / 2)
|
||||
}
|
||||
})
|
||||
const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([])
|
||||
const registerRef = useCallback(
|
||||
(scrollRef: AnimatedRef<any> | null, atIndex: number) => {
|
||||
scrollRefs.modify(refs => {
|
||||
'worklet'
|
||||
refs[atIndex] = scrollRef
|
||||
return refs
|
||||
})
|
||||
},
|
||||
[scrollRefs],
|
||||
)
|
||||
|
||||
const renderTabBar = React.useCallback(
|
||||
(props: RenderTabBarFnProps) => {
|
||||
return (
|
||||
<PagerHeaderProvider
|
||||
scrollY={scrollY}
|
||||
headerHeight={headerOnlyHeight}>
|
||||
<PagerTabBar
|
||||
headerOnlyHeight={headerOnlyHeight}
|
||||
items={items}
|
||||
isHeaderReady={isHeaderReady}
|
||||
renderHeader={renderHeader}
|
||||
currentPage={currentPage}
|
||||
onCurrentPageSelected={onCurrentPageSelected}
|
||||
onTabBarLayout={onTabBarLayout}
|
||||
onHeaderOnlyLayout={onHeaderOnlyLayout}
|
||||
onSelect={props.onSelect}
|
||||
scrollY={scrollY}
|
||||
testID={testID}
|
||||
allowHeaderOverScroll={allowHeaderOverScroll}
|
||||
dragProgress={props.dragProgress}
|
||||
dragState={props.dragState}
|
||||
/>
|
||||
</PagerHeaderProvider>
|
||||
)
|
||||
},
|
||||
[
|
||||
headerOnlyHeight,
|
||||
items,
|
||||
isHeaderReady,
|
||||
renderHeader,
|
||||
currentPage,
|
||||
onCurrentPageSelected,
|
||||
onTabBarLayout,
|
||||
onHeaderOnlyLayout,
|
||||
scrollY,
|
||||
testID,
|
||||
allowHeaderOverScroll,
|
||||
],
|
||||
)
|
||||
|
||||
const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([])
|
||||
const registerRef = React.useCallback(
|
||||
(scrollRef: AnimatedRef<any> | null, atIndex: number) => {
|
||||
scrollRefs.modify(refs => {
|
||||
'worklet'
|
||||
refs[atIndex] = scrollRef
|
||||
return refs
|
||||
})
|
||||
},
|
||||
[scrollRefs],
|
||||
)
|
||||
|
||||
const lastForcedScrollY = useSharedValue(0)
|
||||
const adjustScrollForOtherPages = () => {
|
||||
const lastForcedScrollY = useSharedValue(0)
|
||||
const adjustScrollForOtherPages = useCallback(
|
||||
(scrollState: 'idle' | 'dragging' | 'settling') => {
|
||||
'worklet'
|
||||
if (scrollState !== 'dragging') return
|
||||
const currentScrollY = scrollY.get()
|
||||
const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
|
||||
if (lastForcedScrollY.get() !== forcedScrollY) {
|
||||
@@ -152,75 +153,69 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[currentPage, headerOnlyHeight, lastForcedScrollY, scrollRefs, scrollY],
|
||||
)
|
||||
|
||||
const throttleTimeout = React.useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null,
|
||||
)
|
||||
const queueThrottledOnScroll = useNonReactiveCallback(() => {
|
||||
if (!throttleTimeout.current) {
|
||||
throttleTimeout.current = setTimeout(() => {
|
||||
throttleTimeout.current = null
|
||||
runOnUI(adjustScrollForOtherPages)()
|
||||
}, 80 /* Sync often enough you're unlikely to catch it unsynced */)
|
||||
const onScrollWorklet = useCallback(
|
||||
(e: NativeScrollEvent) => {
|
||||
'worklet'
|
||||
const nextScrollY = e.contentOffset.y
|
||||
// HACK: onScroll is reporting some strange values on load (negative header height).
|
||||
// Highly improbable that you'd be overscrolled by over 400px -
|
||||
// in fact, I actually can't do it, so let's just ignore those. -sfn
|
||||
const isPossiblyInvalid =
|
||||
headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
|
||||
if (!isPossiblyInvalid) {
|
||||
scrollY.set(nextScrollY)
|
||||
}
|
||||
})
|
||||
},
|
||||
[scrollY, headerHeight],
|
||||
)
|
||||
|
||||
const onScrollWorklet = React.useCallback(
|
||||
(e: NativeScrollEvent) => {
|
||||
'worklet'
|
||||
const nextScrollY = e.contentOffset.y
|
||||
// HACK: onScroll is reporting some strange values on load (negative header height).
|
||||
// Highly improbable that you'd be overscrolled by over 400px -
|
||||
// in fact, I actually can't do it, so let's just ignore those. -sfn
|
||||
const isPossiblyInvalid =
|
||||
headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
|
||||
if (!isPossiblyInvalid) {
|
||||
scrollY.set(nextScrollY)
|
||||
runOnJS(queueThrottledOnScroll)()
|
||||
}
|
||||
},
|
||||
[scrollY, queueThrottledOnScroll, headerHeight],
|
||||
)
|
||||
const onPageSelectedInner = useCallback(
|
||||
(index: number) => {
|
||||
setCurrentPage(index)
|
||||
onPageSelected?.(index)
|
||||
},
|
||||
[onPageSelected, setCurrentPage],
|
||||
)
|
||||
|
||||
const onPageSelectedInner = React.useCallback(
|
||||
(index: number) => {
|
||||
setCurrentPage(index)
|
||||
onPageSelected?.(index)
|
||||
},
|
||||
[onPageSelected, setCurrentPage],
|
||||
)
|
||||
const onTabPressed = useCallback(() => {
|
||||
runOnUI(adjustScrollForOtherPages)('dragging')
|
||||
}, [adjustScrollForOtherPages])
|
||||
|
||||
return (
|
||||
<Pager
|
||||
ref={ref}
|
||||
testID={testID}
|
||||
initialPage={initialPage}
|
||||
onPageSelected={onPageSelectedInner}
|
||||
renderTabBar={renderTabBar}>
|
||||
{toArray(children)
|
||||
.filter(Boolean)
|
||||
.map((child, i) => {
|
||||
const isReady =
|
||||
isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0
|
||||
return (
|
||||
<View key={i} collapsable={false}>
|
||||
<PagerItem
|
||||
headerHeight={headerHeight}
|
||||
index={i}
|
||||
isReady={isReady}
|
||||
isFocused={i === currentPage}
|
||||
onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
|
||||
registerRef={registerRef}
|
||||
renderTab={child}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</Pager>
|
||||
)
|
||||
},
|
||||
)
|
||||
return (
|
||||
<Pager
|
||||
ref={ref}
|
||||
testID={testID}
|
||||
initialPage={initialPage}
|
||||
onTabPressed={onTabPressed}
|
||||
onPageSelected={onPageSelectedInner}
|
||||
renderTabBar={renderTabBar}
|
||||
onPageScrollStateChanged={adjustScrollForOtherPages}>
|
||||
{toArray(children)
|
||||
.filter(Boolean)
|
||||
.map((child, i) => {
|
||||
const isReady =
|
||||
isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0
|
||||
return (
|
||||
<View key={i} collapsable={false}>
|
||||
<PagerItem
|
||||
headerHeight={headerHeight}
|
||||
index={i}
|
||||
isReady={isReady}
|
||||
isFocused={i === currentPage}
|
||||
onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
|
||||
registerRef={registerRef}
|
||||
renderTab={child}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</Pager>
|
||||
)
|
||||
}
|
||||
|
||||
let PagerTabBar = ({
|
||||
currentPage,
|
||||
@@ -258,7 +253,7 @@ let PagerTabBar = ({
|
||||
dragState: SharedValue<'idle' | 'dragging' | 'settling'>
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const [minimumHeaderHeight, setMinimumHeaderHeight] = React.useState(0)
|
||||
const [minimumHeaderHeight, setMinimumHeaderHeight] = useState(0)
|
||||
const headerTransform = useAnimatedStyle(() => {
|
||||
const translateY =
|
||||
Math.min(
|
||||
@@ -275,7 +270,7 @@ let PagerTabBar = ({
|
||||
],
|
||||
}
|
||||
})
|
||||
const headerRef = React.useRef(null)
|
||||
const headerRef = useRef(null)
|
||||
return (
|
||||
<Animated.View
|
||||
pointerEvents={isIOS ? 'auto' : 'box-none'}
|
||||
@@ -327,7 +322,7 @@ let PagerTabBar = ({
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
PagerTabBar = React.memo(PagerTabBar)
|
||||
PagerTabBar = memo(PagerTabBar)
|
||||
|
||||
function PagerItem({
|
||||
headerHeight,
|
||||
@@ -348,7 +343,7 @@ function PagerItem({
|
||||
}) {
|
||||
const scrollElRef = useAnimatedRef()
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
registerRef(scrollElRef, index)
|
||||
return () => {
|
||||
registerRef(null, index)
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
import React from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
|
||||
import {AppBskyGraphDefs} from '@atproto/api'
|
||||
import Animated, {
|
||||
measure,
|
||||
type MeasuredDimensions,
|
||||
runOnJS,
|
||||
runOnUI,
|
||||
useAnimatedRef,
|
||||
} from 'react-native-reanimated'
|
||||
import {type AppBskyGraphDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {emitSoftReset} from '#/state/events'
|
||||
import {useLightboxControls} from '#/state/lightbox'
|
||||
import {TextLink} from '#/view/com/util/Link'
|
||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {UserAvatar, UserAvatarType} from '#/view/com/util/UserAvatar'
|
||||
import {UserAvatar, type UserAvatarType} from '#/view/com/util/UserAvatar'
|
||||
import {StarterPack} from '#/components/icons/StarterPack'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
@@ -52,7 +57,7 @@ export function ProfileSubpageHeader({
|
||||
const {openLightbox} = useLightboxControls()
|
||||
const pal = usePalette('default')
|
||||
const canGoBack = navigation.canGoBack()
|
||||
const aviRef = useHandleRef()
|
||||
const aviRef = useAnimatedRef()
|
||||
|
||||
const _openLightbox = React.useCallback(
|
||||
(uri: string, thumbRect: MeasuredDimensions | null) => {
|
||||
@@ -81,10 +86,9 @@ export function ProfileSubpageHeader({
|
||||
if (
|
||||
avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
|
||||
) {
|
||||
const aviHandle = aviRef.current
|
||||
runOnUI(() => {
|
||||
'worklet'
|
||||
const rect = measureHandle(aviHandle)
|
||||
const rect = measure(aviRef)
|
||||
runOnJS(_openLightbox)(avatar, rect)
|
||||
})()
|
||||
}
|
||||
@@ -111,7 +115,7 @@ export function ProfileSubpageHeader({
|
||||
paddingBottom: 14,
|
||||
paddingHorizontal: isMobile ? 12 : 14,
|
||||
}}>
|
||||
<View ref={aviRef} collapsable={false}>
|
||||
<Animated.View ref={aviRef} collapsable={false}>
|
||||
<Pressable
|
||||
testID="headerAviButton"
|
||||
onPress={onPressAvi}
|
||||
@@ -125,7 +129,7 @@ export function ProfileSubpageHeader({
|
||||
<UserAvatar type={avatarType} size={58} avatar={avatar} />
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
</Animated.View>
|
||||
<View style={{flex: 1, gap: 4}}>
|
||||
{isLoading ? (
|
||||
<LoadingPlaceholder
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
type FontAwesomeIconStyle,
|
||||
type Props as FontAwesomeProps,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import type React from 'react'
|
||||
|
||||
const DURATION = 3500
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import React, {useRef} from 'react'
|
||||
import {DimensionValue, Pressable, View} from 'react-native'
|
||||
import {type DimensionValue, Pressable, View} from 'react-native'
|
||||
import Animated, {
|
||||
type AnimatedRef,
|
||||
useAnimatedRef,
|
||||
} from 'react-native-reanimated'
|
||||
import {Image} from 'expo-image'
|
||||
import {AppBskyEmbedImages} from '@atproto/api'
|
||||
import {type AppBskyEmbedImages} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {HandleRef, useHandleRef} from '#/lib/hooks/useHandleRef'
|
||||
import type {Dimensions} from '#/lib/media/types'
|
||||
import {type Dimensions} from '#/lib/media/types'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
@@ -68,14 +71,17 @@ export function AutoSizedImage({
|
||||
image: AppBskyEmbedImages.ViewImage
|
||||
crop?: 'none' | 'square' | 'constrained'
|
||||
hideBadge?: boolean
|
||||
onPress?: (containerRef: HandleRef, fetchedDims: Dimensions | null) => void
|
||||
onPress?: (
|
||||
containerRef: AnimatedRef<any>,
|
||||
fetchedDims: Dimensions | null,
|
||||
) => void
|
||||
onLongPress?: () => void
|
||||
onPressIn?: () => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const largeAlt = useLargeAltBadgeEnabled()
|
||||
const containerRef = useHandleRef()
|
||||
const containerRef = useAnimatedRef()
|
||||
const fetchedDimsRef = useRef<{width: number; height: number} | null>(null)
|
||||
|
||||
let aspectRatio: number | undefined
|
||||
@@ -103,7 +109,7 @@ export function AutoSizedImage({
|
||||
const hasAlt = !!image.alt
|
||||
|
||||
const contents = (
|
||||
<View ref={containerRef} collapsable={false} style={{flex: 1}}>
|
||||
<Animated.View ref={containerRef} collapsable={false} style={{flex: 1}}>
|
||||
<Image
|
||||
contentFit={isContain ? 'contain' : 'cover'}
|
||||
style={[a.w_full, a.h_full]}
|
||||
@@ -185,7 +191,7 @@ export function AutoSizedImage({
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</Animated.View>
|
||||
)
|
||||
|
||||
if (cropDisabled) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react'
|
||||
import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {Image, ImageStyle} from 'expo-image'
|
||||
import {AppBskyEmbedImages} from '@atproto/api'
|
||||
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {type AnimatedRef} from 'react-native-reanimated'
|
||||
import {Image, type ImageStyle} from 'expo-image'
|
||||
import {type AppBskyEmbedImages} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react'
|
||||
|
||||
import {HandleRef} from '#/lib/hooks/useHandleRef'
|
||||
import {Dimensions} from '#/lib/media/types'
|
||||
import {type Dimensions} from '#/lib/media/types'
|
||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
@@ -20,7 +20,7 @@ interface Props {
|
||||
index: number
|
||||
onPress?: (
|
||||
index: number,
|
||||
containerRefs: HandleRef[],
|
||||
containerRefs: AnimatedRef<any>[],
|
||||
fetchedDims: (Dimensions | null)[],
|
||||
) => void
|
||||
onLongPress?: EventFunction
|
||||
@@ -28,7 +28,7 @@ interface Props {
|
||||
imageStyle?: StyleProp<ImageStyle>
|
||||
viewContext?: PostEmbedViewContext
|
||||
insetBorderStyle?: StyleProp<ViewStyle>
|
||||
containerRefs: HandleRef[]
|
||||
containerRefs: AnimatedRef<any>[]
|
||||
thumbDimsRef: React.MutableRefObject<(Dimensions | null)[]>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {AppBskyEmbedImages} from '@atproto/api'
|
||||
import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native'
|
||||
import {type AnimatedRef, useAnimatedRef} from 'react-native-reanimated'
|
||||
import {type AppBskyEmbedImages} from '@atproto/api'
|
||||
|
||||
import {HandleRef, useHandleRef} from '#/lib/hooks/useHandleRef'
|
||||
import {atoms as a, useBreakpoints} from '#/alf'
|
||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||
import {Dimensions} from '../../lightbox/ImageViewing/@types'
|
||||
import {type Dimensions} from '../../lightbox/ImageViewing/@types'
|
||||
import {GalleryItem} from './Gallery'
|
||||
|
||||
interface ImageLayoutGridProps {
|
||||
images: AppBskyEmbedImages.ViewImage[]
|
||||
onPress?: (
|
||||
index: number,
|
||||
containerRefs: HandleRef[],
|
||||
containerRefs: AnimatedRef<any>[],
|
||||
fetchedDims: (Dimensions | null)[],
|
||||
) => void
|
||||
onLongPress?: (index: number) => void
|
||||
@@ -43,7 +43,7 @@ interface ImageLayoutGridInnerProps {
|
||||
images: AppBskyEmbedImages.ViewImage[]
|
||||
onPress?: (
|
||||
index: number,
|
||||
containerRefs: HandleRef[],
|
||||
containerRefs: AnimatedRef<any>[],
|
||||
fetchedDims: (Dimensions | null)[],
|
||||
) => void
|
||||
onLongPress?: (index: number) => void
|
||||
@@ -56,10 +56,10 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
|
||||
const gap = props.gap
|
||||
const count = props.images.length
|
||||
|
||||
const containerRef1 = useHandleRef()
|
||||
const containerRef2 = useHandleRef()
|
||||
const containerRef3 = useHandleRef()
|
||||
const containerRef4 = useHandleRef()
|
||||
const containerRef1 = useAnimatedRef()
|
||||
const containerRef2 = useAnimatedRef()
|
||||
const containerRef3 = useAnimatedRef()
|
||||
const containerRef4 = useAnimatedRef()
|
||||
const thumbDimsRef = React.useRef<(Dimensions | null)[]>([])
|
||||
|
||||
switch (count) {
|
||||
|
||||
Reference in New Issue
Block a user