@@ -23,37 +23,40 @@ import {
2323 BeaconIdentifier ,
2424 Beacon ,
2525 getBeaconInfoIdentifier ,
26+ EventType ,
2627} from 'matrix-js-sdk/src/matrix' ;
2728import { ExtensibleEvent , MessageEvent , M_POLL_KIND_DISCLOSED , PollStartEvent } from 'matrix-events-sdk' ;
2829import { Thread } from "matrix-js-sdk/src/models/thread" ;
2930import { mocked } from "jest-mock" ;
3031import { act } from '@testing-library/react' ;
3132
32- import * as TestUtils from '../../../test-utils' ;
3333import { MatrixClientPeg } from '../../../../src/MatrixClientPeg' ;
3434import RoomContext , { TimelineRenderingType } from "../../../../src/contexts/RoomContext" ;
3535import { IRoomState } from "../../../../src/components/structures/RoomView" ;
36- import { canEditContent , isContentActionable } from "../../../../src/utils/EventUtils" ;
36+ import { canEditContent } from "../../../../src/utils/EventUtils" ;
3737import { copyPlaintext , getSelectedText } from "../../../../src/utils/strings" ;
3838import MessageContextMenu from "../../../../src/components/views/context_menus/MessageContextMenu" ;
39- import { makeBeaconEvent , makeBeaconInfoEvent } from '../../../test-utils' ;
39+ import { makeBeaconEvent , makeBeaconInfoEvent , stubClient } from '../../../test-utils' ;
4040import dispatcher from '../../../../src/dispatcher/dispatcher' ;
41+ import SettingsStore from '../../../../src/settings/SettingsStore' ;
42+ import { ReadPinsEventId } from '../../../../src/components/views/right_panel/types' ;
4143
4244jest . mock ( "../../../../src/utils/strings" , ( ) => ( {
4345 copyPlaintext : jest . fn ( ) ,
4446 getSelectedText : jest . fn ( ) ,
4547} ) ) ;
4648jest . 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
5254const roomId = 'roomid' ;
5355
5456describe ( '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+
299451function 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