import {
Container,
ButtonBase,
CardActions,
CardContent,
CircularProgress,
Fade,
Link,
Modal,
Snackbar,
Stack,
Tooltip,
Card,
Typography,
IconButton,
Box,
Button,
} from "@mui/material";
import * as React from "react";
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";
import InfiniteScroll from "react-infinite-scroll-component";
import { Trans, useTranslation } from "react-i18next";
import { useOutletContext } from "react-router-dom";
import {
formatBytes,
formatMessage,
formatShortDateTime,
formatTitle,
maybeAppendActionErrors,
openUrl,
shortUrl,
topicShortUrl,
unmatchedTags,
} from "../app/utils";
import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles";
import subscriptionManager from "../app/SubscriptionManager";
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";
import AttachmentIcon from "./AttachmentIcon";
import { useAutoSubscribe } from "./hooks";
const priorityFiles = {
1: priority1,
2: priority2,
4: priority4,
5: priority5,
};
export const AllSubscriptions = () => {
const { subscriptions } = useOutletContext();
if (!subscriptions) {
return ;
}
return ;
};
export const SingleSubscription = () => {
const { subscriptions, selected } = useOutletContext();
useAutoSubscribe(subscriptions, selected);
if (!selected) {
return ;
}
return ;
};
const AllSubscriptionsList = (props) => {
const { subscriptions } = props;
const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []);
if (notifications === null || notifications === undefined) {
return ;
}
if (subscriptions.length === 0) {
return ;
}
if (notifications.length === 0) {
return ;
}
return ;
};
const SingleSubscriptionList = (props) => {
const { subscription } = props;
const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]);
if (notifications === null || notifications === undefined) {
return ;
}
if (notifications.length === 0) {
return ;
}
return ;
};
const NotificationList = (props) => {
const { t } = useTranslation();
const pageSize = 20;
const { notifications } = props;
const [snackOpen, setSnackOpen] = useState(false);
const [maxCount, setMaxCount] = useState(pageSize);
const count = Math.min(notifications.length, maxCount);
useEffect(
() => () => {
setMaxCount(pageSize);
const main = document.getElementById("main");
if (main) {
main.scrollTo(0, 0);
}
},
[props.id]
);
return (
setMaxCount((prev) => prev + pageSize)}
hasMore={count < notifications.length}
loader={<>Loading ...>}
scrollThreshold={0.7}
scrollableTarget="main"
>
{notifications.slice(0, count).map((notification) => (
setSnackOpen(true)} />
))}
setSnackOpen(false)}
message={t("notifications_copied_to_clipboard")}
/>
);
};
/**
* Replace links with 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] = (
{shortUrl(parts[i])}
);
}
return <>{parts}>;
};
const NotificationItem = (props) => {
const { t } = useTranslation();
const { notification } = props;
const { attachment } = notification;
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 () => {
console.log(`[Notifications] Marking notification ${notification.id} as read`);
await subscriptionManager.markNotificationRead(notification.id);
};
const handleCopy = (s) => {
navigator.clipboard.writeText(s);
props.onShowSnack();
};
const expired = attachment && attachment.expires && attachment.expires < Date.now() / 1000;
const hasAttachmentActions = attachment && !expired;
const hasClickAction = notification.click;
const hasUserActions = notification.actions && notification.actions.length > 0;
const showActions = hasAttachmentActions || hasClickAction || hasUserActions;
return (
{notification.new === 1 && (
)}
{date}
{[1, 2, 4, 5].includes(notification.priority) && (
)}
{notification.new === 1 && (
)}
{notification.title && (
{formatTitle(notification)}
)}
{autolink(maybeAppendActionErrors(formatMessage(notification), notification))}
{attachment && }
{tags && (
{t("notifications_tags")}: {tags}
)}
{showActions && (
{hasAttachmentActions && (
<>
>
)}
{hasClickAction && (
<>
>
)}
{hasUserActions && }
)}
);
};
const Attachment = (props) => {
const { t } = useTranslation();
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
if (displayableImage) {
return ;
}
// 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 ? (
<>
{infos.join(", ")}
>
) : null;
// If expired, just show infos without click target
if (expired) {
return (
{attachment.name}
{maybeInfoText}
);
}
// Not expired
return (
{attachment.name}
{maybeInfoText}
);
};
const Image = (props) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
return (
<>
setOpen(true)}
sx={{
marginTop: 2,
borderRadius: "4px",
boxShadow: 2,
width: 1,
maxHeight: "400px",
objectFit: "cover",
cursor: "pointer",
}}
/>
setOpen(false)} BackdropComponent={LightboxBackdrop}>
>
);
};
const UserActions = (props) => (
<>
{props.notification.actions.map((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) => {
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.`);
}
};
const UserAction = (props) => {
const { t } = useTranslation();
const { notification } = props;
const { action } = props;
if (action.action === "broadcast") {
return (
);
}
if (action.action === "view") {
return (
);
}
if (action.action === "http") {
const method = action.method ?? "POST";
const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? "");
return (
);
}
return null; // Others
};
const NoNotifications = (props) => {
const { t } = useTranslation();
const topicShortUrlResolved = topicShortUrl(props.subscription.baseUrl, props.subscription.topic);
return (
{t("notifications_none_for_topic_title")}
{t("notifications_none_for_topic_description")}
{t("notifications_example")}:
{'$ curl -d "Hi" '}
{topicShortUrlResolved}
);
};
const NoNotificationsWithoutSubscription = (props) => {
const { t } = useTranslation();
const subscription = props.subscriptions[0];
const topicShortUrlResolved = topicShortUrl(subscription.baseUrl, subscription.topic);
return (
{t("notifications_none_for_any_title")}
{t("notifications_none_for_any_description")}
{t("notifications_example")}:
{'$ curl -d "Hi" '}
{topicShortUrlResolved}
);
};
const NoSubscriptions = () => {
const { t } = useTranslation();
return (
{t("notifications_no_subscriptions_title")}
{t("notifications_no_subscriptions_description", {
linktext: t("nav_button_subscribe"),
})}
);
};
const ForMoreDetails = () => (
,
docsLink: ,
}}
/>
);
const Loading = () => {
const { t } = useTranslation();
return (
{t("notifications_loading")}
);
};