about summary refs log tree commit diff
path: root/src/view/shell
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/shell')
-rw-r--r--src/view/shell/desktop-web/index.tsx (renamed from src/view/shell/desktop-web/shell.tsx)0
-rw-r--r--src/view/shell/desktop-web/left-column.tsx6
-rw-r--r--src/view/shell/index.tsx12
-rw-r--r--src/view/shell/mobile/history-menu.tsx99
-rw-r--r--src/view/shell/mobile/index.tsx235
-rw-r--r--src/view/shell/mobile/tabs-selector.tsx158
6 files changed, 494 insertions, 16 deletions
diff --git a/src/view/shell/desktop-web/shell.tsx b/src/view/shell/desktop-web/index.tsx
index 13acbbfed..13acbbfed 100644
--- a/src/view/shell/desktop-web/shell.tsx
+++ b/src/view/shell/desktop-web/index.tsx
diff --git a/src/view/shell/desktop-web/left-column.tsx b/src/view/shell/desktop-web/left-column.tsx
index 082231ec9..fabb8bc94 100644
--- a/src/view/shell/desktop-web/left-column.tsx
+++ b/src/view/shell/desktop-web/left-column.tsx
@@ -1,13 +1,11 @@
 import React from 'react'
 import {Pressable, View, StyleSheet} from 'react-native'
