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-router-dom": "^6.17.0",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"rehype-katex": "^6.0.3", "rehype-katex": "^6.0.3",
"rehype-raw": "^7.0.0",
"remark-breaks": "^4.0.0", "remark-breaks": "^4.0.0",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"remark-math": "^5.1.1", "remark-math": "^5.1.1",

143
app/pnpm-lock.yaml generated
View File

@ -128,6 +128,9 @@ dependencies:
rehype-katex: rehype-katex:
specifier: ^6.0.3 specifier: ^6.0.3
version: 6.0.3 version: 6.0.3
rehype-raw:
specifier: ^7.0.0
version: 7.0.0
remark-breaks: remark-breaks:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0 version: 4.0.0
@ -2150,6 +2153,12 @@ packages:
'@types/unist': 2.0.9 '@types/unist': 2.0.9
dev: false 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: /@types/hoist-non-react-statics@3.3.4:
resolution: {integrity: sha512-ZchYkbieA+7tnxwX/SCBySx9WwvWR8TaP5tb2jRAzwvLb/rWchGw3v0w3pqUbUvj0GCwW2Xz/AVPSk6kUGctXQ==} resolution: {integrity: sha512-ZchYkbieA+7tnxwX/SCBySx9WwvWR8TaP5tb2jRAzwvLb/rWchGw3v0w3pqUbUvj0GCwW2Xz/AVPSk6kUGctXQ==}
dependencies: dependencies:
@ -2393,7 +2402,6 @@ packages:
/@ungap/structured-clone@1.2.0: /@ungap/structured-clone@1.2.0:
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
dev: true
/@vitejs/plugin-react-swc@3.4.0(vite@4.5.0): /@vitejs/plugin-react-swc@3.4.0(vite@4.5.0):
resolution: {integrity: sha512-m7UaA4Uvz82N/0EOVpZL4XsFIakRqrFKeSNxa1FBLSXGvWrWRBwmZb4qxk+ZIVAZcW3c3dn5YosomDgx62XWcQ==} resolution: {integrity: sha512-m7UaA4Uvz82N/0EOVpZL4XsFIakRqrFKeSNxa1FBLSXGvWrWRBwmZb4qxk+ZIVAZcW3c3dn5YosomDgx62XWcQ==}
@ -3497,6 +3505,19 @@ packages:
web-namespaces: 2.0.1 web-namespaces: 2.0.1
dev: false 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: /hast-util-is-element@2.1.3:
resolution: {integrity: sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==} resolution: {integrity: sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==}
dependencies: dependencies:
@ -3514,6 +3535,42 @@ packages:
'@types/hast': 2.3.7 '@types/hast': 2.3.7
dev: false 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: /hast-util-to-text@3.1.2:
resolution: {integrity: sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==} resolution: {integrity: sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==}
dependencies: dependencies:
@ -3547,6 +3604,16 @@ packages:
space-separated-tokens: 2.0.2 space-separated-tokens: 2.0.2
dev: false 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: /he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true hasBin: true
@ -3582,6 +3649,10 @@ packages:
void-elements: 3.1.0 void-elements: 3.1.0
dev: false dev: false
/html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
dev: false
/i18next@23.6.0: /i18next@23.6.0:
resolution: {integrity: sha512-z0Cxr0MGkt+kli306WS4nNNM++9cgt2b2VCMprY92j+AIab/oclgPxdwtTZVLP1zn5t5uo8M6uLsZmYrcjr3HA==} resolution: {integrity: sha512-z0Cxr0MGkt+kli306WS4nNNM++9cgt2b2VCMprY92j+AIab/oclgPxdwtTZVLP1zn5t5uo8M6uLsZmYrcjr3HA==}
dependencies: dependencies:
@ -4062,6 +4133,20 @@ packages:
unist-util-visit: 4.1.2 unist-util-visit: 4.1.2
dev: false 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: /mdast-util-to-markdown@1.5.0:
resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==} resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==}
dependencies: dependencies:
@ -4247,6 +4332,13 @@ packages:
micromark-util-types: 1.1.0 micromark-util-types: 1.1.0
dev: false 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: /micromark-util-chunked@1.1.0:
resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==} resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==}
dependencies: dependencies:
@ -4287,6 +4379,10 @@ packages:
resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==}
dev: false dev: false
/micromark-util-encode@2.0.0:
resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==}
dev: false
/micromark-util-html-tag-name@1.2.0: /micromark-util-html-tag-name@1.2.0:
resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==}
dev: false dev: false
@ -4311,6 +4407,14 @@ packages:
micromark-util-symbol: 1.1.0 micromark-util-symbol: 1.1.0
dev: false 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: /micromark-util-subtokenize@1.1.0:
resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==} resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==}
dependencies: dependencies:
@ -4324,10 +4428,18 @@ packages:
resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==}
dev: false dev: false
/micromark-util-symbol@2.0.0:
resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==}
dev: false
/micromark-util-types@1.1.0: /micromark-util-types@1.1.0:
resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==}
dev: false dev: false
/micromark-util-types@2.0.0:
resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==}
dev: false
/micromark@3.2.0: /micromark@3.2.0:
resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==}
dependencies: dependencies:
@ -5098,6 +5210,14 @@ packages:
unist-util-visit: 4.1.2 unist-util-visit: 4.1.2
dev: false 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: /relateurl@0.2.7:
resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
@ -5598,6 +5718,12 @@ packages:
'@types/unist': 2.0.9 '@types/unist': 2.0.9
dev: false 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: /unist-util-remove-position@4.0.2:
resolution: {integrity: sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==} resolution: {integrity: sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==}
dependencies: dependencies:
@ -5639,6 +5765,14 @@ packages:
unist-util-visit-parents: 5.1.3 unist-util-visit-parents: 5.1.3
dev: false 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: /universalify@2.0.0:
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
@ -5740,6 +5874,13 @@ packages:
vfile: 5.3.7 vfile: 5.3.7
dev: false 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: /vfile-message@3.1.4:
resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==}
dependencies: dependencies:

View File

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

View File

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

View File

@ -1,4 +1,6 @@
import { updateDocumentTitle, updateFavicon } from "@/utils/dom.ts"; import { updateDocumentTitle, updateFavicon } from "@/utils/dom.ts";
import { getMemory, setMemory } from "@/utils/memory.ts";
import { announcementEvent } from "@/events/announcement.ts";
export let appName = export let appName =
localStorage.getItem("app_name") || localStorage.getItem("app_name") ||
@ -71,7 +73,7 @@ export function setAppName(name: string): void {
* set the app name in localStorage * set the app name in localStorage
*/ */
name = name.trim() || "Chat Nio"; name = name.trim() || "Chat Nio";
localStorage.setItem("app_name", name); setMemory("app_name", name);
appName = name; appName = name;
updateDocumentTitle(name); updateDocumentTitle(name);
@ -82,7 +84,7 @@ export function setAppLogo(logo: string): void {
* set the app logo in localStorage * set the app logo in localStorage
*/ */
logo = logo.trim() || "/favicon.ico"; logo = logo.trim() || "/favicon.ico";
localStorage.setItem("app_logo", logo); setMemory("app_logo", logo);
appLogo = logo; appLogo = logo;
updateFavicon(logo); updateFavicon(logo);
@ -93,7 +95,7 @@ export function setDocsUrl(url: string): void {
* set the docs url in localStorage * set the docs url in localStorage
*/ */
url = url.trim() || "https://docs.chatnio.net"; url = url.trim() || "https://docs.chatnio.net";
localStorage.setItem("docs_url", url); setMemory("docs_url", url);
docsEndpoint = url; docsEndpoint = url;
} }
@ -102,6 +104,16 @@ export function setBlobEndpoint(endpoint: string): void {
* set the blob endpoint in localStorage * set the blob endpoint in localStorage
*/ */
endpoint = endpoint.trim() || "https://blob.chatnio.net"; endpoint = endpoint.trim() || "https://blob.chatnio.net";
localStorage.setItem("blob_endpoint", endpoint); setMemory("blob_endpoint", endpoint);
blobEndpoint = 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": "上移", "upward": "上移",
"downward": "下移", "downward": "下移",
"save": "保存", "save": "保存",
"announcement": "站点公告",
"i-know": "我已知晓",
"auth": { "auth": {
"username": "用户名", "username": "用户名",
"username-placeholder": "请输入用户名", "username-placeholder": "请输入用户名",

View File

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

View File

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

View File

@ -550,5 +550,7 @@
"remove": "Убрать", "remove": "Убрать",
"upward": "Выше", "upward": "Выше",
"downward": "Ниже", "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> <Label>{t("admin.system.announcement")}</Label>
<Textarea <Textarea
value={data.announcement} value={data.announcement}
rows={12}
onChange={(e) => onChange={(e) =>
dispatch({ dispatch({
type: "update:site.announcement", type: "update:site.announcement",