Files
mobile/src/App.tsx
2020-08-19 22:11:22 +02:00

524 lines
16 KiB
TypeScript

import { HeaderTitleView } from '@Components/HeaderTitleView';
import { IoniconsHeaderButton } from '@Components/IoniconsHeaderButton';
import { ActionSheetProvider } from '@expo/react-native-action-sheet';
import { MobileApplication } from '@Lib/application';
import { ApplicationGroup } from '@Lib/applicationGroup';
import {
AppStateEventType,
AppStateType,
TabletModeChangeData,
} from '@Lib/ApplicationState';
import { navigationRef } from '@Lib/NavigationService';
import { useHasEditor, useIsLocked } from '@Lib/snjsHooks';
import {
DefaultTheme,
NavigationContainer,
RouteProp,
} from '@react-navigation/native';
import {
createStackNavigator,
StackNavigationProp,
} from '@react-navigation/stack';
import { Authenticate } from '@Screens/Authenticate/Authenticate';
import { Compose } from '@Screens/Compose/Compose';
import { PasscodeInputModal } from '@Screens/InputModal/PasscodeInputModal';
import { TagInputModal } from '@Screens/InputModal/TagInputModal';
import { Root } from '@Screens/Root';
import { Settings } from '@Screens/Settings/Settings';
import { MainSideMenu } from '@Screens/SideMenu/MainSideMenu';
import { NoteSideMenu } from '@Screens/SideMenu/NoteSideMenu';
import { ICON_CHECKMARK, ICON_CLOSE, ICON_MENU } from '@Style/icons';
import { StyleKit, StyleKitContext } from '@Style/StyleKit';
import { StyleKitTheme } from '@Style/Themes/styled-components';
import { getDefaultDrawerWidth } from '@Style/Util/getDefaultDraerWidth';
import React, {
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import {
Dimensions,
Keyboard,
Platform,
ScaledSize,
StatusBar,
} from 'react-native';
import DrawerLayout, {
DrawerState,
} from 'react-native-gesture-handler/DrawerLayout';
import { HeaderButtons, Item } from 'react-navigation-header-buttons';
import { Challenge } from 'snjs';
import { ThemeContext, ThemeProvider } from 'styled-components/native';
import { ApplicationContext } from './ApplicationContext';
import {
SCREEN_AUTHENTICATE,
SCREEN_COMPOSE,
SCREEN_INPUT_MODAL_PASSCODE,
SCREEN_INPUT_MODAL_TAG,
SCREEN_NOTES,
SCREEN_SETTINGS,
} from './screens/screens';
type HeaderTitleParams = {
title?: string;
subTitle?: string;
subTitleColor?: string;
};
type AppStackNavigatorParamList = {
[SCREEN_NOTES]: HeaderTitleParams;
[SCREEN_COMPOSE]: HeaderTitleParams | undefined;
};
type ModalStackNavigatorParamList = {
AppStack: undefined;
[SCREEN_SETTINGS]: undefined;
[SCREEN_INPUT_MODAL_TAG]: HeaderTitleParams & {
tagUuid?: string;
noteUuid?: string;
};
[SCREEN_INPUT_MODAL_PASSCODE]: undefined;
[SCREEN_AUTHENTICATE]: {
challenge: Challenge;
};
};
export type AppStackNavigationProp<
T extends keyof AppStackNavigatorParamList
> = {
navigation: StackNavigationProp<AppStackNavigatorParamList, T>;
route: RouteProp<AppStackNavigatorParamList, T>;
};
export type ModalStackNavigationProp<
T extends keyof ModalStackNavigatorParamList
> = {
navigation: StackNavigationProp<ModalStackNavigatorParamList, T>;
route: RouteProp<ModalStackNavigatorParamList, T>;
};
const MainStack = createStackNavigator<ModalStackNavigatorParamList>();
const AppStack = createStackNavigator<AppStackNavigatorParamList>();
const AppStackComponent = (props: ModalStackNavigationProp<'AppStack'>) => {
const application = useContext(ApplicationContext);
const theme = useContext(ThemeContext);
const drawerRef = useRef<DrawerLayout>(null);
const noteDrawerRef = useRef<DrawerLayout>(null);
const [dimensions, setDimensions] = useState(() => Dimensions.get('window'));
const [isInTabletMode, setIsInTabletMode] = useState(
() => application?.getAppState().isInTabletMode
);
const isLocked = useIsLocked();
useEffect(() => {
const removeObserver = application
?.getAppState()
.addStateChangeObserver(event => {
if (event === AppStateType.EditorClosed) {
noteDrawerRef.current?.closeDrawer();
if (!isInTabletMode) {
props.navigation.popToTop();
}
}
});
return removeObserver;
}, [application, props.navigation, isInTabletMode]);
const hasEditor = useHasEditor();
useEffect(() => {
const updateDimensions = ({ window }: { window: ScaledSize }) => {
setDimensions(window);
};
Dimensions.addEventListener('change', updateDimensions);
return () => Dimensions.removeEventListener('change', updateDimensions);
}, []);
useEffect(() => {
const remoteTabletModeSubscription = application
?.getAppState()
.addStateEventObserver((event, data) => {
if (event === AppStateEventType.TabletModeChange) {
const eventData = data as TabletModeChangeData;
if (eventData.new_isInTabletMode && !eventData.old_isInTabletMode) {
setIsInTabletMode(true);
} else if (
!eventData.new_isInTabletMode &&
eventData.old_isInTabletMode
) {
setIsInTabletMode(false);
}
}
});
return remoteTabletModeSubscription;
}, [application]);
const handleDrawerStateChange = useCallback(
(newState: DrawerState, drawerWillShow: boolean) => {
if (newState !== 'Idle' && drawerWillShow) {
application?.getAppState().onDrawerOpen();
}
},
[application]
);
return (
<DrawerLayout
ref={drawerRef}
drawerWidth={getDefaultDrawerWidth(dimensions)}
drawerPosition={'left'}
drawerType="slide"
onDrawerStateChanged={handleDrawerStateChange}
renderNavigationView={() =>
!isLocked && <MainSideMenu drawerRef={drawerRef.current} />
}
>
<DrawerLayout
ref={noteDrawerRef}
drawerWidth={getDefaultDrawerWidth(dimensions)}
onDrawerStateChanged={handleDrawerStateChange}
drawerPosition={'right'}
drawerType="slide"
drawerLockMode="locked-closed"
renderNavigationView={() =>
hasEditor && <NoteSideMenu drawerRef={noteDrawerRef.current} />
}
>
<AppStack.Navigator
screenOptions={() => ({
headerStyle: {
backgroundColor: theme.stylekitContrastBackgroundColor,
},
headerTintColor: theme.stylekitInfoColor,
headerTitle: ({ children }) => {
return <HeaderTitleView title={children || ''} />;
},
})}
initialRouteName={SCREEN_NOTES}
>
<AppStack.Screen
name={SCREEN_NOTES}
options={({ route }) => ({
title: 'All notes',
headerTitle: ({ children }) => {
return (
<HeaderTitleView
title={route.params?.title ?? (children || '')}
subtitle={route.params?.subTitle}
subtitleColor={route.params?.subTitleColor}
/>
);
},
headerLeft: () => (
<HeaderButtons HeaderButtonComponent={IoniconsHeaderButton}>
<Item
testID="drawerButton"
disabled={false}
title={''}
iconName={StyleKit.nameForIcon(ICON_MENU)}
onPress={() => {
Keyboard.dismiss();
drawerRef.current?.openDrawer();
}}
/>
</HeaderButtons>
),
headerRight: () =>
isInTabletMode &&
hasEditor && (
<HeaderButtons HeaderButtonComponent={IoniconsHeaderButton}>
<Item
testID="noteDrawerButton"
disabled={false}
title={''}
iconName={StyleKit.nameForIcon(ICON_MENU)}
onPress={() => {
Keyboard.dismiss();
noteDrawerRef.current?.openDrawer();
}}
/>
</HeaderButtons>
),
})}
component={Root}
/>
<AppStack.Screen
name={SCREEN_COMPOSE}
options={({ route }) => ({
headerTitle: ({ children }) => {
return (
<HeaderTitleView
title={route.params?.title ?? (children || '')}
subtitle={route.params?.subTitle}
subtitleColor={route.params?.subTitleColor}
/>
);
},
headerRight: () =>
!isInTabletMode && (
<HeaderButtons HeaderButtonComponent={IoniconsHeaderButton}>
<Item
testID="noteDrawerButton"
disabled={false}
title={''}
iconName={StyleKit.nameForIcon(ICON_MENU)}
onPress={() => {
Keyboard.dismiss();
noteDrawerRef.current?.openDrawer();
}}
/>
</HeaderButtons>
),
})}
component={Compose}
/>
</AppStack.Navigator>
</DrawerLayout>
</DrawerLayout>
);
};
const MainStackComponent = ({ env }: { env: 'prod' | 'dev' }) => {
const application = useContext(ApplicationContext);
const theme = useContext(ThemeContext);
return (
<MainStack.Navigator
screenOptions={{
gestureEnabled: false,
headerStyle: {
backgroundColor: theme.stylekitContrastBackgroundColor,
},
}}
mode="modal"
initialRouteName="AppStack"
>
<MainStack.Screen
name={'AppStack'}
options={{
headerShown: false,
}}
component={AppStackComponent}
/>
<MainStack.Screen
name={SCREEN_SETTINGS}
options={() => ({
title: 'Settings',
headerTitle: ({ children }) => {
return <HeaderTitleView title={children || ''} />;
},
headerLeft: ({ disabled, onPress }) => (
<HeaderButtons HeaderButtonComponent={IoniconsHeaderButton}>
<Item
testID="headerButton"
disabled={disabled}
title={Platform.OS === 'ios' ? 'Done' : ''}
iconName={
Platform.OS === 'ios'
? undefined
: StyleKit.nameForIcon(ICON_CHECKMARK)
}
onPress={onPress}
/>
</HeaderButtons>
),
headerRight: () =>
env === 'dev' && (
<HeaderButtons HeaderButtonComponent={IoniconsHeaderButton}>
<Item
testID="headerButton"
title={'Destroy Data'}
onPress={async () => {
await application?.deviceInterface?.removeAllRawStorageValues();
await application?.deviceInterface?.removeAllRawDatabasePayloads();
application?.deinit();
}}
/>
</HeaderButtons>
),
})}
component={Settings}
/>
<MainStack.Screen
name={SCREEN_INPUT_MODAL_PASSCODE}
options={{
title: 'Setup Passcode',
headerTitle: ({ children }) => {
return <HeaderTitleView title={children || ''} />;
},
headerLeft: ({ disabled, onPress }) => (
<HeaderButtons HeaderButtonComponent={IoniconsHeaderButton}>
<Item
testID="headerButton"
disabled={disabled}
title={Platform.OS === 'ios' ? 'Cancel' : ''}
iconName={
Platform.OS === 'ios'
? undefined
: StyleKit.nameForIcon(ICON_CLOSE)
}
onPress={onPress}
/>
</HeaderButtons>
),
}}
component={PasscodeInputModal}
/>
<MainStack.Screen
name={SCREEN_INPUT_MODAL_TAG}
options={({ route }) => ({
title: 'Tag',
gestureEnabled: false,
headerTitle: ({ children }) => {
return (
<HeaderTitleView
title={route.params?.title ?? (children || '')}
/>
);
},
headerLeft: ({ disabled, onPress }) => (
<HeaderButtons HeaderButtonComponent={IoniconsHeaderButton}>
<Item
testID="headerButton"
disabled={disabled}
title={Platform.OS === 'ios' ? 'Cancel' : ''}
iconName={
Platform.OS === 'ios'
? undefined
: StyleKit.nameForIcon(ICON_CLOSE)
}
onPress={onPress}
/>
</HeaderButtons>
),
})}
component={TagInputModal}
/>
<MainStack.Screen
name={SCREEN_AUTHENTICATE}
options={() => ({
title: 'Authenticate',
headerLeft: () => undefined,
headerTitle: ({ children }) => {
return <HeaderTitleView title={children || ''} />;
},
})}
component={Authenticate}
/>
</MainStack.Navigator>
);
};
const AppComponent: React.FC<{
application: MobileApplication;
env: 'prod' | 'dev';
}> = ({ application, env }) => {
const [ready, setReady] = useState(false);
const styleKit = useRef<StyleKit>();
const [activeTheme, setActiveTheme] = useState<StyleKitTheme | undefined>();
const setStyleKitRef = useCallback((node: StyleKit | undefined) => {
if (node) {
node.addThemeChangeObserver(() => {
setActiveTheme(node.theme);
});
}
// Save a reference to the node
styleKit.current = node;
}, []);
useEffect(() => {
let styleKitInstance: StyleKit;
const loadApplication = async () => {
await application?.prepareForLaunch({
receiveChallenge: async challenge => {
application!.promptForChallenge(challenge);
},
});
styleKitInstance = new StyleKit(application);
await styleKitInstance.init();
setStyleKitRef(styleKitInstance);
setActiveTheme(styleKitInstance.theme);
setReady(true);
};
setReady(false);
loadApplication();
return () => {
styleKitInstance?.deinit();
setStyleKitRef(undefined);
};
}, [application, env, setStyleKitRef]);
if (!ready || !styleKit.current || !activeTheme) {
return null;
}
return (
<NavigationContainer
theme={{
...DefaultTheme,
colors: {
...DefaultTheme.colors,
background: activeTheme.stylekitBackgroundColor,
border: activeTheme.stylekitBorderColor,
},
}}
onReady={() => {
application?.launch(false);
}}
ref={navigationRef}
>
<StatusBar translucent />
{styleKit.current && (
<>
<ThemeProvider theme={activeTheme}>
<ActionSheetProvider>
<StyleKitContext.Provider value={styleKit.current}>
<MainStackComponent env={env} />
</StyleKitContext.Provider>
</ActionSheetProvider>
</ThemeProvider>
</>
)}
</NavigationContainer>
);
};
const AppGroupInstance = new ApplicationGroup();
export const App = (props: { env: 'prod' | 'dev' }) => {
const applicationGroupRef = useRef(AppGroupInstance);
const [application, setApplication] = useState<
MobileApplication | undefined
>();
useEffect(() => {
const removeAppChangeObserver = applicationGroupRef.current.addApplicationChangeObserver(
() => {
setApplication(applicationGroupRef.current.application);
}
);
return removeAppChangeObserver;
}, [applicationGroupRef.current.application]);
return (
<ApplicationContext.Provider value={application}>
{application && (
<AppComponent
env={props.env}
key={application.Uuid}
application={application}
/>
)}
</ApplicationContext.Provider>
);
};