Merge pull request #395 from aviraldg/fix-emoji

Various fixes and improvements to emojification.
This commit is contained in:
David Baker 2016-08-11 10:23:00 +01:00 committed by GitHub
commit 46899a0086
13 changed files with 136 additions and 34 deletions

View file

@ -24,8 +24,39 @@ import escape from 'lodash/escape';
import emojione from 'emojione'; import emojione from 'emojione';
import classNames from 'classnames'; import classNames from 'classnames';
emojione.imagePathSVG = 'emojione/svg/';
emojione.imageType = 'svg';
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi"); 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 = { var sanitizeHtmlParams = {
allowedTags: [ allowedTags: [
'font', // custom to matrix for IRC-style font coloring 'font', // custom to matrix for IRC-style font coloring
@ -211,8 +242,7 @@ module.exports = {
}; };
} }
safeBody = sanitizeHtml(body, sanitizeHtmlParams); safeBody = sanitizeHtml(body, sanitizeHtmlParams);
emojione.imageType = 'svg'; safeBody = unicodeToImage(safeBody);
safeBody = emojione.unicodeToImage(safeBody);
} }
finally { finally {
delete sanitizeHtmlParams.textFilter; delete sanitizeHtmlParams.textFilter;
@ -239,7 +269,6 @@ module.exports = {
}, },
emojifyText: function(text) { emojifyText: function(text) {
emojione.imageType = 'svg';
return { return {
__html: emojione.unicodeToImage(escape(text)), __html: emojione.unicodeToImage(escape(text)),
}; };

View file

@ -26,7 +26,7 @@ export default class EmojiProvider extends AutocompleteProvider {
completion: shortnameToUnicode(shortname), completion: shortnameToUnicode(shortname),
component: ( component: (
<div className="mx_Autocomplete_Completion"> <div className="mx_Autocomplete_Completion">
<span dangerouslySetInnerHTML={{__html: imageHTML}}></span> {shortname} <span style={{maxWidth: '1em'}} dangerouslySetInnerHTML={{__html: imageHTML}}></span>&nbsp;&nbsp;{shortname}
</div> </div>
), ),
range, range,

View file

@ -54,6 +54,7 @@ module.exports.components['views.dialogs.SetDisplayNameDialog'] = require('./com
module.exports.components['views.dialogs.TextInputDialog'] = require('./components/views/dialogs/TextInputDialog'); module.exports.components['views.dialogs.TextInputDialog'] = require('./components/views/dialogs/TextInputDialog');
module.exports.components['views.elements.EditableText'] = require('./components/views/elements/EditableText'); module.exports.components['views.elements.EditableText'] = require('./components/views/elements/EditableText');
module.exports.components['views.elements.EditableTextContainer'] = require('./components/views/elements/EditableTextContainer'); module.exports.components['views.elements.EditableTextContainer'] = require('./components/views/elements/EditableTextContainer');
module.exports.components['views.elements.EmojiText'] = require('./components/views/elements/EmojiText');
module.exports.components['views.elements.PowerSelector'] = require('./components/views/elements/PowerSelector'); module.exports.components['views.elements.PowerSelector'] = require('./components/views/elements/PowerSelector');
module.exports.components['views.elements.ProgressBar'] = require('./components/views/elements/ProgressBar'); module.exports.components['views.elements.ProgressBar'] = require('./components/views/elements/ProgressBar');
module.exports.components['views.elements.TintableSvg'] = require('./components/views/elements/TintableSvg'); module.exports.components['views.elements.TintableSvg'] = require('./components/views/elements/TintableSvg');
@ -72,6 +73,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.MImageBody'] = require('./components/views/messages/MImageBody');
module.exports.components['views.messages.MVideoBody'] = require('./components/views/messages/MVideoBody'); 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.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.TextualBody'] = require('./components/views/messages/TextualBody');
module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent'); module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent');
module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody'); module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');

View file

@ -189,6 +189,7 @@ module.exports = React.createClass({
_getContent: function() { _getContent: function() {
var TabCompleteBar = sdk.getComponent('rooms.TabCompleteBar'); var TabCompleteBar = sdk.getComponent('rooms.TabCompleteBar');
var TintableSvg = sdk.getComponent("elements.TintableSvg"); var TintableSvg = sdk.getComponent("elements.TintableSvg");
const EmojiText = sdk.getComponent('elements.EmojiText');
// no conn bar trumps unread count since you can't get unread messages // no conn bar trumps unread count since you can't get unread messages
// without a connection! (technically may already have some but meh) // without a connection! (technically may already have some but meh)
@ -262,7 +263,7 @@ module.exports = React.createClass({
if (typingString) { if (typingString) {
return ( return (
<div className="mx_RoomStatusBar_typingBar"> <div className="mx_RoomStatusBar_typingBar">
{typingString} <EmojiText>{typingString}</EmojiText>
</div> </div>
); );
} }

View file

@ -18,7 +18,7 @@ limitations under the License.
var React = require('react'); var React = require('react');
var AvatarLogic = require("../../../Avatar"); var AvatarLogic = require("../../../Avatar");
import {emojifyText} from '../../../HtmlUtils'; import sdk from '../../../index';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'BaseAvatar', displayName: 'BaseAvatar',
@ -133,6 +133,7 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
var imageUrl = this.state.imageUrls[this.state.urlsIndex]; var imageUrl = this.state.imageUrls[this.state.urlsIndex];
const { const {
@ -142,15 +143,13 @@ module.exports = React.createClass({
} = this.props; } = this.props;
if (imageUrl === this.state.defaultImageUrl) { if (imageUrl === this.state.defaultImageUrl) {
var initialLetter = emojifyText(this._getInitialLetter(name)); const initialLetter = this._getInitialLetter(name);
return ( return (
<span className="mx_BaseAvatar" {...otherProps}> <span className="mx_BaseAvatar" {...otherProps}>
<span className="mx_BaseAvatar_initial" aria-hidden="true" <EmojiText 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}</EmojiText>
dangerouslySetInnerHTML={initialLetter}>
</span>
<img className="mx_BaseAvatar_image" src={imageUrl} <img className="mx_BaseAvatar_image" src={imageUrl}
alt="" title={title} onError={this.onError} alt="" title={title} onError={this.onError}
width={width} height={height} /> width={width} height={height} />

View file

@ -0,0 +1,33 @@
/*
Copyright 2016 Aviral Dasgupta
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 {emojifyText} from '../../../HtmlUtils';
export default function EmojiText(props) {
const {element, children, ...restProps} = props;
restProps.dangerouslySetInnerHTML = emojifyText(children);
return React.createElement(element, restProps);
}
EmojiText.propTypes = {
element: React.PropTypes.string,
children: React.PropTypes.string.isRequired,
};
EmojiText.defaultProps = {
element: 'span',
};

View file

@ -0,0 +1,42 @@
/*
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 sdk from '../../../index';
export default function SenderProfile(props) {
const EmojiText = sdk.getComponent('elements.EmojiText');
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 (
<EmojiText className="mx_SenderProfile"
onClick={props.onClick}>{`${name || ''} ${props.aux || ''}`}</EmojiText>
);
}
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,
};

View file

@ -177,6 +177,7 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
var mxEvent = this.props.mxEvent; var mxEvent = this.props.mxEvent;
var content = mxEvent.getContent(); var content = mxEvent.getContent();
var body = HtmlUtils.bodyToHtml(content, this.props.highlights, {}); var body = HtmlUtils.bodyToHtml(content, this.props.highlights, {});
@ -200,10 +201,10 @@ module.exports = React.createClass({
switch (content.msgtype) { switch (content.msgtype) {
case "m.emote": case "m.emote":
var name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
return ( return (
<span ref="content" className="mx_MEmoteBody mx_EventTile_content"> <span ref="content" className="mx_MEmoteBody mx_EventTile_content">
* { name } { body } * <EmojiText>{name}</EmojiText> { body }
{ widgets } { widgets }
</span> </span>
); );

View file

@ -19,7 +19,7 @@ limitations under the License.
var React = require('react'); var React = require('react');
var TextForEvent = require('../../../TextForEvent'); var TextForEvent = require('../../../TextForEvent');
import {emojifyText} from '../../../HtmlUtils'; import sdk from '../../../index';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'TextualEvent', displayName: 'TextualEvent',
@ -31,13 +31,11 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
var text = TextForEvent.textForEvent(this.props.mxEvent); var text = TextForEvent.textForEvent(this.props.mxEvent);
if (text == null || text.length === 0) return null; if (text == null || text.length === 0) return null;
let textHTML = emojifyText(TextForEvent.textForEvent(this.props.mxEvent));
return ( return (
<div className="mx_TextualEvent" dangerouslySetInnerHTML={textHTML}> <EmojiText element="div" className="mx_TextualEvent">{text}</EmojiText>
</div>
); );
}, },
}); });

View file

@ -20,7 +20,6 @@ var React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg'); var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index'); var sdk = require('../../../index');
import {emojifyText} from '../../../HtmlUtils';
var PRESENCE_CLASS = { var PRESENCE_CLASS = {
@ -103,8 +102,9 @@ module.exports = React.createClass({
var mainClassName = "mx_EntityTile "; var mainClassName = "mx_EntityTile ";
mainClassName += presenceClass + (this.props.className ? (" " + this.props.className) : ""); mainClassName += presenceClass + (this.props.className ? (" " + this.props.className) : "");
var nameEl; var nameEl;
let nameHTML = emojifyText(this.props.name); const {name} = this.props;
const EmojiText = sdk.getComponent('elements.EmojiText');
if (this.state.hover && !this.props.suppressOnHover) { if (this.state.hover && !this.props.suppressOnHover) {
var activeAgo = this.props.presenceLastActiveAgo ? var activeAgo = this.props.presenceLastActiveAgo ?
(Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1; (Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1;
@ -114,7 +114,7 @@ module.exports = React.createClass({
nameEl = ( nameEl = (
<div className="mx_EntityTile_details"> <div className="mx_EntityTile_details">
<img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12"/> <img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
<div className="mx_EntityTile_name_hover" dangerouslySetInnerHTML={nameHTML}></div> <EmojiText element="div" className="mx_EntityTile_name_hover">{name}</EmojiText>
<PresenceLabel activeAgo={ activeAgo } <PresenceLabel activeAgo={ activeAgo }
currentlyActive={this.props.presenceCurrentlyActive} currentlyActive={this.props.presenceCurrentlyActive}
presenceState={this.props.presenceState} /> presenceState={this.props.presenceState} />
@ -123,8 +123,7 @@ module.exports = React.createClass({
} }
else { else {
nameEl = ( nameEl = (
<div className="mx_EntityTile_name" dangerouslySetInnerHTML={nameHTML}> <EmojiText element="div" className="mx_EntityTile_name">{name}</EmojiText>
</div>
); );
} }

View file

@ -32,7 +32,6 @@ var Modal = require("../../../Modal");
var sdk = require('../../../index'); var sdk = require('../../../index');
var UserSettingsStore = require('../../../UserSettingsStore'); var UserSettingsStore = require('../../../UserSettingsStore');
var createRoom = require('../../../createRoom'); var createRoom = require('../../../createRoom');
import {emojifyText} from '../../../HtmlUtils';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MemberInfo', displayName: 'MemberInfo',
@ -637,10 +636,11 @@ module.exports = React.createClass({
</div> </div>
} }
let memberNameHTML = emojifyText(this.props.member.name); const memberName = this.props.member.name;
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var PowerSelector = sdk.getComponent('elements.PowerSelector'); var PowerSelector = sdk.getComponent('elements.PowerSelector');
const EmojiText = sdk.getComponent('elements.EmojiText');
return ( return (
<div className="mx_MemberInfo"> <div className="mx_MemberInfo">
<img className="mx_MemberInfo_cancel" src="img/cancel.svg" width="18" height="18" onClick={this.onCancel}/> <img className="mx_MemberInfo_cancel" src="img/cancel.svg" width="18" height="18" onClick={this.onCancel}/>
@ -648,7 +648,7 @@ module.exports = React.createClass({
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} /> <MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
</div> </div>
<h2 dangerouslySetInnerHTML={memberNameHTML}></h2> <EmojiText element="h2">{memberNameHTML}</EmojiText>
<div className="mx_MemberInfo_profile"> <div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField"> <div className="mx_MemberInfo_profileField">

View file

@ -24,7 +24,6 @@ var Modal = require("../../../Modal");
var linkify = require('linkifyjs'); var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element'); var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../../linkify-matrix'); var linkifyMatrix = require('../../../linkify-matrix');
import {emojifyText} from '../../../HtmlUtils';
linkifyMatrix(linkify); linkifyMatrix(linkify);
@ -145,6 +144,7 @@ module.exports = React.createClass({
var RoomAvatar = sdk.getComponent("avatars.RoomAvatar"); var RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
var ChangeAvatar = sdk.getComponent("settings.ChangeAvatar"); var ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
var TintableSvg = sdk.getComponent("elements.TintableSvg"); var TintableSvg = sdk.getComponent("elements.TintableSvg");
const EmojiText = sdk.getComponent('elements.EmojiText');
var header; var header;
var name = null; var name = null;
@ -212,13 +212,12 @@ module.exports = React.createClass({
roomName = this.props.room.name; roomName = this.props.room.name;
} }
let roomNameHTML = emojifyText(roomName);
name = name =
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}> <div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
<div className={ "mx_RoomHeader_nametext " + (settingsHint ? "mx_RoomHeader_settingsHint" : "") } title={ roomName } dangerouslySetInnerHTML={roomNameHTML}></div> <EmojiText element="div" className={ "mx_RoomHeader_nametext " + (settingsHint ? "mx_RoomHeader_settingsHint" : "") } title={ roomName }>{roomName}</EmojiText>
{ searchStatus } { searchStatus }
</div> </div>;
} }
if (can_set_room_topic) { if (can_set_room_topic) {

View file

@ -22,7 +22,6 @@ var dis = require("../../../dispatcher");
var MatrixClientPeg = require('../../../MatrixClientPeg'); var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index'); var sdk = require('../../../index');
var ContextualMenu = require('../../structures/ContextualMenu'); var ContextualMenu = require('../../structures/ContextualMenu');
import {emojifyText} from '../../../HtmlUtils';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RoomTile', displayName: 'RoomTile',
@ -187,6 +186,7 @@ module.exports = React.createClass({
badge = <div className={ badgeClasses } onClick={this.onBadgeClicked} onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}>{ badgeContent }</div>; badge = <div className={ badgeClasses } onClick={this.onBadgeClicked} onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}>{ badgeContent }</div>;
const EmojiText = sdk.getComponent('elements.EmojiText');
var label; var label;
var tooltip; var tooltip;
if (!this.props.collapsed) { if (!this.props.collapsed) {
@ -196,13 +196,12 @@ module.exports = React.createClass({
'mx_RoomTile_badgeShown': this.props.highlight || (notificationCount > 0 && !this.state.areNotifsMuted) || this.state.badgeHover || this.state.menu, 'mx_RoomTile_badgeShown': this.props.highlight || (notificationCount > 0 && !this.state.areNotifsMuted) || this.state.badgeHover || this.state.menu,
}); });
let nameHTML = emojifyText(name);
if (this.props.selected) { if (this.props.selected) {
let nameSelected = <span dangerouslySetInnerHTML={nameHTML}></span>; let nameSelected = <EmojiText>{name}</EmojiText>;
label = <div title={ name } className={ nameClasses }>{ nameSelected }</div>; label = <div title={ name } className={ nameClasses }>{ nameSelected }</div>;
} else { } else {
label = <div title={ name } className={ nameClasses } dangerouslySetInnerHTML={nameHTML}></div>; label = <EmojiText element="div" title={ name } className={ nameClasses }>{name}</EmojiText>;
} }
} }
else if (this.state.hover) { else if (this.state.hover) {