Continued work on send dialog and drag and drop
This commit is contained in:
		
							parent
							
								
									2c8b258ae7
								
							
						
					
					
						commit
						f98743dd9b
					
				
					 5 changed files with 145 additions and 100 deletions
				
			
		|  | @ -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 ( | ||||
|         <> | ||||
|             {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> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue