application-amicale/src/screens/Home/HomeScreen.js
2020-08-08 16:39:26 +02:00

525 lines
14 KiB
JavaScript

// @flow
import * as React from 'react';
import {FlatList} from 'react-native';
import i18n from 'i18n-js';
import {ActivityIndicator, Headline, withTheme} from 'react-native-paper';
import {CommonActions} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack';
import * as Animatable from 'react-native-animatable';
import {View} from 'react-native-animatable';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import DashboardItem from '../../components/Home/EventDashboardItem';
import WebSectionList from '../../components/Screens/WebSectionList';
import FeedItem from '../../components/Home/FeedItem';
import SmallDashboardItem from '../../components/Home/SmallDashboardItem';
import PreviewEventDashboardItem from '../../components/Home/PreviewEventDashboardItem';
import ActionsDashBoardItem from '../../components/Home/ActionsDashboardItem';
import MaterialHeaderButtons, {
Item,
} from '../../components/Overrides/CustomHeaderButton';
import AnimatedFAB from '../../components/Animations/AnimatedFAB';
import type {CustomThemeType} 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 {ServiceItemType} from '../../managers/ServicesManager';
import {getDisplayEvent, getFutureEvents} from '../../utils/Home';
// import DATA from "../dashboard_data.json";
const DATA_URL =
'https://etud.insa-toulouse.fr/~amicale_app/v2/dashboard/dashboard_data.json';
const FEED_ITEM_HEIGHT = 500;
const SECTIONS_ID = ['dashboard', 'news_feed'];
const REFRESH_TIME = 1000 * 20; // Refresh every 20 seconds
export type FeedItemType = {
id: string,
message: string,
url: string,
image: string | null,
video: string | null,
link: string | null,
time: number,
page_id: string,
};
export type EventType = {
id: number,
title: string,
logo: string | null,
date_begin: string,
date_end: string,
description: string,
club: string,
category_id: number,
url: string,
};
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 RawNewsFeedType = {[key: string]: Array<FeedItemType>};
type RawDashboardType = {
news_feed: RawNewsFeedType,
dashboard: FullDashboardType,
};
type PropsType = {
navigation: StackNavigationProp,
route: {params: {nextScreen: string, data: {...}}},
theme: CustomThemeType,
};
type StateType = {
dialogVisible: boolean,
};
/**
* Class defining the app's home screen
*/
class HomeScreen extends React.Component<PropsType, StateType> {
static sortFeedTime = (a: FeedItemType, b: FeedItemType): number =>
b.time - a.time;
static generateNewsFeed(rawFeed: RawNewsFeedType): Array<FeedItemType> {
const finalFeed = [];
Object.keys(rawFeed).forEach((key: string) => {
const category: Array<FeedItemType> | null = rawFeed[key];
if (category != null && category.length > 0) finalFeed.push(...category);
});
finalFeed.sort(HomeScreen.sortFeedTime);
return finalFeed;
}
isLoggedIn: boolean | null;
fabRef: {current: null | AnimatedFAB};
currentNewFeed: Array<FeedItemType>;
currentDashboard: FullDashboardType | null;
dashboardManager: DashboardManager;
constructor(props: PropsType) {
super(props);
this.fabRef = React.createRef();
this.dashboardManager = new DashboardManager(props.navigation);
this.currentNewFeed = [];
this.currentDashboard = null;
this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn();
props.navigation.setOptions({
headerRight: this.getHeaderButton,
});
this.state = {
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);
}
/**
* Updates login state and navigation parameters on screen focus
*/
onScreenFocus = () => {
const {props} = this;
if (ConnectionManager.getInstance().isLoggedIn() !== this.isLoggedIn) {
this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn();
props.navigation.setOptions({
headerRight: this.getHeaderButton,
});
}
// handle link open when home is not focused or created
this.handleNavigationParams();
};
/**
* Gets header buttons based on login state
*
* @returns {*}
*/
getHeaderButton = (): React.Node => {
const {props} = this;
let onPressLog = (): void =>
props.navigation.navigate('login', {nextScreen: 'profile'});
let logIcon = 'login';
let logColor = props.theme.colors.primary;
if (this.isLoggedIn) {
onPressLog = (): void => this.showDisconnectDialog();
logIcon = 'logout';
logColor = props.theme.colors.text;
}
const onPressSettings = (): void => props.navigation.navigate('settings');
return (
<MaterialHeaderButtons>
<Item
title="log"
iconName={logIcon}
color={logColor}
onPress={onPressLog}
/>
<Item
title={i18n.t('screens.settings.title')}
iconName="cog"
onPress={onPressSettings}
/>
</MaterialHeaderButtons>
);
};
/**
* Gets the event dashboard render item.
* If a preview is available, it will be rendered inside
*
* @param content
* @return {*}
*/
getDashboardEvent(content: Array<EventType>): React.Node {
const futureEvents = getFutureEvents(content);
const displayEvent = getDisplayEvent(futureEvents);
// const clickPreviewAction = () =>
// this.props.navigation.navigate('students', {
// screen: 'planning-information',
// params: {data: displayEvent}
// });
return (
<DashboardItem
eventNumber={futureEvents.length}
clickAction={this.onEventContainerClick}>
<PreviewEventDashboardItem
event={displayEvent}
clickAction={this.onEventContainerClick}
/>
</DashboardItem>
);
}
/**
* Gets a dashboard item with action buttons
*
* @returns {*}
*/
getDashboardActions(): React.Node {
const {props} = this;
return (
<ActionsDashBoardItem
navigation={props.navigation}
isLoggedIn={this.isLoggedIn}
/>
);
}
/**
* Gets a dashboard item with a row of shortcut buttons.
*
* @param content
* @return {*}
*/
getDashboardRow(content: Array<ServiceItemType | null>): React.Node {
return (
// $FlowFixMe
<FlatList
data={content}
renderItem={this.getDashboardRowRenderItem}
horizontal
contentContainerStyle={{
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 10,
marginBottom: 10,
}}
/>
);
}
/**
* Gets a dashboard shortcut item
*
* @param item
* @returns {*}
*/
getDashboardRowRenderItem = ({
item,
}: {
item: ServiceItemType | null,
}): React.Node => {
if (item != null)
return (
<SmallDashboardItem
image={item.image}
onPress={item.onPress}
badgeCount={
this.currentDashboard != null && item.badgeFunction != null
? item.badgeFunction(this.currentDashboard)
: null
}
/>
);
return <SmallDashboardItem image={null} onPress={null} badgeCount={null} />;
};
/**
* Gets a render item for the given feed object
*
* @param item The feed item to display
* @return {*}
*/
getFeedItem(item: FeedItemType): React.Node {
const {props} = this;
return (
<FeedItem
navigation={props.navigation}
item={item}
height={FEED_ITEM_HEIGHT}
/>
);
}
/**
* Gets a FlatList render item
*
* @param item The item to display
* @param section The current section
* @return {*}
*/
getRenderItem = ({item}: {item: FeedItemType}): React.Node =>
this.getFeedItem(item);
getRenderSectionHeader = (
data: {
section: {
data: Array<{...}>,
title: string,
},
},
isLoading: boolean,
): React.Node => {
const {props} = this;
if (data.section.data.length > 0)
return (
<Headline
style={{
textAlign: 'center',
marginTop: 50,
marginBottom: 10,
}}>
{data.section.title}
</Headline>
);
return (
<View>
<Headline
style={{
textAlign: 'center',
marginTop: 50,
marginBottom: 10,
marginLeft: 20,
marginRight: 20,
color: props.theme.colors.textDisabled,
}}>
{data.section.title}
</Headline>
{isLoading ? (
<ActivityIndicator
style={{
marginTop: 10,
}}
/>
) : (
<MaterialCommunityIcons
name="access-point-network-off"
size={100}
color={props.theme.colors.textDisabled}
style={{
marginLeft: 'auto',
marginRight: 'auto',
}}
/>
)}
</View>
);
};
getListHeader = (fetchedData: RawDashboardType): React.Node => {
let dashboard = null;
if (fetchedData != null) dashboard = fetchedData.dashboard;
return (
<Animatable.View animation="fadeInDown" duration={500} useNativeDriver>
{this.getDashboardActions()}
{this.getDashboardRow(this.dashboardManager.getCurrentDashboard())}
{this.getDashboardEvent(
dashboard == null ? [] : dashboard.today_events,
)}
</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 = HomeScreen.generateNewsFeed(
fetchedData.news_feed,
);
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.
* This hides the banner and takes the user to the login page.
*/
onLogin = () => {
const {props} = this;
props.navigation.navigate('login', {
nextScreen: 'profile',
});
};
render(): React.Node {
const {props, state} = this;
return (
<View style={{flex: 1}}>
<View
style={{
position: 'absolute',
width: '100%',
height: '100%',
}}>
<WebSectionList
navigation={props.navigation}
createDataset={this.createDataset}
autoRefreshTime={REFRESH_TIME}
refreshOnFocus
fetchUrl={DATA_URL}
renderItem={this.getRenderItem}
itemHeight={FEED_ITEM_HEIGHT}
onScroll={this.onScroll}
showError={false}
renderSectionHeader={this.getRenderSectionHeader}
renderListHeaderComponent={this.getListHeader}
/>
</View>
{!this.isLoggedIn ? (
<MascotPopup
prefKey={AsyncStorageManager.PREFERENCES.homeShowBanner.key}
title={i18n.t('screens.home.mascotDialog.title')}
message={i18n.t('screens.home.mascotDialog.message')}
icon="human-greeting"
buttons={{
action: {
message: i18n.t('screens.home.mascotDialog.login'),
icon: 'login',
onPress: this.onLogin,
},
cancel: {
message: i18n.t('screens.home.mascotDialog.later'),
icon: 'close',
color: props.theme.colors.warning,
},
}}
emotion={MASCOT_STYLE.CUTE}
/>
) : null}
<AnimatedFAB
ref={this.fabRef}
icon="qrcode-scan"
onPress={this.openScanner}
/>
<LogoutDialog
navigation={props.navigation}
visible={state.dialogVisible}
onDismiss={this.hideDisconnectDialog}
/>
</View>
);
}
}
export default withTheme(HomeScreen);