Web app: implement markdown support

This commit is contained in:
Nihal Gonsalves 2023-07-05 19:33:45 +02:00
parent 56ed4f0515
commit f989fd0743
7 changed files with 648 additions and 8 deletions

View file

@ -55,6 +55,15 @@ class Prefs {
async setTheme(mode) {
await this.db.prefs.put({ key: "theme", value: mode });
}
async markdownAlwaysEnabled() {
const record = await this.db.prefs.get("markdownAlwaysEnabled");
return record?.value ?? false;
}
async setMarkdownAlwaysEnabled(enabled) {
await this.db.prefs.put({ key: "markdownAlwaysEnabled", value: enabled });
}
}
const prefs = new Prefs(db());

View file

@ -89,15 +89,15 @@ export const maybeWithAuth = (headers, user) => {
return headers;
};
export const maybeAppendActionErrors = (message, notification) => {
export const maybeActionErrors = (notification) => {
const actionErrors = (notification.actions ?? [])
.map((action) => action.error)
.filter((action) => !!action)
.join("\n");
if (actionErrors.length === 0) {
return message;
return undefined;
}
return `${message}\n\n${actionErrors}`;
return actionErrors;
};
export const shuffle = (arr) => {

View file

@ -24,7 +24,9 @@ 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 { formatBytes, formatShortDateTime, maybeAppendActionErrors, openUrl, shortUrl, topicShortUrl, unmatchedTags } from "../app/utils";
import { useRemark } from "react-remark";
import styled from "@emotion/styled";
import { formatBytes, formatShortDateTime, maybeActionErrors, openUrl, shortUrl, topicShortUrl, unmatchedTags } from "../app/utils";
import { formatMessage, formatTitle } from "../app/notificationUtils";
import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles";
import subscriptionManager from "../app/SubscriptionManager";
@ -35,6 +37,7 @@ import priority5 from "../img/priority-5.svg";
import logoOutline from "../img/ntfy-outline.svg";
import AttachmentIcon from "./AttachmentIcon";
import { useAutoSubscribe } from "./hooks";
import prefs from "../app/Prefs";
const priorityFiles = {
1: priority1,
@ -159,6 +162,63 @@ const autolink = (s) => {
return <>{parts}</>;
};
const MarkdownContainer = styled("div")`
line-height: 1;
h1,
h2,
h3,
h4,
h5,
h6,
p,
pre,
ul,
ol {
margin: 0;
}
p {
line-height: 1.2;
}
blockquote {
margin: 0;
padding-inline: 1rem;
background: ${(theme) => (theme.mode === "light" ? "#f1f1f1" : "#aeaeae")};
}
ul,
ol {
padding-inline: 1rem;
}
`;
const MarkdownContent = ({ content }) => {
const [reactContent, setMarkdownSource] = useRemark();
useEffect(() => {
setMarkdownSource(content);
}, [content]);
return <MarkdownContainer>{reactContent}</MarkdownContainer>;
};
const NotificationBody = ({ notification }) => {
const markdownAlwaysEnabled = useLiveQuery(async () => prefs.markdownAlwaysEnabled());
// TODO: check notification content-type when implemented on the server
const displayAsMarkdown = markdownAlwaysEnabled;
const formatted = formatMessage(notification);
if (displayAsMarkdown) {
return <MarkdownContent content={formatted} />;
}
return autolink(formatted);
};
const NotificationItem = (props) => {
const { t, i18n } = useTranslation();
const { notification } = props;
@ -183,6 +243,7 @@ const NotificationItem = (props) => {
const hasClickAction = notification.click;
const hasUserActions = notification.actions && notification.actions.length > 0;
const showActions = hasAttachmentActions || hasClickAction || hasUserActions;
return (
<Card sx={{ padding: 1 }} role="listitem" aria-label={t("notifications_list_item")}>
<CardContent>
@ -230,7 +291,8 @@ const NotificationItem = (props) => {
</Typography>
)}
<Typography variant="body1" sx={{ whiteSpace: "pre-line" }}>
{autolink(maybeAppendActionErrors(formatMessage(notification), notification))}
<NotificationBody notification={notification} />
{maybeActionErrors(notification)}
</Typography>
{attachment && <Attachment attachment={attachment} />}
{tags && (

View file

@ -259,6 +259,26 @@ const Theme = () => {
);
};
const MarkdownAlwaysEnabled = () => {
const { t } = useTranslation();
const labelId = "prefMarkdown";
const enabled = useLiveQuery(async () => prefs.markdownAlwaysEnabled());
const handleChange = async (ev) => {
await prefs.setMarkdownAlwaysEnabled(ev.target.value);
};
return (
<Pref labelId={labelId} title={t("prefs_appearance_markdown_always_enabled_title")}>
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
<Select value={enabled ?? false} onChange={handleChange} aria-labelledby={labelId}>
<MenuItem value>{t("prefs_appearance_markdown_always_enabled_on")}</MenuItem>
<MenuItem value={false}>{t("prefs_appearance_markdown_always_enabled_off")}</MenuItem>
</Select>
</FormControl>
</Pref>
);
};
const WebPushEnabled = () => {
const { t } = useTranslation();
const labelId = "prefWebPushEnabled";
@ -513,6 +533,7 @@ const Appearance = () => {
<PrefGroup>
<Theme />
<Language />
<MarkdownAlwaysEnabled />
</PrefGroup>
</Card>
);