From c67b41fbde06e302e0ca296d99fbcea9f95b4a78 Mon Sep 17 00:00:00 2001 From: Kerry Date: Tue, 10 May 2022 18:21:20 +0200 Subject: [PATCH] Enable forwarding static locations (#8553) * enable forwarding for location events Signed-off-by: Kerry Archibald * allow forwarding of static locations Signed-off-by: Kerry Archibald * add comment Signed-off-by: Kerry Archibald --- .../views/dialogs/ForwardDialog.tsx | 39 ++++++-- src/utils/EventUtils.ts | 1 - .../views/dialogs/ForwardDialog-test.tsx | 98 +++++++++++++++++++ test/test-utils/location.ts | 4 +- test/utils/EventUtils-test.ts | 4 +- 5 files changed, 135 insertions(+), 11 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 100b04bc5a..bd81fa1857 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -21,6 +21,8 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { EventType } from "matrix-js-sdk/src/@types/event"; +import { ILocationContent, LocationAssetType, M_TIMESTAMP } from "matrix-js-sdk/src/@types/location"; +import { makeLocationContent } from "matrix-js-sdk/src/content-helpers"; import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; @@ -47,6 +49,8 @@ import { Action } from "../../../dispatcher/actions"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { ButtonEvent } from "../elements/AccessibleButton"; import { roomContextDetailsText } from "../../../utils/i18n-helpers"; +import { isLocationEvent } from "../../../utils/EventUtils"; +import { isSelfLocation, locationEventGeoUri } from "../../../utils/location"; const AVATAR_SIZE = 30; @@ -156,6 +160,34 @@ const Entry: React.FC = ({ room, type, content, matrixClient: cli, ; }; +const getStrippedEventContent = (event: MatrixEvent): IContent => { + const { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + "m.relates_to": _, // strip relations - in future we will attach a relation pointing at the original event + // We're taking a shallow copy here to avoid https://github.com/vector-im/element-web/issues/10924 + ...content + } = event.getContent(); + + // self location shares should have their description removed + // and become 'pin' share type + if (isLocationEvent(event) && isSelfLocation(content as ILocationContent)) { + const timestamp = M_TIMESTAMP.findIn(content); + const geoUri = locationEventGeoUri(event); + return { + ...content, + ...makeLocationContent( + undefined, // text + geoUri, + timestamp || Date.now(), + undefined, // description + LocationAssetType.Pin, + ), + }; + } + + return content; +}; + const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCreator, onFinished }) => { const userId = cli.getUserId(); const [profileInfo, setProfileInfo] = useState({}); @@ -163,12 +195,7 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr cli.getProfileInfo(userId).then(info => setProfileInfo(info)); }, [cli, userId]); - const { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - "m.relates_to": _, // strip relations - in future we will attach a relation pointing at the original event - // We're taking a shallow copy here to avoid https://github.com/vector-im/element-web/issues/10924 - ...content - } = event.getContent(); + const content = getStrippedEventContent(event); // For the message preview we fake the sender as ourselves const mockEvent = new MatrixEvent({ diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index bbfaf3f613..57df815ca3 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -277,7 +277,6 @@ export const isLocationEvent = (event: MatrixEvent): boolean => { export function canForward(event: MatrixEvent): boolean { return !( - isLocationEvent(event) || M_POLL_START.matches(event.getType()) ); } diff --git a/test/components/views/dialogs/ForwardDialog-test.tsx b/test/components/views/dialogs/ForwardDialog-test.tsx index 089a92a2b3..aedd1cfc16 100644 --- a/test/components/views/dialogs/ForwardDialog-test.tsx +++ b/test/components/views/dialogs/ForwardDialog-test.tsx @@ -18,6 +18,9 @@ import React from "react"; import { mount } from "enzyme"; import { act } from "react-dom/test-utils"; import { MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; +import { ReactWrapper } from "enzyme"; +import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "matrix-js-sdk/src/@types/location"; +import { TEXT_NODE_TYPE } from "matrix-js-sdk/src/@types/extensible_events"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import ForwardDialog from "../../../../src/components/views/dialogs/ForwardDialog"; @@ -25,10 +28,13 @@ import DMRoomMap from "../../../../src/utils/DMRoomMap"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; import { getMockClientWithEventEmitter, + makeLegacyLocationEvent, + makeLocationEvent, mkEvent, mkMessage, mkStubRoom, } from "../../../test-utils"; +import { TILE_SERVER_WK_KEY } from "../../../../src/utils/WellKnownUtils"; describe("ForwardDialog", () => { const sourceRoom = "!111111111111111111:example.org"; @@ -58,6 +64,9 @@ describe("ForwardDialog", () => { }), decryptEventIfNeeded: jest.fn(), sendEvent: jest.fn(), + getClientWellKnown: jest.fn().mockReturnValue({ + [TILE_SERVER_WK_KEY.name]: { map_style_url: 'maps.com' }, + }), }); const defaultRooms = ["a", "A", "b"].map(name => mkStubRoom(name, name, mockClient)); @@ -199,4 +208,93 @@ describe("ForwardDialog", () => { const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").last(); expect(secondButton.prop("disabled")).toBe(false); }); + + describe('Location events', () => { + // 14.03.2022 16:15 + const now = 1647270879403; + const roomId = "a"; + const geoUri = "geo:51.5076,-0.1276"; + const legacyLocationEvent = makeLegacyLocationEvent(geoUri); + const modernLocationEvent = makeLocationEvent(geoUri); + const pinDropLocationEvent = makeLocationEvent(geoUri, LocationAssetType.Pin); + + beforeEach(() => { + // legacy events will default timestamp to Date.now() + // mock a stable now for easy assertion + jest.spyOn(Date, 'now').mockReturnValue(now); + }); + + afterAll(() => { + jest.spyOn(Date, 'now').mockRestore(); + }); + + const sendToFirstRoom = (wrapper: ReactWrapper): void => + act(() => { + const sendToFirstRoomButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first(); + sendToFirstRoomButton.simulate("click"); + }); + + it('converts legacy location events to pin drop shares', async () => { + const wrapper = await mountForwardDialog(legacyLocationEvent); + + expect(wrapper.find('MLocationBody').length).toBeTruthy(); + sendToFirstRoom(wrapper); + + // text and description from original event are removed + // text gets new default message from event values + // timestamp is defaulted to now + const text = `Location ${geoUri} at ${new Date(now).toISOString()}`; + const expectedStrippedContent = { + ...modernLocationEvent.getContent(), + body: text, + [TEXT_NODE_TYPE.name]: text, + [M_TIMESTAMP.name]: now, + [M_ASSET.name]: { type: LocationAssetType.Pin }, + [M_LOCATION.name]: { + uri: geoUri, + description: undefined, + }, + }; + expect(mockClient.sendEvent).toHaveBeenCalledWith( + roomId, legacyLocationEvent.getType(), expectedStrippedContent, + ); + }); + + it('removes personal information from static self location shares', async () => { + const wrapper = await mountForwardDialog(modernLocationEvent); + + expect(wrapper.find('MLocationBody').length).toBeTruthy(); + sendToFirstRoom(wrapper); + + const timestamp = M_TIMESTAMP.findIn(modernLocationEvent.getContent()); + // text and description from original event are removed + // text gets new default message from event values + const text = `Location ${geoUri} at ${new Date(timestamp).toISOString()}`; + const expectedStrippedContent = { + ...modernLocationEvent.getContent(), + body: text, + [TEXT_NODE_TYPE.name]: text, + [M_ASSET.name]: { type: LocationAssetType.Pin }, + [M_LOCATION.name]: { + uri: geoUri, + description: undefined, + }, + }; + expect(mockClient.sendEvent).toHaveBeenCalledWith( + roomId, modernLocationEvent.getType(), expectedStrippedContent, + ); + }); + + it('forwards pin drop event', async () => { + const wrapper = await mountForwardDialog(pinDropLocationEvent); + + expect(wrapper.find('MLocationBody').length).toBeTruthy(); + + sendToFirstRoom(wrapper); + + expect(mockClient.sendEvent).toHaveBeenCalledWith( + roomId, pinDropLocationEvent.getType(), pinDropLocationEvent.getContent(), + ); + }); + }); }); diff --git a/test/test-utils/location.ts b/test/test-utils/location.ts index 05bba66958..39d84ef3d6 100644 --- a/test/test-utils/location.ts +++ b/test/test-utils/location.ts @@ -16,14 +16,14 @@ limitations under the License. import { LocationAssetType, M_LOCATION } from "matrix-js-sdk/src/@types/location"; import { makeLocationContent } from "matrix-js-sdk/src/content-helpers"; -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; let id = 1; export const makeLegacyLocationEvent = (geoUri: string): MatrixEvent => { return new MatrixEvent( { "event_id": `$${++id}`, - "type": M_LOCATION.name, + "type": EventType.RoomMessage, "content": { "body": "Something about where I am", "msgtype": "m.location", diff --git a/test/utils/EventUtils-test.ts b/test/utils/EventUtils-test.ts index 674162f548..df65b7e5f3 100644 --- a/test/utils/EventUtils-test.ts +++ b/test/utils/EventUtils-test.ts @@ -315,11 +315,11 @@ describe('EventUtils', () => { }); describe('canForward()', () => { - it('returns false for a location event', () => { + it('returns true for a location event', () => { const event = new MatrixEvent({ type: M_LOCATION.name, }); - expect(canForward(event)).toBe(false); + expect(canForward(event)).toBe(true); }); it('returns false for a poll event', () => { const event = makePollStartEvent('Who?', userId);