@@ -37,6 +37,15 @@ import { MatrixClientPeg } from '../../MatrixClientPeg';
37
37
import { E2EStatus } from '../../utils/ShieldUtils' ;
38
38
import EditorStateTransfer from '../../utils/EditorStateTransfer' ;
39
39
import RoomContext , { TimelineRenderingType } from '../../contexts/RoomContext' ;
40
+ import { ChevronFace , ContextMenuTooltipButton } from './ContextMenu' ;
41
+ import { _t } from '../../languageHandler' ;
42
+ import IconizedContextMenu , {
43
+ IconizedContextMenuOption ,
44
+ IconizedContextMenuOptionList ,
45
+ } from '../views/context_menus/IconizedContextMenu' ;
46
+ import { ButtonEvent } from '../views/elements/AccessibleButton' ;
47
+ import { copyPlaintext } from '../../utils/strings' ;
48
+ import { sleep } from 'matrix-js-sdk/src/utils' ;
40
49
41
50
interface IProps {
42
51
room : Room ;
@@ -48,13 +57,28 @@ interface IProps {
48
57
initialEvent ?: MatrixEvent ;
49
58
initialEventHighlighted ?: boolean ;
50
59
}
51
-
52
60
interface IState {
53
61
thread ?: Thread ;
54
62
editState ?: EditorStateTransfer ;
55
63
replyToEvent ?: MatrixEvent ;
64
+ threadOptionsPosition : DOMRect | null ;
65
+ copyingPhase : CopyingPhase ;
66
+ }
67
+
68
+ enum CopyingPhase {
69
+ Idle ,
70
+ Copying ,
71
+ Failed ,
56
72
}
57
73
74
+ const contextMenuBelow = ( elementRect : DOMRect ) => {
75
+ // align the context menu's icons with the icon which opened the context menu
76
+ const left = elementRect . left + window . pageXOffset + elementRect . width ;
77
+ const top = elementRect . bottom + window . pageYOffset + 17 ;
78
+ const chevronFace = ChevronFace . None ;
79
+ return { left, top, chevronFace } ;
80
+ } ;
81
+
58
82
@replaceableComponent ( "structures.ThreadView" )
59
83
export default class ThreadView extends React . Component < IProps , IState > {
60
84
static contextType = RoomContext ;
@@ -64,7 +88,10 @@ export default class ThreadView extends React.Component<IProps, IState> {
64
88
65
89
constructor ( props : IProps ) {
66
90
super ( props ) ;
67
- this . state = { } ;
91
+ this . state = {
92
+ threadOptionsPosition : null ,
93
+ copyingPhase : CopyingPhase . Idle ,
94
+ } ;
68
95
}
69
96
70
97
public componentDidMount ( ) : void {
@@ -181,6 +208,98 @@ export default class ThreadView extends React.Component<IProps, IState> {
181
208
}
182
209
} ;
183
210
211
+ private onThreadOptionsClick = ( ev : ButtonEvent ) : void => {
212
+ if ( this . isThreadOptionsVisible ) {
213
+ this . closeThreadOptions ( ) ;
214
+ } else {
215
+ const position = ev . currentTarget . getBoundingClientRect ( ) ;
216
+ this . setState ( {
217
+ threadOptionsPosition : position ,
218
+ } ) ;
219
+ }
220
+ } ;
221
+
222
+ private closeThreadOptions = ( ) : void => {
223
+ this . setState ( {
224
+ threadOptionsPosition : null ,
225
+ } ) ;
226
+ } ;
227
+
228
+ private get isThreadOptionsVisible ( ) : boolean {
229
+ return ! ! this . state . threadOptionsPosition ;
230
+ }
231
+
232
+ private viewInRoom = ( evt : ButtonEvent ) : void => {
233
+ evt . preventDefault ( ) ;
234
+ evt . stopPropagation ( ) ;
235
+ dis . dispatch ( {
236
+ action : 'view_room' ,
237
+ event_id : this . props . mxEvent . getId ( ) ,
238
+ highlighted : true ,
239
+ room_id : this . props . mxEvent . getRoomId ( ) ,
240
+ } ) ;
241
+ this . closeThreadOptions ( ) ;
242
+ } ;
243
+
244
+ private copyLinkToThread = async ( evt : ButtonEvent ) : Promise < void > => {
245
+ evt . preventDefault ( ) ;
246
+ evt . stopPropagation ( ) ;
247
+
248
+ const matrixToUrl = this . props . permalinkCreator . forEvent ( this . props . mxEvent . getId ( ) ) ;
249
+
250
+ this . setState ( {
251
+ copyingPhase : CopyingPhase . Copying ,
252
+ } ) ;
253
+
254
+ const hasSuccessfullyCopied = await copyPlaintext ( matrixToUrl ) ;
255
+
256
+ if ( hasSuccessfullyCopied ) {
257
+ await sleep ( 500 ) ;
258
+ } else {
259
+ this . setState ( { copyingPhase : CopyingPhase . Failed } ) ;
260
+ await sleep ( 2500 ) ;
261
+ }
262
+
263
+ this . setState ( { copyingPhase : CopyingPhase . Idle } ) ;
264
+
265
+ if ( hasSuccessfullyCopied ) {
266
+ this . closeThreadOptions ( ) ;
267
+ }
268
+ } ;
269
+
270
+ private renderThreadViewHeader = ( ) : JSX . Element => {
271
+ return < div className = "mx_ThreadPanel__header" >
272
+ < span > { _t ( "Thread" ) } </ span >
273
+ < ContextMenuTooltipButton
274
+ className = "mx_ThreadPanel_button mx_ThreadPanel_OptionsButton"
275
+ onClick = { this . onThreadOptionsClick }
276
+ title = { _t ( "Thread options" ) }
277
+ isExpanded = { this . isThreadOptionsVisible }
278
+ />
279
+ { this . isThreadOptionsVisible && ( < IconizedContextMenu
280
+ onFinished = { this . closeThreadOptions }
281
+ className = "mx_RoomTile_contextMenu"
282
+ compact
283
+ rightAligned
284
+ { ...contextMenuBelow ( this . state . threadOptionsPosition ) }
285
+ >
286
+ < IconizedContextMenuOptionList >
287
+ < IconizedContextMenuOption
288
+ onClick = { ( e ) => this . viewInRoom ( e ) }
289
+ label = { _t ( "View in room" ) }
290
+ iconClassName = "mx_ThreadPanel_viewInRoom"
291
+ />
292
+ < IconizedContextMenuOption
293
+ onClick = { ( e ) => this . copyLinkToThread ( e ) }
294
+ label = { _t ( "Copy link to thread" ) }
295
+ iconClassName = "mx_ThreadPanel_copyLinkToThread"
296
+ />
297
+ </ IconizedContextMenuOptionList >
298
+ </ IconizedContextMenu > ) }
299
+
300
+ </ div > ;
301
+ } ;
302
+
184
303
public render ( ) : JSX . Element {
185
304
const highlightedEventId = this . props . initialEventHighlighted
186
305
? this . props . initialEvent ?. getId ( )
@@ -193,10 +312,11 @@ export default class ThreadView extends React.Component<IProps, IState> {
193
312
} } >
194
313
195
314
< BaseCard
196
- className = "mx_ThreadView"
315
+ className = "mx_ThreadView mx_ThreadPanel "
197
316
onClose = { this . props . onClose }
198
317
previousPhase = { RightPanelPhases . ThreadPanel }
199
318
withoutScrollContainer = { true }
319
+ header = { this . renderThreadViewHeader ( ) }
200
320
>
201
321
{ this . state . thread && (
202
322
< TimelinePanel
@@ -209,7 +329,6 @@ export default class ThreadView extends React.Component<IProps, IState> {
209
329
showUrlPreview = { true }
210
330
tileShape = { TileShape . Thread }
211
331
empty = { < div > empty</ div > }
212
- alwaysShowTimestamps = { true }
213
332
layout = { Layout . Group }
214
333
hideThreadedMessages = { false }
215
334
hidden = { false }
0 commit comments