Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 31b3b2e

Browse files
author
Germain Souquet
committed
Fix Sticker and Emoji picker opening on narrow mode
1 parent 333cb61 commit 31b3b2e

File tree

4 files changed

+180
-107
lines changed

4 files changed

+180
-107
lines changed

res/css/views/rooms/_MessageComposer.scss

+9
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,15 @@ limitations under the License.
237237
mask-image: url('$(res)/img/element-icons/room/composer/sticker.svg');
238238
}
239239

240+
.mx_MessageComposer_buttonMenu::before {
241+
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
242+
}
243+
244+
.mx_MessageComposer_closeButtonMenu::before {
245+
transform: rotate(180deg);
246+
transform-origin: center;
247+
}
248+
240249
.mx_MessageComposer_sendMessage {
241250
cursor: pointer;
242251
position: relative;

src/components/views/rooms/MessageComposer.tsx

+139-42
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
16-
import React from 'react';
16+
import React, { createRef } from 'react';
1717
import classNames from 'classnames';
1818
import { _t } from '../../../languageHandler';
1919
import { MatrixClientPeg } from '../../../MatrixClientPeg';
@@ -27,7 +27,14 @@ import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalin
2727
import ContentMessages from '../../../ContentMessages';
2828
import E2EIcon from './E2EIcon';
2929
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";
3138
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
3239
import ReplyPreview from "./ReplyPreview";
3340
import { UIFeature } from "../../../settings/UIFeature";
@@ -45,6 +52,9 @@ import { Action } from "../../../dispatcher/actions";
4552
import EditorModel from "../../../editor/model";
4653
import EmojiPicker from '../emojipicker/EmojiPicker';
4754
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
55+
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
56+
57+
const NARROW_MODE_BREAKPOINT = 500;
4858

4959
interface 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")
199212
export 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

Comments
 (0)