Admin writes a markdown announcement in Site settings; logged-in users see it as a post-login modal rendered read-only. "Don't show again" stores the content in user settings — editing the text re-shows it to everyone. - setting.Provider.Announcement + announcement key (wired the dead Pro scaffold field) - UserSetting.dismissed_announcement + dismiss_announcement PATCH flag - GET /user/setting/announcement (empty when dismissed/unset) - AnnouncementDialog in the file-manager dialog host (lazy MDXEditor) - en-US + zh-CN locales; roadmap status Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>pull/3587/head
parent
1bbaddf832
commit
f7a90a5928
@ -0,0 +1,85 @@
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import { Box, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, Skeleton, useTheme } from "@mui/material";
|
||||
import { lazy, Suspense, useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getAnnouncement, sendUpdateUserSetting } from "../../../api/api.ts";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
|
||||
const MarkdownEditor = lazy(() => import("../../Viewers/MarkdownEditor/Editor.tsx"));
|
||||
|
||||
const Loading = () => (
|
||||
<Box>
|
||||
<Skeleton variant="text" width="100%" height={24} />
|
||||
<Skeleton variant="text" width="60%" height={24} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
// AnnouncementDialog fetches the current site announcement once on mount and
|
||||
// shows it as a post-login modal. Dismissing with "don't show again" records
|
||||
// the content server-side so the modal stays silent until the admin edits it.
|
||||
const AnnouncementDialog = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const dispatch = useAppDispatch();
|
||||
const [content, setContent] = useState<string | undefined>();
|
||||
const [dontShow, setDontShow] = useState(false);
|
||||
const [closing, setClosing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
dispatch(getAnnouncement())
|
||||
.then((res) => {
|
||||
if (mounted && res.content) {
|
||||
setContent(res.content);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const close = useCallback(() => {
|
||||
if (dontShow) {
|
||||
setClosing(true);
|
||||
dispatch(sendUpdateUserSetting({ dismiss_announcement: true }))
|
||||
.catch(() => {})
|
||||
.finally(() => setContent(undefined));
|
||||
} else {
|
||||
setContent(undefined);
|
||||
}
|
||||
}, [dispatch, dontShow]);
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open onClose={close} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{t("application:announcement.title")}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<MarkdownEditor
|
||||
displayOnly
|
||||
value={content}
|
||||
darkMode={theme.palette.mode === "dark"}
|
||||
readOnly={true}
|
||||
onChange={() => {}}
|
||||
initialValue={content}
|
||||
/>
|
||||
</Suspense>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ justifyContent: "space-between" }}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={dontShow} onChange={(e) => setDontShow(e.target.checked)} />}
|
||||
label={t("application:announcement.dontShowAgain")}
|
||||
/>
|
||||
<LoadingButton loading={closing} variant="contained" onClick={close}>
|
||||
{t("common:ok")}
|
||||
</LoadingButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnnouncementDialog;
|
||||
Loading…
Reference in new issue