Compare commits
No commits in common. "de8820eada84a357a825380156f2c0402a16bcae" and "53ec2bb578b3e0b7e2f14b482bdeb1db847d19d0" have entirely different histories.
de8820eada
...
53ec2bb578
10 changed files with 325 additions and 442 deletions
4
App.tsx
4
App.tsx
|
|
@ -52,6 +52,10 @@ 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
502
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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": "6.1.0",
|
"react-native-render-html": "5.1.1",
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Text, useTheme } from 'react-native-paper';
|
import { Text } from 'react-native-paper';
|
||||||
import HTML, {
|
import HTML from 'react-native-render-html';
|
||||||
CustomRendererProps,
|
import { GestureResponderEvent, Linking } from 'react-native';
|
||||||
TBlock,
|
|
||||||
TText,
|
|
||||||
} from 'react-native-render-html';
|
|
||||||
import { Dimensions, GestureResponderEvent, Linking } from 'react-native';
|
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
html: string;
|
html: string;
|
||||||
|
|
@ -34,54 +30,37 @@ 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Why is this so complex?? I just want to replace the default Text element with the one
|
const getBasicText = (
|
||||||
// from react-native-paper
|
_htmlAttribs: any,
|
||||||
// Might need to read the doc a bit more: https://meliorence.github.io/react-native-render-html/
|
children: any,
|
||||||
// For now this seems to work
|
_convertedCSSStyles: any,
|
||||||
const getBasicText = (rendererProps: CustomRendererProps<TBlock>) => {
|
passProps: any
|
||||||
let text: TText | undefined;
|
) => {
|
||||||
if (rendererProps.tnode.children.length > 0) {
|
return <Text {...passProps}>{children}</Text>;
|
||||||
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
|
||||||
// Surround description with p to allow text styling if the description is not html
|
html={`<p>${props.html}</p>`}
|
||||||
source={{ html: `<p>${props.html}</p>` }}
|
|
||||||
// Use Paper Text instead of React
|
|
||||||
renderers={{
|
renderers={{
|
||||||
p: getBasicText,
|
p: getBasicText,
|
||||||
li: getBasicText,
|
li: getBasicText,
|
||||||
}}
|
}}
|
||||||
// Sometimes we have images inside the text, just ignore them
|
listsPrefixesRenderers={{
|
||||||
ignoredDomTags={['img']}
|
ul: getListBullet,
|
||||||
// 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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ function ClubListScreen() {
|
||||||
headerTitleContainerStyle:
|
headerTitleContainerStyle:
|
||||||
Platform.OS === 'ios'
|
Platform.OS === 'ios'
|
||||||
? { marginHorizontal: 0, width: '70%' }
|
? { marginHorizontal: 0, width: '70%' }
|
||||||
: { width: '100%' },
|
: { marginHorizontal: 0, right: 50, left: 50 },
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [navigation]);
|
}, [navigation]);
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ 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>;
|
||||||
|
|
||||||
|
|
@ -101,7 +100,6 @@ 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();
|
||||||
|
|
|
||||||
|
|
@ -81,15 +81,6 @@ 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,
|
||||||
|
|
@ -100,6 +91,16 @@ 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
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,6 @@ 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);
|
||||||
|
|
@ -131,70 +130,6 @@ 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,
|
||||||
|
|
@ -202,9 +137,21 @@ function ProximoListScreen(props: Props) {
|
||||||
headerTitleContainerStyle:
|
headerTitleContainerStyle:
|
||||||
Platform.OS === 'ios'
|
Platform.OS === 'ios'
|
||||||
? { marginHorizontal: 0, width: '70%' }
|
? { marginHorizontal: 0, width: '70%' }
|
||||||
: { width: '100%' },
|
: { marginHorizontal: 0, right: 50, left: 50 },
|
||||||
});
|
});
|
||||||
}, [navigation, currentSortMode, navParams.shouldFocusSearchBar]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [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.
|
||||||
|
|
@ -219,6 +166,19 @@ 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
|
||||||
*
|
*
|
||||||
|
|
@ -237,6 +197,35 @@ 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
|
||||||
*
|
*
|
||||||
|
|
@ -273,6 +262,42 @@ 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
|
||||||
*
|
*
|
||||||
|
|
@ -316,8 +341,8 @@ function ProximoListScreen(props: Props) {
|
||||||
data: data
|
data: data
|
||||||
.filter(
|
.filter(
|
||||||
(d) =>
|
(d) =>
|
||||||
navParams.category === -1 ||
|
props.route.params.category === -1 ||
|
||||||
navParams.category === d.category_id
|
props.route.params.category === d.category_id
|
||||||
)
|
)
|
||||||
.sort(sortModes[currentSortMode]),
|
.sort(sortModes[currentSortMode]),
|
||||||
keyExtractor: keyExtractor,
|
keyExtractor: keyExtractor,
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,16 @@ 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(token: string): Promise<void> {
|
export async function saveLoginToken(
|
||||||
|
email: string,
|
||||||
|
token: string
|
||||||
|
): Promise<void> {
|
||||||
return new Promise((resolve: () => void, reject: () => void) => {
|
return new Promise((resolve: () => void, reject: () => void) => {
|
||||||
Keychain.setGenericPassword('amicale', token).then(resolve).catch(reject);
|
Keychain.setGenericPassword(email, token).then(resolve).catch(reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue