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", ""} errHTTPBadRequestTopicInvalid = &errHTTP{40009, http.StatusBadRequest, "invalid topic: path invalid", ""}
errHTTPBadRequestTopicDisallowed = &errHTTP{40010, http.StatusBadRequest, "invalid topic: topic name is disallowed", ""} errHTTPBadRequestTopicDisallowed = &errHTTP{40010, http.StatusBadRequest, "invalid topic: topic name is disallowed", ""}
errHTTPBadRequestMessageNotUTF8 = &errHTTP{40011, http.StatusBadRequest, "invalid message: message must be UTF-8 encoded", ""} 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"} 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"} 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"} 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", ""} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""}
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"}
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "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"} 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"} 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"} 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 { if err != nil {
return err return err
} }
return errHTTPEntityTooLargeAttachmentTooLarge
body, err := util.Peak(r.Body, s.config.MessageLimit) body, err := util.Peak(r.Body, s.config.MessageLimit)
if err != nil { if err != nil {
return err 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 if contentLengthStr != "" { // Early "do-not-trust" check, hard limit see below
contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64) contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64)
if err == nil && (contentLength > remainingVisitorAttachmentSize || contentLength > s.config.AttachmentFileSizeLimit) { if err == nil && (contentLength > remainingVisitorAttachmentSize || contentLength > s.config.AttachmentFileSizeLimit) {
return errHTTPBadRequestAttachmentTooLarge return errHTTPEntityTooLargeAttachmentTooLarge
} }
} }
if m.Attachment == nil { 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)) m.Attachment.Size, err = s.fileCache.Write(m.ID, body, v.BandwidthLimiter(), util.NewFixedLimiter(remainingVisitorAttachmentSize))
if err == util.ErrLimitReached { if err == util.ErrLimitReached {
return errHTTPBadRequestAttachmentTooLarge return errHTTPEntityTooLargeAttachmentTooLarge
} else if err != nil { } else if err != nil {
return err return err
} }

View file

@ -52,19 +52,16 @@ class Api {
const send = new Promise(function (resolve, reject) { const send = new Promise(function (resolve, reject) {
xhr.open("PUT", url); xhr.open("PUT", url);
xhr.addEventListener('readystatechange', (ev) => { 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) { 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); resolve(xhr.response);
} else if (xhr.readyState === 4) { } else if (xhr.readyState === 4) {
console.log(`[Api] Publish failed (1)`, ev); console.log(`[Api] Publish failed`, xhr.status, xhr.responseText, xhr);
xhr.abort(); xhr.abort();
reject(ev); reject(ev);
} }
}) })
xhr.onerror = (ev) => {
console.log(`[Api] Publish failed (2)`, ev);
reject(ev);
};
xhr.upload.addEventListener("progress", onProgress); xhr.upload.addEventListener("progress", onProgress);
if (body.type) { if (body.type) {
xhr.overrideMimeType(body.type); xhr.overrideMimeType(body.type);

View file

@ -82,7 +82,6 @@ const Layout = () => {
return ( return (
<Box sx={{display: 'flex'}}> <Box sx={{display: 'flex'}}>
<CssBaseline/> <CssBaseline/>
<DropZone/>
<ActionBar <ActionBar
selected={selected} selected={selected}
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
@ -99,7 +98,7 @@ const Layout = () => {
<Toolbar/> <Toolbar/>
<Outlet context={{ subscriptions, selected }}/> <Outlet context={{ subscriptions, selected }}/>
</Main> </Main>
<Sender selected={selected}/> <Messaging selected={selected}/>
</Box> </Box>
); );
} }
@ -125,79 +124,28 @@ const Main = (props) => {
); );
}; };
const Sender = (props) => { const Messaging = (props) => {
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const [sendDialogKey, setSendDialogKey] = useState(0); const [dialogKey, setDialogKey] = useState(0);
const [sendDialogOpen, setSendDialogOpen] = useState(false); const [showDialog, setShowDialog] = 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 [showDropZone, setShowDropZone] = useState(false); const [showDropZone, setShowDropZone] = useState(false);
const subscription = props.selected;
const selectedTopicUrl = (subscription) ? topicUrl(subscription.baseUrl, subscription.topic) : "";
useEffect(() => { 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 allowSubmit = () => true;
const allowDrag = (e) => { const allowDrag = (e) => {
@ -212,22 +160,68 @@ const DropZone = (props) => {
console.log(e.dataTransfer.files[0]); console.log(e.dataTransfer.files[0]);
}; };
if (!showDropZone) {
return null;
}
return ( return (
<Backdrop <>
sx={{ color: '#fff', zIndex: 3500 }} {subscription && <MessageBar
open={showDropZone} subscription={subscription}
onClick={() => setShowDropZone(false)} message={message}
onDragEnter={allowDrag} onMessageChange={setMessage}
onDragOver={allowDrag} onOpenDialogClick={() => setShowDialog(true)}
onDragLeave={() => setShowDropZone(false)} />}
onDrop={handleDrop} <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>
); );
}; };

View file

@ -40,7 +40,7 @@ const SendDialog = (props) => {
const [delay, setDelay] = useState(""); const [delay, setDelay] = useState("");
const [publishAnother, setPublishAnother] = useState(false); const [publishAnother, setPublishAnother] = useState(false);
const [showTopicUrl, setShowTopicUrl] = useState(props.topicUrl === ""); const [showTopicUrl, setShowTopicUrl] = useState(props.topicUrl === ""); // FIXME
const [showClickUrl, setShowClickUrl] = useState(false); const [showClickUrl, setShowClickUrl] = useState(false);
const [showAttachUrl, setShowAttachUrl] = useState(false); const [showAttachUrl, setShowAttachUrl] = useState(false);
const [showEmail, setShowEmail] = useState(false); const [showEmail, setShowEmail] = useState(false);
@ -49,17 +49,21 @@ const SendDialog = (props) => {
const showAttachFile = !!attachFile && !showAttachUrl; const showAttachFile = !!attachFile && !showAttachUrl;
const attachFileInput = useRef(); const attachFileInput = useRef();
const [sendRequest, setSendRequest] = useState(null); const [activeRequest, setActiveRequest] = useState(null);
const [statusText, setStatusText] = useState(""); const [statusText, setStatusText] = useState("");
const disabled = !!sendRequest; const disabled = !!activeRequest;
const dropZone = props.dropZone;
const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const sendButtonEnabled = (() => { const sendButtonEnabled = (() => {
if (!validTopicUrl(topicUrl)) { if (!validTopicUrl(topicUrl)) {
return false; return false;
} }
return true; return true;
})(); })();
const handleSubmit = async () => { const handleSubmit = async () => {
const { baseUrl, topic } = splitTopicUrl(topicUrl); const { baseUrl, topic } = splitTopicUrl(topicUrl);
const headers = {}; const headers = {};
@ -106,7 +110,7 @@ const SendDialog = (props) => {
} }
}; };
const request = api.publishXHR(baseUrl, topic, body, headers, progressFn); const request = api.publishXHR(baseUrl, topic, body, headers, progressFn);
setSendRequest(request); setActiveRequest(request);
await request; await request;
if (!publishAnother) { if (!publishAnother) {
props.onClose(); props.onClose();
@ -117,11 +121,13 @@ const SendDialog = (props) => {
console.log("error", e); console.log("error", e);
setStatusText("An error occurred"); setStatusText("An error occurred");
} }
setSendRequest(null); setActiveRequest(null);
}; };
const handleAttachFileClick = () => { const handleAttachFileClick = () => {
attachFileInput.current.click(); attachFileInput.current.click();
}; };
const handleAttachFileChanged = (ev) => { const handleAttachFileChanged = (ev) => {
const file = ev.target.files[0]; const file = ev.target.files[0];
setAttachFile(file); setAttachFile(file);
@ -129,10 +135,57 @@ const SendDialog = (props) => {
console.log(ev.target.files[0]); console.log(ev.target.files[0]);
console.log(URL.createObjectURL(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 ( return (
<Dialog maxWidth="md" open={props.open} onClose={props.onCancel} fullScreen={fullScreen}> <Dialog maxWidth="md" open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
<DialogTitle>Publish to {shortUrl(topicUrl)}</DialogTitle> <DialogTitle>Publish to {shortUrl(topicUrl)}</DialogTitle>
<DialogContent> <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 && {showTopicUrl &&
<ClosableRow disabled={disabled} onClose={() => { <ClosableRow disabled={disabled} onClose={() => {
setTopicUrl(props.topicUrl); setTopicUrl(props.topicUrl);
@ -203,7 +256,7 @@ const SendDialog = (props) => {
disabled={disabled} disabled={disabled}
> >
{[5,4,3,2,1].map(priority => {[5,4,3,2,1].map(priority =>
<MenuItem value={priority}> <MenuItem key={`priorityMenuItem${priority}`} value={priority}>
<div style={{ display: 'flex', alignItems: 'center' }}> <div style={{ display: 'flex', alignItems: 'center' }}>
<img src={priorities[priority].file} style={{marginRight: "8px"}}/> <img src={priorities[priority].file} style={{marginRight: "8px"}}/>
<div>{priorities[priority].label}</div> <div>{priorities[priority].label}</div>
@ -348,8 +401,8 @@ const SendDialog = (props) => {
</Typography> </Typography>
</DialogContent> </DialogContent>
<DialogFooter status={statusText}> <DialogFooter status={statusText}>
{sendRequest && <Button onClick={() => sendRequest.abort()}>Cancel sending</Button>} {activeRequest && <Button onClick={() => activeRequest.abort()}>Cancel sending</Button>}
{!sendRequest && {!activeRequest &&
<> <>
<FormControlLabel <FormControlLabel
label="Publish another" label="Publish another"