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…
Reference in a new issue