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 { 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 (
<PaperProvider theme={state.currentTheme}>
<OverflowMenuProvider>
<View
style={{
backgroundColor: ThemeManager.getCurrentTheme().colors.background,
...GENERAL_STYLES.flex,
}}
>
<SafeAreaView style={GENERAL_STYLES.flex}>
<NavigationContainer
theme={state.currentTheme}
ref={this.navigatorRef}
>
<MainNavigator
defaultHomeRoute={this.defaultHomeRoute}
defaultHomeData={this.defaultHomeData}
/>
</NavigationContainer>
</SafeAreaView>
</View>
</OverflowMenuProvider>
<CollapsibleProvider>
<OverflowMenuProvider>
<View
style={{
backgroundColor: ThemeManager.getCurrentTheme().colors
.background,
...GENERAL_STYLES.flex,
}}
>
<SafeAreaView style={GENERAL_STYLES.flex}>
<NavigationContainer
theme={state.currentTheme}
ref={this.navigatorRef}
>
<MainNavigator
defaultHomeRoute={this.defaultHomeRoute}
defaultHomeData={this.defaultHomeData}
/>
</NavigationContainer>
</SafeAreaView>
</View>
</OverflowMenuProvider>
</CollapsibleProvider>
</PaperProvider>
);
}

46
package-lock.json generated
View file

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

View file

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

View file

