From dbbea6322747420bc50ab7d83f9ca6883ddcee07 Mon Sep 17 00:00:00 2001
From: Aviral Dasgupta <me@aviraldg.com>
Date: Tue, 9 Aug 2016 21:40:05 +0530
Subject: [PATCH] Various fixes and improvements to emojification.

- Use locally hosted emoji
- Emojify SenderProfile and m.emote
- Add emoji shortcodes as titles
---
 src/HtmlUtils.js                              | 35 +++++++++++++--
 src/component-index.js                        |  1 +
 src/components/structures/RoomStatusBar.js    |  4 +-
 .../views/messages/SenderProfile.js           | 43 +++++++++++++++++++
 src/components/views/messages/TextualBody.js  |  6 ++-
 5 files changed, 83 insertions(+), 6 deletions(-)
 create mode 100644 src/components/views/messages/SenderProfile.js

diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js
index 2ab635081f..c0792e6d14 100644
--- a/src/HtmlUtils.js
+++ b/src/HtmlUtils.js
@@ -24,8 +24,39 @@ import escape from 'lodash/escape';
 import emojione from 'emojione';
 import classNames from 'classnames';
 
+emojione.imagePathSVG = 'emojione/svg/';
+emojione.imageType = 'svg';
+
 const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
 
+/* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js
+ * because we want to include emoji shortnames in title text
+ */
+export function unicodeToImage(str) {
+    let replaceWith, unicode, alt;
+    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];
+
+            // depending on the settings, we'll either add the native unicode as the alt tag, otherwise the shortname
+            alt = (emojione.unicodeAlt) ? emojione.convert(unicode.toUpperCase()) : mappedUnicode[unicode];
+            const title = mappedUnicode[unicode];
+
+            replaceWith = `<img class="emojione" title="${title}" alt="${alt}" src="${emojione.imagePathSVG}${unicode}.svg${emojione.cacheBustParam}"/>`;
+            return replaceWith;
+        }
+    });
+
+    return str;
+};
+
 var sanitizeHtmlParams = {
     allowedTags: [
         'font', // custom to matrix for IRC-style font coloring
@@ -211,8 +242,7 @@ module.exports = {
                 };
             }
             safeBody = sanitizeHtml(body, sanitizeHtmlParams);
-            emojione.imageType = 'svg';
-            safeBody = emojione.unicodeToImage(safeBody);
+            safeBody = unicodeToImage(safeBody);
         }
         finally {
             delete sanitizeHtmlParams.textFilter;
@@ -239,7 +269,6 @@ module.exports = {
     },
 
     emojifyText: function(text) {
-        emojione.imageType = 'svg';
         return {
             __html: emojione.unicodeToImage(escape(text)),
         };
diff --git a/src/component-index.js b/src/component-index.js
index 97f8882b82..d4bf2a7aab 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -72,6 +72,7 @@ module.exports.components['views.messages.MFileBody'] = require('./components/vi
 module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody');
 module.exports.components['views.messages.MVideoBody'] = require('./components/views/messages/MVideoBody');
 module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent');
+module.exports.components['views.messages.SenderProfile'] = require('./components/views/messages/SenderProfile');
 module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody');
 module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent');
 module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');
diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js
index 9a0d3dbbdd..245117387e 100644
--- a/src/components/structures/RoomStatusBar.js
+++ b/src/components/structures/RoomStatusBar.js
@@ -19,6 +19,7 @@ var sdk = require('../../index');
 var dis = require("../../dispatcher");
 var WhoIsTyping = require("../../WhoIsTyping");
 var MatrixClientPeg = require("../../MatrixClientPeg");
+import {emojifyText} from '../../HtmlUtils';
 
 module.exports = React.createClass({
     displayName: 'RoomStatusBar',
@@ -259,10 +260,11 @@ module.exports = React.createClass({
         }
 
         var typingString = this.state.whoisTypingString;
+        const typingHtml = emojifyText(typingString);
         if (typingString) {
             return (
                 <div className="mx_RoomStatusBar_typingBar">
-                    {typingString}
+                    <span dangerouslySetInnerHTML={typingHtml} />
                 </div>
             );
         }
diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js
new file mode 100644
index 0000000000..e331e9843c
--- /dev/null
+++ b/src/components/views/messages/SenderProfile.js
@@ -0,0 +1,43 @@
+/*
+ Copyright 2015, 2016 OpenMarket 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.
+ */
+
+'use strict';
+
+import React from 'react';
+import {emojifyText} from '../../../HtmlUtils';
+
+export default function SenderProfile(props) {
+    const {mxEvent} = props;
+    const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
+    const {msgtype} = mxEvent.getContent();
+
+    if (msgtype === 'm.emote') {
+        return <span />; // emote message must include the name so don't duplicate it
+    }
+
+    return (
+        <span className="mx_SenderProfile"
+              dangerouslySetInnerHTML={emojifyText(`${name || ''} ${props.aux || ''}`)}
+              onClick={props.onClick}>
+        </span>
+    );
+}
+
+SenderProfile.propTypes = {
+    mxEvent: React.PropTypes.object.isRequired, // event whose sender we're showing
+    aux: React.PropTypes.string, // stuff to go after the sender name, if anything
+    onClick: React.PropTypes.func,
+};
diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 8c6cf455dc..3c5d173e33 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -23,6 +23,7 @@ var linkify = require('linkifyjs');
 var linkifyElement = require('linkifyjs/element');
 var linkifyMatrix = require('../../../linkify-matrix');
 var sdk = require('../../../index');
+import {emojifyText} from '../../../HtmlUtils';
 
 linkifyMatrix(linkify);
 
@@ -200,10 +201,11 @@ module.exports = React.createClass({
 
         switch (content.msgtype) {
             case "m.emote":
-                var name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
+                const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
+                const nameHtml = emojifyText(name);
                 return (
                     <span ref="content" className="mx_MEmoteBody mx_EventTile_content">
-                        * { name } { body }
+                        * <span dangerouslySetInnerHTML={nameHtml} /> { body }
                         { widgets }
                     </span>
                 );