gem 'devise-two-factor', '~> 3.0'
gem 'doorkeeper', '~> 4.2'
gem 'fast_blank', '~> 1.0'
+gem 'gemoji', '~> 3.0'
gem 'goldfinger', '~> 1.2'
gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.5'
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
- charlock_holmes (0.7.3)
case_transform (0.2)
activesupport
+ charlock_holmes (0.7.3)
chunky_png (1.3.8)
cld3 (3.1.3)
ffi (>= 1.1.0, < 1.10.0)
fuubar (2.2.0)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
+ gemoji (3.0.0)
globalid (0.4.0)
activesupport (>= 4.2.0)
goldfinger (1.2.0)
faker (~> 1.7)
fast_blank (~> 1.0)
fuubar (~> 2.2)
+ gemoji (~> 3.0)
goldfinger (~> 1.2)
hamlit-rails (~> 0.2)
hiredis (~> 0.6)
--- /dev/null
+# frozen_string_literal: true
+
+module EmojiHelper
+ EMOJI_PATTERN = /(?<=[^[:alnum:]:]|\n|^):([\w+-]+):(?=[^[:alnum:]:]|$)/x
+
+ def emojify(text)
+ return text if text.blank?
+
+ text.gsub(EMOJI_PATTERN) do |match|
+ emoji = Emoji.find_by_alias($1) # rubocop:disable Rails/DynamicFindBy,Style/PerlBackrefs
+
+ if emoji
+ emoji.raw
+ else
+ match
+ end
+ end
+ end
+end
import { updateTimeline } from './timelines';
-import * as emojione from 'emojione';
-
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
export function submitCompose() {
return function (dispatch, getState) {
- const status = emojione.shortnameToUnicode(getState().getIn(['compose', 'text'], ''));
+ const status = getState().getIn(['compose', 'text'], '');
+
if (!status || !status.length) {
return;
}
+
dispatch(submitComposeRequest());
+
api(getState).post('/api/v1/statuses', {
status,
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
function emojify(str) {
// This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.)
- // and replacing valid shortnames like :smile: and :wink: as well as unicode strings
+ // and replacing valid unicode strings
// that _aren't_ within tags with an <img> version.
- // The goal is to be the same as an emojione.regShortNames/regUnicode replacement, but faster.
+ // The goal is to be the same as an emojione.regUnicode replacement, but faster.
let i = -1;
let insideTag = false;
- let insideShortname = false;
- let shortnameStartIndex = -1;
let match;
while (++i < str.length) {
const char = str.charAt(i);
- if (insideShortname && char === ':') {
- const shortname = str.substring(shortnameStartIndex, i + 1);
- if (shortname in emojione.emojioneList) {
- const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1];
- const alt = emojione.convert(unicode.toUpperCase());
- const replacement = `<img draggable="false" class="emojione" alt="${alt}" title="${shortname}" src="/emoji/${unicode}.svg" />`;
- str = str.substring(0, shortnameStartIndex) + replacement + str.substring(i + 1);
- i += (replacement.length - shortname.length - 1); // jump ahead the length we've added to the string
- } else {
- i--; // stray colon, try again
- }
- insideShortname = false;
- } else if (insideTag && char === '>') {
+ if (insideTag && char === '>') {
insideTag = false;
} else if (char === '<') {
insideTag = true;
- insideShortname = false;
- } else if (!insideTag && char === ':') {
- insideShortname = true;
- shortnameStartIndex = i;
} else if (!insideTag && (match = trie.search(str.substring(i)))) {
const unicodeStr = match;
if (unicodeStr in emojione.jsEscapeMap) {
handleEmojiPick = (data) => {
const position = this.autosuggestTextarea.textarea.selectionStart;
- this._restoreCaret = position + data.shortname.length + 1;
+ const emojiChar = String.fromCodePoint(parseInt(data.unicode, 16));
+ this._restoreCaret = position + emojiChar.length + 1;
this.props.onPickEmoji(position, data);
}
<Dropdown ref={this.setRef} className='emoji-picker__dropdown' onShow={this.onShowDropdown} onHide={this.onHideDropdown}>
<DropdownTrigger className='emoji-button' title={intl.formatMessage(messages.emoji)}>
<img
- draggable='false'
className={`emojione ${active && loading ? 'pulse-loading' : ''}`}
- alt='🙂' src='/emoji/1f602.svg'
+ alt='🙂'
+ src='/emoji/1f602.svg'
/>
</DropdownTrigger>
+
<DropdownContent className='dropdown__left'>
{
this.state.active && !this.state.loading &&
};
const insertEmoji = (state, position, emojiData) => {
- const emoji = emojiData.shortname;
+ const emoji = String.fromCodePoint(parseInt(emojiData.unicode, 16));
return state.withMutations(map => {
map.update('text', oldText => `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`);
margin-left: 2px;
width: 24px;
outline: 0;
+ cursor: pointer;
&:active,
&:focus {
include AccountInteractions
include Attachmentable
include Remotable
+ include EmojiHelper
# Local users
has_one :user, inverse_of: :account
before_create :generate_keys
before_validation :normalize_domain
+ before_validation :prepare_contents, if: :local?
private
+ def prepare_contents
+ display_name&.strip!
+ note&.strip!
+
+ self.display_name = emojify(display_name)
+ self.note = emojify(note)
+ end
+
def generate_keys
return unless local?
include Streamable
include Cacheable
include StatusThreadingConcern
+ include EmojiHelper
enum visibility: [:public, :unlisted, :private, :direct], _suffix: :visibility
!sensitive? && media_attachments.any?
end
- before_validation :prepare_contents
+ before_validation :prepare_contents, if: :local?
before_validation :set_reblog
before_validation :set_visibility
before_validation :set_conversation
def prepare_contents
text&.strip!
spoiler_text&.strip!
+
+ self.text = emojify(text)
+ self.spoiler_text = emojify(spoiler_text)
end
def set_reblog
media = validate_media!(options[:media_ids])
status = nil
+
ApplicationRecord.transaction do
status = account.statuses.create!(text: text,
thread: in_reply_to,
application: options[:application])
attach_media(status, media)
end
+
process_mentions_service.call(status)
process_hashtags_service.call(status)
--- /dev/null
+require 'rails_helper'
+
+RSpec.describe EmojiHelper, type: :helper do
+ describe '#emojify' do
+ it 'converts shortcodes to unicode' do
+ text = ':book: Book'
+ expect(emojify(text)).to eq '📖 Book'
+ end
+
+ it 'does not convert shortcodes that are part of a string into unicode' do
+ text = ':see_no_evil::hear_no_evil::speak_no_evil:'
+ expect(emojify(text)).to eq text
+ end
+ end
+end
+++ /dev/null
-require 'rails_helper'
-
-RSpec.describe RoutingHelper, type: :helper do
-
-end
import emojify from '../../../app/javascript/mastodon/emoji';
describe('emojify', () => {
- it('does a basic emojify', () => {
- expect(emojify(':smile:')).to.equal(
- '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" />');
- });
-
- it('does a double emojify', () => {
- expect(emojify(':smile: and :wink:')).to.equal(
- '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /> and <img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" />');
- });
-
- it('works with random colons', () => {
- expect(emojify(':smile: : :wink:')).to.equal(
- '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /> : <img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" />');
- expect(emojify(':smile::::wink:')).to.equal(
- '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" />::<img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" />');
- expect(emojify(':smile:::::wink:')).to.equal(
- '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" />:::<img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" />');
- });
-
- it('works with tags', () => {
- expect(emojify('<p>:smile:</p>')).to.equal(
- '<p><img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /></p>');
- expect(emojify('<p>:smile:</p> and <p>:wink:</p>')).to.equal(
- '<p><img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /></p> and <p><img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" /></p>');
- });
-
it('ignores unknown shortcodes', () => {
expect(emojify(':foobarbazfake:')).to.equal(':foobarbazfake:');
});
expect(emojify(':smile')).to.equal(':smile');
});
- it('does two emoji next to each other', () => {
- expect(emojify(':smile::wink:')).to.equal(
- '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /><img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" />');
- });
-
it('does unicode', () => {
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).to.equal(
'<img draggable="false" class="emojione" alt="👩👩👦👦" title=":family_wwbb:" src="/emoji/1f469-1f469-1f466-1f466.svg" />');
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" /> bar');
});
- it('does mixed unicode and shortnames', () => {
- expect(emojify(':smile:#\uFE0F\u20E3:wink:\u2757')).to.equal('<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" /><img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" /><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
- });
-
it('ignores unicode inside of tags', () => {
expect(emojify('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>')).to.equal('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>');
});
-
});