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,37 +2,46 @@
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<Props> { class ActionsDashBoardItem extends React.Component<PropsType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return nextProps.theme.dark !== props.theme.dark;
}
shouldComponentUpdate(nextProps: Props): boolean { render(): React.Node {
return (nextProps.theme.dark !== this.props.theme.dark); const {props} = this;
} return (
<View>
render() { <List.Item
return ( title={i18n.t('screens.feedback.homeButtonTitle')}
<View> description={i18n.t('screens.feedback.homeButtonSubtitle')}
<List.Item left={({size}: {size: number}): React.Node => (
title={i18n.t("screens.feedback.homeButtonTitle")} <List.Icon size={size} icon="comment-quote" />
description={i18n.t("screens.feedback.homeButtonSubtitle")} )}
left={props => <List.Icon {...props} icon={"comment-quote"}/>} right={({size}: {size: number}): React.Node => (
right={props => <List.Icon {...props} icon={"chevron-right"}/>} <List.Icon size={size} icon="chevron-right" />
onPress={() => this.props.navigation.navigate("feedback")} )}
style={{paddingTop: 0, paddingBottom: 0, marginLeft: 10, marginRight: 10}} onPress={(): void => props.navigation.navigate('feedback')}
/> style={{
</View> paddingTop: 0,
paddingBottom: 0,
); marginLeft: 10,
} marginRight: 10,
}}
/>
</View>
);
}
} }
export default withTheme(ActionsDashBoardItem); export default withTheme(ActionsDashBoardItem);

View file

@ -1,91 +1,96 @@
// @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,
} };
const styles = StyleSheet.create({
card: {
width: 'auto',
marginLeft: 10,
marginRight: 10,
marginTop: 10,
overflow: 'hidden',
},
avatar: {
backgroundColor: 'transparent',
},
});
/** /**
* Component used to display a dashboard item containing a preview event * Component used to display a dashboard item containing a preview event
*/ */
class EventDashBoardItem extends React.Component<Props> { class EventDashBoardItem extends React.Component<PropsType> {
static defaultProps = {
children: null,
};
shouldComponentUpdate(nextProps: Props) { shouldComponentUpdate(nextProps: PropsType): boolean {
return (nextProps.theme.dark !== this.props.theme.dark) const {props} = this;
|| (nextProps.eventNumber !== this.props.eventNumber); return (
} nextProps.theme.dark !== props.theme.dark ||
nextProps.eventNumber !== 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>
);
}
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>
);
}
} }
const styles = StyleSheet.create({
card: {
width: 'auto',
marginLeft: 10,
marginRight: 10,
marginTop: 10,
overflow: 'hidden',
},
avatar: {
backgroundColor: 'transparent'
}
});
export default withTheme(EventDashBoardItem); export default withTheme(EventDashBoardItem);

View file

@ -2,126 +2,114 @@
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 {
return false;
}
shouldComponentUpdate() { onPress = () => {
return false; const {props} = this;
} props.navigation.navigate('feed-information', {
data: props.item,
date: props.subtitle,
});
};
/** render(): React.Node {
* Gets the amicale INSAT logo const {props} = this;
* const {item} = props;
* @return {*} const hasImage =
*/ item.full_picture !== '' && item.full_picture !== undefined;
getAvatar() {
return ( const cardMargin = 10;
<Image const cardHeight = props.height - 2 * cardMargin;
size={48} const imageSize = 250;
source={ICON_AMICALE} const titleHeight = 80;
style={{ const actionsHeight = 60;
const textHeight = hasImage
? cardHeight - titleHeight - actionsHeight - imageSize
: cardHeight - titleHeight - actionsHeight;
return (
<Card
style={{
margin: cardMargin,
height: cardHeight,
}}>
<TouchableRipple style={{flex: 1}} onPress={this.onPress}>
<View>
<Card.Title
title={props.title}
subtitle={props.subtitle}
left={(): React.Node => (
<Image
size={48}
source={ICON_AMICALE}
style={{
width: 48, width: 48,
height: 48, height: 48,
}}/> }}
); />
} )}
style={{height: titleHeight}}
onPress = () => { />
this.props.navigation.navigate( {hasImage ? (
'feed-information', <View style={{marginLeft: 'auto', marginRight: 'auto'}}>
{ <ImageModal
data: this.props.item, resizeMode="contain"
date: this.props.subtitle imageBackgroundColor="#000"
}); style={{
}; width: imageSize,
height: imageSize,
render() { }}
const item = this.props.item; source={{
const hasImage = item.full_picture !== '' && item.full_picture !== undefined; uri: item.full_picture,
}}
const cardMargin = 10; />
const cardHeight = this.props.height - 2 * cardMargin; </View>
const imageSize = 250; ) : null}
const titleHeight = 80; <Card.Content>
const actionsHeight = 60; {item.message !== undefined ? (
const textHeight = hasImage <Autolink
? cardHeight - titleHeight - actionsHeight - imageSize text={item.message}
: cardHeight - titleHeight - actionsHeight; hashtag="facebook"
return ( component={Text}
<Card style={{height: textHeight}}
style={{ />
margin: cardMargin, ) : null}
height: cardHeight, </Card.Content>
}} <Card.Actions style={{height: actionsHeight}}>
> <Button
<TouchableRipple onPress={this.onPress}
style={{flex: 1}} icon="plus"
onPress={this.onPress}> style={{marginLeft: 'auto'}}>
<View> {i18n.t('screens.home.dashboard.seeMore')}
<Card.Title </Button>
title={this.props.title} </Card.Actions>
subtitle={this.props.subtitle} </View>
left={this.getAvatar} </TouchableRipple>
style={{height: titleHeight}} </Card>
/> );
{hasImage ? }
<View style={{marginLeft: 'auto', marginRight: 'auto'}}>
<ImageModal
resizeMode="contain"
imageBackgroundColor={"#000"}
style={{
width: imageSize,
height: imageSize,
}}
source={{
uri: item.full_picture,
}}
/></View> : null}
<Card.Content>
{item.message !== undefined ?
<Autolink
text={item.message}
hashtag="facebook"
component={Text}
style={{height: textHeight}}
/> : null
}
</Card.Content>
<Card.Actions style={{height: actionsHeight}}>
<Button
onPress={this.onPress}
icon={'plus'}
style={{marginLeft: 'auto'}}>
{i18n.t('screens.home.dashboard.seeMore')}
</Button>
</Card.Actions>
</View>
</TouchableRipple>
</Card>
);
}
} }
export default FeedItem; export default FeedItem;

View file

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

View file

@ -2,15 +2,15 @@
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,
}; };
const AnimatableBadge = Animatable.createAnimatableComponent(Badge); const AnimatableBadge = Animatable.createAnimatableComponent(Badge);
@ -18,69 +18,68 @@ 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: PropsType) {
super(props);
this.itemSize = Dimensions.get('window').width / 8;
}
constructor(props: Props) { shouldComponentUpdate(nextProps: PropsType): boolean {
super(props); const {props} = this;
this.itemSize = Dimensions.get('window').width / 8; return (
} nextProps.theme.dark !== props.theme.dark ||
nextProps.badgeCount !== props.badgeCount
shouldComponentUpdate(nextProps: Props) { );
return (nextProps.theme.dark !== this.props.theme.dark) }
|| (nextProps.badgeCount !== this.props.badgeCount);
}
render() {
const props = this.props;
return (
<TouchableRipple
onPress={this.props.onPress}
borderless={true}
style={{
marginLeft: this.itemSize / 6,
marginRight: this.itemSize / 6,
}}
>
<View style={{
width: this.itemSize,
height: this.itemSize,
}}>
<Image
source={{uri: props.image}}
style={{
width: "80%",
height: "80%",
marginLeft: "auto",
marginRight: "auto",
marginTop: "auto",
marginBottom: "auto",
}}
/>
{
props.badgeCount != null && props.badgeCount > 0 ?
<AnimatableBadge
animation={"zoomIn"}
duration={300}
useNativeDriver
style={{
position: 'absolute',
top: 0,
right: 0,
backgroundColor: props.theme.colors.primary,
borderColor: props.theme.colors.background,
borderWidth: 2,
}}>
{props.badgeCount}
</AnimatableBadge> : null
}
</View>
</TouchableRipple>
);
}
render(): React.Node {
const {props} = this;
return (
<TouchableRipple
onPress={props.onPress}
borderless
style={{
marginLeft: this.itemSize / 6,
marginRight: this.itemSize / 6,
}}>
<View
style={{
width: this.itemSize,
height: this.itemSize,
}}>
<Image
source={{uri: props.image}}
style={{
width: '80%',
height: '80%',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 'auto',
marginBottom: 'auto',
}}
/>
{props.badgeCount != null && props.badgeCount > 0 ? (
<AnimatableBadge
animation="zoomIn"
duration={300}
useNativeDriver
style={{
position: 'absolute',
top: 0,
right: 0,
backgroundColor: props.theme.colors.primary,
borderColor: props.theme.colors.background,
borderWidth: 2,
}}>
{props.badgeCount}
</AnimatableBadge>
) : null}
</View>
</TouchableRipple>
);
}
} }
export default withTheme(SmallDashboardItem); export default withTheme(SmallDashboardItem);

