Emoji picker
This commit is contained in:
		
							parent
							
								
									f2d4af04e3
								
							
						
					
					
						commit
						4eba641ec3
					
				
					 3 changed files with 89 additions and 13 deletions
				
			
		|  | @ -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; | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -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"; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue