Continued work on send dialog and drag and drop

This commit is contained in:
Philipp Heckel 2022-04-02 17:06:26 -04:00
parent 2c8b258ae7
commit f98743dd9b
5 changed files with 145 additions and 100 deletions

View file

@ -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"}

View file

@ -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
}

View file

@ -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);

View file

@ -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 (
<>
{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}
/>
</>
);
}
const MessageBar = (props) => {
const subscription = props.subscription;
const handleSendClick = () => {
api.publish(subscription.baseUrl, subscription.topic, props.message); // FIXME
props.onMessageChange("");
};
return (
<Backdrop
sx={{ color: '#fff', zIndex: 3500 }}
open={showDropZone}
onClick={() => setShowDropZone(false)}
onDragEnter={allowDrag}
onDragOver={allowDrag}
onDragLeave={() => setShowDropZone(false)}
onDrop={handleDrop}
<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]
}}
>
</Backdrop>
<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>
);
};

View file

@ -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"