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:
Kerry 2022-03-09 18:14:07 +01:00 committed by GitHub
parent 288e47fd81
commit 14684c6296
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 541 additions and 145 deletions

View file

@ -15,12 +15,11 @@ limitations under the License.
*/
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 { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import { ClientEvent, IClientWellKnown } from 'matrix-js-sdk/src/client';
import DialogButtons from "../elements/DialogButtons";
import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import MemberAvatar from '../avatars/MemberAvatar';
@ -29,15 +28,26 @@ import Modal from '../../../Modal';
import ErrorDialog from '../dialogs/ErrorDialog';
import { findMapStyleUrl } from '../messages/MLocationBody';
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 {
sender: RoomMember;
shareType: LocationShareType;
onChoose(uri: string, ts: number): unknown;
onFinished(ev?: SyntheticEvent): void;
}
interface IPosition {
latitude: number;
longitude: number;
altitude?: number;
accuracy?: number;
timestamp: number;
}
interface IState {
position?: GeolocationPosition;
position?: IPosition;
error: Error;
}
@ -88,15 +98,8 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
},
trackUserLocation: true,
});
this.map.addControl(this.geolocate);
this.marker = new maplibregl.Marker({
element: document.getElementById(this.getMarkerId()),
anchor: 'bottom',
offset: [0, -1],
})
.setLngLat(new maplibregl.LngLat(0, 0))
.addTo(this.map);
this.map.addControl(this.geolocate);
this.map.on('error', (e) => {
logger.error(
@ -112,7 +115,18 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
});
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) {
logger.error("Failed to render map", e);
this.setState({ error: e });
@ -122,9 +136,19 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
componentWillUnmount() {
this.geolocate?.off('error', this.onGeolocateError);
this.geolocate?.off('geolocate', this.onGeolocate);
this.map?.off('click', this.onClick);
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) => {
const style = tileServerFromWellKnown(clientWellKnown)?.["map_style_url"];
if (style) {
@ -133,7 +157,10 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
};
private onGeolocate = (position: GeolocationPosition) => {
this.setState({ position });
if (!this.marker) {
this.addMarkerToMap();
}
this.setState({ position: genericPositionFromGeolocation(position) });
this.marker?.setLngLat(
new maplibregl.LngLat(
position.coords.longitude,
@ -142,18 +169,40 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
);
};
private onGeolocateError = (e: GeolocationPositionError) => {
this.props.onFinished();
logger.error("Could not fetch location", e);
Modal.createTrackedDialog(
'Could not fetch location',
'',
ErrorDialog,
{
title: _t("Could not fetch location"),
description: positionFailureMessage(e.code),
private onClick = (event: MapMouseEvent) => {
if (!this.marker) {
this.addMarkerToMap();
}
this.marker?.setLngLat(event.lngLat);
this.setState({
position: {
timestamp: Date.now(),
latitude: event.lngLat.lat,
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 = () => {
@ -165,33 +214,46 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
render() {
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") }
</div> : null;
return (
<div className="mx_LocationPicker">
<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 }
<div className="mx_LocationPicker_footer">
<form onSubmit={this.onOk}>
<DialogButtons
primaryButton={_t('Share location')}
primaryIsSubmit={true}
onPrimaryButtonClick={this.onOk}
hasCancel={false}
primaryDisabled={!this.state.position}
/>
<AccessibleButton
data-test-id="location-picker-submit-button"
type="submit"
element='button'
kind='primary'
className='mx_LocationPicker_submitButton'
disabled={!this.state.position}
onClick={this.onOk}>
{ _t('Share location') }
</AccessibleButton>
</form>
</div>
<div className="mx_MLocationBody_marker" id={this.getMarkerId()}>
<div className="mx_MLocationBody_markerBorder">
<MemberAvatar
member={this.props.sender}
width={27}
height={27}
viewUserOnClick={false}
/>
{ this.props.shareType === LocationShareType.Own ?
<MemberAvatar
member={this.props.sender}
width={27}
height={27}
viewUserOnClick={false}
/>
: <LocationIcon className="mx_MLocationBody_markerIcon" />
}
</div>
<div
className="mx_MLocationBody_pointer"
@ -202,17 +264,27 @@ class LocationPicker extends React.Component<ILocationPickerProps, IState> {
}
}
export function getGeoUri(position: GeolocationPosition): string {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
const genericPositionFromGeolocation = (geoPosition: GeolocationPosition): IPosition => {
const {
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 = (
Number.isFinite(position.coords.altitude)
? `,${position.coords.altitude}`
Number.isFinite(position.altitude)
? `,${position.altitude}`
: ""
);
const acc = (
Number.isFinite(position.coords.accuracy)
? `;u=${ position.coords.accuracy }`
Number.isFinite(position.accuracy)
? `;u=${position.accuracy}`
: ""
);
return `geo:${lat},${lon}${alt}${acc}`;

View file

@ -23,10 +23,11 @@ import ContextMenu, { AboveLeftOf } from '../../structures/ContextMenu';
import LocationPicker, { ILocationPickerProps } from "./LocationPicker";
import { shareLocation } from './shareLocation';
import SettingsStore from '../../../settings/SettingsStore';
import ShareType, { LocationShareType } from './ShareType';
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;
menuPosition: AboveLeftOf;
openMenu: () => void;
@ -70,7 +71,8 @@ const LocationShareMenu: React.FC<Props> = ({
<div className="mx_LocationShareMenu">
{ shareType ? <LocationPicker
sender={sender}
onChoose={shareLocation(matrixClient, roomId, relation, openMenu)}
shareType={shareType}
onChoose={shareLocation(matrixClient, roomId, shareType, relation, openMenu)}
onFinished={onFinished}
/>
:

View file

@ -25,6 +25,7 @@ import AccessibleButton from '../elements/AccessibleButton';
import Heading from '../typography/Heading';
import { Icon as LocationIcon } from '../../../../res/img/element-icons/location.svg';
import { Icon as LiveLocationIcon } from '../../../../res/img/location/live-location.svg';
import { LocationShareType } from './shareLocation';
const UserAvatar = () => {
const matrixClient = useContext(MatrixClientContext);
@ -48,12 +49,6 @@ const UserAvatar = () => {
</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 };
const ShareTypeOption: React.FC<ShareTypeOptionProps> = ({
onClick, label, shareType, ...rest
@ -62,7 +57,7 @@ const ShareTypeOption: React.FC<ShareTypeOptionProps> = ({
className='mx_ShareType_option'
onClick={onClick}
// not yet implemented
disabled={shareType !== LocationShareType.Own}
disabled={shareType === LocationShareType.Live}
{...rest}>
{ shareType === LocationShareType.Own && <UserAvatar /> }
{ shareType === LocationShareType.Pin &&

View file

@ -19,15 +19,23 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
import { makeLocationContent } from "matrix-js-sdk/src/content-helpers";
import { logger } from "matrix-js-sdk/src/logger";
import { IEventRelation } from "matrix-js-sdk/src/models/event";
import { LocationAssetType } from "matrix-js-sdk/src/@types/location";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import QuestionDialog from "../dialogs/QuestionDialog";
import SdkConfig from "../../../SdkConfig";
export enum LocationShareType {
Own = 'Own',
Pin = 'Pin',
Live = 'Live'
}
export const shareLocation = (
client: MatrixClient,
roomId: string,
shareType: LocationShareType,
relation: IEventRelation | undefined,
openMenu: () => void,
) => async (uri: string, ts: number) => {
@ -35,7 +43,8 @@ export const shareLocation = (
try {
const text = textForLocation(uri, ts, 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) {
logger.error("We couldn't send your location", e);