Improve composer visiblity (#8578)

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Germain 2022-06-07 08:28:29 +01:00 committed by GitHub
parent 8c13a0f8d4
commit f14374a51c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 323 additions and 174 deletions

View file

@ -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<typeof AccessibleButton>) {
const classNames = classnames("mx_CancelButton", props.className ?? "");
const vars = {
"--size": `${props.size}px`,
} as React.CSSProperties;
return <AccessibleButton
{...props}
className={classNames}
style={vars}
>
<CancelIcon />
</AccessibleButton>;
}
CancelButton.defaultProps = {
size: "16",
};

View file

@ -28,9 +28,14 @@ interface IProps {
onClick?(): void;
colored?: boolean;
emphasizeDisplayName?: boolean;
as?: string;
}
export default class DisambiguatedProfile extends React.Component<IProps> {
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<IProps> {
[colorClass]: true,
});
return (
<div className="mx_DisambiguatedProfile" onClick={onClick}>
<span className={displayNameClasses} dir="auto">
{ rawDisplayName }
</span>
{ mxidElement }
</div>
);
return React.createElement(this.props.as, {
className: "mx_DisambiguatedProfile",
onClick,
}, <>
<span className={displayNameClasses} dir="auto">
{ rawDisplayName }
</span>
{ mxidElement }
</>);
}
}

View file

@ -27,12 +27,17 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
interface IProps {
mxEvent: MatrixEvent;
onClick?(): void;
as?: string;
}
export default class SenderProfile extends React.PureComponent<IProps> {
public static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
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<IProps> {
member={member}
colored={true}
emphasizeDisplayName={true}
as={this.props.as}
/>
);
} }

View file

@ -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<IProps, IState> {
} 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<IProps, IState> {
};
public render() {
const controls = [
this.props.e2eStatus ?
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" /> :
null,
];
const controls = [];
let menuPosition: AboveLeftOf | undefined;
if (this.ref.current) {
@ -363,6 +363,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
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<IProps, IState> {
toggleStickerPickerOpen={this.toggleStickerPickerOpen}
/>,
);
controls.push(<VoiceRecordComposerTile
key="controls_voice_record"
ref={this.voiceRecordingButton}
room={this.props.room} />);
} else if (this.context.tombstone) {
} else if (roomReplaced) {
const replacementRoomId = this.context.tombstone.getContent()['replacement_room'];
const continuesLink = replacementRoomId ? (
<a href={makeRoomPermalink(replacementRoomId)}
className="mx_MessageComposer_roomReplaced_link"
onClick={this.onTombstoneClick}
>
{ _t("The conversation continues here.") }
</a>
) : '';
controls.push(<div className="mx_MessageComposer_replaced_wrapper" key="room_replaced">
<div className="mx_MessageComposer_replaced_valign">
<img className="mx_MessageComposer_roomReplaced_icon"
src={require("../../../../res/img/room_replaced.svg").default}
/>
<span className="mx_MessageComposer_roomReplaced_header">
{ _t("This room has been replaced and is no longer active.") }
</span><br />
{ continuesLink }
</div>
</div>);
controls.push(<p key="room_replaced">
<InfoIcon width={24} />
&nbsp;
{ _t("This room has been replaced and is no longer active.") }
&nbsp;
{ replacementRoomId && (
<a href={makeRoomPermalink(replacementRoomId)}
className="mx_MessageComposer_roomReplaced_link"
onClick={this.onTombstoneClick}
>
{ _t("The conversation continues here.") }
</a>
) }
</p>);
} else {
controls.push(
<div key="controls_error" className="mx_MessageComposer_noperm_error">
@ -441,6 +432,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {
const showSendButton = !this.state.isComposerEmpty || this.state.haveRecording;
if (this.props.e2eStatus) {
controls.push(
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" />,
);
}
const classes = classNames({
"mx_MessageComposer": true,
"mx_GroupLayout": true,
@ -455,8 +452,17 @@ export default class MessageComposer extends React.Component<IProps, IState> {
<ReplyPreview
replyToEvent={this.props.replyToEvent}
permalinkCreator={this.props.permalinkCreator} />
<div className="mx_MessageComposer_row">
<div
className="mx_MessageComposer_row"
aria-disabled={!canSendMessages && !roomReplaced}
data-notice={roomReplaced}>
{ controls }
</div>
<div className="mx_MessageComposer_controls">
{ canSendMessages && <VoiceRecordComposerTile
key="controls_voice_record"
ref={this.voiceRecordingButton}
room={this.props.room} /> }
{ canSendMessages && <MessageComposerButtons
addEmoji={this.addEmoji}
haveRecording={this.state.haveRecording}
@ -476,13 +482,20 @@ export default class MessageComposer extends React.Component<IProps, IState> {
showStickersButton={this.state.showStickersButton}
toggleButtonMenu={this.toggleButtonMenu}
/> }
{ showSendButton && (
<SendButton
key="controls_send"
onClick={this.sendMessage}
title={this.state.haveRecording ? _t("Send voice message") : undefined}
/>
) }
<CSSTransition
in={showSendButton}
classNames="mx_MessageComposer_sendMessageWrapper"
addEndListener={() => {}}
>
<div className='mx_MessageComposer_sendMessageWrapper'>
<SendButton
key="controls_send"
onClick={this.sendMessage}
title={this.state.haveRecording ? _t("Send voice message") : undefined}
aria-hidden={!showSendButton}
/>
</div>
</CSSTransition>
</div>
</div>
</div>

View file

@ -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<IProps> {
if (!this.props.replyToEvent) return null;
return <div className="mx_ReplyPreview">
<div className="mx_ReplyPreview_section">
<div className="mx_ReplyPreview_header">
<span>{ _t('Replying') }</span>
<AccessibleButton
className="mx_ReplyPreview_header_cancel"
onClick={() => cancelQuoting(this.context.timelineRenderingType)}
/>
</div>
<ReplyTile
mxEvent={this.props.replyToEvent}
permalinkCreator={this.props.permalinkCreator}
/>
<div className="mx_ReplyPreview_header">
<ReplyIcon />
{ _t('Reply to <User />', {}, {
'User': () => <SenderProfile mxEvent={this.props.replyToEvent} as="span" />,
}) } &nbsp;
<CancelButton onClick={() => cancelQuoting(this.context.timelineRenderingType)} />
</div>
<ReplyTile
mxEvent={this.props.replyToEvent}
permalinkCreator={this.props.permalinkCreator}
showSenderProfile={false}
/>
</div>;
}
}

View file

@ -44,6 +44,7 @@ interface IProps {
getRelationsForEvent?: (
(eventId: string, relationType: string, eventType: string) => Relations
);
showSenderProfile?: boolean;
}
export default class ReplyTile extends React.PureComponent<IProps> {
@ -51,6 +52,7 @@ export default class ReplyTile extends React.PureComponent<IProps> {
static defaultProps = {
onHeightChanged: () => {},
showSenderProfile: true,
};
componentDidMount() {
@ -136,7 +138,8 @@ export default class ReplyTile extends React.PureComponent<IProps> {
let sender;
const needsSenderProfile = (
!isInfoMessage
this.props.showSenderProfile
&& !isInfoMessage
&& msgType !== MsgType.Image
&& evType !== EventType.Sticker
&& evType !== EventType.RoomCreate

View file

@ -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 <User />": "Reply to <User />",
"Room %(name)s": "Room %(name)s",
"Recently visited rooms": "Recently visited rooms",
"No recently visited rooms": "No recently visited rooms",