diff --git a/src/utils/AutoHideHandler.js b/src/utils/AutoHideHandler.js index dbb7740..feabf99 100644 --- a/src/utils/AutoHideHandler.js +++ b/src/utils/AutoHideHandler.js @@ -4,6 +4,9 @@ import * as React from 'react'; const speedOffset = 5; +/** + * Class used to detect when to show or hide a component based on scrolling + */ export default class AutoHideHandler { lastOffset: number; @@ -16,16 +19,42 @@ export default class AutoHideHandler { this.isHidden = startHidden; } + /** + * Adds a listener to the hide event + * + * @param listener + */ addListener(listener: Function) { this.listeners.push(listener); } + /** + * Notifies every listener whether they should hide or show. + * + * @param shouldHide + */ notifyListeners(shouldHide: boolean) { for (let i = 0; i < this.listeners.length; i++) { this.listeners[i](shouldHide); } } + /** + * Callback to be used on the onScroll animated component event. + * + * Detects if the current speed exceeds a threshold and notifies listeners to hide or show. + * + * The hide even is triggered when the user scrolls down, and the show event on scroll up. + * This does not take into account the speed when the y coordinate is negative, to prevent hiding on over scroll. + * (When scrolling up and hitting the top on ios for example) + * + * //TODO Known issue: + * When refreshing a list with the pull down gesture on ios, + * this can trigger the hide event as it scrolls down the list to show the refresh indicator. + * Android shows the refresh indicator on top of the list so this is not an issue. + * + * @param nativeEvent The scroll event generated by the animated component onScroll prop + */ onScroll({nativeEvent}: Object) { const speed = nativeEvent.contentOffset.y < 0 ? 0 : this.lastOffset - nativeEvent.contentOffset.y; if (speed < -speedOffset && !this.isHidden) { // Go down @@ -38,4 +67,4 @@ export default class AutoHideHandler { this.lastOffset = nativeEvent.contentOffset.y; } -} \ No newline at end of file +} diff --git a/src/utils/CollapsibleUtils.js b/src/utils/CollapsibleUtils.js index ec461ac..952a5a9 100644 --- a/src/utils/CollapsibleUtils.js +++ b/src/utils/CollapsibleUtils.js @@ -5,6 +5,21 @@ import {useTheme} from "react-native-paper"; import {createCollapsibleStack} from "react-navigation-collapsible"; import StackNavigator, {StackNavigationOptions} from "@react-navigation/stack"; +/** + * Creates a navigation stack with the collapsible library, allowing the header to collapse on scroll. + * + * Please use the getWebsiteStack function if your screen uses a webview as their main component as it needs special parameters. + * + * @param name The screen name in the navigation stack + * @param Stack The stack component + * @param component The screen component + * @param title The screen title shown in the header (needs to be translated) + * @param useNativeDriver Whether to use the native driver for animations. + * Set to false if the screen uses a webview as this component does not support native driver. + * In all other cases, set it to true for increase performance. + * @param options Screen options to use, or null if no options are necessary. + * @returns {JSX.Element} + */ export function createScreenCollapsibleStack( name: string, Stack: StackNavigator, @@ -33,6 +48,18 @@ export function createScreenCollapsibleStack( ) } +/** + * Creates a navigation stack with the collapsible library, allowing the header to collapse on scroll. + * + * This is a preset for screens using a webview as their main component, as it uses special parameters to work. + * (aka a dirty workaround) + * + * @param name + * @param Stack + * @param component + * @param title + * @returns {JSX.Element} + */ export function getWebsiteStack(name: string, Stack: any, component: any, title: string) { return createScreenCollapsibleStack(name, Stack, component, title, false); -} \ No newline at end of file +} diff --git a/src/utils/Notifications.js b/src/utils/Notifications.js index 56e19ea..4790f7c 100644 --- a/src/utils/Notifications.js +++ b/src/utils/Notifications.js @@ -6,10 +6,12 @@ import i18n from "i18n-js"; const PushNotification = require("react-native-push-notification"); -const reminderIdMultiplicator = 100; +// Used to multiply the normal notification id to create the reminder one. It allows to find it back easily +const reminderIdFactor = 100; /** - * Async function asking permission to send notifications to the user + * Async function asking permission to send notifications to the user. + * Used on ios. * * @returns {Promise} */ @@ -32,10 +34,18 @@ export async function askPermissions() { })); } +/** + * Creates a notification for the given machine id at the given date. + * + * This creates 2 notifications. One at the exact date, and one a few minutes before, according to user preference. + * + * @param machineID The machine id to schedule notifications for. This is used as id and in the notification string. + * @param date The date to trigger the notification at + */ function createNotifications(machineID: string, date: Date) { let reminder = parseInt(AsyncStorageManager.getInstance().preferences.proxiwashNotifications.current); - if (!isNaN(reminder)) { - let id = reminderIdMultiplicator * parseInt(machineID); + if (!isNaN(reminder) && reminder > 0) { + let id = reminderIdFactor * parseInt(machineID); let reminderDate = new Date(date); reminderDate.setMinutes(reminderDate.getMinutes() - reminder); PushNotification.localNotificationSchedule({ @@ -57,11 +67,14 @@ function createNotifications(machineID: string, date: Date) { } /** - * Asks the server to enable/disable notifications for the specified machine + * Enables or disables notifications for the given machine. * - * @param machineID The machine ID + * The function is async as we need to ask user permissions. + * If user denies, the promise will be rejected, otherwise it will succeed. + * + * @param machineID The machine ID to setup notifications for * @param isEnabled True to enable notifications, false to disable - * @param endDate + * @param endDate The trigger date, or null if disabling notifications */ export async function setupMachineNotification(machineID: string, isEnabled: boolean, endDate: Date | null) { return new Promise((resolve, reject) => { @@ -76,9 +89,9 @@ export async function setupMachineNotification(machineID: string, isEnabled: boo }); } else { PushNotification.cancelLocalNotifications({id: machineID}); - let reminderId = reminderIdMultiplicator * parseInt(machineID); + let reminderId = reminderIdFactor * parseInt(machineID); PushNotification.cancelLocalNotifications({id: reminderId.toString()}); resolve(); } }); -} \ No newline at end of file +} diff --git a/src/utils/Planning.js b/src/utils/Planning.js index 8a1b405..ee1fd19 100644 --- a/src/utils/Planning.js +++ b/src/utils/Planning.js @@ -22,7 +22,7 @@ const dateRegExp = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/; * @return {string} The string representation */ export function getCurrentDateString(): string { - return dateToString(new Date(Date.now())); + return dateToString(new Date(Date.now()), false); } /** @@ -117,6 +117,7 @@ export function stringToDate(dateString: string): Date | null { * YYYY-MM-DD HH-MM-SS * * @param date The date object to convert + * @param isUTC Whether to treat the date as UTC * @return {string} The converted string */ export function dateToString(date: Date, isUTC: boolean): string { @@ -198,7 +199,7 @@ export function generateEmptyCalendar(numberOfMonths: number): Object { let daysOfYear = {}; for (let d = new Date(Date.now()); d <= end; d.setDate(d.getDate() + 1)) { const dateString = getDateOnlyString( - dateToString(new Date(d))); + dateToString(new Date(d), false)); if (dateString !== null) daysOfYear[dateString] = [] } diff --git a/src/utils/Search.js b/src/utils/Search.js index 501cf58..9c6d437 100644 --- a/src/utils/Search.js +++ b/src/utils/Search.js @@ -1,8 +1,10 @@ - +// @flow /** - * Sanitizes the given string to improve search performance + * Sanitizes the given string to improve search performance. + * + * This removes the case, accents, spaces and underscores. * * @param str The string to sanitize * @return {string} The sanitized string @@ -15,10 +17,24 @@ export function sanitizeString(str: string): string { .replace(/_/g, ""); } +/** + * Checks if the given string matches the query. + * + * @param str The string to check + * @param query The query string used to find a match + * @returns {boolean} + */ export function stringMatchQuery(str: string, query: string) { return sanitizeString(str).includes(sanitizeString(query)); } +/** + * Checks if the given arrays have an item in common + * + * @param filter The filter array + * @param categories The item's categories array + * @returns {boolean} True if at least one entry is in both arrays + */ export function isItemInCategoryFilter(filter: Array, categories: Array) { for (const category of categories) { if (filter.indexOf(category) !== -1) diff --git a/src/utils/URLHandler.js b/src/utils/URLHandler.js index e9f0e89..c060967 100644 --- a/src/utils/URLHandler.js +++ b/src/utils/URLHandler.js @@ -2,9 +2,12 @@ import {Linking} from 'react-native'; +/** + * Class use to handle depp links scanned or clicked. + */ export default class URLHandler { - static SCHEME = "campus-insat://"; + static SCHEME = "campus-insat://"; // Urls beginning with this string will be opened in the app static CLUB_INFO_URL_PATH = "club"; static EVENT_INFO_URL_PATH = "event"; @@ -20,27 +23,12 @@ export default class URLHandler { this.onDetectURL = onDetectURL; } - listen() { - Linking.addEventListener('url', this.onUrl); - Linking.getInitialURL().then(this.onInitialUrl); - } - - onUrl = ({url}: { url: string }) => { - if (url != null) { - let data = URLHandler.getUrlData(URLHandler.parseUrl(url)); - if (data !== null) - this.onDetectURL(data); - } - }; - - onInitialUrl = (url: ?string) => { - if (url != null) { - let data = URLHandler.getUrlData(URLHandler.parseUrl(url)); - if (data !== null) - this.onInitialURLParsed(data); - } - }; - + /** + * Parses the given url to retrieve the corresponding app path and associated arguments. + * + * @param url The url to parse + * @returns {{path: string, queryParams: {}}} + */ static parseUrl(url: string) { let params = {}; let path = ""; @@ -63,7 +51,15 @@ export default class URLHandler { return {path: path, queryParams: params}; } - static getUrlData({path, queryParams}: Object) { + /** + * Gets routing data corresponding to the given url. + * If the url does not match any existing route, null will be returned. + * + * @param path Url path + * @param queryParams Url parameters + * @returns {null} + */ + static getUrlData({path, queryParams}: { path: string, queryParams: { [key: string]: string } }) { let data = null; if (path !== null) { if (URLHandler.isClubInformationLink(path)) @@ -74,18 +70,42 @@ export default class URLHandler { return data; } + /** + * Checks if the given url is in a valid format + * + * @param url The url to check + * @returns {boolean} + */ static isUrlValid(url: string) { return this.getUrlData(URLHandler.parseUrl(url)) !== null; } + /** + * Check if the given path links to the club information screen + * + * @param path The url to check + * @returns {boolean} + */ static isClubInformationLink(path: string) { return path === URLHandler.CLUB_INFO_URL_PATH; } + /** + * Check if the given path links to the planning information screen + * + * @param path The url to check + * @returns {boolean} + */ static isPlanningInformationLink(path: string) { return path === URLHandler.EVENT_INFO_URL_PATH; } + /** + * Generates data formatted for the club information screen from the url parameters. + * + * @param params Url parameters to convert + * @returns {null|{route: string, data: {clubId: number}}} + */ static generateClubInformationData(params: Object): Object | null { if (params !== undefined && params.id !== undefined) { let id = parseInt(params.id); @@ -96,6 +116,12 @@ export default class URLHandler { return null; } + /** + * Generates data formatted for the planning information screen from the url parameters. + * + * @param params Url parameters to convert + * @returns {null|{route: string, data: {clubId: number}}} + */ static generatePlanningInformationData(params: Object): Object | null { if (params !== undefined && params.id !== undefined) { let id = parseInt(params.id); @@ -106,4 +132,44 @@ export default class URLHandler { return null; } + /** + * Starts listening to events. + * + * There are 2 types of event. + * + * A classic event, triggered while the app is active. + * An initial event, called when the app was opened by clicking on a link + * + */ + listen() { + Linking.addEventListener('url', this.onUrl); + Linking.getInitialURL().then(this.onInitialUrl); + } + + /** + * Gets data from the given url and calls the classic callback with it. + * + * @param url The url detected + */ + onUrl = ({url}: { url: string }) => { + if (url != null) { + let data = URLHandler.getUrlData(URLHandler.parseUrl(url)); + if (data !== null) + this.onDetectURL(data); + } + }; + + /** + * Gets data from the given url and calls the initial callback with it. + * + * @param url The url detected + */ + onInitialUrl = (url: ?string) => { + if (url != null) { + let data = URLHandler.getUrlData(URLHandler.parseUrl(url)); + if (data !== null) + this.onInitialURLParsed(data); + } + }; + } diff --git a/src/utils/WebData.js b/src/utils/WebData.js index 3fecfe3..c28d231 100644 --- a/src/utils/WebData.js +++ b/src/utils/WebData.js @@ -21,7 +21,18 @@ type response_format = { const API_ENDPOINT = "https://www.amicale-insat.fr/api/"; -export async function apiRequest(path: string, method: string, params: ?Object) { +/** + * Sends a request to the Amicale Website backend + * + * In case of failure, the promise will be rejected with the error code. + * In case of success, the promise will return the data object. + * + * @param path The API path from the API endpoint + * @param method The HTTP method to use (GET or POST) + * @param params The params to use for this request + * @returns {Promise} + */ +export async function apiRequest(path: string, method: string, params: ?{ [key: string]: string }) { if (params === undefined || params === null) params = {}; @@ -51,6 +62,14 @@ export async function apiRequest(path: string, method: string, params: ?Object) }); } +/** + * Checks if the given API response is valid. + * + * For a request to be valid, it must match the response_format as defined in this file. + * + * @param response + * @returns {boolean} + */ export function isResponseValid(response: response_format) { let valid = response !== undefined && response.error !== undefined @@ -63,7 +82,11 @@ export function isResponseValid(response: response_format) { } /** - * Read data from FETCH_URL and return it. + * Reads data from the given url and returns it. + * + * Only use this function for non API calls. + * For Amicale API calls, please use the apiRequest function. + * * If no data was found, returns an empty object * * @param url The urls to fetch data from diff --git a/src/utils/withCollapsible.js b/src/utils/withCollapsible.js index c773c2a..69ec754 100644 --- a/src/utils/withCollapsible.js +++ b/src/utils/withCollapsible.js @@ -1,7 +1,19 @@ import React from 'react'; -import {StatusBar} from 'react-native'; import {useCollapsibleStack} from "react-navigation-collapsible"; +/** + * Function used to manipulate Collapsible Hooks from a class. + * + * Usage : + * + * export withCollapsible(Component) + * + * replacing Component with the one you want to use. + * This component will then receive the collapsibleStack prop. + * + * @param Component The component to use Collapsible with + * @returns {React.ComponentType>} + */ export const withCollapsible = (Component: any) => { return React.forwardRef((props: any, ref: any) => { @@ -14,7 +26,6 @@ export const withCollapsible = (Component: any) => { progress, opacity, } = useCollapsibleStack(); - const statusbarHeight = StatusBar.currentHeight != null ? StatusBar.currentHeight : 0; return