Phecda

feat: add RNCWebView

@@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
4 "parser": "@typescript-eslint/parser", 4 "parser": "@typescript-eslint/parser",
5 "plugins": ["@typescript-eslint"], 5 "plugins": ["@typescript-eslint"],
6 "rules": { 6 "rules": {
7 - "@typescript-eslint/no-unused-vars": "off" 7 + "@typescript-eslint/no-unused-vars": "off",
  8 + "no-console": "warn"
8 } 9 }
9 } 10 }
@@ -235,6 +235,8 @@ PODS: @@ -235,6 +235,8 @@ PODS:
235 - React-jsinspector (0.62.1) 235 - React-jsinspector (0.62.1)
236 - react-native-safe-area-context (0.7.3): 236 - react-native-safe-area-context (0.7.3):
237 - React 237 - React
  238 + - react-native-webview (9.1.1):
  239 + - React
238 - React-RCTActionSheet (0.62.1): 240 - React-RCTActionSheet (0.62.1):
239 - React-Core/RCTActionSheetHeaders (= 0.62.1) 241 - React-Core/RCTActionSheetHeaders (= 0.62.1)
240 - React-RCTAnimation (0.62.1): 242 - React-RCTAnimation (0.62.1):
@@ -294,6 +296,8 @@ PODS: @@ -294,6 +296,8 @@ PODS:
294 - React-cxxreact (= 0.62.1) 296 - React-cxxreact (= 0.62.1)
295 - React-jsi (= 0.62.1) 297 - React-jsi (= 0.62.1)
296 - ReactCommon/callinvoker (= 0.62.1) 298 - ReactCommon/callinvoker (= 0.62.1)
  299 + - ReactNativeART (1.2.0):
  300 + - React
297 - ReactNativeDarkMode (0.2.2): 301 - ReactNativeDarkMode (0.2.2):
298 - React 302 - React
299 - RNCMaskedView (0.1.7): 303 - RNCMaskedView (0.1.7):
@@ -335,6 +339,7 @@ DEPENDENCIES: @@ -335,6 +339,7 @@ DEPENDENCIES:
335 - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) 339 - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
336 - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) 340 - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
337 - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) 341 - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
  342 + - react-native-webview (from `../node_modules/react-native-webview`)
