diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 197092ca69c..11e173975dd 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -35,7 +35,6 @@ import { canPinEvent, editEvent, isContentActionable, - isLocationEvent, } from '../../../utils/EventUtils'; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu'; import { ReadPinsEventId } from "../right_panel/types"; @@ -58,6 +57,7 @@ import { OpenForwardDialogPayload } from "../../../dispatcher/payloads/OpenForwa import { OpenReportEventDialogPayload } from "../../../dispatcher/payloads/OpenReportEventDialogPayload"; import { createMapSiteLinkFromEvent } from '../../../utils/location'; import { getForwardableEvent } from '../../../events/forward/getForwardableEvent'; +import { getShareableLocationEvent } from '../../../events/location/getShareableLocationEvent'; interface IProps extends IPosition { chevronFace: ChevronFace; @@ -145,10 +145,6 @@ 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 ( M_POLL_START.matches(mxEvent.getType()) && @@ -369,8 +365,9 @@ export default class MessageContextMenu extends React.Component } let openInMapSiteButton: JSX.Element; - if (this.canOpenInMapSite(mxEvent)) { - const mapSiteLink = createMapSiteLinkFromEvent(mxEvent); + const shareableLocationEvent = getShareableLocationEvent(mxEvent, cli); + if (shareableLocationEvent) { + const mapSiteLink = createMapSiteLinkFromEvent(shareableLocationEvent); openInMapSiteButton = ( MatrixEvent | null; +export type ActionableEventTransformFunction = (event: MatrixEvent, cli: MatrixClient) => MatrixEvent | null; diff --git a/src/events/index.ts b/src/events/index.ts new file mode 100644 index 00000000000..67ebedbb4d3 --- /dev/null +++ b/src/events/index.ts @@ -0,0 +1,18 @@ +/* +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. +*/ + +export { getForwardableEvent } from './forward/getForwardableEvent'; +export { getShareableLocationEvent } from './location/getShareableLocationEvent'; diff --git a/src/events/location/getShareableLocationEvent.ts b/src/events/location/getShareableLocationEvent.ts new file mode 100644 index 00000000000..09b84dbf8fe --- /dev/null +++ b/src/events/location/getShareableLocationEvent.ts @@ -0,0 +1,36 @@ +/* +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 { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon"; +import { MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix"; + +import { getShareableLocationEventForBeacon } from "../../utils/beacon/getShareableLocation"; +import { isLocationEvent } from "../../utils/EventUtils"; + +/** + * Get event that is shareable as a location + * If an event does not have a shareable location, return null + */ +export const getShareableLocationEvent = (event: MatrixEvent, cli: MatrixClient): MatrixEvent | null => { + if (isLocationEvent(event)) { + return event; + } + + if (M_BEACON_INFO.matches(event.getType())) { + return getShareableLocationEventForBeacon(event, cli); + } + return null; +}; diff --git a/src/events/types.ts b/src/events/types.ts new file mode 100644 index 00000000000..f30b3144814 --- /dev/null +++ b/src/events/types.ts @@ -0,0 +1,19 @@ +/* +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 { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; + +export type ActionableEventTransformFunction = (event: MatrixEvent, cli: MatrixClient) => MatrixEvent | null; diff --git a/src/events/forward/getForwardableBeacon.ts b/src/utils/beacon/getShareableLocation.ts similarity index 69% rename from src/events/forward/getForwardableBeacon.ts rename to src/utils/beacon/getShareableLocation.ts index 600937dce62..b2a63db0607 100644 --- a/src/events/forward/getForwardableBeacon.ts +++ b/src/utils/beacon/getShareableLocation.ts @@ -14,15 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { getBeaconInfoIdentifier } from "matrix-js-sdk/src/matrix"; - -import { ForwardableEventTransformFunction } from "./types"; +import { + MatrixClient, + MatrixEvent, + getBeaconInfoIdentifier, +} from "matrix-js-sdk/src/matrix"; /** - * Live location beacons should forward their latest location as a static pin location - * If the beacon is not live, or doesn't have a location forwarding is not allowed + * Beacons should only have shareable locations (open in external mapping tool, forward) + * when they are live and have a location + * If not live, returns null */ -export const getForwardableBeaconEvent: ForwardableEventTransformFunction = (event, cli) => { +export const getShareableLocationEventForBeacon = (event: MatrixEvent, cli: MatrixClient): MatrixEvent | null => { const room = cli.getRoom(event.getRoomId()); const beacon = room.currentState.beacons?.get(getBeaconInfoIdentifier(event)); const latestLocationEvent = beacon?.latestLocationEvent; diff --git a/test/components/views/context_menus/MessageContextMenu-test.tsx b/test/components/views/context_menus/MessageContextMenu-test.tsx index 2b56f9a629b..9a8699b2784 100644 --- a/test/components/views/context_menus/MessageContextMenu-test.tsx +++ b/test/components/views/context_menus/MessageContextMenu-test.tsx @@ -36,7 +36,7 @@ import { IRoomState } from "../../../../src/components/structures/RoomView"; import { canEditContent } from "../../../../src/utils/EventUtils"; import { copyPlaintext, getSelectedText } from "../../../../src/utils/strings"; import MessageContextMenu from "../../../../src/components/views/context_menus/MessageContextMenu"; -import { makeBeaconEvent, makeBeaconInfoEvent, stubClient } from '../../../test-utils'; +import { makeBeaconEvent, makeBeaconInfoEvent, makeLocationEvent, stubClient } from '../../../test-utils'; import dispatcher from '../../../../src/dispatcher/dispatcher'; import SettingsStore from '../../../../src/settings/SettingsStore'; import { ReadPinsEventId } from '../../../../src/components/views/right_panel/types'; @@ -308,6 +308,49 @@ describe('MessageContextMenu', () => { }); }); + describe('open as map link', () => { + it('does not allow opening a plain message in open street maps', () => { + const eventContent = MessageEvent.from("hello"); + const menu = createMenuWithContent(eventContent); + expect(menu.find('a[aria-label="Open in OpenStreetMap"]')).toHaveLength(0); + }); + + it('does not allow opening a beacon that does not have a shareable location event', () => { + const deadBeaconEvent = makeBeaconInfoEvent('@alice', roomId, { isLive: false }); + const beacon = new Beacon(deadBeaconEvent); + const beacons = new Map(); + beacons.set(getBeaconInfoIdentifier(deadBeaconEvent), beacon); + const menu = createMenu(deadBeaconEvent, {}, {}, beacons); + expect(menu.find('a[aria-label="Open in OpenStreetMap"]')).toHaveLength(0); + }); + + it('allows opening a location event in open street map', () => { + const locationEvent = makeLocationEvent('geo:50,50'); + const menu = createMenu(locationEvent); + // exists with a href with the lat/lon from the location event + expect( + menu.find('a[aria-label="Open in OpenStreetMap"]').at(0).props().href, + ).toEqual('https://www.openstreetmap.org/?mlat=50&mlon=50#map=16/50/50'); + }); + + it('allows opening a beacon that has a shareable location event', () => { + const liveBeaconEvent = makeBeaconInfoEvent('@alice', roomId, { isLive: true }); + const beaconLocation = makeBeaconEvent( + '@alice', { beaconInfoId: liveBeaconEvent.getId(), geoUri: 'geo:51,41' }, + ); + const beacon = new Beacon(liveBeaconEvent); + // @ts-ignore illegally set private prop + beacon._latestLocationEvent = beaconLocation; + const beacons = new Map(); + beacons.set(getBeaconInfoIdentifier(liveBeaconEvent), beacon); + const menu = createMenu(liveBeaconEvent, {}, {}, beacons); + // exists with a href with the lat/lon from the location event + expect( + menu.find('a[aria-label="Open in OpenStreetMap"]').at(0).props().href, + ).toEqual('https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41'); + }); + }); + describe("right click", () => { it('copy button does work as expected', () => { const text = "hello"; diff --git a/test/events/forward/getForwardableEvent-test.ts b/test/events/forward/getForwardableEvent-test.ts new file mode 100644 index 00000000000..2985f527bc1 --- /dev/null +++ b/test/events/forward/getForwardableEvent-test.ts @@ -0,0 +1,87 @@ +/* +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 { + EventType, + MatrixEvent, + MsgType, +} from "matrix-js-sdk/src/matrix"; + +import { getForwardableEvent } from "../../../src/events"; +import { + getMockClientWithEventEmitter, + makeBeaconEvent, + makeBeaconInfoEvent, + makePollStartEvent, + makeRoomWithBeacons, +} from "../../test-utils"; + +describe('getForwardableEvent()', () => { + const userId = '@alice:server.org'; + const roomId = '!room:server.org'; + const client = getMockClientWithEventEmitter({ + getRoom: jest.fn(), + }); + + it('returns the event for a room message', () => { + const alicesMessageEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + room_id: roomId, + content: { + msgtype: MsgType.Text, + body: 'Hello', + }, + }); + + expect(getForwardableEvent(alicesMessageEvent, client)).toBe(alicesMessageEvent); + }); + + it('returns null for a poll start event', () => { + const pollStartEvent = makePollStartEvent('test?', userId); + + expect(getForwardableEvent(pollStartEvent, client)).toBe(null); + }); + + describe('beacons', () => { + it('returns null for a beacon that is not live', () => { + const notLiveBeacon = makeBeaconInfoEvent(userId, roomId, { isLive: false }); + makeRoomWithBeacons(roomId, client, [notLiveBeacon]); + + expect(getForwardableEvent(notLiveBeacon, client)).toBe(null); + }); + + it('returns null for a live beacon that does not have a location', () => { + const liveBeacon = makeBeaconInfoEvent(userId, roomId, { isLive: true }); + makeRoomWithBeacons(roomId, client, [liveBeacon]); + + expect(getForwardableEvent(liveBeacon, client)).toBe(null); + }); + + it('returns the latest location event for a live beacon with location', () => { + const liveBeacon = makeBeaconInfoEvent(userId, roomId, { isLive: true }, 'id'); + const locationEvent = makeBeaconEvent(userId, { + beaconInfoId: liveBeacon.getId(), + geoUri: 'geo:52,42', + // make sure its in live period + timestamp: Date.now() + 1, + }); + makeRoomWithBeacons(roomId, client, [liveBeacon], [locationEvent]); + + expect(getForwardableEvent(liveBeacon, client)).toBe(locationEvent); + }); + }); +}); diff --git a/test/events/location/getShareableLocationEvent-test.ts b/test/events/location/getShareableLocationEvent-test.ts new file mode 100644 index 00000000000..fe2d83174ca --- /dev/null +++ b/test/events/location/getShareableLocationEvent-test.ts @@ -0,0 +1,87 @@ +/* +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 { + EventType, + MatrixEvent, + MsgType, +} from "matrix-js-sdk/src/matrix"; + +import { getShareableLocationEvent } from "../../../src/events"; +import { + getMockClientWithEventEmitter, + makeBeaconEvent, + makeBeaconInfoEvent, + makeLocationEvent, + makeRoomWithBeacons, +} from "../../test-utils"; + +describe('getShareableLocationEvent()', () => { + const userId = '@alice:server.org'; + const roomId = '!room:server.org'; + const client = getMockClientWithEventEmitter({ + getRoom: jest.fn(), + }); + + it('returns null for a non-location event', () => { + const alicesMessageEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + room_id: roomId, + content: { + msgtype: MsgType.Text, + body: 'Hello', + }, + }); + + expect(getShareableLocationEvent(alicesMessageEvent, client)).toBe(null); + }); + + it('returns the event for a location event', () => { + const locationEvent = makeLocationEvent('geo:52,42'); + + expect(getShareableLocationEvent(locationEvent, client)).toBe(locationEvent); + }); + + describe('beacons', () => { + it('returns null for a beacon that is not live', () => { + const notLiveBeacon = makeBeaconInfoEvent(userId, roomId, { isLive: false }); + makeRoomWithBeacons(roomId, client, [notLiveBeacon]); + + expect(getShareableLocationEvent(notLiveBeacon, client)).toBe(null); + }); + + it('returns null for a live beacon that does not have a location', () => { + const liveBeacon = makeBeaconInfoEvent(userId, roomId, { isLive: true }); + makeRoomWithBeacons(roomId, client, [liveBeacon]); + + expect(getShareableLocationEvent(liveBeacon, client)).toBe(null); + }); + + it('returns the latest location event for a live beacon with location', () => { + const liveBeacon = makeBeaconInfoEvent(userId, roomId, { isLive: true }, 'id'); + const locationEvent = makeBeaconEvent(userId, { + beaconInfoId: liveBeacon.getId(), + geoUri: 'geo:52,42', + // make sure its in live period + timestamp: Date.now() + 1, + }); + makeRoomWithBeacons(roomId, client, [liveBeacon], [locationEvent]); + + expect(getShareableLocationEvent(liveBeacon, client)).toBe(locationEvent); + }); + }); +});