diff --git a/web/src/app/Api.js b/web/src/app/Api.js index 8f823ca..73a6dde 100644 --- a/web/src/app/Api.js +++ b/web/src/app/Api.js @@ -52,14 +52,22 @@ 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 (HTTP ${xhr.status})`, xhr.response); resolve(xhr.response); } else if (xhr.readyState === 4) { - console.log(`[Api] Publish failed`, xhr.status, xhr.responseText, xhr); + console.log(`[Api] Publish failed (HTTP ${xhr.status})`, xhr.responseText); + let errorText; + try { + const error = JSON.parse(xhr.responseText); + if (error.code && error.error) { + errorText = `Error ${error.code}: ${error.error}`; + } + } catch (e) { + // Nothing + } xhr.abort(); - reject(ev); + reject(errorText ?? "An error occurred"); } }) xhr.upload.addEventListener("progress", onProgress); diff --git a/web/src/components/App.js b/web/src/components/App.js index e25c732..0174ca7 100644 --- a/web/src/components/App.js +++ b/web/src/components/App.js @@ -18,10 +18,8 @@ import {expandUrl, topicUrl} from "../app/utils"; import ErrorBoundary from "./ErrorBoundary"; import routes from "./routes"; import {useAutoSubscribe, useBackgroundProcesses, useConnectionListeners} from "./hooks"; -import {Backdrop} from "@mui/material"; import Paper from "@mui/material/Paper"; import IconButton from "@mui/material/IconButton"; -import {MoreVert} from "@mui/icons-material"; import TextField from "@mui/material/TextField"; import SendIcon from "@mui/icons-material/Send"; import api from "../app/Api"; @@ -127,50 +125,54 @@ const Main = (props) => { const Messaging = (props) => { const [message, setMessage] = useState(""); const [dialogKey, setDialogKey] = useState(0); + const [dialogOpenMode, setDialogOpenMode] = useState(""); const [showDialog, setShowDialog] = useState(false); const [showDropZone, setShowDropZone] = useState(false); const subscription = props.selected; const selectedTopicUrl = (subscription) ? topicUrl(subscription.baseUrl, subscription.topic) : ""; + const handleWindowDragEnter = () => { + setDialogOpenMode(prev => (prev) ? prev : SendDialog.OPEN_MODE_DRAG); // Only update if not already open + setShowDialog(true); + setShowDropZone(true); + }; + useEffect(() => { - window.addEventListener('dragenter', () => { - setShowDialog(true); - setShowDropZone(true); - }); + window.addEventListener('dragenter', handleWindowDragEnter); }, []); + const handleOpenDialogClick = () => { + setDialogOpenMode(SendDialog.OPEN_MODE_DEFAULT); + setShowDialog(true); + setShowDropZone(false); + }; + const handleSendDialogClose = () => { setShowDialog(false); setShowDropZone(false); + setDialogOpenMode(""); setDialogKey(prev => prev+1); }; - const allowSubmit = () => true; - - const allowDrag = (e) => { - if (allowSubmit()) { - e.dataTransfer.dropEffect = 'copy'; - e.preventDefault(); - } - }; - return ( <> {subscription && setShowDialog(true)} + onOpenDialogClick={handleOpenDialogClick} />} setShowDropZone(false)} topicUrl={selectedTopicUrl} message={message} + open={showDialog} + openMode={dialogOpenMode} + dropZone={showDropZone} + onClose={handleSendDialogClose} + onHideDropZone={() => setShowDropZone(false)} + onResetOpenMode={() => setDialogOpenMode(SendDialog.OPEN_MODE_DEFAULT)} /> ); diff --git a/web/src/components/DialogFooter.js b/web/src/components/DialogFooter.js index 199398a..3f00f87 100644 --- a/web/src/components/DialogFooter.js +++ b/web/src/components/DialogFooter.js @@ -10,16 +10,16 @@ const DialogFooter = (props) => { flexDirection: 'row', justifyContent: 'space-between', paddingLeft: '24px', - paddingTop: '8px 24px', - paddingBottom: '8px 24px', + paddingBottom: '8px', }}> - {props.status} - + {props.children} diff --git a/web/src/components/SendDialog.js b/web/src/components/SendDialog.js index 4d6eb6f..da8ba95 100644 --- a/web/src/components/SendDialog.js +++ b/web/src/components/SendDialog.js @@ -27,7 +27,7 @@ import userManager from "../app/UserManager"; const SendDialog = (props) => { const [topicUrl, setTopicUrl] = useState(""); - const [message, setMessage] = useState(props.message || ""); + const [message, setMessage] = useState(""); const [title, setTitle] = useState(""); const [tags, setTags] = useState(""); const [priority, setPriority] = useState(3); @@ -51,7 +51,7 @@ const SendDialog = (props) => { const [attachFileError, setAttachFileError] = useState(""); const [activeRequest, setActiveRequest] = useState(null); - const [statusText, setStatusText] = useState(""); + const [status, setStatus] = useState(""); const disabled = !!activeRequest; const [sendButtonEnabled, setSendButtonEnabled] = useState(true); @@ -65,9 +65,14 @@ const SendDialog = (props) => { }, [props.topicUrl]); useEffect(() => { - setSendButtonEnabled(validTopicUrl(topicUrl) && !attachFileError); + const valid = validTopicUrl(topicUrl) && !attachFileError; + setSendButtonEnabled(valid); }, [topicUrl, attachFileError]); + useEffect(() => { + setMessage(props.message); + }, [props.message]); + const handleSubmit = async () => { const { baseUrl, topic } = splitTopicUrl(topicUrl); const headers = {}; @@ -105,12 +110,11 @@ const SendDialog = (props) => { headers["Authorization"] = basicAuth(user.username, user.password); } const progressFn = (ev) => { - console.log(ev); if (ev.loaded > 0 && ev.total > 0) { const percent = Math.round(ev.loaded * 100.0 / ev.total); - setStatusText(`Uploading ${formatBytes(ev.loaded)}/${formatBytes(ev.total)} (${percent}%) ...`); + setStatus(`Uploading ${formatBytes(ev.loaded)}/${formatBytes(ev.total)} (${percent}%) ...`); } else { - setStatusText(`Uploading ...`); + setStatus(`Uploading ...`); } }; const request = api.publishXHR(baseUrl, topic, body, headers, progressFn); @@ -119,13 +123,13 @@ const SendDialog = (props) => { if (!publishAnother) { props.onClose(); } else { - setStatusText("Message published"); + setStatus("Message published"); + setActiveRequest(null); } } catch (e) { - console.log("error", e); - setStatusText("An error occurred"); + setStatus({e}); + setActiveRequest(null); } - setActiveRequest(null); }; const checkAttachmentLimits = async (file) => { @@ -133,17 +137,17 @@ const SendDialog = (props) => { const { baseUrl } = splitTopicUrl(topicUrl); const stats = await api.userStats(baseUrl); console.log(`[SendDialog] Visitor attachment limits`, stats); - const fileSizeLimit = stats.attachmentFileSizeLimit ?? 0; - if (fileSizeLimit > 0 && file.size > fileSizeLimit) { - return setAttachFileError(`exceeds ${formatBytes(fileSizeLimit)} limit`); - } - const remainingBytes = stats.visitorAttachmentBytesRemaining ?? 0; - if (remainingBytes > 0 && file.size > remainingBytes) { + const fileSizeLimitReached = fileSizeLimit > 0 && file.size > fileSizeLimit; + const quotaReached = remainingBytes > 0 && file.size > remainingBytes; + if (fileSizeLimitReached && quotaReached) { + return setAttachFileError(`exceeds ${formatBytes(fileSizeLimit)} file limit, quota reached: ${formatBytes(remainingBytes)} remaining`); + } else if (fileSizeLimitReached) { + return setAttachFileError(`exceeds ${formatBytes(fileSizeLimit)} file limit`); + } else if (quotaReached) { return setAttachFileError(`quota reached, only ${formatBytes(remainingBytes)} remaining`); } - setAttachFileError(""); } catch (e) { console.log(`[SendDialog] Retrieving attachment limits failed`, e); @@ -161,291 +165,272 @@ const SendDialog = (props) => { const handleAttachFileDrop = async (ev) => { ev.preventDefault(); - props.onDrop(); + props.onHideDropZone(); await updateAttachFile(ev.dataTransfer.files[0]); }; const updateAttachFile = async (file) => { setAttachFile(file); setFilename(file.name); + props.onResetOpenMode(); await checkAttachmentLimits(file); }; - const allowDrag = (ev) => { - if (true /* allowSubmit */) { - ev.dataTransfer.dropEffect = 'copy'; - ev.preventDefault(); + const handleAttachFileDragLeave = () => { + // When the dialog was opened by dragging a file in, close it. If it was open + // before, keep it open. + + console.log(`open mode ${props.openMode}`); + if (props.openMode === SendDialog.OPEN_MODE_DRAG) { + props.onClose(); + } else { + props.onHideDropZone(); } }; return ( - - Publish to {shortUrl(topicUrl)} - - {dropZone && - - - Drop file here - - - } - {showTopicUrl && - { - setTopicUrl(props.topicUrl); - setShowTopicUrl(false); - }}> - setTopicUrl(ev.target.value)} - disabled={disabled} - type="text" - variant="standard" - fullWidth - required - /> - - } - setTitle(ev.target.value)} - disabled={disabled} - type="text" - fullWidth - variant="standard" - placeholder="Notification title, e.g. Disk space alert" - /> - setMessage(ev.target.value)} - disabled={disabled} - type="text" - variant="standard" - rows={5} - fullWidth - autoFocus - multiline - /> -
- null}> + <> + {dropZone && + } + + Publish to {shortUrl(topicUrl)} + + {dropZone && } + {showTopicUrl && + { + setTopicUrl(props.topicUrl); + setShowTopicUrl(false); + }}> + setTopicUrl(ev.target.value)} + disabled={disabled} + type="text" + variant="standard" + fullWidth + required + /> + + } setTags(ev.target.value)} + label="Title" + value={title} + onChange={ev => setTitle(ev.target.value)} + disabled={disabled} + type="text" + fullWidth + variant="standard" + placeholder="Notification title, e.g. Disk space alert" + /> + setMessage(ev.target.value)} disabled={disabled} type="text" variant="standard" - sx={{flexGrow: 1, marginRight: 1}} + rows={5} + fullWidth + autoFocus + multiline /> - - - - -
- {showClickUrl && - { - setClickUrl(""); - setShowClickUrl(false); - }}> - setClickUrl(ev.target.value)} - disabled={disabled} - type="url" - fullWidth - variant="standard" - /> - - } - {showEmail && - { - setEmail(""); - setShowEmail(false); - }}> - setEmail(ev.target.value)} - disabled={disabled} - type="email" - variant="standard" - fullWidth - /> - - } - {showAttachUrl && - { - setAttachUrl(""); - setFilename(""); - setFilenameEdited(false); - setShowAttachUrl(false); - }}> - { - const url = ev.target.value; - setAttachUrl(url); - if (!filenameEdited) { - try { - const u = new URL(url); - const parts = u.pathname.split("/"); - if (parts.length > 0) { - setFilename(parts[parts.length-1]); + + + + + {showClickUrl && + { + setClickUrl(""); + setShowClickUrl(false); + }}> + setClickUrl(ev.target.value)} + disabled={disabled} + type="url" + fullWidth + variant="standard" + /> + + } + {showEmail && + { + setEmail(""); + setShowEmail(false); + }}> + setEmail(ev.target.value)} + disabled={disabled} + type="email" + variant="standard" + fullWidth + /> + + } + {showAttachUrl && + { + setAttachUrl(""); + setFilename(""); + setFilenameEdited(false); + setShowAttachUrl(false); + }}> + { + const url = ev.target.value; + setAttachUrl(url); + if (!filenameEdited) { + try { + const u = new URL(url); + const parts = u.pathname.split("/"); + if (parts.length > 0) { + setFilename(parts[parts.length-1]); + } + } catch (e) { + // Do nothing } - } catch (e) { - // Do nothing } - } - }} - disabled={disabled} - type="url" - variant="standard" - sx={{flexGrow: 5, marginRight: 1}} - /> - { - setFilename(ev.target.value); - setFilenameEdited(true); - }} - disabled={disabled} - type="text" - variant="standard" - sx={{flexGrow: 1}} - /> - - } - - {showAttachFile && setFilename(f)} - onClose={() => { - setAttachFile(null); - setAttachFileError(""); - setFilename(""); - }} - />} - {showDelay && - { - setDelay(""); - setShowDelay(false); - }}> - setDelay(ev.target.value)} - disabled={disabled} - type="text" - variant="standard" - fullWidth - /> - - } - - Other features: - -
- {!showClickUrl && setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>} - {!showEmail && setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>} - {!showAttachUrl && !showAttachFile && setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>} - {!showAttachFile && !showAttachUrl && handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>} - {!showDelay && setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>} - {!showTopicUrl && setShowTopicUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>} -
- - For examples and a detailed description of all send features, please - refer to the documentation. - -
- - {activeRequest && } - {!activeRequest && - <> - setPublishAnother(ev.target.checked)} /> - } /> - - - - } - -
+ }} + disabled={disabled} + type="url" + variant="standard" + sx={{flexGrow: 5, marginRight: 1}} + /> + { + setFilename(ev.target.value); + setFilenameEdited(true); + }} + disabled={disabled} + type="text" + variant="standard" + sx={{flexGrow: 1}} + /> + + } + + {showAttachFile && setFilename(f)} + onClose={() => { + setAttachFile(null); + setAttachFileError(""); + setFilename(""); + }} + />} + {showDelay && + { + setDelay(""); + setShowDelay(false); + }}> + setDelay(ev.target.value)} + disabled={disabled} + type="text" + variant="standard" + fullWidth + /> + + } + + Other features: + +
+ {!showClickUrl && setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>} + {!showEmail && setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>} + {!showAttachUrl && !showAttachFile && setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>} + {!showAttachFile && !showAttachUrl && handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>} + {!showDelay && setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>} + {!showTopicUrl && setShowTopicUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>} +
+ + For examples and a detailed description of all send features, please + refer to the documentation. + + + + {activeRequest && } + {!activeRequest && + <> + setPublishAnother(ev.target.checked)} /> + } /> + + + + } + + + ); }; @@ -539,7 +524,7 @@ const ExpandingTextField = (props) => { ref={invisibleFieldRef} component="span" variant={props.variant} - sx={{position: "absolute", left: "-100%"}} + sx={{position: "absolute", left: "-200%"}} > {props.value} @@ -559,12 +544,74 @@ const ExpandingTextField = (props) => { ) }; +const DropArea = (props) => { + const allowDrag = (ev) => { + // This is where we could disallow certain files to be dragged in. + // For now we allow all files. + + ev.dataTransfer.dropEffect = 'copy'; + ev.preventDefault(); + }; + + return ( + + ); +}; + +const DropBox = () => { + return ( + + + Drop file here + + + ); +} + const priorities = { - 1: { label: "Minimum priority", file: priority1 }, + 1: { label: "Min. priority", file: priority1 }, 2: { label: "Low priority", file: priority2 }, 3: { label: "Default priority", file: priority3 }, 4: { label: "High priority", file: priority4 }, - 5: { label: "Maximum priority", file: priority5 } + 5: { label: "Max. priority", file: priority5 } }; +SendDialog.OPEN_MODE_DEFAULT = "default"; +SendDialog.OPEN_MODE_DRAG = "drag"; + export default SendDialog;