View file

@ -4,108 +4,114 @@ 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;
headerRight: this.getHeaderButton, props.navigation.setOptions({
}); headerRight: this.getHeaderButton,
} });
}
/** /**
* Opens the feed item out link in browser or compatible app * Opens the feed item out link in browser or compatible app
*/ */
onOutLinkPress = () => { onOutLinkPress = () => {
Linking.openURL(this.displayData.permalink_url); Linking.openURL(this.displayData.permalink_url);
}; };
/** /**
* Gets the out link header button * Gets the out link header button
* *
* @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;
*/ return (
getAvatar() { <CollapsibleScrollView style={{margin: 5}} hasTab>
return ( <Card.Title
<Avatar.Image size={48} source={ICON_AMICALE} title={NAME_AMICALE}
style={{backgroundColor: 'transparent'}}/> subtitle={this.date}
); left={(): React.Node => (
} <Avatar.Image
size={48}
render() { source={ICON_AMICALE}
const hasImage = this.displayData.full_picture !== '' && this.displayData.full_picture != null; style={{backgroundColor: 'transparent'}}
return ( />
<CollapsibleScrollView )}
style={{margin: 5,}} />
hasTab={true} {hasImage ? (
> <View style={{marginLeft: 'auto', marginRight: 'auto'}}>
<Card.Title <ImageModal
title={NAME_AMICALE} resizeMode="contain"
subtitle={this.date} imageBackgroundColor="#000"
left={this.getAvatar} style={{
/> width: 250,
{hasImage ? height: 250,
<View style={{marginLeft: 'auto', marginRight: 'auto'}}> }}
<ImageModal source={{
resizeMode="contain" uri: this.displayData.full_picture,
imageBackgroundColor={"#000"} }}
style={{ />
width: 250, </View>
height: 250, ) : null}
}} <Card.Content style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
source={{ {this.displayData.message !== undefined ? (
uri: this.displayData.full_picture, <Autolink
}} text={this.displayData.message}
/></View> : null} hashtag="facebook"
<Card.Content style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}> component={Text}
{this.displayData.message !== undefined ? />
<Autolink ) : null}
text={this.displayData.message} </Card.Content>
hashtag="facebook" </CollapsibleScrollView>
component={Text} );
/> : null }
}
</Card.Content>
</CollapsibleScrollView>
);
}
} }
export default withTheme(FeedItemScreen); export default withTheme(FeedItemScreen);

File diff suppressed because it is too large Load diff

View file

@ -1,238 +1,237 @@
// @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, loading: boolean,
dialogTitle: string,
dialogMessage: string,
loading: boolean,
}; };
class ScannerScreen extends React.Component<Props, State> {
state = {
hasPermission: false,
scanned: false,
mascotDialogVisible: false,
dialogVisible: false,
dialogTitle: "",
dialogMessage: "",
loading: false,
};
constructor() {
super();
}
componentDidMount() {
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
*
* @returns {*}
*/
getPermissionScreen() {
return <View style={{marginLeft: 10, marginRight: 10}}>
<Text>{i18n.t("screens.scanner.permissions.error")}</Text>
<Button
icon="camera"
mode="contained"
onPress={this.requestPermissions}
style={{
marginTop: 10,
marginLeft: 'auto',
marginRight: 'auto',
}}
>
{i18n.t("screens.scanner.permissions.button")}
</Button>
</View>
}
/**
* Shows a dialog indicating how to use the scanner
*/
showHelpDialog = () => {
this.setState({
mascotDialogVisible: true,
scanned: true,
});
};
/**
* Shows a loading dialog
*/
showOpeningDialog = () => {
this.setState({
loading: true,
scanned: true,
});
};
/**
* Shows a dialog indicating the user the scanned code was invalid
*/
showErrorDialog() {
this.setState({
dialogVisible: true,
scanned: true,
});
}
/**
* Hide any dialog
*/
onDialogDismiss = () => this.setState({
dialogVisible: false,
scanned: false,
});
onMascotDialogDismiss = () => this.setState({
mascotDialogVisible: false,
scanned: false,
});
/**
* 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() {
return (
<RNCamera
onBarCodeRead={this.state.scanned ? undefined : this.handleCodeScanned}
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>
);
}
render() {
return (
<View style={{
...styles.container,
marginBottom: CustomTabBar.TAB_BAR_HEIGHT
}}>
{this.state.hasPermission
? this.getScanner()
: this.getPermissionScreen()
}
<Button
icon="information"
mode="contained"
onPress={this.showHelpDialog}
style={styles.button}
>
{i18n.t("screens.scanner.help.button")}
</Button>
<MascotPopup
visible={this.state.mascotDialogVisible}
title={i18n.t("screens.scanner.mascotDialog.title")}
message={i18n.t("screens.scanner.mascotDialog.message")}
icon={"camera-iris"}
buttons={{
action: null,
cancel: {
message: i18n.t("screens.scanner.mascotDialog.button"),
icon: "check",
onPress: this.onMascotDialogDismiss,
}
}}
emotion={MASCOT_STYLE.NORMAL}
/>
<AlertDialog
visible={this.state.dialogVisible}
onDismiss={this.onDialogDismiss}
title={i18n.t("screens.scanner.error.title")}
message={i18n.t("screens.scanner.error.message")}
/>
<LoadingConfirmDialog
visible={this.state.loading}
titleLoading={i18n.t("general.loading")}
startLoading={true}
/>
</View>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
}, },
button: { button: {
position: 'absolute', position: 'absolute',
bottom: 20, bottom: 20,
width: '80%', width: '80%',
left: '10%' left: '10%',
}, },
}); });
class ScannerScreen extends React.Component<null, StateType> {
constructor() {
super();
this.state = {
hasPermission: false,
scanned: false,
mascotDialogVisible: false,
dialogVisible: false,
loading: false,
};
}
componentDidMount() {
this.requestPermissions();
}
/**
* Gets a view asking user for permission to use the camera
*
* @returns {*}
*/
getPermissionScreen(): React.Node {
return (
<View style={{marginLeft: 10, marginRight: 10}}>
<Text>{i18n.t('screens.scanner.permissions.error')}</Text>
<Button
icon="camera"
mode="contained"
onPress={this.requestPermissions}
style={{
marginTop: 10,
marginLeft: 'auto',
marginRight: 'auto',
}}>
{i18n.t('screens.scanner.permissions.button')}
</Button>
</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,
});
}
/**
* Shows a dialog indicating how to use the scanner
*/
showHelpDialog = () => {
this.setState({
mascotDialogVisible: true,
scanned: true,
});
};
/**
* Shows a loading dialog
*/
showOpeningDialog = () => {
this.setState({
loading: true,
scanned: true,
});
};
/**
* Hide any dialog
*/
onDialogDismiss = () => {
this.setState({
dialogVisible: false,
scanned: false,
});
};
onMascotDialogDismiss = () => {
this.setState({
mascotDialogVisible: false,
scanned: false,
});
};
/**
* 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
*/
onCodeScanned = ({data}: {data: string}) => {
if (!URLHandler.isUrlValid(data)) this.showErrorDialog();
else {
this.showOpeningDialog();
Linking.openURL(data);
}
};
render(): React.Node {
const {state} = this;
return (
<View
style={{
...styles.container,
marginBottom: CustomTabBar.TAB_BAR_HEIGHT,
}}>
{state.hasPermission ? this.getScanner() : this.getPermissionScreen()}
<Button
icon="information"
mode="contained"
onPress={this.showHelpDialog}
style={styles.button}>
{i18n.t('screens.scanner.help.button')}
</Button>
<MascotPopup
visible={state.mascotDialogVisible}
title={i18n.t('screens.scanner.mascotDialog.title')}
message={i18n.t('screens.scanner.mascotDialog.message')}
icon="camera-iris"
buttons={{
action: null,
cancel: {
message: i18n.t('screens.scanner.mascotDialog.button'),
icon: 'check',
onPress: this.onMascotDialogDismiss,
},
}}
emotion={MASCOT_STYLE.NORMAL}
/>
<AlertDialog
visible={state.dialogVisible}
onDismiss={this.onDialogDismiss}
title={i18n.t('screens.scanner.error.title')}
message={i18n.t('screens.scanner.error.message')}
/>
<LoadingConfirmDialog
visible={state.loading}
titleLoading={i18n.t('general.loading')}
startLoading
/>
</View>
);
}
}
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;
}