Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit b334522

Browse files
committed
feat: code cleanup & emoji replacement in composer
1 parent a2b6479 commit b334522

2 files changed

Lines changed: 135 additions & 48 deletions

File tree

‎src/RichText.js‎

Lines changed: 97 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import {
33
Editor,
4+
EditorState,
45
Modifier,
56
ContentState,
67
ContentBlock,
@@ -9,12 +10,13 @@ import {
910
DefaultDraftInlineStyle,
1011
CompositeDecorator,
1112
SelectionState,
13+
Entity,
1214
} from 'draft-js';
1315
import * as sdk from './index';
1416
import * as emojione from 'emojione';
1517

1618
const BLOCK_RENDER_MAP = DefaultDraftBlockRenderMap.set('unstyled', {
17-
element: 'span'
19+
element: 'span',
1820
/*
1921
draft uses <div> by default which we don't really like, so we're using <span>
2022
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 {
6567
let result = `<${elem}>${content.join('')}</${elem}>`;
6668

6769
// dirty hack because we don't want block level tags by default, but breaks
68-
if(elem === 'span')
70+
if (elem === 'span')
6971
result += '<br />';
7072
return result;
7173
}).join('');
@@ -75,6 +77,48 @@ export function HTMLtoContentState(html: string): ContentState {
7577
return ContentState.createFromBlockArray(convertFromHTML(html));
7678
}
7779

80+
function unicodeToEmojiUri(str) {
81+
let replaceWith, unicode, alt;
82+
if ((!emojione.unicodeAlt) || (emojione.sprites)) {
83+
// if we are using the shortname as the alt tag then we need a reversed array to map unicode code point to shortnames
84+
let mappedUnicode = emojione.mapUnicodeToShort();
85+
}
86+
87+
str = str.replace(emojione.regUnicode, function(unicodeChar) {
88+
if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) {
89+
// if the unicodeChar doesnt exist just return the entire match
90+
return unicodeChar;
91+
} else {
92+
// get the unicode codepoint from the actual char
93+
unicode = emojione.jsEscapeMap[unicodeChar];
94+
return emojione.imagePathSVG+unicode+'.svg'+emojione.cacheBustParam;
95+
}
96+
});
97+
98+
return str;
99+
}
100+
101+
// Unused for now, due to https://github.com/facebook/draft-js/issues/414
102+
let emojiDecorator = {
103+
strategy: (contentBlock, callback) => {
104+
findWithRegex(EMOJI_REGEX, contentBlock, callback);
105+
},
106+
component: (props) => {
107+
let uri = unicodeToEmojiUri(props.children[0].props.text);
108+
let shortname = emojione.toShort(props.children[0].props.text);
109+
let style = {
110+
display: 'inline-block',
111+
width: '1em',
112+
maxHeight: '1em',
113+
background: `url(${uri})`,
114+
backgroundSize: 'contain',
115+
backgroundPosition: 'center center',
116+
overflow: 'hidden',
117+
};
118+
return (<span title={shortname} style={style}><span style={{color: 'transparent'}}>{props.children}</span></span>);
119+
},
120+
};
121+
78122
/**
79123
* Returns a composite decorator which has access to provided scope.
80124
*/
@@ -90,7 +134,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
90134
// unused until we make these decorators immutable (autocomplete needed)
91135
let name = member ? member.name : null;
92136
let avatar = member ? <MemberAvatar member={member} width={16} height={16}/> : null;
93-
return <span className="mx_UserPill">{avatar} {props.children}</span>;
137+
return <span className="mx_UserPill">{avatar}{props.children}</span>;
94138
}
95139
};
96140

@@ -103,17 +147,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
103147
}
104148
};
105149

106-
// Unused for now, due to https://github.com/facebook/draft-js/issues/414
107-
let emojiDecorator = {
108-
strategy: (contentBlock, callback) => {
109-
findWithRegex(EMOJI_REGEX, contentBlock, callback);
110-
},
111-
component: (props) => {
112-
return <span dangerouslySetInnerHTML={{__html: ' ' + emojione.unicodeToImage(props.children[0].props.text)}}/>
113-
}
114-
};
115-
116-
return [usernameDecorator, roomDecorator];
150+
return [usernameDecorator, roomDecorator, emojiDecorator];
117151
}
118152

119153
export function getScopedMDDecorators(scope: any): CompositeDecorator {
@@ -139,6 +173,7 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator {
139173
</a>
140174
)
141175
});
176+
markdownDecorators.push(emojiDecorator);
142177

