diff --git a/package.json b/package.json index bd3d7b3e36..de2e861da5 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,6 @@ "react-responsive": "^9.0.2", "rn-fetch-blob": "^0.12.0", "sentry-expo": "~7.0.0", - "smitter": "^1.1.1", "tippy.js": "^6.3.7", "tlds": "^1.234.0", "zeego": "^1.6.2", diff --git a/src/view/com/composer/TagInput.tsx b/src/view/com/composer/TagInput.tsx index 32552e60c4..540b5c0771 100644 --- a/src/view/com/composer/TagInput.tsx +++ b/src/view/com/composer/TagInput.tsx @@ -13,12 +13,8 @@ import { FontAwesomeIcon, FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' +import BottomSheet, {BottomSheetBackdrop} from '@gorhom/bottom-sheet' -import { - useSheet, - Sheet as BottomSheet, - Backdrop as BottomSheetBackdrop, -} from 'view/com/util/BottomSheet' import {Portal} from 'view/com/util/Portal' import {TagsAutocompleteModel} from 'state/models/ui/tags-autocomplete' import {usePalette} from 'lib/hooks/usePalette' @@ -53,22 +49,7 @@ export function TagInput({ const [suggestions, setSuggestions] = React.useState([]) const [isInitialLoad, setIsInitialLoad] = React.useState(true) - const sheet = useSheet({ - index: 0, - snaps: [0, '90%'], - async onStateChange(state) { - if (state.index > 0) { - model.setActive(true) - await model.search('') // get default results - setSuggestions(model.suggestions) - setIsInitialLoad(false) - } else { - reset() - setIsInitialLoad(true) - input.current?.blur() - } - }, - }) + const sheet = React.useRef(null) const reset = React.useCallback(() => { setValue('') @@ -145,10 +126,28 @@ export function TagInput({ ) const openSheet = React.useCallback(() => { - sheet.index = 1 + sheet.current?.snapToIndex(0) input.current?.focus() }, [sheet]) + const onSheetChange = React.useCallback( + async (index: number) => { + if (index > -1) { + model.setActive(true) + await model.search('') // get default results + setSuggestions(model.suggestions) + setIsInitialLoad(false) + } + }, + [model, setSuggestions, setIsInitialLoad], + ) + + const onCloseSheet = React.useCallback(() => { + reset() + setIsInitialLoad(true) + input.current?.blur() + }, [reset, setIsInitialLoad]) + return ( - - - + ( + + )} + handleIndicatorStyle={{backgroundColor: pal.text.color}} + handleStyle={{display: 'none'}} + onChange={onSheetChange} + onClose={onCloseSheet}> diff --git a/src/view/com/util/BottomSheet.tsx b/src/view/com/util/BottomSheet.tsx deleted file mode 100644 index af159a9820..0000000000 --- a/src/view/com/util/BottomSheet.tsx +++ /dev/null @@ -1,394 +0,0 @@ -import React from 'react' -import {View, Dimensions, Pressable} from 'react-native' -import {Gesture, GestureDetector} from 'react-native-gesture-handler' -import Animated, { - useSharedValue, - useAnimatedStyle, - withTiming, - Easing, - runOnJS, -} from 'react-native-reanimated' -import {smitter} from 'smitter' - -type BottomSheetState = { - index: number - minIndex: number - maxIndex: number - position: number - pinned: boolean - offset: number - snaps: number[] -} - -type BottomSheetProps = { - sheet: ReturnType -} - -type InternalEvents = { - syncState: BottomSheetState -} - -export function useSheet({ - index: initialIndex = 0, - minIndex: initialMinIndex = 0, - snaps, - onStateChange, -}: { - index?: number - minIndex?: number - maxIndex?: number - snaps: (number | string)[] - onStateChange?: (state: BottomSheetState) => void -}) { - const internal = React.useMemo(() => smitter(), []) - const dimensions = React.useMemo(() => Dimensions.get('window'), []) // TODO needs change? - const snapPoints = React.useMemo(() => { - return snaps.map(p => { - const px = - typeof p === 'number' ? p : (parseInt(p) / 100) * dimensions.height - return px - }) - }, [snaps, dimensions.height]) - - const index = React.useRef(initialIndex) - const minIndex = React.useRef(Math.max(initialMinIndex, 0)) - const maxIndex = React.useRef(snaps.length - 1) - const position = React.useRef( - index.current > -1 ? snapPoints[index.current] : 0, - ) - const pinned = React.useRef(false) - const offset = React.useRef(0) - - const getState = React.useCallback(() => { - return { - index: index.current, - minIndex: minIndex.current, - maxIndex: maxIndex.current, - position: position.current, - pinned: pinned.current, - offset: offset.current, - snaps: snapPoints, - } - }, [snapPoints]) - - const syncState = React.useCallback( - (state: Partial) => { - if (state.minIndex !== undefined) { - minIndex.current = Math.max(state.minIndex, 0) - } - if (state.maxIndex !== undefined) { - maxIndex.current = Math.min(state.maxIndex, snapPoints.length - 1) - } - if (state.index !== undefined) { - index.current = Math.max( - Math.min(state.index, maxIndex.current), - minIndex.current, - ) - position.current = snapPoints[index.current] - } - if (state.position !== undefined) { - position.current = Math.max( - Math.min(state.position, snapPoints[maxIndex.current]), - snapPoints[minIndex.current], - ) - } - if (state.pinned !== undefined) { - pinned.current = state.pinned - } - if (state.offset !== undefined) { - offset.current = state.offset - } - - onStateChange?.(getState()) - }, - [getState, onStateChange, snapPoints], - ) - - const setState = React.useCallback( - (state: Partial) => { - syncState(state) - internal.emit('syncState', getState()) - }, - [syncState, getState, internal], - ) - - return { - get state() { - return getState() - }, - open() { - setState({ - index: 1, - }) - }, - close() { - setState({ - index: 0, - minIndex: 0, - }) - }, - set index(value: number) { - setState({index: value}) - }, - get index() { - return index.current - }, - set position(value: number | string) { - const position = - typeof value === 'number' - ? value - : (parseInt(value) / 100) * dimensions.height - setState({position}) - }, - get position() { - return position.current - }, - set minIndex(index: number) { - setState({minIndex: index}) - }, - get minIndex() { - return minIndex.current - }, - set maxIndex(index: number) { - setState({maxIndex: index}) - }, - get maxIndex() { - return maxIndex.current - }, - set pinned(value: boolean) { - setState({pinned: value}) - }, - get pinned() { - return pinned.current - }, - set offset(offset: number) { - setState({offset}) - }, - get offset() { - return offset.current - }, - events: { - internal, - }, - _syncState: syncState, - } -} - -export function Sheet({ - children, - sheet, -}: React.PropsWithChildren) { - const state = useSharedValue(sheet.state) - state.value = sheet.state - - const {index, snaps} = state.value - - const dimensions = React.useMemo(() => Dimensions.get('window'), []) - - const top = useSharedValue(index > -1 ? snaps[index] : 0) - const animatedSheetStyles = useAnimatedStyle(() => ({ - transform: [{translateY: -top.value}], - })) - - const offset = useSharedValue(dimensions.height) - const animatedOuterStyles = useAnimatedStyle(() => ({ - transform: [{translateY: offset.value}], - })) - - React.useEffect(() => { - function goToPosition(pos: number) { - top.value = withTiming(pos, { - duration: 500, - easing: Easing.out(Easing.exp), - }) - } - - sheet.events.internal.on('syncState', s => { - if (state.value.index != s.index) { - const pos = index > -1 ? snaps[s.index] : 0 - goToPosition(pos) - } - if (state.value.position != s.position) { - goToPosition(s.position) - } - if (state.value.offset != s.offset) { - offset.value = withTiming(dimensions.height - s.offset, { - duration: 500, - easing: Easing.out(Easing.exp), - }) - } - - state.value = s - }) - }, [ - snaps, - dimensions.height, - index, - offset, - sheet.events.internal, - state, - top, - ]) - - const pan = Gesture.Pan() - .onChange(e => { - top.value = top.value - e.changeY - }) - .onFinalize(e => { - // ignore taps - if (Math.abs(e.translationY) < 5) return - - let y = top.value // from the bottom - - const dir = e.velocityY > 0 ? 1 : -1 - let v = Math.abs(e.velocityY) / 100 - - let decayDistance = 0 - while (v > 0.1) { - v *= 1 - 0.15 - decayDistance += v - } - - decayDistance = decayDistance * dir - y = y - decayDistance - - let {index, minIndex, maxIndex, position, pinned} = state.value - let nextPosition = position - - if (!pinned) { - for (let i = index; i < snaps.length; i++) { - const lower = snaps[i - 1] || snaps[0] - const curr = snaps[i] - const upper = snaps[i + 1] || snaps[snaps.length - 1] - - const lowerThreshold = (curr - lower) / 2 + lower - const upperThreshold = (upper - curr) / 2 + curr - - if (y < curr && y < lowerThreshold) { - index = Math.max(i - 1, minIndex) - break - } else if ( - (y <= curr && // less than current snap point - y > lowerThreshold) || // more than half way to current snap point - (y >= curr && // more than current snap point - y < upperThreshold) // less than half way to upper snap point - ) { - index = i - break - } else if ( - y > upper && // less than upper snap point - y > upperThreshold // more than current snap point - ) { - index = Math.min(i + 1, maxIndex) - break - } - } - - nextPosition = index > 0 ? snaps[index] : 0 - } - - top.value = withTiming(nextPosition, { - duration: 500, - easing: Easing.out(Easing.exp), - }) - - // update UI thread state - state.value = { - ...state.value, - index, - position: nextPosition, - } - - // update JS thread state without cyclical emit - runOnJS(sheet._syncState)({index, position: nextPosition}) - }) - - return ( - - - - - {children} - - - - - - - ) -} - -export function Backdrop({sheet}: {sheet: ReturnType}) { - const active = sheet.position > 0 - const opacity = useSharedValue(0) - const style = useAnimatedStyle(() => ({ - position: 'absolute', - top: '-200%', - bottom: '-200%', - left: 0, - right: 0, - backgroundColor: '#000', - zIndex: 0, - opacity: opacity.value, - display: opacity.value > 0 ? 'flex' : 'none', - })) - - React.useEffect(() => { - opacity.value = withTiming(active ? 0.5 : 0, { - duration: 500, - easing: Easing.out(Easing.exp), - }) - }, [active, opacity]) - - return ( - - sheet.close()} - style={{ - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0, - }} - /> - - ) -} diff --git a/yarn.lock b/yarn.lock index c8dfc2c133..1685200e13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16985,11 +16985,6 @@ slugify@^1.3.4: resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.6.tgz#2d4ac0eacb47add6af9e04d3be79319cbcc7924b" integrity sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw== -smitter@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/smitter/-/smitter-1.1.1.tgz#cade535ccd3b2cc8ad274a9fe9b02937f50a316f" - integrity sha512-6AwxCy1VfHVBpCljZb/QCGUcRmZKL6s3o5NRjJfJKAQxtiC8GCJUpy1OFs3RcJinykoj/p7jIkPrM3Z3bYmgZg== - sockjs@^0.3.24: version "0.3.24" resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce"