2022-02-21 21:24:13 +00:00
|
|
|
import Container from "@mui/material/Container";
|
2022-03-07 21:36:49 +00:00
|
|
|
import {ButtonBase, CardActions, CardContent, CircularProgress, Fade, Link, Modal, Stack} from "@mui/material";
|
2022-02-21 21:24:13 +00:00
|
|
|
import Card from "@mui/material/Card";
|
|
|
|
import Typography from "@mui/material/Typography";
|
|
|
|
import * as React from "react";
|
2022-03-03 21:52:07 +00:00
|
|
|
import {useState} from "react";
|
2022-03-04 16:08:32 +00:00
|
|
|
import {
|
|
|
|
formatBytes,
|
|
|
|
formatMessage,
|
|
|
|
formatShortDateTime,
|
|
|
|
formatTitle,
|
|
|
|
openUrl,
|
|
|
|
topicShortUrl,
|
|
|
|
unmatchedTags
|
|
|
|
} from "../app/utils";
|
2022-02-25 15:23:04 +00:00
|
|
|
import IconButton from "@mui/material/IconButton";
|
|
|
|
import CloseIcon from '@mui/icons-material/Close';
|
2022-03-04 01:28:16 +00:00
|
|
|
import {LightboxBackdrop, Paragraph, VerticallyCenteredContainer} from "./styles";
|
2022-03-02 03:41:49 +00:00
|
|
|
import {useLiveQuery} from "dexie-react-hooks";
|
2022-03-03 01:22:53 +00:00
|
|
|
import Box from "@mui/material/Box";
|
|
|
|
import Button from "@mui/material/Button";
|
2022-03-03 21:52:07 +00:00
|
|
|
import subscriptionManager from "../app/SubscriptionManager";
|
2022-02-21 21:24:13 +00:00
|
|
|
|
2022-02-28 16:52:50 +00:00
|
|
|
const Notifications = (props) => {
|
2022-03-07 21:36:49 +00:00
|
|
|
if (props.mode === "all") {
|
|
|
|
return (props.subscriptions) ? <AllSubscriptions subscriptions={props.subscriptions}/> : <Loading/>;
|
2022-03-04 21:10:04 +00:00
|
|
|
}
|
2022-03-07 21:36:49 +00:00
|
|
|
return (props.subscription) ? <SingleSubscription subscription={props.subscription}/> : <Loading/>;
|
2022-03-05 13:52:52 +00:00
|
|
|
}
|
2022-03-04 21:10:04 +00:00
|
|
|
|
2022-03-07 21:36:49 +00:00
|
|
|
const AllSubscriptions = () => {
|
|
|
|
const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []);
|
|
|
|
if (notifications === null || notifications === undefined) {
|
|
|
|
return <Loading/>;
|
|
|
|
} else if (notifications.length === 0) {
|
|
|
|
return <NoSubscriptions/>;
|
|
|
|
}
|
|
|
|
return <NotificationList notifications={notifications}/>;
|
|
|
|
}
|
|
|
|
|
|
|
|
const SingleSubscription = (props) => {
|
2022-02-25 15:23:04 +00:00
|
|
|
const subscription = props.subscription;
|
2022-03-04 21:10:04 +00:00
|
|
|
const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]);
|
2022-03-07 21:36:49 +00:00
|
|
|
if (notifications === null || notifications === undefined) {
|
|
|
|
return <Loading/>;
|
|
|
|
} else if (notifications.length === 0) {
|
|
|
|
return <NoNotifications subscription={subscription}/>;
|
2022-02-28 16:52:50 +00:00
|
|
|
}
|
2022-03-07 21:36:49 +00:00
|
|
|
return <NotificationList notifications={notifications}/>;
|
|
|
|
}
|
|
|
|
|
|
|
|
const NotificationList = (props) => {
|
|
|
|
const sortedNotifications = props.notifications;
|
|
|
|
/*const sortedNotifications = Array.from(props.notifications)
|
|
|
|
.sort((a, b) => a.time < b.time ? 1 : -1);*/
|
2022-02-21 21:24:13 +00:00
|
|
|
return (
|
2022-03-03 19:51:56 +00:00
|
|
|
<Container maxWidth="md" sx={{marginTop: 3, marginBottom: 3}}>
|
2022-02-24 17:26:07 +00:00
|
|
|
<Stack spacing={3}>
|
2022-02-21 21:24:13 +00:00
|
|
|
{sortedNotifications.map(notification =>
|
2022-02-25 15:23:04 +00:00
|
|
|
<NotificationItem
|
|
|
|
key={notification.id}
|
|
|
|
notification={notification}
|
|
|
|
/>)}
|
2022-02-21 21:24:13 +00:00
|
|
|
</Stack>
|
|
|
|
</Container>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const NotificationItem = (props) => {
|
|
|
|
const notification = props.notification;
|
2022-03-07 21:36:49 +00:00
|
|
|
const subscriptionId = notification.subscriptionId;
|
2022-03-03 19:51:56 +00:00
|
|
|
const attachment = notification.attachment;
|
|
|
|
const date = formatShortDateTime(notification.time);
|
2022-02-24 17:26:07 +00:00
|
|
|
const otherTags = unmatchedTags(notification.tags);
|
|
|
|
const tags = (otherTags.length > 0) ? otherTags.join(', ') : null;
|
2022-03-02 21:16:30 +00:00
|
|
|
const handleDelete = async () => {
|
|
|
|
console.log(`[Notifications] Deleting notification ${notification.id} from ${subscriptionId}`);
|
2022-03-03 21:52:07 +00:00
|
|
|
await subscriptionManager.deleteNotification(notification.id)
|
2022-03-02 21:16:30 +00:00
|
|
|
}
|
2022-03-03 19:51:56 +00:00
|
|
|
const expired = attachment && attachment.expires && attachment.expires < Date.now()/1000;
|
2022-03-04 16:08:32 +00:00
|
|
|
const showAttachmentActions = attachment && !expired;
|
|
|
|
const showClickAction = notification.click;
|
|
|
|
const showActions = showAttachmentActions || showClickAction;
|
2022-02-21 21:24:13 +00:00
|
|
|
return (
|
2022-03-03 19:51:56 +00:00
|
|
|
<Card sx={{ minWidth: 275, padding: 1 }}>
|
2022-02-21 21:24:13 +00:00
|
|
|
<CardContent>
|
2022-03-02 21:16:30 +00:00
|
|
|
<IconButton onClick={handleDelete} sx={{ float: 'right', marginRight: -1, marginTop: -1 }}>
|
2022-02-25 15:23:04 +00:00
|
|
|
<CloseIcon />
|
|
|
|
</IconButton>
|
2022-02-24 17:26:07 +00:00
|
|
|
<Typography sx={{ fontSize: 14 }} color="text.secondary">
|
|
|
|
{date}
|
|
|
|
{[1,2,4,5].includes(notification.priority) &&
|
|
|
|
<img
|
2022-03-06 03:11:32 +00:00
|
|
|
src={`/static/img/priority-${notification.priority}.svg`}
|
2022-02-24 17:26:07 +00:00
|
|
|
alt={`Priority ${notification.priority}`}
|
|
|
|
style={{ verticalAlign: 'bottom' }}
|
|
|
|
/>}
|
2022-03-07 03:37:13 +00:00
|
|
|
{notification.new === 1 &&
|
|
|
|
<svg style={{ width: '8px', height: '8px', marginLeft: '4px' }} viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
|
|
|
<circle cx="50" cy="50" r="50" fill="#338574"/>
|
|
|
|
</svg>}
|
2022-02-24 17:26:07 +00:00
|
|
|
</Typography>
|
|
|
|
{notification.title && <Typography variant="h5" component="div">{formatTitle(notification)}</Typography>}
|
|
|
|
<Typography variant="body1" sx={{ whiteSpace: 'pre-line' }}>{formatMessage(notification)}</Typography>
|
2022-03-03 19:51:56 +00:00
|
|
|
{attachment && <Attachment attachment={attachment}/>}
|
2022-02-21 21:24:13 +00:00
|
|
|
{tags && <Typography sx={{ fontSize: 14 }} color="text.secondary">Tags: {tags}</Typography>}
|
|
|
|
</CardContent>
|
2022-03-04 16:08:32 +00:00
|
|
|
{showActions &&
|
2022-03-03 19:51:56 +00:00
|
|
|
<CardActions sx={{paddingTop: 0}}>
|
2022-03-04 16:08:32 +00:00
|
|
|
{showAttachmentActions && <>
|
|
|
|
<Button onClick={() => navigator.clipboard.writeText(attachment.url)}>Copy URL</Button>
|
|
|
|
<Button onClick={() => openUrl(attachment.url)}>Open attachment</Button>
|
|
|
|
</>}
|
|
|
|
{showClickAction && <Button onClick={() => openUrl(notification.click)}>Open link</Button>}
|
2022-03-03 01:22:53 +00:00
|
|
|
</CardActions>
|
|
|
|
}
|
2022-02-21 21:24:13 +00:00
|
|
|
</Card>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-03-03 19:51:56 +00:00
|
|
|
const Attachment = (props) => {
|
|
|
|
const attachment = props.attachment;
|
|
|
|
const expired = attachment.expires && attachment.expires < Date.now()/1000;
|
|
|
|
const expires = attachment.expires && attachment.expires > Date.now()/1000;
|
|
|
|
const displayableImage = !expired && attachment.type && attachment.type.startsWith("image/");
|
|
|
|
|
|
|
|
// Unexpired image
|
|
|
|
if (displayableImage) {
|
|
|
|
return <Image attachment={attachment}/>;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Anything else: Show box
|
|
|
|
const infos = [];
|
|
|
|
if (attachment.size) {
|
|
|
|
infos.push(formatBytes(attachment.size));
|
|
|
|
}
|
|
|
|
if (expires) {
|
|
|
|
infos.push(`link expires ${formatShortDateTime(attachment.expires)}`);
|
|
|
|
}
|
|
|
|
if (expired) {
|
|
|
|
infos.push(`download link expired`);
|
|
|
|
}
|
|
|
|
const maybeInfoText = (infos.length > 0) ? <><br/>{infos.join(", ")}</> : null;
|
|
|
|
|
|
|
|
// If expired, just show infos without click target
|
|
|
|
if (expired) {
|
|
|
|
return (
|
|
|
|
<Box sx={{
|
|
|
|
display: 'flex',
|
|
|
|
alignItems: 'center',
|
|
|
|
marginTop: 2,
|
|
|
|
padding: 1,
|
|
|
|
borderRadius: '4px',
|
|
|
|
}}>
|
|
|
|
<Icon type={attachment.type}/>
|
|
|
|
<Typography variant="body2" sx={{ marginLeft: 1, textAlign: 'left', color: 'text.primary' }}>
|
|
|
|
<b>{attachment.name}</b>
|
|
|
|
{maybeInfoText}
|
|
|
|
</Typography>
|
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Not expired
|
|
|
|
return (
|
|
|
|
<ButtonBase sx={{
|
|
|
|
marginTop: 2,
|
|
|
|
}}>
|
|
|
|
<Link
|
|
|
|
href={attachment.url}
|
|
|
|
target="_blank"
|
|
|
|
rel="noopener"
|
|
|
|
underline="none"
|
|
|
|
sx={{
|
|
|
|
display: 'flex',
|
|
|
|
alignItems: 'center',
|
|
|
|
padding: 1,
|
|
|
|
borderRadius: '4px',
|
|
|
|
'&:hover': {
|
|
|
|
backgroundColor: 'rgba(0, 0, 0, 0.05)'
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon type={attachment.type}/>
|
|
|
|
<Typography variant="body2" sx={{ marginLeft: 1, textAlign: 'left', color: 'text.primary' }}>
|
|
|
|
<b>{attachment.name}</b>
|
|
|
|
{maybeInfoText}
|
|
|
|
</Typography>
|
|
|
|
</Link>
|
|
|
|
</ButtonBase>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const Image = (props) => {
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<Box
|
|
|
|
component="img"
|
|
|
|
src={`${props.attachment.url}`}
|
|
|
|
loading="lazy"
|
|
|
|
onClick={() => setOpen(true)}
|
|
|
|
sx={{
|
|
|
|
marginTop: 2,
|
|
|
|
borderRadius: '4px',
|
|
|
|
boxShadow: 2,
|
|
|
|
width: 1,
|
|
|
|
maxHeight: '400px',
|
|
|
|
objectFit: 'cover',
|
|
|
|
cursor: 'pointer'
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<Modal
|
|
|
|
open={open}
|
|
|
|
onClose={() => setOpen(false)}
|
2022-03-04 01:28:16 +00:00
|
|
|
BackdropComponent={LightboxBackdrop}
|
2022-03-03 19:51:56 +00:00
|
|
|
>
|
|
|
|
<Fade in={open}>
|
|
|
|
<Box
|
|
|
|
component="img"
|
|
|
|
src={`${props.attachment.url}`}
|
|
|
|
loading="lazy"
|
|
|
|
sx={{
|
|
|
|
maxWidth: 1,
|
|
|
|
maxHeight: 1,
|
|
|
|
position: 'absolute',
|
|
|
|
top: '50%',
|
|
|
|
left: '50%',
|
|
|
|
transform: 'translate(-50%, -50%)',
|
|
|
|
padding: 4,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</Fade>
|
|
|
|
</Modal>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const Icon = (props) => {
|
|
|
|
const type = props.type;
|
|
|
|
let imageFile;
|
|
|
|
if (!type) {
|
|
|
|
imageFile = 'file-document.svg';
|
|
|
|
} else if (type.startsWith('image/')) {
|
|
|
|
imageFile = 'file-image.svg';
|
|
|
|
} else if (type.startsWith('video/')) {
|
|
|
|
imageFile = 'file-video.svg';
|
|
|
|
} else if (type.startsWith('audio/')) {
|
|
|
|
imageFile = 'file-audio.svg';
|
|
|
|
} else if (type === "application/vnd.android.package-archive") {
|
|
|
|
imageFile = 'file-app.svg';
|
|
|
|
} else {
|
|
|
|
imageFile = 'file-document.svg';
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<Box
|
|
|
|
component="img"
|
2022-03-06 15:42:05 +00:00
|
|
|
src={`/static/img/${imageFile}`}
|
2022-03-03 19:51:56 +00:00
|
|
|
loading="lazy"
|
|
|
|
sx={{
|
|
|
|
width: '28px',
|
|
|
|
height: '28px'
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
2022-03-03 01:22:53 +00:00
|
|
|
|
2022-03-07 21:36:49 +00:00
|
|
|
const NoNotifications = (props) => {
|
2022-03-02 21:16:30 +00:00
|
|
|
const shortUrl = topicShortUrl(props.subscription.baseUrl, props.subscription.topic);
|
2022-02-28 16:52:50 +00:00
|
|
|
return (
|
|
|
|
<VerticallyCenteredContainer maxWidth="xs">
|
|
|
|
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
2022-03-06 01:24:10 +00:00
|
|
|
<img src="/static/img/ntfy-outline.svg" height="64" width="64" alt="No notifications"/><br />
|
2022-02-28 16:52:50 +00:00
|
|
|
You haven't received any notifications for this topic yet.
|
|
|
|
</Typography>
|
|
|
|
<Paragraph>
|
|
|
|
To send notifications to this topic, simply PUT or POST to the topic URL.
|
|
|
|
</Paragraph>
|
|
|
|
<Paragraph>
|
|
|
|
Example:<br/>
|
|
|
|
<tt>
|
2022-03-02 21:16:30 +00:00
|
|
|
$ curl -d "Hi" {shortUrl}
|
2022-02-28 16:52:50 +00:00
|
|
|
</tt>
|
|
|
|
</Paragraph>
|
|
|
|
<Paragraph>
|
|
|
|
For more detailed instructions, check out the <Link href="https://ntfy.sh" target="_blank" rel="noopener">website</Link> or
|
|
|
|
{" "}<Link href="https://ntfy.sh/docs" target="_blank" rel="noopener">documentation</Link>.
|
|
|
|
</Paragraph>
|
|
|
|
</VerticallyCenteredContainer>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2022-03-07 21:36:49 +00:00
|
|
|
const NoSubscriptions = () => {
|
|
|
|
return (
|
|
|
|
<VerticallyCenteredContainer maxWidth="xs">
|
|
|
|
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
|
|
|
<img src="/static/img/ntfy-outline.svg" height="64" width="64" alt="No topics"/><br />
|
|
|
|
It looks like you don't have any subscriptions yet.
|
|
|
|
</Typography>
|
|
|
|
<Paragraph>
|
|
|
|
Click the "Add subscription" link to create or subscribe to a topic. After that, you can send messages
|
|
|
|
via PUT or POST and you'll receive notifications here.
|
|
|
|
</Paragraph>
|
|
|
|
<Paragraph>
|
|
|
|
For more information, check out the <Link href="https://ntfy.sh" target="_blank" rel="noopener">website</Link> or
|
|
|
|
{" "}<Link href="https://ntfy.sh/docs" target="_blank" rel="noopener">documentation</Link>.
|
|
|
|
</Paragraph>
|
|
|
|
</VerticallyCenteredContainer>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const Loading = () => {
|
|
|
|
return (
|
|
|
|
<VerticallyCenteredContainer>
|
|
|
|
<Typography variant="h5" color="text.secondary" align="center" sx={{ paddingBottom: 1 }}>
|
|
|
|
<CircularProgress disableShrink sx={{marginBottom: 1}}/><br />
|
|
|
|
Loading notifications ...
|
|
|
|
</Typography>
|
|
|
|
</VerticallyCenteredContainer>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2022-02-28 16:52:50 +00:00
|
|
|
export default Notifications;
|