Emoji picker
This commit is contained in:
		
							parent
							
								
									d44ee2bbf6
								
							
						
					
					
						commit
						f2d4af04e3
					
				
					 6 changed files with 197 additions and 95 deletions
				
			
		|  | @ -10,7 +10,7 @@ if [ -z "$1" ]; then | |||
|     echo "Syntax: $0 FILE.(js|json|md)" | ||||
|     echo "Example:" | ||||
|     echo "  $0 emoji-converted.json" | ||||
|     echo "  $0 $ROOTDIR/server/static/js/emoji.js" | ||||
|     echo "  $0 $ROOTDIR/web/src/app/emojis.js" | ||||
|     echo "  $0 $ROOTDIR/docs/emojis.md" | ||||
|     exit 1 | ||||
| fi | ||||
|  | @ -19,7 +19,7 @@ if [[ "$1" == *.js ]]; then | |||
|   echo -n "// This file is generated by scripts/emoji-convert.sh to reduce the size | ||||
| // Original data source: https://github.com/github/gemoji/blob/master/db/emoji.json | ||||
| export const rawEmojis = " > "$1" | ||||
|     cat "$SCRIPTDIR/emoji.json" | jq -rc 'map({emoji: .emoji,aliases: .aliases})' >> "$1" | ||||
|     cat "$SCRIPTDIR/emoji.json" | jq -rc 'map({emoji: .emoji,aliases: .aliases, tags: .tags, category: .category, description: .description})' >> "$1" | ||||
| elif [[ "$1" == *.md ]]; then | ||||
|   echo "# Emoji reference | ||||
| 
 | ||||
|  |  | |||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -14,19 +14,13 @@ import {useLiveQuery} from "dexie-react-hooks"; | |||
| import subscriptionManager from "../app/SubscriptionManager"; | ||||
| import userManager from "../app/UserManager"; | ||||
| import {BrowserRouter, Outlet, Route, Routes, useOutletContext, useParams} from "react-router-dom"; | ||||
| import {expandUrl, topicUrl} from "../app/utils"; | ||||
| import {expandUrl} from "../app/utils"; | ||||
| import ErrorBoundary from "./ErrorBoundary"; | ||||
| import routes from "./routes"; | ||||
| import {useAutoSubscribe, useBackgroundProcesses, useConnectionListeners} from "./hooks"; | ||||
| import Paper from "@mui/material/Paper"; | ||||
| import IconButton from "@mui/material/IconButton"; | ||||
| import TextField from "@mui/material/TextField"; | ||||
| import SendIcon from "@mui/icons-material/Send"; | ||||
| import api from "../app/Api"; | ||||
| import SendDialog from "./SendDialog"; | ||||
| import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; | ||||
| import Messaging from "./Messaging"; | ||||
| 
 | ||||
| // TODO add drag and drop
 | ||||
| // TODO races when two tabs are open
 | ||||
| // TODO investigate service workers
 | ||||
| 
 | ||||
