update react native collapsible

This commit is contained in:
Arnaud Vergnet 2021-05-08 22:27:05 +02:00
parent 0b4f115a14
commit 286c1e6411
28 changed files with 1147 additions and 1155 deletions

44
App.tsx
View file

@ -37,6 +37,7 @@ import { setupStatusBar } from './src/utils/Utils';
import initLocales from './src/utils/Locales'; import initLocales from './src/utils/Locales';
import { NavigationContainerRef } from '@react-navigation/core'; import { NavigationContainerRef } from '@react-navigation/core';
import GENERAL_STYLES from './src/constants/Styles'; import GENERAL_STYLES from './src/constants/Styles';
import CollapsibleProvider from './src/components/providers/CollapsibleProvider';
// Native optimizations https://reactnavigation.org/docs/react-native-screens // Native optimizations https://reactnavigation.org/docs/react-native-screens
// Crashes app when navigating away from webview on android 9+ // Crashes app when navigating away from webview on android 9+
@ -210,26 +211,29 @@ export default class App extends React.Component<{}, StateType> {
} }
return ( return (
<PaperProvider theme={state.currentTheme}> <PaperProvider theme={state.currentTheme}>
<OverflowMenuProvider> <CollapsibleProvider>
<View <OverflowMenuProvider>
style={{ <View
backgroundColor: ThemeManager.getCurrentTheme().colors.background, style={{
...GENERAL_STYLES.flex, backgroundColor: ThemeManager.getCurrentTheme().colors
}} .background,
> ...GENERAL_STYLES.flex,
<SafeAreaView style={GENERAL_STYLES.flex}> }}
<NavigationContainer >
theme={state.currentTheme} <SafeAreaView style={GENERAL_STYLES.flex}>
ref={this.navigatorRef} <NavigationContainer
> theme={state.currentTheme}
<MainNavigator ref={this.navigatorRef}
defaultHomeRoute={this.defaultHomeRoute} >
defaultHomeData={this.defaultHomeData} <MainNavigator
/> defaultHomeRoute={this.defaultHomeRoute}
</NavigationContainer> defaultHomeData={this.defaultHomeData}
</SafeAreaView> />
</View> </NavigationContainer>
</OverflowMenuProvider> </SafeAreaView>
</View>
</OverflowMenuProvider>
</CollapsibleProvider>
</PaperProvider> </PaperProvider>
); );
} }

46
package-lock.json generated
View file

@ -3931,7 +3931,6 @@
"version": "3.1.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz",
"integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==", "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==",
"dev": true,
"requires": { "requires": {
"node-fetch": "2.6.1" "node-fetch": "2.6.1"
} }
@ -10596,14 +10595,39 @@
"integrity": "sha512-beZjdgbT9Y/Pg591Xy5XkKG20HffJiVad4n9bfcUF/f783A+tvOVXnqvbS58Lkaym93mi4jcDPMuW9Vc1t6rqg==" "integrity": "sha512-beZjdgbT9Y/Pg591Xy5XkKG20HffJiVad4n9bfcUF/f783A+tvOVXnqvbS58Lkaym93mi4jcDPMuW9Vc1t6rqg=="
}, },
"react-native-gesture-handler": { "react-native-gesture-handler": {
"version": "1.8.0", "version": "1.10.3",
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.8.0.tgz", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.10.3.tgz",
"integrity": "sha512-E2FZa0qZ5Bi0Z8Jg4n9DaFomHvedSjwbO2DPmUUHYRy1lH2yxXUpSrqJd6yymu+Efzmjg2+JZzsjFYA2Iq8VEQ==", "integrity": "sha512-cBGMi1IEsIVMgoox4RvMx7V2r6bNKw0uR1Mu1o7NbuHS6BRSVLq0dP34l2ecnPlC+jpWd3le6Yg1nrdCjby2Mw==",
"requires": { "requires": {
"@egjs/hammerjs": "^2.0.17", "@egjs/hammerjs": "^2.0.17",
"fbjs": "^3.0.0",
"hoist-non-react-statics": "^3.3.0", "hoist-non-react-statics": "^3.3.0",
"invariant": "^2.2.4", "invariant": "^2.2.4",
"prop-types": "^15.7.2" "prop-types": "^15.7.2"
},
"dependencies": {
"fbjs": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.0.tgz",
"integrity": "sha512-dJd4PiDOFuhe7vk4F80Mba83Vr2QuK86FoxtgPmzBqEJahncp+13YCmfoa53KHCo6OnlXLG7eeMWPfB5CrpVKg==",
"requires": {
"cross-fetch": "^3.0.4",
"fbjs-css-vars": "^1.0.0",
"loose-envify": "^1.0.0",
"object-assign": "^4.1.0",
"promise": "^7.1.1",
"setimmediate": "^1.0.5",
"ua-parser-js": "^0.7.18"
}
},
"promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"requires": {
"asap": "~2.0.3"
}
}
} }
}, },
"react-native-image-pan-zoom": { "react-native-image-pan-zoom": {
@ -10871,11 +10895,12 @@
} }
}, },
"react-navigation-collapsible": { "react-navigation-collapsible": {
"version": "5.6.4", "version": "5.9.1",
"resolved": "https://registry.npmjs.org/react-navigation-collapsible/-/react-navigation-collapsible-5.6.4.tgz", "resolved": "https://registry.npmjs.org/react-navigation-collapsible/-/react-navigation-collapsible-5.9.1.tgz",
"integrity": "sha512-dXMbDw2TQ6s5XLk9h+2hUShXoS8KPChfdh/xmmLqfKmntS5YteE01+x78gU5KogB3etDraH1kvhW7xDnbG9AfA==", "integrity": "sha512-yUwHe8Z7++A8ThrjPI+Mcm7LqBhIqJc+1F4XszpI7EoHz3bJElzczbfyfuEvjSbYU9AgW3MdBWzaRIDluxcEuA==",
"requires": { "requires": {
"react-native-iphone-x-helper": "^1.2.1" "react-native-iphone-x-helper": "^1.3.0",
"shallowequal": "^1.1.0"
} }
}, },
"react-navigation-header-buttons": { "react-navigation-header-buttons": {
@ -11453,6 +11478,11 @@
"kind-of": "^6.0.2" "kind-of": "^6.0.2"
} }
}, },
"shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
},
"shebang-command": { "shebang-command": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",

View file

@ -53,7 +53,7 @@
"react-native-splash-screen": "3.2.0", "react-native-splash-screen": "3.2.0",
"react-native-vector-icons": "8.1.0", "react-native-vector-icons": "8.1.0",
"react-native-webview": "11.4.3", "react-native-webview": "11.4.3",
"react-navigation-collapsible": "5.6.4", "react-navigation-collapsible": "5.9.1",
"react-navigation-header-buttons": "7.0.1" "react-navigation-header-buttons": "7.0.1"
}, },
"devDependencies": { "devDependencies": {

View file

@ -28,7 +28,7 @@ import { FAB, IconButton, Surface, withTheme } from 'react-native-paper';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import AutoHideHandler from '../../utils/AutoHideHandler'; import AutoHideHandler from '../../utils/AutoHideHandler';
import CustomTabBar from '../Tabbar/CustomTabBar'; import CustomTabBar, { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
const AnimatedFAB = Animatable.createAnimatableComponent(FAB); const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
@ -159,7 +159,7 @@ class AnimatedBottomBar extends React.Component<PropsType, StateType> {
useNativeDriver useNativeDriver
style={{ style={{
...styles.container, ...styles.container,
bottom: 10 + CustomTabBar.TAB_BAR_HEIGHT, bottom: 10 + TAB_BAR_HEIGHT,
}} }}
> >
<Surface style={styles.surface}> <Surface style={styles.surface}>

View file

@ -27,7 +27,7 @@ import {
import { FAB } from 'react-native-paper'; import { FAB } from 'react-native-paper';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import AutoHideHandler from '../../utils/AutoHideHandler'; import AutoHideHandler from '../../utils/AutoHideHandler';
import CustomTabBar from '../Tabbar/CustomTabBar'; import CustomTabBar, { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
type PropsType = { type PropsType = {
icon: string; icon: string;
@ -82,7 +82,7 @@ export default class AnimatedFAB extends React.Component<PropsType> {
useNativeDriver={true} useNativeDriver={true}
style={{ style={{
...styles.fab, ...styles.fab,
bottom: CustomTabBar.TAB_BAR_HEIGHT, bottom: TAB_BAR_HEIGHT,
}} }}
> >
<FAB icon={props.icon} onPress={props.onPress} /> <FAB icon={props.icon} onPress={props.onPress} />

View file

@ -17,22 +17,27 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as React from 'react'; import React, { useCallback } from 'react';
import { useCollapsibleStack } from 'react-navigation-collapsible'; import { useCollapsibleHeader } from 'react-navigation-collapsible';
import CustomTabBar from '../Tabbar/CustomTabBar'; import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
import { import {
NativeScrollEvent, NativeScrollEvent,
NativeSyntheticEvent, NativeSyntheticEvent,
StyleSheet, StyleSheet,
} from 'react-native'; } from 'react-native';
import { useTheme } from 'react-native-paper';
import { useCollapsible } from '../../utils/CollapsibleContext';
import { useFocusEffect } from '@react-navigation/core';
export type CollapsibleComponentPropsType = { export type CollapsibleComponentPropsType = {
children?: React.ReactNode; children?: React.ReactNode;
hasTab?: boolean; hasTab?: boolean;
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void; onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
paddedProps?: (paddingTop: number) => Record<string, any>;
headerColors: string;
}; };
type PropsType = CollapsibleComponentPropsType & { type Props = CollapsibleComponentPropsType & {
component: React.ComponentType<any>; component: React.ComponentType<any>;
}; };
@ -42,22 +47,46 @@ const styles = StyleSheet.create({
}, },
}); });
function CollapsibleComponent(props: PropsType) { function CollapsibleComponent(props: Props) {
const { paddedProps, headerColors } = props;
const Comp = props.component;
const theme = useTheme();
const { setCollapsible } = useCollapsible();
const collapsible = useCollapsibleHeader({
config: {
collapsedColor: headerColors ? headerColors : theme.colors.surface,
useNativeDriver: true,
},
});
useFocusEffect(
useCallback(() => {
setCollapsible(collapsible);
}, [collapsible, setCollapsible])
);
const {
containerPaddingTop,
scrollIndicatorInsetTop,
onScrollWithListener,
} = collapsible;
const paddingBottom = props.hasTab ? TAB_BAR_HEIGHT : 0;
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => { const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
if (props.onScroll) { if (props.onScroll) {
props.onScroll(event); props.onScroll(event);
} }
}; };
const Comp = props.component;
const { const pprops =
containerPaddingTop, paddedProps !== undefined ? paddedProps(containerPaddingTop) : undefined;
scrollIndicatorInsetTop,
onScrollWithListener,
} = useCollapsibleStack();
const paddingBottom = props.hasTab ? CustomTabBar.TAB_BAR_HEIGHT : 0;
return ( return (
<Comp <Comp
{...props} {...props}
{...pprops}
onScroll={onScrollWithListener(onScroll)} onScroll={onScrollWithListener(onScroll)}
contentContainerStyle={{ contentContainerStyle={{
paddingTop: containerPaddingTop, paddingTop: containerPaddingTop,

View file

@ -21,7 +21,7 @@ import * as React from 'react';
import { useTheme } from 'react-native-paper'; import { useTheme } from 'react-native-paper';
import { Modalize } from 'react-native-modalize'; import { Modalize } from 'react-native-modalize';
import { View } from 'react-native-animatable'; import { View } from 'react-native-animatable';
import CustomTabBar from '../Tabbar/CustomTabBar'; import CustomTabBar, { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
/** /**
* Abstraction layer for Modalize component, using custom configuration * Abstraction layer for Modalize component, using custom configuration
@ -45,7 +45,7 @@ function CustomModal(props: {
> >
<View <View
style={{ style={{
paddingBottom: CustomTabBar.TAB_BAR_HEIGHT, paddingBottom: TAB_BAR_HEIGHT,
}} }}
> >
{children} {children}

View file

@ -32,10 +32,10 @@ import { Collapsible } from 'react-navigation-collapsible';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import ErrorView from './ErrorView'; import ErrorView from './ErrorView';
import BasicLoadingScreen from './BasicLoadingScreen'; import BasicLoadingScreen from './BasicLoadingScreen';
import withCollapsible from '../../utils/withCollapsible'; import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
import CustomTabBar from '../Tabbar/CustomTabBar';
import { ERROR_TYPE, readData } from '../../utils/WebData'; import { ERROR_TYPE, readData } from '../../utils/WebData';
import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList'; import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList';
import GENERAL_STYLES from '../../constants/Styles';
export type SectionListDataType<ItemT> = Array<{ export type SectionListDataType<ItemT> = Array<{
title: string; title: string;
@ -260,19 +260,20 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
dataset = props.createDataset(state.fetchedData, state.refreshing); dataset = props.createDataset(state.fetchedData, state.refreshing);
} }
const { containerPaddingTop } = props.collapsibleStack;
return ( return (
<View> <View style={GENERAL_STYLES.flex}>
<CollapsibleSectionList <CollapsibleSectionList
sections={dataset} sections={dataset}
extraData={props.updateData} extraData={props.updateData}
refreshControl={ paddedProps={(paddingTop) => ({
<RefreshControl refreshControl: (
progressViewOffset={containerPaddingTop} <RefreshControl
refreshing={state.refreshing} progressViewOffset={paddingTop}
onRefresh={this.onRefresh} refreshing={state.refreshing}
/> onRefresh={this.onRefresh}
} />
),
})}
renderSectionHeader={this.getRenderSectionHeader} renderSectionHeader={this.getRenderSectionHeader}
renderItem={this.getRenderItem} renderItem={this.getRenderItem}
stickySectionHeadersEnabled={props.stickyHeader} stickySectionHeadersEnabled={props.stickyHeader}
@ -299,7 +300,7 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
: undefined : undefined
} }
onScroll={this.onScroll} onScroll={this.onScroll}
hasTab hasTab={true}
/> />
<Snackbar <Snackbar
visible={state.snackbarVisible} visible={state.snackbarVisible}
@ -310,7 +311,7 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
}} }}
duration={4000} duration={4000}
style={{ style={{
bottom: CustomTabBar.TAB_BAR_HEIGHT, bottom: TAB_BAR_HEIGHT,
}} }}
> >
{i18n.t('general.listUpdateFail')} {i18n.t('general.listUpdateFail')}
@ -320,4 +321,4 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
} }
} }
export default withCollapsible(WebSectionList); export default WebSectionList;

View file

@ -17,7 +17,13 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as React from 'react'; import React, {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import WebView from 'react-native-webview'; import WebView from 'react-native-webview';
import { import {
Divider, Divider,
@ -34,23 +40,21 @@ import {
StyleSheet, StyleSheet,
} from 'react-native'; } from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { withTheme } from 'react-native-paper'; import { useTheme } from 'react-native-paper';
import { StackNavigationProp } from '@react-navigation/stack'; import { useCollapsibleHeader } from 'react-navigation-collapsible';
import { Collapsible } from 'react-navigation-collapsible';
import withCollapsible from '../../utils/withCollapsible';
import MaterialHeaderButtons, { Item } from '../Overrides/CustomHeaderButton'; import MaterialHeaderButtons, { Item } from '../Overrides/CustomHeaderButton';
import { ERROR_TYPE } from '../../utils/WebData'; import { ERROR_TYPE } from '../../utils/WebData';
import ErrorView from './ErrorView'; import ErrorView from './ErrorView';
import BasicLoadingScreen from './BasicLoadingScreen'; import BasicLoadingScreen from './BasicLoadingScreen';
import { useFocusEffect, useNavigation } from '@react-navigation/core';
import { useCollapsible } from '../../utils/CollapsibleContext';
type PropsType = { type Props = {
navigation: StackNavigationProp<any>;
theme: ReactNativePaper.Theme;
url: string; url: string;
collapsibleStack: Collapsible; onMessage?: (event: { nativeEvent: { data: string } }) => void;
onMessage: (event: { nativeEvent: { data: string } }) => void; onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void; initialJS?: string;
customJS?: string; injectJS?: string;
customPaddingFunction?: null | ((padding: number) => string); customPaddingFunction?: null | ((padding: number) => string);
showAdvancedControls?: boolean; showAdvancedControls?: boolean;
}; };
@ -66,134 +70,113 @@ const styles = StyleSheet.create({
/** /**
* Class defining a webview screen. * Class defining a webview screen.
*/ */
class WebViewScreen extends React.PureComponent<PropsType> { function WebViewScreen(props: Props) {
static defaultProps = { const [currentUrl, setCurrentUrl] = useState(props.url);
customJS: '', const [canGoBack, setCanGoBack] = useState(false);
showAdvancedControls: true, const navigation = useNavigation();
customPaddingFunction: null, const theme = useTheme();
}; const webviewRef = useRef<WebView>();
currentUrl: string; const { setCollapsible } = useCollapsible();
const collapsible = useCollapsibleHeader({
config: { collapsedColor: theme.colors.surface, useNativeDriver: false },
});
const { containerPaddingTop, onScrollWithListener } = collapsible;
webviewRef: { current: null | WebView }; const [currentInjectedJS, setCurrentInjectedJS] = useState(props.injectJS);
canGoBack: boolean; useFocusEffect(
useCallback(() => {
constructor(props: PropsType) { setCollapsible(collapsible);
super(props);
this.webviewRef = React.createRef();
this.canGoBack = false;
this.currentUrl = props.url;
}
/**
* Creates header buttons and listens to events after mounting
*/
componentDidMount() {
const { props } = this;
props.navigation.setOptions({
headerRight: props.showAdvancedControls
? this.getAdvancedButtons
: this.getBasicButton,
});
props.navigation.addListener('focus', () => {
BackHandler.addEventListener( BackHandler.addEventListener(
'hardwareBackPress', 'hardwareBackPress',
this.onBackButtonPressAndroid onBackButtonPressAndroid
); );
}); return () => {
props.navigation.addListener('blur', () => { BackHandler.removeEventListener(
BackHandler.removeEventListener( 'hardwareBackPress',
'hardwareBackPress', onBackButtonPressAndroid
this.onBackButtonPressAndroid );
); };
}); // eslint-disable-next-line react-hooks/exhaustive-deps
} }, [collapsible, setCollapsible])
);
/** useLayoutEffect(() => {
* Goes back on the webview or on the navigation stack if we cannot go back anymore navigation.setOptions({
* headerRight: props.showAdvancedControls
* @returns {boolean} ? getAdvancedButtons
*/ : getBasicButton,
onBackButtonPressAndroid = (): boolean => { });
if (this.canGoBack) { // eslint-disable-next-line react-hooks/exhaustive-deps
this.onGoBackClicked(); }, [navigation, props.showAdvancedControls]);
useEffect(() => {
if (props.injectJS && props.injectJS !== currentInjectedJS) {
injectJavaScript(props.injectJS);
setCurrentInjectedJS(props.injectJS);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.injectJS]);
const onBackButtonPressAndroid = () => {
if (canGoBack) {
onGoBackClicked();
return true; return true;
} }
return false; return false;
}; };
/** const getBasicButton = () => {
* Gets header refresh and open in browser buttons
*
* @return {*}
*/
getBasicButton = () => {
return ( return (
<MaterialHeaderButtons> <MaterialHeaderButtons>
<Item <Item
title="refresh" title={'refresh'}
iconName="refresh" iconName={'refresh'}
onPress={this.onRefreshClicked} onPress={onRefreshClicked}
/> />
<Item <Item
title={i18n.t('general.openInBrowser')} title={i18n.t('general.openInBrowser')}
iconName="open-in-new" iconName={'open-in-new'}
onPress={this.onOpenClicked} onPress={onOpenClicked}
/> />
</MaterialHeaderButtons> </MaterialHeaderButtons>
); );
}; };
/** const getAdvancedButtons = () => {
* Creates advanced header control buttons.
* These buttons allows the user to refresh, go back, go forward and open in the browser.
*
* @returns {*}
*/
getAdvancedButtons = () => {
const { props } = this;
return ( return (
<MaterialHeaderButtons> <MaterialHeaderButtons>
<Item <Item title="refresh" iconName="refresh" onPress={onRefreshClicked} />
title="refresh"
iconName="refresh"
onPress={this.onRefreshClicked}
/>
<OverflowMenu <OverflowMenu
style={styles.overflow} style={styles.overflow}
OverflowIcon={ OverflowIcon={
<MaterialCommunityIcons <MaterialCommunityIcons
name="dots-vertical" name="dots-vertical"
size={26} size={26}
color={props.theme.colors.text} color={theme.colors.text}
/> />
} }
> >
<HiddenItem <HiddenItem
title={i18n.t('general.goBack')} title={i18n.t('general.goBack')}
onPress={this.onGoBackClicked} onPress={onGoBackClicked}
/> />
<HiddenItem <HiddenItem
title={i18n.t('general.goForward')} title={i18n.t('general.goForward')}
onPress={this.onGoForwardClicked} onPress={onGoForwardClicked}
/> />
<Divider /> <Divider />
<HiddenItem <HiddenItem
title={i18n.t('general.openInBrowser')} title={i18n.t('general.openInBrowser')}
onPress={this.onOpenClicked} onPress={onOpenClicked}
/> />
</OverflowMenu> </OverflowMenu>
</MaterialHeaderButtons> </MaterialHeaderButtons>
); );
}; };
/** const getRenderLoading = () => <BasicLoadingScreen isAbsolute={true} />;
* Gets the loading indicator
*
* @return {*}
*/
getRenderLoading = () => <BasicLoadingScreen isAbsolute />;
/** /**
* Gets the javascript needed to generate a padding on top of the page * Gets the javascript needed to generate a padding on top of the page
@ -202,91 +185,78 @@ class WebViewScreen extends React.PureComponent<PropsType> {
* @param padding The padding to add in pixels * @param padding The padding to add in pixels
* @returns {string} * @returns {string}
*/ */
getJavascriptPadding(padding: number): string { const getJavascriptPadding = (padding: number) => {
const { props } = this;
const customPadding = const customPadding =
props.customPaddingFunction != null props.customPaddingFunction != null
? props.customPaddingFunction(padding) ? props.customPaddingFunction(padding)
: ''; : '';
return `document.getElementsByTagName('body')[0].style.paddingTop = '${padding}px';${customPadding}true;`; return `document.getElementsByTagName('body')[0].style.paddingTop = '${padding}px';${customPadding}true;`;
} };
/** const onRefreshClicked = () => {
* Callback to use when refresh button is clicked. Reloads the webview. //@ts-ignore
*/ if (webviewRef.current) {
onRefreshClicked = () => { //@ts-ignore
if (this.webviewRef.current != null) { webviewRef.current.reload();
this.webviewRef.current.reload();
} }
}; };
onGoBackClicked = () => { const onGoBackClicked = () => {
if (this.webviewRef.current != null) { //@ts-ignore
this.webviewRef.current.goBack(); if (webviewRef.current) {
//@ts-ignore
webviewRef.current.goBack();
} }
}; };
onGoForwardClicked = () => { const onGoForwardClicked = () => {
if (this.webviewRef.current != null) { //@ts-ignore
this.webviewRef.current.goForward(); if (webviewRef.current) {
//@ts-ignore
webviewRef.current.goForward();
} }
}; };
onOpenClicked = () => { const onOpenClicked = () => Linking.openURL(currentUrl);
Linking.openURL(this.currentUrl);
};
onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => { const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { onScroll } = this.props; if (props.onScroll) {
if (onScroll) { props.onScroll(event);
onScroll(event);
} }
}; };
/** const injectJavaScript = (script: string) => {
* Injects the given javascript string into the web page //@ts-ignore
* if (webviewRef.current) {
* @param script The script to inject //@ts-ignore
*/ webviewRef.current.injectJavaScript(script);
injectJavaScript = (script: string) => {
if (this.webviewRef.current != null) {
this.webviewRef.current.injectJavaScript(script);
} }
}; };
render() { return (
const { props } = this; <AnimatedWebView
const { ref={webviewRef}
containerPaddingTop, source={{ uri: props.url }}
onScrollWithListener, startInLoadingState={true}
} = props.collapsibleStack; injectedJavaScript={props.initialJS}
return ( javaScriptEnabled={true}
<AnimatedWebView renderLoading={getRenderLoading}
ref={this.webviewRef} renderError={() => (
source={{ uri: props.url }} <ErrorView
startInLoadingState errorCode={ERROR_TYPE.CONNECTION_ERROR}
injectedJavaScript={props.customJS} onRefresh={onRefreshClicked}
javaScriptEnabled />
renderLoading={this.getRenderLoading} )}
renderError={() => ( onNavigationStateChange={(navState) => {
<ErrorView setCurrentUrl(navState.url);
errorCode={ERROR_TYPE.CONNECTION_ERROR} setCanGoBack(navState.canGoBack);
onRefresh={this.onRefreshClicked} }}
/> onMessage={props.onMessage}
)} onLoad={() => injectJavaScript(getJavascriptPadding(containerPaddingTop))}
onNavigationStateChange={(navState) => { // Animations
this.currentUrl = navState.url; onScroll={onScrollWithListener(onScroll)}
this.canGoBack = navState.canGoBack; />
}} );
onMessage={props.onMessage}
onLoad={() => {
this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop));
}}
// Animations
onScroll={(event) => onScrollWithListener(this.onScroll)(event)}
/>
);
}
} }
export default withCollapsible(withTheme(WebViewScreen)); export default WebViewScreen;

View file

@ -17,211 +17,88 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as React from 'react'; import React from 'react';
import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';
import { Animated, StyleSheet } from 'react-native'; import { Animated, StyleSheet } from 'react-native';
import { withTheme } from 'react-native-paper';
import { Collapsible } from 'react-navigation-collapsible';
import TabIcon from './TabIcon'; import TabIcon from './TabIcon';
import TabHomeIcon from './TabHomeIcon'; import { useTheme } from 'react-native-paper';
import { BottomTabBarProps } from '@react-navigation/bottom-tabs'; import { useCollapsible } from '../../utils/CollapsibleContext';
import { NavigationState } from '@react-navigation/native';
import {
PartialState,
Route,
} from '@react-navigation/routers/lib/typescript/src/types';
type RouteType = Route<string> & { export const TAB_BAR_HEIGHT = 50;
state?: NavigationState | PartialState<NavigationState>;
};
interface PropsType extends BottomTabBarProps { function CustomTabBar(
theme: ReactNativePaper.Theme; props: BottomTabBarProps<any> & {
icons: {
[key: string]: {
normal: string;
focused: string;
};
};
labels: {
[key: string]: string;
};
}
) {
const state = props.state;
const theme = useTheme();
const { collapsible } = useCollapsible();
let translateY: number | Animated.AnimatedInterpolation = 0;
if (collapsible) {
translateY = Animated.multiply(-1.5, collapsible.translateY);
}
return (
<Animated.View
style={{
...styles.bar,
backgroundColor: theme.colors.surface,
transform: [{ translateY: translateY }],
}}
>
{state.routes.map(
(
route: {
key: string;
name: string;
params?: object | undefined;
},
index: number
) => {
const iconData = props.icons[route.name];
return (
<TabIcon
isMiddle={index === 2}
onPress={() => props.navigation.navigate(route.name)}
icon={iconData.normal}
focusedIcon={iconData.focused}
label={props.labels[route.name]}
focused={state.index === index}
key={route.key}
/>
);
}
)}
</Animated.View>
);
} }
type StateType = {
translateY: any;
};
type validRoutes = 'proxiwash' | 'services' | 'planning' | 'planex';
const TAB_ICONS = {
proxiwash: 'tshirt-crew',
services: 'account-circle',
planning: 'calendar-range',
planex: 'clock',
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { bar: {
flexDirection: 'row', flexDirection: 'row',
width: '100%', width: '100%',
height: 50,
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
}, },
}); });
class CustomTabBar extends React.Component<PropsType, StateType> { function areEqual(
static TAB_BAR_HEIGHT = 48; prevProps: BottomTabBarProps<any>,
nextProps: BottomTabBarProps<any>
constructor(props: PropsType) { ) {
super(props); return prevProps.state.index === nextProps.state.index;
this.state = {
translateY: new Animated.Value(0),
};
// @ts-ignore
props.navigation.addListener('state', this.onRouteChange);
}
/**
* Navigates to the given route if it is different from the current one
*
* @param route Destination route
* @param currentIndex The current route index
* @param destIndex The destination route index
*/
onItemPress(route: RouteType, currentIndex: number, destIndex: number) {
const { navigation } = this.props;
if (currentIndex !== destIndex) {
navigation.navigate(route.name);
}
}
/**
* Navigates to tetris screen on home button long press
*
* @param route
*/
onItemLongPress(route: RouteType) {
const { navigation } = this.props;
if (route.name === 'home') {
navigation.navigate('game-start');
}
}
/**
* Finds the active route and syncs the tab bar animation with the header bar
*/
onRouteChange = () => {
const { props } = this;
props.state.routes.map(this.syncTabBar);
};
/**
* Gets an icon for the given route if it is not the home one as it uses a custom button
*
* @param route
* @param focused
* @returns {null}
*/
getTabBarIcon = (route: RouteType, focused: boolean) => {
let icon = TAB_ICONS[route.name as validRoutes];
icon = focused ? icon : `${icon}-outline`;
if (route.name !== 'home') {
return icon;
}
return '';
};
/**
* Gets a tab icon render.
* If the given route is focused, it syncs the tab bar and header bar animations together
*
* @param route The route for the icon
* @param index The index of the current route
* @returns {*}
*/
getRenderIcon = (route: RouteType, index: number) => {
const { props } = this;
const { state } = props;
const { options } = props.descriptors[route.key];
let label;
if (options.tabBarLabel != null) {
label = options.tabBarLabel;
} else if (options.title != null) {
label = options.title;
} else {
label = route.name;
}
const onPress = () => {
this.onItemPress(route, state.index, index);
};
const onLongPress = () => {
this.onItemLongPress(route);
};
const isFocused = state.index === index;
const color = isFocused
? props.theme.colors.primary
: props.theme.colors.tabIcon;
if (route.name !== 'home') {
return (
<TabIcon
onPress={onPress}
onLongPress={onLongPress}
icon={this.getTabBarIcon(route, isFocused)}
color={color}
label={label as string}
focused={isFocused}
extraData={state.index > index}
key={route.key}
/>
);
}
return (
<TabHomeIcon
onPress={onPress}
onLongPress={onLongPress}
focused={isFocused}
key={route.key}
tabBarHeight={CustomTabBar.TAB_BAR_HEIGHT}
/>
);
};
getIcons() {
const { props } = this;
return props.state.routes.map(this.getRenderIcon);
}
syncTabBar = (route: RouteType, index: number) => {
const { state } = this.props;
const isFocused = state.index === index;
if (isFocused) {
const stackState = route.state;
const stackRoute =
stackState && stackState.index != null
? stackState.routes[stackState.index]
: null;
const params: { collapsible: Collapsible } | null | undefined = stackRoute
? (stackRoute.params as { collapsible: Collapsible })
: null;
const collapsible = params != null ? params.collapsible : null;
if (collapsible != null) {
this.setState({
translateY: Animated.multiply(-1.5, collapsible.translateY), // Hide tab bar faster than header bar
});
}
}
};
render() {
const { props, state } = this;
const icons = this.getIcons();
return (
<Animated.View
style={{
height: CustomTabBar.TAB_BAR_HEIGHT,
backgroundColor: props.theme.colors.surface,
transform: [{ translateY: state.translateY }],
...styles.container,
}}
>
{icons}
</Animated.View>
);
}
} }
export default withTheme(CustomTabBar); export default React.memo(CustomTabBar, areEqual);

