From f911a33342bf5adae3581db049bed6c2970fd50a Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Fri, 27 Oct 2023 14:38:44 +0200 Subject: [PATCH 01/17] Auto map panning when location rights granted --- src/CONST.ts | 4 + src/ONYXKEYS.ts | 4 + src/components/MapView/MapView.web.tsx | 144 ++++++++++++++++++------- src/types/onyx/UserLocation.ts | 3 + src/types/onyx/index.ts | 2 + 5 files changed, 119 insertions(+), 38 deletions(-) create mode 100644 src/types/onyx/UserLocation.ts diff --git a/src/CONST.ts b/src/CONST.ts index 9d912a4df20e..08da9f601468 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -875,6 +875,10 @@ const CONST = { YOUR_LOCATION_TEXT: 'Your Location', + // Value indicating the maximum age in milliseconds + // of a possible cached position that is acceptable to return + MAXIMUM_AGE_OF_CACHED_USER_LOCATION: 10 * 60 * 1000, + ATTACHMENT_MESSAGE_TEXT: '[Attachment]', // This is a placeholder for attachment which is uploading ATTACHMENT_UPLOADING_MESSAGE_HTML: 'Uploading attachment...', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 646e23c8af1e..8378f62b6bba 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -84,6 +84,9 @@ const ONYXKEYS = { /** Contains all the users settings for the Settings page and sub pages */ USER: 'user', + /** Contains latitude and longitude of user's last known location */ + USER_LOCATION: 'userLocation', + /** Contains metadata (partner, login, validation date) for all of the user's logins */ LOGIN_LIST: 'loginList', @@ -333,6 +336,7 @@ type OnyxValues = { [ONYXKEYS.COUNTRY_CODE]: number; [ONYXKEYS.COUNTRY]: string; [ONYXKEYS.USER]: OnyxTypes.User; + [ONYXKEYS.USER_LOCATION]: OnyxTypes.UserLocation; [ONYXKEYS.LOGIN_LIST]: OnyxTypes.Login; [ONYXKEYS.SESSION]: OnyxTypes.Session; [ONYXKEYS.BETAS]: OnyxTypes.Beta[]; diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index 78c5a9175594..b433099ca6e4 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -5,25 +5,81 @@ import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useState} from 'react'; import {View} from 'react-native'; +import {useFocusEffect} from '@react-navigation/native'; import Map, {MapRef, Marker} from 'react-map-gl'; import mapboxgl from 'mapbox-gl'; - +import Onyx, { OnyxEntry, withOnyx } from 'react-native-onyx'; import responder from './responder'; import utils from './utils'; - import CONST from '../../CONST'; +import ONYXKEYS from '../../ONYXKEYS'; +import styles from '../../styles/styles'; +import * as OnyxTypes from '../../types/onyx'; import * as StyleUtils from '../../styles/StyleUtils'; import themeColors from '../../styles/themes/default'; import Direction from './Direction'; import {MapViewHandle, MapViewProps} from './MapViewTypes'; - +import getCurrentPosition from '../../libs/getCurrentPosition'; +import Text from '../../components/Text' import 'mapbox-gl/dist/mapbox-gl.css'; -const MapView = forwardRef( - ({style, styleURL, waypoints, mapPadding, accessToken, directionCoordinates, initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM}}, ref) => { +type MapViewOnyxProps = { + userLocation: OnyxEntry; +} + +type ComponentProps = MapViewProps & MapViewOnyxProps + +const MapView = forwardRef( + ({style, styleURL, waypoints, mapPadding, accessToken, userLocation: cachedUserLocation, directionCoordinates, initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM}}, ref) => { const [mapRef, setMapRef] = useState(null); + const [userLocation, setUserLocation] = useState(cachedUserLocation); + const [isCurrentPositionFetching, setIsCurrentPositionFetching] = useState(true); + const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); const setRef = useCallback((newRef: MapRef | null) => setMapRef(newRef), []); + useFocusEffect( + useCallback(() => { + console.log('Start location search') + getCurrentPosition((params) => { + setUserLocation({latitude: params.coords.latitude, longitude: params.coords.longitude}) + Onyx.merge(ONYXKEYS.USER_LOCATION, { latitude: params.coords.latitude, longitude: params.coords.longitude}) + setIsCurrentPositionFetching(false); + console.log('Location search success') + }, + () => { + // On error do nothing - an already cached location + // or the default location will be presented to the user + console.log('Location search error') + setIsCurrentPositionFetching(false); + }) + }, []) + ) + + useEffect(() => { + if (!userLocation || !mapRef) { + return; + } + + console.log('Map loaded') + + if (waypoints && waypoints.length > 0) { + console.log('Waypoints existing. Dont jump') + return; + } + + if (userInteractedWithMap) { + console.log('User already started clicking or dragging through the map. Dont jump') + return; + } + + console.log('No waypoints added. JumpTo') + + mapRef.jumpTo({ + center: [userLocation.longitude, userLocation.latitude], + zoom: CONST.MAPBOX.DEFAULT_ZOOM, + }); + }, [userLocation, userInteractedWithMap, mapRef]) + useEffect(() => { if (!waypoints || waypoints.length === 0) { return; @@ -36,7 +92,7 @@ const MapView = forwardRef( if (waypoints.length === 1) { mapRef.flyTo({ center: waypoints[0].coordinate, - zoom: 15, + zoom: CONST.MAPBOX.DEFAULT_ZOOM, }); return; } @@ -80,40 +136,52 @@ const MapView = forwardRef( ); return ( - - + + setUserInteractedWithMap(true)} + ref={setRef} + mapLib={mapboxgl} + mapboxAccessToken={accessToken} + initialViewState={{ + longitude: initialState.location[0], + latitude: initialState.location[1], + zoom: initialState.zoom, + }} + style={StyleUtils.getTextColorStyle(themeColors.mapAttributionText) as React.CSSProperties} + mapStyle={styleURL} + > + {waypoints?.map(({coordinate, markerComponent, id}) => { + const MarkerComponent = markerComponent; + return ( + + + + ); + })} + {directionCoordinates && } + + + - {waypoints?.map(({coordinate, markerComponent, id}) => { - const MarkerComponent = markerComponent; - return ( - - - - ); - })} - {directionCoordinates && } - - + {isCurrentPositionFetching ? 'Finding your location...' : ' '} + + ); }, ); -export default MapView; +export default withOnyx({ + userLocation: { + key: ONYXKEYS.USER_LOCATION, + } +})(MapView) \ No newline at end of file diff --git a/src/types/onyx/UserLocation.ts b/src/types/onyx/UserLocation.ts new file mode 100644 index 000000000000..b22802bfefb1 --- /dev/null +++ b/src/types/onyx/UserLocation.ts @@ -0,0 +1,3 @@ +type UserLocation = Pick; + +export default UserLocation; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index cbe1074f630d..ca679da8c9a9 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -11,6 +11,7 @@ import Task from './Task'; import Currency from './Currency'; import ScreenShareRequest from './ScreenShareRequest'; import User from './User'; +import UserLocation from './UserLocation'; import Login from './Login'; import Session from './Session'; import Beta from './Beta'; @@ -63,6 +64,7 @@ export type { Currency, ScreenShareRequest, User, + UserLocation, Login, Session, Beta, From 6b2527666b7cfbc7cd6db3a07865bb7ffe8031c2 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Fri, 27 Oct 2023 16:42:42 +0200 Subject: [PATCH 02/17] Code cleanup - helper method; drop console logs --- src/components/MapView/MapView.web.tsx | 40 +++++++++----------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index b433099ca6e4..f39fab19bc7e 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -32,53 +32,42 @@ type ComponentProps = MapViewProps & MapViewOnyxProps const MapView = forwardRef( ({style, styleURL, waypoints, mapPadding, accessToken, userLocation: cachedUserLocation, directionCoordinates, initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM}}, ref) => { const [mapRef, setMapRef] = useState(null); - const [userLocation, setUserLocation] = useState(cachedUserLocation); + const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); const [isCurrentPositionFetching, setIsCurrentPositionFetching] = useState(true); const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); const setRef = useCallback((newRef: MapRef | null) => setMapRef(newRef), []); useFocusEffect( useCallback(() => { - console.log('Start location search') getCurrentPosition((params) => { - setUserLocation({latitude: params.coords.latitude, longitude: params.coords.longitude}) + setCurrentPosition({latitude: params.coords.latitude, longitude: params.coords.longitude}) Onyx.merge(ONYXKEYS.USER_LOCATION, { latitude: params.coords.latitude, longitude: params.coords.longitude}) setIsCurrentPositionFetching(false); - console.log('Location search success') }, - () => { - // On error do nothing - an already cached location - // or the default location will be presented to the user - console.log('Location search error') - setIsCurrentPositionFetching(false); - }) + () => { setIsCurrentPositionFetching(false); }) }, []) ) useEffect(() => { - if (!userLocation || !mapRef) { + if (!currentPosition || !mapRef) { return; } - console.log('Map loaded') - - if (waypoints && waypoints.length > 0) { - console.log('Waypoints existing. Dont jump') + if (!shouldPanMapToCurrentPosition()) { return; } - if (userInteractedWithMap) { - console.log('User already started clicking or dragging through the map. Dont jump') - return; - } - - console.log('No waypoints added. JumpTo') - mapRef.jumpTo({ - center: [userLocation.longitude, userLocation.latitude], + center: [currentPosition.longitude, currentPosition.latitude], zoom: CONST.MAPBOX.DEFAULT_ZOOM, }); - }, [userLocation, userInteractedWithMap, mapRef]) + }, [currentPosition, userInteractedWithMap, mapRef]) + + // 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 = () => !userInteractedWithMap && (!waypoints || waypoints.length === 0) useEffect(() => { if (!waypoints || waypoints.length === 0) { @@ -139,7 +128,6 @@ const MapView = forwardRef( <> ( - {isCurrentPositionFetching ? 'Finding your location...' : ' '} + {isCurrentPositionFetching && shouldPanMapToCurrentPosition() ? 'Finding your location...' : ' '} ); From 111921a3ac3ad3dcaa46d5ce4f0c54bdc318449b Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 30 Oct 2023 09:45:29 +0100 Subject: [PATCH 03/17] Drop Location loading indicator --- src/components/MapView/MapView.web.tsx | 71 +++++++++++--------------- 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index f39fab19bc7e..dd30aab22558 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -13,14 +13,12 @@ import responder from './responder'; import utils from './utils'; import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; -import styles from '../../styles/styles'; import * as OnyxTypes from '../../types/onyx'; import * as StyleUtils from '../../styles/StyleUtils'; import themeColors from '../../styles/themes/default'; import Direction from './Direction'; import {MapViewHandle, MapViewProps} from './MapViewTypes'; import getCurrentPosition from '../../libs/getCurrentPosition'; -import Text from '../../components/Text' import 'mapbox-gl/dist/mapbox-gl.css'; type MapViewOnyxProps = { @@ -125,45 +123,38 @@ const MapView = forwardRef( ); return ( - <> - + setUserInteractedWithMap(true)} + ref={setRef} + mapLib={mapboxgl} + mapboxAccessToken={accessToken} + initialViewState={{ + longitude: initialState.location[0], + latitude: initialState.location[1], + zoom: initialState.zoom, + }} + style={StyleUtils.getTextColorStyle(themeColors.mapAttributionText) as React.CSSProperties} + mapStyle={styleURL} > - setUserInteractedWithMap(true)} - ref={setRef} - mapLib={mapboxgl} - mapboxAccessToken={accessToken} - initialViewState={{ - longitude: initialState.location[0], - latitude: initialState.location[1], - zoom: initialState.zoom, - }} - style={StyleUtils.getTextColorStyle(themeColors.mapAttributionText) as React.CSSProperties} - mapStyle={styleURL} - > - {waypoints?.map(({coordinate, markerComponent, id}) => { - const MarkerComponent = markerComponent; - return ( - - - - ); - })} - {directionCoordinates && } - - - - {isCurrentPositionFetching && shouldPanMapToCurrentPosition() ? 'Finding your location...' : ' '} - - + {waypoints?.map(({coordinate, markerComponent, id}) => { + const MarkerComponent = markerComponent; + return ( + + + + ); + })} + {directionCoordinates && } + + ); }, ); From 4f1563682703656fec5e064f425e34eae1d18ec7 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 30 Oct 2023 11:27:12 +0100 Subject: [PATCH 04/17] Move PendingMapView directly to MapView --- .../distanceMapViewPropTypes.js | 2 +- .../DistanceRequest/DistanceRequestFooter.js | 39 +++----- src/components/MapView/MapView.web.tsx | 93 +++++++++++-------- 3 files changed, 70 insertions(+), 64 deletions(-) diff --git a/src/components/DistanceMapView/distanceMapViewPropTypes.js b/src/components/DistanceMapView/distanceMapViewPropTypes.js index 05068cbc9b34..89aca61510e1 100644 --- a/src/components/DistanceMapView/distanceMapViewPropTypes.js +++ b/src/components/DistanceMapView/distanceMapViewPropTypes.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; const propTypes = { // Public access token to be used to fetch map data from Mapbox. - accessToken: PropTypes.string.isRequired, + accessToken: PropTypes.string, // Style applied to MapView component. Note some of the View Style props are not available on ViewMap style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), diff --git a/src/components/DistanceRequest/DistanceRequestFooter.js b/src/components/DistanceRequest/DistanceRequestFooter.js index eaa02968c388..f7216ae199c2 100644 --- a/src/components/DistanceRequest/DistanceRequestFooter.js +++ b/src/components/DistanceRequest/DistanceRequestFooter.js @@ -8,14 +8,12 @@ import _ from 'underscore'; import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; import styles from '../../styles/styles'; -import useNetwork from '../../hooks/useNetwork'; import useLocalize from '../../hooks/useLocalize'; import theme from '../../styles/themes/default'; import * as TransactionUtils from '../../libs/TransactionUtils'; import Button from '../Button'; import DistanceMapView from '../DistanceMapView'; import * as Expensicons from '../Icon/Expensicons'; -import PendingMapView from '../MapView/PendingMapView'; import transactionPropTypes from '../transactionPropTypes'; const MAX_WAYPOINTS = 25; @@ -55,7 +53,6 @@ const defaultProps = { transaction: {}, }; function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navigateToWaypointEditPage}) { - const {isOffline} = useNetwork(); const {translate} = useLocalize(); const numberOfWaypoints = _.size(waypoints); @@ -109,28 +106,20 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig /> - {!isOffline && Boolean(mapboxAccessToken.token) ? ( - - ) : ( - - )} + ); diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index dd30aab22558..a689adbe5723 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -8,6 +8,7 @@ import {View} from 'react-native'; import {useFocusEffect} from '@react-navigation/native'; import Map, {MapRef, Marker} from 'react-map-gl'; import mapboxgl from 'mapbox-gl'; +import PendingMapView from '../MapView/PendingMapView'; import Onyx, { OnyxEntry, withOnyx } from 'react-native-onyx'; import responder from './responder'; import utils from './utils'; @@ -19,6 +20,9 @@ import themeColors from '../../styles/themes/default'; import Direction from './Direction'; import {MapViewHandle, MapViewProps} from './MapViewTypes'; import getCurrentPosition from '../../libs/getCurrentPosition'; +import useNetwork from '../../hooks/useNetwork'; +import useLocalize from '../../hooks/useLocalize'; +import styles from '../../styles/styles'; import 'mapbox-gl/dist/mapbox-gl.css'; type MapViewOnyxProps = { @@ -29,20 +33,23 @@ type ComponentProps = MapViewProps & MapViewOnyxProps const MapView = forwardRef( ({style, styleURL, waypoints, mapPadding, accessToken, userLocation: cachedUserLocation, directionCoordinates, initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM}}, ref) => { + const {isOffline} = useNetwork(); + const {translate} = useLocalize(); + const [mapRef, setMapRef] = useState(null); const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); - const [isCurrentPositionFetching, setIsCurrentPositionFetching] = useState(true); const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); const setRef = useCallback((newRef: MapRef | null) => setMapRef(newRef), []); useFocusEffect( useCallback(() => { getCurrentPosition((params) => { - setCurrentPosition({latitude: params.coords.latitude, longitude: params.coords.longitude}) - Onyx.merge(ONYXKEYS.USER_LOCATION, { latitude: params.coords.latitude, longitude: params.coords.longitude}) - setIsCurrentPositionFetching(false); + setCurrentPosition({longitude: params.coords.longitude, latitude: params.coords.latitude}) + Onyx.merge(ONYXKEYS.USER_LOCATION, { longitude: params.coords.longitude, latitude: params.coords.latitude}) }, - () => { setIsCurrentPositionFetching(false); }) + () => { + setCurrentPosition({ longitude: initialState.location[0], latitude: initialState.location[1] }) + }) }, []) ) @@ -55,7 +62,7 @@ const MapView = forwardRef( return; } - mapRef.jumpTo({ + mapRef.flyTo({ center: [currentPosition.longitude, currentPosition.latitude], zoom: CONST.MAPBOX.DEFAULT_ZOOM, }); @@ -123,38 +130,48 @@ const MapView = forwardRef( ); return ( - - setUserInteractedWithMap(true)} - ref={setRef} - mapLib={mapboxgl} - mapboxAccessToken={accessToken} - initialViewState={{ - longitude: initialState.location[0], - latitude: initialState.location[1], - zoom: initialState.zoom, - }} - style={StyleUtils.getTextColorStyle(themeColors.mapAttributionText) as React.CSSProperties} - mapStyle={styleURL} - > - {waypoints?.map(({coordinate, markerComponent, id}) => { - const MarkerComponent = markerComponent; - return ( - - - - ); - })} - {directionCoordinates && } - - + <> + {!isOffline && Boolean(accessToken) && Boolean(currentPosition) ? ( + + setUserInteractedWithMap(true)} + ref={setRef} + mapLib={mapboxgl} + mapboxAccessToken={accessToken} + initialViewState={{ + longitude: currentPosition?.longitude, + latitude: currentPosition?.latitude, + zoom: initialState.zoom, + }} + style={StyleUtils.getTextColorStyle(themeColors.mapAttributionText) as React.CSSProperties} + mapStyle={styleURL} + > + {waypoints?.map(({coordinate, markerComponent, id}) => { + const MarkerComponent = markerComponent; + return ( + + + + ); + })} + {directionCoordinates && } + + + ) : ( + + )} + ); }, ); From 88dc1a74bab22205067fa533684cdcd4e5aae888 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 30 Oct 2023 12:01:16 +0100 Subject: [PATCH 05/17] Drop duplicate import --- src/types/onyx/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 794f69f943a9..832e1009f9d0 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -1,5 +1,4 @@ import Account from './Account'; -import Task from './Task'; import UserLocation from './UserLocation'; import AccountData from './AccountData'; import BankAccount from './BankAccount'; From b6362d4b33d263e98dced059a9d0f66192fadffa Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 30 Oct 2023 12:05:15 +0100 Subject: [PATCH 06/17] Blank lines --- src/components/MapView/MapView.web.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index 1f43ddc63f58..709ae8a0bfe7 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -2,7 +2,6 @@ // 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 React, {forwardRef, useCallback, useEffect, useImperativeHandle, useState} from 'react'; import {View} from 'react-native'; import {useFocusEffect} from '@react-navigation/native'; @@ -180,4 +179,4 @@ export default withOnyx({ userLocation: { key: ONYXKEYS.USER_LOCATION, } -})(MapView) \ No newline at end of file +})(MapView) From 103eed2e57539f8efba865ecc94a394daf37f974 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 30 Oct 2023 14:10:34 +0100 Subject: [PATCH 07/17] Linter and typecheck --- .../DistanceRequest/DistanceRequestFooter.js | 2 - src/components/MapView/MapView.web.tsx | 90 +++++++++++-------- 2 files changed, 54 insertions(+), 38 deletions(-) diff --git a/src/components/DistanceRequest/DistanceRequestFooter.js b/src/components/DistanceRequest/DistanceRequestFooter.js index 138fcc003401..039cbb807e94 100644 --- a/src/components/DistanceRequest/DistanceRequestFooter.js +++ b/src/components/DistanceRequest/DistanceRequestFooter.js @@ -8,10 +8,8 @@ import _ from 'underscore'; import Button from '@components/Button'; import DistanceMapView from '@components/DistanceMapView'; import * as Expensicons from '@components/Icon/Expensicons'; -import PendingMapView from '@components/MapView/PendingMapView'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; import * as TransactionUtils from '@libs/TransactionUtils'; import styles from '@styles/styles'; import theme from '@styles/themes/default'; diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index 709ae8a0bfe7..5ee06b682b02 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -2,37 +2,51 @@ // 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 React, {forwardRef, useCallback, useEffect, useImperativeHandle, useState} from 'react'; -import {View} from 'react-native'; import {useFocusEffect} from '@react-navigation/native'; -import Map, {MapRef, Marker} from 'react-map-gl'; import mapboxgl from 'mapbox-gl'; -import PendingMapView from '../MapView/PendingMapView'; -import Onyx, { OnyxEntry, withOnyx } from 'react-native-onyx'; -import responder from './responder'; -import utils from './utils'; +import 'mapbox-gl/dist/mapbox-gl.css'; +import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useState} from 'react'; +import Map, {MapRef, Marker} from 'react-map-gl'; +import {View} from 'react-native'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import * as StyleUtils from '@styles/StyleUtils'; +import themeColors from '@styles/themes/default'; +import setUserLocation 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 styles from '@src/styles/styles'; import * as OnyxTypes from '@src/types/onyx'; -import * as StyleUtils from '@styles/StyleUtils'; -import themeColors from '@styles/themes/default'; import Direction from './Direction'; import {MapViewHandle, MapViewProps} from './MapViewTypes'; -import getCurrentPosition from '@src/libs/getCurrentPosition'; -import useNetwork from '@src/hooks/useNetwork'; -import useLocalize from '@src/hooks/useLocalize'; -import styles from '@src/styles/styles'; -import 'mapbox-gl/dist/mapbox-gl.css'; +import PendingMapView from './PendingMapView'; +import responder from './responder'; +import utils from './utils'; type MapViewOnyxProps = { userLocation: OnyxEntry; -} +}; -type ComponentProps = MapViewProps & MapViewOnyxProps +type ComponentProps = MapViewProps & MapViewOnyxProps; const MapView = forwardRef( - ({style, styleURL, waypoints, mapPadding, accessToken, userLocation: cachedUserLocation, directionCoordinates, initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM}}, ref) => { + ( + { + style, + styleURL, + waypoints, + mapPadding, + accessToken, + userLocation: cachedUserLocation, + directionCoordinates, + initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM}, + }, + ref, + ) => { const {isOffline} = useNetwork(); + // @ts-ignore - useLocalize not migrated to TypeScript yet const {translate} = useLocalize(); const [mapRef, setMapRef] = useState(null); @@ -42,15 +56,24 @@ const MapView = forwardRef( useFocusEffect( useCallback(() => { - getCurrentPosition((params) => { - setCurrentPosition({longitude: params.coords.longitude, latitude: params.coords.latitude}) - Onyx.merge(ONYXKEYS.USER_LOCATION, { longitude: params.coords.longitude, latitude: params.coords.latitude}) - }, - () => { - setCurrentPosition({ longitude: initialState.location[0], latitude: initialState.location[1] }) - }) - }, []) - ) + getCurrentPosition( + (params) => { + const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; + setCurrentPosition(currentCoords); + setUserLocation(currentCoords); + }, + () => { + setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); + }, + ); + }, [initialState.location]), + ); + + // 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]); useEffect(() => { if (!currentPosition || !mapRef) { @@ -65,13 +88,7 @@ const MapView = forwardRef( center: [currentPosition.longitude, currentPosition.latitude], zoom: CONST.MAPBOX.DEFAULT_ZOOM, }); - }, [currentPosition, userInteractedWithMap, mapRef]) - - // 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 = () => !userInteractedWithMap && (!waypoints || waypoints.length === 0) + }, [currentPosition, userInteractedWithMap, mapRef, shouldPanMapToCurrentPosition]); useEffect(() => { if (!waypoints || waypoints.length === 0) { @@ -133,6 +150,7 @@ const MapView = forwardRef( {!isOffline && Boolean(accessToken) && Boolean(currentPosition) ? ( ( }, ); -export default withOnyx({ +export default withOnyx({ userLocation: { key: ONYXKEYS.USER_LOCATION, - } -})(MapView) + }, +})(MapView); From c6e25015156e5959df3c1ebee80db893cb91aed2 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 30 Oct 2023 14:10:54 +0100 Subject: [PATCH 08/17] Set user location action --- src/libs/actions/UserLocation.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/libs/actions/UserLocation.ts diff --git a/src/libs/actions/UserLocation.ts b/src/libs/actions/UserLocation.ts new file mode 100644 index 000000000000..4c58f7a83284 --- /dev/null +++ b/src/libs/actions/UserLocation.ts @@ -0,0 +1,12 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {UserLocation} from '@src/types/onyx'; + +/** + * Sets the longitude and latitude of user's current location + */ +function setUserLocation({longitude, latitude}: UserLocation) { + Onyx.set(ONYXKEYS.USER_LOCATION, {longitude, latitude}); +} + +export default setUserLocation; From 86d4468c990785f5922bd47db132b62cdfa7f62f Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 30 Oct 2023 15:28:31 +0100 Subject: [PATCH 09/17] Move import down --- src/types/onyx/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 832e1009f9d0..382ba0bbd9bf 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -1,5 +1,4 @@ import Account from './Account'; -import UserLocation from './UserLocation'; import AccountData from './AccountData'; import BankAccount from './BankAccount'; import Beta from './Beta'; @@ -43,6 +42,7 @@ import Session from './Session'; import Task from './Task'; import Transaction from './Transaction'; import User from './User'; +import UserLocation from './UserLocation'; import UserWallet from './UserWallet'; import WalletAdditionalDetails from './WalletAdditionalDetails'; import WalletOnfido from './WalletOnfido'; From 85cc9515e41936810c608a831a0396fdaa0fcfb1 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 30 Oct 2023 15:28:59 +0100 Subject: [PATCH 10/17] Handle offline mode and cached locations --- src/components/MapView/MapView.web.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index 5ee06b682b02..567e613fe9f1 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -46,8 +46,7 @@ const MapView = forwardRef( ref, ) => { const {isOffline} = useNetwork(); - // @ts-ignore - useLocalize not migrated to TypeScript yet - const {translate} = useLocalize(); + const {translate} = useLocalize() const [mapRef, setMapRef] = useState(null); const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); @@ -56,6 +55,10 @@ const MapView = forwardRef( useFocusEffect( useCallback(() => { + if (isOffline) { + return; + } + getCurrentPosition( (params) => { const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; @@ -63,10 +66,14 @@ const MapView = forwardRef( setUserLocation(currentCoords); }, () => { + if (cachedUserLocation) { + return; + } + setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); }, ); - }, [initialState.location]), + }, [cachedUserLocation, isOffline, initialState.location]), ); // Determines if map can be panned to user's detected From a66411034b1e1a1cb9d9b4e85c8316bf4593bbdc Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Tue, 31 Oct 2023 18:16:26 +0100 Subject: [PATCH 11/17] Auto map panning for mobile --- src/components/MapView/MapView.tsx | 193 ++++++++++++++++++------- src/components/MapView/MapView.web.tsx | 14 +- src/components/MapView/types.ts | 14 ++ 3 files changed, 157 insertions(+), 64 deletions(-) create mode 100644 src/components/MapView/types.ts diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index c91dc63a3bd1..1facc5dae242 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -4,15 +4,76 @@ import {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, u import {View} from 'react-native'; import styles from '@styles/styles'; import CONST from '@src/CONST'; +import useLocalize from '@src/hooks/useLocalize'; +import useNetwork from '@src/hooks/useNetwork'; +import { withOnyx } from 'react-native-onyx'; +import compose from '@libs/compose'; +import ONYXKEYS from '@src/ONYXKEYS'; +import PendingMapView from './PendingMapView'; import Direction from './Direction'; -import {MapViewHandle, MapViewProps} from './MapViewTypes'; +import {MapViewHandle} from './MapViewTypes'; import responder from './responder'; import utils from './utils'; +import { ComponentProps, MapViewOnyxProps } from './types'; +import getCurrentPosition from '@libs/getCurrentPosition'; +import setUserLocation from '@libs/actions/UserLocation'; +import Text from '@components/Text'; -const MapView = forwardRef(({accessToken, style, mapPadding, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady}, ref) => { +const MapView = forwardRef(({accessToken, style, mapPadding, userLocation: cachedUserLocation, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady}, ref) => { + const navigation = useNavigation(); + const {isOffline} = useNetwork(); + const {translate} = useLocalize(); + + const [logger, setLogger] = useState(''); const cameraRef = useRef(null); const [isIdle, setIsIdle] = useState(false); - const navigation = useNavigation(); + const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); + const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); + + useFocusEffect( + useCallback(() => { + if (isOffline) { + return; + } + + getCurrentPosition( + (params) => { + const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; + setCurrentPosition(currentCoords); + setUserLocation(currentCoords); + }, + () => { + if (cachedUserLocation || !initialState) { + return; + } + + setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); + } + ) + }, []) + ) + + // 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]); + + useEffect(() => { + if (!currentPosition || !cameraRef.current) { + return; + } + + if (!shouldPanMapToCurrentPosition()) { + return; + } + + cameraRef.current.setCamera({ + zoomLevel: CONST.MAPBOX.DEFAULT_ZOOM, + animationDuration: 1500, + centerCoordinate: [currentPosition.longitude, currentPosition.latitude], + }); + }, [currentPosition, cameraRef.current, shouldPanMapToCurrentPosition]); useImperativeHandle( ref, @@ -29,22 +90,23 @@ const MapView = forwardRef(({accessToken, style, ma // When the page regains focus, the onIdled method of the map will set the actual "idled" state, // which in turn triggers the callback. useFocusEffect( - // eslint-disable-next-line rulesdir/prefer-early-return useCallback(() => { - if (waypoints?.length && isIdle) { - if (waypoints.length === 1) { - cameraRef.current?.setCamera({ - zoomLevel: 15, - animationDuration: 1500, - centerCoordinate: waypoints[0].coordinate, - }); - } else { - const {southWest, northEast} = utils.getBounds( - waypoints.map((waypoint) => waypoint.coordinate), - directionCoordinates, - ); - cameraRef.current?.fitBounds(northEast, southWest, mapPadding, 1000); - } + if (!waypoints || waypoints.length === 0 || !isIdle) { + return; + } + + if (waypoints.length === 1) { + cameraRef.current?.setCamera({ + zoomLevel: 15, + animationDuration: 1500, + centerCoordinate: waypoints[0].coordinate, + }); + } else { + const {southWest, northEast} = utils.getBounds( + waypoints.map((waypoint) => waypoint.coordinate), + directionCoordinates, + ); + cameraRef.current?.fitBounds(northEast, southWest, mapPadding, 1000); } }, [mapPadding, waypoints, isIdle, directionCoordinates]), ); @@ -71,43 +133,66 @@ const MapView = forwardRef(({accessToken, style, ma }; return ( - - - - - {waypoints?.map(({coordinate, markerComponent, id}) => { - const MarkerComponent = markerComponent; - return ( - + {!isOffline && Boolean(accessToken) && Boolean(currentPosition) ? ( + <> + + setUserInteractedWithMap(true)} + pitchEnabled={pitchEnabled} + attributionPosition={{...styles.r2, ...styles.b2}} + scaleBarEnabled={false} + logoPosition={{...styles.l2, ...styles.b2}} + // eslint-disable-next-line react/jsx-props-no-spreading + {...responder.panHandlers} > - - - ); - })} - - {directionCoordinates && } - - + + + {waypoints?.map(({coordinate, markerComponent, id}) => { + const MarkerComponent = markerComponent; + return ( + + + + ); + })} + + {directionCoordinates && } + + + + {logger} + + + ): ( + + )} + ); }); -export default memo(MapView); +export default compose( + withOnyx({ + userLocation: { + key: ONYXKEYS.USER_LOCATION, + }, + }), + memo, +)(MapView); diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index 567e613fe9f1..b7cb84b16dbc 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -8,7 +8,7 @@ import 'mapbox-gl/dist/mapbox-gl.css'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useState} from 'react'; import Map, {MapRef, Marker} from 'react-map-gl'; import {View} from 'react-native'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import * as StyleUtils from '@styles/StyleUtils'; import themeColors from '@styles/themes/default'; import setUserLocation from '@userActions/UserLocation'; @@ -18,18 +18,12 @@ import useNetwork from '@src/hooks/useNetwork'; import getCurrentPosition from '@src/libs/getCurrentPosition'; import ONYXKEYS from '@src/ONYXKEYS'; import styles from '@src/styles/styles'; -import * as OnyxTypes from '@src/types/onyx'; import Direction from './Direction'; -import {MapViewHandle, MapViewProps} from './MapViewTypes'; +import {MapViewHandle} from './MapViewTypes'; import PendingMapView from './PendingMapView'; import responder from './responder'; import utils from './utils'; - -type MapViewOnyxProps = { - userLocation: OnyxEntry; -}; - -type ComponentProps = MapViewProps & MapViewOnyxProps; +import { ComponentProps, MapViewOnyxProps } from './types'; const MapView = forwardRef( ( @@ -46,7 +40,7 @@ const MapView = forwardRef( ref, ) => { const {isOffline} = useNetwork(); - const {translate} = useLocalize() + const {translate} = useLocalize(); const [mapRef, setMapRef] = useState(null); const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); diff --git a/src/components/MapView/types.ts b/src/components/MapView/types.ts new file mode 100644 index 000000000000..2253e9018be1 --- /dev/null +++ b/src/components/MapView/types.ts @@ -0,0 +1,14 @@ +import {OnyxEntry} from 'react-native-onyx'; +import * as OnyxTypes from '@src/types/onyx'; +import {MapViewProps} from './MapViewTypes'; + +type MapViewOnyxProps = { + userLocation: OnyxEntry; +}; + +type ComponentProps = MapViewProps & MapViewOnyxProps; + +export type { + MapViewOnyxProps, + ComponentProps +} From 1c4f7277d3df08265ea0a84ce93546b7130e9068 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Thu, 2 Nov 2023 17:03:39 +0100 Subject: [PATCH 12/17] Drop unused const --- src/CONST.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 5be25e0ff220..db8a9cc49dc0 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -877,10 +877,6 @@ const CONST = { YOUR_LOCATION_TEXT: 'Your Location', - // Value indicating the maximum age in milliseconds - // of a possible cached position that is acceptable to return - MAXIMUM_AGE_OF_CACHED_USER_LOCATION: 10 * 60 * 1000, - ATTACHMENT_MESSAGE_TEXT: '[Attachment]', // This is a placeholder for attachment which is uploading ATTACHMENT_UPLOADING_MESSAGE_HTML: 'Uploading attachment...', From 5754b001b0916aa6992164ac08ed883d14090ea3 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 6 Nov 2023 09:35:37 +0100 Subject: [PATCH 13/17] Prettier --- src/components/MapView/MapView.web.tsx | 2 +- src/components/MapView/types.ts | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index b7cb84b16dbc..14d105e265bd 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -22,8 +22,8 @@ import Direction from './Direction'; import {MapViewHandle} from './MapViewTypes'; import PendingMapView from './PendingMapView'; import responder from './responder'; +import {ComponentProps, MapViewOnyxProps} from './types'; import utils from './utils'; -import { ComponentProps, MapViewOnyxProps } from './types'; const MapView = forwardRef( ( diff --git a/src/components/MapView/types.ts b/src/components/MapView/types.ts index 2253e9018be1..2c8b9240c445 100644 --- a/src/components/MapView/types.ts +++ b/src/components/MapView/types.ts @@ -3,12 +3,9 @@ import * as OnyxTypes from '@src/types/onyx'; import {MapViewProps} from './MapViewTypes'; type MapViewOnyxProps = { - userLocation: OnyxEntry; + userLocation: OnyxEntry; }; type ComponentProps = MapViewProps & MapViewOnyxProps; -export type { - MapViewOnyxProps, - ComponentProps -} +export type {MapViewOnyxProps, ComponentProps}; From a86094559f697d01e06b3424e2a3aa1f9a2aa09d Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 6 Nov 2023 09:35:56 +0100 Subject: [PATCH 14/17] Auto map panning for native --- src/components/MapView/MapView.tsx | 260 ++++++++++++++--------------- 1 file changed, 128 insertions(+), 132 deletions(-) diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index 1facc5dae242..db3e076eacca 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -2,140 +2,139 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native'; import Mapbox, {MapState, MarkerView, setAccessToken} from '@rnmapbox/maps'; import {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import setUserLocation from '@libs/actions/UserLocation'; +import compose from '@libs/compose'; +import getCurrentPosition from '@libs/getCurrentPosition'; import styles from '@styles/styles'; import CONST from '@src/CONST'; import useLocalize from '@src/hooks/useLocalize'; import useNetwork from '@src/hooks/useNetwork'; -import { withOnyx } from 'react-native-onyx'; -import compose from '@libs/compose'; import ONYXKEYS from '@src/ONYXKEYS'; -import PendingMapView from './PendingMapView'; import Direction from './Direction'; import {MapViewHandle} from './MapViewTypes'; +import PendingMapView from './PendingMapView'; import responder from './responder'; +import {ComponentProps, MapViewOnyxProps} from './types'; import utils from './utils'; -import { ComponentProps, MapViewOnyxProps } from './types'; -import getCurrentPosition from '@libs/getCurrentPosition'; -import setUserLocation from '@libs/actions/UserLocation'; -import Text from '@components/Text'; - -const MapView = forwardRef(({accessToken, style, mapPadding, userLocation: cachedUserLocation, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady}, ref) => { - const navigation = useNavigation(); - const {isOffline} = useNetwork(); - const {translate} = useLocalize(); - - const [logger, setLogger] = useState(''); - const cameraRef = useRef(null); - const [isIdle, setIsIdle] = useState(false); - const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); - const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); - - useFocusEffect( - useCallback(() => { - if (isOffline) { + +const MapView = forwardRef( + ({accessToken, style, mapPadding, userLocation: cachedUserLocation, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady}, ref) => { + const navigation = useNavigation(); + const {isOffline} = useNetwork(); + const {translate} = useLocalize(); + + const cameraRef = useRef(null); + const [isIdle, setIsIdle] = useState(false); + const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); + const [userInteractedWithMap, setUserInteractedWithMap] = useState(false); + + useFocusEffect( + useCallback(() => { + if (isOffline) { + return; + } + + getCurrentPosition( + (params) => { + const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; + setCurrentPosition(currentCoords); + setUserLocation(currentCoords); + }, + () => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (cachedUserLocation || !initialState) { + return; + } + + setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); + }, + ); + }, [cachedUserLocation, initialState, isOffline]), + ); + + // 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]); + + useEffect(() => { + if (!currentPosition || !cameraRef.current) { return; } - getCurrentPosition( - (params) => { - const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude}; - setCurrentPosition(currentCoords); - setUserLocation(currentCoords); - }, - () => { - if (cachedUserLocation || !initialState) { - return; - } - - setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]}); - } - ) - }, []) - ) - - // 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]); - - useEffect(() => { - if (!currentPosition || !cameraRef.current) { - return; - } - - if (!shouldPanMapToCurrentPosition()) { - return; - } - - cameraRef.current.setCamera({ - zoomLevel: CONST.MAPBOX.DEFAULT_ZOOM, - animationDuration: 1500, - centerCoordinate: [currentPosition.longitude, currentPosition.latitude], - }); - }, [currentPosition, cameraRef.current, shouldPanMapToCurrentPosition]); - - useImperativeHandle( - ref, - () => ({ - flyTo: (location: [number, number], zoomLevel: number = CONST.MAPBOX.DEFAULT_ZOOM, animationDuration?: number) => - cameraRef.current?.setCamera({zoomLevel, centerCoordinate: location, animationDuration}), - fitBounds: (northEast: [number, number], southWest: [number, number], paddingConfig?: number | number[] | undefined, animationDuration?: number | undefined) => - cameraRef.current?.fitBounds(northEast, southWest, paddingConfig, animationDuration), - }), - [], - ); - - // When the page loses focus, we temporarily set the "idled" state to false. - // When the page regains focus, the onIdled method of the map will set the actual "idled" state, - // which in turn triggers the callback. - useFocusEffect( - useCallback(() => { - if (!waypoints || waypoints.length === 0 || !isIdle) { + if (!shouldPanMapToCurrentPosition()) { return; } - if (waypoints.length === 1) { - cameraRef.current?.setCamera({ - zoomLevel: 15, - animationDuration: 1500, - centerCoordinate: waypoints[0].coordinate, - }); - } else { - const {southWest, northEast} = utils.getBounds( - waypoints.map((waypoint) => waypoint.coordinate), - directionCoordinates, - ); - cameraRef.current?.fitBounds(northEast, southWest, mapPadding, 1000); + cameraRef.current.setCamera({ + zoomLevel: CONST.MAPBOX.DEFAULT_ZOOM, + animationDuration: 1500, + centerCoordinate: [currentPosition.longitude, currentPosition.latitude], + }); + }, [currentPosition, shouldPanMapToCurrentPosition]); + + useImperativeHandle( + ref, + () => ({ + flyTo: (location: [number, number], zoomLevel: number = CONST.MAPBOX.DEFAULT_ZOOM, animationDuration?: number) => + cameraRef.current?.setCamera({zoomLevel, centerCoordinate: location, animationDuration}), + fitBounds: (northEast: [number, number], southWest: [number, number], paddingConfig?: number | number[] | undefined, animationDuration?: number | undefined) => + cameraRef.current?.fitBounds(northEast, southWest, paddingConfig, animationDuration), + }), + [], + ); + + // When the page loses focus, we temporarily set the "idled" state to false. + // When the page regains focus, the onIdled method of the map will set the actual "idled" state, + // which in turn triggers the callback. + useFocusEffect( + useCallback(() => { + if (!waypoints || waypoints.length === 0 || !isIdle) { + return; + } + + if (waypoints.length === 1) { + cameraRef.current?.setCamera({ + zoomLevel: 15, + animationDuration: 1500, + centerCoordinate: waypoints[0].coordinate, + }); + } else { + const {southWest, northEast} = utils.getBounds( + waypoints.map((waypoint) => waypoint.coordinate), + directionCoordinates, + ); + cameraRef.current?.fitBounds(northEast, southWest, mapPadding, 1000); + } + }, [mapPadding, waypoints, isIdle, directionCoordinates]), + ); + + useEffect(() => { + const unsubscribe = navigation.addListener('blur', () => { + setIsIdle(false); + }); + return unsubscribe; + }, [navigation]); + + useEffect(() => { + setAccessToken(accessToken); + }, [accessToken]); + + const setMapIdle = (e: MapState) => { + if (e.gestures.isGestureActive) { + return; } - }, [mapPadding, waypoints, isIdle, directionCoordinates]), - ); - - useEffect(() => { - const unsubscribe = navigation.addListener('blur', () => { - setIsIdle(false); - }); - return unsubscribe; - }, [navigation]); - - useEffect(() => { - setAccessToken(accessToken); - }, [accessToken]); - - const setMapIdle = (e: MapState) => { - if (e.gestures.isGestureActive) { - return; - } - setIsIdle(true); - if (onMapReady) { - onMapReady(); - } - }; - - return ( - <> - {!isOffline && Boolean(accessToken) && Boolean(currentPosition) ? ( - <> + setIsIdle(true); + if (onMapReady) { + onMapReady(); + } + }; + + return ( + <> + {!isOffline && Boolean(accessToken) && Boolean(currentPosition) ? ( (({accessToken, style, {directionCoordinates && } - - {logger} - - - ): ( - - )} - - ); -}); + ) : ( + + )} + + ); + }, +); export default compose( withOnyx({ From 59ba483433ac4a190f12e668831b5402ccba3638 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 6 Nov 2023 10:00:42 +0100 Subject: [PATCH 15/17] Add type definitions to useLocalize that is pending TS migration --- src/hooks/useLocalize.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/hooks/useLocalize.js b/src/hooks/useLocalize.js index 71968cdb6e61..7b810eae63d9 100644 --- a/src/hooks/useLocalize.js +++ b/src/hooks/useLocalize.js @@ -1,6 +1,24 @@ import {useContext} from 'react'; import {LocaleContext} from '@components/LocaleContextProvider'; +/** + * @typedef {Object} LocalizationContext + * @property {(phrase: string, variables?: object) => string} translate Translates a provided phrase using the preferred locale and optional variables. + * @property {(number: number, options: Intl.NumberFormatOptions) => string} numberFormat Formats a provided number using the preferred locale and optional format options. + * @property {(datetime: string) => string} datetimeToRelative Converts an ISO-formatted datetime to a relative time string in the preferred locale. + * @property {(datetime: string, includeTimezone? boolean, isLowercase: boolean) => string} datetimeToCalendarTime Converts an ISO-formatted datetime to a calendar time string in the preferred locale. Optional includeTimezone and isLowercase. + * @property {() => void} updateLocale Updates internal date-fns locale to the user's preferred locale. + * @property {(phoneNumber: string) => string} formatPhoneNumber Formats given phoneNumber. + * @property {(digit: string) => string} toLocaleDigit Converts the provided digit to the locale digit. + * @property {(localeDigit: string) => string} fromLocaleDigit Reverses the operation of `toLocaleDigit`, taking a locale-specific digit and returning the equivalent in the standard number system. + * @property {string} preferredLocale The preferred locale value. + */ + +/** + * Hook to access the localization context which provides multiple utility functions and locale. + * + * @returns {LocalizationContext} The localization context + */ export default function useLocalize() { return useContext(LocaleContext); } From e9815870f2a4ced4b21e85441e135ba5d1c4dbba Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Wed, 8 Nov 2023 16:12:02 +0100 Subject: [PATCH 16/17] Reorder import --- src/components/MapView/MapView.web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx index 4cb9db36dc46..1aed425ec6ea 100644 --- a/src/components/MapView/MapView.web.tsx +++ b/src/components/MapView/MapView.web.tsx @@ -19,12 +19,12 @@ import getCurrentPosition from '@src/libs/getCurrentPosition'; import ONYXKEYS from '@src/ONYXKEYS'; import styles from '@src/styles/styles'; import Direction from './Direction'; +import './mapbox.css'; import {MapViewHandle} from './MapViewTypes'; import PendingMapView from './PendingMapView'; import responder from './responder'; import {ComponentProps, MapViewOnyxProps} from './types'; import utils from './utils'; -import './mapbox.css'; const MapView = forwardRef( ( From 76e093bbf7acf439b490a07d6e960f3c95b8d541 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Thu, 23 Nov 2023 20:39:34 +0100 Subject: [PATCH 17/17] Restore isRequired on accessToken --- src/components/DistanceMapView/distanceMapViewPropTypes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DistanceMapView/distanceMapViewPropTypes.js b/src/components/DistanceMapView/distanceMapViewPropTypes.js index 89aca61510e1..05068cbc9b34 100644 --- a/src/components/DistanceMapView/distanceMapViewPropTypes.js +++ b/src/components/DistanceMapView/distanceMapViewPropTypes.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; const propTypes = { // Public access token to be used to fetch map data from Mapbox. - accessToken: PropTypes.string, + accessToken: PropTypes.string.isRequired, // Style applied to MapView component. Note some of the View Style props are not available on ViewMap style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),