kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'next-emoji-reacts' into 'next'
Next: Improve emoji utils and normalization See merge request soapbox-pub/soapbox-fe!1201next-virtuoso-proof
commit
2ea13e2582
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,9 +1,9 @@
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import { getHost } from 'soapbox/actions/instance';
|
import { getHost } from 'soapbox/actions/instance';
|
||||||
import { normalizeSoapboxConfig } from 'soapbox/normalizers';
|
import { normalizeSoapboxConfig } from 'soapbox/normalizers';
|
||||||
import KVStore from 'soapbox/storage/kv_store';
|
import KVStore from 'soapbox/storage/kv_store';
|
||||||
|
import { removeVS16s } from 'soapbox/utils/emoji';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
|
||||||
import api, { staticClient } from '../api';
|
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_SUCCESS = 'SOAPBOX_CONFIG_REMEMBER_SUCCESS';
|
||||||
export const SOAPBOX_CONFIG_REMEMBER_FAIL = 'SOAPBOX_CONFIG_REMEMBER_FAIL';
|
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([
|
export const getSoapboxConfig = createSelector([
|
||||||
state => state.get('soapbox'),
|
state => state.soapbox,
|
||||||
state => getFeatures(state.get('instance')),
|
state => getFeatures(state.instance),
|
||||||
], (soapbox, features) => {
|
], (soapbox, features) => {
|
||||||
const defaultConfig = makeDefaultConfig(features);
|
// Do some additional normalization with the state
|
||||||
return normalizeSoapboxConfig(soapbox).merge(defaultConfig);
|
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(removeVS16s));
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export function rememberSoapboxConfig(host) {
|
export function rememberSoapboxConfig(host) {
|
||||||
|
|
|
@ -578,9 +578,10 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
|
||||||
'😮': messages.reactionOpenMouth,
|
'😮': messages.reactionOpenMouth,
|
||||||
'😢': messages.reactionCry,
|
'😢': messages.reactionCry,
|
||||||
'😩': messages.reactionWeary,
|
'😩': messages.reactionWeary,
|
||||||
|
'': messages.favourite,
|
||||||
};
|
};
|
||||||
|
|
||||||
const meEmojiTitle = intl.formatMessage(meEmojiReact ? reactMessages[meEmojiReact] : messages.favourite);
|
const meEmojiTitle = intl.formatMessage(reactMessages[meEmojiReact || ''] || messages.favourite);
|
||||||
|
|
||||||
const menu = this._makeMenu(publicStatus);
|
const menu = this._makeMenu(publicStatus);
|
||||||
let reblogIcon = require('@tabler/icons/icons/repeat.svg');
|
let reblogIcon = require('@tabler/icons/icons/repeat.svg');
|
||||||
|
|
|
@ -1,34 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { removeVS16s, toCodePoints } from 'soapbox/utils/emoji';
|
||||||
import { joinPublicPath } from 'soapbox/utils/static';
|
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<HTMLImageElement> {
|
interface IEmoji extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||||
emoji: string,
|
emoji: string,
|
||||||
}
|
}
|
||||||
|
|
|
@ -355,9 +355,10 @@ class ActionBar extends React.PureComponent<IActionBar, IActionBarState> {
|
||||||
'😮': messages.reactionOpenMouth,
|
'😮': messages.reactionOpenMouth,
|
||||||
'😢': messages.reactionCry,
|
'😢': messages.reactionCry,
|
||||||
'😩': messages.reactionWeary,
|
'😩': messages.reactionWeary,
|
||||||
|
'': messages.favourite,
|
||||||
};
|
};
|
||||||
|
|
||||||
const meEmojiTitle = intl.formatMessage(meEmojiReact ? reactMessages[meEmojiReact] : messages.favourite);
|
const meEmojiTitle = intl.formatMessage(reactMessages[meEmojiReact || ''] || messages.favourite);
|
||||||
|
|
||||||
const menu: Menu = [];
|
const menu: Menu = [];
|
||||||
|
|
||||||
|
|
|
@ -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']);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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,
|
||||||
|
};
|
|
@ -83,7 +83,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
chats: v.software === PLEROMA && gte(v.version, '2.1.0'),
|
chats: v.software === PLEROMA && gte(v.version, '2.1.0'),
|
||||||
chatsV2: v.software === PLEROMA && gte(v.version, '2.3.0'),
|
chatsV2: v.software === PLEROMA && gte(v.version, '2.3.0'),
|
||||||
scopes: v.software === PLEROMA ? 'read write follow push admin' : 'read write follow push',
|
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,
|
richText: v.software === PLEROMA,
|
||||||
securityAPI: any([
|
securityAPI: any([
|
||||||
v.software === PLEROMA,
|
v.software === PLEROMA,
|
||||||
|
|
Ładowanie…
Reference in New Issue