From 4486509d881d8bc22654a459edfa9d7c89ebf017 Mon Sep 17 00:00:00 2001 From: Kerry Date: Wed, 29 Jun 2022 11:07:36 +0200 Subject: [PATCH] Live location share - redact related locations on beacon redaction (PSF-1151) (#8926) * redact beacon locations on redaction * redact beacon locations on beacon info redaction * fussy import ordering * use real fake redaction in beaconbody test --- src/components/views/messages/MBeaconBody.tsx | 48 ++++++- .../__snapshots__/BeaconMarker-test.tsx.snap | 1 + .../views/messages/MBeaconBody-test.tsx | 125 ++++++++++++++++++ test/test-utils/beacon.ts | 2 + 4 files changed, 172 insertions(+), 4 deletions(-) diff --git a/src/components/views/messages/MBeaconBody.tsx b/src/components/views/messages/MBeaconBody.tsx index ddb700c07a..fda27633ec 100644 --- a/src/components/views/messages/MBeaconBody.tsx +++ b/src/components/views/messages/MBeaconBody.tsx @@ -14,10 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useContext, useEffect, useState } from 'react'; -import { Beacon, BeaconEvent, MatrixEvent } from 'matrix-js-sdk/src/matrix'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import { + Beacon, + BeaconEvent, + MatrixEvent, + MatrixEventEvent, + MatrixClient, + RelationType, +} from 'matrix-js-sdk/src/matrix'; import { BeaconLocationState } from 'matrix-js-sdk/src/content-helpers'; import { randomString } from 'matrix-js-sdk/src/randomstring'; +import { M_BEACON } from 'matrix-js-sdk/src/@types/beacon'; import MatrixClientContext from '../../../contexts/MatrixClientContext'; import { useEventEmitterState } from '../../../hooks/useEventEmitter'; @@ -27,10 +35,11 @@ import { isBeaconWaitingToStart, useBeacon } from '../../../utils/beacon'; import { isSelfLocation } from '../../../utils/location'; import { BeaconDisplayStatus, getBeaconDisplayStatus } from '../beacon/displayStatus'; import BeaconStatus from '../beacon/BeaconStatus'; +import OwnBeaconStatus from '../beacon/OwnBeaconStatus'; import Map from '../location/Map'; import MapFallback from '../location/MapFallback'; import SmartMarker from '../location/SmartMarker'; -import OwnBeaconStatus from '../beacon/OwnBeaconStatus'; +import { GetRelationsForEvent } from '../rooms/EventTile'; import BeaconViewDialog from '../beacon/BeaconViewDialog'; import { IBodyProps } from "./IBodyProps"; @@ -87,7 +96,36 @@ const useUniqueId = (eventId: string): string => { return id; }; -const MBeaconBody: React.FC = React.forwardRef(({ mxEvent }, ref) => { +// remove related beacon locations on beacon redaction +const useHandleBeaconRedaction = ( + event: MatrixEvent, + getRelationsForEvent: GetRelationsForEvent, + cli: MatrixClient, +): void => { + const onBeforeBeaconInfoRedaction = useCallback((_event: MatrixEvent, redactionEvent: MatrixEvent) => { + const relations = getRelationsForEvent ? + getRelationsForEvent(event.getId(), RelationType.Reference, M_BEACON.name) : + undefined; + + relations?.getRelations()?.forEach(locationEvent => { + cli.redactEvent( + locationEvent.getRoomId(), + locationEvent.getId(), + undefined, + redactionEvent.getContent(), + ); + }); + }, [event, getRelationsForEvent, cli]); + + useEffect(() => { + event.addListener(MatrixEventEvent.BeforeRedaction, onBeforeBeaconInfoRedaction); + return () => { + event.removeListener(MatrixEventEvent.BeforeRedaction, onBeforeBeaconInfoRedaction); + }; + }, [event, onBeforeBeaconInfoRedaction]); +}; + +const MBeaconBody: React.FC = React.forwardRef(({ mxEvent, getRelationsForEvent }, ref) => { const { beacon, isLive, @@ -102,6 +140,8 @@ const MBeaconBody: React.FC = React.forwardRef(({ mxEvent }, ref) => const markerRoomMember = isSelfLocation(mxEvent.getContent()) ? mxEvent.sender : undefined; const isOwnBeacon = beacon?.beaconInfoOwner === matrixClient.getUserId(); + useHandleBeaconRedaction(mxEvent, getRelationsForEvent, matrixClient); + const onClick = () => { if (displayStatus !== BeaconDisplayStatus.Active) { return; diff --git a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap index 6a763ddc53..291a64349f 100644 --- a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap @@ -38,6 +38,7 @@ exports[` renders marker when beacon has location 1`] = ` }, "org.matrix.msc3488.ts": 1647270879404, }, + "room_id": undefined, "sender": "@alice:server", "type": "org.matrix.msc3672.beacon", }, diff --git a/test/components/views/messages/MBeaconBody-test.tsx b/test/components/views/messages/MBeaconBody-test.tsx index afcb604534..c38e145a9f 100644 --- a/test/components/views/messages/MBeaconBody-test.tsx +++ b/test/components/views/messages/MBeaconBody-test.tsx @@ -21,7 +21,12 @@ import maplibregl from 'maplibre-gl'; import { BeaconEvent, getBeaconInfoIdentifier, + RelationType, + MatrixEvent, + EventType, } from 'matrix-js-sdk/src/matrix'; +import { Relations } from 'matrix-js-sdk/src/models/relations'; +import { M_BEACON } from 'matrix-js-sdk/src/@types/beacon'; import MBeaconBody from '../../../../src/components/views/messages/MBeaconBody'; import { @@ -53,6 +58,7 @@ describe('', () => { }), getUserId: jest.fn().mockReturnValue(aliceId), getRoom: jest.fn(), + redactEvent: jest.fn(), }); const defaultEvent = makeBeaconInfoEvent(aliceId, @@ -333,4 +339,123 @@ describe('', () => { expect(mockMarker.setLngLat).toHaveBeenCalledWith({ lat: 52, lon: 42 }); }); }); + + describe('redaction', () => { + const makeEvents = (): { + beaconInfoEvent: MatrixEvent; + location1: MatrixEvent; + location2: MatrixEvent; + } => { + const beaconInfoEvent = makeBeaconInfoEvent( + aliceId, + roomId, + { isLive: true }, + '$alice-room1-1', + ); + + const location1 = makeBeaconEvent( + aliceId, { beaconInfoId: beaconInfoEvent.getId(), geoUri: 'geo:51,41', timestamp: now + 1 }, + roomId, + ); + location1.event.event_id = '1'; + const location2 = makeBeaconEvent( + aliceId, { beaconInfoId: beaconInfoEvent.getId(), geoUri: 'geo:52,42', timestamp: now + 10000 }, + roomId, + ); + location2.event.event_id = '2'; + return { beaconInfoEvent, location1, location2 }; + }; + + const redactionEvent = new MatrixEvent({ type: EventType.RoomRedaction, content: { reason: 'test reason' } }); + + const setupRoomWithBeacon = (beaconInfoEvent, locationEvents: MatrixEvent[] = []) => { + const room = makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient }); + const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(beaconInfoEvent)); + beaconInstance.addLocations(locationEvents); + }; + const mockGetRelationsForEvent = (locationEvents: MatrixEvent[] = []) => { + const relations = new Relations(RelationType.Reference, M_BEACON.name, mockClient); + jest.spyOn(relations, 'getRelations').mockReturnValue(locationEvents); + + const getRelationsForEvent = jest.fn().mockReturnValue(relations); + + return getRelationsForEvent; + }; + + it('does nothing when getRelationsForEvent is falsy', () => { + const { beaconInfoEvent, location1, location2 } = makeEvents(); + setupRoomWithBeacon(beaconInfoEvent, [location1, location2]); + + getComponent({ mxEvent: beaconInfoEvent }); + + act(() => { + beaconInfoEvent.makeRedacted(redactionEvent); + }); + + // no error, no redactions + expect(mockClient.redactEvent).not.toHaveBeenCalled(); + }); + + it('cleans up redaction listener on unmount', () => { + const { beaconInfoEvent, location1, location2 } = makeEvents(); + setupRoomWithBeacon(beaconInfoEvent, [location1, location2]); + const removeListenerSpy = jest.spyOn(beaconInfoEvent, 'removeListener'); + + const component = getComponent({ mxEvent: beaconInfoEvent }); + + act(() => { + component.unmount(); + }); + + expect(removeListenerSpy).toHaveBeenCalled(); + }); + + it('does nothing when beacon has no related locations', async () => { + const { beaconInfoEvent } = makeEvents(); + // no locations + setupRoomWithBeacon(beaconInfoEvent, []); + const getRelationsForEvent = await mockGetRelationsForEvent(); + + getComponent({ mxEvent: beaconInfoEvent, getRelationsForEvent }); + + act(() => { + beaconInfoEvent.makeRedacted(redactionEvent); + }); + + expect(getRelationsForEvent).toHaveBeenCalledWith( + beaconInfoEvent.getId(), RelationType.Reference, M_BEACON.name, + ); + expect(mockClient.redactEvent).not.toHaveBeenCalled(); + }); + + it('redacts related locations on beacon redaction', async () => { + const { beaconInfoEvent, location1, location2 } = makeEvents(); + setupRoomWithBeacon(beaconInfoEvent, [location1, location2]); + + const getRelationsForEvent = await mockGetRelationsForEvent([location1, location2]); + + getComponent({ mxEvent: beaconInfoEvent, getRelationsForEvent }); + + act(() => { + beaconInfoEvent.makeRedacted(redactionEvent); + }); + + expect(getRelationsForEvent).toHaveBeenCalledWith( + beaconInfoEvent.getId(), RelationType.Reference, M_BEACON.name, + ); + expect(mockClient.redactEvent).toHaveBeenCalledTimes(2); + expect(mockClient.redactEvent).toHaveBeenCalledWith( + roomId, + location1.getId(), + undefined, + { reason: 'test reason' }, + ); + expect(mockClient.redactEvent).toHaveBeenCalledWith( + roomId, + location2.getId(), + undefined, + { reason: 'test reason' }, + ); + }); + }); }); diff --git a/test/test-utils/beacon.ts b/test/test-utils/beacon.ts index 3eee57b39f..9501cd2cb3 100644 --- a/test/test-utils/beacon.ts +++ b/test/test-utils/beacon.ts @@ -97,6 +97,7 @@ const DEFAULT_CONTENT_PROPS: ContentProps = { export const makeBeaconEvent = ( sender: string, contentProps: Partial = {}, + roomId?: string, ): MatrixEvent => { const { geoUri, timestamp, beaconInfoId, description } = { ...DEFAULT_CONTENT_PROPS, @@ -105,6 +106,7 @@ export const makeBeaconEvent = ( return new MatrixEvent({ type: M_BEACON.name, + room_id: roomId, sender, content: makeBeaconContent(geoUri, timestamp, beaconInfoId, description), });