Compare commits
4 commits
53ec2bb578
...
de8820eada
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de8820eada | ||
|
|
dc944060e1 | ||
|
|
2c11addf40 | ||
|
|
8bacddc7b5 |
10 changed files with 442 additions and 325 deletions
4
App.tsx
4
App.tsx
|
|
@ -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
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": "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",
|
||||||
|
|
|
||||||
|
|
@ -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}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue