diff --git a/App.tsx b/App.tsx index 234895d..53ecbfd 100644 --- a/App.tsx +++ b/App.tsx @@ -37,6 +37,7 @@ import { setupStatusBar } from './src/utils/Utils'; import initLocales from './src/utils/Locales'; import { NavigationContainerRef } from '@react-navigation/core'; import GENERAL_STYLES from './src/constants/Styles'; +import CollapsibleProvider from './src/components/providers/CollapsibleProvider'; // Native optimizations https://reactnavigation.org/docs/react-native-screens // Crashes app when navigating away from webview on android 9+ @@ -210,26 +211,29 @@ export default class App extends React.Component<{}, StateType> { } return ( - - - - - - - - - + + + + + + + + + + + ); } diff --git a/package-lock.json b/package-lock.json index 7577f26..dea2b78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3931,7 +3931,6 @@ "version": "3.1.4", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz", "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==", - "dev": true, "requires": { "node-fetch": "2.6.1" } @@ -10596,14 +10595,39 @@ "integrity": "sha512-beZjdgbT9Y/Pg591Xy5XkKG20HffJiVad4n9bfcUF/f783A+tvOVXnqvbS58Lkaym93mi4jcDPMuW9Vc1t6rqg==" }, "react-native-gesture-handler": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.8.0.tgz", - "integrity": "sha512-E2FZa0qZ5Bi0Z8Jg4n9DaFomHvedSjwbO2DPmUUHYRy1lH2yxXUpSrqJd6yymu+Efzmjg2+JZzsjFYA2Iq8VEQ==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.10.3.tgz", + "integrity": "sha512-cBGMi1IEsIVMgoox4RvMx7V2r6bNKw0uR1Mu1o7NbuHS6BRSVLq0dP34l2ecnPlC+jpWd3le6Yg1nrdCjby2Mw==", "requires": { "@egjs/hammerjs": "^2.0.17", + "fbjs": "^3.0.0", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4", "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": { @@ -10871,11 +10895,12 @@ } }, "react-navigation-collapsible": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/react-navigation-collapsible/-/react-navigation-collapsible-5.6.4.tgz", - "integrity": "sha512-dXMbDw2TQ6s5XLk9h+2hUShXoS8KPChfdh/xmmLqfKmntS5YteE01+x78gU5KogB3etDraH1kvhW7xDnbG9AfA==", + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/react-navigation-collapsible/-/react-navigation-collapsible-5.9.1.tgz", + "integrity": "sha512-yUwHe8Z7++A8ThrjPI+Mcm7LqBhIqJc+1F4XszpI7EoHz3bJElzczbfyfuEvjSbYU9AgW3MdBWzaRIDluxcEuA==", "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": { @@ -11453,6 +11478,11 @@ "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": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", diff --git a/package.json b/package.json index f8eedc3..ed2a768 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "react-native-splash-screen": "3.2.0", "react-native-vector-icons": "8.1.0", "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" }, "devDependencies": { diff --git a/src/components/Animations/AnimatedBottomBar.tsx b/src/components/Animations/AnimatedBottomBar.tsx index 5237a0c..600526a 100644 --- a/src/components/Animations/AnimatedBottomBar.tsx +++ b/src/components/Animations/AnimatedBottomBar.tsx @@ -28,7 +28,7 @@ import { FAB, IconButton, Surface, withTheme } from 'react-native-paper'; import * as Animatable from 'react-native-animatable'; import { StackNavigationProp } from '@react-navigation/stack'; import AutoHideHandler from '../../utils/AutoHideHandler'; -import CustomTabBar from '../Tabbar/CustomTabBar'; +import CustomTabBar, { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar'; const AnimatedFAB = Animatable.createAnimatableComponent(FAB); @@ -159,7 +159,7 @@ class AnimatedBottomBar extends React.Component { useNativeDriver style={{ ...styles.container, - bottom: 10 + CustomTabBar.TAB_BAR_HEIGHT, + bottom: 10 + TAB_BAR_HEIGHT, }} > diff --git a/src/components/Animations/AnimatedFAB.tsx b/src/components/Animations/AnimatedFAB.tsx index 63b3174..debd674 100644 --- a/src/components/Animations/AnimatedFAB.tsx +++ b/src/components/Animations/AnimatedFAB.tsx @@ -27,7 +27,7 @@ import { import { FAB } from 'react-native-paper'; import * as Animatable from 'react-native-animatable'; import AutoHideHandler from '../../utils/AutoHideHandler'; -import CustomTabBar from '../Tabbar/CustomTabBar'; +import CustomTabBar, { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar'; type PropsType = { icon: string; @@ -82,7 +82,7 @@ export default class AnimatedFAB extends React.Component { useNativeDriver={true} style={{ ...styles.fab, - bottom: CustomTabBar.TAB_BAR_HEIGHT, + bottom: TAB_BAR_HEIGHT, }} > diff --git a/src/components/Collapsible/CollapsibleComponent.tsx b/src/components/Collapsible/CollapsibleComponent.tsx index f34d55a..5b2b59d 100644 --- a/src/components/Collapsible/CollapsibleComponent.tsx +++ b/src/components/Collapsible/CollapsibleComponent.tsx @@ -17,22 +17,27 @@ * along with Campus INSAT. If not, see . */ -import * as React from 'react'; -import { useCollapsibleStack } from 'react-navigation-collapsible'; -import CustomTabBar from '../Tabbar/CustomTabBar'; +import React, { useCallback } from 'react'; +import { useCollapsibleHeader } from 'react-navigation-collapsible'; +import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar'; import { NativeScrollEvent, NativeSyntheticEvent, StyleSheet, } from 'react-native'; +import { useTheme } from 'react-native-paper'; +import { useCollapsible } from '../../utils/CollapsibleContext'; +import { useFocusEffect } from '@react-navigation/core'; export type CollapsibleComponentPropsType = { children?: React.ReactNode; hasTab?: boolean; onScroll?: (event: NativeSyntheticEvent) => void; + paddedProps?: (paddingTop: number) => Record; + headerColors: string; }; -type PropsType = CollapsibleComponentPropsType & { +type Props = CollapsibleComponentPropsType & { component: React.ComponentType; }; @@ -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) => { if (props.onScroll) { props.onScroll(event); } }; - const Comp = props.component; - const { - containerPaddingTop, - scrollIndicatorInsetTop, - onScrollWithListener, - } = useCollapsibleStack(); - const paddingBottom = props.hasTab ? CustomTabBar.TAB_BAR_HEIGHT : 0; + + const pprops = + paddedProps !== undefined ? paddedProps(containerPaddingTop) : undefined; + return ( {children} diff --git a/src/components/Screens/WebSectionList.tsx b/src/components/Screens/WebSectionList.tsx index 9abd227..b061d18 100644 --- a/src/components/Screens/WebSectionList.tsx +++ b/src/components/Screens/WebSectionList.tsx @@ -32,10 +32,10 @@ import { Collapsible } from 'react-navigation-collapsible'; import { StackNavigationProp } from '@react-navigation/stack'; import ErrorView from './ErrorView'; import BasicLoadingScreen from './BasicLoadingScreen'; -import withCollapsible from '../../utils/withCollapsible'; -import CustomTabBar from '../Tabbar/CustomTabBar'; +import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar'; import { ERROR_TYPE, readData } from '../../utils/WebData'; import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList'; +import GENERAL_STYLES from '../../constants/Styles'; export type SectionListDataType = Array<{ title: string; @@ -260,19 +260,20 @@ class WebSectionList extends React.PureComponent< dataset = props.createDataset(state.fetchedData, state.refreshing); } - const { containerPaddingTop } = props.collapsibleStack; return ( - + - } + paddedProps={(paddingTop) => ({ + refreshControl: ( + + ), + })} renderSectionHeader={this.getRenderSectionHeader} renderItem={this.getRenderItem} stickySectionHeadersEnabled={props.stickyHeader} @@ -299,7 +300,7 @@ class WebSectionList extends React.PureComponent< : undefined } onScroll={this.onScroll} - hasTab + hasTab={true} /> extends React.PureComponent< }} duration={4000} style={{ - bottom: CustomTabBar.TAB_BAR_HEIGHT, + bottom: TAB_BAR_HEIGHT, }} > {i18n.t('general.listUpdateFail')} @@ -320,4 +321,4 @@ class WebSectionList extends React.PureComponent< } } -export default withCollapsible(WebSectionList); +export default WebSectionList; diff --git a/src/components/Screens/WebViewScreen.tsx b/src/components/Screens/WebViewScreen.tsx index 38cef0e..bcae42f 100644 --- a/src/components/Screens/WebViewScreen.tsx +++ b/src/components/Screens/WebViewScreen.tsx @@ -17,7 +17,13 @@ * along with Campus INSAT. If not, see . */ -import * as React from 'react'; +import React, { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from 'react'; import WebView from 'react-native-webview'; import { Divider, @@ -34,23 +40,21 @@ import { StyleSheet, } from 'react-native'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; -import { withTheme } from 'react-native-paper'; -import { StackNavigationProp } from '@react-navigation/stack'; -import { Collapsible } from 'react-navigation-collapsible'; -import withCollapsible from '../../utils/withCollapsible'; +import { useTheme } from 'react-native-paper'; +import { useCollapsibleHeader } from 'react-navigation-collapsible'; import MaterialHeaderButtons, { Item } from '../Overrides/CustomHeaderButton'; import { ERROR_TYPE } from '../../utils/WebData'; import ErrorView from './ErrorView'; import BasicLoadingScreen from './BasicLoadingScreen'; +import { useFocusEffect, useNavigation } from '@react-navigation/core'; +import { useCollapsible } from '../../utils/CollapsibleContext'; -type PropsType = { - navigation: StackNavigationProp; - theme: ReactNativePaper.Theme; +type Props = { url: string; - collapsibleStack: Collapsible; - onMessage: (event: { nativeEvent: { data: string } }) => void; - onScroll: (event: NativeSyntheticEvent) => void; - customJS?: string; + onMessage?: (event: { nativeEvent: { data: string } }) => void; + onScroll?: (event: NativeSyntheticEvent) => void; + initialJS?: string; + injectJS?: string; customPaddingFunction?: null | ((padding: number) => string); showAdvancedControls?: boolean; }; @@ -66,134 +70,113 @@ const styles = StyleSheet.create({ /** * Class defining a webview screen. */ -class WebViewScreen extends React.PureComponent { - static defaultProps = { - customJS: '', - showAdvancedControls: true, - customPaddingFunction: null, - }; +function WebViewScreen(props: Props) { + const [currentUrl, setCurrentUrl] = useState(props.url); + const [canGoBack, setCanGoBack] = useState(false); + const navigation = useNavigation(); + const theme = useTheme(); + const webviewRef = useRef(); - 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; - - constructor(props: PropsType) { - 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', () => { + useFocusEffect( + useCallback(() => { + setCollapsible(collapsible); BackHandler.addEventListener( 'hardwareBackPress', - this.onBackButtonPressAndroid + onBackButtonPressAndroid ); - }); - props.navigation.addListener('blur', () => { - BackHandler.removeEventListener( - 'hardwareBackPress', - this.onBackButtonPressAndroid - ); - }); - } + return () => { + BackHandler.removeEventListener( + 'hardwareBackPress', + onBackButtonPressAndroid + ); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [collapsible, setCollapsible]) + ); - /** - * Goes back on the webview or on the navigation stack if we cannot go back anymore - * - * @returns {boolean} - */ - onBackButtonPressAndroid = (): boolean => { - if (this.canGoBack) { - this.onGoBackClicked(); + useLayoutEffect(() => { + navigation.setOptions({ + headerRight: props.showAdvancedControls + ? getAdvancedButtons + : getBasicButton, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [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 false; }; - /** - * Gets header refresh and open in browser buttons - * - * @return {*} - */ - getBasicButton = () => { + const getBasicButton = () => { return ( ); }; - /** - * 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; + const getAdvancedButtons = () => { return ( - + } > ); }; - /** - * Gets the loading indicator - * - * @return {*} - */ - getRenderLoading = () => ; + const getRenderLoading = () => ; /** * Gets the javascript needed to generate a padding on top of the page @@ -202,91 +185,78 @@ class WebViewScreen extends React.PureComponent { * @param padding The padding to add in pixels * @returns {string} */ - getJavascriptPadding(padding: number): string { - const { props } = this; + const getJavascriptPadding = (padding: number) => { const customPadding = props.customPaddingFunction != null ? props.customPaddingFunction(padding) : ''; return `document.getElementsByTagName('body')[0].style.paddingTop = '${padding}px';${customPadding}true;`; - } + }; - /** - * Callback to use when refresh button is clicked. Reloads the webview. - */ - onRefreshClicked = () => { - if (this.webviewRef.current != null) { - this.webviewRef.current.reload(); + const onRefreshClicked = () => { + //@ts-ignore + if (webviewRef.current) { + //@ts-ignore + webviewRef.current.reload(); } }; - onGoBackClicked = () => { - if (this.webviewRef.current != null) { - this.webviewRef.current.goBack(); + const onGoBackClicked = () => { + //@ts-ignore + if (webviewRef.current) { + //@ts-ignore + webviewRef.current.goBack(); } }; - onGoForwardClicked = () => { - if (this.webviewRef.current != null) { - this.webviewRef.current.goForward(); + const onGoForwardClicked = () => { + //@ts-ignore + if (webviewRef.current) { + //@ts-ignore + webviewRef.current.goForward(); } }; - onOpenClicked = () => { - Linking.openURL(this.currentUrl); - }; + const onOpenClicked = () => Linking.openURL(currentUrl); - onScroll = (event: NativeSyntheticEvent) => { - const { onScroll } = this.props; - if (onScroll) { - onScroll(event); + const onScroll = (event: NativeSyntheticEvent) => { + if (props.onScroll) { + props.onScroll(event); } }; - /** - * Injects the given javascript string into the web page - * - * @param script The script to inject - */ - injectJavaScript = (script: string) => { - if (this.webviewRef.current != null) { - this.webviewRef.current.injectJavaScript(script); + const injectJavaScript = (script: string) => { + //@ts-ignore + if (webviewRef.current) { + //@ts-ignore + webviewRef.current.injectJavaScript(script); } }; - render() { - const { props } = this; - const { - containerPaddingTop, - onScrollWithListener, - } = props.collapsibleStack; - return ( - ( - - )} - onNavigationStateChange={(navState) => { - this.currentUrl = navState.url; - this.canGoBack = navState.canGoBack; - }} - onMessage={props.onMessage} - onLoad={() => { - this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop)); - }} - // Animations - onScroll={(event) => onScrollWithListener(this.onScroll)(event)} - /> - ); - } + return ( + ( + + )} + onNavigationStateChange={(navState) => { + setCurrentUrl(navState.url); + setCanGoBack(navState.canGoBack); + }} + onMessage={props.onMessage} + onLoad={() => injectJavaScript(getJavascriptPadding(containerPaddingTop))} + // Animations + onScroll={onScrollWithListener(onScroll)} + /> + ); } -export default withCollapsible(withTheme(WebViewScreen)); +export default WebViewScreen; diff --git a/src/components/Tabbar/CustomTabBar.tsx b/src/components/Tabbar/CustomTabBar.tsx index 5757de1..11748a3 100644 --- a/src/components/Tabbar/CustomTabBar.tsx +++ b/src/components/Tabbar/CustomTabBar.tsx @@ -17,211 +17,88 @@ * along with Campus INSAT. If not, see . */ -import * as React from 'react'; +import React from 'react'; +import type { BottomTabBarProps } from '@react-navigation/bottom-tabs'; import { Animated, StyleSheet } from 'react-native'; -import { withTheme } from 'react-native-paper'; -import { Collapsible } from 'react-navigation-collapsible'; import TabIcon from './TabIcon'; -import TabHomeIcon from './TabHomeIcon'; -import { BottomTabBarProps } from '@react-navigation/bottom-tabs'; -import { NavigationState } from '@react-navigation/native'; -import { - PartialState, - Route, -} from '@react-navigation/routers/lib/typescript/src/types'; +import { useTheme } from 'react-native-paper'; +import { useCollapsible } from '../../utils/CollapsibleContext'; -type RouteType = Route & { - state?: NavigationState | PartialState; -}; +export const TAB_BAR_HEIGHT = 50; -interface PropsType extends BottomTabBarProps { - theme: ReactNativePaper.Theme; +function CustomTabBar( + props: BottomTabBarProps & { + 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 ( + + {state.routes.map( + ( + route: { + key: string; + name: string; + params?: object | undefined; + }, + index: number + ) => { + const iconData = props.icons[route.name]; + return ( + props.navigation.navigate(route.name)} + icon={iconData.normal} + focusedIcon={iconData.focused} + label={props.labels[route.name]} + focused={state.index === index} + key={route.key} + /> + ); + } + )} + + ); } -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({ - container: { + bar: { flexDirection: 'row', width: '100%', + height: 50, position: 'absolute', bottom: 0, left: 0, }, }); -class CustomTabBar extends React.Component { - static TAB_BAR_HEIGHT = 48; - - constructor(props: PropsType) { - super(props); - 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 ( - index} - key={route.key} - /> - ); - } - return ( - - ); - }; - - 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 ( - - {icons} - - ); - } +function areEqual( + prevProps: BottomTabBarProps, + nextProps: BottomTabBarProps +) { + return prevProps.state.index === nextProps.state.index; } -export default withTheme(CustomTabBar); +export default React.memo(CustomTabBar, areEqual); diff --git a/src/components/Tabbar/TabHomeIcon.tsx b/src/components/Tabbar/TabHomeIcon.tsx index d5c2614..a5e41d5 100644 --- a/src/components/Tabbar/TabHomeIcon.tsx +++ b/src/components/Tabbar/TabHomeIcon.tsx @@ -1,135 +1,114 @@ -/* - * 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 . - */ - -import * as React from 'react'; -import { Image, StyleSheet, View } from 'react-native'; +import React from 'react'; +import { View, StyleSheet, Image } from 'react-native'; import { FAB } from 'react-native-paper'; 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; 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({ - container: { + outer: { flex: 1, justifyContent: 'center', }, - subcontainer: { + inner: { position: 'absolute', bottom: 0, left: 0, width: '100%', - marginBottom: -15, + height: 60, }, fab: { - marginTop: 15, marginLeft: 'auto', marginRight: 'auto', }, }); -/** - * Abstraction layer for Agenda component, using custom configuration - */ -class TabHomeIcon extends React.Component { - 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, - }, - }, - }); - } +const FOCUSED_ICON = require('../../../assets/tab-icon.png'); +const UNFOCUSED_ICON = require('../../../assets/tab-icon-outline.png'); - shouldComponentUpdate(nextProps: PropsType): boolean { - const { focused } = this.props; - return nextProps.focused !== focused; - } - - getIconRender = ({ size, color }: { size: number; color: string }) => { - const { focused } = this.props; +function TabHomeIcon(props: Props) { + const getImage = (iconProps: { size: number; color: string }) => { return ( - + + + ); }; - render() { - const { props } = this; - return ( - - + + - - + - ); - } + + ); } export default TabHomeIcon; diff --git a/src/components/Tabbar/TabIcon.tsx b/src/components/Tabbar/TabIcon.tsx index e674741..0fd0fad 100644 --- a/src/components/Tabbar/TabIcon.tsx +++ b/src/components/Tabbar/TabIcon.tsx @@ -1,143 +1,41 @@ -/* - * 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 . - */ +import React from 'react'; +import TabHomeIcon from './TabHomeIcon'; +import TabSideIcon from './TabSideIcon'; -import * as React from 'react'; -import { StyleSheet, View } from 'react-native'; -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 = { +interface Props { + isMiddle: boolean; focused: boolean; - color: string; - label: string; + label: string | undefined; icon: string; + focusedIcon: string; onPress: () => void; - onLongPress: () => void; - theme: ReactNativePaper.Theme; - extraData: null | boolean | number | string; -}; +} -const styles = StyleSheet.create({ - container: { - 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 { - 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; +function TabIcon(props: Props) { + if (props.isMiddle) { return ( - nextProps.focused !== props.focused || - nextProps.theme.dark !== props.theme.dark || - nextProps.extraData !== props.extraData - ); - } - - render() { - const { props } = this; - return ( - - - - - - - {props.label} - - - + /> + ); + } else { + return ( + ); } } -export default withTheme(TabIcon); +function areEqual(prevProps: Props, nextProps: Props) { + return prevProps.focused === nextProps.focused; +} + +export default React.memo(TabIcon, areEqual); diff --git a/src/components/Tabbar/TabSideIcon.tsx b/src/components/Tabbar/TabSideIcon.tsx new file mode 100644 index 0000000..c1ab538 --- /dev/null +++ b/src/components/Tabbar/TabSideIcon.tsx @@ -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 ( + + + + + + + {props.label} + + + + ); +} + +const styles = StyleSheet.create({ + ripple: { + flex: 1, + justifyContent: 'center', + }, + text: { + ...GENERAL_STYLES.centerHorizontal, + fontSize: 10, + }, +}); + +export default TabSideIcon; diff --git a/src/components/providers/CollapsibleProvider.tsx b/src/components/providers/CollapsibleProvider.tsx new file mode 100644 index 0000000..4820248 --- /dev/null +++ b/src/components/providers/CollapsibleProvider.tsx @@ -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({ + collapsible: undefined, + setCollapsible: setCollapsible, + }); + + return ( + + {props.children} + + ); +} diff --git a/src/navigation/MainNavigator.tsx b/src/navigation/MainNavigator.tsx index e11f863..4f3d931 100644 --- a/src/navigation/MainNavigator.tsx +++ b/src/navigation/MainNavigator.tsx @@ -18,12 +18,8 @@ */ import * as React from 'react'; -import { - createStackNavigator, - TransitionPresets, -} from '@react-navigation/stack'; +import { createStackNavigator } from '@react-navigation/stack'; import i18n from 'i18n-js'; -import { Platform } from 'react-native'; import SettingsScreen from '../screens/Other/Settings/SettingsScreen'; import AboutScreen from '../screens/About/AboutScreen'; import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen'; @@ -40,10 +36,6 @@ import ProfileScreen from '../screens/Amicale/ProfileScreen'; import ClubListScreen from '../screens/Amicale/Clubs/ClubListScreen'; import ClubAboutScreen from '../screens/Amicale/Clubs/ClubAboutScreen'; import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen'; -import { - CreateScreenCollapsibleStack, - getWebsiteStack, -} from '../utils/CollapsibleUtils'; import BugReportScreen from '../screens/Other/FeedbackScreen'; import WebsiteScreen from '../screens/Services/WebsiteScreen'; import EquipmentScreen, { @@ -54,6 +46,7 @@ import EquipmentConfirmScreen from '../screens/Amicale/Equipment/EquipmentConfir import DashboardEditScreen from '../screens/Other/Settings/DashboardEditScreen'; import GameStartScreen from '../screens/Game/screens/GameStartScreen'; import ImageGalleryScreen from '../screens/Media/ImageGalleryScreen'; +import Test from '../screens/Test'; export enum MainRoutes { Main = 'main', @@ -83,7 +76,7 @@ export enum MainRoutes { type DefaultParams = { [key in MainRoutes]: object | undefined }; -export interface FullParamsList extends DefaultParams { +export type FullParamsList = DefaultParams & { 'login': { nextScreen: string }; 'equipment-confirm': { item?: DeviceType; @@ -91,34 +84,22 @@ export interface FullParamsList extends DefaultParams { }; 'equipment-rent': { item?: DeviceType }; 'gallery': { images: Array<{ url: string }> }; -} +}; // 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 export type MainStackParamsList = FullParamsList & Record; -const modalTransition = - Platform.OS === 'ios' - ? TransitionPresets.ModalPresentationIOS - : TransitionPresets.ModalTransition; - -const defaultScreenOptions = { - gestureEnabled: true, - cardOverlayEnabled: true, - ...TransitionPresets.SlideFromRightIOS, -}; - const MainStack = createStackNavigator(); -function MainStackComponent(props: { createTabNavigator: () => JSX.Element }) { +function MainStackComponent(props: { + createTabNavigator: () => React.ReactElement; +}) { const { createTabNavigator } = props; return ( - + + JSX.Element }) { component={ImageGalleryScreen} options={{ headerShown: false, - ...modalTransition, }} /> - {CreateScreenCollapsibleStack( - MainRoutes.Settings, - MainStack, - SettingsScreen, - i18n.t('screens.settings.title') - )} - {CreateScreenCollapsibleStack( - MainRoutes.DashboardEdit, - MainStack, - DashboardEditScreen, - i18n.t('screens.settings.dashboardEdit.title') - )} - {CreateScreenCollapsibleStack( - MainRoutes.About, - MainStack, - AboutScreen, - i18n.t('screens.about.title') - )} - {CreateScreenCollapsibleStack( - MainRoutes.Dependencies, - MainStack, - AboutDependenciesScreen, - i18n.t('screens.about.libs') - )} - {CreateScreenCollapsibleStack( - MainRoutes.Debug, - MainStack, - DebugScreen, - i18n.t('screens.about.debug') - )} - - {CreateScreenCollapsibleStack( - MainRoutes.GameStart, - MainStack, - GameStartScreen, - i18n.t('screens.game.title'), - true, - undefined, - 'transparent' - )} + + + + + + JSX.Element }) { title: i18n.t('screens.game.title'), }} /> - {CreateScreenCollapsibleStack( - MainRoutes.Login, - MainStack, - LoginScreen, - i18n.t('screens.login.title'), - true, - { headerTintColor: '#fff' }, - 'transparent' - )} - {getWebsiteStack('website', MainStack, WebsiteScreen, '')} - - {CreateScreenCollapsibleStack( - MainRoutes.SelfMenu, - MainStack, - SelfMenuScreen, - i18n.t('screens.menu.title') - )} - {CreateScreenCollapsibleStack( - MainRoutes.Proximo, - MainStack, - ProximoMainScreen, - i18n.t('screens.proximo.title') - )} - {CreateScreenCollapsibleStack( - MainRoutes.ProximoList, - MainStack, - ProximoListScreen, - i18n.t('screens.proximo.articleList') - )} - {CreateScreenCollapsibleStack( - MainRoutes.ProximoAbout, - MainStack, - ProximoAboutScreen, - i18n.t('screens.proximo.title'), - true, - { ...modalTransition } - )} - - {CreateScreenCollapsibleStack( - MainRoutes.Profile, - MainStack, - ProfileScreen, - i18n.t('screens.profile.title') - )} - {CreateScreenCollapsibleStack( - MainRoutes.ClubList, - MainStack, - ClubListScreen, - i18n.t('screens.clubs.title') - )} - {CreateScreenCollapsibleStack( - MainRoutes.ClubInformation, - MainStack, - ClubDisplayScreen, - i18n.t('screens.clubs.details'), - true, - { ...modalTransition } - )} - {CreateScreenCollapsibleStack( - MainRoutes.ClubAbout, - MainStack, - ClubAboutScreen, - i18n.t('screens.clubs.title'), - true, - { ...modalTransition } - )} - {CreateScreenCollapsibleStack( - MainRoutes.EquipmentList, - MainStack, - EquipmentScreen, - i18n.t('screens.equipment.title') - )} - {CreateScreenCollapsibleStack( - MainRoutes.EquipmentRent, - MainStack, - EquipmentLendScreen, - i18n.t('screens.equipment.book') - )} - {CreateScreenCollapsibleStack( - MainRoutes.EquipmentConfirm, - MainStack, - EquipmentConfirmScreen, - i18n.t('screens.equipment.confirm') - )} - {CreateScreenCollapsibleStack( - MainRoutes.Vote, - MainStack, - VoteScreen, - i18n.t('screens.vote.title') - )} - {CreateScreenCollapsibleStack( - MainRoutes.Feedback, - MainStack, - BugReportScreen, - i18n.t('screens.feedback.title') - )} + + + + + + + + + + + + + + + ); } diff --git a/src/navigation/TabNavigator.tsx b/src/navigation/TabNavigator.tsx index 972eb4e..437a380 100644 --- a/src/navigation/TabNavigator.tsx +++ b/src/navigation/TabNavigator.tsx @@ -18,16 +18,12 @@ */ import * as React from 'react'; -import { - createStackNavigator, - TransitionPresets, -} from '@react-navigation/stack'; +import { createStackNavigator } from '@react-navigation/stack'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { Title, useTheme } from 'react-native-paper'; -import { Platform, StyleSheet } from 'react-native'; +import { StyleSheet } from 'react-native'; import i18n from 'i18n-js'; -import { createCollapsibleStack } from 'react-navigation-collapsible'; import { View } from 'react-native-animatable'; import HomeScreen from '../screens/Home/HomeScreen'; import PlanningScreen from '../screens/Planning/PlanningScreen'; @@ -44,23 +40,8 @@ import CustomTabBar from '../components/Tabbar/CustomTabBar'; import WebsitesHomeScreen from '../screens/Services/ServicesScreen'; import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen'; import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen'; -import { - CreateScreenCollapsibleStack, - getWebsiteStack, -} from '../utils/CollapsibleUtils'; 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({ header: { flexDirection: 'row', @@ -79,29 +60,22 @@ const ServicesStack = createStackNavigator(); function ServicesStackComponent() { return ( - - {CreateScreenCollapsibleStack( - 'index', - ServicesStack, - WebsitesHomeScreen, - i18n.t('screens.services.title') - )} - {CreateScreenCollapsibleStack( - 'services-section', - ServicesStack, - ServicesSectionScreen, - 'SECTION' - )} - {CreateScreenCollapsibleStack( - 'amicale-contact', - ServicesStack, - AmicaleContactScreen, - i18n.t('screens.amicaleAbout.title') - )} + + + + ); } @@ -110,23 +84,17 @@ const ProxiwashStack = createStackNavigator(); function ProxiwashStackComponent() { return ( - - {CreateScreenCollapsibleStack( - 'index', - ProxiwashStack, - ProxiwashScreen, - i18n.t('screens.proxiwash.title') - )} - {CreateScreenCollapsibleStack( - 'proxiwash-about', - ProxiwashStack, - ProxiwashAboutScreen, - i18n.t('screens.proxiwash.title') - )} + + + ); } @@ -135,22 +103,17 @@ const PlanningStack = createStackNavigator(); function PlanningStackComponent() { return ( - + - {CreateScreenCollapsibleStack( - 'planning-information', - PlanningStack, - PlanningDisplayScreen, - i18n.t('screens.planning.eventDetails') - )} + ); } @@ -167,73 +130,63 @@ function HomeStackComponent( } const { colors } = useTheme(); return ( - - {createCollapsibleStack( - ( - - - - {i18n.t('screens.home.title')} - - - ), - }} - initialParams={params} - />, - { - collapsedColor: colors.surface, - useNativeDriver: true, - } - )} + ( + + + {headerProps.children} + + ), + }} + initialParams={params} + /> + - - {CreateScreenCollapsibleStack( - 'club-information', - HomeStack, - ClubDisplayScreen, - i18n.t('screens.clubs.details') - )} - {CreateScreenCollapsibleStack( - 'feed-information', - HomeStack, - FeedItemScreen, - i18n.t('screens.home.feed') - )} - {CreateScreenCollapsibleStack( - 'planning-information', - HomeStack, - PlanningDisplayScreen, - i18n.t('screens.planning.eventDetails') - )} + + + ); } @@ -242,23 +195,21 @@ const PlanexStack = createStackNavigator(); function PlanexStackComponent() { return ( - - {getWebsiteStack( - 'index', - PlanexStack, - PlanexScreen, - i18n.t('screens.planex.title') - )} - {CreateScreenCollapsibleStack( - 'group-select', - PlanexStack, - GroupSelectionScreen, - '' - )} + + + ); } @@ -270,6 +221,34 @@ type PropsType = { 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 { defaultRoute: string; createHomeStackComponent: () => any; @@ -287,33 +266,44 @@ export default class TabNavigator extends React.Component { } 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 ( } + tabBar={(tabProps) => ( + + )} > diff --git a/src/screens/Amicale/Clubs/ClubDisplayScreen.tsx b/src/screens/Amicale/Clubs/ClubDisplayScreen.tsx index b59f2f2..fdae27f 100644 --- a/src/screens/Amicale/Clubs/ClubDisplayScreen.tsx +++ b/src/screens/Amicale/Clubs/ClubDisplayScreen.tsx @@ -31,7 +31,9 @@ import i18n from 'i18n-js'; import { StackNavigationProp } from '@react-navigation/stack'; import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen'; 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 { ERROR_TYPE } from '../../../utils/WebData'; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; @@ -174,7 +176,7 @@ class ClubDisplayScreen extends React.Component { return ( diff --git a/src/screens/Amicale/LoginScreen.tsx b/src/screens/Amicale/LoginScreen.tsx index 0394736..5bbb0b0 100644 --- a/src/screens/Amicale/LoginScreen.tsx +++ b/src/screens/Amicale/LoginScreen.tsx @@ -441,7 +441,7 @@ class LoginScreen extends React.Component { enabled keyboardVerticalOffset={100} > - + {this.getMainCard()} { style={styles.button} /> ) : null} - + {this.displayData.message !== undefined ? ( { {state.hasPermission ? this.getScanner() : this.getPermissionScreen()} diff --git a/src/screens/Planex/PlanexScreen.tsx b/src/screens/Planex/PlanexScreen.tsx index 818a16f..8828864 100644 --- a/src/screens/Planex/PlanexScreen.tsx +++ b/src/screens/Planex/PlanexScreen.tsx @@ -54,6 +54,7 @@ type StateType = { dialogTitle: string | React.ReactNode; dialogMessage: string; currentGroup: PlanexGroupType; + injectJS: string; }; 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 */ class PlanexScreen extends React.Component { - webScreenRef: { current: null | WebViewScreen }; - barRef: { current: null | AnimatedBottomBar }; - customInjectedJS: string; - /** * Defines custom injected JavaScript to improve the page display on mobile */ constructor(props: PropsType) { super(props); - this.webScreenRef = React.createRef(); this.barRef = React.createRef(); - this.customInjectedJS = ''; let currentGroupString = AsyncStorageManager.getString( AsyncStorageManager.PREFERENCES.planexCurrentGroup.key ); @@ -184,8 +179,8 @@ class PlanexScreen extends React.Component { dialogTitle: '', dialogMessage: '', currentGroup, + injectJS: '', }; - this.generateInjectedJS(currentGroup.id); } /** @@ -196,20 +191,6 @@ class PlanexScreen extends React.Component { 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. * @@ -218,6 +199,7 @@ class PlanexScreen extends React.Component { getWebView() { const { props, state } = this; const showWebview = state.currentGroup.id !== -1; + console.log(state.injectJS); return ( @@ -230,10 +212,9 @@ class PlanexScreen extends React.Component { /> ) : null} { } else { command = `$('#calendar').fullCalendar('${action}', '${data}')`; } - if (this.webScreenRef.current != null) { - this.webScreenRef.current.injectJavaScript(`${command};true;`); - } // Injected javascript must end with true + // String must resolve to true to prevent crash on iOS + command += ';true;'; + // Change the injected + if (command === this.state.injectJS) { + command += ';true;'; + } + this.setState({ injectJS: command }); }; /** @@ -373,7 +358,6 @@ class PlanexScreen extends React.Component { group ); navigation.setOptions({ title: getPrettierPlanexGroupName(group.name) }); - this.generateInjectedJS(group.id); } /** @@ -382,16 +366,20 @@ class PlanexScreen extends React.Component { * @param groupID The current group selected */ generateInjectedJS(groupID: number) { - this.customInjectedJS = `$(document).ready(function() {${OBSERVE_MUTATIONS_INJECTED}${FULL_CALENDAR_SETTINGS}displayAde(${groupID});${ - // Reset Ade - DateManager.isWeekend(new Date()) ? 'calendar.next()' : '' - }${INJECT_STYLE}`; - + let customInjectedJS = `$(document).ready(function() { + ${OBSERVE_MUTATIONS_INJECTED} + ${FULL_CALENDAR_SETTINGS} + displayAde(${groupID}); + ${INJECT_STYLE}`; + if (DateManager.isWeekend(new Date())) { + customInjectedJS += `calendar.next();`; + } if (ThemeManager.getNightMode()) { - this.customInjectedJS += `$('head').append('');`; + customInjectedJS += `$('head').append('');`; } - this.customInjectedJS += 'removeAlpha();});true;'; // Prevents crash on ios + customInjectedJS += 'removeAlpha();});true;'; // Prevents crash on ios + return customInjectedJS; } render() { diff --git a/src/screens/Planning/PlanningDisplayScreen.tsx b/src/screens/Planning/PlanningDisplayScreen.tsx index 7dc8f11..5acc106 100644 --- a/src/screens/Planning/PlanningDisplayScreen.tsx +++ b/src/screens/Planning/PlanningDisplayScreen.tsx @@ -28,7 +28,9 @@ import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen'; import { apiRequest, ERROR_TYPE } from '../../utils/WebData'; import ErrorView from '../../components/Screens/ErrorView'; 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 type { PlanningEventType } from '../../utils/Planning'; import ImageGalleryButton from '../../components/Media/ImageGalleryButton'; @@ -145,9 +147,7 @@ class PlanningDisplayScreen extends React.Component { ) : null} {displayData.description !== null ? ( - + ) : ( diff --git a/src/screens/Services/Proximo/ProximoAboutScreen.tsx b/src/screens/Services/Proximo/ProximoAboutScreen.tsx index 3df631a..030ea29 100644 --- a/src/screens/Services/Proximo/ProximoAboutScreen.tsx +++ b/src/screens/Services/Proximo/ProximoAboutScreen.tsx @@ -21,7 +21,9 @@ import * as React from 'react'; import { Image, StyleSheet, View } from 'react-native'; import i18n from 'i18n-js'; 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'; const LOGO = 'https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png'; @@ -72,7 +74,7 @@ export default function ProximoAboutScreen() { ; route: { params: { host: string; path: string | null; title: string } }; }; +type State = { + url: string; +}; + const ENABLE_MOBILE_STRING = ''; @@ -43,18 +47,18 @@ const BIB_BACK_BUTTON = '' + ''; -class WebsiteScreen extends React.Component { - fullUrl: string; - +class WebsiteScreen extends React.Component { injectedJS: { [key: string]: string }; - customPaddingFunctions: { [key: string]: (padding: string) => string }; + customPaddingFunctions: { [key: string]: (padding: number) => string }; host: string; - constructor(props: PropsType) { + constructor(props: Props) { super(props); - this.fullUrl = ''; + this.state = { + url: '', + }; this.host = ''; props.navigation.addListener('focus', this.onScreenFocus); this.injectedJS = {}; @@ -70,7 +74,7 @@ class WebsiteScreen extends React.Component { `$(".hero-unit-form").append("${BIB_BACK_BUTTON}");true;`; this.customPaddingFunctions[AvailableWebsites.websites.BLUEMIND] = ( - padding: string + padding: number ): string => { return ( `$('head').append('${ENABLE_MOBILE_STRING}');` + @@ -79,7 +83,7 @@ class WebsiteScreen extends React.Component { ); }; this.customPaddingFunctions[AvailableWebsites.websites.WIKETUD] = ( - padding: string + padding: number ): string => { return ( `$('#p-logo-text').css('top', 10 + ${padding});` + @@ -99,16 +103,20 @@ class WebsiteScreen extends React.Component { */ handleNavigationParams() { const { route, navigation } = this.props; + if (route.params != null) { + console.log(route.params); this.host = route.params.host; let { path } = route.params; const { title } = route.params; + let fullUrl = ''; if (this.host != null && path != null) { path = path.replace(this.host, ''); - this.fullUrl = this.host + path; + fullUrl = this.host + path; } else { - this.fullUrl = this.host; + fullUrl = this.host; } + this.setState({ url: fullUrl }); if (title != null) { navigation.setOptions({ title }); @@ -117,7 +125,6 @@ class WebsiteScreen extends React.Component { } render() { - const { navigation } = this.props; let injectedJavascript = ''; let customPadding = null; if (this.host != null && this.injectedJS[this.host] != null) { @@ -127,12 +134,11 @@ class WebsiteScreen extends React.Component { customPadding = this.customPaddingFunctions[this.host]; } - if (this.fullUrl != null) { + if (this.state.url) { return ( ); diff --git a/src/screens/Test.tsx b/src/screens/Test.tsx new file mode 100644 index 0000000..ad99763 --- /dev/null +++ b/src/screens/Test.tsx @@ -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 ( +// +// TEST +// +// ); +// }; + +// return ( +// +// ); +// } + +type Props = { + navigation: StackNavigationProp; + 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 { + createDataset = (): Array<{ + title: string; + data: [] | Array; + 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 }) => ( + + ); + + render() { + const renderItem = () => { + return ( + + TEST + + ); + }; + + const props = this.props; + // return ( + // + // ); + // return ( + // + // ); + return ( + + ); + } +} + +export default Test; diff --git a/src/utils/CollapsibleContext.ts b/src/utils/CollapsibleContext.ts new file mode 100644 index 0000000..8bd13e4 --- /dev/null +++ b/src/utils/CollapsibleContext.ts @@ -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({ + collapsible: undefined, + setCollapsible: () => undefined, +}); + +export function useCollapsible() { + return useContext(CollapsibleContext); +} diff --git a/src/utils/CollapsibleUtils.tsx b/src/utils/CollapsibleUtils.tsx deleted file mode 100644 index 11a7ed1..0000000 --- a/src/utils/CollapsibleUtils.tsx +++ /dev/null @@ -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 . - */ - -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, - StackNavigationState, - 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, - component: React.ComponentType, - title: string, - useNativeDriver: boolean = true, - options: StackNavigationOptions = {}, - headerColor?: string -) { - const { colors } = useTheme(); - return createCollapsibleStack( - , - { - 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, - component: React.ComponentType, - title: string -) { - return CreateScreenCollapsibleStack(name, Stack, component, title, false); -} diff --git a/src/utils/withCollapsible.tsx b/src/utils/withCollapsible.tsx index 6cfb2d1..52d981a 100644 --- a/src/utils/withCollapsible.tsx +++ b/src/utils/withCollapsible.tsx @@ -18,29 +18,28 @@ */ import * as React from 'react'; -import { useCollapsibleStack } from 'react-navigation-collapsible'; +import { useTheme } from 'react-native-paper'; +import { + useCollapsibleHeader, + UseCollapsibleOptions, +} from 'react-navigation-collapsible'; -/** - * Function used to manipulate Collapsible Hooks from a class. - * - * Usage : - * - * export withCollapsible(Component) - * - * replacing Component with the one you want to use. - * This component will then receive the collapsibleStack prop. - * - * @param Component The component to use Collapsible with - * @returns {React.ComponentType} - */ -export default function withCollapsible(Component: React.ComponentType) { - return React.forwardRef((props: any, ref: any) => { - return ( - - ); - }); +export default function withCollapsible( + Component: React.ComponentType, + options?: UseCollapsibleOptions +) { + return function WrappedComponent(props: T) { + const theme = useTheme(); + if (!options?.config?.collapsedColor) { + options = { + ...options, + config: { + ...options?.config, + collapsedColor: theme.colors.surface, + }, + }; + } + const collapsible = useCollapsibleHeader(options); + return ; + }; }