Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/meteor/app/emoji-native/lib/generateEmojiData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const groupToCategory: Record<number, string> = {
};

export type EmojiEntry = {
name: string;
uc_base: string;
uc_output: string;
uc_match: string;
Expand Down Expand Up @@ -73,6 +74,7 @@ function buildEmojiData() {
const hex = hexFromEmoji(emojiData.emoji);

const entry: EmojiEntry = {
name: primaryShortcode,
uc_base: hex,
uc_output: hex,
uc_match: hex,
Expand Down Expand Up @@ -108,6 +110,7 @@ function buildEmojiData() {
const skinHex = hexFromEmoji(skin.emoji);

const skinEntry: EmojiEntry = {
name: primaryShortcode,
uc_base: skinHex,
uc_output: skinHex,
uc_match: skinHex,
Expand Down
72 changes: 35 additions & 37 deletions apps/meteor/app/emoji/client/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,57 +157,55 @@ export const getEmojisBySearchTerm = (
setRecentEmojis: (emojis: string[]) => void,
) => {
const emojis = [];
const seenEmojis = new Set<(typeof emoji.list)[string]>();
const searchRegExp = new RegExp(escapeRegExp(searchTerm.replace(/:/g, '')), 'i');

for (let current in emoji.list) {
if (!emoji.list.hasOwnProperty(current)) {
if (!emoji.list.hasOwnProperty(current) || !searchRegExp.test(current)) {
continue;
}

if (searchRegExp.test(current)) {
const emojiObject = emoji.list[current];
const { emojiPackage, shortnames = [] } = emojiObject;
let tone = '';
current = current.replace(/:/g, '');
const alias = shortnames[0] !== undefined ? shortnames[0].replace(/:/g, '') : shortnames[0];
const emojiObject = emoji.list[current];

if (actualTone > 0 && emoji.packages[emojiPackage].toneList.hasOwnProperty(current)) {
tone = `_tone${actualTone}`;
}
// Skip duplicates (same emoji in different packages)
if (seenEmojis.has(emojiObject)) {
continue;
}
Comment thread
cardoso marked this conversation as resolved.

const mixedTones = current.match(MIXED_TONE_SUFFIX);
const categoryName = current.replace(MIXED_TONE_SUFFIX, '');
if (mixedTones && !(actualTone > 0 && Number(mixedTones[1]) === actualTone)) {
continue;
}
const { emojiPackage, shortnames = [], name } = emojiObject;
let tone = '';
current = current.replace(/:/g, '');

let emojiFound = false;

for (const key in emoji.packages[emojiPackage].emojisByCategory) {
if (emoji.packages[emojiPackage].emojisByCategory.hasOwnProperty(key)) {
const contents = emoji.packages[emojiPackage].emojisByCategory[key];
const searchValArray = alias !== undefined ? alias.replace(/:/g, '').split('_') : alias;
if (contents.indexOf(categoryName) !== -1 || searchValArray?.includes(searchTerm)) {
emojiFound = true;
break;
}
}
}
if (actualTone > 0 && emoji.packages[emojiPackage].toneList.hasOwnProperty(name ?? current)) {
tone = `_tone${actualTone}`;
}

if (emojiFound) {
const emojiToRender = `:${current}${tone}:`;
const mixedTones = current.match(MIXED_TONE_SUFFIX);
const categoryName = current.replace(MIXED_TONE_SUFFIX, '');
if (mixedTones && !(actualTone > 0 && Number(mixedTones[1]) === actualTone)) {
continue;
}

const actualEmoji = emoji.list[emojiToRender];
if (!actualEmoji) {
removeFromRecent(emojiToRender, recentEmojis, setRecentEmojis);
break;
}
const isCategoryEmoji = Object.values(emoji.packages[emojiPackage].emojisByCategory).some(
(contents) => contents.indexOf(categoryName) !== -1,
);
if (!isCategoryEmoji && shortnames.length === 0) {
continue;
}

const actualPackage = actualEmoji.emojiPackage;
const emojiToRender = `:${current}${tone}:`;

emojis.push({ emoji: current, image: emoji.packages[actualPackage].renderPicker(emojiToRender) });
}
const actualEmoji = emoji.list[emojiToRender];
if (!actualEmoji) {
removeFromRecent(emojiToRender, recentEmojis, setRecentEmojis);
continue;
}

seenEmojis.add(emojiObject);

const actualPackage = actualEmoji.emojiPackage;

emojis.push({ emoji: `${current}${tone}`, image: emoji.packages[actualPackage].renderPicker(emojiToRender) });
}

return emojis;
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/app/emoji/lib/rocketchat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type EmojiPackages = {
list: {
[key: keyof NonNullable<EmojiPackages['packages']>]:
| {
name?: string;
category: string;
emojiPackage: string;
shortnames: string[];
Expand All @@ -34,6 +35,7 @@ export type EmojiPackages = {
unicode?: string;
}
| {
name?: undefined;
emojiPackage: string;
aliasOf: string;
extension?: undefined;
Expand Down
70 changes: 68 additions & 2 deletions apps/meteor/tests/unit/app/emoji/helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,80 @@
import { expect } from 'chai';
import { describe, it, beforeEach } from 'mocha';
import { describe, it, beforeEach, before } from 'mocha';

import { updateRecent, removeFromRecent, replaceEmojiInRecent } from '../../../../app/emoji/client/helpers';
import { getEmojisBySearchTerm, updateRecent, removeFromRecent, replaceEmojiInRecent } from '../../../../app/emoji/client/helpers';
import { emoji } from '../../../../app/emoji/client/lib';
import { getEmojiConfig } from '../../../../app/emoji-native/lib/getEmojiConfig';

const registerNativeEmojis = () => {
const config = getEmojiConfig(emoji);

emoji.packages.native = {
emojiCategories: config.emojiCategories as any,
emojisByCategory: config.emojisByCategory,
toneList: config.toneList,
render: config.render,
renderPicker: config.renderPicker,
sprites: config.sprites,
};

for (const [key, currentEmoji] of Object.entries(config.emojiList)) {
currentEmoji.emojiPackage = 'native';
emoji.list[key] = currentEmoji as any;

if (currentEmoji.shortnames) {
currentEmoji.shortnames.forEach((shortname: string) => {
emoji.list[shortname] = currentEmoji as any;
});
}
}
};

describe('Emoji Client Helpers', () => {
beforeEach(() => {
emoji.packages.base.emojisByCategory.recent = [];
});

describe('getEmojisBySearchTerm', () => {
before(registerNativeEmojis);

const search = (term: string) => getEmojisBySearchTerm(term, 0, [], () => undefined);
const names = (term: string) => search(term).map((result) => result.emoji);
const rendersThumbsUp = (term: string) => search(term).some((result) => result.image?.includes('👍'));

it('finds an emoji by its primary shortcode', () => {
expect(names('+1')).to.include('+1');
});

it('finds an emoji by its first alias (thumbsup -> 👍)', () => {
expect(rendersThumbsUp('thumbsup')).to.be.true;
});

it('finds an emoji by a secondary alias (yes -> 👍)', () => {
expect(rendersThumbsUp('yes')).to.be.true;
});

it('matches aliases partially (thumb -> thumbsup)', () => {
expect(names('thumb')).to.include('thumbsup');
});

it('does not list the same emoji more than once', () => {
const images = search('grinning').map((result) => result.image);
expect(images.length).to.equal(new Set(images).size);
});

it('excludes skin-tone variants from the results', () => {
expect(names('+1').some((name) => /_tone[1-5]/.test(name))).to.be.false;
});

it('applies the selected skin tone when searching by an alias', () => {
const result = getEmojisBySearchTerm('thumbsup', 2, [], () => undefined).find(({ image }) => image?.includes('👍'));

expect(result).to.exist;
expect(result?.image).to.include('👍🏼');
expect(result?.emoji).to.equal('thumbsup_tone2');
});
});

describe('updateRecent', () => {
it('should update recent emojis with the provided emojis', () => {
const recentEmojis = ['emoji1', 'emoji2'];
Expand Down
Loading