replace emojione with twemoji. completely untested & debugged & unoptimised
This commit is contained in:
parent
7a244b85c1
commit
dc72641264
30 changed files with 103 additions and 343 deletions
|
@ -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",
|
||||||
|
|
BIN
res/fonts/Twemoji_Mozilla/TwemojiMozilla.woff2
Normal file
BIN
res/fonts/Twemoji_Mozilla/TwemojiMozilla.woff2
Normal file
Binary file not shown.
|
@ -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');
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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');
|
||||||
|
|
108
src/HtmlUtils.js
108
src/HtmlUtils.js
|
@ -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'.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
|
||||||
};
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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', {
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>",
|
||||||
{
|
{
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
*
|
*
|
||||||
<EmojiText
|
<span
|
||||||
className="mx_MEmoteBody_sender"
|
className="mx_MEmoteBody_sender"
|
||||||
onClick={this.onEmoteSenderClick}
|
onClick={this.onEmoteSenderClick}
|
||||||
>
|
>
|
||||||
{ name }
|
{ name }
|
||||||
</EmojiText>
|
</span>
|
||||||
|
|
||||||
{ body }
|
{ body }
|
||||||
{ widgets }
|
{ widgets }
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
||||||
|
|
|
@ -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" />;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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]))}
|
||||||
|
|
Loading…
Reference in a new issue