-import {Link} from '@react-navigation/native'
-import {useRoute} from '@react-navigation/native'
 
 export const NavItem: React.FC<{label: string; screen: string}> = ({
   label,
   screen,
 }) => {
-  const route = useRoute()
+  const Link = <></> // TODO
   return (
     <View>
       <Pressable
@@ -18,7 +16,7 @@ export const NavItem: React.FC<{label: string; screen: string}> = ({
         <Link
           style={[
             styles.navItemLink,
-            route.name === screen && styles.navItemLinkSelected,
+            false /* TODO route.name === screen*/ && styles.navItemLinkSelected,
           ]}
           to={{screen, params: {}}}>
           {label}
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
deleted file mode 100644
index db60ed149..000000000
--- a/src/view/shell/index.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react'
-import {SafeAreaView} from 'react-native'
-import {isDesktopWeb} from '../../platform/detection'
-import {DesktopWebShell} from './desktop-web/shell'
-
-export const Shell: React.FC = ({children}) => {
-  return isDesktopWeb ? (
-    <DesktopWebShell>{children}</DesktopWebShell>
-  ) : (
-    <SafeAreaView>{children}</SafeAreaView>
-  )
-}
diff --git a/src/view/shell/mobile/history-menu.tsx b/src/view/shell/mobile/history-menu.tsx
new file mode 100644
index 000000000..b625162d4
--- /dev/null
+++ b/src/view/shell/mobile/history-menu.tsx
@@ -0,0 +1,99 @@
+import React from 'react'
+import {
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  TouchableWithoutFeedback,
+  View,
+} from 'react-native'
+import RootSiblings from 'react-native-root-siblings'
+import {NavigationTabModel} from '../../../state/models/navigation'
+
+export function createBackMenu(tab: NavigationTabModel): RootSiblings {
+  const onPressItem = (index: number) => {
+    sibling.destroy()
+    tab.goToIndex(index)
+  }
+  const onOuterPress = () => sibling.destroy()
+  const sibling = new RootSiblings(
+    (
+      <>
+        <TouchableWithoutFeedback onPress={onOuterPress}>
+          <View style={styles.bg} />
+        </TouchableWithoutFeedback>
+        <View style={[styles.menu, styles.back]}>
+          {tab.backTen.map((item, i) => (
+            <TouchableOpacity
+              key={item.index}
+              style={[styles.menuItem, i !== 0 && styles.menuItemBorder]}
+              onPress={() => onPressItem(item.index)}>
+              <Text>{item.title || item.url}</Text>
+            </TouchableOpacity>
+          ))}
+        </View>
+      </>
+    ),
+  )
+  return sibling
+}
+export function createForwardMenu(tab: NavigationTabModel): RootSiblings {
+  const onPressItem = (index: number) => {
+    sibling.destroy()
+    tab.goToIndex(index)
+  }
+  const onOuterPress = () => sibling.destroy()
+  const sibling = new RootSiblings(
+    (
+      <>
+        <TouchableWithoutFeedback onPress={onOuterPress}>
+          <View style={styles.bg} />
+        </TouchableWithoutFeedback>
+        <View style={[styles.menu, styles.forward]}>
+          {tab.forwardTen.reverse().map((item, i) => (
+            <TouchableOpacity
+              key={item.index}
+              style={[styles.menuItem, i !== 0 && styles.menuItemBorder]}
+              onPress={() => onPressItem(item.index)}>
+              <Text>{item.title || item.url}</Text>
+            </TouchableOpacity>
+          ))}
+        </View>
+      </>
+    ),
+  )
+  return sibling
+}
+
+const styles = StyleSheet.create({
+  bg: {
+    position: 'absolute',
+    top: 0,
+    right: 0,
+    bottom: 0,
+    left: 0,
+    backgroundColor: '#000',
+    opacity: 0.1,
+  },
+  menu: {
+    position: 'absolute',
+    bottom: 80,
+    backgroundColor: '#fff',
+    borderRadius: 8,
+    opacity: 1,
+  },
+  back: {
+    left: 10,
+  },
+  forward: {
+    left: 60,
+  },
+  menuItem: {
+    paddingVertical: 10,
+    paddingLeft: 15,
+    paddingRight: 30,
+  },
+  menuItemBorder: {
+    borderTopWidth: 1,
+    borderTopColor: '#ddd',
+  },
+})
diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx
new file mode 100644
index 000000000..7b0098c51
--- /dev/null
+++ b/src/view/shell/mobile/index.tsx
@@ -0,0 +1,235 @@
+import React, {useRef} from 'react'
+import {observer} from 'mobx-react-lite'
+import {
+  GestureResponderEvent,
+  SafeAreaView,
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  View,
+} from 'react-native'
+import {ScreenContainer, Screen} from 'react-native-screens'
+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 {TabsSelectorModal} from './tabs-selector'
+import {createBackMenu, createForwardMenu} from './history-menu'
+
+const Location = ({icon, title}: {icon: IconProp; title?: string}) => {
+  return (
+    <TouchableOpacity style={styles.location}>
+      {title ? (
+        <FontAwesomeIcon size={16} style={styles.locationIcon} icon={icon} />
+      ) : (
+        <FontAwesomeIcon
+          size={16}
+          style={styles.locationIconLight}
+          icon="magnifying-glass"
+        />
+      )}
+      <Text style={title ? styles.locationText : styles.locationTextLight}>
+        {title || 'Search'}
+      </Text>
+    </TouchableOpacity>
+  )
+}
+
+const Btn = ({
+  icon,
+  inactive,
+  onPress,
+  onLongPress,
+}: {
+  icon: IconProp
+  inactive?: boolean
+  onPress?: (event: GestureResponderEvent) => void
+  onLongPress?: (event: GestureResponderEvent) => void
+}) => {
+  if (inactive) {
+    return (
+      <View style={styles.ctrl}>
+        <FontAwesomeIcon
+          size={18}
+          style={[styles.ctrlIcon, styles.inactive]}
+          icon={icon}
+        />
+      </View>
+    )
+  }
+  return (
+    <TouchableOpacity
+      style={styles.ctrl}
+      onPress={onPress}
+      onLongPress={onLongPress}>
+      <FontAwesomeIcon size={18} style={styles.ctrlIcon} icon={icon} />
+    </TouchableOpacity>
+  )
+}
+
+export const MobileShell: React.FC = observer(() => {
+  const stores = useStores()
+  const tabSelectorRef = useRef<{open: () => void}>()
+  const screenRenderDesc = constructScreenRenderDesc(stores.nav)
+
+  const onPressBack = () => stores.nav.tab.goBack()
+  const onPressForward = () => stores.nav.tab.goForward()
+  const onPressHome = () => stores.nav.navigate('/')
+  const onPressNotifications = () => stores.nav.navigate('/notifications')
+  const onPressTabs = () => tabSelectorRef.current?.open()
+
+  const onLongPressBack = () => createBackMenu(stores.nav.tab)
+  const onLongPressForward = () => createForwardMenu(stores.nav.tab)
+
+  const onNewTab = () => stores.nav.newTab('/')
+  const onChangeTab = (tabIndex: number) => stores.nav.setActiveTab(tabIndex)
+  const onCloseTab = (tabIndex: number) => stores.nav.closeTab(tabIndex)
+
+  return (
+    <View style={styles.outerContainer}>
+      <View style={styles.topBar}>
+        <Location
+          icon={screenRenderDesc.icon}
+          title={stores.nav.tab.current.title}
+        />
+      </View>
+      <SafeAreaView style={styles.innerContainer}>
+        <ScreenContainer>
+          {screenRenderDesc.screens.map(({Com, params, key, activityState}) => (
+            <Screen
+              key={key}
+              style={{backgroundColor: '#fff'}}
+              activityState={activityState}>
+              <Com params={params} />
+            </Screen>
+          ))}
+        </ScreenContainer>
+      </SafeAreaView>
+      <View style={styles.bottomBar}>
+        <Btn
+          icon="angle-left"
+          inactive={!stores.nav.tab.canGoBack}
+          onPress={onPressBack}
+          onLongPress={onLongPressBack}
+        />
+        <Btn
+          icon="angle-right"
+          inactive={!stores.nav.tab.canGoForward}
+          onPress={onPressForward}
+          onLongPress={onLongPressForward}
+        />
+        <Btn icon="house" onPress={onPressHome} />
+        <Btn icon={['far', 'bell']} onPress={onPressNotifications} />
+        <Btn icon={['far', 'clone']} onPress={onPressTabs} />
+      </View>
+      <TabsSelectorModal
+        ref={tabSelectorRef}
+        tabs={stores.nav.tabs}
+        currentTabIndex={stores.nav.tabIndex}
+        onNewTab={onNewTab}
+        onChangeTab={onChangeTab}
+        onCloseTab={onCloseTab}
+      />
+    </View>
+  )
+})
+
+/**
+ * This method produces the information needed by the shell to
+ * render the current screens with screen-caching behaviors.
+ */
+type ScreenRenderDesc = MatchResult & {key: string; activityState: 0 | 1 | 2}
+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 matchRes = match(screen.url)
+      if (isCurrent) {
+        icon = matchRes.icon
+      }
+      return Object.assign(matchRes, {
+        key: `t${tab.id}-s${screen.index}`,
+        activityState: isCurrent ? 2 : 0,
+      })
+    })
+    screens = screens.concat(parsedTabScreens)
+  }
+  return {
+    icon,
+    screens,
+  }
+}
+
+const styles = StyleSheet.create({
+  outerContainer: {
+    height: '100%',
+  },
+  innerContainer: {
+    flex: 1,
+  },
+  topBar: {
+    flexDirection: 'row',
+    backgroundColor: '#fff',
+    borderBottomWidth: 1,
+    borderBottomColor: '#ccc',
+    paddingLeft: 10,
+    paddingRight: 10,
+    paddingTop: 40,
+    paddingBottom: 5,
+  },
+  location: {
+    flex: 1,
+    flexDirection: 'row',
+    borderRadius: 4,
+    paddingLeft: 10,
+    paddingRight: 6,
+    paddingTop: 6,
+    paddingBottom: 6,
+    backgroundColor: '#F8F3F3',
+  },
+  locationIcon: {
+    color: '#DB00FF',
+    marginRight: 8,
+  },
+  locationIconLight: {
+    color: '#909090',
+    marginRight: 8,
+  },
+  locationText: {
+    color: '#000',
+  },
+  locationTextLight: {
+    color: '#868788',
+  },
+  bottomBar: {
+    flexDirection: 'row',
+    backgroundColor: '#fff',
+    borderTopWidth: 1,
+    borderTopColor: '#ccc',
+    paddingLeft: 5,
+    paddingRight: 15,
+    paddingBottom: 20,
+  },
+  ctrl: {
+    flex: 1,
+    paddingTop: 15,
+    paddingBottom: 15,
+  },
+  ctrlIcon: {
+    marginLeft: 'auto',
+    marginRight: 'auto',
+  },
+  inactive: {
+    color: '#888',
+  },
+})
diff --git a/src/view/shell/mobile/tabs-selector.tsx b/src/view/shell/mobile/tabs-selector.tsx
new file mode 100644
index 000000000..10651ba1f
--- /dev/null
+++ b/src/view/shell/mobile/tabs-selector.tsx
@@ -0,0 +1,158 @@
+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',
+  },
+})