Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit f70186e

Browse files
author
Kerry
authored
Live location sharing: set map bounds to include all locations (#8324)
* open a dialog with map centered around first beacon Signed-off-by: Kerry Archibald <[email protected]> * test dialog opening from beacon body Signed-off-by: Kerry Archibald <[email protected]> * test beaconmarker Signed-off-by: Kerry Archibald <[email protected]> * add bounds to Map comp Signed-off-by: Kerry Archibald <[email protected]> * add focusBeacon to beaconviewdialog, use bounds Signed-off-by: Kerry Archibald <[email protected]> * lint Signed-off-by: Kerry Archibald <[email protected]> * use membercolor on beacon view markers Signed-off-by: Kerry Archibald <[email protected]> * add lnglatbounds to maplibre mock Signed-off-by: Kerry Archibald <[email protected]> * update snapshots for expanded maplibre Map mock Signed-off-by: Kerry Archibald <[email protected]> * test map bounds Signed-off-by: Kerry Archibald <[email protected]> * tidy copy paste comment Signed-off-by: Kerry Archibald <[email protected]> * add fallback when no more live locations Signed-off-by: Kerry Archibald <[email protected]> * accurate signature for getBoundsCenter Signed-off-by: Kerry Archibald <[email protected]>
1 parent 6b13988 commit f70186e

File tree

16 files changed

+246
-21
lines changed

16 files changed

+246
-21
lines changed

__mocks__/maplibre-gl.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const EventEmitter = require("events");
2-
const { LngLat, NavigationControl } = require('maplibre-gl');
2+
const { LngLat, NavigationControl, LngLatBounds } = require('maplibre-gl');
33

44
class MockMap extends EventEmitter {
55
addControl = jest.fn();
@@ -8,6 +8,7 @@ class MockMap extends EventEmitter {
88
zoomOut = jest.fn();
99
setCenter = jest.fn();
1010
setStyle = jest.fn();
11+
fitBounds = jest.fn();
1112
}
1213
const MockMapInstance = new MockMap();
1314

@@ -24,5 +25,6 @@ module.exports = {
2425
GeolocateControl: jest.fn().mockReturnValue(MockGeolocateInstance),
2526
Marker: jest.fn().mockReturnValue(MockMarker),
2627
LngLat,
28+
LngLatBounds,
2729
NavigationControl,
2830
};

res/css/components/views/beacon/_BeaconViewDialog.scss

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,25 @@ limitations under the License.
5555
height: 80vh;
5656
border-radius: 8px;
5757
}
58+
59+
.mx_BeaconViewDialog_mapFallback {
60+
box-sizing: border-box;
61+
display: flex;
62+
flex-direction: column;
63+
justify-content: center;
64+
align-items: center;
65+
66+
background: url('$(res)/img/location/map.svg');
67+
background-size: cover;
68+
}
69+
70+
.mx_BeaconViewDialog_mapFallbackIcon {
71+
width: 65px;
72+
margin-bottom: $spacing-16;
73+
color: $quaternary-content;
74+
}
75+
76+
.mx_BeaconViewDialog_mapFallbackMessage {
77+
color: $secondary-content;
78+
margin-bottom: $spacing-16;
79+
}

src/components/views/beacon/BeaconMarker.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const BeaconMarker: React.FC<Props> = ({ map, beacon }) => {
5858
id={beacon.identifier}
5959
geoUri={geoUri}
6060
roomMember={markerRoomMember}
61+
useMemberColor
6162
/>;
6263
};
6364

src/components/views/beacon/BeaconViewDialog.tsx

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,29 +29,43 @@ import { IDialogProps } from "../dialogs/IDialogProps";
2929
import Map from '../location/Map';
3030
import ZoomButtons from '../location/ZoomButtons';
3131
import BeaconMarker from './BeaconMarker';
32+
import { Bounds, getBeaconBounds } from '../../../utils/beacon/bounds';
33+
import { getGeoUri } from '../../../utils/beacon';
34+
import { Icon as LocationIcon } from '../../../../res/img/element-icons/location.svg';
35+
import { _t } from '../../../languageHandler';
36+
import AccessibleButton from '../elements/AccessibleButton';
3237

3338
interface IProps extends IDialogProps {
3439
roomId: Room['roomId'];
3540
matrixClient: MatrixClient;
41+
// open the map centered on this beacon's location
42+
focusBeacon?: Beacon;
3643
}
3744

38-
// TODO actual center is coming soon
39-
// for now just center around first beacon in list
40-
const getMapCenterUri = (beacons: Beacon[]): string => {
41-
const firstBeaconWithLocation = beacons.find(beacon => beacon.latestLocationState);
42-
43-
return firstBeaconWithLocation?.latestLocationState?.uri;
45+
const getBoundsCenter = (bounds: Bounds): string | undefined => {
46+
if (!bounds) {
47+
return;
48+
}
49+
return getGeoUri({
50+
latitude: (bounds.north + bounds.south) / 2,
51+
longitude: (bounds.east + bounds.west) / 2,
52+
timestamp: Date.now(),
53+
});
4454
};
4555

4656
/**
4757
* Dialog to view live beacons maximised
4858
*/
49-
const BeaconViewDialog: React.FC<IProps> = ({ roomId, matrixClient, onFinished }) => {
59+
const BeaconViewDialog: React.FC<IProps> = ({
60+
focusBeacon,
61+
roomId,
62+
matrixClient,
63+
onFinished,
64+
}) => {
5065
const liveBeacons = useLiveBeacons(roomId, matrixClient);
5166

52-
const mapCenterUri = getMapCenterUri(liveBeacons);
53-
// TODO probably show loader or placeholder when there is no location
54-
// to center the map on
67+
const bounds = getBeaconBounds(liveBeacons);
68+
const centerGeoUri = focusBeacon?.latestLocationState?.uri || getBoundsCenter(bounds);
5569

5670
return (
5771
<BaseDialog
@@ -60,9 +74,10 @@ const BeaconViewDialog: React.FC<IProps> = ({ roomId, matrixClient, onFinished }
6074
fixedWidth={false}
6175
>
6276
<MatrixClientContext.Provider value={matrixClient}>
63-
<Map
77+
{ !!bounds ? <Map
6478
id='mx_BeaconViewDialog'
65-
centerGeoUri={mapCenterUri}
79+
bounds={bounds}
80+
centerGeoUri={centerGeoUri}
6681
interactive
6782
className="mx_BeaconViewDialog_map"
6883
>
@@ -77,7 +92,22 @@ const BeaconViewDialog: React.FC<IProps> = ({ roomId, matrixClient, onFinished }
7792
<ZoomButtons map={map} />
7893
</>
7994
}
80-
</Map>
95+
</Map> :
96+
<div
97+
data-test-id='beacon-view-dialog-map-fallback'
98+
className='mx_BeaconViewDialog_map mx_BeaconViewDialog_mapFallback'
99+
>
100+
<LocationIcon className='mx_BeaconViewDialog_mapFallbackIcon' />
101+
<span className='mx_BeaconViewDialog_mapFallbackMessage'>{ _t('No live locations') }</span>
102+
<AccessibleButton
103+
kind='primary'
104+
onClick={onFinished}
105+
data-test-id='beacon-view-dialog-fallback-close'
106+
>
107+
{ _t('Close') }
108+
</AccessibleButton>
109+
</div>
110+
}
81111
</MatrixClientContext.Provider>
82112
</BaseDialog>
83113
);

src/components/views/location/Map.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ limitations under the License.
1616

1717
import React, { ReactNode, useContext, useEffect } from 'react';
1818
import classNames from 'classnames';
19+
import maplibregl from 'maplibre-gl';
1920
import { ClientEvent, IClientWellKnown } from 'matrix-js-sdk/src/matrix';
2021
import { logger } from 'matrix-js-sdk/src/logger';
2122

@@ -24,8 +25,9 @@ import { useEventEmitterState } from '../../../hooks/useEventEmitter';
2425
import { parseGeoUri } from '../../../utils/location';
2526
import { tileServerFromWellKnown } from '../../../utils/WellKnownUtils';
2627
import { useMap } from '../../../utils/location/useMap';
28+
import { Bounds } from '../../../utils/beacon/bounds';
2729

28-
const useMapWithStyle = ({ id, centerGeoUri, onError, interactive }) => {
30+
const useMapWithStyle = ({ id, centerGeoUri, onError, interactive, bounds }) => {
2931
const bodyId = `mx_Map_${id}`;
3032

3133
// style config
@@ -55,6 +57,20 @@ const useMapWithStyle = ({ id, centerGeoUri, onError, interactive }) => {
5557
}
5658
}, [map, centerGeoUri]);
5759

60+
useEffect(() => {
61+
if (map && bounds) {
62+
try {
63+
const lngLatBounds = new maplibregl.LngLatBounds(
64+
[bounds.west, bounds.south],
65+
[bounds.east, bounds.north],
66+
);
67+
map.fitBounds(lngLatBounds, { padding: 100 });
68+
} catch (error) {
69+
logger.error('Invalid map bounds', error);
70+
}
71+
}
72+
}, [map, bounds]);
73+
5874
return {
5975
map,
6076
bodyId,
@@ -65,6 +81,7 @@ interface MapProps {
6581
id: string;
6682
interactive?: boolean;
6783
centerGeoUri?: string;
84+
bounds?: Bounds;
6885
className?: string;
6986
onClick?: () => void;
7087
onError?: (error: Error) => void;
@@ -74,9 +91,15 @@ interface MapProps {
7491
}
7592

7693
const Map: React.FC<MapProps> = ({
77-
centerGeoUri, className, id, onError, onClick, children, interactive,
94+
bounds,
95+
centerGeoUri,
96+
children,
97+
className,
98+
id,
99+
interactive,
100+
onError, onClick,
78101
}) => {
79-
const { map, bodyId } = useMapWithStyle({ centerGeoUri, onError, id, interactive });
102+
const { map, bodyId } = useMapWithStyle({ centerGeoUri, onError, id, interactive, bounds });
80103

81104
const onMapClick = (
82105
event: React.MouseEvent<HTMLDivElement, MouseEvent>,

src/components/views/messages/MBeaconBody.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ import Spinner from '../elements/Spinner';
3232
import Map from '../location/Map';
3333
import SmartMarker from '../location/SmartMarker';
3434
import OwnBeaconStatus from '../beacon/OwnBeaconStatus';
35-
import { IBodyProps } from "./IBodyProps";
3635
import BeaconViewDialog from '../beacon/BeaconViewDialog';
36+
import { IBodyProps } from "./IBodyProps";
3737

3838
const useBeaconState = (beaconInfoEvent: MatrixEvent): {
3939
beacon?: Beacon;
@@ -105,6 +105,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent }, ref) =>
105105
{
106106
roomId: mxEvent.getRoomId(),
107107
matrixClient,
108+
focusBeacon: beacon,
108109
},
109110
"mx_BeaconViewDialog_wrapper",
110111
false, // isPriority

src/i18n/strings/en_EN.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2915,6 +2915,7 @@
29152915
"Loading live location...": "Loading live location...",
29162916
"Live location ended": "Live location ended",
29172917
"Live location error": "Live location error",
2918+
"No live locations": "No live locations",
29182919
"An error occured whilst sharing your live location": "An error occured whilst sharing your live location",
29192920
"You are sharing your live location": "You are sharing your live location",
29202921
"%(timeRemaining)s left": "%(timeRemaining)s left",

test/components/views/beacon/BeaconViewDialog-test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626

2727
import BeaconViewDialog from '../../../../src/components/views/beacon/BeaconViewDialog';
2828
import {
29+
findByTestId,
2930
getMockClientWithEventEmitter,
3031
makeBeaconEvent,
3132
makeBeaconInfoEvent,
@@ -118,4 +119,37 @@ describe('<BeaconViewDialog />', () => {
118119
// two markers now!
119120
expect(component.find('BeaconMarker').length).toEqual(2);
120121
});
122+
123+
it('renders a fallback when no live beacons remain', () => {
124+
const onFinished = jest.fn();
125+
const room = makeRoomWithStateEvents([defaultEvent]);
126+
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent));
127+
beacon.addLocations([location1]);
128+
const component = getComponent({ onFinished });
129+
expect(component.find('BeaconMarker').length).toEqual(1);
130+
131+
// this will replace the defaultEvent
132+
// leading to no more live beacons
133+
const anotherBeaconEvent = makeBeaconInfoEvent(aliceId,
134+
roomId,
135+
{ isLive: false },
136+
'$bob-room1-1',
137+
);
138+
139+
act(() => {
140+
// emits RoomStateEvent.BeaconLiveness
141+
room.currentState.setStateEvents([anotherBeaconEvent]);
142+
});
143+
144+
component.setProps({});
145+
146+
// map placeholder
147+
expect(findByTestId(component, 'beacon-view-dialog-map-fallback')).toMatchSnapshot();
148+
149+
act(() => {
150+
findByTestId(component, 'beacon-view-dialog-fallback-close').at(0).simulate('click');
151+
});
152+
153+
expect(onFinished).toHaveBeenCalled();
154+
});
121155
});

test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ exports[`<BeaconMarker /> renders marker when beacon has location 1`] = `
6161
"_eventsCount": 0,
6262
"_maxListeners": undefined,
6363
"addControl": [MockFunction],
64+
"fitBounds": [MockFunction],
6465
"removeControl": [MockFunction],
6566
"setCenter": [MockFunction],
6667
"setStyle": [MockFunction],
@@ -79,6 +80,7 @@ exports[`<BeaconMarker /> renders marker when beacon has location 1`] = `
7980
"_eventsCount": 0,
8081
"_maxListeners": undefined,
8182
"addControl": [MockFunction],
83+
"fitBounds": [MockFunction],
8284
"removeControl": [MockFunction],
8385
"setCenter": [MockFunction],
8486
"setStyle": [MockFunction],
@@ -111,6 +113,7 @@ exports[`<BeaconMarker /> renders marker when beacon has location 1`] = `
111113
Symbol(kCapture): false,
112114
}
113115
}
116+
useMemberColor={true}
114117
>
115118
<span>
116119
<ForwardRef
@@ -139,9 +142,10 @@ exports[`<BeaconMarker /> renders marker when beacon has location 1`] = `
139142
Symbol(kCapture): false,
140143
}
141144
}
145+
useMemberColor={true}
142146
>
143147
<div
144-
className="mx_Marker mx_Marker_defaultColor"
148+
className="mx_Marker mx_Username_color4"
145149
id="!room:server_@alice:server"
146150
>
147151
<div
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`<BeaconViewDialog /> renders a fallback when no live beacons remain 1`] = `
4+
<div
5+
className="mx_BeaconViewDialog_map mx_BeaconViewDialog_mapFallback"
6+
data-test-id="beacon-view-dialog-map-fallback"
7+
>
8+
<div
9+
className="mx_BeaconViewDialog_mapFallbackIcon"
10+
/>
11+
<span
12+
className="mx_BeaconViewDialog_mapFallbackMessage"
13+
>
14+
No live locations
15+
</span>
16+
<AccessibleButton
17+
data-test-id="beacon-view-dialog-fallback-close"
18+
element="div"
19+
kind="primary"
20+
onClick={[MockFunction]}
21+
role="button"
22+
tabIndex={0}
23+
>
24+
<div
25+
className="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
26+
data-test-id="beacon-view-dialog-fallback-close"
27+
onClick={[MockFunction]}
28+
onKeyDown={[Function]}
29+
onKeyUp={[Function]}
30+
role="button"
31+
tabIndex={0}
32+
>
33+
Close
34+
</div>
35+
</AccessibleButton>
36+
</div>
37+
`;

0 commit comments

Comments
 (0)