Merge branch 'develop' into matthew/low_bandwidth
This commit is contained in:
commit
d81804e0fe
589 changed files with 37701 additions and 15344 deletions
|
@ -40,30 +40,28 @@ import Analytics from '../../../Analytics';
|
|||
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
import * as RichText from '../../../RichText';
|
||||
import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import Autocomplete from './Autocomplete';
|
||||
import {Completion} from "../../../autocomplete/Autocompleter";
|
||||
import Markdown from '../../../Markdown';
|
||||
import ComposerHistoryManager from '../../../ComposerHistoryManager';
|
||||
import MessageComposerStore from '../../../stores/MessageComposerStore';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
|
||||
import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix';
|
||||
|
||||
import {
|
||||
asciiRegexp, unicodeRegexp, shortnameToUnicode,
|
||||
asciiList, mapUnicodeToShort, toShort,
|
||||
} from 'emojione';
|
||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import {makeUserPermalink} from "../../../matrix-to";
|
||||
import ReplyPreview from "./ReplyPreview";
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import {ContentHelpers} from 'matrix-js-sdk';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||
|
||||
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
||||
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
|
||||
const EMOJI_REGEX = new RegExp(unicodeRegexp, 'g');
|
||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
||||
|
||||
const TYPING_USER_TIMEOUT = 10000; const TYPING_SERVER_TIMEOUT = 30000;
|
||||
|
||||
|
@ -134,21 +132,14 @@ function rangeEquals(a: Range, b: Range): boolean {
|
|||
*/
|
||||
export default class MessageComposerInput extends React.Component {
|
||||
static propTypes = {
|
||||
// a callback which is called when the height of the composer is
|
||||
// changed due to a change in content.
|
||||
onResize: PropTypes.func,
|
||||
|
||||
// js-sdk Room object
|
||||
room: PropTypes.object.isRequired,
|
||||
|
||||
onFilesPasted: PropTypes.func,
|
||||
|
||||
onInputStateChanged: PropTypes.func,
|
||||
};
|
||||
|
||||
client: MatrixClient;
|
||||
autocomplete: Autocomplete;
|
||||
historyManager: ComposerHistoryManager;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
@ -277,9 +268,8 @@ export default class MessageComposerInput extends React.Component {
|
|||
case 'emoji':
|
||||
// XXX: apparently you can't return plain strings from serializer rules
|
||||
// until https://github.com/ianstormtaylor/slate/pull/1854 is merged.
|
||||
// So instead we temporarily wrap emoji from RTE in an arbitrary tag
|
||||
// (<b/>). <span/> would be nicer, but in practice it causes CSS issues.
|
||||
return <b>{ obj.data.get('emojiUnicode') }</b>;
|
||||
// So instead we temporarily wrap emoji from RTE in a span.
|
||||
return <span>{ obj.data.get('emojiUnicode') }</span>;
|
||||
}
|
||||
return this.renderNode({
|
||||
node: obj,
|
||||
|
@ -339,7 +329,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
componentWillMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -379,7 +368,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
const html = HtmlUtils.bodyToHtml(payload.event.getContent(), null, {
|
||||
forComposerQuote: true,
|
||||
returnString: true,
|
||||
emojiOne: false,
|
||||
});
|
||||
const fragment = this.html.deserialize(html);
|
||||
// FIXME: do we want to put in a permalink to the original quote here?
|
||||
|
@ -543,17 +531,15 @@ export default class MessageComposerInput extends React.Component {
|
|||
// Automatic replacement of plaintext emoji to Unicode emoji
|
||||
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
||||
// The first matched group includes just the matched plaintext emoji
|
||||
const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset));
|
||||
if (emojiMatch) {
|
||||
// plaintext -> hex unicode
|
||||
const emojiUc = asciiList[emojiMatch[1]];
|
||||
// hex unicode -> shortname -> actual unicode
|
||||
const unicodeEmoji = shortnameToUnicode(EMOJI_UNICODE_TO_SHORTNAME[emojiUc]);
|
||||
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(text.slice(0, currentStartOffset));
|
||||
if (emoticonMatch) {
|
||||
const data = EMOJIBASE.find(e => e.emoticon === emoticonMatch[1]);
|
||||
const unicodeEmoji = data ? data.unicode : '';
|
||||
|
||||
const range = Range.create({
|
||||
anchor: {
|
||||
key: editorState.startText.key,
|
||||
offset: currentStartOffset - emojiMatch[1].length - 1,
|
||||
offset: currentStartOffset - emoticonMatch[1].length - 1,
|
||||
},
|
||||
focus: {
|
||||
key: editorState.startText.key,
|
||||
|
@ -566,54 +552,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
// emojioneify any emoji
|
||||
let foundEmoji;
|
||||
do {
|
||||
foundEmoji = false;
|
||||
|
||||
for (const node of editorState.document.getTexts()) {
|
||||
if (node.text !== '' && HtmlUtils.containsEmoji(node.text)) {
|
||||
let match;
|
||||
EMOJI_REGEX.lastIndex = 0;
|
||||
while ((match = EMOJI_REGEX.exec(node.text)) !== null) {
|
||||
const range = Range.create({
|
||||
anchor: {
|
||||
key: node.key,
|
||||
offset: match.index,
|
||||
},
|
||||
focus: {
|
||||
key: node.key,
|
||||
offset: match.index + match[0].length,
|
||||
},
|
||||
});
|
||||
const inline = Inline.create({
|
||||
type: 'emoji',
|
||||
data: { emojiUnicode: match[0] },
|
||||
});
|
||||
change = change.insertInlineAtRange(range, inline);
|
||||
editorState = change.value;
|
||||
|
||||
// if we replaced an emoji, start again looking for more
|
||||
// emoji in the new editor state since doing the replacement
|
||||
// will change the node structure & offsets so we can't compute
|
||||
// insertion ranges from node.key / match.index anymore.
|
||||
foundEmoji = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (foundEmoji);
|
||||
|
||||
// work around weird bug where inserting emoji via the macOS
|
||||
// emoji picker can leave the selection stuck in the emoji's
|
||||
// child text. This seems to happen due to selection getting
|
||||
// moved in the normalisation phase after calculating these changes
|
||||
if (editorState.selection.anchor.key &&
|
||||
editorState.document.getParent(editorState.selection.anchor.key).type === 'emoji') {
|
||||
change = change.moveToStartOfNextText();
|
||||
editorState = change.value;
|
||||
}
|
||||
|
||||
if (this.props.onInputStateChanged && editorState.blocks.size > 0) {
|
||||
let blockType = editorState.blocks.first().type;
|
||||
// console.log("onInputStateChanged; current block type is " + blockType + " and marks are " + editorState.activeMarks);
|
||||
|
@ -628,7 +566,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
const inputState = {
|
||||
marks: editorState.activeMarks,
|
||||
isRichTextEnabled: this.state.isRichTextEnabled,
|
||||
blockType,
|
||||
};
|
||||
this.props.onInputStateChanged(inputState);
|
||||
|
@ -686,20 +623,22 @@ export default class MessageComposerInput extends React.Component {
|
|||
enableRichtext(enabled: boolean) {
|
||||
if (enabled === this.state.isRichTextEnabled) return;
|
||||
|
||||
let editorState = null;
|
||||
if (enabled) {
|
||||
editorState = this.mdToRichEditorState(this.state.editorState);
|
||||
} else {
|
||||
editorState = this.richToMdEditorState(this.state.editorState);
|
||||
}
|
||||
|
||||
Analytics.setRichtextMode(enabled);
|
||||
|
||||
this.setState({
|
||||
editorState: this.createEditorState(enabled, editorState),
|
||||
editorState: this.createEditorState(
|
||||
enabled,
|
||||
this.state.editorState,
|
||||
this.state.isRichTextEnabled,
|
||||
),
|
||||
isRichTextEnabled: enabled,
|
||||
}, ()=>{
|
||||
}, () => {
|
||||
this._editor.focus();
|
||||
if (this.props.onInputStateChanged) {
|
||||
this.props.onInputStateChanged({
|
||||
isRichTextEnabled: enabled,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
SettingsStore.setValue("MessageComposerInput.isRichTextEnabled", null, SettingLevel.ACCOUNT, enabled);
|
||||
|
@ -1012,7 +951,13 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
switch (transfer.type) {
|
||||
case 'files':
|
||||
return this.props.onFilesPasted(transfer.files);
|
||||
// This actually not so much for 'files' as such (at time of writing
|
||||
// neither chrome nor firefox let you paste a plain file copied
|
||||
// from Finder) but more images copied from a different website
|
||||
// / word processor etc.
|
||||
return ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
transfer.files, this.props.room.roomId, this.client,
|
||||
);
|
||||
case 'html': {
|
||||
if (this.state.isRichTextEnabled) {
|
||||
// FIXME: https://github.com/ianstormtaylor/slate/issues/1497 means
|
||||
|
@ -1039,10 +984,17 @@ export default class MessageComposerInput extends React.Component {
|
|||
};
|
||||
|
||||
handleReturn = (ev, change) => {
|
||||
if (ev.shiftKey) {
|
||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
if (ev.shiftKey || (isMac && ev.altKey)) {
|
||||
return change.insertText('\n');
|
||||
}
|
||||
|
||||
if (this.autocomplete.countCompletions() > 0) {
|
||||
this.autocomplete.hide();
|
||||
ev.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
||||
const editorState = this.state.editorState;
|
||||
|
||||
const lastBlock = editorState.blocks.last();
|
||||
|
@ -1084,7 +1036,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
if (cmd) {
|
||||
if (!cmd.error) {
|
||||
this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown');
|
||||
this.setState({
|
||||
editorState: this.createEditorState(),
|
||||
}, ()=>{
|
||||
|
@ -1162,11 +1113,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
let sendHtmlFn = ContentHelpers.makeHtmlMessage;
|
||||
let sendTextFn = ContentHelpers.makeTextMessage;
|
||||
|
||||
this.historyManager.save(
|
||||
editorState,
|
||||
this.state.isRichTextEnabled ? 'rich' : 'markdown',
|
||||
);
|
||||
|
||||
if (commandText && commandText.startsWith('/me')) {
|
||||
if (replyingToEv) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
@ -1194,7 +1140,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
// Part of Replies fallback support - prepend the text we're sending
|
||||
// with the text we're replying to
|
||||
const nestedReply = ReplyThread.getNestedReplyText(replyingToEv);
|
||||
const nestedReply = ReplyThread.getNestedReplyText(replyingToEv, this.props.permalinkCreator);
|
||||
if (nestedReply) {
|
||||
if (content.formatted_body) {
|
||||
content.formatted_body = nestedReply.html + content.formatted_body;
|
||||
|
@ -1241,14 +1187,16 @@ export default class MessageComposerInput extends React.Component {
|
|||
// and we must be at the edge of the document (up=start, down=end)
|
||||
if (up) {
|
||||
if (!selection.anchor.isAtStartOfNode(document)) return;
|
||||
} else {
|
||||
if (!selection.anchor.isAtEndOfNode(document)) return;
|
||||
}
|
||||
|
||||
const selected = this.selectHistory(up);
|
||||
if (selected) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
e.preventDefault();
|
||||
const editEvent = findEditableEvent(this.props.room, false);
|
||||
if (editEvent) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'edit_event',
|
||||
event: editEvent,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.moveAutocompleteSelection(up);
|
||||
|
@ -1256,54 +1204,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
selectHistory = async (up) => {
|
||||
const delta = up ? -1 : 1;
|
||||
|
||||
// True if we are not currently selecting history, but composing a message
|
||||
if (this.historyManager.currentIndex === this.historyManager.history.length) {
|
||||
// We can't go any further - there isn't any more history, so nop.
|
||||
if (!up) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
currentlyComposedEditorState: this.state.editorState,
|
||||
});
|
||||
} else if (this.historyManager.currentIndex + delta === this.historyManager.history.length) {
|
||||
// True when we return to the message being composed currently
|
||||
this.setState({
|
||||
editorState: this.state.currentlyComposedEditorState,
|
||||
});
|
||||
this.historyManager.currentIndex = this.historyManager.history.length;
|
||||
return;
|
||||
}
|
||||
|
||||
let editorState;
|
||||
const historyItem = this.historyManager.getItem(delta);
|
||||
if (!historyItem) return;
|
||||
|
||||
if (historyItem.format === 'rich' && !this.state.isRichTextEnabled) {
|
||||
editorState = this.richToMdEditorState(historyItem.value);
|
||||
} else if (historyItem.format === 'markdown' && this.state.isRichTextEnabled) {
|
||||
editorState = this.mdToRichEditorState(historyItem.value);
|
||||
} else {
|
||||
editorState = historyItem.value;
|
||||
}
|
||||
|
||||
// Move selection to the end of the selected history
|
||||
const change = editorState.change().moveToEndOfNode(editorState.document);
|
||||
|
||||
// We don't call this.onChange(change) now, as fixups on stuff like emoji
|
||||
// should already have been done and persisted in the history.
|
||||
editorState = change.value;
|
||||
|
||||
this.suppressAutoComplete = true;
|
||||
|
||||
this.setState({ editorState }, ()=>{
|
||||
this._editor.focus();
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
onTab = async (e) => {
|
||||
this.setState({
|
||||
someCompletions: null,
|
||||
|
@ -1472,17 +1372,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
case 'emoji': {
|
||||
const { data } = node;
|
||||
const emojiUnicode = data.get('emojiUnicode');
|
||||
const uri = RichText.unicodeToEmojiUri(emojiUnicode);
|
||||
const shortname = toShort(emojiUnicode);
|
||||
const className = classNames('mx_emojione', {
|
||||
mx_emojione_selected: isSelected,
|
||||
});
|
||||
const style = {};
|
||||
if (props.selected) style.border = '1px solid blue';
|
||||
return <img className={ className } src={ uri }
|
||||
title={ shortname } alt={ emojiUnicode } style={style}
|
||||
/>;
|
||||
return data.get('emojiUnicode');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1583,10 +1473,15 @@ export default class MessageComposerInput extends React.Component {
|
|||
placeholder = undefined;
|
||||
}
|
||||
|
||||
const markdownClasses = classNames({
|
||||
mx_MessageComposer_input_markdownIndicator: true,
|
||||
mx_MessageComposer_markdownDisabled: this.state.isRichTextEnabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx_MessageComposer_input_wrapper" onClick={this.focusComposer}>
|
||||
<div className="mx_MessageComposer_autocomplete_wrapper">
|
||||
<ReplyPreview />
|
||||
<ReplyPreview permalinkCreator={this.props.permalinkCreator} />
|
||||
<Autocomplete
|
||||
ref={(e) => this.autocomplete = e}
|
||||
room={this.props.room}
|
||||
|
@ -1597,10 +1492,10 @@ export default class MessageComposerInput extends React.Component {
|
|||
/>
|
||||
</div>
|
||||
<div className={className}>
|
||||
<img className="mx_MessageComposer_input_markdownIndicator mx_filterFlipColor"
|
||||
onMouseDown={this.onMarkdownToggleClicked}
|
||||
title={this.state.isRichTextEnabled ? _t("Markdown is disabled") : _t("Markdown is enabled")}
|
||||
src={require(`../../../../res/img/button-md-${!this.state.isRichTextEnabled}.png`)} />
|
||||
<AccessibleButton className={markdownClasses}
|
||||
onClick={this.onMarkdownToggleClicked}
|
||||
title={this.state.isRichTextEnabled ? _t("Markdown is disabled") : _t("Markdown is enabled")}
|
||||
/>
|
||||
<Editor ref={this._collectEditor}
|
||||
dir="auto"
|
||||
className="mx_MessageComposer_editor"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue