Send pin drop location share events (#7967)
* center icon better Signed-off-by: Kerry Archibald <kerrya@element.io> * remove debug Signed-off-by: Kerry Archibald <kerrya@element.io> * retrigger all builds Signed-off-by: Kerry Archibald <kerrya@element.io> * set assetType on share event Signed-off-by: Kerry Archibald <kerrya@element.io> * use pin marker on map for pin drop share Signed-off-by: Kerry Archibald <kerrya@element.io> * lint Signed-off-by: Kerry Archibald <kerrya@element.io> * test events Signed-off-by: Kerry Archibald <kerrya@element.io> * pin drop helper text Signed-off-by: Kerry Archibald <kerrya@element.io> * use generic location type Signed-off-by: Kerry Archibald <kerrya@element.io> * add navigationcontrol when in pin mode Signed-off-by: Kerry Archibald <kerrya@element.io> * allow pin drop without location permissions Signed-off-by: Kerry Archibald <kerrya@element.io> * remove geolocate control when pin dropping without geo perms Signed-off-by: Kerry Archibald <kerrya@element.io> * test locationpicker Signed-off-by: Kerry Archibald <kerrya@element.io> * test marker type, tidy Signed-off-by: Kerry Archibald <kerrya@element.io> * tweak style Signed-off-by: Kerry Archibald <kerrya@element.io> * lint Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
parent
288e47fd81
commit
14684c6296
9 changed files with 541 additions and 145 deletions
|
@ -1,20 +1,23 @@
|
||||||
const EventEmitter = require("events");
|
const EventEmitter = require("events");
|
||||||
const { LngLat } = require('maplibre-gl');
|
const { LngLat, NavigationControl } = require('maplibre-gl');
|
||||||
|
|
||||||
class MockMap extends EventEmitter {
|
class MockMap extends EventEmitter {
|
||||||
addControl = jest.fn();
|
addControl = jest.fn();
|
||||||
removeControl = jest.fn();
|
removeControl = jest.fn();
|
||||||
}
|
}
|
||||||
class MockGeolocateControl extends EventEmitter {
|
const MockMapInstance = new MockMap();
|
||||||
|
|
||||||
|
class MockGeolocateControl extends EventEmitter {
|
||||||
|
trigger = jest.fn();
|
||||||
}
|
}
|
||||||
class MockMarker extends EventEmitter {
|
const MockGeolocateInstance = new MockGeolocateControl();
|
||||||
setLngLat = jest.fn().mockReturnValue(this);
|
const MockMarker = {}
|
||||||
addTo = jest.fn();
|
MockMarker.setLngLat = jest.fn().mockReturnValue(MockMarker);
|
||||||
}
|
MockMarker.addTo = jest.fn().mockReturnValue(MockMarker);
|
||||||
module.exports = {
|
module.exports = {
|
||||||
Map: MockMap,
|
Map: jest.fn().mockReturnValue(MockMapInstance),
|
||||||
GeolocateControl: MockGeolocateControl,
|
GeolocateControl: jest.fn().mockReturnValue(MockGeolocateInstance),
|
||||||
Marker: MockMarker,
|
Marker: jest.fn().mockReturnValue(MockMarker),
|
||||||
LngLat,
|
LngLat,
|
||||||
|
NavigationControl
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,21 +19,25 @@ limitations under the License.
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
#mx_LocationPicker_map {
|
#mx_LocationPicker_map {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.maplibregl-ctrl.maplibregl-ctrl-group,
|
||||||
|
.maplibregl-ctrl.maplibregl-ctrl-attrib {
|
||||||
|
margin-right: $spacing-16;
|
||||||
|
}
|
||||||
|
|
||||||
.maplibregl-ctrl.maplibregl-ctrl-group {
|
.maplibregl-ctrl.maplibregl-ctrl-group {
|
||||||
// place below the close button
|
// place below the close button
|
||||||
// padding-16 + 24px close button + padding-10
|
// padding-16 + 24px close button + padding-10
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
margin-right: $spacing-16;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.maplibregl-ctrl-bottom-right {
|
.maplibregl-ctrl-bottom-right {
|
||||||
bottom: 68px;
|
bottom: 80px;
|
||||||
margin-right: $spacing-16;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.maplibregl-user-location-accuracy-circle {
|
.maplibregl-user-location-accuracy-circle {
|
||||||
|
@ -51,10 +55,9 @@ limitations under the License.
|
||||||
background-color: $accent;
|
background-color: $accent;
|
||||||
filter: drop-shadow(0px 3px 5px rgba(0, 0, 0, 0.2));
|
filter: drop-shadow(0px 3px 5px rgba(0, 0, 0, 0.2));
|
||||||
|
|
||||||
.mx_BaseAvatar {
|
display: flex;
|
||||||
margin-top: 2px;
|
align-items: center;
|
||||||
margin-left: 2px;
|
justify-content: center;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MLocationBody_pointer {
|
.mx_MLocationBody_pointer {
|
||||||
|
@ -83,19 +86,13 @@ limitations under the License.
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: $spacing-16;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
.mx_Dialog_buttons {
|
background-color: $header-panel-bg-color;
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
/* Note the `button` prefix and `not()` clauses are needed to make
|
|
||||||
these selectors more specific than those in _common.scss. */
|
|
||||||
|
|
||||||
button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton) {
|
|
||||||
margin: 0px 0px 16px 0px;
|
|
||||||
min-width: 328px;
|
|
||||||
min-height: 48px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LocationPicker_error {
|
.mx_LocationPicker_error {
|
||||||
|
@ -103,3 +100,33 @@ limitations under the License.
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_MLocationBody_markerIcon {
|
||||||
|
color: white;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LocationPicker_pinText {
|
||||||
|
position: absolute;
|
||||||
|
top: $spacing-16;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: center;
|
||||||
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
span {
|
||||||
|
box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: $spacing-8;
|
||||||
|
background-color: $background;
|
||||||
|
color: $primary-content;
|
||||||
|
|
||||||
|
font-size: $font-12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LocationPicker_submitButton {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
|
@ -15,12 +15,11 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { SyntheticEvent } from 'react';
|
import React, { SyntheticEvent } from 'react';
|
||||||
import maplibregl from 'maplibre-gl';
|
import maplibregl, { MapMouseEvent } from 'maplibre-gl';
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||||
import { ClientEvent, IClientWellKnown } from 'matrix-js-sdk/src/client';
|
import { ClientEvent, IClientWellKnown } from 'matrix-js-sdk/src/client';
|
||||||
|
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import MemberAvatar from '../avatars/MemberAvatar';
|
import MemberAvatar from '../avatars/MemberAvatar';
|
||||||
|
@ -29,15 +28,26 @@ import Modal from '../../../Modal';
|
||||||
import ErrorDialog from '../dialogs/ErrorDialog';
|
import ErrorDialog from '../dialogs/ErrorDialog';
|
||||||
import { findMapStyleUrl } from '../messages/MLocationBody';
|
import { findMapStyleUrl } from '../messages/MLocationBody';
|
||||||
import { tileServerFromWellKnown } from '../../../utils/WellKnownUtils';
|
import { tileServerFromWellKnown } from '../../../utils/WellKnownUtils';
|
||||||
|
import { LocationShareType } from './shareLocation';
|
||||||
|
import { Icon as LocationIcon } from '../../../../res/img/element-icons/location.svg';
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
|
||||||
export interface ILocationPickerProps {
|
export interface ILocationPickerProps {
|
||||||
sender: RoomMember;
|
sender: RoomMember;
|
||||||
|
shareType: LocationShareType;
|
||||||
onChoose(uri: string, ts: number): unknown;
|
onChoose(uri: string, ts: number): unknown;
|
||||||
onFinished(ev?: SyntheticEvent): void;
|
onFinished(ev?: SyntheticEvent): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IPosition {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
altitude?: number;
|
||||||
|
accuracy?: number;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
interface IState {
|
interface IState {
|
||||||
position?: GeolocationPosition;
|
position?: IPosition;
|
||||||
error: Error;
|
error: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,15 +98,8 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
|
||||||
},
|
},
|
||||||
trackUserLocation: true,
|
trackUserLocation: true,
|
||||||
});
|
});
|
||||||
this.map.addControl(this.geolocate);
|
|
||||||
|
|
||||||
this.marker = new maplibregl.Marker({
|
this.map.addControl(this.geolocate);
|
||||||
element: document.getElementById(this.getMarkerId()),
|
|
||||||
anchor: 'bottom',
|
|
||||||
offset: [0, -1],
|
|
||||||
})
|
|
||||||
.setLngLat(new maplibregl.LngLat(0, 0))
|
|
||||||
.addTo(this.map);
|
|
||||||
|
|
||||||
this.map.on('error', (e) => {
|
this.map.on('error', (e) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@ -112,7 +115,18 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.geolocate.on('error', this.onGeolocateError);
|
this.geolocate.on('error', this.onGeolocateError);
|
||||||
this.geolocate.on('geolocate', this.onGeolocate);
|
|
||||||
|
if (this.props.shareType === LocationShareType.Own) {
|
||||||
|
this.geolocate.on('geolocate', this.onGeolocate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.shareType === LocationShareType.Pin) {
|
||||||
|
const navigationControl = new maplibregl.NavigationControl({
|
||||||
|
showCompass: false, showZoom: true,
|
||||||
|
});
|
||||||
|
this.map.addControl(navigationControl, 'bottom-right');
|
||||||
|
this.map.on('click', this.onClick);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Failed to render map", e);
|
logger.error("Failed to render map", e);
|
||||||
this.setState({ error: e });
|
this.setState({ error: e });
|
||||||
|
@ -122,9 +136,19 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.geolocate?.off('error', this.onGeolocateError);
|
this.geolocate?.off('error', this.onGeolocateError);
|
||||||
this.geolocate?.off('geolocate', this.onGeolocate);
|
this.geolocate?.off('geolocate', this.onGeolocate);
|
||||||
|
this.map?.off('click', this.onClick);
|
||||||
this.context.off(ClientEvent.ClientWellKnown, this.updateStyleUrl);
|
this.context.off(ClientEvent.ClientWellKnown, this.updateStyleUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addMarkerToMap = () => {
|
||||||
|
this.marker = new maplibregl.Marker({
|
||||||
|
element: document.getElementById(this.getMarkerId()),
|
||||||
|
anchor: 'bottom',
|
||||||
|
offset: [0, -1],
|
||||||
|
}).setLngLat(new maplibregl.LngLat(0, 0))
|
||||||
|
.addTo(this.map);
|
||||||
|
};
|
||||||
|
|
||||||
private updateStyleUrl = (clientWellKnown: IClientWellKnown) => {
|
private updateStyleUrl = (clientWellKnown: IClientWellKnown) => {
|
||||||
const style = tileServerFromWellKnown(clientWellKnown)?.["map_style_url"];
|
const style = tileServerFromWellKnown(clientWellKnown)?.["map_style_url"];
|
||||||
if (style) {
|
if (style) {
|
||||||
|
@ -133,7 +157,10 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onGeolocate = (position: GeolocationPosition) => {
|
private onGeolocate = (position: GeolocationPosition) => {
|
||||||
this.setState({ position });
|
if (!this.marker) {
|
||||||
|
this.addMarkerToMap();
|
||||||
|
}
|
||||||
|
this.setState({ position: genericPositionFromGeolocation(position) });
|
||||||
this.marker?.setLngLat(
|
this.marker?.setLngLat(
|
||||||
new maplibregl.LngLat(
|
new maplibregl.LngLat(
|
||||||
position.coords.longitude,
|
position.coords.longitude,
|
||||||
|
@ -142,18 +169,40 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onGeolocateError = (e: GeolocationPositionError) => {
|
private onClick = (event: MapMouseEvent) => {
|
||||||
this.props.onFinished();
|
if (!this.marker) {
|
||||||
logger.error("Could not fetch location", e);
|
this.addMarkerToMap();
|
||||||
Modal.createTrackedDialog(
|
}
|
||||||
'Could not fetch location',
|
this.marker?.setLngLat(event.lngLat);
|
||||||
'',
|
this.setState({
|
||||||
ErrorDialog,
|
position: {
|
||||||
{
|
timestamp: Date.now(),
|
||||||
title: _t("Could not fetch location"),
|
latitude: event.lngLat.lat,
|
||||||
description: positionFailureMessage(e.code),
|
longitude: event.lngLat.lng,
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onGeolocateError = (e: GeolocationPositionError) => {
|
||||||
|
logger.error("Could not fetch location", e);
|
||||||
|
// close the dialog and show an error when trying to share own location
|
||||||
|
// pin drop location without permissions is ok
|
||||||
|
if (this.props.shareType === LocationShareType.Own) {
|
||||||
|
this.props.onFinished();
|
||||||
|
Modal.createTrackedDialog(
|
||||||
|
'Could not fetch location',
|
||||||
|
'',
|
||||||
|
ErrorDialog,
|
||||||
|
{
|
||||||
|
title: _t("Could not fetch location"),
|
||||||
|
description: positionFailureMessage(e.code),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.geolocate) {
|
||||||
|
this.map?.removeControl(this.geolocate);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onOk = () => {
|
private onOk = () => {
|
||||||
|
@ -165,33 +214,46 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const error = this.state.error ?
|
const error = this.state.error ?
|
||||||
<div className="mx_LocationPicker_error">
|
<div data-test-id='location-picker-error' className="mx_LocationPicker_error">
|
||||||
{ _t("Failed to load map") }
|
{ _t("Failed to load map") }
|
||||||
</div> : null;
|
</div> : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_LocationPicker">
|
<div className="mx_LocationPicker">
|
||||||
<div id="mx_LocationPicker_map" />
|
<div id="mx_LocationPicker_map" />
|
||||||
|
{ this.props.shareType === LocationShareType.Pin && <div className="mx_LocationPicker_pinText">
|
||||||
|
<span>
|
||||||
|
{ this.state.position ? _t("Click to move the pin") : _t("Click to drop a pin") }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
{ error }
|
{ error }
|
||||||
<div className="mx_LocationPicker_footer">
|
<div className="mx_LocationPicker_footer">
|
||||||
<form onSubmit={this.onOk}>
|
<form onSubmit={this.onOk}>
|
||||||
<DialogButtons
|
|
||||||
primaryButton={_t('Share location')}
|
<AccessibleButton
|
||||||
primaryIsSubmit={true}
|
data-test-id="location-picker-submit-button"
|
||||||
onPrimaryButtonClick={this.onOk}
|
type="submit"
|
||||||
hasCancel={false}
|
element='button'
|
||||||
primaryDisabled={!this.state.position}
|
kind='primary'
|
||||||
/>
|
className='mx_LocationPicker_submitButton'
|
||||||
|
disabled={!this.state.position}
|
||||||
|
onClick={this.onOk}>
|
||||||
|
{ _t('Share location') }
|
||||||
|
</AccessibleButton>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_MLocationBody_marker" id={this.getMarkerId()}>
|
<div className="mx_MLocationBody_marker" id={this.getMarkerId()}>
|
||||||
<div className="mx_MLocationBody_markerBorder">
|
<div className="mx_MLocationBody_markerBorder">
|
||||||
<MemberAvatar
|
{ this.props.shareType === LocationShareType.Own ?
|
||||||
member={this.props.sender}
|
<MemberAvatar
|
||||||
width={27}
|
member={this.props.sender}
|
||||||
height={27}
|
width={27}
|
||||||
viewUserOnClick={false}
|
height={27}
|
||||||
/>
|
viewUserOnClick={false}
|
||||||
|
/>
|
||||||
|
: <LocationIcon className="mx_MLocationBody_markerIcon" />
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="mx_MLocationBody_pointer"
|
className="mx_MLocationBody_pointer"
|
||||||
|
@ -202,17 +264,27 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGeoUri(position: GeolocationPosition): string {
|
const genericPositionFromGeolocation = (geoPosition: GeolocationPosition): IPosition => {
|
||||||
const lat = position.coords.latitude;
|
const {
|
||||||
const lon = position.coords.longitude;
|
latitude, longitude, altitude, accuracy,
|
||||||
|
} = geoPosition.coords;
|
||||||
|
return {
|
||||||
|
timestamp: geoPosition.timestamp,
|
||||||
|
latitude, longitude, altitude, accuracy,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getGeoUri(position: IPosition): string {
|
||||||
|
const lat = position.latitude;
|
||||||
|
const lon = position.longitude;
|
||||||
const alt = (
|
const alt = (
|
||||||
Number.isFinite(position.coords.altitude)
|
Number.isFinite(position.altitude)
|
||||||
? `,${position.coords.altitude}`
|
? `,${position.altitude}`
|
||||||
: ""
|
: ""
|
||||||
);
|
);
|
||||||
const acc = (
|
const acc = (
|
||||||
Number.isFinite(position.coords.accuracy)
|
Number.isFinite(position.accuracy)
|
||||||
? `;u=${ position.coords.accuracy }`
|
? `;u=${position.accuracy}`
|
||||||
: ""
|
: ""
|
||||||
);
|
);
|
||||||
return `geo:${lat},${lon}${alt}${acc}`;
|
return `geo:${lat},${lon}${alt}${acc}`;
|
||||||
|
|
|
@ -23,10 +23,11 @@ import ContextMenu, { AboveLeftOf } from '../../structures/ContextMenu';
|
||||||
import LocationPicker, { ILocationPickerProps } from "./LocationPicker";
|
import LocationPicker, { ILocationPickerProps } from "./LocationPicker";
|
||||||
import { shareLocation } from './shareLocation';
|
import { shareLocation } from './shareLocation';
|
||||||
import SettingsStore from '../../../settings/SettingsStore';
|
import SettingsStore from '../../../settings/SettingsStore';
|
||||||
import ShareType, { LocationShareType } from './ShareType';
|
|
||||||
import ShareDialogButtons from './ShareDialogButtons';
|
import ShareDialogButtons from './ShareDialogButtons';
|
||||||
|
import ShareType from './ShareType';
|
||||||
|
import { LocationShareType } from './shareLocation';
|
||||||
|
|
||||||
type Props = Omit<ILocationPickerProps, 'onChoose'> & {
|
type Props = Omit<ILocationPickerProps, 'onChoose' | 'shareType'> & {
|
||||||
onFinished: (ev?: SyntheticEvent) => void;
|
onFinished: (ev?: SyntheticEvent) => void;
|
||||||
menuPosition: AboveLeftOf;
|
menuPosition: AboveLeftOf;
|
||||||
openMenu: () => void;
|
openMenu: () => void;
|
||||||
|
@ -70,7 +71,8 @@ const LocationShareMenu: React.FC<Props> = ({
|
||||||
<div className="mx_LocationShareMenu">
|
<div className="mx_LocationShareMenu">
|
||||||
{ shareType ? <LocationPicker
|
{ shareType ? <LocationPicker
|
||||||
sender={sender}
|
sender={sender}
|
||||||
onChoose={shareLocation(matrixClient, roomId, relation, openMenu)}
|
shareType={shareType}
|
||||||
|
onChoose={shareLocation(matrixClient, roomId, shareType, relation, openMenu)}
|
||||||
onFinished={onFinished}
|
onFinished={onFinished}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
|
|
|
@ -25,6 +25,7 @@ import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import Heading from '../typography/Heading';
|
import Heading from '../typography/Heading';
|
||||||
import { Icon as LocationIcon } from '../../../../res/img/element-icons/location.svg';
|
import { Icon as LocationIcon } from '../../../../res/img/element-icons/location.svg';
|
||||||
import { Icon as LiveLocationIcon } from '../../../../res/img/location/live-location.svg';
|
import { Icon as LiveLocationIcon } from '../../../../res/img/location/live-location.svg';
|
||||||
|
import { LocationShareType } from './shareLocation';
|
||||||
|
|
||||||
const UserAvatar = () => {
|
const UserAvatar = () => {
|
||||||
const matrixClient = useContext(MatrixClientContext);
|
const matrixClient = useContext(MatrixClientContext);
|
||||||
|
@ -48,12 +49,6 @@ const UserAvatar = () => {
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO this will be defined somewhere better
|
|
||||||
export enum LocationShareType {
|
|
||||||
Own = 'Own',
|
|
||||||
Pin = 'Pin',
|
|
||||||
Live = 'Live'
|
|
||||||
}
|
|
||||||
type ShareTypeOptionProps = HTMLAttributes<Element> & { label: string, shareType: LocationShareType };
|
type ShareTypeOptionProps = HTMLAttributes<Element> & { label: string, shareType: LocationShareType };
|
||||||
const ShareTypeOption: React.FC<ShareTypeOptionProps> = ({
|
const ShareTypeOption: React.FC<ShareTypeOptionProps> = ({
|
||||||
onClick, label, shareType, ...rest
|
onClick, label, shareType, ...rest
|
||||||
|
@ -62,7 +57,7 @@ const ShareTypeOption: React.FC<ShareTypeOptionProps> = ({
|
||||||
className='mx_ShareType_option'
|
className='mx_ShareType_option'
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
// not yet implemented
|
// not yet implemented
|
||||||
disabled={shareType !== LocationShareType.Own}
|
disabled={shareType === LocationShareType.Live}
|
||||||
{...rest}>
|
{...rest}>
|
||||||
{ shareType === LocationShareType.Own && <UserAvatar /> }
|
{ shareType === LocationShareType.Own && <UserAvatar /> }
|
||||||
{ shareType === LocationShareType.Pin &&
|
{ shareType === LocationShareType.Pin &&
|
||||||
|
|
|
@ -19,15 +19,23 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { makeLocationContent } from "matrix-js-sdk/src/content-helpers";
|
import { makeLocationContent } from "matrix-js-sdk/src/content-helpers";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { IEventRelation } from "matrix-js-sdk/src/models/event";
|
import { IEventRelation } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { LocationAssetType } from "matrix-js-sdk/src/@types/location";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
|
|
||||||
|
export enum LocationShareType {
|
||||||
|
Own = 'Own',
|
||||||
|
Pin = 'Pin',
|
||||||
|
Live = 'Live'
|
||||||
|
}
|
||||||
|
|
||||||
export const shareLocation = (
|
export const shareLocation = (
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
roomId: string,
|
roomId: string,
|
||||||
|
shareType: LocationShareType,
|
||||||
relation: IEventRelation | undefined,
|
relation: IEventRelation | undefined,
|
||||||
openMenu: () => void,
|
openMenu: () => void,
|
||||||
) => async (uri: string, ts: number) => {
|
) => async (uri: string, ts: number) => {
|
||||||
|
@ -35,7 +43,8 @@ export const shareLocation = (
|
||||||
try {
|
try {
|
||||||
const text = textForLocation(uri, ts, null);
|
const text = textForLocation(uri, ts, null);
|
||||||
const threadId = relation?.rel_type === RelationType.Thread ? relation.event_id : null;
|
const threadId = relation?.rel_type === RelationType.Thread ? relation.event_id : null;
|
||||||
await client.sendMessage(roomId, threadId, makeLocationContent(text, uri, ts, null));
|
const assetType = shareType === LocationShareType.Pin ? LocationAssetType.Pin : LocationAssetType.Self;
|
||||||
|
await client.sendMessage(roomId, threadId, makeLocationContent(text, uri, ts, null, assetType));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("We couldn't send your location", e);
|
logger.error("We couldn't send your location", e);
|
||||||
|
|
||||||
|
|
|
@ -2175,6 +2175,8 @@
|
||||||
"toggle event": "toggle event",
|
"toggle event": "toggle event",
|
||||||
"Location": "Location",
|
"Location": "Location",
|
||||||
"Could not fetch location": "Could not fetch location",
|
"Could not fetch location": "Could not fetch location",
|
||||||
|
"Click to move the pin": "Click to move the pin",
|
||||||
|
"Click to drop a pin": "Click to drop a pin",
|
||||||
"Share location": "Share location",
|
"Share location": "Share location",
|
||||||
"Element was denied permission to fetch your location. Please allow location access in your browser settings.": "Element was denied permission to fetch your location. Please allow location access in your browser settings.",
|
"Element was denied permission to fetch your location. Please allow location access in your browser settings.": "Element was denied permission to fetch your location. Please allow location access in your browser settings.",
|
||||||
"Failed to fetch your location. Please try again later.": "Failed to fetch your location. Please try again later.",
|
"Failed to fetch your location. Please try again later.": "Failed to fetch your location. Please try again later.",
|
||||||
|
|
|
@ -13,90 +13,305 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import maplibregl from "maplibre-gl";
|
||||||
|
import { mount } from "enzyme";
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
|
import { mocked } from 'jest-mock';
|
||||||
|
import { logger } from 'matrix-js-sdk/src/logger';
|
||||||
|
|
||||||
import "../../../skinned-sdk"; // Must be first for skinning to work
|
import "../../../skinned-sdk"; // Must be first for skinning to work
|
||||||
import { getGeoUri } from "../../../../src/components/views/location/LocationPicker";
|
import LocationPicker, { getGeoUri } from "../../../../src/components/views/location/LocationPicker";
|
||||||
|
import { LocationShareType } from "../../../../src/components/views/location/shareLocation";
|
||||||
|
import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
|
||||||
|
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
|
||||||
|
import { findByTestId } from '../../../test-utils';
|
||||||
|
|
||||||
|
jest.mock('../../../../src/components/views/messages/MLocationBody', () => ({
|
||||||
|
findMapStyleUrl: jest.fn().mockReturnValue('tileserver.com'),
|
||||||
|
}));
|
||||||
|
|
||||||
describe("LocationPicker", () => {
|
describe("LocationPicker", () => {
|
||||||
describe("getGeoUri", () => {
|
describe("getGeoUri", () => {
|
||||||
it("Renders a URI with only lat and lon", () => {
|
it("Renders a URI with only lat and lon", () => {
|
||||||
const pos: GeolocationPosition = {
|
const pos = {
|
||||||
coords: {
|
latitude: 43.2,
|
||||||
latitude: 43.2,
|
longitude: 12.4,
|
||||||
longitude: 12.4,
|
altitude: undefined,
|
||||||
altitude: undefined,
|
accuracy: undefined,
|
||||||
accuracy: undefined,
|
|
||||||
altitudeAccuracy: undefined,
|
|
||||||
heading: undefined,
|
|
||||||
speed: undefined,
|
|
||||||
},
|
|
||||||
timestamp: 12334,
|
timestamp: 12334,
|
||||||
};
|
};
|
||||||
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4");
|
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Nulls in location are not shown in URI", () => {
|
it("Nulls in location are not shown in URI", () => {
|
||||||
const pos: GeolocationPosition = {
|
const pos = {
|
||||||
coords: {
|
latitude: 43.2,
|
||||||
latitude: 43.2,
|
longitude: 12.4,
|
||||||
longitude: 12.4,
|
altitude: null,
|
||||||
altitude: null,
|
accuracy: null,
|
||||||
accuracy: null,
|
|
||||||
altitudeAccuracy: null,
|
|
||||||
heading: null,
|
|
||||||
speed: null,
|
|
||||||
},
|
|
||||||
timestamp: 12334,
|
timestamp: 12334,
|
||||||
};
|
};
|
||||||
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4");
|
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Renders a URI with 3 coords", () => {
|
it("Renders a URI with 3 coords", () => {
|
||||||
const pos: GeolocationPosition = {
|
const pos = {
|
||||||
coords: {
|
latitude: 43.2,
|
||||||
latitude: 43.2,
|
longitude: 12.4,
|
||||||
longitude: 12.4,
|
altitude: 332.54,
|
||||||
altitude: 332.54,
|
accuracy: undefined,
|
||||||
accuracy: undefined,
|
|
||||||
altitudeAccuracy: undefined,
|
|
||||||
heading: undefined,
|
|
||||||
speed: undefined,
|
|
||||||
},
|
|
||||||
timestamp: 12334,
|
timestamp: 12334,
|
||||||
};
|
};
|
||||||
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4,332.54");
|
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4,332.54");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Renders a URI with accuracy", () => {
|
it("Renders a URI with accuracy", () => {
|
||||||
const pos: GeolocationPosition = {
|
const pos = {
|
||||||
coords: {
|
latitude: 43.2,
|
||||||
latitude: 43.2,
|
longitude: 12.4,
|
||||||
longitude: 12.4,
|
altitude: undefined,
|
||||||
altitude: undefined,
|
accuracy: 21,
|
||||||
accuracy: 21,
|
|
||||||
altitudeAccuracy: undefined,
|
|
||||||
heading: undefined,
|
|
||||||
speed: undefined,
|
|
||||||
},
|
|
||||||
timestamp: 12334,
|
timestamp: 12334,
|
||||||
};
|
};
|
||||||
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4;u=21");
|
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4;u=21");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Renders a URI with accuracy and altitude", () => {
|
it("Renders a URI with accuracy and altitude", () => {
|
||||||
const pos: GeolocationPosition = {
|
const pos = {
|
||||||
coords: {
|
latitude: 43.2,
|
||||||
latitude: 43.2,
|
longitude: 12.4,
|
||||||
longitude: 12.4,
|
altitude: 12.3,
|
||||||
altitude: 12.3,
|
accuracy: 21,
|
||||||
accuracy: 21,
|
|
||||||
altitudeAccuracy: undefined,
|
|
||||||
heading: undefined,
|
|
||||||
speed: undefined,
|
|
||||||
},
|
|
||||||
timestamp: 12334,
|
timestamp: 12334,
|
||||||
};
|
};
|
||||||
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4,12.3;u=21");
|
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4,12.3;u=21");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('<LocationPicker />', () => {
|
||||||
|
const roomId = '!room:server.org';
|
||||||
|
const userId = '@user:server.org';
|
||||||
|
const sender = new RoomMember(roomId, userId);
|
||||||
|
const defaultProps = {
|
||||||
|
sender,
|
||||||
|
shareType: LocationShareType.Own,
|
||||||
|
onChoose: jest.fn(),
|
||||||
|
onFinished: jest.fn(),
|
||||||
|
};
|
||||||
|
const mockClient = {
|
||||||
|
on: jest.fn(),
|
||||||
|
off: jest.fn(),
|
||||||
|
isGuest: jest.fn(),
|
||||||
|
getClientWellKnown: jest.fn(),
|
||||||
|
};
|
||||||
|
const getComponent = (props = {}) => mount(<LocationPicker {...defaultProps} {...props} />, {
|
||||||
|
wrappingComponent: MatrixClientContext.Provider,
|
||||||
|
wrappingComponentProps: { value: mockClient },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockMap = new maplibregl.Map();
|
||||||
|
const mockGeolocate = new maplibregl.GeolocateControl();
|
||||||
|
const mockMarker = new maplibregl.Marker();
|
||||||
|
|
||||||
|
const mockGeolocationPosition = {
|
||||||
|
coords: {
|
||||||
|
latitude: 43.2,
|
||||||
|
longitude: 12.4,
|
||||||
|
altitude: 12.3,
|
||||||
|
accuracy: 21,
|
||||||
|
},
|
||||||
|
timestamp: 123,
|
||||||
|
};
|
||||||
|
const mockClickEvent = {
|
||||||
|
lngLat: {
|
||||||
|
lat: 43.2,
|
||||||
|
lng: 12.4,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(logger, 'error').mockRestore();
|
||||||
|
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient as unknown as MatrixClient);
|
||||||
|
jest.clearAllMocks();
|
||||||
|
mocked(mockMap).addControl.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays error when map emits an error', () => {
|
||||||
|
// suppress expected error log
|
||||||
|
jest.spyOn(logger, 'error').mockImplementation(() => { });
|
||||||
|
const wrapper = getComponent();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
mocked(mockMap).emit('error', { error: 'Something went wrong' });
|
||||||
|
wrapper.setProps({});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findByTestId(wrapper, 'location-picker-error').length).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays error when map setup throws', () => {
|
||||||
|
// suppress expected error log
|
||||||
|
jest.spyOn(logger, 'error').mockImplementation(() => { });
|
||||||
|
|
||||||
|
// throw an error
|
||||||
|
mocked(mockMap).addControl.mockImplementation(() => { throw new Error('oups'); });
|
||||||
|
|
||||||
|
const wrapper = getComponent();
|
||||||
|
wrapper.setProps({});
|
||||||
|
|
||||||
|
expect(findByTestId(wrapper, 'location-picker-error').length).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initiates map with geolocation', () => {
|
||||||
|
getComponent();
|
||||||
|
|
||||||
|
expect(mockMap.addControl).toHaveBeenCalledWith(mockGeolocate);
|
||||||
|
act(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
mocked(mockMap).emit('load');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGeolocate.trigger).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('for Own location share type', () => {
|
||||||
|
it('closes and displays error when geolocation errors', () => {
|
||||||
|
// suppress expected error log
|
||||||
|
jest.spyOn(logger, 'error').mockImplementation(() => { });
|
||||||
|
const onFinished = jest.fn();
|
||||||
|
getComponent({ onFinished });
|
||||||
|
|
||||||
|
expect(mockMap.addControl).toHaveBeenCalledWith(mockGeolocate);
|
||||||
|
act(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
mockMap.emit('load');
|
||||||
|
// @ts-ignore
|
||||||
|
mockGeolocate.emit('error', {});
|
||||||
|
});
|
||||||
|
|
||||||
|
// dialog is closed on error
|
||||||
|
expect(onFinished).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets position on geolocate event', () => {
|
||||||
|
const wrapper = getComponent();
|
||||||
|
act(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
mocked(mockGeolocate).emit('geolocate', mockGeolocationPosition);
|
||||||
|
wrapper.setProps({});
|
||||||
|
});
|
||||||
|
|
||||||
|
// marker added
|
||||||
|
expect(maplibregl.Marker).toHaveBeenCalled();
|
||||||
|
expect(mockMarker.setLngLat).toHaveBeenCalledWith(new maplibregl.LngLat(
|
||||||
|
12.4, 43.2,
|
||||||
|
));
|
||||||
|
// submit button is enabled when position is truthy
|
||||||
|
expect(findByTestId(wrapper, 'location-picker-submit-button').at(0).props().disabled).toBeFalsy();
|
||||||
|
expect(wrapper.find('MemberAvatar').length).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submits location', () => {
|
||||||
|
const onChoose = jest.fn();
|
||||||
|
const wrapper = getComponent({ onChoose });
|
||||||
|
act(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
mocked(mockGeolocate).emit('geolocate', mockGeolocationPosition);
|
||||||
|
// make sure button is enabled
|
||||||
|
wrapper.setProps({});
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
findByTestId(wrapper, 'location-picker-submit-button').at(0).simulate('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
// content of this call is tested in LocationShareMenu-test
|
||||||
|
expect(onChoose).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('for Pin drop location share type', () => {
|
||||||
|
const shareType = LocationShareType.Pin;
|
||||||
|
it('initiates map with geolocation', () => {
|
||||||
|
getComponent({ shareType });
|
||||||
|
|
||||||
|
expect(mockMap.addControl).toHaveBeenCalledWith(mockGeolocate);
|
||||||
|
act(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
mocked(mockMap).emit('load');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGeolocate.trigger).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes geolocation control on geolocation error', () => {
|
||||||
|
// suppress expected error log
|
||||||
|
jest.spyOn(logger, 'error').mockImplementation(() => { });
|
||||||
|
const onFinished = jest.fn();
|
||||||
|
getComponent({ onFinished, shareType });
|
||||||
|
act(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
mockMap.emit('load');
|
||||||
|
// @ts-ignore
|
||||||
|
mockGeolocate.emit('error', {});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockMap.removeControl).toHaveBeenCalledWith(mockGeolocate);
|
||||||
|
// dialog is not closed
|
||||||
|
expect(onFinished).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not set position on geolocate event', () => {
|
||||||
|
getComponent({ shareType });
|
||||||
|
act(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
mocked(mockGeolocate).emit('geolocate', mockGeolocationPosition);
|
||||||
|
});
|
||||||
|
|
||||||
|
// marker added
|
||||||
|
expect(maplibregl.Marker).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets position on click event', () => {
|
||||||
|
const wrapper = getComponent({ shareType });
|
||||||
|
act(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
mocked(mockMap).emit('click', mockClickEvent);
|
||||||
|
wrapper.setProps({});
|
||||||
|
});
|
||||||
|
|
||||||
|
// marker added
|
||||||
|
expect(maplibregl.Marker).toHaveBeenCalled();
|
||||||
|
expect(mockMarker.setLngLat).toHaveBeenCalledWith(new maplibregl.LngLat(
|
||||||
|
12.4, 43.2,
|
||||||
|
));
|
||||||
|
|
||||||
|
// marker is set, icon not avatar
|
||||||
|
expect(wrapper.find('.mx_MLocationBody_markerIcon').length).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submits location', () => {
|
||||||
|
const onChoose = jest.fn();
|
||||||
|
const wrapper = getComponent({ onChoose, shareType });
|
||||||
|
act(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
mocked(mockMap).emit('click', mockClickEvent);
|
||||||
|
wrapper.setProps({});
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
findByTestId(wrapper, 'location-picker-submit-button').at(0).simulate('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
// content of this call is tested in LocationShareMenu-test
|
||||||
|
expect(onChoose).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import { mocked } from 'jest-mock';
|
import { mocked } from 'jest-mock';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { ASSET_NODE_TYPE, LocationAssetType } from 'matrix-js-sdk/src/@types/location';
|
||||||
|
|
||||||
import '../../../skinned-sdk';
|
import '../../../skinned-sdk';
|
||||||
import LocationShareMenu from '../../../../src/components/views/location/LocationShareMenu';
|
import LocationShareMenu from '../../../../src/components/views/location/LocationShareMenu';
|
||||||
|
@ -27,7 +28,7 @@ import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
|
||||||
import { ChevronFace } from '../../../../src/components/structures/ContextMenu';
|
import { ChevronFace } from '../../../../src/components/structures/ContextMenu';
|
||||||
import SettingsStore from '../../../../src/settings/SettingsStore';
|
import SettingsStore from '../../../../src/settings/SettingsStore';
|
||||||
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
|
||||||
import { LocationShareType } from '../../../../src/components/views/location/ShareType';
|
import { LocationShareType } from '../../../../src/components/views/location/shareLocation';
|
||||||
import { findByTestId } from '../../../test-utils';
|
import { findByTestId } from '../../../test-utils';
|
||||||
|
|
||||||
jest.mock('../../../../src/components/views/messages/MLocationBody', () => ({
|
jest.mock('../../../../src/components/views/messages/MLocationBody', () => ({
|
||||||
|
@ -58,6 +59,7 @@ describe('<LocationShareMenu />', () => {
|
||||||
getClientWellKnown: jest.fn().mockResolvedValue({
|
getClientWellKnown: jest.fn().mockResolvedValue({
|
||||||
map_style_url: 'maps.com',
|
map_style_url: 'maps.com',
|
||||||
}),
|
}),
|
||||||
|
sendMessage: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
|
@ -70,6 +72,17 @@ describe('<LocationShareMenu />', () => {
|
||||||
roomId: '!room:server.org',
|
roomId: '!room:server.org',
|
||||||
sender: new RoomMember('!room:server.org', userId),
|
sender: new RoomMember('!room:server.org', userId),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const position = {
|
||||||
|
coords: {
|
||||||
|
latitude: -36.24484561954707,
|
||||||
|
longitude: 175.46884959563613,
|
||||||
|
accuracy: 10,
|
||||||
|
},
|
||||||
|
timestamp: 1646305006802,
|
||||||
|
type: 'geolocate',
|
||||||
|
};
|
||||||
|
|
||||||
const getComponent = (props = {}) =>
|
const getComponent = (props = {}) =>
|
||||||
mount(<LocationShareMenu {...defaultProps} {...props} />, {
|
mount(<LocationShareMenu {...defaultProps} {...props} />, {
|
||||||
wrappingComponent: MatrixClientContext.Provider,
|
wrappingComponent: MatrixClientContext.Provider,
|
||||||
|
@ -81,6 +94,8 @@ describe('<LocationShareMenu />', () => {
|
||||||
(settingName) => settingName === "feature_location_share_pin_drop",
|
(settingName) => settingName === "feature_location_share_pin_drop",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
mockClient.sendMessage.mockClear();
|
||||||
|
|
||||||
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient as unknown as MatrixClient);
|
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient as unknown as MatrixClient);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -88,6 +103,21 @@ describe('<LocationShareMenu />', () => {
|
||||||
findByTestId(component, `share-location-option-${shareType}`);
|
findByTestId(component, `share-location-option-${shareType}`);
|
||||||
const getBackButton = component => findByTestId(component, 'share-dialog-buttons-back');
|
const getBackButton = component => findByTestId(component, 'share-dialog-buttons-back');
|
||||||
const getCancelButton = component => findByTestId(component, 'share-dialog-buttons-cancel');
|
const getCancelButton = component => findByTestId(component, 'share-dialog-buttons-cancel');
|
||||||
|
const getSubmitButton = component => findByTestId(component, 'location-picker-submit-button');
|
||||||
|
const setLocation = (component) => {
|
||||||
|
// set the location
|
||||||
|
const locationPickerInstance = component.find('LocationPicker').instance();
|
||||||
|
act(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
locationPickerInstance.onGeolocate(position);
|
||||||
|
// make sure button gets enabled
|
||||||
|
component.setProps({});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const setShareType = (component, shareType) => act(() => {
|
||||||
|
getShareTypeOption(component, shareType).at(0).simulate('click');
|
||||||
|
component.setProps({});
|
||||||
|
});
|
||||||
|
|
||||||
describe('when only Own share type is enabled', () => {
|
describe('when only Own share type is enabled', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -115,6 +145,28 @@ describe('<LocationShareMenu />', () => {
|
||||||
|
|
||||||
expect(onFinished).toHaveBeenCalled();
|
expect(onFinished).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('creates static own location share event on submission', () => {
|
||||||
|
const onFinished = jest.fn();
|
||||||
|
const component = getComponent({ onFinished });
|
||||||
|
|
||||||
|
setLocation(component);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
getSubmitButton(component).at(0).simulate('click');
|
||||||
|
component.setProps({});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onFinished).toHaveBeenCalled();
|
||||||
|
const [messageRoomId, relation, messageBody] = mockClient.sendMessage.mock.calls[0];
|
||||||
|
expect(messageRoomId).toEqual(defaultProps.roomId);
|
||||||
|
expect(relation).toEqual(null);
|
||||||
|
expect(messageBody).toEqual(expect.objectContaining({
|
||||||
|
[ASSET_NODE_TYPE.name]: {
|
||||||
|
type: LocationAssetType.Self,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with pin drop share type enabled', () => {
|
describe('with pin drop share type enabled', () => {
|
||||||
|
@ -147,11 +199,7 @@ describe('<LocationShareMenu />', () => {
|
||||||
it('selecting own location share type advances to location picker', () => {
|
it('selecting own location share type advances to location picker', () => {
|
||||||
const component = getComponent();
|
const component = getComponent();
|
||||||
|
|
||||||
act(() => {
|
setShareType(component, LocationShareType.Own);
|
||||||
getShareTypeOption(component, LocationShareType.Own).at(0).simulate('click');
|
|
||||||
});
|
|
||||||
|
|
||||||
component.setProps({});
|
|
||||||
|
|
||||||
expect(component.find('LocationPicker').length).toBeTruthy();
|
expect(component.find('LocationPicker').length).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
@ -162,10 +210,7 @@ describe('<LocationShareMenu />', () => {
|
||||||
const component = getComponent({ onFinished });
|
const component = getComponent({ onFinished });
|
||||||
|
|
||||||
// advance to location picker
|
// advance to location picker
|
||||||
act(() => {
|
setShareType(component, LocationShareType.Own);
|
||||||
getShareTypeOption(component, LocationShareType.Own).at(0).simulate('click');
|
|
||||||
component.setProps({});
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(component.find('LocationPicker').length).toBeTruthy();
|
expect(component.find('LocationPicker').length).toBeTruthy();
|
||||||
|
|
||||||
|
@ -177,5 +222,31 @@ describe('<LocationShareMenu />', () => {
|
||||||
// back to share type
|
// back to share type
|
||||||
expect(component.find('ShareType').length).toBeTruthy();
|
expect(component.find('ShareType').length).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('creates pin drop location share event on submission', () => {
|
||||||
|
// feature_location_share_pin_drop is set to enabled by default mocking
|
||||||
|
const onFinished = jest.fn();
|
||||||
|
const component = getComponent({ onFinished });
|
||||||
|
|
||||||
|
// advance to location picker
|
||||||
|
setShareType(component, LocationShareType.Pin);
|
||||||
|
|
||||||
|
setLocation(component);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
getSubmitButton(component).at(0).simulate('click');
|
||||||
|
component.setProps({});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onFinished).toHaveBeenCalled();
|
||||||
|
const [messageRoomId, relation, messageBody] = mockClient.sendMessage.mock.calls[0];
|
||||||
|
expect(messageRoomId).toEqual(defaultProps.roomId);
|
||||||
|
expect(relation).toEqual(null);
|
||||||
|
expect(messageBody).toEqual(expect.objectContaining({
|
||||||
|
[ASSET_NODE_TYPE.name]: {
|
||||||
|
type: LocationAssetType.Pin,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue