forked from mirrors/ntfy
File upload
This commit is contained in:
parent
2bdae49425
commit
aabae53e5d
2 changed files with 115 additions and 33 deletions
|
@ -1,4 +1,6 @@
|
|||
import {
|
||||
basicAuth,
|
||||
encodeBase64,
|
||||
fetchLinesIterator,
|
||||
maybeWithBasicAuth,
|
||||
topicShortUrl,
|
||||
|
@ -42,6 +44,43 @@ class Api {
|
|||
});
|
||||
}
|
||||
|
||||
publishXHR(baseUrl, topic, body, headers, onProgress) {
|
||||
const url = topicUrl(baseUrl, topic);
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
console.log(`[Api] Publishing message to ${url}`);
|
||||
const send = new Promise(function (resolve, reject) {
|
||||
xhr.open("PUT", url);
|
||||
xhr.addEventListener('readystatechange', (ev) => {
|
||||
if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status <= 299) {
|
||||
console.log(`[Api] Publish successful`, ev);
|
||||
resolve(xhr.response);
|
||||
} else if (xhr.readyState === 4) {
|
||||
console.log(`[Api] Publish failed (1)`, ev);
|
||||
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);
|
||||
}
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
xhr.setRequestHeader(key, value);
|
||||
}
|
||||
xhr.send(body);
|
||||
});
|
||||
send.abort = () => {
|
||||
console.log(`[Api] Publish aborted by user`);
|
||||
xhr.abort();
|
||||
}
|
||||
return send;
|
||||
}
|
||||
|
||||
async auth(baseUrl, topic, user) {
|
||||
const url = topicUrlAuth(baseUrl, topic);
|
||||
console.log(`[Api] Checking auth for ${url}`);
|
||||
|
|
|
@ -18,7 +18,7 @@ import IconButton from "@mui/material/IconButton";
|
|||
import InsertEmoticonIcon from '@mui/icons-material/InsertEmoticon';
|
||||
import {Close} from "@mui/icons-material";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import {formatBytes, shortUrl, splitNoEmpty, splitTopicUrl, validTopicUrl} from "../app/utils";
|
||||
import {basicAuth, formatBytes, shortUrl, splitNoEmpty, splitTopicUrl, validTopicUrl} from "../app/utils";
|
||||
import Box from "@mui/material/Box";
|
||||
import Icon from "./Icon";
|
||||
import DialogFooter from "./DialogFooter";
|
||||
|
@ -26,6 +26,7 @@ import api from "../app/Api";
|
|||
import Divider from "@mui/material/Divider";
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import userManager from "../app/UserManager";
|
||||
|
||||
const SendDialog = (props) => {
|
||||
const [topicUrl, setTopicUrl] = useState(props.topicUrl);
|
||||
|
@ -50,7 +51,9 @@ const SendDialog = (props) => {
|
|||
const showAttachFile = !!attachFile && !showAttachUrl;
|
||||
const attachFileInput = useRef();
|
||||
|
||||
const [errorText, setErrorText] = useState("");
|
||||
const [sendRequest, setSendRequest] = useState(null);
|
||||
const [statusText, setStatusText] = useState("");
|
||||
const disabled = !!sendRequest;
|
||||
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const sendButtonEnabled = (() => {
|
||||
|
@ -61,38 +64,59 @@ const SendDialog = (props) => {
|
|||
})();
|
||||
const handleSubmit = async () => {
|
||||
const { baseUrl, topic } = splitTopicUrl(topicUrl);
|
||||
const options = {};
|
||||
const headers = {};
|
||||
if (title.trim()) {
|
||||
options["title"] = title.trim();
|
||||
headers["X-Title"] = title.trim();
|
||||
}
|
||||
if (tags.trim()) {
|
||||
options["tags"] = splitNoEmpty(tags, ",");
|
||||
headers["X-Tags"] = tags.trim();
|
||||
}
|
||||
if (priority && priority !== 3) {
|
||||
options["priority"] = priority;
|
||||
headers["X-Priority"] = priority.toString();
|
||||
}
|
||||
if (clickUrl.trim()) {
|
||||
options["click"] = clickUrl.trim();
|
||||
headers["X-Click"] = clickUrl.trim();
|
||||
}
|
||||
if (attachUrl.trim()) {
|
||||
options["attach"] = attachUrl.trim();
|
||||
headers["X-Attach"] = attachUrl.trim();
|
||||
}
|
||||
if (filename.trim()) {
|
||||
options["filename"] = filename.trim();
|
||||
headers["X-Filename"] = filename.trim();
|
||||
}
|
||||
if (email.trim()) {
|
||||
options["email"] = email.trim();
|
||||
headers["X-Email"] = email.trim();
|
||||
}
|
||||
if (delay.trim()) {
|
||||
options["delay"] = delay.trim();
|
||||
headers["X-Delay"] = delay.trim();
|
||||
}
|
||||
if (attachFile && message.trim()) {
|
||||
headers["X-Message"] = message.replaceAll("\n", "\\n").trim();
|
||||
}
|
||||
const body = (attachFile) ? attachFile : message;
|
||||
try {
|
||||
const response = await api.publish(baseUrl, topic, message, options);
|
||||
console.log(response);
|
||||
props.onClose();
|
||||
const user = await userManager.get(baseUrl);
|
||||
if (user) {
|
||||
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}%) ...`);
|
||||
} else {
|
||||
setStatusText(`Uploading ...`);
|
||||
}
|
||||
};
|
||||
const request = api.publishXHR(baseUrl, topic, body, headers, progressFn);
|
||||
setSendRequest(request);
|
||||
await request;
|
||||
setStatusText("Message published");
|
||||
//props.onClose();
|
||||
} catch (e) {
|
||||
setErrorText(e);
|
||||
console.log("error", e);
|
||||
setStatusText("An error occurred");
|
||||
}
|
||||
setSendRequest(null);
|
||||
};
|
||||
const handleAttachFileClick = () => {
|
||||
attachFileInput.current.click();
|
||||
|
@ -109,7 +133,7 @@ const SendDialog = (props) => {
|
|||
<DialogTitle>Publish to {shortUrl(topicUrl)}</DialogTitle>
|
||||
<DialogContent>
|
||||
{showTopicUrl &&
|
||||
<ClosableRow onClose={() => {
|
||||
<ClosableRow disabled={disabled} onClose={() => {
|
||||
setTopicUrl(props.topicUrl);
|
||||
setShowTopicUrl(false);
|
||||
}}>
|
||||
|
@ -118,6 +142,7 @@ const SendDialog = (props) => {
|
|||
label="Topic URL"
|
||||
value={topicUrl}
|
||||
onChange={ev => setTopicUrl(ev.target.value)}
|
||||
disabled={disabled}
|
||||
type="text"
|
||||
variant="standard"
|
||||
fullWidth
|
||||
|
@ -130,6 +155,7 @@ const SendDialog = (props) => {
|
|||
label="Title"
|
||||
value={title}
|
||||
onChange={ev => setTitle(ev.target.value)}
|
||||
disabled={disabled}
|
||||
type="text"
|
||||
fullWidth
|
||||
variant="standard"
|
||||
|
@ -141,6 +167,7 @@ const SendDialog = (props) => {
|
|||
placeholder="Type the main message body here."
|
||||
value={message}
|
||||
onChange={ev => setMessage(ev.target.value)}
|
||||
disabled={disabled}
|
||||
type="text"
|
||||
variant="standard"
|
||||
rows={5}
|
||||
|
@ -149,13 +176,14 @@ const SendDialog = (props) => {
|
|||
multiline
|
||||
/>
|
||||
<div style={{display: 'flex'}}>
|
||||
<DialogIconButton onClick={() => null}><InsertEmoticonIcon/></DialogIconButton>
|
||||
<DialogIconButton disabled={disabled} onClick={() => null}><InsertEmoticonIcon/></DialogIconButton>
|
||||
<TextField
|
||||
margin="dense"
|
||||
label="Tags"
|
||||
placeholder="Comma-separated list of tags, e.g. warning, srv1-backup"
|
||||
value={tags}
|
||||
onChange={ev => setTags(ev.target.value)}
|
||||
disabled={disabled}
|
||||
type="text"
|
||||
variant="standard"
|
||||
sx={{flexGrow: 1, marginRight: 1}}
|
||||
|
@ -171,6 +199,7 @@ const SendDialog = (props) => {
|
|||
margin="dense"
|
||||
value={priority}
|
||||
onChange={(ev) => setPriority(ev.target.value)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{[5,4,3,2,1].map(priority =>
|
||||
<MenuItem value={priority}>
|
||||
|
@ -184,7 +213,7 @@ const SendDialog = (props) => {
|
|||
</FormControl>
|
||||
</div>
|
||||
{showClickUrl &&
|
||||
<ClosableRow onClose={() => {
|
||||
<ClosableRow disabled={disabled} onClose={() => {
|
||||
setClickUrl("");
|
||||
setShowClickUrl(false);
|
||||
}}>
|
||||
|
@ -194,6 +223,7 @@ const SendDialog = (props) => {
|
|||
placeholder="URL that is opened when notification is clicked"
|
||||
value={clickUrl}
|
||||
onChange={ev => setClickUrl(ev.target.value)}
|
||||
disabled={disabled}
|
||||
type="url"
|
||||
fullWidth
|
||||
variant="standard"
|
||||
|
@ -201,7 +231,7 @@ const SendDialog = (props) => {
|
|||
</ClosableRow>
|
||||
}
|
||||
{showEmail &&
|
||||
<ClosableRow onClose={() => {
|
||||
<ClosableRow disabled={disabled} onClose={() => {
|
||||
setEmail("");
|
||||
setShowEmail(false);
|
||||
}}>
|
||||
|
@ -211,6 +241,7 @@ const SendDialog = (props) => {
|
|||
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
|
||||
|
@ -218,7 +249,7 @@ const SendDialog = (props) => {
|
|||
</ClosableRow>
|
||||
}
|
||||
{showAttachUrl &&
|
||||
<ClosableRow onClose={() => {
|
||||
<ClosableRow disabled={disabled} onClose={() => {
|
||||
setAttachUrl("");
|
||||
setFilename("");
|
||||
setFilenameEdited(false);
|
||||
|
@ -244,6 +275,7 @@ const SendDialog = (props) => {
|
|||
}
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
type="url"
|
||||
variant="standard"
|
||||
sx={{flexGrow: 5, marginRight: 1}}
|
||||
|
@ -257,6 +289,7 @@ const SendDialog = (props) => {
|
|||
setFilename(ev.target.value);
|
||||
setFilenameEdited(true);
|
||||
}}
|
||||
disabled={disabled}
|
||||
type="text"
|
||||
variant="standard"
|
||||
sx={{flexGrow: 1}}
|
||||
|
@ -272,6 +305,7 @@ const SendDialog = (props) => {
|
|||
{showAttachFile && <AttachmentBox
|
||||
file={attachFile}
|
||||
filename={filename}
|
||||
disabled={disabled}
|
||||
onChangeFilename={(f) => setFilename(f)}
|
||||
onClose={() => {
|
||||
setAttachFile(null);
|
||||
|
@ -279,7 +313,7 @@ const SendDialog = (props) => {
|
|||
}}
|
||||
/>}
|
||||
{showDelay &&
|
||||
<ClosableRow onClose={() => {
|
||||
<ClosableRow disabled={disabled} onClose={() => {
|
||||
setDelay("");
|
||||
setShowDelay(false);
|
||||
}}>
|
||||
|
@ -289,6 +323,7 @@ const SendDialog = (props) => {
|
|||
placeholder="Unix timestamp, duration or English natural language"
|
||||
value={delay}
|
||||
onChange={ev => setDelay(ev.target.value)}
|
||||
disabled={disabled}
|
||||
type="text"
|
||||
variant="standard"
|
||||
fullWidth
|
||||
|
@ -299,21 +334,26 @@ const SendDialog = (props) => {
|
|||
Other features:
|
||||
</Typography>
|
||||
<div>
|
||||
{!showClickUrl && <Chip clickable label="Click URL" onClick={() => setShowClickUrl(true)} sx={{marginRight: 1}}/>}
|
||||
{!showEmail && <Chip clickable label="Forward to email" onClick={() => setShowEmail(true)} sx={{marginRight: 1}}/>}
|
||||
{!showAttachUrl && !showAttachFile && <Chip clickable label="Attach file by URL" onClick={() => setShowAttachUrl(true)} sx={{marginRight: 1}}/>}
|
||||
{!showAttachFile && !showAttachUrl && <Chip clickable label="Attach local file" onClick={() => handleAttachFileClick()} sx={{marginRight: 1}}/>}
|
||||
{!showDelay && <Chip clickable label="Delay delivery" onClick={() => setShowDelay(true)} sx={{marginRight: 1}}/>}
|
||||
{!showTopicUrl && <Chip clickable label="Change topic" onClick={() => setShowTopicUrl(true)} sx={{marginRight: 1}}/>}
|
||||
{!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: 2, marginBottom: 1}}>
|
||||
<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={errorText}>
|
||||
<Button onClick={props.onClose}>Cancel</Button>
|
||||
<Button onClick={handleSubmit} disabled={!sendButtonEnabled}>Send</Button>
|
||||
<DialogFooter status={statusText}>
|
||||
{sendRequest && <Button onClick={() => sendRequest.abort()}>Cancel sending</Button>}
|
||||
{!sendRequest &&
|
||||
<>
|
||||
<Button onClick={props.onClose}>Cancel</Button>
|
||||
<Button onClick={handleSubmit} disabled={!sendButtonEnabled}>Send</Button>
|
||||
</>
|
||||
}
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
|
@ -331,7 +371,7 @@ const ClosableRow = (props) => {
|
|||
return (
|
||||
<Row>
|
||||
{props.children}
|
||||
<DialogIconButton onClick={props.onClose} sx={{marginLeft: "6px"}}><Close/></DialogIconButton>
|
||||
<DialogIconButton disabled={props.disabled} onClick={props.onClose} sx={{marginLeft: "6px"}}><Close/></DialogIconButton>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
@ -345,6 +385,7 @@ const DialogIconButton = (props) => {
|
|||
edge="start"
|
||||
sx={{height: "45px", marginTop: "17px", ...sx}}
|
||||
onClick={props.onClick}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
{props.children}
|
||||
</IconButton>
|
||||
|
@ -371,11 +412,12 @@ const AttachmentBox = (props) => {
|
|||
variant="body2"
|
||||
value={props.filename}
|
||||
onChange={(ev) => props.onChangeFilename(ev.target.value)}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
<br/>
|
||||
{formatBytes(file.size)}
|
||||
</Typography>
|
||||
<DialogIconButton onClick={props.onClose} sx={{marginLeft: "6px"}}><Close/></DialogIconButton>
|
||||
<DialogIconButton disabled={props.disabled} onClick={props.onClose} sx={{marginLeft: "6px"}}><Close/></DialogIconButton>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
@ -414,6 +456,7 @@ const ExpandingTextField = (props) => {
|
|||
sx={{ width: `${textWidth}px`, borderBottom: "none" }}
|
||||
InputProps={{ style: { fontSize: theme.typography[props.variant].fontSize } }}
|
||||
inputProps={{ style: { paddingBottom: 0, paddingTop: 0 } }}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue