@@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313See the License for the specific language governing permissions and
1414limitations under the License.
1515*/
16- import React from 'react' ;
16+ import React , { createRef } from 'react' ;
1717import classNames from 'classnames' ;
1818import { _t } from '../../../languageHandler' ;
1919import { MatrixClientPeg } from '../../../MatrixClientPeg' ;
@@ -27,7 +27,13 @@ import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalin
2727import ContentMessages from '../../../ContentMessages' ;
2828import E2EIcon from './E2EIcon' ;
2929import 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+ } from "../../structures/ContextMenu" ;
3137import AccessibleTooltipButton from "../elements/AccessibleTooltipButton" ;
3238import ReplyPreview from "./ReplyPreview" ;
3339import { UIFeature } from "../../../settings/UIFeature" ;
@@ -45,6 +51,9 @@ import { Action } from "../../../dispatcher/actions";
4551import EditorModel from "../../../editor/model" ;
4652import EmojiPicker from '../emojipicker/EmojiPicker' ;
4753import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar" ;
54+ import UIStore , { UI_EVENTS } from '../../../stores/UIStore' ;
55+
56+ const NARROW_MODE_BREAKPOINT = 500 ;
4857
4958interface IComposerAvatarProps {
5059 me : object ;
@@ -71,13 +80,13 @@ function SendButton(props: ISendButtonProps) {
7180 ) ;
7281}
7382
74- const EmojiButton = ( { addEmoji } ) => {
83+ const EmojiButton = ( { addEmoji, menuPosition } ) => {
7584 const [ menuDisplayed , button , openMenu , closeMenu ] = useContextMenu ( ) ;
7685
7786 let contextMenu ;
7887 if ( menuDisplayed ) {
79- const buttonRect = button . current . getBoundingClientRect ( ) ;
80- contextMenu = < ContextMenu { ...aboveLeftOf ( buttonRect ) } onFinished = { closeMenu } managed = { false } >
88+ const position = menuPosition ?? aboveLeftOf ( button . current . getBoundingClientRect ( ) ) ;
89+ contextMenu = < ContextMenu { ...position } onFinished = { closeMenu } managed = { false } >
8190 < EmojiPicker onChoose = { addEmoji } showQuickReactions = { true } />
8291 </ ContextMenu > ;
8392 }
@@ -196,13 +205,17 @@ interface IState {
196205 haveRecording : boolean ;
197206 recordingTimeLeftSeconds ?: number ;
198207 me ?: RoomMember ;
208+ narrowMode ?: boolean ;
209+ isMenuOpen : boolean ;
210+ showStickers : boolean ;
199211}
200212
201213@replaceableComponent ( "views.rooms.MessageComposer" )
202214export default class MessageComposer extends React . Component < IProps , IState > {
203215 private dispatcherRef : string ;
204216 private messageComposerInput : SendMessageComposer ;
205217 private voiceRecordingButton : VoiceRecordComposerTile ;
218+ private ref : React . RefObject < HTMLDivElement > = createRef ( ) ;
206219
207220 static defaultProps = {
208221 replyInThread : false ,
@@ -220,15 +233,30 @@ export default class MessageComposer extends React.Component<IProps, IState> {
220233 isComposerEmpty : true ,
221234 haveRecording : false ,
222235 recordingTimeLeftSeconds : null , // when set to a number, shows a toast
236+ isMenuOpen : false ,
237+ showStickers : false ,
223238 } ;
224239 }
225240
226241 componentDidMount ( ) {
227242 this . dispatcherRef = dis . register ( this . onAction ) ;
228243 MatrixClientPeg . get ( ) . on ( "RoomState.events" , this . onRoomStateEvents ) ;
229244 this . waitForOwnMember ( ) ;
245+ UIStore . instance . trackElementDimensions ( "MessageComposer" , this . ref . current ) ;
246+ UIStore . instance . on ( "MessageComposer" , this . onResize ) ;
230247 }
231248
249+ private onResize = ( type : UI_EVENTS , entry : ResizeObserverEntry ) => {
250+ if ( type === UI_EVENTS . Resize ) {
251+ const narrowMode = entry . contentRect . width <= NARROW_MODE_BREAKPOINT ;
252+ this . setState ( {
253+ narrowMode,
254+ isMenuOpen : ! narrowMode ? false : this . state . isMenuOpen ,
255+ showStickers : false ,
256+ } ) ;
257+ }
258+ } ;
259+
232260 private onAction = ( payload : ActionPayload ) => {
233261 if ( payload . action === 'reply_to_event' ) {
234262 // add a timeout for the reply preview to be rendered, so
@@ -263,6 +291,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
263291 }
264292 VoiceRecordingStore . instance . off ( UPDATE_EVENT , this . onVoiceStoreUpdate ) ;
265293 dis . unregister ( this . dispatcherRef ) ;
294+ UIStore . instance . stopTrackingElementDimensions ( "MessageComposer" ) ;
295+ UIStore . instance . removeListener ( "MessageComposer" , this . onResize ) ;
266296 }
267297
268298 private onRoomStateEvents = ( ev , state ) => {
@@ -369,6 +399,96 @@ export default class MessageComposer extends React.Component<IProps, IState> {
369399 }
370400 } ;
371401
402+ private shouldShowStickerPicker = ( ) : boolean => {
403+ return SettingsStore . getValue ( UIFeature . Widgets )
404+ && SettingsStore . getValue ( "MessageComposerInput.showStickersButton" )
405+ && ! this . state . haveRecording ;
406+ } ;
407+
408+ private showStickers = ( showStickers : boolean ) => {
409+ this . setState ( { showStickers } ) ;
410+ } ;
411+
412+ private toggleButtonMenu = ( ) : void => {
413+ this . setState ( {
414+ isMenuOpen : ! this . state . isMenuOpen ,
415+ } ) ;
416+ } ;
417+
418+ private renderButtons ( menuPosition ) : JSX . Element | JSX . Element [ ] {
419+ const buttons = new Map < string , JSX . Element > ( ) ;
420+ if ( ! this . state . haveRecording ) {
421+ buttons . set (
422+ _t ( "Send File" ) ,
423+ < UploadButton key = "controls_upload" roomId = { this . props . room . roomId } /> ,
424+ ) ;
425+ buttons . set (
426+ _t ( "Show Emojis" ) ,
427+ < EmojiButton key = "emoji_button" addEmoji = { this . addEmoji } menuPosition = { menuPosition } /> ,
428+ ) ;
429+ }
430+ if ( this . shouldShowStickerPicker ( ) ) {
431+ buttons . set (
432+ _t ( "Show Stickers" ) ,
433+ < AccessibleTooltipButton
434+ id = 'stickersButton'
435+ key = "controls_stickers"
436+ className = "mx_MessageComposer_button mx_MessageComposer_stickers"
437+ onClick = { ( ) => this . showStickers ( ! this . state . showStickers ) }
438+ title = { this . state . showStickers ? _t ( "Hide Stickers" ) : _t ( "Show Stickers" ) }
439+ /> ,
440+ ) ;
441+ }
442+ if ( ! this . state . haveRecording && ! this . state . narrowMode ) {
443+ buttons . set (
444+ _t ( "Send voice message" ) ,
445+ < AccessibleTooltipButton
446+ className = "mx_MessageComposer_button mx_MessageComposer_voiceMessage"
447+ onClick = { ( ) => this . voiceRecordingButton ?. onRecordStartEndClick ( ) }
448+ title = { _t ( "Send voice message" ) }
449+ /> ,
450+ ) ;
451+ }
452+
453+ if ( ! this . state . narrowMode ) {
454+ return Array . from ( buttons . values ( ) ) ;
455+ } else {
456+ const classnames = classNames ( {
457+ mx_MessageComposer_button : true ,
458+ mx_MessageComposer_buttonMenu : true ,
459+ mx_MessageComposer_closeButtonMenu : this . state . isMenuOpen ,
460+ } ) ;
461+
462+ return < >
463+ { buttons [ 0 ] }
464+ < AccessibleTooltipButton
465+ className = { classnames }
466+ onClick = { this . toggleButtonMenu }
467+ title = { _t ( "Composer menu" ) }
468+ tooltip = { false }
469+ />
470+ { this . state . isMenuOpen && (
471+ < ContextMenu
472+ onFinished = { this . toggleButtonMenu }
473+ { ...menuPosition }
474+ menuPaddingRight = { 10 }
475+ menuPaddingTop = { 5 }
476+ menuPaddingBottom = { 5 }
477+ menuWidth = { 150 }
478+ wrapperClassName = "mx_MessageComposer_Menu"
479+ >
480+ { Array . from ( buttons ) . slice ( 1 ) . map ( ( [ label , button ] ) => (
481+ < MenuItem className = "mx_CallContextMenu_item" key = { label } onClick = { this . toggleButtonMenu } >
482+ { button }
483+ { label }
484+ </ MenuItem >
485+ ) ) }
486+ </ ContextMenu >
487+ ) }
488+ </ > ;
489+ }
490+ }
491+
372492 render ( ) {
373493 const controls = [
374494 this . state . me && ! this . props . compact ? < ComposerAvatar key = "controls_avatar" me = { this . state . me } /> : null ,
@@ -377,6 +497,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {
377497 null ,
378498 ] ;
379499
500+ let menuPosition ;
501+ if ( this . ref . current ) {
502+ const contentRect = this . ref . current . getBoundingClientRect ( ) ;
503+ menuPosition = aboveLeftOf ( contentRect ) ;
504+ }
505+
380506 if ( ! this . state . tombstone && this . state . canSendMessages ) {
381507 controls . push (
382508 < SendMessageComposer
@@ -392,33 +518,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
392518 /> ,
393519 ) ;
394520
395- if ( ! this . state . haveRecording ) {
396- controls . push (
397- < UploadButton key = "controls_upload" roomId = { this . props . room . roomId } /> ,
398- < EmojiButton key = "emoji_button" addEmoji = { this . addEmoji } /> ,
399- ) ;
400- }
401-
402- if ( SettingsStore . getValue ( UIFeature . Widgets ) &&
403- SettingsStore . getValue ( "MessageComposerInput.showStickersButton" ) &&
404- ! this . state . haveRecording ) {
405- controls . push ( < Stickerpicker key = "stickerpicker_controls_button" room = { this . props . room } /> ) ;
406- }
407-
408521 controls . push ( < VoiceRecordComposerTile
409522 key = "controls_voice_record"
410523 ref = { c => this . voiceRecordingButton = c }
411524 room = { this . props . room } /> ) ;
412-
413- if ( ! this . state . isComposerEmpty || this . state . haveRecording ) {
414- controls . push (
415- < SendButton
416- key = "controls_send"
417- onClick = { this . sendMessage }
418- title = { this . state . haveRecording ? _t ( "Send voice message" ) : undefined }
419- /> ,
420- ) ;
421- }
422525 } else if ( this . state . tombstone ) {
423526 const replacementRoomId = this . state . tombstone . getContent ( ) [ 'replacement_room' ] ;
424527
@@ -459,6 +562,15 @@ export default class MessageComposer extends React.Component<IProps, IState> {
459562 yOffset = { - 50 }
460563 /> ;
461564 }
565+ controls . push (
566+ < Stickerpicker
567+ room = { this . props . room }
568+ showStickers = { this . state . showStickers }
569+ setShowStickers = { this . showStickers }
570+ menuPosition = { menuPosition } /> ,
571+ ) ;
572+
573+ const showSendButton = ! this . state . isComposerEmpty || this . state . haveRecording ;
462574
463575 const classes = classNames ( {
464576 "mx_MessageComposer" : true ,
@@ -467,14 +579,22 @@ export default class MessageComposer extends React.Component<IProps, IState> {
467579 } ) ;
468580
469581 return (
470- < div className = { classes } >
582+ < div className = { classes } ref = { this . ref } >
471583 { recordingTooltip }
472584 < div className = "mx_MessageComposer_wrapper" >
473585 { this . props . showReplyPreview && (
474586 < ReplyPreview permalinkCreator = { this . props . permalinkCreator } />
475587 ) }
476588 < div className = "mx_MessageComposer_row" >
477589 { controls }
590+ { this . renderButtons ( menuPosition ) }
591+ { showSendButton && (
592+ < SendButton
593+ key = "controls_send"
594+ onClick = { this . sendMessage }
595+ title = { this . state . haveRecording ? _t ( "Send voice message" ) : undefined }
596+ />
597+ ) }
478598 </ div >
479599 </ div >
480600 </ div >
0 commit comments