|  | @ -128,89 +122,6 @@ const Main = (props) => { | |||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const Messaging = (props) => { | ||||
|     const [message, setMessage] = useState(""); | ||||
|     const [dialogKey, setDialogKey] = useState(0); | ||||
| 
 | ||||
|     const dialogOpenMode = props.dialogOpenMode; | ||||
|     const subscription = props.selected; | ||||
|     const selectedTopicUrl = (subscription) ? topicUrl(subscription.baseUrl, subscription.topic) : ""; | ||||
| 
 | ||||
|     const handleOpenDialogClick = () => { | ||||
|         props.onDialogOpenModeChange(SendDialog.OPEN_MODE_DEFAULT); | ||||
|     }; | ||||
| 
 | ||||
|     const handleSendDialogClose = () => { | ||||
|         props.onDialogOpenModeChange(""); | ||||
|         setDialogKey(prev => prev+1); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             {subscription && <MessageBar | ||||
|                 subscription={subscription} | ||||
|                 message={message} | ||||
|                 onMessageChange={setMessage} | ||||
|                 onOpenDialogClick={handleOpenDialogClick} | ||||
|             />} | ||||
|             <SendDialog | ||||
|                 key={`sendDialog${dialogKey}`} // Resets dialog when canceled/closed
 | ||||
|                 openMode={dialogOpenMode} | ||||
|                 topicUrl={selectedTopicUrl} | ||||
|                 message={message} | ||||
|                 onClose={handleSendDialogClose} | ||||
|                 onDragEnter={() => props.onDialogOpenModeChange(prev => (prev) ? prev : SendDialog.OPEN_MODE_DRAG)} // Only update if not already open
 | ||||
|                 onResetOpenMode={() => props.onDialogOpenModeChange(SendDialog.OPEN_MODE_DEFAULT)} | ||||
|             /> | ||||
|         </> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| const MessageBar = (props) => { | ||||
|     const subscription = props.subscription; | ||||
|     const handleSendClick = () => { | ||||
|         api.publish(subscription.baseUrl, subscription.topic, props.message); // FIXME
 | ||||
|         props.onMessageChange(""); | ||||
|     }; | ||||
|     return ( | ||||
|         <Paper | ||||
|             elevation={3} | ||||
|             sx={{ | ||||
|                 display: "flex", | ||||
|                 position: 'fixed', | ||||
|                 bottom: 0, | ||||
|                 right: 0, | ||||
|                 padding: 2, | ||||
|                 width: `calc(100% - ${Navigation.width}px)`, | ||||
|                 backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900] | ||||
|             }} | ||||
|         > | ||||
|             <IconButton color="inherit" size="large" edge="start" onClick={props.onOpenDialogClick}> | ||||
|                 <KeyboardArrowUpIcon/> | ||||
|             </IconButton> | ||||
|             <TextField | ||||
|                 autoFocus | ||||
|                 margin="dense" | ||||
|                 placeholder="Message" | ||||
|                 type="text" | ||||
|                 fullWidth | ||||
|                 variant="standard" | ||||
|                 value={props.message} | ||||
|                 onChange={ev => props.onMessageChange(ev.target.value)} | ||||
|                 onKeyPress={(ev) => { | ||||
|                     if (ev.key === 'Enter') { | ||||
|                         ev.preventDefault(); | ||||
|                         handleSendClick(); | ||||
|                     } | ||||
|                 }} | ||||
|             /> | ||||
|             <IconButton color="inherit" size="large" edge="end" onClick={handleSendClick}> | ||||
|                 <SendIcon/> | ||||
|             </IconButton> | ||||
|         </Paper> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const updateTitle = (newNotificationsCount) => { | ||||
|     document.title = (newNotificationsCount > 0) ? `(${newNotificationsCount}) ntfy` : "ntfy"; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										72
									
								
								web/src/components/EmojiPicker.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								web/src/components/EmojiPicker.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,72 @@ | |||
| import * as React 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"; | ||||
| 
 | ||||
| const emojisByCategory = {}; | ||||
| rawEmojis.forEach(emoji => { | ||||
|     if (!emojisByCategory[emoji.category]) { | ||||
|         emojisByCategory[emoji.category] = []; | ||||
|     } | ||||
|     emojisByCategory[emoji.category].push(emoji); | ||||
| }); | ||||
| 
 | ||||
| const EmojiPicker = (props) => { | ||||
|     const open = Boolean(props.anchorEl); | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <Popover | ||||
|                 open={open} | ||||
|                 anchorEl={props.anchorEl} | ||||
|                 onClose={props.onClose} | ||||
|                 anchorOrigin={{ | ||||
|                     vertical: 'bottom', | ||||
|                     horizontal: 'left', | ||||
|                 }} | ||||
|             > | ||||
|                 <Box sx={{ padding: 2, paddingRight: 0, width: "370px", maxHeight: "300px" }}> | ||||
|                     {Object.keys(emojisByCategory).map(category => | ||||
|                         <Category title={category} emojis={emojisByCategory[category]} onPick={props.onEmojiPick}/> | ||||
|                     )} | ||||
|                 </Box> | ||||
|             </Popover> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const Category = (props) => { | ||||
|     return ( | ||||
|         <> | ||||
|             <Typography variant="body2">{props.title}</Typography> | ||||
|             <Box sx={{ display: "flex", flexWrap: "wrap", paddingRight: 0, marginBottom: 1 }}> | ||||
|                 {props.emojis.map(emoji => <Emoji emoji={emoji} onClick={() => props.onPick(emoji.aliases[0])}/>)} | ||||
|             </Box> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const Emoji = (props) => { | ||||
|     const emoji = props.emoji; | ||||
|     return ( | ||||
|         <div | ||||
|             onClick={props.onClick} | ||||
|             title={`${emoji.description} (${emoji.aliases[0]})`} | ||||
|             style={{ | ||||
|                 fontSize: "30px", | ||||
|                 width: "30px", | ||||
|                 height: "30px", | ||||
|                 marginTop: "8px", | ||||
|                 marginBottom: "8px", | ||||
|                 marginRight: "8px", | ||||
|                 lineHeight: "30px", | ||||
|                 cursor: "pointer" | ||||
|             }} | ||||
|         > | ||||
|             {props.emoji.emoji} | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export default EmojiPicker; | ||||
							
								
								
									
										97
									
								
								web/src/components/Messaging.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								web/src/components/Messaging.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,97 @@ | |||
| import * as React from 'react'; | ||||
| import {useState} from 'react'; | ||||
| import Navigation from "./Navigation"; | ||||
| import {topicUrl} from "../app/utils"; | ||||
| import Paper from "@mui/material/Paper"; | ||||
| import IconButton from "@mui/material/IconButton"; | ||||
| import TextField from "@mui/material/TextField"; | ||||
| import SendIcon from "@mui/icons-material/Send"; | ||||
| import api from "../app/Api"; | ||||
| import SendDialog from "./SendDialog"; | ||||
| import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; | ||||
| import EmojiPicker from "./EmojiPicker"; | ||||
| 
 | ||||
| const Messaging = (props) => { | ||||
|     const [message, setMessage] = useState(""); | ||||
|     const [dialogKey, setDialogKey] = useState(0); | ||||
| 
 | ||||
|     const dialogOpenMode = props.dialogOpenMode; | ||||
|     const subscription = props.selected; | ||||
|     const selectedTopicUrl = (subscription) ? topicUrl(subscription.baseUrl, subscription.topic) : ""; | ||||
| 
 | ||||
|     const handleOpenDialogClick = () => { | ||||
|         props.onDialogOpenModeChange(SendDialog.OPEN_MODE_DEFAULT); | ||||
|     }; | ||||
| 
 | ||||
|     const handleSendDialogClose = () => { | ||||
|         props.onDialogOpenModeChange(""); | ||||
|         setDialogKey(prev => prev+1); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             {subscription && <MessageBar | ||||
|                 subscription={subscription} | ||||
|                 message={message} | ||||
|                 onMessageChange={setMessage} | ||||
|                 onOpenDialogClick={handleOpenDialogClick} | ||||
|             />} | ||||
|             <SendDialog | ||||
|                 key={`sendDialog${dialogKey}`} // Resets dialog when canceled/closed
 | ||||
|                 openMode={dialogOpenMode} | ||||
|                 topicUrl={selectedTopicUrl} | ||||
|                 message={message} | ||||
|                 onClose={handleSendDialogClose} | ||||
|                 onDragEnter={() => props.onDialogOpenModeChange(prev => (prev) ? prev : SendDialog.OPEN_MODE_DRAG)} // Only update if not already open
 | ||||
|                 onResetOpenMode={() => props.onDialogOpenModeChange(SendDialog.OPEN_MODE_DEFAULT)} | ||||
|             /> | ||||
|         </> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| const MessageBar = (props) => { | ||||
|     const subscription = props.subscription; | ||||
|     const handleSendClick = () => { | ||||
|         api.publish(subscription.baseUrl, subscription.topic, props.message); // FIXME
 | ||||
|         props.onMessageChange(""); | ||||
|     }; | ||||
|     return ( | ||||
|         <Paper | ||||
|             elevation={3} | ||||
|             sx={{ | ||||
|                 display: "flex", | ||||
|                 position: 'fixed', | ||||
|                 bottom: 0, | ||||
|                 right: 0, | ||||
|                 padding: 2, | ||||
|                 width: `calc(100% - ${Navigation.width}px)`, | ||||
|                 backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900] | ||||
|             }} | ||||
|         > | ||||
|             <IconButton color="inherit" size="large" edge="start" onClick={props.onOpenDialogClick}> | ||||
|                 <KeyboardArrowUpIcon/> | ||||
|             </IconButton> | ||||
|             <TextField | ||||
|                 autoFocus | ||||
|                 margin="dense" | ||||
|                 placeholder="Message" | ||||
|                 type="text" | ||||
|                 fullWidth | ||||
|                 variant="standard" | ||||
|                 value={props.message} | ||||
|                 onChange={ev => props.onMessageChange(ev.target.value)} | ||||
|                 onKeyPress={(ev) => { | ||||
|                     if (ev.key === 'Enter') { | ||||
|                         ev.preventDefault(); | ||||
|                         handleSendClick(); | ||||
|                     } | ||||
|                 }} | ||||
|             /> | ||||
|             <IconButton color="inherit" size="large" edge="end" onClick={handleSendClick}> | ||||
|                 <SendIcon/> | ||||
|             </IconButton> | ||||
|         </Paper> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export default Messaging; | ||||
|  | @ -24,6 +24,7 @@ import AttachmentIcon from "./AttachmentIcon"; | |||
| import DialogFooter from "./DialogFooter"; | ||||
| import api from "../app/Api"; | ||||
| import userManager from "../app/UserManager"; | ||||
| import EmojiPicker from "./EmojiPicker"; | ||||
| 
 | ||||
| const SendDialog = (props) => { | ||||
|     const [topicUrl, setTopicUrl] = useState(""); | ||||
|  | @ -54,6 +55,8 @@ const SendDialog = (props) => { | |||
|     const [status, setStatus] = useState(""); | ||||
|     const disabled = !!activeRequest; | ||||
| 
 | ||||
|     const [emojiPickerAnchorEl, setEmojiPickerAnchorEl] = useState(null); | ||||
| 
 | ||||
|     const [dropZone, setDropZone] = useState(false); | ||||
|     const [sendButtonEnabled, setSendButtonEnabled] = useState(true); | ||||
| 
 | ||||
|  | @ -191,6 +194,18 @@ const SendDialog = (props) => { | |||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const handleEmojiClick = (ev) => { | ||||
|         setEmojiPickerAnchorEl(ev.currentTarget); | ||||
|     }; | ||||
| 
 | ||||
|     const handleEmojiPick = (emoji) => { | ||||
|         setTags(tags => (tags.trim()) ? `${tags.trim()}, ${emoji}` : emoji); | ||||
|     }; | ||||
| 
 | ||||
|     const handleEmojiClose = () => { | ||||
|         setEmojiPickerAnchorEl(null); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             {dropZone && <DropArea | ||||
|  | @ -245,7 +260,14 @@ const SendDialog = (props) => { | |||
|                         multiline | ||||
|                     /> | ||||
|                     <div style={{display: 'flex'}}> | ||||
|                         <DialogIconButton disabled={disabled} onClick={() => null}><InsertEmoticonIcon/></DialogIconButton> | ||||
|                         <EmojiPicker | ||||
|                             anchorEl={emojiPickerAnchorEl} | ||||
|                             onEmojiPick={handleEmojiPick} | ||||
|                             onClose={handleEmojiClose} | ||||
|                         /> | ||||
|                         <DialogIconButton disabled={disabled} onClick={handleEmojiClick}> | ||||
|                             <InsertEmoticonIcon/> | ||||
|                         </DialogIconButton> | ||||
|                         <TextField | ||||
|                             margin="dense" | ||||
|                             label="Tags" | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue