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 { 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();
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-push-notification": "8.1.0",
"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-screens": "3.7.0",
"react-native-splash-screen": "3.2.0",

View file

@ -18,9 +18,13 @@
*/
import * as React from 'react';
import { Text } from 'react-native-paper';
import HTML from 'react-native-render-html';
import { GestureResponderEvent, Linking } from 'react-native';
import { Text, useTheme } from 'react-native-paper';
import HTML, {
CustomRendererProps,
TBlock,
TText,
} from 'react-native-render-html';
import { Dimensions, GestureResponderEvent, Linking } from 'react-native';
type PropsType = {
html: string;
@ -30,37 +34,54 @@ type PropsType = {
* Abstraction layer for Agenda component, using custom configuration
*/
function CustomHTML(props: PropsType) {
const theme = useTheme();
const openWebLink = (_event: GestureResponderEvent, link: string) => {
Linking.openURL(link);
};
const getBasicText = (
_htmlAttribs: any,
children: any,
_convertedCSSStyles: any,
passProps: any
) => {
return <Text {...passProps}>{children}</Text>;
// Why is this so complex?? I just want to replace the default Text element with the one
// from react-native-paper
// Might need to read the doc a bit more: https://meliorence.github.io/react-native-render-html/
// For now this seems to work
const getBasicText = (rendererProps: CustomRendererProps<TBlock>) => {
let text: TText | undefined;
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 (
<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={{
p: getBasicText,
li: getBasicText,
}}
listsPrefixesRenderers={{
ul: getListBullet,
// Sometimes we have images inside the text, just ignore them
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:
Platform.OS === 'ios'
? { marginHorizontal: 0, width: '70%' }
: { marginHorizontal: 0, right: 50, left: 50 },
: { width: '100%' },
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigation]);

View file

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

View file

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

View file

@ -120,6 +120,7 @@ function ProximoListScreen(props: Props) {
const theme = useTheme();
const { articles, setArticles } = useCachedProximoArticles();
const modalRef = useRef<Modalize>(null);
const navParams = props.route.params;
const [currentSearchString, setCurrentSearchString] = useState('');
const [currentSortMode, setCurrentSortMode] = useState(2);
@ -130,6 +131,70 @@ function ProximoListScreen(props: Props) {
const sortModes = [sortPrice, sortPriceReverse, sortName, sortNameReverse];
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({
headerRight: getSortMenuButton,
headerTitle: getSearchBar,
@ -137,21 +202,9 @@ function ProximoListScreen(props: Props) {
headerTitleContainerStyle:
Platform.OS === 'ios'
? { marginHorizontal: 0, width: '70%' }
: { marginHorizontal: 0, right: 50, left: 50 },
: { width: '100%' },
});
// 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();
}
};
}, [navigation, currentSortMode, navParams.shouldFocusSearchBar]);
/**
* 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
*
@ -197,35 +237,6 @@ function ProximoListScreen(props: Props) {
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
*
@ -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
*
@ -341,8 +316,8 @@ function ProximoListScreen(props: Props) {
data: data
.filter(
(d) =>
props.route.params.category === -1 ||
props.route.params.category === d.category_id
navParams.category === -1 ||
navParams.category === d.category_id
)
.sort(sortModes[currentSortMode]),
keyExtractor: keyExtractor,

View file

@ -21,16 +21,12 @@ export async function retrieveLoginToken(): Promise<string | undefined> {
/**
* Saves the login token in the secure keychain
*
* @param email
* @param token
* @returns Promise<void>
*/
export async function saveLoginToken(
email: string,
token: string
): Promise<void> {
export async function saveLoginToken(token: string): Promise<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 { useLogin } from '../context/loginContext';
import { deleteLoginToken } from './loginToken';
export const useLogout = () => {
const { setLogin } = useLogin();
const onLogout = useCallback(() => {
deleteLoginToken();
setLogin(undefined);
}, [setLogin]);
return onLogout;