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
+ }
+ }
+ }
},
});