mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: display memo with updated ts (#1760)
This commit is contained in:
@ -34,6 +34,7 @@ type MemoResponse struct {
|
||||
UpdatedTs int64 `json:"updatedTs"`
|
||||
|
||||
// Domain specific fields
|
||||
DisplayTs int64 `json:"displayTs"`
|
||||
Content string `json:"content"`
|
||||
Visibility Visibility `json:"visibility"`
|
||||
Pinned bool `json:"pinned"`
|
||||
|
@ -24,6 +24,8 @@ type SystemStatus struct {
|
||||
CustomizedProfile CustomizedProfile `json:"customizedProfile"`
|
||||
// Storage service ID.
|
||||
StorageServiceID int `json:"storageServiceId"`
|
||||
// Local storage path
|
||||
// Local storage path.
|
||||
LocalStoragePath string `json:"localStoragePath"`
|
||||
// Memo display with updated timestamp.
|
||||
MemoDisplayWithUpdatedTs bool `json:"memoDisplayWithUpdatedTs"`
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ const (
|
||||
SystemSettingOpenAIConfigName SystemSettingName = "openai-config"
|
||||
// SystemSettingTelegramRobotToken is the name of Telegram Robot Token.
|
||||
SystemSettingTelegramRobotTokenName SystemSettingName = "telegram-robot-token"
|
||||
SystemSettingMemoDisplayWithUpdatedTsName SystemSettingName = "memo-display-with-updated-ts"
|
||||
)
|
||||
|
||||
// CustomizedProfile is the struct definition for SystemSettingCustomizedProfileName system setting item.
|
||||
@ -88,6 +89,8 @@ func (key SystemSettingName) String() string {
|
||||
return "openai-config"
|
||||
case SystemSettingTelegramRobotTokenName:
|
||||
return "telegram-robot-token"
|
||||
case SystemSettingMemoDisplayWithUpdatedTsName:
|
||||
return "memo-display-with-updated-ts"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@ -111,43 +114,36 @@ func (upsert SystemSettingUpsert) Validate() error {
|
||||
switch settingName := upsert.Name; settingName {
|
||||
case SystemSettingServerIDName:
|
||||
return fmt.Errorf("updating %v is not allowed", settingName)
|
||||
|
||||
case SystemSettingAllowSignUpName:
|
||||
var value bool
|
||||
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||
}
|
||||
|
||||
case SystemSettingIgnoreUpgradeName:
|
||||
var value bool
|
||||
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||
}
|
||||
|
||||
case SystemSettingDisablePublicMemosName:
|
||||
var value bool
|
||||
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||
}
|
||||
|
||||
case SystemSettingMaxUploadSizeMiBName:
|
||||
var value int
|
||||
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||
}
|
||||
|
||||
case SystemSettingAdditionalStyleName:
|
||||
var value string
|
||||
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||
}
|
||||
|
||||
case SystemSettingAdditionalScriptName:
|
||||
var value string
|
||||
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||
}
|
||||
|
||||
case SystemSettingCustomizedProfileName:
|
||||
customizedProfile := CustomizedProfile{
|
||||
Name: "memos",
|
||||
@ -157,7 +153,6 @@ func (upsert SystemSettingUpsert) Validate() error {
|
||||
Appearance: "system",
|
||||
ExternalURL: "",
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(upsert.Value), &customizedProfile); err != nil {
|
||||
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||
}
|
||||
@ -167,26 +162,22 @@ func (upsert SystemSettingUpsert) Validate() error {
|
||||
if !slices.Contains(UserSettingAppearanceValue, customizedProfile.Appearance) {
|
||||
return fmt.Errorf(`invalid appearance value for system setting "%v"`, settingName)
|
||||
}
|
||||
|
||||
case SystemSettingStorageServiceIDName:
|
||||
value := DatabaseStorage
|
||||
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||
}
|
||||
return nil
|
||||
|
||||
case SystemSettingLocalStoragePathName:
|
||||
value := ""
|
||||
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||
}
|
||||
|
||||
case SystemSettingOpenAIConfigName:
|
||||
value := OpenAIConfig{}
|
||||
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||
}
|
||||
|
||||
case SystemSettingTelegramRobotTokenName:
|
||||
if upsert.Value == "" {
|
||||
return nil
|
||||
@ -195,11 +186,14 @@ func (upsert SystemSettingUpsert) Validate() error {
|
||||
if len(fragments) != 2 {
|
||||
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||
}
|
||||
|
||||
case SystemSettingMemoDisplayWithUpdatedTsName:
|
||||
var value bool
|
||||
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
|
||||
return fmt.Errorf(systemSettingUnmarshalError, settingName)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid system setting name")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -609,19 +609,39 @@ func (s *Server) composeMemoMessageToMemoResponse(ctx context.Context, memoMessa
|
||||
Pinned: memoMessage.Pinned,
|
||||
}
|
||||
|
||||
// Compose creator name.
|
||||
user, err := s.Store.FindUser(ctx, &api.UserFind{
|
||||
ID: &memoResponse.CreatorID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.Nickname != "" {
|
||||
memoResponse.CreatorName = user.Nickname
|
||||
} else {
|
||||
memoResponse.CreatorName = user.Username
|
||||
}
|
||||
|
||||
// Compose display ts.
|
||||
memoResponse.DisplayTs = memoResponse.CreatedTs
|
||||
// Find memo display with updated ts setting.
|
||||
memoDisplayWithUpdatedTsSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{
|
||||
Name: api.SystemSettingMemoDisplayWithUpdatedTsName,
|
||||
})
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return nil, errors.Wrap(err, "failed to find system setting")
|
||||
}
|
||||
if memoDisplayWithUpdatedTsSetting != nil {
|
||||
memoDisplayWithUpdatedTs := false
|
||||
err = json.Unmarshal([]byte(memoDisplayWithUpdatedTsSetting.Value), &memoDisplayWithUpdatedTs)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal system setting value")
|
||||
}
|
||||
if memoDisplayWithUpdatedTs {
|
||||
memoResponse.DisplayTs = memoResponse.UpdatedTs
|
||||
}
|
||||
}
|
||||
|
||||
relationList := []*api.MemoRelation{}
|
||||
for _, relation := range memoMessage.RelationList {
|
||||
relationList = append(relationList, convertMemoRelationMessageToMemoRelation(relation))
|
||||
|
@ -57,6 +57,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
||||
},
|
||||
StorageServiceID: api.DatabaseStorage,
|
||||
LocalStoragePath: "assets/{timestamp}_{filename}",
|
||||
MemoDisplayWithUpdatedTs: false,
|
||||
}
|
||||
|
||||
systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
|
||||
@ -78,35 +79,28 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
||||
switch systemSetting.Name {
|
||||
case api.SystemSettingAllowSignUpName:
|
||||
systemStatus.AllowSignUp = baseValue.(bool)
|
||||
|
||||
case api.SystemSettingIgnoreUpgradeName:
|
||||
systemStatus.IgnoreUpgrade = baseValue.(bool)
|
||||
|
||||
case api.SystemSettingDisablePublicMemosName:
|
||||
systemStatus.DisablePublicMemos = baseValue.(bool)
|
||||
|
||||
case api.SystemSettingMaxUploadSizeMiBName:
|
||||
systemStatus.MaxUploadSizeMiB = int(baseValue.(float64))
|
||||
|
||||
case api.SystemSettingAdditionalStyleName:
|
||||
systemStatus.AdditionalStyle = baseValue.(string)
|
||||
|
||||
case api.SystemSettingAdditionalScriptName:
|
||||
systemStatus.AdditionalScript = baseValue.(string)
|
||||
|
||||
case api.SystemSettingCustomizedProfileName:
|
||||
customizedProfile := api.CustomizedProfile{}
|
||||
if err := json.Unmarshal([]byte(systemSetting.Value), &customizedProfile); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting customized profile value").SetInternal(err)
|
||||
}
|
||||
systemStatus.CustomizedProfile = customizedProfile
|
||||
|
||||
case api.SystemSettingStorageServiceIDName:
|
||||
systemStatus.StorageServiceID = int(baseValue.(float64))
|
||||
|
||||
case api.SystemSettingLocalStoragePathName:
|
||||
systemStatus.LocalStoragePath = baseValue.(string)
|
||||
|
||||
case api.SystemSettingMemoDisplayWithUpdatedTsName:
|
||||
systemStatus.MemoDisplayWithUpdatedTs = baseValue.(bool)
|
||||
default:
|
||||
log.Warn("Unknown system setting name", zap.String("setting name", systemSetting.Name.String()))
|
||||
}
|
||||
|
@ -9,12 +9,12 @@ interface Props {
|
||||
|
||||
const DailyMemo: React.FC<Props> = (props: Props) => {
|
||||
const { memo } = props;
|
||||
const createdTimeStr = getTimeString(memo.createdTs);
|
||||
const displayTimeStr = getTimeString(memo.displayTs);
|
||||
|
||||
return (
|
||||
<div className="daily-memo-wrapper">
|
||||
<div className="time-wrapper">
|
||||
<span className="normal-text">{createdTimeStr}</span>
|
||||
<span className="normal-text">{displayTimeStr}</span>
|
||||
</div>
|
||||
<div className="memo-container">
|
||||
<MemoContent content={memo.content} showFull={true} />
|
||||
|
@ -33,7 +33,7 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
const userStore = useUserStore();
|
||||
const memoStore = useMemoStore();
|
||||
const memoCacheStore = useMemoCacheStore();
|
||||
const [createdTimeStr, setCreatedTimeStr] = useState<string>(getRelativeTimeString(memo.createdTs));
|
||||
const [createdTimeStr, setCreatedTimeStr] = useState<string>(getRelativeTimeString(memo.displayTs));
|
||||
const [relatedMemoList, setRelatedMemoList] = useState<Memo[]>([]);
|
||||
const memoContainerRef = useRef<HTMLDivElement>(null);
|
||||
const isVisitorMode = userStore.isVisitorMode() || readonly;
|
||||
@ -54,9 +54,9 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
let intervalFlag: any = -1;
|
||||
if (Date.now() - memo.createdTs < 1000 * 60 * 60 * 24) {
|
||||
if (Date.now() - memo.displayTs < 1000 * 60 * 60 * 24) {
|
||||
intervalFlag = setInterval(() => {
|
||||
setCreatedTimeStr(getRelativeTimeString(memo.createdTs));
|
||||
setCreatedTimeStr(getRelativeTimeString(memo.displayTs));
|
||||
}, 1000 * 1);
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,7 @@ const MemoList = () => {
|
||||
if (
|
||||
duration &&
|
||||
duration.from < duration.to &&
|
||||
(getTimeStampByDate(memo.createdTs) < duration.from || getTimeStampByDate(memo.createdTs) > duration.to)
|
||||
(getTimeStampByDate(memo.displayTs) < duration.from || getTimeStampByDate(memo.displayTs) > duration.to)
|
||||
) {
|
||||
shouldShow = false;
|
||||
}
|
||||
@ -82,7 +82,7 @@ const MemoList = () => {
|
||||
const pinnedMemos = shownMemos.filter((m) => m.pinned);
|
||||
const unpinnedMemos = shownMemos.filter((m) => !m.pinned);
|
||||
const memoSort = (mi: Memo, mj: Memo) => {
|
||||
return mj.createdTs - mi.createdTs;
|
||||
return mj.displayTs - mi.displayTs;
|
||||
};
|
||||
pinnedMemos.sort(memoSort);
|
||||
unpinnedMemos.sort(memoSort);
|
||||
@ -168,7 +168,7 @@ const MemoList = () => {
|
||||
return (
|
||||
<div className="memo-list-container">
|
||||
{sortedMemos.map((memo) => (
|
||||
<Memo key={`${memo.id}-${memo.createdTs}`} memo={memo} />
|
||||
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} />
|
||||
))}
|
||||
{isFetching ? (
|
||||
<div className="status-text-container fetching-tip">
|
||||
|
@ -17,6 +17,7 @@ interface State {
|
||||
additionalStyle: string;
|
||||
additionalScript: string;
|
||||
maxUploadSizeMiB: number;
|
||||
memoDisplayWithUpdatedTs: boolean;
|
||||
}
|
||||
|
||||
const SystemSection = () => {
|
||||
@ -31,6 +32,7 @@ const SystemSection = () => {
|
||||
additionalScript: systemStatus.additionalScript,
|
||||
disablePublicMemos: systemStatus.disablePublicMemos,
|
||||
maxUploadSizeMiB: systemStatus.maxUploadSizeMiB,
|
||||
memoDisplayWithUpdatedTs: systemStatus.memoDisplayWithUpdatedTs,
|
||||
});
|
||||
const [telegramRobotToken, setTelegramRobotToken] = useState<string>("");
|
||||
const [openAIConfig, setOpenAIConfig] = useState<OpenAIConfig>({
|
||||
@ -65,6 +67,7 @@ const SystemSection = () => {
|
||||
additionalScript: systemStatus.additionalScript,
|
||||
disablePublicMemos: systemStatus.disablePublicMemos,
|
||||
maxUploadSizeMiB: systemStatus.maxUploadSizeMiB,
|
||||
memoDisplayWithUpdatedTs: systemStatus.memoDisplayWithUpdatedTs,
|
||||
});
|
||||
}, [systemStatus]);
|
||||
|
||||
@ -202,6 +205,18 @@ const SystemSection = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleMemoDisplayWithUpdatedTs = async (value: boolean) => {
|
||||
setState({
|
||||
...state,
|
||||
memoDisplayWithUpdatedTs: value,
|
||||
});
|
||||
globalStore.setSystemStatus({ disablePublicMemos: value });
|
||||
await api.upsertSystemSetting({
|
||||
name: "memo-display-with-updated-ts",
|
||||
value: JSON.stringify(value),
|
||||
});
|
||||
};
|
||||
|
||||
const handleMaxUploadSizeChanged = async (event: React.FocusEvent<HTMLInputElement>) => {
|
||||
// fixes cursor skipping position on mobile
|
||||
event.target.selectionEnd = event.target.value.length;
|
||||
@ -254,6 +269,10 @@ const SystemSection = () => {
|
||||
<span className="normal-text">{t("setting.system-section.disable-public-memos")}</span>
|
||||
<Switch checked={state.disablePublicMemos} onChange={(event) => handleDisablePublicMemosChanged(event.target.checked)} />
|
||||
</div>
|
||||
<div className="form-label">
|
||||
<span className="normal-text">Display with updated time</span>
|
||||
<Switch checked={state.memoDisplayWithUpdatedTs} onChange={(event) => handleMemoDisplayWithUpdatedTs(event.target.checked)} />
|
||||
</div>
|
||||
<div className="form-label">
|
||||
<div className="flex flex-row items-center">
|
||||
<span className="text-sm mr-1">{t("setting.system-section.max-upload-size")}</span>
|
||||
|
@ -46,7 +46,7 @@ const ShareMemoDialog: React.FC<Props> = (props: Props) => {
|
||||
const memoElRef = useRef<HTMLDivElement>(null);
|
||||
const memo = {
|
||||
...propsMemo,
|
||||
createdAtStr: getDateTimeString(propsMemo.createdTs),
|
||||
displayTsStr: getDateTimeString(propsMemo.displayTs),
|
||||
};
|
||||
const createdDays = Math.ceil((Date.now() - getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24);
|
||||
|
||||
@ -174,7 +174,7 @@ const ShareMemoDialog: React.FC<Props> = (props: Props) => {
|
||||
className="w-full h-auto select-none relative flex flex-col justify-start items-start bg-white dark:bg-zinc-800"
|
||||
ref={memoElRef}
|
||||
>
|
||||
<span className="w-full px-6 pt-5 pb-2 text-sm text-gray-500">{memo.createdAtStr}</span>
|
||||
<span className="w-full px-6 pt-5 pb-2 text-sm text-gray-500">{memo.displayTsStr}</span>
|
||||
<div className="w-full px-6 text-base pb-4">
|
||||
<MemoContent content={memo.content} showFull={true} />
|
||||
<MemoResourceListView className="!grid-cols-2" resourceList={memo.resourceList} />
|
||||
|
@ -203,9 +203,9 @@ export const checkShouldShowMemo = (memo: Memo, filter: Filter) => {
|
||||
}
|
||||
} else if (type === "DISPLAY_TIME") {
|
||||
if (operator === "BEFORE") {
|
||||
return memo.createdTs < getUnixTimeMillis(value);
|
||||
return memo.displayTs < getUnixTimeMillis(value);
|
||||
} else {
|
||||
return memo.createdTs >= getUnixTimeMillis(value);
|
||||
return memo.displayTs >= getUnixTimeMillis(value);
|
||||
}
|
||||
} else if (type === "VISIBILITY") {
|
||||
let matched = memo.visibility === value;
|
||||
|
@ -28,15 +28,15 @@ const DailyReview = () => {
|
||||
const currentDate = new Date(currentDateStamp);
|
||||
const dailyMemos = memos
|
||||
.filter((m) => {
|
||||
const createdTimestamp = getTimeStampByDate(m.createdTs);
|
||||
const displayTimestamp = getTimeStampByDate(m.displayTs);
|
||||
const currentDateStampWithOffset = currentDateStamp + convertToMillis(localSetting);
|
||||
return (
|
||||
m.rowStatus === "NORMAL" &&
|
||||
createdTimestamp >= currentDateStampWithOffset &&
|
||||
createdTimestamp < currentDateStampWithOffset + DAILY_TIMESTAMP
|
||||
displayTimestamp >= currentDateStampWithOffset &&
|
||||
displayTimestamp < currentDateStampWithOffset + DAILY_TIMESTAMP
|
||||
);
|
||||
})
|
||||
.sort((a, b) => getTimeStampByDate(a.createdTs) - getTimeStampByDate(b.createdTs));
|
||||
.sort((a, b) => getTimeStampByDate(a.displayTs) - getTimeStampByDate(b.displayTs));
|
||||
|
||||
useEffect(() => {
|
||||
let offset = 0;
|
||||
@ -46,7 +46,7 @@ const DailyReview = () => {
|
||||
offset += fetchedMemos.length;
|
||||
if (fetchedMemos.length === DEFAULT_MEMO_LIMIT) {
|
||||
const lastMemo = last(fetchedMemos);
|
||||
if (lastMemo && lastMemo.createdTs > currentDateStamp) {
|
||||
if (lastMemo && lastMemo.displayTs > currentDateStamp) {
|
||||
await fetchMoreMemos();
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ const EmbedMemo = () => {
|
||||
<main className="w-full max-w-lg mx-auto my-auto shadow px-4 py-4 rounded-lg">
|
||||
<div className="w-full flex flex-col justify-start items-start">
|
||||
<div className="w-full mb-2 flex flex-row justify-start items-center text-sm text-gray-400 dark:text-gray-300">
|
||||
<span>{getDateTimeString(state.memo.createdTs)}</span>
|
||||
<span>{getDateTimeString(state.memo.displayTs)}</span>
|
||||
<a className="ml-2 hover:underline hover:text-green-600" href={`/u/${state.memo.creatorId}`}>
|
||||
@{state.memo.creatorName}
|
||||
</a>
|
||||
|
@ -90,7 +90,7 @@ const Explore = () => {
|
||||
<main className="relative w-full h-auto flex flex-col justify-start items-start -mt-2">
|
||||
<MemoFilter />
|
||||
{sortedMemos.map((memo) => {
|
||||
return <Memo key={`${memo.id}-${memo.createdTs}`} memo={memo} readonly={true} />;
|
||||
return <Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} readonly={true} />;
|
||||
})}
|
||||
{isComplete ? (
|
||||
state.memos.length === 0 ? (
|
||||
|
@ -16,6 +16,7 @@ export const initialGlobalState = async () => {
|
||||
maxUploadSizeMiB: 0,
|
||||
additionalStyle: "",
|
||||
additionalScript: "",
|
||||
memoDisplayWithUpdatedTs: false,
|
||||
customizedProfile: {
|
||||
name: "memos",
|
||||
logoUrl: "/logo.webp",
|
||||
|
@ -11,6 +11,7 @@ export const convertResponseModelMemo = (memo: Memo): Memo => {
|
||||
...memo,
|
||||
createdTs: memo.createdTs * 1000,
|
||||
updatedTs: memo.updatedTs * 1000,
|
||||
displayTs: memo.displayTs * 1000,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -23,6 +23,7 @@ const globalSlice = createSlice({
|
||||
disablePublicMemos: false,
|
||||
additionalStyle: "",
|
||||
additionalScript: "",
|
||||
memoDisplayWithUpdatedTs: false,
|
||||
customizedProfile: {
|
||||
name: "memos",
|
||||
logoUrl: "/logo.webp",
|
||||
|
1
web/src/types/modules/memo.d.ts
vendored
1
web/src/types/modules/memo.d.ts
vendored
@ -10,6 +10,7 @@ interface Memo {
|
||||
updatedTs: TimeStamp;
|
||||
rowStatus: RowStatus;
|
||||
|
||||
displayTs: TimeStamp;
|
||||
content: string;
|
||||
visibility: Visibility;
|
||||
pinned: boolean;
|
||||
|
1
web/src/types/modules/system.d.ts
vendored
1
web/src/types/modules/system.d.ts
vendored
@ -31,6 +31,7 @@ interface SystemStatus {
|
||||
customizedProfile: CustomizedProfile;
|
||||
storageServiceId: number;
|
||||
localStoragePath: string;
|
||||
memoDisplayWithUpdatedTs: boolean;
|
||||
}
|
||||
|
||||
interface SystemSetting {
|
||||
|
Reference in New Issue
Block a user