Compare commits

..

5 commits

10 changed files with 407 additions and 91 deletions

View file

@ -0,0 +1,117 @@
// @flow
import * as React from 'react';
import {StyleSheet, View} from "react-native";
import {IconButton, Surface, withTheme} from "react-native-paper";
import AutoHideComponent from "./AutoHideComponent";
type Props = {
theme: Object,
onPress: Function,
}
type State = {
currentMode: string,
}
const DISPLAY_MODES = {
DAY: "agendaDay",
WEEK: "agendaWeek",
MONTH: "month",
}
class AnimatedBottomBar extends React.Component<Props, State> {
ref: Object;
displayModeIcons: Object;
state = {
currentMode: DISPLAY_MODES.WEEK,
}
constructor() {
super();
this.ref = React.createRef();
this.displayModeIcons = {};
this.displayModeIcons[DISPLAY_MODES.DAY] = "calendar-text";
this.displayModeIcons[DISPLAY_MODES.WEEK] = "calendar-week";
this.displayModeIcons[DISPLAY_MODES.MONTH] = "calendar-range";
}
onScroll = (event: Object) => {
this.ref.current.onScroll(event);
};
changeDisplayMode = () => {
let newMode;
switch (this.state.currentMode) {
case DISPLAY_MODES.DAY:
newMode = DISPLAY_MODES.WEEK;
break;
case DISPLAY_MODES.WEEK:
newMode = DISPLAY_MODES.MONTH;
break;
case DISPLAY_MODES.MONTH:
newMode = DISPLAY_MODES.DAY;
break;
}
this.setState({currentMode: newMode});
this.props.onPress("changeView", newMode);
};
render() {
const buttonColor = this.props.theme.colors.primary;
return (
<AutoHideComponent
ref={this.ref}
style={styles.container}>
<Surface style={styles.surface}>
<View style={{flexDirection: 'row'}}>
<IconButton
icon={this.displayModeIcons[this.state.currentMode]}
color={buttonColor}
onPress={this.changeDisplayMode}/>
<IconButton
icon="clock-in"
color={buttonColor}
style={{marginLeft: 5}}
onPress={() => this.props.onPress('today', undefined)}/>
</View>
<View style={{flexDirection: 'row'}}>
<IconButton
icon="chevron-left"
color={buttonColor}
onPress={() => this.props.onPress('prev', undefined)}/>
<IconButton
icon="chevron-right"
color={buttonColor}
style={{marginLeft: 5}}
onPress={() => this.props.onPress('next', undefined)}/>
</View>
</Surface>
</AutoHideComponent>
);
}
}
const styles = StyleSheet.create({
container: {
position: 'absolute',
left: '5%',
bottom: 10,
width: '90%',
},
surface: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderRadius: 50,
elevation: 2,
padding: 10,
paddingHorizontal: 20,
}
});
export default withTheme(AnimatedBottomBar);

View file

@ -0,0 +1,52 @@
// @flow
import * as React from 'react';
import {StyleSheet} from "react-native";
import {FAB} from "react-native-paper";
import {AnimatedValue} from "react-native-reanimated";
import AutoHideComponent from "./AutoHideComponent";
type Props = {
icon: string,
onPress: Function,
}
type State = {
fabPosition: AnimatedValue
}
export default class AnimatedFAB extends React.Component<Props, State> {
ref: Object;
constructor() {
super();
this.ref = React.createRef();
}
onScroll = (event: Object) => {
this.ref.current.onScroll(event);
};
render() {
return (
<AutoHideComponent
ref={this.ref}
style={styles.fab}>
<FAB
icon={this.props.icon}
onPress={this.props.onPress}
/>
</AutoHideComponent>
);
}
}
const styles = StyleSheet.create({
fab: {
position: 'absolute',
margin: 16,
right: 0,
bottom: 0,
},
});

View file

