Browse Source

Continued work on send dialog and drag and drop

Philipp Heckel 2 years ago
parent
commit
f98743dd9b
5 changed files with 126 additions and 81 deletions
  1. 1 1
      server/errors.go
  2. 3 2
      server/server.go
  3. 3 6
      web/src/app/Api.js
  4. 58 64
      web/src/components/App.js
  5. 61 8
      web/src/components/SendDialog.js

+ 1 - 1
server/errors.go

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

+ 3 - 2
server/server.go

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

+ 3 - 6
web/src/app/Api.js

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

+ 58 - 64
web/src/components/App.js

@@ -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,26 +124,68 @@ const Main = (props) => {
     );
     );
 };
 };
 
 
-const Sender = (props) => {
+const Messaging = (props) => {
     const [message, setMessage] = useState("");
     const [message, setMessage] = useState("");
-    const [sendDialogKey, setSendDialogKey] = useState(0);
-    const [sendDialogOpen, setSendDialogOpen] = useState(false);
+    const [dialogKey, setDialogKey] = useState(0);
+    const [showDialog, setShowDialog] = useState(false);
+    const [showDropZone, setShowDropZone] = useState(false);
+
     const subscription = props.selected;
     const subscription = props.selected;
+    const selectedTopicUrl = (subscription) ? topicUrl(subscription.baseUrl, subscription.topic) : "";
 
 
-    const handleSendClick = () => {
-        api.publish(subscription.baseUrl, subscription.topic, message); // FIXME
-        setMessage("");
-    };
+    useEffect(() => {
+        window.addEventListener('dragenter', () => {
+            setShowDialog(true);
+            setShowDropZone(true);
+        });
+    }, []);
 
 
     const handleSendDialogClose = () => {
     const handleSendDialogClose = () => {
-        setSendDialogOpen(false);
-        setSendDialogKey(prev => prev+1);
+        setShowDialog(false);
+        setShowDropZone(false);
+        setDialogKey(prev => prev+1);
     };
     };
 
 
-    if (!props.selected) {
-        return null;
-    }
+    const allowSubmit = () => true;
 
 
+    const allowDrag = (e) => {
+        if (allowSubmit()) {
+            e.dataTransfer.dropEffect = 'copy';
+            e.preventDefault();
+        }
+    };
+    const handleDrop = (e) => {
+        e.preventDefault();
+        setShowDropZone(false);
+        console.log(e.dataTransfer.files[0]);
+    };
+
+    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 (
     return (
         <Paper
         <Paper
             elevation={3}
             elevation={3}
@@ -158,7 +199,7 @@ const Sender = (props) => {
                 backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900]
                 backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900]
             }}
             }}
         >
         >
-            <IconButton color="inherit" size="large" edge="start" onClick={() => setSendDialogOpen(true)}>
+            <IconButton color="inherit" size="large" edge="start" onClick={props.onOpenDialogClick}>
                 <KeyboardArrowUpIcon/>
                 <KeyboardArrowUpIcon/>
             </IconButton>
             </IconButton>
             <TextField
             <TextField
@@ -168,8 +209,8 @@ const Sender = (props) => {
                 type="text"
                 type="text"
                 fullWidth
                 fullWidth
                 variant="standard"
                 variant="standard"
-                value={message}
-                onChange={ev => setMessage(ev.target.value)}
+                value={props.message}
+                onChange={ev => props.onMessageChange(ev.target.value)}
                 onKeyPress={(ev) => {
                 onKeyPress={(ev) => {
                     if (ev.key === 'Enter') {
                     if (ev.key === 'Enter') {
                         ev.preventDefault();
                         ev.preventDefault();
@@ -180,57 +221,10 @@ const Sender = (props) => {
             <IconButton color="inherit" size="large" edge="end" onClick={handleSendClick}>
             <IconButton color="inherit" size="large" edge="end" onClick={handleSendClick}>
                 <SendIcon/>
                 <SendIcon/>
             </IconButton>
             </IconButton>
-            <SendDialog
-                key={`sendDialog${sendDialogKey}`} // Resets dialog when canceled/closed
-                open={sendDialogOpen}
-                onClose={handleSendDialogClose}
-                topicUrl={topicUrl(subscription.baseUrl, subscription.topic)}
-                message={message}
-            />
         </Paper>
         </Paper>
     );
     );
 };
 };
 
 
-const DropZone = (props) => {
-    const [showDropZone, setShowDropZone] = useState(false);
-
-    useEffect(() => {
-        window.addEventListener('dragenter', () => setShowDropZone(true));
-    }, []);
-
-    const allowSubmit = () => true;
-
-    const allowDrag = (e) => {
-        if (allowSubmit()) {
-            e.dataTransfer.dropEffect = 'copy';
-            e.preventDefault();
-        }
-    };
-    const handleDrop = (e) => {
-        e.preventDefault();
-        setShowDropZone(false);
-        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}
-        >
-
-        </Backdrop>
-    );
-};
-
 const updateTitle = (newNotificationsCount) => {
 const updateTitle = (newNotificationsCount) => {
     document.title = (newNotificationsCount > 0) ? `(${newNotificationsCount}) ntfy` : "ntfy";
     document.title = (newNotificationsCount > 0) ? `(${newNotificationsCount}) ntfy` : "ntfy";
 }
 }

+ 61 - 8
web/src/components/SendDialog.js

@@ -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>}
-                {!sendRequest &&
+                {activeRequest && <Button onClick={() => activeRequest.abort()}>Cancel sending</Button>}
+                {!activeRequest &&
                     <>
                     <>
                         <FormControlLabel
                         <FormControlLabel
                             label="Publish another"
                             label="Publish another"