diff --git a/res/css/views/context_menus/_MessageContextMenu.scss b/res/css/views/context_menus/_MessageContextMenu.scss index 0189384dd9..e743619f8f 100644 --- a/res/css/views/context_menus/_MessageContextMenu.scss +++ b/res/css/views/context_menus/_MessageContextMenu.scss @@ -54,6 +54,10 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/settings/appearance.svg'); } + .mx_MessageContextMenu_iconOpenInMapSite::before { + mask-image: url('$(res)/img/external-link.svg'); + } + .mx_MessageContextMenu_iconEndPoll::before { mask-image: url('$(res)/img/element-icons/check-white.svg'); } diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 34782c00e2..a856853ff7 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -46,6 +46,7 @@ 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"; export function canCancel(eventStatus: EventStatus): boolean { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; @@ -133,9 +134,13 @@ export default class MessageContextMenu extends React.Component return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId()); } + private canOpenInMapSite(mxEvent: MatrixEvent): boolean { + return isLocationEvent(mxEvent); + } + private canEndPoll(mxEvent: MatrixEvent): boolean { return ( - mxEvent.getType() === POLL_START_EVENT_TYPE.name && + POLL_START_EVENT_TYPE.matches(mxEvent.getType()) && this.state.canRedact && !isPollEnded(mxEvent, MatrixClientPeg.get(), this.props.getRelationsForEvent) ); @@ -278,6 +283,7 @@ export default class MessageContextMenu extends React.Component const eventStatus = mxEvent.status; const unsentReactionsCount = this.getUnsentReactions().length; + let openInMapSiteButton: JSX.Element; let endPollButton: JSX.Element; let resendReactionsButton: JSX.Element; let redactButton: JSX.Element; @@ -313,6 +319,25 @@ export default class MessageContextMenu extends React.Component ); } + if (this.canOpenInMapSite(mxEvent)) { + const mapSiteLink = createMapSiteLink(mxEvent); + openInMapSiteButton = ( + + ); + } + if (isContentActionable(mxEvent)) { if (canForward(mxEvent)) { forwardButton = ( @@ -459,6 +484,7 @@ export default class MessageContextMenu extends React.Component label={_t("View in room")} onClick={this.viewInRoom} /> } + { openInMapSiteButton } { endPollButton } { quoteButton } { forwardButton } diff --git a/src/components/views/location/LocationButton.tsx b/src/components/views/location/LocationButton.tsx index 92ee2ee852..d98bb65653 100644 --- a/src/components/views/location/LocationButton.tsx +++ b/src/components/views/location/LocationButton.tsx @@ -90,3 +90,4 @@ export function textForLocation( } } +export default LocationButton; diff --git a/src/components/views/messages/MLocationBody.tsx b/src/components/views/messages/MLocationBody.tsx index 56bbfa6af8..9fc3316c49 100644 --- a/src/components/views/messages/MLocationBody.tsx +++ b/src/components/views/messages/MLocationBody.tsx @@ -18,6 +18,7 @@ import React from 'react'; import maplibregl from 'maplibre-gl'; import { logger } from "matrix-js-sdk/src/logger"; import { LOCATION_EVENT_TYPE } from 'matrix-js-sdk/src/@types/location'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import SdkConfig from '../../../SdkConfig'; import { replaceableComponent } from "../../../utils/replaceableComponent"; @@ -147,3 +148,29 @@ export function parseGeoUri(uri: string): GeolocationCoordinates { 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[LOCATION_EVENT_TYPE.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/components/views/rooms/CollapsibleButton.tsx b/src/components/views/rooms/CollapsibleButton.tsx index c66129afbe..13834c289d 100644 --- a/src/components/views/rooms/CollapsibleButton.tsx +++ b/src/components/views/rooms/CollapsibleButton.tsx @@ -32,3 +32,5 @@ export const CollapsibleButton = ({ narrowMode, title, ...props }: ICollapsibleB label={narrowMode ? title : undefined} />; }; + +export default CollapsibleButton; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e07cf5a32b..b3f7b66d39 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2832,6 +2832,7 @@ "Are you sure you want to reject the invitation?": "Are you sure you want to reject the invitation?", "Unable to reject invite": "Unable to reject invite", "Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)", + "Open in OpenStreetMap": "Open in OpenStreetMap", "Forward": "Forward", "View source": "View source", "Show preview": "Show preview", diff --git a/test/components/views/messages/MLocationBody-test.tsx b/test/components/views/messages/MLocationBody-test.tsx index 31281bec6a..316f2438b1 100644 --- a/test/components/views/messages/MLocationBody-test.tsx +++ b/test/components/views/messages/MLocationBody-test.tsx @@ -14,8 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { makeLocationContent } from "matrix-js-sdk/src/content-helpers"; +import { LOCATION_EVENT_TYPE } from "matrix-js-sdk/src/@types/location"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + import sdk from "../../../skinned-sdk"; -import { parseGeoUri } from "../../../../src/components/views/messages/MLocationBody"; +import { createMapSiteLink, parseGeoUri } from "../../../../src/components/views/messages/MLocationBody"; sdk.getComponent("views.messages.MLocationBody"); @@ -159,4 +163,80 @@ describe("MLocationBody", () => { ); }); }); + + 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", + ); + }); + }); }); + +function oldLocationEvent(geoUri: string): MatrixEvent { + return new MatrixEvent( + { + "event_id": nextId(), + "type": LOCATION_EVENT_TYPE.name, + "content": { + "body": "Something about where I am", + "msgtype": "m.location", + "geo_uri": geoUri, + }, + }, + ); +} + +function modernLocationEvent(geoUri: string): MatrixEvent { + return new MatrixEvent( + { + "event_id": nextId(), + "type": LOCATION_EVENT_TYPE.name, + "content": makeLocationContent( + `Found at ${geoUri} at 2021-12-21T12:22+0000`, + geoUri, + 252523, + "Human-readable label", + ), + }, + ); +} + +function nonLocationEvent(): MatrixEvent { + return new MatrixEvent( + { + "event_id": nextId(), + "type": "some.event.type", + "content": { + "m.relates_to": { + "rel_type": "m.reference", + "event_id": "$mypoll", + }, + }, + }, + ); +} + +let EVENT_ID = 0; +function nextId(): string { + EVENT_ID++; + return EVENT_ID.toString(); +}