Philipp Heckel 3 лет назад
Родитель
Сommit
6791c7395b
4 измененных файлов с 359 добавлено и 302 удалено
  1. 11 3
      web/src/app/Api.js
  2. 21 19
      web/src/components/App.js
  3. 4 4
      web/src/components/DialogFooter.js
  4. 323 276
      web/src/components/SendDialog.js

+ 11 - 3
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);

+ 21 - 19
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 && <MessageBar
                 subscription={subscription}
                 message={message}
                 onMessageChange={setMessage}
-                onOpenDialogClick={() => setShowDialog(true)}
+                onOpenDialogClick={handleOpenDialogClick}
             />}
             <SendDialog
                 key={`sendDialog${dialogKey}`} // Resets dialog when canceled/closed
+                topicUrl={selectedTopicUrl}
+                message={message}
                 open={showDialog}
+                openMode={dialogOpenMode}
                 dropZone={showDropZone}
                 onClose={handleSendDialogClose}
-                onDrop={() => setShowDropZone(false)}
-                topicUrl={selectedTopicUrl}
-                message={message}
+                onHideDropZone={() => setShowDropZone(false)}
+                onResetOpenMode={() => setDialogOpenMode(SendDialog.OPEN_MODE_DEFAULT)}
             />
         </>
     );

+ 4 - 4
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',
         }}>
