replace emojione with twemoji. completely untested & debugged & unoptimised

This commit is contained in:
Matthew Hodgson 2019-05-19 15:23:43 +01:00
parent 7a244b85c1
commit dc72641264
30 changed files with 103 additions and 343 deletions

View file

@ -65,7 +65,8 @@
"classnames": "^2.1.2", "classnames": "^2.1.2",
"commonmark": "^0.28.1", "commonmark": "^0.28.1",
"counterpart": "^0.18.0", "counterpart": "^0.18.0",
"emojione": "2.2.7", "emojibase-data": "^4.0.0",
"emojibase-regex": "^4.0.0",
"file-saver": "^1.3.3", "file-saver": "^1.3.3",
"filesize": "3.5.6", "filesize": "3.5.6",
"flux": "2.1.1", "flux": "2.1.1",

Binary file not shown.

View file

@ -15,22 +15,22 @@
/* the 'src' links are relative to the bundle.css, which is in a subdirectory. /* the 'src' links are relative to the bundle.css, which is in a subdirectory.
*/ */
@font-face { @font-face {
font-family: 'Nunito'; font-family: 'Nunito';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url('$(res)/fonts/Nunito/Nunito-Regular.ttf') format('truetype'); src: url('$(res)/fonts/Nunito/Nunito-Regular.ttf') format('truetype');
} }
@font-face { @font-face {
font-family: 'Nunito'; font-family: 'Nunito';
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
src: url('$(res)/fonts/Nunito/Nunito-SemiBold.ttf') format('truetype'); src: url('$(res)/fonts/Nunito/Nunito-SemiBold.ttf') format('truetype');
} }
@font-face { @font-face {
font-family: 'Nunito'; font-family: 'Nunito';
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: url('$(res)/fonts/Nunito/Nunito-Bold.ttf') format('truetype'); src: url('$(res)/fonts/Nunito/Nunito-Bold.ttf') format('truetype');
} }
/* /*
@ -51,3 +51,13 @@
font-weight: 700; font-weight: 700;
font-style: normal; font-style: normal;
} }
/* a COLR/CPAL version of Twemoji used for consistent cross-browser emoji
* taken from https://github.com/mozilla/twemoji-colr
* using the fix from https://github.com/mozilla/twemoji-colr/issues/50 to
* work on macOS
*/
@font-face {
font-family: "Twemoji Mozilla";
src: url('$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla.woff2') format('woff2');
}

View file

@ -1,10 +1,11 @@
// XXX: check this? // XXX: check this?
/* Nunito lacks combining diacritics, so these will fall through /* Nunito lacks combining diacritics, so these will fall through
to the next font. Helevetica's diacritics however do not combine to the next font. Helevetica's diacritics however do not combine
nicely with Open Sans (on OSX, at least) and result in a huge nicely (on OSX, at least) and result in a huge horizontal mess.
horizontal mess. Arial empirically gets it right, hence prioritising Arial empirically gets it right, hence prioritising Arial here. */
Arial here. */ /* We fall through to Twemoji for emoji rather than falling through
$font-family: 'Nunito', Arial, Helvetica, Sans-Serif; to native Emoji fonts (if any) to ensure cross-browser consistency */
$font-family: Nunito, Arial, Helvetica, Sans-Serif, 'Twemoji Mozilla';
// unified palette // unified palette
// try to use these colors when possible // try to use these colors when possible

View file

@ -1,4 +1,12 @@
#!/usr/bin/env node #!/usr/bin/env node
// This generates src/stripped-emoji.json as used by the EmojiProvider autocomplete
// provider.
// FIXME: we no longer depends on emojione, so this generation script no longer
// works, but the expectation is that we will shift to using emojimart or
// similar as an emoji picker before this next needs to be run again.
const EMOJI_DATA = require('emojione/emoji.json'); const EMOJI_DATA = require('emojione/emoji.json');
const EMOJI_SUPPORTED = Object.keys(require('emojione').emojioneList); const EMOJI_SUPPORTED = Object.keys(require('emojione').emojioneList);
const fs = require('fs'); const fs = require('fs');

View file