@ -0,0 +1,74 @@
// @flow
import * as React from 'react';
import {Animated} from 'react-native'
import {AnimatedValue} from "react-native-reanimated";
type Props = {
children: React.Node,
style: Object,
}
type State = {
fabPosition: AnimatedValue
}
export default class AutoHideComponent extends React.Component<Props, State> {
isAnimationDownPlaying: boolean;
isAnimationUpPlaying: boolean;
downAnimation;
upAnimation;
state = {
fabPosition: new Animated.Value(0),
};
constructor() {
super();
}
onScroll({nativeEvent}: Object) {
if (nativeEvent.velocity.y > 0.2) { // Go down
if (!this.isAnimationDownPlaying) {
this.isAnimationDownPlaying = true;
if (this.isAnimationUpPlaying)
this.upAnimation.stop();
this.downAnimation = Animated.spring(this.state.fabPosition, {
toValue: 100,
duration: 50,
useNativeDriver: true,
});
this.downAnimation.start(() => {
this.isAnimationDownPlaying = false
});
}
} else if (nativeEvent.velocity.y < -0.2) { // Go up
if (!this.isAnimationUpPlaying) {
this.isAnimationUpPlaying = true;
if (this.isAnimationDownPlaying)
this.downAnimation.stop();
this.upAnimation = Animated.spring(this.state.fabPosition, {
toValue: 0,
duration: 50,
useNativeDriver: true,
});
this.upAnimation.start(() => {
this.isAnimationUpPlaying = false
});
}
}
}
render() {
return (
<Animated.View style={{
...this.props.style,
transform: [{translateY: this.state.fabPosition}]
}}>
{this.props.children}
</Animated.View>
);
}
}

View file

