<commit_before>/*
Copyright 2015, 2016 OpenMarket Ltd

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 type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent';

import {Editor, EditorState, RichUtils, CompositeDecorator, Modifier,
    getDefaultKeyBinding, KeyBindingUtil, ContentState, ContentBlock, SelectionState,
    Entity} from 'draft-js';

import classNames from 'classnames';
import escape from 'lodash/escape';
import Promise from 'bluebird';

import MatrixClientPeg from '../../../MatrixClientPeg';
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
import SlashCommands from '../../../SlashCommands';
import KeyCode from '../../../KeyCode';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';

import dis from '../../../dispatcher';
import UserSettingsStore from '../../../UserSettingsStore';

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 {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
const REGEX_EMOJI_WHITESPACE = new RegExp('(' + asciiRegexp + ')\\s$');

const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;

const ZWS_CODE = 8203;
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
function stateToMarkdown(state) {
    return __stateToMarkdown(state)
        .replace(
            ZWS, // draft-js-export-markdown adds these
            ''); // this is *not* a zero width space, trust me :)
}

function onSendMessageFailed(err, room) {
    // XXX: temporary logging to try to diagnose
    // https://github.com/vector-im/riot-web/issues/3148
    console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
    if (err.name === "UnknownDeviceError") {
        dis.dispatch({
            action: 'unknown_device_error',
            err: err,
            room: room,
        });
    }
    dis.dispatch({
        action: 'message_send_failed',
    });
}

/*
 * The textInput part of the MessageComposer
 */
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: React.PropTypes.func,

        // js-sdk Room object
        room: React.PropTypes.object.isRequired,

        // called with current plaintext content (as a string) whenever it changes
        onContentChanged: React.PropTypes.func,

        onInputStateChanged: React.PropTypes.func,
    };

    static getKeyBinding(e: SyntheticKeyboardEvent): string {
        // C-m => Toggles between rich text and markdown modes
        if (e.keyCode === KeyCode.KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
            return 'toggle-mode';
        }

        // Allow opening of dev tools. getDefaultKeyBinding would be 'italic' for KEY_I
        if (e.keyCode === KeyCode.KEY_I && e.shiftKey && e.ctrlKey) {
            // When null is returned, draft-js will NOT preventDefault, allowing dev tools
            // to be toggled when the editor is focussed
            return null;
        }

        return getDefaultKeyBinding(e);
    }

    static getBlockStyle(block: ContentBlock): ?string {
        if (block.getType() === 'strikethrough') {
            return 'mx_Markdown_STRIKETHROUGH';
        }

        return null;
    }

    client: MatrixClient;
    autocomplete: Autocomplete;
    historyManager: ComposerHistoryManager;

    constructor(props, context) {
        super(props, context);
        this.onAction = this.onAction.bind(this);
        this.handleReturn = this.handleReturn.bind(this);
        this.handleKeyCommand = this.handleKeyCommand.bind(this);
        this.onEditorContentChanged = this.onEditorContentChanged.bind(this);
        this.onUpArrow = this.onUpArrow.bind(this);
        this.onDownArrow = this.onDownArrow.bind(this);
        this.onTab = this.onTab.bind(this);
        this.onEscape = this.onEscape.bind(this);
        this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
        this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
        this.onTextPasted = this.onTextPasted.bind(this);

        const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);

        this.state = {
            // whether we're in rich text or markdown mode
            isRichtextEnabled,

            // the currently displayed editor state (note: this is always what is modified on input)
            editorState: this.createEditorState(
                isRichtextEnabled,
                MessageComposerStore.getContentState(this.props.room.roomId),
            ),

            // the original editor state, before we started tabbing through completions
            originalEditorState: null,

            // the virtual state "above" the history stack, the message currently being composed that
            // we want to persist whilst browsing history
            currentlyComposedEditorState: null,

            // whether there were any completions
            someCompletions: null,
        };

        this.client = MatrixClientPeg.get();
    }

    findLinkEntities(contentBlock, callback) {
        contentBlock.findEntityRanges(
            (character) => {
                const entityKey = character.getEntity();
                return (
                    entityKey !== null &&
                    Entity.get(entityKey).getType() === 'LINK'
                );
            }, callback,
        );
    }
    /*
     * "Does the right thing" to create an EditorState, based on:
     * - whether we've got rich text mode enabled
     * - contentState was passed in
     */
    createEditorState(richText: boolean, contentState: ?ContentState): EditorState {
        const decorators = richText ? RichText.getScopedRTDecorators(this.props) :
                RichText.getScopedMDDecorators(this.props);
        decorators.push({
            strategy: this.findLinkEntities.bind(this),
            component: (props) => {
                const {url} = Entity.get(props.entityKey).getData();
                return (
                    <a href={url}>
                        {props.children}<commit_msg>Decorate http://matrix.to links in the composer as pills<commit_after>/*
Copyright 2015, 2016 OpenMarket Ltd

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 type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent';

import {Editor, EditorState, RichUtils, CompositeDecorator, Modifier,
    getDefaultKeyBinding, KeyBindingUtil, ContentState, ContentBlock, SelectionState,
    Entity} from 'draft-js';

import classNames from 'classnames';
import escape from 'lodash/escape';
import Promise from 'bluebird';

import MatrixClientPeg from '../../../MatrixClientPeg';
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
import {RoomMember} from 'matrix-js-sdk';
import SlashCommands from '../../../SlashCommands';
import KeyCode from '../../../KeyCode';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';

import dis from '../../../dispatcher';
import UserSettingsStore from '../../../UserSettingsStore';

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 {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix';
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);

import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
const REGEX_EMOJI_WHITESPACE = new RegExp('(' + asciiRegexp + ')\\s$');

const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;

const ZWS_CODE = 8203;
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
function stateToMarkdown(state) {
    return __stateToMarkdown(state)
        .replace(
            ZWS, // draft-js-export-markdown adds these
            ''); // this is *not* a zero width space, trust me :)
}

function onSendMessageFailed(err, room) {
    // XXX: temporary logging to try to diagnose
    // https://github.com/vector-im/riot-web/issues/3148
    console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
    if (err.name === "UnknownDeviceError") {
        dis.dispatch({
            action: 'unknown_device_error',
            err: err,
            room: room,
        });
    }
    dis.dispatch({
        action: 'message_send_failed',
    });
}

/*
 * The textInput part of the MessageComposer
 */
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: React.PropTypes.func,

        // js-sdk Room object
        room: React.PropTypes.object.isRequired,

        // called with current plaintext content (as a string) whenever it changes
        onContentChanged: React.PropTypes.func,

        onInputStateChanged: React.PropTypes.func,
    };

    static getKeyBinding(e: SyntheticKeyboardEvent): string {
        // C-m => Toggles between rich text and markdown modes
        if (e.keyCode === KeyCode.KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
            return 'toggle-mode';
        }

        // Allow opening of dev tools. getDefaultKeyBinding would be 'italic' for KEY_I
        if (e.keyCode === KeyCode.KEY_I && e.shiftKey && e.ctrlKey) {
            // When null is returned, draft-js will NOT preventDefault, allowing dev tools
            // to be toggled when the editor is focussed
            return null;
        }

        return getDefaultKeyBinding(e);
    }

    static getBlockStyle(block: ContentBlock): ?string {
        if (block.getType() === 'strikethrough') {
            return 'mx_Markdown_STRIKETHROUGH';
        }

        return null;
    }

    client: MatrixClient;
    autocomplete: Autocomplete;
    historyManager: ComposerHistoryManager;

    constructor(props, context) {
        super(props, context);
        this.onAction = this.onAction.bind(this);
        this.handleReturn = this.handleReturn.bind(this);
        this.handleKeyCommand = this.handleKeyCommand.bind(this);
        this.onEditorContentChanged = this.onEditorContentChanged.bind(this);
        this.onUpArrow = this.onUpArrow.bind(this);
        this.onDownArrow = this.onDownArrow.bind(this);
        this.onTab = this.onTab.bind(this);
        this.onEscape = this.onEscape.bind(this);
        this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
        this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
        this.onTextPasted = this.onTextPasted.bind(this);

        const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);

        this.state = {
            // whether we're in rich text or markdown mode
            isRichtextEnabled,

            // the currently displayed editor state (note: this is always what is modified on input)
            editorState: this.createEditorState(
                isRichtextEnabled,
                MessageComposerStore.getContentState(this.props.room.roomId),
            ),

            // the original editor state, before we started tabbing through completions
            originalEditorState: null,

            // the virtual state "above" the history stack, the message currently being composed that
            // we want to persist whilst browsing history
            currentlyComposedEditorState: null,

            // whether there were any completions
            someCompletions: null,
        };

        this.client = MatrixClientPeg.get();
    }

    findLinkEntities(contentBlock, callback) {
        contentBlock.findEntityRanges(
            (character) => {
                const entityKey = character.getEntity();
                return (
                    entityKey !== null &&
                    Entity.get(entityKey).getType() === 'LINK'
                );
            }, callback,
        );
    }
    /*
     * "Does the right thing" to create an EditorState, based on:
     * - whether we've got rich text mode enabled
     * - contentState was passed in
     */
    createEditorState(richText: boolean, contentState: ?ContentState): EditorState {
        const decorators = richText ? RichText.getScopedRTDecorators(this.props) :
                RichText.getScopedMDDecorators(this.props);
        decorators.push({
            strategy: this.findLinkEntities.bind(this),
            component: (props) => {
                const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
                const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
                const {url} = Entity.get(props.entityKey).getData();
                const matrixToMatch = REGEX_MATRIXTO.exec(url);
                const isUserPill = matrixToMatch[2] === '@';
                const isRoomPill = matrixToMatch[2] === '#' || matrixToMatch[2] === '!';

                const classes = classNames({
                    "mx_UserPill": isUserPill,
                    "mx_RoomPill": isRoomPill,
                });

                let avatar = null;
                if (isUserPill) {
                    // If this user is not a member of this room, default to the empty
                    // member. This could be improved by doing an async profile lookup.
                    const member = this.props.room.getMember(matrixToMatch[1]) ||
                        new RoomMember(null, matrixToMatch[1]);
                    avatar = member ? <MemberAvatar member={member} width={16} height={16}/> : null;
                } else if (isRoomPill) {
                    const room = matrixToMatch[2] === '#' ?
                        MatrixClientPeg.get().getRooms().find((r) => {
                            return r.getCanonicalAlias() === matrixToMatch[1];
                        }) : MatrixClientPeg.get().getRoom(matrixToMatch[1]);
                    avatar = room ? <RoomAvatar room={room} width={16} height={16}/> : null;
                }

                if (isUserPill || isRoomPill) {
                    return (
                        <span className={classes}>
                            {avatar}
                            {props.children}
                        </span>
                    );
                }

                return (
                    <a href={url}>
                        {props.children}
{'fraction_non_alphanumeric': 0.1094302793705014, 'fraction_numerical': 0.004635842381359034, 'mean_word_length': 2.4249007729266765, 'pattern_counts': {'":': 2, '<': 9, '<?xml version=': 0, '>': 16, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6339664', 'n_tokens_mistral': 4520, 'n_tokens_neox': 4375, 'n_words': 1219}