View file

@ -1,135 +1,114 @@
/* import React from 'react';
* Copyright (c) 2019 - 2020 Arnaud Vergnet. import { View, StyleSheet, Image } from 'react-native';
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { Image, StyleSheet, View } from 'react-native';
import { FAB } from 'react-native-paper'; import { FAB } from 'react-native-paper';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
const FOCUSED_ICON = require('../../../assets/tab-icon.png');
const UNFOCUSED_ICON = require('../../../assets/tab-icon-outline.png');
type PropsType = { interface Props {
icon: string;
focusedIcon: string;
focused: boolean; focused: boolean;
onPress: () => void; onPress: () => void;
onLongPress: () => void; }
tabBarHeight: number;
};
const AnimatedFAB = Animatable.createAnimatableComponent(FAB); Animatable.initializeRegistryWithDefinitions({
fabFocusIn: {
0: {
// @ts-ignore
scale: 1,
translateY: 0,
},
0.4: {
// @ts-ignore
scale: 1.2,
translateY: -9,
},
0.6: {
// @ts-ignore
scale: 1.05,
translateY: -6,
},
0.8: {
// @ts-ignore
scale: 1.15,
translateY: -6,
},
1: {
// @ts-ignore
scale: 1.1,
translateY: -6,
},
},
fabFocusOut: {
0: {
// @ts-ignore
scale: 1.1,
translateY: -6,
},
1: {
// @ts-ignore
scale: 1,
translateY: 0,
},
},
});
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { outer: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
}, },
subcontainer: { inner: {
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
width: '100%', width: '100%',
marginBottom: -15, height: 60,
}, },
fab: { fab: {
marginTop: 15,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
}, },
}); });
/** const FOCUSED_ICON = require('../../../assets/tab-icon.png');
* Abstraction layer for Agenda component, using custom configuration const UNFOCUSED_ICON = require('../../../assets/tab-icon-outline.png');
*/
class TabHomeIcon extends React.Component<PropsType> {
constructor(props: PropsType) {
super(props);
Animatable.initializeRegistryWithDefinitions({
fabFocusIn: {
'0': {
// @ts-ignore
scale: 1,
translateY: 0,
},
'0.9': {
scale: 1.2,
translateY: -9,
},
'1': {
scale: 1.1,
translateY: -7,
},
},
fabFocusOut: {
'0': {
// @ts-ignore
scale: 1.1,
translateY: -6,
},
'1': {
scale: 1,
translateY: 0,
},
},
});
}
shouldComponentUpdate(nextProps: PropsType): boolean { function TabHomeIcon(props: Props) {
const { focused } = this.props; const getImage = (iconProps: { size: number; color: string }) => {
return nextProps.focused !== focused;
}
getIconRender = ({ size, color }: { size: number; color: string }) => {
const { focused } = this.props;
return ( return (
<Image <Animatable.View useNativeDriver={true} animation={'rubberBand'}>
source={focused ? FOCUSED_ICON : UNFOCUSED_ICON} <Image
style={{ source={props.focused ? FOCUSED_ICON : UNFOCUSED_ICON}
width: size, style={{
height: size, width: iconProps.size,
tintColor: color, height: iconProps.size,
}} tintColor: iconProps.color,
/> }}
/>
</Animatable.View>
); );
}; };
render() { return (
const { props } = this; <View style={styles.outer}>
return ( <View style={styles.inner}>
<View style={styles.container}> <Animatable.View
<View style={styles.fab}
style={{ useNativeDriver={true}
height: props.tabBarHeight + 30, duration={props.focused ? 500 : 200}
...styles.subcontainer, animation={props.focused ? 'fabFocusIn' : 'fabFocusOut'}
}} easing={'ease-out'}
> >
<AnimatedFAB <FAB
duration={200}
easing="ease-out"
animation={props.focused ? 'fabFocusIn' : 'fabFocusOut'}
icon={this.getIconRender}
onPress={props.onPress} onPress={props.onPress}
onLongPress={props.onLongPress} animated={false}
style={styles.fab} icon={getImage}
color={'#fff'}
/> />
</View> </Animatable.View>
</View> </View>
); </View>
} );
} }
export default TabHomeIcon; export default TabHomeIcon;

