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

Create narrow mode for Composer #6682

Merged
merged 16 commits into from
Sep 7, 2021
Merged
9 changes: 9 additions & 0 deletions res/css/views/rooms/_MessageComposer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,15 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/room/composer/sticker.svg');
}

.mx_MessageComposer_buttonMenu::before {
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
}

.mx_MessageComposer_closeButtonMenu::before {
transform: rotate(180deg);
transform-origin: center;
}

.mx_MessageComposer_sendMessage {
cursor: pointer;
position: relative;
Expand Down
168 changes: 139 additions & 29 deletions src/components/views/rooms/MessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, { createRef } from 'react';
import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
Expand All @@ -27,7 +27,13 @@ import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalin
import ContentMessages from '../../../ContentMessages';
import E2EIcon from './E2EIcon';
import SettingsStore from "../../../settings/SettingsStore";
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
import {
aboveLeftOf,
ContextMenu,
ContextMenuTooltipButton,
useContextMenu,
MenuItem,
} from "../../structures/ContextMenu";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import ReplyPreview from "./ReplyPreview";
import { UIFeature } from "../../../settings/UIFeature";
Expand All @@ -45,6 +51,9 @@ import { Action } from "../../../dispatcher/actions";
import EditorModel from "../../../editor/model";
import EmojiPicker from '../emojipicker/EmojiPicker';
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';

const NARROW_MODE_BREAKPOINT = 500;

interface IComposerAvatarProps {
me: object;
Expand All @@ -71,13 +80,13 @@ function SendButton(props: ISendButtonProps) {
);
}

const EmojiButton = ({ addEmoji }) => {
const EmojiButton = ({ addEmoji, menuPosition }) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();

let contextMenu;
if (menuDisplayed) {
const buttonRect = button.current.getBoundingClientRect();
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
const position = menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect());
contextMenu = <ContextMenu {...position} onFinished={closeMenu} managed={false}>
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
</ContextMenu>;
}
Expand Down Expand Up @@ -193,13 +202,17 @@ interface IState {
haveRecording: boolean;
recordingTimeLeftSeconds?: number;
me?: RoomMember;
narrowMode?: boolean;
isMenuOpen: boolean;
showStickers: boolean;
}

@replaceableComponent("views.rooms.MessageComposer")
export default class MessageComposer extends React.Component<IProps, IState> {
private dispatcherRef: string;
private messageComposerInput: SendMessageComposer;
private voiceRecordingButton: VoiceRecordComposerTile;
private ref: React.RefObject<HTMLDivElement> = createRef();

constructor(props) {
super(props);
Expand All @@ -211,15 +224,30 @@ export default class MessageComposer extends React.Component<IProps, IState> {
isComposerEmpty: true,
haveRecording: false,
recordingTimeLeftSeconds: null, // when set to a number, shows a toast
isMenuOpen: false,
showStickers: false,
};
}

componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
this.waitForOwnMember();
UIStore.instance.trackElementDimensions("MessageComposer", this.ref.current);
UIStore.instance.on("MessageComposer", this.onResize);
}

private onResize = (type: UI_EVENTS, entry: ResizeObserverEntry) => {
if (type === UI_EVENTS.Resize) {
const narrowMode = entry.contentRect.width <= NARROW_MODE_BREAKPOINT;
this.setState({
narrowMode,
isMenuOpen: !narrowMode ? false : this.state.isMenuOpen,
showStickers: false,
});
}
};

private onAction = (payload: ActionPayload) => {
if (payload.action === 'reply_to_event') {
// add a timeout for the reply preview to be rendered, so
Expand Down Expand Up @@ -254,6 +282,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
}
VoiceRecordingStore.instance.off(UPDATE_EVENT, this.onVoiceStoreUpdate);
dis.unregister(this.dispatcherRef);
UIStore.instance.stopTrackingElementDimensions("MessageComposer");
UIStore.instance.removeListener("MessageComposer", this.onResize);
}

private onRoomStateEvents = (ev, state) => {
Expand Down Expand Up @@ -360,6 +390,85 @@ export default class MessageComposer extends React.Component<IProps, IState> {
}
};

private shouldShowStickerPicker = (): boolean => {
return SettingsStore.getValue(UIFeature.Widgets)
&& SettingsStore.getValue("MessageComposerInput.showStickersButton")
&& !this.state.haveRecording;
};

private showStickers = (showStickers: boolean) => {
this.setState({ showStickers });
};

private toggleButtonMenu = (): void => {
this.setState({
isMenuOpen: !this.state.isMenuOpen,
});
};

private renderButtons(menuPosition): JSX.Element | JSX.Element[] {
const buttons = [];

if (!this.state.haveRecording) {
buttons.push(
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} menuPosition={menuPosition} />,
);
}
if (this.shouldShowStickerPicker()) {
buttons.push(<AccessibleTooltipButton
id='stickersButton'
key="controls_stickers"
className="mx_MessageComposer_button mx_MessageComposer_stickers"
onClick={() => this.showStickers(!this.state.showStickers)}
title={this.state.showStickers ? _t("Hide Stickers") : _t("Show Stickers")}
/>);
}
if (!this.state.haveRecording) {
buttons.push(
<AccessibleTooltipButton
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
onClick={() => this.voiceRecordingButton?.onRecordStartEndClick()}
title={_t("Send voice message")}
/>,
);
}

if (!this.state.narrowMode) {
return buttons;
} else {
const classnames = classNames({
mx_MessageComposer_button: true,
mx_MessageComposer_buttonMenu: true,
mx_MessageComposer_closeButtonMenu: this.state.isMenuOpen,
});

return <>
{ buttons[0] }
<AccessibleTooltipButton
className={classnames}
onClick={this.toggleButtonMenu}
title={_t("view more options")}
/>
{ this.state.isMenuOpen && (
<ContextMenu
onFinished={this.toggleButtonMenu}
{...menuPosition}
menuPaddingRight={10}
menuPaddingTop={16}
menuWidth={50}
>
{ buttons.slice(1).map((button, index) => (
<MenuItem className="mx_CallContextMenu_item" key={index} onClick={this.toggleButtonMenu}>
{ button }
</MenuItem>
)) }
</ContextMenu>
) }
</>;
}
}

