Improved doc

This commit is contained in:
Arnaud Vergnet 2020-06-28 12:39:13 +02:00
parent 56ab0e562f
commit 401c7d85ef
8 changed files with 228 additions and 42 deletions

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View file

@ -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();
}
});
}
}

View file

@ -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] = []
}

View file

@ -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<string>, categories: Array<string>) {
for (const category of categories) {
if (filter.indexOf(category) !== -1)

View file

@ -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);
}
};
}

View file

@ -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<R>}
*/
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

View file

@ -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<React.ClassAttributes<unknown>>}
*/
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 <Component
collapsibleStack={{
onScroll,