Skip to content
Open
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 on lines +171 to +173

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="apps/meteor/app/emoji/client/helpers.ts"

# Map the file structure first, then inspect the relevant section.
ast-grep outline "$file" --view expanded || true
wc -l "$file"
sed -n '140,235p' "$file"

Repository: RocketChat/Rocket.Chat

Length of output: 4526


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where emoji.list is defined/populated and inspect the relevant parts.
rg -n "emoji\.list|packages\.base|renderPicker|emojisByCategory|toneList" apps/meteor/app/emoji -g '*.ts' -g '*.js' -g '*.tsx' || true

# Inspect the helper around the dedupe logic and nearby calls.
sed -n '153,235p' apps/meteor/app/emoji/client/helpers.ts

# Inspect the emoji data source if present in the repo.
fd -a "emoji" apps/meteor/app | sed -n '1,40p'

Repository: RocketChat/Rocket.Chat

Length of output: 7018


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect emoji list construction and package loading for object identity / aliasing.
sed -n '1,220p' apps/meteor/app/emoji/client/lib.ts
printf '\n---\n'
sed -n '1,220p' apps/meteor/app/emoji/server/lib.ts
printf '\n---\n'
sed -n '1,260p' apps/meteor/app/emoji/lib/rocketchat.ts
printf '\n---\n'
rg -n "emoji\.list\s*=|list\[[^]]+\]|alias|shortnames|emojiPackage" apps/meteor/app/emoji -g '*.ts' -g '*.js'

Repository: RocketChat/Rocket.Chat

Length of output: 6246


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the category picker path that uses the same emoji lookup pattern.
sed -n '60,120p' apps/meteor/app/emoji/client/helpers.ts

# Show the search helper with line numbers for exact control flow.
cat -n apps/meteor/app/emoji/client/helpers.ts | sed -n '153,210p'

# Look for alias handling in emoji data generation / loading.
rg -n "aliasOf|aliases|shortnames|emojiToRender|seenEmojis" apps/meteor/app/emoji-native apps/meteor/app/emoji-custom apps/meteor/app/emoji/client -g '*.ts' -g '*.js'

Repository: RocketChat/Rocket.Chat

Length of output: 9332


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the emoji data generation/loading path where list entries and aliases are created.
cat -n apps/meteor/app/emoji-native/lib/generateEmojiData.ts | sed -n '1,220p'
printf '\n---\n'
cat -n apps/meteor/app/emoji-native/server/lib.ts | sed -n '1,220p'
printf '\n---\n'
cat -n apps/meteor/app/emoji-custom/server/lib/insertOrUpdateEmoji.ts | sed -n '1,240p'

Repository: RocketChat/Rocket.Chat

Length of output: 12901


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect custom emoji registration to see whether multiple keys can share or replace objects.
rg -n "emoji\.list\[|emojiCustom|aliases|renderPicker" apps/meteor/app/emoji-custom apps/meteor/app/emoji -g '*.ts' -g '*.js'

Repository: RocketChat/Rocket.Chat

Length of output: 4568


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect custom emoji client/server registration code to see how aliases map into emoji.list.
rg -n "setAliases|updateCustom|emoji.list\\[.*alias|emojiCustom" apps/meteor/app -g '*.ts' -g '*.js'

Repository: RocketChat/Rocket.Chat

Length of output: 2012


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the native emoji config path that handles custom overrides.
cat -n apps/meteor/app/emoji-native/lib/getEmojiConfig.ts | sed -n '1,220p'

printf '\n---\n'

# Find direct emoji.list assignments across the emoji codepaths.
rg -n "emoji\.list\s*\[.*\]\s*=" apps/meteor/app/emoji apps/meteor/app/emoji-native apps/meteor/app/emoji-custom -g '*.ts' -g '*.js'

printf '\n---\n'

# Inspect any custom emoji package registration/loading path if present.
fd -a "emoji-custom" apps/meteor/app | sed -n '1,80p'

Repository: RocketChat/Rocket.Chat

Length of output: 4952


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the custom emoji startup/registration path to see how its entries enter emoji.list.
sed -n '1,220p' apps/meteor/app/emoji-custom/server/startup/emoji-custom.js
printf '\n---\n'
rg -n "emoji\.list|EmojiCustom|emoji.updateCustom|aliases" apps/meteor/app/emoji-custom/server -g '*.ts' -g '*.js'

Repository: RocketChat/Rocket.Chat

Length of output: 14383


Deduplicate on the resolved emoji object. emojiObject can differ while actualEmoji is the same (for example, base and tone-specific keys resolving to one render target), so duplicate rows still slip through. Move the seenEmojis check/add to actualEmoji.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/app/emoji/client/helpers.ts` around lines 171 - 173, Update the
deduplication logic in the emoji-processing flow to check and record the
resolved actualEmoji value rather than emojiObject. Move the seenEmojis
check/add so entries resolving to the same actualEmoji are emitted only once,
while preserving the existing row-generation behavior.


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, 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
69 changes: 67 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,79 @@
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('👍🏼');
});
});

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