@ -27,22 +27,18 @@ import linkifyMatrix from './linkify-matrix';
import _linkifyElement from 'linkifyjs/element'; import _linkifyElement from 'linkifyjs/element';
import _linkifyString from 'linkifyjs/string'; import _linkifyString from 'linkifyjs/string';
import escape from 'lodash/escape'; import escape from 'lodash/escape';
import emojione from 'emojione';
import classNames from 'classnames'; import classNames from 'classnames';
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import url from 'url'; import url from 'url';
linkifyMatrix(linkify); import EMOJIBASE from 'emojibase-data/en/compact.json';
import EMOJI_REGEX from 'emojibase-regex';
emojione.imagePathSVG = 'emojione/svg/'; linkifyMatrix(linkify);
// Store PNG path for displaying many flags at once (for increased performance over SVG)
emojione.imagePathPNG = 'emojione/png/';
// Use SVGs for emojis
emojione.imageType = 'svg';
// Anything outside the basic multilingual plane will be a surrogate pair // Anything outside the basic multilingual plane will be a surrogate pair
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/; const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
// And there a bunch more symbol characters that emojione has within the // And there a bunch more symbol characters that emojibase has within the
// BMP, so this includes the ranges from 'letterlike symbols' to // BMP, so this includes the ranges from 'letterlike symbols' to
// 'miscellaneous symbols and arrows' which should catch all of them // 'miscellaneous symbols and arrows' which should catch all of them
// (with plenty of false positives, but that's OK) // (with plenty of false positives, but that's OK)
@ -54,15 +50,13 @@ const ZWJ_REGEX = new RegExp("\u200D|\u2003", "g");
// Regex pattern for whitespace characters // Regex pattern for whitespace characters
const WHITESPACE_REGEX = new RegExp("\\s", "g"); const WHITESPACE_REGEX = new RegExp("\\s", "g");
// And this is emojione's complete regex
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet']; const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
/* /*
* Return true if the given string contains emoji * Return true if the given string contains emoji
* Uses a much, much simpler regex than emojione's so will give false * Uses a much, much simpler regex than emojibase's so will give false
* positives, but useful for fast-path testing strings to see if they * positives, but useful for fast-path testing strings to see if they
* need emojification. * need emojification.
* unicodeToImage uses this function. * unicodeToImage uses this function.
@ -71,73 +65,26 @@ export function containsEmoji(str) {
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str); return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
} }
/* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js
* because we want to include emoji shortnames in title text
*/
function unicodeToImage(str, addAlt) {
if (addAlt === undefined) addAlt = true;
let replaceWith; let unicode; let short; let fname;
const 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];
short = mappedUnicode[unicode];
fname = emojione.emojioneList[short].fname;
// depending on the settings, we'll either add the native unicode as the alt tag, otherwise the shortname
const title = mappedUnicode[unicode];
if (addAlt) {
const alt = (emojione.unicodeAlt) ? emojione.convert(unicode.toUpperCase()) : mappedUnicode[unicode];
replaceWith = `<img class="mx_emojione" title="${title}" alt="${alt}" src="${emojione.imagePathSVG}${fname}.svg${emojione.cacheBustParam}"/>`;
} else {
replaceWith = `<img class="mx_emojione" src="${emojione.imagePathSVG}${fname}.svg${emojione.cacheBustParam}"/>`;
}
return replaceWith;
}
});
return str;
}
/** /**
* Returns the shortcode for an emoji character. * Returns the shortcode for an emoji character.
* *
* @param {String} char The emoji character * @param {String} char The emoji character
* @return {String} The shortcode (such as :thumbup:) * @return {String} The shortcode (such as :thumbup:)
*/ */
export function unicodeToShort(char) { export function unicodeToShortcode(char) {
const unicode = emojione.jsEscapeMap[char]; const data = EMOJIBASE.find((e)=>{ e.unicode === char });
return emojione.mapUnicodeToShort()[unicode]; return (data && data.shortcodes ? data.shortcodes[0] : '';
} }
/** /**
* Given one or more unicode characters (represented by unicode * Returns the unicode character for an emoji shortcode
* character number), return an image node with the corresponding
* emoji.
* *
* @param alt {string} String to use for the image alt text * @param {String} shortcode The shortcode (such as :thumbup:)
* @param useSvg {boolean} Whether to use SVG image src. If False, PNG will be used. * @return {String} The emoji character
* @param unicode {integer} One or more integers representing unicode characters
* @returns A img node with the corresponding emoji
*/ */
export function charactersToImageNode(alt, useSvg, ...unicode) { export function shortcodeToUnicode(shortcode) {
const fileName = unicode.map((u) => { const data = EMOJIBASE.find((e)=>{ e.shortcodes && e.shortcodes.contains(shortcode) });
return u.toString(16); return data.unicode;
}).join('-');
const path = useSvg ? emojione.imagePathSVG : emojione.imagePathPNG;
const fileType = useSvg ? 'svg' : 'png';
return <img
alt={alt}
src={`${path}${fileName}.${fileType}${emojione.cacheBustParam}`}
/>;
} }
export function processHtmlForSending(html: string): string { export function processHtmlForSending(html: string): string {
@ -444,13 +391,10 @@ class TextHighlighter extends BaseHighlighter {
* opts.disableBigEmoji: optional argument to disable the big emoji class. * opts.disableBigEmoji: optional argument to disable the big emoji class.
* opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing * opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing
* opts.returnString: return an HTML string rather than JSX elements * opts.returnString: return an HTML string rather than JSX elements
* opts.emojiOne: optional param to do emojiOne (default true)
* opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer * opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer
*/ */
export function bodyToHtml(content, highlights, opts={}) { export function bodyToHtml(content, highlights, opts={}) {
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body; const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
const doEmojiOne = opts.emojiOne === undefined ? true : opts.emojiOne;
let bodyHasEmoji = false; let bodyHasEmoji = false;
let sanitizeParams = sanitizeHtmlParams; let sanitizeParams = sanitizeHtmlParams;
@ -481,28 +425,12 @@ export function bodyToHtml(content, highlights, opts={}) {
if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody); if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody);
strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(content.body) : content.body; strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(content.body) : content.body;
if (doEmojiOne) { bodyHasEmoji = containsEmoji(isHtmlMessage ? formattedBody : content.body);
bodyHasEmoji = containsEmoji(isHtmlMessage ? formattedBody : content.body);
}
// Only generate safeBody if the message was sent as org.matrix.custom.html // Only generate safeBody if the message was sent as org.matrix.custom.html
if (isHtmlMessage) { if (isHtmlMessage) {
isDisplayedWithHtml = true; isDisplayedWithHtml = true;
safeBody = sanitizeHtml(formattedBody, sanitizeParams); safeBody = sanitizeHtml(formattedBody, sanitizeParams);
} else {
// ... or if there are emoji, which we insert as HTML alongside the
// escaped plaintext body.
if (bodyHasEmoji) {
isDisplayedWithHtml = true;
safeBody = sanitizeHtml(escape(strippedBody), sanitizeParams);
}
}
// An HTML message with emoji
// or a plaintext message with emoji that was escaped and sanitized into
// HTML.
if (bodyHasEmoji) {
safeBody = unicodeToImage(safeBody);
} }
} finally { } finally {
delete sanitizeParams.textFilter; delete sanitizeParams.textFilter;
@ -545,12 +473,6 @@ export function bodyToHtml(content, highlights, opts={}) {
<span className={className} dir="auto">{ strippedBody }</span>; <span className={className} dir="auto">{ strippedBody }</span>;
} }
export function emojifyText(text, addAlt) {
return {
__html: unicodeToImage(escape(text), addAlt),
};
}
/** /**
* Linkifies the given string. This is a wrapper around 'linkifyjs/string'. * Linkifies the given string. This is a wrapper around 'linkifyjs/string'.
* *

View file

@ -1,40 +0,0 @@
/*
Copyright 2015 - 2017 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as emojione from 'emojione';
export function unicodeToEmojiUri(str) {
const mappedUnicode = emojione.mapUnicodeToShort();
// remove any zero width joiners/spaces used in conjugate emojis as the emojione URIs don't contain them
return str.replace(emojione.regUnicode, function(unicodeChar) {
if ((typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap))) {
// if the unicodeChar doesn't exist just return the entire match
return unicodeChar;
} else {
// get the unicode codepoint from the actual char
const unicode = emojione.jsEscapeMap[unicodeChar];
const short = mappedUnicode[unicode];
const fname = emojione.emojioneList[short].fname;
return emojione.imagePathSVG+fname+'.svg'+emojione.cacheBustParam;
}
});
}

View file

@ -19,7 +19,6 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import {shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione';
import QueryMatcher from './QueryMatcher'; import QueryMatcher from './QueryMatcher';
import sdk from '../index'; import sdk from '../index';
import {PillCompletion} from './Components'; import {PillCompletion} from './Components';
@ -27,7 +26,11 @@ import type {Completion, SelectionRange} from './Autocompleter';
import _uniq from 'lodash/uniq'; import _uniq from 'lodash/uniq';
import _sortBy from 'lodash/sortBy'; import _sortBy from 'lodash/sortBy';
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
import { shortcodeToUnicode } from './HtmlUtils';
import UNICODE_REGEX from 'emojibase-regex';
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
import SHORTCODE_REGEX from 'emojibase-regex/shortcode';
import EmojiData from '../stripped-emoji.json'; import EmojiData from '../stripped-emoji.json';
const LIMIT = 20; const LIMIT = 20;
@ -44,15 +47,15 @@ const CATEGORY_ORDER = [
'modifier', 'modifier',
]; ];
// Match for ":wink:" or ascii-style ";-)" provided by emojione // Match for ":wink:" or ascii-style ";-)" provided by emojibase
// (^|\s|(emojiUnicode)) to make sure we're either at the start of the string or there's a // (^|\s|(emojiUnicode)) to make sure we're either at the start of the string or there's a
// whitespace character or an emoji before the emoji. The reason for unicodeRegexp is // whitespace character or an emoji before the emoji. The reason for unicodeRegexp is
// that we need to support inputting multiple emoji with no space between them. // that we need to support inputting multiple emoji with no space between them.
const EMOJI_REGEX = new RegExp('(?:^|\\s|' + unicodeRegexp + ')(' + asciiRegexp + '|:[+-\\w]*:?)$', 'g'); const EMOJI_REGEX = new RegExp('(?:^|\\s|' + UNICODE_REGEX + ')(' + EMOTICON_REGEX + '|:[+-\\w]*:?)$', 'g');
// We also need to match the non-zero-length prefixes to remove them from the final match, // We also need to match the non-zero-length prefixes to remove them from the final match,
// and update the range so that we don't replace the whitespace or the previous emoji. // and update the range so that we don't replace the whitespace or the previous emoji.
const MATCH_PREFIX_REGEX = new RegExp('(\\s|' + unicodeRegexp + ')'); const MATCH_PREFIX_REGEX = new RegExp('(\\s|' + UNICODE_REGEX + ')');
const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sort( const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sort(
(a, b) => { (a, b) => {
@ -101,8 +104,6 @@ export default class EmojiProvider extends AutocompleteProvider {
return []; // don't give any suggestions if the user doesn't want them return []; // don't give any suggestions if the user doesn't want them
} }
const EmojiText = sdk.getComponent('views.elements.EmojiText');
let completions = []; let completions = [];
const {command, range} = this.getCurrentCommand(query, selection); const {command, range} = this.getCurrentCommand(query, selection);
if (command) { if (command) {
@ -133,12 +134,12 @@ export default class EmojiProvider extends AutocompleteProvider {
completions = _sortBy(_uniq(completions), sorters); completions = _sortBy(_uniq(completions), sorters);
completions = completions.map((result) => { completions = completions.map((result) => {
const {shortname} = result; const { shortname } = result;
const unicode = shortnameToUnicode(shortname); const unicode = shortcodeToUnicode(shortname);
return { return {
completion: unicode, completion: unicode,
component: ( component: (
<PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{ unicode }</EmojiText>} /> <PillCompletion title={shortname} initialComponent={<span style={{maxWidth: '1em'}}>{ unicode }</span>} />
), ),
range, range,
}; };

View file

@ -303,9 +303,7 @@ module.exports = React.createClass({
}, },
// return suitable content for the main (text) part of the status bar. // return suitable content for the main (text) part of the status bar.
_getContent: function() { _getContent: function() {s
const EmojiText = sdk.getComponent('elements.EmojiText');
if (this._shouldShowConnectionError()) { if (this._shouldShowConnectionError()) {
return ( return (
<div className="mx_RoomStatusBar_connectionLostBar"> <div className="mx_RoomStatusBar_connectionLostBar">

View file

@ -166,7 +166,6 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
const imageUrl = this.state.imageUrls[this.state.urlsIndex]; const imageUrl = this.state.imageUrls[this.state.urlsIndex];
const { const {
@ -178,13 +177,13 @@ module.exports = React.createClass({
if (imageUrl === this.state.defaultImageUrl) { if (imageUrl === this.state.defaultImageUrl) {
const initialLetter = this._getInitialLetter(name); const initialLetter = this._getInitialLetter(name);
const textNode = ( const textNode = (
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true" <span className="mx_BaseAvatar_initial" aria-hidden="true"
style={{ fontSize: (width * 0.65) + "px", style={{ fontSize: (width * 0.65) + "px",
width: width + "px", width: width + "px",
lineHeight: height + "px" }} lineHeight: height + "px" }}
> >
{ initialLetter } { initialLetter }
</EmojiText> </span>
); );
const imgNode = ( const imgNode = (
<img className="mx_BaseAvatar_image" src={imageUrl} <img className="mx_BaseAvatar_image" src={imageUrl}

View file

@ -1,43 +0,0 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {emojifyText, containsEmoji} from '../../../HtmlUtils';
export default function EmojiText(props) {
const {element, children, addAlt, ...restProps} = props;
// fast path: simple regex to detect strings that don't contain
// emoji and just return them
if (containsEmoji(children)) {
restProps.dangerouslySetInnerHTML = emojifyText(children, addAlt);
return React.createElement(element, restProps);
} else {
return React.createElement(element, restProps, children);
}
}
EmojiText.propTypes = {
element: PropTypes.string,
children: PropTypes.string.isRequired,
};
EmojiText.defaultProps = {
element: 'span',
addAlt: true,
};

View file

@ -114,13 +114,9 @@ module.exports = React.createClass({
return null; return null;
} }
const EmojiText = sdk.getComponent('elements.EmojiText');
return ( return (
<span className="mx_TextualEvent mx_MemberEventListSummary_summary"> <span className="mx_TextualEvent mx_MemberEventListSummary_summary">
<EmojiText> { summaries.join(", ") }
{ summaries.join(", ") }
</EmojiText>
</span> </span>
); );
}, },

View file

@ -117,7 +117,6 @@ export default React.createClass({
render: function() { render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const EmojiText = sdk.getComponent('elements.EmojiText');
const groupName = this.props.group.name || this.props.group.groupId; const groupName = this.props.group.name || this.props.group.groupId;
const httpAvatarUrl = this.props.group.avatarUrl ? const httpAvatarUrl = this.props.group.avatarUrl ?
@ -129,9 +128,9 @@ export default React.createClass({
'mx_RoomTile_badgeShown': this.state.badgeHover || this.state.menuDisplayed, 'mx_RoomTile_badgeShown': this.state.badgeHover || this.state.menuDisplayed,
}); });
const label = <EmojiText element="div" title={this.props.group.groupId} className={nameClasses} dir="auto"> const label = <div title={this.props.group.groupId} className={nameClasses} dir="auto">
{ groupName } { groupName }
</EmojiText>; </div>;
const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed; const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed;
const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', { const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', {

View file

@ -180,7 +180,6 @@ module.exports = React.createClass({
this.props.groupMember.displayname || this.props.groupMember.userId this.props.groupMember.displayname || this.props.groupMember.userId
); );
const EmojiText = sdk.getComponent('elements.EmojiText');
const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper'); const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
return ( return (
<div className="mx_MemberInfo"> <div className="mx_MemberInfo">
@ -189,7 +188,7 @@ module.exports = React.createClass({
<img src={require("../../../../res/img/cancel.svg")} width="18" height="18" className="mx_filterFlipColor" /> <img src={require("../../../../res/img/cancel.svg")} width="18" height="18" className="mx_filterFlipColor" />
</AccessibleButton> </AccessibleButton>
{ avatarElement } { avatarElement }
<EmojiText element="h2">{ groupMemberName }</EmojiText> <h2>{ groupMemberName }</h2>
<div className="mx_MemberInfo_profile"> <div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField"> <div className="mx_MemberInfo_profileField">

View file

@ -149,7 +149,6 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
@ -221,7 +220,7 @@ module.exports = React.createClass({
</AccessibleButton> </AccessibleButton>
{ avatarElement } { avatarElement }
<EmojiText element="h2">{ groupRoomName }</EmojiText> <h2>{ groupRoomName }</h2>
<div className="mx_MemberInfo_profile"> <div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField"> <div className="mx_MemberInfo_profileField">

View file

@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index'; import sdk from '../../../index';
import { unicodeToShort } from '../../../HtmlUtils'; import { unicodeToShortcode } from '../../../HtmlUtils';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
export default class ReactionsRowButtonTooltip extends React.PureComponent { export default class ReactionsRowButtonTooltip extends React.PureComponent {
@ -45,7 +45,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent {
const { name } = room.getMember(reactionEvent.getSender()); const { name } = room.getMember(reactionEvent.getSender());
senders.push(name); senders.push(name);
} }
const shortName = unicodeToShort(content) || content; const shortName = unicodeToShortcode(content) || content;
tooltipLabel = <div>{_t( tooltipLabel = <div>{_t(
"<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>", "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
{ {

View file

@ -95,7 +95,6 @@ export default React.createClass({
}, },
render() { render() {
const EmojiText = sdk.getComponent('elements.EmojiText');
const {mxEvent} = this.props; const {mxEvent} = this.props;
const colorClass = getUserNameColorClass(mxEvent.getSender()); const colorClass = getUserNameColorClass(mxEvent.getSender());
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
@ -117,7 +116,7 @@ export default React.createClass({
/>; />;
} }
const nameElem = <EmojiText key='name'>{ name || '' }</EmojiText>; const nameElem = name || '';
// Name + flair // Name + flair
const nameFlair = <span> const nameFlair = <span>

View file

@ -463,7 +463,6 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
const mxEvent = this.props.mxEvent; const mxEvent = this.props.mxEvent;
const content = mxEvent.getContent(); const content = mxEvent.getContent();
@ -502,12 +501,12 @@ module.exports = React.createClass({
return ( return (
<span ref="content" className="mx_MEmoteBody mx_EventTile_content"> <span ref="content" className="mx_MEmoteBody mx_EventTile_content">
*&nbsp; *&nbsp;
<EmojiText <span
className="mx_MEmoteBody_sender" className="mx_MEmoteBody_sender"
onClick={this.onEmoteSenderClick} onClick={this.onEmoteSenderClick}
> >
{ name } { name }
</EmojiText> </span>
&nbsp; &nbsp;
{ body } { body }
{ widgets } { widgets }

View file

@ -31,11 +31,10 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
const text = TextForEvent.textForEvent(this.props.mxEvent); const text = TextForEvent.textForEvent(this.props.mxEvent);
if (text == null || text.length === 0) return null; if (text == null || text.length === 0) return null;
return ( return (
<EmojiText element="div" className="mx_TextualEvent">{ text }</EmojiText> <div className="mx_TextualEvent">{ text }</div>
); );
}, },
}); });

View file

@ -256,8 +256,6 @@ export default class Autocomplete extends React.Component {
} }
render() { render() {
const EmojiText = sdk.getComponent('views.elements.EmojiText');
let position = 1; let position = 1;
const renderedCompletions = this.state.completions.map((completionResult, i) => { const renderedCompletions = this.state.completions.map((completionResult, i) => {
const completions = completionResult.completions.map((completion, i) => { const completions = completionResult.completions.map((completion, i) => {
@ -282,7 +280,7 @@ export default class Autocomplete extends React.Component {
return completions.length > 0 ? ( return completions.length > 0 ? (
<div key={i} className="mx_Autocomplete_ProviderSection"> <div key={i} className="mx_Autocomplete_ProviderSection">
<EmojiText element="div" className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</EmojiText> <div className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</div>
{ completionResult.provider.renderCompletions(completions) } { completionResult.provider.renderCompletions(completions) }
</div> </div>
) : null; ) : null;

View file

@ -111,7 +111,6 @@ const EntityTile = React.createClass({
let nameEl; let nameEl;
const {name} = this.props; const {name} = this.props;
const EmojiText = sdk.getComponent('elements.EmojiText');
if (!this.props.suppressOnHover) { if (!this.props.suppressOnHover) {
const activeAgo = this.props.presenceLastActiveAgo ? const activeAgo = this.props.presenceLastActiveAgo ?
(Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1; (Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1;
@ -128,24 +127,24 @@ const EntityTile = React.createClass({
} }
nameEl = ( nameEl = (
<div className="mx_EntityTile_details"> <div className="mx_EntityTile_details">
<EmojiText element="div" className="mx_EntityTile_name" dir="auto"> <div className="mx_EntityTile_name" dir="auto">
{ name } { name }
</EmojiText> </div>
{presenceLabel} {presenceLabel}
</div> </div>
); );
} else if (this.props.subtextLabel) { } else if (this.props.subtextLabel) {
nameEl = ( nameEl = (
<div className="mx_EntityTile_details"> <div className="mx_EntityTile_details">
<EmojiText element="div" className="mx_EntityTile_name" dir="auto"> <div className="mx_EntityTile_name" dir="auto">
{name} {name}
</EmojiText> </div>
<span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span> <span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>
</div> </div>
); );
} else { } else {
nameEl = ( nameEl = (
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">{ name }</EmojiText> <div className="mx_EntityTile_name" dir="auto">{ name }</div>
); );
} }

View file

@ -674,14 +674,13 @@ module.exports = withMatrixClient(React.createClass({
switch (this.props.tileShape) { switch (this.props.tileShape) {
case 'notif': { case 'notif': {
const EmojiText = sdk.getComponent('elements.EmojiText');
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId()); const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
return ( return (
<div className={classes}> <div className={classes}>
<div className="mx_EventTile_roomName"> <div className="mx_EventTile_roomName">
<EmojiText element="a" href={permalink} onClick={this.onPermalinkClicked}> <a href={permalink} onClick={this.onPermalinkClicked}>
{ room ? room.name : '' } { room ? room.name : '' }
</EmojiText> </a>
</div> </div>
<div className="mx_EventTile_senderDetails"> <div className="mx_EventTile_senderDetails">
{ avatar } { avatar }

View file

@ -978,7 +978,6 @@ module.exports = withMatrixClient(React.createClass({
} }
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
const EmojiText = sdk.getComponent('elements.EmojiText');
let backButton; let backButton;
if (this.props.member.roomId) { if (this.props.member.roomId) {
@ -993,7 +992,7 @@ module.exports = withMatrixClient(React.createClass({
<div className="mx_MemberInfo_name"> <div className="mx_MemberInfo_name">
{ backButton } { backButton }
{ e2eIconElement } { e2eIconElement }
<EmojiText element="h2">{ memberName }</EmojiText> <h2>{ memberName }</h2>
</div> </div>
{ avatarElement } { avatarElement }
<div className="mx_MemberInfo_container"> <div className="mx_MemberInfo_container">

View file

@ -51,10 +51,9 @@ import ContentMessages from '../../../ContentMessages';
import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix'; import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix';
import { import EMOJIBASE from 'emojibase-data/en/compact.json';
asciiRegexp, unicodeRegexp, shortnameToUnicode, import EMOTICON_REGEX from 'emojibase-regex/emoticon';
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";
@ -63,9 +62,7 @@ import ReplyThread from "../elements/ReplyThread";
import {ContentHelpers} from 'matrix-js-sdk'; import {ContentHelpers} from 'matrix-js-sdk';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort(); const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX + ')\\s$');
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
const EMOJI_REGEX = new RegExp(unicodeRegexp, 'g');
const TYPING_USER_TIMEOUT = 10000; const TYPING_SERVER_TIMEOUT = 30000; const TYPING_USER_TIMEOUT = 10000; const TYPING_SERVER_TIMEOUT = 30000;
@ -273,9 +270,8 @@ export default class MessageComposerInput extends React.Component {
case 'emoji': case 'emoji':
// XXX: apparently you can't return plain strings from serializer rules // XXX: apparently you can't return plain strings from serializer rules
// until https://github.com/ianstormtaylor/slate/pull/1854 is merged. // until https://github.com/ianstormtaylor/slate/pull/1854 is merged.
// So instead we temporarily wrap emoji from RTE in an arbitrary tag // So instead we temporarily wrap emoji from RTE in a span.
// (<b/>). <span/> would be nicer, but in practice it causes CSS issues. return <span>{ obj.data.get('emojiUnicode') }</span>;
return <b>{ obj.data.get('emojiUnicode') }</b>;
} }
return this.renderNode({ return this.renderNode({
node: obj, node: obj,
@ -375,7 +371,6 @@ export default class MessageComposerInput extends React.Component {
const html = HtmlUtils.bodyToHtml(payload.event.getContent(), null, { const html = HtmlUtils.bodyToHtml(payload.event.getContent(), null, {
forComposerQuote: true, forComposerQuote: true,
returnString: true, returnString: true,
emojiOne: false,
}); });
const fragment = this.html.deserialize(html); const fragment = this.html.deserialize(html);
// FIXME: do we want to put in a permalink to the original quote here? // FIXME: do we want to put in a permalink to the original quote here?
@ -540,10 +535,7 @@ export default class MessageComposerInput extends React.Component {
// The first matched group includes just the matched plaintext emoji // The first matched group includes just the matched plaintext emoji
const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset)); const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset));
if (emojiMatch) { if (emojiMatch) {
// plaintext -> hex unicode const unicodeEmoji = EMOJIBASE.find(e => e.emoticon && e.emoticon.contains(emoijMatch[1]));
const emojiUc = asciiList[emojiMatch[1]];
// hex unicode -> shortname -> actual unicode
const unicodeEmoji = shortnameToUnicode(EMOJI_UNICODE_TO_SHORTNAME[emojiUc]);
const range = Range.create({ const range = Range.create({
anchor: { anchor: {
@ -561,54 +553,6 @@ export default class MessageComposerInput extends React.Component {
} }
} }
// emojioneify any emoji
let foundEmoji;
do {
foundEmoji = false;
for (const node of editorState.document.getTexts()) {
if (node.text !== '' && HtmlUtils.containsEmoji(node.text)) {
let match;
EMOJI_REGEX.lastIndex = 0;
while ((match = EMOJI_REGEX.exec(node.text)) !== null) {
const range = Range.create({
anchor: {
key: node.key,
offset: match.index,
},
focus: {
key: node.key,
offset: match.index + match[0].length,
},
});
const inline = Inline.create({
type: 'emoji',
data: { emojiUnicode: match[0] },
});
change = change.insertInlineAtRange(range, inline);
editorState = change.value;
// if we replaced an emoji, start again looking for more
// emoji in the new editor state since doing the replacement
// will change the node structure & offsets so we can't compute
// insertion ranges from node.key / match.index anymore.
foundEmoji = true;
break;
}
}
}
} while (foundEmoji);
// work around weird bug where inserting emoji via the macOS
// emoji picker can leave the selection stuck in the emoji's
// child text. This seems to happen due to selection getting
// moved in the normalisation phase after calculating these changes
if (editorState.selection.anchor.key &&
editorState.document.getParent(editorState.selection.anchor.key).type === 'emoji') {
change = change.moveToStartOfNextText();
editorState = change.value;
}
if (this.props.onInputStateChanged && editorState.blocks.size > 0) { if (this.props.onInputStateChanged && editorState.blocks.size > 0) {
let blockType = editorState.blocks.first().type; let blockType = editorState.blocks.first().type;
// console.log("onInputStateChanged; current block type is " + blockType + " and marks are " + editorState.activeMarks); // console.log("onInputStateChanged; current block type is " + blockType + " and marks are " + editorState.activeMarks);
@ -1295,7 +1239,7 @@ 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().moveToEndOfNode(editorState.document); const change = editorState.change().moveToEndOfNode(editorState.document);
// We don't call this.onChange(change) now, as fixups on stuff like emoji // We don't call this.onChange(change) now, as fixups on stuff like pills
// should already have been done and persisted in the history. // should already have been done and persisted in the history.
editorState = change.value; editorState = change.value;
@ -1473,20 +1417,8 @@ export default class MessageComposerInput extends React.Component {
</a>; </a>;
} }
} }
case 'emoji': { case 'emoji':
const { data } = node; return data.get('emojiUnicode');
const emojiUnicode = data.get('emojiUnicode');
const uri = RichText.unicodeToEmojiUri(emojiUnicode);
const shortname = toShort(emojiUnicode);
const className = classNames('mx_emojione', {
mx_emojione_selected: isSelected,
});
const style = {};
if (props.selected) style.border = '1px solid blue';
return <img className={ className } src={ uri }
title={ shortname } alt={ emojiUnicode } style={style}
/>;
}
} }
}; };

View file

@ -66,13 +66,12 @@ export default class ReplyPreview extends React.Component {
if (!this.state.event) return null; if (!this.state.event) return null;
const EventTile = sdk.getComponent('rooms.EventTile'); const EventTile = sdk.getComponent('rooms.EventTile');
const EmojiText = sdk.getComponent('views.elements.EmojiText');
return <div className="mx_ReplyPreview"> return <div className="mx_ReplyPreview">
<div className="mx_ReplyPreview_section"> <div className="mx_ReplyPreview_section">
<EmojiText element="div" className="mx_ReplyPreview_header mx_ReplyPreview_title"> <div className="mx_ReplyPreview_header mx_ReplyPreview_title">
{ '💬 ' + _t('Replying') } { '💬 ' + _t('Replying') }
</EmojiText> </div>
<div className="mx_ReplyPreview_header mx_ReplyPreview_cancel"> <div className="mx_ReplyPreview_header mx_ReplyPreview_cancel">
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18" <img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18"
onClick={cancelQuoting} /> onClick={cancelQuoting} />

View file

@ -147,7 +147,6 @@ module.exports = React.createClass({
render: function() { render: function() {
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar"); const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
const EmojiText = sdk.getComponent('elements.EmojiText');
let searchStatus = null; let searchStatus = null;
let cancelButton = null; let cancelButton = null;
@ -191,10 +190,10 @@ module.exports = React.createClass({
roomName = this.props.room.name; roomName = this.props.room.name;
} }
const emojiTextClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint }); const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
const name = const name =
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}> <div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
<EmojiText dir="auto" element="div" className={emojiTextClasses} title={roomName}>{ roomName }</EmojiText> <div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>
{ searchStatus } { searchStatus }
</div>; </div>;

View file

@ -342,7 +342,6 @@ module.exports = React.createClass({
badge = <div className={badgeClasses}>{ badgeContent }</div>; badge = <div className={badgeClasses}>{ badgeContent }</div>;
} }
const EmojiText = sdk.getComponent('elements.EmojiText');
let label; let label;
let subtextLabel; let subtextLabel;
let tooltip; let tooltip;
@ -354,14 +353,7 @@ module.exports = React.createClass({
}); });
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null; subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
label = <div title={name} className={nameClasses} dir="auto">{ nameSelected }</div>;
if (this.state.selected) {
const nameSelected = <EmojiText>{ name }</EmojiText>;
label = <div title={name} className={nameClasses} dir="auto">{ nameSelected }</div>;
} else {
label = <EmojiText element="div" title={name} className={nameClasses} dir="auto">{ name }</EmojiText>;
}
} else if (this.state.hover) { } else if (this.state.hover) {
const Tooltip = sdk.getComponent("elements.Tooltip"); const Tooltip = sdk.getComponent("elements.Tooltip");
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />; tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;

View file

@ -212,15 +212,13 @@ module.exports = React.createClass({
return (<div className="mx_WhoIsTypingTile_empty" />); return (<div className="mx_WhoIsTypingTile_empty" />);
} }
const EmojiText = sdk.getComponent('elements.EmojiText');
return ( return (
<li className="mx_WhoIsTypingTile"> <li className="mx_WhoIsTypingTile">
<div className="mx_WhoIsTypingTile_avatars"> <div className="mx_WhoIsTypingTile_avatars">
{ this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) } { this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
</div> </div>
<div className="mx_WhoIsTypingTile_label"> <div className="mx_WhoIsTypingTile_label">
<EmojiText>{ typingString }</EmojiText> { typingString }
</div> </div>
</li> </li>
); );

View file

@ -174,14 +174,13 @@ export default class KeyBackupPanel extends React.PureComponent {
} else if (this.state.loading) { } else if (this.state.loading) {
return <Spinner />; return <Spinner />;
} else if (this.state.backupInfo) { } else if (this.state.backupInfo) {
const EmojiText = sdk.getComponent('elements.EmojiText');
let clientBackupStatus; let clientBackupStatus;
let restoreButtonCaption = _t("Restore from Backup"); let restoreButtonCaption = _t("Restore from Backup");
if (MatrixClientPeg.get().getKeyBackupEnabled()) { if (MatrixClientPeg.get().getKeyBackupEnabled()) {
clientBackupStatus = <div> clientBackupStatus = <div>
<p>{encryptedMessageAreEncrypted}</p> <p>{encryptedMessageAreEncrypted}</p>
<p>{_t("This device is backing up your keys. ")}<EmojiText></EmojiText></p> <p>{_t("This device is backing up your keys. ")}</p>
</div>; </div>;
} else { } else {
clientBackupStatus = <div> clientBackupStatus = <div>

View file

@ -36,7 +36,6 @@ export default class VerificationShowSas extends React.Component {
render() { render() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const EmojiText = sdk.getComponent('views.elements.EmojiText');
let sasDisplay; let sasDisplay;
let sasCaption; let sasCaption;
@ -44,7 +43,7 @@ export default class VerificationShowSas extends React.Component {
const emojiBlocks = this.props.sas.emoji.map( const emojiBlocks = this.props.sas.emoji.map(
(emoji, i) => <div className="mx_VerificationShowSas_emojiSas_block" key={i}> (emoji, i) => <div className="mx_VerificationShowSas_emojiSas_block" key={i}>
<div className="mx_VerificationShowSas_emojiSas_emoji"> <div className="mx_VerificationShowSas_emojiSas_emoji">
<EmojiText addAlt={false}>{emoji[0]}</EmojiText> { emoji[0] }
</div> </div>
<div className="mx_VerificationShowSas_emojiSas_label"> <div className="mx_VerificationShowSas_emojiSas_label">
{_t(capFirst(emoji[1]))} {_t(capFirst(emoji[1]))}