From 1397652f526cada5b87cc8dccaa76fa49e39d0cb Mon Sep 17 00:00:00 2001 From: Kerry Date: Fri, 25 Mar 2022 15:36:22 +0100 Subject: [PATCH] Extract location utils from components (#8152) * extract util functions from MLocationBody Signed-off-by: Kerry Archibald * disassemble mlocationbody Signed-off-by: Kerry Archibald * tidy and add copyrights Signed-off-by: Kerry Archibald * move types and utils from components/location to utils Signed-off-by: Kerry Archibald * i18n Signed-off-by: Kerry Archibald * empty line Signed-off-by: Kerry Archibald --- .../context_menus/MessageContextMenu.tsx | 2 +- .../views/location/LocationPicker.tsx | 4 +- .../views/location/LocationViewDialog.tsx | 3 +- src/components/views/location/MapError.tsx | 2 +- .../views/messages/MLocationBody.tsx | 122 +--------- src/i18n/strings/en_EN.json | 4 +- .../location/LocationShareErrors.ts | 2 +- .../location/findMapStyleUrl.ts | 4 +- src/utils/location/index.ts | 21 ++ src/utils/location/locationEventGeoUri.ts | 31 +++ src/utils/location/map.ts | 93 ++++++++ src/utils/location/parseGeoUri.ts | 45 ++++ .../views/location/LocationPicker-test.tsx | 5 +- .../views/location/LocationShareMenu-test.tsx | 2 +- .../views/location/MapError-test.tsx | 2 +- .../views/messages/MLocationBody-test.tsx | 222 +----------------- test/test-utils/location.ts | 50 ++++ .../location/locationEventGeoUri-test.ts | 28 +++ test/utils/location/map-test.ts | 47 ++++ test/utils/location/parseGeoUri-test.ts | 157 +++++++++++++ 20 files changed, 496 insertions(+), 350 deletions(-) rename src/{components/views => utils}/location/LocationShareErrors.ts (96%) rename src/{components/views => utils}/location/findMapStyleUrl.ts (92%) create mode 100644 src/utils/location/index.ts create mode 100644 src/utils/location/locationEventGeoUri.ts create mode 100644 src/utils/location/map.ts create mode 100644 src/utils/location/parseGeoUri.ts create mode 100644 test/test-utils/location.ts create mode 100644 test/utils/location/locationEventGeoUri-test.ts create mode 100644 test/utils/location/map-test.ts create mode 100644 test/utils/location/parseGeoUri-test.ts diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 4f08139840..8899c13a60 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -47,8 +47,8 @@ import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInse import { WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore'; import EndPollDialog from '../dialogs/EndPollDialog'; import { isPollEnded } from '../messages/MPollBody'; -import { createMapSiteLink } from "../messages/MLocationBody"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; +import { createMapSiteLink } from '../../../utils/location'; export function canCancel(status: EventStatus): boolean { return status === EventStatus.QUEUED || status === EventStatus.NOT_SENT || status === EventStatus.ENCRYPTING; diff --git a/src/components/views/location/LocationPicker.tsx b/src/components/views/location/LocationPicker.tsx index b7d51df191..bd93225953 100644 --- a/src/components/views/location/LocationPicker.tsx +++ b/src/components/views/location/LocationPicker.tsx @@ -28,16 +28,16 @@ import MatrixClientContext from '../../../contexts/MatrixClientContext'; import Modal from '../../../Modal'; import ErrorDialog from '../dialogs/ErrorDialog'; import { tileServerFromWellKnown } from '../../../utils/WellKnownUtils'; -import { findMapStyleUrl } from './findMapStyleUrl'; import { LocationShareType, ShareLocationFn } from './shareLocation'; import { Icon as LocationIcon } from '../../../../res/img/element-icons/location.svg'; -import { LocationShareError } from './LocationShareErrors'; import AccessibleButton from '../elements/AccessibleButton'; import { MapError } from './MapError'; import { getUserNameColorClass } from '../../../utils/FormattingUtils'; import LiveDurationDropdown, { DEFAULT_DURATION_MS } from './LiveDurationDropdown'; import { GenericPosition, genericPositionFromGeolocation, getGeoUri } from '../../../utils/beacon'; import SdkConfig from '../../../SdkConfig'; +import { LocationShareError, findMapStyleUrl } from '../../../utils/location'; + export interface ILocationPickerProps { sender: RoomMember; shareType: LocationShareType; diff --git a/src/components/views/location/LocationViewDialog.tsx b/src/components/views/location/LocationViewDialog.tsx index bc40255c21..2c6e154ac0 100644 --- a/src/components/views/location/LocationViewDialog.tsx +++ b/src/components/views/location/LocationViewDialog.tsx @@ -21,8 +21,9 @@ import { ClientEvent, IClientWellKnown, MatrixClient } from 'matrix-js-sdk/src/c import { replaceableComponent } from "../../../utils/replaceableComponent"; import BaseDialog from "../dialogs/BaseDialog"; import { IDialogProps } from "../dialogs/IDialogProps"; -import { createMap, LocationBodyContent, locationEventGeoUri, parseGeoUri } from '../messages/MLocationBody'; +import { LocationBodyContent } from '../messages/MLocationBody'; import { tileServerFromWellKnown } from '../../../utils/WellKnownUtils'; +import { parseGeoUri, locationEventGeoUri, createMap } from '../../../utils/location'; interface IProps extends IDialogProps { matrixClient: MatrixClient; diff --git a/src/components/views/location/MapError.tsx b/src/components/views/location/MapError.tsx index 7ab5d681ab..7bbcbd0b90 100644 --- a/src/components/views/location/MapError.tsx +++ b/src/components/views/location/MapError.tsx @@ -18,9 +18,9 @@ import React from 'react'; import { Icon as WarningBadge } from '../../../../res/img/element-icons/warning-badge.svg'; import { _t } from '../../../languageHandler'; +import { getLocationShareErrorMessage, LocationShareError } from '../../../utils/location'; import AccessibleButton from '../elements/AccessibleButton'; import Heading from '../typography/Heading'; -import { getLocationShareErrorMessage, LocationShareError } from './LocationShareErrors'; interface Props { onFinished: () => void; diff --git a/src/components/views/messages/MLocationBody.tsx b/src/components/views/messages/MLocationBody.tsx index 1167320052..d824917dad 100644 --- a/src/components/views/messages/MLocationBody.tsx +++ b/src/components/views/messages/MLocationBody.tsx @@ -16,13 +16,11 @@ limitations under the License. import React from 'react'; import maplibregl from 'maplibre-gl'; -import { logger } from "matrix-js-sdk/src/logger"; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { M_ASSET, LocationAssetType, ILocationContent, - M_LOCATION, } from 'matrix-js-sdk/src/@types/location'; import { ClientEvent, IClientWellKnown } from 'matrix-js-sdk/src/client'; @@ -31,14 +29,19 @@ import { IBodyProps } from "./IBodyProps"; import { _t } from '../../../languageHandler'; import MemberAvatar from '../avatars/MemberAvatar'; import Modal from '../../../Modal'; +import { + parseGeoUri, + locationEventGeoUri, + createMap, + getLocationShareErrorMessage, + LocationShareError, +} from '../../../utils/location'; import LocationViewDialog from '../location/LocationViewDialog'; import TooltipTarget from '../elements/TooltipTarget'; import { Alignment } from '../elements/Tooltip'; import AccessibleButton from '../elements/AccessibleButton'; import { tileServerFromWellKnown } from '../../../utils/WellKnownUtils'; import MatrixClientContext from '../../../contexts/MatrixClientContext'; -import { findMapStyleUrl } from '../location/findMapStyleUrl'; -import { getLocationShareErrorMessage, LocationShareError } from '../location/LocationShareErrors'; interface IState { error: Error; @@ -238,114 +241,3 @@ function ZoomButtons(props: IZoomButtonsProps): React.ReactElement; } -export function createMap( - coords: GeolocationCoordinates, - interactive: boolean, - bodyId: string, - markerId: string, - onError: (error: Error) => void, -): maplibregl.Map { - try { - const styleUrl = findMapStyleUrl(); - const coordinates = new maplibregl.LngLat(coords.longitude, coords.latitude); - - const map = new maplibregl.Map({ - container: bodyId, - style: styleUrl, - center: coordinates, - zoom: 15, - interactive, - }); - - new maplibregl.Marker({ - element: document.getElementById(markerId), - anchor: 'bottom', - offset: [0, -1], - }) - .setLngLat(coordinates) - .addTo(map); - - map.on('error', (e) => { - logger.error( - "Failed to load map: check map_style_url in config.json has a " - + "valid URL and API key", - e.error, - ); - onError(new Error(LocationShareError.MapStyleUrlNotReachable)); - }); - - return map; - } catch (e) { - logger.error("Failed to render map", e); - onError(e); - } -} - -/** - * Find the geo-URI contained within a location event. - */ -export function locationEventGeoUri(mxEvent: MatrixEvent): string { - // unfortunately we're stuck supporting legacy `content.geo_uri` - // events until the end of days, or until we figure out mutable - // events - so folks can read their old chat history correctly. - // https://github.com/matrix-org/matrix-doc/issues/3516 - const content = mxEvent.getContent(); - const loc = M_LOCATION.findIn(content) as { uri?: string }; - return loc ? loc.uri : content['geo_uri']; -} - -export function parseGeoUri(uri: string): GeolocationCoordinates { - function parse(s: string): number { - const ret = parseFloat(s); - if (Number.isNaN(ret)) { - return undefined; - } else { - return ret; - } - } - - const m = uri.match(/^\s*geo:(.*?)\s*$/); - if (!m) return; - const parts = m[1].split(';'); - const coords = parts[0].split(','); - let uncertainty: number; - for (const param of parts.slice(1)) { - const m = param.match(/u=(.*)/); - if (m) uncertainty = parse(m[1]); - } - return { - latitude: parse(coords[0]), - longitude: parse(coords[1]), - altitude: parse(coords[2]), - accuracy: uncertainty, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, - }; -} - -function makeLink(coords: GeolocationCoordinates): string { - return ( - "https://www.openstreetmap.org/" + - `?mlat=${coords.latitude}` + - `&mlon=${coords.longitude}` + - `#map=16/${coords.latitude}/${coords.longitude}` - ); -} - -export function createMapSiteLink(event: MatrixEvent): string { - const content: Object = event.getContent(); - const mLocation = content[M_LOCATION.name]; - if (mLocation !== undefined) { - const uri = mLocation["uri"]; - if (uri !== undefined) { - return makeLink(parseGeoUri(uri)); - } - } else { - const geoUri = content["geo_uri"]; - if (geoUri) { - return makeLink(parseGeoUri(geoUri)); - } - } - return null; -} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6d6699045c..1e9e25fd94 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -734,6 +734,8 @@ "Invite to %(spaceName)s": "Invite to %(spaceName)s", "Share your public space": "Share your public space", "Unknown App": "Unknown App", + "This homeserver is not configured to display maps.": "This homeserver is not configured to display maps.", + "This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.": "This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.", "Are you sure you want to exit during this export?": "Are you sure you want to exit during this export?", "Generating a ZIP": "Generating a ZIP", "Fetched %(count)s events out of %(total)s|other": "Fetched %(count)s events out of %(total)s", @@ -2144,8 +2146,6 @@ "Failed to fetch your location. Please try again later.": "Failed to fetch your location. Please try again later.", "Timed out trying to fetch your location. Please try again later.": "Timed out trying to fetch your location. Please try again later.", "Unknown error fetching location. Please try again later.": "Unknown error fetching location. Please try again later.", - "This homeserver is not configured to display maps.": "This homeserver is not configured to display maps.", - "This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.": "This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.", "We couldn't send your location": "We couldn't send your location", "%(brand)s could not send your location. Please try again later.": "%(brand)s could not send your location. Please try again later.", "%(displayName)s's live location": "%(displayName)s's live location", diff --git a/src/components/views/location/LocationShareErrors.ts b/src/utils/location/LocationShareErrors.ts similarity index 96% rename from src/components/views/location/LocationShareErrors.ts rename to src/utils/location/LocationShareErrors.ts index 9e244e4dab..81d4e50d31 100644 --- a/src/components/views/location/LocationShareErrors.ts +++ b/src/utils/location/LocationShareErrors.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { _t } from "../../../languageHandler"; +import { _t } from "../../languageHandler"; export enum LocationShareError { MapStyleUrlNotConfigured = 'MapStyleUrlNotConfigured', diff --git a/src/components/views/location/findMapStyleUrl.ts b/src/utils/location/findMapStyleUrl.ts similarity index 92% rename from src/components/views/location/findMapStyleUrl.ts rename to src/utils/location/findMapStyleUrl.ts index bf3823da7d..9eb9a6d307 100644 --- a/src/components/views/location/findMapStyleUrl.ts +++ b/src/utils/location/findMapStyleUrl.ts @@ -16,8 +16,8 @@ limitations under the License. import { logger } from "matrix-js-sdk/src/logger"; -import SdkConfig from "../../../SdkConfig"; -import { getTileServerWellKnown } from "../../../utils/WellKnownUtils"; +import SdkConfig from "../../SdkConfig"; +import { getTileServerWellKnown } from "../WellKnownUtils"; import { LocationShareError } from "./LocationShareErrors"; /** diff --git a/src/utils/location/index.ts b/src/utils/location/index.ts new file mode 100644 index 0000000000..49c6d8112d --- /dev/null +++ b/src/utils/location/index.ts @@ -0,0 +1,21 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export * from './findMapStyleUrl'; +export * from './locationEventGeoUri'; +export * from './LocationShareErrors'; +export * from './map'; +export * from './parseGeoUri'; diff --git a/src/utils/location/locationEventGeoUri.ts b/src/utils/location/locationEventGeoUri.ts new file mode 100644 index 0000000000..eb81ac87c0 --- /dev/null +++ b/src/utils/location/locationEventGeoUri.ts @@ -0,0 +1,31 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { M_LOCATION } from "matrix-js-sdk/src/@types/location"; + +/** + * Find the geo-URI contained within a location event. + */ +export const locationEventGeoUri = (mxEvent: MatrixEvent): string => { + // unfortunately we're stuck supporting legacy `content.geo_uri` + // events until the end of days, or until we figure out mutable + // events - so folks can read their old chat history correctly. + // https://github.com/matrix-org/matrix-doc/issues/3516 + const content = mxEvent.getContent(); + const loc = M_LOCATION.findIn(content) as { uri?: string }; + return loc ? loc.uri : content['geo_uri']; +}; diff --git a/src/utils/location/map.ts b/src/utils/location/map.ts new file mode 100644 index 0000000000..f1e87acc28 --- /dev/null +++ b/src/utils/location/map.ts @@ -0,0 +1,93 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import maplibregl from "maplibre-gl"; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { M_LOCATION } from "matrix-js-sdk/src/@types/location"; +import { logger } from "matrix-js-sdk/src/logger"; + +import { parseGeoUri } from "./parseGeoUri"; +import { findMapStyleUrl } from "./findMapStyleUrl"; +import { LocationShareError } from "./LocationShareErrors"; + +export const createMap = ( + coords: GeolocationCoordinates, + interactive: boolean, + bodyId: string, + markerId: string, + onError: (error: Error) => void, +): maplibregl.Map => { + try { + const styleUrl = findMapStyleUrl(); + const coordinates = new maplibregl.LngLat(coords.longitude, coords.latitude); + + const map = new maplibregl.Map({ + container: bodyId, + style: styleUrl, + center: coordinates, + zoom: 15, + interactive, + }); + + new maplibregl.Marker({ + element: document.getElementById(markerId), + anchor: 'bottom', + offset: [0, -1], + }) + .setLngLat(coordinates) + .addTo(map); + + map.on('error', (e) => { + logger.error( + "Failed to load map: check map_style_url in config.json has a " + + "valid URL and API key", + e.error, + ); + onError(new Error(LocationShareError.MapStyleUrlNotReachable)); + }); + + return map; + } catch (e) { + logger.error("Failed to render map", e); + onError(e); + } +}; + +const makeLink = (coords: GeolocationCoordinates): string => { + return ( + "https://www.openstreetmap.org/" + + `?mlat=${coords.latitude}` + + `&mlon=${coords.longitude}` + + `#map=16/${coords.latitude}/${coords.longitude}` + ); +}; + +export const createMapSiteLink = (event: MatrixEvent): string => { + const content: Object = event.getContent(); + const mLocation = content[M_LOCATION.name]; + if (mLocation !== undefined) { + const uri = mLocation["uri"]; + if (uri !== undefined) { + return makeLink(parseGeoUri(uri)); + } + } else { + const geoUri = content["geo_uri"]; + if (geoUri) { + return makeLink(parseGeoUri(geoUri)); + } + } + return null; +}; diff --git a/src/utils/location/parseGeoUri.ts b/src/utils/location/parseGeoUri.ts new file mode 100644 index 0000000000..4c7291cd3e --- /dev/null +++ b/src/utils/location/parseGeoUri.ts @@ -0,0 +1,45 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export const parseGeoUri = (uri: string): GeolocationCoordinates => { + function parse(s: string): number { + const ret = parseFloat(s); + if (Number.isNaN(ret)) { + return undefined; + } else { + return ret; + } + } + + const m = uri.match(/^\s*geo:(.*?)\s*$/); + if (!m) return; + const parts = m[1].split(';'); + const coords = parts[0].split(','); + let uncertainty: number; + for (const param of parts.slice(1)) { + const m = param.match(/u=(.*)/); + if (m) uncertainty = parse(m[1]); + } + return { + latitude: parse(coords[0]), + longitude: parse(coords[1]), + altitude: parse(coords[2]), + accuracy: uncertainty, + altitudeAccuracy: undefined, + heading: undefined, + speed: undefined, + }; +}; diff --git a/test/components/views/location/LocationPicker-test.tsx b/test/components/views/location/LocationPicker-test.tsx index b3d29e7089..ac8c6da238 100644 --- a/test/components/views/location/LocationPicker-test.tsx +++ b/test/components/views/location/LocationPicker-test.tsx @@ -29,10 +29,9 @@ import { LocationShareType } from "../../../../src/components/views/location/sha import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; import { findById, findByTestId, mockPlatformPeg } from '../../../test-utils'; -import { findMapStyleUrl } from '../../../../src/components/views/location/findMapStyleUrl'; -import { LocationShareError } from '../../../../src/components/views/location/LocationShareErrors'; +import { findMapStyleUrl, LocationShareError } from '../../../../src/utils/location'; -jest.mock('../../../../src/components/views/location/findMapStyleUrl', () => ({ +jest.mock('../../../../src/utils/location/findMapStyleUrl', () => ({ findMapStyleUrl: jest.fn().mockReturnValue('tileserver.com'), })); diff --git a/test/components/views/location/LocationShareMenu-test.tsx b/test/components/views/location/LocationShareMenu-test.tsx index 1fa19201a1..b79d6936ba 100644 --- a/test/components/views/location/LocationShareMenu-test.tsx +++ b/test/components/views/location/LocationShareMenu-test.tsx @@ -35,7 +35,7 @@ import { findByTagAndTestId, flushPromises } from '../../../test-utils'; import Modal from '../../../../src/Modal'; import { DEFAULT_DURATION_MS } from '../../../../src/components/views/location/LiveDurationDropdown'; -jest.mock('../../../../src/components/views/location/findMapStyleUrl', () => ({ +jest.mock('../../../../src/utils/location/findMapStyleUrl', () => ({ findMapStyleUrl: jest.fn().mockReturnValue('test'), })); diff --git a/test/components/views/location/MapError-test.tsx b/test/components/views/location/MapError-test.tsx index e482d043d0..706e8e9188 100644 --- a/test/components/views/location/MapError-test.tsx +++ b/test/components/views/location/MapError-test.tsx @@ -19,7 +19,7 @@ import { mount } from 'enzyme'; import '../../../skinned-sdk'; import { MapError } from '../../../../src/components/views/location/MapError'; -import { LocationShareError } from '../../../../src/components/views/location/LocationShareErrors'; +import { LocationShareError } from '../../../../src/utils/location'; describe('', () => { const defaultProps = { diff --git a/test/components/views/messages/MLocationBody-test.tsx b/test/components/views/messages/MLocationBody-test.tsx index 67c274ef9e..1954609efa 100644 --- a/test/components/views/messages/MLocationBody-test.tsx +++ b/test/components/views/messages/MLocationBody-test.tsx @@ -26,200 +26,27 @@ import { M_TIMESTAMP, } from "matrix-js-sdk/src/@types/location"; import { TEXT_NODE_TYPE } from "matrix-js-sdk/src/@types/extensible_events"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import maplibregl from 'maplibre-gl'; import { logger } from 'matrix-js-sdk/src/logger'; import sdk from "../../../skinned-sdk"; import MLocationBody, { - createMapSiteLink, isSelfLocation, - parseGeoUri, } from "../../../../src/components/views/messages/MLocationBody"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper"; import { getTileServerWellKnown } from "../../../../src/utils/WellKnownUtils"; import SdkConfig from "../../../../src/SdkConfig"; +import { makeLocationEvent } from "../../../test-utils/location"; jest.mock("../../../../src/utils/WellKnownUtils", () => ({ getTileServerWellKnown: jest.fn(), })); -let EVENT_ID = 0; -function nextId(): string { - EVENT_ID++; - return EVENT_ID.toString(); -} sdk.getComponent("views.messages.MLocationBody"); describe("MLocationBody", () => { - describe("parseGeoUri", () => { - it("fails if the supplied URI is empty", () => { - expect(parseGeoUri("")).toBeFalsy(); - }); - - // We use some examples from the spec, but don't check semantics - // like two textually-different URIs being equal, since we are - // just a humble parser. - - // Note: we do not understand geo URIs with percent-encoded coords - // or accuracy. It is RECOMMENDED in the spec never to percent-encode - // these, but it is permitted, and we will fail to parse in that case. - - it("rfc5870 6.1 Simple 3-dimensional", () => { - expect(parseGeoUri("geo:48.2010,16.3695,183")).toEqual( - { - latitude: 48.2010, - longitude: 16.3695, - altitude: 183, - accuracy: undefined, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, - }, - ); - }); - - it("rfc5870 6.2 Explicit CRS and accuracy", () => { - expect(parseGeoUri("geo:48.198634,16.371648;crs=wgs84;u=40")).toEqual( - { - latitude: 48.198634, - longitude: 16.371648, - altitude: undefined, - accuracy: 40, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, - }, - ); - }); - - it("rfc5870 6.4 Negative longitude and explicit CRS", () => { - expect(parseGeoUri("geo:90,-22.43;crs=WGS84")).toEqual( - { - latitude: 90, - longitude: -22.43, - altitude: undefined, - accuracy: undefined, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, - }, - ); - }); - - it("rfc5870 6.4 Integer lat and lon", () => { - expect(parseGeoUri("geo:90,46")).toEqual( - { - latitude: 90, - longitude: 46, - altitude: undefined, - accuracy: undefined, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, - }, - ); - }); - - it("rfc5870 6.4 Percent-encoded param value", () => { - expect(parseGeoUri("geo:66,30;u=6.500;FOo=this%2dthat")).toEqual( - { - latitude: 66, - longitude: 30, - altitude: undefined, - accuracy: 6.500, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, - }, - ); - }); - - it("rfc5870 6.4 Unknown param", () => { - expect(parseGeoUri("geo:66.0,30;u=6.5;foo=this-that>")).toEqual( - { - latitude: 66.0, - longitude: 30, - altitude: undefined, - accuracy: 6.5, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, - }, - ); - }); - - it("rfc5870 6.4 Multiple unknown params", () => { - expect(parseGeoUri("geo:70,20;foo=1.00;bar=white")).toEqual( - { - latitude: 70, - longitude: 20, - altitude: undefined, - accuracy: undefined, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, - }, - ); - }); - - it("Negative latitude", () => { - expect(parseGeoUri("geo:-7.5,20")).toEqual( - { - latitude: -7.5, - longitude: 20, - altitude: undefined, - accuracy: undefined, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, - }, - ); - }); - - it("Zero altitude is not unknown", () => { - expect(parseGeoUri("geo:-7.5,-20,0")).toEqual( - { - latitude: -7.5, - longitude: -20, - altitude: 0, - accuracy: undefined, - altitudeAccuracy: undefined, - heading: undefined, - speed: undefined, - }, - ); - }); - }); - - describe("createMapSiteLink", () => { - it("returns null if event does not contain geouri", () => { - expect(createMapSiteLink(nonLocationEvent())).toBeNull(); - }); - - it("returns OpenStreetMap link if event contains m.location", () => { - expect( - createMapSiteLink(modernLocationEvent("geo:51.5076,-0.1276")), - ).toEqual( - "https://www.openstreetmap.org/" + - "?mlat=51.5076&mlon=-0.1276" + - "#map=16/51.5076/-0.1276", - ); - }); - - it("returns OpenStreetMap link if event contains geo_uri", () => { - expect( - createMapSiteLink(oldLocationEvent("geo:51.5076,-0.1276")), - ).toEqual( - "https://www.openstreetmap.org/" + - "?mlat=51.5076&mlon=-0.1276" + - "#map=16/51.5076/-0.1276", - ); - }); - }); - describe("isSelfLocation", () => { it("Returns true for a full m.asset event", () => { const content = makeLocationContent("", '0'); @@ -271,7 +98,7 @@ describe("MLocationBody", () => { on: jest.fn(), off: jest.fn(), }; - const defaultEvent = modernLocationEvent("geo:51.5076,-0.1276", LocationAssetType.Pin); + const defaultEvent = makeLocationEvent("geo:51.5076,-0.1276", LocationAssetType.Pin); const defaultProps = { mxEvent: defaultEvent, highlights: [], @@ -318,48 +145,3 @@ describe("MLocationBody", () => { }); }); }); - -function oldLocationEvent(geoUri: string): MatrixEvent { - return new MatrixEvent( - { - "event_id": nextId(), - "type": M_LOCATION.name, - "content": { - "body": "Something about where I am", - "msgtype": "m.location", - "geo_uri": geoUri, - }, - }, - ); -} - -function modernLocationEvent(geoUri: string, assetType?: LocationAssetType): MatrixEvent { - return new MatrixEvent( - { - "event_id": nextId(), - "type": M_LOCATION.name, - "content": makeLocationContent( - `Found at ${geoUri} at 2021-12-21T12:22+0000`, - geoUri, - 252523, - "Human-readable label", - assetType, - ), - }, - ); -} - -function nonLocationEvent(): MatrixEvent { - return new MatrixEvent( - { - "event_id": nextId(), - "type": "some.event.type", - "content": { - "m.relates_to": { - "rel_type": "m.reference", - "event_id": "$mypoll", - }, - }, - }, - ); -} diff --git a/test/test-utils/location.ts b/test/test-utils/location.ts new file mode 100644 index 0000000000..16ebcf1020 --- /dev/null +++ b/test/test-utils/location.ts @@ -0,0 +1,50 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { LocationAssetType, M_LOCATION } from "matrix-js-sdk/src/@types/location"; +import { makeLocationContent } from "matrix-js-sdk/src/content-helpers"; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; + +let id = 1; +export const makeLegacyLocationEvent = (geoUri: string): MatrixEvent => { + return new MatrixEvent( + { + "event_id": `$${++id}`, + "type": M_LOCATION.name, + "content": { + "body": "Something about where I am", + "msgtype": "m.location", + "geo_uri": geoUri, + }, + }, + ); +}; + +export const makeLocationEvent = (geoUri: string, assetType?: LocationAssetType): MatrixEvent => { + return new MatrixEvent( + { + "event_id": `$${++id}`, + "type": M_LOCATION.name, + "content": makeLocationContent( + `Found at ${geoUri} at 2021-12-21T12:22+0000`, + geoUri, + 252523, + "Human-readable label", + assetType, + ), + }, + ); +}; diff --git a/test/utils/location/locationEventGeoUri-test.ts b/test/utils/location/locationEventGeoUri-test.ts new file mode 100644 index 0000000000..52626f98b7 --- /dev/null +++ b/test/utils/location/locationEventGeoUri-test.ts @@ -0,0 +1,28 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { locationEventGeoUri } from "../../../src/utils/location"; +import { makeLegacyLocationEvent, makeLocationEvent } from "../../test-utils/location"; + +describe('locationEventGeoUri()', () => { + it('returns m.location uri when available', () => { + expect(locationEventGeoUri(makeLocationEvent("geo:51.5076,-0.1276"))).toEqual("geo:51.5076,-0.1276"); + }); + + it('returns legacy uri when m.location content not found', () => { + expect(locationEventGeoUri(makeLegacyLocationEvent("geo:51.5076,-0.1276"))).toEqual("geo:51.5076,-0.1276"); + }); +}); diff --git a/test/utils/location/map-test.ts b/test/utils/location/map-test.ts new file mode 100644 index 0000000000..f389f12cfd --- /dev/null +++ b/test/utils/location/map-test.ts @@ -0,0 +1,47 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { createMapSiteLink } from "../../../src/utils/location"; +import { mkMessage } from "../../test-utils"; +import { makeLegacyLocationEvent, makeLocationEvent } from "../../test-utils/location"; + +describe("createMapSiteLink", () => { + it("returns null if event does not contain geouri", () => { + expect(createMapSiteLink(mkMessage({ + room: '1', user: '@sender:server', event: true, + }))).toBeNull(); + }); + + it("returns OpenStreetMap link if event contains m.location", () => { + expect( + createMapSiteLink(makeLocationEvent("geo:51.5076,-0.1276")), + ).toEqual( + "https://www.openstreetmap.org/" + + "?mlat=51.5076&mlon=-0.1276" + + "#map=16/51.5076/-0.1276", + ); + }); + + it("returns OpenStreetMap link if event contains geo_uri", () => { + expect( + createMapSiteLink(makeLegacyLocationEvent("geo:51.5076,-0.1276")), + ).toEqual( + "https://www.openstreetmap.org/" + + "?mlat=51.5076&mlon=-0.1276" + + "#map=16/51.5076/-0.1276", + ); + }); +}); diff --git a/test/utils/location/parseGeoUri-test.ts b/test/utils/location/parseGeoUri-test.ts new file mode 100644 index 0000000000..b0d36929d2 --- /dev/null +++ b/test/utils/location/parseGeoUri-test.ts @@ -0,0 +1,157 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { parseGeoUri } from "../../../src/utils/location/parseGeoUri"; + +describe("parseGeoUri", () => { + it("fails if the supplied URI is empty", () => { + expect(parseGeoUri("")).toBeFalsy(); + }); + + // We use some examples from the spec, but don't check semantics + // like two textually-different URIs being equal, since we are + // just a humble parser. + + // Note: we do not understand geo URIs with percent-encoded coords + // or accuracy. It is RECOMMENDED in the spec never to percent-encode + // these, but it is permitted, and we will fail to parse in that case. + + it("rfc5870 6.1 Simple 3-dimensional", () => { + expect(parseGeoUri("geo:48.2010,16.3695,183")).toEqual( + { + latitude: 48.2010, + longitude: 16.3695, + altitude: 183, + accuracy: undefined, + altitudeAccuracy: undefined, + heading: undefined, + speed: undefined, + }, + ); + }); + + it("rfc5870 6.2 Explicit CRS and accuracy", () => { + expect(parseGeoUri("geo:48.198634,16.371648;crs=wgs84;u=40")).toEqual( + { + latitude: 48.198634, + longitude: 16.371648, + altitude: undefined, + accuracy: 40, + altitudeAccuracy: undefined, + heading: undefined, + speed: undefined, + }, + ); + }); + + it("rfc5870 6.4 Negative longitude and explicit CRS", () => { + expect(parseGeoUri("geo:90,-22.43;crs=WGS84")).toEqual( + { + latitude: 90, + longitude: -22.43, + altitude: undefined, + accuracy: undefined, + altitudeAccuracy: undefined, + heading: undefined, + speed: undefined, + }, + ); + }); + + it("rfc5870 6.4 Integer lat and lon", () => { + expect(parseGeoUri("geo:90,46")).toEqual( + { + latitude: 90, + longitude: 46, + altitude: undefined, + accuracy: undefined, + altitudeAccuracy: undefined, + heading: undefined, + speed: undefined, + }, + ); + }); + + it("rfc5870 6.4 Percent-encoded param value", () => { + expect(parseGeoUri("geo:66,30;u=6.500;FOo=this%2dthat")).toEqual( + { + latitude: 66, + longitude: 30, + altitude: undefined, + accuracy: 6.500, + altitudeAccuracy: undefined, + heading: undefined, + speed: undefined, + }, + ); + }); + + it("rfc5870 6.4 Unknown param", () => { + expect(parseGeoUri("geo:66.0,30;u=6.5;foo=this-that>")).toEqual( + { + latitude: 66.0, + longitude: 30, + altitude: undefined, + accuracy: 6.5, + altitudeAccuracy: undefined, + heading: undefined, + speed: undefined, + }, + ); + }); + + it("rfc5870 6.4 Multiple unknown params", () => { + expect(parseGeoUri("geo:70,20;foo=1.00;bar=white")).toEqual( + { + latitude: 70, + longitude: 20, + altitude: undefined, + accuracy: undefined, + altitudeAccuracy: undefined, + heading: undefined, + speed: undefined, + }, + ); + }); + + it("Negative latitude", () => { + expect(parseGeoUri("geo:-7.5,20")).toEqual( + { + latitude: -7.5, + longitude: 20, + altitude: undefined, + accuracy: undefined, + altitudeAccuracy: undefined, + heading: undefined, + speed: undefined, + }, + ); + }); + + it("Zero altitude is not unknown", () => { + expect(parseGeoUri("geo:-7.5,-20,0")).toEqual( + { + latitude: -7.5, + longitude: -20, + altitude: 0, + accuracy: undefined, + altitudeAccuracy: undefined, + heading: undefined, + speed: undefined, + }, + ); + }); +});