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
|
@ -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;
|
||||
|
|
|
@ -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});
|
||||
};
|
||||
|
||||
|
|
2
internal_packages/composer-emoji/lib/emoji-data.js
Normal 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;">`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'
|
||||
];
|
|
@ -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', ()=> {
|
||||
|
|
|
@ -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! <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! <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);
|
||||
});
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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:
|
||||
|
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
BIN
static/images/composer-emoji/apple/1f004.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
static/images/composer-emoji/apple/1f0cf.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
BIN
static/images/composer-emoji/apple/1f18e.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
static/images/composer-emoji/apple/1f191.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
static/images/composer-emoji/apple/1f192.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
static/images/composer-emoji/apple/1f193.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
static/images/composer-emoji/apple/1f194.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
static/images/composer-emoji/apple/1f195.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
static/images/composer-emoji/apple/1f196.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
static/images/composer-emoji/apple/1f197.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
static/images/composer-emoji/apple/1f198.png
Normal file
After Width: | Height: | Size: 5 KiB |
BIN
static/images/composer-emoji/apple/1f199.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
static/images/composer-emoji/apple/1f19a.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
static/images/composer-emoji/apple/1f1e6-1f1e8.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
static/images/composer-emoji/apple/1f1e6-1f1e9.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
static/images/composer-emoji/apple/1f1e6-1f1ea.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
static/images/composer-emoji/apple/1f1e6-1f1eb.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
static/images/composer-emoji/apple/1f1e6-1f1ec.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
static/images/composer-emoji/apple/1f1e6-1f1ee.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
static/images/composer-emoji/apple/1f1e6-1f1f1.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
static/images/composer-emoji/apple/1f1e6-1f1f2.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
static/images/composer-emoji/apple/1f1e6-1f1f4.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
static/images/composer-emoji/apple/1f1e6-1f1f6.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
static/images/composer-emoji/apple/1f1e6-1f1f7.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
static/images/composer-emoji/apple/1f1e6-1f1f8.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
static/images/composer-emoji/apple/1f1e6-1f1f9.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
static/images/composer-emoji/apple/1f1e6-1f1fa.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
static/images/composer-emoji/apple/1f1e6-1f1fc.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
static/images/composer-emoji/apple/1f1e6-1f1fd.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
static/images/composer-emoji/apple/1f1e6-1f1ff.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1e6.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1e7.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1e9.png
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1ea.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1eb.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1ec.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1ed.png
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1ee.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1ef.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1f1.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1f2.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1f3.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1f4.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1f6.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1f7.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1f8.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1f9.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1fb.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1fc.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1fe.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
static/images/composer-emoji/apple/1f1e7-1f1ff.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1e6.png
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1e8.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1e9.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1eb.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1ec.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1ed.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1ee.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1f0.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1f1.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1f2.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1f3.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1f4.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1f5.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1f7.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1fa.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1fb.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1fc.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1fd.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1fe.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
static/images/composer-emoji/apple/1f1e8-1f1ff.png
Normal file
After Width: | Height: | Size: 3.1 KiB |