mirror of
https://github.com/binwiederhier/ntfy.git
synced 2024-11-23 03:24:27 +03:00
Continued work on send dialog and drag and drop
This commit is contained in:
parent
2c8b258ae7
commit
f98743dd9b
@ -34,7 +34,6 @@ var (
|
||||
errHTTPBadRequestTopicInvalid = &errHTTP{40009, http.StatusBadRequest, "invalid topic: path invalid", ""}
|
||||
errHTTPBadRequestTopicDisallowed = &errHTTP{40010, http.StatusBadRequest, "invalid topic: topic name is disallowed", ""}
|
||||
errHTTPBadRequestMessageNotUTF8 = &errHTTP{40011, http.StatusBadRequest, "invalid message: message must be UTF-8 encoded", ""}
|
||||
errHTTPBadRequestAttachmentTooLarge = &errHTTP{40012, http.StatusBadRequest, "invalid request: attachment too large, or bandwidth limit reached", ""}
|
||||
errHTTPBadRequestAttachmentURLInvalid = &errHTTP{40013, http.StatusBadRequest, "invalid request: attachment URL is invalid", "https://ntfy.sh/docs/publish/#attachments"}
|
||||
errHTTPBadRequestAttachmentsDisallowed = &errHTTP{40014, http.StatusBadRequest, "invalid request: attachments not allowed", "https://ntfy.sh/docs/config/#attachments"}
|
||||
errHTTPBadRequestAttachmentsExpiryBeforeDelivery = &errHTTP{40015, http.StatusBadRequest, "invalid request: attachment expiry before delayed delivery date", "https://ntfy.sh/docs/publish/#scheduled-delivery"}
|
||||
@ -43,6 +42,7 @@ var (
|
||||
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""}
|
||||
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"}
|
||||
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"}
|
||||
errHTTPEntityTooLargeAttachmentTooLarge = &errHTTP{41301, http.StatusRequestEntityTooLarge, "attachment too large, or bandwidth limit reached", ""}
|
||||
errHTTPTooManyRequestsLimitRequests = &errHTTP{42901, http.StatusTooManyRequests, "limit reached: too many requests, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
|
||||
errHTTPTooManyRequestsLimitEmails = &errHTTP{42902, http.StatusTooManyRequests, "limit reached: too many emails, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
|
||||
errHTTPTooManyRequestsLimitSubscriptions = &errHTTP{42903, http.StatusTooManyRequests, "limit reached: too many active subscriptions, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
|
||||
|
@ -395,6 +395,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return errHTTPEntityTooLargeAttachmentTooLarge
|
||||
body, err := util.Peak(r.Body, s.config.MessageLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -590,7 +591,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
|
||||
if contentLengthStr != "" { // Early "do-not-trust" check, hard limit see below
|
||||
contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64)
|
||||
if err == nil && (contentLength > remainingVisitorAttachmentSize || contentLength > s.config.AttachmentFileSizeLimit) {
|
||||
return errHTTPBadRequestAttachmentTooLarge
|
||||
return errHTTPEntityTooLargeAttachmentTooLarge
|
||||
}
|
||||
}
|
||||
if m.Attachment == nil {
|
||||
@ -609,7 +610,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
|
||||
}
|
||||
m.Attachment.Size, err = s.fileCache.Write(m.ID, body, v.BandwidthLimiter(), util.NewFixedLimiter(remainingVisitorAttachmentSize))
|
||||
if err == util.ErrLimitReached {
|
||||
return errHTTPBadRequestAttachmentTooLarge
|
||||
return errHTTPEntityTooLargeAttachmentTooLarge
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -52,19 +52,16 @@ class Api {
|
||||
const send = new Promise(function (resolve, reject) {
|
||||
xhr.open("PUT", url);
|
||||
xhr.addEventListener('readystatechange', (ev) => {
|
||||
console.log("read change", xhr.readyState, xhr.status, xhr.responseText, xhr)
|
||||
if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status <= 299) {
|
||||
console.log(`[Api] Publish successful`, ev);
|
||||
console.log(`[Api] Publish successful (HTTP ${xhr.status})`, xhr.response);
|
||||
resolve(xhr.response);
|
||||
} else if (xhr.readyState === 4) {
|
||||
console.log(`[Api] Publish failed (1)`, ev);
|
||||
console.log(`[Api] Publish failed`, xhr.status, xhr.responseText, xhr);
|
||||
xhr.abort();
|
||||
reject(ev);
|
||||
}
|
||||
})
|
||||
xhr.onerror = (ev) => {
|
||||
console.log(`[Api] Publish failed (2)`, ev);
|
||||
reject(ev);
|
||||
};
|
||||
xhr.upload.addEventListener("progress", onProgress);
|
||||
if (body.type) {
|
||||
xhr.overrideMimeType(body.type);
|
||||
|
@ -82,7 +82,6 @@ const Layout = () => {
|
||||
return (
|
||||
<Box sx={{display: 'flex'}}>
|
||||
<CssBaseline/>
|
||||
<DropZone/>
|
||||
<ActionBar
|
||||
selected={selected}
|
||||
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
|
||||
@ -99,7 +98,7 @@ const Layout = () => {
|
||||
<Toolbar/>
|
||||
<Outlet context={{ subscriptions, selected }}/>
|
||||
</Main>
|
||||
<Sender selected={selected}/>
|
||||
<Messaging selected={selected}/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@ -125,79 +124,28 @@ const Main = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Sender = (props) => {
|
||||
const Messaging = (props) => {
|
||||
const [message, setMessage] = useState("");
|
||||
const [sendDialogKey, setSendDialogKey] = useState(0);
|
||||
const [sendDialogOpen, setSendDialogOpen] = useState(false);
|
||||
const subscription = props.selected;
|
||||
|
||||
const handleSendClick = () => {
|
||||
api.publish(subscription.baseUrl, subscription.topic, message); // FIXME
|
||||
setMessage("");
|
||||
};
|
||||
|
||||
const handleSendDialogClose = () => {
|
||||
setSendDialogOpen(false);
|
||||
setSendDialogKey(prev => prev+1);
|
||||
};
|
||||
|
||||
if (!props.selected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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={() => setSendDialogOpen(true)}>
|
||||
<KeyboardArrowUpIcon/>
|
||||
</IconButton>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
placeholder="Message"
|
||||
type="text"
|
||||
fullWidth
|
||||
variant="standard"
|
||||
value={message}
|
||||
onChange={ev => setMessage(ev.target.value)}
|
||||
onKeyPress={(ev) => {
|
||||
if (ev.key === 'Enter') {
|
||||
ev.preventDefault();
|
||||
handleSendClick();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton color="inherit" size="large" edge="end" onClick={handleSendClick}>
|
||||
<SendIcon/>
|
||||
</IconButton>
|
||||
<SendDialog
|
||||
key={`sendDialog${sendDialogKey}`} // Resets dialog when canceled/closed
|
||||
open={sendDialogOpen}
|
||||
onClose={handleSendDialogClose}
|
||||
topicUrl={topicUrl(subscription.baseUrl, subscription.topic)}
|
||||
message={message}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
const DropZone = (props) => {
|
||||
const [dialogKey, setDialogKey] = useState(0);
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [showDropZone, setShowDropZone] = useState(false);
|
||||
|
||||
const subscription = props.selected;
|
||||
const selectedTopicUrl = (subscription) ? topicUrl(subscription.baseUrl, subscription.topic) : "";
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('dragenter', () => setShowDropZone(true));
|
||||
window.addEventListener('dragenter', () => {
|
||||
setShowDialog(true);
|
||||
setShowDropZone(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSendDialogClose = () => {
|
||||
setShowDialog(false);
|
||||
setShowDropZone(false);
|
||||
setDialogKey(prev => prev+1);
|
||||
};
|
||||
|
||||
const allowSubmit = () => true;
|
||||
|
||||
const allowDrag = (e) => {
|
||||
@ -212,22 +160,68 @@ const DropZone = (props) => {
|
||||
console.log(e.dataTransfer.files[0]);
|
||||
};
|
||||
|
||||
if (!showDropZone) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Backdrop
|
||||
sx={{ color: '#fff', zIndex: 3500 }}
|
||||
open={showDropZone}
|
||||
onClick={() => setShowDropZone(false)}
|
||||
onDragEnter={allowDrag}
|
||||
onDragOver={allowDrag}
|
||||
onDragLeave={() => setShowDropZone(false)}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<>
|
||||
{subscription && <MessageBar
|
||||
subscription={subscription}
|
||||
message={message}
|
||||
onMessageChange={setMessage}
|
||||
onOpenDialogClick={() => setShowDialog(true)}
|
||||
/>}
|
||||
<SendDialog
|
||||
key={`sendDialog${dialogKey}`} // Resets dialog when canceled/closed
|
||||
open={showDialog}
|
||||
dropZone={showDropZone}
|
||||
onClose={handleSendDialogClose}
|
||||
topicUrl={selectedTopicUrl}
|
||||
message={message}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
</Backdrop>
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -40,7 +40,7 @@ const SendDialog = (props) => {
|
||||
const [delay, setDelay] = useState("");
|
||||
const [publishAnother, setPublishAnother] = useState(false);
|
||||
|
||||
const [showTopicUrl, setShowTopicUrl] = useState(props.topicUrl === "");
|
||||
const [showTopicUrl, setShowTopicUrl] = useState(props.topicUrl === ""); // FIXME
|
||||
const [showClickUrl, setShowClickUrl] = useState(false);
|
||||
const [showAttachUrl, setShowAttachUrl] = useState(false);
|
||||
const [showEmail, setShowEmail] = useState(false);
|
||||
@ -49,17 +49,21 @@ const SendDialog = (props) => {
|
||||
const showAttachFile = !!attachFile && !showAttachUrl;
|
||||
const attachFileInput = useRef();
|
||||
|
||||
const [sendRequest, setSendRequest] = useState(null);
|
||||
const [activeRequest, setActiveRequest] = useState(null);
|
||||
const [statusText, setStatusText] = useState("");
|
||||
const disabled = !!sendRequest;
|
||||
const disabled = !!activeRequest;
|
||||
|
||||
const dropZone = props.dropZone;
|
||||
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
const sendButtonEnabled = (() => {
|
||||
if (!validTopicUrl(topicUrl)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const { baseUrl, topic } = splitTopicUrl(topicUrl);
|
||||
const headers = {};
|
||||
@ -106,7 +110,7 @@ const SendDialog = (props) => {
|
||||
}
|
||||
};
|
||||
const request = api.publishXHR(baseUrl, topic, body, headers, progressFn);
|
||||
setSendRequest(request);
|
||||
setActiveRequest(request);
|
||||
await request;
|
||||
if (!publishAnother) {
|
||||
props.onClose();
|
||||
@ -117,11 +121,13 @@ const SendDialog = (props) => {
|
||||
console.log("error", e);
|
||||
setStatusText("An error occurred");
|
||||
}
|
||||
setSendRequest(null);
|
||||
setActiveRequest(null);
|
||||
};
|
||||
|
||||
const handleAttachFileClick = () => {
|
||||
attachFileInput.current.click();
|
||||
};
|
||||
|
||||
const handleAttachFileChanged = (ev) => {
|
||||
const file = ev.target.files[0];
|
||||
setAttachFile(file);
|
||||
@ -129,10 +135,57 @@ const SendDialog = (props) => {
|
||||
console.log(ev.target.files[0]);
|
||||
console.log(URL.createObjectURL(ev.target.files[0]));
|
||||
};
|
||||
|
||||
const handleDrop = (ev) => {
|
||||
ev.preventDefault();
|
||||
const file = ev.dataTransfer.files[0];
|
||||
setAttachFile(file);
|
||||
setFilename(file.name);
|
||||
};
|
||||
|
||||
const allowDrag = (ev) => {
|
||||
if (true /* allowSubmit */) {
|
||||
ev.dataTransfer.dropEffect = 'copy';
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog maxWidth="md" open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
|
||||
<DialogTitle>Publish to {shortUrl(topicUrl)}</DialogTitle>
|
||||
<DialogContent>
|
||||
{dropZone &&
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 10000,
|
||||
backgroundColor: "#ffffffbb"
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
border: '3px dashed #ccc',
|
||||
borderRadius: '5px',
|
||||
left: "40px",
|
||||
top: "40px",
|
||||
right: "40px",
|
||||
bottom: "40px",
|
||||
zIndex: 10001,
|
||||
display: 'flex',
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
onDragEnter={allowDrag}
|
||||
onDragOver={allowDrag}
|
||||
>
|
||||
<Typography variant="h5">Drop file here</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
{showTopicUrl &&
|
||||
<ClosableRow disabled={disabled} onClose={() => {
|
||||
setTopicUrl(props.topicUrl);
|
||||
@ -203,7 +256,7 @@ const SendDialog = (props) => {
|
||||
disabled={disabled}
|
||||
>
|
||||
{[5,4,3,2,1].map(priority =>
|
||||
<MenuItem value={priority}>
|
||||
<MenuItem key={`priorityMenuItem${priority}`} value={priority}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<img src={priorities[priority].file} style={{marginRight: "8px"}}/>
|
||||
<div>{priorities[priority].label}</div>
|
||||
@ -348,8 +401,8 @@ const SendDialog = (props) => {
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogFooter status={statusText}>
|
||||
{sendRequest && <Button onClick={() => sendRequest.abort()}>Cancel sending</Button>}
|
||||
{!sendRequest &&
|
||||
{activeRequest && <Button onClick={() => activeRequest.abort()}>Cancel sending</Button>}
|
||||
{!activeRequest &&
|
||||
<>
|
||||
<FormControlLabel
|
||||
label="Publish another"
|
||||
|
Loading…
Reference in New Issue
Block a user