Skip to content

Commit

Permalink
Merge pull request #30506 from software-mansion-labs/automatically-pa…
Browse files Browse the repository at this point in the history
…n-initial-map-based-on-user-location
  • Loading branch information
thienlnam authored Nov 27, 2023
2 parents 84c3b7a + 76e093b commit 1950104
Show file tree
Hide file tree
Showing 8 changed files with 336 additions and 148 deletions.
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 @@ -372,6 +375,7 @@ type OnyxValues = {
[ONYXKEYS.COUNTRY_CODE]: number;
[ONYXKEYS.COUNTRY]: string;
[ONYXKEYS.USER]: OnyxTypes.User;
[ONYXKEYS.USER_LOCATION]: OnyxTypes.UserLocation;
[ONYXKEYS.LOGIN_LIST]: Record<string, OnyxTypes.Login>;
[ONYXKEYS.SESSION]: OnyxTypes.Session;
[ONYXKEYS.BETAS]: OnyxTypes.Beta[];
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 useTheme from '@styles/themes/useTheme';
import useThemeStyles from '@styles/useThemeStyles';
Expand Down Expand Up @@ -57,7 +55,6 @@ const defaultProps = {
function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navigateToWaypointEditPage}) {
const theme = useTheme();
const styles = useThemeStyles();
const {isOffline} = useNetwork();
const {translate} = useLocalize();

const numberOfWaypoints = _.size(waypoints);
Expand Down Expand Up @@ -114,28 +111,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

0 comments on commit 1950104

Please sign in to comment.