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
|
@ -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}`;
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
:
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue