Merge pull request #6753 from matrix-org/revert-6752-revert-6682-gsouquet/compact-composer-18533

This commit is contained in:
Germain 2021-09-09 18:49:38 +01:00 committed by GitHub
commit d475b7f1ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 273 additions and 127 deletions

View file

@ -733,4 +733,8 @@ $hover-select-border: 4px;
padding-bottom: 5px; padding-bottom: 5px;
margin-bottom: 5px; margin-bottom: 5px;
} }
.mx_MessageComposer_sendMessage {
margin-right: 0;
}
} }

View file

@ -186,11 +186,14 @@ limitations under the License.
} }
.mx_MessageComposer_button { .mx_MessageComposer_button {
--size: 26px;
position: relative; position: relative;
margin-right: 6px; margin-right: 6px;
cursor: pointer; cursor: pointer;
height: 26px; height: var(--size);
width: 26px; line-height: var(--size);
width: auto;
padding-left: calc(var(--size) + 5px);
border-radius: 100%; border-radius: 100%;
&::before { &::before {
@ -207,8 +210,22 @@ limitations under the License.
mask-position: center; mask-position: center;
} }
&:hover { &::after {
content: '';
position: absolute;
left: 0;
top: 0;
z-index: 0;
width: var(--size);
height: var(--size);
border-radius: 50%;
}
&:hover,
&.mx_MessageComposer_closeButtonMenu {
&::after {
background: rgba($accent-color, 0.1); background: rgba($accent-color, 0.1);
}
&::before { &::before {
background-color: $accent-color; background-color: $accent-color;
@ -237,10 +254,18 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/room/composer/sticker.svg'); 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 { .mx_MessageComposer_sendMessage {
cursor: pointer; cursor: pointer;
position: relative; position: relative;
margin-right: 6px;
width: 32px; width: 32px;
height: 32px; height: 32px;
border-radius: 100%; border-radius: 100%;
@ -349,10 +374,19 @@ limitations under the License.
margin-right: 0; margin-right: 0;
.mx_MessageComposer_wrapper { .mx_MessageComposer_wrapper {
padding: 0; padding: 0 0 0 25px;
} }
.mx_MessageComposer_button:last-child { .mx_MessageComposer_button:last-child {
margin-right: 0; margin-right: 0;
} }
.mx_MessageComposer_e2eIcon {
left: 0;
}
}
.mx_MessageComposer_Menu .mx_CallContextMenu_item {
display: flex;
align-items: center;
} }

View file

@ -53,6 +53,7 @@ import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/SpaceStore";
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
import { E2EStatus } from '../../utils/ShieldUtils';
interface IProps { interface IProps {
room?: Room; // if showing panels for a given room, this is set 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 user?: User; // used if we know the user ahead of opening the panel
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
permalinkCreator?: RoomPermalinkCreator; permalinkCreator?: RoomPermalinkCreator;
e2eStatus?: E2EStatus;
} }
interface IState { interface IState {
@ -319,7 +321,8 @@ export default class RightPanel extends React.Component<IProps, IState> {
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
onClose={this.onClose} onClose={this.onClose}
mxEvent={this.state.event} mxEvent={this.state.event}
permalinkCreator={this.props.permalinkCreator} />; permalinkCreator={this.props.permalinkCreator}
e2eStatus={this.props.e2eStatus} />;
break; break;
case RightPanelPhases.ThreadPanel: case RightPanelPhases.ThreadPanel:

View file

@ -2063,7 +2063,8 @@ export default class RoomView extends React.Component<IProps, IState> {
? <RightPanel ? <RightPanel
room={this.state.room} room={this.state.room}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)} /> permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)}
e2eStatus={this.state.e2eStatus} />
: null; : null;
const timelineClasses = classNames("mx_RoomView_timeline", { const timelineClasses = classNames("mx_RoomView_timeline", {

View file

@ -33,6 +33,7 @@ import { ActionPayload } from '../../dispatcher/payloads';
import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload'; import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload';
import { Action } from '../../dispatcher/actions'; import { Action } from '../../dispatcher/actions';
import { MatrixClientPeg } from '../../MatrixClientPeg'; import { MatrixClientPeg } from '../../MatrixClientPeg';
import { E2EStatus } from '../../utils/ShieldUtils';
interface IProps { interface IProps {
room: Room; room: Room;
@ -40,6 +41,7 @@ interface IProps {
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
mxEvent: MatrixEvent; mxEvent: MatrixEvent;
permalinkCreator?: RoomPermalinkCreator; permalinkCreator?: RoomPermalinkCreator;
e2eStatus?: E2EStatus;
} }
interface IState { interface IState {
@ -144,6 +146,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
replyToEvent={this.state?.thread?.replyToEvent} replyToEvent={this.state?.thread?.replyToEvent}
showReplyPreview={false} showReplyPreview={false}
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
e2eStatus={this.props.e2eStatus}
compact={true} compact={true}
/> />
</BaseCard> </BaseCard>

View file

@ -25,6 +25,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> { interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
title: string; title: string;
tooltip?: React.ReactNode; tooltip?: React.ReactNode;
label?: React.ReactNode;
tooltipClassName?: string; tooltipClassName?: string;
forceHide?: boolean; forceHide?: boolean;
yOffset?: number; yOffset?: number;
@ -84,7 +85,8 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
aria-label={title} aria-label={title}
> >
{ children } { children }
{ tip } { this.props.label }
{ (tooltip || title) && tip }
</AccessibleButton> </AccessibleButton>
); );
} }

View file

@ -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 See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, { createRef } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
@ -27,7 +27,12 @@ import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalin
import ContentMessages from '../../../ContentMessages'; import ContentMessages from '../../../ContentMessages';
import E2EIcon from './E2EIcon'; import E2EIcon from './E2EIcon';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; import {
aboveLeftOf,
ContextMenu,
useContextMenu,
MenuItem,
} from "../../structures/ContextMenu";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import ReplyPreview from "./ReplyPreview"; import ReplyPreview from "./ReplyPreview";
import { UIFeature } from "../../../settings/UIFeature"; import { UIFeature } from "../../../settings/UIFeature";
@ -45,6 +50,10 @@ import { Action } from "../../../dispatcher/actions";
import EditorModel from "../../../editor/model"; import EditorModel from "../../../editor/model";
import EmojiPicker from '../emojipicker/EmojiPicker'; import EmojiPicker from '../emojipicker/EmojiPicker';
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar"; import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
let instanceCount = 0;
const NARROW_MODE_BREAKPOINT = 500;
interface IComposerAvatarProps { interface IComposerAvatarProps {
me: object; me: object;
@ -71,13 +80,19 @@ function SendButton(props: ISendButtonProps) {
); );
} }
const EmojiButton = ({ addEmoji }) => { interface IEmojiButtonProps {
addEmoji: (unicode: string) => boolean;
menuPosition: any; // TODO: Types
narrowMode: boolean;
}
const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition, narrowMode }) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
let contextMenu; let contextMenu;
if (menuDisplayed) { if (menuDisplayed) {
const buttonRect = button.current.getBoundingClientRect(); const position = menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect());
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}> contextMenu = <ContextMenu {...position} onFinished={closeMenu} managed={false}>
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} /> <EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
</ContextMenu>; </ContextMenu>;
} }
@ -93,12 +108,11 @@ const EmojiButton = ({ addEmoji }) => {
// TODO: replace ContextMenuTooltipButton with a unified representation of // TODO: replace ContextMenuTooltipButton with a unified representation of
// the header buttons and the right panel buttons // the header buttons and the right panel buttons
return <React.Fragment> return <React.Fragment>
<ContextMenuTooltipButton <AccessibleTooltipButton
className={className} className={className}
onClick={openMenu} onClick={openMenu}
isExpanded={menuDisplayed} title={!narrowMode && _t('Emoji picker')}
title={_t('Emoji picker')} label={narrowMode && _t("Add emoji")}
inputRef={button}
/> />
{ contextMenu } { contextMenu }
@ -196,6 +210,9 @@ interface IState {
haveRecording: boolean; haveRecording: boolean;
recordingTimeLeftSeconds?: number; recordingTimeLeftSeconds?: number;
me?: RoomMember; me?: RoomMember;
narrowMode?: boolean;
isMenuOpen: boolean;
showStickers: boolean;
} }
@replaceableComponent("views.rooms.MessageComposer") @replaceableComponent("views.rooms.MessageComposer")
@ -203,6 +220,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
private dispatcherRef: string; private dispatcherRef: string;
private messageComposerInput: SendMessageComposer; private messageComposerInput: SendMessageComposer;
private voiceRecordingButton: VoiceRecordComposerTile; private voiceRecordingButton: VoiceRecordComposerTile;
private ref: React.RefObject<HTMLDivElement> = createRef();
private instanceId: number;
static defaultProps = { static defaultProps = {
replyInThread: false, replyInThread: false,
@ -220,15 +239,32 @@ export default class MessageComposer extends React.Component<IProps, IState> {
isComposerEmpty: true, isComposerEmpty: true,
haveRecording: false, haveRecording: false,
recordingTimeLeftSeconds: null, // when set to a number, shows a toast recordingTimeLeftSeconds: null, // when set to a number, shows a toast
isMenuOpen: false,
showStickers: false,
}; };
this.instanceId = instanceCount++;
} }
componentDidMount() { componentDidMount() {
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents); MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
this.waitForOwnMember(); this.waitForOwnMember();
UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current);
UIStore.instance.on(`MessageComposer${this.instanceId}`, 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) => { private onAction = (payload: ActionPayload) => {
if (payload.action === 'reply_to_event') { if (payload.action === 'reply_to_event') {
// add a timeout for the reply preview to be rendered, so // add a timeout for the reply preview to be rendered, so
@ -263,6 +299,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
} }
VoiceRecordingStore.instance.off(UPDATE_EVENT, this.onVoiceStoreUpdate); VoiceRecordingStore.instance.off(UPDATE_EVENT, this.onVoiceStoreUpdate);
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
UIStore.instance.stopTrackingElementDimensions(`MessageComposer${this.instanceId}`);
UIStore.instance.removeListener(`MessageComposer${this.instanceId}`, this.onResize);
} }
private onRoomStateEvents = (ev, state) => { private onRoomStateEvents = (ev, state) => {
@ -312,7 +350,11 @@ export default class MessageComposer extends React.Component<IProps, IState> {
private renderPlaceholderText = () => { private renderPlaceholderText = () => {
if (this.props.replyToEvent) { 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…'); return _t('Send an encrypted reply…');
} else { } else {
return _t('Send a reply…'); return _t('Send a reply…');
@ -326,11 +368,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {
} }
}; };
private addEmoji(emoji: string) { private addEmoji(emoji: string): boolean {
dis.dispatch<ComposerInsertPayload>({ dis.dispatch<ComposerInsertPayload>({
action: Action.ComposerInsert, action: Action.ComposerInsert,
text: emoji, text: emoji,
}); });
return true;
} }
private sendMessage = async () => { private sendMessage = async () => {
@ -369,6 +412,97 @@ export default class MessageComposer extends React.Component<IProps, IState> {
} }
}; };
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: JSX.Element[] = [];
if (!this.state.haveRecording) {
buttons.push(
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
);
buttons.push(
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} menuPosition={menuPosition} narrowMode={this.state.narrowMode} />,
);
}
if (this.shouldShowStickerPicker()) {
let title;
if (!this.state.narrowMode) {
title = this.state.showStickers ? _t("Hide Stickers") : _t("Show Stickers");
}
buttons.push(
<AccessibleTooltipButton
id='stickersButton'
key="controls_stickers"
className="mx_MessageComposer_button mx_MessageComposer_stickers"
onClick={() => this.showStickers(!this.state.showStickers)}
title={title}
label={this.state.narrowMode && _t("Send a sticker")}
/>,
);
}
if (!this.state.haveRecording && !this.state.narrowMode) {
buttons.push(
<AccessibleTooltipButton
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
onClick={() => 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] }
<AccessibleTooltipButton
className={classnames}
onClick={this.toggleButtonMenu}
title={_t("More options")}
tooltip={false}
/>
{ this.state.isMenuOpen && (
<ContextMenu
onFinished={this.toggleButtonMenu}
{...menuPosition}
menuPaddingRight={10}
menuPaddingTop={5}
menuPaddingBottom={5}
menuWidth={150}
wrapperClassName="mx_MessageComposer_Menu"
>
{ buttons.slice(1).map((button, index) => (
<MenuItem className="mx_CallContextMenu_item" key={index} onClick={this.toggleButtonMenu}>
{ button }
</MenuItem>
)) }
</ContextMenu>
) }
</>;
}
}
render() { render() {
const controls = [ const controls = [
this.state.me && !this.props.compact ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null, this.state.me && !this.props.compact ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
@ -377,6 +511,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {
null, null,
]; ];
let menuPosition;
if (this.ref.current) {
const contentRect = this.ref.current.getBoundingClientRect();
menuPosition = aboveLeftOf(contentRect);
}
if (!this.state.tombstone && this.state.canSendMessages) { if (!this.state.tombstone && this.state.canSendMessages) {
controls.push( controls.push(
<SendMessageComposer <SendMessageComposer
@ -392,33 +532,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
/>, />,
); );
if (!this.state.haveRecording) {
controls.push(
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
);
}
if (SettingsStore.getValue(UIFeature.Widgets) &&
SettingsStore.getValue("MessageComposerInput.showStickersButton") &&
!this.state.haveRecording) {
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
}
controls.push(<VoiceRecordComposerTile controls.push(<VoiceRecordComposerTile
key="controls_voice_record" key="controls_voice_record"
ref={c => this.voiceRecordingButton = c} ref={c => this.voiceRecordingButton = c}
room={this.props.room} />); room={this.props.room} />);
if (!this.state.isComposerEmpty || this.state.haveRecording) {
controls.push(
<SendButton
key="controls_send"
onClick={this.sendMessage}
title={this.state.haveRecording ? _t("Send voice message") : undefined}
/>,
);
}
} else if (this.state.tombstone) { } else if (this.state.tombstone) {
const replacementRoomId = this.state.tombstone.getContent()['replacement_room']; const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
@ -459,6 +576,15 @@ export default class MessageComposer extends React.Component<IProps, IState> {
yOffset={-50} yOffset={-50}
/>; />;
} }
controls.push(
<Stickerpicker
room={this.props.room}
showStickers={this.state.showStickers}
setShowStickers={this.showStickers}
menuPosition={menuPosition} />,
);
const showSendButton = !this.state.isComposerEmpty || this.state.haveRecording;
const classes = classNames({ const classes = classNames({
"mx_MessageComposer": true, "mx_MessageComposer": true,
@ -467,7 +593,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
}); });
return ( return (
<div className={classes}> <div className={classes} ref={this.ref}>
{ recordingTooltip } { recordingTooltip }
<div className="mx_MessageComposer_wrapper"> <div className="mx_MessageComposer_wrapper">
{ this.props.showReplyPreview && ( { this.props.showReplyPreview && (
@ -475,6 +601,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
) } ) }
<div className="mx_MessageComposer_row"> <div className="mx_MessageComposer_row">
{ controls } { controls }
{ this.renderButtons(menuPosition) }
{ showSendButton && (
<SendButton
key="controls_send"
onClick={this.sendMessage}
title={this.state.haveRecording ? _t("Send voice message") : undefined}
/>
) }
</div> </div>
</div> </div>
</div> </div>

View file

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import { Room } from 'matrix-js-sdk/src/models/room'; import { Room } from 'matrix-js-sdk/src/models/room';
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import AppTile from '../elements/AppTile'; import AppTile from '../elements/AppTile';
@ -27,7 +26,6 @@ import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { ChevronFace, ContextMenu } from "../../structures/ContextMenu"; import { ChevronFace, ContextMenu } from "../../structures/ContextMenu";
import { WidgetType } from "../../../widgets/WidgetType"; import { WidgetType } from "../../../widgets/WidgetType";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore"; import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -44,10 +42,12 @@ const PERSISTED_ELEMENT_KEY = "stickerPicker";
interface IProps { interface IProps {
room: Room; room: Room;
showStickers: boolean;
menuPosition?: any;
setShowStickers: (showStickers: boolean) => void;
} }
interface IState { interface IState {
showStickers: boolean;
imError: string; imError: string;
stickerpickerX: number; stickerpickerX: number;
stickerpickerY: number; stickerpickerY: number;
@ -72,7 +72,6 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {
showStickers: false,
imError: null, imError: null,
stickerpickerX: null, stickerpickerX: null,
stickerpickerY: null, stickerpickerY: null,
@ -114,7 +113,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
console.warn('No widget ID specified, not disabling assets'); console.warn('No widget ID specified, not disabling assets');
} }
this.setState({ showStickers: false }); this.props.setShowStickers(false);
WidgetUtils.removeStickerpickerWidgets().then(() => { WidgetUtils.removeStickerpickerWidgets().then(() => {
this.forceUpdate(); this.forceUpdate();
}).catch((e) => { }).catch((e) => {
@ -146,15 +145,15 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
} }
public componentDidUpdate(prevProps: IProps, prevState: IState): void { public componentDidUpdate(prevProps: IProps, prevState: IState): void {
this.sendVisibilityToWidget(this.state.showStickers); this.sendVisibilityToWidget(this.props.showStickers);
} }
private imError(errorMsg: string, e: Error): void { private imError(errorMsg: string, e: Error): void {
console.error(errorMsg, e); console.error(errorMsg, e);
this.setState({ this.setState({
showStickers: false,
imError: _t(errorMsg), imError: _t(errorMsg),
}); });
this.props.setShowStickers(false);
} }
private updateWidget = (): void => { private updateWidget = (): void => {
@ -194,12 +193,12 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
this.forceUpdate(); this.forceUpdate();
break; break;
case "stickerpicker_close": case "stickerpicker_close":
this.setState({ showStickers: false }); this.props.setShowStickers(false);
break; break;
case Action.AfterRightPanelPhaseChange: case Action.AfterRightPanelPhaseChange:
case "show_left_panel": case "show_left_panel":
case "hide_left_panel": case "hide_left_panel":
this.setState({ showStickers: false }); this.props.setShowStickers(false);
break; break;
} }
}; };
@ -338,8 +337,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19; const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
this.props.setShowStickers(true);
this.setState({ this.setState({
showStickers: true,
stickerpickerX: x, stickerpickerX: x,
stickerpickerY: y, stickerpickerY: y,
stickerpickerChevronOffset, stickerpickerChevronOffset,
@ -351,8 +350,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
* @param {Event} ev Event that triggered the function call * @param {Event} ev Event that triggered the function call
*/ */
private onHideStickersClick = (ev: React.MouseEvent): void => { private onHideStickersClick = (ev: React.MouseEvent): void => {
if (this.state.showStickers) { if (this.props.showStickers) {
this.setState({ showStickers: false }); this.props.setShowStickers(false);
} }
}; };
@ -360,8 +359,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
* Called when the window is resized * Called when the window is resized
*/ */
private onResize = (): void => { private onResize = (): void => {
if (this.state.showStickers) { if (this.props.showStickers) {
this.setState({ showStickers: false }); this.props.setShowStickers(false);
} }
}; };
@ -369,8 +368,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
* The stickers picker was hidden * The stickers picker was hidden
*/ */
private onFinished = (): void => { private onFinished = (): void => {
if (this.state.showStickers) { if (this.props.showStickers) {
this.setState({ showStickers: false }); this.props.setShowStickers(false);
} }
}; };
@ -395,26 +394,9 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
}; };
public render(): JSX.Element { public render(): JSX.Element {
let stickerPicker; if (!this.props.showStickers) return null;
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 =
<AccessibleButton
id='stickersButton'
key="controls_hide_stickers"
className={className}
onClick={this.onHideStickersClick}
title={_t("Hide Stickers")}
/>;
stickerPicker = <ContextMenu return <ContextMenu
chevronOffset={this.state.stickerpickerChevronOffset} chevronOffset={this.state.stickerpickerChevronOffset}
chevronFace={ChevronFace.Bottom} chevronFace={ChevronFace.Bottom}
left={this.state.stickerpickerX} left={this.state.stickerpickerX}
@ -426,23 +408,9 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
menuPaddingLeft={0} menuPaddingLeft={0}
menuPaddingRight={0} menuPaddingRight={0}
zIndex={STICKERPICKER_Z_INDEX} zIndex={STICKERPICKER_Z_INDEX}
{...this.props.menuPosition}
> >
<GenericElementContextMenu element={this.getStickerpickerContent()} onResize={this.onFinished} /> <GenericElementContextMenu element={this.getStickerpickerContent()} onResize={this.onFinished} />
</ContextMenu>; </ContextMenu>;
} else {
// Show show-stickers button
stickersButton =
<AccessibleTooltipButton
id='stickersButton'
key="controls_show_stickers"
className="mx_MessageComposer_button mx_MessageComposer_stickers"
onClick={this.onShowStickersClick}
title={_t("Show Stickers")}
/>;
}
return <React.Fragment>
{ stickersButton }
{ stickerPicker }
</React.Fragment>;
} }
} }

View file

@ -20,7 +20,6 @@ import React, { ReactNode } from "react";
import { IUpload, RecordingState, VoiceRecording } from "../../../audio/VoiceRecording"; import { IUpload, RecordingState, VoiceRecording } from "../../../audio/VoiceRecording";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import classNames from "classnames";
import LiveRecordingWaveform from "../audio_messages/LiveRecordingWaveform"; import LiveRecordingWaveform from "../audio_messages/LiveRecordingWaveform";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import LiveRecordingClock from "../audio_messages/LiveRecordingClock"; import LiveRecordingClock from "../audio_messages/LiveRecordingClock";
@ -137,7 +136,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
await this.disposeRecording(); await this.disposeRecording();
}; };
private onRecordStartEndClick = async () => { public onRecordStartEndClick = async () => {
if (this.state.recorder) { if (this.state.recorder) {
await this.state.recorder.stop(); await this.state.recorder.stop();
return; return;
@ -215,27 +214,23 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
} }
public render(): ReactNode { public render(): ReactNode {
let stopOrRecordBtn; if (!this.state.recordingPhase) return null;
let deleteButton;
if (!this.state.recordingPhase || this.state.recordingPhase === RecordingState.Started) {
const classes = classNames({
'mx_MessageComposer_button': !this.state.recorder,
'mx_MessageComposer_voiceMessage': !this.state.recorder,
'mx_VoiceRecordComposerTile_stop': this.state.recorder?.isRecording,
});
let stopBtn;
let deleteButton;
if (this.state.recordingPhase === RecordingState.Started) {
let tooltip = _t("Send voice message"); let tooltip = _t("Send voice message");
if (!!this.state.recorder) { if (!!this.state.recorder) {
tooltip = _t("Stop recording"); tooltip = _t("Stop recording");
} }
stopOrRecordBtn = <AccessibleTooltipButton stopBtn = <AccessibleTooltipButton
className={classes} className="mx_VoiceRecordComposerTile_stop"
onClick={this.onRecordStartEndClick} onClick={this.onRecordStartEndClick}
title={tooltip} title={tooltip}
/>; />;
if (this.state.recorder && !this.state.recorder?.isRecording) { if (this.state.recorder && !this.state.recorder?.isRecording) {
stopOrRecordBtn = null; stopBtn = null;
} }
} }
@ -264,13 +259,10 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
</span>; </span>;
} }
// 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 (<> return (<>
{ uploadIndicator } { uploadIndicator }
{ deleteButton } { deleteButton }
{ stopOrRecordBtn } { stopBtn }
{ this.renderWaveformArea() } { this.renderWaveformArea() }
</>); </>);
} }

View file

@ -1559,12 +1559,19 @@
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)", "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)",
"Send message": "Send message", "Send message": "Send message",
"Emoji picker": "Emoji picker", "Emoji picker": "Emoji picker",
"Add emoji": "Add emoji",
"Upload file": "Upload file", "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 an encrypted reply…": "Send an encrypted reply…",
"Send a reply…": "Send a reply…", "Send a reply…": "Send a reply…",
"Send an encrypted message…": "Send an encrypted message…", "Send an encrypted message…": "Send an encrypted message…",
"Send a message…": "Send a message…", "Send a message…": "Send a message…",
"Hide Stickers": "Hide Stickers",
"Show Stickers": "Show Stickers",
"Send a sticker": "Send a sticker",
"Send voice message": "Send voice message", "Send voice message": "Send voice message",
"More options": "More options",
"The conversation continues here.": "The conversation continues here.", "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.", "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", "You do not have permission to post to this room": "You do not have permission to post to this room",
@ -1726,8 +1733,6 @@
"You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled",
"Add some now": "Add some now", "Add some now": "Add some now",
"Stickerpack": "Stickerpack", "Stickerpack": "Stickerpack",
"Hide Stickers": "Hide Stickers",
"Show Stickers": "Show Stickers",
"Failed to revoke invite": "Failed to revoke invite", "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.", "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", "Admin Tools": "Admin Tools",