diff --git a/res/css/_components.scss b/res/css/_components.scss index 9b808463ac..7e69d2b17f 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -14,6 +14,7 @@ @import "./components/views/beacon/_LiveTimeRemaining.scss"; @import "./components/views/beacon/_OwnBeaconStatus.scss"; @import "./components/views/beacon/_RoomLiveShareWarning.scss"; +@import "./components/views/beacon/_ShareLatestLocation.scss"; @import "./components/views/beacon/_StyledLiveBeaconIcon.scss"; @import "./components/views/location/_EnableLiveShare.scss"; @import "./components/views/location/_LiveDurationDropdown.scss"; diff --git a/res/css/components/views/beacon/_BeaconStatusTooltip.scss b/res/css/components/views/beacon/_BeaconStatusTooltip.scss index 07b3a43cc0..d6ed72e455 100644 --- a/res/css/components/views/beacon/_BeaconStatusTooltip.scss +++ b/res/css/components/views/beacon/_BeaconStatusTooltip.scss @@ -21,11 +21,6 @@ limitations under the License. height: 38px; box-sizing: content-box; padding-top: $spacing-8; - - // override copyable text style to make compact - .mx_CopyableText_copyButton { - margin-left: 0 !important; - } } .mx_BeaconStatusTooltip_inner { diff --git a/res/css/components/views/beacon/_ShareLatestLocation.scss b/res/css/components/views/beacon/_ShareLatestLocation.scss new file mode 100644 index 0000000000..5d037fdbd5 --- /dev/null +++ b/res/css/components/views/beacon/_ShareLatestLocation.scss @@ -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. +*/ + +.mx_ShareLatestLocation_icon { + height: 13px; + width: 13px; + color: $secondary-content; +} + +.mx_ShareLatestLocation_copy { + // override copyable text style to make compact + .mx_CopyableText_copyButton { + margin-left: $spacing-8 !important; + } +} diff --git a/res/img/external-link.svg b/res/img/external-link.svg index 459e790fe3..cae1446a68 100644 --- a/res/img/external-link.svg +++ b/res/img/external-link.svg @@ -1,5 +1,5 @@ - + diff --git a/src/components/views/beacon/BeaconListItem.tsx b/src/components/views/beacon/BeaconListItem.tsx index eda1580700..bcfb497176 100644 --- a/src/components/views/beacon/BeaconListItem.tsx +++ b/src/components/views/beacon/BeaconListItem.tsx @@ -23,10 +23,10 @@ import { useEventEmitterState } from '../../../hooks/useEventEmitter'; import { humanizeTime } from '../../../utils/humanize'; import { _t } from '../../../languageHandler'; import MemberAvatar from '../avatars/MemberAvatar'; -import CopyableText from '../elements/CopyableText'; import BeaconStatus from './BeaconStatus'; import { BeaconDisplayStatus } from './displayStatus'; import StyledLiveBeaconIcon from './StyledLiveBeaconIcon'; +import ShareLatestLocation from './ShareLatestLocation'; interface Props { beacon: Beacon; @@ -69,10 +69,7 @@ const BeaconListItem: React.FC = ({ beacon }) => { label={beaconMember?.name || beacon.beaconInfo.description || beacon.beaconInfoOwner} displayStatus={BeaconDisplayStatus.Active} > - latestLocationState?.uri} - /> + { _t("Updated %(humanizedUpdateTime)s", { humanizedUpdateTime }) } diff --git a/src/components/views/beacon/BeaconStatusTooltip.tsx b/src/components/views/beacon/BeaconStatusTooltip.tsx index bc9f360939..688abc510a 100644 --- a/src/components/views/beacon/BeaconStatusTooltip.tsx +++ b/src/components/views/beacon/BeaconStatusTooltip.tsx @@ -19,9 +19,9 @@ import { Beacon } from 'matrix-js-sdk/src/matrix'; import { LocationAssetType } from 'matrix-js-sdk/src/@types/location'; import MatrixClientContext from '../../../contexts/MatrixClientContext'; -import CopyableText from '../elements/CopyableText'; import BeaconStatus from './BeaconStatus'; import { BeaconDisplayStatus } from './displayStatus'; +import ShareLatestLocation from './ShareLatestLocation'; interface Props { beacon: Beacon; @@ -50,10 +50,7 @@ const BeaconStatusTooltip: React.FC = ({ beacon }) => { displayLiveTimeRemaining className='mx_BeaconStatusTooltip_inner' > - beacon.latestLocationState?.uri} - /> + ; }; diff --git a/src/components/views/beacon/ShareLatestLocation.tsx b/src/components/views/beacon/ShareLatestLocation.tsx new file mode 100644 index 0000000000..09c179f6d6 --- /dev/null +++ b/src/components/views/beacon/ShareLatestLocation.tsx @@ -0,0 +1,66 @@ +/* +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, { useEffect, useState } from 'react'; +import { BeaconLocationState } from 'matrix-js-sdk/src/content-helpers'; + +import { Icon as ExternalLinkIcon } from '../../../../res/img/external-link.svg'; +import { _t } from '../../../languageHandler'; +import { makeMapSiteLink, parseGeoUri } from '../../../utils/location'; +import CopyableText from '../elements/CopyableText'; +import TooltipTarget from '../elements/TooltipTarget'; + +interface Props { + latestLocationState?: BeaconLocationState; +} + +const ShareLatestLocation: React.FC = ({ latestLocationState }) => { + const [coords, setCoords] = useState(null); + useEffect(() => { + if (!latestLocationState) { + return; + } + const coords = parseGeoUri(latestLocationState.uri); + setCoords(coords); + }, [latestLocationState]); + + if (!latestLocationState || !coords) { + return null; + } + + const latLonString = `${coords.latitude},${coords.longitude}`; + const mapLink = makeMapSiteLink(coords); + + return <> + + + + + + latLonString} + /> + ; +}; + +export default ShareLatestLocation; diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 725065e95d..8372ca14bf 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -50,7 +50,7 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile"; import { OpenForwardDialogPayload } from "../../../dispatcher/payloads/OpenForwardDialogPayload"; import { OpenReportEventDialogPayload } from "../../../dispatcher/payloads/OpenReportEventDialogPayload"; -import { createMapSiteLink } from '../../../utils/location'; +import { createMapSiteLinkFromEvent } from '../../../utils/location'; interface IProps extends IPosition { chevronFace: ChevronFace; @@ -360,7 +360,7 @@ export default class MessageContextMenu extends React.Component let openInMapSiteButton: JSX.Element; if (this.canOpenInMapSite(mxEvent)) { - const mapSiteLink = createMapSiteLink(mxEvent); + const mapSiteLink = createMapSiteLinkFromEvent(mxEvent); openInMapSiteButton = ( string; border?: boolean; + className?: string; } -const CopyableText: React.FC = ({ children, getTextToCopy, border=true }) => { +const CopyableText: React.FC = ({ children, getTextToCopy, border=true, className }) => { const [tooltip, setTooltip] = useState(undefined); const onCopyClickInternal = async (e: ButtonEvent) => { @@ -44,11 +45,11 @@ const CopyableText: React.FC = ({ children, getTextToCopy, border=true } } }; - const className = classNames("mx_CopyableText", { + const combinedClassName = classNames("mx_CopyableText", className, { mx_CopyableText_border: border, }); - return
+ return
{ children } { +export const makeMapSiteLink = (coords: GeolocationCoordinates): string => { return ( "https://www.openstreetmap.org/" + `?mlat=${coords.latitude}` + @@ -74,18 +74,18 @@ const makeLink = (coords: GeolocationCoordinates): string => { ); }; -export const createMapSiteLink = (event: MatrixEvent): string => { +export const createMapSiteLinkFromEvent = (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)); + return makeMapSiteLink(parseGeoUri(uri)); } } else { const geoUri = content["geo_uri"]; if (geoUri) { - return makeLink(parseGeoUri(geoUri)); + return makeMapSiteLink(parseGeoUri(geoUri)); } } return null; diff --git a/test/components/views/beacon/ShareLatestLocation-test.tsx b/test/components/views/beacon/ShareLatestLocation-test.tsx new file mode 100644 index 0000000000..28d36bc977 --- /dev/null +++ b/test/components/views/beacon/ShareLatestLocation-test.tsx @@ -0,0 +1,59 @@ +/* +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 { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; + +import ShareLatestLocation from '../../../../src/components/views/beacon/ShareLatestLocation'; +import { copyPlaintext } from '../../../../src/utils/strings'; +import { flushPromises } from '../../../test-utils'; + +jest.mock('../../../../src/utils/strings', () => ({ + copyPlaintext: jest.fn().mockResolvedValue(undefined), +})); + +describe('', () => { + const defaultProps = { + latestLocationState: { + uri: 'geo:51,42;u=35', + timestamp: 123, + }, + }; + const getComponent = (props = {}) => + mount(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders null when no location', () => { + const component = getComponent({ latestLocationState: undefined }); + expect(component.html()).toBeNull(); + }); + + it('renders share buttons when there is a location', async () => { + const component = getComponent(); + expect(component).toMatchSnapshot(); + + await act(async () => { + component.find('.mx_CopyableText_copyButton').at(0).simulate('click'); + await flushPromises(); + }); + + expect(copyPlaintext).toHaveBeenCalledWith('51,42'); + }); +}); diff --git a/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap b/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap index 1518a60dba..221d534c02 100644 --- a/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` when a beacon is live and has locations renders beacon info 1`] = `"
  • Alice's carLive until 16:04
    Updated a few seconds ago
  • "`; +exports[` when a beacon is live and has locations renders beacon info 1`] = `"
  • Alice's carLive until 16:04
    Updated a few seconds ago
  • "`; diff --git a/test/components/views/beacon/__snapshots__/ShareLatestLocation-test.tsx.snap b/test/components/views/beacon/__snapshots__/ShareLatestLocation-test.tsx.snap new file mode 100644 index 0000000000..5f55d3103d --- /dev/null +++ b/test/components/views/beacon/__snapshots__/ShareLatestLocation-test.tsx.snap @@ -0,0 +1,79 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders share buttons when there is a location 1`] = ` + + +
    + +
    + +
    + + +
    + + +
    + + +
    + + +`; diff --git a/test/utils/location/map-test.ts b/test/utils/location/map-test.ts index f389f12cfd..d090926f07 100644 --- a/test/utils/location/map-test.ts +++ b/test/utils/location/map-test.ts @@ -14,20 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { createMapSiteLink } from "../../../src/utils/location"; +import { createMapSiteLinkFromEvent } from "../../../src/utils/location"; import { mkMessage } from "../../test-utils"; import { makeLegacyLocationEvent, makeLocationEvent } from "../../test-utils/location"; -describe("createMapSiteLink", () => { +describe("createMapSiteLinkFromEvent", () => { it("returns null if event does not contain geouri", () => { - expect(createMapSiteLink(mkMessage({ + expect(createMapSiteLinkFromEvent(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")), + createMapSiteLinkFromEvent(makeLocationEvent("geo:51.5076,-0.1276")), ).toEqual( "https://www.openstreetmap.org/" + "?mlat=51.5076&mlon=-0.1276" + @@ -37,7 +37,7 @@ describe("createMapSiteLink", () => { it("returns OpenStreetMap link if event contains geo_uri", () => { expect( - createMapSiteLink(makeLegacyLocationEvent("geo:51.5076,-0.1276")), + createMapSiteLinkFromEvent(makeLegacyLocationEvent("geo:51.5076,-0.1276")), ).toEqual( "https://www.openstreetmap.org/" + "?mlat=51.5076&mlon=-0.1276" +