ntfy/web/src/components/Notifications.jsx

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

623 lines
19 KiB
React
Raw Normal View History

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