Refactor renderButtons() into MessageComposerButtons component (#7664)
This commit is contained in:
parent
db09d16205
commit
2229437424
4 changed files with 525 additions and 283 deletions
|
@ -19,7 +19,6 @@ import { MatrixEvent, IEventRelation } from "matrix-js-sdk/src/models/event";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { RelationType } from 'matrix-js-sdk/src/@types/event';
|
import { RelationType } from 'matrix-js-sdk/src/@types/event';
|
||||||
import { M_POLL_START } from "matrix-events-sdk";
|
|
||||||
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
|
@ -27,15 +26,9 @@ import dis from '../../../dispatcher/dispatcher';
|
||||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import Stickerpicker from './Stickerpicker';
|
import Stickerpicker from './Stickerpicker';
|
||||||
import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||||
import ContentMessages from '../../../ContentMessages';
|
|
||||||
import E2EIcon from './E2EIcon';
|
import E2EIcon from './E2EIcon';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import ContextMenu, {
|
import { aboveLeftOf, AboveLeftOf } from "../../structures/ContextMenu";
|
||||||
aboveLeftOf,
|
|
||||||
useContextMenu,
|
|
||||||
MenuItem,
|
|
||||||
AboveLeftOf,
|
|
||||||
} from "../../structures/ContextMenu";
|
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import ReplyPreview from "./ReplyPreview";
|
import ReplyPreview from "./ReplyPreview";
|
||||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
|
@ -50,15 +43,10 @@ import SendMessageComposer, { SendMessageComposer as SendMessageComposerClass }
|
||||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import EditorModel from "../../../editor/model";
|
import EditorModel from "../../../editor/model";
|
||||||
import EmojiPicker from '../emojipicker/EmojiPicker';
|
|
||||||
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
|
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
|
||||||
import Modal from "../../../Modal";
|
|
||||||
import RoomContext from '../../../contexts/RoomContext';
|
import RoomContext from '../../../contexts/RoomContext';
|
||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
|
||||||
import PollCreateDialog from "../elements/PollCreateDialog";
|
|
||||||
import { SettingUpdatedPayload } from "../../../dispatcher/payloads/SettingUpdatedPayload";
|
import { SettingUpdatedPayload } from "../../../dispatcher/payloads/SettingUpdatedPayload";
|
||||||
import { CollapsibleButton, ICollapsibleButtonProps } from './CollapsibleButton';
|
import MessageComposerButtons from './MessageComposerButtons';
|
||||||
import LocationButton from '../location/LocationButton';
|
|
||||||
|
|
||||||
let instanceCount = 0;
|
let instanceCount = 0;
|
||||||
const NARROW_MODE_BREAKPOINT = 500;
|
const NARROW_MODE_BREAKPOINT = 500;
|
||||||
|
@ -78,164 +66,6 @@ function SendButton(props: ISendButtonProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IEmojiButtonProps extends Pick<ICollapsibleButtonProps, "narrowMode"> {
|
|
||||||
addEmoji: (unicode: string) => boolean;
|
|
||||||
menuPosition: AboveLeftOf;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition, narrowMode }) => {
|
|
||||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
|
||||||
|
|
||||||
let contextMenu;
|
|
||||||
if (menuDisplayed) {
|
|
||||||
const position = menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect());
|
|
||||||
contextMenu = <ContextMenu {...position} onFinished={closeMenu} managed={false}>
|
|
||||||
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
|
|
||||||
</ContextMenu>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const className = classNames(
|
|
||||||
"mx_MessageComposer_button",
|
|
||||||
"mx_MessageComposer_emoji",
|
|
||||||
{
|
|
||||||
"mx_MessageComposer_button_highlight": menuDisplayed,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: replace ContextMenuTooltipButton with a unified representation of
|
|
||||||
// the header buttons and the right panel buttons
|
|
||||||
return <React.Fragment>
|
|
||||||
<CollapsibleButton
|
|
||||||
className={className}
|
|
||||||
onClick={openMenu}
|
|
||||||
narrowMode={narrowMode}
|
|
||||||
title={_t("Add emoji")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{ contextMenu }
|
|
||||||
</React.Fragment>;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IUploadButtonProps {
|
|
||||||
roomId: string;
|
|
||||||
relation?: IEventRelation | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
class UploadButton extends React.Component<IUploadButtonProps> {
|
|
||||||
private uploadInput = React.createRef<HTMLInputElement>();
|
|
||||||
private dispatcherRef: string;
|
|
||||||
|
|
||||||
constructor(props: IUploadButtonProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
dis.unregister(this.dispatcherRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload) => {
|
|
||||||
if (payload.action === "upload_file") {
|
|
||||||
this.onUploadClick();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onUploadClick = () => {
|
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
|
||||||
dis.dispatch({ action: 'require_registration' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.uploadInput.current.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onUploadFileInputChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (ev.target.files.length === 0) return;
|
|
||||||
|
|
||||||
// take a copy so we can safely reset the value of the form control
|
|
||||||
// (Note it is a FileList: we can't use slice or sensible iteration).
|
|
||||||
const tfiles = [];
|
|
||||||
for (let i = 0; i < ev.target.files.length; ++i) {
|
|
||||||
tfiles.push(ev.target.files[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
|
||||||
tfiles,
|
|
||||||
this.props.roomId,
|
|
||||||
this.props.relation,
|
|
||||||
MatrixClientPeg.get(),
|
|
||||||
this.context.timelineRenderingType,
|
|
||||||
);
|
|
||||||
|
|
||||||
// This is the onChange handler for a file form control, but we're
|
|
||||||
// not keeping any state, so reset the value of the form control
|
|
||||||
// to empty.
|
|
||||||
// NB. we need to set 'value': the 'files' property is immutable.
|
|
||||||
ev.target.value = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const uploadInputStyle = { display: 'none' };
|
|
||||||
return (
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className="mx_MessageComposer_button mx_MessageComposer_upload"
|
|
||||||
onClick={this.onUploadClick}
|
|
||||||
title={_t('Upload file')}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
ref={this.uploadInput}
|
|
||||||
type="file"
|
|
||||||
style={uploadInputStyle}
|
|
||||||
multiple
|
|
||||||
onChange={this.onUploadFileInputChange}
|
|
||||||
/>
|
|
||||||
</AccessibleTooltipButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IPollButtonProps extends Pick<ICollapsibleButtonProps, "narrowMode"> {
|
|
||||||
room: Room;
|
|
||||||
}
|
|
||||||
|
|
||||||
class PollButton extends React.PureComponent<IPollButtonProps> {
|
|
||||||
private onCreateClick = () => {
|
|
||||||
const canSend = this.props.room.currentState.maySendEvent(
|
|
||||||
M_POLL_START.name,
|
|
||||||
MatrixClientPeg.get().getUserId(),
|
|
||||||
);
|
|
||||||
if (!canSend) {
|
|
||||||
Modal.createTrackedDialog('Polls', 'permissions error: cannot start', ErrorDialog, {
|
|
||||||
title: _t("Permission Required"),
|
|
||||||
description: _t("You do not have permission to start polls in this room."),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Modal.createTrackedDialog(
|
|
||||||
'Polls',
|
|
||||||
'create',
|
|
||||||
PollCreateDialog,
|
|
||||||
{
|
|
||||||
room: this.props.room,
|
|
||||||
},
|
|
||||||
'mx_CompoundDialog',
|
|
||||||
false, // isPriorityModal
|
|
||||||
true, // isStaticModal
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<CollapsibleButton
|
|
||||||
className="mx_MessageComposer_button mx_MessageComposer_poll"
|
|
||||||
onClick={this.onCreateClick}
|
|
||||||
narrowMode={this.props.narrowMode}
|
|
||||||
title={_t("Create poll")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
|
@ -509,108 +339,6 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderButtons(menuPosition: AboveLeftOf): JSX.Element | JSX.Element[] {
|
|
||||||
if (this.state.haveRecording) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
let uploadButtonIndex = 0;
|
|
||||||
const buttons: JSX.Element[] = [];
|
|
||||||
buttons.push(
|
|
||||||
<PollButton
|
|
||||||
key="polls"
|
|
||||||
room={this.props.room}
|
|
||||||
narrowMode={this.state.narrowMode}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
uploadButtonIndex = buttons.length;
|
|
||||||
buttons.push(
|
|
||||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} relation={this.props.relation} />,
|
|
||||||
);
|
|
||||||
if (this.state.showLocationButton) {
|
|
||||||
const sender = this.props.room.getMember(
|
|
||||||
MatrixClientPeg.get().getUserId(),
|
|
||||||
);
|
|
||||||
buttons.push(
|
|
||||||
<LocationButton
|
|
||||||
key="location"
|
|
||||||
roomId={this.props.room.roomId}
|
|
||||||
sender={sender}
|
|
||||||
menuPosition={menuPosition}
|
|
||||||
narrowMode={this.state.narrowMode}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
buttons.push(
|
|
||||||
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} menuPosition={menuPosition} narrowMode={this.state.narrowMode} />,
|
|
||||||
);
|
|
||||||
if (this.state.showStickersButton) {
|
|
||||||
let title: string;
|
|
||||||
if (!this.state.narrowMode) {
|
|
||||||
title = this.state.isStickerPickerOpen ? _t("Hide Stickers") : _t("Show Stickers");
|
|
||||||
}
|
|
||||||
|
|
||||||
buttons.push(
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
id='stickersButton'
|
|
||||||
key="controls_stickers"
|
|
||||||
className="mx_MessageComposer_button mx_MessageComposer_stickers"
|
|
||||||
onClick={() => this.setStickerPickerOpen(!this.state.isStickerPickerOpen)}
|
|
||||||
title={title}
|
|
||||||
label={this.state.narrowMode ? _t("Send a sticker") : null}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// XXX: the recording UI does not work well in narrow mode, so we hide this button for now
|
|
||||||
if (!this.state.narrowMode) {
|
|
||||||
buttons.push(
|
|
||||||
<CollapsibleButton
|
|
||||||
key="voice_message_send"
|
|
||||||
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
|
|
||||||
onClick={() => this.voiceRecordingButton.current?.onRecordStartEndClick()}
|
|
||||||
title={_t("Send voice message")}
|
|
||||||
narrowMode={this.state.narrowMode}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.state.narrowMode) {
|
|
||||||
return buttons;
|
|
||||||
}
|
|
||||||
|
|
||||||
const classnames = classNames({
|
|
||||||
mx_MessageComposer_button: true,
|
|
||||||
mx_MessageComposer_buttonMenu: true,
|
|
||||||
mx_MessageComposer_closeButtonMenu: this.state.isMenuOpen,
|
|
||||||
});
|
|
||||||
|
|
||||||
// we render the uploadButton at top level as it is a very common interaction, splice it out of the rest
|
|
||||||
const [uploadButton] = buttons.splice(uploadButtonIndex, 1);
|
|
||||||
return <>
|
|
||||||
{ uploadButton }
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className={classnames}
|
|
||||||
onClick={this.toggleButtonMenu}
|
|
||||||
title={_t("More options")}
|
|
||||||
tooltip={false}
|
|
||||||
/>
|
|
||||||
{ this.state.isMenuOpen && (
|
|
||||||
<ContextMenu
|
|
||||||
onFinished={this.toggleButtonMenu}
|
|
||||||
{...menuPosition}
|
|
||||||
wrapperClassName="mx_MessageComposer_Menu"
|
|
||||||
>
|
|
||||||
{ buttons.map((button, index) => (
|
|
||||||
<MenuItem className="mx_CallContextMenu_item" key={index} onClick={this.toggleButtonMenu}>
|
|
||||||
{ button }
|
|
||||||
</MenuItem>
|
|
||||||
)) }
|
|
||||||
</ContextMenu>
|
|
||||||
) }
|
|
||||||
</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const controls = [
|
const controls = [
|
||||||
this.props.e2eStatus ?
|
this.props.e2eStatus ?
|
||||||
|
@ -717,7 +445,20 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
permalinkCreator={this.props.permalinkCreator} />
|
permalinkCreator={this.props.permalinkCreator} />
|
||||||
<div className="mx_MessageComposer_row">
|
<div className="mx_MessageComposer_row">
|
||||||
{ controls }
|
{ controls }
|
||||||
{ this.renderButtons(menuPosition) }
|
<MessageComposerButtons
|
||||||
|
addEmoji={this.addEmoji}
|
||||||
|
haveRecording={this.state.haveRecording}
|
||||||
|
isMenuOpen={this.state.isMenuOpen}
|
||||||
|
isStickerPickerOpen={this.state.isStickerPickerOpen}
|
||||||
|
menuPosition={menuPosition}
|
||||||
|
narrowMode={this.state.narrowMode}
|
||||||
|
relation={this.props.relation}
|
||||||
|
onRecordStartEndClick={() => this.voiceRecordingButton.current?.onRecordStartEndClick()}
|
||||||
|
setStickerPickerOpen={this.setStickerPickerOpen}
|
||||||
|
showLocationButton={this.state.showLocationButton}
|
||||||
|
showStickersButton={this.state.showStickersButton}
|
||||||
|
toggleButtonMenu={this.toggleButtonMenu}
|
||||||
|
/>
|
||||||
{ showSendButton && (
|
{ showSendButton && (
|
||||||
<SendButton
|
<SendButton
|
||||||
key="controls_send"
|
key="controls_send"
|
||||||
|
|
315
src/components/views/rooms/MessageComposerButtons.tsx
Normal file
315
src/components/views/rooms/MessageComposerButtons.tsx
Normal file
|
@ -0,0 +1,315 @@
|
||||||
|
/*
|
||||||
|
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 classNames from 'classnames';
|
||||||
|
import { IEventRelation } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { M_POLL_START } from "matrix-events-sdk";
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
|
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
import { CollapsibleButton, ICollapsibleButtonProps } from './CollapsibleButton';
|
||||||
|
import ContextMenu, { aboveLeftOf, AboveLeftOf, MenuItem, useContextMenu } from '../../structures/ContextMenu';
|
||||||
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
import EmojiPicker from '../emojipicker/EmojiPicker';
|
||||||
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
|
import LocationButton from '../location/LocationButton';
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
import PollCreateDialog from "../elements/PollCreateDialog";
|
||||||
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
|
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||||
|
import ContentMessages from '../../../ContentMessages';
|
||||||
|
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
||||||
|
import RoomContext from '../../../contexts/RoomContext';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
addEmoji: (emoji: string) => boolean;
|
||||||
|
haveRecording: boolean;
|
||||||
|
isMenuOpen: boolean;
|
||||||
|
isStickerPickerOpen: boolean;
|
||||||
|
menuPosition: AboveLeftOf;
|
||||||
|
narrowMode?: boolean;
|
||||||
|
onRecordStartEndClick: () => void;
|
||||||
|
relation?: IEventRelation;
|
||||||
|
setStickerPickerOpen: (isStickerPickerOpen: boolean) => void;
|
||||||
|
showLocationButton: boolean;
|
||||||
|
showStickersButton: boolean;
|
||||||
|
toggleButtonMenu: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
||||||
|
const matrixClient: MatrixClient = useContext(MatrixClientContext);
|
||||||
|
const { room, roomId } = useContext(RoomContext);
|
||||||
|
|
||||||
|
if (props.haveRecording) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let uploadButtonIndex = 0;
|
||||||
|
const buttons: JSX.Element[] = [];
|
||||||
|
buttons.push(
|
||||||
|
<PollButton
|
||||||
|
key="polls"
|
||||||
|
room={room}
|
||||||
|
narrowMode={props.narrowMode}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
uploadButtonIndex = buttons.length;
|
||||||
|
buttons.push(
|
||||||
|
<UploadButton key="controls_upload" roomId={roomId} relation={props.relation} />,
|
||||||
|
);
|
||||||
|
if (props.showLocationButton) {
|
||||||
|
const sender = room.getMember(matrixClient.getUserId());
|
||||||
|
buttons.push(
|
||||||
|
<LocationButton
|
||||||
|
key="location"
|
||||||
|
roomId={roomId}
|
||||||
|
sender={sender}
|
||||||
|
menuPosition={props.menuPosition}
|
||||||
|
narrowMode={props.narrowMode}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
buttons.push(
|
||||||
|
<EmojiButton key="emoji_button" addEmoji={props.addEmoji} menuPosition={props.menuPosition} narrowMode={props.narrowMode} />,
|
||||||
|
);
|
||||||
|
if (props.showStickersButton) {
|
||||||
|
let title: string;
|
||||||
|
if (!props.narrowMode) {
|
||||||
|
title = props.isStickerPickerOpen ? _t("Hide Stickers") : _t("Show Stickers");
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
id='stickersButton'
|
||||||
|
key="controls_stickers"
|
||||||
|
className="mx_MessageComposer_button mx_MessageComposer_stickers"
|
||||||
|
onClick={() => props.setStickerPickerOpen(!props.isStickerPickerOpen)}
|
||||||
|
title={title}
|
||||||
|
label={props.narrowMode ? _t("Send a sticker") : null}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: the recording UI does not work well in narrow mode, so we hide this button for now
|
||||||
|
if (!props.narrowMode) {
|
||||||
|
buttons.push(
|
||||||
|
<CollapsibleButton
|
||||||
|
key="voice_message_send"
|
||||||
|
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
|
||||||
|
onClick={props.onRecordStartEndClick}
|
||||||
|
title={_t("Send voice message")}
|
||||||
|
narrowMode={props.narrowMode}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.narrowMode) {
|
||||||
|
return <>{ buttons }</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const classnames = classNames({
|
||||||
|
mx_MessageComposer_button: true,
|
||||||
|
mx_MessageComposer_buttonMenu: true,
|
||||||
|
mx_MessageComposer_closeButtonMenu: props.isMenuOpen,
|
||||||
|
});
|
||||||
|
|
||||||
|
// we render the uploadButton at top level as it is a very common interaction, splice it out of the rest
|
||||||
|
const [uploadButton] = buttons.splice(uploadButtonIndex, 1);
|
||||||
|
return <>
|
||||||
|
{ uploadButton }
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className={classnames}
|
||||||
|
onClick={props.toggleButtonMenu}
|
||||||
|
title={_t("More options")}
|
||||||
|
tooltip={false}
|
||||||
|
/>
|
||||||
|
{ props.isMenuOpen && (
|
||||||
|
<ContextMenu
|
||||||
|
onFinished={props.toggleButtonMenu}
|
||||||
|
{...props.menuPosition}
|
||||||
|
wrapperClassName="mx_MessageComposer_Menu"
|
||||||
|
>
|
||||||
|
{ buttons.map((button, index) => (
|
||||||
|
<MenuItem className="mx_CallContextMenu_item" key={index} onClick={props.toggleButtonMenu}>
|
||||||
|
{ button }
|
||||||
|
</MenuItem>
|
||||||
|
)) }
|
||||||
|
</ContextMenu>
|
||||||
|
) }
|
||||||
|
</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IEmojiButtonProps extends Pick<ICollapsibleButtonProps, "narrowMode"> {
|
||||||
|
addEmoji: (unicode: string) => boolean;
|
||||||
|
menuPosition: AboveLeftOf;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition, narrowMode }) => {
|
||||||
|
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||||
|
|
||||||
|
let contextMenu: React.ReactElement | null = null;
|
||||||
|
if (menuDisplayed) {
|
||||||
|
const position = menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect());
|
||||||
|
contextMenu = <ContextMenu {...position} onFinished={closeMenu} managed={false}>
|
||||||
|
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
|
||||||
|
</ContextMenu>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const className = classNames(
|
||||||
|
"mx_MessageComposer_button",
|
||||||
|
"mx_MessageComposer_emoji",
|
||||||
|
{
|
||||||
|
"mx_MessageComposer_button_highlight": menuDisplayed,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: replace ContextMenuTooltipButton with a unified representation of
|
||||||
|
// the header buttons and the right panel buttons
|
||||||
|
return <React.Fragment>
|
||||||
|
<CollapsibleButton
|
||||||
|
className={className}
|
||||||
|
onClick={openMenu}
|
||||||
|
narrowMode={narrowMode}
|
||||||
|
title={_t("Add emoji")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{ contextMenu }
|
||||||
|
</React.Fragment>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IUploadButtonProps {
|
||||||
|
roomId: string;
|
||||||
|
relation?: IEventRelation | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UploadButton extends React.Component<IUploadButtonProps> {
|
||||||
|
private uploadInput = React.createRef<HTMLInputElement>();
|
||||||
|
private dispatcherRef: string;
|
||||||
|
|
||||||
|
constructor(props: IUploadButtonProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onAction = (payload: ActionPayload) => {
|
||||||
|
if (payload.action === "upload_file") {
|
||||||
|
this.onUploadClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onUploadClick = () => {
|
||||||
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
|
dis.dispatch({ action: 'require_registration' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.uploadInput.current.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onUploadFileInputChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (ev.target.files.length === 0) return;
|
||||||
|
|
||||||
|
// take a copy so we can safely reset the value of the form control
|
||||||
|
// (Note it is a FileList: we can't use slice or sensible iteration).
|
||||||
|
const tfiles = [];
|
||||||
|
for (let i = 0; i < ev.target.files.length; ++i) {
|
||||||
|
tfiles.push(ev.target.files[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||||
|
tfiles,
|
||||||
|
this.props.roomId,
|
||||||
|
this.props.relation,
|
||||||
|
MatrixClientPeg.get(),
|
||||||
|
this.context.timelineRenderingType,
|
||||||
|
);
|
||||||
|
|
||||||
|
// This is the onChange handler for a file form control, but we're
|
||||||
|
// not keeping any state, so reset the value of the form control
|
||||||
|
// to empty.
|
||||||
|
// NB. we need to set 'value': the 'files' property is immutable.
|
||||||
|
ev.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const uploadInputStyle = { display: 'none' };
|
||||||
|
return (
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_MessageComposer_button mx_MessageComposer_upload"
|
||||||
|
onClick={this.onUploadClick}
|
||||||
|
title={_t('Upload file')}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={this.uploadInput}
|
||||||
|
type="file"
|
||||||
|
style={uploadInputStyle}
|
||||||
|
multiple
|
||||||
|
onChange={this.onUploadFileInputChange}
|
||||||
|
/>
|
||||||
|
</AccessibleTooltipButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interface IPollButtonProps extends Pick<ICollapsibleButtonProps, "narrowMode"> {
|
||||||
|
room: Room;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PollButton extends React.PureComponent<IPollButtonProps> {
|
||||||
|
private onCreateClick = () => {
|
||||||
|
const canSend = this.props.room.currentState.maySendEvent(
|
||||||
|
M_POLL_START.name,
|
||||||
|
MatrixClientPeg.get().getUserId(),
|
||||||
|
);
|
||||||
|
if (!canSend) {
|
||||||
|
Modal.createTrackedDialog('Polls', 'permissions error: cannot start', ErrorDialog, {
|
||||||
|
title: _t("Permission Required"),
|
||||||
|
description: _t("You do not have permission to start polls in this room."),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Modal.createTrackedDialog(
|
||||||
|
'Polls',
|
||||||
|
'create',
|
||||||
|
PollCreateDialog,
|
||||||
|
{
|
||||||
|
room: this.props.room,
|
||||||
|
},
|
||||||
|
'mx_CompoundDialog',
|
||||||
|
false, // isPriorityModal
|
||||||
|
true, // isStaticModal
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<CollapsibleButton
|
||||||
|
className="mx_MessageComposer_button mx_MessageComposer_poll"
|
||||||
|
onClick={this.onCreateClick}
|
||||||
|
narrowMode={this.props.narrowMode}
|
||||||
|
title={_t("Create poll")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MessageComposerButtons;
|
|
@ -1683,24 +1683,24 @@
|
||||||
"Filter room members": "Filter room members",
|
"Filter room members": "Filter room members",
|
||||||
"%(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",
|
||||||
"Add emoji": "Add emoji",
|
|
||||||
"Upload file": "Upload file",
|
|
||||||
"You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.",
|
|
||||||
"Create poll": "Create poll",
|
|
||||||
"Reply to encrypted thread…": "Reply to encrypted thread…",
|
"Reply to encrypted thread…": "Reply to encrypted thread…",
|
||||||
"Reply to thread…": "Reply to 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",
|
|
||||||
"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",
|
||||||
"%(seconds)ss left": "%(seconds)ss left",
|
"%(seconds)ss left": "%(seconds)ss left",
|
||||||
|
"Send voice message": "Send voice message",
|
||||||
|
"Hide Stickers": "Hide Stickers",
|
||||||
|
"Show Stickers": "Show Stickers",
|
||||||
|
"Send a sticker": "Send a sticker",
|
||||||
|
"Add emoji": "Add emoji",
|
||||||
|
"Upload file": "Upload file",
|
||||||
|
"You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.",
|
||||||
|
"Create poll": "Create poll",
|
||||||
"Bold": "Bold",
|
"Bold": "Bold",
|
||||||
"Italics": "Italics",
|
"Italics": "Italics",
|
||||||
"Strikethrough": "Strikethrough",
|
"Strikethrough": "Strikethrough",
|
||||||
|
|
186
test/components/views/rooms/MessageComposerButtons-test.tsx
Normal file
186
test/components/views/rooms/MessageComposerButtons-test.tsx
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
/*
|
||||||
|
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 from "react";
|
||||||
|
import { mount, ReactWrapper } from "enzyme";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
|
||||||
|
import * as TestUtils from "../../../test-utils";
|
||||||
|
import sdk from "../../../skinned-sdk";
|
||||||
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||||
|
import { Layout } from "../../../../src/settings/enums/Layout";
|
||||||
|
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
||||||
|
import { createTestClient } from "../../../test-utils";
|
||||||
|
import { IRoomState } from "../../../../src/components/structures/RoomView";
|
||||||
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
|
|
||||||
|
const _MessageComposerButtons = sdk.getComponent("views.rooms.MessageComposerButtons");
|
||||||
|
const MessageComposerButtons = TestUtils.wrapInMatrixClientContext(
|
||||||
|
_MessageComposerButtons,
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("MessageComposerButtons", () => {
|
||||||
|
it("Renders all buttons in wide mode", () => {
|
||||||
|
const buttons = wrapAndRender(
|
||||||
|
<MessageComposerButtons
|
||||||
|
isMenuOpen={false}
|
||||||
|
narrowMode={false}
|
||||||
|
showLocationButton={true}
|
||||||
|
showStickersButton={true}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(buttonLabels(buttons)).toEqual([
|
||||||
|
"Create poll",
|
||||||
|
"Upload file",
|
||||||
|
"Share location",
|
||||||
|
"Add emoji",
|
||||||
|
"Show Stickers",
|
||||||
|
"Send voice message",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Renders only some buttons in narrow mode", () => {
|
||||||
|
const buttons = wrapAndRender(
|
||||||
|
<MessageComposerButtons
|
||||||
|
isMenuOpen={false}
|
||||||
|
narrowMode={true}
|
||||||
|
showLocationButton={true}
|
||||||
|
showStickersButton={true}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(buttonLabels(buttons)).toEqual([
|
||||||
|
"Upload file",
|
||||||
|
"More options",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Renders other buttons in menu (except voice messages) in narrow mode", () => {
|
||||||
|
const buttons = wrapAndRender(
|
||||||
|
<MessageComposerButtons
|
||||||
|
isMenuOpen={true}
|
||||||
|
narrowMode={true}
|
||||||
|
showLocationButton={true}
|
||||||
|
showStickersButton={true}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(buttonLabels(buttons)).toEqual([
|
||||||
|
"Upload file",
|
||||||
|
"More options",
|
||||||
|
[
|
||||||
|
"Create poll",
|
||||||
|
"Share location",
|
||||||
|
"Add emoji",
|
||||||
|
"Send a sticker",
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function wrapAndRender(component: React.ReactElement): ReactWrapper {
|
||||||
|
const mockClient = MatrixClientPeg.matrixClient = createTestClient();
|
||||||
|
const roomId = "myroomid";
|
||||||
|
const mockRoom: any = {
|
||||||
|
currentState: undefined,
|
||||||
|
roomId,
|
||||||
|
client: mockClient,
|
||||||
|
getMember: function(userId: string): RoomMember {
|
||||||
|
return new RoomMember(roomId, userId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const roomState = createRoomState(mockRoom);
|
||||||
|
|
||||||
|
return mount(
|
||||||
|
<MatrixClientContext.Provider value={mockClient}>
|
||||||
|
<RoomContext.Provider value={roomState}>
|
||||||
|
{ component }
|
||||||
|
</RoomContext.Provider>
|
||||||
|
</MatrixClientContext.Provider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRoomState(room: Room): IRoomState {
|
||||||
|
return {
|
||||||
|
room: room,
|
||||||
|
roomId: room.roomId,
|
||||||
|
roomLoading: true,
|
||||||
|
peekLoading: false,
|
||||||
|
shouldPeek: true,
|
||||||
|
membersLoaded: false,
|
||||||
|
numUnreadMessages: 0,
|
||||||
|
draggingFile: false,
|
||||||
|
searching: false,
|
||||||
|
guestsCanJoin: false,
|
||||||
|
canPeek: false,
|
||||||
|
showApps: false,
|
||||||
|
isPeeking: false,
|
||||||
|
showRightPanel: true,
|
||||||
|
joining: false,
|
||||||
|
atEndOfLiveTimeline: true,
|
||||||
|
atEndOfLiveTimelineInit: false,
|
||||||
|
showTopUnreadMessagesBar: false,
|
||||||
|
statusBarVisible: false,
|
||||||
|
canReact: false,
|
||||||
|
canReply: false,
|
||||||
|
layout: Layout.Group,
|
||||||
|
lowBandwidth: false,
|
||||||
|
alwaysShowTimestamps: false,
|
||||||
|
showTwelveHourTimestamps: false,
|
||||||
|
readMarkerInViewThresholdMs: 3000,
|
||||||
|
readMarkerOutOfViewThresholdMs: 30000,
|
||||||
|
showHiddenEventsInTimeline: false,
|
||||||
|
showReadReceipts: true,
|
||||||
|
showRedactions: true,
|
||||||
|
showJoinLeaves: true,
|
||||||
|
showAvatarChanges: true,
|
||||||
|
showDisplaynameChanges: true,
|
||||||
|
matrixClientIsReady: false,
|
||||||
|
dragCounter: 0,
|
||||||
|
timelineRenderingType: TimelineRenderingType.Room,
|
||||||
|
liveTimeline: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buttonLabels(buttons: ReactWrapper): any[] {
|
||||||
|
// Note: Depends on the fact that the mini buttons use aria-label
|
||||||
|
// and the labels under More options use label
|
||||||
|
const mainButtons = (
|
||||||
|
buttons
|
||||||
|
.find('div')
|
||||||
|
.map((button: ReactWrapper) => button.prop("aria-label"))
|
||||||
|
.filter(x => x)
|
||||||
|
);
|
||||||
|
|
||||||
|
let extraButtons = (
|
||||||
|
buttons
|
||||||
|
.find('div')
|
||||||
|
.map((button: ReactWrapper) => button.prop("label"))
|
||||||
|
.filter(x => x)
|
||||||
|
);
|
||||||
|
if (extraButtons.length === 0) {
|
||||||
|
extraButtons = [];
|
||||||
|
} else {
|
||||||
|
extraButtons = [extraButtons];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
...mainButtons,
|
||||||
|
...extraButtons,
|
||||||
|
];
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue