emojioneify the composer

and also fix up the selectedness CSS for pills and emoji
pull/21833/head
Matthew Hodgson 2018-05-14 03:02:12 +01:00
parent b10f9a9cb7
commit c1000a7cd5
6 changed files with 88 additions and 98 deletions

View File

@ -291,6 +291,10 @@ textarea {
vertical-align: middle;
}
.mx_emojione_selected {
background-color: $accent-color;
}
::-moz-selection {
background-color: $accent-color;
color: $selection-fg-color;

View File

@ -25,6 +25,10 @@
padding-right: 5px;
}
.mx_UserPill_selected {
background-color: $accent-color ! important;
}
.mx_EventTile_highlight .mx_EventTile_content .markdown-body a.mx_UserPill_me,
.mx_EventTile_content .mx_AtRoomPill,
.mx_MessageComposer_input .mx_AtRoomPill {

View File

@ -51,10 +51,6 @@ const MARKDOWN_REGEX = {
STRIKETHROUGH: /~{2}[^~]*~{2}/g,
};
const USERNAME_REGEX = /@\S+:\S+/g;
const ROOM_REGEX = /#\S+:\S+/g;
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g');
const ZWS_CODE = 8203;
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
@ -73,7 +69,7 @@ export function htmlToEditorState(html: string): Value {
return Html.serialize(html);
}
function unicodeToEmojiUri(str) {
export 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
@ -113,27 +109,6 @@ function findWithRegex(regex, contentBlock: ContentBlock, callback: (start: numb
}
}
// Workaround for https://github.com/facebook/draft-js/issues/414
const emojiDecorator = {
strategy: (contentState, contentBlock, callback) => {
findWithRegex(EMOJI_REGEX, contentBlock, callback);
},
component: (props) => {
const uri = unicodeToEmojiUri(props.children[0].props.text);
const shortname = emojione.toShort(props.children[0].props.text);
const 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.
*/
@ -223,60 +198,6 @@ export function selectionStateToTextOffsets(selectionState: 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 = newContentState.getEntity(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);
newContentState = newContentState.createEntity(
'emoji', 'IMMUTABLE', { emojiUnicode: emojiText },
);
const entityKey = newContentState.getLastCreatedEntityKey();
newContentState = Modifier.replaceText(
newContentState,
selection,
emojiText,
null,
entityKey,
);
};
findWithRegex(EMOJI_REGEX, block, addEntityToEmoji);
});
if (!newContentState.equals(contentState)) {
const oldSelection = editorState.getSelection();
editorState = EditorState.push(
editorState,
newContentState,
'convert-to-immutable-emojis',
);
// this is somewhat of a hack, we're undoing selection changes caused above
// it would be better not to make those changes in the first place
editorState = EditorState.forceSelection(editorState, oldSelection);
}
return editorState;
}
export function hasMultiLineSelection(editorState: EditorState): boolean {
const selectionState = editorState.getSelection();
const anchorKey = selectionState.getAnchorKey();

View File

@ -61,6 +61,9 @@ class PlainWithPillsSerializer {
(node.object == 'block' && Block.isBlockList(node.nodes))
) {
return node.nodes.map(this._serializeNode).join('\n');
}
else if (node.type == 'emoji') {
return node.data.get('emojiUnicode');
} else if (node.type == 'pill') {
switch (this.pillFormat) {
case 'plain':

View File

@ -59,6 +59,8 @@ const Pill = React.createClass({
room: PropTypes.instanceOf(Room),
// Whether to include an avatar in the pill
shouldShowPillAvatar: PropTypes.bool,
// Whether to render this pill as if it were highlit by a selection
isSelected: PropTypes.bool,
},
@ -233,6 +235,7 @@ const Pill = React.createClass({
const classes = classNames(pillClass, {
"mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId,
"mx_UserPill_selected": this.props.isSelected,
});
if (this.state.pillType) {

View File

@ -58,7 +58,7 @@ import {MATRIXTO_URL_PATTERN, MATRIXTO_MD_LINK_PATTERN} from '../../../linkify-m
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g');
import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
import {asciiRegexp, unicodeRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort, toShort} from 'emojione';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import {makeUserPermalink} from "../../../matrix-to";
import ReplyPreview from "./ReplyPreview";
@ -69,6 +69,7 @@ import {ContentHelpers} from 'matrix-js-sdk';
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
const EMOJI_REGEX = new RegExp(unicodeRegexp, 'g');
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
@ -76,6 +77,7 @@ const ENTITY_TYPES = {
AT_ROOM_PILL: 'ATROOMPILL',
};
function onSendMessageFailed(err, room) {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
@ -351,12 +353,20 @@ export default class MessageComposerInput extends React.Component {
if (this.direction !== '') {
const focusedNode = editorState.focusInline || editorState.focusText;
if (focusedNode.isVoid) {
if (editorState.isCollapsed) {
change = change[`collapseToEndOf${ this.direction }Text`]();
}
else {
const block = this.direction === 'Previous' ? editorState.previousText : editorState.nextText;
if (block) {
change = change.moveFocusToEndOf(block)
}
}
editorState = change.value;
}
}
if (editorState.document.getFirstText().text !== '') {
if (!editorState.document.isEmpty) {
this.onTypingActivity();
} else {
this.onFinishedTyping();
@ -369,9 +379,33 @@ export default class MessageComposerInput extends React.Component {
}
*/
/*
editorState = RichText.attachImmutableEntitiesToEmoji(editorState);
// emojioneify any emoji
// deliberately lose any inlines and pills via Plain.serialize as we know
// they won't contain emoji
// XXX: is getTextsAsArray a private API?
editorState.document.getTextsAsArray().forEach(node => {
if (node.text !== '' && HtmlUtils.containsEmoji(node.text)) {
let match;
while ((match = EMOJI_REGEX.exec(node.text)) !== null) {
const range = Range.create({
anchorKey: node.key,
anchorOffset: match.index,
focusKey: node.key,
focusOffset: match.index + match[0].length,
});
const inline = Inline.create({
type: 'emoji',
data: { emojiUnicode: match[0] },
isVoid: true,
});
change = change.insertInlineAtRange(range, inline);
editorState = change.value;
}
}
});
/*
const currentBlock = editorState.getSelection().getStartKey();
const currentSelection = editorState.getSelection();
const currentStartOffset = editorState.getSelection().getStartOffset();
@ -400,7 +434,6 @@ export default class MessageComposerInput extends React.Component {
editorState = EditorState.forceSelection(editorState, currentSelection);
}
*/
const text = editorState.startText.text;
const currentStartOffset = editorState.startOffset;
@ -912,8 +945,12 @@ export default class MessageComposerInput extends React.Component {
// Move selection to the end of the selected history
const change = editorState.change().collapseToEndOf(editorState.document);
// XXX: should we be calling this.onChange(change) now?
// we skip it for now given we know we're about to setState anyway
// Answer: yes, if we want it to do any of the fixups on stuff like emoji.
// however, this should already have been done and persisted in the history,
// so shouldn't be necessary.
editorState = change.value;
this.suppressAutoComplete = true;
@ -991,11 +1028,6 @@ export default class MessageComposerInput extends React.Component {
// we can't put text in here otherwise the editor tries to select it
isVoid: true,
});
} else {
inline = Inline.create({
type: 'autocompletion',
nodes: [Text.create(completion)]
});
}
let editorState = activeEditorState;
@ -1007,13 +1039,23 @@ export default class MessageComposerInput extends React.Component {
editorState = change.value;
}
const change = editorState.change()
let change;
if (inline) {
change = editorState.change()
.insertInlineAtRange(editorState.selection, inline)
.insertText(suffix);
}
else {
change = editorState.change()
.insertTextAtRange(editorState.selection, completion)
.insertText(suffix);
}
editorState = change.value;
this.setState({ editorState, originalEditorState: activeEditorState }, ()=>{
// this.refs.editor.focus();
this.onChange(change);
this.setState({
originalEditorState: activeEditorState
});
return true;
@ -1027,7 +1069,7 @@ export default class MessageComposerInput extends React.Component {
return <p {...attributes}>{children}</p>
}
case 'pill': {
const { data, text } = node;
const { data } = node;
const url = data.get('url');
const completion = data.get('completion');
@ -1039,6 +1081,7 @@ export default class MessageComposerInput extends React.Component {
type={Pill.TYPE_AT_ROOM_MENTION}
room={this.props.room}
shouldShowPillAvatar={shouldShowPillAvatar}
isSelected={isSelected}
/>;
}
else if (Pill.isPillUrl(url)) {
@ -1046,14 +1089,26 @@ export default class MessageComposerInput extends React.Component {
url={url}
room={this.props.room}
shouldShowPillAvatar={shouldShowPillAvatar}
isSelected={isSelected}
/>;
}
else {
const { text } = node;
return <a href={url} {...props.attributes}>
{ text }
</a>;
}
}
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
});
return <img className={ className } src={ uri } title={ shortname } alt={ emojiUnicode }/>;
}
}
};