Compare commits

...

3 commits

Author SHA1 Message Date
Arnaud Vergnet
e4530ded18 Update Home base components to use TypeScript 2020-09-22 18:06:08 +02:00
Arnaud Vergnet
140bcf3675 Update animated components to use TypeScript 2020-09-22 17:43:40 +02:00
Arnaud Vergnet
f43dc55735 Update basic screen components to use TypeScript 2020-09-22 17:23:17 +02:00
17 changed files with 680 additions and 702 deletions

View file

@ -17,42 +17,37 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View, ViewStyle} from 'react-native';
import {List, withTheme} from 'react-native-paper'; import {List, withTheme} from 'react-native-paper';
import Collapsible from 'react-native-collapsible'; import Collapsible from 'react-native-collapsible';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import type {CustomThemeType} from '../../managers/ThemeManager';
import type {ListIconPropsType} from '../../constants/PaperStyles';
type PropsType = { type PropsType = {
theme: CustomThemeType, theme: ReactNativePaper.Theme;
title: string, title: string;
subtitle?: string, subtitle?: string;
left?: () => React.Node, style: ViewStyle;
opened?: boolean, left?: (props: {
unmountWhenCollapsed?: boolean, color: string;
children?: React.Node, style?: {
marginRight: number;
marginVertical?: number;
};
}) => React.ReactNode;
opened?: boolean;
unmountWhenCollapsed?: boolean;
children?: React.ReactNode;
}; };
type StateType = { type StateType = {
expanded: boolean, expanded: boolean;
}; };
const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon); const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
class AnimatedAccordion extends React.Component<PropsType, StateType> { class AnimatedAccordion extends React.Component<PropsType, StateType> {
static defaultProps = { chevronRef: {current: null | (typeof AnimatedListIcon & List.Icon)};
subtitle: '',
left: null,
opened: null,
unmountWhenCollapsed: false,
children: null,
};
chevronRef: {current: null | AnimatedListIcon};
chevronIcon: string; chevronIcon: string;
@ -62,6 +57,9 @@ class AnimatedAccordion extends React.Component<PropsType, StateType> {
constructor(props: PropsType) { constructor(props: PropsType) {
super(props); super(props);
this.chevronIcon = '';
this.animStart = '';
this.animEnd = '';
this.state = { this.state = {
expanded: props.opened != null ? props.opened : false, expanded: props.opened != null ? props.opened : false,
}; };
@ -71,8 +69,9 @@ class AnimatedAccordion extends React.Component<PropsType, StateType> {
shouldComponentUpdate(nextProps: PropsType): boolean { shouldComponentUpdate(nextProps: PropsType): boolean {
const {state, props} = this; const {state, props} = this;
if (nextProps.opened != null && nextProps.opened !== props.opened) if (nextProps.opened != null && nextProps.opened !== props.opened) {
state.expanded = nextProps.opened; state.expanded = nextProps.opened;
}
return true; return true;
} }
@ -101,17 +100,17 @@ class AnimatedAccordion extends React.Component<PropsType, StateType> {
} }
}; };
render(): React.Node { render() {
const {props, state} = this; const {props, state} = this;
const {colors} = props.theme; const {colors} = props.theme;
return ( return (
<View> <View style={props.style}>
<List.Item <List.Item
title={props.title} title={props.title}
subtitle={props.subtitle} description={props.subtitle}
titleStyle={state.expanded ? {color: colors.primary} : null} titleStyle={state.expanded ? {color: colors.primary} : null}
onPress={this.toggleAccordion} onPress={this.toggleAccordion}
right={(iconProps: ListIconPropsType): React.Node => ( right={(iconProps) => (
<AnimatedListIcon <AnimatedListIcon
ref={this.chevronRef} ref={this.chevronRef}
style={iconProps.style} style={iconProps.style}

View file

@ -17,29 +17,30 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {StyleSheet, View} from 'react-native'; import {
NativeScrollEvent,
NativeSyntheticEvent,
StyleSheet,
View,
} from 'react-native';
import {FAB, IconButton, Surface, withTheme} from 'react-native-paper'; import {FAB, IconButton, Surface, withTheme} from 'react-native-paper';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
import AutoHideHandler from '../../utils/AutoHideHandler'; import AutoHideHandler from '../../utils/AutoHideHandler';
import CustomTabBar from '../Tabbar/CustomTabBar'; import CustomTabBar from '../Tabbar/CustomTabBar';
import type {CustomThemeType} from '../../managers/ThemeManager';
import type {OnScrollType} from '../../utils/AutoHideHandler';
const AnimatedFAB = Animatable.createAnimatableComponent(FAB); const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
type PropsType = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp<any>;
theme: CustomThemeType, theme: ReactNativePaper.Theme;
onPress: (action: string, data?: string) => void, onPress: (action: string, data?: string) => void;
seekAttention: boolean, seekAttention: boolean;
}; };
type StateType = { type StateType = {
currentMode: string, currentMode: string;
}; };
const DISPLAY_MODES = { const DISPLAY_MODES = {
@ -78,14 +79,14 @@ const styles = StyleSheet.create({
}); });
class AnimatedBottomBar extends React.Component<PropsType, StateType> { class AnimatedBottomBar extends React.Component<PropsType, StateType> {
ref: {current: null | Animatable.View}; ref: {current: null | (Animatable.View & View)};
hideHandler: AutoHideHandler; hideHandler: AutoHideHandler;
displayModeIcons: {[key: string]: string}; displayModeIcons: {[key: string]: string};
constructor() { constructor(props: PropsType) {
super(); super(props);
this.state = { this.state = {
currentMode: DISPLAY_MODES.WEEK, currentMode: DISPLAY_MODES.WEEK,
}; };
@ -108,13 +109,17 @@ class AnimatedBottomBar extends React.Component<PropsType, StateType> {
} }
onHideChange = (shouldHide: boolean) => { onHideChange = (shouldHide: boolean) => {
if (this.ref.current != null) { const ref = this.ref;
if (shouldHide) this.ref.current.fadeOutDown(500); if (ref && ref.current && ref.current.fadeOutDown && ref.current.fadeInUp) {
else this.ref.current.fadeInUp(500); if (shouldHide) {
ref.current.fadeOutDown(500);
} else {
ref.current.fadeInUp(500);
}
} }
}; };
onScroll = (event: OnScrollType) => { onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
this.hideHandler.onScroll(event); this.hideHandler.onScroll(event);
}; };
@ -139,7 +144,7 @@ class AnimatedBottomBar extends React.Component<PropsType, StateType> {
props.onPress('changeView', newMode); props.onPress('changeView', newMode);
}; };
render(): React.Node { render() {
const {props, state} = this; const {props, state} = this;
const buttonColor = props.theme.colors.primary; const buttonColor = props.theme.colors.primary;
return ( return (

View file

@ -17,22 +17,23 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {StyleSheet} from 'react-native'; import {
NativeScrollEvent,
NativeSyntheticEvent,
StyleSheet,
View,
} from 'react-native';
import {FAB} from 'react-native-paper'; import {FAB} from 'react-native-paper';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import AutoHideHandler from '../../utils/AutoHideHandler'; import AutoHideHandler from '../../utils/AutoHideHandler';
import CustomTabBar from '../Tabbar/CustomTabBar'; import CustomTabBar from '../Tabbar/CustomTabBar';
type PropsType = { type PropsType = {
icon: string, icon: string;
onPress: () => void, onPress: () => void;
}; };
const AnimatedFab = Animatable.createAnimatableComponent(FAB);
const styles = StyleSheet.create({ const styles = StyleSheet.create({
fab: { fab: {
position: 'absolute', position: 'absolute',
@ -42,34 +43,42 @@ const styles = StyleSheet.create({
}); });
export default class AnimatedFAB extends React.Component<PropsType> { export default class AnimatedFAB extends React.Component<PropsType> {
ref: {current: null | Animatable.View}; ref: {current: null | (Animatable.View & View)};
hideHandler: AutoHideHandler; hideHandler: AutoHideHandler;
constructor() { constructor(props: PropsType) {
super(); super(props);
this.ref = React.createRef(); this.ref = React.createRef();
this.hideHandler = new AutoHideHandler(false); this.hideHandler = new AutoHideHandler(false);
this.hideHandler.addListener(this.onHideChange); this.hideHandler.addListener(this.onHideChange);
} }
onScroll = (event: SyntheticEvent<EventTarget>) => { onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
this.hideHandler.onScroll(event); this.hideHandler.onScroll(event);
}; };
onHideChange = (shouldHide: boolean) => { onHideChange = (shouldHide: boolean) => {
if (this.ref.current != null) { const ref = this.ref;
if (shouldHide) this.ref.current.bounceOutDown(1000); if (
else this.ref.current.bounceInUp(1000); ref &&
ref.current &&
ref.current.bounceOutDown &&
ref.current.bounceInUp
) {
if (shouldHide) {
ref.current.bounceOutDown(1000);
} else {
ref.current.bounceInUp(1000);
}
} }
}; };
render(): React.Node { render() {
const {props} = this; const {props} = this;
return ( return (
<AnimatedFab <Animatable.View ref={this.ref} useNativeDriver={true}>
ref={this.ref} <FAB
useNativeDriver
icon={props.icon} icon={props.icon}
onPress={props.onPress} onPress={props.onPress}
style={{ style={{
@ -77,6 +86,7 @@ export default class AnimatedFAB extends React.Component<PropsType> {
bottom: CustomTabBar.TAB_BAR_HEIGHT, bottom: CustomTabBar.TAB_BAR_HEIGHT,
}} }}
/> />
</Animatable.View>
); );
} }
} }

View file

@ -1,75 +0,0 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {List, withTheme} from 'react-native-paper';
import {View} from 'react-native';
import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack';
import type {CustomThemeType} from '../../managers/ThemeManager';
import type {ListIconPropsType} from '../../constants/PaperStyles';
type PropsType = {
navigation: StackNavigationProp,
theme: CustomThemeType,
};
class ActionsDashBoardItem extends React.Component<PropsType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return nextProps.theme.dark !== props.theme.dark;
}
render(): React.Node {
const {navigation} = this.props;
return (
<View>
<List.Item
title={i18n.t('screens.feedback.homeButtonTitle')}
description={i18n.t('screens.feedback.homeButtonSubtitle')}
left={(props: ListIconPropsType): React.Node => (
<List.Icon
color={props.color}
style={props.style}
icon="comment-quote"
/>
)}
right={(props: ListIconPropsType): React.Node => (
<List.Icon
color={props.color}
style={props.style}
icon="chevron-right"
/>
)}
onPress={(): void => navigation.navigate('feedback')}
style={{
paddingTop: 0,
paddingBottom: 0,
marginLeft: 10,
marginRight: 10,
}}
/>
</View>
);
}
}
export default withTheme(ActionsDashBoardItem);

View file

@ -0,0 +1,59 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {List} from 'react-native-paper';
import {View} from 'react-native';
import i18n from 'i18n-js';
import {useNavigation} from '@react-navigation/native';
function ActionsDashBoardItem() {
const navigation = useNavigation();
return (
<View>
<List.Item
title={i18n.t('screens.feedback.homeButtonTitle')}
description={i18n.t('screens.feedback.homeButtonSubtitle')}
left={(props) => (
<List.Icon
color={props.color}
style={props.style}
icon="comment-quote"
/>
)}
right={(props) => (
<List.Icon
color={props.color}
style={props.style}
icon="chevron-right"
/>
)}
onPress={(): void => navigation.navigate('feedback')}
style={{
paddingTop: 0,
paddingBottom: 0,
marginLeft: 10,
marginRight: 10,
}}
/>
</View>
);
}
export default ActionsDashBoardItem;

View file

@ -1,116 +0,0 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {
Avatar,
Card,
Text,
TouchableRipple,
withTheme,
} from 'react-native-paper';
import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js';
import type {CustomThemeType} from '../../managers/ThemeManager';
import type {CardTitleIconPropsType} from '../../constants/PaperStyles';
type PropsType = {
eventNumber: number,
clickAction: () => void,
theme: CustomThemeType,
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
*/
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={(iconProps: CardTitleIconPropsType): React.Node => (
<Avatar.Icon
icon="calendar-range"
color={iconColor}
size={iconProps.size}
style={styles.avatar}
/>
)}
/>
<Card.Content>{props.children}</Card.Content>
</View>
</TouchableRipple>
</Card>
);
}
}
export default withTheme(EventDashBoardItem);

View file

@ -0,0 +1,104 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {
Avatar,
Card,
Text,
TouchableRipple,
useTheme,
} from 'react-native-paper';
import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js';
type PropsType = {
eventNumber: number;
clickAction: () => void;
children?: React.ReactNode;
};
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
*/
function EventDashBoardItem(props: PropsType) {
const theme = useTheme();
const isAvailable = props.eventNumber > 0;
const iconColor = isAvailable
? theme.colors.planningColor
: theme.colors.textDisabled;
const textColor = isAvailable ? theme.colors.text : theme.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={(iconProps) => (
<Avatar.Icon
icon="calendar-range"
color={iconColor}
size={iconProps.size}
style={styles.avatar}
/>
)}
/>
<Card.Content>{props.children}</Card.Content>
</View>
</TouchableRipple>
</Card>
);
}
const areEqual = (prevProps: PropsType, nextProps: PropsType): boolean => {
return nextProps.eventNumber !== prevProps.eventNumber;
};
export default React.memo(EventDashBoardItem, areEqual);

View file

@ -1,139 +0,0 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Button, Card, Text, TouchableRipple} from 'react-native-paper';
import {Image, View} from 'react-native';
import Autolink from 'react-native-autolink';
import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack';
import type {FeedItemType} from '../../screens/Home/HomeScreen';
import NewsSourcesConstants from '../../constants/NewsSourcesConstants';
import type {NewsSourceType} from '../../constants/NewsSourcesConstants';
import ImageGalleryButton from '../Media/ImageGalleryButton';
type PropsType = {
navigation: StackNavigationProp,
item: FeedItemType,
height: number,
};
/**
* Component used to display a feed item
*/
class FeedItem extends React.Component<PropsType> {
/**
* Converts a dateString using Unix Timestamp to a formatted date
*
* @param dateString {string} The Unix Timestamp representation of a date
* @return {string} The formatted output date
*/
static getFormattedDate(dateString: number): string {
const date = new Date(dateString * 1000);
return date.toLocaleString();
}
shouldComponentUpdate(): boolean {
return false;
}
onPress = () => {
const {item, navigation} = this.props;
navigation.navigate('feed-information', {
data: item,
date: FeedItem.getFormattedDate(item.time),
});
};
render(): React.Node {
const {item, height, navigation} = this.props;
const image = item.image !== '' && item.image != null ? item.image : null;
const pageSource: NewsSourceType = NewsSourcesConstants[item.page_id];
const cardMargin = 10;
const cardHeight = height - 2 * cardMargin;
const imageSize = 250;
const titleHeight = 80;
const actionsHeight = 60;
const textHeight =
image != null
? 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={pageSource.name}
subtitle={FeedItem.getFormattedDate(item.time)}
left={(): React.Node => (
<Image
size={48}
source={pageSource.icon}
style={{
width: 48,
height: 48,
}}
/>
)}
style={{height: titleHeight}}
/>
{image != null ? (
<ImageGalleryButton
navigation={navigation}
images={[{url: image}]}
style={{
width: imageSize,
height: imageSize,
marginLeft: 'auto',
marginRight: 'auto',
}}
/>
) : 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;

View file

@ -0,0 +1,128 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {Button, Card, Text, TouchableRipple} from 'react-native-paper';
import {Image, View} from 'react-native';
import Autolink from 'react-native-autolink';
import i18n from 'i18n-js';
import type {FeedItemType} from '../../screens/Home/HomeScreen';
import NewsSourcesConstants, {
AvailablePages,
} from '../../constants/NewsSourcesConstants';
import type {NewsSourceType} from '../../constants/NewsSourcesConstants';
import ImageGalleryButton from '../Media/ImageGalleryButton';
import {useNavigation} from '@react-navigation/native';
type PropsType = {
item: FeedItemType;
height: number;
};
/**
* Converts a dateString using Unix Timestamp to a formatted date
*
* @param dateString {string} The Unix Timestamp representation of a date
* @return {string} The formatted output date
*/
function getFormattedDate(dateString: number): string {
const date = new Date(dateString * 1000);
return date.toLocaleString();
}
/**
* Component used to display a feed item
*/
function FeedItem(props: PropsType) {
const navigation = useNavigation();
const onPress = () => {
navigation.navigate('feed-information', {
data: item,
date: getFormattedDate(props.item.time),
});
};
const {item, height} = props;
const image = item.image !== '' && item.image != null ? item.image : null;
const pageSource: NewsSourceType =
NewsSourcesConstants[item.page_id as AvailablePages];
const cardMargin = 10;
const cardHeight = height - 2 * cardMargin;
const imageSize = 250;
const titleHeight = 80;
const actionsHeight = 60;
const textHeight =
image != null
? cardHeight - titleHeight - actionsHeight - imageSize
: cardHeight - titleHeight - actionsHeight;
return (
<Card
style={{
margin: cardMargin,
height: cardHeight,
}}>
<TouchableRipple style={{flex: 1}} onPress={onPress}>
<View>
<Card.Title
title={pageSource.name}
subtitle={getFormattedDate(item.time)}
left={() => (
<Image
source={pageSource.icon}
style={{
width: 48,
height: 48,
}}
/>
)}
style={{height: titleHeight}}
/>
{image != null ? (
<ImageGalleryButton
images={[{url: image}]}
style={{
width: imageSize,
height: imageSize,
marginLeft: 'auto',
marginRight: 'auto',
}}
/>
) : null}
<Card.Content>
{item.message !== undefined ? (
<Autolink<typeof Text>
text={item.message}
hashtag="facebook"
component={Text}
style={{height: textHeight}}
/>
) : null}
</Card.Content>
<Card.Actions style={{height: actionsHeight}}>
<Button onPress={onPress} icon="plus" style={{marginLeft: 'auto'}}>
{i18n.t('screens.home.dashboard.seeMore')}
</Button>
</Card.Actions>
</View>
</TouchableRipple>
</Card>
);
}
export default React.memo(FeedItem, () => true);

View file

@ -1,113 +0,0 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js';
import {Avatar, Button, Card, TouchableRipple} from 'react-native-paper';
import {getTimeOnlyString, isDescriptionEmpty} from '../../utils/Planning';
import CustomHTML from '../Overrides/CustomHTML';
import type {PlanningEventType} from '../../utils/Planning';
type PropsType = {
event?: PlanningEventType | null,
clickAction: () => void,
};
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
*/
// 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={getTimeOnlyString(event.date_begin)}
left={getImage}
/>
) : (
<Card.Title
title={event.title}
subtitle={getTimeOnlyString(event.date_begin)}
/>
)}
{!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;

View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js';
import {Avatar, Button, Card, TouchableRipple} from 'react-native-paper';
import {getTimeOnlyString, isDescriptionEmpty} from '../../utils/Planning';
import CustomHTML from '../Overrides/CustomHTML';
import type {PlanningEventType} from '../../utils/Planning';
type PropsType = {
event?: PlanningEventType | null;
clickAction: () => void;
};
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
*/
function PreviewEventDashboardItem(props: PropsType) {
const {event} = props;
const isEmpty = event == null ? true : isDescriptionEmpty(event.description);
if (event != null) {
const logo = event.logo;
const getImage = logo
? () => (
<Avatar.Image source={{uri: logo}} size={50} style={styles.avatar} />
)
: () => null;
return (
<Card style={styles.card} elevation={3}>
<TouchableRipple style={{flex: 1}} onPress={props.clickAction}>
<View>
<Card.Title
title={event.title}
subtitle={getTimeOnlyString(event.date_begin)}
left={getImage}
/>
{!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;

View file

@ -1,106 +0,0 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Badge, TouchableRipple, withTheme} from 'react-native-paper';
import {Dimensions, Image, View} from 'react-native';
import * as Animatable from 'react-native-animatable';
import type {CustomThemeType} from '../../managers/ThemeManager';
type PropsType = {
image: string | null,
onPress: () => void | null,
badgeCount: number | null,
theme: CustomThemeType,
};
/**
* Component used to render a small dashboard item
*/
class SmallDashboardItem extends React.Component<PropsType> {
itemSize: number;
constructor(props: PropsType) {
super(props);
this.itemSize = Dimensions.get('window').width / 8;
}
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return (
nextProps.theme.dark !== props.theme.dark ||
nextProps.badgeCount !== props.badgeCount
);
}
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 ? (
<Animatable.View
animation="zoomIn"
duration={300}
useNativeDriver
style={{
position: 'absolute',
top: 0,
right: 0,
}}>
<Badge
style={{
backgroundColor: props.theme.colors.primary,
borderColor: props.theme.colors.background,
borderWidth: 2,
}}>
{props.badgeCount}
</Badge>
</Animatable.View>
) : null}
</View>
</TouchableRipple>
);
}
}
export default withTheme(SmallDashboardItem);

View file

@ -0,0 +1,94 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {Badge, TouchableRipple, useTheme} from 'react-native-paper';
import {Dimensions, Image, View} from 'react-native';
import * as Animatable from 'react-native-animatable';
type PropsType = {
image: string | null;
onPress: () => void | null;
badgeCount: number | null;
};
/**
* Component used to render a small dashboard item
*/
function SmallDashboardItem(props: PropsType) {
const itemSize = Dimensions.get('window').width / 8;
const theme = useTheme();
const {image} = props;
return (
<TouchableRipple
onPress={props.onPress}
borderless
style={{
marginLeft: itemSize / 6,
marginRight: itemSize / 6,
}}>
<View
style={{
width: itemSize,
height: itemSize,
}}>
{image ? (
<Image
source={{uri: image}}
style={{
width: '80%',
height: '80%',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 'auto',
marginBottom: 'auto',
}}
/>
) : null}
{props.badgeCount != null && props.badgeCount > 0 ? (
<Animatable.View
animation="zoomIn"
duration={300}
useNativeDriver
style={{
position: 'absolute',
top: 0,
right: 0,
}}>
<Badge
visible={true}
style={{
backgroundColor: theme.colors.primary,
borderColor: theme.colors.background,
borderWidth: 2,
}}>
{props.badgeCount}
</Badge>
</Animatable.View>
) : null}
</View>
</TouchableRipple>
);
}
const areEqual = (prevProps: PropsType, nextProps: PropsType): boolean => {
return nextProps.badgeCount !== prevProps.badgeCount;
};
export default React.memo(SmallDashboardItem, areEqual);

View file

@ -39,7 +39,9 @@ const MaterialHeaderButton = (props: HeaderButtonProps) => {
); );
}; };
const MaterialHeaderButtons = (props: HeaderButtonsProps) => { const MaterialHeaderButtons = (
props: HeaderButtonsProps & {children?: React.ReactNode},
) => {
return ( return (
<HeaderButtons {...props} HeaderButtonComponent={MaterialHeaderButton} /> <HeaderButtons {...props} HeaderButtonComponent={MaterialHeaderButton} />
); );

View file

@ -17,12 +17,15 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {Snackbar} from 'react-native-paper'; import {Snackbar} from 'react-native-paper';
import {RefreshControl, View} from 'react-native'; import {
NativeSyntheticEvent,
RefreshControl,
SectionListData,
View,
} from 'react-native';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import {Collapsible} from 'react-navigation-collapsible'; import {Collapsible} from 'react-navigation-collapsible';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
@ -32,42 +35,43 @@ import withCollapsible from '../../utils/withCollapsible';
import CustomTabBar from '../Tabbar/CustomTabBar'; import CustomTabBar from '../Tabbar/CustomTabBar';
import {ERROR_TYPE, readData} from '../../utils/WebData'; import {ERROR_TYPE, readData} from '../../utils/WebData';
import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList'; import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList';
import type {ApiGenericDataType} from '../../utils/WebData';
export type SectionListDataType<T> = Array<{ export type SectionListDataType<ItemT> = Array<{
title: string, title: string;
data: Array<T>, data: Array<ItemT>;
keyExtractor?: (T) => string, keyExtractor?: (data: ItemT) => string;
}>; }>;
type PropsType<T> = { type PropsType<ItemT, RawData> = {
navigation: StackNavigationProp, navigation: StackNavigationProp<any>;
fetchUrl: string, fetchUrl: string;
autoRefreshTime: number, autoRefreshTime: number;
refreshOnFocus: boolean, refreshOnFocus: boolean;
renderItem: (data: {item: T}) => React.Node, renderItem: (data: {item: ItemT}) => React.ReactNode;
createDataset: ( createDataset: (
data: ApiGenericDataType | null, data: RawData | null,
isLoading?: boolean, isLoading?: boolean,
) => SectionListDataType<T>, ) => SectionListDataType<ItemT>;
onScroll: (event: SyntheticEvent<EventTarget>) => void, onScroll: (event: NativeSyntheticEvent<EventTarget>) => void;
collapsibleStack: Collapsible, collapsibleStack: Collapsible;
showError?: boolean, showError?: boolean;
itemHeight?: number | null, itemHeight?: number | null;
updateData?: number, updateData?: number;
renderListHeaderComponent?: (data: ApiGenericDataType | null) => React.Node, renderListHeaderComponent?: (
data: RawData | null,
) => React.ComponentType<any> | React.ReactElement | null;
renderSectionHeader?: ( renderSectionHeader?: (
data: {section: {title: string}}, data: {section: SectionListData<ItemT>},
isLoading?: boolean, isLoading?: boolean,
) => React.Node, ) => React.ReactElement | null;
stickyHeader?: boolean, stickyHeader?: boolean;
}; };
type StateType = { type StateType<RawData> = {
refreshing: boolean, refreshing: boolean;
fetchedData: ApiGenericDataType | null, fetchedData: RawData | null;
snackbarVisible: boolean, snackbarVisible: boolean;
}; };
const MIN_REFRESH_TIME = 5 * 1000; const MIN_REFRESH_TIME = 5 * 1000;
@ -78,22 +82,25 @@ const MIN_REFRESH_TIME = 5 * 1000;
* This is a pure component, meaning it will only update if a shallow comparison of state and props is different. * This is a pure component, meaning it will only update if a shallow comparison of state and props is different.
* To force the component to update, change the value of updateData. * To force the component to update, change the value of updateData.
*/ */
class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> { class WebSectionList<ItemT, RawData> extends React.PureComponent<
PropsType<ItemT, RawData>,
StateType<RawData>
> {
static defaultProps = { static defaultProps = {
showError: true, showError: true,
itemHeight: null, itemHeight: null,
updateData: 0, updateData: 0,
renderListHeaderComponent: (): React.Node => null, renderListHeaderComponent: () => null,
renderSectionHeader: (): React.Node => null, renderSectionHeader: () => null,
stickyHeader: false, stickyHeader: false,
}; };
refreshInterval: IntervalID; refreshInterval: NodeJS.Timeout | undefined;
lastRefresh: Date | null; lastRefresh: Date | undefined;
constructor() { constructor(props: PropsType<ItemT, RawData>) {
super(); super(props);
this.state = { this.state = {
refreshing: false, refreshing: false,
fetchedData: null, fetchedData: null,
@ -109,7 +116,7 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
const {navigation} = this.props; const {navigation} = this.props;
navigation.addListener('focus', this.onScreenFocus); navigation.addListener('focus', this.onScreenFocus);
navigation.addListener('blur', this.onScreenBlur); navigation.addListener('blur', this.onScreenBlur);
this.lastRefresh = null; this.lastRefresh = undefined;
this.onRefresh(); this.onRefresh();
} }
@ -121,15 +128,18 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
if (props.refreshOnFocus && this.lastRefresh) { if (props.refreshOnFocus && this.lastRefresh) {
setTimeout(this.onRefresh, 200); setTimeout(this.onRefresh, 200);
} }
if (props.autoRefreshTime > 0) if (props.autoRefreshTime > 0) {
this.refreshInterval = setInterval(this.onRefresh, props.autoRefreshTime); this.refreshInterval = setInterval(this.onRefresh, props.autoRefreshTime);
}
}; };
/** /**
* Removes any interval on un-focus * Removes any interval on un-focus
*/ */
onScreenBlur = () => { onScreenBlur = () => {
if (this.refreshInterval) {
clearInterval(this.refreshInterval); clearInterval(this.refreshInterval);
}
}; };
/** /**
@ -138,7 +148,7 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
* *
* @param fetchedData The newly fetched data * @param fetchedData The newly fetched data
*/ */
onFetchSuccess = (fetchedData: ApiGenericDataType) => { onFetchSuccess = (fetchedData: RawData) => {
this.setState({ this.setState({
fetchedData, fetchedData,
refreshing: false, refreshing: false,
@ -167,7 +177,9 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
if (this.lastRefresh != null) { if (this.lastRefresh != null) {
const last = this.lastRefresh; const last = this.lastRefresh;
canRefresh = new Date().getTime() - last.getTime() > MIN_REFRESH_TIME; canRefresh = new Date().getTime() - last.getTime() > MIN_REFRESH_TIME;
} else canRefresh = true; } else {
canRefresh = true;
}
if (canRefresh) { if (canRefresh) {
this.setState({refreshing: true}); this.setState({refreshing: true});
readData(fetchUrl).then(this.onFetchSuccess).catch(this.onFetchError); readData(fetchUrl).then(this.onFetchSuccess).catch(this.onFetchError);
@ -189,19 +201,18 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
}; };
getItemLayout = ( getItemLayout = (
data: T, height: number,
data: Array<SectionListData<ItemT>> | null,
index: number, index: number,
): {length: number, offset: number, index: number} | null => { ): {length: number; offset: number; index: number} => {
const {itemHeight} = this.props;
if (itemHeight == null) return null;
return { return {
length: itemHeight, length: height,
offset: itemHeight * index, offset: height * index,
index, index,
}; };
}; };
getRenderSectionHeader = (data: {section: {title: string}}): React.Node => { getRenderSectionHeader = (data: {section: SectionListData<ItemT>}) => {
const {renderSectionHeader} = this.props; const {renderSectionHeader} = this.props;
const {refreshing} = this.state; const {refreshing} = this.state;
if (renderSectionHeader != null) { if (renderSectionHeader != null) {
@ -214,7 +225,7 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
return null; return null;
}; };
getRenderItem = (data: {item: T}): React.Node => { getRenderItem = (data: {item: ItemT}) => {
const {renderItem} = this.props; const {renderItem} = this.props;
return ( return (
<Animatable.View animation="fadeInUp" duration={500} useNativeDriver> <Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
@ -223,19 +234,23 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
); );
}; };
onScroll = (event: SyntheticEvent<EventTarget>) => { onScroll = (event: NativeSyntheticEvent<EventTarget>) => {
const {onScroll} = this.props; const {onScroll} = this.props;
if (onScroll != null) onScroll(event); if (onScroll != null) {
onScroll(event);
}
}; };
render(): React.Node { render() {
const {props, state} = this; const {props, state} = this;
let dataset = []; const {itemHeight} = props;
let dataset: SectionListDataType<ItemT> = [];
if ( if (
state.fetchedData != null || state.fetchedData != null ||
(state.fetchedData == null && !props.showError) (state.fetchedData == null && !props.showError)
) ) {
dataset = props.createDataset(state.fetchedData, state.refreshing); dataset = props.createDataset(state.fetchedData, state.refreshing);
}
const {containerPaddingTop} = props.collapsibleStack; const {containerPaddingTop} = props.collapsibleStack;
return ( return (
@ -270,7 +285,11 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
/> />
) )
} }
getItemLayout={props.itemHeight != null ? this.getItemLayout : null} getItemLayout={
itemHeight
? (data, index) => this.getItemLayout(itemHeight, data, index)
: undefined
}
onScroll={this.onScroll} onScroll={this.onScroll}
hasTab hasTab
/> />

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import WebView from 'react-native-webview'; import WebView from 'react-native-webview';
import { import {
@ -27,12 +25,17 @@ import {
OverflowMenu, OverflowMenu,
} from 'react-navigation-header-buttons'; } from 'react-navigation-header-buttons';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {Animated, BackHandler, Linking} from 'react-native'; import {
Animated,
BackHandler,
Linking,
NativeScrollEvent,
NativeSyntheticEvent,
} from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
import {Collapsible} from 'react-navigation-collapsible'; import {Collapsible} from 'react-navigation-collapsible';
import type {CustomThemeType} from '../../managers/ThemeManager';
import withCollapsible from '../../utils/withCollapsible'; import withCollapsible from '../../utils/withCollapsible';
import MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton'; import MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton';
import {ERROR_TYPE} from '../../utils/WebData'; import {ERROR_TYPE} from '../../utils/WebData';
@ -40,15 +43,15 @@ import ErrorView from './ErrorView';
import BasicLoadingScreen from './BasicLoadingScreen'; import BasicLoadingScreen from './BasicLoadingScreen';
type PropsType = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp<any>;
theme: CustomThemeType, theme: ReactNativePaper.Theme;
url: string, url: string;
collapsibleStack: Collapsible, collapsibleStack: Collapsible;
onMessage: (event: {nativeEvent: {data: string}}) => void, onMessage: (event: {nativeEvent: {data: string}}) => void;
onScroll: (event: SyntheticEvent<EventTarget>) => void, onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
customJS?: string, customJS?: string;
customPaddingFunction?: null | ((padding: number) => string), customPaddingFunction?: null | ((padding: number) => string);
showAdvancedControls?: boolean, showAdvancedControls?: boolean;
}; };
const AnimatedWebView = Animated.createAnimatedComponent(WebView); const AnimatedWebView = Animated.createAnimatedComponent(WebView);
@ -67,8 +70,8 @@ class WebViewScreen extends React.PureComponent<PropsType> {
canGoBack: boolean; canGoBack: boolean;
constructor() { constructor(props: PropsType) {
super(); super(props);
this.webviewRef = React.createRef(); this.webviewRef = React.createRef();
this.canGoBack = false; this.canGoBack = false;
} }
@ -115,7 +118,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
* *
* @return {*} * @return {*}
*/ */
getBasicButton = (): React.Node => { getBasicButton = () => {
return ( return (
<MaterialHeaderButtons> <MaterialHeaderButtons>
<Item <Item
@ -138,7 +141,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
* *
* @returns {*} * @returns {*}
*/ */
getAdvancedButtons = (): React.Node => { getAdvancedButtons = () => {
const {props} = this; const {props} = this;
return ( return (
<MaterialHeaderButtons> <MaterialHeaderButtons>
@ -179,7 +182,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
* *
* @return {*} * @return {*}
*/ */
getRenderLoading = (): React.Node => <BasicLoadingScreen isAbsolute />; getRenderLoading = () => <BasicLoadingScreen isAbsolute />;
/** /**
* Gets the javascript needed to generate a padding on top of the page * Gets the javascript needed to generate a padding on top of the page
@ -201,15 +204,21 @@ class WebViewScreen extends React.PureComponent<PropsType> {
* Callback to use when refresh button is clicked. Reloads the webview. * Callback to use when refresh button is clicked. Reloads the webview.
*/ */
onRefreshClicked = () => { onRefreshClicked = () => {
if (this.webviewRef.current != null) this.webviewRef.current.reload(); if (this.webviewRef.current != null) {
this.webviewRef.current.reload();
}
}; };
onGoBackClicked = () => { onGoBackClicked = () => {
if (this.webviewRef.current != null) this.webviewRef.current.goBack(); if (this.webviewRef.current != null) {
this.webviewRef.current.goBack();
}
}; };
onGoForwardClicked = () => { onGoForwardClicked = () => {
if (this.webviewRef.current != null) this.webviewRef.current.goForward(); if (this.webviewRef.current != null) {
this.webviewRef.current.goForward();
}
}; };
onOpenClicked = () => { onOpenClicked = () => {
@ -217,9 +226,11 @@ class WebViewScreen extends React.PureComponent<PropsType> {
Linking.openURL(url); Linking.openURL(url);
}; };
onScroll = (event: SyntheticEvent<EventTarget>) => { onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const {onScroll} = this.props; const {onScroll} = this.props;
if (onScroll) onScroll(event); if (onScroll) {
onScroll(event);
}
}; };
/** /**
@ -228,11 +239,12 @@ class WebViewScreen extends React.PureComponent<PropsType> {
* @param script The script to inject * @param script The script to inject
*/ */
injectJavaScript = (script: string) => { injectJavaScript = (script: string) => {
if (this.webviewRef.current != null) if (this.webviewRef.current != null) {
this.webviewRef.current.injectJavaScript(script); this.webviewRef.current.injectJavaScript(script);
}
}; };
render(): React.Node { render() {
const {props} = this; const {props} = this;
const {containerPaddingTop, onScrollWithListener} = props.collapsibleStack; const {containerPaddingTop, onScrollWithListener} = props.collapsibleStack;
return ( return (
@ -243,7 +255,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
injectedJavaScript={props.customJS} injectedJavaScript={props.customJS}
javaScriptEnabled javaScriptEnabled
renderLoading={this.getRenderLoading} renderLoading={this.getRenderLoading}
renderError={(): React.Node => ( renderError={() => (
<ErrorView <ErrorView
errorCode={ERROR_TYPE.CONNECTION_ERROR} errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={this.onRefreshClicked} onRefresh={this.onRefreshClicked}
@ -257,7 +269,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop)); this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop));
}} }}
// Animations // Animations
onScroll={onScrollWithListener(this.onScroll)} onScroll={(event) => onScrollWithListener(this.onScroll)(event)}
/> />
); );
} }

View file

@ -27,6 +27,8 @@ export type NewsSourceType = {
name: string; name: string;
}; };
export type AvailablePages = 'amicale.deseleves' | 'campus.insat';
export default { export default {
'amicale.deseleves': { 'amicale.deseleves': {
icon: ICON_AMICALE, icon: ICON_AMICALE,