View file

@ -1,143 +1,41 @@
/* import React from 'react';
* Copyright (c) 2019 - 2020 Arnaud Vergnet. import TabHomeIcon from './TabHomeIcon';
* import TabSideIcon from './TabSideIcon';
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react'; interface Props {
import { StyleSheet, View } from 'react-native'; isMiddle: boolean;
import { TouchableRipple, withTheme } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import * as Animatable from 'react-native-animatable';
import GENERAL_STYLES from '../../constants/Styles';
type PropsType = {
focused: boolean; focused: boolean;
color: string; label: string | undefined;
label: string;
icon: string; icon: string;
focusedIcon: string;
onPress: () => void; onPress: () => void;
onLongPress: () => void; }
theme: ReactNativePaper.Theme;
extraData: null | boolean | number | string;
};
const styles = StyleSheet.create({ function TabIcon(props: Props) {
container: { if (props.isMiddle) {
flex: 1,
justifyContent: 'center',
borderRadius: 10,
},
text: {
marginLeft: 'auto',
marginRight: 'auto',
fontSize: 10,
},
});
/**
* Abstraction layer for Agenda component, using custom configuration
*/
class TabIcon extends React.Component<PropsType> {
firstRender: boolean;
constructor(props: PropsType) {
super(props);
Animatable.initializeRegistryWithDefinitions({
focusIn: {
'0': {
// @ts-ignore
scale: 1,
translateY: 0,
},
'0.9': {
scale: 1.3,
translateY: 7,
},
'1': {
scale: 1.2,
translateY: 6,
},
},
focusOut: {
'0': {
// @ts-ignore
scale: 1.2,
translateY: 6,
},
'1': {
scale: 1,
translateY: 0,
},
},
});
this.firstRender = true;
}
componentDidMount() {
this.firstRender = false;
}
shouldComponentUpdate(nextProps: PropsType): boolean {
const { props } = this;
return ( return (
nextProps.focused !== props.focused || <TabHomeIcon
nextProps.theme.dark !== props.theme.dark || icon={props.icon}
nextProps.extraData !== props.extraData focusedIcon={props.focusedIcon}
); focused={props.focused}
}
render() {
const { props } = this;
return (
<TouchableRipple
onPress={props.onPress} onPress={props.onPress}
onLongPress={props.onLongPress} />
rippleColor={props.theme.colors.primary} );
borderless={true} } else {
style={styles.container} return (
> <TabSideIcon
<View> focused={props.focused}
<Animatable.View label={props.label}
duration={200} icon={props.icon}
easing="ease-out" focusedIcon={props.focusedIcon}
animation={props.focused ? 'focusIn' : 'focusOut'} onPress={props.onPress}
useNativeDriver />
>
<MaterialCommunityIcons
name={props.icon}
color={props.color}
size={26}
style={GENERAL_STYLES.centerHorizontal}
/>
</Animatable.View>
<Animatable.Text
animation={props.focused ? 'fadeOutDown' : 'fadeIn'}
useNativeDriver
style={{
color: props.color,
...styles.text,
}}
>
{props.label}
</Animatable.Text>
</View>
</TouchableRipple>
); );
} }
} }
export default withTheme(TabIcon); function areEqual(prevProps: Props, nextProps: Props) {
return prevProps.focused === nextProps.focused;
}
export default React.memo(TabIcon, areEqual);

View file

@ -0,0 +1,113 @@
import React from 'react';
import { TouchableRipple, useTheme } from 'react-native-paper';
import { StyleSheet, View } from 'react-native';
import * as Animatable from 'react-native-animatable';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import GENERAL_STYLES from '../../constants/Styles';
interface Props {
focused: boolean;
label: string | undefined;
icon: string;
focusedIcon: string;
onPress: () => void;
}
Animatable.initializeRegistryWithDefinitions({
focusIn: {
0: {
// @ts-ignore
scale: 1,
translateY: 0,
},
0.4: {
// @ts-ignore
scale: 1.3,
translateY: 6,
},
0.6: {
// @ts-ignore
scale: 1.1,
translateY: 6,
},
0.8: {
// @ts-ignore
scale: 1.25,
translateY: 6,
},
1: {
// @ts-ignore
scale: 1.2,
translateY: 6,
},
},
focusOut: {
0: {
// @ts-ignore
scale: 1.2,
translateY: 6,
},
1: {
// @ts-ignore
scale: 1,
translateY: 0,
},
},
});
function TabSideIcon(props: Props) {
const theme = useTheme();
const color = props.focused ? theme.colors.primary : theme.colors.disabled;
let icon = props.focused ? props.focusedIcon : props.icon;
return (
<TouchableRipple
onPress={props.onPress}
borderless
rippleColor={theme.colors.primary}
style={{
...styles.ripple,
borderTopEndRadius: theme.roundness,
borderTopStartRadius: theme.roundness,
}}
>
<View>
<Animatable.View
duration={props.focused ? 500 : 200}
easing="ease-out"
animation={props.focused ? 'focusIn' : 'focusOut'}
useNativeDriver
>
<MaterialCommunityIcons
name={icon}
color={color}
size={26}
style={GENERAL_STYLES.centerHorizontal}
/>
</Animatable.View>
<Animatable.Text
animation={props.focused ? 'fadeOutDown' : 'fadeIn'}
useNativeDriver
style={{
color: color,
...styles.text,
}}
>
{props.label}
</Animatable.Text>
</View>
</TouchableRipple>
);
}
const styles = StyleSheet.create({
ripple: {
flex: 1,
justifyContent: 'center',
},
text: {
...GENERAL_STYLES.centerHorizontal,
fontSize: 10,
},
});
export default TabSideIcon;

View file

@ -0,0 +1,33 @@
import React, { useState } from 'react';
import { Collapsible } from 'react-navigation-collapsible';
import {
CollapsibleContext,
CollapsibleContextType,
} from '../../utils/CollapsibleContext';
type Props = {
children: React.ReactChild;
};
export default function CollapsibleProvider(props: Props) {
const setCollapsible = (collapsible: Collapsible) => {
setCurrentCollapsible((prevState) => ({
...prevState,
collapsible,
}));
};
const [
currentCollapsible,
setCurrentCollapsible,
] = useState<CollapsibleContextType>({
collapsible: undefined,
setCollapsible: setCollapsible,
});
return (
<CollapsibleContext.Provider value={currentCollapsible}>
{props.children}
</CollapsibleContext.Provider>
);
}

View file

@ -18,12 +18,8 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import { import { createStackNavigator } from '@react-navigation/stack';
createStackNavigator,
TransitionPresets,
} from '@react-navigation/stack';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { Platform } from 'react-native';
import SettingsScreen from '../screens/Other/Settings/SettingsScreen'; import SettingsScreen from '../screens/Other/Settings/SettingsScreen';
import AboutScreen from '../screens/About/AboutScreen'; import AboutScreen from '../screens/About/AboutScreen';
import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen'; import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen';
@ -40,10 +36,6 @@ import ProfileScreen from '../screens/Amicale/ProfileScreen';
import ClubListScreen from '../screens/Amicale/Clubs/ClubListScreen'; import ClubListScreen from '../screens/Amicale/Clubs/ClubListScreen';
import ClubAboutScreen from '../screens/Amicale/Clubs/ClubAboutScreen'; import ClubAboutScreen from '../screens/Amicale/Clubs/ClubAboutScreen';
import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen'; import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen';
import {
CreateScreenCollapsibleStack,
getWebsiteStack,
} from '../utils/CollapsibleUtils';
import BugReportScreen from '../screens/Other/FeedbackScreen'; import BugReportScreen from '../screens/Other/FeedbackScreen';
import WebsiteScreen from '../screens/Services/WebsiteScreen'; import WebsiteScreen from '../screens/Services/WebsiteScreen';
import EquipmentScreen, { import EquipmentScreen, {
@ -54,6 +46,7 @@ import EquipmentConfirmScreen from '../screens/Amicale/Equipment/EquipmentConfir
import DashboardEditScreen from '../screens/Other/Settings/DashboardEditScreen'; import DashboardEditScreen from '../screens/Other/Settings/DashboardEditScreen';
import GameStartScreen from '../screens/Game/screens/GameStartScreen'; import GameStartScreen from '../screens/Game/screens/GameStartScreen';
import ImageGalleryScreen from '../screens/Media/ImageGalleryScreen'; import ImageGalleryScreen from '../screens/Media/ImageGalleryScreen';
import Test from '../screens/Test';
export enum MainRoutes { export enum MainRoutes {
Main = 'main', Main = 'main',
@ -83,7 +76,7 @@ export enum MainRoutes {
type DefaultParams = { [key in MainRoutes]: object | undefined }; type DefaultParams = { [key in MainRoutes]: object | undefined };
export interface FullParamsList extends DefaultParams { export type FullParamsList = DefaultParams & {
'login': { nextScreen: string }; 'login': { nextScreen: string };
'equipment-confirm': { 'equipment-confirm': {
item?: DeviceType; item?: DeviceType;
@ -91,34 +84,22 @@ export interface FullParamsList extends DefaultParams {
}; };
'equipment-rent': { item?: DeviceType }; 'equipment-rent': { item?: DeviceType };
'gallery': { images: Array<{ url: string }> }; 'gallery': { images: Array<{ url: string }> };
} };
// Don't know why but TS is complaining without this // Don't know why but TS is complaining without this
// See: https://stackoverflow.com/questions/63652687/interface-does-not-satisfy-the-constraint-recordstring-object-undefined // See: https://stackoverflow.com/questions/63652687/interface-does-not-satisfy-the-constraint-recordstring-object-undefined
export type MainStackParamsList = FullParamsList & export type MainStackParamsList = FullParamsList &
Record<string, object | undefined>; Record<string, object | undefined>;
const modalTransition =
Platform.OS === 'ios'
? TransitionPresets.ModalPresentationIOS
: TransitionPresets.ModalTransition;
const defaultScreenOptions = {
gestureEnabled: true,
cardOverlayEnabled: true,
...TransitionPresets.SlideFromRightIOS,
};
const MainStack = createStackNavigator<MainStackParamsList>(); const MainStack = createStackNavigator<MainStackParamsList>();
function MainStackComponent(props: { createTabNavigator: () => JSX.Element }) { function MainStackComponent(props: {
createTabNavigator: () => React.ReactElement;
}) {
const { createTabNavigator } = props; const { createTabNavigator } = props;
return ( return (
<MainStack.Navigator <MainStack.Navigator initialRouteName={MainRoutes.Main} headerMode="screen">
initialRouteName={MainRoutes.Main} <MainStack.Screen name={'test'} component={Test} />
headerMode="screen"
screenOptions={defaultScreenOptions}
>
<MainStack.Screen <MainStack.Screen
name={MainRoutes.Main} name={MainRoutes.Main}
component={createTabNavigator} component={createTabNavigator}
@ -132,49 +113,53 @@ function MainStackComponent(props: { createTabNavigator: () => JSX.Element }) {
component={ImageGalleryScreen} component={ImageGalleryScreen}
options={{ options={{
headerShown: false, headerShown: false,
...modalTransition,
}} }}
/> />
{CreateScreenCollapsibleStack( <MainStack.Screen
MainRoutes.Settings, name={MainRoutes.Settings}
MainStack, component={SettingsScreen}
SettingsScreen, options={{
i18n.t('screens.settings.title') title: i18n.t('screens.settings.title'),
)} }}
{CreateScreenCollapsibleStack( />
MainRoutes.DashboardEdit, <MainStack.Screen
MainStack, name={MainRoutes.DashboardEdit}
DashboardEditScreen, component={DashboardEditScreen}
i18n.t('screens.settings.dashboardEdit.title') options={{
)} title: i18n.t('screens.settings.dashboardEdit.title'),
{CreateScreenCollapsibleStack( }}
MainRoutes.About, />
MainStack, <MainStack.Screen
AboutScreen, name={MainRoutes.About}
i18n.t('screens.about.title') component={AboutScreen}
)} options={{
{CreateScreenCollapsibleStack( title: i18n.t('screens.about.title'),
MainRoutes.Dependencies, }}
MainStack, />
AboutDependenciesScreen, <MainStack.Screen
i18n.t('screens.about.libs') name={MainRoutes.Dependencies}
)} component={AboutDependenciesScreen}
{CreateScreenCollapsibleStack( options={{
MainRoutes.Debug, title: i18n.t('screens.about.libs'),
MainStack, }}
DebugScreen, />
i18n.t('screens.about.debug') <MainStack.Screen
)} name={MainRoutes.Debug}
component={DebugScreen}
{CreateScreenCollapsibleStack( options={{
MainRoutes.GameStart, title: i18n.t('screens.about.debug'),
MainStack, }}
GameStartScreen, />
i18n.t('screens.game.title'), <MainStack.Screen
true, name={MainRoutes.GameStart}
undefined, component={GameStartScreen}
'transparent' options={{
)} title: i18n.t('screens.game.title'),
headerStyle: {
backgroundColor: 'transparent',
},
}}
/>
<MainStack.Screen <MainStack.Screen
name={MainRoutes.GameMain} name={MainRoutes.GameMain}
component={GameMainScreen} component={GameMainScreen}
@ -182,102 +167,114 @@ function MainStackComponent(props: { createTabNavigator: () => JSX.Element }) {
title: i18n.t('screens.game.title'), title: i18n.t('screens.game.title'),
}} }}
/> />
{CreateScreenCollapsibleStack( <MainStack.Screen
MainRoutes.Login, name={MainRoutes.Login}
MainStack, component={LoginScreen}
LoginScreen, options={{
i18n.t('screens.login.title'), title: i18n.t('screens.login.title'),
true, headerStyle: {
{ headerTintColor: '#fff' }, backgroundColor: 'transparent',
'transparent' },
)} }}
{getWebsiteStack('website', MainStack, WebsiteScreen, '')} />
<MainStack.Screen
{CreateScreenCollapsibleStack( name={'website'}
MainRoutes.SelfMenu, component={WebsiteScreen}
MainStack, options={{
SelfMenuScreen, title: '',
i18n.t('screens.menu.title') }}
)} />
{CreateScreenCollapsibleStack( <MainStack.Screen
MainRoutes.Proximo, name={MainRoutes.SelfMenu}
MainStack, component={SelfMenuScreen}
ProximoMainScreen, options={{
i18n.t('screens.proximo.title') title: i18n.t('screens.menu.title'),
)} }}
{CreateScreenCollapsibleStack( />
MainRoutes.ProximoList, <MainStack.Screen
MainStack, name={MainRoutes.Proximo}
ProximoListScreen, component={ProximoMainScreen}
i18n.t('screens.proximo.articleList') options={{
)} title: i18n.t('screens.proximo.title'),
{CreateScreenCollapsibleStack( }}
MainRoutes.ProximoAbout, />
MainStack, <MainStack.Screen
ProximoAboutScreen, name={MainRoutes.ProximoList}
i18n.t('screens.proximo.title'), component={ProximoListScreen}
true, options={{
{ ...modalTransition } title: i18n.t('screens.proximo.articleList'),
)} }}
/>
{CreateScreenCollapsibleStack( <MainStack.Screen
MainRoutes.Profile, name={MainRoutes.ProximoAbout}
MainStack, component={ProximoAboutScreen}
ProfileScreen, options={{
i18n.t('screens.profile.title') title: i18n.t('screens.proximo.title'),
)} }}
{CreateScreenCollapsibleStack( />
MainRoutes.ClubList, <MainStack.Screen
MainStack, name={MainRoutes.Profile}
ClubListScreen, component={ProfileScreen}
i18n.t('screens.clubs.title') options={{
)} title: i18n.t('screens.profile.title'),
{CreateScreenCollapsibleStack( }}
MainRoutes.ClubInformation, />
MainStack, <MainStack.Screen
ClubDisplayScreen, name={MainRoutes.ClubList}
i18n.t('screens.clubs.details'), component={ClubListScreen}
true, options={{
{ ...modalTransition } title: i18n.t('screens.clubs.title'),
)} }}
{CreateScreenCollapsibleStack( />
MainRoutes.ClubAbout, <MainStack.Screen
MainStack, name={MainRoutes.ClubInformation}
ClubAboutScreen, component={ClubDisplayScreen}
i18n.t('screens.clubs.title'), options={{
true, title: i18n.t('screens.clubs.details'),
{ ...modalTransition } }}
)} />
{CreateScreenCollapsibleStack( <MainStack.Screen
MainRoutes.EquipmentList, name={MainRoutes.ClubAbout}
MainStack, component={ClubAboutScreen}
EquipmentScreen, options={{
i18n.t('screens.equipment.title') title: i18n.t('screens.clubs.title'),
)} }}
{CreateScreenCollapsibleStack( />
MainRoutes.EquipmentRent, <MainStack.Screen
MainStack, name={MainRoutes.EquipmentList}
EquipmentLendScreen, component={EquipmentScreen}
i18n.t('screens.equipment.book') options={{
)} title: i18n.t('screens.equipment.title'),
{CreateScreenCollapsibleStack( }}
MainRoutes.EquipmentConfirm, />
MainStack, <MainStack.Screen
EquipmentConfirmScreen, name={MainRoutes.EquipmentRent}
i18n.t('screens.equipment.confirm') component={EquipmentLendScreen}
)} options={{
{CreateScreenCollapsibleStack( title: i18n.t('screens.equipment.book'),
MainRoutes.Vote, }}
MainStack, />
VoteScreen, <MainStack.Screen
i18n.t('screens.vote.title') name={MainRoutes.EquipmentConfirm}
)} component={EquipmentConfirmScreen}
{CreateScreenCollapsibleStack( options={{
MainRoutes.Feedback, title: i18n.t('screens.equipment.confirm'),
MainStack, }}
BugReportScreen, />
i18n.t('screens.feedback.title') <MainStack.Screen
)} name={MainRoutes.Vote}
component={VoteScreen}
options={{
title: i18n.t('screens.vote.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.Feedback}
component={BugReportScreen}
options={{
title: i18n.t('screens.feedback.title'),
}}
/>
</MainStack.Navigator> </MainStack.Navigator>
); );
} }

View file

@ -18,16 +18,12 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import { import { createStackNavigator } from '@react-navigation/stack';
createStackNavigator,
TransitionPresets,
} from '@react-navigation/stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Title, useTheme } from 'react-native-paper'; import { Title, useTheme } from 'react-native-paper';
import { Platform, StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { createCollapsibleStack } from 'react-navigation-collapsible';
import { View } from 'react-native-animatable'; import { View } from 'react-native-animatable';
import HomeScreen from '../screens/Home/HomeScreen'; import HomeScreen from '../screens/Home/HomeScreen';
import PlanningScreen from '../screens/Planning/PlanningScreen'; import PlanningScreen from '../screens/Planning/PlanningScreen';
@ -44,23 +40,8 @@ import CustomTabBar from '../components/Tabbar/CustomTabBar';
import WebsitesHomeScreen from '../screens/Services/ServicesScreen'; import WebsitesHomeScreen from '../screens/Services/ServicesScreen';
import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen'; import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen';
import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen'; import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen';
import {
CreateScreenCollapsibleStack,
getWebsiteStack,
} from '../utils/CollapsibleUtils';
import Mascot, { MASCOT_STYLE } from '../components/Mascot/Mascot'; import Mascot, { MASCOT_STYLE } from '../components/Mascot/Mascot';
const modalTransition =
Platform.OS === 'ios'
? TransitionPresets.ModalPresentationIOS
: TransitionPresets.ModalTransition;
const defaultScreenOptions = {
gestureEnabled: true,
cardOverlayEnabled: true,
...modalTransition,
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
header: { header: {
flexDirection: 'row', flexDirection: 'row',
@ -79,29 +60,22 @@ const ServicesStack = createStackNavigator();
function ServicesStackComponent() { function ServicesStackComponent() {
return ( return (
<ServicesStack.Navigator <ServicesStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
initialRouteName="index" <ServicesStack.Screen
headerMode="screen" name={'index'}
screenOptions={defaultScreenOptions} component={WebsitesHomeScreen}
> options={{ title: i18n.t('screens.services.title') }}
{CreateScreenCollapsibleStack( />
'index', <ServicesStack.Screen
ServicesStack, name={'services-section'}
WebsitesHomeScreen, component={ServicesSectionScreen}
i18n.t('screens.services.title') options={{ title: 'SECTION' }}
)} />
{CreateScreenCollapsibleStack( <ServicesStack.Screen
'services-section', name={'amicale-contact'}
ServicesStack, component={AmicaleContactScreen}
ServicesSectionScreen, options={{ title: i18n.t('screens.amicaleAbout.title') }}
'SECTION' />
)}
{CreateScreenCollapsibleStack(
'amicale-contact',
ServicesStack,
AmicaleContactScreen,
i18n.t('screens.amicaleAbout.title')
)}
</ServicesStack.Navigator> </ServicesStack.Navigator>
); );
} }
@ -110,23 +84,17 @@ const ProxiwashStack = createStackNavigator();
function ProxiwashStackComponent() { function ProxiwashStackComponent() {
return ( return (
<ProxiwashStack.Navigator <ProxiwashStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
initialRouteName="index" <ProxiwashStack.Screen
headerMode="screen" name={'index-contact'}
screenOptions={defaultScreenOptions} component={ProxiwashScreen}
> options={{ title: i18n.t('screens.proxiwash.title') }}
{CreateScreenCollapsibleStack( />
'index', <ProxiwashStack.Screen
ProxiwashStack, name={'proxiwash-about'}
ProxiwashScreen, component={ProxiwashAboutScreen}
i18n.t('screens.proxiwash.title') options={{ title: i18n.t('screens.proxiwash.title') }}
)} />
{CreateScreenCollapsibleStack(
'proxiwash-about',
ProxiwashStack,
ProxiwashAboutScreen,
i18n.t('screens.proxiwash.title')
)}
</ProxiwashStack.Navigator> </ProxiwashStack.Navigator>
); );
} }
@ -135,22 +103,17 @@ const PlanningStack = createStackNavigator();
function PlanningStackComponent() { function PlanningStackComponent() {
return ( return (
<PlanningStack.Navigator <PlanningStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
initialRouteName="index"
headerMode="screen"
screenOptions={defaultScreenOptions}
>
<PlanningStack.Screen <PlanningStack.Screen
name="index" name={'index'}
component={PlanningScreen} component={PlanningScreen}
options={{ title: i18n.t('screens.planning.title') }} options={{ title: i18n.t('screens.planning.title') }}
/> />
{CreateScreenCollapsibleStack( <PlanningStack.Screen
'planning-information', name={'planning-information'}
PlanningStack, component={PlanningDisplayScreen}
PlanningDisplayScreen, options={{ title: i18n.t('screens.planning.eventDetails') }}
i18n.t('screens.planning.eventDetails') />
)}
</PlanningStack.Navigator> </PlanningStack.Navigator>
); );
} }
@ -167,73 +130,63 @@ function HomeStackComponent(
} }
const { colors } = useTheme(); const { colors } = useTheme();
return ( return (
<HomeStack.Navigator <HomeStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
initialRouteName="index"
headerMode="screen"
screenOptions={defaultScreenOptions}
>
{createCollapsibleStack(
<HomeStack.Screen
name="index"
component={HomeScreen}
options={{
title: i18n.t('screens.home.title'),
headerStyle: {
backgroundColor: colors.surface,
},
headerTitle: () => (
<View style={styles.header}>
<Mascot
style={styles.mascot}
emotion={MASCOT_STYLE.RANDOM}
animated
entryAnimation={{
animation: 'bounceIn',
duration: 1000,
}}
loopAnimation={{
animation: 'pulse',
duration: 2000,
iterationCount: 'infinite',
}}
/>
<Title style={styles.title}>
{i18n.t('screens.home.title')}
</Title>
</View>
),
}}
initialParams={params}
/>,
{
collapsedColor: colors.surface,
useNativeDriver: true,
}
)}
<HomeStack.Screen <HomeStack.Screen
name="scanner" name={'index'}
component={HomeScreen}
options={{
title: i18n.t('screens.home.title'),
headerStyle: {
backgroundColor: colors.surface,
},
headerTitle: (headerProps) => (
<View style={styles.header}>
<Mascot
style={styles.mascot}
emotion={MASCOT_STYLE.RANDOM}
animated
entryAnimation={{
animation: 'bounceIn',
duration: 1000,
}}
loopAnimation={{
animation: 'pulse',
duration: 2000,
iterationCount: 'infinite',
}}
/>
<Title style={styles.title}>{headerProps.children}</Title>
</View>
),
}}
initialParams={params}
/>
<HomeStack.Screen
name={'scanner'}
component={ScannerScreen} component={ScannerScreen}
options={{ title: i18n.t('screens.scanner.title') }} options={{ title: i18n.t('screens.scanner.title') }}
/> />
<HomeStack.Screen
{CreateScreenCollapsibleStack( name={'club-information'}
'club-information', component={ClubDisplayScreen}
HomeStack, options={{
ClubDisplayScreen, title: i18n.t('screens.clubs.details'),
i18n.t('screens.clubs.details') }}
)} />
{CreateScreenCollapsibleStack( <HomeStack.Screen
'feed-information', name={'feed-information'}
HomeStack, component={FeedItemScreen}
FeedItemScreen, options={{
i18n.t('screens.home.feed') title: i18n.t('screens.home.feed'),
)} }}
{CreateScreenCollapsibleStack( />
'planning-information', <HomeStack.Screen
HomeStack, name={'planning-information'}
PlanningDisplayScreen, component={PlanningDisplayScreen}
i18n.t('screens.planning.eventDetails') options={{
)} title: i18n.t('screens.planning.eventDetails'),
}}
/>
</HomeStack.Navigator> </HomeStack.Navigator>
); );
} }
@ -242,23 +195,21 @@ const PlanexStack = createStackNavigator();
function PlanexStackComponent() { function PlanexStackComponent() {
return ( return (
<PlanexStack.Navigator <PlanexStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
initialRouteName="index" <PlanexStack.Screen
headerMode="screen" name={'index'}
screenOptions={defaultScreenOptions} component={PlanexScreen}
> options={{
{getWebsiteStack( title: i18n.t('screens.planex.title'),
'index', }}
PlanexStack, />
PlanexScreen, <PlanexStack.Screen
i18n.t('screens.planex.title') name={'group-select'}
)} component={GroupSelectionScreen}
{CreateScreenCollapsibleStack( options={{
'group-select', title: '',
PlanexStack, }}
GroupSelectionScreen, />
''
)}
</PlanexStack.Navigator> </PlanexStack.Navigator>
); );
} }
@ -270,6 +221,34 @@ type PropsType = {
defaultHomeData: { [key: string]: string }; defaultHomeData: { [key: string]: string };
}; };
const ICONS: {
[key: string]: {
normal: string;
focused: string;
};
} = {
services: {
normal: 'account-circle-outline',
focused: 'account-circle',
},
proxiwash: {
normal: 'tshirt-crew-outline',
focused: 'tshirt-crew',
},
home: {
normal: '',
focused: '',
},
planning: {
normal: 'calendar-range-outline',
focused: 'calendar-range',
},
planex: {
normal: 'clock-outline',
focused: 'clock',
},
};
export default class TabNavigator extends React.Component<PropsType> { export default class TabNavigator extends React.Component<PropsType> {
defaultRoute: string; defaultRoute: string;
createHomeStackComponent: () => any; createHomeStackComponent: () => any;
@ -287,33 +266,44 @@ export default class TabNavigator extends React.Component<PropsType> {
} }
render() { render() {
const LABELS: {
[key: string]: string;
} = {
services: i18n.t('screens.services.title'),
proxiwash: i18n.t('screens.proxiwash.title'),
home: i18n.t('screens.home.title'),
planning: i18n.t('screens.planning.title'),
planex: i18n.t('screens.planex.title'),
};
return ( return (
<Tab.Navigator <Tab.Navigator
initialRouteName={this.defaultRoute} initialRouteName={this.defaultRoute}
tabBar={(tabProps) => <CustomTabBar {...tabProps} />} tabBar={(tabProps) => (
<CustomTabBar {...tabProps} labels={LABELS} icons={ICONS} />
)}
> >
<Tab.Screen <Tab.Screen
name="services" name={'services'}
component={ServicesStackComponent} component={ServicesStackComponent}
options={{ title: i18n.t('screens.services.title') }} options={{ title: i18n.t('screens.services.title') }}
/> />
<Tab.Screen <Tab.Screen
name="proxiwash" name={'proxiwash'}
component={ProxiwashStackComponent} component={ProxiwashStackComponent}
options={{ title: i18n.t('screens.proxiwash.title') }} options={{ title: i18n.t('screens.proxiwash.title') }}
/> />
<Tab.Screen <Tab.Screen
name="home" name={'home'}
component={this.createHomeStackComponent} component={this.createHomeStackComponent}
options={{ title: i18n.t('screens.home.title') }} options={{ title: i18n.t('screens.home.title') }}
/> />
<Tab.Screen <Tab.Screen
name="planning" name={'planning'}
component={PlanningStackComponent} component={PlanningStackComponent}
options={{ title: i18n.t('screens.planning.title') }} options={{ title: i18n.t('screens.planning.title') }}
/> />
<Tab.Screen <Tab.Screen
name="planex" name={'planex'}
component={PlanexStackComponent} component={PlanexStackComponent}
options={{ title: i18n.t('screens.planex.title') }} options={{ title: i18n.t('screens.planex.title') }}
/> />

View file

@ -31,7 +31,9 @@ import i18n from 'i18n-js';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen'; import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
import CustomHTML from '../../../components/Overrides/CustomHTML'; import CustomHTML from '../../../components/Overrides/CustomHTML';
import CustomTabBar from '../../../components/Tabbar/CustomTabBar'; import CustomTabBar, {
TAB_BAR_HEIGHT,
} from '../../../components/Tabbar/CustomTabBar';
import type { ClubCategoryType, ClubType } from './ClubListScreen'; import type { ClubCategoryType, ClubType } from './ClubListScreen';
import { ERROR_TYPE } from '../../../utils/WebData'; import { ERROR_TYPE } from '../../../utils/WebData';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
@ -174,7 +176,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
return ( return (
<Card <Card
style={{ style={{
marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20, marginBottom: TAB_BAR_HEIGHT + 20,
...styles.card, ...styles.card,
}} }}
> >

View file

@ -441,7 +441,7 @@ class LoginScreen extends React.Component<Props, StateType> {
enabled enabled
keyboardVerticalOffset={100} keyboardVerticalOffset={100}
> >
<CollapsibleScrollView> <CollapsibleScrollView headerColors={'transparent'}>
<View style={GENERAL_STYLES.flex}>{this.getMainCard()}</View> <View style={GENERAL_STYLES.flex}>{this.getMainCard()}</View>
<MascotPopup <MascotPopup
visible={mascotDialogVisible} visible={mascotDialogVisible}

View file

@ -25,7 +25,9 @@ import { StackNavigationProp } from '@react-navigation/stack';
import MaterialHeaderButtons, { import MaterialHeaderButtons, {
Item, Item,
} from '../../components/Overrides/CustomHeaderButton'; } from '../../components/Overrides/CustomHeaderButton';
import CustomTabBar from '../../components/Tabbar/CustomTabBar'; import CustomTabBar, {
TAB_BAR_HEIGHT,
} from '../../components/Tabbar/CustomTabBar';
import type { FeedItemType } from './HomeScreen'; import type { FeedItemType } from './HomeScreen';
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
import ImageGalleryButton from '../../components/Media/ImageGalleryButton'; import ImageGalleryButton from '../../components/Media/ImageGalleryButton';
@ -117,9 +119,7 @@ class FeedItemScreen extends React.Component<PropsType> {
style={styles.button} style={styles.button}
/> />
) : null} ) : null}
<Card.Content <Card.Content style={{ paddingBottom: TAB_BAR_HEIGHT + 20 }}>
style={{ paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20 }}
>
{this.displayData.message !== undefined ? ( {this.displayData.message !== undefined ? (
<Autolink <Autolink
text={this.displayData.message} text={this.displayData.message}

View file

@ -26,7 +26,9 @@ import i18n from 'i18n-js';
import { PERMISSIONS, request, RESULTS } from 'react-native-permissions'; import { PERMISSIONS, request, RESULTS } from 'react-native-permissions';
import URLHandler from '../../utils/URLHandler'; import URLHandler from '../../utils/URLHandler';
import AlertDialog from '../../components/Dialogs/AlertDialog'; import AlertDialog from '../../components/Dialogs/AlertDialog';
import CustomTabBar from '../../components/Tabbar/CustomTabBar'; import CustomTabBar, {
TAB_BAR_HEIGHT,
} from '../../components/Tabbar/CustomTabBar';
import LoadingConfirmDialog from '../../components/Dialogs/LoadingConfirmDialog'; import LoadingConfirmDialog from '../../components/Dialogs/LoadingConfirmDialog';
import { MASCOT_STYLE } from '../../components/Mascot/Mascot'; import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup'; import MascotPopup from '../../components/Mascot/MascotPopup';
@ -218,7 +220,7 @@ class ScannerScreen extends React.Component<{}, StateType> {
<View <View
style={{ style={{
...styles.container, ...styles.container,
marginBottom: CustomTabBar.TAB_BAR_HEIGHT, marginBottom: TAB_BAR_HEIGHT,
}} }}
> >
{state.hasPermission ? this.getScanner() : this.getPermissionScreen()} {state.hasPermission ? this.getScanner() : this.getPermissionScreen()}

View file

@ -54,6 +54,7 @@ type StateType = {
dialogTitle: string | React.ReactNode; dialogTitle: string | React.ReactNode;
dialogMessage: string; dialogMessage: string;
currentGroup: PlanexGroupType; currentGroup: PlanexGroupType;
injectJS: string;
}; };
const PLANEX_URL = 'http://planex.insa-toulouse.fr/'; const PLANEX_URL = 'http://planex.insa-toulouse.fr/';
@ -153,20 +154,14 @@ const styles = StyleSheet.create({
* This screen uses a webview to render the page * This screen uses a webview to render the page
*/ */
class PlanexScreen extends React.Component<PropsType, StateType> { class PlanexScreen extends React.Component<PropsType, StateType> {
webScreenRef: { current: null | WebViewScreen };
barRef: { current: null | AnimatedBottomBar }; barRef: { current: null | AnimatedBottomBar };
customInjectedJS: string;
/** /**
* Defines custom injected JavaScript to improve the page display on mobile * Defines custom injected JavaScript to improve the page display on mobile
*/ */
constructor(props: PropsType) { constructor(props: PropsType) {
super(props); super(props);
this.webScreenRef = React.createRef();
this.barRef = React.createRef(); this.barRef = React.createRef();
this.customInjectedJS = '';
let currentGroupString = AsyncStorageManager.getString( let currentGroupString = AsyncStorageManager.getString(
AsyncStorageManager.PREFERENCES.planexCurrentGroup.key AsyncStorageManager.PREFERENCES.planexCurrentGroup.key
); );
@ -184,8 +179,8 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
dialogTitle: '', dialogTitle: '',
dialogMessage: '', dialogMessage: '',
currentGroup, currentGroup,
injectJS: '',
}; };
this.generateInjectedJS(currentGroup.id);
} }
/** /**
@ -196,20 +191,6 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
navigation.addListener('focus', this.onScreenFocus); navigation.addListener('focus', this.onScreenFocus);
} }
/**
* Only update the screen if the dark theme changed
*
* @param nextProps
* @returns {boolean}
*/
shouldComponentUpdate(nextProps: PropsType): boolean {
const { props, state } = this;
if (nextProps.theme.dark !== props.theme.dark) {
this.generateInjectedJS(state.currentGroup.id);
}
return true;
}
/** /**
* Gets the Webview, with an error view on top if no group is selected. * Gets the Webview, with an error view on top if no group is selected.
* *
@ -218,6 +199,7 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
getWebView() { getWebView() {
const { props, state } = this; const { props, state } = this;
const showWebview = state.currentGroup.id !== -1; const showWebview = state.currentGroup.id !== -1;
console.log(state.injectJS);
return ( return (
<View style={GENERAL_STYLES.flex}> <View style={GENERAL_STYLES.flex}>
@ -230,10 +212,9 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
/> />
) : null} ) : null}
<WebViewScreen <WebViewScreen
ref={this.webScreenRef}
navigation={props.navigation}
url={PLANEX_URL} url={PLANEX_URL}
customJS={this.customInjectedJS} initialJS={this.generateInjectedJS(this.state.currentGroup.id)}
injectJS={this.state.injectJS}
onMessage={this.onMessage} onMessage={this.onMessage}
onScroll={this.onScroll} onScroll={this.onScroll}
showAdvancedControls={false} showAdvancedControls={false}
@ -269,9 +250,13 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
} else { } else {
command = `$('#calendar').fullCalendar('${action}', '${data}')`; command = `$('#calendar').fullCalendar('${action}', '${data}')`;
} }
if (this.webScreenRef.current != null) { // String must resolve to true to prevent crash on iOS
this.webScreenRef.current.injectJavaScript(`${command};true;`); command += ';true;';
} // Injected javascript must end with true // Change the injected
if (command === this.state.injectJS) {
command += ';true;';
}
this.setState({ injectJS: command });
}; };
/** /**
@ -373,7 +358,6 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
group group
); );
navigation.setOptions({ title: getPrettierPlanexGroupName(group.name) }); navigation.setOptions({ title: getPrettierPlanexGroupName(group.name) });
this.generateInjectedJS(group.id);
} }
/** /**
@ -382,16 +366,20 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
* @param groupID The current group selected * @param groupID The current group selected
*/ */
generateInjectedJS(groupID: number) { generateInjectedJS(groupID: number) {
this.customInjectedJS = `$(document).ready(function() {${OBSERVE_MUTATIONS_INJECTED}${FULL_CALENDAR_SETTINGS}displayAde(${groupID});${ let customInjectedJS = `$(document).ready(function() {
// Reset Ade ${OBSERVE_MUTATIONS_INJECTED}
DateManager.isWeekend(new Date()) ? 'calendar.next()' : '' ${FULL_CALENDAR_SETTINGS}
}${INJECT_STYLE}`; displayAde(${groupID});
${INJECT_STYLE}`;
if (DateManager.isWeekend(new Date())) {
customInjectedJS += `calendar.next();`;
}
if (ThemeManager.getNightMode()) { if (ThemeManager.getNightMode()) {
this.customInjectedJS += `$('head').append('<style>${CUSTOM_CSS_DARK}</style>');`; customInjectedJS += `$('head').append('<style>${CUSTOM_CSS_DARK}</style>');`;
} }
this.customInjectedJS += 'removeAlpha();});true;'; // Prevents crash on ios customInjectedJS += 'removeAlpha();});true;'; // Prevents crash on ios
return customInjectedJS;
} }
render() { render() {

View file

@ -28,7 +28,9 @@ import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
import { apiRequest, ERROR_TYPE } from '../../utils/WebData'; import { apiRequest, ERROR_TYPE } from '../../utils/WebData';
import ErrorView from '../../components/Screens/ErrorView'; import ErrorView from '../../components/Screens/ErrorView';
import CustomHTML from '../../components/Overrides/CustomHTML'; import CustomHTML from '../../components/Overrides/CustomHTML';
import CustomTabBar from '../../components/Tabbar/CustomTabBar'; import CustomTabBar, {
TAB_BAR_HEIGHT,
} from '../../components/Tabbar/CustomTabBar';
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
import type { PlanningEventType } from '../../utils/Planning'; import type { PlanningEventType } from '../../utils/Planning';
import ImageGalleryButton from '../../components/Media/ImageGalleryButton'; import ImageGalleryButton from '../../components/Media/ImageGalleryButton';
@ -145,9 +147,7 @@ class PlanningDisplayScreen extends React.Component<PropsType, StateType> {
) : null} ) : null}
{displayData.description !== null ? ( {displayData.description !== null ? (
<Card.Content <Card.Content style={{ paddingBottom: TAB_BAR_HEIGHT + 20 }}>
style={{ paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20 }}
>
<CustomHTML html={displayData.description} /> <CustomHTML html={displayData.description} />
</Card.Content> </Card.Content>
) : ( ) : (

View file

@ -21,7 +21,9 @@ import * as React from 'react';
import { Image, StyleSheet, View } from 'react-native'; import { Image, StyleSheet, View } from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { Card, Avatar, Paragraph, Text } from 'react-native-paper'; import { Card, Avatar, Paragraph, Text } from 'react-native-paper';
import CustomTabBar from '../../../components/Tabbar/CustomTabBar'; import CustomTabBar, {
TAB_BAR_HEIGHT,
} from '../../../components/Tabbar/CustomTabBar';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
const LOGO = 'https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png'; const LOGO = 'https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png';
@ -72,7 +74,7 @@ export default function ProximoAboutScreen() {
<Card <Card
style={{ style={{
...styles.card, ...styles.card,
marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20, marginBottom: TAB_BAR_HEIGHT + 20,
}} }}
> >
<Card.Title <Card.Title

View file

@ -23,11 +23,15 @@ import WebViewScreen from '../../components/Screens/WebViewScreen';
import AvailableWebsites from '../../constants/AvailableWebsites'; import AvailableWebsites from '../../constants/AvailableWebsites';
import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen'; import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
type PropsType = { type Props = {
navigation: StackNavigationProp<any>; navigation: StackNavigationProp<any>;
route: { params: { host: string; path: string | null; title: string } }; route: { params: { host: string; path: string | null; title: string } };
}; };
type State = {
url: string;
};
const ENABLE_MOBILE_STRING = const ENABLE_MOBILE_STRING =
'<meta name="viewport" content="width=device-width, initial-scale=1.0">'; '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
@ -43,18 +47,18 @@ const BIB_BACK_BUTTON =
'</a>' + '</a>' +
'</div>'; '</div>';
class WebsiteScreen extends React.Component<PropsType> { class WebsiteScreen extends React.Component<Props, State> {
fullUrl: string;
injectedJS: { [key: string]: string }; injectedJS: { [key: string]: string };
customPaddingFunctions: { [key: string]: (padding: string) => string }; customPaddingFunctions: { [key: string]: (padding: number) => string };
host: string; host: string;
constructor(props: PropsType) { constructor(props: Props) {
super(props); super(props);
this.fullUrl = ''; this.state = {
url: '',
};
this.host = ''; this.host = '';
props.navigation.addListener('focus', this.onScreenFocus); props.navigation.addListener('focus', this.onScreenFocus);
this.injectedJS = {}; this.injectedJS = {};
@ -70,7 +74,7 @@ class WebsiteScreen extends React.Component<PropsType> {
`$(".hero-unit-form").append("${BIB_BACK_BUTTON}");true;`; `$(".hero-unit-form").append("${BIB_BACK_BUTTON}");true;`;
this.customPaddingFunctions[AvailableWebsites.websites.BLUEMIND] = ( this.customPaddingFunctions[AvailableWebsites.websites.BLUEMIND] = (
padding: string padding: number
): string => { ): string => {
return ( return (
`$('head').append('${ENABLE_MOBILE_STRING}');` + `$('head').append('${ENABLE_MOBILE_STRING}');` +
@ -79,7 +83,7 @@ class WebsiteScreen extends React.Component<PropsType> {
); );
}; };
this.customPaddingFunctions[AvailableWebsites.websites.WIKETUD] = ( this.customPaddingFunctions[AvailableWebsites.websites.WIKETUD] = (
padding: string padding: number
): string => { ): string => {
return ( return (
`$('#p-logo-text').css('top', 10 + ${padding});` + `$('#p-logo-text').css('top', 10 + ${padding});` +
@ -99,16 +103,20 @@ class WebsiteScreen extends React.Component<PropsType> {
*/ */
handleNavigationParams() { handleNavigationParams() {
const { route, navigation } = this.props; const { route, navigation } = this.props;
if (route.params != null) { if (route.params != null) {
console.log(route.params);
this.host = route.params.host; this.host = route.params.host;
let { path } = route.params; let { path } = route.params;
const { title } = route.params; const { title } = route.params;
let fullUrl = '';
if (this.host != null && path != null) { if (this.host != null && path != null) {
path = path.replace(this.host, ''); path = path.replace(this.host, '');
this.fullUrl = this.host + path; fullUrl = this.host + path;
} else { } else {
this.fullUrl = this.host; fullUrl = this.host;
} }
this.setState({ url: fullUrl });
if (title != null) { if (title != null) {
navigation.setOptions({ title }); navigation.setOptions({ title });
@ -117,7 +125,6 @@ class WebsiteScreen extends React.Component<PropsType> {
} }
render() { render() {
const { navigation } = this.props;
let injectedJavascript = ''; let injectedJavascript = '';
let customPadding = null; let customPadding = null;
if (this.host != null && this.injectedJS[this.host] != null) { if (this.host != null && this.injectedJS[this.host] != null) {
@ -127,12 +134,11 @@ class WebsiteScreen extends React.Component<PropsType> {
customPadding = this.customPaddingFunctions[this.host]; customPadding = this.customPaddingFunctions[this.host];
} }
if (this.fullUrl != null) { if (this.state.url) {
return ( return (
<WebViewScreen <WebViewScreen
navigation={navigation} url={this.state.url}
url={this.fullUrl} initialJS={injectedJavascript}
customJS={injectedJavascript}
customPaddingFunction={customPadding} customPaddingFunction={customPadding}
/> />
); );

157
src/screens/Test.tsx Normal file
View file

@ -0,0 +1,157 @@
import { useNavigation } from '@react-navigation/core';
import { StackNavigationProp } from '@react-navigation/stack';
import React from 'react';
import { Animated, View } from 'react-native';
import { Text } from 'react-native-paper';
import {
Collapsible,
useCollapsibleHeader,
} from 'react-navigation-collapsible';
import CollapsibleFlatList from '../components/Collapsible/CollapsibleFlatList';
import FeedItem from '../components/Home/FeedItem';
import WebSectionList from '../components/Screens/WebSectionList';
import withCollapsible from '../utils/withCollapsible';
import { FeedItemType } from './Home/HomeScreen';
import i18n from 'i18n-js';
import CollapsibleSectionList from '../components/Collapsible/CollapsibleSectionList';
// export default function Test() {
// const {
// onScroll /* Event handler */,
// onScrollWithListener /* Event handler creator */,
// containerPaddingTop /* number */,
// scrollIndicatorInsetTop /* number */,
// /* Animated.AnimatedValue contentOffset from scrolling */
// positionY /* 0.0 ~ length of scrollable component (contentOffset)
// /* Animated.AnimatedInterpolation by scrolling */,
// translateY /* 0.0 ~ -headerHeight */,
// progress /* 0.0 ~ 1.0 */,
// opacity /* 1.0 ~ 0.0 */,
// } = useCollapsibleHeader();
// const renderItem = () => {
// return (
// <View
// style={{
// marginTop: 50,
// marginBottom: 50,
// }}
// >
// <Text>TEST</Text>
// </View>
// );
// };
// return (
// <Animated.FlatList
// onScroll={onScroll}
// contentContainerStyle={{ paddingTop: containerPaddingTop }}
// scrollIndicatorInsets={{ top: scrollIndicatorInsetTop }}
// data={[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}
// renderItem={renderItem}
// />
// );
// }
type Props = {
navigation: StackNavigationProp<any>;
collapsibleStack: Collapsible;
};
const DATA_URL =
'https://etud.insa-toulouse.fr/~amicale_app/v2/dashboard/dashboard_data.json';
const FEED_ITEM_HEIGHT = 500;
const REFRESH_TIME = 1000 * 20; // Refresh every 20 seconds
class Test extends React.Component<Props> {
createDataset = (): Array<{
title: string;
data: [] | Array<FeedItemType>;
id: string;
}> => {
return [
{
title: 'title',
data: [
{
id: '0',
message: 'message',
image: '',
link: '',
page_id: 'amicale.deseleves',
time: 0,
url: '',
video: '',
},
{
id: '1',
message: 'message',
image: '',
link: '',
page_id: 'amicale.deseleves',
time: 0,
url: '',
video: '',
},
{
id: '2',
message: 'message',
image: '',
link: '',
page_id: 'amicale.deseleves',
time: 0,
url: '',
video: '',
},
],
id: '0',
},
];
};
getRenderItem = ({ item }: { item: FeedItemType }) => (
<FeedItem item={item} height={FEED_ITEM_HEIGHT} />
);
render() {
const renderItem = () => {
return (
<View
style={{
marginTop: 50,
marginBottom: 50,
}}
>
<Text>TEST</Text>
</View>
);
};
const props = this.props;
// return (
// <CollapsibleFlatList
// data={[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}
// renderItem={renderItem}
// />
// );
// return (
// <CollapsibleSectionList
// sections={[{ title: '', data: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }]}
// renderItem={renderItem}
// />
// );
return (
<WebSectionList
navigation={props.navigation}
createDataset={this.createDataset}
autoRefreshTime={REFRESH_TIME}
refreshOnFocus
fetchUrl={DATA_URL}
renderItem={this.getRenderItem}
itemHeight={FEED_ITEM_HEIGHT}
showError={false}
/>
);
}
}
export default Test;

View file

@ -0,0 +1,16 @@
import React, { useContext } from 'react';
import { Collapsible } from 'react-navigation-collapsible';
export type CollapsibleContextType = {
collapsible?: Collapsible;
setCollapsible: (collapsible: Collapsible) => void;
};
export const CollapsibleContext = React.createContext<CollapsibleContextType>({
collapsible: undefined,
setCollapsible: () => undefined,
});
export function useCollapsible() {
return useContext(CollapsibleContext);
}

View file

@ -1,101 +0,0 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { useTheme } from 'react-native-paper';
import { createCollapsibleStack } from 'react-navigation-collapsible';
import StackNavigator, {
StackNavigationOptions,
} from '@react-navigation/stack';
import { StackNavigationState, TypedNavigator } from '@react-navigation/native';
import { StackNavigationEventMap } from '@react-navigation/stack/lib/typescript/src/types';
type StackNavigatorType = import('@react-navigation/native').TypedNavigator<
Record<string, object | undefined>,
StackNavigationState<any>,
StackNavigationOptions,
StackNavigationEventMap,
typeof StackNavigator
>;
/**
* Creates a navigation stack with the collapsible library, allowing the header to collapse on scroll.
*
* Please use the getWebsiteStack function if your screen uses a webview as their main component as it needs special parameters.
*
* @param name The screen name in the navigation stack
* @param Stack The stack component
* @param component The screen component
* @param title The screen title shown in the header (needs to be translated)
* @param useNativeDriver Whether to use the native driver for animations.
* Set to false if the screen uses a webview as this component does not support native driver.
* In all other cases, set it to true for increase performance.
* @param options Screen options to use, or null if no options are necessary.
* @param headerColor The color of the header. Uses default color if not specified
* @returns {JSX.Element}
*/
export function CreateScreenCollapsibleStack(
name: string,
Stack: TypedNavigator<any, any, any, any, any>,
component: React.ComponentType<any>,
title: string,
useNativeDriver: boolean = true,
options: StackNavigationOptions = {},
headerColor?: string
) {
const { colors } = useTheme();
return createCollapsibleStack(
<Stack.Screen
name={name}
component={component}
options={{
title,
headerStyle: {
backgroundColor: headerColor != null ? headerColor : colors.surface,
},
...options,
}}
/>,
{
collapsedColor: headerColor != null ? headerColor : colors.surface,
useNativeDriver: useNativeDriver, // native driver does not work with webview
}
);
}
/**
* Creates a navigation stack with the collapsible library, allowing the header to collapse on scroll.
*
* This is a preset for screens using a webview as their main component, as it uses special parameters to work.
* (aka a dirty workaround)
*
* @param name
* @param Stack
* @param component
* @param title
* @returns {JSX.Element}
*/
export function getWebsiteStack(
name: string,
Stack: TypedNavigator<any, any, any, any, any>,
component: React.ComponentType<any>,
title: string
) {
return CreateScreenCollapsibleStack(name, Stack, component, title, false);
}

View file

@ -18,29 +18,28 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import { useCollapsibleStack } from 'react-navigation-collapsible'; import { useTheme } from 'react-native-paper';
import {
useCollapsibleHeader,
UseCollapsibleOptions,
} from 'react-navigation-collapsible';
/** export default function withCollapsible<T>(
* Function used to manipulate Collapsible Hooks from a class. Component: React.ComponentType<any>,
* options?: UseCollapsibleOptions
* Usage : ) {
* return function WrappedComponent(props: T) {
* export withCollapsible(Component) const theme = useTheme();
* if (!options?.config?.collapsedColor) {
* replacing Component with the one you want to use. options = {
* This component will then receive the collapsibleStack prop. ...options,
* config: {
* @param Component The component to use Collapsible with ...options?.config,
* @returns {React.ComponentType<any>} collapsedColor: theme.colors.surface,
*/ },
export default function withCollapsible(Component: React.ComponentType<any>) { };
return React.forwardRef((props: any, ref: any) => { }
return ( const collapsible = useCollapsibleHeader(options);
<Component return <Component {...props} collapsible={collapsible} />;
collapsibleStack={useCollapsibleStack()} };
ref={ref}
{...props}
/>
);
});
} }