Compare commits

..

10 commits

32 changed files with 892 additions and 691 deletions

130
App.js
View file

@ -1,11 +1,12 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Platform, StatusBar, YellowBox} from 'react-native'; import {Platform, StatusBar, View, YellowBox} from 'react-native';
import LocaleManager from './src/managers/LocaleManager'; import LocaleManager from './src/managers/LocaleManager';
import AsyncStorageManager from "./src/managers/AsyncStorageManager"; import AsyncStorageManager from "./src/managers/AsyncStorageManager";
import CustomIntroSlider from "./src/components/Overrides/CustomIntroSlider"; import CustomIntroSlider from "./src/components/Overrides/CustomIntroSlider";
import {SplashScreen} from 'expo'; import {SplashScreen} from 'expo';
import type {CustomTheme} from "./src/managers/ThemeManager";
import ThemeManager from './src/managers/ThemeManager'; import ThemeManager from './src/managers/ThemeManager';
import {NavigationContainer} from '@react-navigation/native'; import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack'; import {createStackNavigator} from '@react-navigation/stack';
@ -17,6 +18,7 @@ import Update from "./src/constants/Update";
import ConnectionManager from "./src/managers/ConnectionManager"; import ConnectionManager from "./src/managers/ConnectionManager";
import URLHandler from "./src/utils/URLHandler"; import URLHandler from "./src/utils/URLHandler";
import {setSafeBounceHeight} from "react-navigation-collapsible"; import {setSafeBounceHeight} from "react-navigation-collapsible";
import * as Animatable from 'react-native-animatable';
YellowBox.ignoreWarnings([ // collapsible headers cause this warning, just ignore as it is not an issue YellowBox.ignoreWarnings([ // collapsible headers cause this warning, just ignore as it is not an issue
'Non-serializable values were found in the navigation state', 'Non-serializable values were found in the navigation state',
@ -29,7 +31,7 @@ type State = {
showIntro: boolean, showIntro: boolean,
showUpdate: boolean, showUpdate: boolean,
showAprilFools: boolean, showAprilFools: boolean,
currentTheme: ?Object, currentTheme: CustomTheme | null,
}; };
const Stack = createStackNavigator(); const Stack = createStackNavigator();
@ -44,42 +46,58 @@ export default class App extends React.Component<Props, State> {
currentTheme: null, currentTheme: null,
}; };
navigatorRef: Object; navigatorRef: { current: null | NavigationContainer };
defaultRoute: string | null; defaultHomeRoute: string | null;
defaultData: Object; defaultHomeData: { [key: string]: any }
createDrawerNavigator: Function; createDrawerNavigator: () => React.Node;
urlHandler: URLHandler; urlHandler: URLHandler;
storageManager: AsyncStorageManager;
constructor() { constructor() {
super(); super();
LocaleManager.initTranslations(); LocaleManager.initTranslations();
SplashScreen.preventAutoHide(); SplashScreen.preventAutoHide();
this.navigatorRef = React.createRef(); this.navigatorRef = React.createRef();
this.defaultRoute = null; this.defaultHomeRoute = null;
this.defaultData = {}; this.defaultHomeData = {};
this.storageManager = AsyncStorageManager.getInstance();
this.urlHandler = new URLHandler(this.onInitialURLParsed, this.onDetectURL); this.urlHandler = new URLHandler(this.onInitialURLParsed, this.onDetectURL);
this.urlHandler.listen(); this.urlHandler.listen();
setSafeBounceHeight(Platform.OS === 'ios' ? 100 : 20); setSafeBounceHeight(Platform.OS === 'ios' ? 100 : 20);
} }
onInitialURLParsed = ({route, data}: Object) => { /**
this.defaultRoute = route; * THe app has been started by an url, and it has been parsed.
this.defaultData = data; * Set a new default start route based on the data parsed.
}; *
* @param parsedData The data parsed from the url
onDetectURL = ({route, data}: Object) => { */
// Navigate to nested navigator and pass data to the index screen onInitialURLParsed = (parsedData: { route: string, data: { [key: string]: any } }) => {
this.navigatorRef.current.navigate('home', { this.defaultHomeRoute = parsedData.route;
screen: 'index', this.defaultHomeData = parsedData.data;
params: {nextScreen: route, data: data}
});
}; };
/** /**
* Updates the theme * An url has been opened and parsed while the app was active.
* Redirect the user to the screen according to parsed data.
*
* @param parsedData The data parsed from the url
*/
onDetectURL = (parsedData: { route: string, data: { [key: string]: any } }) => {
// Navigate to nested navigator and pass data to the index screen
if (this.navigatorRef.current != null) {
this.navigatorRef.current.navigate('home', {
screen: 'index',
params: {nextScreen: parsedData.route, data: parsedData.data}
});
}
};
/**
* Updates the current theme
*/ */
onUpdateTheme = () => { onUpdateTheme = () => {
this.setState({ this.setState({
@ -88,6 +106,10 @@ export default class App extends React.Component<Props, State> {
this.setupStatusBar(); this.setupStatusBar();
}; };
/**
* Updates status bar content color if on iOS only,
* as the android status bar is always set to black.
*/
setupStatusBar() { setupStatusBar() {
if (Platform.OS === 'ios') { if (Platform.OS === 'ios') {
if (ThemeManager.getNightMode()) { if (ThemeManager.getNightMode()) {
@ -96,12 +118,10 @@ export default class App extends React.Component<Props, State> {
StatusBar.setBarStyle('dark-content', true); StatusBar.setBarStyle('dark-content', true);
} }
} }
// StatusBar.setTranslucent(false);
// StatusBar.setBackgroundColor(ThemeManager.getCurrentTheme().colors.surface);
} }
/** /**
* Callback when user ends the intro. Save in preferences to avaoid showing back the introSlides * Callback when user ends the intro. Save in preferences to avoid showing back the introSlides
*/ */
onIntroDone = () => { onIntroDone = () => {
this.setState({ this.setState({
@ -109,46 +129,52 @@ export default class App extends React.Component<Props, State> {
showUpdate: false, showUpdate: false,
showAprilFools: false, showAprilFools: false,
}); });
AsyncStorageManager.getInstance().savePref(AsyncStorageManager.getInstance().preferences.showIntro.key, '0'); this.storageManager.savePref(this.storageManager.preferences.showIntro.key, '0');
AsyncStorageManager.getInstance().savePref(AsyncStorageManager.getInstance().preferences.updateNumber.key, Update.number.toString()); this.storageManager.savePref(this.storageManager.preferences.updateNumber.key, Update.number.toString());
AsyncStorageManager.getInstance().savePref(AsyncStorageManager.getInstance().preferences.showAprilFoolsStart.key, '0'); this.storageManager.savePref(this.storageManager.preferences.showAprilFoolsStart.key, '0');
}; };
async componentDidMount() { componentDidMount() {
await this.loadAssetsAsync(); this.loadAssetsAsync().then(() => {
this.onLoadFinished();
});
} }
/**
* Loads every async data
*
* @returns {Promise<void>}
*/
async loadAssetsAsync() { async loadAssetsAsync() {
// Wait for custom fonts to be loaded before showing the app await this.storageManager.loadPreferences();
await AsyncStorageManager.getInstance().loadPreferences();
ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme);
await initExpoToken(); await initExpoToken();
try { try {
await ConnectionManager.getInstance().recoverLogin(); await ConnectionManager.getInstance().recoverLogin();
} catch (e) { } catch (e) {
} }
this.createDrawerNavigator = () => <DrawerNavigator defaultRoute={this.defaultRoute}
defaultData={this.defaultData}/>;
this.onLoadFinished();
} }
/**
* Async loading is done, finish processing startup data
*/
onLoadFinished() { onLoadFinished() {
// console.log("finished");
// Only show intro if this is the first time starting the app // Only show intro if this is the first time starting the app
this.setState({ this.createDrawerNavigator = () => <DrawerNavigator
isLoading: false, defaultHomeRoute={this.defaultHomeRoute}
currentTheme: ThemeManager.getCurrentTheme(), defaultHomeData={this.defaultHomeData}/>;
showIntro: AsyncStorageManager.getInstance().preferences.showIntro.current === '1', ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme);
showUpdate: AsyncStorageManager.getInstance().preferences.updateNumber.current !== Update.number.toString(),
showAprilFools: AprilFoolsManager.getInstance().isAprilFoolsEnabled() && AsyncStorageManager.getInstance().preferences.showAprilFoolsStart.current === '1',
});
// Status bar goes dark if set too fast on ios // Status bar goes dark if set too fast on ios
if (Platform.OS === 'ios') if (Platform.OS === 'ios')
setTimeout(this.setupStatusBar, 1000); setTimeout(this.setupStatusBar, 1000);
else else
this.setupStatusBar(); this.setupStatusBar();
SplashScreen.hide(); this.setState({
isLoading: false,
currentTheme: ThemeManager.getCurrentTheme(),
showIntro: this.storageManager.preferences.showIntro.current === '1',
showUpdate: this.storageManager.preferences.updateNumber.current !== Update.number.toString(),
showAprilFools: AprilFoolsManager.getInstance().isAprilFoolsEnabled() && this.storageManager.preferences.showAprilFoolsStart.current === '1',
});
} }
/** /**
@ -166,11 +192,27 @@ export default class App extends React.Component<Props, State> {
} else { } else {
return ( return (
<PaperProvider theme={this.state.currentTheme}> <PaperProvider theme={this.state.currentTheme}>
<View style={{
flex: 1,
backgroundColor: ThemeManager.getCurrentTheme().colors.background
}}>
<Animatable.View
style={{flex: 1,}}
animation={"fadeIn"}
duration={1000}
useNativeDriver
onAnimationBegin={() => {
// delay the hiding even 1ms is enough to prevent flickering
setTimeout(() => SplashScreen.hide(), 1);
}}
>
<NavigationContainer theme={this.state.currentTheme} ref={this.navigatorRef}> <NavigationContainer theme={this.state.currentTheme} ref={this.navigatorRef}>
<Stack.Navigator headerMode="none"> <Stack.Navigator headerMode="none">
<Stack.Screen name="Root" component={this.createDrawerNavigator}/> <Stack.Screen name="Root" component={this.createDrawerNavigator}/>
</Stack.Navigator> </Stack.Navigator>
</NavigationContainer> </NavigationContainer>
</Animatable.View>
</View>
</PaperProvider> </PaperProvider>
); );
} }

View file

@ -5,15 +5,16 @@ import ConnectionManager from "../../managers/ConnectionManager";
import {ERROR_TYPE} from "../../utils/WebData"; import {ERROR_TYPE} from "../../utils/WebData";
import ErrorView from "../Screens/ErrorView"; import ErrorView from "../Screens/ErrorView";
import BasicLoadingScreen from "../Screens/BasicLoadingScreen"; import BasicLoadingScreen from "../Screens/BasicLoadingScreen";
import {StackNavigationProp} from "@react-navigation/stack";
type Props = { type Props = {
navigation: Object, navigation: StackNavigationProp,
requests: Array<{ requests: Array<{
link: string, link: string,
params: Object, params: Object,
mandatory: boolean mandatory: boolean
}>, }>,
renderFunction: Function, renderFunction: (Array<{ [key: string]: any } | null>) => React.Node,
} }
type State = { type State = {
@ -29,7 +30,7 @@ class AuthenticatedScreen extends React.Component<Props, State> {
currentUserToken: string | null; currentUserToken: string | null;
connectionManager: ConnectionManager; connectionManager: ConnectionManager;
errors: Array<number>; errors: Array<number>;
fetchedData: Array<Object>; fetchedData: Array<{ [key: string]: any } | null>;
constructor(props: Object) { constructor(props: Object) {
super(props); super(props);
@ -88,7 +89,7 @@ class AuthenticatedScreen extends React.Component<Props, State> {
* @param index The index for the data * @param index The index for the data
* @param error The error code received * @param error The error code received
*/ */
onRequestFinished(data: Object | null, index: number, error: number) { onRequestFinished(data: { [key: string]: any } | null, index: number, error: number) {
if (index >= 0 && index < this.props.requests.length) { if (index >= 0 && index < this.props.requests.length) {
this.fetchedData[index] = data; this.fetchedData[index] = data;
this.errors[index] = error; this.errors[index] = error;

View file

@ -4,17 +4,18 @@ import * as React from 'react';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import LoadingConfirmDialog from "../Dialogs/LoadingConfirmDialog"; import LoadingConfirmDialog from "../Dialogs/LoadingConfirmDialog";
import ConnectionManager from "../../managers/ConnectionManager"; import ConnectionManager from "../../managers/ConnectionManager";
import {StackNavigationProp} from "@react-navigation/stack";
type Props = { type Props = {
navigation: Object, navigation: StackNavigationProp,
visible: boolean, visible: boolean,
onDismiss: Function, onDismiss: () => void,
} }
class LogoutDialog extends React.PureComponent<Props> { class LogoutDialog extends React.PureComponent<Props> {
onClickAccept = async () => { onClickAccept = async () => {
return new Promise((resolve, reject) => { return new Promise((resolve) => {
ConnectionManager.getInstance().disconnect() ConnectionManager.getInstance().disconnect()
.then(() => { .then(() => {
this.props.navigation.reset({ this.props.navigation.reset({

View file

@ -4,21 +4,23 @@ import * as React from 'react';
import {Avatar, Card, List, ProgressBar, Subheading, withTheme} from "react-native-paper"; import {Avatar, Card, List, ProgressBar, Subheading, withTheme} from "react-native-paper";
import {FlatList, StyleSheet} from "react-native"; import {FlatList, StyleSheet} from "react-native";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {team} from "../../../screens/Amicale/VoteScreen";
import type {CustomTheme} from "../../../managers/ThemeManager";
type Props = { type Props = {
teams: Array<Object>, teams: Array<team>,
dateEnd: string, dateEnd: string,
theme: CustomTheme,
} }
class VoteResults extends React.Component<Props> { class VoteResults extends React.Component<Props> {
totalVotes: number; totalVotes: number;
winnerIds: Array<number>; winnerIds: Array<number>;
colors: Object;
constructor(props) { constructor(props) {
super(); super();
this.colors = props.theme.colors;
props.teams.sort(this.sortByVotes); props.teams.sort(this.sortByVotes);
this.getTotalVotes(props.teams); this.getTotalVotes(props.teams);
this.getWinnerIds(props.teams); this.getWinnerIds(props.teams);
@ -28,16 +30,16 @@ class VoteResults extends React.Component<Props> {
return false; return false;
} }
sortByVotes = (a: Object, b: Object) => b.votes - a.votes; sortByVotes = (a: team, b: team) => b.votes - a.votes;
getTotalVotes(teams: Array<Object>) { getTotalVotes(teams: Array<team>) {
this.totalVotes = 0; this.totalVotes = 0;
for (let i = 0; i < teams.length; i++) { for (let i = 0; i < teams.length; i++) {
this.totalVotes += teams[i].votes; this.totalVotes += teams[i].votes;
} }
} }
getWinnerIds(teams: Array<Object>){ getWinnerIds(teams: Array<team>) {
let max = teams[0].votes; let max = teams[0].votes;
this.winnerIds = []; this.winnerIds = [];
for (let i = 0; i < teams.length; i++) { for (let i = 0; i < teams.length; i++) {
@ -48,11 +50,12 @@ class VoteResults extends React.Component<Props> {
} }
} }
voteKeyExtractor = (item: Object) => item.id.toString(); voteKeyExtractor = (item: team) => item.id.toString();
resultRenderItem = ({item}: Object) => { resultRenderItem = ({item}: { item: team }) => {
const isWinner = this.winnerIds.indexOf(item.id) !== -1; const isWinner = this.winnerIds.indexOf(item.id) !== -1;
const isDraw = this.winnerIds.length > 1; const isDraw = this.winnerIds.length > 1;
const colors = this.props.theme.colors;
return ( return (
<Card style={{ <Card style={{
marginTop: 10, marginTop: 10,
@ -62,16 +65,16 @@ class VoteResults extends React.Component<Props> {
title={item.name} title={item.name}
description={item.votes + ' ' + i18n.t('voteScreen.results.votes')} description={item.votes + ' ' + i18n.t('voteScreen.results.votes')}
left={props => isWinner left={props => isWinner
? <List.Icon {...props} icon={isDraw ? "trophy-outline" : "trophy"} color={this.colors.primary}/> ? <List.Icon {...props} icon={isDraw ? "trophy-outline" : "trophy"} color={colors.primary}/>
: null} : null}
titleStyle={{ titleStyle={{
color: isWinner color: isWinner
? this.colors.primary ? colors.primary
: this.colors.text : colors.text
}} }}
style={{padding: 0}} style={{padding: 0}}
/> />
<ProgressBar progress={item.votes / this.totalVotes} color={this.colors.primary}/> <ProgressBar progress={item.votes / this.totalVotes} color={colors.primary}/>
</Card> </Card>
); );
}; };

View file

@ -7,11 +7,12 @@ import ConnectionManager from "../../../managers/ConnectionManager";
import LoadingConfirmDialog from "../../Dialogs/LoadingConfirmDialog"; import LoadingConfirmDialog from "../../Dialogs/LoadingConfirmDialog";
import ErrorDialog from "../../Dialogs/ErrorDialog"; import ErrorDialog from "../../Dialogs/ErrorDialog";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {team} from "../../../screens/Amicale/VoteScreen";
type Props = { type Props = {
teams: Array<Object>, teams: Array<team>,
onVoteSuccess: Function, onVoteSuccess: () => void,
onVoteError: Function, onVoteError: () => void,
} }
type State = { type State = {
@ -33,16 +34,16 @@ export default class VoteSelect extends React.PureComponent<Props, State> {
onVoteSelectionChange = (team: string) => this.setState({selectedTeam: team}); onVoteSelectionChange = (team: string) => this.setState({selectedTeam: team});
voteKeyExtractor = (item: Object) => item.id.toString(); voteKeyExtractor = (item: team) => item.id.toString();
voteRenderItem = ({item}: Object) => <RadioButton.Item label={item.name} value={item.id.toString()}/>; voteRenderItem = ({item}: { item: team }) => <RadioButton.Item label={item.name} value={item.id.toString()}/>;
showVoteDialog = () => this.setState({voteDialogVisible: true}); showVoteDialog = () => this.setState({voteDialogVisible: true});
onVoteDialogDismiss = () => this.setState({voteDialogVisible: false,}); onVoteDialogDismiss = () => this.setState({voteDialogVisible: false,});
onVoteDialogAccept = async () => { onVoteDialogAccept = async () => {
return new Promise((resolve, reject) => { return new Promise((resolve) => {
ConnectionManager.getInstance().authenticatedRequest( ConnectionManager.getInstance().authenticatedRequest(
"elections/vote", "elections/vote",
{"team": parseInt(this.state.selectedTeam)}) {"team": parseInt(this.state.selectedTeam)})
@ -76,7 +77,8 @@ export default class VoteSelect extends React.PureComponent<Props, State> {
<Card.Title <Card.Title
title={i18n.t('voteScreen.select.title')} title={i18n.t('voteScreen.select.title')}
subtitle={i18n.t('voteScreen.select.subtitle')} subtitle={i18n.t('voteScreen.select.subtitle')}
left={(props) => <Avatar.Icon left={(props) =>
<Avatar.Icon
{...props} {...props}
icon={"alert-decagram"} icon={"alert-decagram"}
/>} />}

View file

@ -7,9 +7,7 @@ import i18n from 'i18n-js';
const ICON_AMICALE = require('../../../../assets/amicale.png'); const ICON_AMICALE = require('../../../../assets/amicale.png');
type Props = {} export default class VoteTitle extends React.Component<{}> {
export default class VoteTitle extends React.Component<Props> {
shouldComponentUpdate() { shouldComponentUpdate() {
return false; return false;

View file

@ -4,28 +4,25 @@ import * as React from 'react';
import {ActivityIndicator, Card, Paragraph, withTheme} from "react-native-paper"; import {ActivityIndicator, Card, Paragraph, withTheme} from "react-native-paper";
import {StyleSheet} from "react-native"; import {StyleSheet} from "react-native";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {CustomTheme} from "../../../managers/ThemeManager";
type Props = { type Props = {
startDate: string | null, startDate: string | null,
justVoted: boolean, justVoted: boolean,
hasVoted: boolean, hasVoted: boolean,
isVoteRunning: boolean, isVoteRunning: boolean,
theme: CustomTheme,
} }
class VoteWait extends React.Component<Props> { class VoteWait extends React.Component<Props> {
colors: Object;
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
shouldComponentUpdate() { shouldComponentUpdate() {
return false; return false;
} }
render() { render() {
const colors = this.props.theme.colors;
const startDate = this.props.startDate;
return ( return (
<Card style={styles.card}> <Card style={styles.card}>
<Card.Title <Card.Title
@ -38,22 +35,22 @@ class VoteWait extends React.Component<Props> {
<Card.Content> <Card.Content>
{ {
this.props.justVoted this.props.justVoted
? <Paragraph style={{color: this.colors.success}}> ? <Paragraph style={{color: colors.success}}>
{i18n.t('voteScreen.wait.messageSubmitted')} {i18n.t('voteScreen.wait.messageSubmitted')}
</Paragraph> </Paragraph>
: null : null
} }
{ {
this.props.hasVoted this.props.hasVoted
? <Paragraph style={{color: this.colors.success}}> ? <Paragraph style={{color: colors.success}}>
{i18n.t('voteScreen.wait.messageVoted')} {i18n.t('voteScreen.wait.messageVoted')}
</Paragraph> </Paragraph>
: null : null
} }
{ {
this.props.startDate !== null startDate != null
? <Paragraph> ? <Paragraph>
{i18n.t('voteScreen.wait.messageDate') + ' ' + this.props.startDate} {i18n.t('voteScreen.wait.messageDate') + ' ' + startDate}
</Paragraph> </Paragraph>
: <Paragraph>{i18n.t('voteScreen.wait.messageDateUndefined')}</Paragraph> : <Paragraph>{i18n.t('voteScreen.wait.messageDateUndefined')}</Paragraph>
} }

View file

@ -0,0 +1,87 @@
// @flow
import * as React from 'react';
import {View} from "react-native";
import {List, withTheme} from 'react-native-paper';
import Collapsible from "react-native-collapsible";
import * as Animatable from "react-native-animatable";
import type {CustomTheme} from "../../managers/ThemeManager";
type Props = {
theme: CustomTheme,
title: string,
subtitle?: string,
left?: (props: { [keys: string]: any }) => React.Node,
startOpen: boolean,
keepOpen: boolean,
unmountWhenCollapsed: boolean,
children?: React.Node,
}
type State = {
expanded: boolean
}
const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
class AnimatedAccordion extends React.PureComponent<Props, State> {
static defaultProps = {
startOpen: false,
keepOpen: false,
unmountWhenCollapsed: false,
}
chevronRef: { current: null | AnimatedListIcon };
state = {
expanded: false,
}
constructor(props) {
super(props);
this.chevronRef = React.createRef();
}
componentDidMount() {
if (this.props.startOpen)
this.toggleAccordion();
}
toggleAccordion = () => {
if (!this.props.keepOpen) {
if (this.chevronRef.current != null)
this.chevronRef.current.transitionTo({rotate: this.state.expanded ? '0deg' : '180deg'});
this.setState({expanded: !this.state.expanded})
}
};
render() {
const colors = this.props.theme.colors;
return (
<View>
<List.Item
{...this.props}
title={this.props.title}
subtitle={this.props.subtitle}
titleStyle={this.state.expanded ? {color: colors.primary} : undefined}
onPress={this.toggleAccordion}
right={(props) => <AnimatedListIcon
ref={this.chevronRef}
{...props}
icon={"chevron-down"}
color={this.state.expanded ? colors.primary : undefined}
useNativeDriver
/>}
left={this.props.left}
/>
<Collapsible collapsed={!this.props.keepOpen && !this.state.expanded}>
{!this.props.unmountWhenCollapsed || (this.props.unmountWhenCollapsed && this.state.expanded)
? this.props.children
: null}
</Collapsible>
</View>
);
}
}
export default withTheme(AnimatedAccordion);

View file

@ -6,13 +6,15 @@ import {FAB, IconButton, Surface, withTheme} from "react-native-paper";
import AutoHideHandler from "../../utils/AutoHideHandler"; import AutoHideHandler from "../../utils/AutoHideHandler";
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import CustomTabBar from "../Tabbar/CustomTabBar"; import CustomTabBar from "../Tabbar/CustomTabBar";
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../managers/ThemeManager";
const AnimatedFAB = Animatable.createAnimatableComponent(FAB); const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
type Props = { type Props = {
navigation: Object, navigation: StackNavigationProp,
theme: Object, theme: CustomTheme,
onPress: Function, onPress: (action: string, data: any) => void,
seekAttention: boolean, seekAttention: boolean,
} }
@ -28,10 +30,10 @@ const DISPLAY_MODES = {
class AnimatedBottomBar extends React.Component<Props, State> { class AnimatedBottomBar extends React.Component<Props, State> {
ref: Object; ref: { current: null | Animatable.View };
hideHandler: AutoHideHandler; hideHandler: AutoHideHandler;
displayModeIcons: Object; displayModeIcons: { [key: string]: string };
state = { state = {
currentMode: DISPLAY_MODES.WEEK, currentMode: DISPLAY_MODES.WEEK,
@ -55,7 +57,7 @@ class AnimatedBottomBar extends React.Component<Props, State> {
} }
onHideChange = (shouldHide: boolean) => { onHideChange = (shouldHide: boolean) => {
if (this.ref.current) { if (this.ref.current != null) {
if (shouldHide) if (shouldHide)
this.ref.current.bounceOutDown(1000); this.ref.current.bounceOutDown(1000);
else else
@ -63,7 +65,7 @@ class AnimatedBottomBar extends React.Component<Props, State> {
} }
} }
onScroll = (event: Object) => { onScroll = (event: SyntheticEvent<EventTarget>) => {
this.hideHandler.onScroll(event); this.hideHandler.onScroll(event);
}; };

View file

@ -6,18 +6,19 @@ import {FAB} from "react-native-paper";
import AutoHideHandler from "../../utils/AutoHideHandler"; import AutoHideHandler from "../../utils/AutoHideHandler";
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import CustomTabBar from "../Tabbar/CustomTabBar"; import CustomTabBar from "../Tabbar/CustomTabBar";
import {StackNavigationProp} from "@react-navigation/stack";
type Props = { type Props = {
navigation: Object, navigation: StackNavigationProp,
icon: string, icon: string,
onPress: Function, onPress: () => void,
} }
const AnimatedFab = Animatable.createAnimatableComponent(FAB); const AnimatedFab = Animatable.createAnimatableComponent(FAB);
export default class AnimatedFAB extends React.Component<Props> { export default class AnimatedFAB extends React.Component<Props> {
ref: Object; ref: { current: null | Animatable.View };
hideHandler: AutoHideHandler; hideHandler: AutoHideHandler;
constructor() { constructor() {
@ -27,12 +28,12 @@ export default class AnimatedFAB extends React.Component<Props> {
this.hideHandler.addListener(this.onHideChange); this.hideHandler.addListener(this.onHideChange);
} }
onScroll = (event: Object) => { onScroll = (event: SyntheticEvent<EventTarget>) => {
this.hideHandler.onScroll(event); this.hideHandler.onScroll(event);
}; };
onHideChange = (shouldHide: boolean) => { onHideChange = (shouldHide: boolean) => {
if (this.ref.current) { if (this.ref.current != null) {
if (shouldHide) if (shouldHide)
this.ref.current.bounceOutDown(1000); this.ref.current.bounceOutDown(1000);
else else

View file

@ -3,16 +3,17 @@
import * as React from 'react'; import * as React from 'react';
import * as Animatable from "react-native-animatable"; import * as Animatable from "react-native-animatable";
import {CommonActions} from "@react-navigation/native"; import {CommonActions} from "@react-navigation/native";
import {StackNavigationProp} from "@react-navigation/stack";
type Props = { type Props = {
navigation: Object, navigation: StackNavigationProp,
route: Object, route: { params?: any, ... },
children: React$Node children: React.Node
} }
export default class AnimatedFocusView extends React.Component<Props> { export default class AnimatedFocusView extends React.Component<Props> {
ref: Object; ref: { current: null | Animatable.View };
constructor() { constructor() {
super(); super();
@ -24,7 +25,7 @@ export default class AnimatedFocusView extends React.Component<Props> {
} }
onScreenFocus = () => { onScreenFocus = () => {
if (this.props.route.params !== undefined) { if (this.props.route.params != null) {
if (this.props.route.params.animationDir && this.ref.current) { if (this.props.route.params.animationDir && this.ref.current) {
if (this.props.route.params.animationDir === "right") if (this.props.route.params.animationDir === "right")
this.ref.current.fadeInRight(300); this.ref.current.fadeInRight(300);

View file

@ -1,9 +1,11 @@
// @flow
import * as React from 'react'; import * as React from 'react';
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper'; import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
type Props = { type Props = {
visible: boolean, visible: boolean,
onDismiss: Function, onDismiss: () => void,
title: string, title: string,
message: string, message: string,
} }

View file

@ -1,3 +1,5 @@
// @flow
import * as React from 'react'; import * as React from 'react';
import i18n from "i18n-js"; import i18n from "i18n-js";
import {ERROR_TYPE} from "../../utils/WebData"; import {ERROR_TYPE} from "../../utils/WebData";
@ -5,7 +7,7 @@ import AlertDialog from "./AlertDialog";
type Props = { type Props = {
visible: boolean, visible: boolean,
onDismiss: Function, onDismiss: () => void,
errorCode: number, errorCode: number,
} }

View file

@ -1,11 +1,13 @@
// @flow
import * as React from 'react'; import * as React from 'react';
import {ActivityIndicator, Button, Dialog, Paragraph, Portal} from 'react-native-paper'; import {ActivityIndicator, Button, Dialog, Paragraph, Portal} from 'react-native-paper';
import i18n from "i18n-js"; import i18n from "i18n-js";
type Props = { type Props = {
visible: boolean, visible: boolean,
onDismiss: Function, onDismiss: () => void,
onAccept: Function, // async function to be executed onAccept: () => Promise<void>, // async function to be executed
title: string, title: string,
titleLoading: string, titleLoading: string,
message: string, message: string,
@ -21,18 +23,25 @@ class LoadingConfirmDialog extends React.PureComponent<Props, State> {
loading: false, loading: false,
}; };
/**
* Set the dialog into loading state and closes it when operation finishes
*/
onClickAccept = () => { onClickAccept = () => {
this.setState({loading: true}); this.setState({loading: true});
this.props.onAccept() this.props.onAccept().then(this.hideLoading);
.then(() => { };
//Wait for fade out animations to finish before hiding loading
setTimeout(() => { /**
* Waits for fade out animations to finish before hiding loading
* @returns {TimeoutID}
*/
hideLoading = () => setTimeout(() => {
this.setState({loading: false}) this.setState({loading: false})
}, 200); }, 200);
}); /**
}; * Hide the dialog if it is not loading
*/
onDismiss = () => { onDismiss = () => {
if (!this.state.loading) if (!this.state.loading)
this.props.onDismiss(); this.props.onDismiss();

View file

@ -4,10 +4,12 @@ import * as React from 'react';
import {Button, Card, withTheme} from 'react-native-paper'; import {Button, Card, withTheme} from 'react-native-paper';
import {Platform, StyleSheet} from "react-native"; import {Platform, StyleSheet} from "react-native";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {DrawerNavigationProp} from "@react-navigation/drawer";
import type {CustomTheme} from "../../managers/ThemeManager";
type Props = { type Props = {
navigation: Object, navigation: DrawerNavigationProp,
theme: Object, theme: CustomTheme,
} }
class ActionsDashBoardItem extends React.Component<Props> { class ActionsDashBoardItem extends React.Component<Props> {

View file

@ -4,11 +4,13 @@ import * as React from 'react';
import {Avatar, Card, Text, withTheme} from 'react-native-paper'; import {Avatar, Card, Text, withTheme} from 'react-native-paper';
import {StyleSheet} from "react-native"; import {StyleSheet} from "react-native";
import i18n from "i18n-js"; import i18n from "i18n-js";
import type {CustomTheme} from "../../managers/ThemeManager";
type Props = { type Props = {
eventNumber: number; eventNumber: number;
clickAction: Function, clickAction: () => void,
theme: Object, theme: CustomTheme,
children?: React.Node
} }
/** /**

View file

@ -1,15 +1,21 @@
// @flow
import * as React from 'react'; import * as React from 'react';
import {Avatar, Button, Card, Text} from 'react-native-paper'; import {Avatar, Button, Card, Text} from 'react-native-paper';
import {View} from "react-native"; import {View} from "react-native";
import Autolink from "react-native-autolink"; import Autolink from "react-native-autolink";
import i18n from "i18n-js"; import i18n from "i18n-js";
import ImageModal from 'react-native-image-modal'; import ImageModal from 'react-native-image-modal';
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../managers/ThemeManager";
import type {feedItem} from "../../screens/Home/HomeScreen";
const ICON_AMICALE = require('../../../assets/amicale.png'); const ICON_AMICALE = require('../../../assets/amicale.png');
type Props = { type Props = {
navigation: Object, navigation: StackNavigationProp,
theme: Object, theme: CustomTheme,
item: feedItem,
title: string, title: string,
subtitle: string, subtitle: string,
height: number, height: number,
@ -32,17 +38,19 @@ class FeedItem extends React.Component<Props> {
*/ */
getAvatar() { getAvatar() {
return ( return (
<Avatar.Image size={48} source={ICON_AMICALE} <Avatar.Image
size={48} source={ICON_AMICALE}
style={{backgroundColor: 'transparent'}}/> style={{backgroundColor: 'transparent'}}/>
); );
} }
onPress = () => { onPress = () => {
this.props.navigation.navigate('feed-information', this.props.navigation.navigate(
'feed-information',
{ {
data: this.props.item, data: this.props.item,
date: this.props.subtitle date: this.props.subtitle
}) });
}; };
render() { render() {

View file

@ -6,10 +6,13 @@ import i18n from "i18n-js";
import {Avatar, Button, Card} from 'react-native-paper'; import {Avatar, Button, Card} from 'react-native-paper';
import {getFormattedEventTime, isDescriptionEmpty} from "../../utils/Planning"; import {getFormattedEventTime, isDescriptionEmpty} from "../../utils/Planning";
import CustomHTML from "../Overrides/CustomHTML"; import CustomHTML from "../Overrides/CustomHTML";
import type {CustomTheme} from "../../managers/ThemeManager";
import type {event} from "../../screens/Home/HomeScreen";
type Props = { type Props = {
event: Object, event?: event,
clickAction: Function, clickAction: () => void,
theme?: CustomTheme,
} }
/** /**
@ -19,14 +22,15 @@ class PreviewEventDashboardItem extends React.Component<Props> {
render() { render() {
const props = this.props; const props = this.props;
const isEmpty = props.event === undefined const isEmpty = props.event == null
? true ? true
: isDescriptionEmpty(props.event['description']); : isDescriptionEmpty(props.event.description);
if (props.event !== undefined && props.event !== null) { if (props.event != null) {
const hasImage = props.event['logo'] !== '' && props.event['logo'] !== null; const event = props.event;
const hasImage = event.logo !== '' && event.logo != null;
const getImage = () => <Avatar.Image const getImage = () => <Avatar.Image
source={{uri: props.event['logo']}} source={{uri: event.logo}}
size={50} size={50}
style={styles.avatar}/>; style={styles.avatar}/>;
return ( return (
@ -37,17 +41,17 @@ class PreviewEventDashboardItem extends React.Component<Props> {
> >
{hasImage ? {hasImage ?
<Card.Title <Card.Title
title={props.event['title']} title={event.title}
subtitle={getFormattedEventTime(props.event['date_begin'], props.event['date_end'])} subtitle={getFormattedEventTime(event.date_begin, event.date_end)}
left={getImage} left={getImage}
/> : /> :
<Card.Title <Card.Title
title={props.event['title']} title={event.title}
subtitle={getFormattedEventTime(props.event['date_begin'], props.event['date_end'])} subtitle={getFormattedEventTime(event.date_begin, event.date_end)}
/>} />}
{!isEmpty ? {!isEmpty ?
<Card.Content style={styles.content}> <Card.Content style={styles.content}>
<CustomHTML html={props.event['description']}/> <CustomHTML html={event.description}/>
</Card.Content> : null} </Card.Content> : null}
<Card.Actions style={styles.actions}> <Card.Actions style={styles.actions}>

View file

@ -3,15 +3,16 @@
import * as React from 'react'; import * as React from 'react';
import {Badge, IconButton, withTheme} from 'react-native-paper'; import {Badge, IconButton, withTheme} from 'react-native-paper';
import {View} from "react-native"; import {View} from "react-native";
import type {CustomTheme} from "../../managers/ThemeManager";
type Props = { type Props = {
color: string, color: string,
icon: string, icon: string,
clickAction: Function, clickAction: () => void,
isAvailable: boolean, isAvailable: boolean,
badgeNumber: number, badgeNumber: number,
theme: Object, theme: CustomTheme,
}; };
/** /**

View file

@ -4,21 +4,15 @@ import * as React from 'react';
import {Card, List, Text, withTheme} from 'react-native-paper'; import {Card, List, Text, withTheme} from 'react-native-paper';
import {StyleSheet, View} from "react-native"; import {StyleSheet, View} from "react-native";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import AnimatedAccordion from "../../Animations/AnimatedAccordion";
type Props = { type Props = {
categoryRender: Function, categoryRender: Function,
categories: Array<Object>, categories: Array<Object>,
} }
type State = { class ClubListHeader extends React.Component<Props> {
expanded: boolean,
}
class ClubListHeader extends React.Component<Props, State> {
state = {
expanded: true
};
colors: Object; colors: Object;
@ -35,22 +29,19 @@ class ClubListHeader extends React.Component<Props, State> {
return final; return final;
} }
onPress = () => this.setState({expanded: !this.state.expanded});
render() { render() {
return ( return (
<Card style={styles.card}> <Card style={styles.card}>
<List.Accordion <AnimatedAccordion
title={i18n.t("clubs.categories")} title={i18n.t("clubs.categories")}
left={props => <List.Icon {...props} icon="star"/>} left={props => <List.Icon {...props} icon="star"/>}
expanded={this.state.expanded} startOpen={true}
onPress={this.onPress}
> >
<Text style={styles.text}>{i18n.t("clubs.categoriesFilterMessage")}</Text> <Text style={styles.text}>{i18n.t("clubs.categoriesFilterMessage")}</Text>
<View style={styles.chipContainer}> <View style={styles.chipContainer}>
{this.getCategoriesRender()} {this.getCategoriesRender()}
</View> </View>
</List.Accordion> </AnimatedAccordion>
</Card> </Card>
); );
} }

View file

@ -4,9 +4,9 @@ import * as React from 'react';
import {List, withTheme} from 'react-native-paper'; import {List, withTheme} from 'react-native-paper';
import {FlatList, View} from "react-native"; import {FlatList, View} from "react-native";
import {stringMatchQuery} from "../../../utils/Search"; import {stringMatchQuery} from "../../../utils/Search";
import Collapsible from "react-native-collapsible";
import * as Animatable from "react-native-animatable"; import * as Animatable from "react-native-animatable";
import GroupListItem from "./GroupListItem"; import GroupListItem from "./GroupListItem";
import AnimatedAccordion from "../../Animations/AnimatedAccordion";
type Props = { type Props = {
item: Object, item: Object,
@ -47,11 +47,6 @@ class GroupListAccordion extends React.Component<Props, State> {
|| (nextProps.item.content.length !== this.props.item.content.length); || (nextProps.item.content.length !== this.props.item.content.length);
} }
onPress = () => {
this.chevronRef.current.transitionTo({rotate: this.state.expanded ? '0deg' : '180deg'});
this.setState({expanded: !this.state.expanded})
};
keyExtractor = (item: Object) => item.id.toString(); keyExtractor = (item: Object) => item.id.toString();
renderItem = ({item}: Object) => { renderItem = ({item}: Object) => {
@ -73,20 +68,14 @@ class GroupListAccordion extends React.Component<Props, State> {
render() { render() {
const item = this.props.item; const item = this.props.item;
const accordionColor = this.state.expanded
? this.props.theme.colors.primary
: this.props.theme.colors.text;
// console.log(item.id);
return ( return (
<View> <View>
<List.Item <AnimatedAccordion
title={item.name} title={item.name}
onPress={this.onPress}
style={{ style={{
height: this.props.height, height: this.props.height,
justifyContent: 'center', justifyContent: 'center',
}} }}
titleStyle={{color: accordionColor}}
left={props => left={props =>
item.id === "0" item.id === "0"
? <List.Icon ? <List.Icon
@ -95,23 +84,9 @@ class GroupListAccordion extends React.Component<Props, State> {
color={this.props.theme.colors.tetrisScore} color={this.props.theme.colors.tetrisScore}
/> />
: null} : null}
right={(props) => <AnimatedListIcon unmountWhenCollapsed={true}// Only render list if expanded for increased performance
ref={this.chevronRef}
{...props}
icon={"chevron-down"}
color={this.state.expanded
? this.props.theme.colors.primary
: props.color
}
useNativeDriver
/>}
/>
<Collapsible
collapsed={!this.state.expanded}
ease={"easeInOut"}
> >
{this.state.expanded // Only render list if expanded for increased performance <FlatList
? <FlatList
data={item.content} data={item.content}
extraData={this.props.currentSearchString} extraData={this.props.currentSearchString}
renderItem={this.renderItem} renderItem={this.renderItem}
@ -121,9 +96,7 @@ class GroupListAccordion extends React.Component<Props, State> {
getItemLayout={this.itemLayout} // Broken with search getItemLayout={this.itemLayout} // Broken with search
removeClippedSubviews={true} removeClippedSubviews={true}
/> />
: null} </AnimatedAccordion>
</Collapsible>
</View> </View>
); );
} }

View file

@ -10,26 +10,28 @@ import BasicLoadingScreen from "./BasicLoadingScreen";
import {withCollapsible} from "../../utils/withCollapsible"; import {withCollapsible} from "../../utils/withCollapsible";
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import CustomTabBar from "../Tabbar/CustomTabBar"; import CustomTabBar from "../Tabbar/CustomTabBar";
import {Collapsible} from "react-navigation-collapsible";
type Props = { type Props = {
navigation: Object, navigation: { [key: string]: any },
fetchUrl: string, fetchUrl: string,
autoRefreshTime: number, autoRefreshTime: number,
refreshOnFocus: boolean, refreshOnFocus: boolean,
renderItem: React.Node, renderItem: (data: { [key: string]: any }) => React.Node,
renderSectionHeader: React.Node, createDataset: (data: { [key: string]: any }) => Array<Object>,
stickyHeader: boolean, onScroll: (event: SyntheticEvent<EventTarget>) => void,
createDataset: Function, collapsibleStack: Collapsible,
updateData: number,
itemHeight: number | null, itemHeight?: number,
onScroll: Function, updateData?: number,
collapsibleStack: Object, renderSectionHeader?: (data: { [key: string]: any }) => React.Node,
stickyHeader?: boolean,
} }
type State = { type State = {
refreshing: boolean, refreshing: boolean,
firstLoading: boolean, firstLoading: boolean,
fetchedData: ?Object, fetchedData: { [key: string]: any } | null,
snackbarVisible: boolean snackbarVisible: boolean
}; };
@ -45,37 +47,21 @@ const MIN_REFRESH_TIME = 5 * 1000;
class WebSectionList extends React.PureComponent<Props, State> { class WebSectionList extends React.PureComponent<Props, State> {
static defaultProps = { static defaultProps = {
renderSectionHeader: null,
stickyHeader: false, stickyHeader: false,
updateData: 0, updateData: 0,
itemHeight: null,
}; };
scrollRef: Object; scrollRef: { current: null | Animated.SectionList };
refreshInterval: IntervalID; refreshInterval: IntervalID;
lastRefresh: Date; lastRefresh: Date | null;
state = { state = {
refreshing: false, refreshing: false,
firstLoading: true, firstLoading: true,
fetchedData: undefined, fetchedData: null,
snackbarVisible: false snackbarVisible: false
}; };
onRefresh: Function;
onFetchSuccess: Function;
onFetchError: Function;
getEmptySectionHeader: Function;
constructor() {
super();
// creating references to functions used in render()
this.onRefresh = this.onRefresh.bind(this);
this.onFetchSuccess = this.onFetchSuccess.bind(this);
this.onFetchError = this.onFetchError.bind(this);
this.getEmptySectionHeader = this.getEmptySectionHeader.bind(this);
}
/** /**
* Registers react navigation events on first screen load. * Registers react navigation events on first screen load.
* Allows to detect when the screen is focused * Allows to detect when the screen is focused
@ -87,13 +73,14 @@ class WebSectionList extends React.PureComponent<Props, State> {
this.props.navigation.addListener('blur', onScreenBlur); this.props.navigation.addListener('blur', onScreenBlur);
this.scrollRef = React.createRef(); this.scrollRef = React.createRef();
this.onRefresh(); this.onRefresh();
this.lastRefresh = null;
} }
/** /**
* Refreshes data when focusing the screen and setup a refresh interval if asked to * Refreshes data when focusing the screen and setup a refresh interval if asked to
*/ */
onScreenFocus() { onScreenFocus() {
if (this.props.refreshOnFocus && this.lastRefresh !== undefined) if (this.props.refreshOnFocus && this.lastRefresh)
this.onRefresh(); this.onRefresh();
if (this.props.autoRefreshTime > 0) if (this.props.autoRefreshTime > 0)
this.refreshInterval = setInterval(this.onRefresh, this.props.autoRefreshTime) this.refreshInterval = setInterval(this.onRefresh, this.props.autoRefreshTime)
@ -115,36 +102,37 @@ class WebSectionList extends React.PureComponent<Props, State> {
* *
* @param fetchedData The newly fetched data * @param fetchedData The newly fetched data
*/ */
onFetchSuccess(fetchedData: Object) { onFetchSuccess = (fetchedData: { [key: string]: any }) => {
this.setState({ this.setState({
fetchedData: fetchedData, fetchedData: fetchedData,
refreshing: false, refreshing: false,
firstLoading: false firstLoading: false
}); });
this.lastRefresh = new Date(); this.lastRefresh = new Date();
} };
/** /**
* Callback used when fetch encountered an error. * Callback used when fetch encountered an error.
* It will reset the displayed data and show an error. * It will reset the displayed data and show an error.
*/ */
onFetchError() { onFetchError = () => {
this.setState({ this.setState({
fetchedData: undefined, fetchedData: null,
refreshing: false, refreshing: false,
firstLoading: false firstLoading: false
}); });
this.showSnackBar(); this.showSnackBar();
} };
/** /**
* Refreshes data and shows an animations while doing it * Refreshes data and shows an animations while doing it
*/ */
onRefresh() { onRefresh = () => {
let canRefresh; let canRefresh;
if (this.lastRefresh !== undefined) if (this.lastRefresh != null) {
canRefresh = (new Date().getTime() - this.lastRefresh.getTime()) > MIN_REFRESH_TIME; const last = this.lastRefresh;
else canRefresh = (new Date().getTime() - last.getTime()) > MIN_REFRESH_TIME;
} else
canRefresh = true; canRefresh = true;
if (canRefresh) { if (canRefresh) {
this.setState({refreshing: true}); this.setState({refreshing: true});
@ -152,17 +140,7 @@ class WebSectionList extends React.PureComponent<Props, State> {
.then(this.onFetchSuccess) .then(this.onFetchSuccess)
.catch(this.onFetchError); .catch(this.onFetchError);
} }
} };
/**
* Gets an empty section header
*
* @param section The current section
* @return {*}
*/
getEmptySectionHeader({section}: Object) {
return <View/>;
}
/** /**
* Shows the error popup * Shows the error popup
@ -174,13 +152,19 @@ class WebSectionList extends React.PureComponent<Props, State> {
*/ */
hideSnackBar = () => this.setState({snackbarVisible: false}); hideSnackBar = () => this.setState({snackbarVisible: false});
itemLayout = (data: Object, index: number) => ({ itemLayout = (data: { [key: string]: any }, index: number) => {
length: this.props.itemHeight, const height = this.props.itemHeight;
offset: this.props.itemHeight * index, if (height == null)
return undefined;
return {
length: height,
offset: height * index,
index index
}); }
};
renderSectionHeader = (data: Object) => { renderSectionHeader = (data: { section: { [key: string]: any } }) => {
if (this.props.renderSectionHeader != null) {
return ( return (
<Animatable.View <Animatable.View
animation={"fadeInUp"} animation={"fadeInUp"}
@ -190,9 +174,16 @@ class WebSectionList extends React.PureComponent<Props, State> {
{this.props.renderSectionHeader(data)} {this.props.renderSectionHeader(data)}
</Animatable.View> </Animatable.View>
); );
} else
return null;
} }
renderItem = (data: Object) => { renderItem = (data: {
item: { [key: string]: any },
index: number,
section: { [key: string]: any },
separators: { [key: string]: any },
}) => {
return ( return (
<Animatable.View <Animatable.View
animation={"fadeInUp"} animation={"fadeInUp"}
@ -204,20 +195,18 @@ class WebSectionList extends React.PureComponent<Props, State> {
); );
} }
onScroll = (event: Object) => { onScroll = (event: SyntheticEvent<EventTarget>) => {
if (this.props.onScroll) if (this.props.onScroll)
this.props.onScroll(event); this.props.onScroll(event);
} }
render() { render() {
let dataset = []; let dataset = [];
if (this.state.fetchedData !== undefined) if (this.state.fetchedData != null)
dataset = this.props.createDataset(this.state.fetchedData); dataset = this.props.createDataset(this.state.fetchedData);
const shouldRenderHeader = this.props.renderSectionHeader !== null;
const {containerPaddingTop, scrollIndicatorInsetTop, onScrollWithListener} = this.props.collapsibleStack; const {containerPaddingTop, scrollIndicatorInsetTop, onScrollWithListener} = this.props.collapsibleStack;
return ( return (
<View> <View>
{/*$FlowFixMe*/}
<Animated.SectionList <Animated.SectionList
ref={this.scrollRef} ref={this.scrollRef}
sections={dataset} sections={dataset}
@ -229,9 +218,7 @@ class WebSectionList extends React.PureComponent<Props, State> {
onRefresh={this.onRefresh} onRefresh={this.onRefresh}
/> />
} }
//$FlowFixMe renderSectionHeader={this.renderSectionHeader}
renderSectionHeader={shouldRenderHeader ? this.renderSectionHeader : this.getEmptySectionHeader}
//$FlowFixMe
renderItem={this.renderItem} renderItem={this.renderItem}
stickySectionHeadersEnabled={this.props.stickyHeader} stickySectionHeadersEnabled={this.props.stickyHeader}
style={{minHeight: '100%'}} style={{minHeight: '100%'}}
@ -242,7 +229,7 @@ class WebSectionList extends React.PureComponent<Props, State> {
errorCode={ERROR_TYPE.CONNECTION_ERROR} errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={this.onRefresh}/> onRefresh={this.onRefresh}/>
} }
getItemLayout={this.props.itemHeight !== null ? this.itemLayout : undefined} getItemLayout={this.props.itemHeight != null ? this.itemLayout : undefined}
// Animations // Animations
onScroll={onScrollWithListener(this.onScroll)} onScroll={onScrollWithListener(this.onScroll)}
contentContainerStyle={{ contentContainerStyle={{

View file

@ -1,11 +1,10 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {FlatList, View} from "react-native"; import {FlatList} from "react-native";
import {Drawer, List, withTheme} from 'react-native-paper'; import {Drawer, withTheme} from 'react-native-paper';
import {Linking} from "expo"; import {Linking} from "expo";
import Collapsible from "react-native-collapsible"; import AnimatedAccordion from "../Animations/AnimatedAccordion";
import * as Animatable from "react-native-animatable";
type Props = { type Props = {
navigation: Object, navigation: Object,
@ -17,28 +16,17 @@ type Props = {
listData: Array<Object>, listData: Array<Object>,
} }
type State = {
expanded: boolean
}
const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
const LIST_ITEM_HEIGHT = 48; const LIST_ITEM_HEIGHT = 48;
class SideBarSection extends React.PureComponent<Props, State> { class SideBarSection extends React.PureComponent<Props> {
state = {
expanded: this.props.startOpen,
};
colors: Object; colors: Object;
shouldExpand: boolean; accordionRef: {current: null | AnimatedAccordion};
chevronRef: Object;
constructor(props) { constructor(props) {
super(props); super(props);
this.colors = props.theme.colors; this.colors = props.theme.colors;
this.chevronRef = React.createRef(); this.accordionRef = React.createRef();
} }
/** /**
@ -50,7 +38,7 @@ class SideBarSection extends React.PureComponent<Props, State> {
shouldExpandList() { shouldExpandList() {
for (let i = 0; i < this.props.listData.length; i++) { for (let i = 0; i < this.props.listData.length; i++) {
if (this.props.listData[i].route === this.props.activeRoute) { if (this.props.listData[i].route === this.props.activeRoute) {
return this.state.expanded === false; return true;
} }
} }
return false; return false;
@ -109,13 +97,6 @@ class SideBarSection extends React.PureComponent<Props, State> {
); );
}; };
toggleAccordion = () => {
if ((!this.state.expanded && this.shouldExpand) || !this.shouldExpand) {
this.chevronRef.current.transitionTo({ rotate: this.state.expanded ? '0deg' : '180deg' });
this.setState({expanded: !this.state.expanded})
}
};
shouldRenderAccordion() { shouldRenderAccordion() {
let itemsToRender = 0; let itemsToRender = 0;
for (let i = 0; i < this.props.listData.length; i++) { for (let i = 0; i < this.props.listData.length; i++) {
@ -144,26 +125,13 @@ class SideBarSection extends React.PureComponent<Props, State> {
render() { render() {
if (this.shouldRenderAccordion()) { if (this.shouldRenderAccordion()) {
this.shouldExpand = this.shouldExpandList();
if (this.shouldExpand)
this.toggleAccordion();
return ( return (
<View> <AnimatedAccordion
<List.Item
title={this.props.sectionName} title={this.props.sectionName}
// expanded={this.state.expanded} keepOpen={this.shouldExpandList()}
onPress={this.toggleAccordion} >
right={(props) => <AnimatedListIcon
ref={this.chevronRef}
{...props}
icon={"chevron-down"}
useNativeDriver
/>}
/>
<Collapsible collapsed={!this.state.expanded}>
{this.getFlatList()} {this.getFlatList()}
</Collapsible> </AnimatedAccordion>
</View>
); );
} else } else
return this.getFlatList(); return this.getFlatList();

View file

@ -12,8 +12,7 @@ const deviceWidth = Dimensions.get("window").width;
type Props = { type Props = {
navigation: Object, navigation: Object,
state: Object, theme?: Object,
theme: Object,
}; };
type State = { type State = {

View file

@ -2,7 +2,6 @@ import * as React from 'react';
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import TabIcon from "./TabIcon"; import TabIcon from "./TabIcon";
import TabHomeIcon from "./TabHomeIcon"; import TabHomeIcon from "./TabHomeIcon";
import {AnimatedValue} from "react-native-reanimated";
import {Animated} from 'react-native'; import {Animated} from 'react-native';
type Props = { type Props = {
@ -17,6 +16,13 @@ type State = {
translateY: AnimatedValue, translateY: AnimatedValue,
} }
const TAB_ICONS = {
planning: 'calendar-range',
proxiwash: 'tshirt-crew',
proximo: 'cart',
planex: 'clock',
};
/** /**
* Abstraction layer for Agenda component, using custom configuration * Abstraction layer for Agenda component, using custom configuration
*/ */
@ -25,6 +31,8 @@ class CustomTabBar extends React.Component<Props, State> {
static TAB_BAR_HEIGHT = 48; static TAB_BAR_HEIGHT = 48;
barSynced: boolean; // Is the bar synced with the header for animations? barSynced: boolean; // Is the bar synced with the header for animations?
activeColor: string;
inactiveColor: string;
state = { state = {
translateY: new Animated.Value(0), translateY: new Animated.Value(0),
@ -37,10 +45,12 @@ class CustomTabBar extends React.Component<Props, State> {
tabRef: Object; tabRef: Object;
constructor() { constructor(props) {
super(); super(props);
this.tabRef = React.createRef(); this.tabRef = React.createRef();
this.barSynced = false; this.barSynced = false;
this.activeColor = props.theme.colors.primary;
this.inactiveColor = props.theme.colors.tabIcon;
} }
onItemPress(route: Object, currentIndex: number, destIndex: number) { onItemPress(route: Object, currentIndex: number, destIndex: number) {
@ -58,14 +68,73 @@ class CustomTabBar extends React.Component<Props, State> {
} }
} }
tabBarIcon = (route, focused) => {
let icon = TAB_ICONS[route.name];
icon = focused ? icon : icon + ('-outline');
if (route.name !== "home")
return icon;
else
return null;
};
onRouteChange = () => { onRouteChange = () => {
this.barSynced = false; this.barSynced = false;
} }
render() { renderIcon = (route, index) => {
const state = this.props.state; const state = this.props.state;
const descriptors = this.props.descriptors; const {options} = this.props.descriptors[route.key];
const navigation = this.props.navigation; const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;
const isFocused = state.index === index;
const onPress = () => this.onItemPress(route, state.index, index);
const onLongPress = () => {
this.props.navigation.emit({
type: 'tabLongPress',
target: route.key,
});
};
if (isFocused) {
const stackState = route.state;
const stackRoute = route.state ? stackState.routes[stackState.index] : undefined;
const params = stackRoute ? stackRoute.params : undefined;
const collapsible = params ? params.collapsible : undefined;
if (collapsible && !this.barSynced) {
this.barSynced = true;
this.setState({translateY: Animated.multiply(-1.5, collapsible.translateY)});
}
}
const color = isFocused ? this.activeColor : this.inactiveColor;
if (route.name !== "home") {
return <TabIcon
onPress={onPress}
onLongPress={onLongPress}
icon={this.tabBarIcon(route, isFocused)}
color={color}
label={label}
focused={isFocused}
extraData={state.index > index}
key={route.key}
/>
} else
return <TabHomeIcon
onPress={onPress}
onLongPress={onLongPress}
focused={isFocused}
key={route.key}
/>
};
render() {
this.props.navigation.addListener('state', this.onRouteChange); this.props.navigation.addListener('state', this.onRouteChange);
return ( return (
<Animated.View <Animated.View
@ -84,57 +153,7 @@ class CustomTabBar extends React.Component<Props, State> {
transform: [{translateY: this.state.translateY}] transform: [{translateY: this.state.translateY}]
}} }}
> >
{state.routes.map((route, index) => { {this.props.state.routes.map(this.renderIcon)}
const {options} = descriptors[route.key];
const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;
const isFocused = state.index === index;
const onPress = () => this.onItemPress(route, state.index, index);
const onLongPress = () => {
navigation.emit({
type: 'tabLongPress',
target: route.key,
});
};
if (isFocused) {
const stackState = route.state;
const stackRoute = route.state ? stackState.routes[stackState.index] : undefined;
const params = stackRoute ? stackRoute.params : undefined;
const collapsible = params ? params.collapsible : undefined;
if (collapsible && !this.barSynced) {
this.barSynced = true;
this.setState({translateY: Animated.multiply(-1.5, collapsible.translateY)});
}
}
const color = isFocused ? options.activeColor : options.inactiveColor;
const iconData = {focused: isFocused, color: color};
if (route.name !== "home") {
return <TabIcon
onPress={onPress}
onLongPress={onLongPress}
icon={options.tabBarIcon(iconData)}
color={color}
label={label}
focused={isFocused}
extraData={state.index > index}
key={route.key}
/>
} else
return <TabHomeIcon
onPress={onPress}
onLongPress={onLongPress}
focused={isFocused}
key={route.key}
/>
})}
</Animated.View> </Animated.View>
); );
} }

View file

@ -1,12 +1,61 @@
// @flow // @flow
import AsyncStorageManager from "./AsyncStorageManager"; import AsyncStorageManager from "./AsyncStorageManager";
import {DarkTheme, DefaultTheme} from 'react-native-paper'; import {DarkTheme, DefaultTheme, Theme} from 'react-native-paper';
import AprilFoolsManager from "./AprilFoolsManager"; import AprilFoolsManager from "./AprilFoolsManager";
import {Appearance} from 'react-native-appearance'; import {Appearance} from 'react-native-appearance';
const colorScheme = Appearance.getColorScheme(); const colorScheme = Appearance.getColorScheme();
export type CustomTheme = {
...Theme,
colors: {
primary: string,
accent: string,
tabIcon: string,
card: string,
dividerBackground: string,
ripple: string,
textDisabled: string,
icon: string,
subtitle: string,
success: string,
warning: string,
danger: string,
// Calendar/Agenda
agendaBackgroundColor: string,
agendaDayTextColor: string,
// PROXIWASH
proxiwashFinishedColor: string,
proxiwashReadyColor: string,
proxiwashRunningColor: string,
proxiwashRunningBgColor: string,
proxiwashBrokenColor: string,
proxiwashErrorColor: string,
// Screens
planningColor: string,
proximoColor: string,
proxiwashColor: string,
menuColor: string,
tutorinsaColor: string,
// Tetris
tetrisBackground: string,
tetrisBorder:string,
tetrisScore:string,
tetrisI : string,
tetrisO : string,
tetrisT : string,
tetrisS : string,
tetrisZ : string,
tetrisJ : string,
tetrisL : string,
},
}
/** /**
* Singleton class used to manage themes * Singleton class used to manage themes
*/ */
@ -22,9 +71,9 @@ export default class ThemeManager {
/** /**
* Gets the light theme * Gets the light theme
* *
* @return {Object} Object containing theme variables * @return {CustomTheme} Object containing theme variables
* */ * */
static getWhiteTheme(): Object { static getWhiteTheme(): CustomTheme {
return { return {
...DefaultTheme, ...DefaultTheme,
colors: { colors: {
@ -41,6 +90,7 @@ export default class ThemeManager {
success: "#5cb85c", success: "#5cb85c",
warning: "#f0ad4e", warning: "#f0ad4e",
danger: "#d9534f", danger: "#d9534f",
cc: 'dst',
// Calendar/Agenda // Calendar/Agenda
agendaBackgroundColor: '#f3f3f4', agendaBackgroundColor: '#f3f3f4',
@ -79,9 +129,9 @@ export default class ThemeManager {
/** /**
* Gets the dark theme * Gets the dark theme
* *
* @return {Object} Object containing theme variables * @return {CustomTheme} Object containing theme variables
* */ * */
static getDarkTheme(): Object { static getDarkTheme(): CustomTheme {
return { return {
...DarkTheme, ...DarkTheme,
colors: { colors: {
@ -162,9 +212,9 @@ export default class ThemeManager {
/** /**
* Get the current theme based on night mode and events * Get the current theme based on night mode and events
* *
* @returns {Object} The current theme * @returns {CustomTheme} The current theme
*/ */
static getCurrentTheme(): Object { static getCurrentTheme(): CustomTheme {
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
return AprilFoolsManager.getAprilFoolsTheme(ThemeManager.getWhiteTheme()); return AprilFoolsManager.getAprilFoolsTheme(ThemeManager.getWhiteTheme());
else else
@ -174,9 +224,9 @@ export default class ThemeManager {
/** /**
* Get the theme based on night mode * Get the theme based on night mode
* *
* @return {Object} The theme * @return {CustomTheme} The theme
*/ */
static getBaseTheme() { static getBaseTheme(): CustomTheme {
if (ThemeManager.getNightMode()) if (ThemeManager.getNightMode())
return ThemeManager.getDarkTheme(); return ThemeManager.getDarkTheme();
else else
@ -188,7 +238,7 @@ export default class ThemeManager {
* *
* @param callback Function to call after theme change * @param callback Function to call after theme change
*/ */
setUpdateThemeCallback(callback: ?Function) { setUpdateThemeCallback(callback: () => void) {
this.updateThemeCallback = callback; this.updateThemeCallback = callback;
} }
@ -200,7 +250,7 @@ export default class ThemeManager {
setNightMode(isNightMode: boolean) { setNightMode(isNightMode: boolean) {
let nightModeKey = AsyncStorageManager.getInstance().preferences.nightMode.key; let nightModeKey = AsyncStorageManager.getInstance().preferences.nightMode.key;
AsyncStorageManager.getInstance().savePref(nightModeKey, isNightMode ? '1' : '0'); AsyncStorageManager.getInstance().savePref(nightModeKey, isNightMode ? '1' : '0');
if (this.updateThemeCallback !== null) if (this.updateThemeCallback != null)
this.updateThemeCallback(); this.updateThemeCallback();
} }

View file

@ -1,7 +1,7 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {createDrawerNavigator} from '@react-navigation/drawer'; import {createDrawerNavigator, DrawerNavigationProp} from '@react-navigation/drawer';
import TabNavigator from './MainTabNavigator'; import TabNavigator from './MainTabNavigator';
import SettingsScreen from '../screens/Other/SettingsScreen'; import SettingsScreen from '../screens/Other/SettingsScreen';
import AboutScreen from '../screens/About/AboutScreen'; import AboutScreen from '../screens/About/AboutScreen';
@ -35,7 +35,7 @@ const defaultScreenOptions = {
...TransitionPresets.SlideFromRightIOS, ...TransitionPresets.SlideFromRightIOS,
}; };
function getDrawerButton(navigation: Object) { function getDrawerButton(navigation: DrawerNavigationProp) {
return ( return (
<MaterialHeaderButtons left={true}> <MaterialHeaderButtons left={true}>
<Item title="menu" iconName="menu" onPress={navigation.openDrawer}/> <Item title="menu" iconName="menu" onPress={navigation.openDrawer}/>
@ -415,11 +415,9 @@ function ProfileStackComponent() {
<ClubStack.Screen <ClubStack.Screen
name="club-information" name="club-information"
component={ClubDisplayScreen} component={ClubDisplayScreen}
options={({navigation}) => { options={{
return {
title: i18n.t('screens.clubDisplayScreen'), title: i18n.t('screens.clubDisplayScreen'),
...TransitionPresets.ModalSlideFromBottomIOS, ...TransitionPresets.ModalSlideFromBottomIOS,
};
}} }}
/> />
</ProfileStack.Navigator> </ProfileStack.Navigator>
@ -509,21 +507,17 @@ function ClubStackComponent() {
<ClubStack.Screen <ClubStack.Screen
name="club-information" name="club-information"
component={ClubDisplayScreen} component={ClubDisplayScreen}
options={({navigation}) => { options={{
return {
title: i18n.t('screens.clubDisplayScreen'), title: i18n.t('screens.clubDisplayScreen'),
...TransitionPresets.ModalSlideFromBottomIOS, ...TransitionPresets.ModalSlideFromBottomIOS,
};
}} }}
/> />
<ClubStack.Screen <ClubStack.Screen
name="club-about" name="club-about"
component={ClubAboutScreen} component={ClubAboutScreen}
options={({navigation}) => { options={{
return {
title: i18n.t('screens.clubsAbout'), title: i18n.t('screens.clubsAbout'),
...TransitionPresets.ModalSlideFromBottomIOS, ...TransitionPresets.ModalSlideFromBottomIOS,
};
}} }}
/> />
</ClubStack.Navigator> </ClubStack.Navigator>
@ -533,27 +527,22 @@ function ClubStackComponent() {
const Drawer = createDrawerNavigator(); const Drawer = createDrawerNavigator();
function getDrawerContent(props) {
return <Sidebar {...props}/>
}
type Props = { type Props = {
defaultRoute: string | null, defaultHomeRoute: string | null,
defaultData: Object defaultHomeData: { [key: string]: any }
} }
export default class DrawerNavigator extends React.Component<Props> { export default class DrawerNavigator extends React.Component<Props> {
createTabNavigator: Object; createTabNavigator: () => React.Element<TabNavigator>;
constructor(props: Object) { constructor(props: Props) {
super(props); super(props);
this.createTabNavigator = () => <TabNavigator {...props}/>
this.createTabNavigator = () => <TabNavigator defaultRoute={props.defaultRoute}
defaultData={props.defaultData}/>
} }
getDrawerContent = (props: { navigation: DrawerNavigationProp }) => <Sidebar {...props}/>
render() { render() {
return ( return (
<Drawer.Navigator <Drawer.Navigator
@ -561,7 +550,7 @@ export default class DrawerNavigator extends React.Component<Props> {
headerMode={'float'} headerMode={'float'}
backBehavior={'initialRoute'} backBehavior={'initialRoute'}
drawerType={'front'} drawerType={'front'}
drawerContent={(props) => getDrawerContent(props)} drawerContent={this.getDrawerContent}
screenOptions={defaultScreenOptions} screenOptions={defaultScreenOptions}
> >
<Drawer.Screen <Drawer.Screen

View file

@ -12,7 +12,7 @@ import ProximoListScreen from "../screens/Proximo/ProximoListScreen";
import ProximoAboutScreen from "../screens/Proximo/ProximoAboutScreen"; import ProximoAboutScreen from "../screens/Proximo/ProximoAboutScreen";
import PlanexScreen from '../screens/Planex/PlanexScreen'; import PlanexScreen from '../screens/Planex/PlanexScreen';
import AsyncStorageManager from "../managers/AsyncStorageManager"; import AsyncStorageManager from "../managers/AsyncStorageManager";
import {useTheme, withTheme} from 'react-native-paper'; import {useTheme} from 'react-native-paper';
import i18n from "i18n-js"; import i18n from "i18n-js";
import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen"; import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen";
import ScannerScreen from "../screens/Home/ScannerScreen"; import ScannerScreen from "../screens/Home/ScannerScreen";
@ -21,14 +21,7 @@ import FeedItemScreen from "../screens/Home/FeedItemScreen";
import {createCollapsibleStack} from "react-navigation-collapsible"; import {createCollapsibleStack} from "react-navigation-collapsible";
import GroupSelectionScreen from "../screens/Planex/GroupSelectionScreen"; import GroupSelectionScreen from "../screens/Planex/GroupSelectionScreen";
import CustomTabBar from "../components/Tabbar/CustomTabBar"; import CustomTabBar from "../components/Tabbar/CustomTabBar";
import {DrawerNavigationProp} from "@react-navigation/drawer";
const TAB_ICONS = {
home: 'triangle',
planning: 'calendar-range',
proxiwash: 'tshirt-crew',
proximo: 'cart',
planex: 'clock',
};
const defaultScreenOptions = { const defaultScreenOptions = {
gestureEnabled: true, gestureEnabled: true,
@ -36,7 +29,7 @@ const defaultScreenOptions = {
...TransitionPresets.SlideFromRightIOS, ...TransitionPresets.SlideFromRightIOS,
}; };
function getDrawerButton(navigation: Object) { function getDrawerButton(navigation: DrawerNavigationProp) {
return ( return (
<MaterialHeaderButtons left={true}> <MaterialHeaderButtons left={true}>
<Item title="menu" iconName="menu" onPress={navigation.openDrawer}/> <Item title="menu" iconName="menu" onPress={navigation.openDrawer}/>
@ -147,7 +140,6 @@ function ProxiwashStackComponent() {
const PlanningStack = createStackNavigator(); const PlanningStack = createStackNavigator();
function PlanningStackComponent() { function PlanningStackComponent() {
const {colors} = useTheme();
return ( return (
<PlanningStack.Navigator <PlanningStack.Navigator
initialRouteName="index" initialRouteName="index"
@ -179,10 +171,10 @@ function PlanningStackComponent() {
const HomeStack = createStackNavigator(); const HomeStack = createStackNavigator();
function HomeStackComponent(initialRoute: string | null, defaultData: Object) { function HomeStackComponent(initialRoute: string | null, defaultData: { [key: string]: any }) {
let data; let params = undefined;
if (initialRoute !== null) if (initialRoute != null)
data = {data: defaultData, nextScreen: initialRoute, shouldOpen: true}; params = {data: defaultData, nextScreen: initialRoute, shouldOpen: true};
const {colors} = useTheme(); const {colors} = useTheme();
return ( return (
<HomeStack.Navigator <HomeStack.Navigator
@ -204,7 +196,7 @@ function HomeStackComponent(initialRoute: string | null, defaultData: Object) {
}, },
}; };
}} }}
initialParams={data} initialParams={params}
/>, />,
{ {
collapsedColor: 'transparent', collapsedColor: 'transparent',
@ -222,31 +214,25 @@ function HomeStackComponent(initialRoute: string | null, defaultData: Object) {
<HomeStack.Screen <HomeStack.Screen
name="home-club-information" name="home-club-information"
component={ClubDisplayScreen} component={ClubDisplayScreen}
options={({navigation}) => { options={{
return {
title: i18n.t('screens.clubDisplayScreen'), title: i18n.t('screens.clubDisplayScreen'),
...TransitionPresets.ModalSlideFromBottomIOS, ...TransitionPresets.ModalSlideFromBottomIOS,
};
}} }}
/> />
<HomeStack.Screen <HomeStack.Screen
name="feed-information" name="feed-information"
component={FeedItemScreen} component={FeedItemScreen}
options={({navigation}) => { options={{
return {
title: i18n.t('screens.feedDisplayScreen'), title: i18n.t('screens.feedDisplayScreen'),
...TransitionPresets.ModalSlideFromBottomIOS, ...TransitionPresets.ModalSlideFromBottomIOS,
};
}} }}
/> />
<HomeStack.Screen <HomeStack.Screen
name="scanner" name="scanner"
component={ScannerScreen} component={ScannerScreen}
options={({navigation}) => { options={{
return {
title: i18n.t('screens.scanner'), title: i18n.t('screens.scanner'),
...TransitionPresets.ModalSlideFromBottomIOS, ...TransitionPresets.ModalSlideFromBottomIOS,
};
}} }}
/> />
</HomeStack.Navigator> </HomeStack.Navigator>
@ -287,14 +273,12 @@ function PlanexStackComponent() {
<PlanexStack.Screen <PlanexStack.Screen
name="group-select" name="group-select"
component={GroupSelectionScreen} component={GroupSelectionScreen}
options={({navigation}) => { options={{
return {
title: 'GroupSelectionScreen', title: 'GroupSelectionScreen',
headerStyle: { headerStyle: {
backgroundColor: colors.surface, backgroundColor: colors.surface,
}, },
...TransitionPresets.ModalSlideFromBottomIOS, ...TransitionPresets.ModalSlideFromBottomIOS,
};
}} }}
/>, />,
{ {
@ -309,44 +293,28 @@ function PlanexStackComponent() {
const Tab = createBottomTabNavigator(); const Tab = createBottomTabNavigator();
type Props = { type Props = {
defaultRoute: string | null, defaultHomeRoute: string | null,
defaultData: Object defaultHomeData: { [key: string]: any }
} }
class TabNavigator extends React.Component<Props> { export default class TabNavigator extends React.Component<Props> {
createHomeStackComponent: Object;
createHomeStackComponent: () => HomeStackComponent;
defaultRoute: string; defaultRoute: string;
constructor(props) { constructor(props) {
super(props); super(props);
this.defaultRoute = AsyncStorageManager.getInstance().preferences.defaultStartScreen.current.toLowerCase(); if (props.defaultHomeRoute != null)
if (props.defaultRoute !== null)
this.defaultRoute = 'home'; this.defaultRoute = 'home';
else
this.createHomeStackComponent = () => HomeStackComponent(props.defaultRoute, props.defaultData); this.defaultRoute = AsyncStorageManager.getInstance().preferences.defaultStartScreen.current.toLowerCase();
this.createHomeStackComponent = () => HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData);
} }
render() { render() {
return ( return (
<Tab.Navigator <Tab.Navigator
initialRouteName={this.defaultRoute} initialRouteName={this.defaultRoute}
barStyle={{backgroundColor: this.props.theme.colors.surface, overflow: 'visible'}}
screenOptions={({route}) => ({
tabBarIcon: ({focused, color}) => {
let icon = TAB_ICONS[route.name];
icon = focused ? icon : icon + ('-outline');
if (route.name !== "home")
return icon;
else
return null;
},
tabBarLabel: route.name !== 'home' ? undefined : '',
activeColor: this.props.theme.colors.primary,
inactiveColor: this.props.theme.colors.tabIcon,
})}
tabBar={props => <CustomTabBar {...props} />} tabBar={props => <CustomTabBar {...props} />}
> >
<Tab.Screen <Tab.Screen
@ -379,5 +347,3 @@ class TabNavigator extends React.Component<Props> {
); );
} }
} }
export default withTheme(TabNavigator);

View file

@ -10,53 +10,78 @@ import VoteSelect from "../../components/Amicale/Vote/VoteSelect";
import VoteResults from "../../components/Amicale/Vote/VoteResults"; import VoteResults from "../../components/Amicale/Vote/VoteResults";
import VoteWait from "../../components/Amicale/Vote/VoteWait"; import VoteWait from "../../components/Amicale/Vote/VoteWait";
const FAKE_DATE = { export type team = {
"date_begin": "2020-04-09 15:50", id: number,
"date_end": "2020-04-09 15:50", name: string,
"date_result_begin": "2020-04-09 15:50", votes: number,
"date_result_end": "2020-04-09 22:50", }
type teamResponse = {
has_voted: boolean,
teams: Array<team>,
}; };
const FAKE_DATE2 = { type stringVoteDates = {
"date_begin": null, date_begin: string,
"date_end": null, date_end: string,
"date_result_begin": null, date_result_begin: string,
"date_result_end": null, date_result_end: string,
}; }
const FAKE_TEAMS = { type objectVoteDates = {
has_voted: false, date_begin: Date,
teams: [ date_end: Date,
{ date_result_begin: Date,
id: 1, date_result_end: Date,
name: "TEST TEAM1", }
},
{ // const FAKE_DATE = {
id: 2, // "date_begin": "2020-04-19 15:50",
name: "TEST TEAM2", // "date_end": "2020-04-19 15:50",
}, // "date_result_begin": "2020-04-19 19:50",
], // "date_result_end": "2020-04-19 22:50",
}; // };
const FAKE_TEAMS2 = { //
has_voted: false, // const FAKE_DATE2 = {
teams: [ // "date_begin": null,
{ // "date_end": null,
id: 1, // "date_result_begin": null,
name: "TEST TEAM1", // "date_result_end": null,
votes: 9, // };
}, //
{ // const FAKE_TEAMS = {
id: 2, // has_voted: false,
name: "TEST TEAM2", // teams: [
votes: 9, // {
}, // id: 1,
{ // name: "TEST TEAM1",
id: 3, // },
name: "TEST TEAM3", // {
votes: 5, // id: 2,
}, // name: "TEST TEAM2",
], // },
}; // ],
// };
// const FAKE_TEAMS2 = {
// has_voted: false,
// teams: [
// {
// id: 1,
// name: "TEST TEAM1",
// votes: 9,
// },
// {
// id: 2,
// name: "TEST TEAM2",
// votes: 9,
// },
// {
// id: 3,
// name: "TEST TEAM3",
// votes: 5,
// },
// ],
// };
const MIN_REFRESH_TIME = 5 * 1000; const MIN_REFRESH_TIME = 5 * 1000;
@ -74,17 +99,17 @@ export default class VoteScreen extends React.Component<Props, State> {
hasVoted: false, hasVoted: false,
}; };
teams: Array<Object>; teams: Array<team>;
hasVoted: boolean; hasVoted: boolean;
datesString: Object; datesString: null | stringVoteDates;
dates: Object; dates: null | objectVoteDates;
today: Date; today: Date;
mainFlatListData: Array<Object>; mainFlatListData: Array<{ key: string }>;
lastRefresh: Date; lastRefresh: Date;
authRef: Object; authRef: { current: null | AuthenticatedScreen };
constructor() { constructor() {
super(); super();
@ -103,69 +128,81 @@ export default class VoteScreen extends React.Component<Props, State> {
canRefresh = (new Date().getTime() - this.lastRefresh.getTime()) > MIN_REFRESH_TIME; canRefresh = (new Date().getTime() - this.lastRefresh.getTime()) > MIN_REFRESH_TIME;
else else
canRefresh = true; canRefresh = true;
if (canRefresh) if (canRefresh && this.authRef.current != null)
this.authRef.current.reload() this.authRef.current.reload()
}; };
generateDateObject() { generateDateObject() {
const strings = this.datesString;
if (strings != null) {
const dateBegin = stringToDate(strings.date_begin);
const dateEnd = stringToDate(strings.date_end);
const dateResultBegin = stringToDate(strings.date_result_begin);
const dateResultEnd = stringToDate(strings.date_result_end);
if (dateBegin != null && dateEnd != null && dateResultBegin != null && dateResultEnd != null) {
this.dates = { this.dates = {
date_begin: stringToDate(this.datesString.date_begin), date_begin: dateBegin,
date_end: stringToDate(this.datesString.date_end), date_end: dateEnd,
date_result_begin: stringToDate(this.datesString.date_result_begin), date_result_begin: dateResultBegin,
date_result_end: stringToDate(this.datesString.date_result_end), date_result_end: dateResultEnd,
}; };
} else
this.dates = null;
} else
this.dates = null;
} }
getDateString(date: Date, dateString: string): string { getDateString(date: Date, dateString: string): string {
if (this.today.getDate() === date.getDate()) { if (this.today.getDate() === date.getDate()) {
const str = getTimeOnlyString(dateString); const str = getTimeOnlyString(dateString);
return str !== null ? str : ""; return str != null ? str : "";
} else } else
return dateString; return dateString;
} }
isVoteAvailable() {
return this.dates.date_begin !== null;
}
isVoteRunning() { isVoteRunning() {
return this.today > this.dates.date_begin && this.today < this.dates.date_end; return this.dates != null && this.today > this.dates.date_begin && this.today < this.dates.date_end;
} }
isVoteStarted() { isVoteStarted() {
return this.today > this.dates.date_begin; return this.dates != null && this.today > this.dates.date_begin;
} }
isResultRunning() { isResultRunning() {
return this.today > this.dates.date_result_begin && this.today < this.dates.date_result_end; return this.dates != null && this.today > this.dates.date_result_begin && this.today < this.dates.date_result_end;
} }
isResultStarted() { isResultStarted() {
return this.today > this.dates.date_result_begin; return this.dates != null && this.today > this.dates.date_result_begin;
} }
mainRenderItem = ({item}: Object) => { mainRenderItem = ({item}: Object) => {
if (item.key === 'info') if (item.key === 'info')
return <VoteTitle/>; return <VoteTitle/>;
else if (item.key === 'main' && this.isVoteAvailable()) else if (item.key === 'main' && this.dates != null)
return this.getContent(); return this.getContent();
else else
return null; return null;
}; };
getScreen = (data: Array<Object | null>) => { getScreen = (data: Array<{ [key: string]: any } | null>) => {
// data[0] = FAKE_TEAMS2; // data[0] = FAKE_TEAMS2;
// data[1] = FAKE_DATE; // data[1] = FAKE_DATE;
this.lastRefresh = new Date(); this.lastRefresh = new Date();
if (data[1] === null) const teams : teamResponse | null = data[0];
data[1] = {date_begin: null}; const dateStrings : stringVoteDates | null = data[1];
if (data[0] !== null) { if (dateStrings != null && dateStrings.date_begin == null)
this.teams = data[0].teams; this.datesString = null;
this.hasVoted = data[0].has_voted; else
this.datesString = dateStrings;
if (teams != null) {
this.teams = teams.teams;
this.hasVoted = teams.has_voted;
} }
this.datesString = data[1];
this.generateDateObject(); this.generateDateObject();
return ( return (
<View> <View>
@ -211,15 +248,26 @@ export default class VoteScreen extends React.Component<Props, State> {
* Votes have ended, results can be displayed * Votes have ended, results can be displayed
*/ */
getVoteResultCard() { getVoteResultCard() {
return <VoteResults teams={this.teams} if (this.dates != null && this.datesString != null)
dateEnd={this.getDateString(this.dates.date_result_end, this.datesString.date_result_end)}/>; return <VoteResults
teams={this.teams}
dateEnd={this.getDateString(
this.dates.date_result_end,
this.datesString.date_result_end)}
/>;
else
return null;
} }
/** /**
* Vote will open shortly * Vote will open shortly
*/ */
getTeaseVoteCard() { getTeaseVoteCard() {
return <VoteTease startDate={this.getDateString(this.dates.date_begin, this.datesString.date_begin)}/>; if (this.dates != null && this.datesString != null)
return <VoteTease
startDate={this.getDateString(this.dates.date_begin, this.datesString.date_begin)}/>;
else
return null;
} }
/** /**
@ -227,7 +275,7 @@ export default class VoteScreen extends React.Component<Props, State> {
*/ */
getWaitVoteCard() { getWaitVoteCard() {
let startDate = null; let startDate = null;
if (this.dates.date_result_begin !== null) if (this.dates != null && this.datesString != null && this.dates.date_result_begin != null)
startDate = this.getDateString(this.dates.date_result_begin, this.datesString.date_result_begin); startDate = this.getDateString(this.dates.date_result_begin, this.datesString.date_result_begin);
return <VoteWait startDate={startDate} hasVoted={this.hasVoted || this.state.hasVoted} return <VoteWait startDate={startDate} hasVoted={this.hasVoted || this.state.hasVoted}
justVoted={this.state.hasVoted} justVoted={this.state.hasVoted}

View file

@ -1,7 +1,7 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Animated, FlatList} from 'react-native'; import {FlatList} from 'react-native';
import i18n from "i18n-js"; import i18n from "i18n-js";
import DashboardItem from "../../components/Home/EventDashboardItem"; import DashboardItem from "../../components/Home/EventDashboardItem";
import WebSectionList from "../../components/Screens/WebSectionList"; import WebSectionList from "../../components/Screens/WebSectionList";
@ -14,9 +14,10 @@ import ActionsDashBoardItem from "../../components/Home/ActionsDashboardItem";
import ConnectionManager from "../../managers/ConnectionManager"; import ConnectionManager from "../../managers/ConnectionManager";
import {CommonActions} from '@react-navigation/native'; import {CommonActions} from '@react-navigation/native';
import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton"; import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton";
import {AnimatedValue} from "react-native-reanimated";
import AnimatedFAB from "../../components/Animations/AnimatedFAB"; import AnimatedFAB from "../../components/Animations/AnimatedFAB";
import AnimatedFocusView from "../../components/Animations/AnimatedFocusView"; import AnimatedFocusView from "../../components/Animations/AnimatedFocusView";
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../managers/ThemeManager";
// import DATA from "../dashboard_data.json"; // import DATA from "../dashboard_data.json";
@ -31,30 +32,80 @@ const SECTIONS_ID = [
const REFRESH_TIME = 1000 * 20; // Refresh every 20 seconds const REFRESH_TIME = 1000 * 20; // Refresh every 20 seconds
type Props = { type rawDashboard = {
navigation: Object, news_feed: {
route: Object, data: Array<feedItem>,
theme: Object, },
dashboard: fullDashboard,
} }
type State = { export type feedItem = {
fabPosition: AnimatedValue full_picture: string,
message: string,
permalink_url: string,
created_time: number,
id: string,
};
type fullDashboard = {
today_menu: Array<{ [key: string]: any }>,
proximo_articles: number,
available_machines: {
dryers: number,
washers: number,
},
today_events: Array<{ [key: string]: any }>,
available_tutorials: number,
}
type dashboardItem = {
id: string,
content: Array<{ [key: string]: any }>
};
type dashboardSmallItem = {
id: string,
data: number,
icon: string,
color: string,
onPress: () => void,
isAvailable: boolean
};
export type event = {
id: number,
title: string,
logo: string | null,
date_begin: string,
date_end: string,
description: string,
club: string,
category_id: number,
url: string,
}
type listSection = {
title: string,
data: Array<dashboardItem> | Array<feedItem>,
id: string
};
type Props = {
navigation: StackNavigationProp,
route: { params: any, ... },
theme: CustomTheme,
} }
/** /**
* Class defining the app's home screen * Class defining the app's home screen
*/ */
class HomeScreen extends React.Component<Props, State> { class HomeScreen extends React.Component<Props> {
colors: Object; colors: Object;
isLoggedIn: boolean | null; isLoggedIn: boolean | null;
fabRef: Object; fabRef: { current: null | AnimatedFAB };
state = {
fabPosition: new Animated.Value(0),
};
constructor(props) { constructor(props) {
super(props); super(props);
@ -69,8 +120,8 @@ class HomeScreen extends React.Component<Props, State> {
* @param dateString {string} The Unix Timestamp representation of a date * @param dateString {string} The Unix Timestamp representation of a date
* @return {string} The formatted output date * @return {string} The formatted output date
*/ */
static getFormattedDate(dateString: string) { static getFormattedDate(dateString: number) {
let date = new Date(Number.parseInt(dateString) * 1000); let date = new Date(dateString * 1000);
return date.toLocaleString(); return date.toLocaleString();
} }
@ -92,8 +143,8 @@ class HomeScreen extends React.Component<Props, State> {
}; };
handleNavigationParams = () => { handleNavigationParams = () => {
if (this.props.route.params !== undefined) { if (this.props.route.params != null) {
if (this.props.route.params.nextScreen !== undefined && this.props.route.params.nextScreen !== null) { if (this.props.route.params.nextScreen != null) {
this.props.navigation.navigate(this.props.route.params.nextScreen, this.props.route.params.data); this.props.navigation.navigate(this.props.route.params.nextScreen, this.props.route.params.data);
// reset params to prevent infinite loop // reset params to prevent infinite loop
this.props.navigation.dispatch(CommonActions.setParams({nextScreen: null})); this.props.navigation.dispatch(CommonActions.setParams({nextScreen: null}));
@ -138,14 +189,14 @@ class HomeScreen extends React.Component<Props, State> {
* @param fetchedData * @param fetchedData
* @return {*} * @return {*}
*/ */
createDataset = (fetchedData: Object) => { createDataset = (fetchedData: rawDashboard) => {
// fetchedData = DATA; // fetchedData = DATA;
let newsData = []; let newsData = [];
let dashboardData = []; let dashboardData = [];
if (fetchedData['news_feed'] !== undefined) if (fetchedData.news_feed != null)
newsData = fetchedData['news_feed']['data']; newsData = fetchedData.news_feed.data;
if (fetchedData['dashboard'] !== undefined) if (fetchedData.dashboard != null)
dashboardData = this.generateDashboardDataset(fetchedData['dashboard']); dashboardData = this.generateDashboardDataset(fetchedData.dashboard);
return [ return [
{ {
title: '', title: '',
@ -164,79 +215,61 @@ class HomeScreen extends React.Component<Props, State> {
* Generates the dataset associated to the dashboard to be displayed in the FlatList as a section * Generates the dataset associated to the dashboard to be displayed in the FlatList as a section
* *
* @param dashboardData * @param dashboardData
* @return {*} * @return {Array<dashboardItem>}
*/ */
generateDashboardDataset(dashboardData: Object) { generateDashboardDataset(dashboardData: fullDashboard): Array<dashboardItem> {
let dataset = [ return [
{ {
id: 'top', id: 'top',
content: [] content: [
},
{ {
id: 'actions',
content: undefined
},
{
id: 'event',
content: undefined
},
];
for (let [key, value: number | Object | Array<string>] of Object.entries(dashboardData)) {
switch (key) {
case 'available_machines':
dataset[0]['content'][0] = {
id: 'washers', id: 'washers',
data: value.washers, data: dashboardData.available_machines.washers,
icon: 'washing-machine', icon: 'washing-machine',
color: this.colors.proxiwashColor, color: this.colors.proxiwashColor,
onPress: this.onProxiwashClick, onPress: this.onProxiwashClick,
isAvailable: value.washers > 0 isAvailable: dashboardData.available_machines.washers > 0
}; },
dataset[0]['content'][1] = { {
...dataset[0]['content'][0],
id: 'dryers', id: 'dryers',
data: value.dryers, data: dashboardData.available_machines.dryers,
icon: 'tumble-dryer', icon: 'tumble-dryer',
isAvailable: value.dryers > 0 color: this.colors.proxiwashColor,
}; onPress: this.onProxiwashClick,
break; isAvailable: dashboardData.available_machines.dryers > 0
case 'available_tutorials': },
dataset[0]['content'][2] = { {
id: key, id: 'available_tutorials',
data: value, data: dashboardData.available_tutorials,
icon: 'school', icon: 'school',
color: this.colors.tutorinsaColor, color: this.colors.tutorinsaColor,
onPress: this.onTutorInsaClick, onPress: this.onTutorInsaClick,
isAvailable: parseInt(value) > 0 isAvailable: dashboardData.available_tutorials > 0
}; },
break; {
case 'proximo_articles': id: 'proximo_articles',
dataset[0]['content'][3] = { data: dashboardData.proximo_articles,
id: key,
data: value,
icon: 'shopping', icon: 'shopping',
color: this.colors.proximoColor, color: this.colors.proximoColor,
onPress: this.onProximoClick, onPress: this.onProximoClick,
isAvailable: parseInt(value) > 0 isAvailable: dashboardData.proximo_articles > 0
}; },
break; {
case 'today_menu': id: 'silverware-fork-knife',
dataset[0]['content'][4] = { data: dashboardData.today_menu,
id: key, icon: 'shopping',
data: 0,
icon: 'silverware-fork-knife',
color: this.colors.menuColor, color: this.colors.menuColor,
onPress: this.onMenuClick, onPress: this.onMenuClick,
isAvailable: value.length > 0 isAvailable: dashboardData.today_menu.length > 0
}; },
break; ]
case 'today_events': },
dataset[2]['content'] = value; {id: 'actions', content: []},
break; {
} id: 'event',
} content: dashboardData.today_events
return dataset },
];
} }
/** /**
@ -245,11 +278,11 @@ class HomeScreen extends React.Component<Props, State> {
* @param item The item to display * @param item The item to display
* @return {*} * @return {*}
*/ */
getDashboardItem(item: Object) { getDashboardItem(item: dashboardItem) {
let content = item['content']; let content = item.content;
if (item['id'] === 'event') if (item.id === 'event')
return this.getDashboardEvent(content); return this.getDashboardEvent(content);
else if (item['id'] === 'top') else if (item.id === 'top')
return this.getDashboardRow(content); return this.getDashboardRow(content);
else else
return this.getDashboardActions(); return this.getDashboardActions();
@ -278,14 +311,14 @@ class HomeScreen extends React.Component<Props, State> {
/** /**
* Gets the duration (in milliseconds) of an event * Gets the duration (in milliseconds) of an event
* *
* @param event {Object} * @param event {event}
* @return {number} The number of milliseconds * @return {number} The number of milliseconds
*/ */
getEventDuration(event: Object): number { getEventDuration(event: event): number {
let start = stringToDate(event['date_begin']); let start = stringToDate(event.date_begin);
let end = stringToDate(event['date_end']); let end = stringToDate(event.date_end);
let duration = 0; let duration = 0;
if (start !== undefined && start !== null && end !== undefined && end !== null) if (start != null && end != null)
duration = end - start; duration = end - start;
return duration; return duration;
} }
@ -297,11 +330,11 @@ class HomeScreen extends React.Component<Props, State> {
* @param limit * @param limit
* @return {Array<Object>} * @return {Array<Object>}
*/ */
getEventsAfterLimit(events: Object, limit: Date): Array<Object> { getEventsAfterLimit(events: Array<event>, limit: Date): Array<event> {
let validEvents = []; let validEvents = [];
for (let event of events) { for (let event of events) {
let startDate = stringToDate(event['date_begin']); let startDate = stringToDate(event.date_begin);
if (startDate !== undefined && startDate !== null && startDate >= limit) { if (startDate != null && startDate >= limit) {
validEvents.push(event); validEvents.push(event);
} }
} }
@ -314,7 +347,7 @@ class HomeScreen extends React.Component<Props, State> {
* *
* @param events * @param events
*/ */
getLongestEvent(events: Array<Object>): Object { getLongestEvent(events: Array<event>): event {
let longestEvent = events[0]; let longestEvent = events[0];
let longestTime = 0; let longestTime = 0;
for (let event of events) { for (let event of events) {
@ -332,16 +365,16 @@ class HomeScreen extends React.Component<Props, State> {
* *
* @param events * @param events
*/ */
getFutureEvents(events: Array<Object>): Array<Object> { getFutureEvents(events: Array<event>): Array<event> {
let validEvents = []; let validEvents = [];
let now = new Date(); let now = new Date();
for (let event of events) { for (let event of events) {
let startDate = stringToDate(event['date_begin']); let startDate = stringToDate(event.date_begin);
let endDate = stringToDate(event['date_end']); let endDate = stringToDate(event.date_end);
if (startDate !== undefined && startDate !== null) { if (startDate != null) {
if (startDate > now) if (startDate > now)
validEvents.push(event); validEvents.push(event);
else if (endDate !== undefined && endDate !== null) { else if (endDate != null) {
if (endDate > now || endDate < startDate) // Display event if it ends the following day if (endDate > now || endDate < startDate) // Display event if it ends the following day
validEvents.push(event); validEvents.push(event);
} }
@ -356,8 +389,8 @@ class HomeScreen extends React.Component<Props, State> {
* @param events * @param events
* @return {Object} * @return {Object}
*/ */
getDisplayEvent(events: Array<Object>): Object { getDisplayEvent(events: Array<event>): event | null {
let displayEvent = undefined; let displayEvent = null;
if (events.length > 1) { if (events.length > 1) {
let eventsAfterLimit = this.getEventsAfterLimit(events, this.getTodayEventTimeLimit()); let eventsAfterLimit = this.getEventsAfterLimit(events, this.getTodayEventTimeLimit());
if (eventsAfterLimit.length > 0) { if (eventsAfterLimit.length > 0) {
@ -383,7 +416,7 @@ class HomeScreen extends React.Component<Props, State> {
* @param content * @param content
* @return {*} * @return {*}
*/ */
getDashboardEvent(content: Array<Object>) { getDashboardEvent(content: Array<event>) {
let futureEvents = this.getFutureEvents(content); let futureEvents = this.getFutureEvents(content);
let displayEvent = this.getDisplayEvent(futureEvents); let displayEvent = this.getDisplayEvent(futureEvents);
const clickPreviewAction = () => const clickPreviewAction = () =>
@ -394,14 +427,14 @@ class HomeScreen extends React.Component<Props, State> {
clickAction={this.onEventContainerClick} clickAction={this.onEventContainerClick}
> >
<PreviewEventDashboardItem <PreviewEventDashboardItem
event={displayEvent} event={displayEvent != null ? displayEvent : undefined}
clickAction={clickPreviewAction} clickAction={clickPreviewAction}
/> />
</DashboardItem> </DashboardItem>
); );
} }
dashboardRowRenderItem = ({item}: Object) => { dashboardRowRenderItem = ({item}: { item: dashboardSmallItem }) => {
return ( return (
<SquareDashboardItem <SquareDashboardItem
color={item.color} color={item.color}
@ -419,8 +452,10 @@ class HomeScreen extends React.Component<Props, State> {
* @param content * @param content
* @return {*} * @return {*}
*/ */
getDashboardRow(content: Array<Object>) { getDashboardRow(content: Array<dashboardSmallItem>) {
return <FlatList return (
//$FlowFixMe
<FlatList
data={content} data={content}
renderItem={this.dashboardRowRenderItem} renderItem={this.dashboardRowRenderItem}
horizontal={true} horizontal={true}
@ -428,7 +463,7 @@ class HomeScreen extends React.Component<Props, State> {
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
}} }}
/>; />);
} }
/** /**
@ -437,7 +472,7 @@ class HomeScreen extends React.Component<Props, State> {
* @param item The feed item to display * @param item The feed item to display
* @return {*} * @return {*}
*/ */
getFeedItem(item: Object) { getFeedItem(item: feedItem) {
return ( return (
<FeedItem <FeedItem
{...this.props} {...this.props}
@ -456,27 +491,34 @@ class HomeScreen extends React.Component<Props, State> {
* @param section The current section * @param section The current section
* @return {*} * @return {*}
*/ */
getRenderItem = ({item, section}: Object) => { getRenderItem = ({item, section}: {
return (section['id'] === SECTIONS_ID[0] item: { [key: string]: any },
? this.getDashboardItem(item) section: listSection
: this.getFeedItem(item)); }) => {
if (section.id === SECTIONS_ID[0]) {
const data: dashboardItem = item;
return this.getDashboardItem(data);
} else {
const data: feedItem = item;
return this.getFeedItem(data);
}
}; };
openScanner = () => this.props.navigation.navigate("scanner"); openScanner = () => this.props.navigation.navigate("scanner");
onScroll = (event: Object) => { onScroll = (event: SyntheticEvent<EventTarget>) => {
if (this.fabRef.current != null)
this.fabRef.current.onScroll(event); this.fabRef.current.onScroll(event);
}; };
render() { render() {
const nav = this.props.navigation;
return ( return (
<AnimatedFocusView <AnimatedFocusView
{...this.props} {...this.props}
> >
<WebSectionList <WebSectionList
{...this.props}
createDataset={this.createDataset} createDataset={this.createDataset}
navigation={nav}
autoRefreshTime={REFRESH_TIME} autoRefreshTime={REFRESH_TIME}
refreshOnFocus={true} refreshOnFocus={true}
fetchUrl={DATA_URL} fetchUrl={DATA_URL}

View file

@ -8,6 +8,7 @@ import AsyncStorageManager from "../../managers/AsyncStorageManager";
import {setMachineReminderNotificationTime} from "../../utils/Notifications"; import {setMachineReminderNotificationTime} from "../../utils/Notifications";
import {Card, List, Switch, ToggleButton} from 'react-native-paper'; import {Card, List, Switch, ToggleButton} from 'react-native-paper';
import {Appearance} from "react-native-appearance"; import {Appearance} from "react-native-appearance";
import AnimatedAccordion from "../../components/Animations/AnimatedAccordion";
type Props = { type Props = {
navigation: Object, navigation: Object,
@ -89,6 +90,7 @@ export default class SettingsScreen extends React.Component<Props, State> {
<ToggleButton.Row <ToggleButton.Row
onValueChange={this.onProxiwashNotifPickerValueChange} onValueChange={this.onProxiwashNotifPickerValueChange}
value={this.state.proxiwashNotifPickerSelected} value={this.state.proxiwashNotifPickerSelected}
style={{marginLeft: 'auto', marginRight: 'auto'}}
> >
<ToggleButton icon="close" value="never"/> <ToggleButton icon="close" value="never"/>
<ToggleButton icon="numeric-2" value="2"/> <ToggleButton icon="numeric-2" value="2"/>
@ -107,6 +109,7 @@ export default class SettingsScreen extends React.Component<Props, State> {
<ToggleButton.Row <ToggleButton.Row
onValueChange={this.onStartScreenPickerValueChange} onValueChange={this.onStartScreenPickerValueChange}
value={this.state.startScreenPickerSelected} value={this.state.startScreenPickerSelected}
style={{marginLeft: 'auto', marginRight: 'auto'}}
> >
<ToggleButton icon="shopping" value="proximo"/> <ToggleButton icon="shopping" value="proximo"/>
<ToggleButton icon="calendar-range" value="planning"/> <ToggleButton icon="calendar-range" value="planning"/>
@ -188,25 +191,25 @@ export default class SettingsScreen extends React.Component<Props, State> {
this.state.nightMode this.state.nightMode
) : null ) : null
} }
<List.Accordion <AnimatedAccordion
title={i18n.t('settingsScreen.startScreen')} title={i18n.t('settingsScreen.startScreen')}
description={i18n.t('settingsScreen.startScreenSub')} subtitle={i18n.t('settingsScreen.startScreenSub')}
left={props => <List.Icon {...props} icon="power"/>} left={props => <List.Icon {...props} icon="power"/>}
> >
{this.getStartScreenPicker()} {this.getStartScreenPicker()}
</List.Accordion> </AnimatedAccordion>
</List.Section> </List.Section>
</Card> </Card>
<Card style={{margin: 5}}> <Card style={{margin: 5}}>
<Card.Title title="Proxiwash"/> <Card.Title title="Proxiwash"/>
<List.Section> <List.Section>
<List.Accordion <AnimatedAccordion
title={i18n.t('settingsScreen.proxiwashNotifReminder')} title={i18n.t('settingsScreen.proxiwashNotifReminder')}
description={i18n.t('settingsScreen.proxiwashNotifReminderSub')} description={i18n.t('settingsScreen.proxiwashNotifReminderSub')}
left={props => <List.Icon {...props} icon="washing-machine"/>} left={props => <List.Icon {...props} icon="washing-machine"/>}
> >
{this.getProxiwashNotifPicker()} {this.getProxiwashNotifPicker()}
</List.Accordion> </AnimatedAccordion>
</List.Section> </List.Section>
</Card> </Card>
</ScrollView> </ScrollView>

View file

@ -14,12 +14,13 @@ export const withCollapsible = (Component: any) => {
progress, progress,
opacity, opacity,
} = useCollapsibleStack(); } = useCollapsibleStack();
const statusbarHeight = StatusBar.currentHeight != null ? StatusBar.currentHeight : 0;
return <Component return <Component
collapsibleStack={{ collapsibleStack={{
onScroll, onScroll,
onScrollWithListener, onScrollWithListener,
containerPaddingTop: containerPaddingTop - StatusBar.currentHeight, containerPaddingTop: containerPaddingTop - statusbarHeight,
scrollIndicatorInsetTop: scrollIndicatorInsetTop - StatusBar.currentHeight, scrollIndicatorInsetTop: scrollIndicatorInsetTop - statusbarHeight,
translateY, translateY,
progress, progress,
opacity, opacity,