diff --git a/res/css/_components.scss b/res/css/_components.scss index f0aff95be0..8b5dc63a0e 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -5,6 +5,7 @@ @import "./_font-weights.scss"; @import "./_spacing.scss"; @import "./components/views/beacon/_BeaconStatus.scss"; +@import "./components/views/beacon/_BeaconViewDialog.scss"; @import "./components/views/beacon/_LeftPanelLiveShareWarning.scss"; @import "./components/views/beacon/_LiveTimeRemaining.scss"; @import "./components/views/beacon/_OwnBeaconStatus.scss"; diff --git a/res/css/components/views/beacon/_BeaconViewDialog.scss b/res/css/components/views/beacon/_BeaconViewDialog.scss new file mode 100644 index 0000000000..901b456439 --- /dev/null +++ b/res/css/components/views/beacon/_BeaconViewDialog.scss @@ -0,0 +1,57 @@ +/* +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_BeaconViewDialog_wrapper .mx_Dialog { + padding: 0px; + + /* Unset contain and position to allow the close button + to appear outside the dialog */ + contain: unset; + position: unset; +} + +.mx_BeaconViewDialog { + /* 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_BeaconViewDialog_map { + width: 80vw; + height: 80vh; + border-radius: 8px; +} diff --git a/res/css/components/views/messages/_MBeaconBody.scss b/res/css/components/views/messages/_MBeaconBody.scss index ae90d25132..dc63d6676d 100644 --- a/res/css/components/views/messages/_MBeaconBody.scss +++ b/res/css/components/views/messages/_MBeaconBody.scss @@ -27,6 +27,10 @@ limitations under the License. height: 100%; width: 100%; z-index: 0; // keeps the entire map under the message action bar + + &:not(.mx_MBeaconBody_mapFallback) { + cursor: pointer; + } } .mx_MBeaconBody_mapFallback { diff --git a/src/components/views/beacon/BeaconMarker.tsx b/src/components/views/beacon/BeaconMarker.tsx new file mode 100644 index 0000000000..8c176ab9c0 --- /dev/null +++ b/src/components/views/beacon/BeaconMarker.tsx @@ -0,0 +1,64 @@ +/* +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, { useContext } from 'react'; +import maplibregl from 'maplibre-gl'; +import { + Beacon, + BeaconEvent, +} from 'matrix-js-sdk/src/matrix'; +import { LocationAssetType } from 'matrix-js-sdk/src/@types/location'; + +import MatrixClientContext from '../../../contexts/MatrixClientContext'; +import { useEventEmitterState } from '../../../hooks/useEventEmitter'; +import SmartMarker from '../location/SmartMarker'; + +interface Props { + map: maplibregl.Map; + beacon: Beacon; +} + +/** + * Updates a map SmartMarker with latest location from given beacon + */ +const BeaconMarker: React.FC = ({ map, beacon }) => { + const latestLocationState = useEventEmitterState( + beacon, + BeaconEvent.LocationUpdate, + () => beacon.latestLocationState, + ); + const matrixClient = useContext(MatrixClientContext); + const room = matrixClient.getRoom(beacon.roomId); + + if (!latestLocationState || !beacon.isLive) { + return null; + } + + const geoUri = latestLocationState?.uri; + + const markerRoomMember = beacon.beaconInfo.assetType === LocationAssetType.Self ? + room.getMember(beacon.beaconInfoOwner) : + undefined; + + return ; +}; + +export default BeaconMarker; diff --git a/src/components/views/beacon/BeaconViewDialog.tsx b/src/components/views/beacon/BeaconViewDialog.tsx new file mode 100644 index 0000000000..052a456fe6 --- /dev/null +++ b/src/components/views/beacon/BeaconViewDialog.tsx @@ -0,0 +1,86 @@ +/* +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 { MatrixClient } from 'matrix-js-sdk/src/client'; +import { + Beacon, + Room, +} from 'matrix-js-sdk/src/matrix'; +import maplibregl from 'maplibre-gl'; + +import { useLiveBeacons } from '../../../utils/beacon/useLiveBeacons'; +import MatrixClientContext from '../../../contexts/MatrixClientContext'; +import BaseDialog from "../dialogs/BaseDialog"; +import { IDialogProps } from "../dialogs/IDialogProps"; +import Map from '../location/Map'; +import ZoomButtons from '../location/ZoomButtons'; +import BeaconMarker from './BeaconMarker'; + +interface IProps extends IDialogProps { + roomId: Room['roomId']; + matrixClient: MatrixClient; +} + +// TODO actual center is coming soon +// for now just center around first beacon in list +const getMapCenterUri = (beacons: Beacon[]): string => { + const firstBeaconWithLocation = beacons.find(beacon => beacon.latestLocationState); + + return firstBeaconWithLocation?.latestLocationState?.uri; +}; + +/** + * Dialog to view live beacons maximised + */ +const BeaconViewDialog: React.FC = ({ roomId, matrixClient, onFinished }) => { + const liveBeacons = useLiveBeacons(roomId, matrixClient); + + const mapCenterUri = getMapCenterUri(liveBeacons); + // TODO probably show loader or placeholder when there is no location + // to center the map on + + return ( + + + + { + ({ map }: { map: maplibregl.Map}) => + <> + { liveBeacons.map(beacon => ) } + + + } + + + + ); +}; + +export default BeaconViewDialog; diff --git a/src/components/views/location/LocationViewDialog.tsx b/src/components/views/location/LocationViewDialog.tsx index 9719013ee4..9651b74458 100644 --- a/src/components/views/location/LocationViewDialog.tsx +++ b/src/components/views/location/LocationViewDialog.tsx @@ -34,6 +34,9 @@ interface IState { error: Error; } +/** + * Dialog to view m.location events maximised + */ export default class LocationViewDialog extends React.Component { constructor(props: IProps) { super(props); diff --git a/src/components/views/location/SmartMarker.tsx b/src/components/views/location/SmartMarker.tsx index 29207eb6e3..c4a7c61e32 100644 --- a/src/components/views/location/SmartMarker.tsx +++ b/src/components/views/location/SmartMarker.tsx @@ -72,12 +72,21 @@ interface SmartMarkerProps { const SmartMarker: React.FC = ({ id, map, geoUri, roomMember, useMemberColor }) => { const { onElementRef } = useMapMarker(map, geoUri); - return ; + return ( + // maplibregl hijacks the Marker dom element + // and removes it from the dom when the maplibregl.Marker instance + // is removed + // wrap in a span so that react doesn't get confused + // when trying to unmount this component + + + + ); }; export default SmartMarker; diff --git a/src/components/views/messages/MBeaconBody.tsx b/src/components/views/messages/MBeaconBody.tsx index a2142e7b0b..f61ec346e4 100644 --- a/src/components/views/messages/MBeaconBody.tsx +++ b/src/components/views/messages/MBeaconBody.tsx @@ -23,6 +23,7 @@ import { Icon as LocationMarkerIcon } from '../../../../res/img/element-icons/lo import MatrixClientContext from '../../../contexts/MatrixClientContext'; import { useEventEmitterState } from '../../../hooks/useEventEmitter'; import { _t } from '../../../languageHandler'; +import Modal from '../../../Modal'; import { useBeacon } from '../../../utils/beacon'; import { isSelfLocation } from '../../../utils/location'; import { BeaconDisplayStatus, getBeaconDisplayStatus } from '../beacon/displayStatus'; @@ -32,6 +33,7 @@ import Map from '../location/Map'; import SmartMarker from '../location/SmartMarker'; import OwnBeaconStatus from '../beacon/OwnBeaconStatus'; import { IBodyProps } from "./IBodyProps"; +import BeaconViewDialog from '../beacon/BeaconViewDialog'; const useBeaconState = (beaconInfoEvent: MatrixEvent): { beacon?: Beacon; @@ -85,13 +87,31 @@ const MBeaconBody: React.FC = React.forwardRef(({ mxEvent }, ref) => latestLocationState, } = useBeaconState(mxEvent); const mapId = useUniqueId(mxEvent.getId()); - const [error, setError] = useState(); + const matrixClient = useContext(MatrixClientContext); + const [error, setError] = useState(); const displayStatus = getBeaconDisplayStatus(isLive, latestLocationState, error); const markerRoomMember = isSelfLocation(mxEvent.getContent()) ? mxEvent.sender : undefined; - const isOwnBeacon = beacon?.beaconInfoOwner === matrixClient.getUserId(); + const onClick = () => { + if (displayStatus !== BeaconDisplayStatus.Active) { + return; + } + Modal.createTrackedDialog( + 'Beacon View', + '', + BeaconViewDialog, + { + roomId: mxEvent.getRoomId(), + matrixClient, + }, + "mx_BeaconViewDialog_wrapper", + false, // isPriority + true, // isStatic + ); + }; + return (
{ displayStatus === BeaconDisplayStatus.Active ? @@ -99,6 +119,7 @@ const MBeaconBody: React.FC = React.forwardRef(({ mxEvent }, ref) => id={mapId} centerGeoUri={latestLocationState.uri} onError={setError} + onClick={onClick} className="mx_MBeaconBody_map" > { diff --git a/src/utils/WellKnownUtils.ts b/src/utils/WellKnownUtils.ts index cb3e92a7bd..e41adabb90 100644 --- a/src/utils/WellKnownUtils.ts +++ b/src/utils/WellKnownUtils.ts @@ -22,7 +22,7 @@ import { MatrixClientPeg } from '../MatrixClientPeg'; const CALL_BEHAVIOUR_WK_KEY = "io.element.call_behaviour"; const E2EE_WK_KEY = "io.element.e2ee"; const E2EE_WK_KEY_DEPRECATED = "im.vector.riot.e2ee"; -const TILE_SERVER_WK_KEY = new UnstableValue( +export const TILE_SERVER_WK_KEY = new UnstableValue( "m.tile_server", "org.matrix.msc3488.tile_server"); /* eslint-disable camelcase */ diff --git a/src/utils/beacon/useLiveBeacons.ts b/src/utils/beacon/useLiveBeacons.ts new file mode 100644 index 0000000000..cbde1a40e7 --- /dev/null +++ b/src/utils/beacon/useLiveBeacons.ts @@ -0,0 +1,41 @@ +/* +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 { + Beacon, + Room, + RoomStateEvent, + MatrixClient, +} from "matrix-js-sdk/src/matrix"; + +import { useEventEmitterState } from "../../hooks/useEventEmitter"; + +/** + * Returns an array of all live beacon ids for a given room + * + * Beacons are removed from array when they become inactive + */ +export const useLiveBeacons = (roomId: Room['roomId'], matrixClient: MatrixClient): Beacon[] => { + const room = matrixClient.getRoom(roomId); + + const liveBeacons = useEventEmitterState( + room.currentState, + RoomStateEvent.BeaconLiveness, + () => room.currentState?.liveBeaconIds.map(beaconIdentifier => room.currentState.beacons.get(beaconIdentifier)), + ); + + return liveBeacons; +}; diff --git a/test/components/views/beacon/BeaconMarker-test.tsx b/test/components/views/beacon/BeaconMarker-test.tsx new file mode 100644 index 0000000000..5b730ff438 --- /dev/null +++ b/test/components/views/beacon/BeaconMarker-test.tsx @@ -0,0 +1,136 @@ +/* +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 maplibregl from 'maplibre-gl'; +import { act } from 'react-dom/test-utils'; +import { + Beacon, + Room, + RoomMember, + getBeaconInfoIdentifier, +} from 'matrix-js-sdk/src/matrix'; + +import BeaconMarker from '../../../../src/components/views/beacon/BeaconMarker'; +import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; +import { getMockClientWithEventEmitter, makeBeaconEvent, makeBeaconInfoEvent } from '../../../test-utils'; +import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils'; + +describe('', () => { + // 14.03.2022 16:15 + const now = 1647270879403; + // stable date for snapshots + jest.spyOn(global.Date, 'now').mockReturnValue(now); + const roomId = '!room:server'; + const aliceId = '@alice:server'; + + const aliceMember = new RoomMember(roomId, aliceId); + + const mockMap = new maplibregl.Map(); + + const mockClient = getMockClientWithEventEmitter({ + getClientWellKnown: jest.fn().mockReturnValue({ + [TILE_SERVER_WK_KEY.name]: { map_style_url: 'maps.com' }, + }), + getUserId: jest.fn().mockReturnValue(aliceId), + getRoom: jest.fn(), + isGuest: jest.fn().mockReturnValue(false), + }); + + // make fresh rooms every time + // as we update room state + const makeRoomWithStateEvents = (stateEvents = []): Room => { + const room1 = new Room(roomId, mockClient, aliceId); + + room1.currentState.setStateEvents(stateEvents); + jest.spyOn(room1, 'getMember').mockReturnValue(aliceMember); + mockClient.getRoom.mockReturnValue(room1); + + return room1; + }; + + const defaultEvent = makeBeaconInfoEvent(aliceId, + roomId, + { isLive: true }, + '$alice-room1-1', + ); + const notLiveEvent = makeBeaconInfoEvent(aliceId, + roomId, + { isLive: false }, + '$alice-room1-2', + ); + + const location1 = makeBeaconEvent( + aliceId, { beaconInfoId: defaultEvent.getId(), geoUri: 'geo:51,41', timestamp: now + 1 }, + ); + const location2 = makeBeaconEvent( + aliceId, { beaconInfoId: defaultEvent.getId(), geoUri: 'geo:52,42', timestamp: now + 10000 }, + ); + + const defaultProps = { + map: mockMap, + beacon: new Beacon(defaultEvent), + }; + + const getComponent = (props = {}) => + mount(, { + wrappingComponent: MatrixClientContext.Provider, + wrappingComponentProps: { value: mockClient }, + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders nothing when beacon is not live', () => { + const room = makeRoomWithStateEvents([notLiveEvent]); + const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(notLiveEvent)); + const component = getComponent({ beacon }); + expect(component.html()).toBe(null); + }); + + it('renders nothing when beacon has no location', () => { + const room = makeRoomWithStateEvents([defaultEvent]); + const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); + const component = getComponent({ beacon }); + expect(component.html()).toBe(null); + }); + + it('renders marker when beacon has location', () => { + const room = makeRoomWithStateEvents([defaultEvent]); + const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); + beacon.addLocations([location1]); + const component = getComponent({ beacon }); + expect(component).toMatchSnapshot(); + }); + + it('updates with new locations', () => { + const room = makeRoomWithStateEvents([defaultEvent]); + const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); + beacon.addLocations([location1]); + const component = getComponent({ beacon }); + expect(component.find('SmartMarker').props()['geoUri']).toEqual('geo:51,41'); + + act(() => { + beacon.addLocations([location2]); + }); + component.setProps({}); + + // updated to latest location + expect(component.find('SmartMarker').props()['geoUri']).toEqual('geo:52,42'); + }); +}); diff --git a/test/components/views/beacon/BeaconViewDialog-test.tsx b/test/components/views/beacon/BeaconViewDialog-test.tsx new file mode 100644 index 0000000000..70dddd2710 --- /dev/null +++ b/test/components/views/beacon/BeaconViewDialog-test.tsx @@ -0,0 +1,121 @@ +/* +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 { + MatrixClient, + Room, + RoomMember, + getBeaconInfoIdentifier, +} from 'matrix-js-sdk/src/matrix'; + +import BeaconViewDialog from '../../../../src/components/views/beacon/BeaconViewDialog'; +import { + getMockClientWithEventEmitter, + makeBeaconEvent, + makeBeaconInfoEvent, +} from '../../../test-utils'; +import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils'; + +describe('', () => { + // 14.03.2022 16:15 + const now = 1647270879403; + // stable date for snapshots + jest.spyOn(global.Date, 'now').mockReturnValue(now); + const roomId = '!room:server'; + const aliceId = '@alice:server'; + const bobId = '@bob:server'; + + const aliceMember = new RoomMember(roomId, aliceId); + + const mockClient = getMockClientWithEventEmitter({ + getClientWellKnown: jest.fn().mockReturnValue({ + [TILE_SERVER_WK_KEY.name]: { map_style_url: 'maps.com' }, + }), + getUserId: jest.fn().mockReturnValue(aliceId), + getRoom: jest.fn(), + isGuest: jest.fn().mockReturnValue(false), + }); + + // make fresh rooms every time + // as we update room state + const makeRoomWithStateEvents = (stateEvents = []): Room => { + const room1 = new Room(roomId, mockClient, aliceId); + + room1.currentState.setStateEvents(stateEvents); + jest.spyOn(room1, 'getMember').mockReturnValue(aliceMember); + mockClient.getRoom.mockReturnValue(room1); + + return room1; + }; + + const defaultEvent = makeBeaconInfoEvent(aliceId, + roomId, + { isLive: true }, + '$alice-room1-1', + ); + + const location1 = makeBeaconEvent( + aliceId, { beaconInfoId: defaultEvent.getId(), geoUri: 'geo:51,41', timestamp: now + 1 }, + ); + + const defaultProps = { + onFinished: jest.fn(), + roomId, + matrixClient: mockClient as MatrixClient, + }; + + const getComponent = (props = {}) => + mount(); + + it('renders a map with markers', () => { + const room = makeRoomWithStateEvents([defaultEvent]); + const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); + beacon.addLocations([location1]); + const component = getComponent(); + expect(component.find('Map').props()).toEqual(expect.objectContaining({ + centerGeoUri: 'geo:51,41', + interactive: true, + })); + expect(component.find('SmartMarker').length).toEqual(1); + }); + + it('updates markers on changes to beacons', () => { + const room = makeRoomWithStateEvents([defaultEvent]); + const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent)); + beacon.addLocations([location1]); + const component = getComponent(); + expect(component.find('BeaconMarker').length).toEqual(1); + + const anotherBeaconEvent = makeBeaconInfoEvent(bobId, + roomId, + { isLive: true }, + '$bob-room1-1', + ); + + act(() => { + // emits RoomStateEvent.BeaconLiveness + room.currentState.setStateEvents([anotherBeaconEvent]); + }); + + component.setProps({}); + + // two markers now! + expect(component.find('BeaconMarker').length).toEqual(2); + }); +}); diff --git a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap new file mode 100644 index 0000000000..cde5fd8232 --- /dev/null +++ b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap @@ -0,0 +1,229 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders marker when beacon has location 1`] = ` + + + + +
+
+ + + + + + + + +
+
+
+
+
+
+`; diff --git a/test/components/views/location/LocationViewDialog-test.tsx b/test/components/views/location/LocationViewDialog-test.tsx index 432d7107fb..fa23192bbf 100644 --- a/test/components/views/location/LocationViewDialog-test.tsx +++ b/test/components/views/location/LocationViewDialog-test.tsx @@ -20,6 +20,7 @@ import { RoomMember } from 'matrix-js-sdk/src/matrix'; import { LocationAssetType } from 'matrix-js-sdk/src/@types/location'; import LocationViewDialog from '../../../../src/components/views/location/LocationViewDialog'; +import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils'; import { getMockClientWithEventEmitter, makeLocationEvent } from '../../../test-utils'; describe('', () => { @@ -27,7 +28,7 @@ describe('', () => { const userId = '@user:server'; const mockClient = getMockClientWithEventEmitter({ getClientWellKnown: jest.fn().mockReturnValue({ - "m.tile_server": { map_style_url: 'maps.com' }, + [TILE_SERVER_WK_KEY.name]: { map_style_url: 'maps.com' }, }), isGuest: jest.fn().mockReturnValue(false), }); diff --git a/test/components/views/location/Map-test.tsx b/test/components/views/location/Map-test.tsx index b5fce12a88..40aaee01a0 100644 --- a/test/components/views/location/Map-test.tsx +++ b/test/components/views/location/Map-test.tsx @@ -24,6 +24,7 @@ import { logger } from 'matrix-js-sdk/src/logger'; import Map from '../../../../src/components/views/location/Map'; import { findByTestId, getMockClientWithEventEmitter } from '../../../test-utils'; import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; +import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils'; describe('', () => { const defaultProps = { @@ -34,7 +35,7 @@ describe('', () => { }; const matrixClient = getMockClientWithEventEmitter({ getClientWellKnown: jest.fn().mockReturnValue({ - "m.tile_server": { map_style_url: 'maps.com' }, + [TILE_SERVER_WK_KEY.name]: { map_style_url: 'maps.com' }, }), }); const getComponent = (props = {}) => @@ -46,7 +47,7 @@ describe('', () => { beforeEach(() => { jest.clearAllMocks(); matrixClient.getClientWellKnown.mockReturnValue({ - "m.tile_server": { map_style_url: 'maps.com' }, + [TILE_SERVER_WK_KEY.name]: { map_style_url: 'maps.com' }, }); jest.spyOn(logger, 'error').mockRestore(); @@ -65,7 +66,7 @@ describe('', () => { act(() => { matrixClient.emit(ClientEvent.ClientWellKnown, { - "m.tile_server": { map_style_url: 'new.maps.com' }, + [TILE_SERVER_WK_KEY.name]: { map_style_url: 'new.maps.com' }, }); }); @@ -77,7 +78,7 @@ describe('', () => { act(() => { matrixClient.emit(ClientEvent.ClientWellKnown, { - "m.tile_server": { map_style_url: undefined }, + [TILE_SERVER_WK_KEY.name]: { map_style_url: undefined }, }); }); diff --git a/test/components/views/location/__snapshots__/LocationViewDialog-test.tsx.snap b/test/components/views/location/__snapshots__/LocationViewDialog-test.tsx.snap index 99cd996a10..41b4044c5a 100644 --- a/test/components/views/location/__snapshots__/LocationViewDialog-test.tsx.snap +++ b/test/components/views/location/__snapshots__/LocationViewDialog-test.tsx.snap @@ -48,22 +48,24 @@ exports[` renders map correctly 1`] = ` } } > - -
+
+ className="mx_Marker_border" + > +
+
-
-
+ + creates a marker on mount 1`] = ` } } > - -
+ +
+ className="mx_Marker_border" + > +
+
-
-
+ +
`; @@ -52,18 +54,20 @@ exports[` removes marker on unmount 1`] = ` } } > - -
+ +
+ className="mx_Marker_border" + > +
+
-
-
+ +
`; diff --git a/test/components/views/messages/MBeaconBody-test.tsx b/test/components/views/messages/MBeaconBody-test.tsx index 772d83ae02..9ec5db5f2e 100644 --- a/test/components/views/messages/MBeaconBody-test.tsx +++ b/test/components/views/messages/MBeaconBody-test.tsx @@ -29,6 +29,8 @@ import { getMockClientWithEventEmitter, makeBeaconEvent, makeBeaconInfoEvent } f import { RoomPermalinkCreator } from '../../../../src/utils/permalinks/Permalinks'; import { MediaEventHelper } from '../../../../src/utils/MediaEventHelper'; import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; +import Modal from '../../../../src/Modal'; +import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils'; describe('', () => { // 14.03.2022 16:15 @@ -43,7 +45,7 @@ describe('', () => { const mockClient = getMockClientWithEventEmitter({ getClientWellKnown: jest.fn().mockReturnValue({ - "m.tile_server": { map_style_url: 'maps.com' }, + [TILE_SERVER_WK_KEY.name]: { map_style_url: 'maps.com' }, }), getUserId: jest.fn().mockReturnValue(aliceId), getRoom: jest.fn(), @@ -83,6 +85,8 @@ describe('', () => { wrappingComponentProps: { value: mockClient }, }); + const modalSpy = jest.spyOn(Modal, 'createTrackedDialog').mockReturnValue(undefined); + beforeEach(() => { jest.clearAllMocks(); }); @@ -110,6 +114,23 @@ describe('', () => { expect(component.text()).toEqual("Live location ended"); }); + it('does not open maximised map when on click when beacon is stopped', () => { + const beaconInfoEvent = makeBeaconInfoEvent(aliceId, + roomId, + // puts this beacons live period in the past + { isLive: true, timestamp: now - 600000, timeout: 500 }, + '$alice-room1-1', + ); + makeRoomWithStateEvents([beaconInfoEvent]); + const component = getComponent({ mxEvent: beaconInfoEvent }); + + act(() => { + component.find('.mx_MBeaconBody_map').simulate('click'); + }); + + expect(modalSpy).not.toHaveBeenCalled(); + }); + it('renders stopped UI when a beacon event is not the latest beacon for a user', () => { const aliceBeaconInfo1 = makeBeaconInfoEvent( aliceId, @@ -213,6 +234,40 @@ describe('', () => { expect(component.text()).toEqual("Loading live location..."); }); + it('does nothing on click when a beacon has no location', () => { + makeRoomWithStateEvents([aliceBeaconInfo]); + const component = getComponent({ mxEvent: aliceBeaconInfo }); + + act(() => { + component.find('.mx_MBeaconBody_map').simulate('click'); + }); + + expect(modalSpy).not.toHaveBeenCalled(); + }); + + it('renders a live beacon with a location correctly', () => { + const room = makeRoomWithStateEvents([aliceBeaconInfo]); + const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); + beaconInstance.addLocations([location1]); + const component = getComponent({ mxEvent: aliceBeaconInfo }); + + expect(component.find('Map').length).toBeTruthy; + }); + + it('opens maximised map view on click when beacon has a live location', () => { + const room = makeRoomWithStateEvents([aliceBeaconInfo]); + const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo)); + beaconInstance.addLocations([location1]); + const component = getComponent({ mxEvent: aliceBeaconInfo }); + + act(() => { + component.find('Map').simulate('click'); + }); + + // opens modal + expect(modalSpy).toHaveBeenCalled(); + }); + it('updates latest location', () => { const room = makeRoomWithStateEvents([aliceBeaconInfo]); const component = getComponent({ mxEvent: aliceBeaconInfo }); diff --git a/test/components/views/messages/MLocationBody-test.tsx b/test/components/views/messages/MLocationBody-test.tsx index c1133e81c0..cc5a6105e5 100644 --- a/test/components/views/messages/MLocationBody-test.tsx +++ b/test/components/views/messages/MLocationBody-test.tsx @@ -28,6 +28,7 @@ import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalink import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper"; import Modal from '../../../../src/Modal'; import SdkConfig from "../../../../src/SdkConfig"; +import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils'; import { makeLocationEvent } from "../../../test-utils/location"; import { getMockClientWithEventEmitter } from '../../../test-utils'; @@ -37,7 +38,7 @@ describe("MLocationBody", () => { const userId = '@user:server'; const mockClient = getMockClientWithEventEmitter({ getClientWellKnown: jest.fn().mockReturnValue({ - "m.tile_server": { map_style_url: 'maps.com' }, + [TILE_SERVER_WK_KEY.name]: { map_style_url: 'maps.com' }, }), isGuest: jest.fn().mockReturnValue(false), }); @@ -78,7 +79,7 @@ describe("MLocationBody", () => { it('displays correct fallback content when map_style_url is misconfigured', () => { const mockMap = new maplibregl.Map(); mockClient.getClientWellKnown.mockReturnValue({ - "m.tile_server": { map_style_url: 'bad-tile-server.com' }, + [TILE_SERVER_WK_KEY.name]: { map_style_url: 'bad-tile-server.com' }, }); const component = getComponent(); @@ -93,7 +94,7 @@ describe("MLocationBody", () => { describe('without error', () => { beforeEach(() => { mockClient.getClientWellKnown.mockReturnValue({ - "m.tile_server": { map_style_url: 'maps.com' }, + [TILE_SERVER_WK_KEY.name]: { map_style_url: 'maps.com' }, }); // MLocationBody uses random number for map id diff --git a/test/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap b/test/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap index c422d64fca..e32cedfde4 100644 --- a/test/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap +++ b/test/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap @@ -158,22 +158,24 @@ exports[`MLocationBody without error renders map correctly 1`] = } } > - -
+
+ className="mx_Marker_border" + > +
+
-
-
+ +