diff --git a/src/screens/About/AboutDependenciesScreen.js b/src/screens/About/AboutDependenciesScreen.js index 8e77afa..9c21693 100644 --- a/src/screens/About/AboutDependenciesScreen.js +++ b/src/screens/About/AboutDependenciesScreen.js @@ -4,6 +4,7 @@ import * as React from 'react'; import {FlatList} from "react-native"; import packageJson from '../../../package'; import {List} from 'react-native-paper'; +import {StackNavigationProp} from "@react-navigation/stack"; type listItem = { name: string, @@ -16,7 +17,7 @@ type listItem = { * @param object The raw json * @return {Array} */ -function generateListFromObject(object: { [string]: string }): Array { +function generateListFromObject(object: { [key: string]: string }): Array { let list = []; let keys = Object.keys(object); let values = Object.values(object); @@ -28,8 +29,7 @@ function generateListFromObject(object: { [string]: string }): Array { } type Props = { - navigation: Object, - route: Object + navigation: StackNavigationProp, } const LIST_ITEM_HEIGHT = 64; @@ -39,23 +39,23 @@ const LIST_ITEM_HEIGHT = 64; */ export default class AboutDependenciesScreen extends React.Component { - data: Array; + data: Array; constructor() { super(); this.data = generateListFromObject(packageJson.dependencies); } - keyExtractor = (item: Object) => item.name; + keyExtractor = (item: listItem) => item.name; - renderItem = ({item}: Object) => + renderItem = ({item}: { item: listItem }) => ; - itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index}); + itemLayout = (data: any, index: number) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index}); render() { return ( diff --git a/src/screens/About/AboutScreen.js b/src/screens/About/AboutScreen.js index d2544db..ceddccf 100644 --- a/src/screens/About/AboutScreen.js +++ b/src/screens/About/AboutScreen.js @@ -5,6 +5,14 @@ import {FlatList, Linking, Platform, View} from 'react-native'; import i18n from "i18n-js"; import {Avatar, Card, List, Title, withTheme} from 'react-native-paper'; import packageJson from "../../../package.json"; +import {StackNavigationProp} from "@react-navigation/stack"; + +type ListItem = { + onPressCallback: () => void, + icon: string, + text: string, + showChevron: boolean +}; const links = { appstore: 'https://apps.apple.com/us/app/campus-amicale-insat/id1477722148', @@ -29,7 +37,7 @@ const links = { }; type Props = { - navigation: Object, + navigation: StackNavigationProp, }; /** @@ -48,7 +56,7 @@ class AboutScreen extends React.Component { /** * Data to be displayed in the app card */ - appData: Array = [ + appData = [ { onPressCallback: () => openWebLink(Platform.OS === "ios" ? links.appstore : links.playstore), icon: Platform.OS === "ios" ? 'apple' : 'google-play', @@ -83,7 +91,7 @@ class AboutScreen extends React.Component { /** * Data to be displayed in the author card */ - authorData: Array = [ + authorData = [ { onPressCallback: () => openWebLink(links.meme), icon: 'account-circle', @@ -106,7 +114,7 @@ class AboutScreen extends React.Component { /** * Data to be displayed in the additional developer card */ - additionalDevData: Array = [ + additionalDevData = [ { onPressCallback: () => console.log('Meme this'), icon: 'account', @@ -129,7 +137,7 @@ class AboutScreen extends React.Component { /** * Data to be displayed in the technologies card */ - technoData: Array = [ + technoData = [ { onPressCallback: () => openWebLink(links.react), icon: 'react', @@ -146,7 +154,7 @@ class AboutScreen extends React.Component { /** * Order of information cards */ - dataOrder: Array = [ + dataOrder = [ { id: 'app', }, @@ -158,16 +166,9 @@ class AboutScreen extends React.Component { }, ]; - - colors: Object; - - constructor(props) { - super(props); - this.colors = props.theme.colors; - } - /** * Gets the app icon + * * @param props * @return {*} */ @@ -187,7 +188,7 @@ class AboutScreen extends React.Component { * @param item The item to extract the key from * @return {string} The extracted key */ - keyExtractor(item: Object): string { + keyExtractor(item: ListItem): string { return item.icon; } @@ -271,7 +272,7 @@ class AboutScreen extends React.Component { * @param props * @return {*} */ - getChevronIcon(props: Object) { + getChevronIcon(props) { return ( ); @@ -284,18 +285,18 @@ class AboutScreen extends React.Component { * @param props * @return {*} */ - getItemIcon(item: Object, props: Object) { + getItemIcon(item: ListItem, props) { return ( ); } /** - * Get a clickable card item to be rendered inside a card. + * Gets a clickable card item to be rendered inside a card. * * @returns {*} */ - getCardItem = ({item}: Object) => { + getCardItem = ({item}: { item: ListItem }) => { const getItemIcon = this.getItemIcon.bind(this, item); if (item.showChevron) { return ( @@ -323,7 +324,7 @@ class AboutScreen extends React.Component { * @param item The item to show * @return {*} */ - getMainCard = ({item}: Object) => { + getMainCard = ({item}: { item: { id: string } }) => { switch (item.id) { case 'app': return this.getAppCard(); diff --git a/src/screens/About/DebugScreen.js b/src/screens/About/DebugScreen.js index e9f8e8d..f1a0453 100644 --- a/src/screens/About/DebugScreen.js +++ b/src/screens/About/DebugScreen.js @@ -5,14 +5,24 @@ import {FlatList, View} from "react-native"; import AsyncStorageManager from "../../managers/AsyncStorageManager"; import CustomModal from "../../components/Overrides/CustomModal"; import {Button, List, Subheading, TextInput, Title, withTheme} from 'react-native-paper'; +import {StackNavigationProp} from "@react-navigation/stack"; +import {Modalize} from "react-native-modalize"; +import type {CustomTheme} from "../../managers/ThemeManager"; + +type PreferenceItem = { + key: string, + default: string, + current: string, +} type Props = { - navigation: Object, + navigation: StackNavigationProp, + theme: CustomTheme }; type State = { - modalCurrentDisplayItem: Object, - currentPreferences: Array, + modalCurrentDisplayItem: PreferenceItem, + currentPreferences: Array, } /** @@ -21,20 +31,20 @@ type State = { */ class DebugScreen extends React.Component { - modalRef: Object; - modalInputValue = ''; - - onModalRef: Function; - - colors: Object; + modalRef: Modalize; + modalInputValue: string; + /** + * Copies user preferences to state for easier manipulation + * + * @param props + */ constructor(props) { super(props); - this.onModalRef = this.onModalRef.bind(this); - this.colors = props.theme.colors; + this.modalInputValue = ""; let copy = {...AsyncStorageManager.getInstance().preferences}; - let currentPreferences = []; - Object.values(copy).map((object) => { + let currentPreferences : Array = []; + Object.values(copy).map((object: any) => { currentPreferences.push(object); }); this.state = { @@ -44,10 +54,11 @@ class DebugScreen extends React.Component { } /** - * Show the edit modal + * Shows the edit modal + * * @param item */ - showEditModal(item: Object) { + showEditModal(item: PreferenceItem) { this.setState({ modalCurrentDisplayItem: item }); @@ -81,14 +92,14 @@ class DebugScreen extends React.Component { @@ -98,6 +109,12 @@ class DebugScreen extends React.Component { ); } + /** + * Finds the index of the given key in the preferences array + * + * @param key THe key to find the index of + * @returns {number} + */ findIndexOfKey(key: string) { let index = -1; for (let i = 0; i < this.state.currentPreferences.length; i++) { @@ -130,11 +147,11 @@ class DebugScreen extends React.Component { * * @param ref */ - onModalRef(ref: Object) { + onModalRef = (ref: Modalize) => { this.modalRef = ref; } - renderItem = ({item}: Object) => { + renderItem = ({item}: {item: PreferenceItem}) => { return ( { - +class AmicaleContactScreen extends React.Component { + // Dataset containing information about contacts CONTACT_DATASET = [ { name: i18n.t("amicaleAbout.roles.interSchools"), @@ -68,18 +72,11 @@ class AmicaleContactScreen extends React.Component { }, ]; - colors: Object; + keyExtractor = (item: DatasetItem) => item.email; - constructor(props) { - super(props); - this.colors = props.theme.colors; - } + getChevronIcon = (props) => ; - keyExtractor = (item: Object) => item.email; - - getChevronIcon = (props: Object) => ; - - renderItem = ({item}: Object) => { + renderItem = ({item}: { item: DatasetItem }) => { const onPress = () => Linking.openURL('mailto:' + item.email); return { - - state = {}; - - colors: Object; - - constructor(props) { - super(props); - - this.colors = props.theme.colors; - } - - render() { - const nav = this.props.navigation; - return ( - - - - - - - - ); - } -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - flexDirection: 'column', - justifyContent: 'center', - }, - card: { - margin: 10, - }, - header: { - fontSize: 36, - marginBottom: 48 - }, - textInput: {}, - btnContainer: { - marginTop: 5, - marginBottom: 10, - } -}); - -export default withTheme(AmicaleHomeScreen); diff --git a/src/screens/Amicale/Clubs/ClubAboutScreen.js b/src/screens/Amicale/Clubs/ClubAboutScreen.js index b0f4aa5..fa44a4b 100644 --- a/src/screens/Amicale/Clubs/ClubAboutScreen.js +++ b/src/screens/Amicale/Clubs/ClubAboutScreen.js @@ -6,25 +6,11 @@ import {Card, List, Text, withTheme} from 'react-native-paper'; import i18n from 'i18n-js'; import Autolink from "react-native-autolink"; -type Props = { -}; - -type State = { -}; +type Props = {}; const CONTACT_LINK = 'clubs@amicale-insat.fr'; -/** - * Class defining a planning event information page. - */ -class ClubAboutScreen extends React.Component { - - colors: Object; - - constructor(props) { - super(props); - this.colors = props.theme.colors; - } +class ClubAboutScreen extends React.Component { render() { return ( diff --git a/src/screens/Amicale/Clubs/ClubDisplayScreen.js b/src/screens/Amicale/Clubs/ClubDisplayScreen.js index e1b8221..71787d3 100644 --- a/src/screens/Amicale/Clubs/ClubDisplayScreen.js +++ b/src/screens/Amicale/Clubs/ClubDisplayScreen.js @@ -65,6 +65,12 @@ class ClubDisplayScreen extends React.Component { } } + /** + * Gets the name of the category with the given ID + * + * @param id The category's ID + * @returns {string|*} + */ getCategoryName(id: number) { if (this.categories !== null) { for (let i = 0; i < this.categories.length; i++) { @@ -75,6 +81,12 @@ class ClubDisplayScreen extends React.Component { return ""; } + /** + * Gets the view for rendering categories + * + * @param categories The categories to display (max 2) + * @returns {null|*} + */ getCategoriesRender(categories: [number, number]) { if (this.categories === null) return null; @@ -95,12 +107,19 @@ class ClubDisplayScreen extends React.Component { return {final}; } - getManagersRender(resp: Array, email: string | null) { - let final = []; - for (let i = 0; i < resp.length; i++) { - final.push({resp[i]}) + /** + * Gets the view for rendering club managers if any + * + * @param managers The list of manager names + * @param email The club contact email + * @returns {*} + */ + getManagersRender(managers: Array, email: string | null) { + let managersListView = []; + for (let i = 0; i < managers.length; i++) { + managersListView.push({managers[i]}) } - const hasManagers = resp.length > 0; + const hasManagers = managers.length > 0; return ( { icon="account-tie"/>} /> - {final} + {managersListView} {this.getEmailButton(email, hasManagers)} ); } + /** + * Gets the email button to contact the club, or the amicale if the club does not have any managers + * + * @param email The club contact email + * @param hasManagers True if the club has managers + * @returns {*} + */ getEmailButton(email: string | null, hasManagers: boolean) { const destinationEmail = email != null && hasManagers ? email @@ -141,13 +167,21 @@ class ClubDisplayScreen extends React.Component { ); } - updateHeaderTitle(data: Object) { + /** + * Updates the header title to match the given club + * + * @param data The club data + */ + updateHeaderTitle(data: club) { this.props.navigation.setOptions({title: data.name}) } - getScreen = (response: Array) => { - let data: club = response[0]; - this.updateHeaderTitle(data); + getScreen = (response: Array<{ [key: string]: any } | null>) => { + let data: club | null = null; + if (response[0] != null) { + data = response[0]; + this.updateHeaderTitle(data); + } if (data != null) { return ( @@ -184,7 +218,6 @@ class ClubDisplayScreen extends React.Component { ); } else return null; - }; render() { diff --git a/src/screens/Amicale/Clubs/ClubListScreen.js b/src/screens/Amicale/Clubs/ClubListScreen.js index 0418197..9c7be04 100644 --- a/src/screens/Amicale/Clubs/ClubListScreen.js +++ b/src/screens/Amicale/Clubs/ClubListScreen.js @@ -131,6 +131,15 @@ class ClubListScreen extends React.Component { onChipSelect = (id: number) => this.updateFilteredData(null, id); + /** + * Updates the search string and category filter, saving them to the State. + * + * If the given category is already in the filter, it removes it. + * Otherwise it adds it to the filter. + * + * @param filterStr The new filter string to use + * @param categoryId The category to add/remove from the filter + */ updateFilteredData(filterStr: string | null, categoryId: number | null) { let newCategoriesState = [...this.state.currentlySelectedCategories]; let newStrState = this.state.currentSearchString; @@ -150,6 +159,11 @@ class ClubListScreen extends React.Component { }) } + /** + * Gets the list header, with controls to change the categories filter + * + * @returns {*} + */ getListHeader() { return { />; } + /** + * Gets the category object of the given ID + * + * @param id The ID of the category to find + * @returns {*} + */ getCategoryOfId = (id: number) => { for (let i = 0; i < this.categories.length; i++) { if (id === this.categories[i].id) @@ -165,6 +185,12 @@ class ClubListScreen extends React.Component { } }; + /** + * Checks if the given item should be rendered according to current name and category filters + * + * @param item The club to check + * @returns {boolean} + */ shouldRenderItem(item: club) { let shouldRender = this.state.currentlySelectedCategories.length === 0 || isItemInCategoryFilter(this.state.currentlySelectedCategories, item.category); diff --git a/src/screens/Amicale/LoginScreen.js b/src/screens/Amicale/LoginScreen.js index d6429d5..5bf4ee5 100644 --- a/src/screens/Amicale/LoginScreen.js +++ b/src/screens/Amicale/LoginScreen.js @@ -11,10 +11,11 @@ import {Collapsible} from "react-navigation-collapsible"; import CustomTabBar from "../../components/Tabbar/CustomTabBar"; import type {CustomTheme} from "../../managers/ThemeManager"; import AsyncStorageManager from "../../managers/AsyncStorageManager"; +import {StackNavigationProp} from "@react-navigation/stack"; type Props = { - navigation: Object, - route: Object, + navigation: StackNavigationProp, + route: { params: { nextScreen: string } }, collapsibleStack: Collapsible, theme: CustomTheme } @@ -47,9 +48,9 @@ class LoginScreen extends React.Component { dialogError: 0, }; - onEmailChange: Function; - onPasswordChange: Function; - passwordInputRef: Object; + onEmailChange: (value: string) => null; + onPasswordChange: (value: string) => null; + passwordInputRef: { current: null | TextInput }; nextScreen: string | null; @@ -64,7 +65,10 @@ class LoginScreen extends React.Component { this.handleNavigationParams(); }; - handleNavigationParams () { + /** + * Saves the screen to navigate to after a successful login if one was provided in navigation parameters + */ + handleNavigationParams() { if (this.props.route.params != null) { if (this.props.route.params.nextScreen != null) this.nextScreen = this.props.route.params.nextScreen; @@ -73,6 +77,11 @@ class LoginScreen extends React.Component { } } + /** + * Shows an error dialog with the corresponding login error + * + * @param error The error given by the login request + */ showErrorDialog = (error: number) => this.setState({ dialogVisible: true, @@ -81,6 +90,10 @@ class LoginScreen extends React.Component { hideErrorDialog = () => this.setState({dialogVisible: false}); + /** + * Navigates to the screen specified in navigation parameters or simply go back tha stack. + * Saves in user preferences to not show the login banner again. + */ handleSuccess = () => { // Do not show the login banner again AsyncStorageManager.getInstance().savePref( @@ -93,32 +106,75 @@ class LoginScreen extends React.Component { this.props.navigation.replace(this.nextScreen); }; + /** + * Navigates to the Amicale website screen with the reset password link as navigation parameters + */ onResetPasswordClick = () => this.props.navigation.navigate('amicale-website', {path: RESET_PASSWORD_PATH}); + /** + * The user has unfocused the input, his email is ready to be validated + */ validateEmail = () => this.setState({isEmailValidated: true}); + /** + * Checks if the entered email is valid (matches the regex) + * + * @returns {boolean} + */ isEmailValid() { return emailRegex.test(this.state.email); } + /** + * Checks if we should tell the user his email is invalid. + * We should only show this if his email is invalid and has been checked when un-focusing the input + * + * @returns {boolean|boolean} + */ shouldShowEmailError() { return this.state.isEmailValidated && !this.isEmailValid(); } + /** + * The user has unfocused the input, his password is ready to be validated + */ validatePassword = () => this.setState({isPasswordValidated: true}); + /** + * Checks if the user has entered a password + * + * @returns {boolean} + */ isPasswordValid() { return this.state.password !== ''; } + /** + * Checks if we should tell the user his password is invalid. + * We should only show this if his password is invalid and has been checked when un-focusing the input + * + * @returns {boolean|boolean} + */ shouldShowPasswordError() { return this.state.isPasswordValidated && !this.isPasswordValid(); } + /** + * If the email and password are valid, and we are not loading a request, then the login button can be enabled + * + * @returns {boolean} + */ shouldEnableLogin() { return this.isEmailValid() && this.isPasswordValid() && !this.state.loading; } + /** + * Called when the user input changes in the email or password field. + * This saves the new value in the State and disabled input validation (to prevent errors to show while typing) + * + * @param isEmail True if the field is the email field + * @param value The new field value + */ onInputChange(isEmail: boolean, value: string) { if (isEmail) { this.setState({ @@ -133,8 +189,23 @@ class LoginScreen extends React.Component { } } - onEmailSubmit = () => this.passwordInputRef.focus(); + /** + * Focuses the password field when the email field is done + * + * @returns {*} + */ + onEmailSubmit = () => { + if (this.passwordInputRef.current != null) + this.passwordInputRef.current.focus(); + } + /** + * Called when the user clicks on login or finishes to type his password. + * + * Checks if we should allow the user to login, + * then makes the login request and enters a loading state until the request finishes + * + */ onSubmit = () => { if (this.shouldEnableLogin()) { this.setState({loading: true}); @@ -147,6 +218,11 @@ class LoginScreen extends React.Component { } }; + /** + * Gets the form input + * + * @returns {*} + */ getFormInput() { return ( @@ -173,9 +249,7 @@ class LoginScreen extends React.Component { {i18n.t("loginScreen.emailError")} { - this.passwordInputRef = ref; - }} + ref={this.passwordInputRef} label={i18n.t("loginScreen.password")} mode='outlined' value={this.state.password} @@ -201,6 +275,10 @@ class LoginScreen extends React.Component { ); } + /** + * Gets the card containing the input form + * @returns {*} + */ getMainCard() { return ( @@ -239,6 +317,11 @@ class LoginScreen extends React.Component { ); } + /** + * Gets the card containing the information about the Amicale account + * + * @returns {*} + */ getSecondaryCard() { return ( diff --git a/src/screens/Amicale/ProfileScreen.js b/src/screens/Amicale/ProfileScreen.js index b6644c1..189da6b 100644 --- a/src/screens/Amicale/ProfileScreen.js +++ b/src/screens/Amicale/ProfileScreen.js @@ -12,10 +12,12 @@ import {Collapsible} from "react-navigation-collapsible"; import {withCollapsible} from "../../utils/withCollapsible"; import type {cardList} from "../../components/Lists/CardList/CardList"; import CardList from "../../components/Lists/CardList/CardList"; +import {StackNavigationProp} from "@react-navigation/stack"; +import type {CustomTheme} from "../../managers/ThemeManager"; type Props = { - navigation: Object, - theme: Object, + navigation: StackNavigationProp, + theme: CustomTheme, collapsibleStack: Collapsible, } @@ -23,6 +25,23 @@ type State = { dialogVisible: boolean, } +type ProfileData = { + first_name: string, + last_name: string, + email: string, + birthday: string, + phone: string, + branch: string, + link: string, + validity: boolean, + clubs: Array, +} +type Club = { + id: number, + name: string, + is_manager: boolean, +} + const CLUBS_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Clubs.png"; const VOTE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Vote.png"; @@ -34,9 +53,9 @@ class ProfileScreen extends React.Component { dialogVisible: false, }; - data: Object; + data: ProfileData; - flatListData: Array; + flatListData: Array<{ id: string }>; amicaleDataset: cardList; constructor() { @@ -79,12 +98,25 @@ class ProfileScreen extends React.Component { hideDisconnectDialog = () => this.setState({dialogVisible: false}); + /** + * Gets the logout header button + * + * @returns {*} + */ getHeaderButton = () => ; - getScreen = (data: Object) => { - this.data = data[0]; + /** + * Gets the main screen component with the fetched data + * + * @param data The data fetched from the server + * @returns {*} + */ + getScreen = (data: Array<{ [key: string]: any } | null>) => { + if (data[0] != null) { + this.data = data[0]; + } const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack; return ( @@ -109,7 +141,7 @@ class ProfileScreen extends React.Component { ) }; - getRenderItem = ({item}: Object) => { + getRenderItem = ({item}: { item: { id: string } }) => { switch (item.id) { case '0': return this.getWelcomeCard(); @@ -122,6 +154,11 @@ class ProfileScreen extends React.Component { } }; + /** + * Gets the list of services available with the Amicale account + * + * @returns {*} + */ getServicesList() { return ( { ); } + /** + * Gets a card welcoming the user to his account + * + * @returns {*} + */ getWelcomeCard() { return ( { * @param item The club to render * @return {*} */ - clubListItem = ({item}: Object) => { + clubListItem = ({item}: { item: Club }) => { const onPress = () => this.openClubDetailsScreen(item.id); let description = i18n.t("profileScreen.isMember"); let icon = (props) => ; @@ -356,9 +398,9 @@ class ProfileScreen extends React.Component { />; }; - clubKeyExtractor = (item: Object) => item.name; + clubKeyExtractor = (item: Club) => item.name; - sortClubList = (a: Object, b: Object) => a.is_manager ? -1 : 1; + sortClubList = (a: Club, b: Club) => a.is_manager ? -1 : 1; /** * Renders the list of clubs the user is part of @@ -366,7 +408,7 @@ class ProfileScreen extends React.Component { * @param list The club list * @return {*} */ - getClubList(list: Array) { + getClubList(list: Array) { list.sort(this.sortClubList); return ( //$FlowFixMe diff --git a/src/screens/Amicale/VoteScreen.js b/src/screens/Amicale/VoteScreen.js index 39defc8..44e08f4 100644 --- a/src/screens/Amicale/VoteScreen.js +++ b/src/screens/Amicale/VoteScreen.js @@ -9,6 +9,7 @@ import VoteTease from "../../components/Amicale/Vote/VoteTease"; import VoteSelect from "../../components/Amicale/Vote/VoteSelect"; import VoteResults from "../../components/Amicale/Vote/VoteResults"; import VoteWait from "../../components/Amicale/Vote/VoteWait"; +import {StackNavigationProp} from "@react-navigation/stack"; export type team = { id: number, @@ -86,13 +87,16 @@ type objectVoteDates = { const MIN_REFRESH_TIME = 5 * 1000; type Props = { - navigation: Object + navigation: StackNavigationProp } type State = { hasVoted: boolean, } +/** + * Screen displaying vote information and controls + */ export default class VoteScreen extends React.Component { state = { @@ -107,7 +111,7 @@ export default class VoteScreen extends React.Component { today: Date; mainFlatListData: Array<{ key: string }>; - lastRefresh: Date; + lastRefresh: Date | null; authRef: { current: null | AuthenticatedScreen }; @@ -116,22 +120,30 @@ export default class VoteScreen extends React.Component { this.hasVoted = false; this.today = new Date(); this.authRef = React.createRef(); + this.lastRefresh = null; this.mainFlatListData = [ {key: 'main'}, {key: 'info'}, ] } + /** + * Reloads vote data if last refresh delta is smaller than the minimum refresh time + */ reloadData = () => { let canRefresh; - if (this.lastRefresh !== undefined) - canRefresh = (new Date().getTime() - this.lastRefresh.getTime()) > MIN_REFRESH_TIME; + const lastRefresh = this.lastRefresh; + if (lastRefresh != null) + canRefresh = (new Date().getTime() - lastRefresh.getTime()) > MIN_REFRESH_TIME; else canRefresh = true; if (canRefresh && this.authRef.current != null) this.authRef.current.reload() }; + /** + * Generates the objects containing string and Date representations of key vote dates + */ generateDateObject() { const strings = this.datesString; if (strings != null) { @@ -152,6 +164,16 @@ export default class VoteScreen extends React.Component { this.dates = null; } + /** + * Gets the string representation of the given date. + * + * If the given date is the same day as today, only return the tile. + * Otherwise, return the full date. + * + * @param date The Date object representation of the wanted date + * @param dateString The string representation of the wanted date + * @returns {string} + */ getDateString(date: Date, dateString: string): string { if (this.today.getDate() === date.getDate()) { const str = getTimeOnlyString(dateString); @@ -176,7 +198,7 @@ export default class VoteScreen extends React.Component { return this.dates != null && this.today > this.dates.date_result_begin; } - mainRenderItem = ({item}: Object) => { + mainRenderItem = ({item}: { item: { key: string } }) => { if (item.key === 'info') return ; else if (item.key === 'main' && this.dates != null) @@ -190,8 +212,8 @@ export default class VoteScreen extends React.Component { // data[1] = FAKE_DATE; this.lastRefresh = new Date(); - const teams : teamResponse | null = data[0]; - const dateStrings : stringVoteDates | null = data[1]; + const teams: teamResponse | null = data[0]; + const dateStrings: stringVoteDates | null = data[1]; if (dateStrings != null && dateStrings.date_begin == null) this.datesString = null; @@ -282,6 +304,13 @@ export default class VoteScreen extends React.Component { isVoteRunning={this.isVoteRunning()}/>; } + /** + * Renders the authenticated screen. + * + * Teams and dates are not mandatory to allow showing the information box even if api requests fail + * + * @returns {*} + */ render() { return ( { - displayData: Object; + displayData: feedItem; date: string; - colors: Object; - constructor(props) { super(props); - this.colors = props.theme.colors; - this.displayData = this.props.route.params.data; - this.date = this.props.route.params.date; + this.displayData = props.route.params.data; + this.date = props.route.params.date; } componentDidMount() { @@ -38,16 +38,29 @@ class FeedItemScreen extends React.Component { }); } + /** + * Opens the feed item out link in browser or compatible app + */ onOutLinkPress = () => { Linking.openURL(this.displayData.permalink_url); }; + /** + * Gets the out link header button + * + * @returns {*} + */ getHeaderButton = () => { return ; }; + /** + * Gets the Amicale INSA avatar + * + * @returns {*} + */ getAvatar() { return ( { ); } - getContent() { - const hasImage = this.displayData.full_picture !== '' && this.displayData.full_picture !== undefined; + render() { + const hasImage = this.displayData.full_picture !== '' && this.displayData.full_picture != null; return ( { ); } - - render() { - return this.getContent(); - } } export default withTheme(FeedItemScreen); diff --git a/src/screens/Home/HomeScreen.js b/src/screens/Home/HomeScreen.js index a0492f0..1aae2dd 100644 --- a/src/screens/Home/HomeScreen.js +++ b/src/screens/Home/HomeScreen.js @@ -111,8 +111,6 @@ type State = { */ class HomeScreen extends React.Component { - colors: Object; - isLoggedIn: boolean | null; fabRef: { current: null | AnimatedFAB }; @@ -125,7 +123,6 @@ class HomeScreen extends React.Component { constructor(props) { super(props); - this.colors = props.theme.colors; this.fabRef = React.createRef(); this.currentNewFeed = []; this.isLoggedIn = null; @@ -155,6 +152,9 @@ class HomeScreen extends React.Component { }) } + /** + * Updates login state and navigation parameters on screen focus + */ onScreenFocus = () => { if (ConnectionManager.getInstance().isLoggedIn() !== this.isLoggedIn) { this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn(); @@ -169,6 +169,9 @@ class HomeScreen extends React.Component { this.handleNavigationParams(); }; + /** + * Navigates to the a new screen if navigation parameters specify one + */ handleNavigationParams = () => { if (this.props.route.params != null) { if (this.props.route.params.nextScreen != null) { @@ -179,6 +182,11 @@ class HomeScreen extends React.Component { } }; + /** + * Gets header buttons based on login state + * + * @returns {*} + */ getHeaderButton = () => { let onPressLog = () => this.props.navigation.navigate("login", {nextScreen: "profile"}); let logIcon = "login"; @@ -262,7 +270,7 @@ class HomeScreen extends React.Component { id: 'washers', data: dashboardData == null ? 0 : dashboardData.available_machines.washers, icon: 'washing-machine', - color: this.colors.proxiwashColor, + color: this.props.theme.colors.proxiwashColor, onPress: this.onProxiwashClick, isAvailable: dashboardData == null ? false : dashboardData.available_machines.washers > 0 }, @@ -270,7 +278,7 @@ class HomeScreen extends React.Component { id: 'dryers', data: dashboardData == null ? 0 : dashboardData.available_machines.dryers, icon: 'tumble-dryer', - color: this.colors.proxiwashColor, + color: this.props.theme.colors.proxiwashColor, onPress: this.onProxiwashClick, isAvailable: dashboardData == null ? false : dashboardData.available_machines.dryers > 0 }, @@ -278,7 +286,7 @@ class HomeScreen extends React.Component { id: 'available_tutorials', data: dashboardData == null ? 0 : dashboardData.available_tutorials, icon: 'school', - color: this.colors.tutorinsaColor, + color: this.props.theme.colors.tutorinsaColor, onPress: this.onTutorInsaClick, isAvailable: dashboardData == null ? false : dashboardData.available_tutorials > 0 }, @@ -286,7 +294,7 @@ class HomeScreen extends React.Component { id: 'proximo_articles', data: dashboardData == null ? 0 : dashboardData.proximo_articles, icon: 'shopping', - color: this.colors.proximoColor, + color: this.props.theme.colors.proximoColor, onPress: this.onProximoClick, isAvailable: dashboardData == null ? false : dashboardData.proximo_articles > 0 }, @@ -294,7 +302,7 @@ class HomeScreen extends React.Component { id: 'today_menu', data: dashboardData == null ? [] : dashboardData.today_menu, icon: 'silverware-fork-knife', - color: this.colors.menuColor, + color: this.props.theme.colors.menuColor, onPress: this.onMenuClick, isAvailable: dashboardData == null ? false : dashboardData.today_menu.length > 0 }, @@ -324,6 +332,11 @@ class HomeScreen extends React.Component { return this.getDashboardActions(); } + /** + * Gets a dashboard item with action buttons + * + * @returns {*} + */ getDashboardActions() { return ; } @@ -446,7 +459,7 @@ class HomeScreen extends React.Component { onEventContainerClick = () => this.props.navigation.navigate('planning'); /** - * Gets the event render item. + * Gets the event dashboard render item. * If a preview is available, it will be rendered inside * * @param content @@ -473,6 +486,12 @@ class HomeScreen extends React.Component { ); } + /** + * Gets a dashboard shortcut item + * + * @param item + * @returns {*} + */ dashboardRowRenderItem = ({item}: { item: dashboardSmallItem }) => { return ( { }; /** - * Gets a classic dashboard item. + * Gets a dashboard item with a row of shortcut buttons. * * @param content * @return {*} @@ -553,7 +572,7 @@ class HomeScreen extends React.Component { /** * Callback used when closing the banner. - * This hides the banner and saves to preferences to prevent it from reopening + * This hides the banner and saves to preferences to prevent it from reopening. */ onHideBanner = () => { this.setState({bannerVisible: false}); @@ -563,6 +582,10 @@ class HomeScreen extends React.Component { ); }; + /** + * Callback when pressing the login button on the banner. + * This hides the banner and takes the user to the login page. + */ onLoginBanner = () => { this.onHideBanner(); this.props.navigation.navigate("login", {nextScreen: "profile"}); diff --git a/src/screens/Home/ScannerScreen.js b/src/screens/Home/ScannerScreen.js index 7ee8fc5..9e91639 100644 --- a/src/screens/Home/ScannerScreen.js +++ b/src/screens/Home/ScannerScreen.js @@ -41,6 +41,9 @@ class ScannerScreen extends React.Component { this.requestPermissions(); } + /** + * Requests permission to use the camera + */ requestPermissions = () => { if (Platform.OS === 'android') request(PERMISSIONS.ANDROID.CAMERA).then(this.updatePermissionStatus) @@ -48,8 +51,19 @@ class ScannerScreen extends React.Component { 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(); @@ -59,6 +73,11 @@ class ScannerScreen extends React.Component { } }; + /** + * Gets a view asking user for permission to use the camera + * + * @returns {*} + */ getPermissionScreen() { return {i18n.t("scannerScreen.errorPermission")} @@ -77,6 +96,9 @@ class ScannerScreen extends React.Component { } + /** + * Shows a dialog indicating how to use the scanner + */ showHelpDialog = () => { this.setState({ dialogVisible: true, @@ -86,6 +108,9 @@ class ScannerScreen extends React.Component { }); }; + /** + * Shows a loading dialog + */ showOpeningDialog = () => { this.setState({ loading: true, @@ -93,6 +118,9 @@ class ScannerScreen extends React.Component { }); }; + /** + * Shows a dialog indicating the user the scanned code was invalid + */ showErrorDialog() { this.setState({ dialogVisible: true, @@ -102,11 +130,21 @@ class ScannerScreen extends React.Component { }); } + /** + * Hide any dialog + */ onDialogDismiss = () => this.setState({ dialogVisible: 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 ( , categories: Array) { +export function isItemInCategoryFilter(filter: Array, categories: [number, number]) { for (const category of categories) { if (filter.indexOf(category) !== -1) return true;