import React, {useState, useRef, useMemo} from 'react' import {observer} from 'mobx-react-lite' import { useWindowDimensions, GestureResponderEvent, Image, SafeAreaView, StyleSheet, Text, TouchableOpacity, View, } from 'react-native' import {ScreenContainer, Screen} from 'react-native-screens' import {GestureDetector, Gesture} from 'react-native-gesture-handler' import Animated, { useSharedValue, useAnimatedStyle, withTiming, runOnJS, interpolate, } from 'react-native-reanimated' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {IconProp} from '@fortawesome/fontawesome-svg-core' import {useStores} from '../../../state' import {NavigationModel} from '../../../state/models/navigation' import {match, MatchResult} from '../../routes' import {Modal} from '../../com/modals/Modal' import {TabsSelectorModal} from './tabs-selector' import {LocationNavigator} from './location-navigator' import {createBackMenu, createForwardMenu} from './history-menu' import {createAccountsMenu} from './accounts-menu' import {createLocationMenu} from './location-menu' import {s, colors} from '../../lib/styles' import {AVIS} from '../../lib/assets' const locationIconNeedsNudgeUp = (icon: IconProp) => icon === 'house' const SWIPE_GESTURE_HIT_SLOP = {left: 0, top: 0, width: 20, bottom: 0} const SWIPE_GESTURE_TRIGGER = 0.5 const Location = ({ icon, title, onPress, }: { icon: IconProp title?: string onPress?: (event: GestureResponderEvent) => void }) => { const nudgeUp = locationIconNeedsNudgeUp(icon) return ( {title ? ( ) : ( )} {title || 'Search'} ) } const Btn = ({ icon, inactive, onPress, onLongPress, }: { icon: IconProp inactive?: boolean onPress?: (event: GestureResponderEvent) => void onLongPress?: (event: GestureResponderEvent) => void }) => { if (inactive) { return ( ) } return ( ) } export const MobileShell: React.FC = observer(() => { const store = useStores() const tabSelectorRef = useRef<{open: () => void}>() const [isLocationMenuActive, setLocationMenuActive] = useState(false) const winDim = useWindowDimensions() const swipeGestureInterp = useSharedValue(0) const screenRenderDesc = constructScreenRenderDesc(store.nav) const onPressAvi = () => createAccountsMenu({ debug_onPressItem: () => store.nav.navigate('/profile/alice.com'), }) const onPressLocation = () => setLocationMenuActive(true) const onPressEllipsis = () => createLocationMenu() const onNavigateLocation = (url: string) => { setLocationMenuActive(false) store.nav.navigate(url) } const onDismissLocationNavigator = () => setLocationMenuActive(false) const onPressBack = () => store.nav.tab.goBack() const onPressForward = () => store.nav.tab.goForward() const onPressHome = () => store.nav.navigate('/') const onPressNotifications = () => store.nav.navigate('/notifications') const onPressTabs = () => tabSelectorRef.current?.open() const onLongPressBack = () => createBackMenu(store.nav.tab) const onLongPressForward = () => createForwardMenu(store.nav.tab) const onNewTab = () => store.nav.newTab('/') const onChangeTab = (tabIndex: number) => store.nav.setActiveTab(tabIndex) const onCloseTab = (tabIndex: number) => store.nav.closeTab(tabIndex) const goBack = useMemo(() => { return () => { store.nav.tab.goBack() } }, [store.nav.tab]) const swipeGesture = useMemo( () => Gesture.Pan() .hitSlop(SWIPE_GESTURE_HIT_SLOP) .onUpdate(e => { if (store.nav.tab.canGoBack) { swipeGestureInterp.value = Math.max( e.translationX / winDim.width, 0, ) } }) .onEnd(_e => { if (swipeGestureInterp.value >= SWIPE_GESTURE_TRIGGER) { runOnJS(goBack)() swipeGestureInterp.value = withTiming(1, {duration: 100}, () => { swipeGestureInterp.value = 0 }) } else { swipeGestureInterp.value = withTiming(0, {duration: 100}) } }), [swipeGestureInterp, winDim, store.nav.tab.canGoBack, goBack], ) const swipeTransform = useAnimatedStyle(() => ({ transform: [{translateX: swipeGestureInterp.value * winDim.width}], })) const swipeOpacity = useAnimatedStyle(() => ({ opacity: interpolate(swipeGestureInterp.value, [0, 1.0], [0.6, 0.0]), })) return ( {screenRenderDesc.screens.map( ({Com, params, key, current, previous}) => { return ( ) }, )} {isLocationMenuActive && ( )} ) }) /** * This method produces the information needed by the shell to * render the current screens with screen-caching behaviors. */ type ScreenRenderDesc = MatchResult & { key: string current: boolean previous: boolean } function constructScreenRenderDesc(nav: NavigationModel): { icon: IconProp screens: ScreenRenderDesc[] } { let icon: IconProp = 'magnifying-glass' let screens: ScreenRenderDesc[] = [] for (const tab of nav.tabs) { const tabScreens = [ ...tab.getBackList(5), Object.assign({}, tab.current, {index: tab.index}), ] const parsedTabScreens = tabScreens.map(screen => { const isCurrent = nav.isCurrentScreen(tab.id, screen.index) const isPrevious = nav.isCurrentScreen(tab.id, screen.index + 1) const matchRes = match(screen.url) if (isCurrent) { icon = matchRes.icon } return Object.assign(matchRes, { key: `t${tab.id}-s${screen.index}`, current: isCurrent, previous: isPrevious, }) as ScreenRenderDesc }) screens = screens.concat(parsedTabScreens) } return { icon, screens, } } const styles = StyleSheet.create({ outerContainer: { height: '100%', }, innerContainer: { flex: 1, }, screenContainer: { flex: 1, }, screen: { backgroundColor: colors.gray1, }, screenMask: { position: 'absolute', top: 0, bottom: 0, left: 0, right: 0, backgroundColor: '#000', opacity: 0.5, }, topBar: { flexDirection: 'row', backgroundColor: colors.white, borderBottomWidth: 1, borderBottomColor: colors.gray2, paddingLeft: 10, paddingRight: 10, paddingTop: 40, paddingBottom: 5, }, avi: { width: 28, height: 28, marginRight: 8, borderRadius: 14, }, location: { flex: 1, flexDirection: 'row', borderRadius: 6, paddingLeft: 10, paddingRight: 6, paddingTop: 6, paddingBottom: 6, backgroundColor: colors.gray1, // justifyContent: 'center', }, locationIcon: { color: colors.gray5, marginTop: 3, marginRight: 6, }, locationIconNudgeUp: { marginTop: 2, }, locationIconLight: { color: colors.gray5, marginTop: 2, marginRight: 8, }, locationText: { color: colors.black, }, locationTextLight: { color: colors.gray4, }, topBarBtn: { marginLeft: 8, justifyContent: 'center', borderRadius: 6, paddingHorizontal: 6, }, bottomBar: { flexDirection: 'row', backgroundColor: colors.white, borderTopWidth: 1, borderTopColor: colors.gray2, paddingLeft: 5, paddingRight: 15, paddingBottom: 20, }, ctrl: { flex: 1, paddingTop: 15, paddingBottom: 15, }, ctrlIcon: { marginLeft: 'auto', marginRight: 'auto', }, inactive: { color: colors.gray3, }, })