2022-02-21 22:24:13 +01:00
|
|
|
import Container from "@mui/material/Container";
|
2023-05-24 01:29:47 +02:00
|
|
|
import { ButtonBase, CardActions, CardContent, CircularProgress, Fade, Link, Modal, Snackbar, Stack, Tooltip } from "@mui/material";
|
2022-02-21 22:24:13 +01:00
|
|
|
import Card from "@mui/material/Card";
|
|
|
|
import Typography from "@mui/material/Typography";
|
|
|
|
import * as React from "react";
|
2023-05-23 21:13:01 +02:00
|
|
|
import { useEffect, useState } from "react";
|
2022-03-04 17:08:32 +01:00
|
|
|
import {
|
2023-05-23 21:13:01 +02:00
|
|
|
formatBytes,
|
|
|
|
formatMessage,
|
|
|
|
formatShortDateTime,
|
|
|
|
formatTitle,
|
|
|
|
maybeAppendActionErrors,
|
|
|
|
openUrl,
|
|
|
|
shortUrl,
|
|
|
|
topicShortUrl,
|
|
|
|
unmatchedTags,
|
2022-03-04 17:08:32 +01:00
|
|
|
} from "../app/utils";
|
2022-02-25 16:23:04 +01:00
|
|
|
import IconButton from "@mui/material/IconButton";
|
2023-05-23 21:13:01 +02:00
|
|
|
import CheckIcon from "@mui/icons-material/Check";
|
|
|
|
import CloseIcon from "@mui/icons-material/Close";
|
2023-05-24 01:29:47 +02:00
|
|
|
import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles";
|
2023-05-23 21:13:01 +02:00
|
|
|
import { useLiveQuery } from "dexie-react-hooks";
|
2022-03-03 02:22:53 +01:00
|
|
|
import Box from "@mui/material/Box";
|
|
|
|
import Button from "@mui/material/Button";
|
2022-03-03 22:52:07 +01:00
|
|
|
import subscriptionManager from "../app/SubscriptionManager";
|
2022-03-08 05:07:07 +01:00
|
|
|
import InfiniteScroll from "react-infinite-scroll-component";
|
2022-03-09 21:58:21 +01:00
|
|
|
import priority1 from "../img/priority-1.svg";
|
|
|
|
import priority2 from "../img/priority-2.svg";
|
|
|
|
import priority4 from "../img/priority-4.svg";
|
|
|
|
import priority5 from "../img/priority-5.svg";
|
|
|
|
import logoOutline from "../img/ntfy-outline.svg";
|
2022-04-04 14:40:54 +02:00
|
|
|
import AttachmentIcon from "./AttachmentIcon";
|
2023-05-23 21:13:01 +02:00
|
|
|
import { Trans, useTranslation } from "react-i18next";
|
|
|
|
import { useOutletContext } from "react-router-dom";
|
|
|
|
import { useAutoSubscribe } from "./hooks";
|
2022-02-21 22:24:13 +01:00
|
|
|
|
2023-01-10 02:37:13 +01:00
|
|
|
export const AllSubscriptions = () => {
|
2023-05-23 21:13:01 +02:00
|
|
|
const { subscriptions } = useOutletContext();
|
|
|
|
if (!subscriptions) {
|
|
|
|
return <Loading />;
|
|
|
|
}
|
|
|
|
return <AllSubscriptionsList subscriptions={subscriptions} />;
|
2023-01-10 02:37:13 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
export const SingleSubscription = () => {
|
2023-05-23 21:13:01 +02:00
|
|
|
const { subscriptions, selected } = useOutletContext();
|
|
|
|
useAutoSubscribe(subscriptions, selected);
|
|
|
|
if (!selected) {
|
|
|
|
return <Loading />;
|
|
|
|
}
|
|
|
|
return <SingleSubscriptionList subscription={selected} />;
|
2023-01-10 02:37:13 +01:00
|
|
|
};
|
2022-03-04 22:10:04 +01:00
|
|
|
|
2023-01-10 02:37:13 +01:00
|
|
|
const AllSubscriptionsList = (props) => {
|
2023-05-23 21:13:01 +02:00
|
|
|
const subscriptions = props.subscriptions;
|
2023-05-24 01:29:47 +02:00
|
|
|
const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []);
|
2023-05-23 21:13:01 +02:00
|
|
|
if (notifications === null || notifications === undefined) {
|
|
|
|
return <Loading />;
|
|
|
|
} else if (subscriptions.length === 0) {
|
|
|
|
return <NoSubscriptions />;
|
|
|
|
} else if (notifications.length === 0) {
|
|
|
|
return <NoNotificationsWithoutSubscription subscriptions={subscriptions} />;
|
|
|
|
}
|
2023-05-24 01:29:47 +02:00
|
|
|
return <NotificationList key="all" notifications={notifications} messageBar={false} />;
|
2023-05-23 21:13:01 +02:00
|
|
|
};
|
2022-03-07 22:36:49 +01:00
|
|
|
|
2023-01-10 02:37:13 +01:00
|
|
|
const SingleSubscriptionList = (props) => {
|
2023-05-23 21:13:01 +02:00
|
|
|
const subscription = props.subscription;
|
2023-05-24 01:29:47 +02:00
|
|
|
const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]);
|
2023-05-23 21:13:01 +02:00
|
|
|
if (notifications === null || notifications === undefined) {
|
|
|
|
return <Loading />;
|
|
|
|
} else if (notifications.length === 0) {
|
|
|
|
return <NoNotifications subscription={subscription} />;
|
|
|
|
}
|
2023-05-24 01:29:47 +02:00
|
|
|
return <NotificationList id={subscription.id} notifications={notifications} messageBar={true} />;
|
2023-05-23 21:13:01 +02:00
|
|
|
};
|
2022-03-07 22:36:49 +01:00
|
|
|
|
|
|
|
const NotificationList = (props) => {
|
2023-05-23 21:13:01 +02:00
|
|
|
const { t } = useTranslation();
|
|
|
|
const pageSize = 20;
|
|
|
|
const notifications = props.notifications;
|
|
|
|
const [snackOpen, setSnackOpen] = useState(false);
|
|
|
|
const [maxCount, setMaxCount] = useState(pageSize);
|
|
|
|
const count = Math.min(notifications.length, maxCount);
|
2022-03-08 17:21:11 +01:00
|
|
|
|
2023-05-23 21:13:01 +02:00
|
|
|
useEffect(() => {
|
|
|
|
return () => {
|
|
|
|
setMaxCount(pageSize);
|
|
|
|
const main = document.getElementById("main");
|
|
|
|
if (main) {
|
|
|
|
main.scrollTo(0, 0);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}, [props.id]);
|
2022-03-08 17:21:11 +01:00
|
|
|
|
2023-05-23 21:13:01 +02:00
|
|
|
return (
|
|
|
|
<InfiniteScroll
|
|
|
|
dataLength={count}
|
|
|
|
next={() => setMaxCount((prev) => prev + pageSize)}
|
|
|
|
hasMore={count < notifications.length}
|
|
|
|
loader={<>Loading ...</>}
|
|
|
|
scrollThreshold={0.7}
|
|
|
|
scrollableTarget="main"
|
|
|
|
>
|
|
|
|
<Container
|
|
|
|
maxWidth="md"
|
|
|
|
role="list"
|
|
|
|
aria-label={t("notifications_list")}
|
|
|
|
sx={{
|
|
|
|
marginTop: 3,
|
|
|
|
marginBottom: props.messageBar ? "100px" : 3, // Hack to avoid hiding notifications behind the message bar
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Stack spacing={3}>
|
|
|
|
{notifications.slice(0, count).map((notification) => (
|
2023-05-24 01:29:47 +02:00
|
|
|
<NotificationItem key={notification.id} notification={notification} onShowSnack={() => setSnackOpen(true)} />
|
2023-05-23 21:13:01 +02:00
|
|
|
))}
|
2023-05-24 02:16:29 +02:00
|
|
|
<Snackbar
|
|
|
|
open={snackOpen}
|
|
|
|
autoHideDuration={3000}
|
|
|
|
onClose={() => setSnackOpen(false)}
|
|
|
|
message={t("notifications_copied_to_clipboard")}
|
|
|
|
/>
|
2023-05-23 21:13:01 +02:00
|
|
|
</Stack>
|
|
|
|
</Container>
|
|
|
|
</InfiniteScroll>
|
|
|
|
);
|
|
|
|
};
|
2022-02-21 22:24:13 +01:00
|
|
|
|
|
|
|
const NotificationItem = (props) => {
|
2023-05-23 21:13:01 +02:00
|
|
|
const { t } = useTranslation();
|
|
|
|
const notification = props.notification;
|
|
|
|
const attachment = notification.attachment;
|
|
|
|
const date = formatShortDateTime(notification.time);
|
|
|
|
const otherTags = unmatchedTags(notification.tags);
|
|
|
|
const tags = otherTags.length > 0 ? otherTags.join(", ") : null;
|
|
|
|
const handleDelete = async () => {
|
|
|
|
console.log(`[Notifications] Deleting notification ${notification.id}`);
|
|
|
|
await subscriptionManager.deleteNotification(notification.id);
|
|
|
|
};
|
|
|
|
const handleMarkRead = async () => {
|
2023-05-24 01:29:47 +02:00
|
|
|
console.log(`[Notifications] Marking notification ${notification.id} as read`);
|
2023-05-23 21:13:01 +02:00
|
|
|
await subscriptionManager.markNotificationRead(notification.id);
|
|
|
|
};
|
|
|
|
const handleCopy = (s) => {
|
|
|
|
navigator.clipboard.writeText(s);
|
|
|
|
props.onShowSnack();
|
|
|
|
};
|
2023-05-24 01:29:47 +02:00
|
|
|
const expired = attachment && attachment.expires && attachment.expires < Date.now() / 1000;
|
2023-05-23 21:13:01 +02:00
|
|
|
const hasAttachmentActions = attachment && !expired;
|
|
|
|
const hasClickAction = notification.click;
|
2023-05-24 01:29:47 +02:00
|
|
|
const hasUserActions = notification.actions && notification.actions.length > 0;
|
2023-05-23 21:13:01 +02:00
|
|
|
const showActions = hasAttachmentActions || hasClickAction || hasUserActions;
|
|
|
|
return (
|
2023-05-24 01:29:47 +02:00
|
|
|
<Card sx={{ minWidth: 275, padding: 1 }} role="listitem" aria-label={t("notifications_list_item")}>
|
2023-05-23 21:13:01 +02:00
|
|
|
<CardContent>
|
|
|
|
<Tooltip title={t("notifications_delete")} enterDelay={500}>
|
2023-05-24 01:29:47 +02:00
|
|
|
<IconButton onClick={handleDelete} sx={{ float: "right", marginRight: -1, marginTop: -1 }} aria-label={t("notifications_delete")}>
|
2023-05-23 21:13:01 +02:00
|
|
|
<CloseIcon />
|
|
|
|
</IconButton>
|
|
|
|
</Tooltip>
|
|
|
|
{notification.new === 1 && (
|
|
|
|
<Tooltip title={t("notifications_mark_read")} enterDelay={500}>
|
2023-05-24 02:16:29 +02:00
|
|
|
<IconButton
|
|
|
|
onClick={handleMarkRead}
|
|
|
|
sx={{ float: "right", marginRight: -0.5, marginTop: -1 }}
|
|
|
|
aria-label={t("notifications_mark_read")}
|
|
|
|
>
|
2023-05-23 21:13:01 +02:00
|
|
|
<CheckIcon />
|
|
|
|
</IconButton>
|
|
|
|
</Tooltip>
|
|
|
|
)}
|
|
|
|
<Typography sx={{ fontSize: 14 }} color="text.secondary">
|
|
|
|
{date}
|
|
|
|
{[1, 2, 4, 5].includes(notification.priority) && (
|
|
|
|
<img
|
|
|
|
src={priorityFiles[notification.priority]}
|
|
|
|
alt={t("notifications_priority_x", {
|
|
|
|
priority: notification.priority,
|
|
|
|
})}
|
|
|
|
style={{ verticalAlign: "bottom" }}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
{notification.new === 1 && (
|
|
|
|
<svg
|
|
|
|
style={{ width: "8px", height: "8px", marginLeft: "4px" }}
|
|
|
|
viewBox="0 0 100 100"
|
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
aria-label={t("notifications_new_indicator")}
|
|
|
|
>
|
|
|
|
<circle cx="50" cy="50" r="50" fill="#338574" />
|
|
|
|
</svg>
|
|
|
|
)}
|
|
|
|
</Typography>
|
|
|
|
{notification.title && (
|
|
|
|
<Typography variant="h5" component="div" role="rowheader">
|
|
|
|
{formatTitle(notification)}
|
|
|
|
</Typography>
|
|
|
|
)}
|
|
|
|
<Typography variant="body1" sx={{ whiteSpace: "pre-line" }}>
|
2023-05-24 01:29:47 +02:00
|
|
|
{autolink(maybeAppendActionErrors(formatMessage(notification), notification))}
|
2023-05-23 21:13:01 +02:00
|
|
|
</Typography>
|
|
|
|
{attachment && <Attachment attachment={attachment} />}
|
|
|
|
{tags && (
|
|
|
|
<Typography sx={{ fontSize: 14 }} color="text.secondary">
|
|
|
|
{t("notifications_tags")}: {tags}
|
|
|
|
</Typography>
|
|
|
|
)}
|
|
|
|
</CardContent>
|
|
|
|
{showActions && (
|
|
|
|
<CardActions sx={{ paddingTop: 0 }}>
|
|
|
|
{hasAttachmentActions && (
|
|
|
|
<>
|
|
|
|
<Tooltip title={t("notifications_attachment_copy_url_title")}>
|
2023-05-24 01:29:47 +02:00
|
|
|
<Button onClick={() => handleCopy(attachment.url)}>{t("notifications_attachment_copy_url_button")}</Button>
|
2023-05-23 21:13:01 +02:00
|
|
|
</Tooltip>
|
|
|
|
<Tooltip
|
|
|
|
title={t("notifications_attachment_open_title", {
|
|
|
|
url: attachment.url,
|
|
|
|
})}
|
|
|
|
>
|
2023-05-24 01:29:47 +02:00
|
|
|
<Button onClick={() => openUrl(attachment.url)}>{t("notifications_attachment_open_button")}</Button>
|
2023-05-23 21:13:01 +02:00
|
|
|
</Tooltip>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
{hasClickAction && (
|
|
|
|
<>
|
|
|
|
<Tooltip title={t("notifications_click_copy_url_title")}>
|
2023-05-24 01:29:47 +02:00
|
|
|
<Button onClick={() => handleCopy(notification.click)}>{t("notifications_click_copy_url_button")}</Button>
|
2023-05-23 21:13:01 +02:00
|
|
|
</Tooltip>
|
|
|
|
<Tooltip
|
|
|
|
title={t("notifications_actions_open_url_title", {
|
|
|
|
url: notification.click,
|
|
|
|
})}
|
|
|
|
>
|
2023-05-24 01:29:47 +02:00
|
|
|
<Button onClick={() => openUrl(notification.click)}>{t("notifications_click_open_button")}</Button>
|
2023-05-23 21:13:01 +02:00
|
|
|
</Tooltip>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
{hasUserActions && <UserActions notification={notification} />}
|
|
|
|
</CardActions>
|
|
|
|
)}
|
|
|
|
</Card>
|
|
|
|
);
|
|
|
|
};
|
2022-02-21 22:24:13 +01:00
|
|
|
|
2022-03-11 17:46:19 +01:00
|
|
|
/**
|
|
|
|
* Replace links with <Link/> components; this is a combination of the genius function
|
|
|
|
* in [1] and the regex in [2].
|
|
|
|
*
|
|
|
|
* [1] https://github.com/facebook/react/issues/3386#issuecomment-78605760
|
|
|
|
* [2] https://github.com/bryanwoods/autolink-js/blob/master/autolink.js#L9
|
|
|
|
*/
|
|
|
|
const autolink = (s) => {
|
2023-05-24 01:29:47 +02:00
|
|
|
const parts = s.split(/(\bhttps?:\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|]\b)/gi);
|
2023-05-23 21:13:01 +02:00
|
|
|
for (let i = 1; i < parts.length; i += 2) {
|
|
|
|
parts[i] = (
|
2023-05-24 01:29:47 +02:00
|
|
|
<Link key={i} href={parts[i]} underline="hover" target="_blank" rel="noreferrer,noopener">
|
2023-05-23 21:13:01 +02:00
|
|
|
{shortUrl(parts[i])}
|
|
|
|
</Link>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return <>{parts}</>;
|
2022-03-11 17:46:19 +01:00
|
|
|
};
|
|
|
|
|
2022-03-09 21:58:21 +01:00
|
|
|
const priorityFiles = {
|
2023-05-23 21:13:01 +02:00
|
|
|
1: priority1,
|
|
|
|
2: priority2,
|
|
|
|
4: priority4,
|
|
|
|
5: priority5,
|
2022-03-09 21:58:21 +01:00
|
|
|
};
|
|
|
|
|
2022-03-03 20:51:56 +01:00
|
|
|
const Attachment = (props) => {
|
2023-05-23 21:13:01 +02:00
|
|
|
const { t } = useTranslation();
|
|
|
|
const attachment = props.attachment;
|
|
|
|
const expired = attachment.expires && attachment.expires < Date.now() / 1000;
|
|
|
|
const expires = attachment.expires && attachment.expires > Date.now() / 1000;
|
2023-05-24 01:29:47 +02:00
|
|
|
const displayableImage = !expired && attachment.type && attachment.type.startsWith("image/");
|
2022-03-03 20:51:56 +01:00
|
|
|
|
2023-05-23 21:13:01 +02:00
|
|
|
// Unexpired image
|
|
|
|
if (displayableImage) {
|
|
|
|
return <Image attachment={attachment} />;
|
|
|
|
}
|
2022-03-03 20:51:56 +01:00
|
|
|
|
2023-05-23 21:13:01 +02:00
|
|
|
// Anything else: Show box
|
|
|
|
const infos = [];
|
|
|
|
if (attachment.size) {
|
|
|
|
infos.push(formatBytes(attachment.size));
|
|
|
|
}
|
|
|
|
if (expires) {
|
|
|
|
infos.push(
|
|
|
|
t("notifications_attachment_link_expires", {
|
|
|
|
date: formatShortDateTime(attachment.expires),
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (expired) {
|
|
|
|
infos.push(t("notifications_attachment_link_expired"));
|
|
|
|
}
|
|
|
|
const maybeInfoText =
|
|
|
|
infos.length > 0 ? (
|
|
|
|
<>
|
|
|
|
<br />
|
|
|
|
{infos.join(", ")}
|
|
|
|
</>
|
|
|
|
) : null;
|
2022-03-03 20:51:56 +01:00
|
|
|
|
2023-05-23 21:13:01 +02:00
|
|
|
// If expired, just show infos without click target
|
|
|
|
if (expired) {
|
2022-03-03 20:51:56 +01:00
|
|
|
return (
|
2023-05-23 21:13:01 +02:00
|
|
|
<Box
|
|
|
|
sx={{
|
|
|
|
display: "flex",
|
|
|
|
alignItems: "center",
|
|
|
|
marginTop: 2,
|
|
|
|
padding: 1,
|
|
|
|
borderRadius: "4px",
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<AttachmentIcon type={attachment.type} />
|
2023-05-24 01:29:47 +02:00
|
|
|
<Typography variant="body2" sx={{ marginLeft: 1, textAlign: "left", color: "text.primary" }}>
|
2023-05-23 21:13:01 +02:00
|
|
|
<b>{attachment.name}</b>
|
|
|
|
{maybeInfoText}
|
|
|
|
</Typography>
|
|
|
|
</Box>
|
2022-03-03 20:51:56 +01:00
|
|
|
);
|
2023-05-23 21:13:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)",
|
|
|
|
},
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<AttachmentIcon type={attachment.type} />
|
2023-05-24 01:29:47 +02:00
|
|
|
<Typography variant="body2" sx={{ marginLeft: 1, textAlign: "left", color: "text.primary" }}>
|
2023-05-23 21:13:01 +02:00
|
|
|
<b>{attachment.name}</b>
|
|
|
|
{maybeInfoText}
|
|
|
|
</Typography>
|
|
|
|
</Link>
|
|
|
|
</ButtonBase>
|
|
|
|
);
|
2022-03-03 20:51:56 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
const Image = (props) => {
|
2023-05-23 21:13:01 +02:00
|
|
|
const { t } = useTranslation();
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<Box
|
|
|
|
component="img"
|
|
|
|
src={props.attachment.url}
|
|
|
|
loading="lazy"
|
|
|
|
alt={t("notifications_attachment_image")}
|
|
|
|
onClick={() => setOpen(true)}
|
|
|
|
sx={{
|
|
|
|
marginTop: 2,
|
|
|
|
borderRadius: "4px",
|
|
|
|
boxShadow: 2,
|
|
|
|
width: 1,
|
|
|
|
maxHeight: "400px",
|
|
|
|
objectFit: "cover",
|
|
|
|
cursor: "pointer",
|
|
|
|
}}
|
|
|
|
/>
|
2023-05-24 01:29:47 +02:00
|
|
|
<Modal open={open} onClose={() => setOpen(false)} BackdropComponent={LightboxBackdrop}>
|
2023-05-23 21:13:01 +02:00
|
|
|
<Fade in={open}>
|
|
|
|
<Box
|
|
|
|
component="img"
|
|
|
|
src={props.attachment.url}
|
|
|
|
alt={t("notifications_attachment_image")}
|
|
|
|
loading="lazy"
|
|
|
|
sx={{
|
|
|
|
maxWidth: 1,
|
|
|
|
maxHeight: 1,
|
|
|
|
position: "absolute",
|
|
|
|
top: "50%",
|
|
|
|
left: "50%",
|
|
|
|
transform: "translate(-50%, -50%)",
|
|
|
|
padding: 4,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</Fade>
|
|
|
|
</Modal>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
2022-03-03 20:51:56 +01:00
|
|
|
|
2022-04-21 22:33:49 +02:00
|
|
|
const UserActions = (props) => {
|
2023-05-23 21:13:01 +02:00
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{props.notification.actions.map((action) => (
|
2023-05-24 01:29:47 +02:00
|
|
|
<UserAction key={action.id} notification={props.notification} action={action} />
|
2023-05-23 21:13:01 +02:00
|
|
|
))}
|
|
|
|
</>
|
|
|
|
);
|
2022-04-21 22:33:49 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
const UserAction = (props) => {
|
2023-05-23 21:13:01 +02:00
|
|
|
const { t } = useTranslation();
|
|
|
|
const notification = props.notification;
|
|
|
|
const action = props.action;
|
|
|
|
if (action.action === "broadcast") {
|
|
|
|
return (
|
|
|
|
<Tooltip title={t("notifications_actions_not_supported")}>
|
|
|
|
<span>
|
2023-05-24 01:29:47 +02:00
|
|
|
<Button disabled aria-label={t("notifications_actions_not_supported")}>
|
2023-05-23 21:13:01 +02:00
|
|
|
{action.label}
|
|
|
|
</Button>
|
|
|
|
</span>
|
|
|
|
</Tooltip>
|
|
|
|
);
|
|
|
|
} else if (action.action === "view") {
|
|
|
|
return (
|
2023-05-24 01:29:47 +02:00
|
|
|
<Tooltip title={t("notifications_actions_open_url_title", { url: action.url })}>
|
2023-05-23 21:13:01 +02:00
|
|
|
<Button
|
|
|
|
onClick={() => openUrl(action.url)}
|
|
|
|
aria-label={t("notifications_actions_open_url_title", {
|
|
|
|
url: action.url,
|
|
|
|
})}
|
|
|
|
>
|
|
|
|
{action.label}
|
|
|
|
</Button>
|
|
|
|
</Tooltip>
|
|
|
|
);
|
|
|
|
} else if (action.action === "http") {
|
|
|
|
const method = action.method ?? "POST";
|
2023-05-24 01:29:47 +02:00
|
|
|
const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? "");
|
2023-05-23 21:13:01 +02:00
|
|
|
return (
|
|
|
|
<Tooltip
|
|
|
|
title={t("notifications_actions_http_request_title", {
|
|
|
|
method: method,
|
|
|
|
url: action.url,
|
|
|
|
})}
|
|
|
|
>
|
|
|
|
<Button
|
|
|
|
onClick={() => performHttpAction(notification, action)}
|
|
|
|
aria-label={t("notifications_actions_http_request_title", {
|
|
|
|
method: method,
|
|
|
|
url: action.url,
|
|
|
|
})}
|
|
|
|
>
|
|
|
|
{label}
|
|
|
|
</Button>
|
|
|
|
</Tooltip>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return null; // Others
|
2022-04-21 22:33:49 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
const performHttpAction = async (notification, action) => {
|
2023-05-23 21:13:01 +02:00
|
|
|
console.log(`[Notifications] Performing HTTP user action`, action);
|
|
|
|
try {
|
|
|
|
updateActionStatus(notification, action, ACTION_PROGRESS_ONGOING, null);
|
|
|
|
const response = await fetch(action.url, {
|
|
|
|
method: action.method ?? "POST",
|
|
|
|
headers: action.headers ?? {},
|
|
|
|
// This must not null-coalesce to a non nullish value. Otherwise, the fetch API
|
|
|
|
// will reject it for "having a body"
|
|
|
|
body: action.body,
|
|
|
|
});
|
|
|
|
console.log(`[Notifications] HTTP user action response`, response);
|
|
|
|
const success = response.status >= 200 && response.status <= 299;
|
|
|
|
if (success) {
|
|
|
|
updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null);
|
|
|
|
} else {
|
2023-05-24 01:29:47 +02:00
|
|
|
updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: Unexpected response HTTP ${response.status}`);
|
2022-04-21 22:33:49 +02:00
|
|
|
}
|
2023-05-23 21:13:01 +02:00
|
|
|
} catch (e) {
|
|
|
|
console.log(`[Notifications] HTTP action failed`, e);
|
2023-05-24 01:29:47 +02:00
|
|
|
updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: ${e} Check developer console for details.`);
|
2023-05-23 21:13:01 +02:00
|
|
|
}
|
2022-04-21 22:33:49 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
const updateActionStatus = (notification, action, progress, error) => {
|
2023-05-23 21:13:01 +02:00
|
|
|
notification.actions = notification.actions.map((a) => {
|
|
|
|
if (a.id !== action.id) {
|
|
|
|
return a;
|
|
|
|
}
|
|
|
|
return { ...a, progress: progress, error: error };
|
|
|
|
});
|
|
|
|
subscriptionManager.updateNotification(notification);
|
|
|
|
};
|
2022-04-21 22:33:49 +02:00
|
|
|
|
|
|
|
const ACTION_PROGRESS_ONGOING = 1;
|
|
|
|
const ACTION_PROGRESS_SUCCESS = 2;
|
|
|
|
const ACTION_PROGRESS_FAILED = 3;
|
|
|
|
|
|
|
|
const ACTION_LABEL_SUFFIX = {
|
2023-05-23 21:13:01 +02:00
|
|
|
[ACTION_PROGRESS_ONGOING]: " …",
|
|
|
|
[ACTION_PROGRESS_SUCCESS]: " ✔",
|
|
|
|
[ACTION_PROGRESS_FAILED]: " ❌",
|
2022-04-21 22:33:49 +02:00
|
|
|
};
|
|
|
|
|
2022-03-07 22:36:49 +01:00
|
|
|
const NoNotifications = (props) => {
|
2023-05-23 21:13:01 +02:00
|
|
|
const { t } = useTranslation();
|
2023-05-24 01:29:47 +02:00
|
|
|
const shortUrl = topicShortUrl(props.subscription.baseUrl, props.subscription.topic);
|
2023-05-23 21:13:01 +02:00
|
|
|
return (
|
|
|
|
<VerticallyCenteredContainer maxWidth="xs">
|
|
|
|
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
2023-05-24 01:29:47 +02:00
|
|
|
<img src={logoOutline} height="64" width="64" alt={t("action_bar_logo_alt")} />
|
2023-05-23 21:13:01 +02:00
|
|
|
<br />
|
|
|
|
{t("notifications_none_for_topic_title")}
|
|
|
|
</Typography>
|
|
|
|
<Paragraph>{t("notifications_none_for_topic_description")}</Paragraph>
|
|
|
|
<Paragraph>
|
|
|
|
{t("notifications_example")}:<br />
|
|
|
|
<tt>$ curl -d "Hi" {shortUrl}</tt>
|
|
|
|
</Paragraph>
|
|
|
|
<Paragraph>
|
|
|
|
<ForMoreDetails />
|
|
|
|
</Paragraph>
|
|
|
|
</VerticallyCenteredContainer>
|
|
|
|
);
|
2022-02-28 17:52:50 +01:00
|
|
|
};
|
|
|
|
|
2022-03-09 00:56:28 +01:00
|
|
|
const NoNotificationsWithoutSubscription = (props) => {
|
2023-05-23 21:13:01 +02:00
|
|
|
const { t } = useTranslation();
|
|
|
|
const subscription = props.subscriptions[0];
|
|
|
|
const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic);
|
|
|
|
return (
|
|
|
|
<VerticallyCenteredContainer maxWidth="xs">
|
|
|
|
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
2023-05-24 01:29:47 +02:00
|
|
|
<img src={logoOutline} height="64" width="64" alt={t("action_bar_logo_alt")} />
|
2023-05-23 21:13:01 +02:00
|
|
|
<br />
|
|
|
|
{t("notifications_none_for_any_title")}
|
|
|
|
</Typography>
|
|
|
|
<Paragraph>{t("notifications_none_for_any_description")}</Paragraph>
|
|
|
|
<Paragraph>
|
|
|
|
{t("notifications_example")}:<br />
|
|
|
|
<tt>$ curl -d "Hi" {shortUrl}</tt>
|
|
|
|
</Paragraph>
|
|
|
|
<Paragraph>
|
|
|
|
<ForMoreDetails />
|
|
|
|
</Paragraph>
|
|
|
|
</VerticallyCenteredContainer>
|
|
|
|
);
|
2022-03-09 00:56:28 +01:00
|
|
|
};
|
|
|
|
|
2022-03-07 22:36:49 +01:00
|
|
|
const NoSubscriptions = () => {
|
2023-05-23 21:13:01 +02:00
|
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
|
|
<VerticallyCenteredContainer maxWidth="xs">
|
|
|
|
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
2023-05-24 01:29:47 +02:00
|
|
|
<img src={logoOutline} height="64" width="64" alt={t("action_bar_logo_alt")} />
|
2023-05-23 21:13:01 +02:00
|
|
|
<br />
|
|
|
|
{t("notifications_no_subscriptions_title")}
|
|
|
|
</Typography>
|
|
|
|
<Paragraph>
|
|
|
|
{t("notifications_no_subscriptions_description", {
|
|
|
|
linktext: t("nav_button_subscribe"),
|
|
|
|
})}
|
|
|
|
</Paragraph>
|
|
|
|
<Paragraph>
|
|
|
|
<ForMoreDetails />
|
|
|
|
</Paragraph>
|
|
|
|
</VerticallyCenteredContainer>
|
|
|
|
);
|
2022-03-07 22:36:49 +01:00
|
|
|
};
|
|
|
|
|
2022-04-08 03:46:33 +02:00
|
|
|
const ForMoreDetails = () => {
|
2023-05-23 21:13:01 +02:00
|
|
|
return (
|
|
|
|
<Trans
|
|
|
|
i18nKey="notifications_more_details"
|
|
|
|
components={{
|
2023-05-24 01:29:47 +02:00
|
|
|
websiteLink: <Link href="https://ntfy.sh" target="_blank" rel="noopener" />,
|
|
|
|
docsLink: <Link href="https://ntfy.sh/docs" target="_blank" rel="noopener" />,
|
2023-05-23 21:13:01 +02:00
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
2022-04-08 03:46:33 +02:00
|
|
|
};
|
|
|
|
|
2022-03-07 22:36:49 +01:00
|
|
|
const Loading = () => {
|
2023-05-23 21:13:01 +02:00
|
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
|
|
<VerticallyCenteredContainer>
|
2023-05-24 01:29:47 +02:00
|
|
|
<Typography variant="h5" color="text.secondary" align="center" sx={{ paddingBottom: 1 }}>
|
2023-05-23 21:13:01 +02:00
|
|
|
<CircularProgress disableShrink sx={{ marginBottom: 1 }} />
|
|
|
|
<br />
|
|
|
|
{t("notifications_loading")}
|
|
|
|
</Typography>
|
|
|
|
</VerticallyCenteredContainer>
|
|
|
|
);
|
2022-03-07 22:36:49 +01:00
|
|
|
};
|