Compare commits

...

4 commits

Author SHA1 Message Date
Arnaud Vergnet
de8820eada feat: save login token 2021-09-12 23:39:23 +02:00
Arnaud Vergnet
dc944060e1 feat: update html render 2021-09-12 23:31:17 +02:00
Arnaud Vergnet
2c11addf40 fix: make search fields take whole header 2021-09-12 22:38:52 +02:00
Arnaud Vergnet
8bacddc7b5 chore: remove comment 2021-09-12 22:27:40 +02:00
10 changed files with 442 additions and 325 deletions

View file

@ -52,10 +52,6 @@ import { retrieveLoginToken } from './src/utils/loginToken';
import { setupNotifications } from './src/utils/Notifications'; import { setupNotifications } from './src/utils/Notifications';
import { TabRoutes } from './src/navigation/TabNavigator'; import { TabRoutes } from './src/navigation/TabNavigator';
// Native optimizations https://reactnavigation.org/docs/react-native-screens
// Crashes app when navigating away from webview on android 9+
// enableScreens(true);
initLocales(); initLocales();
setupNotifications(); setupNotifications();

502
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -48,7 +48,7 @@
"react-native-permissions": "3.0.5", "react-native-permissions": "3.0.5",
"react-native-push-notification": "8.1.0", "react-native-push-notification": "8.1.0",
"react-native-reanimated": "1.13.2", "react-native-reanimated": "1.13.2",
"react-native-render-html": "5.1.1", "react-native-render-html": "6.1.0",
"react-native-safe-area-context": "3.3.2", "react-native-safe-area-context": "3.3.2",
"react-native-screens": "3.7.0", "react-native-screens": "3.7.0",
"react-native-splash-screen": "3.2.0", "react-native-splash-screen": "3.2.0",

View file

@ -18,9 +18,13 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import { Text } from 'react-native-paper'; import { Text, useTheme } from 'react-native-paper';
import HTML from 'react-native-render-html'; import HTML, {
import { GestureResponderEvent, Linking } from 'react-native'; CustomRendererProps,
TBlock,
TText,
} from 'react-native-render-html';
import { Dimensions, GestureResponderEvent, Linking } from 'react-native';
type PropsType = { type PropsType = {
html: string; html: string;
@ -30,37 +34,54 @@ type PropsType = {
* Abstraction layer for Agenda component, using custom configuration * Abstraction layer for Agenda component, using custom configuration
*/ */
function CustomHTML(props: PropsType) { function CustomHTML(props: PropsType) {
const theme = useTheme();
const openWebLink = (_event: GestureResponderEvent, link: string) => { const openWebLink = (_event: GestureResponderEvent, link: string) => {
Linking.openURL(link); Linking.openURL(link);
}; };
const getBasicText = ( // Why is this so complex?? I just want to replace the default Text element with the one
_htmlAttribs: any, // from react-native-paper
children: any, // Might need to read the doc a bit more: https://meliorence.github.io/react-native-render-html/
_convertedCSSStyles: any, // For now this seems to work
passProps: any const getBasicText = (rendererProps: CustomRendererProps<TBlock>) => {
) => { let text: TText | undefined;
return <Text {...passProps}>{children}</Text>; if (rendererProps.tnode.children.length > 0) {
const phrasing = rendererProps.tnode.children[0];
if (phrasing.children.length > 0) {
text = phrasing.children[0] as TText;
}
}
if (text) {
return <Text>{text.data}</Text>;
} else {
return null;
}
}; };
const getListBullet = () => {
return <Text>- </Text>;
};
// Surround description with p to allow text styling if the description is not html
return ( return (
<HTML <HTML
html={`<p>${props.html}</p>`} // Surround description with p to allow text styling if the description is not html
source={{ html: `<p>${props.html}</p>` }}
// Use Paper Text instead of React
renderers={{ renderers={{
p: getBasicText, p: getBasicText,
li: getBasicText, li: getBasicText,
}} }}
listsPrefixesRenderers={{ // Sometimes we have images inside the text, just ignore them
ul: getListBullet, ignoredDomTags={['img']}
// Ignore text color
ignoredStyles={['color', 'backgroundColor']}
contentWidth={Dimensions.get('window').width - 50}
renderersProps={{
a: {
onPress: openWebLink,
},
ul: {
markerTextStyle: {
color: theme.colors.text,
},
},
}} }}
ignoredTags={['img']}
ignoredStyles={['color', 'background-color']}
onLinkPress={openWebLink}
/> />
); );
} }

View file

@ -93,7 +93,7 @@ function ClubListScreen() {
headerTitleContainerStyle: headerTitleContainerStyle:
Platform.OS === 'ios' Platform.OS === 'ios'
? { marginHorizontal: 0, width: '70%' } ? { marginHorizontal: 0, width: '70%' }
: { marginHorizontal: 0, right: 50, left: 50 }, : { width: '100%' },
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigation]); }, [navigation]);

