Merge pull request #335 from aviraldg/feature-composer-emoji
feat: code cleanup & emoji replacement in composer
This commit is contained in:
commit
1b39f02141
2 changed files with 135 additions and 48 deletions
112
src/RichText.js
112
src/RichText.js
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Editor,
|
||||
EditorState,
|
||||
Modifier,
|
||||
ContentState,
|
||||
ContentBlock,
|
||||
|
@ -9,12 +10,13 @@ import {
|
|||
DefaultDraftInlineStyle,
|
||||
CompositeDecorator,
|
||||
SelectionState,
|
||||
Entity,
|
||||
} from 'draft-js';
|
||||
import * as sdk from './index';
|
||||
import * as emojione from 'emojione';
|
||||
|
||||
const BLOCK_RENDER_MAP = DefaultDraftBlockRenderMap.set('unstyled', {
|
||||
element: 'span'
|
||||
element: 'span',
|
||||
/*
|
||||
draft uses <div> by default which we don't really like, so we're using <span>
|
||||
this is probably not a good idea since <span> is not a block level element but
|
||||
|
@ -65,7 +67,7 @@ export function contentStateToHTML(contentState: ContentState): string {
|
|||
let result = `<${elem}>${content.join('')}</${elem}>`;
|
||||
|
||||
// dirty hack because we don't want block level tags by default, but breaks
|
||||
if(elem === 'span')
|
||||
if (elem === 'span')
|
||||
result += '<br />';
|
||||
return result;
|
||||
}).join('');
|
||||
|
@ -75,6 +77,48 @@ export function HTMLtoContentState(html: string): ContentState {
|
|||
return ContentState.createFromBlockArray(convertFromHTML(html));
|
||||
}
|
||||
|
||||
function unicodeToEmojiUri(str) {
|
||||
let replaceWith, unicode, alt;
|
||||
if ((!emojione.unicodeAlt) || (emojione.sprites)) {
|
||||
// if we are using the shortname as the alt tag then we need a reversed array to map unicode code point to shortnames
|
||||
let mappedUnicode = emojione.mapUnicodeToShort();
|
||||
}
|
||||
|
||||
str = str.replace(emojione.regUnicode, function(unicodeChar) {
|
||||
if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) {
|
||||
// if the unicodeChar doesnt exist just return the entire match
|
||||
return unicodeChar;
|
||||
} else {
|
||||
// get the unicode codepoint from the actual char
|
||||
unicode = emojione.jsEscapeMap[unicodeChar];
|
||||
return emojione.imagePathSVG+unicode+'.svg'+emojione.cacheBustParam;
|
||||
}
|
||||
});
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
// Workaround for https://github.com/facebook/draft-js/issues/414
|
||||
let emojiDecorator = {
|
||||
strategy: (contentBlock, callback) => {
|
||||
findWithRegex(EMOJI_REGEX, contentBlock, callback);
|
||||
},
|
||||
component: (props) => {
|
||||
let uri = unicodeToEmojiUri(props.children[0].props.text);
|
||||
let shortname = emojione.toShort(props.children[0].props.text);
|
||||
let style = {
|
||||
display: 'inline-block',
|
||||
width: '1em',
|
||||
maxHeight: '1em',
|
||||
background: `url(${uri})`,
|
||||
backgroundSize: 'contain',
|
||||
backgroundPosition: 'center center',
|
||||
overflow: 'hidden',
|
||||
};
|
||||
return (<span title={shortname} style={style}><span style={{opacity: 0}}>{props.children}</span></span>);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a composite decorator which has access to provided scope.
|
||||
*/
|
||||
|
@ -90,7 +134,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
|
|||
// unused until we make these decorators immutable (autocomplete needed)
|
||||
let name = member ? member.name : null;
|
||||
let avatar = member ? <MemberAvatar member={member} width={16} height={16}/> : null;
|
||||
return <span className="mx_UserPill">{avatar} {props.children}</span>;
|
||||
return <span className="mx_UserPill">{avatar}{props.children}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -103,17 +147,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
|
|||
}
|
||||
};
|
||||
|
||||
// Unused for now, due to https://github.com/facebook/draft-js/issues/414
|
||||
let emojiDecorator = {
|
||||
strategy: (contentBlock, callback) => {
|
||||
findWithRegex(EMOJI_REGEX, contentBlock, callback);
|
||||
},
|
||||
component: (props) => {
|
||||
return <span dangerouslySetInnerHTML={{__html: ' ' + emojione.unicodeToImage(props.children[0].props.text)}}/>
|
||||
}
|
||||
};
|
||||
|
||||
return [usernameDecorator, roomDecorator];
|
||||
return [usernameDecorator, roomDecorator, emojiDecorator];
|
||||
}
|
||||
|
||||
export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
||||
|
@ -139,6 +173,7 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
|||
</a>
|
||||
)
|
||||
});
|
||||
markdownDecorators.push(emojiDecorator);
|
||||
|
||||
return markdownDecorators;
|
||||
}
|
||||
|
@ -193,7 +228,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
|
|||
export function selectionStateToTextOffsets(selectionState: SelectionState,
|
||||
contentBlocks: Array<ContentBlock>): {start: number, end: number} {
|
||||
let offset = 0, start = 0, end = 0;
|
||||
for(let block of contentBlocks) {
|
||||
for (let block of contentBlocks) {
|
||||
if (selectionState.getStartKey() === block.getKey()) {
|
||||
start = offset + selectionState.getStartOffset();
|
||||
}
|
||||
|
@ -240,3 +275,50 @@ export function textOffsetsToSelectionState({start, end}: {start: number, end: n
|
|||
|
||||
return selectionState;
|
||||
}
|
||||
|
||||
// modified version of https://github.com/draft-js-plugins/draft-js-plugins/blob/master/draft-js-emoji-plugin/src/modifiers/attachImmutableEntitiesToEmojis.js
|
||||
export function attachImmutableEntitiesToEmoji(editorState: EditorState): EditorState {
|
||||
const contentState = editorState.getCurrentContent();
|
||||
const blocks = contentState.getBlockMap();
|
||||
let newContentState = contentState;
|
||||
|
||||
blocks.forEach((block) => {
|
||||
const plainText = block.getText();
|
||||
|
||||
const addEntityToEmoji = (start, end) => {
|
||||
const existingEntityKey = block.getEntityAt(start);
|
||||
if (existingEntityKey) {
|
||||
// avoid manipulation in case the emoji already has an entity
|
||||
const entity = Entity.get(existingEntityKey);
|
||||
if (entity && entity.get('type') === 'emoji') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const selection = SelectionState.createEmpty(block.getKey())
|
||||
.set('anchorOffset', start)
|
||||
.set('focusOffset', end);
|
||||
const emojiText = plainText.substring(start, end);
|
||||
const entityKey = Entity.create('emoji', 'IMMUTABLE', { emojiUnicode: emojiText });
|
||||
newContentState = Modifier.replaceText(
|
||||
newContentState,
|
||||
selection,
|
||||
emojiText,
|
||||
null,
|
||||
entityKey,
|
||||
);
|
||||
};
|
||||
|
||||
findWithRegex(EMOJI_REGEX, block, addEntityToEmoji);
|
||||
});
|
||||
|
||||
if (!newContentState.equals(contentState)) {
|
||||
return EditorState.push(
|
||||
editorState,
|
||||
newContentState,
|
||||
'convert-to-immutable-emojis',
|
||||
);
|
||||
}
|
||||
|
||||
return editorState;
|
||||
}
|
||||
|
|
|
@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
var marked = require("marked");
|
||||
import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent';
|
||||
import marked from 'marked';
|
||||
marked.setOptions({
|
||||
renderer: new marked.Renderer(),
|
||||
gfm: true,
|
||||
|
@ -24,7 +24,7 @@ marked.setOptions({
|
|||
pedantic: false,
|
||||
sanitize: true,
|
||||
smartLists: true,
|
||||
smartypants: false
|
||||
smartypants: false,
|
||||
});
|
||||
|
||||
import {Editor, EditorState, RichUtils, CompositeDecorator,
|
||||
|
@ -33,14 +33,14 @@ import {Editor, EditorState, RichUtils, CompositeDecorator,
|
|||
|
||||
import {stateToMarkdown} from 'draft-js-export-markdown';
|
||||
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var SlashCommands = require("../../../SlashCommands");
|
||||
var Modal = require("../../../Modal");
|
||||
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
|
||||
var sdk = require('../../../index');
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
||||
import SlashCommands from '../../../SlashCommands';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
|
||||
var dis = require("../../../dispatcher");
|
||||
var KeyCode = require("../../../KeyCode");
|
||||
import dis from '../../../dispatcher';
|
||||
import KeyCode from '../../../KeyCode';
|
||||
|
||||
import * as RichText from '../../../RichText';
|
||||
|
||||
|
@ -49,8 +49,8 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
|||
const KEY_M = 77;
|
||||
|
||||
// FIXME Breaks markdown with multiple paragraphs, since it only strips first and last <p>
|
||||
function mdownToHtml(mdown) {
|
||||
var html = marked(mdown) || "";
|
||||
function mdownToHtml(mdown: string): string {
|
||||
let html = marked(mdown) || "";
|
||||
html = html.trim();
|
||||
// strip start and end <p> tags else you get 'orrible spacing
|
||||
if (html.indexOf("<p>") === 0) {
|
||||
|
@ -66,6 +66,17 @@ function mdownToHtml(mdown) {
|
|||
* The textInput part of the MessageComposer
|
||||
*/
|
||||
export default class MessageComposerInput extends React.Component {
|
||||
static getKeyBinding(e: SyntheticKeyboardEvent): string {
|
||||
// C-m => Toggles between rich text and markdown modes
|
||||
if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
|
||||
return 'toggle-mode';
|
||||
}
|
||||
|
||||
return getDefaultKeyBinding(e);
|
||||
}
|
||||
|
||||
client: MatrixClient;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onAction = this.onAction.bind(this);
|
||||
|
@ -79,7 +90,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this);
|
||||
|
||||
let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled');
|
||||
if(isRichtextEnabled == null) {
|
||||
if (isRichtextEnabled == null) {
|
||||
isRichtextEnabled = 'true';
|
||||
}
|
||||
isRichtextEnabled = isRichtextEnabled === 'true';
|
||||
|
@ -95,15 +106,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
this.client = MatrixClientPeg.get();
|
||||
}
|
||||
|
||||
static getKeyBinding(e: SyntheticKeyboardEvent): string {
|
||||
// C-m => Toggles between rich text and markdown modes
|
||||
if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
|
||||
return 'toggle-mode';
|
||||
}
|
||||
|
||||
return getDefaultKeyBinding(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* "Does the right thing" to create an EditorState, based on:
|
||||
* - whether we've got rich text mode enabled
|
||||
|
@ -347,15 +349,16 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
setEditorState(editorState: EditorState) {
|
||||
editorState = RichText.attachImmutableEntitiesToEmoji(editorState);
|
||||
this.setState({editorState});
|
||||
|
||||
if(editorState.getCurrentContent().hasText()) {
|
||||
this.onTypingActivity()
|
||||
if (editorState.getCurrentContent().hasText()) {
|
||||
this.onTypingActivity();
|
||||
} else {
|
||||
this.onFinishedTyping();
|
||||
}
|
||||
|
||||
if(this.props.onContentChanged) {
|
||||
if (this.props.onContentChanged) {
|
||||
this.props.onContentChanged(editorState.getCurrentContent().getPlainText(),
|
||||
RichText.selectionStateToTextOffsets(editorState.getSelection(),
|
||||
editorState.getCurrentContent().getBlocksAsArray()));
|
||||
|
@ -380,7 +383,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
handleKeyCommand(command: string): boolean {
|
||||
if(command === 'toggle-mode') {
|
||||
if (command === 'toggle-mode') {
|
||||
this.enableRichtext(!this.state.isRichtextEnabled);
|
||||
return true;
|
||||
}
|
||||
|
@ -388,7 +391,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
let newState: ?EditorState = null;
|
||||
|
||||
// Draft handles rich text mode commands by default but we need to do it ourselves for Markdown.
|
||||
if(!this.state.isRichtextEnabled) {
|
||||
if (!this.state.isRichtextEnabled) {
|
||||
let contentState = this.state.editorState.getCurrentContent(),
|
||||
selection = this.state.editorState.getSelection();
|
||||
|
||||
|
@ -396,10 +399,10 @@ export default class MessageComposerInput extends React.Component {
|
|||
bold: text => `**${text}**`,
|
||||
italic: text => `*${text}*`,
|
||||
underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
|
||||
code: text => `\`${text}\``
|
||||
code: text => `\`${text}\``,
|
||||
}[command];
|
||||
|
||||
if(modifyFn) {
|
||||
if (modifyFn) {
|
||||
newState = EditorState.push(
|
||||
this.state.editorState,
|
||||
RichText.modifyText(contentState, selection, modifyFn),
|
||||
|
@ -408,7 +411,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
if(newState == null)
|
||||
if (newState == null)
|
||||
newState = RichUtils.handleKeyCommand(this.state.editorState, command);
|
||||
|
||||
if (newState != null) {
|
||||
|
@ -523,9 +526,11 @@ export default class MessageComposerInput extends React.Component {
|
|||
content
|
||||
);
|
||||
|
||||
this.setState({
|
||||
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'),
|
||||
});
|
||||
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
||||
|
||||
editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
|
||||
|
||||
this.setEditorState(editorState);
|
||||
|
||||
// for some reason, doing this right away does not update the editor :(
|
||||
setTimeout(() => this.refs.editor.focus(), 50);
|
||||
|
|
Loading…
Reference in a new issue