-            <DialogContentText sx={{
+            <DialogContentText component="div" sx={{
                 margin: '0px',
                 paddingTop: '12px',
+                paddingBottom: '4px'
             }}>
                 {props.status}
             </DialogContentText>
-            <DialogActions>
+            <DialogActions sx={{paddingRight: 2}}>
                 {props.children}
             </DialogActions>
         </Box>

+ 323 - 276
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(<Typography sx={{color: 'error.main', maxWidth: "400px"}}>{e}</Typography>);
+            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 (
-        <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={handleAttachFileDrop}
-                            onDragEnter={allowDrag}
-                            onDragOver={allowDrag}
-                        >
-                            <Typography variant="h5">Drop file here</Typography>
-                        </Box>
-                    </Box>
-                }
-                {showTopicUrl &&
-                    <ClosableRow disabled={disabled} onClose={() => {
-                        setTopicUrl(props.topicUrl);
-                        setShowTopicUrl(false);
-                    }}>
-                        <TextField
-                            margin="dense"
-                            label="Topic URL"
-                            value={topicUrl}
-                            onChange={ev => setTopicUrl(ev.target.value)}
-                            disabled={disabled}
-                            type="text"
-                            variant="standard"
-                            fullWidth
-                            required
-                        />
-                    </ClosableRow>
-                }
-                <TextField
-                    margin="dense"
-                    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"
-                />
-                <TextField
-                    margin="dense"
-                    label="Message"
-                    placeholder="Type the main message body here."
-                    value={message}
-                    onChange={ev => setMessage(ev.target.value)}
-                    disabled={disabled}
-                    type="text"
-                    variant="standard"
-                    rows={5}
-                    fullWidth
-                    autoFocus
-                    multiline
-                />
-                <div style={{display: 'flex'}}>
-                    <DialogIconButton disabled={disabled} onClick={() => null}><InsertEmoticonIcon/></DialogIconButton>
+        <>
+            {dropZone && <DropArea
+                onDrop={handleAttachFileDrop}
+                onDragLeave={handleAttachFileDragLeave}/>
+            }
+            <Dialog maxWidth="md" open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
+                <DialogTitle>Publish to {shortUrl(topicUrl)}</DialogTitle>
+                <DialogContent>
+                    {dropZone && <DropBox/>}
+                    {showTopicUrl &&
+                        <ClosableRow disabled={disabled} onClose={() => {
+                            setTopicUrl(props.topicUrl);
+                            setShowTopicUrl(false);
+                        }}>
+                            <TextField
+                                margin="dense"
+                                label="Topic URL"
+                                value={topicUrl}
+                                onChange={ev => setTopicUrl(ev.target.value)}
+                                disabled={disabled}
+                                type="text"
+                                variant="standard"
+                                fullWidth
+                                required
+                            />
+                        </ClosableRow>
+                    }
                     <TextField
                         margin="dense"
-                        label="Tags"
-                        placeholder="Comma-separated list of tags, e.g. warning, srv1-backup"
-                        value={tags}
-                        onChange={ev => setTags(ev.target.value)}
+                        label="Title"
+                        value={title}
+                        onChange={ev => setTitle(ev.target.value)}
                         disabled={disabled}
                         type="text"
+                        fullWidth
                         variant="standard"
-                        sx={{flexGrow: 1, marginRight: 1}}
+                        placeholder="Notification title, e.g. Disk space alert"
                     />
-                    <FormControl
-                        variant="standard"
+                    <TextField
                         margin="dense"
-                        sx={{minWidth: 120, maxWidth: 200, flexGrow: 1}}
-                    >
-                        <InputLabel/>
-                        <Select
-                            label="Priority"
-                            margin="dense"
-                            value={priority}
-                            onChange={(ev) => setPriority(ev.target.value)}
-                            disabled={disabled}
-                        >
-                            {[5,4,3,2,1].map(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>
-                                    </div>
-                                </MenuItem>
-                            )}
-                        </Select>
-                    </FormControl>
-                </div>
-                {showClickUrl &&
-                    <ClosableRow disabled={disabled} onClose={() => {
-                        setClickUrl("");
-                        setShowClickUrl(false);
-                    }}>
+                        label="Message"
+                        placeholder="Type the main message body here."
+                        value={message}
+                        onChange={ev => setMessage(ev.target.value)}
+                        disabled={disabled}
+                        type="text"
+                        variant="standard"
+                        rows={5}
+                        fullWidth
+                        autoFocus
+                        multiline
+                    />
+                    <div style={{display: 'flex'}}>
+                        <DialogIconButton disabled={disabled} onClick={() => null}><InsertEmoticonIcon/></DialogIconButton>
                         <TextField
                             margin="dense"
-                            label="Click URL"
-                            placeholder="URL that is opened when notification is clicked"
-                            value={clickUrl}
-                            onChange={ev => setClickUrl(ev.target.value)}
+                            label="Tags"
+                            placeholder="Comma-separated list of tags, e.g. warning, srv1-backup"
+                            value={tags}
+                            onChange={ev => setTags(ev.target.value)}
                             disabled={disabled}
-                            type="url"
-                            fullWidth
+                            type="text"
                             variant="standard"
+                            sx={{flexGrow: 1, marginRight: 1}}
                         />
-                    </ClosableRow>
-                }
-                {showEmail &&
-                    <ClosableRow disabled={disabled} onClose={() => {
-                        setEmail("");
-                        setShowEmail(false);
-                    }}>
-                        <TextField
-                            margin="dense"
-                            label="Email"
-                            placeholder="Address to forward the message to, e.g. phil@example.com"
-                            value={email}
-                            onChange={ev => setEmail(ev.target.value)}
-                            disabled={disabled}
-                            type="email"
+                        <FormControl
                             variant="standard"
-                            fullWidth
-                        />
-                    </ClosableRow>
-                }
-                {showAttachUrl &&
-                    <ClosableRow disabled={disabled} onClose={() => {
-                        setAttachUrl("");
-                        setFilename("");
-                        setFilenameEdited(false);
-                        setShowAttachUrl(false);
-                    }}>
-                        <TextField
                             margin="dense"
-                            label="Attachment URL"
-                            placeholder="Attach file by URL, e.g. https://f-droid.org/F-Droid.apk"
-                            value={attachUrl}
-                            onChange={ev => {
-                                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]);
+                            sx={{minWidth: 120, maxWidth: 200, flexGrow: 1}}
+                        >
+                            <InputLabel/>
+                            <Select
+                                label="Priority"
+                                margin="dense"
+                                value={priority}
+                                onChange={(ev) => setPriority(ev.target.value)}
+                                disabled={disabled}
+                            >
+                                {[5,4,3,2,1].map(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>
+                                        </div>
+                                    </MenuItem>
+                                )}
+                            </Select>
+                        </FormControl>
+                    </div>
+                    {showClickUrl &&
+                        <ClosableRow disabled={disabled} onClose={() => {
+                            setClickUrl("");
+                            setShowClickUrl(false);
+                        }}>
+                            <TextField
+                                margin="dense"
+                                label="Click URL"
+                                placeholder="URL that is opened when notification is clicked"
+                                value={clickUrl}
+                                onChange={ev => setClickUrl(ev.target.value)}
+                                disabled={disabled}
+                                type="url"
+                                fullWidth
+                                variant="standard"
+                            />
+                        </ClosableRow>
+                    }
+                    {showEmail &&
+                        <ClosableRow disabled={disabled} onClose={() => {
+                            setEmail("");
+                            setShowEmail(false);
+                        }}>
+                            <TextField
+                                margin="dense"
+                                label="Email"
+                                placeholder="Address to forward the message to, e.g. phil@example.com"
+                                value={email}
+                                onChange={ev => setEmail(ev.target.value)}
+                                disabled={disabled}
+                                type="email"
+                                variant="standard"
+                                fullWidth
+                            />
+                        </ClosableRow>
+                    }
+                    {showAttachUrl &&
+                        <ClosableRow disabled={disabled} onClose={() => {
+                            setAttachUrl("");
+                            setFilename("");
+                            setFilenameEdited(false);
+                            setShowAttachUrl(false);
+                        }}>
+                            <TextField
+                                margin="dense"
+                                label="Attachment URL"
+                                placeholder="Attach file by URL, e.g. https://f-droid.org/F-Droid.apk"
+                                value={attachUrl}
+                                onChange={ev => {
+                                    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}}
-                        />
-                        <TextField
-                            margin="dense"
-                            label="Filename"
-                            placeholder="Attachment filename"
-                            value={filename}
-                            onChange={ev => {
-                                setFilename(ev.target.value);
-                                setFilenameEdited(true);
-                            }}
-                            disabled={disabled}
-                            type="text"
-                            variant="standard"
-                            sx={{flexGrow: 1}}
-                        />
-                    </ClosableRow>
-                }
-                <input
-                    type="file"
-                    ref={attachFileInput}
-                    onChange={handleAttachFileChanged}
-                    style={{ display: 'none' }}
-                />
-                {showAttachFile && <AttachmentBox
-                    file={attachFile}
-                    filename={filename}
-                    disabled={disabled}
-                    error={attachFileError}
-                    onChangeFilename={(f) => setFilename(f)}
-                    onClose={() => {
-                        setAttachFile(null);
-                        setAttachFileError("");
-                        setFilename("");
-                    }}
-                />}
-                {showDelay &&
-                    <ClosableRow disabled={disabled} onClose={() => {
-                        setDelay("");
-                        setShowDelay(false);
-                    }}>
-                        <TextField
-                            margin="dense"
-                            label="Delay"
-                            placeholder="Unix timestamp, duration or English natural language"
-                            value={delay}
-                            onChange={ev => setDelay(ev.target.value)}
-                            disabled={disabled}
-                            type="text"
-                            variant="standard"
-                            fullWidth
-                        />
-                    </ClosableRow>
-                }
-                <Typography variant="body1" sx={{marginTop: 2, marginBottom: 1}}>
-                    Other features:
-                </Typography>
-                <div>
-                    {!showClickUrl && <Chip clickable disabled={disabled} label="Click URL" onClick={() => setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
-                    {!showEmail && <Chip clickable disabled={disabled} label="Forward to email" onClick={() => setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
-                    {!showAttachUrl && !showAttachFile && <Chip clickable disabled={disabled} label="Attach file by URL" onClick={() => setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
-                    {!showAttachFile && !showAttachUrl && <Chip clickable disabled={disabled} label="Attach local file" onClick={() => handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>}
-                    {!showDelay && <Chip clickable disabled={disabled} label="Delay delivery" onClick={() => setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
-                    {!showTopicUrl && <Chip clickable disabled={disabled} label="Change topic" onClick={() => setShowTopicUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
-                </div>
-                <Typography variant="body1" sx={{marginTop: 1, marginBottom: 1}}>
-                    For examples and a detailed description of all send features, please
-                    refer to the <Link href="/docs">documentation</Link>.
-                </Typography>
-            </DialogContent>
-            <DialogFooter status={statusText}>
-                {activeRequest && <Button onClick={() => activeRequest.abort()}>Cancel sending</Button>}
-                {!activeRequest &&
-                    <>
-                        <FormControlLabel
-                            label="Publish another"
-                            sx={{marginRight: 2}}
-                            control={
-                                <Checkbox size="small" checked={publishAnother} onChange={(ev) => setPublishAnother(ev.target.checked)} />
-                            } />
-                        <Button onClick={props.onClose}>Cancel</Button>
-                        <Button onClick={handleSubmit} disabled={!sendButtonEnabled}>Send</Button>
-                    </>
-                }
-            </DialogFooter>
-        </Dialog>
+                                }}
+                                disabled={disabled}
+                                type="url"
+                                variant="standard"
+                                sx={{flexGrow: 5, marginRight: 1}}
+                            />
+                            <TextField
+                                margin="dense"
+                                label="Filename"
+                                placeholder="Attachment filename"
+                                value={filename}
+                                onChange={ev => {
+                                    setFilename(ev.target.value);
+                                    setFilenameEdited(true);
+                                }}
+                                disabled={disabled}
+                                type="text"
+                                variant="standard"
+                                sx={{flexGrow: 1}}
+                            />
+                        </ClosableRow>
+                    }
+                    <input
+                        type="file"
+                        ref={attachFileInput}
+                        onChange={handleAttachFileChanged}
+                        style={{ display: 'none' }}
+                    />
+                    {showAttachFile && <AttachmentBox
+                        file={attachFile}
+                        filename={filename}
+                        disabled={disabled}
+                        error={attachFileError}
+                        onChangeFilename={(f) => setFilename(f)}
+                        onClose={() => {
+                            setAttachFile(null);
+                            setAttachFileError("");
+                            setFilename("");
+                        }}
+                    />}
+                    {showDelay &&
+                        <ClosableRow disabled={disabled} onClose={() => {
+                            setDelay("");
+                            setShowDelay(false);
+                        }}>
+                            <TextField
+                                margin="dense"
+                                label="Delay"
+                                placeholder="Delay delivery, e.g. 1649029748, 30m, or tomorrow, 9am"
+                                value={delay}
+                                onChange={ev => setDelay(ev.target.value)}
+                                disabled={disabled}
+                                type="text"
+                                variant="standard"
+                                fullWidth
+                            />
+                        </ClosableRow>
+                    }
+                    <Typography variant="body1" sx={{marginTop: 2, marginBottom: 1}}>
+                        Other features:
+                    </Typography>
+                    <div>
+                        {!showClickUrl && <Chip clickable disabled={disabled} label="Click URL" onClick={() => setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
+                        {!showEmail && <Chip clickable disabled={disabled} label="Forward to email" onClick={() => setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
+                        {!showAttachUrl && !showAttachFile && <Chip clickable disabled={disabled} label="Attach file by URL" onClick={() => setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
+                        {!showAttachFile && !showAttachUrl && <Chip clickable disabled={disabled} label="Attach local file" onClick={() => handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>}
+                        {!showDelay && <Chip clickable disabled={disabled} label="Delay delivery" onClick={() => setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
+                        {!showTopicUrl && <Chip clickable disabled={disabled} label="Change topic" onClick={() => setShowTopicUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
+                    </div>
+                    <Typography variant="body1" sx={{marginTop: 1, marginBottom: 1}}>
+                        For examples and a detailed description of all send features, please
+                        refer to the <Link href="/docs" target="_blank">documentation</Link>.
+                    </Typography>
+                </DialogContent>
+                <DialogFooter status={status}>
+                    {activeRequest && <Button onClick={() => activeRequest.abort()}>Cancel sending</Button>}
+                    {!activeRequest &&
+                        <>
+                            <FormControlLabel
+                                label="Publish another"
+                                sx={{marginRight: 2}}
+                                control={
+                                    <Checkbox size="small" checked={publishAnother} onChange={(ev) => setPublishAnother(ev.target.checked)} />
+                                } />
+                            <Button onClick={props.onClose}>Cancel</Button>
+                            <Button onClick={handleSubmit} disabled={!sendButtonEnabled}>Send</Button>
+                        </>
+                    }
+                </DialogFooter>
+            </Dialog>
+        </>
     );
 };
 
@@ -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}
             </Typography>
@@ -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 (
+        <Box
+            sx={{
+                position: 'absolute',
+                left: 0,
+                top: 0,
+                right: 0,
+                bottom: 0,
+                zIndex: 10002,
+            }}
+            onDrop={props.onDrop}
+            onDragEnter={allowDrag}
+            onDragOver={allowDrag}
+            onDragLeave={props.onDragLeave}
+        />
+    );
+};
+
+const DropBox = () => {
+    return (
+        <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",
+                }}
+            >
+                <Typography variant="h5">Drop file here</Typography>
+            </Box>
+        </Box>
+    );
+}
+
 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;