Improve Home components to match linter

This commit is contained in:
Arnaud Vergnet 2020-08-03 18:36:52 +02:00
parent 34ccf9c4c9
commit 6b12b4cde2
9 changed files with 1273 additions and 1198 deletions

View file

@ -2,35 +2,44 @@
import * as React from 'react'; import * as React from 'react';
import {List, withTheme} from 'react-native-paper'; import {List, withTheme} from 'react-native-paper';
import {View} from "react-native"; import {View} from 'react-native';
import type {CustomTheme} from "../../managers/ThemeManager";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {StackNavigationProp} from "@react-navigation/stack"; import {StackNavigationProp} from '@react-navigation/stack';
import type {CustomTheme} from '../../managers/ThemeManager';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, theme: CustomTheme,
};
class ActionsDashBoardItem extends React.Component<PropsType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return nextProps.theme.dark !== props.theme.dark;
} }
class ActionsDashBoardItem extends React.Component<Props> { render(): React.Node {
const {props} = this;
shouldComponentUpdate(nextProps: Props): boolean {
return (nextProps.theme.dark !== this.props.theme.dark);
}
render() {
return ( return (
<View> <View>
<List.Item <List.Item
title={i18n.t("screens.feedback.homeButtonTitle")} title={i18n.t('screens.feedback.homeButtonTitle')}
description={i18n.t("screens.feedback.homeButtonSubtitle")} description={i18n.t('screens.feedback.homeButtonSubtitle')}
left={props => <List.Icon {...props} icon={"comment-quote"}/>} left={({size}: {size: number}): React.Node => (
right={props => <List.Icon {...props} icon={"chevron-right"}/>} <List.Icon size={size} icon="comment-quote" />
onPress={() => this.props.navigation.navigate("feedback")} )}
style={{paddingTop: 0, paddingBottom: 0, marginLeft: 10, marginRight: 10}} right={({size}: {size: number}): React.Node => (
<List.Icon size={size} icon="chevron-right" />
)}
onPress={(): void => props.navigation.navigate('feedback')}
style={{
paddingTop: 0,
paddingBottom: 0,
marginLeft: 10,
marginRight: 10,
}}
/> />
</View> </View>
); );
} }
} }

View file

@ -1,79 +1,23 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Avatar, Card, Text, TouchableRipple, withTheme} from 'react-native-paper'; import {
import {StyleSheet, View} from "react-native"; Avatar,
import i18n from "i18n-js"; Card,
import type {CustomTheme} from "../../managers/ThemeManager"; Text,
TouchableRipple,
withTheme,
} from 'react-native-paper';
import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js';
import type {CustomTheme} from '../../managers/ThemeManager';
type Props = { type PropsType = {
eventNumber: number; eventNumber: number,
clickAction: () => void, clickAction: () => void,
theme: CustomTheme, theme: CustomTheme,
children?: React.Node children?: React.Node,
} };
/**
* Component used to display a dashboard item containing a preview event
*/
class EventDashBoardItem extends React.Component<Props> {
shouldComponentUpdate(nextProps: Props) {
return (nextProps.theme.dark !== this.props.theme.dark)
|| (nextProps.eventNumber !== this.props.eventNumber);
}
render() {
const props = this.props;
const colors = props.theme.colors;
const isAvailable = props.eventNumber > 0;
const iconColor = isAvailable ?
colors.planningColor :
colors.textDisabled;
const textColor = isAvailable ?
colors.text :
colors.textDisabled;
let subtitle;
if (isAvailable) {
subtitle =
<Text>
<Text style={{fontWeight: "bold"}}>{props.eventNumber}</Text>
<Text>
{props.eventNumber > 1
? i18n.t('screens.home.dashboard.todayEventsSubtitlePlural')
: i18n.t('screens.home.dashboard.todayEventsSubtitle')}
</Text>
</Text>;
} else
subtitle = i18n.t('screens.home.dashboard.todayEventsSubtitleNA');
return (
<Card style={styles.card}>
<TouchableRipple
style={{flex: 1}}
onPress={props.clickAction}>
<View>
<Card.Title
title={i18n.t('screens.home.dashboard.todayEventsTitle')}
titleStyle={{color: textColor}}
subtitle={subtitle}
subtitleStyle={{color: textColor}}
left={() =>
<Avatar.Icon
icon={'calendar-range'}
color={iconColor}
size={60}
style={styles.avatar}/>}
/>
<Card.Content>
{props.children}
</Card.Content>
</View>
</TouchableRipple>
</Card>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
@ -84,8 +28,69 @@ const styles = StyleSheet.create({
overflow: 'hidden', overflow: 'hidden',
}, },
avatar: { avatar: {
backgroundColor: 'transparent' backgroundColor: 'transparent',
} },
}); });
/**
* Component used to display a dashboard item containing a preview event
*/
class EventDashBoardItem extends React.Component<PropsType> {
static defaultProps = {
children: null,
};
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return (
nextProps.theme.dark !== props.theme.dark ||
nextProps.eventNumber !== props.eventNumber
);
}
render(): React.Node {
const {props} = this;
const {colors} = props.theme;
const isAvailable = props.eventNumber > 0;
const iconColor = isAvailable ? colors.planningColor : colors.textDisabled;
const textColor = isAvailable ? colors.text : colors.textDisabled;
let subtitle;
if (isAvailable) {
subtitle = (
<Text>
<Text style={{fontWeight: 'bold'}}>{props.eventNumber}</Text>
<Text>
{props.eventNumber > 1
? i18n.t('screens.home.dashboard.todayEventsSubtitlePlural')
: i18n.t('screens.home.dashboard.todayEventsSubtitle')}
</Text>
</Text>
);
} else subtitle = i18n.t('screens.home.dashboard.todayEventsSubtitleNA');
return (
<Card style={styles.card}>
<TouchableRipple style={{flex: 1}} onPress={props.clickAction}>
<View>
<Card.Title
title={i18n.t('screens.home.dashboard.todayEventsTitle')}
titleStyle={{color: textColor}}
subtitle={subtitle}
subtitleStyle={{color: textColor}}
left={(): React.Node => (
<Avatar.Icon
icon="calendar-range"
color={iconColor}
size={60}
style={styles.avatar}
/>
)}
/>
<Card.Content>{props.children}</Card.Content>
</View>
</TouchableRipple>
</Card>
);
}
}
export default withTheme(EventDashBoardItem); export default withTheme(EventDashBoardItem);

View file

