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 {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 notifications={notifications}/>; } const SingleSubscription = (props) => { const subscription = props.subscription; const [offset, setOffset] = useState(0); const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id, offset), [subscription, offset]); if (notifications === null || notifications === undefined) { return <Loading/>; } else if (notifications.length === 0) { return <NoNotifications subscription={subscription}/>; } return <NotificationList notifications={notifications} onFetch={() => { console.log(`setOffset`) setOffset(prev => prev + 20) }}/>; } const NotificationList = (props) => { const notifications = props.notifications; const pageSize = 20; const [count, setCount] = useState(Math.min(notifications.length, pageSize)); console.log(`count ${count}`) return ( <InfiniteScroll dataLength={count} next={() => setCount(prev => Math.min(notifications.length, prev + 20))} hasMore={count < notifications.length} loader={<h1>aa</h1>} scrollThreshold="400px" 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;