fix(composer-emoji): Switch to characters to PNGs (#1898)

* Add PNGs and JSON file

* Add Apple and Twitter emoji

* Fix linter issues and tests

* Get correct path from EmojiStore

* Add emoji regex and update extensions

* Remove the scary regex
This commit is contained in:
Jackie Luo 2016-04-12 10:27:24 -07:00 committed by Ben Gotow
parent 1e073be8a3
commit 2a431fcec7
3250 changed files with 69 additions and 182 deletions

View file

@ -5,9 +5,7 @@ import {RetinaImg, ScrollRegion} from 'nylas-component-kit';
import EmojiStore from './emoji-store';
import EmojiActions from './emoji-actions';
import emoji from 'node-emoji';
import categorizedEmojiList from './categorized-emoji';
import missingEmojiList from './missing-emoji';
class EmojiButtonPopover extends React.Component {
static displayName = 'EmojiButtonPopover';
@ -270,17 +268,12 @@ class EmojiButtonPopover extends React.Component {
ctx.fillStyle = 'black';
if (this.state.categorizedEmoji[category].length === 0) return;
this.state.categorizedEmoji[category].forEach((emojiName, j) => {
if (process.platform === "darwin" && missingEmojiList.indexOf(emojiName) !== -1) {
const img = new Image();
img.src = `images/composer-emoji/missing-emoji/${emojiName}.png`;
const x = position.x;
const y = position.y;
img.onload = () => {
ctx.drawImage(img, x, y - 30, 32, 32);
}
} else {
const emojiChar = emoji.get(emojiName);
ctx.fillText(emojiChar, position.x, position.y);
const img = new Image();
img.src = EmojiStore.getImagePath(emojiName);
const x = position.x;
const y = position.y;
img.onload = () => {
ctx.drawImage(img, x, y - 30, 32, 32);
}
if (position.x > 325 && j < this.state.categorizedEmoji[category].length - 1) {
position.x = 18;

View file

@ -1,8 +1,9 @@
import {DOMUtils, ComposerExtension} from 'nylas-exports';
import {DOMUtils, ComposerExtension, RegExpUtils} from 'nylas-exports';
import EmojiStore from './emoji-store';
import EmojiActions from './emoji-actions';
import EmojiPicker from './emoji-picker';
import emoji from 'node-emoji';
import missingEmojiList from './missing-emoji';
class EmojiComposerExtension extends ComposerExtension {
@ -133,16 +134,16 @@ class EmojiComposerExtension extends ComposerExtension {
static applyTransformsToDraft = ({draft}) => {
const nextDraft = draft.clone();
nextDraft.body = nextDraft.body.replace(/<img class="missing-emoji ([a-zA-Z0-9-_]*)" [^<]+>/g, (match, emojiName) =>
`<span class="broken-emoji ${emojiName}">${emoji.get(emojiName)}</span>`
nextDraft.body = nextDraft.body.replace(/<img class="emoji ([a-zA-Z0-9-_]*)" [^<]+>/g, (match, emojiName) =>
emoji.get(emojiName)
);
return nextDraft;
}
static unapplyTransformsToDraft = ({draft}) => {
const nextDraft = draft.clone();
nextDraft.body = nextDraft.body.replace(/<span class="broken-emoji ([a-zA-Z0-9-_]*)">[^<]+<\/span>/g, (match, emojiName) =>
`<img class="missing-emoji ${emojiName}" src="images/composer-emoji/missing-emoji/${emojiName}.png" width="14" height="14" style="margin-top: -5px;">`
nextDraft.body = nextDraft.body.replace(RegExpUtils.emojiRegex(), (match) =>
`<img class="emoji ${emoji.which(match)}" src="${EmojiStore.getImagePath(emoji.which(match))}" width="14" height="14" style="margin-top: -5px;">`
);
return nextDraft;
}
@ -205,17 +206,13 @@ class EmojiComposerExtension extends ComposerExtension {
}
}
const emojiChar = emoji.get(emojiName);
if (process.platform === "darwin" && missingEmojiList.indexOf(emojiName) !== -1) {
const html = `<img
class="missing-emoji ${emojiName}"
src="images/composer-emoji/missing-emoji/${emojiName}.png"
width="14"
height="14"
style="margin-top: -5px;">`;
editor.insertHTML(html, {selectInsertion: false});
} else {
editor.insertText(emojiChar);
}
const html = `<img
class="emoji ${emojiName}"
src="${EmojiStore.getImagePath(emojiName)}"
width="14"
height="14"
style="margin-top: -5px;">`;
editor.insertHTML(html, {selectInsertion: false});
EmojiActions.useEmoji({emojiName: emojiName, emojiChar: emojiChar});
};

File diff suppressed because one or more lines are too long

View file

@ -1,10 +1,13 @@
import {MessageViewExtension} from 'nylas-exports';
import {MessageViewExtension, RegExpUtils} from 'nylas-exports';
import EmojiStore from './emoji-store';
import emoji from 'node-emoji';
class EmojiMessageExtension extends MessageViewExtension {
static formatMessageBody({message}) {
message.body = message.body.replace(/<span class="broken-emoji ([a-zA-Z0-9-_]*)">.*<\/span>/g, (match, emojiName) =>
`<span class="missing-emoji ${emojiName}"><img src="images/composer-emoji/missing-emoji/${emojiName}.png" width="14" height="14" style="margin-top: -5px;" /></span>`
message.body = message.body.replace(RegExpUtils.emojiRegex(), (match) =>
`<img class="emoji ${emoji.which(match)}" src="${EmojiStore.getImagePath(emoji.which(match))}" width="14" height="14" style="margin-top: -5px;">`
);
}
}

View file

@ -1,7 +1,8 @@
import {React, ReactDOM} from 'nylas-exports';
import EmojiStore from './emoji-store';
import EmojiActions from './emoji-actions';
import emoji from 'node-emoji';
import missingEmojiList from './missing-emoji';
class EmojiPicker extends React.Component {
@ -35,13 +36,11 @@ class EmojiPicker extends React.Component {
this.props.emojiOptions.forEach((emojiOption, i) => {
const emojiClass = emojiIndex === i ? "btn btn-icon emoji-option" : "btn btn-icon";
let emojiChar = emoji.get(emojiOption);
if (process.platform === "darwin" && missingEmojiList.indexOf(emojiOption) !== -1) {
emojiChar = (<img
src={`images/composer-emoji/missing-emoji/${emojiOption}.png`}
width="16"
height="16"
style={{marginTop: "-4px", marginRight: "3px"}} />);
}
emojiChar = (<img
src={EmojiStore.getImagePath(emojiOption)}
width="16"
height="16"
style={{marginTop: "-4px", marginRight: "3px"}} />);
emojiButtons.push(
<button
key={emojiOption}

View file

@ -4,6 +4,7 @@ import _ from 'underscore';
import {DatabaseStore} from 'nylas-exports';
import EmojiActions from './emoji-actions';
import emojiData from './emoji-data';
const EmojiJSONBlobKey = 'emoji';
@ -39,6 +40,17 @@ class EmojiStore extends NylasStore {
return sortedEmojiNames;
}
getImagePath(emojiName) {
for (const emoji of emojiData) {
if (emoji.short_names.indexOf(emojiName) !== -1) {
if (process.platform === "darwin") {
return `images/composer-emoji/apple/${emoji.image}`;
}
return `images/composer-emoji/twitter/${emoji.image}`;
}
}
}
_onUseEmoji = (emoji) => {
const savedEmoji = _.find(this._emoji, (curEmoji) => {
return curEmoji.emojiChar === emoji.emojiChar;

View file

@ -1,101 +0,0 @@
/** @babel */
export default missingEmojiList = [
'relaxed',
'v',
'point_up',
'writing_hand',
'woman-heart-woman',
'man-heart-man',
'woman-kiss-woman',
'man-kiss-man',
'sunny',
'cloud',
'snowflake',
'showman',
'baseball',
'airplane',
'envelope',
'email',
'scissors',
'black_nib',
'pencil2',
'heart',
'heavy_heart_exclamation_mark_ornament',
'latin_cross',
'star_of_david',
'yin_yang',
'u7a7a',
'u5272',
'u6709',
'u7121',
'u7533',
'u55b6',
'u6708',
'eight_pointed_black_star',
'accept',
'ideograph_advantage',
'secret',
'congratulations',
'u5408',
'u6e80',
'u7981',
'a',
'b',
'o2',
'hotsprings',
'bangbang',
'interrobang',
'part_alternation_mark',
'warning',
'recycle',
'u6307',
'sparkle',
'eight_spoked_asterisk',
'm',
'sa',
'parking',
'zero',
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine',
'keycap_star',
'arrow_forward',
'arrow_backward',
'arrow_right',
'arrow_left',
'arrow_up',
'arrow_down',
'arrow_upper_right',
'arrow_lower_right',
'arrow_lower_left',
'arrow_upper_left',
'arrow_up_down',
'left_right_arrow',
'arrow_right_hook',
'leftwards_arrow_with_hook',
'arrow_heading_up',
'arrow_heading_down',
'hash',
'information_source',
'wavy_dash',
'heavy_check_mark',
'heavy_multiplication_x',
'copyright',
'registered',
'tm',
'ballot_box_with_check',
'black_small_square',
'white_small_square',
'black_medium_square',
'white_medium_square',
'spades',
'clubs',
'hearts',
'diamonds'
];

View file

@ -21,7 +21,7 @@ describe('EmojiButtonPopover', ()=> {
this.composer = renderIntoDocument(
<Contenteditable
value={'Testing!'}
value={''}
onChange={jasmine.createSpy('onChange')}
extensions={[EmojiComposerExtension]} />
);
@ -32,14 +32,6 @@ describe('EmojiButtonPopover', ()=> {
ReactTestUtils.Simulate.mouseDown(this.canvas);
expect(EmojiComposerExtension._onSelectEmoji).toHaveBeenCalled();
});
it('should insert an image for missing emoji', ()=> {
this.position.x = 140;
this.position.y = 60;
EmojiButtonPopover.prototype.calcPosition.andReturn(this.position);
ReactTestUtils.Simulate.mouseDown(this.canvas);
expect(EmojiComposerExtension._onSelectEmoji).toHaveBeenCalled();
});
});
describe('when searching for emoji', ()=> {

View file

@ -10,10 +10,11 @@ describe('EmojiComposerExtension', ()=> {
beforeEach(()=> {
spyOn(EmojiComposerExtension, 'onContentChanged').andCallThrough()
spyOn(EmojiComposerExtension, '_onSelectEmoji').andCallThrough()
const html = 'Testing!'
const onChange = jasmine.createSpy('onChange')
this.component = renderIntoDocument(
<Contenteditable html={html} onChange={onChange} extensions={[EmojiComposerExtension]}/>
<Contenteditable
html={''}
onChange={jasmine.createSpy('onChange')}
extensions={[EmojiComposerExtension]} />
)
this.editableNode = ReactDOM.findDOMNode(this.component).querySelector('[contenteditable]');
})
@ -29,30 +30,24 @@ describe('EmojiComposerExtension', ()=> {
})
it('should show the emoji picker', ()=> {
runs(()=> {
this._performEdit('Testing! :h');
});
this._performEdit('Testing! :h');
waitsFor(()=> {
return ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'emoji-picker').length > 0
});
})
it('should be focused on the first emoji in the list', ()=> {
runs(()=> {
this._performEdit('Testing! :h');
});
this._performEdit('Testing! :h');
waitsFor(()=> {
return ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'emoji-option').length > 0
});
runs(()=> {
expect(ReactDOM.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(this.component, 'emoji-option')).textContent === "💇 :haircut:").toBe(true);
expect(ReactDOM.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(this.component, 'emoji-option')).textContent.indexOf(":haircut:") !== -1).toBe(true);
});
})
it('should insert an emoji on enter', ()=> {
runs(()=> {
this._performEdit('Testing! :h');
});
this._performEdit('Testing! :h');
waitsFor(()=> {
return ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'emoji-picker').length > 0
});
@ -63,14 +58,12 @@ describe('EmojiComposerExtension', ()=> {
return EmojiComposerExtension._onSelectEmoji.calls.length > 0
})
runs(()=> {
expect(this.editableNode.textContent === "Testing! 💇").toBe(true);
expect(this.editableNode.innerHTML).toEqual(`Testing!&nbsp;<img class="emoji haircut" src="images/composer-emoji/apple/1f487.png" width="14" height="14" style="margin-top: -5px;">`);
});
})
it('should insert an emoji on click', ()=> {
runs(()=> {
this._performEdit('Testing! :h');
});
this._performEdit('Testing! :h');
waitsFor(()=> {
return ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'emoji-picker').length > 0
});
@ -83,14 +76,12 @@ describe('EmojiComposerExtension', ()=> {
return EmojiComposerExtension._onSelectEmoji.calls.length > 0
})
runs(()=> {
expect(this.editableNode.textContent).toEqual("Testing! 💇");
expect(this.editableNode.innerHTML).toEqual(`Testing!&nbsp;<img class="emoji haircut" src="images/composer-emoji/apple/1f487.png" width="14" height="14" style="margin-top: -5px;">`);
});
})
it('should move to the next emoji on arrow down', ()=> {
runs(()=> {
this._performEdit('Testing! :h');
});
this._performEdit('Testing! :h');
waitsFor(()=> {
return ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'emoji-option').length > 0
});
@ -101,16 +92,7 @@ describe('EmojiComposerExtension', ()=> {
return EmojiComposerExtension.onContentChanged.calls.length > 1
});
runs(()=> {
expect(ReactDOM.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(this.component, 'emoji-option')).textContent).toEqual("🍔 :hamburger:");
});
})
it('should be able to insert two emoji next to each other', ()=> {
runs(()=> {
this._performEdit('Testing! 🍔 :h');
});
waitsFor(()=> {
return ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'emoji-picker').length > 0
expect(ReactDOM.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(this.component, 'emoji-option')).textContent.indexOf(":hamburger:") !== -1).toBe(true);
});
})
})

View file

@ -25,6 +25,7 @@
"coffeestack": "^1.1",
"color": "^0.7.3",
"emissary": "^1.3.1",
"emoji-data": "^0.2.0",
"event-kit": "^1.0.2",
"fs-plus": "^2.3.2",
"fstream": "0.1.24",

View file

@ -1,4 +1,6 @@
_ = require('underscore')
EmojiData = require('emoji-data')
RegExpUtils =
# It's important that the regex be wrapped in parens, otherwise
@ -149,6 +151,11 @@ RegExpUtils =
# SO discussion: http://stackoverflow.com/questions/10687099/how-to-test-if-a-url-string-is-absolute-or-relative/31991870#31991870
hasValidSchemeRegex: -> new RegExp('^[a-z][a-z0-9+.-]*:', 'i')
emojiRegex: -> FBS_REGEXP = new RegExp(
"(?:#{EmojiData.chars({include_variants: true}).join("|")})",
"g"
)
looseStyleTag: -> /<style/gim
# Regular expression matching javasript function arguments:

View file

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4 KiB

View file

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4 KiB

View file

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4 KiB

View file

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4 KiB

View file

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Some files were not shown because too many files have changed in this diff Show more