diff --git a/web/public/static/img/file-app.svg b/web/public/static/img/file-app.svg new file mode 100644 index 0000000..b5a43db --- /dev/null +++ b/web/public/static/img/file-app.svg @@ -0,0 +1 @@ + diff --git a/web/public/static/img/file-audio.svg b/web/public/static/img/file-audio.svg new file mode 100644 index 0000000..7f0c959 --- /dev/null +++ b/web/public/static/img/file-audio.svg @@ -0,0 +1 @@ + diff --git a/web/public/static/img/file-document.svg b/web/public/static/img/file-document.svg new file mode 100644 index 0000000..990d633 --- /dev/null +++ b/web/public/static/img/file-document.svg @@ -0,0 +1 @@ + diff --git a/web/public/static/img/file-image.svg b/web/public/static/img/file-image.svg new file mode 100644 index 0000000..453cf73 --- /dev/null +++ b/web/public/static/img/file-image.svg @@ -0,0 +1 @@ + diff --git a/web/public/static/img/file-video.svg b/web/public/static/img/file-video.svg new file mode 100644 index 0000000..b8a33e5 --- /dev/null +++ b/web/public/static/img/file-video.svg @@ -0,0 +1 @@ + diff --git a/web/src/app/utils.js b/web/src/app/utils.js index 0c04a27..1f458d8 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -90,6 +90,20 @@ export const encodeBase64Url = (s) => { .replaceAll('=', ''); } +export const formatShortDateTime = (timestamp) => { + return new Intl.DateTimeFormat('default', {dateStyle: 'short', timeStyle: 'short'}) + .format(new Date(timestamp * 1000)); +} + +export const formatBytes = (bytes, decimals = 2) => { + if (bytes === 0) return '0 bytes'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + // From: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch export async function* fetchLinesIterator(fileURL, headers) { const utf8Decoder = new TextDecoder('utf-8'); diff --git a/web/src/components/Notifications.js b/web/src/components/Notifications.js index b4e9511..7307b79 100644 --- a/web/src/components/Notifications.js +++ b/web/src/components/Notifications.js @@ -1,9 +1,9 @@ import Container from "@mui/material/Container"; -import {CardActions, CardContent, Link, Stack} from "@mui/material"; +import {ButtonBase, CardActions, CardContent, Fade, Link, Modal, Stack, styled} from "@mui/material"; import Card from "@mui/material/Card"; import Typography from "@mui/material/Typography"; import * as React from "react"; -import {formatMessage, formatTitle, topicShortUrl, unmatchedTags} from "../app/utils"; +import {formatBytes, formatMessage, formatShortDateTime, formatTitle, topicShortUrl, unmatchedTags} from "../app/utils"; import IconButton from "@mui/material/IconButton"; import CloseIcon from '@mui/icons-material/Close'; import {Paragraph, VerticallyCenteredContainer} from "./styles"; @@ -11,6 +11,8 @@ import {useLiveQuery} from "dexie-react-hooks"; import db from "../app/db"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; +import theme from "./theme"; +import {useState} from "react"; const Notifications = (props) => { const subscription = props.subscription; @@ -25,7 +27,7 @@ const Notifications = (props) => { const sortedNotifications = Array.from(notifications) .sort((a, b) => a.time < b.time ? 1 : -1); return ( - + {sortedNotifications.map(notification => { const NotificationItem = (props) => { const subscriptionId = props.subscriptionId; const notification = props.notification; - const date = new Intl.DateTimeFormat('default', {dateStyle: 'short', timeStyle: 'short'}) - .format(new Date(notification.time * 1000)); + 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 db.notifications.delete(notification.id); // FIXME } - const attachment = notification.attachment; - const expired = attachment?.expires <= Date.now()/1000; - const image = attachment?.type.startsWith("image/") && !expired; + const expired = attachment && attachment.expires && attachment.expires < Date.now()/1000; return ( - + @@ -69,27 +69,11 @@ const NotificationItem = (props) => { {notification.title && {formatTitle(notification)}} {formatMessage(notification)} - {image && } - {attachment && !image && - - {attachment.name}
- {attachment.size}, {attachment.expires} -
} + {attachment && } {tags && Tags: {tags}}
- {attachment && - + {attachment && !expired && + @@ -98,6 +82,151 @@ const NotificationItem = (props) => { ); } +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 ; + } + + // 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) ? <>
{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 [open, setOpen] = useState(false); + return ( + <> + setOpen(true)} + sx={{ + marginTop: 2, + borderRadius: '4px', + boxShadow: 2, + width: 1, + maxHeight: '400px', + objectFit: 'cover', + cursor: 'pointer' + }} + /> + setOpen(false)} + > + + + + + + ); +} + +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 ( + + ); +} const NothingHereYet = (props) => { const shortUrl = topicShortUrl(props.subscription.baseUrl, props.subscription.topic); diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js index 80487d9..c9913bd 100644 --- a/web/src/components/Preferences.js +++ b/web/src/components/Preferences.js @@ -35,7 +35,7 @@ import DialogActions from "@mui/material/DialogActions"; const Preferences = (props) => { return ( - + diff --git a/web/src/components/styles.js b/web/src/components/styles.js index 0f646a1..3232b01 100644 --- a/web/src/components/styles.js +++ b/web/src/components/styles.js @@ -14,5 +14,5 @@ export const VerticallyCenteredContainer = styled(Container)({ flexDirection: 'column', justifyContent: 'center', alignContent: 'center', - color: theme.palette.body.main + color: theme.palette.text.primary }); diff --git a/web/src/components/theme.js b/web/src/components/theme.js index 6e850bf..3778356 100644 --- a/web/src/components/theme.js +++ b/web/src/components/theme.js @@ -12,9 +12,6 @@ const theme = createTheme({ error: { main: red.A400, }, - body: { - main: '#444', - } }, components: { MuiListItemIcon: { @@ -24,6 +21,13 @@ const theme = createTheme({ }, }, }, + MuiBackdrop: { + styleOverrides: { + root: { + backgroundColor: 'rgba(0, 0, 0, 0.8)' // was: 0.5 + } + } + } }, });