mirror of
				https://github.com/binwiederhier/ntfy.git
				synced 2025-10-31 13:02:24 +01:00 
			
		
		
		
	Good enough emoji picker
This commit is contained in:
		
							parent
							
								
									328aca48ab
								
							
						
					
					
						commit
						35ddcb27f0
					
				
					 1 changed files with 95 additions and 86 deletions
				
			
		|  | @ -1,15 +1,20 @@ | |||
| import * as React from 'react'; | ||||
| import {useRef, useState} from 'react'; | ||||
| import Popover from '@mui/material/Popover'; | ||||
| import Typography from '@mui/material/Typography'; | ||||
| import {rawEmojis} from '../app/emojis'; | ||||
| import Box from "@mui/material/Box"; | ||||
| import TextField from "@mui/material/TextField"; | ||||
| import {InputAdornment} from "@mui/material"; | ||||
| import {ClickAwayListener, Fade, InputAdornment, styled} from "@mui/material"; | ||||
| import IconButton from "@mui/material/IconButton"; | ||||
| import {Close} from "@mui/icons-material"; | ||||
| import Popper from "@mui/material/Popper"; | ||||
| import {splitNoEmpty} from "../app/utils"; | ||||
| 
 | ||||
| // 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.
 | ||||
| 
 | ||||
| // Create emoji list by category; filter emojis that are not supported by Desktop Chrome
 | ||||
| const emojisByCategory = {}; | ||||
| const isDesktopChrome = /Chrome/.test(navigator.userAgent) && !/Mobile/.test(navigator.userAgent); | ||||
| const maxSupportedVersionForDesktopChrome = 11; | ||||
|  | @ -21,7 +26,9 @@ rawEmojis.forEach(emoji => { | |||
|         const unicodeVersion = parseFloat(emoji.unicode_version); | ||||
|         const supportedEmoji = unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome; | ||||
|         if (supportedEmoji) { | ||||
|             emojisByCategory[emoji.category].push(emoji); | ||||
|             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.
 | ||||
|  | @ -32,38 +39,34 @@ const EmojiPicker = (props) => { | |||
|     const open = Boolean(props.anchorEl); | ||||
|     const [search, setSearch] = useState(""); | ||||
|     const searchRef = useRef(null); | ||||
|     const searchFields = splitNoEmpty(search.toLowerCase(), " "); | ||||
| 
 | ||||
|     /* | ||||
|         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 ( | ||||
|         <> | ||||
|             <Popover | ||||
|         <Popper | ||||
|             open={open} | ||||
|                 elevation={3} | ||||
|                 onClose={props.onClose} | ||||
|             anchorEl={props.anchorEl} | ||||
|                 anchorOrigin={{ | ||||
|                     vertical: 'bottom', | ||||
|                     horizontal: 'left', | ||||
|                 }} | ||||
|             placement="bottom-start" | ||||
|             sx={{ zIndex: 10005 }} | ||||
|             transition | ||||
|         > | ||||
|                 <Box sx={{ padding: 2, paddingRight: 0, width: "370px", maxHeight: "300px" }}> | ||||
|             {({ 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" | ||||
|  | @ -74,7 +77,7 @@ const EmojiPicker = (props) => { | |||
|                                 type="text" | ||||
|                                 variant="standard" | ||||
|                                 fullWidth | ||||
|                         sx={{ marginTop: 0, paddingRight: 2 }} | ||||
|                                 sx={{ marginTop: 0, marginBottom: "12px", paddingRight: 2 }} | ||||
|                                 InputProps={{ | ||||
|                                     endAdornment: | ||||
|                                         <InputAdornment position="end" sx={{ display: (search) ? '' : 'none' }}> | ||||
|  | @ -88,23 +91,25 @@ const EmojiPicker = (props) => { | |||
|                                         key={category} | ||||
|                                         title={category} | ||||
|                                         emojis={emojisByCategory[category]} | ||||
|                                 search={search.toLowerCase()} | ||||
|                                         search={searchFields} | ||||
|                                         onPick={props.onEmojiPick} | ||||
|                                     /> | ||||
|                                 )} | ||||
|                             </Box> | ||||
|                         </Box> | ||||
|             </Popover> | ||||
|         </> | ||||
|                     </Fade> | ||||
|                 </ClickAwayListener> | ||||
|             )} | ||||
|         </Popper> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const Category = (props) => { | ||||
|     const showTitle = !props.search; | ||||
|     const showTitle = props.search.length === 0; | ||||
|     return ( | ||||
|         <> | ||||
|             {showTitle && | ||||
|                 <Typography variant="body1" sx={{ width: "100%", marginTop: 1, marginBottom: 1 }}> | ||||
|                 <Typography variant="body1" sx={{ width: "100%", marginBottom: 1 }}> | ||||
|                     {props.title} | ||||
|                 </Typography> | ||||
|             } | ||||
|  | @ -122,19 +127,19 @@ const Category = (props) => { | |||
| 
 | ||||
| const Emoji = (props) => { | ||||
|     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; | ||||
|     } | ||||
|     const matches = emojiMatches(emoji, props.search); | ||||
|     return ( | ||||
|         <div | ||||
|         <EmojiDiv | ||||
|             onClick={props.onClick} | ||||
|             title={`${emoji.description} (${emoji.aliases[0]})`} | ||||
|             style={{ | ||||
|             style={{ display: (matches) ? '' : 'none' }} | ||||
|         > | ||||
|             {props.emoji.emoji} | ||||
|         </EmojiDiv> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const EmojiDiv = styled("div")({ | ||||
|     fontSize: "30px", | ||||
|     width: "30px", | ||||
|     height: "30px", | ||||
|  | @ -142,19 +147,23 @@ const Emoji = (props) => { | |||
|     marginBottom: "8px", | ||||
|     marginRight: "8px", | ||||
|     lineHeight: "30px", | ||||
|                 cursor: "pointer" | ||||
|             }} | ||||
|         > | ||||
|             {props.emoji.emoji} | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
|     cursor: "pointer", | ||||
|     opacity: 0.85, | ||||
|     "&:hover": { | ||||
|         opacity: 1 | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| const matchInArray = (arr, search) => { | ||||
|     if (!arr || !search) { | ||||
| const emojiMatches = (emoji, words) => { | ||||
|     if (words.length === 0) { | ||||
|         return true; | ||||
|     } | ||||
|     for (const word of words) { | ||||
|         if (emoji.searchBase.indexOf(word) === -1) { | ||||
|             return false; | ||||
|         } | ||||
|     return arr.filter(s => s.indexOf(search) !== -1).length > 0; | ||||
|     } | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| export default EmojiPicker; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Philipp Heckel
						Philipp Heckel