From 54d76d6b56d90c17bc0ee8ab322fa05367018315 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 10 Apr 2022 15:25:07 -0500 Subject: [PATCH 1/3] Move emoji utils into its own module --- app/soapbox/components/ui/emoji/emoji.tsx | 28 +--------------- app/soapbox/utils/__tests__/emoji.test.ts | 39 +++++++++++++++++++++++ app/soapbox/utils/emoji.ts | 35 ++++++++++++++++++++ 3 files changed, 75 insertions(+), 27 deletions(-) create mode 100644 app/soapbox/utils/__tests__/emoji.test.ts create mode 100644 app/soapbox/utils/emoji.ts diff --git a/app/soapbox/components/ui/emoji/emoji.tsx b/app/soapbox/components/ui/emoji/emoji.tsx index e9bcda2ed..59d00df2d 100644 --- a/app/soapbox/components/ui/emoji/emoji.tsx +++ b/app/soapbox/components/ui/emoji/emoji.tsx @@ -1,34 +1,8 @@ import React from 'react'; +import { removeVS16s, toCodePoints } from 'soapbox/utils/emoji'; import { joinPublicPath } from 'soapbox/utils/static'; -// Taken from twemoji-parser -// https://github.com/twitter/twemoji-parser/blob/a97ef3994e4b88316812926844d51c296e889f76/src/index.js -const removeVS16s = (rawEmoji: string): string => { - const vs16RegExp = /\uFE0F/g; - const zeroWidthJoiner = String.fromCharCode(0x200d); - return rawEmoji.indexOf(zeroWidthJoiner) < 0 ? rawEmoji.replace(vs16RegExp, '') : rawEmoji; -}; - -const toCodePoints = (unicodeSurrogates: string): string[] => { - const points = []; - let char = 0; - let previous = 0; - let i = 0; - while (i < unicodeSurrogates.length) { - char = unicodeSurrogates.charCodeAt(i++); - if (previous) { - points.push((0x10000 + ((previous - 0xd800) << 10) + (char - 0xdc00)).toString(16)); - previous = 0; - } else if (char > 0xd800 && char <= 0xdbff) { - previous = char; - } else { - points.push(char.toString(16)); - } - } - return points; -}; - interface IEmoji extends React.ImgHTMLAttributes { emoji: string, } diff --git a/app/soapbox/utils/__tests__/emoji.test.ts b/app/soapbox/utils/__tests__/emoji.test.ts new file mode 100644 index 000000000..ab1a4ddaa --- /dev/null +++ b/app/soapbox/utils/__tests__/emoji.test.ts @@ -0,0 +1,39 @@ +import { + removeVS16s, + toCodePoints, +} from '../emoji'; + +const ASCII_HEART = '❤'; // '\u2764\uFE0F' +const RED_HEART_RGI = '❤️'; // '\u2764' +const JOY = '😂'; + +describe('removeVS16s()', () => { + it('removes Variation Selector-16 characters from emoji', () => { + // Sanity check + expect(ASCII_HEART).not.toBe(RED_HEART_RGI); + + // It normalizes an emoji with VS16s + expect(removeVS16s(RED_HEART_RGI)).toBe(ASCII_HEART); + + // Leaves a regular emoji alone + expect(removeVS16s(JOY)).toBe(JOY); + }); +}); + +describe('toCodePoints()', () => { + it('converts a plain emoji', () => { + expect(toCodePoints('😂')).toEqual(['1f602']); + }); + + it('converts a VS16 emoji', () => { + expect(toCodePoints(RED_HEART_RGI)).toEqual(['2764', 'fe0f']); + }); + + it('converts an ASCII character', () => { + expect(toCodePoints(ASCII_HEART)).toEqual(['2764']); + }); + + it('converts a sequence emoji', () => { + expect(toCodePoints('🇺🇸')).toEqual(['1f1fa', '1f1f8']); + }); +}); diff --git a/app/soapbox/utils/emoji.ts b/app/soapbox/utils/emoji.ts new file mode 100644 index 000000000..1d6da69d1 --- /dev/null +++ b/app/soapbox/utils/emoji.ts @@ -0,0 +1,35 @@ +// Taken from twemoji-parser +// https://github.com/twitter/twemoji-parser/blob/a97ef3994e4b88316812926844d51c296e889f76/src/index.js + +/** Remove Variation Selector-16 characters from emoji */ +// https://emojipedia.org/variation-selector-16/ +const removeVS16s = (rawEmoji: string): string => { + const vs16RegExp = /\uFE0F/g; + const zeroWidthJoiner = String.fromCharCode(0x200d); + return rawEmoji.indexOf(zeroWidthJoiner) < 0 ? rawEmoji.replace(vs16RegExp, '') : rawEmoji; +}; + +/** Convert emoji into an array of Unicode codepoints */ +const toCodePoints = (unicodeSurrogates: string): string[] => { + const points = []; + let char = 0; + let previous = 0; + let i = 0; + while (i < unicodeSurrogates.length) { + char = unicodeSurrogates.charCodeAt(i++); + if (previous) { + points.push((0x10000 + ((previous - 0xd800) << 10) + (char - 0xdc00)).toString(16)); + previous = 0; + } else if (char > 0xd800 && char <= 0xdbff) { + previous = char; + } else { + points.push(char.toString(16)); + } + } + return points; +}; + +export { + removeVS16s, + toCodePoints, +}; From 1466a081934331c4a37718c3f706e29d1e98c61e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 10 Apr 2022 15:44:51 -0500 Subject: [PATCH 2/3] Perform better normalization of allowedEmoji --- app/soapbox/actions/__tests__/soapbox.test.ts | 19 ++++++++ app/soapbox/actions/soapbox.js | 48 +++++++------------ app/soapbox/utils/features.ts | 2 +- 3 files changed, 37 insertions(+), 32 deletions(-) create mode 100644 app/soapbox/actions/__tests__/soapbox.test.ts diff --git a/app/soapbox/actions/__tests__/soapbox.test.ts b/app/soapbox/actions/__tests__/soapbox.test.ts new file mode 100644 index 000000000..e3dcf9a85 --- /dev/null +++ b/app/soapbox/actions/__tests__/soapbox.test.ts @@ -0,0 +1,19 @@ +import { rootState } from '../../jest/test-helpers'; +import { getSoapboxConfig } from '../soapbox'; + +const ASCII_HEART = '❤'; // '\u2764\uFE0F' +const RED_HEART_RGI = '❤️'; // '\u2764' + +describe('getSoapboxConfig()', () => { + it('returns RGI heart on Pleroma > 2.3', () => { + const state = rootState.setIn(['instance', 'version'], '2.7.2 (compatible; Pleroma 2.3.0)'); + expect(getSoapboxConfig(state).allowedEmoji.includes(RED_HEART_RGI)).toBe(true); + expect(getSoapboxConfig(state).allowedEmoji.includes(ASCII_HEART)).toBe(false); + }); + + it('returns an ASCII heart on Pleroma < 2.3', () => { + const state = rootState.setIn(['instance', 'version'], '2.7.2 (compatible; Pleroma 2.0.0)'); + expect(getSoapboxConfig(state).allowedEmoji.includes(ASCII_HEART)).toBe(true); + expect(getSoapboxConfig(state).allowedEmoji.includes(RED_HEART_RGI)).toBe(false); + }); +}); diff --git a/app/soapbox/actions/soapbox.js b/app/soapbox/actions/soapbox.js index bc3bdc82f..fd08d0300 100644 --- a/app/soapbox/actions/soapbox.js +++ b/app/soapbox/actions/soapbox.js @@ -1,9 +1,9 @@ -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { createSelector } from 'reselect'; import { getHost } from 'soapbox/actions/instance'; import { normalizeSoapboxConfig } from 'soapbox/normalizers'; import KVStore from 'soapbox/storage/kv_store'; +import { removeVS16s } from 'soapbox/utils/emoji'; import { getFeatures } from 'soapbox/utils/features'; import api, { staticClient } from '../api'; @@ -15,38 +15,24 @@ export const SOAPBOX_CONFIG_REMEMBER_REQUEST = 'SOAPBOX_CONFIG_REMEMBER_REQUEST' export const SOAPBOX_CONFIG_REMEMBER_SUCCESS = 'SOAPBOX_CONFIG_REMEMBER_SUCCESS'; export const SOAPBOX_CONFIG_REMEMBER_FAIL = 'SOAPBOX_CONFIG_REMEMBER_FAIL'; -const allowedEmoji = ImmutableList([ - '👍', - '❤', - '😆', - '😮', - '😢', - '😩', -]); - -// https://git.pleroma.social/pleroma/pleroma/-/issues/2355 -const allowedEmojiRGI = ImmutableList([ - '👍', - '❤️', - '😆', - '😮', - '😢', - '😩', -]); - -export const makeDefaultConfig = features => { - return ImmutableMap({ - allowedEmoji: features.emojiReactsRGI ? allowedEmojiRGI : allowedEmoji, - displayFqn: Boolean(features.federating), - }); -}; - export const getSoapboxConfig = createSelector([ - state => state.get('soapbox'), - state => getFeatures(state.get('instance')), + state => state.soapbox, + state => getFeatures(state.instance), ], (soapbox, features) => { - const defaultConfig = makeDefaultConfig(features); - return normalizeSoapboxConfig(soapbox).merge(defaultConfig); + // Do some additional normalization with the state + return normalizeSoapboxConfig(soapbox).withMutations(soapboxConfig => { + + // If displayFqn isn't set, infer it from federation + if (soapbox.get('displayFqn') === undefined) { + soapboxConfig.set('displayFqn', features.federating); + } + + // If RGI reacts aren't supported, strip VS16s + // // https://git.pleroma.social/pleroma/pleroma/-/issues/2355 + if (!features.emojiReactsRGI) { + soapboxConfig.set('allowedEmoji', soapboxConfig.allowedEmoji.map(emoji => removeVS16s(emoji))); + } + }); }); export function rememberSoapboxConfig(host) { diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 4f70f9341..93529f21e 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -83,7 +83,7 @@ const getInstanceFeatures = (instance: Instance) => { chats: v.software === PLEROMA && gte(v.version, '2.1.0'), chatsV2: v.software === PLEROMA && gte(v.version, '2.3.0'), scopes: v.software === PLEROMA ? 'read write follow push admin' : 'read write follow push', - federating: federation.get('enabled', true), // Assume true unless explicitly false + federating: federation.get('enabled', true) === true, // Assume true unless explicitly false richText: v.software === PLEROMA, securityAPI: any([ v.software === PLEROMA, From ae48c6e619cc352c14d6415dee031f87c9d547c7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 10 Apr 2022 16:07:23 -0500 Subject: [PATCH 3/3] Fix action bar emoji labels --- app/soapbox/actions/soapbox.js | 2 +- app/soapbox/components/status_action_bar.tsx | 3 ++- app/soapbox/features/status/components/action-bar.tsx | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/soapbox/actions/soapbox.js b/app/soapbox/actions/soapbox.js index fd08d0300..0b18c9bc1 100644 --- a/app/soapbox/actions/soapbox.js +++ b/app/soapbox/actions/soapbox.js @@ -30,7 +30,7 @@ export const getSoapboxConfig = createSelector([ // If RGI reacts aren't supported, strip VS16s // // https://git.pleroma.social/pleroma/pleroma/-/issues/2355 if (!features.emojiReactsRGI) { - soapboxConfig.set('allowedEmoji', soapboxConfig.allowedEmoji.map(emoji => removeVS16s(emoji))); + soapboxConfig.set('allowedEmoji', soapboxConfig.allowedEmoji.map(removeVS16s)); } }); }); diff --git a/app/soapbox/components/status_action_bar.tsx b/app/soapbox/components/status_action_bar.tsx index 549c2f4ba..9011bec71 100644 --- a/app/soapbox/components/status_action_bar.tsx +++ b/app/soapbox/components/status_action_bar.tsx @@ -578,9 +578,10 @@ class StatusActionBar extends ImmutablePureComponent { '😮': messages.reactionOpenMouth, '😢': messages.reactionCry, '😩': messages.reactionWeary, + '': messages.favourite, }; - const meEmojiTitle = intl.formatMessage(meEmojiReact ? reactMessages[meEmojiReact] : messages.favourite); + const meEmojiTitle = intl.formatMessage(reactMessages[meEmojiReact || ''] || messages.favourite); const menu: Menu = [];