PWA reload icon, lazy loading: EmojiPicker and MarkdownContent

This commit is contained in:
binwiederhier
2026-06-24 11:44:38 -04:00
parent 8022401728
commit 65e409e9e4
19 changed files with 155 additions and 88 deletions
+2 -1
View File
@@ -1 +1,2 @@
src/app/emojis.js
src/app/emojis.js
src/app/emojisMapped.js
+1
View File
@@ -2,3 +2,4 @@ build/
dist/
public/static/langs/
src/app/emojis.js
src/app/emojisMapped.js
+1
View File
@@ -67,6 +67,7 @@
"action_bar_profile_logout": "Logout",
"action_bar_sign_in": "Sign in",
"action_bar_sign_up": "Sign up",
"action_bar_reload": "Reload app",
"message_bar_type_message": "Type a message here",
"message_bar_error_publishing": "Error publishing notification",
"message_bar_show_dialog": "Show publish dialog",
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -48,7 +48,8 @@ import RefreshIcon from "@mui/icons-material/Refresh";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import CelebrationIcon from "@mui/icons-material/Celebration";
import CloseIcon from "@mui/icons-material/Close";
import { ContentCopy, Public } from "@mui/icons-material";
import ContentCopy from "@mui/icons-material/ContentCopy";
import Public from "@mui/icons-material/Public";
import AddIcon from "@mui/icons-material/Add";
import routes from "./routes";
import { copyToClipboard, formatBytes, formatShortDate, formatShortDateTime, formatShortDuration, openUrl } from "../app/utils";
+30 -1
View File
@@ -6,9 +6,12 @@ import { useLocation, useNavigate } from "react-router-dom";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import NotificationsIcon from "@mui/icons-material/Notifications";
import NotificationsOffIcon from "@mui/icons-material/NotificationsOff";
import RefreshIcon from "@mui/icons-material/Refresh";
import { useTranslation } from "react-i18next";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
import { Logout, Person, Settings } from "@mui/icons-material";
import Logout from "@mui/icons-material/Logout";
import Person from "@mui/icons-material/Person";
import Settings from "@mui/icons-material/Settings";
import session from "../app/Session";
import logo from "../img/ntfy.svg";
import subscriptionManager from "../app/SubscriptionManager";
@@ -88,6 +91,7 @@ const ActionBar = (props) => {
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
{title}
</Typography>
{isLaunchedPWA && <ReloadIcon />}
{props.selected && <SettingsIcons subscription={props.selected} onUnsubscribe={props.onUnsubscribe} />}
<ProfileIcon />
</Toolbar>
@@ -124,6 +128,31 @@ const SettingsIcons = (props) => {
);
};
// ReloadIcon hard-refreshes the app. A plain reload would just serve the precached PWA shell,
// so we first purge the service worker caches to force fresh assets from the network.
const ReloadIcon = () => {
const { t } = useTranslation();
const handleReload = async () => {
try {
if ("caches" in window) {
const keys = await caches.keys();
await Promise.all(keys.map((key) => caches.delete(key)));
}
} catch (e) {
console.warn("[ActionBar] Error clearing caches during reload", e);
} finally {
window.location.reload();
}
};
return (
<IconButton color="inherit" size="large" edge="end" onClick={handleReload} aria-label={t("action_bar_reload")}>
<RefreshIcon />
</IconButton>
);
};
const ProfileIcon = () => {
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState(null);
+1 -1
View File
@@ -1,7 +1,7 @@
import * as React from "react";
import { useRef, useState } from "react";
import { Typography, Box, TextField, ClickAwayListener, Fade, InputAdornment, styled, IconButton, Popper } from "@mui/material";
import { Close } from "@mui/icons-material";
import Close from "@mui/icons-material/Close";
import { useTranslation } from "react-i18next";
import { splitNoEmpty } from "../app/utils";
import { rawEmojis } from "../app/emojis";
+2 -1
View File
@@ -4,7 +4,8 @@ import { Typography, TextField, Button, Box, IconButton, InputAdornment } from "
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
import { NavLink } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Visibility, VisibilityOff } from "@mui/icons-material";
import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from "@mui/icons-material/VisibilityOff";
import accountApi from "../app/AccountApi";
import AvatarBox from "./AvatarBox";
import session from "../app/Session";
+62
View File
@@ -0,0 +1,62 @@
import * as React from "react";
import { useEffect } from "react";
import { useRemark } from "react-remark";
import styled from "@emotion/styled";
const MarkdownContainer = styled("div")`
line-height: 1;
h1,
h2,
h3,
h4,
h5,
h6,
p,
pre,
ul,
ol,
blockquote {
margin: 0;
}
p {
line-height: 1.5;
}
blockquote,
pre {
border-radius: 3px;
background: ${(props) => (props.theme.palette.mode === "light" ? "#f5f5f5" : "#333")};
}
pre {
overflow-x: scroll;
padding: 0.9rem;
}
ul,
ol,
blockquote {
padding-inline: 1rem;
}
img {
max-width: 100%;
}
`;
// Renders text/markdown notification bodies. react-remark (and its remark/unified
// dependency stack) is heavy, so this component lives in its own module and is loaded
// lazily by Notifications.jsx -- it only ships when a markdown message is actually shown.
const MarkdownContent = ({ content }) => {
const [reactContent, setMarkdownSource] = useRemark();
useEffect(() => {
setMarkdownSource(content);
}, [content]);
return <MarkdownContainer>{reactContent}</MarkdownContainer>;
};
export default MarkdownContent;
+4 -1
View File
@@ -27,7 +27,10 @@ import Person from "@mui/icons-material/Person";
import SettingsIcon from "@mui/icons-material/Settings";
import AddIcon from "@mui/icons-material/Add";
import { useLocation, useNavigate } from "react-router-dom";
import { ChatBubble, MoreVert, NotificationsOffOutlined, Send } from "@mui/icons-material";
import ChatBubble from "@mui/icons-material/ChatBubble";
import MoreVert from "@mui/icons-material/MoreVert";
import NotificationsOffOutlined from "@mui/icons-material/NotificationsOffOutlined";
import Send from "@mui/icons-material/Send";
import ArticleIcon from "@mui/icons-material/Article";
import { Trans, useTranslation } from "react-i18next";
import CelebrationIcon from "@mui/icons-material/Celebration";
+9 -56
View File
@@ -17,15 +17,13 @@ import {
Button,
} from "@mui/material";
import * as React from "react";
import { useEffect, useState } from "react";
import { Suspense, lazy, 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 { useRemark } from "react-remark";
import styled from "@emotion/styled";
import {
copyToClipboard,
formatBytes,
@@ -172,64 +170,19 @@ const autolink = (s) => {
return <>{parts}</>;
};
const MarkdownContainer = styled("div")`
line-height: 1;
h1,
h2,
h3,
h4,
h5,
h6,
p,
pre,
ul,
ol,
blockquote {
margin: 0;
}
p {
line-height: 1.5;
}
blockquote,
pre {
border-radius: 3px;
background: ${(props) => (props.theme.palette.mode === "light" ? "#f5f5f5" : "#333")};
}
pre {
overflow-x: scroll;
padding: 0.9rem;
}
ul,
ol,
blockquote {
padding-inline: 1rem;
}
img {
max-width: 100%;
}
`;
const MarkdownContent = ({ content }) => {
const [reactContent, setMarkdownSource] = useRemark();
useEffect(() => {
setMarkdownSource(content);
}, [content]);
return <MarkdownContainer>{reactContent}</MarkdownContainer>;
};
// Loaded lazily so the heavy react-remark/unified markdown stack is only fetched when a
// text/markdown notification is actually rendered (see MarkdownContent.jsx).
const MarkdownContent = lazy(() => import("./MarkdownContent"));
const NotificationBody = ({ notification }) => {
const displayAsMarkdown = notification.content_type === "text/markdown";
const formatted = formatMessage(notification);
if (displayAsMarkdown) {
return <MarkdownContent content={formatted} />;
return (
<Suspense fallback={null}>
<MarkdownContent content={formatted} />
</Suspense>
);
}
return autolink(formatted);
};
+1 -1
View File
@@ -33,7 +33,7 @@ import CloseIcon from "@mui/icons-material/Close";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import { useLiveQuery } from "dexie-react-hooks";
import { useTranslation } from "react-i18next";
import { Info } from "@mui/icons-material";
import Info from "@mui/icons-material/Info";
import { useOutletContext } from "react-router-dom";
import userManager from "../app/UserManager";
import { playSound, shortUrl, shuffle, sounds, validUrl } from "../app/utils";
+9 -4
View File
@@ -1,5 +1,5 @@
import * as React from "react";
import { useContext, useEffect, useRef, useState } from "react";
import { Suspense, lazy, useContext, useEffect, useRef, useState } from "react";
import {
Checkbox,
Chip,
@@ -22,7 +22,7 @@ import {
useTheme,
} from "@mui/material";
import InsertEmoticonIcon from "@mui/icons-material/InsertEmoticon";
import { Close } from "@mui/icons-material";
import Close from "@mui/icons-material/Close";
import { Trans, useTranslation } from "react-i18next";
import priority1 from "../img/priority-1.svg";
import priority2 from "../img/priority-2.svg";
@@ -35,13 +35,16 @@ import AttachmentIcon from "./AttachmentIcon";
import DialogFooter from "./DialogFooter";
import api from "../app/Api";
import userManager from "../app/UserManager";
import EmojiPicker from "./EmojiPicker";
import session from "../app/Session";
import routes from "./routes";
import accountApi from "../app/AccountApi";
import { UnauthorizedError } from "../app/errors";
import AccountContext from "./AccountContext";
// Loaded lazily so the full emoji dataset (~300 KB) is only fetched when the publish
// dialog is opened, not in the initial app bundle (see EmojiPicker.jsx).
const EmojiPicker = lazy(() => import("./EmojiPicker"));
const PublishDialog = (props) => {
const theme = useTheme();
const { t } = useTranslation();
@@ -398,7 +401,9 @@ const PublishDialog = (props) => {
}
/>
<div style={{ display: "flex" }}>
<EmojiPicker anchorEl={emojiPickerAnchorEl} onEmojiPick={handleEmojiPick} onClose={handleEmojiClose} />
<Suspense fallback={null}>
<EmojiPicker anchorEl={emojiPickerAnchorEl} onEmojiPick={handleEmojiPick} onClose={handleEmojiClose} />
</Suspense>
<DialogIconButton disabled={disabled} onClick={handleEmojiClick} aria-label={t("publish_dialog_emoji_picker_show")}>
<InsertEmoticonIcon />
</DialogIconButton>
+2 -1
View File
@@ -17,7 +17,8 @@ import {
useTheme,
} from "@mui/material";
import { useTranslation } from "react-i18next";
import { Check, DeleteForever } from "@mui/icons-material";
import Check from "@mui/icons-material/Check";
import DeleteForever from "@mui/icons-material/DeleteForever";
import { validTopic } from "../app/utils";
import DialogFooter from "./DialogFooter";
import session from "../app/Session";
+2 -1
View File
@@ -1,5 +1,6 @@
import * as React from "react";
import { Lock, Public } from "@mui/icons-material";
import Lock from "@mui/icons-material/Lock";
import Public from "@mui/icons-material/Public";
import { Box } from "@mui/material";
export const PermissionReadWrite = React.forwardRef((props, ref) => <PermissionInternal icon={Public} ref={ref} {...props} />);
+2 -1
View File
@@ -4,7 +4,8 @@ import { TextField, Button, Box, Typography, InputAdornment, IconButton } from "
import { NavLink } from "react-router-dom";
import { useTranslation } from "react-i18next";
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
import { Visibility, VisibilityOff } from "@mui/icons-material";
import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from "@mui/icons-material/VisibilityOff";
import accountApi from "../app/AccountApi";
import AvatarBox from "./AvatarBox";
import session from "../app/Session";
+10 -12
View File
@@ -19,18 +19,16 @@ import {
} from "@mui/material";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import {
Clear,
ClearAll,
Edit,
EnhancedEncryption,
Lock,
LockOpen,
Notifications,
NotificationsOff,
RemoveCircle,
Send,
} from "@mui/icons-material";
import Clear from "@mui/icons-material/Clear";
import ClearAll from "@mui/icons-material/ClearAll";
import Edit from "@mui/icons-material/Edit";
import EnhancedEncryption from "@mui/icons-material/EnhancedEncryption";
import Lock from "@mui/icons-material/Lock";
import LockOpen from "@mui/icons-material/LockOpen";
import Notifications from "@mui/icons-material/Notifications";
import NotificationsOff from "@mui/icons-material/NotificationsOff";
import RemoveCircle from "@mui/icons-material/RemoveCircle";
import Send from "@mui/icons-material/Send";
import subscriptionManager from "../app/SubscriptionManager";
import DialogFooter from "./DialogFooter";
import accountApi, { Role } from "../app/AccountApi";
+2 -1
View File
@@ -24,7 +24,8 @@ import {
useTheme,
} from "@mui/material";
import { Trans, useTranslation } from "react-i18next";
import { Check, Close } from "@mui/icons-material";
import Check from "@mui/icons-material/Check";
import Close from "@mui/icons-material/Close";
import { NavLink } from "react-router-dom";
import { UnauthorizedError } from "../app/errors";
import { formatBytes, formatNumber, formatPrice, formatShortDate } from "../app/utils";