render() {
const controls = [
this.state.me ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
Expand All @@ -368,6 +477,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {
null,
];

let menuPosition;
if (this.ref.current) {
const contentRect = this.ref.current.getBoundingClientRect();
menuPosition = aboveLeftOf(contentRect);
}

if (!this.state.tombstone && this.state.canSendMessages) {
controls.push(
<SendMessageComposer
Expand All @@ -382,33 +497,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
/>,
);

if (!this.state.haveRecording) {
controls.push(
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
);
}

if (SettingsStore.getValue(UIFeature.Widgets) &&
SettingsStore.getValue("MessageComposerInput.showStickersButton") &&
!this.state.haveRecording) {
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
}

controls.push(<VoiceRecordComposerTile
key="controls_voice_record"
ref={c => this.voiceRecordingButton = c}
room={this.props.room} />);

if (!this.state.isComposerEmpty || this.state.haveRecording) {
controls.push(
<SendButton
key="controls_send"
onClick={this.sendMessage}
title={this.state.haveRecording ? _t("Send voice message") : undefined}
/>,
);
}
} else if (this.state.tombstone) {
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];

Expand Down Expand Up @@ -450,13 +542,31 @@ export default class MessageComposer extends React.Component<IProps, IState> {
/>;
}

controls.push(
<Stickerpicker
room={this.props.room}
showStickers={this.state.showStickers}
setShowStickers={this.showStickers}
menuPosition={menuPosition} />,
);

const showSendButton = !this.state.isComposerEmpty || this.state.haveRecording;

return (
<div className="mx_MessageComposer mx_GroupLayout">
<div className="mx_MessageComposer mx_GroupLayout" ref={this.ref}>
{ recordingTooltip }
<div className="mx_MessageComposer_wrapper">
<ReplyPreview permalinkCreator={this.props.permalinkCreator} />
<div className="mx_MessageComposer_row">
{ controls }
{ this.renderButtons(menuPosition) }
{ showSendButton && (
<SendButton
key="controls_send"
onClick={this.sendMessage}
title={this.state.haveRecording ? _t("Send voice message") : undefined}
/>
) }
</div>
</div>
</div>
Expand Down
Loading