Emoji picker

This commit is contained in:
Philipp Heckel 2022-04-04 19:56:21 -04:00
parent f2d4af04e3
commit 4eba641ec3
3 changed files with 89 additions and 13 deletions

View file

@ -1,8 +1,13 @@
import * as React from 'react'; import * as React from 'react';
import {useRef, useState} from 'react';
import Popover from '@mui/material/Popover'; import Popover from '@mui/material/Popover';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import {rawEmojis} from '../app/emojis'; import {rawEmojis} from '../app/emojis';
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField";
import {InputAdornment} from "@mui/material";
import IconButton from "@mui/material/IconButton";
import {Close} from "@mui/icons-material";
const emojisByCategory = {}; const emojisByCategory = {};
rawEmojis.forEach(emoji => { rawEmojis.forEach(emoji => {
@ -14,22 +19,69 @@ rawEmojis.forEach(emoji => {
const EmojiPicker = (props) => { const EmojiPicker = (props) => {
const open = Boolean(props.anchorEl); const open = Boolean(props.anchorEl);
const [search, setSearch] = useState("");
const searchRef = useRef(null);
/*
FIXME Search is inefficient, somehow make it faster
useEffect(() => {
const matching = rawEmojis.filter(e => {
const searchLower = search.toLowerCase();
return e.description.toLowerCase().indexOf(searchLower) !== -1
|| matchInArray(e.aliases, searchLower)
|| matchInArray(e.tags, searchLower);
});
console.log("matching", matching.length);
}, [search]);
*/
const handleSearchClear = () => {
setSearch("");
searchRef.current?.focus();
};
return ( return (
<> <>
<Popover <Popover
open={open} open={open}
anchorEl={props.anchorEl} elevation={3}
onClose={props.onClose} onClose={props.onClose}
anchorEl={props.anchorEl}
anchorOrigin={{ anchorOrigin={{
vertical: 'bottom', vertical: 'bottom',
horizontal: 'left', horizontal: 'left',
}} }}
> >
<Box sx={{ padding: 2, paddingRight: 0, width: "370px", maxHeight: "300px" }}> <Box sx={{ padding: 2, paddingRight: 0, width: "370px", maxHeight: "300px" }}>
{Object.keys(emojisByCategory).map(category => <TextField
<Category title={category} emojis={emojisByCategory[category]} onPick={props.onEmojiPick}/> inputRef={searchRef}
)} margin="dense"
size="small"
placeholder="Search emoji"
value={search}
onChange={ev => setSearch(ev.target.value)}
type="text"
variant="standard"
fullWidth
sx={{ marginTop: 0, paddingRight: 2 }}
InputProps={{
endAdornment:
<InputAdornment position="end" sx={{ display: (search) ? '' : 'none' }}>
<IconButton size="small" onClick={handleSearchClear} edge="end"><Close/></IconButton>
</InputAdornment>
}}
/>
<Box sx={{ display: "flex", flexWrap: "wrap", paddingRight: 0, marginTop: 1 }}>
{Object.keys(emojisByCategory).map(category =>
<Category
key={category}
title={category}
emojis={emojisByCategory[category]}
search={search.toLowerCase()}
onPick={props.onEmojiPick}
/>
)}
</Box>
</Box> </Box>
</Popover> </Popover>
</> </>
@ -37,18 +89,36 @@ const EmojiPicker = (props) => {
}; };
const Category = (props) => { const Category = (props) => {
const showTitle = !props.search;
return ( return (
<> <>
<Typography variant="body2">{props.title}</Typography> {showTitle &&
<Box sx={{ display: "flex", flexWrap: "wrap", paddingRight: 0, marginBottom: 1 }}> <Typography variant="body1" sx={{ width: "100%", marginTop: 1, marginBottom: 1 }}>
{props.emojis.map(emoji => <Emoji emoji={emoji} onClick={() => props.onPick(emoji.aliases[0])}/>)} {props.title}
</Box> </Typography>
}
{props.emojis.map(emoji =>
<Emoji
key={emoji.aliases[0]}
emoji={emoji}
search={props.search}
onClick={() => props.onPick(emoji.aliases[0])}
/>
)}
</> </>
); );
}; };
const Emoji = (props) => { const Emoji = (props) => {
const emoji = props.emoji; const emoji = props.emoji;
const search = props.search;
const matches = search === ""
|| emoji.description.toLowerCase().indexOf(search) !== -1
|| matchInArray(emoji.aliases, search)
|| matchInArray(emoji.tags, search);
if (!matches) {
return null;
}
return ( return (
<div <div
onClick={props.onClick} onClick={props.onClick}
@ -69,4 +139,11 @@ const Emoji = (props) => {
); );
}; };
const matchInArray = (arr, search) => {
if (!arr || !search) {
return false;
}
return arr.filter(s => s.indexOf(search) !== -1).length > 0;
}
export default EmojiPicker; export default EmojiPicker;

View file

@ -213,11 +213,11 @@ const SendDialog = (props) => {
onDragLeave={handleAttachFileDragLeave}/> onDragLeave={handleAttachFileDragLeave}/>
} }
<Dialog maxWidth="md" open={open} onClose={props.onCancel} fullScreen={fullScreen}> <Dialog maxWidth="md" open={open} onClose={props.onCancel} fullScreen={fullScreen}>
<DialogTitle>Publish to {shortUrl(topicUrl)}</DialogTitle> <DialogTitle>{topicUrl ? `Publish to ${shortUrl(topicUrl)}` : "Publish message"}</DialogTitle>
<DialogContent> <DialogContent>
{dropZone && <DropBox/>} {dropZone && <DropBox/>}
{showTopicUrl && {showTopicUrl &&
<ClosableRow disabled={disabled} onClose={() => { <ClosableRow closable={!!props.topicUrl} disabled={disabled} onClose={() => {
setTopicUrl(props.topicUrl); setTopicUrl(props.topicUrl);
setShowTopicUrl(false); setShowTopicUrl(false);
}}> }}>
@ -468,10 +468,11 @@ const Row = (props) => {
}; };
const ClosableRow = (props) => { const ClosableRow = (props) => {
const closable = (props.hasOwnProperty("closable")) ? props.closable : true;
return ( return (
<Row> <Row>
{props.children} {props.children}
<DialogIconButton disabled={props.disabled} onClick={props.onClose} sx={{marginLeft: "6px"}}><Close/></DialogIconButton> {closable && <DialogIconButton disabled={props.disabled} onClick={props.onClose} sx={{marginLeft: "6px"}}><Close/></DialogIconButton>}
</Row> </Row>
); );
}; };

View file

@ -3,7 +3,6 @@ import {useState} from 'react';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import Dialog from '@mui/material/Dialog'; import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent'; import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText'; import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle'; import DialogTitle from '@mui/material/DialogTitle';
@ -11,7 +10,6 @@ import {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/mate
import theme from "./theme"; import theme from "./theme";
import api from "../app/Api"; import api from "../app/Api";
import {topicUrl, validTopic, validUrl} from "../app/utils"; import {topicUrl, validTopic, validUrl} from "../app/utils";
import Box from "@mui/material/Box";
import userManager from "../app/UserManager"; import userManager from "../app/UserManager";
import subscriptionManager from "../app/SubscriptionManager"; import subscriptionManager from "../app/SubscriptionManager";
import poller from "../app/Poller"; import poller from "../app/Poller";