Application Android et IOS pour l'amicale des élèves https://play.google.com/store/apps/details?id=fr.amicaleinsat.application
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

WebViewScreen.tsx 7.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. /*
  2. * Copyright (c) 2019 - 2020 Arnaud Vergnet.
  3. *
  4. * This file is part of Campus INSAT.
  5. *
  6. * Campus INSAT is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * Campus INSAT is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. import React, {
  20. useCallback,
  21. useEffect,
  22. useLayoutEffect,
  23. useRef,
  24. useState,
  25. } from 'react';
  26. import WebView, { WebViewNavigation } from 'react-native-webview';
  27. import {
  28. Divider,
  29. HiddenItem,
  30. OverflowMenu,
  31. } from 'react-navigation-header-buttons';
  32. import i18n from 'i18n-js';
  33. import {
  34. Animated,
  35. BackHandler,
  36. Linking,
  37. NativeScrollEvent,
  38. NativeSyntheticEvent,
  39. StyleSheet,
  40. } from 'react-native';
  41. import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
  42. import { useTheme } from 'react-native-paper';
  43. import { useCollapsibleHeader } from 'react-navigation-collapsible';
  44. import MaterialHeaderButtons, { Item } from '../Overrides/CustomHeaderButton';
  45. import ErrorView from './ErrorView';
  46. import BasicLoadingScreen from './BasicLoadingScreen';
  47. import { useFocusEffect, useNavigation } from '@react-navigation/core';
  48. import { useCollapsible } from '../../context/CollapsibleContext';
  49. import { REQUEST_STATUS } from '../../utils/Requests';
  50. type Props = {
  51. url: string;
  52. onMessage?: (event: { nativeEvent: { data: string } }) => void;
  53. onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
  54. initialJS?: string;
  55. injectJS?: string;
  56. customPaddingFunction?: null | ((padding: number) => string);
  57. showAdvancedControls?: boolean;
  58. showControls?: boolean;
  59. };
  60. const AnimatedWebView = Animated.createAnimatedComponent(WebView);
  61. const styles = StyleSheet.create({
  62. overflow: {
  63. marginHorizontal: 10,
  64. },
  65. });
  66. /**
  67. * Class defining a webview screen.
  68. */
  69. function WebViewScreen(props: Props) {
  70. const [navState, setNavState] = useState<undefined | WebViewNavigation>({
  71. canGoBack: false,
  72. canGoForward: false,
  73. loading: true,
  74. url: props.url,
  75. lockIdentifier: 0,
  76. navigationType: 'click',
  77. title: '',
  78. });
  79. const navigation = useNavigation();
  80. const theme = useTheme();
  81. const webviewRef = useRef<WebView>();
  82. const { setCollapsible } = useCollapsible();
  83. const collapsible = useCollapsibleHeader({
  84. config: { collapsedColor: theme.colors.surface, useNativeDriver: false },
  85. });
  86. const { containerPaddingTop, onScrollWithListener } = collapsible;
  87. const [currentInjectedJS, setCurrentInjectedJS] = useState(props.injectJS);
  88. useFocusEffect(
  89. useCallback(() => {
  90. setCollapsible(collapsible);
  91. BackHandler.addEventListener(
  92. 'hardwareBackPress',
  93. onBackButtonPressAndroid
  94. );
  95. return () => {
  96. BackHandler.removeEventListener(
  97. 'hardwareBackPress',
  98. onBackButtonPressAndroid
  99. );
  100. };
  101. // eslint-disable-next-line react-hooks/exhaustive-deps
  102. }, [collapsible, setCollapsible])
  103. );
  104. useLayoutEffect(() => {
  105. if (props.showControls !== false) {
  106. navigation.setOptions({
  107. headerRight: props.showAdvancedControls
  108. ? getAdvancedButtons
  109. : getBasicButton,
  110. });
  111. }
  112. // eslint-disable-next-line react-hooks/exhaustive-deps
  113. }, [
  114. navigation,
  115. props.showAdvancedControls,
  116. navState?.url,
  117. props.showControls,
  118. ]);
  119. useEffect(() => {
  120. if (props.injectJS && props.injectJS !== currentInjectedJS) {
  121. injectJavaScript(props.injectJS);
  122. setCurrentInjectedJS(props.injectJS);
  123. }
  124. // eslint-disable-next-line react-hooks/exhaustive-deps
  125. }, [props.injectJS]);
  126. const onBackButtonPressAndroid = () => {
  127. if (navState?.canGoBack) {
  128. onGoBackClicked();
  129. return true;
  130. }
  131. return false;
  132. };
  133. const getBasicButton = () => {
  134. return (
  135. <MaterialHeaderButtons>
  136. <Item
  137. title={'refresh'}
  138. iconName={'refresh'}
  139. onPress={onRefreshClicked}
  140. />
  141. <Item
  142. title={i18n.t('general.openInBrowser')}
  143. iconName={'open-in-new'}
  144. onPress={onOpenClicked}
  145. />
  146. </MaterialHeaderButtons>
  147. );
  148. };
  149. const getAdvancedButtons = () => {
  150. return (
  151. <MaterialHeaderButtons>
  152. <Item title="refresh" iconName="refresh" onPress={onRefreshClicked} />
  153. <OverflowMenu
  154. style={styles.overflow}
  155. OverflowIcon={
  156. <MaterialCommunityIcons
  157. name="dots-vertical"
  158. size={26}
  159. color={theme.colors.text}
  160. />
  161. }
  162. >
  163. <HiddenItem
  164. title={i18n.t('general.goBack')}
  165. onPress={onGoBackClicked}
  166. />
  167. <HiddenItem
  168. title={i18n.t('general.goForward')}
  169. onPress={onGoForwardClicked}
  170. />
  171. <Divider />
  172. <HiddenItem
  173. title={i18n.t('general.openInBrowser')}
  174. onPress={onOpenClicked}
  175. />
  176. </OverflowMenu>
  177. </MaterialHeaderButtons>
  178. );
  179. };
  180. const getRenderLoading = () => <BasicLoadingScreen isAbsolute={true} />;
  181. /**
  182. * Gets the javascript needed to generate a padding on top of the page
  183. * This adds padding to the body and runs the custom padding function given in props
  184. *
  185. * @param padding The padding to add in pixels
  186. * @returns {string}
  187. */
  188. const getJavascriptPadding = (padding: number) => {
  189. const customPadding =
  190. props.customPaddingFunction != null
  191. ? props.customPaddingFunction(padding)
  192. : '';
  193. return `document.getElementsByTagName('body')[0].style.paddingTop = '${padding}px';${customPadding}true;`;
  194. };
  195. const onRefreshClicked = () => {
  196. //@ts-ignore
  197. if (webviewRef.current) {
  198. //@ts-ignore
  199. webviewRef.current.reload();
  200. }
  201. };
  202. const onGoBackClicked = () => {
  203. //@ts-ignore
  204. if (webviewRef.current) {
  205. //@ts-ignore
  206. webviewRef.current.goBack();
  207. }
  208. };
  209. const onGoForwardClicked = () => {
  210. //@ts-ignore
  211. if (webviewRef.current) {
  212. //@ts-ignore
  213. webviewRef.current.goForward();
  214. }
  215. };
  216. const onOpenClicked = () =>
  217. navState ? Linking.openURL(navState.url) : undefined;
  218. const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
  219. if (props.onScroll) {
  220. props.onScroll(event);
  221. }
  222. };
  223. const injectJavaScript = (script: string) => {
  224. //@ts-ignore
  225. if (webviewRef.current) {
  226. //@ts-ignore
  227. webviewRef.current.injectJavaScript(script);
  228. }
  229. };
  230. return (
  231. <AnimatedWebView
  232. ref={webviewRef}
  233. source={{ uri: props.url }}
  234. startInLoadingState={true}
  235. injectedJavaScript={props.initialJS}
  236. javaScriptEnabled={true}
  237. renderLoading={getRenderLoading}
  238. renderError={() => (
  239. <ErrorView
  240. status={REQUEST_STATUS.CONNECTION_ERROR}
  241. button={{
  242. icon: 'refresh',
  243. text: i18n.t('general.retry'),
  244. onPress: onRefreshClicked,
  245. }}
  246. />
  247. )}
  248. onNavigationStateChange={setNavState}
  249. onMessage={props.onMessage}
  250. onLoad={() => injectJavaScript(getJavascriptPadding(containerPaddingTop))}
  251. // Animations
  252. onScroll={onScrollWithListener(onScroll)}
  253. />
  254. );
  255. }
  256. export default WebViewScreen;