diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 5c8f6809de6..94452423061 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -237,15 +237,6 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/composer/sticker.svg'); } -.mx_MessageComposer_buttonMenu::before { - mask-image: url('$(res)/img/image-view/more.svg'); -} - -.mx_MessageComposer_closeButtonMenu::before { - transform: rotate(90deg); - transform-origin: center; -} - .mx_MessageComposer_sendMessage { cursor: pointer; position: relative; @@ -365,8 +356,3 @@ limitations under the License. margin-right: 0; } } - -.mx_MessageComposer_Menu .mx_CallContextMenu_item { - display: flex; - align-items: center; -} diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 6b66ae4ba34..466675ac648 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -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, { createRef } from 'react'; +import React from 'react'; import classNames from 'classnames'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; @@ -27,13 +27,7 @@ import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalin import ContentMessages from '../../../ContentMessages'; import E2EIcon from './E2EIcon'; import SettingsStore from "../../../settings/SettingsStore"; -import { - aboveLeftOf, - ContextMenu, - ContextMenuTooltipButton, - useContextMenu, - MenuItem, -} from "../../structures/ContextMenu"; +import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import ReplyPreview from "./ReplyPreview"; import { UIFeature } from "../../../settings/UIFeature"; @@ -51,9 +45,6 @@ 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; @@ -80,13 +71,13 @@ function SendButton(props: ISendButtonProps) { ); } -const EmojiButton = ({ addEmoji, menuPosition }) => { +const EmojiButton = ({ addEmoji }) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); let contextMenu; if (menuDisplayed) { - const position = menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect()); - contextMenu = + const buttonRect = button.current.getBoundingClientRect(); + contextMenu = ; } @@ -205,9 +196,6 @@ interface IState { haveRecording: boolean; recordingTimeLeftSeconds?: number; me?: RoomMember; - narrowMode?: boolean; - isMenuOpen: boolean; - showStickers: boolean; } @replaceableComponent("views.rooms.MessageComposer") @@ -215,7 +203,6 @@ export default class MessageComposer extends React.Component { private dispatcherRef: string; private messageComposerInput: SendMessageComposer; private voiceRecordingButton: VoiceRecordComposerTile; - private ref: React.RefObject = createRef(); static defaultProps = { replyInThread: false, @@ -233,8 +220,6 @@ export default class MessageComposer extends React.Component { isComposerEmpty: true, haveRecording: false, recordingTimeLeftSeconds: null, // when set to a number, shows a toast - isMenuOpen: false, - showStickers: false, }; } @@ -242,21 +227,8 @@ export default class MessageComposer extends React.Component { 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 @@ -291,8 +263,6 @@ export default class MessageComposer extends React.Component { } 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) => { @@ -399,96 +369,6 @@ export default class MessageComposer extends React.Component { } }; - 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 = new Map(); - if (!this.state.haveRecording) { - buttons.set( - _t("Send File"), - , - ); - buttons.set( - _t("Show Emojis"), - , - ); - } - if (this.shouldShowStickerPicker()) { - buttons.set( - _t("Show Stickers"), - this.showStickers(!this.state.showStickers)} - title={this.state.showStickers ? _t("Hide Stickers") : _t("Show Stickers")} - />, - ); - } - if (!this.state.haveRecording && !this.state.narrowMode) { - buttons.set( - _t("Send voice message"), - this.voiceRecordingButton?.onRecordStartEndClick()} - title={_t("Send voice message")} - />, - ); - } - - if (!this.state.narrowMode) { - return Array.from(buttons.values()); - } else { - const classnames = classNames({ - mx_MessageComposer_button: true, - mx_MessageComposer_buttonMenu: true, - mx_MessageComposer_closeButtonMenu: this.state.isMenuOpen, - }); - - return <> - { buttons[0] } - - { this.state.isMenuOpen && ( - - { Array.from(buttons).slice(1).map(([label, button]) => ( - - { button } - { label } - - )) } - - ) } - ; - } - } - render() { const controls = [ this.state.me && !this.props.compact ? : null, @@ -497,12 +377,6 @@ export default class MessageComposer extends React.Component { 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( { />, ); + if (!this.state.haveRecording) { + controls.push( + , + , + ); + } + + if (SettingsStore.getValue(UIFeature.Widgets) && + SettingsStore.getValue("MessageComposerInput.showStickersButton") && + !this.state.haveRecording) { + controls.push(); + } + controls.push( this.voiceRecordingButton = c} room={this.props.room} />); + + if (!this.state.isComposerEmpty || this.state.haveRecording) { + controls.push( + , + ); + } } else if (this.state.tombstone) { const replacementRoomId = this.state.tombstone.getContent()['replacement_room']; @@ -562,15 +459,6 @@ export default class MessageComposer extends React.Component { yOffset={-50} />; } - controls.push( - , - ); - - const showSendButton = !this.state.isComposerEmpty || this.state.haveRecording; const classes = classNames({ "mx_MessageComposer": true, @@ -579,7 +467,7 @@ export default class MessageComposer extends React.Component { }); return ( -
+
{ recordingTooltip }
{ this.props.showReplyPreview && ( @@ -587,14 +475,6 @@ export default class MessageComposer extends React.Component { ) }
{ controls } - { this.renderButtons(menuPosition) } - { showSendButton && ( - - ) }
diff --git a/src/components/views/rooms/Stickerpicker.tsx b/src/components/views/rooms/Stickerpicker.tsx index 0806b4ab9dc..33367c11515 100644 --- a/src/components/views/rooms/Stickerpicker.tsx +++ b/src/components/views/rooms/Stickerpicker.tsx @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import React from 'react'; +import classNames from 'classnames'; import { Room } from 'matrix-js-sdk/src/models/room'; import { _t, _td } from '../../../languageHandler'; import AppTile from '../elements/AppTile'; @@ -26,6 +27,7 @@ import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; import SettingsStore from "../../../settings/SettingsStore"; import { ChevronFace, ContextMenu } from "../../structures/ContextMenu"; import { WidgetType } from "../../../widgets/WidgetType"; +import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { Action } from "../../../dispatcher/actions"; import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore"; import { replaceableComponent } from "../../../utils/replaceableComponent"; @@ -42,12 +44,10 @@ const PERSISTED_ELEMENT_KEY = "stickerPicker"; interface IProps { room: Room; - showStickers: boolean; - menuPosition?: any; - setShowStickers: (showStickers: boolean) => void; } interface IState { + showStickers: boolean; imError: string; stickerpickerX: number; stickerpickerY: number; @@ -72,6 +72,7 @@ export default class Stickerpicker extends React.PureComponent { constructor(props: IProps) { super(props); this.state = { + showStickers: false, imError: null, stickerpickerX: null, stickerpickerY: null, @@ -113,7 +114,7 @@ export default class Stickerpicker extends React.PureComponent { console.warn('No widget ID specified, not disabling assets'); } - this.props.setShowStickers(false); + this.setState({ showStickers: false }); WidgetUtils.removeStickerpickerWidgets().then(() => { this.forceUpdate(); }).catch((e) => { @@ -145,15 +146,15 @@ export default class Stickerpicker extends React.PureComponent { } public componentDidUpdate(prevProps: IProps, prevState: IState): void { - this.sendVisibilityToWidget(this.props.showStickers); + this.sendVisibilityToWidget(this.state.showStickers); } private imError(errorMsg: string, e: Error): void { console.error(errorMsg, e); this.setState({ + showStickers: false, imError: _t(errorMsg), }); - this.props.setShowStickers(false); } private updateWidget = (): void => { @@ -193,12 +194,12 @@ export default class Stickerpicker extends React.PureComponent { this.forceUpdate(); break; case "stickerpicker_close": - this.props.setShowStickers(false); + this.setState({ showStickers: false }); break; case Action.AfterRightPanelPhaseChange: case "show_left_panel": case "hide_left_panel": - this.props.setShowStickers(false); + this.setState({ showStickers: false }); break; } }; @@ -337,8 +338,8 @@ export default class Stickerpicker extends React.PureComponent { const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19; - this.props.setShowStickers(true); this.setState({ + showStickers: true, stickerpickerX: x, stickerpickerY: y, stickerpickerChevronOffset, @@ -350,8 +351,8 @@ export default class Stickerpicker extends React.PureComponent { * @param {Event} ev Event that triggered the function call */ private onHideStickersClick = (ev: React.MouseEvent): void => { - if (this.props.showStickers) { - this.props.setShowStickers(false); + if (this.state.showStickers) { + this.setState({ showStickers: false }); } }; @@ -359,8 +360,8 @@ export default class Stickerpicker extends React.PureComponent { * Called when the window is resized */ private onResize = (): void => { - if (this.props.showStickers) { - this.props.setShowStickers(false); + if (this.state.showStickers) { + this.setState({ showStickers: false }); } }; @@ -368,8 +369,8 @@ export default class Stickerpicker extends React.PureComponent { * The stickers picker was hidden */ private onFinished = (): void => { - if (this.props.showStickers) { - this.props.setShowStickers(false); + if (this.state.showStickers) { + this.setState({ showStickers: false }); } }; @@ -394,23 +395,54 @@ export default class Stickerpicker extends React.PureComponent { }; public render(): JSX.Element { - if (!this.props.showStickers) return null; - - return - - ; + let stickerPicker; + let stickersButton; + const className = classNames( + "mx_MessageComposer_button", + "mx_MessageComposer_stickers", + "mx_Stickers_hideStickers", + "mx_MessageComposer_button_highlight", + ); + if (this.state.showStickers) { + // Show hide-stickers button + stickersButton = + ; + + stickerPicker = + + ; + } else { + // Show show-stickers button + stickersButton = + ; + } + return + { stickersButton } + { stickerPicker } + ; } } diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index 288d97fc501..bd573fa4745 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -20,6 +20,7 @@ import React, { ReactNode } from "react"; import { IUpload, RecordingState, VoiceRecording } from "../../../audio/VoiceRecording"; import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import classNames from "classnames"; import LiveRecordingWaveform from "../audio_messages/LiveRecordingWaveform"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import LiveRecordingClock from "../audio_messages/LiveRecordingClock"; @@ -136,7 +137,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent { + private onRecordStartEndClick = async () => { if (this.state.recorder) { await this.state.recorder.stop(); return; @@ -214,23 +215,27 @@ export default class VoiceRecordComposerTile extends React.PureComponent; if (this.state.recorder && !this.state.recorder?.isRecording) { - stopBtn = null; + stopOrRecordBtn = null; } } @@ -259,10 +264,13 @@ export default class VoiceRecordComposerTile extends React.PureComponent; } + // The record button (mic icon) is meant to be on the right edge, but we also want the + // stop button to be left of the waveform area. Luckily, none of the surrounding UI is + // rendered when we're not recording, so the record button ends up in the correct spot. return (<> { uploadIndicator } { deleteButton } - { stopBtn } + { stopOrRecordBtn } { this.renderWaveformArea() } ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1f8104da1d4..7d754a618ab 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1564,12 +1564,7 @@ "Send a reply…": "Send a reply…", "Send an encrypted message…": "Send an encrypted message…", "Send a message…": "Send a message…", - "Send File": "Send File", - "Show Emojis": "Show Emojis", - "Show Stickers": "Show Stickers", - "Hide Stickers": "Hide Stickers", "Send voice message": "Send voice message", - "Composer menu": "Composer menu", "The conversation continues here.": "The conversation continues here.", "This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.", "You do not have permission to post to this room": "You do not have permission to post to this room", @@ -1730,6 +1725,8 @@ "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", "Add some now": "Add some now", "Stickerpack": "Stickerpack", + "Hide Stickers": "Hide Stickers", + "Show Stickers": "Show Stickers", "Failed to revoke invite": "Failed to revoke invite", "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.", "Admin Tools": "Admin Tools",