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
5 changes: 5 additions & 0 deletions .changeset/thirty-cameras-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Improves the performance of the Emoji Picker.
5 changes: 5 additions & 0 deletions apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const deleteEmojiCustom = (emojiData: IEmoji) => {
}

removeFromRecent(emojiData.name, emoji.packages.base.emojisByCategory.recent);
emoji.dispatchUpdate();
};

export const updateEmojiCustom = (emojiData: IEmoji) => {
Expand Down Expand Up @@ -93,6 +94,8 @@ export const updateEmojiCustom = (emojiData: IEmoji) => {
if (previousExists) {
replaceEmojiInRecent({ oldEmoji: emojiData.previousName, newEmoji: emojiData.name });
}

emoji.dispatchUpdate();
};

const customRender = (html: string) => {
Expand All @@ -103,6 +106,7 @@ const customRender = (html: string) => {
`<object[^>]*>.*?<\/object>|<span[^>]*>.*?<\/span>|<(?:object|embed|svg|img|div|span|p|a)[^>]*>|(${emojisMatchGroup})`,
'gi',
);
emoji.dispatchUpdate();
}

html = html.replace(emoji.packages.emojiCustom._regexp!, (shortname) => {
Expand Down Expand Up @@ -160,6 +164,7 @@ Meteor.startup(() => {
};
}
}
emoji.dispatchUpdate();
} catch (e) {
console.error('Error getting custom emoji', e);
}
Expand Down
3 changes: 3 additions & 0 deletions apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ export const useEmojiOne = () => {
}
}
}
emoji.dispatchUpdate();
});
}, []);

useEffect(() => {
if (emoji.packages.emojione) {
// Additional settings -- ascii emojis
Expand All @@ -51,6 +53,7 @@ export const useEmojiOne = () => {
};

void ascii();
emoji.dispatchUpdate();
}
}, [convertAsciiToEmoji]);
};
77 changes: 62 additions & 15 deletions apps/meteor/app/emoji/client/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,69 @@
import { escapeRegExp } from '@rocket.chat/string-helpers';
import type { TranslationKey } from '@rocket.chat/ui-contexts';

import type { EmojiCategory, EmojiItem } from '.';
import { emoji } from './lib';
import { emoji, emojiEmitter } from './lib';

export const CUSTOM_CATEGORY = 'rocket';

type RowItem = Array<EmojiItem & { category: string }>;
type RowDivider = { category: string; i18n: TranslationKey };
type LoadMoreItem = { loadMore: true };
export type EmojiPickerItem = RowItem | RowDivider | LoadMoreItem;

export type CategoriesIndexes = { key: string; index: number }[];

export const isRowDivider = (item: EmojiPickerItem): item is RowDivider => 'i18n' in item;
export const isLoadMore = (item: EmojiPickerItem): item is LoadMoreItem => 'loadMore' in item;

export const createEmojiListByCategorySubscription = (
customItemsLimit: number,
actualTone: number,
recentEmojis: string[],
setRecentEmojis: (emojis: string[]) => void,
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ReturnType<typeof createPickerEmojis>] => {
let result: ReturnType<typeof createPickerEmojis> = [[], []];
updateRecent(recentEmojis);

const sub = (cb: () => void) => {
result = createPickerEmojis(customItemsLimit, actualTone, recentEmojis, setRecentEmojis);

return emojiEmitter.on('updated', () => {
result = createPickerEmojis(customItemsLimit, actualTone, recentEmojis, setRecentEmojis);
cb();
});
};

return [sub, () => result];
};

export const createPickerEmojis = (
customItemsLimit: number,
actualTone: number,
recentEmojis: string[],
setRecentEmojis: (emojis: string[]) => void,
) => {
): [EmojiPickerItem[], CategoriesIndexes] => {
const categories = getCategoriesList();
const categoriesIndexes: CategoriesIndexes = [];

const mappedCategories = categories.map((category) => ({
key: category.key,
i18n: category.i18n,
emojis: {
list: createEmojiList(category.key, actualTone, recentEmojis, setRecentEmojis),
limit: category.key === CUSTOM_CATEGORY ? customItemsLimit : null,
},
}));
const mappedCategories = categories.reduce<EmojiPickerItem[]>((acc, category) => {
categoriesIndexes.push({ key: category.key, index: acc.length });
acc.push({ category: category.key, i18n: category.i18n });
acc.push(...createEmojiList(customItemsLimit, category.key, actualTone, recentEmojis, setRecentEmojis));
return acc;
}, []);

return mappedCategories;
return [mappedCategories, categoriesIndexes];
};

export const createEmojiList = (
customItemsLimit: number,
category: string,
actualTone: number | null,
recentEmojis: string[],
setRecentEmojis: (emojis: string[]) => void,
) => {
const emojiList: EmojiItem[] = [];
): (RowItem | LoadMoreItem)[] => {
const items: RowItem = [];
const emojiPackages = Object.values(emoji.packages);

emojiPackages.forEach((emojiPackage) => {
Expand All @@ -57,11 +89,23 @@ export const createEmojiList = (
if (!image) {
continue;
}
emojiList.push({ emoji: current, image });
items.push({ emoji: current, image, category });
}
});

return emojiList;
const rowCount = 9;
const rowList: Array<RowItem | LoadMoreItem> = Array.from({ length: Math.ceil(items.length / rowCount) }).map(() => []);

for (let i = 0; i < rowList.length; i++) {
const row = items.slice(i * rowCount, i * rowCount + rowCount);
rowList[i] = row;
}

if (category === CUSTOM_CATEGORY && customItemsLimit < items.length) {
rowList.push({ loadMore: true });
}

return rowList;
};

export const getCategoriesList = () => {
Expand Down Expand Up @@ -149,6 +193,8 @@ export const removeFromRecent = (emoji: string, recentEmojis: string[], setRecen
setRecentEmojis?.(recentEmojis);
};

// There's no need to dispatchUpdate here. This helper is called before the list is generated.
// This means that the recent list will always be up to date by the time it is used.
export const updateRecent = (recentList: string[]) => {
const recentPkgList: string[] = emoji.packages.base.emojisByCategory.recent;
recentList?.forEach((_emoji) => {
Expand All @@ -162,6 +208,7 @@ export const replaceEmojiInRecent = ({ oldEmoji, newEmoji }: { oldEmoji: string;

if (pos !== -1) {
recentPkgList[pos] = newEmoji;
emoji.dispatchUpdate();
}
};

Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/emoji/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './helpers';
export * from './types';
export { emoji } from './lib';
export { emoji, emojiEmitter } from './lib';
8 changes: 7 additions & 1 deletion apps/meteor/app/emoji/client/lib.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Emitter } from '@rocket.chat/emitter';
import emojione from 'emojione';

import type { EmojiPackages } from '../lib/rocketchat';

export const emoji: EmojiPackages = {
export const emojiEmitter = new Emitter<{ updated: void }>();

export const emoji: EmojiPackages & { dispatchUpdate: () => void } = {
packages: {
base: {
emojiCategories: [{ key: 'recent', i18n: 'Frequently_Used' }],
Expand All @@ -23,4 +26,7 @@ export const emoji: EmojiPackages = {
},
},
list: {},
dispatchUpdate() {
emojiEmitter.emit('updated');
},
};
20 changes: 7 additions & 13 deletions apps/meteor/client/contexts/EmojiPickerContext.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import type { MutableRefObject } from 'react';
import { createContext, useContext } from 'react';

import type { EmojiByCategory } from '../../app/emoji/client';

type EmojiCategoryPosition = {
key: string;
top: number;
};
import type { EmojiPickerItem, CategoriesIndexes } from '../../app/emoji/client';

type EmojiPickerContextValue = {
open: (ref: Element, callback: (emoji: string) => void) => void;
Expand All @@ -16,17 +10,17 @@ type EmojiPickerContextValue = {
handlePreview: (emoji: string, name: string) => void;
handleRemovePreview: () => void;
addRecentEmoji: (emoji: string) => void;
getEmojiListsByCategory: () => EmojiByCategory[];
emojiListByCategory: EmojiPickerItem[];
recentEmojis: string[];
setRecentEmojis: (emoji: string[]) => void;
actualTone: number;
currentCategory: string;
setCurrentCategory: (category: string) => void;
categoriesPosition: MutableRefObject<EmojiCategoryPosition[]>;
customItemsLimit: number;
setCustomItemsLimit: (limit: number) => void;
setActualTone: (tone: number) => void;
quickReactions: { emoji: string; image: string }[];
categoriesIndexes: CategoriesIndexes;
};

export const EmojiPickerContext = createContext<EmojiPickerContextValue | undefined>(undefined);
Expand Down Expand Up @@ -56,9 +50,9 @@ export const useEmojiPickerData = () => {
actualTone,
addRecentEmoji,
currentCategory,
categoriesPosition,
categoriesIndexes,
customItemsLimit,
getEmojiListsByCategory,
emojiListByCategory,
quickReactions,
recentEmojis,
setActualTone,
Expand All @@ -69,12 +63,12 @@ export const useEmojiPickerData = () => {

return {
addRecentEmoji,
getEmojiListsByCategory,
emojiListByCategory,
recentEmojis,
setRecentEmojis,
actualTone,
currentCategory,
categoriesPosition,
categoriesIndexes,
setCurrentCategory,
customItemsLimit,
setCustomItemsLimit,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
import { useDebouncedState, useLocalStorage } from '@rocket.chat/fuselage-hooks';
import type { ReactNode, ReactElement, ContextType } from 'react';
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
import { useState, useCallback, useMemo, useSyncExternalStore } from 'react';

import { useUpdateCustomEmoji } from './useUpdateCustomEmoji';
import type { EmojiByCategory } from '../../../app/emoji/client';
import { emoji, getFrequentEmoji, updateRecent, createEmojiList, createPickerEmojis, CUSTOM_CATEGORY } from '../../../app/emoji/client';
import { emoji, getFrequentEmoji, createEmojiListByCategorySubscription } from '../../../app/emoji/client';
import { EmojiPickerContext } from '../../contexts/EmojiPickerContext';
import EmojiPicker from '../../views/composer/EmojiPicker/EmojiPicker';

const DEFAULT_ITEMS_LIMIT = 90;

// limit recent emojis to 27 (3 rows of 9)
const RECENT_EMOJIS_LIMIT = 27;

const EmojiPickerProvider = ({ children }: { children: ReactNode }): ReactElement => {
const [emojiPicker, setEmojiPicker] = useState<ReactElement | null>(null);
const [emojiToPreview, setEmojiToPreview] = useDebouncedState<{ emoji: string; name: string } | null>(null, 100);
const [recentEmojis, setRecentEmojis] = useLocalStorage<string[]>('emoji.recent', []);
const [frequentEmojis, setFrequentEmojis] = useLocalStorage<[string, number][]>('emoji.frequent', []);

const [actualTone, setActualTone] = useLocalStorage('emoji.tone', 0);
const [currentCategory, setCurrentCategory] = useState('recent');
const categoriesPosition = useRef([]);

const [customItemsLimit, setCustomItemsLimit] = useState(DEFAULT_ITEMS_LIMIT);

const [frequentEmojis, setFrequentEmojis] = useLocalStorage<[string, number][]>('emoji.frequent', []);

const [quickReactions, setQuickReactions] = useState<{ emoji: string; image: string }[]>(() =>
getFrequentEmoji(frequentEmojis.map(([emoji]) => emoji)),
);

const [sub, getSnapshot] = useMemo(() => {
return createEmojiListByCategorySubscription(customItemsLimit, actualTone, recentEmojis, setRecentEmojis);
}, [customItemsLimit, actualTone, recentEmojis, setRecentEmojis]);

const [emojiListByCategory, categoriesIndexes] = useSyncExternalStore(sub, getSnapshot);

useUpdateCustomEmoji();

const addFrequentEmojis = useCallback(
Expand All @@ -44,37 +51,6 @@ const EmojiPickerProvider = ({ children }: { children: ReactNode }): ReactElemen
[frequentEmojis, setFrequentEmojis],
);

const [getEmojiListsByCategory, setEmojiListsByCategoryGetter] = useState<() => EmojiByCategory[]>(() => () => []);

// TODO: improve this update
const updateEmojiListByCategory = useCallback(
(categoryKey: string, limit: number = DEFAULT_ITEMS_LIMIT) => {
setEmojiListsByCategoryGetter(
(getEmojiListsByCategory) => () =>
getEmojiListsByCategory().map((category) =>
categoryKey === category.key
? {
...category,
emojis: {
list: createEmojiList(category.key, null, recentEmojis, setRecentEmojis),
limit: category.key === CUSTOM_CATEGORY ? limit | customItemsLimit : null,
},
}
: category,
),
);
},
[customItemsLimit, recentEmojis, setRecentEmojis],
);

useEffect(() => {
if (recentEmojis?.length > 0) {
updateRecent(recentEmojis);
}

setEmojiListsByCategoryGetter(() => () => createPickerEmojis(customItemsLimit, actualTone, recentEmojis, setRecentEmojis));
}, [actualTone, recentEmojis, customItemsLimit, currentCategory, setRecentEmojis, frequentEmojis]);

const addRecentEmoji = useCallback(
(_emoji: string) => {
addFrequentEmojis(_emoji);
Expand All @@ -88,14 +64,13 @@ const EmojiPickerProvider = ({ children }: { children: ReactNode }): ReactElemen

recent.unshift(_emoji);

// limit recent emojis to 27 (3 rows of 9)
recent.splice(27);
recent.splice(RECENT_EMOJIS_LIMIT);

setRecentEmojis(recent);
// If this value is not cloned, the recent list will not be updated
setRecentEmojis([...recent]);
emoji.packages.base.emojisByCategory.recent = recent;
updateEmojiListByCategory('recent');
},
[recentEmojis, setRecentEmojis, updateEmojiListByCategory, addFrequentEmojis],
[recentEmojis, setRecentEmojis, addFrequentEmojis],
);

const open = useCallback((ref: Element, callback: (emoji: string) => void) => {
Expand All @@ -115,13 +90,13 @@ const EmojiPickerProvider = ({ children }: { children: ReactNode }): ReactElemen
handlePreview,
handleRemovePreview,
addRecentEmoji,
getEmojiListsByCategory,
emojiListByCategory,
recentEmojis,
setRecentEmojis,
actualTone,
currentCategory,
setCurrentCategory,
categoriesPosition,
categoriesIndexes,
customItemsLimit,
setCustomItemsLimit,
setActualTone,
Expand All @@ -132,7 +107,8 @@ const EmojiPickerProvider = ({ children }: { children: ReactNode }): ReactElemen
open,
emojiToPreview,
addRecentEmoji,
getEmojiListsByCategory,
emojiListByCategory,
categoriesIndexes,
recentEmojis,
setRecentEmojis,
actualTone,
Expand Down
Loading
Loading