diff --git a/api/system.go b/api/system.go index 9d67cbfa0..d5d87f380 100644 --- a/api/system.go +++ b/api/system.go @@ -10,6 +10,8 @@ type SystemStatus struct { // System settings // Allow sign up. AllowSignUp bool `json:"allowSignUp"` + // Disable public memos. + DisablePublicMemos bool `json:"disablePublicMemos"` // Additional style. AdditionalStyle string `json:"additionalStyle"` // Additional script. diff --git a/api/system_setting.go b/api/system_setting.go index 1e569256d..8571c14a9 100644 --- a/api/system_setting.go +++ b/api/system_setting.go @@ -17,6 +17,8 @@ const ( SystemSettingSecretSessionName SystemSettingName = "secretSessionName" // SystemSettingAllowSignUpName is the key type of allow signup setting. SystemSettingAllowSignUpName SystemSettingName = "allowSignUp" + // SystemSettingsDisablePublicMemos is the key type of disable public memos setting. + SystemSettingDisablePublicMemosName SystemSettingName = "disablePublicMemos" // SystemSettingAdditionalStyleName is the key type of additional style. SystemSettingAdditionalStyleName SystemSettingName = "additionalStyle" // SystemSettingAdditionalScriptName is the key type of additional script. @@ -51,6 +53,8 @@ func (key SystemSettingName) String() string { return "secretSessionName" case SystemSettingAllowSignUpName: return "allowSignUp" + case SystemSettingDisablePublicMemosName: + return "disablePublicMemos" case SystemSettingAdditionalStyleName: return "additionalStyle" case SystemSettingAdditionalScriptName: @@ -64,7 +68,8 @@ func (key SystemSettingName) String() string { } var ( - SystemSettingAllowSignUpValue = []bool{true, false} + SystemSettingAllowSignUpValue = []bool{true, false} + SystemSettingDisbalePublicMemosValue = []bool{true, false} ) type SystemSetting struct { @@ -100,6 +105,24 @@ func (upsert SystemSettingUpsert) Validate() error { if invalid { return fmt.Errorf("invalid system setting allow signup value") } + } else if upsert.Name == SystemSettingDisablePublicMemosName { + value := false + err := json.Unmarshal([]byte(upsert.Value), &value) + if err != nil { + return fmt.Errorf("failed to unmarshal system setting disable public memos value") + } + + invalid := true + for _, v := range SystemSettingDisbalePublicMemosValue { + if value == v { + invalid = false + break + } + } + + if invalid { + return fmt.Errorf("invalid system setting disable public memos value") + } } else if upsert.Name == SystemSettingAdditionalStyleName { value := "" err := json.Unmarshal([]byte(upsert.Value), &value) diff --git a/server/memo.go b/server/memo.go index 5a6e504a5..f0e7e6670 100644 --- a/server/memo.go +++ b/server/memo.go @@ -53,6 +53,25 @@ func (s *Server) registerMemoRoutes(g *echo.Group) { } } + // Find system settings + disablePublicMemosSystemSettingKey := api.SystemSettingDisablePublicMemosName + disablePublicMemosSystemSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{ + Name: &disablePublicMemosSystemSettingKey, + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err) + } + if disablePublicMemosSystemSetting != nil { + disablePublicMemosValue := false + err = json.Unmarshal([]byte(disablePublicMemosSystemSetting.Value), &disablePublicMemosValue) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err) + } + if disablePublicMemosValue { + memoCreate.Visibility = api.Private + } + } + memoCreate.CreatorID = userID memo, err := s.Store.CreateMemo(ctx, memoCreate) if err != nil { diff --git a/server/system.go b/server/system.go index 4850c50ce..6f25f2e5a 100644 --- a/server/system.go +++ b/server/system.go @@ -42,12 +42,13 @@ func (s *Server) registerSystemRoutes(g *echo.Group) { } systemStatus := api.SystemStatus{ - Host: hostUser, - Profile: *s.Profile, - DBSize: 0, - AllowSignUp: false, - AdditionalStyle: "", - AdditionalScript: "", + Host: hostUser, + Profile: *s.Profile, + DBSize: 0, + AllowSignUp: false, + DisablePublicMemos: false, + AdditionalStyle: "", + AdditionalScript: "", CustomizedProfile: api.CustomizedProfile{ Name: "memos", LogoURL: "", @@ -75,6 +76,8 @@ func (s *Server) registerSystemRoutes(g *echo.Group) { if systemSetting.Name == api.SystemSettingAllowSignUpName { systemStatus.AllowSignUp = value.(bool) + } else if systemSetting.Name == api.SystemSettingDisablePublicMemosName { + systemStatus.DisablePublicMemos = value.(bool) } else if systemSetting.Name == api.SystemSettingAdditionalStyleName { systemStatus.AdditionalStyle = value.(string) } else if systemSetting.Name == api.SystemSettingAdditionalScriptName { diff --git a/web/src/components/MemoEditor.tsx b/web/src/components/MemoEditor.tsx index 2095675d4..9ea8130b2 100644 --- a/web/src/components/MemoEditor.tsx +++ b/web/src/components/MemoEditor.tsx @@ -4,7 +4,15 @@ import { useTranslation } from "react-i18next"; import { getMatchedNodes } from "../labs/marked"; import { deleteMemoResource, upsertMemoResource } from "../helpers/api"; import { TAB_SPACE_WIDTH, UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "../helpers/consts"; -import { useEditorStore, useLocationStore, useMemoStore, useResourceStore, useTagStore, useUserStore } from "../store/module"; +import { + useEditorStore, + useGlobalStore, + useLocationStore, + useMemoStore, + useResourceStore, + useTagStore, + useUserStore, +} from "../store/module"; import * as storage from "../helpers/storage"; import Icon from "./Icon"; import toastHelper from "./Toast"; @@ -42,6 +50,9 @@ const MemoEditor = () => { const memoStore = useMemoStore(); const tagStore = useTagStore(); const resourceStore = useResourceStore(); + const { + state: { systemStatus }, + } = useGlobalStore(); const [state, setState] = useState({ fullscreen: false, @@ -63,6 +74,12 @@ const MemoEditor = () => { }; }); + useEffect(() => { + if (systemStatus.disablePublicMemos) { + editorStore.setMemoVisibility("PRIVATE"); + } + }, [systemStatus.disablePublicMemos]); + useEffect(() => { const { editingMemoIdCache } = storage.get(["editingMemoIdCache"]); if (editingMemoIdCache) { @@ -484,7 +501,9 @@ const MemoEditor = () => {
diff --git a/web/src/components/Settings/SystemSection.tsx b/web/src/components/Settings/SystemSection.tsx index 1c47bf026..41921d020 100644 --- a/web/src/components/Settings/SystemSection.tsx +++ b/web/src/components/Settings/SystemSection.tsx @@ -5,11 +5,14 @@ import { useGlobalStore } from "../../store/module"; import * as api from "../../helpers/api"; import toastHelper from "../Toast"; import showUpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog"; +import { useAppDispatch } from "../../store"; +import { setGlobalState } from "../../store/reducer/global"; import "@/less/settings/system-section.less"; interface State { dbSize: number; allowSignUp: boolean; + disablePublicMemos: boolean; additionalStyle: string; additionalScript: string; } @@ -32,8 +35,11 @@ const SystemSection = () => { allowSignUp: systemStatus.allowSignUp, additionalStyle: systemStatus.additionalStyle, additionalScript: systemStatus.additionalScript, + disablePublicMemos: systemStatus.disablePublicMemos, }); + const dispatch = useAppDispatch(); + useEffect(() => { globalStore.fetchSystemStatus(); }, []); @@ -44,6 +50,7 @@ const SystemSection = () => { allowSignUp: systemStatus.allowSignUp, additionalStyle: systemStatus.additionalStyle, additionalScript: systemStatus.additionalScript, + disablePublicMemos: systemStatus.disablePublicMemos, }); }, [systemStatus]); @@ -100,6 +107,19 @@ const SystemSection = () => { }); }; + const handleDisablePublicMemosChanged = async (value: boolean) => { + setState({ + ...state, + disablePublicMemos: value, + }); + // Update global store immediately as MemoEditor/Selector is dependent on this value. + dispatch(setGlobalState({ systemStatus: { ...systemStatus, disablePublicMemos: value } })); + await api.upsertSystemSetting({ + name: "disablePublicMemos", + value: JSON.stringify(value), + }); + }; + const handleSaveAdditionalScript = async () => { try { await api.upsertSystemSetting({ @@ -133,6 +153,10 @@ const SystemSection = () => { {t("setting.system-section.allow-user-signup")} handleAllowSignUpChanged(event.target.checked)} />
+
+ {t("setting.system-section.disable-public-memos")} + handleDisablePublicMemosChanged(event.target.checked)} /> +
{t("setting.system-section.additional-style")} diff --git a/web/src/components/common/Selector.tsx b/web/src/components/common/Selector.tsx index c9aa14334..c15f5512a 100644 --- a/web/src/components/common/Selector.tsx +++ b/web/src/components/common/Selector.tsx @@ -2,6 +2,7 @@ import { memo, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import useToggle from "../../hooks/useToggle"; import Icon from "../Icon"; +import { Tooltip } from "@mui/joy"; import "../../less/common/selector.less"; interface SelectorItem { @@ -14,6 +15,8 @@ interface Props { value: string; dataSource: SelectorItem[]; handleValueChanged?: (value: string) => void; + disabled?: boolean; + tooltipTitle?: string; } const nullItem = { @@ -22,7 +25,7 @@ const nullItem = { }; const Selector: React.FC = (props: Props) => { - const { className, dataSource, handleValueChanged, value } = props; + const { className, dataSource, handleValueChanged, value, disabled, tooltipTitle } = props; const { t } = useTranslation(); const [showSelector, toggleSelectorStatus] = useToggle(false); @@ -58,39 +61,52 @@ const Selector: React.FC = (props: Props) => { }; const handleCurrentValueClick = (event: React.MouseEvent) => { + if (disabled) return; event.stopPropagation(); toggleSelectorStatus(); }; return ( -
-
- {currentItem.text} - - - -
+ ); }; diff --git a/web/src/less/common/selector.less b/web/src/less/common/selector.less index 182c59dfa..0e72106e5 100644 --- a/web/src/less/common/selector.less +++ b/web/src/less/common/selector.less @@ -14,6 +14,10 @@ width: calc(100% - 20px); } + > .lock-text { + @apply flex flex-row justify-center items-center w-4 shrink-0 mr-1; + } + > .arrow-text { @apply flex flex-row justify-center items-center w-4 shrink-0; @@ -41,4 +45,11 @@ @apply px-3 py-1 text-sm text-gray-600; } } + + > .selector-disabled { + @apply cursor-not-allowed; + @apply pointer-events-none; + + @apply bg-gray-200 dark:bg-zinc-700 dark:border-zinc-600 text-gray-400; + } } diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 717499466..498b39ef8 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -101,7 +101,8 @@ "visibility": { "private": "Only visible to you", "protected": "Visible to members", - "public": "Everyone can see" + "public": "Everyone can see", + "disabled": "Public memos are disabled" } }, "memo-list": { @@ -180,6 +181,7 @@ }, "database-file-size": "Database File Size", "allow-user-signup": "Allow user signup", + "disable-public-memos": "Disable public memos", "additional-style": "Additional style", "additional-script": "Additional script", "additional-style-placeholder": "Additional CSS codes", diff --git a/web/src/store/module/global.ts b/web/src/store/module/global.ts index 5cf1b148f..f69ecc8c1 100644 --- a/web/src/store/module/global.ts +++ b/web/src/store/module/global.ts @@ -9,6 +9,7 @@ export const initialGlobalState = async () => { appearance: "system" as Appearance, systemStatus: { allowSignUp: false, + disablePublicMemos: false, additionalStyle: "", additionalScript: "", customizedProfile: { diff --git a/web/src/store/reducer/global.ts b/web/src/store/reducer/global.ts index 6965e1305..e03c5636f 100644 --- a/web/src/store/reducer/global.ts +++ b/web/src/store/reducer/global.ts @@ -19,6 +19,7 @@ const globalSlice = createSlice({ }, dbSize: 0, allowSignUp: false, + disablePublicMemos: false, additionalStyle: "", additionalScript: "", customizedProfile: { diff --git a/web/src/types/modules/system.d.ts b/web/src/types/modules/system.d.ts index 55c1d3431..c04d2357b 100644 --- a/web/src/types/modules/system.d.ts +++ b/web/src/types/modules/system.d.ts @@ -18,6 +18,7 @@ interface SystemStatus { dbSize: number; // System settings allowSignUp: boolean; + disablePublicMemos: boolean; additionalStyle: string; additionalScript: string; customizedProfile: CustomizedProfile;