ntfy/web/src/components/EmojiPicker.js

180 lines
6.5 KiB
JavaScript
Raw Normal View History

2022-04-04 16:04:01 +02:00
import * as React from 'react';
2022-04-05 01:56:21 +02:00
import {useRef, useState} from 'react';
2022-04-04 16:04:01 +02:00
import Typography from '@mui/material/Typography';
import {rawEmojis} from '../app/emojis';
import Box from "@mui/material/Box";
2022-04-05 01:56:21 +02:00
import TextField from "@mui/material/TextField";
2022-04-06 01:40:34 +02:00
import {ClickAwayListener, Fade, InputAdornment, styled} from "@mui/material";
2022-04-05 01:56:21 +02:00
import IconButton from "@mui/material/IconButton";
import {Close} from "@mui/icons-material";
2022-04-06 01:40:34 +02:00
import Popper from "@mui/material/Popper";
import {splitNoEmpty} from "../app/utils";
2022-04-08 03:46:33 +02:00
import {useTranslation} from "react-i18next";
2022-04-06 01:40:34 +02:00
// Create emoji list by category and create a search base (string with all search words)
//
// This also filters emojis that are not supported by Desktop Chrome.
// This is a hack, but on Ubuntu 18.04, with Chrome 99, only Emoji <= 11 are supported.
2022-04-04 16:04:01 +02:00
const emojisByCategory = {};
const isDesktopChrome = /Chrome/.test(navigator.userAgent) && !/Mobile/.test(navigator.userAgent);
const maxSupportedVersionForDesktopChrome = 11;
2022-04-04 16:04:01 +02:00
rawEmojis.forEach(emoji => {
if (!emojisByCategory[emoji.category]) {
emojisByCategory[emoji.category] = [];
}
try {
const unicodeVersion = parseFloat(emoji.unicode_version);
const supportedEmoji = unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome;
if (supportedEmoji) {
2022-04-06 01:40:34 +02:00
const searchBase = `${emoji.description.toLowerCase()} ${emoji.aliases.join(" ")} ${emoji.tags.join(" ")}`;
const emojiWithSearchBase = { ...emoji, searchBase: searchBase };
emojisByCategory[emoji.category].push(emojiWithSearchBase);
}
} catch (e) {
// Nothing. Ignore.
}
2022-04-04 16:04:01 +02:00
});
const EmojiPicker = (props) => {
2022-04-08 03:46:33 +02:00
const { t } = useTranslation();
2022-04-04 16:04:01 +02:00
const open = Boolean(props.anchorEl);
2022-04-05 01:56:21 +02:00
const [search, setSearch] = useState("");
const searchRef = useRef(null);
2022-04-06 01:40:34 +02:00
const searchFields = splitNoEmpty(search.toLowerCase(), " ");
2022-04-05 01:56:21 +02:00
const handleSearchClear = () => {
setSearch("");
searchRef.current?.focus();
};
2022-04-04 16:04:01 +02:00
return (
2022-04-06 01:40:34 +02:00
<Popper
open={open}
anchorEl={props.anchorEl}
placement="bottom-start"
sx={{ zIndex: 10005 }}
transition
>
{({ TransitionProps }) => (
<ClickAwayListener onClickAway={props.onClose}>
<Fade {...TransitionProps} timeout={350}>
<Box sx={{
boxShadow: 3,
padding: 2,
paddingRight: 0,
paddingBottom: 1,
width: "380px",
maxHeight: "300px",
backgroundColor: 'background.paper',
overflowY: "scroll"
}}>
<TextField
inputRef={searchRef}
margin="dense"
size="small"
2022-04-08 03:46:33 +02:00
placeholder={t("emoji_picker_search_placeholder")}
2022-04-06 01:40:34 +02:00
value={search}
onChange={ev => setSearch(ev.target.value)}
type="text"
variant="standard"
fullWidth
sx={{ marginTop: 0, marginBottom: "12px", paddingRight: 2 }}
2022-05-03 02:02:21 +02:00
inputProps={{
role: "searchbox",
"aria-label": t("emoji_picker_search_placeholder")
}}
2022-04-06 01:40:34 +02:00
InputProps={{
endAdornment:
<InputAdornment position="end" sx={{ display: (search) ? '' : 'none' }}>
2022-05-03 01:30:29 +02:00
<IconButton size="small" onClick={handleSearchClear} edge="end" aria-label={t("emoji_picker_search_clear")}>
<Close/>
</IconButton>
2022-04-06 01:40:34 +02:00
</InputAdornment>
}}
2022-04-05 01:56:21 +02:00
/>
2022-04-06 01:40:34 +02:00
<Box sx={{ display: "flex", flexWrap: "wrap", paddingRight: 0, marginTop: 1 }}>
{Object.keys(emojisByCategory).map(category =>
<Category
key={category}
title={category}
emojis={emojisByCategory[category]}
search={searchFields}
onPick={props.onEmojiPick}
/>
)}
</Box>
</Box>
</Fade>
</ClickAwayListener>
)}
</Popper>
2022-04-04 16:04:01 +02:00
);
};
const Category = (props) => {
2022-04-06 01:40:34 +02:00
const showTitle = props.search.length === 0;
2022-04-04 16:04:01 +02:00
return (
<>
2022-04-05 01:56:21 +02:00
{showTitle &&
2022-04-06 01:40:34 +02:00
<Typography variant="body1" sx={{ width: "100%", marginBottom: 1 }}>
2022-04-05 01:56:21 +02:00
{props.title}
</Typography>
}
{props.emojis.map(emoji =>
<Emoji
key={emoji.aliases[0]}
emoji={emoji}
search={props.search}
onClick={() => props.onPick(emoji.aliases[0])}
/>
)}
2022-04-04 16:04:01 +02:00
</>
);
};
const Emoji = (props) => {
const emoji = props.emoji;
2022-04-06 01:40:34 +02:00
const matches = emojiMatches(emoji, props.search);
2022-05-03 01:30:29 +02:00
const title = `${emoji.description} (${emoji.aliases[0]})`;
2022-04-04 16:04:01 +02:00
return (
2022-04-06 01:40:34 +02:00
<EmojiDiv
2022-04-04 16:04:01 +02:00
onClick={props.onClick}
2022-05-03 01:30:29 +02:00
title={title}
aria-label={title}
2022-04-06 01:40:34 +02:00
style={{ display: (matches) ? '' : 'none' }}
2022-04-04 16:04:01 +02:00
>
{props.emoji.emoji}
2022-04-06 01:40:34 +02:00
</EmojiDiv>
2022-04-04 16:04:01 +02:00
);
};
2022-04-06 01:40:34 +02:00
const EmojiDiv = styled("div")({
fontSize: "30px",
width: "30px",
height: "30px",
marginTop: "8px",
marginBottom: "8px",
marginRight: "8px",
lineHeight: "30px",
cursor: "pointer",
opacity: 0.85,
"&:hover": {
opacity: 1
}
});
const emojiMatches = (emoji, words) => {
if (words.length === 0) {
return true;
}
for (const word of words) {
if (emoji.searchBase.indexOf(word) === -1) {
return false;
}
2022-04-05 01:56:21 +02:00
}
2022-04-06 01:40:34 +02:00
return true;
2022-04-05 01:56:21 +02:00
}
2022-04-04 16:04:01 +02:00
export default EmojiPicker;