emojioneify the composer
and also fix up the selectedness CSS for pills and emojipull/21833/head
parent
b10f9a9cb7
commit
c1000a7cd5
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
change = change[`collapseToEndOf${ this.direction }Text`]();
|
||||
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()
|
||||
.insertInlineAtRange(editorState.selection, inline)
|
||||
.insertText(suffix);
|
||||
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 }/>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue