From 707f8cd878c7461e904a594cead756a2f67205d2 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 7 Jan 2022 14:54:45 +0000 Subject: [PATCH] Open map in a dialog when it is clicked (#7465) --- res/css/_common.scss | 19 +- res/css/_components.scss | 1 + .../views/dialogs/_LocationViewDialog.scss | 56 ++++++ res/themes/dark/css/_dark.scss | 1 + res/themes/legacy-dark/css/_legacy-dark.scss | 1 + .../legacy-light/css/_legacy-light.scss | 1 + res/themes/light/css/_light.scss | 1 + .../views/location/LocationViewDialog.tsx | 84 ++++++++ .../views/messages/MLocationBody.tsx | 189 ++++++++++++------ 9 files changed, 286 insertions(+), 67 deletions(-) create mode 100644 res/css/views/dialogs/_LocationViewDialog.scss create mode 100644 src/components/views/location/LocationViewDialog.tsx diff --git a/res/css/_common.scss b/res/css/_common.scss index a5f6c65b15..6a7964c7e0 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -422,8 +422,11 @@ legend { * in the app look the same by being AccessibleButtons, or possibly by having explict button classes. * We should go through and have one consistent set of styles for buttons throughout the app. * For now, I am duplicating the selectors here for mx_Dialog and mx_DialogButtons. + * + * Elements that should not be styled like a dialog button are mentioned in a :not() pseudo-class. + * For the widest browser support, we use multiple :not pseudo-classes instead of :not(.a, .b). */ -.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton), +.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.maplibregl-ctrl-attrib-button):not(.mx_AccessibleButton), .mx_Dialog input[type="submit"], .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton), .mx_Dialog_buttons input[type="submit"] { @@ -440,25 +443,25 @@ legend { font-family: inherit; } -.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):last-child { +.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.maplibregl-ctrl-attrib-button):not(.mx_AccessibleButton):last-child { margin-right: 0px; } -.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):hover, +.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.maplibregl-ctrl-attrib-button):not(.mx_AccessibleButton):hover, .mx_Dialog input[type="submit"]:hover, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):hover, .mx_Dialog_buttons input[type="submit"]:hover { @mixin mx_DialogButton_hover; } -.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus, +.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.maplibregl-ctrl-attrib-button):not(.mx_AccessibleButton):focus, .mx_Dialog input[type="submit"]:focus, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus, .mx_Dialog_buttons input[type="submit"]:focus { filter: brightness($focus-brightness); } -.mx_Dialog button.mx_Dialog_primary, +.mx_Dialog button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.maplibregl-ctrl-attrib-button), .mx_Dialog input[type="submit"].mx_Dialog_primary, .mx_Dialog_buttons button.mx_Dialog_primary, .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary { @@ -467,7 +470,7 @@ legend { min-width: 156px; } -.mx_Dialog button.danger, +.mx_Dialog button.danger:not(.mx_Dialog_nonDialogButton):not(.maplibregl-ctrl-attrib-button), .mx_Dialog input[type="submit"].danger, .mx_Dialog_buttons button.danger, .mx_Dialog_buttons input[type="submit"].danger { @@ -476,13 +479,13 @@ legend { color: $accent-fg-color; } -.mx_Dialog button.warning, +.mx_Dialog button.warning:not(.mx_Dialog_nonDialogButton):not(.maplibregl-ctrl-attrib-button), .mx_Dialog input[type="submit"].warning { border: solid 1px $alert; color: $alert; } -.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled, +.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not(.maplibregl-ctrl-attrib-button):not(.mx_AccessibleButton):disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled, .mx_Dialog_buttons input[type="submit"]:disabled { diff --git a/res/css/_components.scss b/res/css/_components.scss index c172af7e11..8972cdb4b5 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -97,6 +97,7 @@ @import "./views/dialogs/_JoinRuleDropdown.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_LeaveSpaceDialog.scss"; +@import "./views/dialogs/_LocationViewDialog.scss"; @import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_ModalWidgetDialog.scss"; diff --git a/res/css/views/dialogs/_LocationViewDialog.scss b/res/css/views/dialogs/_LocationViewDialog.scss new file mode 100644 index 0000000000..4c37090f72 --- /dev/null +++ b/res/css/views/dialogs/_LocationViewDialog.scss @@ -0,0 +1,56 @@ +/* +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. +*/ + +.mx_LocationViewDialog_wrapper .mx_Dialog { + padding: 0px; + + /* Unset contain and position to allow the close button + to appear outside the dialog */ + contain: unset; + position: unset; +} + +.mx_LocationViewDialog { + /* subtract 0.5px to prevent single-pixel margin due to rounding */ + width: calc(80vw - 0.5px); + height: calc(80vh - 0.5px); + overflow: hidden; + + .mx_Dialog_header { + margin: 0px; + padding: 0px; + position: unset; + + .mx_Dialog_title { + display: none; + } + + .mx_Dialog_cancelButton { + z-index: 4010; + position: absolute; + right: 5vw; + top: 5vh; + width: 20px; + height: 20px; + background-color: $dialog-close-external-color; + } + } + + .mx_MLocationBody .mx_MLocationBody_map { + width: 80vw; + height: 80vh; + } +} diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index cdb6408682..94564aa779 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -104,6 +104,7 @@ $input-lighter-bg-color: #f2f5f8; $dialog-title-fg-color: $primary-content; $dialog-backdrop-color: $menu-border-color; $dialog-close-fg-color: #9fa9ba; +$dialog-close-external-color: $primary-content; // ******************** // RoomList diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 73f9206f77..bd17f4f03f 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -70,6 +70,7 @@ $dialog-title-fg-color: $base-text-color; $dialog-backdrop-color: #000; $dialog-shadow-color: rgba(0, 0, 0, 0.48); $dialog-close-fg-color: #9fa9ba; +$dialog-close-external-color: $text-primary-color; $lightbox-background-bg-color: #000; $lightbox-background-bg-opacity: 0.85; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 881b98a2ee..4760386532 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -93,6 +93,7 @@ $dialog-title-fg-color: #45474a; $dialog-backdrop-color: rgba(46, 48, 51, 0.38); $dialog-shadow-color: rgba(0, 0, 0, 0.48); $dialog-close-fg-color: #c1c1c1; +$dialog-close-external-color: $primary-bg-color; $lightbox-background-bg-color: #000; $lightbox-background-bg-opacity: 0.95; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 20fb259142..c1c8f31fc4 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -153,6 +153,7 @@ $input-fg-color: rgba(74, 74, 74, 0.9); $dialog-title-fg-color: #45474a; $dialog-backdrop-color: rgba(46, 48, 51, 0.38); $dialog-close-fg-color: #c1c1c1; +$dialog-close-external-color: $background; $dialog-shadow-color: rgba(0, 0, 0, 0.48); // ******************** diff --git a/src/components/views/location/LocationViewDialog.tsx b/src/components/views/location/LocationViewDialog.tsx new file mode 100644 index 0000000000..87ea6576ba --- /dev/null +++ b/src/components/views/location/LocationViewDialog.tsx @@ -0,0 +1,84 @@ +/* +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 React from 'react'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; + +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import BaseDialog from "../dialogs/BaseDialog"; +import { IDialogProps } from "../dialogs/IDialogProps"; +import { createMap, LocationBodyContent, locationEventGeoUri, parseGeoUri } from '../messages/MLocationBody'; + +interface IProps extends IDialogProps { + mxEvent: MatrixEvent; +} + +interface IState { + error: Error; +} + +@replaceableComponent("views.location.LocationViewDialog") +export default class LocationViewDialog extends React.Component { + private coords: GeolocationCoordinates; + + constructor(props: IProps) { + super(props); + + this.coords = parseGeoUri(locationEventGeoUri(this.props.mxEvent)); + this.state = { + error: undefined, + }; + } + + componentDidMount() { + if (this.state.error) { + return; + } + + createMap( + this.coords, + true, + this.getBodyId(), + this.getMarkerId(), + (e: Error) => this.setState({ error: e }), + ); + } + + private getBodyId = () => { + return `mx_LocationViewDialog_${this.props.mxEvent.getId()}`; + }; + + private getMarkerId = () => { + return `mx_MLocationViewDialog_marker_${this.props.mxEvent.getId()}`; + }; + + render() { + return ( + + + + ); + } +} diff --git a/src/components/views/messages/MLocationBody.tsx b/src/components/views/messages/MLocationBody.tsx index 9fc3316c49..bb35b55ae1 100644 --- a/src/components/views/messages/MLocationBody.tsx +++ b/src/components/views/messages/MLocationBody.tsx @@ -25,6 +25,8 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { IBodyProps } from "./IBodyProps"; import { _t } from '../../../languageHandler'; import MemberAvatar from '../avatars/MemberAvatar'; +import Modal from '../../../Modal'; +import LocationViewDialog from '../location/LocationViewDialog'; interface IState { error: Error; @@ -32,54 +34,29 @@ interface IState { @replaceableComponent("views.messages.MLocationBody") export default class MLocationBody extends React.Component { - private map: maplibregl.Map; private coords: GeolocationCoordinates; constructor(props: IBodyProps) { super(props); - // 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 = this.props.mxEvent.getContent(); - const loc = content[LOCATION_EVENT_TYPE.name]; - const uri = loc ? loc.uri : content['geo_uri']; - - this.coords = parseGeoUri(uri); + this.coords = parseGeoUri(locationEventGeoUri(this.props.mxEvent)); this.state = { error: undefined, }; } componentDidMount() { - const config = SdkConfig.get(); - const coordinates = new maplibregl.LngLat( - this.coords.longitude, this.coords.latitude); + if (this.state.error) { + return; + } - this.map = new maplibregl.Map({ - container: this.getBodyId(), - style: config.map_style_url, - center: coordinates, - zoom: 13, - }); - - new maplibregl.Marker({ - element: document.getElementById(this.getMarkerId()), - anchor: 'bottom', - offset: [0, -1], - }) - .setLngLat(coordinates) - .addTo(this.map); - - this.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, - ); - this.setState({ error: e.error }); - }); + createMap( + this.coords, + false, + this.getBodyId(), + this.getMarkerId(), + (e: Error) => this.setState({ error: e }), + ); } private getBodyId = () => { @@ -90,33 +67,127 @@ export default class MLocationBody extends React.Component { return `mx_MLocationBody_marker_${this.props.mxEvent.getId()}`; }; - render() { - const error = this.state.error ? -
- { _t("Failed to load map") } -
: null; + private onClick = ( + event: React.MouseEvent, + ) => { + // Don't open map if we clicked the attribution button + const target = event.target as Element; + if (target.classList.contains("maplibregl-ctrl-attrib-button")) { + return; + } - return
-
- { error } -
-
- + Modal.createTrackedDialog( + 'Location View', + '', + LocationViewDialog, + { mxEvent: this.props.mxEvent }, + "mx_LocationViewDialog_wrapper", + false, // isPriority + true, // isStatic + ); + }; + + render() { + return ; + } +} + +interface ILocationBodyContentProps { + mxEvent: MatrixEvent; + bodyId: string; + markerId: string; + error: Error; + onClick?: (event: React.MouseEvent) => void; +} + +export function LocationBodyContent(props: ILocationBodyContentProps) { + return
+ { + props.error + ?
+ { _t("Failed to load map") }
- +
+
+
-
; - } + +
+
; +} + +export function createMap( + coords: GeolocationCoordinates, + interactive: boolean, + bodyId: string, + markerId: string, + onError: (error: Error) => void, +): maplibregl.Map { + const styleUrl = SdkConfig.get().map_style_url; + const coordinates = new maplibregl.LngLat(coords.longitude, coords.latitude); + + const map = new maplibregl.Map({ + container: bodyId, + style: styleUrl, + center: coordinates, + zoom: 13, + 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(e.error); + }); + + return map; +} + +/** + * 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 = LOCATION_EVENT_TYPE.findIn(content) as { uri?: string }; + return loc ? loc.uri : content['geo_uri']; } export function parseGeoUri(uri: string): GeolocationCoordinates {