From 690cd30b0af6e56dede2c7cf895b051ba566d0b8 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Mon, 26 Aug 2024 19:59:09 -0400 Subject: [PATCH] Lazy load MapView --- src/components/MapView/MapView.website.tsx | 353 +++--------------- .../MapView/MapViewImpl.website.tsx | 303 +++++++++++++++ src/languages/en.ts | 2 + src/languages/es.ts | 2 + 4 files changed, 364 insertions(+), 296 deletions(-) create mode 100644 src/components/MapView/MapViewImpl.website.tsx diff --git a/src/components/MapView/MapView.website.tsx b/src/components/MapView/MapView.website.tsx index 618dd5b24cf5..3a28943b575a 100644 --- a/src/components/MapView/MapView.website.tsx +++ b/src/components/MapView/MapView.website.tsx @@ -1,303 +1,64 @@ -// Explanation: Different Mapbox libraries are required for web and native mobile platforms. -// This is why we have separate components for web and native to handle the specific implementations. -// For the web version, we use the Mapbox Web library called react-map-gl, while for the native mobile version, -// we utilize a different Mapbox library @rnmapbox/maps tailored for mobile development. -import {useFocusEffect} from '@react-navigation/native'; -import mapboxgl from 'mapbox-gl'; -import 'mapbox-gl/dist/mapbox-gl.css'; -import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import type {MapRef, ViewState} from 'react-map-gl'; -import Map, {Marker} from 'react-map-gl'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import Button from '@components/Button'; -import * as Expensicons from '@components/Icon/Expensicons'; +import React, {forwardRef, lazy, Suspense, useEffect, useMemo, useState} from 'react'; +import {ErrorBoundary} from 'react-error-boundary'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {GeolocationErrorCallback} from '@libs/getCurrentPosition/getCurrentPosition.types'; -import {GeolocationErrorCode} from '@libs/getCurrentPosition/getCurrentPosition.types'; -import * as UserLocation from '@userActions/UserLocation'; -import CONST from '@src/CONST'; -import useLocalize from '@src/hooks/useLocalize'; -import useNetwork from '@src/hooks/useNetwork'; -import getCurrentPosition from '@src/libs/getCurrentPosition'; -import ONYXKEYS from '@src/ONYXKEYS'; -import Direction from './Direction'; -import './mapbox.css'; import type {MapViewHandle} from './MapViewTypes'; import PendingMapView from './PendingMapView'; -import responder from './responder'; -import type {ComponentProps, MapViewOnyxProps} from './types'; -import utils from './utils'; - -const MapView = forwardRef( - ( - { - style, - styleURL, - waypoints, - mapPadding, - accessToken, - userLocation, - directionCoordinates, - initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM}, - interactive = true, - }, - ref, - ) => { - const {isOffline} = useNetwork(); - const {translate} = useLocalize(); - - const theme = useTheme(); - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - - const [mapRef, setMapRef] = useState(null); - const initialLocation = useMemo(() => ({longitude: initialState.location[0], latitude: initialState.location[1]}), [initialState]); - const currentPosition = userLocation ?? initialLocation; - const prevUserPosition = usePrevious(currentPosition); - const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); - const [shouldResetBoundaries, setShouldResetBoundaries] = useState(false); - const setRef = useCallback((newRef: MapRef | null) => setMapRef(newRef), []); - const shouldInitializeCurrentPosition = useRef(true); - - // Determines if map can be panned to user's detected - // location without bothering the user. It will return - // false if user has already started dragging the map or - // if there are one or more waypoints present. - const shouldPanMapToCurrentPosition = useCallback(() => !userInteractedWithMap && (!waypoints || waypoints.length === 0), [userInteractedWithMap, waypoints]); - - const setCurrentPositionToInitialState: GeolocationErrorCallback = useCallback( - (error) => { - if (error?.code !== GeolocationErrorCode.PERMISSION_DENIED || !initialLocation) { - return; - } - UserLocation.clearUserLocation(); - }, - [initialLocation], - ); - - useFocusEffect( - useCallback(() => { - if (isOffline) { - return; - } - - if (!shouldInitializeCurrentPosition.current) { - return; - } - - shouldInitializeCurrentPosition.current = false; - - if (!shouldPanMapToCurrentPosition()) { - setCurrentPositionToInitialState(); - return; - } - - getCurrentPosition((params) => { - const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; - UserLocation.setUserLocation(currentCoords); - }, setCurrentPositionToInitialState); - }, [isOffline, shouldPanMapToCurrentPosition, setCurrentPositionToInitialState]), - ); - - useEffect(() => { - if (!currentPosition || !mapRef) { - return; - } - - if (!shouldPanMapToCurrentPosition()) { - return; - } - - // Avoid animating the naviagtion to the same location - const shouldAnimate = prevUserPosition.longitude !== currentPosition.longitude || prevUserPosition.latitude !== currentPosition.latitude; - - mapRef.flyTo({ - center: [currentPosition.longitude, currentPosition.latitude], - zoom: CONST.MAPBOX.DEFAULT_ZOOM, - animate: shouldAnimate, - }); - }, [currentPosition, mapRef, prevUserPosition, shouldPanMapToCurrentPosition]); - - const resetBoundaries = useCallback(() => { - if (!waypoints || waypoints.length === 0) { - return; - } - - if (!mapRef) { - return; - } - - if (waypoints.length === 1) { - mapRef.flyTo({ - center: waypoints[0].coordinate, - zoom: CONST.MAPBOX.SINGLE_MARKER_ZOOM, - }); - return; - } - - const map = mapRef.getMap(); - - const {northEast, southWest} = utils.getBounds( - waypoints.map((waypoint) => waypoint.coordinate), - directionCoordinates, - ); - map.fitBounds([northEast, southWest], {padding: mapPadding}); - }, [waypoints, mapRef, mapPadding, directionCoordinates]); - - useEffect(resetBoundaries, [resetBoundaries]); - - useEffect(() => { - if (!shouldResetBoundaries) { - return; - } - - resetBoundaries(); - setShouldResetBoundaries(false); - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- this effect only needs to run when the boundaries reset is forced - }, [shouldResetBoundaries]); - - useEffect(() => { - if (!mapRef) { - return; +import type {ComponentProps} from './types'; + +const MapView = forwardRef((props, ref) => { + const {isOffline} = useNetwork(); + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const [errorResetKey, setErrorResetKey] = useState(0); + + // Retry the error when reconnecting. + const wasOffline = usePrevious(isOffline); + useEffect(() => { + if (!wasOffline || isOffline) { + return; + } + setErrorResetKey((key) => key + 1); + }, [isOffline, wasOffline]); + + // The only way to retry loading the module is to call `React.lazy` again. + const MapViewImpl = useMemo( + () => lazy(() => import('./MapViewImpl.website')), + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + [errorResetKey], + ); + + return ( + } - - const resizeObserver = new ResizeObserver(() => { - mapRef.resize(); - setShouldResetBoundaries(true); - }); - resizeObserver.observe(mapRef.getContainer()); - - return () => { - resizeObserver?.disconnect(); - }; - }, [mapRef]); - - useImperativeHandle( - ref, - () => ({ - flyTo: (location: [number, number], zoomLevel: number = CONST.MAPBOX.DEFAULT_ZOOM, animationDuration?: number) => - mapRef?.flyTo({ - center: location, - zoom: zoomLevel, - duration: animationDuration, - }), - fitBounds: (northEast: [number, number], southWest: [number, number]) => mapRef?.fitBounds([northEast, southWest]), - }), - [mapRef], - ); - - const centerMap = useCallback(() => { - if (!mapRef) { - return; - } - const waypointCoordinates = waypoints?.map((waypoint) => waypoint.coordinate) ?? []; - if (waypointCoordinates.length > 1 || (directionCoordinates ?? []).length > 1) { - const {northEast, southWest} = utils.getBounds(waypoints?.map((waypoint) => waypoint.coordinate) ?? [], directionCoordinates); - const map = mapRef?.getMap(); - map?.fitBounds([southWest, northEast], {padding: mapPadding, animate: true, duration: CONST.MAPBOX.ANIMATION_DURATION_ON_CENTER_ME}); - return; - } - - mapRef.flyTo({ - center: [currentPosition?.longitude ?? 0, currentPosition?.latitude ?? 0], - zoom: CONST.MAPBOX.SINGLE_MARKER_ZOOM, - bearing: 0, - animate: true, - duration: CONST.MAPBOX.ANIMATION_DURATION_ON_CENTER_ME, - }); - }, [directionCoordinates, currentPosition, mapRef, waypoints, mapPadding]); - - const initialViewState: Partial | undefined = useMemo(() => { - if (!interactive) { - if (!waypoints) { - return undefined; + > + } - const {northEast, southWest} = utils.getBounds( - waypoints.map((waypoint) => waypoint.coordinate), - directionCoordinates, - ); - return { - zoom: initialState.zoom, - bounds: [northEast, southWest], - }; - } - return { - longitude: currentPosition?.longitude, - latitude: currentPosition?.latitude, - zoom: initialState.zoom, - }; - }, [waypoints, directionCoordinates, interactive, currentPosition, initialState.zoom]); - - return !isOffline && !!accessToken && !!initialViewState ? ( - - setUserInteractedWithMap(true)} - ref={setRef} - mapLib={mapboxgl} - mapboxAccessToken={accessToken} - initialViewState={initialViewState} - style={StyleUtils.getTextColorStyle(theme.mapAttributionText)} - mapStyle={styleURL} - interactive={interactive} - > - {interactive && ( - - - - )} - {waypoints?.map(({coordinate, markerComponent, id}) => { - const MarkerComponent = markerComponent; - if (utils.areSameCoordinate([coordinate[0], coordinate[1]], [currentPosition?.longitude ?? 0, currentPosition?.latitude ?? 0]) && interactive) { - return null; - } - return ( - - - - ); - })} - {directionCoordinates && } - - {interactive && ( - -