@ -17,6 +17,8 @@ type Props = {
url: string,
customJS: string,
collapsibleStack: Object,
onMessage: Function,
onScroll: Function,
}
const AnimatedWebView = Animated.createAnimatedComponent(WebView);
@ -105,12 +107,16 @@ class WebViewScreen extends React.PureComponent<Props> {
/**
* Callback to use when refresh button is clicked. Reloads the webview.
*/
onRefreshClicked = () => this.webviewRef.current.reload();
onGoBackClicked = () => this.webviewRef.current.goBack();
onGoForwardClicked = () => this.webviewRef.current.goForward();
onRefreshClicked = () => this.webviewRef.current.getNode().reload(); // Need to call getNode() as we are working with animated components
onGoBackClicked = () => this.webviewRef.current.getNode().goBack();
onGoForwardClicked = () => this.webviewRef.current.getNode().goForward();
onOpenClicked = () => Linking.openURL(this.props.url);
postMessage = (message: string) => {
this.webviewRef.current.getNode().postMessage(message);
}
/**
* Gets the loading indicator
*
@ -150,7 +156,7 @@ class WebViewScreen extends React.PureComponent<Props> {
}
render() {
const {containerPaddingTop, onScroll} = this.props.collapsibleStack;
const {containerPaddingTop, onScrollWithListener} = this.props.collapsibleStack;
const customJS = this.getJavascriptPadding(containerPaddingTop);
return (
<AnimatedWebView
@ -167,8 +173,9 @@ class WebViewScreen extends React.PureComponent<Props> {
onNavigationStateChange={navState => {
this.canGoBack = navState.canGoBack;
}}
onMessage={this.props.onMessage}
// Animations
onScroll={onScroll}
onScroll={onScrollWithListener(this.props.onScroll)}
/>
);
}

View file

@ -1,11 +1,11 @@
// @flow
import * as React from 'react';
import {Animated, FlatList, StyleSheet, View} from 'react-native';
import {Animated, FlatList, View} from 'react-native';
import i18n from "i18n-js";
import DashboardItem from "../components/Home/EventDashboardItem";
import WebSectionList from "../components/Lists/WebSectionList";
import {FAB, withTheme} from 'react-native-paper';
import {withTheme} from 'react-native-paper';
import FeedItem from "../components/Home/FeedItem";
import SquareDashboardItem from "../components/Home/SmallDashboardItem";
import PreviewEventDashboardItem from "../components/Home/PreviewEventDashboardItem";
@ -15,6 +15,7 @@ import ConnectionManager from "../managers/ConnectionManager";
import {CommonActions} from '@react-navigation/native';
import MaterialHeaderButtons, {Item} from "../components/Custom/HeaderButton";
import {AnimatedValue} from "react-native-reanimated";
import AnimatedFAB from "../components/Custom/AnimatedFAB";
// import DATA from "../dashboard_data.json";
@ -27,8 +28,6 @@ const SECTIONS_ID = [
'news_feed'
];
const AnimatedFAB = Animated.createAnimatedComponent(FAB);
const REFRESH_TIME = 1000 * 20; // Refresh every 20 seconds
type Props = {
@ -38,7 +37,6 @@ type Props = {
}
type State = {
showFab: boolean,
fabPosition: AnimatedValue
}
@ -50,23 +48,18 @@ class HomeScreen extends React.Component<Props, State> {
colors: Object;
isLoggedIn: boolean | null;
isAnimationDownPlaying: boolean;
isAnimationUpPlaying: boolean;
downAnimation;
upAnimation;
fabRef: Object;
state = {
showFab: true,
fabPosition: new Animated.Value(0),
};
constructor(props) {
super(props);
this.colors = props.theme.colors;
this.isAnimationDownPlaying = false;
this.isAnimationUpPlaying = false;
this.isLoggedIn = null;
this.fabRef = React.createRef();
}
/**
@ -460,36 +453,8 @@ class HomeScreen extends React.Component<Props, State> {
openScanner = () => this.props.navigation.navigate("scanner");
onScroll = ({nativeEvent}: Object) => {
if (nativeEvent.velocity.y > 0.2) { // Go down
if (!this.isAnimationDownPlaying) {
this.isAnimationDownPlaying = true;
if (this.isAnimationUpPlaying)
this.upAnimation.stop();
this.downAnimation = Animated.spring(this.state.fabPosition, {
toValue: 100,
duration: 50,
useNativeDriver: true,
});
this.downAnimation.start(() => {
this.isAnimationDownPlaying = false
});
}
} else if (nativeEvent.velocity.y < -0.2) { // Go up
if (!this.isAnimationUpPlaying) {
this.isAnimationUpPlaying = true;
if (this.isAnimationDownPlaying)
this.downAnimation.stop();
this.upAnimation = Animated.spring(this.state.fabPosition, {
toValue: 0,
duration: 50,
useNativeDriver: true,
});
this.upAnimation.start(() => {
this.isAnimationUpPlaying = false
});
}
}
onScroll = (event: Object) => {
this.fabRef.current.onScroll(event);
};
render() {
@ -507,10 +472,7 @@ class HomeScreen extends React.Component<Props, State> {
onScroll={this.onScroll}
/>
<AnimatedFAB
style={{
...styles.fab,
transform: [{translateY: this.state.fabPosition}]
}}
ref={this.fabRef}
icon="qrcode-scan"
onPress={this.openScanner}
/>
@ -519,13 +481,4 @@ class HomeScreen extends React.Component<Props, State> {
}
}
const styles = StyleSheet.create({
fab: {
position: 'absolute',
margin: 16,
right: 0,
bottom: 0,
},
});
export default withTheme(HomeScreen);

View file

@ -14,6 +14,7 @@ import CustomModal from "../../components/Custom/CustomModal";
import AprilFoolsManager from "../../managers/AprilFoolsManager";
import MaterialHeaderButtons, {Item} from "../../components/Custom/HeaderButton";
import ProxiwashSectionHeader from "../../components/Lists/ProxiwashSectionHeader";
import {withCollapsible} from "../../utils/withCollapsible";
const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/washinsa/washinsa.json";
@ -25,6 +26,7 @@ const LIST_ITEM_HEIGHT = 64;
type Props = {
navigation: Object,
theme: Object,
collapsibleStack: Object,
}
type State = {
@ -416,9 +418,13 @@ class ProxiwashScreen extends React.Component<Props, State> {
render() {
const nav = this.props.navigation;
const {containerPaddingTop} = this.props.collapsibleStack;
return (
<View>
<Banner
style={{
marginTop: this.state.bannerVisible ? containerPaddingTop : 0,
}}
visible={this.state.bannerVisible}
actions={[
{
@ -450,4 +456,4 @@ class ProxiwashScreen extends React.Component<Props, State> {
}
}
export default withTheme(ProxiwashScreen);
export default withCollapsible(withTheme(ProxiwashScreen));

View file

@ -9,7 +9,7 @@ type Props = {
const ROOM_URL = 'http://planex.insa-toulouse.fr/salles.php';
const CUSTOM_CSS_GENERAL = 'https://etud.insa-toulouse.fr/~amicale_app/custom_css/rooms/customMobile.css';
const CUSTOM_CSS_GENERAL = 'https://etud.insa-toulouse.fr/~amicale_app/custom_css/rooms/customMobile2.css';
/**
* Class defining the app's available rooms screen.

View file

@ -3,25 +3,32 @@
import * as React from 'react';
import ThemeManager from "../../managers/ThemeManager";
import WebViewScreen from "../../components/Screens/WebViewScreen";
import {Avatar, Banner} from "react-native-paper";
import {Avatar, Banner, withTheme} from "react-native-paper";
import i18n from "i18n-js";
import {View} from "react-native";
import AsyncStorageManager from "../../managers/AsyncStorageManager";
import AlertDialog from "../../components/Dialog/AlertDialog";
import {withCollapsible} from "../../utils/withCollapsible";
import {dateToString, getTimeOnlyString} from "../../utils/Planning";
import DateManager from "../../managers/DateManager";
import AnimatedBottomBar from "../../components/Custom/AnimatedBottomBar";
type Props = {
navigation: Object,
theme: Object,
collapsibleStack: Object,
}
type State = {
bannerVisible: boolean,
dialogVisible: boolean,
dialogTitle: string,
dialogMessage: string,
}
const PLANEX_URL = 'http://planex.insa-toulouse.fr/';
const CUSTOM_CSS_GENERAL = 'https://etud.insa-toulouse.fr/~amicale_app/custom_css/planex/customMobile3.css';
const CUSTOM_CSS_NIGHTMODE = 'https://etud.insa-toulouse.fr/~amicale_app/custom_css/planex/customDark2.css';
// // JS + JQuery functions used to remove alpha from events. Copy paste in browser console for quick testing
// // Remove alpha from given Jquery node
// function removeAlpha(node) {
@ -79,19 +86,54 @@ const OBSERVE_MUTATIONS_INJECTED =
'$(".fc-event-container .fc-event").each(function(index) {\n' +
' removeAlpha($(this));\n' +
'});';
const FULL_CALENDAR_SETTINGS = `
var calendar = $('#calendar').fullCalendar('getCalendar');
calendar.option({
eventClick: function (data, event, view) {
var message = {
title: data.title,
color: data.color,
start: data.start._d,
end: data.end._d,
};
window.ReactNativeWebView.postMessage(JSON.stringify(message));
}
});`;
const LISTEN_TO_MESSAGES = `
document.addEventListener("message", function(event) {
//alert(event.data);
var data = JSON.parse(event.data);
$('#calendar').fullCalendar(data.action, data.data);
}, false);`
const CUSTOM_CSS = "body>.container{padding-top:20px; padding-bottom: 50px}header{display:none}.fc-toolbar .fc-center{width:100%}.fc-toolbar .fc-center>*{float:none;width:100%;margin:0}#entite{margin-bottom:5px!important}#entite,#groupe{width:calc(100% - 20px);margin:0 10px}#calendar .fc-left,#calendar .fc-right{display:none}#groupe_visibility{width:100%}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-time{font-size:.5rem}#calendar .fc-month-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-month-view .fc-content-skeleton .fc-time{font-size:.7rem}.fc-axis{font-size:.8rem;width:15px!important}.fc-day-header{font-size:.8rem}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}";
const CUSTOM_CSS_DARK = "body{background-color:#121212}.fc-unthemed .fc-content,.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-list-view,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#222}.fc-toolbar .fc-center>*,h2,table{color:#fff}.fc-event-container{color:#121212}.fc-event-container .fc-bg{opacity:0.2;background-color:#000}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}";
const INJECT_STYLE = `
$('head').append('<meta name="viewport" content="width=device-width, initial-scale=0.9">');
$('head').append('<style>` + CUSTOM_CSS + `</style>');
`;
/**
* Class defining the app's Planex screen.
* This screen uses a webview to render the page
*/
export default class PlanexScreen extends React.Component<Props, State> {
class PlanexScreen extends React.Component<Props, State> {
webScreenRef: Object;
barRef: Object;
customInjectedJS: string;
onHideBanner: Function;
onGoToSettings: Function;
state = {
bannerVisible:
AsyncStorageManager.getInstance().preferences.planexShowBanner.current === '1' &&
AsyncStorageManager.getInstance().preferences.defaultStartScreen.current !== 'Planex',
dialogVisible: false,
dialogTitle: "",
dialogMessage: "",
};
/**
@ -99,51 +141,106 @@ export default class PlanexScreen extends React.Component<Props, State> {
*/
constructor() {
super();
this.webScreenRef = React.createRef();
this.barRef = React.createRef();
this.generateInjectedCSS();
}
generateInjectedCSS() {
this.customInjectedJS =
'$(document).ready(function() {' +
"$(document).ready(function() {" +
OBSERVE_MUTATIONS_INJECTED +
'$("head").append(\'<meta name="viewport" content="width=device-width, initial-scale=0.9">\');' +
'$("head").append(\'<link rel="stylesheet" href="' + CUSTOM_CSS_GENERAL + '" type="text/css"/>\');';
FULL_CALENDAR_SETTINGS +
LISTEN_TO_MESSAGES +
INJECT_STYLE;
if (ThemeManager.getNightMode())
this.customInjectedJS += '$("head").append(\'<link rel="stylesheet" href="' + CUSTOM_CSS_NIGHTMODE + '" type="text/css"/>\');';
this.customInjectedJS += "$('head').append('<style>" + CUSTOM_CSS_DARK + "</style>');";
this.customInjectedJS +=
'removeAlpha();' +
'});true;'; // Prevents crash on ios
this.onHideBanner = this.onHideBanner.bind(this);
this.onGoToSettings = this.onGoToSettings.bind(this);
}
componentWillUpdate(prevProps: Props) {
if (prevProps.theme.dark !== this.props.theme.dark)
this.generateInjectedCSS();
}
/**
* Callback used when closing the banner.
* This hides the banner and saves to preferences to prevent it from reopening
*/
onHideBanner() {
onHideBanner = () => {
this.setState({bannerVisible: false});
AsyncStorageManager.getInstance().savePref(
AsyncStorageManager.getInstance().preferences.planexShowBanner.key,
'0'
);
}
};
/**
* Callback used when the used click on the navigate to settings button.
* This will hide the banner and open the SettingsScreen
*
*/
onGoToSettings() {
onGoToSettings = () => {
this.onHideBanner();
this.props.navigation.navigate('settings');
};
sendMessage = (action: string, data: any) => {
this.webScreenRef.current.postMessage(JSON.stringify({action: action, data: data}));
}
onMessage = (event: Object) => {
let data = JSON.parse(event.nativeEvent.data);
let startDate = dateToString(new Date(data.start), true);
let endDate = dateToString(new Date(data.end), true);
let msg = DateManager.getInstance().getTranslatedDate(startDate) + "\n";
msg += getTimeOnlyString(startDate) + ' - ' + getTimeOnlyString(endDate);
this.showDialog(data.title, msg)
};
showDialog = (title: string, message: string) => {
this.setState({
dialogVisible: true,
dialogTitle: title,
dialogMessage: message,
});
};
hideDialog = () => {
this.setState({
dialogVisible: false,
});
};
onScroll = (event: Object) => {
this.barRef.current.onScroll(event);
};
getWebView() {
return (
<WebViewScreen
ref={this.webScreenRef}
navigation={this.props.navigation}
url={PLANEX_URL}
customJS={this.customInjectedJS}
onMessage={this.onMessage}
onScroll={this.onScroll}
/>
);
}
render() {
const nav = this.props.navigation;
const {containerPaddingTop} = this.props.collapsibleStack;
return (
<View style={{
height: '100%'
}}>
<View style={{height: '100%'}}>
<Banner
style={{
marginTop: this.state.bannerVisible ? containerPaddingTop : 0,
}}
visible={this.state.bannerVisible}
actions={[
{
@ -163,12 +260,21 @@ export default class PlanexScreen extends React.Component<Props, State> {
>
{i18n.t('planexScreen.enableStartScreen')}
</Banner>
<WebViewScreen
navigation={nav}
url={PLANEX_URL}
customJS={this.customInjectedJS}/>
<AlertDialog
visible={this.state.dialogVisible}
onDismiss={this.hideDialog}
title={this.state.dialogTitle}
message={this.state.dialogMessage}/>
{this.props.theme.dark // Force component theme update
? this.getWebView()
: <View style={{height: '100%'}}>{this.getWebView()}</View>}
<AnimatedBottomBar
ref={this.barRef}
onPress={this.sendMessage}
/>
</View>
);
}
}
export default withCollapsible(withTheme(PlanexScreen));

View file

@ -119,11 +119,12 @@ export function stringToDate(dateString: string): Date | null {
* @param date The date object to convert
* @return {string} The converted string
*/
export function dateToString(date: Date): string {
export function dateToString(date: Date, isUTC: boolean): string {
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0'); //January is 0!
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const h = isUTC ? date.getUTCHours() : date.getHours();
const hours = String(h).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes;
}

View file

@ -2,7 +2,7 @@ import React from 'react';
import {useCollapsibleStack} from "react-navigation-collapsible";
export const withCollapsible = (Component: any) => {
return (props: any) => {
return <Component collapsibleStack={useCollapsibleStack()} {...props} />;
};
return React.forwardRef((props: any, ref: any) => {
return <Component collapsibleStack={useCollapsibleStack()} ref={ref} {...props} />;
});
};