@ -2,67 +2,47 @@
import * as React from 'react'; import * as React from 'react';
import {Button, Card, Text, TouchableRipple} from 'react-native-paper'; import {Button, Card, Text, TouchableRipple} from 'react-native-paper';
import {Image, View} from "react-native"; import {Image, 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 {StackNavigationProp} from '@react-navigation/stack';
import type {CustomTheme} from "../../managers/ThemeManager"; import type {FeedItemType} from '../../screens/Home/HomeScreen';
import type {feedItem} from "../../screens/Home/HomeScreen";
const ICON_AMICALE = require('../../../assets/amicale.png'); const ICON_AMICALE = require('../../../assets/amicale.png');
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, item: FeedItemType,
item: feedItem,
title: string, title: string,
subtitle: string, subtitle: string,
height: number, height: number,
} };
/** /**
* Component used to display a feed item * Component used to display a feed item
*/ */
class FeedItem extends React.Component<Props> { class FeedItem extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
shouldComponentUpdate() {
return false; return false;
} }
/**
* Gets the amicale INSAT logo
*
* @return {*}
*/
getAvatar() {
return (
<Image
size={48}
source={ICON_AMICALE}
style={{
width: 48,
height: 48,
}}/>
);
}
onPress = () => { onPress = () => {
this.props.navigation.navigate( const {props} = this;
'feed-information', props.navigation.navigate('feed-information', {
{ data: props.item,
data: this.props.item, date: props.subtitle,
date: this.props.subtitle
}); });
}; };
render() { render(): React.Node {
const item = this.props.item; const {props} = this;
const hasImage = item.full_picture !== '' && item.full_picture !== undefined; const {item} = props;
const hasImage =
item.full_picture !== '' && item.full_picture !== undefined;
const cardMargin = 10; const cardMargin = 10;
const cardHeight = this.props.height - 2 * cardMargin; const cardHeight = props.height - 2 * cardMargin;
const imageSize = 250; const imageSize = 250;
const titleHeight = 80; const titleHeight = 80;
const actionsHeight = 60; const actionsHeight = 60;
@ -74,23 +54,29 @@ class FeedItem extends React.Component<Props> {
style={{ style={{
margin: cardMargin, margin: cardMargin,
height: cardHeight, height: cardHeight,
}} }}>
> <TouchableRipple style={{flex: 1}} onPress={this.onPress}>
<TouchableRipple
style={{flex: 1}}
onPress={this.onPress}>
<View> <View>
<Card.Title <Card.Title
title={this.props.title} title={props.title}
subtitle={this.props.subtitle} subtitle={props.subtitle}
left={this.getAvatar} left={(): React.Node => (
<Image
size={48}
source={ICON_AMICALE}
style={{
width: 48,
height: 48,
}}
/>
)}
style={{height: titleHeight}} style={{height: titleHeight}}
/> />
{hasImage ? {hasImage ? (
<View style={{marginLeft: 'auto', marginRight: 'auto'}}> <View style={{marginLeft: 'auto', marginRight: 'auto'}}>
<ImageModal <ImageModal
resizeMode="contain" resizeMode="contain"
imageBackgroundColor={"#000"} imageBackgroundColor="#000"
style={{ style={{
width: imageSize, width: imageSize,
height: imageSize, height: imageSize,
@ -98,21 +84,23 @@ class FeedItem extends React.Component<Props> {
source={{ source={{
uri: item.full_picture, uri: item.full_picture,
}} }}
/></View> : null} />
</View>
) : null}
<Card.Content> <Card.Content>
{item.message !== undefined ? {item.message !== undefined ? (
<Autolink <Autolink
text={item.message} text={item.message}
hashtag="facebook" hashtag="facebook"
component={Text} component={Text}
style={{height: textHeight}} style={{height: textHeight}}
/> : null />
} ) : null}
</Card.Content> </Card.Content>
<Card.Actions style={{height: actionsHeight}}> <Card.Actions style={{height: actionsHeight}}>
<Button <Button
onPress={this.onPress} onPress={this.onPress}
icon={'plus'} icon="plus"
style={{marginLeft: 'auto'}}> style={{marginLeft: 'auto'}}>
{i18n.t('screens.home.dashboard.seeMore')} {i18n.t('screens.home.dashboard.seeMore')}
</Button> </Button>

View file

@ -1,81 +1,21 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {StyleSheet, View} from "react-native"; import {StyleSheet, View} from 'react-native';
import i18n from "i18n-js"; import i18n from 'i18n-js';
import {Avatar, Button, Card, TouchableRipple} from 'react-native-paper'; import {Avatar, Button, Card, TouchableRipple} 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 {EventType} from '../../screens/Home/HomeScreen';
import type {event} from "../../screens/Home/HomeScreen";
type Props = { type PropsType = {
event?: event, event?: EventType | null,
clickAction: () => void, clickAction: () => void,
theme?: CustomTheme, };
}
/**
* Component used to display an event preview if an event is available
*/
class PreviewEventDashboardItem extends React.Component<Props> {
render() {
const props = this.props;
const isEmpty = props.event == null
? true
: isDescriptionEmpty(props.event.description);
if (props.event != null) {
const event = props.event;
const hasImage = event.logo !== '' && event.logo != null;
const getImage = () => <Avatar.Image
source={{uri: event.logo}}
size={50}
style={styles.avatar}/>;
return (
<Card
style={styles.card}
elevation={3}
>
<TouchableRipple
style={{flex: 1}}
onPress={props.clickAction}>
<View>
{hasImage ?
<Card.Title
title={event.title}
subtitle={getFormattedEventTime(event.date_begin, event.date_end)}
left={getImage}
/> :
<Card.Title
title={event.title}
subtitle={getFormattedEventTime(event.date_begin, event.date_end)}
/>}
{!isEmpty ?
<Card.Content style={styles.content}>
<CustomHTML html={event.description}/>
</Card.Content> : null}
<Card.Actions style={styles.actions}>
<Button
icon={'chevron-right'}
>
{i18n.t("screens.home.dashboard.seeMore")}
</Button>
</Card.Actions>
</View>
</TouchableRipple>
</Card>
);
} else
return null;
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
marginBottom: 10 marginBottom: 10,
}, },
content: { content: {
maxHeight: 150, maxHeight: 150,
@ -84,11 +24,77 @@ const styles = StyleSheet.create({
actions: { actions: {
marginLeft: 'auto', marginLeft: 'auto',
marginTop: 'auto', marginTop: 'auto',
flexDirection: 'row' flexDirection: 'row',
}, },
avatar: { avatar: {
backgroundColor: 'transparent' backgroundColor: 'transparent',
} },
}); });
/**
* Component used to display an event preview if an event is available
*/
// eslint-disable-next-line react/prefer-stateless-function
class PreviewEventDashboardItem extends React.Component<PropsType> {
static defaultProps = {
event: null,
};
render(): React.Node {
const {props} = this;
const {event} = props;
const isEmpty =
event == null ? true : isDescriptionEmpty(event.description);
if (event != null) {
const hasImage = event.logo !== '' && event.logo != null;
const getImage = (): React.Node => (
<Avatar.Image
source={{uri: event.logo}}
size={50}
style={styles.avatar}
/>
);
return (
<Card style={styles.card} elevation={3}>
<TouchableRipple style={{flex: 1}} onPress={props.clickAction}>
<View>
{hasImage ? (
<Card.Title
title={event.title}
subtitle={getFormattedEventTime(
event.date_begin,
event.date_end,
)}
left={getImage}
/>
) : (
<Card.Title
title={event.title}
subtitle={getFormattedEventTime(
event.date_begin,
event.date_end,
)}
/>
)}
{!isEmpty ? (
<Card.Content style={styles.content}>
<CustomHTML html={event.description} />
</Card.Content>
) : null}
<Card.Actions style={styles.actions}>
<Button icon="chevron-right">
{i18n.t('screens.home.dashboard.seeMore')}
</Button>
</Card.Actions>
</View>
</TouchableRipple>
</Card>
);
}
return null;
}
}
export default PreviewEventDashboardItem; export default PreviewEventDashboardItem;

View file

@ -2,13 +2,13 @@
import * as React from 'react'; import * as React from 'react';
import {Badge, TouchableRipple, withTheme} from 'react-native-paper'; import {Badge, TouchableRipple, withTheme} from 'react-native-paper';
import {Dimensions, Image, View} from "react-native"; import {Dimensions, Image, View} from 'react-native';
import type {CustomTheme} from "../../managers/ThemeManager"; import * as Animatable from 'react-native-animatable';
import * as Animatable from "react-native-animatable"; import type {CustomTheme} from '../../managers/ThemeManager';
type Props = { type PropsType = {
image: string, image: string | null,
onPress: () => void, onPress: () => void | null,
badgeCount: number | null, badgeCount: number | null,
theme: CustomTheme, theme: CustomTheme,
}; };
@ -18,50 +18,51 @@ const AnimatableBadge = Animatable.createAnimatableComponent(Badge);
/** /**
* Component used to render a small dashboard item * Component used to render a small dashboard item
*/ */
class SmallDashboardItem extends React.Component<Props> { class SmallDashboardItem extends React.Component<PropsType> {
itemSize: number; itemSize: number;
constructor(props: Props) { constructor(props: PropsType) {
super(props); super(props);
this.itemSize = Dimensions.get('window').width / 8; this.itemSize = Dimensions.get('window').width / 8;
} }
shouldComponentUpdate(nextProps: Props) { shouldComponentUpdate(nextProps: PropsType): boolean {
return (nextProps.theme.dark !== this.props.theme.dark) const {props} = this;
|| (nextProps.badgeCount !== this.props.badgeCount); return (
nextProps.theme.dark !== props.theme.dark ||
nextProps.badgeCount !== props.badgeCount
);
} }
render() { render(): React.Node {
const props = this.props; const {props} = this;
return ( return (
<TouchableRipple <TouchableRipple
onPress={this.props.onPress} onPress={props.onPress}
borderless={true} borderless
style={{ style={{
marginLeft: this.itemSize / 6, marginLeft: this.itemSize / 6,
marginRight: this.itemSize / 6, marginRight: this.itemSize / 6,
}} }}>
> <View
<View style={{ style={{
width: this.itemSize, width: this.itemSize,
height: this.itemSize, height: this.itemSize,
}}> }}>
<Image <Image
source={{uri: props.image}} source={{uri: props.image}}
style={{ style={{
width: "80%", width: '80%',
height: "80%", height: '80%',
marginLeft: "auto", marginLeft: 'auto',
marginRight: "auto", marginRight: 'auto',
marginTop: "auto", marginTop: 'auto',
marginBottom: "auto", marginBottom: 'auto',
}} }}
/> />
{ {props.badgeCount != null && props.badgeCount > 0 ? (
props.badgeCount != null && props.badgeCount > 0 ?
<AnimatableBadge <AnimatableBadge
animation={"zoomIn"} animation="zoomIn"
duration={300} duration={300}
useNativeDriver useNativeDriver
style={{ style={{
@ -73,14 +74,12 @@ class SmallDashboardItem extends React.Component<Props> {
borderWidth: 2, borderWidth: 2,
}}> }}>
{props.badgeCount} {props.badgeCount}
</AnimatableBadge> : null </AnimatableBadge>
} ) : null}
</View> </View>
</TouchableRipple> </TouchableRipple>
); );
} }
} }
export default withTheme(SmallDashboardItem); export default withTheme(SmallDashboardItem);

View file

@ -4,37 +4,41 @@ import * as React from 'react';
import {Linking, View} from 'react-native'; import {Linking, View} from 'react-native';
import {Avatar, Card, Text, withTheme} from 'react-native-paper'; import {Avatar, Card, Text, withTheme} from 'react-native-paper';
import ImageModal from 'react-native-image-modal'; import ImageModal from 'react-native-image-modal';
import Autolink from "react-native-autolink"; import Autolink from 'react-native-autolink';
import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton"; import {StackNavigationProp} from '@react-navigation/stack';
import CustomTabBar from "../../components/Tabbar/CustomTabBar"; import MaterialHeaderButtons, {
import {StackNavigationProp} from "@react-navigation/stack"; Item,
import type {feedItem} from "./HomeScreen"; } from '../../components/Overrides/CustomHeaderButton';
import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView"; import CustomTabBar from '../../components/Tabbar/CustomTabBar';
import type {FeedItemType} from './HomeScreen';
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: { params: { data: feedItem, date: string } } route: {params: {data: FeedItemType, date: string}},
}; };
const ICON_AMICALE = require('../../../assets/amicale.png'); const ICON_AMICALE = require('../../../assets/amicale.png');
const NAME_AMICALE = 'Amicale INSA Toulouse'; const NAME_AMICALE = 'Amicale INSA Toulouse';
/** /**
* Class defining a feed item page. * Class defining a feed item page.
*/ */
class FeedItemScreen extends React.Component<Props> { class FeedItemScreen extends React.Component<PropsType> {
displayData: FeedItemType;
displayData: feedItem;
date: string; date: string;
constructor(props) { constructor(props: PropsType) {
super(props); super(props);
this.displayData = props.route.params.data; this.displayData = props.route.params.data;
this.date = props.route.params.date; this.date = props.route.params.date;
} }
componentDidMount() { componentDidMount() {
this.props.navigation.setOptions({ const {props} = this;
props.navigation.setOptions({
headerRight: this.getHeaderButton, headerRight: this.getHeaderButton,
}); });
} }
@ -51,41 +55,41 @@ class FeedItemScreen extends React.Component<Props> {
* *
* @returns {*} * @returns {*}
*/ */
getHeaderButton = () => { getHeaderButton = (): React.Node => {
return <MaterialHeaderButtons> return (
<Item title="main" iconName={'facebook'} color={"#2e88fe"} onPress={this.onOutLinkPress}/> <MaterialHeaderButtons>
</MaterialHeaderButtons>; <Item
title="main"
iconName="facebook"
color="#2e88fe"
onPress={this.onOutLinkPress}
/>
</MaterialHeaderButtons>
);
}; };
/** render(): React.Node {
* Gets the Amicale INSA avatar const hasImage =
* this.displayData.full_picture !== '' &&
* @returns {*} this.displayData.full_picture != null;
*/
getAvatar() {
return ( return (
<Avatar.Image size={48} source={ICON_AMICALE} <CollapsibleScrollView style={{margin: 5}} hasTab>
style={{backgroundColor: 'transparent'}}/>
);
}
render() {
const hasImage = this.displayData.full_picture !== '' && this.displayData.full_picture != null;
return (
<CollapsibleScrollView
style={{margin: 5,}}
hasTab={true}
>
<Card.Title <Card.Title
title={NAME_AMICALE} title={NAME_AMICALE}
subtitle={this.date} subtitle={this.date}
left={this.getAvatar} left={(): React.Node => (
<Avatar.Image
size={48}
source={ICON_AMICALE}
style={{backgroundColor: 'transparent'}}
/> />
{hasImage ? )}
/>
{hasImage ? (
<View style={{marginLeft: 'auto', marginRight: 'auto'}}> <View style={{marginLeft: 'auto', marginRight: 'auto'}}>
<ImageModal <ImageModal
resizeMode="contain" resizeMode="contain"
imageBackgroundColor={"#000"} imageBackgroundColor="#000"
style={{ style={{
width: 250, width: 250,
height: 250, height: 250,
@ -93,15 +97,17 @@ class FeedItemScreen extends React.Component<Props> {
source={{ source={{
uri: this.displayData.full_picture, uri: this.displayData.full_picture,
}} }}
/></View> : null} />
</View>
) : null}
<Card.Content style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}> <Card.Content style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
{this.displayData.message !== undefined ? {this.displayData.message !== undefined ? (
<Autolink <Autolink
text={this.displayData.message} text={this.displayData.message}
hashtag="facebook" hashtag="facebook"
component={Text} component={Text}
/> : null />
} ) : null}
</Card.Content> </Card.Content>
</CollapsibleScrollView> </CollapsibleScrollView>
); );

View file

@ -2,52 +2,44 @@
import * as React from 'react'; import * as React from 'react';
import {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 WebSectionList from "../../components/Screens/WebSectionList";
import {ActivityIndicator, Headline, withTheme} from 'react-native-paper'; import {ActivityIndicator, Headline, withTheme} from 'react-native-paper';
import FeedItem from "../../components/Home/FeedItem";
import SmallDashboardItem from "../../components/Home/SmallDashboardItem";
import PreviewEventDashboardItem from "../../components/Home/PreviewEventDashboardItem";
import {stringToDate} from "../../utils/Planning";
import ActionsDashBoardItem from "../../components/Home/ActionsDashboardItem";
import {CommonActions} from '@react-navigation/native'; import {CommonActions} from '@react-navigation/native';
import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton"; import {StackNavigationProp} from '@react-navigation/stack';
import AnimatedFAB from "../../components/Animations/AnimatedFAB"; import * as Animatable from 'react-native-animatable';
import {StackNavigationProp} from "@react-navigation/stack"; import {View} from 'react-native-animatable';
import type {CustomTheme} from "../../managers/ThemeManager"; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import * as Animatable from "react-native-animatable"; import DashboardItem from '../../components/Home/EventDashboardItem';
import {View} from "react-native-animatable"; import WebSectionList from '../../components/Screens/WebSectionList';
import ConnectionManager from "../../managers/ConnectionManager"; import FeedItem from '../../components/Home/FeedItem';
import LogoutDialog from "../../components/Amicale/LogoutDialog"; import SmallDashboardItem from '../../components/Home/SmallDashboardItem';
import AsyncStorageManager from "../../managers/AsyncStorageManager"; import PreviewEventDashboardItem from '../../components/Home/PreviewEventDashboardItem';
import {MASCOT_STYLE} from "../../components/Mascot/Mascot"; import ActionsDashBoardItem from '../../components/Home/ActionsDashboardItem';
import MascotPopup from "../../components/Mascot/MascotPopup"; import MaterialHeaderButtons, {
import DashboardManager from "../../managers/DashboardManager"; Item,
import type {ServiceItem} from "../../managers/ServicesManager"; } from '../../components/Overrides/CustomHeaderButton';
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; import AnimatedFAB from '../../components/Animations/AnimatedFAB';
import type {CustomTheme} from '../../managers/ThemeManager';
import ConnectionManager from '../../managers/ConnectionManager';
import LogoutDialog from '../../components/Amicale/LogoutDialog';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup';
import DashboardManager from '../../managers/DashboardManager';
import type {ServiceItem} from '../../managers/ServicesManager';
import {getDisplayEvent, getFutureEvents} from '../../utils/Home';
// import DATA from "../dashboard_data.json"; // import DATA from "../dashboard_data.json";
const NAME_AMICALE = 'Amicale INSA Toulouse'; const NAME_AMICALE = 'Amicale INSA Toulouse';
const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/v2/dashboard/dashboard_data.json"; const DATA_URL =
'https://etud.insa-toulouse.fr/~amicale_app/v2/dashboard/dashboard_data.json';
const FEED_ITEM_HEIGHT = 500; const FEED_ITEM_HEIGHT = 500;
const SECTIONS_ID = [ const SECTIONS_ID = ['dashboard', 'news_feed'];
'dashboard',
'news_feed'
];
const REFRESH_TIME = 1000 * 20; // Refresh every 20 seconds const REFRESH_TIME = 1000 * 20; // Refresh every 20 seconds
type rawDashboard = { export type FeedItemType = {
news_feed: {
data: Array<feedItem>,
},
dashboard: fullDashboard,
}
export type feedItem = {
full_picture: string, full_picture: string,
message: string, message: string,
permalink_url: string, permalink_url: string,
@ -55,16 +47,7 @@ export type feedItem = {
id: string, id: string,
}; };
export type fullDashboard = { export type EventType = {
today_menu: Array<{ [key: string]: any }>,
proximo_articles: number,
available_dryers: number,
available_washers: number,
today_events: Array<{ [key: string]: any }>,
available_tutorials: number,
}
export type event = {
id: number, id: number,
title: string, title: string,
logo: string | null, logo: string | null,
@ -74,44 +57,68 @@ export type event = {
club: string, club: string,
category_id: number, category_id: number,
url: string, url: string,
} };
type Props = { export type FullDashboardType = {
today_menu: Array<{[key: string]: {...}}>,
proximo_articles: number,
available_dryers: number,
available_washers: number,
today_events: Array<EventType>,
available_tutorials: number,
};
type RawDashboardType = {
news_feed: {
data: Array<FeedItemType>,
},
dashboard: FullDashboardType,
};
type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: { params: any, ... }, route: {params: {nextScreen: string, data: {...}}},
theme: CustomTheme, theme: CustomTheme,
} };
type State = { type StateType = {
dialogVisible: boolean, dialogVisible: boolean,
} };
/** /**
* 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<PropsType, StateType> {
isLoggedIn: boolean | null; isLoggedIn: boolean | null;
fabRef: {current: null | AnimatedFAB}; fabRef: {current: null | AnimatedFAB};
currentNewFeed: Array<feedItem>;
currentDashboard: fullDashboard | null; currentNewFeed: Array<FeedItemType>;
currentDashboard: FullDashboardType | null;
dashboardManager: DashboardManager; dashboardManager: DashboardManager;
constructor(props) { constructor(props: PropsType) {
super(props); super(props);
this.fabRef = React.createRef(); this.fabRef = React.createRef();
this.dashboardManager = new DashboardManager(this.props.navigation); this.dashboardManager = new DashboardManager(props.navigation);
this.currentNewFeed = []; this.currentNewFeed = [];
this.currentDashboard = null; this.currentDashboard = null;
this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn(); this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn();
this.props.navigation.setOptions({ props.navigation.setOptions({
headerRight: this.getHeaderButton, headerRight: this.getHeaderButton,
}); });
this.state = { this.state = {
dialogVisible: false, dialogVisible: false,
};
} }
componentDidMount() {
const {props} = this;
props.navigation.addListener('focus', this.onScreenFocus);
// Handle link open when home is focused
props.navigation.addListener('state', this.handleNavigationParams);
} }
/** /**
@ -120,24 +127,19 @@ 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: number) { static getFormattedDate(dateString: number): string {
let date = new Date(dateString * 1000); const date = new Date(dateString * 1000);
return date.toLocaleString(); return date.toLocaleString();
} }
componentDidMount() {
this.props.navigation.addListener('focus', this.onScreenFocus);
// Handle link open when home is focused
this.props.navigation.addListener('state', this.handleNavigationParams);
}
/** /**
* Updates login state and navigation parameters on screen focus * Updates login state and navigation parameters on screen focus
*/ */
onScreenFocus = () => { onScreenFocus = () => {
const {props} = this;
if (ConnectionManager.getInstance().isLoggedIn() !== this.isLoggedIn) { if (ConnectionManager.getInstance().isLoggedIn() !== this.isLoggedIn) {
this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn(); this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn();
this.props.navigation.setOptions({ props.navigation.setOptions({
headerRight: this.getHeaderButton, headerRight: this.getHeaderButton,
}); });
} }
@ -145,197 +147,41 @@ class HomeScreen extends React.Component<Props, State> {
this.handleNavigationParams(); this.handleNavigationParams();
}; };
/**
* Navigates to the a new screen if navigation parameters specify one
*/
handleNavigationParams = () => {
if (this.props.route.params != null) {
if (this.props.route.params.nextScreen != null) {
this.props.navigation.navigate(this.props.route.params.nextScreen, this.props.route.params.data);
// reset params to prevent infinite loop
this.props.navigation.dispatch(CommonActions.setParams({nextScreen: null}));
}
}
};
/** /**
* Gets header buttons based on login state * Gets header buttons based on login state
* *
* @returns {*} * @returns {*}
*/ */
getHeaderButton = () => { getHeaderButton = (): React.Node => {
let onPressLog = () => this.props.navigation.navigate("login", {nextScreen: "profile"}); const {props} = this;
let logIcon = "login"; let onPressLog = (): void =>
let logColor = this.props.theme.colors.primary; props.navigation.navigate('login', {nextScreen: 'profile'});
let logIcon = 'login';
let logColor = props.theme.colors.primary;
if (this.isLoggedIn) { if (this.isLoggedIn) {
onPressLog = () => this.showDisconnectDialog(); onPressLog = (): void => this.showDisconnectDialog();
logIcon = "logout"; logIcon = 'logout';
logColor = this.props.theme.colors.text; logColor = props.theme.colors.text;
} }
const onPressSettings = () => this.props.navigation.navigate("settings"); const onPressSettings = (): void => props.navigation.navigate('settings');
return <MaterialHeaderButtons> return (
<Item title="log" iconName={logIcon} color={logColor} onPress={onPressLog}/> <MaterialHeaderButtons>
<Item title={i18n.t("screens.settings.title")} iconName={"cog"} onPress={onPressSettings}/> <Item
</MaterialHeaderButtons>; title="log"
iconName={logIcon}
color={logColor}
onPress={onPressLog}
/>
<Item
title={i18n.t('screens.settings.title')}
iconName="cog"
onPress={onPressSettings}
/>
</MaterialHeaderButtons>
);
}; };
showDisconnectDialog = () => this.setState({dialogVisible: true});
hideDisconnectDialog = () => this.setState({dialogVisible: false});
openScanner = () => this.props.navigation.navigate("scanner");
/**
* Creates the dataset to be used in the FlatList
*
* @param fetchedData
* @param isLoading
* @return {*}
*/
createDataset = (fetchedData: rawDashboard | null, isLoading: boolean) => {
// fetchedData = DATA;
if (fetchedData != null) {
if (fetchedData.news_feed != null)
this.currentNewFeed = fetchedData.news_feed.data;
if (fetchedData.dashboard != null)
this.currentDashboard = fetchedData.dashboard;
}
if (this.currentNewFeed.length > 0)
return [
{
title: i18n.t("screens.home.feedTitle"),
data: this.currentNewFeed,
id: SECTIONS_ID[1]
}
];
else
return [
{
title: isLoading ? i18n.t("screens.home.feedLoading") : i18n.t("screens.home.feedError"),
data: [],
id: SECTIONS_ID[1]
}
];
};
/**
* Gets the time limit depending on the current day:
* 17:30 for every day of the week except for thursday 11:30
* 00:00 on weekends
*/
getTodayEventTimeLimit() {
let now = new Date();
if (now.getDay() === 4) // Thursday
now.setHours(11, 30, 0);
else if (now.getDay() === 6 || now.getDay() === 0) // Weekend
now.setHours(0, 0, 0);
else
now.setHours(17, 30, 0);
return now;
}
/**
* Gets the duration (in milliseconds) of an event
*
* @param event {event}
* @return {number} The number of milliseconds
*/
getEventDuration(event: event): number {
let start = stringToDate(event.date_begin);
let end = stringToDate(event.date_end);
let duration = 0;
if (start != null && end != null)
duration = end - start;
return duration;
}
/**
* Gets events starting after the limit
*
* @param events
* @param limit
* @return {Array<Object>}
*/
getEventsAfterLimit(events: Array<event>, limit: Date): Array<event> {
let validEvents = [];
for (let event of events) {
let startDate = stringToDate(event.date_begin);
if (startDate != null && startDate >= limit) {
validEvents.push(event);
}
}
return validEvents;
}
/**
* Gets the event with the longest duration in the given array.
* If all events have the same duration, return the first in the array.
*
* @param events
*/
getLongestEvent(events: Array<event>): event {
let longestEvent = events[0];
let longestTime = 0;
for (let event of events) {
let time = this.getEventDuration(event);
if (time > longestTime) {
longestTime = time;
longestEvent = event;
}
}
return longestEvent;
}
/**
* Gets events that have not yet ended/started
*
* @param events
*/
getFutureEvents(events: Array<event>): Array<event> {
let validEvents = [];
let now = new Date();
for (let event of events) {
let startDate = stringToDate(event.date_begin);
let endDate = stringToDate(event.date_end);
if (startDate != null) {
if (startDate > now)
validEvents.push(event);
else if (endDate != null) {
if (endDate > now || endDate < startDate) // Display event if it ends the following day
validEvents.push(event);
}
}
}
return validEvents;
}
/**
* Gets the event to display in the preview
*
* @param events
* @return {Object}
*/
getDisplayEvent(events: Array<event>): event | null {
let displayEvent = null;
if (events.length > 1) {
let eventsAfterLimit = this.getEventsAfterLimit(events, this.getTodayEventTimeLimit());
if (eventsAfterLimit.length > 0) {
if (eventsAfterLimit.length === 1)
displayEvent = eventsAfterLimit[0];
else
displayEvent = this.getLongestEvent(events);
} else {
displayEvent = this.getLongestEvent(events);
}
} else if (events.length === 1) {
displayEvent = events[0];
}
return displayEvent;
}
onEventContainerClick = () => this.props.navigation.navigate('planning');
/** /**
* Gets the event dashboard render item. * Gets the event dashboard render item.
* If a preview is available, it will be rendered inside * If a preview is available, it will be rendered inside
@ -343,9 +189,9 @@ class HomeScreen extends React.Component<Props, State> {
* @param content * @param content
* @return {*} * @return {*}
*/ */
getDashboardEvent(content: Array<event>) { getDashboardEvent(content: Array<EventType>): React.Node {
let futureEvents = this.getFutureEvents(content); const futureEvents = getFutureEvents(content);
let displayEvent = this.getDisplayEvent(futureEvents); const displayEvent = getDisplayEvent(futureEvents);
// const clickPreviewAction = () => // const clickPreviewAction = () =>
// this.props.navigation.navigate('students', { // this.props.navigation.navigate('students', {
// screen: 'planning-information', // screen: 'planning-information',
@ -354,10 +200,9 @@ class HomeScreen extends React.Component<Props, State> {
return ( return (
<DashboardItem <DashboardItem
eventNumber={futureEvents.length} eventNumber={futureEvents.length}
clickAction={this.onEventContainerClick} clickAction={this.onEventContainerClick}>
>
<PreviewEventDashboardItem <PreviewEventDashboardItem
event={displayEvent != null ? displayEvent : undefined} event={displayEvent}
clickAction={this.onEventContainerClick} clickAction={this.onEventContainerClick}
/> />
</DashboardItem> </DashboardItem>
@ -369,8 +214,14 @@ class HomeScreen extends React.Component<Props, State> {
* *
* @returns {*} * @returns {*}
*/ */
getDashboardActions() { getDashboardActions(): React.Node {
return <ActionsDashBoardItem {...this.props} isLoggedIn={this.isLoggedIn}/>; const {props} = this;
return (
<ActionsDashBoardItem
navigation={props.navigation}
isLoggedIn={this.isLoggedIn}
/>
);
} }
/** /**
@ -379,20 +230,21 @@ class HomeScreen extends React.Component<Props, State> {
* @param content * @param content
* @return {*} * @return {*}
*/ */
getDashboardRow(content: Array<ServiceItem>) { getDashboardRow(content: Array<ServiceItem | null>): React.Node {
return ( return (
// $FlowFixMe // $FlowFixMe
<FlatList <FlatList
data={content} data={content}
renderItem={this.dashboardRowRenderItem} renderItem={this.getDashboardRowRenderItem}
horizontal={true} horizontal
contentContainerStyle={{ contentContainerStyle={{
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
marginTop: 10, marginTop: 10,
marginBottom: 10, marginBottom: 10,
}} }}
/>); />
);
} }
/** /**
@ -401,16 +253,24 @@ class HomeScreen extends React.Component<Props, State> {
* @param item * @param item
* @returns {*} * @returns {*}
*/ */
dashboardRowRenderItem = ({item}: { item: ServiceItem }) => { getDashboardRowRenderItem = ({
item,
}: {
item: ServiceItem | null,
}): React.Node => {
if (item != null)
return ( return (
<SmallDashboardItem <SmallDashboardItem
image={item.image} image={item.image}
onPress={item.onPress} onPress={item.onPress}
badgeCount={this.currentDashboard != null && item.badgeFunction != null badgeCount={
this.currentDashboard != null && item.badgeFunction != null
? item.badgeFunction(this.currentDashboard) ? item.badgeFunction(this.currentDashboard)
: null} : null
}
/> />
); );
return <SmallDashboardItem image={null} onPress={null} badgeCount={null} />;
}; };
/** /**
@ -419,10 +279,11 @@ 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: feedItem) { getFeedItem(item: FeedItemType): React.Node {
const {props} = this;
return ( return (
<FeedItem <FeedItem
{...this.props} navigation={props.navigation}
item={item} item={item}
title={NAME_AMICALE} title={NAME_AMICALE}
subtitle={HomeScreen.getFormattedDate(item.created_time)} subtitle={HomeScreen.getFormattedDate(item.created_time)}
@ -438,139 +299,218 @@ class HomeScreen extends React.Component<Props, State> {
* @param section The current section * @param section The current section
* @return {*} * @return {*}
*/ */
getRenderItem = ({item}: { item: feedItem, }) => this.getFeedItem(item); getRenderItem = ({item}: {item: FeedItemType}): React.Node =>
this.getFeedItem(item);
onScroll = (event: SyntheticEvent<EventTarget>) => { getRenderSectionHeader = (
if (this.fabRef.current != null) data: {
this.fabRef.current.onScroll(event); section: {
}; data: Array<{...}>,
title: string,
renderSectionHeader = (data: { section: { [key: string]: any } }, isLoading: boolean) => { },
},
isLoading: boolean,
): React.Node => {
const {props} = this;
if (data.section.data.length > 0) if (data.section.data.length > 0)
return ( return (
<Headline style={{ <Headline
textAlign: "center", style={{
textAlign: 'center',
marginTop: 50, marginTop: 50,
marginBottom: 10, marginBottom: 10,
}}> }}>
{data.section.title} {data.section.title}
</Headline> </Headline>
) );
else
return ( return (
<View> <View>
<Headline style={{ <Headline
textAlign: "center", style={{
textAlign: 'center',
marginTop: 50, marginTop: 50,
marginBottom: 10, marginBottom: 10,
marginLeft: 20, marginLeft: 20,
marginRight: 20, marginRight: 20,
color: this.props.theme.colors.textDisabled color: props.theme.colors.textDisabled,
}}> }}>
{data.section.title} {data.section.title}
</Headline> </Headline>
{isLoading {isLoading ? (
? <ActivityIndicator <ActivityIndicator
style={{ style={{
marginTop: 10 marginTop: 10,
}} }}
/> />
: <MaterialCommunityIcons ) : (
name={"access-point-network-off"} <MaterialCommunityIcons
name="access-point-network-off"
size={100} size={100}
color={this.props.theme.colors.textDisabled} color={props.theme.colors.textDisabled}
style={{ style={{
marginLeft: "auto", marginLeft: 'auto',
marginRight: "auto", marginRight: 'auto',
}} }}
/>} />
)}
</View> </View>
); );
} };
getListHeader = (fetchedData: rawDashboard) => { getListHeader = (fetchedData: RawDashboardType): React.Node => {
let dashboard = null; let dashboard = null;
if (fetchedData != null) { if (fetchedData != null) dashboard = fetchedData.dashboard;
dashboard = fetchedData.dashboard;
}
return ( return (
<Animatable.View <Animatable.View animation="fadeInDown" duration={500} useNativeDriver>
animation={"fadeInDown"}
duration={500}
useNativeDriver={true}
>
{this.getDashboardActions()} {this.getDashboardActions()}
{this.getDashboardRow(this.dashboardManager.getCurrentDashboard())} {this.getDashboardRow(this.dashboardManager.getCurrentDashboard())}
{this.getDashboardEvent( {this.getDashboardEvent(
dashboard == null dashboard == null ? [] : dashboard.today_events,
? []
: dashboard.today_events
)} )}
</Animatable.View> </Animatable.View>
); );
};
/**
* Navigates to the a new screen if navigation parameters specify one
*/
handleNavigationParams = () => {
const {props} = this;
if (props.route.params != null) {
if (props.route.params.nextScreen != null) {
props.navigation.navigate(
props.route.params.nextScreen,
props.route.params.data,
);
// reset params to prevent infinite loop
props.navigation.dispatch(CommonActions.setParams({nextScreen: null}));
} }
}
};
showDisconnectDialog = (): void => this.setState({dialogVisible: true});
hideDisconnectDialog = (): void => this.setState({dialogVisible: false});
openScanner = () => {
const {props} = this;
props.navigation.navigate('scanner');
};
/**
* Creates the dataset to be used in the FlatList
*
* @param fetchedData
* @param isLoading
* @return {*}
*/
createDataset = (
fetchedData: RawDashboardType | null,
isLoading: boolean,
): Array<{
title: string,
data: [] | Array<FeedItemType>,
id: string,
}> => {
// fetchedData = DATA;
if (fetchedData != null) {
if (fetchedData.news_feed != null)
this.currentNewFeed = fetchedData.news_feed.data;
if (fetchedData.dashboard != null)
this.currentDashboard = fetchedData.dashboard;
}
if (this.currentNewFeed.length > 0)
return [
{
title: i18n.t('screens.home.feedTitle'),
data: this.currentNewFeed,
id: SECTIONS_ID[1],
},
];
return [
{
title: isLoading
? i18n.t('screens.home.feedLoading')
: i18n.t('screens.home.feedError'),
data: [],
id: SECTIONS_ID[1],
},
];
};
onEventContainerClick = () => {
const {props} = this;
props.navigation.navigate('planning');
};
onScroll = (event: SyntheticEvent<EventTarget>) => {
if (this.fabRef.current != null) this.fabRef.current.onScroll(event);
};
/** /**
* Callback when pressing the login button on the banner. * Callback when pressing the login button on the banner.
* This hides the banner and takes the user to the login page. * This hides the banner and takes the user to the login page.
*/ */
onLogin = () => this.props.navigation.navigate("login", {nextScreen: "profile"}); onLogin = () => {
const {props} = this;
props.navigation.navigate('login', {
nextScreen: 'profile',
});
};
render() { render(): React.Node {
const {props, state} = this;
return ( return (
<View style={{flex: 1}}>
<View <View
style={{flex: 1}} style={{
> position: 'absolute',
<View style={{ width: '100%',
position: "absolute", height: '100%',
width: "100%",
height: "100%",
}}> }}>
<WebSectionList <WebSectionList
{...this.props} navigation={props.navigation}
createDataset={this.createDataset} createDataset={this.createDataset}
autoRefreshTime={REFRESH_TIME} autoRefreshTime={REFRESH_TIME}
refreshOnFocus={true} refreshOnFocus
fetchUrl={DATA_URL} fetchUrl={DATA_URL}
renderItem={this.getRenderItem} renderItem={this.getRenderItem}
itemHeight={FEED_ITEM_HEIGHT} itemHeight={FEED_ITEM_HEIGHT}
onScroll={this.onScroll} onScroll={this.onScroll}
showError={false} showError={false}
renderSectionHeader={this.renderSectionHeader} renderSectionHeader={this.getRenderSectionHeader}
renderListHeaderComponent={this.getListHeader} renderListHeaderComponent={this.getListHeader}
/> />
</View> </View>
{!this.isLoggedIn {!this.isLoggedIn ? (
? <MascotPopup <MascotPopup
prefKey={AsyncStorageManager.PREFERENCES.homeShowBanner.key} prefKey={AsyncStorageManager.PREFERENCES.homeShowBanner.key}
title={i18n.t("screens.home.mascotDialog.title")} title={i18n.t('screens.home.mascotDialog.title')}
message={i18n.t("screens.home.mascotDialog.message")} message={i18n.t('screens.home.mascotDialog.message')}
icon={"human-greeting"} icon="human-greeting"
buttons={{ buttons={{
action: { action: {
message: i18n.t("screens.home.mascotDialog.login"), message: i18n.t('screens.home.mascotDialog.login'),
icon: "login", icon: 'login',
onPress: this.onLogin, onPress: this.onLogin,
}, },
cancel: { cancel: {
message: i18n.t("screens.home.mascotDialog.later"), message: i18n.t('screens.home.mascotDialog.later'),
icon: "close", icon: 'close',
color: this.props.theme.colors.warning, color: props.theme.colors.warning,
} },
}} }}
emotion={MASCOT_STYLE.CUTE} emotion={MASCOT_STYLE.CUTE}
/> : null} />
) : null}
<AnimatedFAB <AnimatedFAB
{...this.props}
ref={this.fabRef} ref={this.fabRef}
icon="qrcode-scan" icon="qrcode-scan"
onPress={this.openScanner} onPress={this.openScanner}
/> />
<LogoutDialog <LogoutDialog
{...this.props} navigation={props.navigation}
visible={this.state.dialogVisible} visible={state.dialogVisible}
onDismiss={this.hideDisconnectDialog} onDismiss={this.hideDisconnectDialog}
/> />
</View> </View>

View file

@ -1,90 +1,65 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Linking, Platform, StyleSheet, View} from "react-native"; import {Linking, Platform, StyleSheet, View} from 'react-native';
import {Button, Text, withTheme} from 'react-native-paper'; import {Button, Text, withTheme} from 'react-native-paper';
import {RNCamera} from 'react-native-camera'; import {RNCamera} from 'react-native-camera';
import {BarcodeMask} from '@nartc/react-native-barcode-mask'; import {BarcodeMask} from '@nartc/react-native-barcode-mask';
import URLHandler from "../../utils/URLHandler";
import AlertDialog from "../../components/Dialogs/AlertDialog";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import CustomTabBar from "../../components/Tabbar/CustomTabBar";
import LoadingConfirmDialog from "../../components/Dialogs/LoadingConfirmDialog";
import {PERMISSIONS, request, RESULTS} from 'react-native-permissions'; import {PERMISSIONS, request, RESULTS} from 'react-native-permissions';
import {MASCOT_STYLE} from "../../components/Mascot/Mascot"; import URLHandler from '../../utils/URLHandler';
import MascotPopup from "../../components/Mascot/MascotPopup"; import AlertDialog from '../../components/Dialogs/AlertDialog';
import CustomTabBar from '../../components/Tabbar/CustomTabBar';
import LoadingConfirmDialog from '../../components/Dialogs/LoadingConfirmDialog';
import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup';
type Props = {}; type StateType = {
type State = {
hasPermission: boolean, hasPermission: boolean,
scanned: boolean, scanned: boolean,
dialogVisible: boolean, dialogVisible: boolean,
mascotDialogVisible: boolean, mascotDialogVisible: boolean,
dialogTitle: string,
dialogMessage: string,
loading: boolean, loading: boolean,
}; };
class ScannerScreen extends React.Component<Props, State> { const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
button: {
position: 'absolute',
bottom: 20,
width: '80%',
left: '10%',
},
});
state = { class ScannerScreen extends React.Component<null, StateType> {
constructor() {
super();
this.state = {
hasPermission: false, hasPermission: false,
scanned: false, scanned: false,
mascotDialogVisible: false, mascotDialogVisible: false,
dialogVisible: false, dialogVisible: false,
dialogTitle: "",
dialogMessage: "",
loading: false, loading: false,
}; };
constructor() {
super();
} }
componentDidMount() { componentDidMount() {
this.requestPermissions(); this.requestPermissions();
} }
/**
* Requests permission to use the camera
*/
requestPermissions = () => {
if (Platform.OS === 'android')
request(PERMISSIONS.ANDROID.CAMERA).then(this.updatePermissionStatus)
else
request(PERMISSIONS.IOS.CAMERA).then(this.updatePermissionStatus)
};
/**
* Updates the state permission status
*
* @param result
*/
updatePermissionStatus = (result) => this.setState({hasPermission: result === RESULTS.GRANTED});
/**
* Opens scanned link if it is a valid app link or shows and error dialog
*
* @param type The barcode type
* @param data The scanned value
*/
handleCodeScanned = ({type, data}) => {
if (!URLHandler.isUrlValid(data))
this.showErrorDialog();
else {
this.showOpeningDialog();
Linking.openURL(data);
}
};
/** /**
* Gets a view asking user for permission to use the camera * Gets a view asking user for permission to use the camera
* *
* @returns {*} * @returns {*}
*/ */
getPermissionScreen() { getPermissionScreen(): React.Node {
return <View style={{marginLeft: 10, marginRight: 10}}> return (
<Text>{i18n.t("screens.scanner.permissions.error")}</Text> <View style={{marginLeft: 10, marginRight: 10}}>
<Text>{i18n.t('screens.scanner.permissions.error')}</Text>
<Button <Button
icon="camera" icon="camera"
mode="contained" mode="contained"
@ -93,11 +68,72 @@ class ScannerScreen extends React.Component<Props, State> {
marginTop: 10, marginTop: 10,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
}} }}>
> {i18n.t('screens.scanner.permissions.button')}
{i18n.t("screens.scanner.permissions.button")}
</Button> </Button>
</View> </View>
);
}
/**
* Gets a view with the scanner.
* This scanner uses the back camera, can only scan qr codes and has a square mask on the center.
* The mask is only for design purposes as a code is scanned as soon as it enters the camera view
*
* @returns {*}
*/
getScanner(): React.Node {
const {state} = this;
return (
<RNCamera
onBarCodeRead={state.scanned ? null : this.onCodeScanned}
type={RNCamera.Constants.Type.back}
barCodeScannerSettings={{
barCodeTypes: [RNCamera.Constants.BarCodeType.qr],
}}
style={StyleSheet.absoluteFill}
captureAudio={false}>
<BarcodeMask
backgroundColor="#000"
maskOpacity={0.5}
animatedLineThickness={1}
animationDuration={1000}
width={250}
height={250}
/>
</RNCamera>
);
}
/**
* Requests permission to use the camera
*/
requestPermissions = () => {
if (Platform.OS === 'android')
request(PERMISSIONS.ANDROID.CAMERA).then(this.updatePermissionStatus);
else request(PERMISSIONS.IOS.CAMERA).then(this.updatePermissionStatus);
};
/**
* Updates the state permission status
*
* @param result
*/
updatePermissionStatus = (result: RESULTS) => {
this.setState({
hasPermission: result === RESULTS.GRANTED,
});
};
/**
* Shows a dialog indicating the user the scanned code was invalid
*/
// eslint-disable-next-line react/sort-comp
showErrorDialog() {
this.setState({
dialogVisible: true,
scanned: true,
});
} }
/** /**
@ -120,119 +156,82 @@ class ScannerScreen extends React.Component<Props, State> {
}); });
}; };
/**
* Shows a dialog indicating the user the scanned code was invalid
*/
showErrorDialog() {
this.setState({
dialogVisible: true,
scanned: true,
});
}
/** /**
* Hide any dialog * Hide any dialog
*/ */
onDialogDismiss = () => this.setState({ onDialogDismiss = () => {
this.setState({
dialogVisible: false, dialogVisible: false,
scanned: false, scanned: false,
}); });
};
onMascotDialogDismiss = () => this.setState({ onMascotDialogDismiss = () => {
this.setState({
mascotDialogVisible: false, mascotDialogVisible: false,
scanned: false, scanned: false,
}); });
};
/** /**
* Gets a view with the scanner. * Opens scanned link if it is a valid app link or shows and error dialog
* This scanner uses the back camera, can only scan qr codes and has a square mask on the center.
* The mask is only for design purposes as a code is scanned as soon as it enters the camera view
* *
* @returns {*} * @param type The barcode type
* @param data The scanned value
*/ */
getScanner() { onCodeScanned = ({data}: {data: string}) => {
return ( if (!URLHandler.isUrlValid(data)) this.showErrorDialog();
<RNCamera else {
onBarCodeRead={this.state.scanned ? undefined : this.handleCodeScanned} this.showOpeningDialog();
type={RNCamera.Constants.Type.back} Linking.openURL(data);
barCodeScannerSettings={{
barCodeTypes: [RNCamera.Constants.BarCodeType.qr],
}}
style={StyleSheet.absoluteFill}
captureAudio={false}
>
<BarcodeMask
backgroundColor={"#000"}
maskOpacity={0.5}
animatedLineThickness={1}
animationDuration={1000}
width={250}
height={250}
/>
</RNCamera>
);
} }
};
render() { render(): React.Node {
const {state} = this;
return ( return (
<View style={{ <View
style={{
...styles.container, ...styles.container,
marginBottom: CustomTabBar.TAB_BAR_HEIGHT marginBottom: CustomTabBar.TAB_BAR_HEIGHT,
}}> }}>
{this.state.hasPermission {state.hasPermission ? this.getScanner() : this.getPermissionScreen()}
? this.getScanner()
: this.getPermissionScreen()
}
<Button <Button
icon="information" icon="information"
mode="contained" mode="contained"
onPress={this.showHelpDialog} onPress={this.showHelpDialog}
style={styles.button} style={styles.button}>
> {i18n.t('screens.scanner.help.button')}
{i18n.t("screens.scanner.help.button")}
</Button> </Button>
<MascotPopup <MascotPopup
visible={this.state.mascotDialogVisible} visible={state.mascotDialogVisible}
title={i18n.t("screens.scanner.mascotDialog.title")} title={i18n.t('screens.scanner.mascotDialog.title')}
message={i18n.t("screens.scanner.mascotDialog.message")} message={i18n.t('screens.scanner.mascotDialog.message')}
icon={"camera-iris"} icon="camera-iris"
buttons={{ buttons={{
action: null, action: null,
cancel: { cancel: {
message: i18n.t("screens.scanner.mascotDialog.button"), message: i18n.t('screens.scanner.mascotDialog.button'),
icon: "check", icon: 'check',
onPress: this.onMascotDialogDismiss, onPress: this.onMascotDialogDismiss,
} },
}} }}
emotion={MASCOT_STYLE.NORMAL} emotion={MASCOT_STYLE.NORMAL}
/> />
<AlertDialog <AlertDialog
visible={this.state.dialogVisible} visible={state.dialogVisible}
onDismiss={this.onDialogDismiss} onDismiss={this.onDialogDismiss}
title={i18n.t("screens.scanner.error.title")} title={i18n.t('screens.scanner.error.title')}
message={i18n.t("screens.scanner.error.message")} message={i18n.t('screens.scanner.error.message')}
/> />
<LoadingConfirmDialog <LoadingConfirmDialog
visible={this.state.loading} visible={state.loading}
titleLoading={i18n.t("general.loading")} titleLoading={i18n.t('general.loading')}
startLoading={true} startLoading
/> />
</View> </View>
); );
} }
} }
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
button: {
position: 'absolute',
bottom: 20,
width: '80%',
left: '10%'
},
});
export default withTheme(ScannerScreen); export default withTheme(ScannerScreen);

123
src/utils/Home.js Normal file
View file

@ -0,0 +1,123 @@
// @flow
import {stringToDate} from './Planning';
import type {EventType} from '../screens/Home/HomeScreen';
/**
* Gets the time limit depending on the current day:
* 17:30 for every day of the week except for thursday 11:30
* 00:00 on weekends
*/
export function getTodayEventTimeLimit(): Date {
const now = new Date();
if (now.getDay() === 4)
// Thursday
now.setHours(11, 30, 0);
else if (now.getDay() === 6 || now.getDay() === 0)
// Weekend
now.setHours(0, 0, 0);
else now.setHours(17, 30, 0);
return now;
}
/**
* Gets the duration (in milliseconds) of an event
*
* @param event {EventType}
* @return {number} The number of milliseconds
*/
export function getEventDuration(event: EventType): number {
const start = stringToDate(event.date_begin);
const end = stringToDate(event.date_end);
let duration = 0;
if (start != null && end != null) duration = end - start;
return duration;
}
/**
* Gets events starting after the limit
*
* @param events
* @param limit
* @return {Array<Object>}
*/
export function getEventsAfterLimit(
events: Array<EventType>,
limit: Date,
): Array<EventType> {
const validEvents = [];
events.forEach((event: EventType) => {
const startDate = stringToDate(event.date_begin);
if (startDate != null && startDate >= limit) {
validEvents.push(event);
}
});
return validEvents;
}
/**
* Gets the event with the longest duration in the given array.
* If all events have the same duration, return the first in the array.
*
* @param events
*/
export function getLongestEvent(events: Array<EventType>): EventType {
let longestEvent = events[0];
let longestTime = 0;
events.forEach((event: EventType) => {
const time = getEventDuration(event);
if (time > longestTime) {
longestTime = time;
longestEvent = event;
}
});
return longestEvent;
}
/**
* Gets events that have not yet ended/started
*
* @param events
*/
export function getFutureEvents(events: Array<EventType>): Array<EventType> {
const validEvents = [];
const now = new Date();
events.forEach((event: EventType) => {
const startDate = stringToDate(event.date_begin);
const endDate = stringToDate(event.date_end);
if (startDate != null) {
if (startDate > now) validEvents.push(event);
else if (endDate != null) {
if (endDate > now || endDate < startDate)
// Display event if it ends the following day
validEvents.push(event);
}
}
});
return validEvents;
}
/**
* Gets the event to display in the preview
*
* @param events
* @return {EventType | null}
*/
export function getDisplayEvent(events: Array<EventType>): EventType | null {
let displayEvent = null;
if (events.length > 1) {
const eventsAfterLimit = getEventsAfterLimit(
events,
getTodayEventTimeLimit(),
);
if (eventsAfterLimit.length > 0) {
if (eventsAfterLimit.length === 1) [displayEvent] = eventsAfterLimit;
else displayEvent = getLongestEvent(events);
} else {
displayEvent = getLongestEvent(events);
}
} else if (events.length === 1) {
[displayEvent] = events;
}
return displayEvent;
}