| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- import Container from "@mui/material/Container";
- import {ButtonBase, CardActions, CardContent, CircularProgress, Fade, Link, Modal, Stack} from "@mui/material";
- import Card from "@mui/material/Card";
- import Typography from "@mui/material/Typography";
- import * as React from "react";
- import {useEffect, useState} from "react";
- import {
- formatBytes,
- formatMessage,
- formatShortDateTime,
- formatTitle,
- openUrl,
- topicShortUrl,
- unmatchedTags
- } from "../app/utils";
- import IconButton from "@mui/material/IconButton";
- import CloseIcon from '@mui/icons-material/Close';
- import {LightboxBackdrop, Paragraph, VerticallyCenteredContainer} from "./styles";
- import {useLiveQuery} from "dexie-react-hooks";
- import Box from "@mui/material/Box";
- import Button from "@mui/material/Button";
- import subscriptionManager from "../app/SubscriptionManager";
- import InfiniteScroll from "react-infinite-scroll-component";
- const Notifications = (props) => {
- if (props.mode === "all") {
- return (props.subscriptions) ? <AllSubscriptions subscriptions={props.subscriptions}/> : <Loading/>;
- }
- return (props.subscription) ? <SingleSubscription subscription={props.subscription}/> : <Loading/>;
- }
- const AllSubscriptions = () => {
- const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []);
- if (notifications === null || notifications === undefined) {
- return <Loading/>;
- } else if (notifications.length === 0) {
- return <NoSubscriptions/>;
- }
- return <NotificationList key="all" notifications={notifications}/>;
- }
- const SingleSubscription = (props) => {
- const subscription = props.subscription;
- const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]);
- if (notifications === null || notifications === undefined) {
- return <Loading/>;
- } else if (notifications.length === 0) {
- return <NoNotifications subscription={subscription}/>;
- }
- return <NotificationList id={subscription.id} notifications={notifications}/>;
- }
- const NotificationList = (props) => {
- const pageSize = 20;
- const notifications = props.notifications;
- const [maxCount, setMaxCount] = useState(pageSize);
- // Reset state when the list identifier changes, i.e when we switch between subscriptions
- useEffect(() => {
- return () => {
- setMaxCount(pageSize);
- document.getElementById("main").scrollTo(0, 0);
- }
- }, [props.id]);
- const count = Math.min(notifications.length, maxCount);
- console.log(`xxx id=${props.id} scrollMax=${maxCount} count=${count} len=${notifications.length}`)
- return (
- <InfiniteScroll
- dataLength={count}
- next={() => setMaxCount(prev => prev + pageSize)}
- hasMore={count < notifications.length}
- loader={<h1>aa</h1>}
- scrollThreshold={0.7}
- scrollableTarget="main"
- >
- <Container maxWidth="md" sx={{marginTop: 3, marginBottom: 3}}>
- <Stack spacing={3}>
- {notifications.slice(0, count).map(notification =>
- <NotificationItem
- key={notification.id}
- notification={notification}
- />)}
- </Stack>
- </Container>
- </InfiniteScroll>
- );
- }
- const NotificationItem = (props) => {
- const notification = props.notification;
- const subscriptionId = notification.subscriptionId;
- 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} from ${subscriptionId}`);
- await subscriptionManager.deleteNotification(notification.id)
- }
- const expired = attachment && attachment.expires && attachment.expires < Date.now()/1000;
- const showAttachmentActions = attachment && !expired;
- const showClickAction = notification.click;
- const showActions = showAttachmentActions || showClickAction;
- return (
- <Card sx={{ minWidth: 275, padding: 1 }}>
- <CardContent>
- <IconButton onClick={handleDelete} sx={{ float: 'right', marginRight: -1, marginTop: -1 }}>
- <CloseIcon />
- </IconButton>
- <Typography sx={{ fontSize: 14 }} color="text.secondary">
- {date}
- {[1,2,4,5].includes(notification.priority) &&
- <img
- src={`/static/img/priority-${notification.priority}.svg`}
- alt={`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">
- <circle cx="50" cy="50" r="50" fill="#338574"/>
- </svg>}
- </Typography>
- {notification.title && <Typography variant="h5" component="div">{formatTitle(notification)}</Typography>}
- <Typography variant="body1" sx={{ whiteSpace: 'pre-line' }}>{formatMessage(notification)}</Typography>
- {attachment && <Attachment attachment={attachment}/>}
- {tags && <Typography sx={{ fontSize: 14 }} color="text.secondary">Tags: {tags}</Typography>}
- </CardContent>
- {showActions &&
- <CardActions sx={{paddingTop: 0}}>
- {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>}
- </CardActions>}
- </Card>
- );
- }
- 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)}
- BackdropComponent={LightboxBackdrop}
- >
- <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"
- src={`/static/img/${imageFile}`}
- loading="lazy"
- sx={{
- width: '28px',
- height: '28px'
- }}
- />
- );
- }
- const NoNotifications = (props) => {
- const shortUrl = topicShortUrl(props.subscription.baseUrl, props.subscription.topic);
- 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 notifications"/><br />
- 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>
- $ curl -d "Hi" {shortUrl}
- </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>
- );
- };
- 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>
- );
- };
- export default Notifications;
|