diff options
author | Paul Frazee <pfrazee@gmail.com> | 2022-09-09 16:20:46 -0500 |
---|---|---|
committer | Paul Frazee <pfrazee@gmail.com> | 2022-09-09 16:20:46 -0500 |
commit | 530243859c8b60417b094c39873905ef96da4558 (patch) | |
tree | b58447cf78c218ab034a7ff1f3f00241f93203cc | |
parent | 2a7c53f307fdd9a1525f5a78fbf2209504873903 (diff) | |
download | voidsky-530243859c8b60417b094c39873905ef96da4558.tar.zst |
Replace tabs selector with better solution, also fix some bugs with the modal state
-rw-r--r-- | src/state/models/shell.ts | 12 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 34 | ||||
-rw-r--r-- | src/view/com/modals/TabsSelector.tsx | 307 | ||||
-rw-r--r-- | src/view/shell/mobile/index.tsx | 17 | ||||
-rw-r--r-- | src/view/shell/mobile/tabs-selector.tsx | 158 | ||||
-rw-r--r-- | todos.txt | 1 |
6 files changed, 347 insertions, 182 deletions
diff --git a/src/state/models/shell.ts b/src/state/models/shell.ts index 2dddb9a33..80ecbdd48 100644 --- a/src/state/models/shell.ts +++ b/src/state/models/shell.ts @@ -1,6 +1,14 @@ -import {makeAutoObservable} from 'mobx' +import {makeAutoObservable, runInAction} from 'mobx' import {ProfileViewModel} from './profile-view' +export class TabsSelectorModel { + name = 'tabs-selector' + + constructor() { + makeAutoObservable(this) + } +} + export class LinkActionsModel { name = 'link-actions' @@ -36,6 +44,7 @@ export class EditProfileModel { export class ShellModel { isModalActive = false activeModal: + | TabsSelectorModel | LinkActionsModel | SharePostModel | ComposePostModel @@ -48,6 +57,7 @@ export class ShellModel { openModal( modal: + | TabsSelectorModel | LinkActionsModel | SharePostModel | ComposePostModel diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 73ac14469..f3a69d2af 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -1,4 +1,4 @@ -import React, {useRef} from 'react' +import React, {useRef, useEffect} from 'react' import {View} from 'react-native' import {observer} from 'mobx-react-lite' import BottomSheet from '@gorhom/bottom-sheet' @@ -7,11 +7,14 @@ import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' import * as models from '../../../state/models/shell' +import * as TabsSelectorModal from './TabsSelector' import * as LinkActionsModal from './LinkActions' import * as SharePostModal from './SharePost.native' import * as ComposePostModal from './ComposePost' import * as EditProfile from './EditProfile' +const CLOSED_SNAPPOINTS = ['10%'] + export const Modal = observer(function Modal() { const store = useStores() const bottomSheetRef = useRef<BottomSheet>(null) @@ -25,12 +28,24 @@ export const Modal = observer(function Modal() { bottomSheetRef.current?.close() } - if (!store.shell.isModalActive) { - return <View /> - } + useEffect(() => { + if (store.shell.isModalActive) { + bottomSheetRef.current?.expand() + } else { + bottomSheetRef.current?.close() + } + }, [store.shell.isModalActive, bottomSheetRef]) - let snapPoints, element - if (store.shell.activeModal?.name === 'link-actions') { + let snapPoints: (string | number)[] = CLOSED_SNAPPOINTS + let element + if (store.shell.activeModal?.name === 'tabs-selector') { + snapPoints = TabsSelectorModal.snapPoints + element = ( + <TabsSelectorModal.Component + {...(store.shell.activeModal as models.TabsSelectorModel)} + /> + ) + } else if (store.shell.activeModal?.name === 'link-actions') { snapPoints = LinkActionsModal.snapPoints element = ( <LinkActionsModal.Component @@ -59,16 +74,19 @@ export const Modal = observer(function Modal() { /> ) } else { - return <View /> + element = <View /> } return ( <BottomSheet ref={bottomSheetRef} snapPoints={snapPoints} + index={store.shell.isModalActive ? 0 : -1} enablePanDownToClose keyboardBehavior="fillParent" - backdropComponent={createCustomBackdrop(onClose)} + backdropComponent={ + store.shell.isModalActive ? createCustomBackdrop(onClose) : undefined + } onChange={onShareBottomSheetChange}> {element} </BottomSheet> diff --git a/src/view/com/modals/TabsSelector.tsx b/src/view/com/modals/TabsSelector.tsx new file mode 100644 index 000000000..ef7d4705f --- /dev/null +++ b/src/view/com/modals/TabsSelector.tsx @@ -0,0 +1,307 @@ +import React, {createRef, useRef, useMemo} from 'react' +import {observer} from 'mobx-react-lite' +import { + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + TouchableWithoutFeedback, + View, +} from 'react-native' +import {IconProp} from '@fortawesome/fontawesome-svg-core' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import Swipeable from 'react-native-gesture-handler/Swipeable' +import LinearGradient from 'react-native-linear-gradient' +import {useStores} from '../../../state' +import {s, colors, gradients} from '../../lib/styles' +import {match} from '../../routes' + +export const snapPoints = [500] + +export const Component = observer(() => { + const store = useStores() + const tabsRef = useRef<ScrollView>(null) + const tabRefs = useMemo( + () => + Array.from({length: store.nav.tabs.length}).map(() => createRef<View>()), + [store.nav.tabs.length], + ) + + // events + // = + + const onPressNewTab = () => { + store.nav.newTab('/') + onClose() + } + const onPressCloneTab = () => { + store.nav.newTab(store.nav.tab.current.url) + onClose() + } + const onPressShareTab = () => { + onClose() + // TODO + } + const onPressChangeTab = (tabIndex: number) => { + store.nav.setActiveTab(tabIndex) + onClose() + } + const onCloseTab = (tabIndex: number) => store.nav.closeTab(tabIndex) + const onNavigate = (url: string) => { + store.nav.navigate(url) + onClose() + } + const onClose = () => { + store.shell.closeModal() + } + const onLayout = () => { + // focus the current tab + const targetTab = tabRefs[store.nav.tabIndex] + if (tabsRef.current && targetTab.current) { + targetTab.current.measureLayout?.( + tabsRef.current, + (_left: number, top: number) => { + tabsRef.current?.scrollTo({y: top, animated: false}) + }, + () => {}, + ) + } + } + + // rendering + // = + + const FatMenuItem = ({ + icon, + label, + url, + gradient, + }: { + icon: IconProp + label: string + url: string + gradient: keyof typeof gradients + }) => ( + <TouchableOpacity + style={styles.fatMenuItem} + onPress={() => onNavigate(url)}> + <LinearGradient + style={[styles.fatMenuItemIconWrapper]} + colors={[gradients[gradient].start, gradients[gradient].end]} + start={{x: 0, y: 0}} + end={{x: 1, y: 1}}> + <FontAwesomeIcon icon={icon} style={styles.fatMenuItemIcon} size={24} /> + </LinearGradient> + <Text style={styles.fatMenuItemLabel}>{label}</Text> + </TouchableOpacity> + ) + + const renderSwipeActions = () => { + return <View style={[s.p2]} /> + } + + const currentTabIndex = store.nav.tabIndex + return ( + <View onLayout={onLayout}> + <View style={[s.p10, styles.section]}> + <View style={styles.fatMenuItems}> + <FatMenuItem icon="house" label="Feed" url="/" gradient="primary" /> + <FatMenuItem + icon="bell" + label="Notifications" + url="/notifications" + gradient="purple" + /> + <FatMenuItem + icon={['far', 'user']} + label="My Profile" + url="/" + gradient="blue" + /> + <FatMenuItem icon="gear" label="Settings" url="/" gradient="blue" /> + </View> + </View> + <View style={[s.p10, styles.section]}> + <View style={styles.btns}> + <TouchableWithoutFeedback onPress={onPressNewTab}> + <View style={[styles.btn]}> + <View style={styles.btnIcon}> + <FontAwesomeIcon size={16} icon="plus" /> + </View> + <Text style={styles.btnText}>New tab</Text> + </View> + </TouchableWithoutFeedback> + <TouchableWithoutFeedback onPress={onPressCloneTab}> + <View style={[styles.btn]}> + <View style={styles.btnIcon}> + <FontAwesomeIcon size={16} icon={['far', 'clone']} /> + </View> + <Text style={styles.btnText}>Clone tab</Text> + </View> + </TouchableWithoutFeedback> + <TouchableWithoutFeedback onPress={onPressShareTab}> + <View style={[styles.btn]}> + <View style={styles.btnIcon}> + <FontAwesomeIcon size={16} icon="share" /> + </View> + <Text style={styles.btnText}>Share</Text> + </View> + </TouchableWithoutFeedback> + </View> + </View> + <View style={[s.p10, styles.section, styles.sectionGrayBg]}> + <ScrollView ref={tabsRef} style={styles.tabs}> + {store.nav.tabs.map((tab, tabIndex) => { + const {icon} = match(tab.current.url) + const isActive = tabIndex === currentTabIndex + return ( + <Swipeable + key={tab.id} + renderLeftActions={renderSwipeActions} + renderRightActions={renderSwipeActions} + leftThreshold={100} + rightThreshold={100} + onSwipeableWillOpen={() => onCloseTab(tabIndex)}> + <View + ref={tabRefs[tabIndex]} + style={[ + styles.tab, + styles.existing, + isActive && styles.active, + ]}> + <TouchableWithoutFeedback + onPress={() => onPressChangeTab(tabIndex)}> + <View style={styles.tabIcon}> + <FontAwesomeIcon size={20} icon={icon} /> + </View> + </TouchableWithoutFeedback> + <TouchableWithoutFeedback + onPress={() => onPressChangeTab(tabIndex)}> + <Text + ellipsizeMode="tail" + numberOfLines={1} + suppressHighlighting={true} + style={[ + styles.tabText, + isActive && styles.tabTextActive, + ]}> + {tab.current.title || tab.current.url} + </Text> + </TouchableWithoutFeedback> + <TouchableWithoutFeedback + onPress={() => onCloseTab(tabIndex)}> + <View style={styles.tabClose}> + <FontAwesomeIcon + size={14} + icon="x" + style={styles.tabCloseIcon} + /> + </View> + </TouchableWithoutFeedback> + </View> + </Swipeable> + ) + })} + </ScrollView> + </View> + </View> + ) +}) + +const styles = StyleSheet.create({ + section: { + borderBottomColor: colors.gray2, + borderBottomWidth: 1, + }, + sectionGrayBg: { + backgroundColor: colors.gray1, + }, + fatMenuItems: { + flexDirection: 'row', + marginTop: 10, + marginBottom: 10, + }, + fatMenuItem: { + width: 90, + alignItems: 'center', + marginRight: 6, + }, + fatMenuItemIconWrapper: { + borderRadius: 6, + width: 60, + height: 60, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 5, + shadowColor: '#000', + shadowOpacity: 0.2, + shadowOffset: {width: 0, height: 2}, + shadowRadius: 2, + }, + fatMenuItemIcon: { + color: colors.white, + }, + fatMenuItemLabel: { + fontSize: 13, + }, + tabs: { + height: 240, + }, + tab: { + flexDirection: 'row', + backgroundColor: colors.gray1, + alignItems: 'center', + borderRadius: 4, + paddingLeft: 12, + paddingRight: 16, + marginBottom: 4, + }, + existing: { + borderColor: colors.gray4, + borderWidth: 1, + }, + active: { + backgroundColor: colors.white, + borderColor: colors.black, + borderWidth: 1, + }, + tabIcon: {}, + tabText: { + flex: 1, + paddingHorizontal: 10, + paddingVertical: 12, + fontSize: 16, + }, + tabTextActive: { + fontWeight: '500', + }, + tabClose: { + padding: 2, + }, + tabCloseIcon: { + color: '#655', + }, + btns: { + flexDirection: 'row', + paddingTop: 2, + }, + btn: { + flexDirection: 'row', + flex: 1, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.gray1, + borderRadius: 4, + marginRight: 5, + paddingLeft: 12, + paddingRight: 16, + paddingVertical: 10, + }, + btnIcon: { + marginRight: 8, + }, + btnText: { + fontWeight: '500', + fontSize: 16, + }, +}) diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx index b752221f3..2a981e100 100644 --- a/src/view/shell/mobile/index.tsx +++ b/src/view/shell/mobile/index.tsx @@ -23,9 +23,9 @@ 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 {TabsSelectorModel} from '../../../state/models/shell' 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' @@ -106,7 +106,6 @@ const Btn = ({ 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<number>(0) @@ -129,15 +128,11 @@ export const MobileShell: React.FC = observer(() => { const onPressForward = () => store.nav.tab.goForward() const onPressHome = () => store.nav.navigate('/') const onPressNotifications = () => store.nav.navigate('/notifications') - const onPressTabs = () => tabSelectorRef.current?.open() + const onPressTabs = () => store.shell.openModal(new TabsSelectorModel()) 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 = () => store.nav.tab.goBack() const swipeGesture = Gesture.Pan() .onUpdate(e => { @@ -231,14 +226,6 @@ export const MobileShell: React.FC = observer(() => { <Btn icon={['far', 'bell']} onPress={onPressNotifications} /> <Btn icon={['far', 'clone']} onPress={onPressTabs} /> </View> - <TabsSelectorModal - ref={tabSelectorRef} - tabs={store.nav.tabs} - currentTabIndex={store.nav.tabIndex} - onNewTab={onNewTab} - onChangeTab={onChangeTab} - onCloseTab={onCloseTab} - /> <Modal /> {isLocationMenuActive && ( <LocationNavigator diff --git a/src/view/shell/mobile/tabs-selector.tsx b/src/view/shell/mobile/tabs-selector.tsx deleted file mode 100644 index 10651ba1f..000000000 --- a/src/view/shell/mobile/tabs-selector.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import React, {forwardRef, useState, useImperativeHandle, useRef} from 'react' -import {StyleSheet, Text, TouchableWithoutFeedback, View} from 'react-native' -import BottomSheet from '@gorhom/bottom-sheet' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {s} from '../../lib/styles' -import {NavigationTabModel} from '../../../state/models/navigation' -import {createCustomBackdrop} from '../../com/util/BottomSheetCustomBackdrop' -import {match} from '../../routes' - -const TAB_HEIGHT = 38 -const TAB_SPACING = 5 -const BOTTOM_MARGIN = 70 - -export const TabsSelectorModal = forwardRef(function TabsSelectorModal( - { - onNewTab, - onChangeTab, - onCloseTab, - tabs, - currentTabIndex, - }: { - onNewTab: () => void - onChangeTab: (tabIndex: number) => void - onCloseTab: (tabIndex: number) => void - tabs: NavigationTabModel[] - currentTabIndex: number - }, - ref, -) { - const [isOpen, setIsOpen] = useState<boolean>(false) - const [snapPoints, setSnapPoints] = useState<number[]>([100]) - const bottomSheetRef = useRef<BottomSheet>(null) - - useImperativeHandle(ref, () => ({ - open() { - setIsOpen(true) - setSnapPoints([ - (tabs.length + 1) * (TAB_HEIGHT + TAB_SPACING) + BOTTOM_MARGIN, - ]) - bottomSheetRef.current?.expand() - }, - })) - - const onShareBottomSheetChange = (snapPoint: number) => { - if (snapPoint === -1) { - setIsOpen(false) - } - } - const onPressNewTab = () => { - onNewTab() - onClose() - } - const onPressChangeTab = (tabIndex: number) => { - onChangeTab(tabIndex) - onClose() - } - const onClose = () => { - setIsOpen(false) - bottomSheetRef.current?.close() - } - return ( - <BottomSheet - ref={bottomSheetRef} - index={-1} - snapPoints={snapPoints} - enablePanDownToClose - backdropComponent={isOpen ? createCustomBackdrop(onClose) : undefined} - onChange={onShareBottomSheetChange}> - <View style={s.p10}> - {tabs.map((tab, tabIndex) => { - const {icon} = match(tab.current.url) - const isActive = tabIndex === currentTabIndex - return ( - <View - key={tabIndex} - style={[styles.tab, styles.existing, isActive && styles.active]}> - <TouchableWithoutFeedback - onPress={() => onPressChangeTab(tabIndex)}> - <View style={styles.tabIcon}> - <FontAwesomeIcon size={16} icon={icon} /> - </View> - </TouchableWithoutFeedback> - <TouchableWithoutFeedback - onPress={() => onPressChangeTab(tabIndex)}> - <Text - style={[styles.tabText, isActive && styles.tabTextActive]}> - {tab.current.title || tab.current.url} - </Text> - </TouchableWithoutFeedback> - <TouchableWithoutFeedback onPress={() => onCloseTab(tabIndex)}> - <View style={styles.tabClose}> - <FontAwesomeIcon - size={16} - icon="x" - style={styles.tabCloseIcon} - /> - </View> - </TouchableWithoutFeedback> - </View> - ) - })} - <TouchableWithoutFeedback onPress={onPressNewTab}> - <View style={[styles.tab, styles.create]}> - <View style={styles.tabIcon}> - <FontAwesomeIcon size={16} icon="plus" /> - </View> - <Text style={styles.tabText}>New tab</Text> - </View> - </TouchableWithoutFeedback> - </View> - </BottomSheet> - ) -}) - -const styles = StyleSheet.create({ - tab: { - flexDirection: 'row', - width: '100%', - borderRadius: 4, - height: TAB_HEIGHT, - marginBottom: TAB_SPACING, - }, - existing: { - borderColor: '#000', - borderWidth: 1, - }, - create: { - backgroundColor: '#F8F3F3', - }, - active: { - backgroundColor: '#faf0f0', - borderColor: '#f00', - borderWidth: 1, - }, - tabIcon: { - paddingTop: 10, - paddingBottom: 10, - paddingLeft: 15, - paddingRight: 10, - }, - tabText: { - flex: 1, - paddingTop: 10, - paddingBottom: 10, - }, - tabTextActive: { - fontWeight: 'bold', - }, - tabClose: { - paddingTop: 10, - paddingBottom: 10, - paddingLeft: 10, - paddingRight: 15, - }, - tabCloseIcon: { - color: '#655', - }, -}) diff --git a/todos.txt b/todos.txt index 1273e977b..2a37f685f 100644 --- a/todos.txt +++ b/todos.txt @@ -2,6 +2,7 @@ Paul's todo list - General - Update to RN 0.70 + - Add close animation to tabs selector - Composer - Update the view after creating a post - Profile |