diff --git a/res/css/_components.scss b/res/css/_components.scss index 7e69d2b17fd..f88cc728c86 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -83,6 +83,7 @@ @import "./views/avatars/_DecoratedRoomAvatar.scss"; @import "./views/avatars/_WidgetAvatar.scss"; @import "./views/beta/_BetaCard.scss"; +@import "./views/buttons/_Cancel.scss"; @import "./views/context_menus/_CallContextMenu.scss"; @import "./views/context_menus/_DeviceContextMenu.scss"; @import "./views/context_menus/_IconizedContextMenu.scss"; diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index eba8ae8f6e8..a3172e39d72 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -32,6 +32,12 @@ limitations under the License. position: relative; } +.mx_MainSplit_timeline { + .mx_MessageComposer_wrapper { + margin: $spacing-8 $spacing-16; + } +} + .mx_RoomView_auxPanel { min-width: 0px; width: 100%; @@ -155,7 +161,7 @@ limitations under the License. .mx_RoomView_messageListWrapper { justify-content: flex-start; - >.mx_RoomView_MessageList > li > ol { + > .mx_RoomView_MessageList > li > ol { list-style-type: none; } } diff --git a/res/css/views/buttons/_Cancel.scss b/res/css/views/buttons/_Cancel.scss new file mode 100644 index 00000000000..50e2cd0931a --- /dev/null +++ b/res/css/views/buttons/_Cancel.scss @@ -0,0 +1,32 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + +.mx_CancelButton { + width: var(--size); + height: var(--size); + + box-sizing: border-box; + padding: calc(var(--size) / 4); + border-radius: 50%; + + line-height: 0; + background: $quinary-content; + + svg { + width: calc(var(--size) / 2); + color: $secondary-content; + } +} diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 9619bad3fe8..6de32107478 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -20,31 +20,48 @@ limitations under the License. margin: auto; border-top: 1px solid $primary-hairline-color; position: relative; - padding-left: 42px; - padding-right: 16px; -} -.mx_MessageComposer_replaced_wrapper { - margin-left: auto; - margin-right: auto; -} + display: grid; + grid-template: + "reply reply" auto + "composer controls" auto + / 1fr auto; -.mx_MessageComposer_replaced_valign { - height: 60px; - display: table-cell; - vertical-align: middle; -} + .mx_ReplyPreview { + grid-area: reply; + } -.mx_MessageComposer_roomReplaced_icon { - float: left; - margin-right: 20px; - margin-top: 5px; - width: 31px; - height: 31px; -} + .mx_MessageComposer_row { + grid-area: composer; + } + + .mx_MessageComposer_controls { + grid-area: controls; + display: flex; + align-items: flex-end; + margin-bottom: 2px; + } + + >[role=button] { + margin-bottom: 7px; + } + + .mx_VoiceRecordComposerTile_delete { + margin-bottom: 9px; + } + + .mx_VoiceRecordComposerTile_stop, + .mx_MessageComposer_sendMessage { + margin-bottom: $spacing-4; + } -.mx_MessageComposer_roomReplaced_header { - font-weight: bold; + .mx_VoiceMessagePrimaryContainer { + margin-right: $spacing-8; + } + + a { + color: $accent; + } } .mx_MessageComposer_autocomplete_wrapper { @@ -56,7 +73,36 @@ limitations under the License. display: flex; flex-direction: row; align-items: center; - width: 100%; + border: 1px solid $quaternary-content; + border-radius: 12px; + padding: $spacing-12 $spacing-8; + margin-right: $spacing-16; + + transition: border-color var(--transition-short); + + &[data-notice=true] { + border-color: transparent; + color: $secondary-content; + + p { + margin: 0; + line-height: 0; + } + + svg { + vertical-align: middle; + position: relative; + top: -2px; + } + } + + &:focus-within { + border-color: $tertiary-content; + } + + &[aria-disabled=true] { + cursor: not-allowed; + } } .mx_MessageComposer .mx_MessageComposer_avatar { @@ -73,22 +119,16 @@ limitations under the License. } .mx_MessageComposer_e2eIcon.mx_E2EIcon { - position: absolute; - left: 20px; - margin-right: 0; // Counteract the E2EIcon class - margin-left: 3px; // Counteract the E2EIcon class + margin: 0 0 2px; width: 12px; height: 12px; + align-self: end; } .mx_MessageComposer_noperm_error { - width: 100%; - height: 60px; font-style: italic; - color: $info-plinth-fg-color; - display: flex; - align-items: center; - justify-content: center; + color: $tertiary-content; + font-size: $font-12px; } .mx_MessageComposer_input_wrapper { @@ -124,13 +164,19 @@ limitations under the License. .mx_MessageComposer_editor > :first-child { margin-top: 0 !important; } + .mx_MessageComposer_editor > :last-child { margin-bottom: 0 !important; } @keyframes visualbell { - from { background-color: $visual-bell-bg-color; } - to { background-color: $background; } + from { + background-color: $visual-bell-bg-color; + } + + to { + background-color: $background; + } } .mx_MessageComposer_input_error { @@ -166,12 +212,14 @@ limitations under the License. color: $accent; opacity: 1.0; } + .mx_MessageComposer_input textarea::-webkit-input-placeholder { color: $accent; } .mx_MessageComposer_button_highlight { background: rgba($accent, 0.25); + // make the icon the accent color too &::before { background-color: $accent !important; @@ -188,6 +236,7 @@ limitations under the License. padding-left: var(--size); border-radius: 50%; margin-right: 6px; + margin-bottom: 7px; &:last-child { margin-right: auto; @@ -261,11 +310,30 @@ limitations under the License. mask-image: url('$(res)/img/image-view/more.svg'); } +.mx_MessageComposer_sendMessageWrapper { + --sendMessageSize: 32px; + transition: all var(--transition-short); +} + +.mx_MessageComposer_sendMessageWrapper, +.mx_MessageComposer_sendMessageWrapper-enter, +.mx_MessageComposer_sendMessageWrapper-exit { + width: 0; + transform: scale(.6); + opacity: 0; +} + +.mx_MessageComposer_sendMessageWrapper-enter-active { + width: var(--sendMessageSize); + transform: scale(1); + opacity: 1; +} + .mx_MessageComposer_sendMessage { cursor: pointer; position: relative; - width: 32px; - height: 32px; + width: var(--sendMessageSize); + height: var(--sendMessageSize); border-radius: 100%; background-color: $accent; @@ -358,10 +426,6 @@ limitations under the License. .mx_MessageComposer_input { min-height: 50px; } - - .mx_MessageComposer_noperm_error { - height: 50px; - } } /** @@ -371,21 +435,7 @@ limitations under the License. .mx_MessageComposer.mx_MessageComposer--compact { margin-right: 0; - .mx_MessageComposer_wrapper { - padding: 0 0 0 25px; - } - - &:not(.mx_MessageComposer_e2eStatus) { - .mx_MessageComposer_wrapper { - padding: 0; - } - } - .mx_MessageComposer_button:last-child { margin-right: 0; } - - .mx_MessageComposer_e2eIcon { - left: 0; - } } diff --git a/res/css/views/rooms/_ReplyPreview.scss b/res/css/views/rooms/_ReplyPreview.scss index 50abcc738ba..e303a22c028 100644 --- a/res/css/views/rooms/_ReplyPreview.scss +++ b/res/css/views/rooms/_ReplyPreview.scss @@ -15,48 +15,44 @@ limitations under the License. */ .mx_ReplyPreview { - border: 1px solid $primary-hairline-color; + border: 1px solid $system; + + margin-left: calc(-1 * $spacing-16); + margin-right: calc(-1 * $spacing-16); + padding: $spacing-8 $spacing-16 0 $spacing-16; + + border-top-left-radius: 16px; + border-top-right-radius: 16px; + border-bottom: none; background: $background; max-height: 50vh; overflow: auto; +} - .mx_ReplyPreview_section { - border-bottom: 1px solid $primary-hairline-color; - display: flex; - flex-flow: column; - row-gap: $spacing-8; - padding: $spacing-8 $spacing-8 0 $spacing-8; - - .mx_ReplyPreview_header { - display: flex; - justify-content: space-between; - column-gap: 8px; - - color: $primary-content; - font-weight: 400; - opacity: 0.4; - - .mx_ReplyPreview_header_cancel { - background-color: $primary-content; - mask: url('$(res)/img/cancel.svg'); - mask-repeat: no-repeat; - mask-position: center; - mask-size: 18px; - width: 18px; - height: 18px; - min-width: 18px; - min-height: 18px; - } - } +.mx_ReplyPreview_header { + display: flex; + font-size: $font-12px; + color: $secondary-content; + position: relative; + + > svg { + width: 1em; + vertical-align: middle; + margin-right: $spacing-8; } -} -.mx_RoomView_body { - .mx_ReplyPreview { - // Add box-shadow to the reply preview on the main (left) panel only. - // It is not added to the preview on the (right) panel for threads and a chat with a maximized widget. - box-shadow: 0px -16px 32px $composer-shadow-color; - border-radius: 8px 8px 0 0; + .mx_CancelButton { + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); } } + +.mx_ReplyPreview_header_cancel { + position: absolute; + right: 0; + color: $primary-content; + width: 18px; +} diff --git a/res/css/views/rooms/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss index 3e2cf68f1db..6805f158ed7 100644 --- a/res/css/views/rooms/_SendMessageComposer.scss +++ b/res/css/views/rooms/_SendMessageComposer.scss @@ -30,14 +30,8 @@ limitations under the License. flex: 1; display: flex; flex-direction: column; - // min-height at this level so the mx_BasicMessageComposer_input - // still stays vertically centered when less than 55px. - // We also set this to ensure the voice message recording widget - // doesn't cause a jump. - min-height: 55px; .mx_BasicMessageComposer_input { - padding: 3px 0; // this will center the contenteditable // in it's parent vertically // while keeping the autocomplete at the top diff --git a/res/img/cancel.svg b/res/img/cancel.svg index e32060025ea..82e38925615 100644 --- a/res/img/cancel.svg +++ b/res/img/cancel.svg @@ -5,6 +5,6 @@ Created with Sketch. - + - \ No newline at end of file + diff --git a/res/img/element-icons/room/message-bar/reply.svg b/res/img/element-icons/room/message-bar/reply.svg index 9900d4d19d4..c32848a0b00 100644 --- a/res/img/element-icons/room/message-bar/reply.svg +++ b/res/img/element-icons/room/message-bar/reply.svg @@ -1,4 +1,4 @@ - - + + diff --git a/res/img/element-icons/room/room-summary.svg b/res/img/element-icons/room/room-summary.svg index b6ac258b189..1f43428ffc8 100644 --- a/res/img/element-icons/room/room-summary.svg +++ b/res/img/element-icons/room/room-summary.svg @@ -1,3 +1,3 @@ - + diff --git a/res/img/element-icons/x-8px.svg b/res/img/element-icons/x-8px.svg index c9730ed6192..8a706c3446f 100644 --- a/res/img/element-icons/x-8px.svg +++ b/res/img/element-icons/x-8px.svg @@ -1,4 +1,4 @@ - - + + diff --git a/src/components/views/buttons/Cancel.tsx b/src/components/views/buttons/Cancel.tsx new file mode 100644 index 00000000000..f0fbee56dbd --- /dev/null +++ b/src/components/views/buttons/Cancel.tsx @@ -0,0 +1,40 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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, { ComponentProps } from "react"; +import classnames from "classnames"; + +import AccessibleButton from "../elements/AccessibleButton"; +import { Icon as CancelIcon } from "../../../../res/img/cancel.svg"; + +export default function CancelButton(props: ComponentProps) { + const classNames = classnames("mx_CancelButton", props.className ?? ""); + const vars = { + "--size": `${props.size}px`, + } as React.CSSProperties; + + return + + ; +} + +CancelButton.defaultProps = { + size: "16", +}; diff --git a/src/components/views/messages/DisambiguatedProfile.tsx b/src/components/views/messages/DisambiguatedProfile.tsx index 36850e916ea..d634277bc61 100644 --- a/src/components/views/messages/DisambiguatedProfile.tsx +++ b/src/components/views/messages/DisambiguatedProfile.tsx @@ -28,9 +28,14 @@ interface IProps { onClick?(): void; colored?: boolean; emphasizeDisplayName?: boolean; + as?: string; } export default class DisambiguatedProfile extends React.Component { + public static defaultProps = { + as: "div", + }; + render() { const { fallbackName, member, colored, emphasizeDisplayName, onClick } = this.props; const rawDisplayName = member?.rawDisplayName || fallbackName; @@ -57,13 +62,14 @@ export default class DisambiguatedProfile extends React.Component { [colorClass]: true, }); - return ( -
- - { rawDisplayName } - - { mxidElement } -
- ); + return React.createElement(this.props.as, { + className: "mx_DisambiguatedProfile", + onClick, + }, <> + + { rawDisplayName } + + { mxidElement } + ); } } diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx index da7d1206d11..a9c77f9ebee 100644 --- a/src/components/views/messages/SenderProfile.tsx +++ b/src/components/views/messages/SenderProfile.tsx @@ -27,12 +27,17 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg"; interface IProps { mxEvent: MatrixEvent; onClick?(): void; + as?: string; } export default class SenderProfile extends React.PureComponent { public static contextType = MatrixClientContext; public context!: React.ContextType; + public static defaultProps = { + as: "div", + }; + render() { const { mxEvent, onClick } = this.props; const msgtype = mxEvent.getContent().msgtype; @@ -60,6 +65,7 @@ export default class SenderProfile extends React.PureComponent { member={member} colored={true} emphasizeDisplayName={true} + as={this.props.as} /> ); } } diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 07912586378..adba298d73b 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -22,6 +22,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { EventType } from 'matrix-js-sdk/src/@types/event'; import { Optional } from "matrix-events-sdk"; import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread'; +import { CSSTransition } from 'react-transition-group'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; @@ -51,12 +52,14 @@ import { SettingUpdatedPayload } from "../../../dispatcher/payloads/SettingUpdat import MessageComposerButtons from './MessageComposerButtons'; import { ButtonEvent } from '../elements/AccessibleButton'; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; +import { Icon as InfoIcon } from "../../../../res/img/element-icons/room/room-summary.svg"; let instanceCount = 0; interface ISendButtonProps { onClick: (ev: ButtonEvent) => void; title?: string; // defaults to something generic + "aria-hidden"?: boolean; } function SendButton(props: ISendButtonProps) { @@ -65,6 +68,7 @@ function SendButton(props: ISendButtonProps) { className="mx_MessageComposer_sendMessage" onClick={props.onClick} title={props.title ?? _t('Send message')} + aria-hidden={props['aria-hidden'] ?? false} /> ); } @@ -263,15 +267,15 @@ export default class MessageComposer extends React.Component { } else if (replyingToThread) { return _t('Reply to thread…'); } else if (this.props.e2eStatus) { - return _t('Send an encrypted reply…'); + return _t('Send encrypted reply…'); } else { - return _t('Send a reply…'); + return _t('Send reply…'); } } else { if (this.props.e2eStatus) { - return _t('Send an encrypted message…'); + return _t('Send encrypted message…'); } else { - return _t('Send a message…'); + return _t('Send message…'); } } }; @@ -351,11 +355,7 @@ export default class MessageComposer extends React.Component { }; public render() { - const controls = [ - this.props.e2eStatus ? - : - null, - ]; + const controls = []; let menuPosition: AboveLeftOf | undefined; if (this.ref.current) { @@ -363,6 +363,8 @@ export default class MessageComposer extends React.Component { menuPosition = aboveLeftOf(contentRect); } + const roomReplaced = !!this.context.tombstone; + const canSendMessages = this.context.canSendMessages && !this.context.tombstone; if (canSendMessages) { controls.push( @@ -379,34 +381,23 @@ export default class MessageComposer extends React.Component { toggleStickerPickerOpen={this.toggleStickerPickerOpen} />, ); - - controls.push(); - } else if (this.context.tombstone) { + } else if (roomReplaced) { const replacementRoomId = this.context.tombstone.getContent()['replacement_room']; - const continuesLink = replacementRoomId ? ( - - { _t("The conversation continues here.") } - - ) : ''; - - controls.push(
-
- - - { _t("This room has been replaced and is no longer active.") } -
- { continuesLink } -
-
); + controls.push(

+ +   + { _t("This room has been replaced and is no longer active.") } +   + { replacementRoomId && ( + + { _t("The conversation continues here.") } + + ) } +

); } else { controls.push(
@@ -441,6 +432,12 @@ export default class MessageComposer extends React.Component { const showSendButton = !this.state.isComposerEmpty || this.state.haveRecording; + if (this.props.e2eStatus) { + controls.push( + , + ); + } + const classes = classNames({ "mx_MessageComposer": true, "mx_GroupLayout": true, @@ -455,8 +452,17 @@ export default class MessageComposer extends React.Component { -
+
{ controls } +
+
+ { canSendMessages && } { canSendMessages && { showStickersButton={this.state.showStickersButton} toggleButtonMenu={this.toggleButtonMenu} /> } - { showSendButton && ( - - ) } + {}} + > +
+ +
+
diff --git a/src/components/views/rooms/ReplyPreview.tsx b/src/components/views/rooms/ReplyPreview.tsx index 611c58f8529..345ce83c211 100644 --- a/src/components/views/rooms/ReplyPreview.tsx +++ b/src/components/views/rooms/ReplyPreview.tsx @@ -22,7 +22,9 @@ import { _t } from '../../../languageHandler'; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import ReplyTile from './ReplyTile'; import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext'; -import AccessibleButton from "../elements/AccessibleButton"; +import SenderProfile from '../messages/SenderProfile'; +import { Icon as ReplyIcon } from "../../../../res/img/element-icons/room/message-bar/reply.svg"; +import CancelButton from '../buttons/Cancel'; function cancelQuoting(context: TimelineRenderingType) { dis.dispatch({ @@ -44,19 +46,19 @@ export default class ReplyPreview extends React.Component { if (!this.props.replyToEvent) return null; return
-
-
- { _t('Replying') } - cancelQuoting(this.context.timelineRenderingType)} - /> -
- +
+ + { _t('Reply to ', {}, { + 'User': () => , + }) }   + + cancelQuoting(this.context.timelineRenderingType)} />
+
; } } diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 2b973abfca5..bc26c02c238 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -44,6 +44,7 @@ interface IProps { getRelationsForEvent?: ( (eventId: string, relationType: string, eventType: string) => Relations ); + showSenderProfile?: boolean; } export default class ReplyTile extends React.PureComponent { @@ -51,6 +52,7 @@ export default class ReplyTile extends React.PureComponent { static defaultProps = { onHeightChanged: () => {}, + showSenderProfile: true, }; componentDidMount() { @@ -136,7 +138,8 @@ export default class ReplyTile extends React.PureComponent { let sender; const needsSenderProfile = ( - !isInfoMessage + this.props.showSenderProfile + && !isInfoMessage && msgType !== MsgType.Image && evType !== EventType.Sticker && evType !== EventType.RoomCreate diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3aa40c62e6e..5d3417e68c1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1712,12 +1712,12 @@ "Send message": "Send message", "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…", - "The conversation continues here.": "The conversation continues here.", + "Send encrypted reply…": "Send encrypted reply…", + "Send reply…": "Send reply…", + "Send encrypted message…": "Send encrypted message…", + "Send message…": "Send message…", "This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.", + "The conversation continues here.": "The conversation continues here.", "You do not have permission to post to this room": "You do not have permission to post to this room", "%(seconds)ss left": "%(seconds)ss left", "Send voice message": "Send voice message", @@ -1770,7 +1770,7 @@ "Seen by %(count)s people|one": "Seen by %(count)s person", "Read receipts": "Read receipts", "Recently viewed": "Recently viewed", - "Replying": "Replying", + "Reply to ": "Reply to ", "Room %(name)s": "Room %(name)s", "Recently visited rooms": "Recently visited rooms", "No recently visited rooms": "No recently visited rooms", diff --git a/test/components/views/rooms/MessageComposer-test.tsx b/test/components/views/rooms/MessageComposer-test.tsx index e756a7653c4..d774aa80604 100644 --- a/test/components/views/rooms/MessageComposer-test.tsx +++ b/test/components/views/rooms/MessageComposer-test.tsx @@ -61,7 +61,7 @@ describe("MessageComposer", () => { expect(wrapper.find("SendMessageComposer")).toHaveLength(0); expect(wrapper.find("MessageComposerButtons")).toHaveLength(0); - expect(wrapper.find(".mx_MessageComposer_roomReplaced_header")).toHaveLength(1); + expect(wrapper.find("p").text()).toContain("room has been replaced"); }); });