338 - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) 343 - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
339 - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) 344 - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
340 - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) 345 - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`)
@@ -346,6 +351,7 @@ DEPENDENCIES: @@ -346,6 +351,7 @@ DEPENDENCIES:
346 - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) 351 - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
347 - ReactCommon/callinvoker (from `../node_modules/react-native/ReactCommon`) 352 - ReactCommon/callinvoker (from `../node_modules/react-native/ReactCommon`)
348 - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) 353 - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
  354 + - "ReactNativeART (from `../node_modules/@react-native-community/art`)"
349 - ReactNativeDarkMode (from `../node_modules/react-native-dark-mode`) 355 - ReactNativeDarkMode (from `../node_modules/react-native-dark-mode`)
350 - "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)" 356 - "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)"
351 - RNDeviceInfo (from `../node_modules/react-native-device-info`) 357 - RNDeviceInfo (from `../node_modules/react-native-device-info`)
@@ -401,6 +407,8 @@ EXTERNAL SOURCES: @@ -401,6 +407,8 @@ EXTERNAL SOURCES:
401 :path: "../node_modules/react-native/ReactCommon/jsinspector" 407 :path: "../node_modules/react-native/ReactCommon/jsinspector"
402 react-native-safe-area-context: 408 react-native-safe-area-context:
403 :path: "../node_modules/react-native-safe-area-context" 409 :path: "../node_modules/react-native-safe-area-context"
  410 + react-native-webview:
  411 + :path: "../node_modules/react-native-webview"
404 React-RCTActionSheet: 412 React-RCTActionSheet:
405 :path: "../node_modules/react-native/Libraries/ActionSheetIOS" 413 :path: "../node_modules/react-native/Libraries/ActionSheetIOS"
406 React-RCTAnimation: 414 React-RCTAnimation:
@@ -421,6 +429,8 @@ EXTERNAL SOURCES: @@ -421,6 +429,8 @@ EXTERNAL SOURCES:
421 :path: "../node_modules/react-native/Libraries/Vibration" 429 :path: "../node_modules/react-native/Libraries/Vibration"
422 ReactCommon: 430 ReactCommon:
423 :path: "../node_modules/react-native/ReactCommon" 431 :path: "../node_modules/react-native/ReactCommon"
  432 + ReactNativeART:
  433 + :path: "../node_modules/@react-native-community/art"
424 ReactNativeDarkMode: 434 ReactNativeDarkMode:
425 :path: "../node_modules/react-native-dark-mode" 435 :path: "../node_modules/react-native-dark-mode"
426 RNCMaskedView: 436 RNCMaskedView:
@@ -465,6 +475,7 @@ SPEC CHECKSUMS: @@ -465,6 +475,7 @@ SPEC CHECKSUMS:
465 React-jsiexecutor: e9698dee4fd43ceb44832baf15d5745f455b0157 475 React-jsiexecutor: e9698dee4fd43ceb44832baf15d5745f455b0157
466 React-jsinspector: f74a62727e5604119abd4a1eda52c0a12144bcd5 476 React-jsinspector: f74a62727e5604119abd4a1eda52c0a12144bcd5
467 react-native-safe-area-context: e200d4433aba6b7e60b52da5f37af11f7a0b0392 477 react-native-safe-area-context: e200d4433aba6b7e60b52da5f37af11f7a0b0392
  478 + react-native-webview: 0633fd7861a9bd7a80bacaee7da763c3afc248fa
468 React-RCTActionSheet: af8f28dd82fec89b8fe29637b8c779829e016a88 479 React-RCTActionSheet: af8f28dd82fec89b8fe29637b8c779829e016a88
469 React-RCTAnimation: 0d21fff7c20fb8ee41de5f2ebb63221127febd96 480 React-RCTAnimation: 0d21fff7c20fb8ee41de5f2ebb63221127febd96
470 React-RCTBlob: 9496bd93130b22069bfbc5d35e98653dae7c35c6 481 React-RCTBlob: 9496bd93130b22069bfbc5d35e98653dae7c35c6
@@ -475,6 +486,7 @@ SPEC CHECKSUMS: @@ -475,6 +486,7 @@ SPEC CHECKSUMS:
475 React-RCTText: 239e040f401505001327a109f9188a4e6dad1bd2 486 React-RCTText: 239e040f401505001327a109f9188a4e6dad1bd2
476 React-RCTVibration: 072c3b427dd29e730c2ee5bfc509cf5054741a50 487 React-RCTVibration: 072c3b427dd29e730c2ee5bfc509cf5054741a50
477 ReactCommon: 3585806280c51d5c2c0d3aa5a99014c3badb629d 488 ReactCommon: 3585806280c51d5c2c0d3aa5a99014c3badb629d
  489 + ReactNativeART: 78edc68dd4a1e675338cd0cd113319cf3a65f2ab
478 ReactNativeDarkMode: 0178ffca3b10f6a7c9f49d6f9810232b328fa949 490 ReactNativeDarkMode: 0178ffca3b10f6a7c9f49d6f9810232b328fa949
479 RNCMaskedView: 76c40a1d41c3e2535df09246a2b5487f04de0814 491 RNCMaskedView: 76c40a1d41c3e2535df09246a2b5487f04de0814
480 RNDeviceInfo: 6a3d16fce033f6979c4a6a41e62244d183e8c765 492 RNDeviceInfo: 6a3d16fce033f6979c4a6a41e62244d183e8c765
@@ -11,7 +11,10 @@ @@ -11,7 +11,10 @@
11 "commit": "git-cz" 11 "commit": "git-cz"
12 }, 12 },
13 "dependencies": { 13 "dependencies": {
  14 + "@huse/boolean": "^1.0.2",
  15 + "@huse/immer": "^1.0.2",
14 "@huse/previous-value": "^1.0.1", 16 "@huse/previous-value": "^1.0.1",
  17 + "@react-native-community/art": "^1.2.0",
15 "@react-native-community/masked-view": "^0.1.7", 18 "@react-native-community/masked-view": "^0.1.7",
16 "@react-navigation/bottom-tabs": "^5.2.5", 19 "@react-navigation/bottom-tabs": "^5.2.5",
17 "@react-navigation/drawer": "^5.4.0", 20 "@react-navigation/drawer": "^5.4.0",
@@ -24,11 +27,14 @@ @@ -24,11 +27,14 @@
24 "react-native-device-info": "^5.5.4", 27 "react-native-device-info": "^5.5.4",
25 "react-native-elements": "^1.2.7", 28 "react-native-elements": "^1.2.7",
26 "react-native-gesture-handler": "^1.6.1", 29 "react-native-gesture-handler": "^1.6.1",
  30 + "react-native-progress": "^4.1.2",
27 "react-native-reanimated": "^1.7.1", 31 "react-native-reanimated": "^1.7.1",
28 "react-native-safe-area-context": "^0.7.3", 32 "react-native-safe-area-context": "^0.7.3",
29 "react-native-screens": "^2.4.0", 33 "react-native-screens": "^2.4.0",
30 "react-native-tab-view": "^2.13.0", 34 "react-native-tab-view": "^2.13.0",
31 - "react-native-vector-icons": "^6.6.0" 35 + "react-native-vector-icons": "^6.6.0",
  36 + "react-native-webview": "9.1.1",
  37 + "url-parse": "^1.4.7"
32 }, 38 },
33 "devDependencies": { 39 "devDependencies": {
34 "@babel/core": "^7.6.2", 40 "@babel/core": "^7.6.2",
@@ -39,6 +45,7 @@ @@ -39,6 +45,7 @@
39 "@types/jest": "^24.0.24", 45 "@types/jest": "^24.0.24",
40 "@types/react-native": "^0.62.0", 46 "@types/react-native": "^0.62.0",
41 "@types/react-test-renderer": "16.9.2", 47 "@types/react-test-renderer": "16.9.2",
  48 + "@types/url-parse": "^1.4.3",
42 "@typescript-eslint/eslint-plugin": "^2.27.0", 49 "@typescript-eslint/eslint-plugin": "^2.27.0",
43 "@typescript-eslint/parser": "^2.27.0", 50 "@typescript-eslint/parser": "^2.27.0",
44 "@welldone-software/why-did-you-render": "^4.0.7", 51 "@welldone-software/why-did-you-render": "^4.0.7",
@@ -16,6 +16,8 @@ import DesignList from './DesignList'; @@ -16,6 +16,8 @@ import DesignList from './DesignList';
16 import { useDarkMode } from 'react-native-dark-mode'; 16 import { useDarkMode } from 'react-native-dark-mode';
17 import { themeForNav } from '../design'; 17 import { themeForNav } from '../design';
18 import RNDeviceInfoList from './RNDeviceInfo'; 18 import RNDeviceInfoList from './RNDeviceInfo';
  19 +import WebviewScreen from './WebviewScreen';
  20 +import { Platform } from 'react-native';
19 21
20 const MainTab = createBottomTabNavigator<MainTabParamList>(); 22 const MainTab = createBottomTabNavigator<MainTabParamList>();
21 23
@@ -74,7 +76,7 @@ const Home = () => { @@ -74,7 +76,7 @@ const Home = () => {
74 76
75 const MainStack = createStackNavigator<MainStackParamList>(); 77 const MainStack = createStackNavigator<MainStackParamList>();
76 78
77 -export default () => { 79 +const Container = () => {
78 const inDarkMode = useDarkMode(); 80 const inDarkMode = useDarkMode();
79 return ( 81 return (
80 <NavigationContainer 82 <NavigationContainer
@@ -96,7 +98,17 @@ export default () => { @@ -96,7 +98,17 @@ export default () => {
96 name="RNDeviceInfoList" 98 name="RNDeviceInfoList"
97 component={RNDeviceInfoList} 99 component={RNDeviceInfoList}
98 /> 100 />
  101 + <MainStack.Screen
  102 + name="WebviewScreen"
  103 + component={WebviewScreen}
  104 + options={({ navigation, route }) => ({
  105 + // FIXME: https://github.com/react-native-community/react-native-webview/issues/575#issuecomment-587267906
  106 + animationEnabled: Platform.OS === 'ios',
  107 + })}
  108 + />
99 </MainStack.Navigator> 109 </MainStack.Navigator>
100 </NavigationContainer> 110 </NavigationContainer>
101 ); 111 );
102 }; 112 };
  113 +
  114 +export default Container;
@@ -48,6 +48,15 @@ const SystemInfo = ({ @@ -48,6 +48,15 @@ const SystemInfo = ({
48 onPress={() => navigation.navigate('RNDeviceInfoList')} 48 onPress={() => navigation.navigate('RNDeviceInfoList')}
49 chevron 49 chevron
50 /> 50 />
  51 + <Divider />
  52 + <ListItem
  53 + title={'RNCWebview'}
  54 + onPress={() =>
  55 + navigation.navigate('WebviewScreen', {
  56 + uri: 'https://www.baidu.com',
  57 + })
  58 + }
  59 + />
51 </Card> 60 </Card>
52 </BGScroll> 61 </BGScroll>
53 ); 62 );
  1 +import React, { useCallback } from 'react';
  2 +import RNCWebView, { WebViewNavigation } from 'react-native-webview';
  3 +import { WebviewState, WebviewActions, webActions } from './reducer';
  4 +import {
  5 + WebViewProgressEvent,
  6 + WebViewErrorEvent,
  7 + OnShouldStartLoadWithRequest,
  8 +} from 'react-native-webview/lib/WebViewTypes';
  9 +import ErrorView from './ErrorView';
  10 +import { Linking } from 'react-native';
  11 +
  12 +const Body = React.forwardRef<
  13 + RNCWebView,
  14 + {
  15 + state: WebviewState;
  16 + dispatch: React.Dispatch<WebviewActions>;
  17 + initialUrl: string;
  18 + }
  19 +>(({ state, dispatch, initialUrl }, ref) => {
  20 + const onNavigationStateChange = useCallback(
  21 + (s: WebViewNavigation) => {
  22 + dispatch(webActions.changeNavigationState(s));
  23 + },
  24 + [dispatch]
  25 + );
  26 +
  27 + const onLoadProgress = useCallback(
  28 + (s: WebViewProgressEvent) => {
  29 + dispatch(webActions.onLoadProgress(s.nativeEvent));
  30 + },
  31 + [dispatch]
  32 + );
  33 +
  34 + const onError = useCallback(
  35 + (s: WebViewErrorEvent) => {
  36 + dispatch(webActions.onLoadError(s.nativeEvent));
  37 + },
  38 + [dispatch]
  39 + );
  40 +
  41 + const shouldRequest: OnShouldStartLoadWithRequest = useCallback(
  42 + (request) => {
  43 + const { url } = request;
  44 + if (url.startsWith('http') || url === 'about:blank') {
  45 + return true;
  46 + } else {
  47 + dispatch(webActions.changeNavigationState(request));
  48 + Linking.canOpenURL(url)
  49 + .then((canOpen) => {
  50 + if (canOpen) {
  51 + return Linking.openURL(url);
  52 + }
  53 + })
  54 + .catch(() => {});
  55 + return false;
  56 + }
  57 + },
  58 + [dispatch]
  59 + );
  60 +
  61 + return (
  62 + <RNCWebView
  63 + ref={ref}
  64 + source={{ uri: initialUrl }}
  65 + onNavigationStateChange={onNavigationStateChange}
  66 + onLoadProgress={onLoadProgress}
  67 + onError={onError}
  68 + onShouldStartLoadWithRequest={shouldRequest}
  69 + renderError={(_, code, description) => (
  70 + <ErrorView code={code} description={description} />
  71 + )}
  72 + />
  73 + );
  74 +});
  75 +
  76 +export default Body;
  1 +import React from 'react';
  2 +import { SafeAreaView, Text, StyleSheet } from 'react-native';
  3 +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
  4 +import {
  5 + DynamicStyleSheet,
  6 + useDynamicStyleSheet,
  7 + useDynamicValue,
  8 +} from 'react-native-dark-mode';
  9 +import { colorPreset } from '../../design';
  10 +
  11 +const dynamicStyles = new DynamicStyleSheet({
  12 + background: {
  13 + backgroundColor: colorPreset.backgroundColor.secondary,
  14 + ...StyleSheet.absoluteFillObject,
  15 + alignItems: 'center',
  16 + justifyContent: 'center',
  17 + padding: 20,
  18 + },
  19 + errorCode: {
  20 + color: colorPreset.labelColor.primary,
  21 + },
  22 + description: {
  23 + color: colorPreset.labelColor.primary,
  24 + },
  25 +});
  26 +
  27 +export default function ErrorView({
  28 + code,
  29 + description,
  30 +}: {
  31 + code: number;
  32 + description: string;
  33 +}) {
  34 + const styles = useDynamicStyleSheet(dynamicStyles);
  35 + const redColor = useDynamicValue(colorPreset.rainbow.red);
  36 + return (
  37 + <SafeAreaView style={styles.background}>
  38 + <MaterialCommunityIcons
  39 + name={'close-circle'}
  40 + size={60}
  41 + color={redColor}
  42 + />
  43 + <Text style={styles.errorCode}>{code}</Text>
  44 + <Text style={styles.description}>{description}</Text>
  45 + </SafeAreaView>
  46 + );
  47 +}
  1 +import React, {
  2 + useState,
  3 + useCallback,
  4 + useEffect,
  5 + useLayoutEffect,
  6 +} from 'react';
  7 +import URL from 'url-parse';
  8 +import { useToggle } from '@huse/boolean';
  9 +import RNCWebview from 'react-native-webview';
  10 +import {
  11 + DynamicStyleSheet,
  12 + useDynamicStyleSheet,
  13 + useDynamicValue,
  14 +} from 'react-native-dark-mode';
  15 +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
  16 +import * as Progress from 'react-native-progress';
  17 +import {
  18 + Platform,
  19 + SafeAreaView,
  20 + View,
  21 + Text,
  22 + TouchableOpacity,
  23 + StyleSheet,
  24 + Animated,
  25 + TextInput,
  26 + NativeSyntheticEvent,
  27 + TextInputSubmitEditingEventData,
  28 + LayoutAnimation,
  29 +} from 'react-native';
  30 +import { colorPreset } from '../../design';
  31 +import { WebviewState, WebviewActions, webActions } from './reducer';
  32 +
  33 +const dynamicStyles = new DynamicStyleSheet({
  34 + headerContainer: {
  35 + backgroundColor: colorPreset.backgroundColor.primary,
  36 + borderBottomWidth: StyleSheet.hairlineWidth,
  37 + borderBottomColor: colorPreset.separator.opaque,
  38 + },
  39 + containerStyle: {
  40 + backgroundColor: colorPreset.backgroundColor.secondary,
  41 + borderRadius: 10,
  42 + margin: 10,
  43 + ...Platform.select({
  44 + ios: {
  45 + height: 36,
  46 + marginTop: 4,
  47 + },
  48 + android: {
  49 + height: 36,
  50 + },
  51 + }),
  52 + overflow: 'hidden',
  53 + },
  54 + labelContainer: {
  55 + flexDirection: 'row',
  56 + flex: 1,
  57 + alignItems: 'center',
  58 + },
  59 + hostLabel: {
  60 + color: colorPreset.labelColor.primary,
  61 + flex: 1,
  62 + textAlign: 'center',
  63 + fontSize: 17,
  64 + },
  65 + refreshButton: {
  66 + width: 36,
  67 + height: 36,
  68 + justifyContent: 'center',
  69 + alignItems: 'center',
  70 + },
  71 + inputContainerStyle: {
  72 + backgroundColor: colorPreset.backgroundColor.secondary,
  73 + },
  74 + inputStyle: {
  75 + flex: 1,
  76 + paddingHorizontal: 16,
  77 + },
  78 + progressBar: {
  79 + position: 'absolute',
  80 + left: 0,
  81 + right: 0,
  82 + bottom: 0,
  83 + height: 2,
  84 + },
  85 +});
  86 +
  87 +interface Props {
  88 + state: WebviewState;
  89 + dispatch: React.Dispatch<WebviewActions>;
  90 + webview: React.RefObject<RNCWebview>;
  91 +}
  92 +
  93 +// EXPERIMENT
  94 +function WebviewHeader({ state, dispatch, webview }: Props) {
  95 + const [uri, setUri] = useState<URL>();
  96 + const [focused, toggleFocused] = useToggle(false);
  97 + const progressBarOpacity = new Animated.Value(1);
  98 +
  99 + const { url, progress, loading } = state;
  100 + useEffect(() => {
  101 + const newUrl = new URL(url);
  102 + setUri(newUrl);
  103 + }, [url]);
  104 +
  105 + useEffect(() => {
  106 + if (progress === 1) {
  107 + Animated.timing(progressBarOpacity, {
  108 + toValue: 0,
  109 + useNativeDriver: true,
  110 + duration: 1000,
  111 + }).start();
  112 + }
  113 + }, [progress, progressBarOpacity]);
  114 +
  115 + const onSubmitEditing = useCallback(
  116 + (e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
  117 + toggleFocused();
  118 + dispatch(webActions.loadText(e.nativeEvent.text));
  119 + },
  120 + [toggleFocused, dispatch]
  121 + );
  122 +
  123 + const onPressLoading = useCallback(() => {
  124 + if (loading) {
  125 + webview.current?.stopLoading();
  126 + } else {
  127 + webview.current?.reload();
  128 + }
  129 + }, [loading, webview]);
  130 +
  131 + useLayoutEffect(() => {
  132 + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
  133 + }, [focused]);
  134 +
  135 + const styles = useDynamicStyleSheet(dynamicStyles);
  136 + const redColor = useDynamicValue(colorPreset.rainbow.red);
  137 + const greenColor = useDynamicValue(colorPreset.rainbow.green);
  138 + const primaryLabelColor = useDynamicValue(colorPreset.labelColor.primary);
  139 + return (
  140 + <SafeAreaView style={styles.headerContainer}>
  141 + <View style={styles.containerStyle}>
  142 + {focused ? (
  143 + <View style={styles.labelContainer}>
  144 + <TextInput
  145 + defaultValue={url}
  146 + onSubmitEditing={onSubmitEditing}
  147 + style={styles.inputStyle}
  148 + underlineColorAndroid="transparent"
  149 + textContentType="URL"
  150 + selectTextOnFocus
  151 + keyboardType="url"
  152 + returnKeyType="go"
  153 + autoCapitalize="none"
  154 + autoCorrect={false}
  155 + autoFocus
  156 + onBlur={(e) => {
  157 + toggleFocused();
  158 + }}
  159 + clearButtonMode="while-editing"
  160 + />
  161 + </View>
  162 + ) : (
  163 + <TouchableOpacity
  164 + activeOpacity={1}
  165 + onPress={toggleFocused}
  166 + style={styles.labelContainer}
  167 + >
  168 + <View style={styles.refreshButton}>
  169 + <MaterialCommunityIcons
  170 + name={
  171 + uri?.protocol === 'https:'
  172 + ? 'shield-outline'
  173 + : 'shield-off-outline'
  174 + }
  175 + color={uri?.protocol === 'https:' ? greenColor : redColor}
  176 + size={20}
  177 + />
  178 + </View>
  179 + <Text style={styles.hostLabel}>
  180 + {uri?.hostname.replace(/^www\./, '')}
  181 + </Text>
  182 + <TouchableOpacity
  183 + onPress={onPressLoading}
  184 + style={styles.refreshButton}
  185 + >
  186 + <MaterialCommunityIcons
  187 + name={loading ? 'close' : 'refresh'}
  188 + color={primaryLabelColor}
  189 + size={20}
  190 + />
  191 + </TouchableOpacity>
  192 + <Animated.View
  193 + style={[{ opacity: progressBarOpacity }, styles.progressBar]}
  194 + >
  195 + <Progress.Bar
  196 + progress={progress}
  197 + borderWidth={0}
  198 + borderRadius={0}
  199 + width={null}
  200 + useNativeDriver
  201 + />
  202 + </Animated.View>
  203 + </TouchableOpacity>
  204 + )}
  205 + </View>
  206 + </SafeAreaView>
  207 + );
  208 +}
  209 +
  210 +export default WebviewHeader;
  1 +import React, { useEffect, useMemo, useState, useRef } from 'react';
  2 +import { Animated, StyleSheet } from 'react-native';
  3 +import * as Progress from 'react-native-progress';
  4 +
  5 +const styles = StyleSheet.create({
  6 + progressBar: {
  7 + position: 'absolute',
  8 + left: 0,
  9 + right: 0,
  10 + top: 0,
  11 + height: 1,
  12 + },
  13 +});
  14 +
  15 +export default function ProgressBar({
  16 + progress,
  17 + loading,
  18 +}: {
  19 + progress: number;
  20 + loading: boolean;
  21 +}) {
  22 + const progressBarOpacity = useRef(new Animated.Value(1)).current;
  23 +
  24 + useEffect(() => {
  25 + if (!loading) {
  26 + Animated.timing(progressBarOpacity, {
  27 + toValue: 0,
  28 + useNativeDriver: true,
  29 + duration: 1000,
  30 + }).start();
  31 + } else {
  32 + progressBarOpacity.setValue(1);
  33 + }
  34 + }, [loading, progressBarOpacity]);
  35 +
  36 + return (
  37 + <Animated.View
  38 + style={[{ opacity: progressBarOpacity }, styles.progressBar]}
  39 + >
  40 + <Progress.Bar
  41 + progress={progress}
  42 + borderWidth={0}
  43 + borderRadius={0}
  44 + width={null}
  45 + height={4}
  46 + useNativeDriver
  47 + />
  48 + </Animated.View>
  49 + );
  50 +}
  1 +import React, { useCallback } from 'react';
  2 +import { SafeAreaView, TouchableOpacity, StyleSheet, View } from 'react-native';
  3 +import RNCWebview from 'react-native-webview';
  4 +import EvilIcons from 'react-native-vector-icons/EvilIcons';
  5 +import { WebviewState, WebviewActions } from './reducer';
  6 +import {
  7 + DynamicStyleSheet,
  8 + useDynamicStyleSheet,
  9 + useDynamicValue,
  10 +} from 'react-native-dark-mode';
  11 +import { colorPreset } from '../../design';
  12 +
  13 +interface Props {
  14 + state: WebviewState;
  15 + dispatch: React.Dispatch<WebviewActions>;
  16 + webview: React.RefObject<RNCWebview>;
  17 +}
  18 +
  19 +const dynamicStyles = new DynamicStyleSheet({
  20 + background: {
  21 + backgroundColor: colorPreset.backgroundColor.primary,
  22 + borderTopWidth: StyleSheet.hairlineWidth,
  23 + borderTopColor: colorPreset.separator.opaque,
  24 + },
  25 + container: {
  26 + height: 44,
  27 + flexDirection: 'row',
  28 + justifyContent: 'space-evenly',
  29 + },
  30 + button: {
  31 + width: 44,
  32 + height: 44,
  33 + justifyContent: 'center',
  34 + alignItems: 'center',
  35 + },
  36 +});
  37 +
  38 +const Toolbar = ({ state, webview }: Props) => {
  39 + const { canGoBack, canGoForward, loading } = state;
  40 + const styles = useDynamicStyleSheet(dynamicStyles);
  41 + const primaryLabelColor = useDynamicValue(colorPreset.labelColor.primary);
  42 + const secondaryLabelColor = useDynamicValue(colorPreset.labelColor.tertiary);
  43 +
  44 + const onPressLoading = useCallback(() => {
  45 + if (loading) {
  46 + webview.current?.stopLoading();
  47 + } else {
  48 + webview.current?.reload();
  49 + }
  50 + }, [loading, webview]);
  51 +
  52 + return (
  53 + <SafeAreaView style={styles.background}>
  54 + <View style={styles.container}>
  55 + <TouchableOpacity
  56 + style={styles.button}
  57 + disabled={!canGoBack}
  58 + onPress={() => webview.current?.goBack()}
  59 + >
  60 + <EvilIcons
  61 + name="chevron-left"
  62 + size={40}
  63 + color={canGoBack ? primaryLabelColor : secondaryLabelColor}
  64 + />
  65 + </TouchableOpacity>
  66 + <TouchableOpacity
  67 + style={styles.button}
  68 + disabled={!canGoForward}
  69 + onPress={() => webview.current?.goForward()}
  70 + >
  71 + <EvilIcons
  72 + name="chevron-right"
  73 + size={40}
  74 + color={canGoForward ? primaryLabelColor : secondaryLabelColor}
  75 + />
  76 + </TouchableOpacity>
  77 + <TouchableOpacity style={styles.button} onPress={onPressLoading}>
  78 + <EvilIcons
  79 + name={loading ? 'close' : 'refresh'}
  80 + size={40}
  81 + color={primaryLabelColor}
  82 + />
  83 + </TouchableOpacity>
  84 + </View>
  85 + </SafeAreaView>
  86 + );
  87 +};
  88 +
  89 +export default Toolbar;
  1 +import React, { useRef, useEffect, useReducer } from 'react';
  2 +import { useImmerReducer } from '@huse/immer';
  3 +import RNCWebview from 'react-native-webview';
  4 +import { reducer, defaultState, webActions, WebviewActions } from './reducer';
  5 +import { BGView } from '../../component/View';
  6 +import Header from './Header';
  7 +import Body from './Body';
  8 +import { MainStackScreenProps } from '../../type/Navigation';
  9 +import Toolbar from './Toolbar';
  10 +import ProgressBar from './ProgressBar';
  11 +
  12 +const WebviewScreen = ({
  13 + navigation,
  14 + route,
  15 +}: MainStackScreenProps<'WebviewScreen'>) => {
  16 + const paramUri = route.params?.uri ?? 'https://about:blank';
  17 + const [state, dispatch] = useImmerReducer(reducer, {
  18 + ...defaultState,
  19 + url: paramUri,
  20 + });
  21 +
  22 + const webview = useRef<RNCWebview>(null);
  23 +
  24 + return (
  25 + <BGView>
  26 + <Body
  27 + initialUrl={paramUri}
  28 + state={state}
  29 + dispatch={dispatch}
  30 + ref={webview}
  31 + />
  32 + <ProgressBar progress={state.progress} loading={state.loading} />
  33 + <Toolbar state={state} dispatch={dispatch} webview={webview} />
  34 + </BGView>
  35 + );
  36 +};
  37 +
  38 +export default WebviewScreen;
  1 +import React, { Reducer } from 'react';
  2 +import { WebViewNavigation } from 'react-native-webview';
  3 +import { ImmerReducer } from '@huse/immer';
  4 +import {
  5 + WebViewError,
  6 + WebViewProgressEvent,
  7 + WebViewNativeProgressEvent,
  8 + WebViewNativeEvent,
  9 +} from 'react-native-webview/lib/WebViewTypes';
  10 +
  11 +export type WebviewActionTypes =
  12 + | 'ChangeNavigationState'
  13 + | 'OnLoadError'
  14 + | 'OnLoadProgress'
  15 + | 'LoadText';
  16 +
  17 +type PayloadAction<Type extends WebviewActionTypes, Payload> = {
  18 + type: Type;
  19 + payload: Payload;
  20 +};
  21 +
  22 +export type WebviewActions =
  23 + | PayloadAction<'ChangeNavigationState', WebViewNavigation>
  24 + | PayloadAction<'LoadText', string>
  25 + | PayloadAction<'OnLoadError', WebViewError>
  26 + | PayloadAction<'OnLoadProgress', WebViewNativeProgressEvent>;
  27 +
  28 +type PayloadOf<
  29 + A extends { type: string; payload: any },
  30 + T extends string
  31 +> = A extends {
  32 + type: T;
  33 + payload: infer R;
  34 +}
  35 + ? R
  36 + : never;
  37 +
  38 +const createAction = <
  39 + T extends WebviewActionTypes,
  40 + P extends PayloadOf<WebviewActions, T>
  41 +>(
  42 + type: T
  43 +) => (payload: P) => ({
  44 + type,
  45 + payload,
  46 +});
  47 +
  48 +export const webActions = {
  49 + changeNavigationState: createAction('ChangeNavigationState'),
  50 + onLoadError: createAction('OnLoadError'),
  51 + onLoadProgress: createAction('OnLoadProgress'),
  52 + loadText: createAction('LoadText'),
  53 +};
  54 +
  55 +export type WebviewState = WebViewNativeEvent & {
  56 + domain?: string;
  57 + code?: number;
  58 + description?: string;
  59 + progress: number;
  60 +};
  61 +
  62 +export const defaultState: WebviewState = {
  63 + canGoBack: false,
  64 + canGoForward: false,
  65 + url: 'https://about:blank',
  66 + title: '',
  67 + lockIdentifier: 0,
  68 + loading: false,
  69 + progress: 0,
  70 +};
  71 +
  72 +export const reducer: ImmerReducer<WebviewState, WebviewActions> = (
  73 + state,
  74 + action
  75 +) => {
  76 + switch (action.type) {
  77 + case 'ChangeNavigationState':
  78 + case 'OnLoadProgress':
  79 + case 'OnLoadError':
  80 + return { ...state, ...action.payload };
  81 + case 'LoadText':
  82 + state.url = action.payload;
  83 + break;
  84 + }
  85 +};
@@ -10,6 +10,7 @@ export type MainTabParamList = { @@ -10,6 +10,7 @@ export type MainTabParamList = {
10 export type MainStackParamList = { 10 export type MainStackParamList = {
11 MainTab: undefined; 11 MainTab: undefined;
12 RNDeviceInfoList: undefined; 12 RNDeviceInfoList: undefined;
  13 + WebviewScreen: { uri: string } | undefined;
13 }; 14 };
14 15
15 export type MainTabScreenProps<RouteName extends keyof MainTabParamList> = { 16 export type MainTabScreenProps<RouteName extends keyof MainTabParamList> = {
This diff is collapsed. Click to expand it.