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 "Syntax: $0 FILE.(js|json|md)" | ||||||
|     echo "Example:" |     echo "Example:" | ||||||
|     echo "  $0 emoji-converted.json" |     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" |     echo "  $0 $ROOTDIR/docs/emojis.md" | ||||||
|     exit 1 |     exit 1 | ||||||
| fi | fi | ||||||
|  | @ -19,7 +19,7 @@ if [[ "$1" == *.js ]]; then | ||||||
|   echo -n "// This file is generated by scripts/emoji-convert.sh to reduce the size |   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 | // Original data source: https://github.com/github/gemoji/blob/master/db/emoji.json | ||||||
| export const rawEmojis = " > "$1" | 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 | elif [[ "$1" == *.md ]]; then | ||||||
|   echo "# Emoji reference |   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 subscriptionManager from "../app/SubscriptionManager"; | ||||||
| import userManager from "../app/UserManager"; | import userManager from "../app/UserManager"; | ||||||
| import {BrowserRouter, Outlet, Route, Routes, useOutletContext, useParams} from "react-router-dom"; | 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 ErrorBoundary from "./ErrorBoundary"; | ||||||
| import routes from "./routes"; | import routes from "./routes"; | ||||||
| import {useAutoSubscribe, useBackgroundProcesses, useConnectionListeners} from "./hooks"; | 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 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 races when two tabs are open
 | ||||||
| // TODO investigate service workers
 | // 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) => { | const updateTitle = (newNotificationsCount) => { | ||||||
|     document.title = (newNotificationsCount > 0) ? `(${newNotificationsCount}) ntfy` : "ntfy"; |     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 DialogFooter from "./DialogFooter"; | ||||||
| import api from "../app/Api"; | import api from "../app/Api"; | ||||||
| import userManager from "../app/UserManager"; | import userManager from "../app/UserManager"; | ||||||
|  | import EmojiPicker from "./EmojiPicker"; | ||||||
| 
 | 
 | ||||||
| const SendDialog = (props) => { | const SendDialog = (props) => { | ||||||
|     const [topicUrl, setTopicUrl] = useState(""); |     const [topicUrl, setTopicUrl] = useState(""); | ||||||
|  | @ -54,6 +55,8 @@ const SendDialog = (props) => { | ||||||
|     const [status, setStatus] = useState(""); |     const [status, setStatus] = useState(""); | ||||||
|     const disabled = !!activeRequest; |     const disabled = !!activeRequest; | ||||||
| 
 | 
 | ||||||
|  |     const [emojiPickerAnchorEl, setEmojiPickerAnchorEl] = useState(null); | ||||||
|  | 
 | ||||||
|     const [dropZone, setDropZone] = useState(false); |     const [dropZone, setDropZone] = useState(false); | ||||||
|     const [sendButtonEnabled, setSendButtonEnabled] = useState(true); |     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 ( |     return ( | ||||||
|         <> |         <> | ||||||
|             {dropZone && <DropArea |             {dropZone && <DropArea | ||||||
|  | @ -245,7 +260,14 @@ const SendDialog = (props) => { | ||||||
|                         multiline |                         multiline | ||||||
|                     /> |                     /> | ||||||
|                     <div style={{display: 'flex'}}> |                     <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 |                         <TextField | ||||||
|                             margin="dense" |                             margin="dense" | ||||||
|                             label="Tags" |                             label="Tags" | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue