diff --git a/app/soapbox/features/emoji/__tests__/emoji.test.ts b/app/soapbox/features/emoji/__tests__/emoji.test.ts index ffe1b1b1f..cd849d0f9 100644 --- a/app/soapbox/features/emoji/__tests__/emoji.test.ts +++ b/app/soapbox/features/emoji/__tests__/emoji.test.ts @@ -51,12 +51,12 @@ describe('emoji', () => { }); it('does an emoji that has no shortcode', () => { - expect(emojify('๐Ÿ‘โ€๐Ÿ—จ')).toEqual('๐Ÿ‘โ€๐Ÿ—จ๏ธ\'); + expect(emojify('๐Ÿ‘โ€๐Ÿ—จ')).toEqual('๐Ÿ‘โ€๐Ÿ—จ'); }); it('skips the textual presentation VS15 character', () => { expect(emojify('โœด๏ธŽ')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15 - .toEqual('โœด๏ธŽ'); + .toEqual('โœด๏ธ'); }); it('full v14 unicode emoji map', () => { diff --git a/app/soapbox/features/emoji/__tests__/emoji_index.test.ts b/app/soapbox/features/emoji/__tests__/emoji_index.test.ts index bf059d204..d01028cdb 100644 --- a/app/soapbox/features/emoji/__tests__/emoji_index.test.ts +++ b/app/soapbox/features/emoji/__tests__/emoji_index.test.ts @@ -1,7 +1,7 @@ -// import { emojiIndex } from 'emoji-mart'; +import { List, Map } from 'immutable'; import pick from 'lodash/pick'; -import search from '../search'; +import search, { addCustomToPool } from '../search'; const trimEmojis = (emoji: any) => pick(emoji, ['id', 'unified', 'native', 'custom']); @@ -38,72 +38,60 @@ describe('emoji_index', () => { expect(search('apple').map(trimEmojis)).toEqual(expected); }); - it('(different behavior from emoji-mart) do not erases custom emoji if not passed again', () => { + it('handles custom emojis', () => { const custom = [ { id: 'mastodon', name: 'mastodon', - short_names: ['mastodon'], - text: '', - emoticons: [], keywords: ['mastodon'], - imageUrl: 'http://example.com', - custom: true, + skins: { src: 'http://example.com' }, }, ]; - search('', { custom }); - // emojiIndex.search('', { custom }); - // const expected = []; + + const custom_emojis = List([ + Map({ static_url: 'http://example.com', shortcode: 'mastodon' }), + ]); + const lightExpected = [ { id: 'mastodon', custom: true, }, ]; - expect(search('masto').map(trimEmojis)).toEqual(lightExpected); - }); - it('(different behavior from emoji-mart) erases custom emoji if another is passed', () => { - const custom = [ - { - id: 'mastodon', - name: 'mastodon', - short_names: ['mastodon'], - text: '', - emoticons: [], - keywords: ['mastodon'], - imageUrl: 'http://example.com', - custom: true, - }, - ]; - search('', { custom }); - // emojiIndex.search('', { custom }); - const expected = []; - expect(search('masto', { custom: [] }).map(trimEmojis)).toEqual(expected); + addCustomToPool(custom); + expect(search('masto', {}, custom_emojis).map(trimEmojis)).toEqual(lightExpected); }); - it('handles custom emoji', () => { + it('updates custom emoji if another is passed', () => { const custom = [ { id: 'mastodon', name: 'mastodon', - short_names: ['mastodon'], - text: '', - emoticons: [], keywords: ['mastodon'], - imageUrl: 'http://example.com', - custom: true, + skins: { src: 'http://example.com' }, }, ]; - search('', { custom }); - // emojiIndex.search('', { custom }); - const expected = [ + + addCustomToPool(custom); + + const custom2 = [ { - id: 'mastodon', - custom: true, + id: 'pleroma', + name: 'pleroma', + keywords: ['pleroma'], + skins: { src: 'http://example.com' }, }, ]; - expect(search('masto', { custom }).map(trimEmojis)).toEqual(expected); + + addCustomToPool(custom2); + + const custom_emojis = List([ + Map({ static_url: 'http://example.com', shortcode: 'pleroma' }), + ]); + + const expected = []; + expect(search('masto', {}, custom_emojis).map(trimEmojis)).toEqual(expected); }); it('does an emoji whose unified name is irregular', () => { diff --git a/app/soapbox/features/emoji/index.ts b/app/soapbox/features/emoji/index.ts index b31c85266..c7c32451f 100644 --- a/app/soapbox/features/emoji/index.ts +++ b/app/soapbox/features/emoji/index.ts @@ -99,7 +99,11 @@ export const emojifyText = (str: string, customEmojis = {}) => { stack = ''; }; - for (const c of split(str)) { + for (let c of split(str)) { + if (c.codePointAt(c.length - 1) === 65038) { + c = c.slice(0, -1) + String.fromCodePoint(65039); + } + const unqualified = c + String.fromCodePoint(65039); if (c in unicodeMapping) { diff --git a/app/soapbox/features/emoji/mapping.ts b/app/soapbox/features/emoji/mapping.ts index b8c239535..baf16590d 100644 --- a/app/soapbox/features/emoji/mapping.ts +++ b/app/soapbox/features/emoji/mapping.ts @@ -20,14 +20,13 @@ interface UnicodeMap { * - fe0f is NOT removed for 1f441-fe0f-200d-1f5e8-fe0f even though it has a 200d * * this is all wrong - */ + */ const blacklist = { '1f441-fe0f-200d-1f5e8-fe0f': true, }; const tweaks = { - '๐Ÿ‘โ€๐Ÿ—จ๏ธ': ['1f441-200d-1f5e8', 'eye-in-speech-bubble'], '#โƒฃ': ['23-20e3', 'hash'], '*โƒฃ': ['2a-20e3', 'keycap_star'], '0โƒฃ': ['30-20e3', 'zero'], @@ -40,37 +39,68 @@ const tweaks = { '7โƒฃ': ['37-20e3', 'seven'], '8โƒฃ': ['38-20e3', 'eight'], '9โƒฃ': ['39-20e3', 'nine'], - '๐Ÿณโ€๐ŸŒˆ': ['1f3f3-fe0f-200d-1f308', 'rainbow-flag'], + 'โคโ€๐Ÿ”ฅ': ['2764-fe0f-200d-1f525', 'heart_on_fire'], + 'โคโ€๐Ÿฉน': ['2764-fe0f-200d-1fa79', 'mending_heart'], + '๐Ÿ‘โ€๐Ÿ—จ๏ธ': ['1f441-fe0f-200d-1f5e8-fe0f', 'eye-in-speech-bubble'], + '๐Ÿ‘๏ธโ€๐Ÿ—จ': ['1f441-fe0f-200d-1f5e8-fe0f', 'eye-in-speech-bubble'], + '๐Ÿ‘โ€๐Ÿ—จ': ['1f441-fe0f-200d-1f5e8-fe0f', 'eye-in-speech-bubble'], + '๐Ÿ•ตโ€โ™‚๏ธ': ['1f575-fe0f-200d-2642-fe0f', 'male-detective'], + '๐Ÿ•ต๏ธโ€โ™‚': ['1f575-fe0f-200d-2642-fe0f', 'male-detective'], + '๐Ÿ•ตโ€โ™‚': ['1f575-fe0f-200d-2642-fe0f', 'male-detective'], + '๐Ÿ•ตโ€โ™€๏ธ': ['1f575-fe0f-200d-2640-fe0f', 'female-detective'], + '๐Ÿ•ต๏ธโ€โ™€': ['1f575-fe0f-200d-2640-fe0f', 'female-detective'], + '๐Ÿ•ตโ€โ™€': ['1f575-fe0f-200d-2640-fe0f', 'female-detective'], + '๐ŸŒโ€โ™‚๏ธ': ['1f3cc-fe0f-200d-2642-fe0f', 'man-golfing'], + '๐ŸŒ๏ธโ€โ™‚': ['1f3cc-fe0f-200d-2642-fe0f', 'man-golfing'], + '๐ŸŒโ€โ™‚': ['1f3cc-fe0f-200d-2642-fe0f', 'man-golfing'], + '๐ŸŒโ€โ™€๏ธ': ['1f3cc-fe0f-200d-2640-fe0f', 'woman-golfing'], + '๐ŸŒ๏ธโ€โ™€': ['1f3cc-fe0f-200d-2640-fe0f', 'woman-golfing'], + '๐ŸŒโ€โ™€': ['1f3cc-fe0f-200d-2640-fe0f', 'woman-golfing'], + 'โ›นโ€โ™‚๏ธ': ['26f9-fe0f-200d-2642-fe0f', 'man-bouncing-ball'], + 'โ›น๏ธโ€โ™‚': ['26f9-fe0f-200d-2642-fe0f', 'man-bouncing-ball'], + 'โ›นโ€โ™‚': ['26f9-fe0f-200d-2642-fe0f', 'man-bouncing-ball'], + 'โ›นโ€โ™€๏ธ': ['26f9-fe0f-200d-2640-fe0f', 'woman-bouncing-ball'], + 'โ›น๏ธโ€โ™€': ['26f9-fe0f-200d-2640-fe0f', 'woman-bouncing-ball'], + 'โ›นโ€โ™€': ['26f9-fe0f-200d-2640-fe0f', 'woman-bouncing-ball'], + '๐Ÿ‹โ€โ™‚๏ธ': ['1f3cb-fe0f-200d-2642-fe0f', 'man-lifting-weights'], + '๐Ÿ‹๏ธโ€โ™‚': ['1f3cb-fe0f-200d-2642-fe0f', 'man-lifting-weights'], + '๐Ÿ‹โ€โ™‚': ['1f3cb-fe0f-200d-2642-fe0f', 'man-lifting-weights'], + '๐Ÿ‹โ€โ™€๏ธ': ['1f3cb-fe0f-200d-2640-fe0f', 'woman-lifting-weights'], + '๐Ÿ‹๏ธโ€โ™€': ['1f3cb-fe0f-200d-2640-fe0f', 'woman-lifting-weights'], + '๐Ÿ‹โ€โ™€': ['1f3cb-fe0f-200d-2640-fe0f', 'woman-lifting-weights'], + '๐Ÿณโ€๐ŸŒˆ': ['1f3f3-fe0f-200d-1f308', 'rainbow_flag'], '๐Ÿณโ€โšง๏ธ': ['1f3f3-fe0f-200d-26a7-fe0f', 'transgender_flag'], + '๐Ÿณ๏ธโ€โšง': ['1f3f3-fe0f-200d-26a7-fe0f', 'transgender_flag'], '๐Ÿณโ€โšง': ['1f3f3-fe0f-200d-26a7-fe0f', 'transgender_flag'], - 'โœด๏ธŽ': ['2734', 'eight_pointed_black_star'], +}; + +const stripcodes = (unified: string, native: string) => { + const stripped = unified.replace(stripLeadingZeros, ''); + + if (unified.includes('200d') && !(unified in blacklist)) { + return stripped; + } else { + return replaceAll(stripped, '-fe0f', ''); + } }; export const generateMappings = (data: EmojiData): UnicodeMap => { - const result = {}; + const result: UnicodeMap = {}; const emojis = Object.values(data.emojis ?? {}); for (const value of emojis) { - // @ts-ignore for (const item of value.skins) { const { unified, native } = item; - const stripped = unified.replace(stripLeadingZeros, ''); - - if (unified.includes('200d') && !(unified in blacklist)) { - // @ts-ignore - result[native] = { unified: stripped, shortcode: value.id }; - } else { - const twemojiCode = replaceAll(stripped, '-fe0f', '').replace('fe0e', ''); + const stripped = stripcodes(unified, native); - // @ts-ignore - result[native] = { unified: twemojiCode, shortcode: value.id }; - } + result[native] = { unified: stripped, shortcode: value.id }; } } - for (const [key, value] of Object.entries(tweaks)) { - // @ts-ignore - result[key] = { unified: value[0], shortcode: value[1] }; + for (const [native, [unified, shortcode]] of Object.entries(tweaks)) { + const stripped = stripcodes(unified, native); + + result[native] = { unified: stripped, shortcode }; } return result;