From d7b3039c9d39b695335210ae69d6abe1abc8d299 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 17 Dec 2021 10:41:12 +0000 Subject: [PATCH 1/4] Remove the Forward and Share buttons for location messages only --- .../views/context_menus/MessageContextMenu.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 34782c00e2e..b6b2c8b9e87 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -83,6 +83,8 @@ interface IProps extends IPosition { interface IState { canRedact: boolean; canPin: boolean; + canForward: boolean; + canShare: boolean; } @replaceableComponent("views.context_menus.MessageContextMenu") @@ -93,6 +95,8 @@ export default class MessageContextMenu extends React.Component state = { canRedact: false, canPin: false, + canForward: false, + canShare: false, }; componentDidMount() { @@ -122,7 +126,11 @@ export default class MessageContextMenu extends React.Component // HACK: Intentionally say we can't pin if the user doesn't want to use the functionality if (!SettingsStore.getValue("feature_pinning")) canPin = false; - this.setState({ canRedact, canPin }); + const isLoc = isLocationEvent(this.props.mxEvent); + const canForward = !isLoc; + const canShare = !isLoc; + + this.setState({ canRedact, canPin, canForward, canShare }); }; private isPinned(): boolean { From 5829dbd4c59e2bfd8e75bc6f06963e56e9d6aa17 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 17 Dec 2021 17:32:41 +0000 Subject: [PATCH 2/4] Display the user's avatar when they shared their location --- res/css/views/messages/_MLocationBody.scss | 29 +++++++++++++--- res/img/location/pointer.svg | 3 ++ .../views/messages/MLocationBody.tsx | 33 ++++++++++++++----- 3 files changed, 53 insertions(+), 12 deletions(-) create mode 100644 res/img/location/pointer.svg diff --git a/res/css/views/messages/_MLocationBody.scss b/res/css/views/messages/_MLocationBody.scss index 5bb90fbf7b0..948822bea8a 100644 --- a/res/css/views/messages/_MLocationBody.scss +++ b/res/css/views/messages/_MLocationBody.scss @@ -14,9 +14,30 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_MLocationBody_map { - width: 450px; - height: 300px; +.mx_MLocationBody { + .mx_MLocationBody_map { + width: 450px; + height: 300px; - border-radius: $timeline-image-border-radius; + border-radius: $timeline-image-border-radius; + } + + .mx_MLocationBody_markerBorder { + width: 31px; + height: 31px; + border-radius: 50%; + background-color: $accent; + filter: drop-shadow(0px 3px 5px rgba(0, 0, 0, 0.2)); + + .mx_BaseAvatar { + margin-top: 2px; + margin-left: 2px; + } + } + + .mx_MLocationBody_pointer { + position: absolute; + bottom: -3px; + left: 12px; + } } diff --git a/res/img/location/pointer.svg b/res/img/location/pointer.svg new file mode 100644 index 00000000000..8a7c5edf712 --- /dev/null +++ b/res/img/location/pointer.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/views/messages/MLocationBody.tsx b/src/components/views/messages/MLocationBody.tsx index a09e9929b2b..56bbfa6af81 100644 --- a/src/components/views/messages/MLocationBody.tsx +++ b/src/components/views/messages/MLocationBody.tsx @@ -23,6 +23,7 @@ import SdkConfig from '../../../SdkConfig'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { IBodyProps } from "./IBodyProps"; import { _t } from '../../../languageHandler'; +import MemberAvatar from '../avatars/MemberAvatar'; interface IState { error: Error; @@ -32,7 +33,6 @@ interface IState { export default class MLocationBody extends React.Component { private map: maplibregl.Map; private coords: GeolocationCoordinates; - private description: string; constructor(props: IBodyProps) { super(props); @@ -49,8 +49,6 @@ export default class MLocationBody extends React.Component { this.state = { error: undefined, }; - - this.description = loc?.description ?? content['body']; } componentDidMount() { @@ -65,13 +63,12 @@ export default class MLocationBody extends React.Component { zoom: 13, }); - new maplibregl.Popup({ - closeButton: false, - closeOnClick: false, - closeOnMove: false, + new maplibregl.Marker({ + element: document.getElementById(this.getMarkerId()), + anchor: 'bottom', + offset: [0, -1], }) .setLngLat(coordinates) - .setHTML(this.description) .addTo(this.map); this.map.on('error', (e)=>{ @@ -88,6 +85,10 @@ export default class MLocationBody extends React.Component { return `mx_MLocationBody_${this.props.mxEvent.getId()}`; }; + private getMarkerId = () => { + return `mx_MLocationBody_marker_${this.props.mxEvent.getId()}`; + }; + render() { const error = this.state.error ?
@@ -97,6 +98,22 @@ export default class MLocationBody extends React.Component { return
{ error } +
+
+ +
+ +
; } } From 07b3d4c7236e63e391b8426f815936bf7a4b0589 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 21 Dec 2021 13:24:54 +0000 Subject: [PATCH 3/4] Allow opening a map view in OpenStreetMap --- .../context_menus/_MessageContextMenu.scss | 4 + .../context_menus/MessageContextMenu.tsx | 28 ++++++- .../views/location/LocationButton.tsx | 1 + .../views/messages/MLocationBody.tsx | 27 ++++++ .../views/rooms/CollapsibleButton.tsx | 2 + src/i18n/strings/en_EN.json | 1 + .../views/messages/MLocationBody-test.tsx | 82 ++++++++++++++++++- 7 files changed, 143 insertions(+), 2 deletions(-) diff --git a/res/css/views/context_menus/_MessageContextMenu.scss b/res/css/views/context_menus/_MessageContextMenu.scss index 0189384dd9a..e743619f8fd 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 b6b2c8b9e87..cde85bace6d 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; @@ -141,9 +142,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) ); @@ -286,6 +291,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; @@ -321,6 +327,25 @@ export default class MessageContextMenu extends React.Component ); } + if (this.canOpenInMapSite(mxEvent)) { + const mapSiteLink = createMapSiteLink(mxEvent); + openInMapSiteButton = ( + + ); + } + if (isContentActionable(mxEvent)) { if (canForward(mxEvent)) { forwardButton = ( @@ -467,6 +492,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 92ee2ee852b..d98bb656530 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 56bbfa6af81..9fc3316c498 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 c66129afbea..13834c289d1 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 e07cf5a32b3..b3f7b66d399 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 31281bec6ad..316f2438b10 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(); +} From 2ff4e71493216a9c4fab563ec4280026d7ff18a0 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 21 Dec 2021 15:32:57 +0000 Subject: [PATCH 4/4] Undo bad merge --- .../views/context_menus/MessageContextMenu.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index cde85bace6d..a856853ff78 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -84,8 +84,6 @@ interface IProps extends IPosition { interface IState { canRedact: boolean; canPin: boolean; - canForward: boolean; - canShare: boolean; } @replaceableComponent("views.context_menus.MessageContextMenu") @@ -96,8 +94,6 @@ export default class MessageContextMenu extends React.Component state = { canRedact: false, canPin: false, - canForward: false, - canShare: false, }; componentDidMount() { @@ -127,11 +123,7 @@ export default class MessageContextMenu extends React.Component // HACK: Intentionally say we can't pin if the user doesn't want to use the functionality if (!SettingsStore.getValue("feature_pinning")) canPin = false; - const isLoc = isLocationEvent(this.props.mxEvent); - const canForward = !isLoc; - const canShare = !isLoc; - - this.setState({ canRedact, canPin, canForward, canShare }); + this.setState({ canRedact, canPin }); }; private isPinned(): boolean {