@@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
See the License for the specific language governing permissions and
14
14
limitations under the License.
15
15
*/
16
- import React from 'react' ;
16
+ import React , { createRef } from 'react' ;
17
17
import classNames from 'classnames' ;
18
18
import { _t } from '../../../languageHandler' ;
19
19
import { MatrixClientPeg } from '../../../MatrixClientPeg' ;
@@ -27,7 +27,14 @@ import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalin
27
27
import ContentMessages from '../../../ContentMessages' ;
28
28
import E2EIcon from './E2EIcon' ;
29
29
import SettingsStore from "../../../settings/SettingsStore" ;
30
- import { aboveLeftOf , ContextMenu , ContextMenuTooltipButton , useContextMenu } from "../../structures/ContextMenu" ;
30
+ import {
31
+ aboveLeftOf ,
32
+ ContextMenu ,
33
+ ContextMenuTooltipButton ,
34
+ useContextMenu ,
35
+ MenuItem ,
36
+ alwaysAboveRightOf ,
37
+ } from "../../structures/ContextMenu" ;
31
38
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton" ;
32
39
import ReplyPreview from "./ReplyPreview" ;
33
40
import { UIFeature } from "../../../settings/UIFeature" ;
@@ -45,6 +52,9 @@ import { Action } from "../../../dispatcher/actions";
45
52
import EditorModel from "../../../editor/model" ;
46
53
import EmojiPicker from '../emojipicker/EmojiPicker' ;
47
54
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar" ;
55
+ import UIStore , { UI_EVENTS } from '../../../stores/UIStore' ;
56
+
57
+ const NARROW_MODE_BREAKPOINT = 500 ;
48
58
49
59
interface IComposerAvatarProps {
50
60
me : object ;
@@ -71,13 +81,13 @@ function SendButton(props: ISendButtonProps) {
71
81
) ;
72
82
}
73
83
74
- const EmojiButton = ( { addEmoji } ) => {
84
+ const EmojiButton = ( { addEmoji, menuPosition } ) => {
75
85
const [ menuDisplayed , button , openMenu , closeMenu ] = useContextMenu ( ) ;
76
86
77
87
let contextMenu ;
78
88
if ( menuDisplayed ) {
79
- const buttonRect = button . current . getBoundingClientRect ( ) ;
80
- contextMenu = < ContextMenu { ...aboveLeftOf ( buttonRect ) } onFinished = { closeMenu } managed = { false } >
89
+ const position = menuPosition ?? aboveLeftOf ( button . current . getBoundingClientRect ( ) ) ;
90
+ contextMenu = < ContextMenu { ...position } onFinished = { closeMenu } managed = { false } >
81
91
< EmojiPicker onChoose = { addEmoji } showQuickReactions = { true } />
82
92
</ ContextMenu > ;
83
93
}
@@ -193,13 +203,17 @@ interface IState {
193
203
haveRecording : boolean ;
194
204
recordingTimeLeftSeconds ?: number ;
195
205
me ?: RoomMember ;
206
+ narrowMode ?: boolean ;
207
+ isMenuOpen : boolean ;
208
+ showStickers : boolean ;
196
209
}
197
210
198
211
@replaceableComponent ( "views.rooms.MessageComposer" )
199
212
export default class MessageComposer extends React . Component < IProps , IState > {
200
213
private dispatcherRef : string ;
201
214
private messageComposerInput : SendMessageComposer ;
202
215
private voiceRecordingButton : VoiceRecordComposerTile ;
216
+ private ref : React . RefObject < HTMLDivElement > = createRef ( ) ;
203
217
204
218
constructor ( props ) {
205
219
super ( props ) ;
@@ -211,15 +225,30 @@ export default class MessageComposer extends React.Component<IProps, IState> {
211
225
isComposerEmpty : true ,
212
226
haveRecording : false ,
213
227
recordingTimeLeftSeconds : null , // when set to a number, shows a toast
228
+ isMenuOpen : false ,
229
+ showStickers : false ,
214
230
} ;
215
231
}
216
232
217
233
componentDidMount ( ) {
218
234
this . dispatcherRef = dis . register ( this . onAction ) ;
219
235
MatrixClientPeg . get ( ) . on ( "RoomState.events" , this . onRoomStateEvents ) ;
220
236
this . waitForOwnMember ( ) ;
237
+ UIStore . instance . trackElementDimensions ( "MessageComposer" , this . ref . current ) ;
238
+ UIStore . instance . on ( "MessageComposer" , this . onResize ) ;
221
239
}
222
240
241
+ private onResize = ( type : UI_EVENTS , entry : ResizeObserverEntry ) => {
242
+ if ( type === UI_EVENTS . Resize ) {
243
+ const narrowMode = entry . contentRect . width <= NARROW_MODE_BREAKPOINT ;
244
+ this . setState ( {
245
+ narrowMode,
246
+ isMenuOpen : ! narrowMode ? false : this . state . isMenuOpen ,
247
+ showStickers : false ,
248
+ } ) ;
249
+ }
250
+ } ;
251
+
223
252
private onAction = ( payload : ActionPayload ) => {
224
253
if ( payload . action === 'reply_to_event' ) {
225
254
// add a timeout for the reply preview to be rendered, so
@@ -254,6 +283,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
254
283
}
255
284
VoiceRecordingStore . instance . off ( UPDATE_EVENT , this . onVoiceStoreUpdate ) ;
256
285
dis . unregister ( this . dispatcherRef ) ;
286
+ UIStore . instance . stopTrackingElementDimensions ( "MessageComposer" ) ;
287
+ UIStore . instance . removeListener ( "MessageComposer" , this . onResize ) ;
257
288
}
258
289
259
290
private onRoomStateEvents = ( ev , state ) => {
@@ -360,6 +391,91 @@ export default class MessageComposer extends React.Component<IProps, IState> {
360
391
}
361
392
} ;
362
393
394
+ private shouldShowStickerPicker = ( ) : boolean => {
395
+ return SettingsStore . getValue ( UIFeature . Widgets )
396
+ && SettingsStore . getValue ( "MessageComposerInput.showStickersButton" )
397
+ && ! this . state . haveRecording ;
398
+ } ;
399
+
400
+ private showStickers = ( showStickers : boolean ) => {
401
+ this . setState ( { showStickers } ) ;
402
+ } ;
403
+
404
+ private toggleButtonMenu = ( ) : void => {
405
+ this . setState ( {
406
+ isMenuOpen : ! this . state . isMenuOpen ,
407
+ } ) ;
408
+ } ;
409
+
410
+ private renderButtons ( ) : JSX . Element | JSX . Element [ ] {
411
+ const buttons = [ ] ;
412
+
413
+ let menuPosition ;
414
+ if ( this . ref . current ) {
415
+ const contentRect = this . ref . current . getBoundingClientRect ( ) ;
416
+ menuPosition = alwaysAboveRightOf ( contentRect ) ;
417
+ }
418
+
419
+ if ( ! this . state . haveRecording ) {
420
+ buttons . push (
421
+ < UploadButton key = "controls_upload" roomId = { this . props . room . roomId } /> ,
422
+ < EmojiButton key = "emoji_button" addEmoji = { this . addEmoji } menuPosition = { menuPosition } /> ,
423
+ ) ;
424
+ }
425
+ if ( this . shouldShowStickerPicker ( ) ) {
426
+ buttons . push ( < AccessibleTooltipButton
427
+ id = 'stickersButton'
428
+ key = "controls_stickers"
429
+ className = "mx_MessageComposer_button mx_MessageComposer_stickers"
430
+ onClick = { ( ) => this . showStickers ( ! this . state . showStickers ) }
431
+ title = { this . state . showStickers ? _t ( "Hide Stickers" ) : _t ( "Show Stickers" ) }
432
+ /> ) ;
433
+ }
434
+ if ( ! this . state . haveRecording ) {
435
+ buttons . push (
436
+ < AccessibleTooltipButton
437
+ className = "mx_MessageComposer_button mx_MessageComposer_voiceMessage"
438
+ onClick = { ( ) => this . voiceRecordingButton ?. onRecordStartEndClick ( ) }
439
+ title = { _t ( "Send voice message" ) }
440
+ /> ,
441
+ ) ;
442
+ }
443
+
444
+ if ( ! this . state . narrowMode ) {
445
+ return buttons ;
446
+ } else {
447
+ const classnames = classNames ( {
448
+ mx_MessageComposer_button : true ,
449
+ mx_MessageComposer_buttonMenu : true ,
450
+ mx_MessageComposer_closeButtonMenu : this . state . isMenuOpen ,
451
+ } ) ;
452
+
453
+ return < >
454
+ { buttons [ 0 ] }
455
+ < AccessibleTooltipButton
456
+ className = { classnames }
457
+ onClick = { this . toggleButtonMenu }
458
+ title = { _t ( "view more options" ) }
459
+ />
460
+ { this . state . isMenuOpen && (
461
+ < ContextMenu
462
+ onFinished = { this . toggleButtonMenu }
463
+ { ...menuPosition }
464
+ menuPaddingRight = { 10 }
465
+ menuPaddingTop = { 16 }
466
+ menuWidth = { 50 }
467
+ >
468
+ { buttons . slice ( 1 ) . map ( ( button , index ) => (
469
+ < MenuItem className = "mx_CallContextMenu_item" key = { index } onClick = { ( ) => setTimeout ( this . toggleButtonMenu , 500 ) } >
470
+ { button }
471
+ </ MenuItem >
472
+ ) ) }
473
+ </ ContextMenu >
474
+ ) }
475
+ </ > ;
476
+ }
477
+ }
478
+
363
479
render ( ) {
364
480
const controls = [
365
481
this . state . me ? < ComposerAvatar key = "controls_avatar" me = { this . state . me } /> : null ,
@@ -368,8 +484,6 @@ export default class MessageComposer extends React.Component<IProps, IState> {
368
484
null ,
369
485
] ;
370
486
371
- const buttons = [ ] ;
372
-
373
487
if ( ! this . state . tombstone && this . state . canSendMessages ) {
374
488
controls . push (
375
489
< SendMessageComposer
@@ -384,43 +498,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
384
498
/> ,
385
499
) ;
386
500
387
- if ( ! this . state . haveRecording ) {
388
- buttons . push (
389
- < UploadButton key = "controls_upload" roomId = { this . props . room . roomId } /> ,
390
- < EmojiButton key = "emoji_button" addEmoji = { this . addEmoji } /> ,
391
- ) ;
392
- }
393
-
394
- if ( SettingsStore . getValue ( UIFeature . Widgets ) &&
395
- SettingsStore . getValue ( "MessageComposerInput.showStickersButton" ) &&
396
- ! this . state . haveRecording ) {
397
- buttons . push ( < Stickerpicker key = "stickerpicker_controls_button" room = { this . props . room } /> ) ;
398
- }
399
-
400
501
controls . push ( < VoiceRecordComposerTile
401
502
key = "controls_voice_record"
402
503
ref = { c => this . voiceRecordingButton = c }
403
504
room = { this . props . room } /> ) ;
404
-
405
- if ( ! this . state . haveRecording ) {
406
- buttons . push (
407
- < AccessibleTooltipButton
408
- className = "mx_MessageComposer_button mx_MessageComposer_voiceMessage"
409
- onClick = { ( ) => this . voiceRecordingButton ?. onRecordStartEndClick ( ) }
410
- title = { _t ( "Send voice message" ) }
411
- /> ,
412
- ) ;
413
- }
414
-
415
- if ( ! this . state . isComposerEmpty || this . state . haveRecording ) {
416
- buttons . push (
417
- < SendButton
418
- key = "controls_send"
419
- onClick = { this . sendMessage }
420
- title = { this . state . haveRecording ? _t ( "Send voice message" ) : undefined }
421
- /> ,
422
- ) ;
423
- }
424
505
} else if ( this . state . tombstone ) {
425
506
const replacementRoomId = this . state . tombstone . getContent ( ) [ 'replacement_room' ] ;
426
507
@@ -462,14 +543,30 @@ export default class MessageComposer extends React.Component<IProps, IState> {
462
543
/> ;
463
544
}
464
545
546
+ controls . push (
547
+ < Stickerpicker
548
+ room = { this . props . room }
549
+ showStickers = { this . state . showStickers }
550
+ setShowStickers = { this . showStickers } /> ,
551
+ ) ;
552
+
553
+ const showSendButton = ! this . state . isComposerEmpty || this . state . haveRecording ;
554
+
465
555
return (
466
- < div className = "mx_MessageComposer mx_GroupLayout" >
556
+ < div className = "mx_MessageComposer mx_GroupLayout" ref = { this . ref } >
467
557
{ recordingTooltip }
468
558
< div className = "mx_MessageComposer_wrapper" >
469
559
< ReplyPreview permalinkCreator = { this . props . permalinkCreator } />
470
560
< div className = "mx_MessageComposer_row" >
471
561
{ controls }
472
- { buttons }
562
+ { this . renderButtons ( ) }
563
+ { showSendButton && (
564
+ < SendButton
565
+ key = "controls_send"
566
+ onClick = { this . sendMessage }
567
+ title = { this . state . haveRecording ? _t ( "Send voice message" ) : undefined }
568
+ />
569
+ ) }
473
570
</ div >
474
571
</ div >
475
572
</ div >
0 commit comments