View file

@ -39,6 +39,7 @@ import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { TabRoutes } from '../../navigation/TabNavigator'; import { TabRoutes } from '../../navigation/TabNavigator';
import { useShouldShowMascot } from '../../context/preferencesContext'; import { useShouldShowMascot } from '../../context/preferencesContext';
import { useLogin } from '../../context/loginContext'; import { useLogin } from '../../context/loginContext';
import { saveLoginToken } from '../../utils/loginToken';
type Props = StackScreenProps<MainStackParamsList, MainRoutes.Login>; type Props = StackScreenProps<MainStackParamsList, MainRoutes.Login>;
@ -100,6 +101,7 @@ function LoginScreen(props: Props) {
if (homeMascot.shouldShow) { if (homeMascot.shouldShow) {
homeMascot.setShouldShow(false); homeMascot.setShouldShow(false);
} }
saveLoginToken(token);
setLogin(token); setLogin(token);
if (!nextScreen) { if (!nextScreen) {
navigation.goBack(); navigation.goBack();

View file

@ -81,6 +81,15 @@ function GroupSelectionScreen() {
const favoriteGroups = getFavoriteGroups(); const favoriteGroups = getFavoriteGroups();
useLayoutEffect(() => { useLayoutEffect(() => {
const getSearchBar = () => {
return (
// @ts-ignore
<Searchbar
placeholder={i18n.t('screens.proximo.search')}
onChangeText={setCurrentSearchString}
/>
);
};
navigation.setOptions({ navigation.setOptions({
headerTitle: getSearchBar, headerTitle: getSearchBar,
headerBackTitleVisible: false, headerBackTitleVisible: false,
@ -91,16 +100,6 @@ function GroupSelectionScreen() {
}); });
}, [navigation]); }, [navigation]);
const getSearchBar = () => {
return (
// @ts-ignore
<Searchbar
placeholder={i18n.t('screens.proximo.search')}
onChangeText={setCurrentSearchString}
/>
);
};
/** /**
* Gets a render item for the given article * Gets a render item for the given article
* *

View file

@ -120,6 +120,7 @@ function ProximoListScreen(props: Props) {
const theme = useTheme(); const theme = useTheme();
const { articles, setArticles } = useCachedProximoArticles(); const { articles, setArticles } = useCachedProximoArticles();
const modalRef = useRef<Modalize>(null); const modalRef = useRef<Modalize>(null);
const navParams = props.route.params;
const [currentSearchString, setCurrentSearchString] = useState(''); const [currentSearchString, setCurrentSearchString] = useState('');
const [currentSortMode, setCurrentSortMode] = useState(2); const [currentSortMode, setCurrentSortMode] = useState(2);
@ -130,6 +131,70 @@ function ProximoListScreen(props: Props) {
const sortModes = [sortPrice, sortPriceReverse, sortName, sortNameReverse]; const sortModes = [sortPrice, sortPriceReverse, sortName, sortNameReverse];
useLayoutEffect(() => { useLayoutEffect(() => {
const getSearchBar = () => {
return (
// @ts-ignore
<Searchbar
placeholder={i18n.t('screens.proximo.search')}
onChangeText={setCurrentSearchString}
autoFocus={navParams.shouldFocusSearchBar}
/>
);
};
const getModalSortMenu = () => {
return (
<View style={styles.modalContainer}>
<Title style={styles.sortTitle}>
{i18n.t('screens.proximo.sortOrder')}
</Title>
<RadioButton.Group
onValueChange={setSortMode}
value={currentSortMode.toString()}
>
<RadioButton.Item
label={i18n.t('screens.proximo.sortPrice')}
value={'0'}
/>
<RadioButton.Item
label={i18n.t('screens.proximo.sortPriceReverse')}
value={'1'}
/>
<RadioButton.Item
label={i18n.t('screens.proximo.sortName')}
value={'2'}
/>
<RadioButton.Item
label={i18n.t('screens.proximo.sortNameReverse')}
value={'3'}
/>
</RadioButton.Group>
</View>
);
};
const setSortMode = (mode: string) => {
const currentMode = parseInt(mode, 10);
setCurrentSortMode(currentMode);
if (modalRef.current && currentMode !== currentSortMode) {
modalRef.current.close();
}
};
const getSortMenuButton = () => {
return (
<MaterialHeaderButtons>
<Item
title="main"
iconName="sort"
onPress={() => {
setModalCurrentDisplayItem(getModalSortMenu());
if (modalRef.current) {
modalRef.current.open();
}
}}
/>
</MaterialHeaderButtons>
);
};
navigation.setOptions({ navigation.setOptions({
headerRight: getSortMenuButton, headerRight: getSortMenuButton,
headerTitle: getSearchBar, headerTitle: getSearchBar,
@ -137,21 +202,9 @@ function ProximoListScreen(props: Props) {
headerTitleContainerStyle: headerTitleContainerStyle:
Platform.OS === 'ios' Platform.OS === 'ios'
? { marginHorizontal: 0, width: '70%' } ? { marginHorizontal: 0, width: '70%' }
: { marginHorizontal: 0, right: 50, left: 50 }, : { width: '100%' },
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps }, [navigation, currentSortMode, navParams.shouldFocusSearchBar]);
}, [navigation, currentSortMode]);
/**
* Callback used when clicking on the sort menu button.
* It will open the modal to show a sort selection
*/
const onSortMenuPress = () => {
setModalCurrentDisplayItem(getModalSortMenu());
if (modalRef.current) {
modalRef.current.open();
}
};
/** /**
* Callback used when clicking an article in the list. * Callback used when clicking an article in the list.
@ -166,19 +219,6 @@ function ProximoListScreen(props: Props) {
} }
}; };
/**
* Sets the current sort mode.
*
* @param mode The number representing the mode
*/
const setSortMode = (mode: string) => {
const currentMode = parseInt(mode, 10);
setCurrentSortMode(currentMode);
if (modalRef.current && currentMode !== currentSortMode) {
modalRef.current.close();
}
};
/** /**
* Gets a color depending on the quantity available * Gets a color depending on the quantity available
* *
@ -197,35 +237,6 @@ function ProximoListScreen(props: Props) {
return color; return color;
}; };
/**
* Gets the sort menu header button
*
* @return {*}
*/
const getSortMenuButton = () => {
return (
<MaterialHeaderButtons>
<Item title="main" iconName="sort" onPress={onSortMenuPress} />
</MaterialHeaderButtons>
);
};
/**
* Gets the header search bar
*
* @return {*}
*/
const getSearchBar = () => {
return (
// @ts-ignore
<Searchbar
placeholder={i18n.t('screens.proximo.search')}
onChangeText={setCurrentSearchString}
autoFocus={props.route.params.shouldFocusSearchBar}
/>
);
};
/** /**
* Gets the modal content depending on the given article * Gets the modal content depending on the given article
* *
@ -262,42 +273,6 @@ function ProximoListScreen(props: Props) {
); );
}; };
/**
* Gets the modal content to display a sort menu
*
* @return {*}
*/
const getModalSortMenu = () => {
return (
<View style={styles.modalContainer}>
<Title style={styles.sortTitle}>
{i18n.t('screens.proximo.sortOrder')}
</Title>
<RadioButton.Group
onValueChange={setSortMode}
value={currentSortMode.toString()}
>
<RadioButton.Item
label={i18n.t('screens.proximo.sortPrice')}
value={'0'}
/>
<RadioButton.Item
label={i18n.t('screens.proximo.sortPriceReverse')}
value={'1'}
/>
<RadioButton.Item
label={i18n.t('screens.proximo.sortName')}
value={'2'}
/>
<RadioButton.Item
label={i18n.t('screens.proximo.sortNameReverse')}
value={'3'}
/>
</RadioButton.Group>
</View>
);
};
/** /**
* Gets a render item for the given article * Gets a render item for the given article
* *
@ -341,8 +316,8 @@ function ProximoListScreen(props: Props) {
data: data data: data
.filter( .filter(
(d) => (d) =>
props.route.params.category === -1 || navParams.category === -1 ||
props.route.params.category === d.category_id navParams.category === d.category_id
) )
.sort(sortModes[currentSortMode]), .sort(sortModes[currentSortMode]),
keyExtractor: keyExtractor, keyExtractor: keyExtractor,

View file

@ -21,16 +21,12 @@ export async function retrieveLoginToken(): Promise<string | undefined> {
/** /**
* Saves the login token in the secure keychain * Saves the login token in the secure keychain
* *
* @param email
* @param token * @param token
* @returns Promise<void> * @returns Promise<void>
*/ */
export async function saveLoginToken( export async function saveLoginToken(token: string): Promise<void> {
email: string,
token: string
): Promise<void> {
return new Promise((resolve: () => void, reject: () => void) => { return new Promise((resolve: () => void, reject: () => void) => {
Keychain.setGenericPassword(email, token).then(resolve).catch(reject); Keychain.setGenericPassword('amicale', token).then(resolve).catch(reject);
}); });
} }

View file

@ -1,10 +1,12 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useLogin } from '../context/loginContext'; import { useLogin } from '../context/loginContext';
import { deleteLoginToken } from './loginToken';
export const useLogout = () => { export const useLogout = () => {
const { setLogin } = useLogin(); const { setLogin } = useLogin();
const onLogout = useCallback(() => { const onLogout = useCallback(() => {
deleteLoginToken();
setLogin(undefined); setLogin(undefined);
}, [setLogin]); }, [setLogin]);
return onLogout; return onLogout;