mirror of https://github.com/vector-im/riot-web
support basic sending with new main composer
this removes all formatting options, as the new editor doesn't have any.pull/21833/head
parent
df8488e194
commit
cfbd2e9cc8
|
@ -159,6 +159,7 @@
|
||||||
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
|
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
|
||||||
@import "./views/rooms/_SearchBar.scss";
|
@import "./views/rooms/_SearchBar.scss";
|
||||||
@import "./views/rooms/_SearchableEntityList.scss";
|
@import "./views/rooms/_SearchableEntityList.scss";
|
||||||
|
@import "./views/rooms/_SendMessageComposer.scss";
|
||||||
@import "./views/rooms/_Stickers.scss";
|
@import "./views/rooms/_Stickers.scss";
|
||||||
@import "./views/rooms/_TopUnreadMessagesBar.scss";
|
@import "./views/rooms/_TopUnreadMessagesBar.scss";
|
||||||
@import "./views/rooms/_WhoIsTypingTile.scss";
|
@import "./views/rooms/_WhoIsTypingTile.scss";
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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_SendMessageComposer {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 50px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.mx_BasicMessageComposer {
|
||||||
|
.mx_BasicMessageComposer_input {
|
||||||
|
padding: 3px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,30 +16,18 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import CallHandler from '../../../CallHandler';
|
import CallHandler from '../../../CallHandler';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
|
||||||
import Stickerpicker from './Stickerpicker';
|
import Stickerpicker from './Stickerpicker';
|
||||||
import { makeRoomPermalink } from '../../../matrix-to';
|
import { makeRoomPermalink } from '../../../matrix-to';
|
||||||
import ContentMessages from '../../../ContentMessages';
|
import ContentMessages from '../../../ContentMessages';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import E2EIcon from './E2EIcon';
|
import E2EIcon from './E2EIcon';
|
||||||
|
|
||||||
const formatButtonList = [
|
|
||||||
_td("bold"),
|
|
||||||
_td("italic"),
|
|
||||||
_td("deleted"),
|
|
||||||
_td("underlined"),
|
|
||||||
_td("inline-code"),
|
|
||||||
_td("block-quote"),
|
|
||||||
_td("bulleted-list"),
|
|
||||||
_td("numbered-list"),
|
|
||||||
];
|
|
||||||
|
|
||||||
function ComposerAvatar(props) {
|
function ComposerAvatar(props) {
|
||||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||||
return <div className="mx_MessageComposer_avatar">
|
return <div className="mx_MessageComposer_avatar">
|
||||||
|
@ -115,28 +103,11 @@ HangupButton.propTypes = {
|
||||||
roomId: PropTypes.string.isRequired,
|
roomId: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
function FormattingButton(props) {
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
return <AccessibleButton
|
|
||||||
element="img"
|
|
||||||
className="mx_MessageComposer_formatting"
|
|
||||||
alt={_t("Show Text Formatting Toolbar")}
|
|
||||||
title={_t("Show Text Formatting Toolbar")}
|
|
||||||
src={require("../../../../res/img/button-text-formatting.svg")}
|
|
||||||
style={{visibility: props.showFormatting ? 'hidden' : 'visible'}}
|
|
||||||
onClick={props.onClickHandler}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
FormattingButton.propTypes = {
|
|
||||||
showFormatting: PropTypes.bool.isRequired,
|
|
||||||
onClickHandler: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
class UploadButton extends React.Component {
|
class UploadButton extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
roomId: PropTypes.string.isRequired,
|
roomId: PropTypes.string.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.onUploadClick = this.onUploadClick.bind(this);
|
this.onUploadClick = this.onUploadClick.bind(this);
|
||||||
|
@ -193,24 +164,14 @@ class UploadButton extends React.Component {
|
||||||
export default class MessageComposer extends React.Component {
|
export default class MessageComposer extends React.Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
|
|
||||||
this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this);
|
|
||||||
this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
|
|
||||||
this.onInputStateChanged = this.onInputStateChanged.bind(this);
|
this.onInputStateChanged = this.onInputStateChanged.bind(this);
|
||||||
this.onEvent = this.onEvent.bind(this);
|
this.onEvent = this.onEvent.bind(this);
|
||||||
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
|
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
|
||||||
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
|
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
|
||||||
this._onTombstoneClick = this._onTombstoneClick.bind(this);
|
this._onTombstoneClick = this._onTombstoneClick.bind(this);
|
||||||
this.renderPlaceholderText = this.renderPlaceholderText.bind(this);
|
this.renderPlaceholderText = this.renderPlaceholderText.bind(this);
|
||||||
this.renderFormatBar = this.renderFormatBar.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
inputState: {
|
|
||||||
marks: [],
|
|
||||||
blockType: null,
|
|
||||||
isRichTextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
|
|
||||||
},
|
|
||||||
showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
|
|
||||||
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
|
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
|
||||||
tombstone: this._getRoomTombstone(),
|
tombstone: this._getRoomTombstone(),
|
||||||
canSendMessages: this.props.room.maySendMessage(),
|
canSendMessages: this.props.room.maySendMessage(),
|
||||||
|
@ -257,6 +218,7 @@ export default class MessageComposer extends React.Component {
|
||||||
onEvent(event) {
|
onEvent(event) {
|
||||||
if (event.getType() !== 'm.room.encryption') return;
|
if (event.getType() !== 'm.room.encryption') return;
|
||||||
if (event.getRoomId() !== this.props.room.roomId) return;
|
if (event.getRoomId() !== this.props.room.roomId) return;
|
||||||
|
// TODO: put (encryption state??) in state
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,34 +243,12 @@ export default class MessageComposer extends React.Component {
|
||||||
this.setState({ isQuoting });
|
this.setState({ isQuoting });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
onInputStateChanged(inputState) {
|
onInputStateChanged(inputState) {
|
||||||
// Merge the new input state with old to support partial updates
|
// Merge the new input state with old to support partial updates
|
||||||
inputState = Object.assign({}, this.state.inputState, inputState);
|
inputState = Object.assign({}, this.state.inputState, inputState);
|
||||||
this.setState({inputState});
|
this.setState({inputState});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAutocompleteConfirm(range, completion) {
|
|
||||||
if (this.messageComposerInput) {
|
|
||||||
this.messageComposerInput.setDisplayedCompletion(range, completion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onFormatButtonClicked(name, event) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.messageComposerInput.onFormatButtonClicked(name, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggleFormattingClicked() {
|
|
||||||
SettingsStore.setValue("MessageComposer.showFormatting", null, SettingLevel.DEVICE, !this.state.showFormatting);
|
|
||||||
this.setState({showFormatting: !this.state.showFormatting});
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggleMarkdownClicked(e) {
|
|
||||||
e.preventDefault(); // don't steal focus from the editor!
|
|
||||||
this.messageComposerInput.enableRichtext(!this.state.inputState.isRichTextEnabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onTombstoneClick(ev) {
|
_onTombstoneClick(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
|
@ -355,47 +295,6 @@ export default class MessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFormatBar() {
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
const {marks, blockType} = this.state.inputState;
|
|
||||||
const formatButtons = formatButtonList.map((name) => {
|
|
||||||
// special-case to match the md serializer and the special-case in MessageComposerInput.js
|
|
||||||
const markName = name === 'inline-code' ? 'code' : name;
|
|
||||||
const active = marks.some(mark => mark.type === markName) || blockType === name;
|
|
||||||
const suffix = active ? '-on' : '';
|
|
||||||
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
|
|
||||||
const className = 'mx_MessageComposer_format_button mx_filterFlipColor';
|
|
||||||
return (
|
|
||||||
<img className={className}
|
|
||||||
title={_t(name)}
|
|
||||||
onMouseDown={onFormatButtonClicked}
|
|
||||||
key={name}
|
|
||||||
src={require(`../../../../res/img/button-text-${name}${suffix}.svg`)}
|
|
||||||
height="17"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx_MessageComposer_formatbar_wrapper">
|
|
||||||
<div className="mx_MessageComposer_formatbar">
|
|
||||||
{ formatButtons }
|
|
||||||
<div style={{ flex: 1 }}></div>
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_MessageComposer_formatbar_markdown mx_MessageComposer_markdownDisabled"
|
|
||||||
onClick={this.onToggleMarkdownClicked}
|
|
||||||
title={_t("Markdown is disabled")}
|
|
||||||
/>
|
|
||||||
<AccessibleButton element="img" title={_t("Hide Text Formatting Toolbar")}
|
|
||||||
onClick={this.onToggleFormattingClicked}
|
|
||||||
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
|
|
||||||
src={require("../../../../res/img/icon-text-cancel.svg")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const controls = [
|
const controls = [
|
||||||
this.state.me ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
|
this.state.me ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
|
||||||
|
@ -409,23 +308,16 @@ export default class MessageComposer extends React.Component {
|
||||||
// check separately for whether we can call, but this is slightly
|
// check separately for whether we can call, but this is slightly
|
||||||
// complex because of conference calls.
|
// complex because of conference calls.
|
||||||
|
|
||||||
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
|
const SendMessageComposer = sdk.getComponent("rooms.SendMessageComposer");
|
||||||
const showFormattingButton = this.state.inputState.isRichTextEnabled;
|
|
||||||
const callInProgress = this.props.callState && this.props.callState !== 'ended';
|
const callInProgress = this.props.callState && this.props.callState !== 'ended';
|
||||||
|
|
||||||
controls.push(
|
controls.push(
|
||||||
<MessageComposerInput
|
<SendMessageComposer
|
||||||
ref={(c) => this.messageComposerInput = c}
|
ref={(c) => this.messageComposerInput = c}
|
||||||
key="controls_input"
|
key="controls_input"
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
placeholder={this.renderPlaceholderText()}
|
placeholder={this.renderPlaceholderText()}
|
||||||
onInputStateChanged={this.onInputStateChanged}
|
|
||||||
permalinkCreator={this.props.permalinkCreator} />,
|
permalinkCreator={this.props.permalinkCreator} />,
|
||||||
showFormattingButton ? <FormattingButton
|
|
||||||
key="controls_formatting"
|
|
||||||
showFormatting={this.state.showFormatting}
|
|
||||||
onClickHandler={this.onToggleFormattingClicked} /> :
|
|
||||||
null,
|
|
||||||
<Stickerpicker key='stickerpicker_controls_button' room={this.props.room} />,
|
<Stickerpicker key='stickerpicker_controls_button' room={this.props.room} />,
|
||||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||||
callInProgress ? <HangupButton key="controls_hangup" roomId={this.props.room.roomId} /> : null,
|
callInProgress ? <HangupButton key="controls_hangup" roomId={this.props.room.roomId} /> : null,
|
||||||
|
@ -461,8 +353,6 @@ export default class MessageComposer extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const showFormatBar = this.state.showFormatting && this.state.inputState.isRichTextEnabled;
|
|
||||||
|
|
||||||
const wrapperClasses = classNames({
|
const wrapperClasses = classNames({
|
||||||
mx_MessageComposer_wrapper: true,
|
mx_MessageComposer_wrapper: true,
|
||||||
mx_MessageComposer_hasE2EIcon: !!this.props.e2eStatus,
|
mx_MessageComposer_hasE2EIcon: !!this.props.e2eStatus,
|
||||||
|
@ -474,7 +364,6 @@ export default class MessageComposer extends React.Component {
|
||||||
{ controls }
|
{ controls }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{ showFormatBar ? this.renderFormatBar() : null }
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 New Vector Ltd
|
||||||
|
Copyright 2019 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 PropTypes from 'prop-types';
|
||||||
|
import dis from '../../../dispatcher';
|
||||||
|
import EditorModel from '../../../editor/model';
|
||||||
|
import {getCaretOffsetAndText} from '../../../editor/dom';
|
||||||
|
import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize';
|
||||||
|
import {PartCreator} from '../../../editor/parts';
|
||||||
|
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||||
|
import {MatrixClient} from 'matrix-js-sdk';
|
||||||
|
import BasicMessageComposer from "./BasicMessageComposer";
|
||||||
|
|
||||||
|
function createMessageContent(model, editedEvent) {
|
||||||
|
const body = textSerialize(model);
|
||||||
|
const content = {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body,
|
||||||
|
};
|
||||||
|
const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: false});
|
||||||
|
if (formattedBody) {
|
||||||
|
content.format = "org.matrix.custom.html";
|
||||||
|
content.formatted_body = formattedBody;
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SendMessageComposer extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
// the message event being edited
|
||||||
|
editState: PropTypes.instanceOf(EditorStateTransfer).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
this.model = null;
|
||||||
|
this._editorRef = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setEditorRef = ref => {
|
||||||
|
this._editorRef = ref;
|
||||||
|
};
|
||||||
|
|
||||||
|
_onKeyDown = (event) => {
|
||||||
|
if (event.metaKey || event.altKey || event.shiftKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
this._sendMessage();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendMessage = () => {
|
||||||
|
const {roomId} = this.props.room;
|
||||||
|
this.context.matrixClient.sendMessage(roomId, createMessageContent(this.model));
|
||||||
|
this.model.reset([]);
|
||||||
|
dis.dispatch({action: 'focus_composer'});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
const sel = document.getSelection();
|
||||||
|
const {caret} = getCaretOffsetAndText(this._editorRef, sel);
|
||||||
|
const parts = this.model.serializeParts();
|
||||||
|
this.props.editState.setEditorState(caret, parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
const partCreator = new PartCreator(this.props.room, this.context.matrixClient);
|
||||||
|
this.model = new EditorModel([], partCreator);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// <div className="mx_MessageComposer_autocomplete_wrapper">
|
||||||
|
// </div>
|
||||||
|
//<ReplyPreview permalinkCreator={this.props.permalinkCreator} />
|
||||||
|
return (
|
||||||
|
<div className="mx_SendMessageComposer" onClick={this.focusComposer} onKeyDown={this._onKeyDown}>
|
||||||
|
<BasicMessageComposer
|
||||||
|
ref={this._setEditorRef}
|
||||||
|
model={this.model}
|
||||||
|
room={this.props.room}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue