From 3c33e3cc613d56596dc97c540a616b18fb8620ab Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 25 Aug 2021 11:35:23 +0100 Subject: [PATCH 01/12] Make all buttons part of a button array Preparation work for narrow layout of the message composer. The voice record button needed to be extracted from VoiceRecordComposerTile --- .../views/rooms/MessageComposer.tsx | 19 ++++++++++++++++--- .../views/rooms/VoiceRecordComposerTile.tsx | 4 +++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 8455e9aa11c..c39d8bebc87 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -368,6 +368,8 @@ export default class MessageComposer extends React.Component { null, ]; + const buttons = []; + if (!this.state.tombstone && this.state.canSendMessages) { controls.push( { ); if (!this.state.haveRecording) { - controls.push( + buttons.push( , , ); @@ -392,7 +394,7 @@ export default class MessageComposer extends React.Component { if (SettingsStore.getValue(UIFeature.Widgets) && SettingsStore.getValue("MessageComposerInput.showStickersButton") && !this.state.haveRecording) { - controls.push(); + buttons.push(); } controls.push( { ref={c => this.voiceRecordingButton = c} room={this.props.room} />); + if (!this.state.haveRecording) { + buttons.push( + this.voiceRecordingButton?.onRecordStartEndClick()} + title={_t("Send voice message")} + />, + ); + } + if (!this.state.isComposerEmpty || this.state.haveRecording) { - controls.push( + buttons.push( {
{ controls } + { buttons }
diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index e8befb90fa1..c6f831de3bc 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -137,7 +137,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent { + public onRecordStartEndClick = async () => { if (this.state.recorder) { await this.state.recorder.stop(); return; @@ -215,6 +215,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent Date: Wed, 25 Aug 2021 14:26:21 +0100 Subject: [PATCH 02/12] Migrate StickerPicker to TypeScript --- .../{Stickerpicker.js => Stickerpicker.tsx} | 162 ++++++++++-------- 1 file changed, 86 insertions(+), 76 deletions(-) rename src/components/views/rooms/{Stickerpicker.js => Stickerpicker.tsx} (77%) diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.tsx similarity index 77% rename from src/components/views/rooms/Stickerpicker.js rename to src/components/views/rooms/Stickerpicker.tsx index 6649948331b..d3d1538bd6f 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.tsx @@ -15,22 +15,25 @@ 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'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import AccessibleButton from '../elements/AccessibleButton'; -import WidgetUtils from '../../../utils/WidgetUtils'; +import WidgetUtils, { IWidgetEvent } from '../../../utils/WidgetUtils'; import PersistedElement from "../elements/PersistedElement"; import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; import SettingsStore from "../../../settings/SettingsStore"; -import { ContextMenu } from "../../structures/ContextMenu"; +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"; +import { ActionPayload } from '../../../dispatcher/payloads'; +import ScalarAuthClient from '../../../ScalarAuthClient'; // This should be below the dialog level (4000), but above the rest of the UI (1000-2000). // We sit in a context menu, so this should be given to the context menu. @@ -39,27 +42,35 @@ const STICKERPICKER_Z_INDEX = 3500; // Key to store the widget's AppTile under in PersistedElement const PERSISTED_ELEMENT_KEY = "stickerPicker"; +interface IProps { + room: Room; +} + +interface IState { + showStickers: boolean; + imError: string; + stickerpickerX: number; + stickerpickerY: number; + stickerpickerChevronOffset?: number; + stickerpickerWidget: IWidgetEvent; + widgetId: string; +} + @replaceableComponent("views.rooms.Stickerpicker") -export default class Stickerpicker extends React.PureComponent { +export default class Stickerpicker extends React.PureComponent { static currentWidget; - constructor(props) { - super(props); - this._onShowStickersClick = this._onShowStickersClick.bind(this); - this._onHideStickersClick = this._onHideStickersClick.bind(this); - this._launchManageIntegrations = this._launchManageIntegrations.bind(this); - this._removeStickerpickerWidgets = this._removeStickerpickerWidgets.bind(this); - this._updateWidget = this._updateWidget.bind(this); - this._onWidgetAction = this._onWidgetAction.bind(this); - this._onResize = this._onResize.bind(this); - this._onFinished = this._onFinished.bind(this); + private dispatcherRef: string; - this.popoverWidth = 300; - this.popoverHeight = 300; + private prevSentVisibility: boolean; - // This is loaded by _acquireScalarClient on an as-needed basis. - this.scalarClient = null; + private popoverWidth = 300; + private popoverHeight = 300; + // This is loaded by _acquireScalarClient on an as-needed basis. + private scalarClient: ScalarAuthClient = null; + constructor(props: IProps) { + super(props); this.state = { showStickers: false, imError: null, @@ -70,7 +81,7 @@ export default class Stickerpicker extends React.PureComponent { }; } - _acquireScalarClient() { + private acquireScalarClient(): Promise { if (this.scalarClient) return Promise.resolve(this.scalarClient); // TODO: Pick the right manager for the widget if (IntegrationManagers.sharedInstance().hasManager()) { @@ -79,15 +90,15 @@ export default class Stickerpicker extends React.PureComponent { this.forceUpdate(); return this.scalarClient; }).catch((e) => { - this._imError(_td("Failed to connect to integration manager"), e); + this.imError(_td("Failed to connect to integration manager"), e); }); } else { IntegrationManagers.sharedInstance().openNoManagerDialog(); } } - async _removeStickerpickerWidgets() { - const scalarClient = await this._acquireScalarClient(); + private removeStickerpickerWidgets = async (): Promise => { + const scalarClient = await this.acquireScalarClient(); console.log('Removing Stickerpicker widgets'); if (this.state.widgetId) { if (scalarClient) { @@ -109,36 +120,36 @@ export default class Stickerpicker extends React.PureComponent { }).catch((e) => { console.error('Failed to remove sticker picker widget', e); }); - } + }; - componentDidMount() { + public componentDidMount(): void { // Close the sticker picker when the window resizes - window.addEventListener('resize', this._onResize); + window.addEventListener('resize', this.onResize); - this.dispatcherRef = dis.register(this._onWidgetAction); + this.dispatcherRef = dis.register(this.onWidgetAction); // Track updates to widget state in account data - MatrixClientPeg.get().on('accountData', this._updateWidget); + MatrixClientPeg.get().on('accountData', this.updateWidget); // Initialise widget state from current account data - this._updateWidget(); + this.updateWidget(); } - componentWillUnmount() { + public componentWillUnmount(): void { const client = MatrixClientPeg.get(); - if (client) client.removeListener('accountData', this._updateWidget); + if (client) client.removeListener('accountData', this.updateWidget); - window.removeEventListener('resize', this._onResize); + window.removeEventListener('resize', this.onResize); if (this.dispatcherRef) { dis.unregister(this.dispatcherRef); } } - componentDidUpdate(prevProps, prevState) { - this._sendVisibilityToWidget(this.state.showStickers); + public componentDidUpdate(prevProps: IProps, prevState: IState): void { + this.sendVisibilityToWidget(this.state.showStickers); } - _imError(errorMsg, e) { + private imError(errorMsg: string, e: Error): void { console.error(errorMsg, e); this.setState({ showStickers: false, @@ -146,7 +157,7 @@ export default class Stickerpicker extends React.PureComponent { }); } - _updateWidget() { + private updateWidget = (): void => { const stickerpickerWidget = WidgetUtils.getStickerpickerWidgets()[0]; if (!stickerpickerWidget) { Stickerpicker.currentWidget = null; @@ -175,9 +186,9 @@ export default class Stickerpicker extends React.PureComponent { stickerpickerWidget, widgetId: stickerpickerWidget ? stickerpickerWidget.id : null, }); - } + }; - _onWidgetAction(payload) { + private onWidgetAction = (payload: ActionPayload): void => { switch (payload.action) { case "user_widget_updated": this.forceUpdate(); @@ -191,11 +202,11 @@ export default class Stickerpicker extends React.PureComponent { this.setState({ showStickers: false }); break; } - } + }; - _defaultStickerpickerContent() { + private defaultStickerpickerContent(): JSX.Element { return ( -

{ _t("You don't currently have any stickerpacks enabled") }

{ _t("Add some now") }

@@ -204,29 +215,29 @@ export default class Stickerpicker extends React.PureComponent { ); } - _errorStickerpickerContent() { + private errorStickerpickerContent(): JSX.Element { return ( -
+

{ this.state.imError }

); } - _sendVisibilityToWidget(visible) { + private sendVisibilityToWidget(visible: boolean): void { if (!this.state.stickerpickerWidget) return; const messaging = WidgetMessagingStore.instance.getMessagingForId(this.state.stickerpickerWidget.id); - if (messaging && visible !== this._prevSentVisibility) { + if (messaging && visible !== this.prevSentVisibility) { messaging.updateVisibility(visible).catch(err => { console.error("Error updating widget visibility: ", err); }); - this._prevSentVisibility = visible; + this.prevSentVisibility = visible; } } - _getStickerpickerContent() { + public getStickerpickerContent(): JSX.Element { // Handle integration manager errors - if (this.state._imError) { - return this._errorStickerpickerContent(); + if (this.state.imError) { + return this.errorStickerpickerContent(); } // Stickers @@ -244,7 +255,7 @@ export default class Stickerpicker extends React.PureComponent { // Load stickerpack content if (stickerpickerWidget && stickerpickerWidget.content && stickerpickerWidget.content.url) { // Set default name - stickerpickerWidget.content.name = stickerpickerWidget.name || _t("Stickerpack"); + stickerpickerWidget.content.name = stickerpickerWidget.content.name || _t("Stickerpack"); // FIXME: could this use the same code as other apps? const stickerApp = { @@ -275,12 +286,12 @@ export default class Stickerpicker extends React.PureComponent { creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId} waitForIframeLoad={true} showMenubar={true} - onEditClick={this._launchManageIntegrations} - onDeleteClick={this._removeStickerpickerWidgets} + onEditClick={this.launchManageIntegrations} + onDeleteClick={this.removeStickerpickerWidgets} showTitle={false} showCancel={false} showPopout={false} - onMinimiseClick={this._onHideStickersClick} + onMinimiseClick={this.onHideStickersClick} handleMinimisePointerEvents={true} userWidget={true} /> @@ -290,7 +301,7 @@ export default class Stickerpicker extends React.PureComponent { ); } else { // Default content to show if stickerpicker widget not added - stickersContent = this._defaultStickerpickerContent(); + stickersContent = this.defaultStickerpickerContent(); } return stickersContent; } @@ -300,7 +311,7 @@ export default class Stickerpicker extends React.PureComponent { * Show the sticker picker overlay * If no stickerpacks have been added, show a link to the integration manager add sticker packs page. */ - _onShowStickersClick(e) { + private onShowStickersClick = (e: React.MouseEvent): void => { if (!SettingsStore.getValue("integrationProvisioning")) { // Intercept this case and spawn a warning. return IntegrationManagers.sharedInstance().showDisabledDialog(); @@ -308,7 +319,7 @@ export default class Stickerpicker extends React.PureComponent { // XXX: Simplify by using a context menu that is positioned relative to the sticker picker button - const buttonRect = e.target.getBoundingClientRect(); + const buttonRect = e.currentTarget.getBoundingClientRect(); // The window X and Y offsets are to adjust position when zoomed in to page let x = buttonRect.right + window.pageXOffset - 41; @@ -324,50 +335,50 @@ export default class Stickerpicker extends React.PureComponent { // Offset the chevron location, which is relative to the left of the context menu // (10 = offset when context menu would not be displayed off viewport) // (2 = context menu borders) - const stickerPickerChevronOffset = Math.max(10, 2 + window.pageXOffset + buttonRect.left - x); + const stickerpickerChevronOffset = Math.max(10, 2 + window.pageXOffset + buttonRect.left - x); const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19; this.setState({ showStickers: true, - stickerPickerX: x, - stickerPickerY: y, - stickerPickerChevronOffset, + stickerpickerX: x, + stickerpickerY: y, + stickerpickerChevronOffset, }); - } + }; /** * Trigger hiding of the sticker picker overlay * @param {Event} ev Event that triggered the function call */ - _onHideStickersClick(ev) { + private onHideStickersClick = (ev: React.MouseEvent): void => { if (this.state.showStickers) { this.setState({ showStickers: false }); } - } + }; /** * Called when the window is resized */ - _onResize() { + private onResize = (): void => { if (this.state.showStickers) { this.setState({ showStickers: false }); } - } + }; /** * The stickers picker was hidden */ - _onFinished() { + private onFinished = (): void => { if (this.state.showStickers) { this.setState({ showStickers: false }); } - } + }; /** * Launch the integration manager on the stickers integration page */ - _launchManageIntegrations = () => { + private launchManageIntegrations = (): void => { // TODO: Open the right integration manager for the widget if (SettingsStore.getValue("feature_many_integration_managers")) { IntegrationManagers.sharedInstance().openAll( @@ -384,7 +395,7 @@ export default class Stickerpicker extends React.PureComponent { } }; - render() { + public render(): JSX.Element { let stickerPicker; let stickersButton; const className = classNames( @@ -400,26 +411,25 @@ export default class Stickerpicker extends React.PureComponent { id='stickersButton' key="controls_hide_stickers" className={className} - onClick={this._onHideStickersClick} - active={this.state.showStickers.toString()} + onClick={this.onHideStickersClick} title={_t("Hide Stickers")} />; const GenericElementContextMenu = sdk.getComponent('context_menus.GenericElementContextMenu'); stickerPicker = - + ; } else { // Show show-stickers button @@ -428,7 +438,7 @@ export default class Stickerpicker extends React.PureComponent { id='stickersButton' key="controls_show_stickers" className="mx_MessageComposer_button mx_MessageComposer_stickers" - onClick={this._onShowStickersClick} + onClick={this.onShowStickersClick} title={_t("Show Stickers")} />; } From 333cb611861c5fec8f0926432e68c5e4c2f102c7 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 25 Aug 2021 14:38:47 +0100 Subject: [PATCH 03/12] Remove sdk.getComponent --- src/components/views/rooms/Stickerpicker.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/views/rooms/Stickerpicker.tsx b/src/components/views/rooms/Stickerpicker.tsx index d3d1538bd6f..33367c11515 100644 --- a/src/components/views/rooms/Stickerpicker.tsx +++ b/src/components/views/rooms/Stickerpicker.tsx @@ -19,7 +19,6 @@ import { Room } from 'matrix-js-sdk/src/models/room'; import { _t, _td } from '../../../languageHandler'; import AppTile from '../elements/AppTile'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import AccessibleButton from '../elements/AccessibleButton'; import WidgetUtils, { IWidgetEvent } from '../../../utils/WidgetUtils'; @@ -34,6 +33,7 @@ import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingSto import { replaceableComponent } from "../../../utils/replaceableComponent"; import { ActionPayload } from '../../../dispatcher/payloads'; import ScalarAuthClient from '../../../ScalarAuthClient'; +import GenericElementContextMenu from "../context_menus/GenericElementContextMenu"; // This should be below the dialog level (4000), but above the rest of the UI (1000-2000). // We sit in a context menu, so this should be given to the context menu. @@ -250,7 +250,6 @@ export default class Stickerpicker extends React.PureComponent { // Use a separate ReactDOM tree to render the AppTile separately so that it persists and does // not unmount when we (a) close the sticker picker (b) switch rooms. It's properties are still // updated. - const PersistedElement = sdk.getComponent("elements.PersistedElement"); // Load stickerpack content if (stickerpickerWidget && stickerpickerWidget.content && stickerpickerWidget.content.url) { @@ -415,7 +414,6 @@ export default class Stickerpicker extends React.PureComponent { title={_t("Hide Stickers")} />; - const GenericElementContextMenu = sdk.getComponent('context_menus.GenericElementContextMenu'); stickerPicker = Date: Wed, 25 Aug 2021 15:36:50 +0100 Subject: [PATCH 04/12] Fix Sticker and Emoji picker opening on narrow mode --- res/css/views/rooms/_MessageComposer.scss | 9 + .../views/rooms/MessageComposer.tsx | 181 ++++++++++++++---- src/components/views/rooms/Stickerpicker.tsx | 96 +++------- src/i18n/strings/en_EN.json | 1 + 4 files changed, 180 insertions(+), 107 deletions(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 5e2eff4047c..a544bca6a67 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/feather-customised/chevron-down.svg'); +} + +.mx_MessageComposer_closeButtonMenu::before { + transform: rotate(180deg); + transform-origin: center; +} + .mx_MessageComposer_sendMessage { cursor: pointer; position: relative; diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index c39d8bebc87..001966c346a 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,14 @@ 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, + alwaysAboveRightOf, +} from "../../structures/ContextMenu"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import ReplyPreview from "./ReplyPreview"; import { UIFeature } from "../../../settings/UIFeature"; @@ -45,6 +52,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 +81,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 = ; } @@ -193,6 +203,9 @@ interface IState { haveRecording: boolean; recordingTimeLeftSeconds?: number; me?: RoomMember; + narrowMode?: boolean; + isMenuOpen: boolean; + showStickers: boolean; } @replaceableComponent("views.rooms.MessageComposer") @@ -200,6 +213,7 @@ export default class MessageComposer extends React.Component { private dispatcherRef: string; private messageComposerInput: SendMessageComposer; private voiceRecordingButton: VoiceRecordComposerTile; + private ref: React.RefObject = createRef(); constructor(props) { super(props); @@ -211,6 +225,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, }; } @@ -218,8 +234,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 @@ -254,6 +283,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) => { @@ -360,6 +391,91 @@ 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(): JSX.Element | JSX.Element[] { + const buttons = []; + + let menuPosition; + if (this.ref.current) { + const contentRect = this.ref.current.getBoundingClientRect(); + menuPosition = alwaysAboveRightOf(contentRect); + } + + if (!this.state.haveRecording) { + buttons.push( + , + , + ); + } + if (this.shouldShowStickerPicker()) { + buttons.push( this.showStickers(!this.state.showStickers)} + title={this.state.showStickers ? _t("Hide Stickers") : _t("Show Stickers")} + />); + } + if (!this.state.haveRecording) { + buttons.push( + 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] } + + { this.state.isMenuOpen && ( + + { buttons.slice(1).map((button, index) => ( + setTimeout(this.toggleButtonMenu, 500)}> + { button } + + )) } + + ) } + ; + } + } + render() { const controls = [ this.state.me ? : null, @@ -368,8 +484,6 @@ export default class MessageComposer extends React.Component { null, ]; - const buttons = []; - if (!this.state.tombstone && this.state.canSendMessages) { controls.push( { />, ); - if (!this.state.haveRecording) { - buttons.push( - , - , - ); - } - - if (SettingsStore.getValue(UIFeature.Widgets) && - SettingsStore.getValue("MessageComposerInput.showStickersButton") && - !this.state.haveRecording) { - buttons.push(); - } - controls.push( this.voiceRecordingButton = c} room={this.props.room} />); - - if (!this.state.haveRecording) { - buttons.push( - this.voiceRecordingButton?.onRecordStartEndClick()} - title={_t("Send voice message")} - />, - ); - } - - if (!this.state.isComposerEmpty || this.state.haveRecording) { - buttons.push( - , - ); - } } else if (this.state.tombstone) { const replacementRoomId = this.state.tombstone.getContent()['replacement_room']; @@ -462,14 +543,30 @@ export default class MessageComposer extends React.Component { />; } + controls.push( + , + ); + + const showSendButton = !this.state.isComposerEmpty || this.state.haveRecording; + return ( -
+
{ recordingTooltip }
{ controls } - { buttons } + { this.renderButtons() } + { showSendButton && ( + + ) }
diff --git a/src/components/views/rooms/Stickerpicker.tsx b/src/components/views/rooms/Stickerpicker.tsx index 33367c11515..821fc350a69 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,11 @@ const PERSISTED_ELEMENT_KEY = "stickerPicker"; interface IProps { room: Room; + showStickers: boolean; + setShowStickers: (showStickers: boolean) => void; } interface IState { - showStickers: boolean; imError: string; stickerpickerX: number; stickerpickerY: number; @@ -72,7 +71,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 +112,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 +144,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 +192,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 +336,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 +349,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 +358,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 +367,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 +393,22 @@ 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 = - ; - - stickerPicker = - - ; - } else { - // Show show-stickers button - stickersButton = - ; - } - return - { stickersButton } - { stickerPicker } - ; + if (!this.props.showStickers) return null; + + return + + ; } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b9a6b5e04c4..3d5973889fa 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1559,6 +1559,7 @@ "Send an encrypted message…": "Send an encrypted message…", "Send a message…": "Send a message…", "Send voice message": "Send voice message", + "view more options": "view 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 9018973f15db077f8f7d669a66eaa648f6576261 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 25 Aug 2021 17:59:06 +0100 Subject: [PATCH 05/12] Position sticker picker correctly --- .../views/rooms/MessageComposer.tsx | 22 +++++++++---------- src/components/views/rooms/Stickerpicker.tsx | 2 ++ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 001966c346a..70b3d8904c5 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -33,7 +33,6 @@ import { ContextMenuTooltipButton, useContextMenu, MenuItem, - alwaysAboveRightOf, } from "../../structures/ContextMenu"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import ReplyPreview from "./ReplyPreview"; @@ -407,15 +406,9 @@ export default class MessageComposer extends React.Component { }); }; - private renderButtons(): JSX.Element | JSX.Element[] { + private renderButtons(menuPosition): JSX.Element | JSX.Element[] { const buttons = []; - let menuPosition; - if (this.ref.current) { - const contentRect = this.ref.current.getBoundingClientRect(); - menuPosition = alwaysAboveRightOf(contentRect); - } - if (!this.state.haveRecording) { buttons.push( , @@ -466,7 +459,7 @@ export default class MessageComposer extends React.Component { menuWidth={50} > { buttons.slice(1).map((button, index) => ( - setTimeout(this.toggleButtonMenu, 500)}> + { button } )) } @@ -484,6 +477,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( { , + setShowStickers={this.showStickers} + menuPosition={menuPosition} />, ); const showSendButton = !this.state.isComposerEmpty || this.state.haveRecording; @@ -559,7 +559,7 @@ export default class MessageComposer extends React.Component {
{ controls } - { this.renderButtons() } + { this.renderButtons(menuPosition) } { showSendButton && ( void; } @@ -407,6 +408,7 @@ export default class Stickerpicker extends React.PureComponent { menuPaddingLeft={0} menuPaddingRight={0} zIndex={STICKERPICKER_Z_INDEX} + {...this.props.menuPosition} > ; From 0208d80695f973038a0760046f1d64a6a11b8ffd Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 25 Aug 2021 18:11:34 +0100 Subject: [PATCH 06/12] Add missing 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 f6108467671..428024f7a89 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1557,6 +1557,8 @@ "Send a reply…": "Send a reply…", "Send an encrypted message…": "Send an encrypted message…", "Send a message…": "Send a message…", + "Hide Stickers": "Hide Stickers", + "Show Stickers": "Show Stickers", "Send voice message": "Send voice message", "view more options": "view more options", "The conversation continues here.": "The conversation continues here.", @@ -1719,8 +1721,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 4e0f52118db24f9a5136cb45961ccd5fd6d2a9dc Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 26 Aug 2021 08:07:51 +0100 Subject: [PATCH 07/12] Update VoiceRecordComposerTile to only end recording --- .../views/rooms/VoiceRecordComposerTile.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index c6f831de3bc..24e9e636cd5 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -217,7 +217,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent; if (this.state.recorder && !this.state.recorder?.isRecording) { - stopOrRecordBtn = null; + stopBtn = null; } } @@ -266,13 +266,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() } ); } From a1f6708a566bda86b3c0b3a7f7c3a95e6a80a6a8 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 27 Aug 2021 11:12:25 +0100 Subject: [PATCH 08/12] Hide voice recording button in narrow mode --- src/components/views/rooms/MessageComposer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 70b3d8904c5..5a1831068f7 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -424,7 +424,7 @@ export default class MessageComposer extends React.Component { title={this.state.showStickers ? _t("Hide Stickers") : _t("Show Stickers")} />); } - if (!this.state.haveRecording) { + if (!this.state.haveRecording && !this.state.narrowMode) { buttons.push( Date: Thu, 2 Sep 2021 10:06:09 +0100 Subject: [PATCH 09/12] Appease linter --- src/components/views/rooms/MessageComposer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 162a0cc86a8..77ce1c4047e 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -548,7 +548,7 @@ export default class MessageComposer extends React.Component { yOffset={-50} />; } - controls.push( + controls.push( Date: Fri, 3 Sep 2021 12:03:19 +0100 Subject: [PATCH 10/12] Implement new narrow mode UI --- res/css/views/rooms/_MessageComposer.scss | 9 +++- .../views/rooms/MessageComposer.tsx | 48 ++++++++++++------- src/i18n/strings/en_EN.json | 4 +- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 5b36d1e9a1f..5c8f6809de6 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -238,11 +238,11 @@ limitations under the License. } .mx_MessageComposer_buttonMenu::before { - mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + mask-image: url('$(res)/img/image-view/more.svg'); } .mx_MessageComposer_closeButtonMenu::before { - transform: rotate(180deg); + transform: rotate(90deg); transform-origin: center; } @@ -365,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 635d372d821..b7ef2a51a43 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -33,6 +33,7 @@ import { ContextMenuTooltipButton, useContextMenu, MenuItem, + ChevronFace, } from "../../structures/ContextMenu"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import ReplyPreview from "./ReplyPreview"; @@ -416,25 +417,32 @@ export default class MessageComposer extends React.Component { }; private renderButtons(menuPosition): JSX.Element | JSX.Element[] { - const buttons = []; - + const buttons = new Map(); if (!this.state.haveRecording) { - buttons.push( + buttons.set( + _t("Send File"), , + ); + buttons.set( + _t("Show Emojis"), , ); } if (this.shouldShowStickerPicker()) { - buttons.push( this.showStickers(!this.state.showStickers)} - title={this.state.showStickers ? _t("Hide Stickers") : _t("Show Stickers")} - />); + 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.push( + buttons.set( + _t("Send voice message"), this.voiceRecordingButton?.onRecordStartEndClick()} @@ -444,7 +452,7 @@ export default class MessageComposer extends React.Component { } if (!this.state.narrowMode) { - return buttons; + return Array.from(buttons.values()); } else { const classnames = classNames({ mx_MessageComposer_button: true, @@ -457,19 +465,23 @@ export default class MessageComposer extends React.Component { { this.state.isMenuOpen && ( - { buttons.slice(1).map((button, index) => ( - + { Array.from(buttons).slice(1).map(([label, button]) => ( + { button } + { label } )) } @@ -568,7 +580,7 @@ export default class MessageComposer extends React.Component { }); return ( -
+
{ recordingTooltip }
{ this.props.showReplyPreview && ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index bfa8e846a9e..28f05bbe8c9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1558,8 +1558,10 @@ "Send a reply…": "Send a reply…", "Send an encrypted message…": "Send an encrypted message…", "Send a message…": "Send a message…", - "Hide Stickers": "Hide Stickers", + "Send File": "Send File", + "Show Emojis": "Show Emojis", "Show Stickers": "Show Stickers", + "Hide Stickers": "Hide Stickers", "Send voice message": "Send voice message", "view more options": "view more options", "The conversation continues here.": "The conversation continues here.", From f549c1d52be4c1b319f0c92f52c55a698122f3f2 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 6 Sep 2021 08:14:49 +0100 Subject: [PATCH 11/12] Code style updates --- src/components/views/rooms/MessageComposer.tsx | 1 - .../views/rooms/VoiceRecordComposerTile.tsx | 11 ++--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index b7ef2a51a43..6b66ae4ba34 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -33,7 +33,6 @@ import { ContextMenuTooltipButton, useContextMenu, MenuItem, - ChevronFace, } from "../../structures/ContextMenu"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import ReplyPreview from "./ReplyPreview"; diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index e172fb7f0da..288d97fc501 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"; @@ -219,20 +218,14 @@ export default class VoiceRecordComposerTile extends React.PureComponent; From 085d8b46a7f2ad391c10a6b9d485aa92e3601aff Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 6 Sep 2021 08:31:54 +0100 Subject: [PATCH 12/12] fix i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 28f05bbe8c9..86d588dfc5e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1563,7 +1563,7 @@ "Show Stickers": "Show Stickers", "Hide Stickers": "Hide Stickers", "Send voice message": "Send voice message", - "view more options": "view more options", + "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",