143178
return markdownDecorators;
144179
}
@@ -193,7 +228,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
193228
export function selectionStateToTextOffsets(selectionState: SelectionState,
194229
contentBlocks: Array<ContentBlock>): {start: number, end: number} {
195230
let offset = 0, start = 0, end = 0;
196-
for(let block of contentBlocks) {
231+
for (let block of contentBlocks) {
197232
if (selectionState.getStartKey() === block.getKey()) {
198233
start = offset + selectionState.getStartOffset();
199234
}
@@ -240,3 +275,50 @@ export function textOffsetsToSelectionState({start, end}: {start: number, end: n
240275

241276
return selectionState;
242277
}
278+
279+
// modified version of https://github.com/draft-js-plugins/draft-js-plugins/blob/master/draft-js-emoji-plugin/src/modifiers/attachImmutableEntitiesToEmojis.js
280+
export function attachImmutableEntitiesToEmoji(editorState: EditorState): EditorState {
281+
const contentState = editorState.getCurrentContent();
282+
const blocks = contentState.getBlockMap();
283+
let newContentState = contentState;
284+
285+
blocks.forEach((block) => {
286+
const plainText = block.getText();
287+
288+
const addEntityToEmoji = (start, end) => {
289+
const existingEntityKey = block.getEntityAt(start);
290+
if (existingEntityKey) {
291+
// avoid manipulation in case the emoji already has an entity
292+
const entity = Entity.get(existingEntityKey);
293+
if (entity && entity.get('type') === 'emoji') {
294+
return;
295+
}
296+
}
297+
298+
const selection = SelectionState.createEmpty(block.getKey())
299+
.set('anchorOffset', start)
300+
.set('focusOffset', end);
301+
const emojiText = plainText.substring(start, end);
302+
const entityKey = Entity.create('emoji', 'IMMUTABLE', { emojiUnicode: emojiText });
303+
newContentState = Modifier.replaceText(
304+
newContentState,
305+
selection,
306+
emojiText,
307+
null,
308+
entityKey,
309+
);
310+
};
311+
312+
findWithRegex(EMOJI_REGEX, block, addEntityToEmoji);
313+
});
314+
315+
if (!newContentState.equals(contentState)) {
316+
return EditorState.push(
317+
editorState,
318+
newContentState,
319+
'convert-to-immutable-emojis',
320+
);
321+
}
322+
323+
return editorState;
324+
}

‎src/components/views/rooms/MessageComposerInput.js‎

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616
import React from 'react';
17-
18-
var marked = require("marked");
17+
import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent';
18+
import marked from 'marked';
1919
marked.setOptions({
2020
renderer: new marked.Renderer(),
2121
gfm: true,
@@ -24,7 +24,7 @@ marked.setOptions({
2424
pedantic: false,
2525
sanitize: true,
2626
smartLists: true,
27-
smartypants: false
27+
smartypants: false,
2828
});
2929

3030
import {Editor, EditorState, RichUtils, CompositeDecorator,
@@ -33,14 +33,14 @@ import {Editor, EditorState, RichUtils, CompositeDecorator,
3333

3434
import {stateToMarkdown} from 'draft-js-export-markdown';
3535

36-
var MatrixClientPeg = require("../../../MatrixClientPeg");
37-
var SlashCommands = require("../../../SlashCommands");
38-
var Modal = require("../../../Modal");
39-
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
40-
var sdk = require('../../../index');
36+
import MatrixClientPeg from '../../../MatrixClientPeg';
37+
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
38+
import SlashCommands from '../../../SlashCommands';
39+
import Modal from '../../../Modal';
40+
import sdk from '../../../index';
4141

42-
var dis = require("../../../dispatcher");
43-
var KeyCode = require("../../../KeyCode");
42+
import dis from '../../../dispatcher';
43+
import KeyCode from '../../../KeyCode';
4444

4545
import * as RichText from '../../../RichText';
4646

@@ -49,8 +49,8 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
4949
const KEY_M = 77;
5050

5151
// FIXME Breaks markdown with multiple paragraphs, since it only strips first and last <p>
52-
function mdownToHtml(mdown) {
53-
var html = marked(mdown) || "";
52+
function mdownToHtml(mdown: string): string {
53+
let html = marked(mdown) || "";
5454
html = html.trim();
5555
// strip start and end <p> tags else you get 'orrible spacing
5656
if (html.indexOf("<p>") === 0) {
@@ -66,6 +66,17 @@ function mdownToHtml(mdown) {
6666
* The textInput part of the MessageComposer
6767
*/
6868
export default class MessageComposerInput extends React.Component {
69+
static getKeyBinding(e: SyntheticKeyboardEvent): string {
70+
// C-m => Toggles between rich text and markdown modes
71+
if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
72+
return 'toggle-mode';
73+
}
74+
75+
return getDefaultKeyBinding(e);
76+
}
77+
78+
client: MatrixClient;
79+
6980
constructor(props, context) {
7081
super(props, context);
7182
this.onAction = this.onAction.bind(this);
@@ -79,7 +90,7 @@ export default class MessageComposerInput extends React.Component {
7990
this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this);
8091

8192
let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled');
82-
if(isRichtextEnabled == null) {
93+
if (isRichtextEnabled == null) {
8394
isRichtextEnabled = 'true';
8495
}
8596
isRichtextEnabled = isRichtextEnabled === 'true';
@@ -95,15 +106,6 @@ export default class MessageComposerInput extends React.Component {
95106
this.client = MatrixClientPeg.get();
96107
}
97108

98-
static getKeyBinding(e: SyntheticKeyboardEvent): string {
99-
// C-m => Toggles between rich text and markdown modes
100-
if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
101-
return 'toggle-mode';
102-
}
103-
104-
return getDefaultKeyBinding(e);
105-
}
106-
107109
/**
108110
* "Does the right thing" to create an EditorState, based on:
109111
* - whether we've got rich text mode enabled
@@ -347,15 +349,16 @@ export default class MessageComposerInput extends React.Component {
347349
}
348350

349351
setEditorState(editorState: EditorState) {
352+
editorState = RichText.attachImmutableEntitiesToEmoji(editorState);
350353
this.setState({editorState});
351354

352-
if(editorState.getCurrentContent().hasText()) {
353-
this.onTypingActivity()
355+
if (editorState.getCurrentContent().hasText()) {
356+
this.onTypingActivity();
354357
} else {
355358
this.onFinishedTyping();
356359
}
357360

358-
if(this.props.onContentChanged) {
361+
if (this.props.onContentChanged) {
359362
this.props.onContentChanged(editorState.getCurrentContent().getPlainText(),
360363
RichText.selectionStateToTextOffsets(editorState.getSelection(),
361364
editorState.getCurrentContent().getBlocksAsArray()));
@@ -380,26 +383,26 @@ export default class MessageComposerInput extends React.Component {
380383
}
381384

382385
handleKeyCommand(command: string): boolean {
383-
if(command === 'toggle-mode') {
386+
if (command === 'toggle-mode') {
384387
this.enableRichtext(!this.state.isRichtextEnabled);
385388
return true;
386389
}
387390

388391
let newState: ?EditorState = null;
389392

390393
// Draft handles rich text mode commands by default but we need to do it ourselves for Markdown.
391-
if(!this.state.isRichtextEnabled) {
394+
if (!this.state.isRichtextEnabled) {
392395
let contentState = this.state.editorState.getCurrentContent(),
393396
selection = this.state.editorState.getSelection();
394397

395398
let modifyFn = {
396399
bold: text => `**${text}**`,
397400
italic: text => `*${text}*`,
398401
underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
399-
code: text => `\`${text}\``
402+
code: text => `\`${text}\``,
400403
}[command];
401404

402-
if(modifyFn) {
405+
if (modifyFn) {
403406
newState = EditorState.push(
404407
this.state.editorState,
405408
RichText.modifyText(contentState, selection, modifyFn),
@@ -408,7 +411,7 @@ export default class MessageComposerInput extends React.Component {
408411
}
409412
}
410413

411-
if(newState == null)
414+
if (newState == null)
412415
newState = RichUtils.handleKeyCommand(this.state.editorState, command);
413416

414417
if (newState != null) {
@@ -533,9 +536,11 @@ export default class MessageComposerInput extends React.Component {
533536
content
534537
);
535538

536-
this.setState({
537-
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'),
538-
});
539+
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
540+
541+
editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
542+
543+
this.setEditorState(editorState);
539544

540545
// for some reason, doing this right away does not update the editor :(
541546
setTimeout(() => this.refs.editor.focus(), 50);

0 commit comments

Comments
 (0)