- Use unicode when selecting emoji through picker - Convert shortcodes to unicode when storing text input server-side - Do not convert shortcodes in JS anymore
This commit is contained in:
		
							
								
								
									
										1
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Gemfile
									
									
									
									
									
								
							| @@ -28,6 +28,7 @@ gem 'devise', '~> 4.2' | ||||
| 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' | ||||
|   | ||||
| @@ -106,9 +106,9 @@ GEM | ||||
|       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) | ||||
| @@ -163,6 +163,7 @@ GEM | ||||
|     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) | ||||
| @@ -518,6 +519,7 @@ DEPENDENCIES | ||||
|   faker (~> 1.7) | ||||
|   fast_blank (~> 1.0) | ||||
|   fuubar (~> 2.2) | ||||
|   gemoji (~> 3.0) | ||||
|   goldfinger (~> 1.2) | ||||
|   hamlit-rails (~> 0.2) | ||||
|   hiredis (~> 0.6) | ||||
|   | ||||
							
								
								
									
										19
									
								
								app/helpers/emoji_helper.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/helpers/emoji_helper.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| # 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 | ||||
| @@ -2,8 +2,6 @@ import api from '../api'; | ||||
|  | ||||
| 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'; | ||||
| @@ -73,11 +71,14 @@ export function mentionCompose(account, router) { | ||||
|  | ||||
| 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), | ||||
|   | ||||
| @@ -6,36 +6,18 @@ const trie = new Trie(Object.keys(emojione.jsEscapeMap)); | ||||
|  | ||||
| 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) { | ||||
|   | ||||
| @@ -136,7 +136,8 @@ export default class ComposeForm extends ImmutablePureComponent { | ||||
|  | ||||
|   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); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -109,11 +109,12 @@ export default class EmojiPickerDropdown extends React.PureComponent { | ||||
|       <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 && | ||||
|   | ||||
| @@ -118,7 +118,7 @@ const insertSuggestion = (state, position, token, completion) => { | ||||
| }; | ||||
|  | ||||
| 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)}`); | ||||
|   | ||||
| @@ -2708,6 +2708,7 @@ button.icon-button.active i.fa-retweet { | ||||
|   margin-left: 2px; | ||||
|   width: 24px; | ||||
|   outline: 0; | ||||
|   cursor: pointer; | ||||
|  | ||||
|   &:active, | ||||
|   &:focus { | ||||
|   | ||||
| @@ -47,6 +47,7 @@ class Account < ApplicationRecord | ||||
|   include AccountInteractions | ||||
|   include Attachmentable | ||||
|   include Remotable | ||||
|   include EmojiHelper | ||||
|  | ||||
|   # Local users | ||||
|   has_one :user, inverse_of: :account | ||||
| @@ -240,9 +241,18 @@ class Account < ApplicationRecord | ||||
|  | ||||
|   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? | ||||
|  | ||||
|   | ||||
| @@ -29,6 +29,7 @@ class Status < ApplicationRecord | ||||
|   include Streamable | ||||
|   include Cacheable | ||||
|   include StatusThreadingConcern | ||||
|   include EmojiHelper | ||||
|  | ||||
|   enum visibility: [:public, :unlisted, :private, :direct], _suffix: :visibility | ||||
|  | ||||
| @@ -120,7 +121,7 @@ class Status < ApplicationRecord | ||||
|     !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 | ||||
| @@ -241,6 +242,9 @@ class Status < ApplicationRecord | ||||
|   def prepare_contents | ||||
|     text&.strip! | ||||
|     spoiler_text&.strip! | ||||
|  | ||||
|     self.text         = emojify(text) | ||||
|     self.spoiler_text = emojify(spoiler_text) | ||||
|   end | ||||
|  | ||||
|   def set_reblog | ||||
|   | ||||
| @@ -21,6 +21,7 @@ class PostStatusService < BaseService | ||||
|  | ||||
|     media  = validate_media!(options[:media_ids]) | ||||
|     status = nil | ||||
|  | ||||
|     ApplicationRecord.transaction do | ||||
|       status = account.statuses.create!(text: text, | ||||
|                                         thread: in_reply_to, | ||||
| @@ -31,6 +32,7 @@ class PostStatusService < BaseService | ||||
|                                         application: options[:application]) | ||||
|       attach_media(status, media) | ||||
|     end | ||||
|  | ||||
|     process_mentions_service.call(status) | ||||
|     process_hashtags_service.call(status) | ||||
|  | ||||
|   | ||||
							
								
								
									
										15
									
								
								spec/helpers/emoji_helper_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								spec/helpers/emoji_helper_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| 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 | ||||
| @@ -1,5 +0,0 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| RSpec.describe RoutingHelper, type: :helper do | ||||
|  | ||||
| end | ||||
| @@ -2,32 +2,6 @@ import { expect } from 'chai'; | ||||
| 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:'); | ||||
|   }); | ||||
| @@ -46,11 +20,6 @@ describe('emojify', () => { | ||||
|     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" />'); | ||||
| @@ -72,12 +41,7 @@ describe('emojify', () => { | ||||
|       '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>'); | ||||
|   }); | ||||
|  | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user