@@ -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,14 @@ 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+ alwaysAboveRightOf ,
37+ } from "../../structures/ContextMenu" ;
3138import AccessibleTooltipButton from "../elements/AccessibleTooltipButton" ;
3239import ReplyPreview from "./ReplyPreview" ;
3340import { UIFeature } from "../../../settings/UIFeature" ;
@@ -45,6 +52,9 @@ import { Action } from "../../../dispatcher/actions";
4552import EditorModel from "../../../editor/model" ;
4653import EmojiPicker from '../emojipicker/EmojiPicker' ;
4754import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar" ;
55+ import UIStore , { UI_EVENTS } from '../../../stores/UIStore' ;
56+
57+ const NARROW_MODE_BREAKPOINT = 500 ;
4858
4959interface IComposerAvatarProps {
5060 me : object ;
@@ -71,13 +81,13 @@ function SendButton(props: ISendButtonProps) {
7181 ) ;
7282}
7383
74- const EmojiButton = ( { addEmoji } ) => {
84+ const EmojiButton = ( { addEmoji, menuPosition } ) => {
7585 const [ menuDisplayed , button , openMenu , closeMenu ] = useContextMenu ( ) ;
7686
7787 let contextMenu ;
7888 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 } >
8191 < EmojiPicker onChoose = { addEmoji } showQuickReactions = { true } />
8292 </ ContextMenu > ;
8393 }
@@ -193,13 +203,17 @@ interface IState {
193203 haveRecording : boolean ;
194204 recordingTimeLeftSeconds ?: number ;
195205 me ?: RoomMember ;
206+ narrowMode ?: boolean ;
207+ isMenuOpen : boolean ;
208+ showStickers : boolean ;
196209}
197210
198211@replaceableComponent ( "views.rooms.MessageComposer" )
199212export default class MessageComposer extends React . Component < IProps , IState > {
200213 private dispatcherRef : string ;
201214 private messageComposerInput : SendMessageComposer ;
202215 private voiceRecordingButton : VoiceRecordComposerTile ;
216+ private ref : React . RefObject < HTMLDivElement > = createRef ( ) ;
203217
204218 constructor ( props ) {
205219 super ( props ) ;
@@ -211,15 +225,30 @@ export default class MessageComposer extends React.Component<IProps, IState> {
211225 isComposerEmpty : true ,
212226 haveRecording : false ,
213227 recordingTimeLeftSeconds : null , // when set to a number, shows a toast
228+ isMenuOpen : false ,
229+ showStickers : false ,
214230 } ;
215231 }
216232
217233 componentDidMount ( ) {
218234 this . dispatcherRef = dis . register ( this . onAction ) ;
219235 MatrixClientPeg . get ( ) . on ( "RoomState.events" , this . onRoomStateEvents ) ;
220236 this . waitForOwnMember ( ) ;
237+ UIStore . instance . trackElementDimensions ( "MessageComposer" , this . ref . current ) ;
238+ UIStore . instance . on ( "MessageComposer" , this . onResize ) ;
221239 }
222240
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+
223252 private onAction = ( payload : ActionPayload ) => {
224253 if ( payload . action === 'reply_to_event' ) {
225254 // add a timeout for the reply preview to be rendered, so
@@ -254,6 +283,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
254283 }
255284 VoiceRecordingStore . instance . off ( UPDATE_EVENT , this . onVoiceStoreUpdate ) ;
256285 dis . unregister ( this . dispatcherRef ) ;
286+ UIStore . instance . stopTrackingElementDimensions ( "MessageComposer" ) ;
287+ UIStore . instance . removeListener ( "MessageComposer" , this . onResize ) ;
257288 }
258289
259290 private onRoomStateEvents = ( ev , state ) => {
@@ -360,6 +391,91 @@ export default class MessageComposer extends React.Component<IProps, IState> {
360391 }
361392 } ;
362393
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+
363479 render ( ) {
364480 const controls = [
365481 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> {
368484 null ,
369485 ] ;
370486
371- const buttons = [ ] ;
372-
373487 if ( ! this . state . tombstone && this . state . canSendMessages ) {
374488 controls . push (
375489 < SendMessageComposer
@@ -384,43 +498,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
384498 /> ,
385499 ) ;
386500
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-
400501 controls . push ( < VoiceRecordComposerTile
401502 key = "controls_voice_record"
402503 ref = { c => this . voiceRecordingButton = c }
403504 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- }
424505 } else if ( this . state . tombstone ) {
425506 const replacementRoomId = this . state . tombstone . getContent ( ) [ 'replacement_room' ] ;
426507
@@ -462,14 +543,30 @@ export default class MessageComposer extends React.Component<IProps, IState> {
462543 /> ;
463544 }
464545
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+
465555 return (
466- < div className = "mx_MessageComposer mx_GroupLayout" >
556+ < div className = "mx_MessageComposer mx_GroupLayout" ref = { this . ref } >
467557 { recordingTooltip }
468558 < div className = "mx_MessageComposer_wrapper" >
469559 < ReplyPreview permalinkCreator = { this . props . permalinkCreator } />
470560 < div className = "mx_MessageComposer_row" >
471561 { 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+ ) }
473570 </ div >
474571 </ div >
475572 </ div >
0 commit comments