fluxer/fluxer_app/src/components/channel/AutocompleteEmoji.tsx
2026-02-17 12:22:36 +00:00

203 lines
6.6 KiB
TypeScript

/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import * as EmojiPickerActionCreators from '@app/actions/EmojiPickerActionCreators';
import {type AutocompleteOption, isEmoji, isMeme, isSticker} from '@app/components/channel/Autocomplete';
import styles from '@app/components/channel/AutocompleteEmoji.module.css';
import {AutocompleteItem} from '@app/components/channel/AutocompleteItem';
import GuildStore from '@app/stores/GuildStore';
import {shouldUseNativeEmoji} from '@app/utils/EmojiUtils';
import {getEmojiDisplayData} from '@app/utils/SkinToneUtils';
import {useLingui} from '@lingui/react/macro';
import {MusicNoteIcon} from '@phosphor-icons/react';
import {observer} from 'mobx-react-lite';
import type React from 'react';
const SectionHeading = observer(({children}: {children: React.ReactNode}) => (
<div className={styles.sectionHeading}>{children}</div>
));
export const AutocompleteEmoji = observer(
({
onSelect,
keyboardFocusIndex,
hoverIndex,
options,
onMouseEnter,
onMouseLeave,
rowRefs,
}: {
onSelect: (option: AutocompleteOption) => void;
keyboardFocusIndex: number;
hoverIndex: number;
options: Array<AutocompleteOption>;
onMouseEnter: (index: number) => void;
onMouseLeave: () => void;
rowRefs?: React.MutableRefObject<Array<HTMLButtonElement | null>>;
}) => {
const {t} = useLingui();
const emojis = options.filter(isEmoji);
const stickers = options.filter(isSticker);
const memes = options.filter(isMeme);
const handleEmojiSelect = (option: AutocompleteOption) => {
if (isEmoji(option)) EmojiPickerActionCreators.trackEmojiUsage(option.emoji);
onSelect(option);
};
return (
<>
{emojis.length > 0 && (
<>
<SectionHeading>{t`Emojis`}</SectionHeading>
{emojis.map((option, index) => {
const isUnicodeEmoji = !option.emoji.guildId && !option.emoji.id;
const useNativeRendering = shouldUseNativeEmoji && isUnicodeEmoji;
const {surrogates: displaySurrogates, url: displayUrl} = getEmojiDisplayData(option.emoji);
return (
<AutocompleteItem
key={option.emoji.name}
name={`:${option.emoji.name}:`}
description={
option.emoji.guildId ? GuildStore.getGuild(option.emoji.guildId)?.name : t`Default emoji`
}
icon={
useNativeRendering ? (
<span className={styles.nativeEmojiIcon}>{displaySurrogates}</span>
) : (
<img
draggable={false}
className={styles.emojiIcon}
src={displayUrl ?? ''}
alt={option.emoji.name}
/>
)
}
isKeyboardSelected={index === keyboardFocusIndex}
isHovered={index === hoverIndex}
onSelect={() => handleEmojiSelect(option)}
onMouseEnter={() => onMouseEnter(index)}
onMouseLeave={onMouseLeave}
innerRef={
rowRefs
? (node) => {
rowRefs.current[index] = node;
}
: undefined
}
/>
);
})}
{(stickers.length > 0 || memes.length > 0) && <div className={styles.divider} aria-hidden={true} />}
</>
)}
{stickers.length > 0 && (
<>
<SectionHeading>{t`Stickers`}</SectionHeading>
{stickers.map((option, index) => {
const currentIndex = emojis.length + index;
return (
<AutocompleteItem
key={option.sticker.id}
name={option.sticker.name}
description={
option.sticker.tags.length > 0
? option.sticker.tags.join(', ')
: option.sticker.description || undefined
}
icon={
<div className={styles.stickerIconWrapper}>
<img
draggable={false}
className={styles.stickerIcon}
src={option.sticker.url}
alt={option.sticker.name}
/>
</div>
}
isKeyboardSelected={currentIndex === keyboardFocusIndex}
isHovered={currentIndex === hoverIndex}
onSelect={() => onSelect(option)}
onMouseEnter={() => onMouseEnter(currentIndex)}
onMouseLeave={onMouseLeave}
innerRef={
rowRefs
? (node) => {
rowRefs.current[currentIndex] = node;
}
: undefined
}
/>
);
})}
{memes.length > 0 && <div className={styles.divider} aria-hidden={true} />}
</>
)}
{memes.length > 0 && (
<>
<SectionHeading>{t`Media`}</SectionHeading>
{memes.map((option, index) => {
const currentIndex = emojis.length + stickers.length + index;
return (
<AutocompleteItem
key={option.meme.id}
name={option.meme.name}
description={option.meme.tags.length > 0 ? option.meme.tags.join(', ') : undefined}
icon={
<div className={styles.memeIconWrapper}>
{option.meme.contentType.startsWith('video/') || option.meme.contentType.includes('gif') ? (
<video src={option.meme.url} className={styles.memeVideo} muted autoPlay loop playsInline />
) : option.meme.contentType.startsWith('audio/') ? (
<div className={styles.audioIconWrapper}>
<MusicNoteIcon className={styles.audioIcon} weight="fill" />
</div>
) : (
<img
draggable={false}
className={styles.memeIcon}
src={option.meme.url}
alt={option.meme.name}
/>
)}
</div>
}
isKeyboardSelected={currentIndex === keyboardFocusIndex}
isHovered={currentIndex === hoverIndex}
onSelect={() => onSelect(option)}
onMouseEnter={() => onMouseEnter(currentIndex)}
onMouseLeave={onMouseLeave}
innerRef={
rowRefs
? (node) => {
rowRefs.current[currentIndex] = node;
}
: undefined
}
/>
);
})}
</>
)}
</>
);
},
);