@ -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<PropsType, StateType> {
useNativeDriver
style={{
...styles.container,
bottom: 10 + CustomTabBar.TAB_BAR_HEIGHT,
bottom: 10 + TAB_BAR_HEIGHT,
}}
>
<Surface style={styles.surface}>

View file

@ -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<PropsType> {
useNativeDriver={true}
style={{
...styles.fab,
bottom: CustomTabBar.TAB_BAR_HEIGHT,
bottom: TAB_BAR_HEIGHT,
}}
>
<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/>.
*/
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<NativeScrollEvent>) => void;
paddedProps?: (paddingTop: number) => Record<string, any>;
headerColors: string;
};
type PropsType = CollapsibleComponentPropsType & {
type Props = CollapsibleComponentPropsType & {
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>) => {
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 (
<Comp
{...props}
{...pprops}
onScroll={onScrollWithListener(onScroll)}
contentContainerStyle={{
paddingTop: containerPaddingTop,

View file

@ -21,7 +21,7 @@ import * as React from 'react';
import { useTheme } from 'react-native-paper';
import { Modalize } from 'react-native-modalize';
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
@ -45,7 +45,7 @@ function CustomModal(props: {
>
<View
style={{
paddingBottom: CustomTabBar.TAB_BAR_HEIGHT,
paddingBottom: TAB_BAR_HEIGHT,
}}
>
{children}

View file

@ -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<ItemT> = Array<{
title: string;
@ -260,19 +260,20 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
dataset = props.createDataset(state.fetchedData, state.refreshing);
}
const { containerPaddingTop } = props.collapsibleStack;
return (
<View>
<View style={GENERAL_STYLES.flex}>
<CollapsibleSectionList
sections={dataset}
extraData={props.updateData}
refreshControl={
<RefreshControl
progressViewOffset={containerPaddingTop}
refreshing={state.refreshing}
onRefresh={this.onRefresh}
/>
}
paddedProps={(paddingTop) => ({
refreshControl: (
<RefreshControl
progressViewOffset={paddingTop}
refreshing={state.refreshing}
onRefresh={this.onRefresh}
/>
),
})}
renderSectionHeader={this.getRenderSectionHeader}
renderItem={this.getRenderItem}
stickySectionHeadersEnabled={props.stickyHeader}
@ -299,7 +300,7 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
: undefined
}
onScroll={this.onScroll}
hasTab
hasTab={true}
/>
<Snackbar
visible={state.snackbarVisible}
@ -310,7 +311,7 @@ class WebSectionList<ItemT, RawData> 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<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/>.
*/
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<any>;
theme: ReactNativePaper.Theme;
type Props = {
url: string;
collapsibleStack: Collapsible;
onMessage: (event: { nativeEvent: { data: string } }) => void;
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
customJS?: string;
onMessage?: (event: { nativeEvent: { data: string } }) => void;
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => 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<PropsType> {
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<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;
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 (
<MaterialHeaderButtons>
<Item
title="refresh"
iconName="refresh"
onPress={this.onRefreshClicked}
title={'refresh'}
iconName={'refresh'}
onPress={onRefreshClicked}
/>
<Item
title={i18n.t('general.openInBrowser')}
iconName="open-in-new"
onPress={this.onOpenClicked}
iconName={'open-in-new'}
onPress={onOpenClicked}
/>
</MaterialHeaderButtons>
);
};
/**
* 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 (
<MaterialHeaderButtons>
<Item
title="refresh"
iconName="refresh"
onPress={this.onRefreshClicked}
/>
<Item title="refresh" iconName="refresh" onPress={onRefreshClicked} />
<OverflowMenu
style={styles.overflow}
OverflowIcon={
<MaterialCommunityIcons
name="dots-vertical"
size={26}
color={props.theme.colors.text}
color={theme.colors.text}
/>
}
>
<HiddenItem
title={i18n.t('general.goBack')}
onPress={this.onGoBackClicked}
onPress={onGoBackClicked}
/>
<HiddenItem
title={i18n.t('general.goForward')}
onPress={this.onGoForwardClicked}
onPress={onGoForwardClicked}
/>
<Divider />
<HiddenItem
title={i18n.t('general.openInBrowser')}
onPress={this.onOpenClicked}
onPress={onOpenClicked}
/>
</OverflowMenu>
</MaterialHeaderButtons>
);
};
/**
* Gets the loading indicator
*
* @return {*}
*/
getRenderLoading = () => <BasicLoadingScreen isAbsolute />;
const getRenderLoading = () => <BasicLoadingScreen isAbsolute={true} />;
/**
* 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
* @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<NativeScrollEvent>) => {
const { onScroll } = this.props;
if (onScroll) {
onScroll(event);
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
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 (
<AnimatedWebView
ref={this.webviewRef}
source={{ uri: props.url }}
startInLoadingState
injectedJavaScript={props.customJS}
javaScriptEnabled
renderLoading={this.getRenderLoading}
renderError={() => (
<ErrorView
errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={this.onRefreshClicked}
/>
)}
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 (
<AnimatedWebView
ref={webviewRef}
source={{ uri: props.url }}
startInLoadingState={true}
injectedJavaScript={props.initialJS}
javaScriptEnabled={true}
renderLoading={getRenderLoading}
renderError={() => (
<ErrorView
errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={onRefreshClicked}
/>
)}
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;

View file

@ -17,211 +17,88 @@
* 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 { 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<string> & {
state?: NavigationState | PartialState<NavigationState>;
};
export const TAB_BAR_HEIGHT = 50;
interface PropsType extends BottomTabBarProps {
theme: ReactNativePaper.Theme;
function CustomTabBar(
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({
container: {
bar: {
flexDirection: 'row',
width: '100%',
height: 50,
position: 'absolute',
bottom: 0,
left: 0,
},
});
class CustomTabBar extends React.Component<PropsType, StateType> {
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 (
<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>
);
}
function areEqual(
prevProps: BottomTabBarProps<any>,
nextProps: BottomTabBarProps<any>
) {
return prevProps.state.index === nextProps.state.index;
}
export default withTheme(CustomTabBar);
export default React.memo(CustomTabBar, areEqual);

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<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,
},
},
});
}
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 (
<Image
source={focused ? FOCUSED_ICON : UNFOCUSED_ICON}
style={{
width: size,
height: size,
tintColor: color,
}}
/>
<Animatable.View useNativeDriver={true} animation={'rubberBand'}>
<Image
source={props.focused ? FOCUSED_ICON : UNFOCUSED_ICON}
style={{
width: iconProps.size,
height: iconProps.size,
tintColor: iconProps.color,
}}
/>
</Animatable.View>
);
};
render() {
const { props } = this;
return (
<View style={styles.container}>
<View
style={{
height: props.tabBarHeight + 30,
...styles.subcontainer,
}}
return (
<View style={styles.outer}>
<View style={styles.inner}>
<Animatable.View
style={styles.fab}
useNativeDriver={true}
duration={props.focused ? 500 : 200}
animation={props.focused ? 'fabFocusIn' : 'fabFocusOut'}
easing={'ease-out'}
>
<AnimatedFAB
duration={200}
easing="ease-out"
animation={props.focused ? 'fabFocusIn' : 'fabFocusOut'}
icon={this.getIconRender}
<FAB
onPress={props.onPress}
onLongPress={props.onLongPress}
style={styles.fab}
animated={false}
icon={getImage}
color={'#fff'}
/>
</View>
</Animatable.View>
</View>
);
}
</View>
);
}
export default TabHomeIcon;

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<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;
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 (
<TouchableRipple
<TabHomeIcon
icon={props.icon}
focusedIcon={props.focusedIcon}
focused={props.focused}
onPress={props.onPress}
onLongPress={props.onLongPress}
rippleColor={props.theme.colors.primary}
borderless={true}
style={styles.container}
>
<View>
<Animatable.View
duration={200}
easing="ease-out"
animation={props.focused ? 'focusIn' : 'focusOut'}
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>
/>
);
} else {
return (
<TabSideIcon
focused={props.focused}
label={props.label}
icon={props.icon}
focusedIcon={props.focusedIcon}
onPress={props.onPress}
/>
);
}
}
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 {
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<string, object | undefined>;
const modalTransition =
Platform.OS === 'ios'
? TransitionPresets.ModalPresentationIOS
: TransitionPresets.ModalTransition;
const defaultScreenOptions = {
gestureEnabled: true,
cardOverlayEnabled: true,
...TransitionPresets.SlideFromRightIOS,
};
const MainStack = createStackNavigator<MainStackParamsList>();
function MainStackComponent(props: { createTabNavigator: () => JSX.Element }) {
function MainStackComponent(props: {
createTabNavigator: () => React.ReactElement;
}) {
const { createTabNavigator } = props;
return (
<MainStack.Navigator
initialRouteName={MainRoutes.Main}
headerMode="screen"
screenOptions={defaultScreenOptions}
>
<MainStack.Navigator initialRouteName={MainRoutes.Main} headerMode="screen">
<MainStack.Screen name={'test'} component={Test} />
<MainStack.Screen
name={MainRoutes.Main}
component={createTabNavigator}
@ -132,49 +113,53 @@ function MainStackComponent(props: { createTabNavigator: () => 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'
)}
<MainStack.Screen
name={MainRoutes.Settings}
component={SettingsScreen}
options={{
title: i18n.t('screens.settings.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.DashboardEdit}
component={DashboardEditScreen}
options={{
title: i18n.t('screens.settings.dashboardEdit.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.About}
component={AboutScreen}
options={{
title: i18n.t('screens.about.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.Dependencies}
component={AboutDependenciesScreen}
options={{
title: i18n.t('screens.about.libs'),
}}
/>
<MainStack.Screen
name={MainRoutes.Debug}
component={DebugScreen}
options={{
title: i18n.t('screens.about.debug'),
}}
/>
<MainStack.Screen
name={MainRoutes.GameStart}
component={GameStartScreen}
options={{
title: i18n.t('screens.game.title'),
headerStyle: {
backgroundColor: 'transparent',
},
}}
/>
<MainStack.Screen
name={MainRoutes.GameMain}
component={GameMainScreen}
@ -182,102 +167,114 @@ function MainStackComponent(props: { createTabNavigator: () => 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')
)}
<MainStack.Screen
name={MainRoutes.Login}
component={LoginScreen}
options={{
title: i18n.t('screens.login.title'),
headerStyle: {
backgroundColor: 'transparent',
},
}}
/>
<MainStack.Screen
name={'website'}
component={WebsiteScreen}
options={{
title: '',
}}
/>
<MainStack.Screen
name={MainRoutes.SelfMenu}
component={SelfMenuScreen}
options={{
title: i18n.t('screens.menu.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.Proximo}
component={ProximoMainScreen}
options={{
title: i18n.t('screens.proximo.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.ProximoList}
component={ProximoListScreen}
options={{
title: i18n.t('screens.proximo.articleList'),
}}
/>
<MainStack.Screen
name={MainRoutes.ProximoAbout}
component={ProximoAboutScreen}
options={{
title: i18n.t('screens.proximo.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.Profile}
component={ProfileScreen}
options={{
title: i18n.t('screens.profile.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.ClubList}
component={ClubListScreen}
options={{
title: i18n.t('screens.clubs.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.ClubInformation}
component={ClubDisplayScreen}
options={{
title: i18n.t('screens.clubs.details'),
}}
/>
<MainStack.Screen
name={MainRoutes.ClubAbout}
component={ClubAboutScreen}
options={{
title: i18n.t('screens.clubs.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.EquipmentList}
component={EquipmentScreen}
options={{
title: i18n.t('screens.equipment.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.EquipmentRent}
component={EquipmentLendScreen}
options={{
title: i18n.t('screens.equipment.book'),
}}
/>
<MainStack.Screen
name={MainRoutes.EquipmentConfirm}
component={EquipmentConfirmScreen}
options={{
title: i18n.t('screens.equipment.confirm'),
}}
/>
<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>
);
}

View file

@ -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 (
<ServicesStack.Navigator
initialRouteName="index"
headerMode="screen"
screenOptions={defaultScreenOptions}
>
{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')
)}
<ServicesStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
<ServicesStack.Screen
name={'index'}
component={WebsitesHomeScreen}
options={{ title: i18n.t('screens.services.title') }}
/>
<ServicesStack.Screen
name={'services-section'}
component={ServicesSectionScreen}
options={{ title: 'SECTION' }}
/>
<ServicesStack.Screen
name={'amicale-contact'}
component={AmicaleContactScreen}
options={{ title: i18n.t('screens.amicaleAbout.title') }}
/>
</ServicesStack.Navigator>
);
}
@ -110,23 +84,17 @@ const ProxiwashStack = createStackNavigator();
function ProxiwashStackComponent() {
return (
<ProxiwashStack.Navigator
initialRouteName="index"
headerMode="screen"
screenOptions={defaultScreenOptions}
>
{CreateScreenCollapsibleStack(
'index',
ProxiwashStack,
ProxiwashScreen,
i18n.t('screens.proxiwash.title')
)}
{CreateScreenCollapsibleStack(
'proxiwash-about',
ProxiwashStack,
ProxiwashAboutScreen,
i18n.t('screens.proxiwash.title')
)}
<ProxiwashStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
<ProxiwashStack.Screen
name={'index-contact'}
component={ProxiwashScreen}
options={{ title: i18n.t('screens.proxiwash.title') }}
/>
<ProxiwashStack.Screen
name={'proxiwash-about'}
component={ProxiwashAboutScreen}
options={{ title: i18n.t('screens.proxiwash.title') }}
/>
</ProxiwashStack.Navigator>
);
}
@ -135,22 +103,17 @@ const PlanningStack = createStackNavigator();
function PlanningStackComponent() {
return (
<PlanningStack.Navigator
initialRouteName="index"
headerMode="screen"
screenOptions={defaultScreenOptions}
>
<PlanningStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
<PlanningStack.Screen
name="index"
name={'index'}
component={PlanningScreen}
options={{ title: i18n.t('screens.planning.title') }}
/>
{CreateScreenCollapsibleStack(
'planning-information',
PlanningStack,
PlanningDisplayScreen,
i18n.t('screens.planning.eventDetails')
)}
<PlanningStack.Screen
name={'planning-information'}
component={PlanningDisplayScreen}
options={{ title: i18n.t('screens.planning.eventDetails') }}
/>
</PlanningStack.Navigator>
);
}
@ -167,73 +130,63 @@ function HomeStackComponent(
}
const { colors } = useTheme();
return (
<HomeStack.Navigator
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.Navigator initialRouteName={'index'} headerMode={'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}
options={{ title: i18n.t('screens.scanner.title') }}
/>
{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')
)}
<HomeStack.Screen
name={'club-information'}
component={ClubDisplayScreen}
options={{
title: i18n.t('screens.clubs.details'),
}}
/>
<HomeStack.Screen
name={'feed-information'}
component={FeedItemScreen}
options={{
title: i18n.t('screens.home.feed'),
}}
/>
<HomeStack.Screen
name={'planning-information'}
component={PlanningDisplayScreen}
options={{
title: i18n.t('screens.planning.eventDetails'),
}}
/>
</HomeStack.Navigator>
);
}
@ -242,23 +195,21 @@ const PlanexStack = createStackNavigator();
function PlanexStackComponent() {
return (
<PlanexStack.Navigator
initialRouteName="index"
headerMode="screen"
screenOptions={defaultScreenOptions}
>
{getWebsiteStack(
'index',
PlanexStack,
PlanexScreen,
i18n.t('screens.planex.title')
)}
{CreateScreenCollapsibleStack(
'group-select',
PlanexStack,
GroupSelectionScreen,
''
)}
<PlanexStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
<PlanexStack.Screen
name={'index'}
component={PlanexScreen}
options={{
title: i18n.t('screens.planex.title'),
}}
/>
<PlanexStack.Screen
name={'group-select'}
component={GroupSelectionScreen}
options={{
title: '',
}}
/>
</PlanexStack.Navigator>
);
}
@ -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<PropsType> {
defaultRoute: string;
createHomeStackComponent: () => any;
@ -287,33 +266,44 @@ export default class TabNavigator extends React.Component<PropsType> {
}
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 (
<Tab.Navigator
initialRouteName={this.defaultRoute}
tabBar={(tabProps) => <CustomTabBar {...tabProps} />}
tabBar={(tabProps) => (
<CustomTabBar {...tabProps} labels={LABELS} icons={ICONS} />
)}
>
<Tab.Screen
name="services"
name={'services'}
component={ServicesStackComponent}
options={{ title: i18n.t('screens.services.title') }}
/>
<Tab.Screen
name="proxiwash"
name={'proxiwash'}
component={ProxiwashStackComponent}
options={{ title: i18n.t('screens.proxiwash.title') }}
/>
<Tab.Screen
name="home"
name={'home'}
component={this.createHomeStackComponent}
options={{ title: i18n.t('screens.home.title') }}
/>
<Tab.Screen
name="planning"
name={'planning'}
component={PlanningStackComponent}
options={{ title: i18n.t('screens.planning.title') }}
/>
<Tab.Screen
name="planex"
name={'planex'}
component={PlanexStackComponent}
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 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<PropsType> {
return (
<Card
style={{
marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20,
marginBottom: TAB_BAR_HEIGHT + 20,
...styles.card,
}}
>

View file

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

View file

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

View file

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

View file

@ -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<PropsType, StateType> {
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<PropsType, StateType> {
dialogTitle: '',
dialogMessage: '',
currentGroup,
injectJS: '',
};
this.generateInjectedJS(currentGroup.id);
}
/**
@ -196,20 +191,6 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
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<PropsType, StateType> {
getWebView() {
const { props, state } = this;
const showWebview = state.currentGroup.id !== -1;
console.log(state.injectJS);
return (
<View style={GENERAL_STYLES.flex}>
@ -230,10 +212,9 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
/>
) : null}
<WebViewScreen
ref={this.webScreenRef}
navigation={props.navigation}
url={PLANEX_URL}
customJS={this.customInjectedJS}
initialJS={this.generateInjectedJS(this.state.currentGroup.id)}
injectJS={this.state.injectJS}
onMessage={this.onMessage}
onScroll={this.onScroll}
showAdvancedControls={false}
@ -269,9 +250,13 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
} 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<PropsType, StateType> {
group
);
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
*/
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('<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() {

View file

@ -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<PropsType, StateType> {
) : null}
{displayData.description !== null ? (
<Card.Content
style={{ paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20 }}
>
<Card.Content style={{ paddingBottom: TAB_BAR_HEIGHT + 20 }}>
<CustomHTML html={displayData.description} />
</Card.Content>
) : (

View file

@ -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() {
<Card
style={{
...styles.card,
marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20,
marginBottom: TAB_BAR_HEIGHT + 20,
}}
>
<Card.Title

View file

@ -23,11 +23,15 @@ import WebViewScreen from '../../components/Screens/WebViewScreen';
import AvailableWebsites from '../../constants/AvailableWebsites';
import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
type PropsType = {
type Props = {
navigation: StackNavigationProp<any>;
route: { params: { host: string; path: string | null; title: string } };
};
type State = {
url: string;
};
const ENABLE_MOBILE_STRING =
'<meta name="viewport" content="width=device-width, initial-scale=1.0">';
@ -43,18 +47,18 @@ const BIB_BACK_BUTTON =
'</a>' +
'</div>';
class WebsiteScreen extends React.Component<PropsType> {
fullUrl: string;
class WebsiteScreen extends React.Component<Props, State> {
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<PropsType> {
`$(".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<PropsType> {
);
};
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<PropsType> {
*/
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<PropsType> {
}
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<PropsType> {
customPadding = this.customPaddingFunctions[this.host];
}
if (this.fullUrl != null) {
if (this.state.url) {
return (
<WebViewScreen
navigation={navigation}
url={this.fullUrl}
customJS={injectedJavascript}
url={this.state.url}
initialJS={injectedJavascript}
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 { 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<any>}
*/
export default function withCollapsible(Component: React.ComponentType<any>) {
return React.forwardRef((props: any, ref: any) => {
return (
<Component
collapsibleStack={useCollapsibleStack()}
ref={ref}
{...props}
/>
);
});
export default function withCollapsible<T>(
Component: React.ComponentType<any>,
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 <Component {...props} collapsible={collapsible} />;
};
}