From 329292eb9b731489abc9605ed3ba05d87e274c3d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 6 Sep 2021 22:11:35 -0600 Subject: [PATCH 1/5] Revert "Revert "Create narrow mode for Composer"" --- res/css/views/rooms/_MessageComposer.scss | 14 ++ .../views/rooms/MessageComposer.tsx | 178 +++++++++++++++--- src/components/views/rooms/Stickerpicker.tsx | 96 ++++------ .../views/rooms/VoiceRecordComposerTile.tsx | 26 +-- src/i18n/strings/en_EN.json | 7 +- 5 files changed, 209 insertions(+), 112 deletions(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 9445242306..5c8f6809de 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -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/image-view/more.svg'); +} + +.mx_MessageComposer_closeButtonMenu::before { + transform: rotate(90deg); + transform-origin: center; +} + .mx_MessageComposer_sendMessage { cursor: pointer; position: relative; @@ -356,3 +365,8 @@ 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 466675ac64..6b66ae4ba3 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 from 'react'; +import React, { createRef } from 'react'; import classNames from 'classnames'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; @@ -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"; @@ -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; @@ -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 = + const position = menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect()); + contextMenu = ; } @@ -196,6 +205,9 @@ interface IState { haveRecording: boolean; recordingTimeLeftSeconds?: number; me?: RoomMember; + narrowMode?: boolean; + isMenuOpen: boolean; + showStickers: boolean; } @replaceableComponent("views.rooms.MessageComposer") @@ -203,6 +215,7 @@ 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, @@ -220,6 +233,8 @@ 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, }; } @@ -227,8 +242,21 @@ 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 @@ -263,6 +291,8 @@ 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) => { @@ -369,6 +399,96 @@ 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, @@ -377,6 +497,12 @@ 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']; @@ -459,6 +562,15 @@ 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, @@ -467,7 +579,7 @@ export default class MessageComposer extends React.Component { }); return ( -
+
{ recordingTooltip }
{ this.props.showReplyPreview && ( @@ -475,6 +587,14 @@ 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 33367c1151..0806b4ab9d 100644 --- a/src/components/views/rooms/Stickerpicker.tsx +++ b/src/components/views/rooms/Stickerpicker.tsx @@ -14,7 +14,6 @@ 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'; @@ -27,7 +26,6 @@ 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"; @@ -44,10 +42,12 @@ 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,7 +72,6 @@ export default class Stickerpicker extends React.PureComponent { constructor(props: IProps) { super(props); this.state = { - showStickers: false, imError: null, stickerpickerX: null, stickerpickerY: null, @@ -114,7 +113,7 @@ export default class Stickerpicker extends React.PureComponent { console.warn('No widget ID specified, not disabling assets'); } - this.setState({ showStickers: false }); + this.props.setShowStickers(false); WidgetUtils.removeStickerpickerWidgets().then(() => { this.forceUpdate(); }).catch((e) => { @@ -146,15 +145,15 @@ export default class Stickerpicker extends React.PureComponent { } public componentDidUpdate(prevProps: IProps, prevState: IState): void { - this.sendVisibilityToWidget(this.state.showStickers); + this.sendVisibilityToWidget(this.props.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 => { @@ -194,12 +193,12 @@ export default class Stickerpicker extends React.PureComponent { this.forceUpdate(); break; case "stickerpicker_close": - this.setState({ showStickers: false }); + this.props.setShowStickers(false); break; case Action.AfterRightPanelPhaseChange: case "show_left_panel": case "hide_left_panel": - this.setState({ showStickers: false }); + this.props.setShowStickers(false); break; } }; @@ -338,8 +337,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, @@ -351,8 +350,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.state.showStickers) { - this.setState({ showStickers: false }); + if (this.props.showStickers) { + this.props.setShowStickers(false); } }; @@ -360,8 +359,8 @@ export default class Stickerpicker extends React.PureComponent { * Called when the window is resized */ private onResize = (): void => { - if (this.state.showStickers) { - this.setState({ showStickers: false }); + if (this.props.showStickers) { + this.props.setShowStickers(false); } }; @@ -369,8 +368,8 @@ export default class Stickerpicker extends React.PureComponent { * The stickers picker was hidden */ private onFinished = (): void => { - if (this.state.showStickers) { - this.setState({ showStickers: false }); + if (this.props.showStickers) { + this.props.setShowStickers(false); } }; @@ -395,54 +394,23 @@ export default class Stickerpicker extends React.PureComponent { }; public render(): JSX.Element { - 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 = - ; + if (!this.props.showStickers) return null; - stickerPicker = - - ; - } else { - // Show show-stickers button - stickersButton = - ; - } - return - { stickersButton } - { stickerPicker } - ; + return + + ; } } diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index bd573fa474..288d97fc50 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -20,7 +20,6 @@ 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"; @@ -137,7 +136,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent { + public onRecordStartEndClick = async () => { if (this.state.recorder) { await this.state.recorder.stop(); return; @@ -215,27 +214,23 @@ export default class VoiceRecordComposerTile extends React.PureComponent; if (this.state.recorder && !this.state.recorder?.isRecording) { - stopOrRecordBtn = null; + stopBtn = null; } } @@ -264,13 +259,10 @@ 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 } - { stopOrRecordBtn } + { stopBtn } { this.renderWaveformArea() } ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7d754a618a..1f8104da1d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1564,7 +1564,12 @@ "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", @@ -1725,8 +1730,6 @@ "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", From 646ef197fe5212ebac395966f38d918785016414 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 7 Sep 2021 16:02:26 +0100 Subject: [PATCH 2/5] Fix PR UI defects --- res/css/views/rooms/_MessageComposer.scss | 6 +++- src/components/structures/RightPanel.tsx | 5 ++- src/components/structures/RoomView.tsx | 3 +- src/components/structures/ThreadView.tsx | 3 ++ .../views/rooms/MessageComposer.tsx | 33 ++++++++++++------- src/i18n/strings/en_EN.json | 12 ++++--- 6 files changed, 43 insertions(+), 19 deletions(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 5c8f6809de..26db5dbafe 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -358,12 +358,16 @@ limitations under the License. margin-right: 0; .mx_MessageComposer_wrapper { - padding: 0; + padding: 0 0 0 25px; } .mx_MessageComposer_button:last-child { margin-right: 0; } + + .mx_MessageComposer_e2eIcon { + left: 0; + } } .mx_MessageComposer_Menu .mx_CallContextMenu_item { diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 67634c63d2..114d020c66 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -53,6 +53,7 @@ import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; import { throttle } from 'lodash'; import SpaceStore from "../../stores/SpaceStore"; import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; +import { E2EStatus } from '../../utils/ShieldUtils'; interface IProps { room?: Room; // if showing panels for a given room, this is set @@ -60,6 +61,7 @@ interface IProps { user?: User; // used if we know the user ahead of opening the panel resizeNotifier: ResizeNotifier; permalinkCreator?: RoomPermalinkCreator; + e2eStatus?: E2EStatus; } interface IState { @@ -319,7 +321,8 @@ export default class RightPanel extends React.Component { resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} mxEvent={this.state.event} - permalinkCreator={this.props.permalinkCreator} />; + permalinkCreator={this.props.permalinkCreator} + e2eStatus={this.props.e2eStatus} />; break; case RightPanelPhases.ThreadPanel: diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 8223c12e77..9dea4b1d1d 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -2054,7 +2054,8 @@ export default class RoomView extends React.Component { ? + permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)} + e2eStatus={this.state.e2eStatus} /> : null; const timelineClasses = classNames("mx_RoomView_timeline", { diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 134f018aed..304479cce3 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -33,6 +33,7 @@ import { ActionPayload } from '../../dispatcher/payloads'; import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload'; import { Action } from '../../dispatcher/actions'; import { MatrixClientPeg } from '../../MatrixClientPeg'; +import { E2EStatus } from '../../utils/ShieldUtils'; interface IProps { room: Room; @@ -40,6 +41,7 @@ interface IProps { resizeNotifier: ResizeNotifier; mxEvent: MatrixEvent; permalinkCreator?: RoomPermalinkCreator; + e2eStatus?: E2EStatus; } interface IState { @@ -144,6 +146,7 @@ export default class ThreadView extends React.Component { replyToEvent={this.state?.thread?.replyToEvent} showReplyPreview={false} permalinkCreator={this.props.permalinkCreator} + e2eStatus={this.props.e2eStatus} compact={true} /> diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 6b66ae4ba3..49acb13592 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -53,6 +53,7 @@ import EmojiPicker from '../emojipicker/EmojiPicker'; import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar"; import UIStore, { UI_EVENTS } from '../../../stores/UIStore'; +let instanceCount = 0; const NARROW_MODE_BREAKPOINT = 500; interface IComposerAvatarProps { @@ -216,6 +217,7 @@ export default class MessageComposer extends React.Component { private messageComposerInput: SendMessageComposer; private voiceRecordingButton: VoiceRecordComposerTile; private ref: React.RefObject = createRef(); + private instanceId: number; static defaultProps = { replyInThread: false, @@ -236,14 +238,16 @@ export default class MessageComposer extends React.Component { isMenuOpen: false, showStickers: false, }; + + this.instanceId = instanceCount++; } 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); + UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current); + UIStore.instance.on(`MessageComposer${this.instanceId}`, this.onResize); } private onResize = (type: UI_EVENTS, entry: ResizeObserverEntry) => { @@ -291,8 +295,8 @@ 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); + UIStore.instance.stopTrackingElementDimensions(`MessageComposer${this.instanceId}`); + UIStore.instance.removeListener(`MessageComposer${this.instanceId}`, this.onResize); } private onRoomStateEvents = (ev, state) => { @@ -342,7 +346,11 @@ export default class MessageComposer extends React.Component { private renderPlaceholderText = () => { if (this.props.replyToEvent) { - if (this.props.e2eStatus) { + if (this.props.replyInThread && this.props.e2eStatus) { + return _t('Reply to encrypted thread…'); + } else if (this.props.replyInThread) { + return _t('Reply to thread…'); + } else if (this.props.e2eStatus) { return _t('Send an encrypted reply…'); } else { return _t('Send a reply…'); @@ -419,17 +427,17 @@ export default class MessageComposer extends React.Component { const buttons = new Map(); if (!this.state.haveRecording) { buttons.set( - _t("Send File"), + _t("Send a file"), , ); buttons.set( - _t("Show Emojis"), + _t("Add emoji"), , ); } if (this.shouldShowStickerPicker()) { buttons.set( - _t("Show Stickers"), + _t("Send a sticker"), { } if (!this.state.haveRecording && !this.state.narrowMode) { buttons.set( - _t("Send voice message"), + _t("Send a voice message"), this.voiceRecordingButton?.onRecordStartEndClick()} @@ -450,8 +458,9 @@ export default class MessageComposer extends React.Component { ); } + const buttonsArray = Array.from(buttons.values()); if (!this.state.narrowMode) { - return Array.from(buttons.values()); + return buttonsArray; } else { const classnames = classNames({ mx_MessageComposer_button: true, @@ -460,11 +469,11 @@ export default class MessageComposer extends React.Component { }); return <> - { buttons[0] } + { buttonsArray[0] } { this.state.isMenuOpen && ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1f8104da1d..b4b1071013 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1560,16 +1560,20 @@ "Send message": "Send message", "Emoji picker": "Emoji picker", "Upload file": "Upload file", + "Reply to encrypted thread…": "Reply to encrypted thread…", + "Reply to thread…": "Reply to thread…", "Send an encrypted reply…": "Send an encrypted reply…", "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", + "Send a file": "Send a file", + "Add emoji": "Add emoji", + "Send a sticker": "Send a sticker", "Hide Stickers": "Hide Stickers", + "Show Stickers": "Show Stickers", + "Send a voice message": "Send a voice message", "Send voice message": "Send voice message", - "Composer menu": "Composer menu", + "More options": "More options", "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", From bbf66a001136cb6240b06b86085891678fce08f7 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 7 Sep 2021 17:10:09 +0100 Subject: [PATCH 3/5] Make label clickable on narrow mode context menu --- res/css/views/rooms/_MessageComposer.scss | 22 ++++++-- .../elements/AccessibleTooltipButton.tsx | 4 +- .../views/rooms/MessageComposer.tsx | 52 +++++++++++-------- src/i18n/strings/en_EN.json | 6 +-- 4 files changed, 53 insertions(+), 31 deletions(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 26db5dbafe..faa3171d67 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -186,11 +186,14 @@ limitations under the License. } .mx_MessageComposer_button { + --size: 26px; position: relative; margin-right: 6px; cursor: pointer; - height: 26px; - width: 26px; + height: var(--size); + line-height: var(--size); + width: auto; + padding-left: calc(var(--size) + 5px); border-radius: 100%; &::before { @@ -207,8 +210,21 @@ limitations under the License. mask-position: center; } + &::after { + content: ''; + position: absolute; + left: 0; + top: 0; + z-index: 0; + width: var(--size); + height: var(--size); + border-radius: 50%; + } + &:hover { - background: rgba($accent-color, 0.1); + &::after { + background: rgba($accent-color, 0.1); + } &::before { background-color: $accent-color; diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index 8ac41ad1a2..d2a4801a2d 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -25,6 +25,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; interface ITooltipProps extends React.ComponentProps { title: string; tooltip?: React.ReactNode; + label?: React.ReactNode; tooltipClassName?: string; forceHide?: boolean; yOffset?: number; @@ -84,7 +85,8 @@ export default class AccessibleTooltipButton extends React.PureComponent { children } - { tip } + { this.props.label } + { (tooltip || title) && tip } ); } diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 49acb13592..6372ecc1f5 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -81,7 +81,13 @@ function SendButton(props: ISendButtonProps) { ); } -const EmojiButton = ({ addEmoji, menuPosition }) => { +interface IEmojiButtonProps { + addEmoji: (unicode: string) => boolean; + menuPosition: any; // TODO: Types + narrowMode: boolean; +} + +const EmojiButton: React.FC = ({ addEmoji, menuPosition, narrowMode }) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); let contextMenu; @@ -103,12 +109,11 @@ const EmojiButton = ({ addEmoji, menuPosition }) => { // TODO: replace ContextMenuTooltipButton with a unified representation of // the header buttons and the right panel buttons return - { contextMenu } @@ -364,11 +369,12 @@ export default class MessageComposer extends React.Component { } }; - private addEmoji(emoji: string) { + private addEmoji(emoji: string): boolean { dis.dispatch({ action: Action.ComposerInsert, text: emoji, }); + return true; } private sendMessage = async () => { @@ -424,32 +430,34 @@ export default class MessageComposer extends React.Component { }; private renderButtons(menuPosition): JSX.Element | JSX.Element[] { - const buttons = new Map(); + const buttons: JSX.Element[] = []; if (!this.state.haveRecording) { - buttons.set( - _t("Send a file"), + buttons.push( , ); - buttons.set( - _t("Add emoji"), - , + buttons.push( + , ); } if (this.shouldShowStickerPicker()) { - buttons.set( - _t("Send a sticker"), + let title; + if (!this.state.narrowMode) { + title = this.state.showStickers ? _t("Hide Stickers") : _t("Show Stickers"); + } + + buttons.push( this.showStickers(!this.state.showStickers)} - title={this.state.showStickers ? _t("Hide Stickers") : _t("Show Stickers")} + title={title} + label={this.state.narrowMode && _t("Send a sticker")} />, ); } if (!this.state.haveRecording && !this.state.narrowMode) { - buttons.set( - _t("Send a voice message"), + buttons.push( this.voiceRecordingButton?.onRecordStartEndClick()} @@ -458,9 +466,8 @@ export default class MessageComposer extends React.Component { ); } - const buttonsArray = Array.from(buttons.values()); if (!this.state.narrowMode) { - return buttonsArray; + return buttons; } else { const classnames = classNames({ mx_MessageComposer_button: true, @@ -469,7 +476,7 @@ export default class MessageComposer extends React.Component { }); return <> - { buttonsArray[0] } + { buttons[0] } { menuWidth={150} wrapperClassName="mx_MessageComposer_Menu" > - { Array.from(buttons).slice(1).map(([label, button]) => ( - + { buttons.slice(1).map((button, index) => ( + { button } - { label } )) } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b4b1071013..1200042f2f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1559,6 +1559,7 @@ "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)", "Send message": "Send message", "Emoji picker": "Emoji picker", + "Send an emoji": "Send an emoji", "Upload file": "Upload file", "Reply to encrypted thread…": "Reply to encrypted thread…", "Reply to thread…": "Reply to thread…", @@ -1566,12 +1567,9 @@ "Send a reply…": "Send a reply…", "Send an encrypted message…": "Send an encrypted message…", "Send a message…": "Send a message…", - "Send a file": "Send a file", - "Add emoji": "Add emoji", - "Send a sticker": "Send a sticker", "Hide Stickers": "Hide Stickers", "Show Stickers": "Show Stickers", - "Send a voice message": "Send a voice message", + "Send an sticker": "Send an sticker", "Send voice message": "Send voice message", "More options": "More options", "The conversation continues here.": "The conversation continues here.", From aa534442670f817ff700a509721a31ace645e61f Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 9 Sep 2021 13:27:25 +0100 Subject: [PATCH 4/5] Improve narrow composer usability --- res/css/views/rooms/_EventTile.scss | 4 ++++ res/css/views/rooms/_MessageComposer.scss | 4 ++-- src/components/views/rooms/MessageComposer.tsx | 3 +-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 351abc5cd9..4495ec4f29 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -733,4 +733,8 @@ $hover-select-border: 4px; padding-bottom: 5px; margin-bottom: 5px; } + + .mx_MessageComposer_sendMessage { + margin-right: 0; + } } diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index faa3171d67..9ba966c083 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -221,7 +221,8 @@ limitations under the License. border-radius: 50%; } - &:hover { + &:hover, + &.mx_MessageComposer_closeButtonMenu { &::after { background: rgba($accent-color, 0.1); } @@ -265,7 +266,6 @@ limitations under the License. .mx_MessageComposer_sendMessage { cursor: pointer; position: relative; - margin-right: 6px; width: 32px; height: 32px; border-radius: 100%; diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 6372ecc1f5..dd6ce10825 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -30,7 +30,6 @@ import SettingsStore from "../../../settings/SettingsStore"; import { aboveLeftOf, ContextMenu, - ContextMenuTooltipButton, useContextMenu, MenuItem, } from "../../structures/ContextMenu"; @@ -113,7 +112,7 @@ const EmojiButton: React.FC = ({ addEmoji, menuPosition, narr className={className} onClick={openMenu} title={!narrowMode && _t('Emoji picker')} - label={narrowMode && _t("Send an emoji")} + label={narrowMode && _t("Add emoji")} /> { contextMenu } From b5bed3297390091d3c1a6418a5274195e64d5ddb Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 9 Sep 2021 13:40:07 +0100 Subject: [PATCH 5/5] Fix i18n --- src/i18n/strings/en_EN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a2262a6afa..f3ae9424e0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1559,7 +1559,7 @@ "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)", "Send message": "Send message", "Emoji picker": "Emoji picker", - "Send an emoji": "Send an emoji", + "Add emoji": "Add emoji", "Upload file": "Upload file", "Reply to encrypted thread…": "Reply to encrypted thread…", "Reply to thread…": "Reply to thread…", @@ -1569,7 +1569,7 @@ "Send a message…": "Send a message…", "Hide Stickers": "Hide Stickers", "Show Stickers": "Show Stickers", - "Send an sticker": "Send an sticker", + "Send a sticker": "Send a sticker", "Send voice message": "Send voice message", "More options": "More options", "The conversation continues here.": "The conversation continues here.",