From c1a9f6b63d8a3c36e2ad452ced2a4ef71bbc2eae Mon Sep 17 00:00:00 2001 From: Radek Czemerys Date: Sun, 21 Jun 2020 20:58:06 +0200 Subject: [PATCH] feature: refactor ActionSheet & use new SIdeMenu --- package.json | 2 +- src/App.tsx | 69 ++++++++------ src/screens/SideMenu/MainSideMenu.styled.ts | 19 ++++ src/screens/SideMenu/MainSideMenu.tsx | 96 +++++++++++++++++++ src/screens/SideMenu/SideMenuHero.styled.ts | 38 ++++++++ src/screens/SideMenu/SideMenuHero.tsx | 72 ++++++++++++++ src/style/ActionSheetWrapper.tsx | 100 -------------------- src/style/useCustomActionSheet.ts | 66 +++++++++++++ yarn.lock | 14 ++- 9 files changed, 339 insertions(+), 137 deletions(-) create mode 100644 src/screens/SideMenu/MainSideMenu.styled.ts create mode 100644 src/screens/SideMenu/MainSideMenu.tsx create mode 100644 src/screens/SideMenu/SideMenuHero.styled.ts create mode 100644 src/screens/SideMenu/SideMenuHero.tsx delete mode 100644 src/style/ActionSheetWrapper.tsx create mode 100644 src/style/useCustomActionSheet.ts diff --git a/package.json b/package.json index acc5664e..8a2e12dc 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "postinstall": "patch-package" }, "dependencies": { + "@expo/react-native-action-sheet": "^3.8.0", "@react-native-community/async-storage": "1.11.0", "@react-native-community/masked-view": "^0.1.10", "@react-navigation/native": "^5.5.1", @@ -33,7 +34,6 @@ "moment": "^2.27.0", "react": "16.11.0", "react-native": "0.62.2", - "react-native-actionsheet": "standardnotes/react-native-actionsheet#9cb323f", "react-native-aes-crypto": "standardnotes/react-native-aes#d156762", "react-native-alternate-icons": "standardnotes/react-native-alternate-icons#1d335d", "react-native-fab": "standardnotes/react-native-fab#cb60e00", diff --git a/src/App.tsx b/src/App.tsx index 8c1a2d90..8cc53e5b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,29 +1,9 @@ import { Client } from 'bugsnag-react-native'; import React, { useState, useEffect, useCallback, useContext } from 'react'; +import { Platform, Dimensions, ScaledSize } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import { ThemeProvider, ThemeContext } from 'styled-components/native'; -// import { createDrawerNavigator, DrawerActions } from 'react-navigation-drawer'; -// import Authenticate from '@Screens/Authentication/Authenticate'; -// import Compose from '@Screens/Compose'; -// import { -// SCREEN_AUTHENTICATE, -// SCREEN_HOME, -// SCREEN_NOTES, -// SCREEN_COMPOSE, -// SCREEN_INPUT_MODAL, -// SCREEN_SETTINGS, -// SCREEN_MANAGE_PRIVILEGES, -// SCREEN_KEY_RECOVERY, -// } from '@Screens/screens'; -// import InputModal from '@Screens/InputModal'; -// import KeyRecovery from '@Screens/KeyRecovery'; -// import MainSideMenu from '@Screens/SideMenu/MainSideMenu'; -// import ManagePrivileges from '@Screens/ManagePrivileges'; -// import NoteSideMenu from '@Screens/SideMenu/NoteSideMenu'; -// import Root from '@Screens/Root'; -// import Settings from '@Screens/Settings/Settings'; -// import SideMenuManager from '@Screens/SideMenu/SideMenuManager'; import { CurrentApplication, ContextProvider } from './ApplicationContext'; import { Root } from '@Screens/Root'; import { @@ -41,8 +21,12 @@ import { import { ICON_MENU } from '@Style/icons'; import { StyleKit } from '@Style/StyleKit'; import Icon from 'react-native-vector-icons/Ionicons'; +import { enableScreens } from 'react-native-screens'; import DrawerLayout from 'react-native-gesture-handler/DrawerLayout'; -import { View, Text } from 'react-native'; +import { ActionSheetProvider } from '@expo/react-native-action-sheet'; +import { MainSideMenu } from '@Screens/SideMenu/MainSideMenu'; + +enableScreens(); if (__DEV__ === false) { // bugsnag @@ -146,22 +130,43 @@ type AppStackNavigatorParamList = { [SCREEN_COMPOSE]: undefined; }; +const getDefaultDrawerWidth = ({ height, width }: ScaledSize) => { + /* + * Default drawer width is screen width - header height + * with a max width of 280 on mobile and 320 on tablet + * https://material.io/guidelines/patterns/navigation-drawer.html + */ + const smallerAxisSize = Math.min(height, width); + const isLandscape = width > height; + const isTablet = smallerAxisSize >= 600; + const appBarHeight = Platform.OS === 'ios' ? (isLandscape ? 32 : 44) : 56; + const maxWidth = isTablet ? 320 : 280; + + return Math.min(smallerAxisSize - appBarHeight, maxWidth); +}; + const AppStack = createStackNavigator(); const AppStackComponent = () => { const theme = useContext(ThemeContext); const drawerRef = React.createRef(); - const renderDrawer = () => { - return ( - - I am in the drawer! - - ); - }; + const [dimensions, setDimensions] = React.useState(() => + Dimensions.get('window') + ); + React.useEffect(() => { + const updateDimensions = ({ window }: { window: ScaledSize }) => { + setDimensions(window); + }; + + Dimensions.addEventListener('change', updateDimensions); + + return () => Dimensions.removeEventListener('change', updateDimensions); + }, []); + const renderDrawer = () => ; return ( { - + + + diff --git a/src/screens/SideMenu/MainSideMenu.styled.ts b/src/screens/SideMenu/MainSideMenu.styled.ts new file mode 100644 index 00000000..148f78c9 --- /dev/null +++ b/src/screens/SideMenu/MainSideMenu.styled.ts @@ -0,0 +1,19 @@ +import styled from 'styled-components/native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +// We want top color to be different from bottom color of safe area. +// See https://stackoverflow.com/questions/47725607/react-native-safeareaview-background-color-how-to-assign-two-different-backgro +export const FirstSafeAreaView = styled(SafeAreaView)` + flex: 0; + background-color: ${({ theme }) => theme.stylekitContrastBackgroundColor}; +`; +export const MainSafeAreaView = styled(SafeAreaView)` + flex: 1; + background-color: ${({ theme }) => theme.stylekitBackgroundColor}; + color: ${({ theme }) => theme.stylekitForegroundColor}; +`; +export const SideMenuSectionContainer = styled.ScrollView` + padding: 15px; + flex: 1; + background-color: ${({ theme }) => theme.stylekitBackgroundColor}; +`; diff --git a/src/screens/SideMenu/MainSideMenu.tsx b/src/screens/SideMenu/MainSideMenu.tsx new file mode 100644 index 00000000..89efd686 --- /dev/null +++ b/src/screens/SideMenu/MainSideMenu.tsx @@ -0,0 +1,96 @@ +import React, { Fragment, useContext, useState, useEffect } from 'react'; +import { Platform } from 'react-native'; +import FAB from 'react-native-fab'; +import { ThemeContext } from 'styled-components/native'; +import { ContentType } from 'snjs'; +import { + MainSafeAreaView, + FirstSafeAreaView, + SideMenuSectionContainer, +} from './MainSideMenu.styled'; +import { SideMenuHero } from './SideMenuHero'; +import Icon from 'react-native-vector-icons/Ionicons'; +import { StyleKit } from '@Style/StyleKit'; +import { ICON_SETTINGS } from '@Style/icons'; +import { SideMenuSection } from './SideMenuSection'; +import { ApplicationContext } from '@Root/ApplicationContext'; +import { useCustomActionSheet } from '@Style/ActionSheetWrapper'; + +export const MainSideMenu = (): JSX.Element => { + const theme = useContext(ThemeContext); + const application = useContext(ApplicationContext); + const { showActionSheet } = useCustomActionSheet(); + const [outOfSync, setOutOfSync] = useState(false); + useEffect(() => { + const performSyncResolution = async () => { + const isOutofSync = await application!.isOutOfSync(); + setOutOfSync(isOutofSync); + }; + performSyncResolution(); + }, [application]); + const outOfSyncPressed = () => { + application!.alertService!.confirm( + "We've detected that the data in the current application session may not match the data on the server. This can happen due to poor network conditions, or if a large note fails to download on your device. To resolve this issue, we recommend first creating a backup of your data in the Settings screen, the signing out of your account and signing back in.", + 'Potentially Out of Sync', + 'Open Settings', + undefined, + () => {} // TODO: nav open settings + ); + }; + return ( + + + + {}} // TODO: nav open settings + onOutOfSyncPress={outOfSyncPressed} + /> + + {/* + + + + + + + + + + */} + + {}} // TODO: nav open settings + visible={true} + size={29} + paddingTop={Platform.OS ? 2 : 0} + iconTextComponent={ + + } + /> + + {/* {this.state.actionSheet && this.state.actionSheet} */} + + + ); +}; diff --git a/src/screens/SideMenu/SideMenuHero.styled.ts b/src/screens/SideMenu/SideMenuHero.styled.ts new file mode 100644 index 00000000..2bd884f2 --- /dev/null +++ b/src/screens/SideMenu/SideMenuHero.styled.ts @@ -0,0 +1,38 @@ +import styled from 'styled-components/native'; + +export const Cell = styled.View` + background-color: ${({ theme }) => theme.stylekitContrastBackgroundColor}; + border-bottom-color: ${({ theme }) => theme.stylekitContrastBorderColor}; + border-bottom-width: 1px; + padding: 15px; + padding-top: 10px; + padding-bottom: 12px; + padding-right: 25px; +`; +export const Touchable = styled.TouchableOpacity``; +export const Title = styled.Text` + font-weight: bold; + font-size: 16px; + color: ${({ theme }) => theme.stylekitContrastForegroundColor}; + margin-bottom: 3px; +`; +export const SubTitle = styled.Text` + font-size: 13px; + color: ${({ theme }) => theme.stylekitContrastForegroundColor}; + opacity: 0.6; +`; +export const OutOfSyncContainer = styled.TouchableOpacity` + flex: 0px; + flex-direction: row; + align-items: center; +`; +export const IconCircle = styled.View` + margin-top: 10px; + width: 15px; +`; +export const OutOfSyncLabel = styled.Text` + margin-top: 10px; + font-size: 13px; + color: ${({ theme }) => theme.stylekitWarningColor}; + font-weight: bold; +`; diff --git a/src/screens/SideMenu/SideMenuHero.tsx b/src/screens/SideMenu/SideMenuHero.tsx new file mode 100644 index 00000000..5256268c --- /dev/null +++ b/src/screens/SideMenu/SideMenuHero.tsx @@ -0,0 +1,72 @@ +import React, { useContext, useCallback } from 'react'; +import { ApplicationContext } from '@Root/ApplicationContext'; +import { ContentType } from 'snjs'; +import { + Cell, + Touchable, + Title, + SubTitle, + OutOfSyncContainer, + IconCircle, + OutOfSyncLabel, +} from './SideMenuHero.styled'; +import { ViewProps } from 'react-native'; +import { Circle } from '@Components/Circle'; +import { ThemeContext } from 'styled-components/native'; + +type Props = { + onPress: () => void; + outOfSync: boolean; + onOutOfSyncPress: () => void; + testID: ViewProps['testID']; +}; + +export const SideMenuHero: React.FC = props => { + const application = useContext(ApplicationContext); + const theme = useContext(ThemeContext); + const getText = useCallback(() => { + const noAccount = application?.noAccount(); + const hasEncryption = !noAccount; // TODO: check this + if (noAccount) { + return { + title: 'Data Not Backed Up', + text: hasEncryption + ? 'Sign in or register to enable sync to your other devices.' + : 'Sign in or register to add encryption and enable sync to your other devices.', + }; + } else { + const user = application?.getUser(); + const email = user?.email; + const items = application!.getItems([ContentType.Note, ContentType.Tag]); + const itemsStatus = + items.length + '/' + items.length + ' notes and tags encrypted'; + return { + title: email, + text: itemsStatus, + }; + } + }, []); + const textData = getText(); + return ( + + + {textData.title} + + + {textData.text} + + {props.outOfSync && ( + + + + + Potentially Out of Sync + + )} + + ); +}; diff --git a/src/style/ActionSheetWrapper.tsx b/src/style/ActionSheetWrapper.tsx deleted file mode 100644 index f334cb42..00000000 --- a/src/style/ActionSheetWrapper.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react'; -import { StyleSheet, Platform } from 'react-native'; -import ActionSheet from 'react-native-actionsheet'; - -type Option = - | { - text: string; - key?: string; - callback: () => void; - destructive?: boolean; - } - | { - text: string; - key?: string; - callback: (option: Option) => void; - destructive?: boolean; - }; - -export default class ActionSheetWrapper { - options: Option[]; - destructiveIndex?: number; - cancelIndex: number; - title: string; - actionSheet = React.createRef(); - static BuildOption({ text, key, callback, destructive }: Option) { - return { - text, - key, - callback, - destructive, - }; - } - - constructor(props: { - title: string; - options: Option[]; - onCancel: () => void; - }) { - const cancelOption: Option[] = [ - { - text: 'Cancel', - callback: props.onCancel, - key: 'CancelItem', - destructive: false, - }, - ]; - this.options = props.options.concat(cancelOption); - - this.destructiveIndex = this.options.findIndex(item => item.destructive); - this.cancelIndex = this.options.length - 1; - this.title = props.title; - } - - show() { - this.actionSheet.current?.show(); - } - - handleActionSheetPress = (index: number) => { - let option = this.options[index]; - option.callback && option.callback(option); - }; - - actionSheetElement() { - return ( - { - return option.text; - })} - cancelButtonIndex={this.cancelIndex} - destructiveButtonIndex={this.destructiveIndex} - onPress={this.handleActionSheetPress} - {...ActionSheetWrapper.actionSheetStyles()} - /> - ); - } - - static actionSheetStyles() { - return { - wrapperStyle: StyleKit.styles.actionSheetWrapper, - overlayStyle: StyleKit.styles.actionSheetOverlay, - bodyStyle: StyleKit.styles.actionSheetBody, - - buttonWrapperStyle: StyleKit.styles.actionSheetButtonWrapper, - buttonTitleStyle: StyleKit.styles.actionSheetButtonTitle, - - titleWrapperStyle: StyleKit.styles.actionSheetTitleWrapper, - titleTextStyle: StyleKit.styles.actionSheetTitleText, - tintColor: - Platform.OS == 'ios' ? undefined : StyleKit.variables.stylekitInfoColor, - - buttonUnderlayColor: StyleKit.variables.stylekitBorderColor, - - cancelButtonWrapperStyle: StyleKit.styles.actionSheetCancelButtonWrapper, - cancelButtonTitleStyle: StyleKit.styles.actionSheetCancelButtonTitle, - cancelMargin: StyleSheet.hairlineWidth, - }; - } -} diff --git a/src/style/useCustomActionSheet.ts b/src/style/useCustomActionSheet.ts new file mode 100644 index 00000000..d48062e1 --- /dev/null +++ b/src/style/useCustomActionSheet.ts @@ -0,0 +1,66 @@ +import { useContext } from 'react'; +import { useActionSheet } from '@expo/react-native-action-sheet'; +import { ThemeContext } from 'styled-components/native'; + +type Option = + | { + text: string; + key?: string; + callback?: () => void; + destructive?: boolean; + } + | { + text: string; + key?: string; + callback?: (option: Option) => void; + destructive?: boolean; + }; + +export const createActionSheetOptions = () => {}; + +export const useCustomActionSheet = () => { + const { showActionSheetWithOptions } = useActionSheet(); + const theme = useContext(ThemeContext); + + const showActionSheet = ( + title: string, + options: Option[], + onCancel?: () => void + ) => { + const cancelOption: Option[] = [ + { + text: 'Cancel', + callback: onCancel || (() => {}), + key: 'CancelItem', + destructive: false, + }, + ]; + const tempOptions = options.concat(cancelOption); + const destructiveIndex = tempOptions.findIndex(item => item.destructive); + const cancelIndex = tempOptions.length - 1; + + showActionSheetWithOptions( + { + options: tempOptions.map(option => option.text), + destructiveButtonIndex: destructiveIndex, + cancelButtonIndex: cancelIndex, + title, + containerStyle: { + backgroundColor: theme.stylekitBorderColor, + }, + textStyle: { + color: theme.stylekitForegroundColor, + }, + titleTextStyle: { + color: theme.stylekitForegroundColor, + }, + }, + buttonIndex => { + let option = tempOptions[buttonIndex]; + option.callback && option.callback(option); + } + ); + }; + + return { showActionSheet }; +}; diff --git a/yarn.lock b/yarn.lock index 0abe33f1..ebde37ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -766,6 +766,14 @@ resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== +"@expo/react-native-action-sheet@^3.8.0": + version "3.8.0" + resolved "https://registry.yarnpkg.com/@expo/react-native-action-sheet/-/react-native-action-sheet-3.8.0.tgz#0db8b70ea8550ceb2983abda8584efa3a61d7389" + integrity sha512-tCfwysuqy0sfaN+aA98IKUrwCLKsbDHSYLcnHrx9wNbawOHNez8rSeFtieAS48/HyrPI75yg/ZGvxe6UsJRS8Q== + dependencies: + "@types/hoist-non-react-statics" "^3.3.1" + hoist-non-react-statics "^3.3.0" + "@hapi/address@2.x.x": version "2.1.4" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" @@ -1291,7 +1299,7 @@ resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.36.tgz#17ce0a235e9ffbcdcdf5095646b374c2bf615a4c" integrity sha512-7TUK/k2/QGpEAv/BCwSHlYu3NXZhQ9ZwBYpzr9tjlPIL2C5BeGhH3DmVavRx3ZNyELX5TLC91JTz/cen6AAtIQ== -"@types/hoist-non-react-statics@*": +"@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.1": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== @@ -6197,10 +6205,6 @@ react-is@^16.12.0, react-is@^16.13.0, react-is@^16.7.0, react-is@^16.8.1, react- resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-native-actionsheet@standardnotes/react-native-actionsheet#9cb323f: - version "2.4.2" - resolved "https://codeload.github.com/standardnotes/react-native-actionsheet/tar.gz/9cb323f2fd5ca9687395354b88c5ae3c1a92c43e" - react-native-aes-crypto@standardnotes/react-native-aes#d156762: version "1.3.9" resolved "https://codeload.github.com/standardnotes/react-native-aes/tar.gz/d156762424897aaf1e020462edecbe52c492c056"