feature: refactor ActionSheet & use new SIdeMenu

This commit is contained in:
Radek Czemerys
2020-06-21 20:58:06 +02:00
parent af10c38b45
commit c1a9f6b63d
9 changed files with 339 additions and 137 deletions

View File

@@ -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",

View File

@@ -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<AppStackNavigatorParamList>();
const AppStackComponent = () => {
const theme = useContext(ThemeContext);
const drawerRef = React.createRef<DrawerLayout>();
const renderDrawer = () => {
return (
<View>
<Text>I am in the drawer!</Text>
</View>
);
};
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 = () => <MainSideMenu />;
return (
<DrawerLayout
ref={drawerRef}
drawerWidth={200}
drawerWidth={getDefaultDrawerWidth(dimensions)}
drawerPosition={'left'}
drawerType="slide"
drawerBackgroundColor="#ddd"
@@ -301,7 +306,9 @@ export const App: React.FC = () => {
<NavigationContainer>
<ThemeProvider theme={CurrentApplication!.getThemeService().theme!}>
<ContextProvider>
<AppStackComponent />
<ActionSheetProvider>
<AppStackComponent />
</ActionSheetProvider>
</ContextProvider>
</ThemeProvider>
</NavigationContainer>

View File

@@ -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};
`;

View File

@@ -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 (
<Fragment>
<FirstSafeAreaView />
<MainSafeAreaView edges={['bottom', 'left']}>
<SideMenuHero
outOfSync={outOfSync}
testID="settingsButton"
onPress={() => {}} // TODO: nav open settings
onOutOfSyncPress={outOfSyncPressed}
/>
{/* <SideMenuSectionContainer>
<SideMenuSection
title="Themes"
key="themes-section"
options={themeOptions}
collapsed={true}
/>
<SideMenuSection title="Views" key="views-section">
<TagSelectionList
key="views-section-list"
contentType={ContentType.SmartTag}
onTagSelect={this.onTagSelect}
selectedTags={selectedTags}
/>
</SideMenuSection>
<SideMenuSection title="Tags" key="tags-section">
<TagSelectionList
key="tags-section-list"
hasBottomPadding={Platform.OS === 'android'}
emptyPlaceholder={'No tags. Create one from the note composer.'}
contentType={ContentType.Tag}
onTagSelect={this.onTagSelect}
selectedTags={selectedTags}
/>
</SideMenuSection>
</SideMenuSectionContainer> */}
<FAB
buttonColor={theme.stylekitInfoColor}
iconTextColor={theme.stylekitInfoContrastColor}
onClickAction={() => {}} // TODO: nav open settings
visible={true}
size={29}
paddingTop={Platform.OS ? 2 : 0}
iconTextComponent={
<Icon name={StyleKit.nameForIcon(ICON_SETTINGS)} />
}
/>
{/* {this.state.actionSheet && this.state.actionSheet} */}
</MainSafeAreaView>
</Fragment>
);
};

View File

@@ -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;
`;

View File

@@ -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> = 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 (
<Cell>
<Touchable testID={props.testID} onPress={props.onPress}>
<Title>{textData.title}</Title>
</Touchable>
<Touchable onPress={props.onPress}>
<SubTitle>{textData.text}</SubTitle>
</Touchable>
{props.outOfSync && (
<OutOfSyncContainer onPress={props.onOutOfSyncPress}>
<IconCircle>
<Circle
size={10}
backgroundColor={theme.stylekitWarningColor}
borderColor={theme.stylekitWarningColor}
/>
</IconCircle>
<OutOfSyncLabel>Potentially Out of Sync</OutOfSyncLabel>
</OutOfSyncContainer>
)}
</Cell>
);
};

View File

@@ -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<ActionSheet>();
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 (
<ActionSheet
ref={this.actionSheet}
title={this.title}
options={this.options.map(option => {
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,
};
}
}

View File

@@ -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 };
};

View File

@@ -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"