Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically pan initial map based on current location #30506

Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f911a33
Auto map panning when location rights granted
MaciejSWM Oct 27, 2023
6b25276
Code cleanup - helper method; drop console logs
MaciejSWM Oct 27, 2023
111921a
Drop Location loading indicator
MaciejSWM Oct 30, 2023
4f15636
Move PendingMapView directly to MapView
MaciejSWM Oct 30, 2023
9bf7242
Merge branch 'main' into automatically-pan-initial-map-based-on-user-…
MaciejSWM Oct 30, 2023
88dc1a7
Drop duplicate import
MaciejSWM Oct 30, 2023
b6362d4
Blank lines
MaciejSWM Oct 30, 2023
103eed2
Linter and typecheck
MaciejSWM Oct 30, 2023
c6e2501
Set user location action
MaciejSWM Oct 30, 2023
86d4468
Move import down
MaciejSWM Oct 30, 2023
85cc951
Handle offline mode and cached locations
MaciejSWM Oct 30, 2023
a664110
Auto map panning for mobile
MaciejSWM Oct 31, 2023
b868abc
Merge branch 'main' into automatically-pan-initial-map-based-on-user-…
MaciejSWM Nov 2, 2023
1c4f727
Drop unused const
MaciejSWM Nov 2, 2023
fe7170f
Merge branch 'main' into automatically-pan-initial-map-based-on-user-…
MaciejSWM Nov 2, 2023
099fbaa
Merge branch 'main' into automatically-pan-initial-map-based-on-user-…
MaciejSWM Nov 6, 2023
5754b00
Prettier
MaciejSWM Nov 6, 2023
a860945
Auto map panning for native
MaciejSWM Nov 6, 2023
59ba483
Add type definitions to useLocalize that is pending TS migration
MaciejSWM Nov 6, 2023
49978a9
Merge branch 'main' into automatically-pan-initial-map-based-on-user-…
MaciejSWM Nov 7, 2023
7899315
Merge branch 'main' into automatically-pan-initial-map-based-on-user-…
MaciejSWM Nov 8, 2023
e981587
Reorder import
MaciejSWM Nov 8, 2023
98394d2
Merge branch 'main' into automatically-pan-initial-map-based-on-user-…
MaciejSWM Nov 20, 2023
184e4b7
Merge branch 'main' into automatically-pan-initial-map-based-on-user-…
MaciejSWM Nov 23, 2023
76e093b
Restore isRequired on accessToken
MaciejSWM Nov 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',

Expand Down Expand Up @@ -336,6 +339,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[];
Expand Down
2 changes: 1 addition & 1 deletion src/components/DistanceMapView/distanceMapViewPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
MaciejSWM marked this conversation as resolved.
Show resolved Hide resolved

// 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]),
Expand Down
39 changes: 14 additions & 25 deletions src/components/DistanceRequest/DistanceRequestFooter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -55,7 +53,6 @@ const defaultProps = {
transaction: {},
};
function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navigateToWaypointEditPage}) {
const {isOffline} = useNetwork();
const {translate} = useLocalize();

const numberOfWaypoints = _.size(waypoints);
Expand Down Expand Up @@ -109,28 +106,20 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig
/>
</View>
<View style={styles.mapViewContainer}>
{!isOffline && Boolean(mapboxAccessToken.token) ? (
<DistanceMapView
accessToken={mapboxAccessToken.token}
mapPadding={CONST.MAPBOX.PADDING}
pitchEnabled={false}
initialState={{
zoom: CONST.MAPBOX.DEFAULT_ZOOM,
location: lodashGet(waypointMarkers, [0, 'coordinate'], CONST.MAPBOX.DEFAULT_COORDINATE),
}}
directionCoordinates={lodashGet(transaction, 'routes.route0.geometry.coordinates', [])}
style={[styles.mapView, styles.mapEditView]}
waypoints={waypointMarkers}
styleURL={CONST.MAPBOX.STYLE_URL}
overlayStyle={styles.mapEditView}
/>
) : (
<PendingMapView
title={translate('distance.mapPending.title')}
subtitle={isOffline ? translate('distance.mapPending.subtitle') : translate('distance.mapPending.onlineSubtitle')}
style={styles.mapEditView}
/>
)}
<DistanceMapView
accessToken={mapboxAccessToken.token}
mapPadding={CONST.MAPBOX.PADDING}
pitchEnabled={false}
initialState={{
zoom: CONST.MAPBOX.DEFAULT_ZOOM,
location: lodashGet(waypointMarkers, [0, 'coordinate'], CONST.MAPBOX.DEFAULT_COORDINATE),
}}
directionCoordinates={lodashGet(transaction, 'routes.route0.geometry.coordinates', [])}
style={[styles.mapView, styles.mapEditView]}
waypoints={waypointMarkers}
styleURL={CONST.MAPBOX.STYLE_URL}
overlayStyle={styles.mapEditView}
/>
</View>
</>
);
Expand Down
253 changes: 167 additions & 86 deletions src/components/MapView/MapView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,99 @@ 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 ONYXKEYS from '@src/ONYXKEYS';
import Direction from './Direction';
import {MapViewHandle, MapViewProps} from './MapViewTypes';
import {MapViewHandle} from './MapViewTypes';
import PendingMapView from './PendingMapView';
import responder from './responder';
import {ComponentProps, MapViewOnyxProps} from './types';
import utils from './utils';

const MapView = forwardRef<MapViewHandle, MapViewProps>(({accessToken, style, mapPadding, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady}, ref) => {
const cameraRef = useRef<Mapbox.Camera>(null);
const [isIdle, setIsIdle] = useState(false);
const navigation = useNavigation();

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(
// eslint-disable-next-line rulesdir/prefer-early-return
useCallback(() => {
if (waypoints?.length && isIdle) {
const MapView = forwardRef<MapViewHandle, ComponentProps>(
({accessToken, style, mapPadding, userLocation: cachedUserLocation, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady}, ref) => {
const navigation = useNavigation();
const {isOffline} = useNetwork();
const {translate} = useLocalize();

const cameraRef = useRef<Mapbox.Camera>(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;
}

if (!shouldPanMapToCurrentPosition()) {
return;
}

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,
Expand All @@ -45,69 +108,87 @@ const MapView = forwardRef<MapViewHandle, MapViewProps>(({accessToken, style, ma
);
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;
}
setIsIdle(true);
if (onMapReady) {
onMapReady();
}
}, [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 (
<View style={style}>
<Mapbox.MapView
style={{flex: 1}}
styleURL={styleURL}
onMapIdle={setMapIdle}
pitchEnabled={pitchEnabled}
attributionPosition={{...styles.r2, ...styles.b2}}
scaleBarEnabled={false}
logoPosition={{...styles.l2, ...styles.b2}}
// eslint-disable-next-line
{...responder.panHandlers}
>
<Mapbox.Camera
ref={cameraRef}
defaultSettings={{
centerCoordinate: initialState?.location,
zoomLevel: initialState?.zoom,
}}
/>

{waypoints?.map(({coordinate, markerComponent, id}) => {
const MarkerComponent = markerComponent;
return (
<MarkerView
id={id}
key={id}
coordinate={coordinate}
};

return (
<>
{!isOffline && Boolean(accessToken) && Boolean(currentPosition) ? (
<View style={style}>
<Mapbox.MapView
style={{flex: 1}}
styleURL={styleURL}
onMapIdle={setMapIdle}
onTouchStart={() => 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}
>
<MarkerComponent />
</MarkerView>
);
})}
<Mapbox.Camera
ref={cameraRef}
defaultSettings={{
centerCoordinate: currentPosition ? [currentPosition.longitude, currentPosition.latitude] : initialState?.location,
zoomLevel: initialState?.zoom,
}}
/>

{waypoints?.map(({coordinate, markerComponent, id}) => {
const MarkerComponent = markerComponent;
return (
<MarkerView
id={id}
key={id}
coordinate={coordinate}
>
<MarkerComponent />
</MarkerView>
);
})}

{directionCoordinates && <Direction coordinates={directionCoordinates} />}
</Mapbox.MapView>
</View>
);
});
{directionCoordinates && <Direction coordinates={directionCoordinates} />}
</Mapbox.MapView>
</View>
) : (
<PendingMapView
title={translate('distance.mapPending.title')}
subtitle={isOffline ? translate('distance.mapPending.subtitle') : translate('distance.mapPending.onlineSubtitle')}
style={styles.mapEditView}
/>
)}
</>
);
},
);

export default memo(MapView);
export default compose(
withOnyx<ComponentProps, MapViewOnyxProps>({
userLocation: {
key: ONYXKEYS.USER_LOCATION,
},
}),
memo,
)(MapView);
Loading
Loading