Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit eaf13d4

Browse files
author
Kerry
authored
Live location share - disallow message pinning (PSF-1084) (#8928)
* unmock isContentActionable * test message pinning * disallow pinning for beacon events * try to make tests more readable
1 parent 035786a commit eaf13d4

File tree

4 files changed

+185
-29
lines changed

4 files changed

+185
-29
lines changed

src/components/views/context_menus/MessageContextMenu.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
Copyright 2019 Michael Telatynski <[email protected]>
3-
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
3+
Copyright 2015 - 2022 The Matrix.org Foundation C.I.C.
44
Copyright 2021 - 2022 Šimon Brandner <[email protected]>
55
66
Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,7 +30,13 @@ import Modal from '../../../Modal';
3030
import Resend from '../../../Resend';
3131
import SettingsStore from '../../../settings/SettingsStore';
3232
import { isUrlPermitted } from '../../../HtmlUtils';
33-
import { canEditContent, editEvent, isContentActionable, isLocationEvent } from '../../../utils/EventUtils';
33+
import {
34+
canEditContent,
35+
canPinEvent,
36+
editEvent,
37+
isContentActionable,
38+
isLocationEvent,
39+
} from '../../../utils/EventUtils';
3440
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu';
3541
import { ReadPinsEventId } from "../right_panel/types";
3642
import { Action } from "../../../dispatcher/actions";
@@ -121,7 +127,9 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
121127
const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId)
122128
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl
123129
&& this.props.mxEvent.getType() !== EventType.RoomEncryption;
124-
let canPin = room.currentState.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli);
130+
131+
let canPin = room.currentState.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli) &&
132+
canPinEvent(this.props.mxEvent);
125133

126134
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
127135
if (!SettingsStore.getValue("feature_pinning")) canPin = false;
@@ -204,6 +212,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
204212
const eventId = this.props.mxEvent.getId();
205213

206214
const pinnedIds = room?.currentState?.getStateEvents(EventType.RoomPinnedEvents, "")?.getContent().pinned || [];
215+
207216
if (pinnedIds.includes(eventId)) {
208217
pinnedIds.splice(pinnedIds.indexOf(eventId), 1);
209218
} else {

src/events/forward/getForwardableBeacon.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ import { ForwardableEventTransformFunction } from "./types";
2525
export const getForwardableBeaconEvent: ForwardableEventTransformFunction = (event, cli) => {
2626
const room = cli.getRoom(event.getRoomId());
2727
const beacon = room.currentState.beacons?.get(getBeaconInfoIdentifier(event));
28-
const latestLocationEvent = beacon.latestLocationEvent;
28+
const latestLocationEvent = beacon?.latestLocationEvent;
2929

30-
if (beacon.isLive && latestLocationEvent) {
30+
if (beacon?.isLive && latestLocationEvent) {
3131
return latestLocationEvent;
3232
}
3333
return null;

src/utils/EventUtils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,3 +284,7 @@ export const isLocationEvent = (event: MatrixEvent): boolean => {
284284
export function hasThreadSummary(event: MatrixEvent): boolean {
285285
return event.isThreadRoot && event.getThread()?.length && !!event.getThread().replyToEvent;
286286
}
287+
288+
export function canPinEvent(event: MatrixEvent): boolean {
289+
return !M_BEACON_INFO.matches(event.getType());
290+
}

test/components/views/context_menus/MessageContextMenu-test.tsx

Lines changed: 167 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,37 +23,40 @@ import {
2323
BeaconIdentifier,
2424
Beacon,
2525
getBeaconInfoIdentifier,
26+
EventType,
2627
} from 'matrix-js-sdk/src/matrix';
2728
import { ExtensibleEvent, MessageEvent, M_POLL_KIND_DISCLOSED, PollStartEvent } from 'matrix-events-sdk';
2829
import { Thread } from "matrix-js-sdk/src/models/thread";
2930
import { mocked } from "jest-mock";
3031
import { act } from '@testing-library/react';
3132

32-
import * as TestUtils from '../../../test-utils';
3333
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
3434
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
3535
import { IRoomState } from "../../../../src/components/structures/RoomView";
36-
import { canEditContent, isContentActionable } from "../../../../src/utils/EventUtils";
36+
import { canEditContent } from "../../../../src/utils/EventUtils";
3737
import { copyPlaintext, getSelectedText } from "../../../../src/utils/strings";
3838
import MessageContextMenu from "../../../../src/components/views/context_menus/MessageContextMenu";
39-
import { makeBeaconEvent, makeBeaconInfoEvent } from '../../../test-utils';
39+
import { makeBeaconEvent, makeBeaconInfoEvent, stubClient } from '../../../test-utils';
4040
import dispatcher from '../../../../src/dispatcher/dispatcher';
41+
import SettingsStore from '../../../../src/settings/SettingsStore';
42+
import { ReadPinsEventId } from '../../../../src/components/views/right_panel/types';
4143

4244
jest.mock("../../../../src/utils/strings", () => ({
4345
copyPlaintext: jest.fn(),
4446
getSelectedText: jest.fn(),
4547
}));
4648
jest.mock("../../../../src/utils/EventUtils", () => ({
49+
// @ts-ignore don't mock everything
50+
...jest.requireActual("../../../../src/utils/EventUtils"),
4751
canEditContent: jest.fn(),
48-
isContentActionable: jest.fn(),
49-
isLocationEvent: jest.fn(),
5052
}));
5153

5254
const roomId = 'roomid';
5355

5456
describe('MessageContextMenu', () => {
5557
beforeEach(() => {
5658
jest.resetAllMocks();
59+
stubClient();
5760
});
5861

5962
it('does show copy link button when supplied a link', () => {
@@ -74,10 +77,151 @@ describe('MessageContextMenu', () => {
7477
expect(copyLinkButton).toHaveLength(0);
7578
});
7679

80+
describe('message pinning', () => {
81+
beforeEach(() => {
82+
jest.spyOn(SettingsStore, 'getValue').mockReturnValue(true);
83+
});
84+
85+
afterAll(() => {
86+
jest.spyOn(SettingsStore, 'getValue').mockRestore();
87+
});
88+
89+
it('does not show pin option when user does not have rights to pin', () => {
90+
const eventContent = MessageEvent.from("hello");
91+
const event = new MatrixEvent(eventContent.serialize());
92+
93+
const room = makeDefaultRoom();
94+
// mock permission to disallow adding pinned messages to room
95+
jest.spyOn(room.currentState, 'mayClientSendStateEvent').mockReturnValue(false);
96+
97+
const menu = createMenu(event, {}, {}, undefined, room);
98+
99+
expect(menu.find('div[aria-label="Pin"]')).toHaveLength(0);
100+
});
101+
102+
it('does not show pin option for beacon_info event', () => {
103+
const deadBeaconEvent = makeBeaconInfoEvent('@alice:server.org', roomId, { isLive: false });
104+
105+
const room = makeDefaultRoom();
106+
// mock permission to allow adding pinned messages to room
107+
jest.spyOn(room.currentState, 'mayClientSendStateEvent').mockReturnValue(true);
108+
109+
const menu = createMenu(deadBeaconEvent, {}, {}, undefined, room);
110+
111+
expect(menu.find('div[aria-label="Pin"]')).toHaveLength(0);
112+
});
113+
114+
it('does not show pin option when pinning feature is disabled', () => {
115+
const eventContent = MessageEvent.from("hello");
116+
const pinnableEvent = new MatrixEvent({ ...eventContent.serialize(), room_id: roomId });
117+
118+
const room = makeDefaultRoom();
119+
// mock permission to allow adding pinned messages to room
120+
jest.spyOn(room.currentState, 'mayClientSendStateEvent').mockReturnValue(true);
121+
// disable pinning feature
122+
jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false);
123+
124+
const menu = createMenu(pinnableEvent, {}, {}, undefined, room);
125+
126+
expect(menu.find('div[aria-label="Pin"]')).toHaveLength(0);
127+
});
128+
129+
it('shows pin option when pinning feature is enabled', () => {
130+
const eventContent = MessageEvent.from("hello");
131+
const pinnableEvent = new MatrixEvent({ ...eventContent.serialize(), room_id: roomId });
132+
133+
const room = makeDefaultRoom();
134+
// mock permission to allow adding pinned messages to room
135+
jest.spyOn(room.currentState, 'mayClientSendStateEvent').mockReturnValue(true);
136+
137+
const menu = createMenu(pinnableEvent, {}, {}, undefined, room);
138+
139+
expect(menu.find('div[aria-label="Pin"]')).toHaveLength(1);
140+
});
141+
142+
it('pins event on pin option click', () => {
143+
const onFinished = jest.fn();
144+
const eventContent = MessageEvent.from("hello");
145+
const pinnableEvent = new MatrixEvent({ ...eventContent.serialize(), room_id: roomId });
146+
pinnableEvent.event.event_id = '!3';
147+
const client = MatrixClientPeg.get();
148+
const room = makeDefaultRoom();
149+
150+
// mock permission to allow adding pinned messages to room
151+
jest.spyOn(room.currentState, 'mayClientSendStateEvent').mockReturnValue(true);
152+
153+
// mock read pins account data
154+
const pinsAccountData = new MatrixEvent({ content: { event_ids: ['!1', '!2'] } });
155+
jest.spyOn(room, 'getAccountData').mockReturnValue(pinsAccountData);
156+
157+
const menu = createMenu(pinnableEvent, { onFinished }, {}, undefined, room);
158+
159+
act(() => {
160+
menu.find('div[aria-label="Pin"]').simulate('click');
161+
});
162+
163+
// added to account data
164+
expect(client.setRoomAccountData).toHaveBeenCalledWith(
165+
roomId,
166+
ReadPinsEventId,
167+
{ event_ids: [
168+
// from account data
169+
'!1', '!2',
170+
pinnableEvent.getId(),
171+
],
172+
},
173+
);
174+
175+
// add to room's pins
176+
expect(client.sendStateEvent).toHaveBeenCalledWith(roomId, EventType.RoomPinnedEvents, {
177+
pinned: [pinnableEvent.getId()] }, "");
178+
179+
expect(onFinished).toHaveBeenCalled();
180+
});
181+
182+
it('unpins event on pin option click when event is pinned', () => {
183+
const eventContent = MessageEvent.from("hello");
184+
const pinnableEvent = new MatrixEvent({ ...eventContent.serialize(), room_id: roomId });
185+
pinnableEvent.event.event_id = '!3';
186+
const client = MatrixClientPeg.get();
187+
const room = makeDefaultRoom();
188+
189+
// make the event already pinned in the room
190+
const pinEvent = new MatrixEvent({
191+
type: EventType.RoomPinnedEvents,
192+
room_id: roomId,
193+
state_key: "",
194+
content: { pinned: [pinnableEvent.getId(), '!another-event'] },
195+
});
196+
room.currentState.setStateEvents([pinEvent]);
197+
198+
// mock permission to allow adding pinned messages to room
199+
jest.spyOn(room.currentState, 'mayClientSendStateEvent').mockReturnValue(true);
200+
201+
// mock read pins account data
202+
const pinsAccountData = new MatrixEvent({ content: { event_ids: ['!1', '!2'] } });
203+
jest.spyOn(room, 'getAccountData').mockReturnValue(pinsAccountData);
204+
205+
const menu = createMenu(pinnableEvent, {}, {}, undefined, room);
206+
207+
act(() => {
208+
menu.find('div[aria-label="Unpin"]').simulate('click');
209+
});
210+
211+
expect(client.setRoomAccountData).not.toHaveBeenCalled();
212+
213+
// add to room's pins
214+
expect(client.sendStateEvent).toHaveBeenCalledWith(
215+
roomId, EventType.RoomPinnedEvents,
216+
// pinnableEvent's id removed, other pins intact
217+
{ pinned: ['!another-event'] },
218+
"",
219+
);
220+
});
221+
});
222+
77223
describe('message forwarding', () => {
78224
it('allows forwarding a room message', () => {
79-
mocked(isContentActionable).mockReturnValue(true);
80-
81225
const eventContent = MessageEvent.from("hello");
82226
const menu = createMenuWithContent(eventContent);
83227
expect(menu.find('div[aria-label="Forward"]')).toHaveLength(1);
@@ -91,9 +235,6 @@ describe('MessageContextMenu', () => {
91235

92236
describe('forwarding beacons', () => {
93237
const aliceId = "@alice:server.org";
94-
beforeEach(() => {
95-
mocked(isContentActionable).mockReturnValue(true);
96-
});
97238

98239
it('does not allow forwarding a beacon that is not live', () => {
99240
const deadBeaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: false });
@@ -212,7 +353,6 @@ describe('MessageContextMenu', () => {
212353
const context = {
213354
canSendMessages: true,
214355
};
215-
mocked(isContentActionable).mockReturnValue(true);
216356

217357
const menu = createRightClickMenuWithContent(eventContent, context);
218358
const replyButton = menu.find('div[aria-label="Reply"]');
@@ -224,9 +364,11 @@ describe('MessageContextMenu', () => {
224364
const context = {
225365
canSendMessages: true,
226366
};
227-
mocked(isContentActionable).mockReturnValue(false);
367+
const unsentMessage = new MatrixEvent(eventContent.serialize());
368+
// queued messages are not actionable
369+
unsentMessage.setStatus(EventStatus.QUEUED);
228370

229-
const menu = createRightClickMenuWithContent(eventContent, context);
371+
const menu = createMenu(unsentMessage, {}, context);
230372
const replyButton = menu.find('div[aria-label="Reply"]');
231373
expect(replyButton).toHaveLength(0);
232374
});
@@ -236,7 +378,6 @@ describe('MessageContextMenu', () => {
236378
const context = {
237379
canReact: true,
238380
};
239-
mocked(isContentActionable).mockReturnValue(true);
240381

241382
const menu = createRightClickMenuWithContent(eventContent, context);
242383
const reactButton = menu.find('div[aria-label="React"]');
@@ -296,24 +437,26 @@ function createMenuWithContent(
296437
return createMenu(mxEvent, props, context);
297438
}
298439

440+
function makeDefaultRoom(): Room {
441+
return new Room(
442+
roomId,
443+
MatrixClientPeg.get(),
444+
"@user:example.com",
445+
{
446+
pendingEventOrdering: PendingEventOrdering.Detached,
447+
},
448+
);
449+
}
450+
299451
function createMenu(
300452
mxEvent: MatrixEvent,
301453
props?: Partial<React.ComponentProps<typeof MessageContextMenu>>,
302454
context: Partial<IRoomState> = {},
303455
beacons: Map<BeaconIdentifier, Beacon> = new Map(),
456+
room: Room = makeDefaultRoom(),
304457
): ReactWrapper {
305-
TestUtils.stubClient();
306458
const client = MatrixClientPeg.get();
307459

308-
const room = new Room(
309-
roomId,
310-
client,
311-
"@user:example.com",
312-
{
313-
pendingEventOrdering: PendingEventOrdering.Detached,
314-
},
315-
);
316-
317460
// @ts-ignore illegally set private prop
318461
room.currentState.beacons = beacons;
319462

0 commit comments

Comments
 (0)