Skip to content

Commit 59de3c9

Browse files
authored
Merge pull request matrix-org#6682 from matrix-org/gsouquet/compact-composer-18533
2 parents a4c0fa8 + 085d8b4 commit 59de3c9

File tree

5 files changed

+210
-113
lines changed

5 files changed

+210
-113
lines changed

res/css/views/rooms/_MessageComposer.scss

Lines changed: 14 additions & 0 deletions
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/image-view/more.svg');
242+
}
243+
244+
.mx_MessageComposer_closeButtonMenu::before {
245+
transform: rotate(90deg);
246+
transform-origin: center;
247+
}
248+
240249
.mx_MessageComposer_sendMessage {
241250
cursor: pointer;
242251
position: relative;
@@ -356,3 +365,8 @@ limitations under the License.
356365
margin-right: 0;
357366
}
358367
}
368+
369+
.mx_MessageComposer_Menu .mx_CallContextMenu_item {
370+
display: flex;
371+
align-items: center;
372+
}

src/components/views/rooms/MessageComposer.tsx

Lines changed: 149 additions & 29 deletions
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,13 @@ 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+
} from "../../structures/ContextMenu";
3137
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
3238
import ReplyPreview from "./ReplyPreview";
3339
import { UIFeature } from "../../../settings/UIFeature";
@@ -45,6 +51,9 @@ import { Action } from "../../../dispatcher/actions";
4551
import EditorModel from "../../../editor/model";
4652
import EmojiPicker from '../emojipicker/EmojiPicker';
4753
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
54+
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
55+
56+
const NARROW_MODE_BREAKPOINT = 500;
4857

4958
interface 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")
202214
export 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

Comments
 (0)