feat: support site announcement (#49)

This commit is contained in:
Zhang Minghan 2024-01-19 14:53:34 +08:00
parent 8e678637fb
commit 1e07a245c5
13 changed files with 256 additions and 16 deletions

View File

@ -53,6 +53,7 @@
"react-router-dom": "^6.17.0",
"react-syntax-highlighter": "^15.5.0",
"rehype-katex": "^6.0.3",
"rehype-raw": "^7.0.0",
"remark-breaks": "^4.0.0",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",

143
app/pnpm-lock.yaml generated
View File

@ -128,6 +128,9 @@ dependencies:
rehype-katex:
specifier: ^6.0.3
version: 6.0.3
rehype-raw:
specifier: ^7.0.0
version: 7.0.0
remark-breaks:
specifier: ^4.0.0
version: 4.0.0
@ -2150,6 +2153,12 @@ packages:
'@types/unist': 2.0.9
dev: false
/@types/hast@3.0.3:
resolution: {integrity: sha512-2fYGlaDy/qyLlhidX42wAH0KBi2TCjKMH8CHmBXgRlJ3Y+OXTiqsPQ6IWarZKwF1JoUcAJdPogv1d4b0COTpmQ==}
dependencies:
'@types/unist': 3.0.2
dev: false
/@types/hoist-non-react-statics@3.3.4:
resolution: {integrity: sha512-ZchYkbieA+7tnxwX/SCBySx9WwvWR8TaP5tb2jRAzwvLb/rWchGw3v0w3pqUbUvj0GCwW2Xz/AVPSk6kUGctXQ==}
dependencies:
@ -2393,7 +2402,6 @@ packages:
/@ungap/structured-clone@1.2.0:
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
dev: true
/@vitejs/plugin-react-swc@3.4.0(vite@4.5.0):
resolution: {integrity: sha512-m7UaA4Uvz82N/0EOVpZL4XsFIakRqrFKeSNxa1FBLSXGvWrWRBwmZb4qxk+ZIVAZcW3c3dn5YosomDgx62XWcQ==}
@ -3497,6 +3505,19 @@ packages:
web-namespaces: 2.0.1
dev: false
/hast-util-from-parse5@8.0.1:
resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==}
dependencies:
'@types/hast': 3.0.3
'@types/unist': 3.0.2
devlop: 1.1.0
hastscript: 8.0.0
property-information: 6.3.0
vfile: 6.0.1
vfile-location: 5.0.2
web-namespaces: 2.0.1
dev: false
/hast-util-is-element@2.1.3:
resolution: {integrity: sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==}
dependencies:
@ -3514,6 +3535,42 @@ packages:
'@types/hast': 2.3.7
dev: false
/hast-util-parse-selector@4.0.0:
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
dependencies:
'@types/hast': 3.0.3
dev: false
/hast-util-raw@9.0.1:
resolution: {integrity: sha512-5m1gmba658Q+lO5uqL5YNGQWeh1MYWZbZmWrM5lncdcuiXuo5E2HT/CIOp0rLF8ksfSwiCVJ3twlgVRyTGThGA==}
dependencies:
'@types/hast': 3.0.3
'@types/unist': 3.0.2
'@ungap/structured-clone': 1.2.0
hast-util-from-parse5: 8.0.1
hast-util-to-parse5: 8.0.0
html-void-elements: 3.0.0
mdast-util-to-hast: 13.1.0
parse5: 7.1.2
unist-util-position: 5.0.0
unist-util-visit: 5.0.0
vfile: 6.0.1
web-namespaces: 2.0.1
zwitch: 2.0.4
dev: false
/hast-util-to-parse5@8.0.0:
resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==}
dependencies:
'@types/hast': 3.0.3
comma-separated-tokens: 2.0.3
devlop: 1.1.0
property-information: 6.3.0
space-separated-tokens: 2.0.2
web-namespaces: 2.0.1
zwitch: 2.0.4
dev: false
/hast-util-to-text@3.1.2:
resolution: {integrity: sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==}
dependencies:
@ -3547,6 +3604,16 @@ packages:
space-separated-tokens: 2.0.2
dev: false
/hastscript@8.0.0:
resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==}
dependencies:
'@types/hast': 3.0.3
comma-separated-tokens: 2.0.3
hast-util-parse-selector: 4.0.0
property-information: 6.3.0
space-separated-tokens: 2.0.2
dev: false
/he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
@ -3582,6 +3649,10 @@ packages:
void-elements: 3.1.0
dev: false
/html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
dev: false
/i18next@23.6.0:
resolution: {integrity: sha512-z0Cxr0MGkt+kli306WS4nNNM++9cgt2b2VCMprY92j+AIab/oclgPxdwtTZVLP1zn5t5uo8M6uLsZmYrcjr3HA==}
dependencies:
@ -4062,6 +4133,20 @@ packages:
unist-util-visit: 4.1.2
dev: false
/mdast-util-to-hast@13.1.0:
resolution: {integrity: sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==}
dependencies:
'@types/hast': 3.0.3
'@types/mdast': 4.0.3
'@ungap/structured-clone': 1.2.0
devlop: 1.1.0
micromark-util-sanitize-uri: 2.0.0
trim-lines: 3.0.1
unist-util-position: 5.0.0
unist-util-visit: 5.0.0
vfile: 6.0.1
dev: false
/mdast-util-to-markdown@1.5.0:
resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==}
dependencies:
@ -4247,6 +4332,13 @@ packages:
micromark-util-types: 1.1.0
dev: false
/micromark-util-character@2.0.1:
resolution: {integrity: sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw==}
dependencies:
micromark-util-symbol: 2.0.0
micromark-util-types: 2.0.0
dev: false
/micromark-util-chunked@1.1.0:
resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==}
dependencies:
@ -4287,6 +4379,10 @@ packages:
resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==}
dev: false
/micromark-util-encode@2.0.0:
resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==}
dev: false
/micromark-util-html-tag-name@1.2.0:
resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==}
dev: false
@ -4311,6 +4407,14 @@ packages:
micromark-util-symbol: 1.1.0
dev: false
/micromark-util-sanitize-uri@2.0.0:
resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==}
dependencies:
micromark-util-character: 2.0.1
micromark-util-encode: 2.0.0
micromark-util-symbol: 2.0.0
dev: false
/micromark-util-subtokenize@1.1.0:
resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==}
dependencies:
@ -4324,10 +4428,18 @@ packages:
resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==}
dev: false
/micromark-util-symbol@2.0.0:
resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==}
dev: false
/micromark-util-types@1.1.0:
resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==}
dev: false
/micromark-util-types@2.0.0:
resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==}
dev: false
/micromark@3.2.0:
resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==}
dependencies:
@ -5098,6 +5210,14 @@ packages:
unist-util-visit: 4.1.2
dev: false
/rehype-raw@7.0.0:
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
dependencies:
'@types/hast': 3.0.3
hast-util-raw: 9.0.1
vfile: 6.0.1
dev: false
/relateurl@0.2.7:
resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==}
engines: {node: '>= 0.10'}
@ -5598,6 +5718,12 @@ packages:
'@types/unist': 2.0.9
dev: false
/unist-util-position@5.0.0:
resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
dependencies:
'@types/unist': 3.0.2
dev: false
/unist-util-remove-position@4.0.2:
resolution: {integrity: sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==}
dependencies:
@ -5639,6 +5765,14 @@ packages:
unist-util-visit-parents: 5.1.3
dev: false
/unist-util-visit@5.0.0:
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
dependencies:
'@types/unist': 3.0.2
unist-util-is: 6.0.0
unist-util-visit-parents: 6.0.1
dev: false
/universalify@2.0.0:
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
engines: {node: '>= 10.0.0'}
@ -5740,6 +5874,13 @@ packages:
vfile: 5.3.7
dev: false
/vfile-location@5.0.2:
resolution: {integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==}
dependencies:
'@types/unist': 3.0.2
vfile: 6.0.1
dev: false
/vfile-message@3.1.4:
resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==}
dependencies:

View File

@ -1,5 +1,6 @@
import axios from "axios";
import {
setAnnouncement,
setAppLogo,
setAppName,
setBlobEndpoint,
@ -11,6 +12,7 @@ export type SiteInfo = {
logo: string;
docs: string;
file: string;
announcement: string;
};
export async function getSiteInfo(): Promise<SiteInfo> {
@ -19,7 +21,7 @@ export async function getSiteInfo(): Promise<SiteInfo> {
return response.data as SiteInfo;
} catch (e) {
console.warn(e);
return { title: "", logo: "", docs: "", file: "" };
return { title: "", logo: "", docs: "", file: "", announcement: "" };
}
}
@ -29,5 +31,6 @@ export function syncSiteInfo() {
setAppLogo(info.logo);
setDocsUrl(info.docs);
setBlobEndpoint(info.file);
setAnnouncement(info.announcement);
});
}

View File

@ -5,6 +5,7 @@ import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import remarkBreaks from "remark-breaks";
import rehypeKatex from "rehype-katex";
import rehypeRaw from "rehype-raw";
import { parseFile } from "./plugins/file.tsx";
import "@/assets/markdown/all.less";
import { useEffect, useMemo } from "react";
@ -24,10 +25,12 @@ import { copyClipboard } from "@/utils/dom.ts";
import { useToast } from "./ui/use-toast.ts";
import { useTranslation } from "react-i18next";
import { parseProgressbar } from "@/components/plugins/progress.tsx";
import { cn } from "@/components/ui/lib/utils.ts";
type MarkdownProps = {
children: string;
className?: string;
acceptHtml?: boolean;
};
function doAction(dispatch: AppDispatch, url: string): boolean {
@ -69,7 +72,7 @@ function getSocialIcon(url: string) {
}
}
function MarkdownContent({ children, className }: MarkdownProps) {
function MarkdownContent({ children, className, acceptHtml }: MarkdownProps) {
const dispatch = useDispatch();
const { t } = useTranslation();
const { toast } = useToast();
@ -82,12 +85,18 @@ function MarkdownContent({ children, className }: MarkdownProps) {
});
}, [children]);
const rehypePlugins = useMemo(() => {
const plugins = [rehypeKatex];
return acceptHtml ? [...plugins, rehypeRaw] : plugins;
}, [acceptHtml]);
return (
<ReactMarkdown
remarkPlugins={[remarkMath, remarkGfm, remarkBreaks]}
rehypePlugins={[rehypeKatex]}
className={`markdown-body ${className}`}
remarkPlugins={[remarkMath, remarkGfm, remarkBreaks]} // @ts-ignore
rehypePlugins={rehypePlugins}
className={cn("markdown-body", className)}
children={children}
skipHtml={!acceptHtml}
components={{
a({ href, children }) {
const url: string = href?.toString() || "";
@ -151,10 +160,14 @@ function MarkdownContent({ children, className }: MarkdownProps) {
function Markdown(props: MarkdownProps) {
// memoize the component
const { children, className } = props;
const { children, className, acceptHtml } = props;
return useMemo(
() => <MarkdownContent className={className}>{children}</MarkdownContent>,
[props.children, props.className],
() => (
<MarkdownContent className={className} acceptHtml={acceptHtml}>
{children}
</MarkdownContent>
),
[props.children, props.className, props.acceptHtml],
);
}

View File

@ -0,0 +1,54 @@
import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { announcementEvent } from "@/events/announcement.ts";
import { Bell, Check } from "lucide-react";
import Markdown from "@/components/Markdown.tsx";
function Announcement() {
const { t } = useTranslation();
const [announcement, setAnnouncement] = useState<string>("");
useEffect(() => {
announcementEvent.bind((data: string) => setAnnouncement(data));
}, []);
return (
<AlertDialog
open={announcement !== ""}
onOpenChange={() => setAnnouncement("")}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle
className={"flex flex-row items-center select-none"}
>
<Bell className="inline-block w-4 h-4 mr-2" />
<p className={`translate-y-[-1px]`}>{t("announcement")}</p>
</AlertDialogTitle>
<AlertDialogDescription>
<Markdown acceptHtml={true}>{announcement}</Markdown>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("close")}</AlertDialogCancel>
<AlertDialogAction>
<Check className="w-4 h-4 mr-1" />
{t("i-know")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
export default Announcement;

View File

@ -19,6 +19,7 @@ import { Model } from "@/api/types.ts";
import { ChargeProps, nonBilling } from "@/admin/charge.ts";
import { dispatchSubscriptionData } from "@/store/globals.ts";
import { marketEvent } from "@/events/market.ts";
import Announcement from "@/components/app/Announcement.tsx";
function AppProvider() {
const dispatch = useDispatch();
@ -57,6 +58,7 @@ function AppProvider() {
return (
<>
<Announcement />
<Broadcast />
<NavBar />
<ThemeProvider />

View File

@ -1,4 +1,6 @@
import { updateDocumentTitle, updateFavicon } from "@/utils/dom.ts";
import { getMemory, setMemory } from "@/utils/memory.ts";
import { announcementEvent } from "@/events/announcement.ts";
export let appName =
localStorage.getItem("app_name") ||
@ -71,7 +73,7 @@ export function setAppName(name: string): void {
* set the app name in localStorage
*/
name = name.trim() || "Chat Nio";
localStorage.setItem("app_name", name);
setMemory("app_name", name);
appName = name;
updateDocumentTitle(name);
@ -82,7 +84,7 @@ export function setAppLogo(logo: string): void {
* set the app logo in localStorage
*/
logo = logo.trim() || "/favicon.ico";
localStorage.setItem("app_logo", logo);
setMemory("app_logo", logo);
appLogo = logo;
updateFavicon(logo);
@ -93,7 +95,7 @@ export function setDocsUrl(url: string): void {
* set the docs url in localStorage
*/
url = url.trim() || "https://docs.chatnio.net";
localStorage.setItem("docs_url", url);
setMemory("docs_url", url);
docsEndpoint = url;
}
@ -102,6 +104,16 @@ export function setBlobEndpoint(endpoint: string): void {
* set the blob endpoint in localStorage
*/
endpoint = endpoint.trim() || "https://blob.chatnio.net";
localStorage.setItem("blob_endpoint", endpoint);
setMemory("blob_endpoint", endpoint);
blobEndpoint = endpoint;
}
export function setAnnouncement(announcement: string): void {
/**
* set the announcement in localStorage
*/
if (getMemory("announcement") === announcement) return;
setMemory("announcement", announcement);
announcementEvent.emit(announcement);
}

View File

@ -0,0 +1,5 @@
import { EventCommitter } from "@/events/struct.ts";
export const announcementEvent = new EventCommitter<string>({
name: "announcement",
});

View File

@ -43,6 +43,8 @@
"upward": "上移",
"downward": "下移",
"save": "保存",
"announcement": "站点公告",
"i-know": "我已知晓",
"auth": {
"username": "用户名",
"username-placeholder": "请输入用户名",

View File

@ -550,5 +550,7 @@
"remove": "remove",
"upward": "Top",
"downward": "Move down",
"save": "Save"
"save": "Save",
"announcement": "Site Announcement",
"i-know": "Yes, I understand."
}

View File

@ -550,5 +550,7 @@
"remove": "追放",
"upward": "上へ移動",
"downward": "下へ移動",
"save": "保存"
"save": "保存",
"announcement": "サイトのお知らせ",
"i-know": "私は知っています"
}

View File

@ -550,5 +550,7 @@
"remove": "Убрать",
"upward": "Выше",
"downward": "Ниже",
"save": "Сохранить"
"save": "Сохранить",
"announcement": "Объявление о площадке",
"i-know": "Мне известно о"
}

View File

@ -404,6 +404,7 @@ function Site({ data, dispatch, onChange }: CompProps<SiteState>) {
<Label>{t("admin.system.announcement")}</Label>
<Textarea
value={data.announcement}
rows={12}
onChange={(e) =>
dispatch({
type: "update:site.announcement",