From 8bff5bbac818330295e497c8f5ce2ee5fde297d8 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 4 Jul 2022 11:20:50 +0200 Subject: [PATCH 1/6] move getForwardableBeacon to beacon utils --- .../beacon/getShareableLocation.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{events/forward/getForwardableBeacon.ts => utils/beacon/getShareableLocation.ts} (100%) diff --git a/src/events/forward/getForwardableBeacon.ts b/src/utils/beacon/getShareableLocation.ts similarity index 100% rename from src/events/forward/getForwardableBeacon.ts rename to src/utils/beacon/getShareableLocation.ts From fb8ab1685aff39dfb5eece7c54c213e9e4493cab Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 4 Jul 2022 11:59:08 +0200 Subject: [PATCH 2/6] move event transform type up --- src/events/{forward => }/types.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/events/{forward => }/types.ts (100%) diff --git a/src/events/forward/types.ts b/src/events/types.ts similarity index 100% rename from src/events/forward/types.ts rename to src/events/types.ts From 3a77295e0e4d3feaf5c3694f8d2f6174326d376a Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 4 Jul 2022 11:59:23 +0200 Subject: [PATCH 3/6] add helper to get shareable-as-locaion events --- src/events/forward/getForwardableEvent.ts | 7 ++-- src/events/forward/types.ts | 19 ++++++++++ src/events/index.ts | 18 ++++++++++ .../location/getShareableLocationEvent.ts | 36 +++++++++++++++++++ src/events/types.ts | 2 +- 5 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 src/events/forward/types.ts create mode 100644 src/events/index.ts create mode 100644 src/events/location/getShareableLocationEvent.ts diff --git a/src/events/forward/getForwardableEvent.ts b/src/events/forward/getForwardableEvent.ts index d1d78a469ce..dea03eae50d 100644 --- a/src/events/forward/getForwardableEvent.ts +++ b/src/events/forward/getForwardableEvent.ts @@ -18,7 +18,7 @@ import { M_POLL_START } from "matrix-events-sdk"; import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon"; import { MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix"; -import { getForwardableBeaconEvent } from "./getForwardableBeacon"; +import { getShareableLocationEventForBeacon } from "../../utils/beacon/getShareableLocation"; /** * Get forwardable event for a given event @@ -28,8 +28,11 @@ export const getForwardableEvent = (event: MatrixEvent, cli: MatrixClient): Matr if (M_POLL_START.matches(event.getType())) { return null; } + + // 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 if (M_BEACON_INFO.matches(event.getType())) { - return getForwardableBeaconEvent(event, cli); + return getShareableLocationEventForBeacon(event, cli); } return event; }; diff --git a/src/events/forward/types.ts b/src/events/forward/types.ts new file mode 100644 index 00000000000..f30b3144814 --- /dev/null +++ b/src/events/forward/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/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 index 531253c725a..f30b3144814 100644 --- a/src/events/types.ts +++ b/src/events/types.ts @@ -16,4 +16,4 @@ limitations under the License. import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; -export type ForwardableEventTransformFunction = (event: MatrixEvent, cli: MatrixClient) => MatrixEvent | null; +export type ActionableEventTransformFunction = (event: MatrixEvent, cli: MatrixClient) => MatrixEvent | null; From d2fb0666f7f79e5ad0156fbeee476c4071f11746 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 4 Jul 2022 11:59:41 +0200 Subject: [PATCH 4/6] use getShareableLocationEvent in MessageContextMenu --- .../context_menus/MessageContextMenu.tsx | 11 +-- src/utils/beacon/getShareableLocation.ts | 15 ++-- .../forward/getForwardableEvent-test.ts | 87 +++++++++++++++++++ .../getShareableLocationEvent-test.ts | 87 +++++++++++++++++++ 4 files changed, 187 insertions(+), 13 deletions(-) create mode 100644 test/events/forward/getForwardableEvent-test.ts create mode 100644 test/events/location/getShareableLocationEvent-test.ts 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 = ( { +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/events/forward/getForwardableEvent-test.ts b/test/events/forward/getForwardableEvent-test.ts new file mode 100644 index 00000000000..cf2d125fd7e --- /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 live + timestamp: Date.now() - 1, + }); + makeRoomWithBeacons(roomId, client, [liveBeacon], [locationEvent]); + + expect(getForwardableEvent(liveBeacon, client)).toBe(null); + }); + }); +}); diff --git a/test/events/location/getShareableLocationEvent-test.ts b/test/events/location/getShareableLocationEvent-test.ts new file mode 100644 index 00000000000..0543f52b802 --- /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 live + timestamp: Date.now() - 1, + }); + makeRoomWithBeacons(roomId, client, [liveBeacon], [locationEvent]); + + expect(getShareableLocationEvent(liveBeacon, client)).toBe(null); + }); + }); +}); From 14956facf6a3b244572d79b1964a5b6accbf65cf Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 4 Jul 2022 12:15:41 +0200 Subject: [PATCH 5/6] test opening in maplink --- .../context_menus/MessageContextMenu-test.tsx | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) 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"; From a707337cd1d8532a27e6fc3688835f1c35f358bd Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 4 Jul 2022 14:49:47 +0200 Subject: [PATCH 6/6] fix bad copy pasted tests --- test/events/forward/getForwardableEvent-test.ts | 6 +++--- test/events/location/getShareableLocationEvent-test.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/events/forward/getForwardableEvent-test.ts b/test/events/forward/getForwardableEvent-test.ts index cf2d125fd7e..2985f527bc1 100644 --- a/test/events/forward/getForwardableEvent-test.ts +++ b/test/events/forward/getForwardableEvent-test.ts @@ -76,12 +76,12 @@ describe('getForwardableEvent()', () => { const locationEvent = makeBeaconEvent(userId, { beaconInfoId: liveBeacon.getId(), geoUri: 'geo:52,42', - // make sure its live - timestamp: Date.now() - 1, + // make sure its in live period + timestamp: Date.now() + 1, }); makeRoomWithBeacons(roomId, client, [liveBeacon], [locationEvent]); - expect(getForwardableEvent(liveBeacon, client)).toBe(null); + expect(getForwardableEvent(liveBeacon, client)).toBe(locationEvent); }); }); }); diff --git a/test/events/location/getShareableLocationEvent-test.ts b/test/events/location/getShareableLocationEvent-test.ts index 0543f52b802..fe2d83174ca 100644 --- a/test/events/location/getShareableLocationEvent-test.ts +++ b/test/events/location/getShareableLocationEvent-test.ts @@ -76,12 +76,12 @@ describe('getShareableLocationEvent()', () => { const locationEvent = makeBeaconEvent(userId, { beaconInfoId: liveBeacon.getId(), geoUri: 'geo:52,42', - // make sure its live - timestamp: Date.now() - 1, + // make sure its in live period + timestamp: Date.now() + 1, }); makeRoomWithBeacons(roomId, client, [liveBeacon], [locationEvent]); - expect(getShareableLocationEvent(liveBeacon, client)).toBe(null); + expect(getShareableLocationEvent(liveBeacon, client)).toBe(locationEvent); }); }); });