snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
249
apps/meteor/client/views/composer/EmojiPicker/EmojiPicker.tsx
Normal file
249
apps/meteor/client/views/composer/EmojiPicker/EmojiPicker.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import { TextInput, Icon, Button, Divider } from '@rocket.chat/fuselage';
|
||||
import { useMediaQuery, useMergedRefs, useOutsideClick } from '@rocket.chat/fuselage-hooks';
|
||||
import {
|
||||
EmojiPickerCategoryHeader,
|
||||
EmojiPickerContainer,
|
||||
EmojiPickerPreviewArea,
|
||||
EmojiPickerHeader,
|
||||
EmojiPickerListArea,
|
||||
EmojiPickerPreview,
|
||||
} from '@rocket.chat/ui-client';
|
||||
import { useTranslation, usePermission, useRoute } from '@rocket.chat/ui-contexts';
|
||||
import type { ChangeEvent, KeyboardEvent, MouseEvent, RefObject } from 'react';
|
||||
import { useLayoutEffect, useState, useEffect, useRef } from 'react';
|
||||
import type { ListRange, VirtuosoHandle } from 'react-virtuoso';
|
||||
|
||||
import CategoriesResult from './CategoriesResult';
|
||||
import EmojiPickerCategoryItem from './EmojiPickerCategoryItem';
|
||||
import EmojiPickerDropdown from './EmojiPickerDropDown';
|
||||
import SearchingResult from './SearchingResult';
|
||||
import ToneSelector from './ToneSelector';
|
||||
import ToneSelectorWrapper from './ToneSelector/ToneSelectorWrapper';
|
||||
import { usePreviewEmoji, useEmojiPickerData } from '../../../contexts/EmojiPickerContext';
|
||||
import { emoji, getCategoriesList, getEmojisBySearchTerm } from '../../../lib/emoji';
|
||||
import type { EmojiItem } from '../../../lib/emoji';
|
||||
import { useIsVisible } from '../../room/hooks/useIsVisible';
|
||||
|
||||
export type EmojiPickerProps = {
|
||||
reference: Element;
|
||||
onClose: () => void;
|
||||
onPickEmoji: (emoji: string) => void;
|
||||
};
|
||||
|
||||
const EmojiPicker = ({ reference, onClose, onPickEmoji }: EmojiPickerProps) => {
|
||||
const t = useTranslation();
|
||||
|
||||
const ref = useRef<Element | null>(reference);
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||
const emojiContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isVisibleRef, isInputVisible] = useIsVisible();
|
||||
const textInputRef = useRef<HTMLInputElement>(undefined);
|
||||
|
||||
const mergedTextInputRef = useMergedRefs(isVisibleRef, textInputRef);
|
||||
|
||||
const emojiCategories = getCategoriesList();
|
||||
|
||||
const canManageEmoji = usePermission('manage-emoji');
|
||||
const customEmojiRoute = useRoute('emoji-custom');
|
||||
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [searchResults, setSearchResults] = useState<EmojiItem[]>([]);
|
||||
|
||||
const { emojiToPreview, handleRemovePreview } = usePreviewEmoji();
|
||||
const {
|
||||
recentEmojis,
|
||||
setCurrentCategory,
|
||||
addRecentEmoji,
|
||||
setRecentEmojis,
|
||||
actualTone,
|
||||
currentCategory,
|
||||
categoriesIndexes,
|
||||
emojiListByCategory,
|
||||
customItemsLimit,
|
||||
setActualTone,
|
||||
setCustomItemsLimit,
|
||||
} = useEmojiPickerData();
|
||||
|
||||
useEffect(() => () => handleRemovePreview(), [handleRemovePreview]);
|
||||
|
||||
const scrollCategories = useMediaQuery('(width < 340px)');
|
||||
|
||||
useOutsideClick([emojiContainerRef], onClose);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!reference) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const anchorRect = reference.getBoundingClientRect();
|
||||
if (anchorRect.width === 0 && anchorRect.height === 0) {
|
||||
// The element is hidden, skip it
|
||||
ref.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
ref.current = reference;
|
||||
});
|
||||
|
||||
resizeObserver.observe(reference);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [reference]);
|
||||
|
||||
useEffect(() => {
|
||||
if (textInputRef.current && isInputVisible) {
|
||||
textInputRef.current.focus();
|
||||
}
|
||||
}, [isInputVisible]);
|
||||
|
||||
const handleSelectEmoji = (event: MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
|
||||
const _emoji = event.currentTarget.dataset?.emoji;
|
||||
|
||||
if (!_emoji) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tone = '';
|
||||
|
||||
// Custom emoji should overwrite native emoji with the same name
|
||||
// Custom emoji with name `:point_right:` should overwrite every tone
|
||||
// Custom emoji with name `:point_right_tone1:` should overwrite only tone1, and not the original or other tones
|
||||
if (emoji.list[`:${_emoji}:`].emojiPackage !== 'emojiCustom') {
|
||||
for (const emojiPackage in emoji.packages) {
|
||||
if (emoji.packages.hasOwnProperty(emojiPackage)) {
|
||||
if (actualTone > 0 && emoji.packages[emojiPackage].toneList.hasOwnProperty(_emoji)) {
|
||||
tone = `_tone${actualTone}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSearchTerm('');
|
||||
|
||||
onPickEmoji(_emoji + tone);
|
||||
addRecentEmoji(_emoji + tone);
|
||||
onClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentCategory('recent');
|
||||
}, [setCurrentCategory]);
|
||||
|
||||
const handleSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setCurrentCategory('');
|
||||
setSearching(e.target.value !== '');
|
||||
|
||||
const emojisResult = getEmojisBySearchTerm(e.target.value, actualTone, recentEmojis, setRecentEmojis);
|
||||
|
||||
if (emojisResult.filter((emoji) => emoji.image).length === 0) {
|
||||
setCurrentCategory('no-results');
|
||||
}
|
||||
setSearchResults(emojisResult as EmojiItem[]);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadMore = () => {
|
||||
setCustomItemsLimit(customItemsLimit + 90);
|
||||
};
|
||||
|
||||
const handleScroll = (range: ListRange) => {
|
||||
const { startIndex } = range;
|
||||
|
||||
const category = categoriesIndexes.find(
|
||||
(category, index) => category.index <= startIndex + 1 && categoriesIndexes[index + 1]?.index >= startIndex,
|
||||
);
|
||||
|
||||
if (!category) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentCategory(category.key);
|
||||
};
|
||||
|
||||
const handleGoToCategory = (category: string) => {
|
||||
setSearching(false);
|
||||
const { index } = categoriesIndexes.find((item) => item.key === category) || {};
|
||||
|
||||
if (index === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
virtuosoRef.current?.scrollToIndex({ index: index > 0 ? index + 1 : 0 });
|
||||
};
|
||||
|
||||
const handleGoToAddCustom = () => {
|
||||
customEmojiRoute.push();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<EmojiPickerDropdown reference={ref as RefObject<HTMLElement | null>} ref={emojiContainerRef}>
|
||||
<EmojiPickerContainer role='dialog' aria-label={t('Emoji_picker')} onKeyDown={handleKeyDown}>
|
||||
<EmojiPickerHeader>
|
||||
<TextInput
|
||||
// FIXME: remove autoFocus prop when rewriting the emojiPicker dropdown
|
||||
autoFocus
|
||||
ref={mergedTextInputRef}
|
||||
value={searchTerm}
|
||||
onChange={handleSearch}
|
||||
endAddon={<Icon name='magnifier' size='x20' />}
|
||||
placeholder={t('Search')}
|
||||
aria-label={t('Search')}
|
||||
/>
|
||||
</EmojiPickerHeader>
|
||||
<EmojiPickerCategoryHeader role='tablist' {...(scrollCategories && { style: { overflowX: 'scroll' } })}>
|
||||
{emojiCategories.map((category) => (
|
||||
<EmojiPickerCategoryItem
|
||||
key={category.key}
|
||||
category={category}
|
||||
active={category.key === currentCategory}
|
||||
handleGoToCategory={() => handleGoToCategory(category.key)}
|
||||
/>
|
||||
))}
|
||||
</EmojiPickerCategoryHeader>
|
||||
<Divider marginBlock={12} />
|
||||
<EmojiPickerListArea role='tabpanel'>
|
||||
{searching && <SearchingResult searchResults={searchResults} handleSelectEmoji={handleSelectEmoji} />}
|
||||
{!searching && (
|
||||
<CategoriesResult
|
||||
ref={virtuosoRef}
|
||||
items={emojiListByCategory}
|
||||
customItemsLimit={customItemsLimit}
|
||||
handleLoadMore={handleLoadMore}
|
||||
handleSelectEmoji={handleSelectEmoji}
|
||||
handleScroll={handleScroll}
|
||||
/>
|
||||
)}
|
||||
</EmojiPickerListArea>
|
||||
<EmojiPickerPreviewArea>
|
||||
<div>
|
||||
{emojiToPreview && <EmojiPickerPreview emoji={emojiToPreview.emoji} name={emojiToPreview.name} />}
|
||||
{canManageEmoji && emojiToPreview === null && (
|
||||
<Button small onClick={handleGoToAddCustom}>
|
||||
{t('Add_emoji')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<ToneSelectorWrapper caption={t('Skin_tone')}>
|
||||
<ToneSelector tone={actualTone} setTone={setActualTone} />
|
||||
</ToneSelectorWrapper>
|
||||
</EmojiPickerPreviewArea>
|
||||
</EmojiPickerContainer>
|
||||
</EmojiPickerDropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmojiPicker;
|
||||
Reference in New Issue
Block a user