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;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_emojione_selected {
|
||||||
|
background-color: $accent-color;
|
||||||
|
}
|
||||||
|
|
||||||
::-moz-selection {
|
::-moz-selection {
|
||||||
background-color: $accent-color;
|
background-color: $accent-color;
|
||||||
color: $selection-fg-color;
|
color: $selection-fg-color;
|
||||||
|
|
|
@ -25,6 +25,10 @@
|
||||||
padding-right: 5px;
|
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_highlight .mx_EventTile_content .markdown-body a.mx_UserPill_me,
|
||||||
.mx_EventTile_content .mx_AtRoomPill,
|
.mx_EventTile_content .mx_AtRoomPill,
|
||||||
.mx_MessageComposer_input .mx_AtRoomPill {
|
.mx_MessageComposer_input .mx_AtRoomPill {
|
||||||
|
|
|
@ -51,10 +51,6 @@ const MARKDOWN_REGEX = {
|
||||||
STRIKETHROUGH: /~{2}[^~]*~{2}/g,
|
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_CODE = 8203;
|
||||||
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
|
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
|
||||||
|
|
||||||
|
@ -73,7 +69,7 @@ export function htmlToEditorState(html: string): Value {
|
||||||
return Html.serialize(html);
|
return Html.serialize(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
function unicodeToEmojiUri(str) {
|
export function unicodeToEmojiUri(str) {
|
||||||
let replaceWith, unicode, alt;
|
let replaceWith, unicode, alt;
|
||||||
if ((!emojione.unicodeAlt) || (emojione.sprites)) {
|
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
|
// 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.
|
* 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 {
|
export function hasMultiLineSelection(editorState: EditorState): boolean {
|
||||||
const selectionState = editorState.getSelection();
|
const selectionState = editorState.getSelection();
|
||||||
const anchorKey = selectionState.getAnchorKey();
|
const anchorKey = selectionState.getAnchorKey();
|
||||||
|
|
|
@ -61,6 +61,9 @@ class PlainWithPillsSerializer {
|
||||||
(node.object == 'block' && Block.isBlockList(node.nodes))
|
(node.object == 'block' && Block.isBlockList(node.nodes))
|
||||||
) {
|
) {
|
||||||
return node.nodes.map(this._serializeNode).join('\n');
|
return node.nodes.map(this._serializeNode).join('\n');
|
||||||
|
}
|
||||||
|
else if (node.type == 'emoji') {
|
||||||
|
return node.data.get('emojiUnicode');
|
||||||
} else if (node.type == 'pill') {
|
} else if (node.type == 'pill') {
|
||||||
switch (this.pillFormat) {
|
switch (this.pillFormat) {
|
||||||
case 'plain':
|
case 'plain':
|
||||||
|
|
|
@ -59,6 +59,8 @@ const Pill = React.createClass({
|
||||||
room: PropTypes.instanceOf(Room),
|
room: PropTypes.instanceOf(Room),
|
||||||
// Whether to include an avatar in the pill
|
// Whether to include an avatar in the pill
|
||||||
shouldShowPillAvatar: PropTypes.bool,
|
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, {
|
const classes = classNames(pillClass, {
|
||||||
"mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId,
|
"mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId,
|
||||||
|
"mx_UserPill_selected": this.props.isSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.state.pillType) {
|
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 = new RegExp(MATRIXTO_URL_PATTERN);
|
||||||
const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g');
|
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 SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
import {makeUserPermalink} from "../../../matrix-to";
|
import {makeUserPermalink} from "../../../matrix-to";
|
||||||
import ReplyPreview from "./ReplyPreview";
|
import ReplyPreview from "./ReplyPreview";
|
||||||
|
@ -69,6 +69,7 @@ import {ContentHelpers} from 'matrix-js-sdk';
|
||||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
||||||
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
||||||
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
|
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;
|
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
||||||
|
|
||||||
|
@ -76,6 +77,7 @@ const ENTITY_TYPES = {
|
||||||
AT_ROOM_PILL: 'ATROOMPILL',
|
AT_ROOM_PILL: 'ATROOMPILL',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function onSendMessageFailed(err, room) {
|
function onSendMessageFailed(err, room) {
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
// https://github.com/vector-im/riot-web/issues/3148
|
// https://github.com/vector-im/riot-web/issues/3148
|
||||||
|
@ -351,12 +353,20 @@ export default class MessageComposerInput extends React.Component {
|
||||||
if (this.direction !== '') {
|
if (this.direction !== '') {
|
||||||
const focusedNode = editorState.focusInline || editorState.focusText;
|
const focusedNode = editorState.focusInline || editorState.focusText;
|
||||||
if (focusedNode.isVoid) {
|
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;
|
editorState = change.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editorState.document.getFirstText().text !== '') {
|
if (!editorState.document.isEmpty) {
|
||||||
this.onTypingActivity();
|
this.onTypingActivity();
|
||||||
} else {
|
} else {
|
||||||
this.onFinishedTyping();
|
this.onFinishedTyping();
|
||||||
|
@ -369,9 +379,33 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
// emojioneify any emoji
|
||||||
editorState = RichText.attachImmutableEntitiesToEmoji(editorState);
|
|
||||||
|
|
||||||
|
// 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 currentBlock = editorState.getSelection().getStartKey();
|
||||||
const currentSelection = editorState.getSelection();
|
const currentSelection = editorState.getSelection();
|
||||||
const currentStartOffset = editorState.getSelection().getStartOffset();
|
const currentStartOffset = editorState.getSelection().getStartOffset();
|
||||||
|
@ -400,7 +434,6 @@ export default class MessageComposerInput extends React.Component {
|
||||||
editorState = EditorState.forceSelection(editorState, currentSelection);
|
editorState = EditorState.forceSelection(editorState, currentSelection);
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const text = editorState.startText.text;
|
const text = editorState.startText.text;
|
||||||
const currentStartOffset = editorState.startOffset;
|
const currentStartOffset = editorState.startOffset;
|
||||||
|
|
||||||
|
@ -912,8 +945,12 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
// Move selection to the end of the selected history
|
// Move selection to the end of the selected history
|
||||||
const change = editorState.change().collapseToEndOf(editorState.document);
|
const change = editorState.change().collapseToEndOf(editorState.document);
|
||||||
|
|
||||||
// XXX: should we be calling this.onChange(change) now?
|
// 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;
|
editorState = change.value;
|
||||||
|
|
||||||
this.suppressAutoComplete = true;
|
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
|
// we can't put text in here otherwise the editor tries to select it
|
||||||
isVoid: true,
|
isVoid: true,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
inline = Inline.create({
|
|
||||||
type: 'autocompletion',
|
|
||||||
nodes: [Text.create(completion)]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let editorState = activeEditorState;
|
let editorState = activeEditorState;
|
||||||
|
@ -1007,13 +1039,23 @@ export default class MessageComposerInput extends React.Component {
|
||||||
editorState = change.value;
|
editorState = change.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const change = editorState.change()
|
let change;
|
||||||
.insertInlineAtRange(editorState.selection, inline)
|
if (inline) {
|
||||||
.insertText(suffix);
|
change = editorState.change()
|
||||||
|
.insertInlineAtRange(editorState.selection, inline)
|
||||||
|
.insertText(suffix);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
change = editorState.change()
|
||||||
|
.insertTextAtRange(editorState.selection, completion)
|
||||||
|
.insertText(suffix);
|
||||||
|
}
|
||||||
editorState = change.value;
|
editorState = change.value;
|
||||||
|
|
||||||
this.setState({ editorState, originalEditorState: activeEditorState }, ()=>{
|
this.onChange(change);
|
||||||
// this.refs.editor.focus();
|
|
||||||
|
this.setState({
|
||||||
|
originalEditorState: activeEditorState
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -1027,7 +1069,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return <p {...attributes}>{children}</p>
|
return <p {...attributes}>{children}</p>
|
||||||
}
|
}
|
||||||
case 'pill': {
|
case 'pill': {
|
||||||
const { data, text } = node;
|
const { data } = node;
|
||||||
const url = data.get('url');
|
const url = data.get('url');
|
||||||
const completion = data.get('completion');
|
const completion = data.get('completion');
|
||||||
|
|
||||||
|
@ -1039,6 +1081,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
type={Pill.TYPE_AT_ROOM_MENTION}
|
type={Pill.TYPE_AT_ROOM_MENTION}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
shouldShowPillAvatar={shouldShowPillAvatar}
|
shouldShowPillAvatar={shouldShowPillAvatar}
|
||||||
|
isSelected={isSelected}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
else if (Pill.isPillUrl(url)) {
|
else if (Pill.isPillUrl(url)) {
|
||||||
|
@ -1046,14 +1089,26 @@ export default class MessageComposerInput extends React.Component {
|
||||||
url={url}
|
url={url}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
shouldShowPillAvatar={shouldShowPillAvatar}
|
shouldShowPillAvatar={shouldShowPillAvatar}
|
||||||
|
isSelected={isSelected}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
const { text } = node;
|
||||||
return <a href={url} {...props.attributes}>
|
return <a href={url} {...props.attributes}>
|
||||||
{ text }
|
{ text }
|
||||||
</a>;
|
</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