feat: add user search, direct messaging, and friend addition settings (#324)
* feat: add user search, direct messaging, and friend addition settings * chore: bump version to v0.9.71
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vocechat-web",
|
||||
"version": "0.9.70",
|
||||
"version": "0.9.71",
|
||||
"homepage": "https://voce.chat",
|
||||
"dependencies": {
|
||||
"@metamask/onboarding": "^1.0.1",
|
||||
|
||||
@@ -30,6 +30,12 @@
|
||||
"set_normal": "User",
|
||||
|
||||
"search_not_found": "Not found, or this user is not allowed to be searched.",
|
||||
"search_disabled": "User search has been disabled by the administrator.",
|
||||
"add_to_contact": "Add to contact",
|
||||
"add_friend_disabled": "Adding friends has been disabled by the administrator.",
|
||||
"dm_disabled": "Direct messaging has been disabled by the administrator.",
|
||||
"send_failed_blocked": "Send failed, blocked maybe",
|
||||
"send_failed": "Send message failed",
|
||||
"search_by_id": "Search by ID",
|
||||
"search_by_id_ph": "Input user ID",
|
||||
"search_by_email": "Search by email",
|
||||
|
||||
@@ -100,6 +100,18 @@
|
||||
"enable": "Enable",
|
||||
"disable": "Disable"
|
||||
},
|
||||
"dm_enable": {
|
||||
"title": "Direct Messaging",
|
||||
"desc": "If disabled, members cannot send direct messages to each other."
|
||||
},
|
||||
"add_friend_enable": {
|
||||
"title": "Add Friend",
|
||||
"desc": "If disabled, members cannot add other users as friends."
|
||||
},
|
||||
"search_user_enable": {
|
||||
"title": "Search Users",
|
||||
"desc": "If disabled, members cannot search for other users."
|
||||
},
|
||||
"enable_msg_url_preview": {
|
||||
"title": "Enable URL Preview in Message",
|
||||
"desc": "If enabled, url in message will show preview content.",
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
"set_normal": "普通用户",
|
||||
|
||||
"search_not_found": "用户不存在,或者不允许搜索",
|
||||
"search_disabled": "管理员已关闭用户搜索功能",
|
||||
"add_to_contact": "加为联系人",
|
||||
"add_friend_disabled": "管理员已关闭加好友功能",
|
||||
"dm_disabled": "管理员已关闭私聊功能",
|
||||
"send_failed_blocked": "发送失败,可能被对方屏蔽",
|
||||
"send_failed": "消息发送失败",
|
||||
"search_by_id": "用户ID",
|
||||
"search_by_id_ph": "请输入用户ID",
|
||||
"search_by_email": "用户邮箱",
|
||||
|
||||
@@ -98,6 +98,18 @@
|
||||
"enable": "开启",
|
||||
"disable": "关闭"
|
||||
},
|
||||
"dm_enable": {
|
||||
"title": "私聊(直接消息)",
|
||||
"desc": "关闭后,普通成员之间无法互发私信"
|
||||
},
|
||||
"add_friend_enable": {
|
||||
"title": "加好友",
|
||||
"desc": "关闭后,普通成员无法将其他用户添加为好友"
|
||||
},
|
||||
"search_user_enable": {
|
||||
"title": "搜索用户",
|
||||
"desc": "关闭后,普通成员无法搜索其他用户"
|
||||
},
|
||||
"enable_msg_url_preview": {
|
||||
"title": "开启消息网址预览",
|
||||
"desc": "如果开启,消息中的网址会显示预览内容",
|
||||
|
||||
@@ -133,7 +133,7 @@ const baseQueryWithTokenCheck = async (args: any, api: any, extraOptions: any) =
|
||||
break;
|
||||
case 403:
|
||||
{
|
||||
const whiteList403 = ["sendMsg", "login"];
|
||||
const whiteList403 = ["sendMsg", "login", "searchUser", "updateContactStatus"];
|
||||
if (!whiteList403.includes(api.endpoint)) {
|
||||
toast.error("Request Not Allowed");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import toast from "react-hot-toast";
|
||||
import { batch } from "react-redux";
|
||||
|
||||
import i18n from "@/i18n";
|
||||
import { ContentTypes } from "../config";
|
||||
import { addMessage, removeMessage } from "../slices/message";
|
||||
import { addChannelMsg, removeChannelMsg } from "../slices/message.channel";
|
||||
@@ -60,12 +61,18 @@ export const onMessageSendStarted = async (
|
||||
}, 300);
|
||||
// dispatch(removePendingMessage({ id, mid:ts, type: from }));
|
||||
} catch (error) {
|
||||
if (error?.error?.status == 403) {
|
||||
// 403 means blocked
|
||||
toast.error(`Send failed, blocked maybe`);
|
||||
// dispatch(updateMessage({ mid: ts, failed: true }));
|
||||
const httpStatus = error?.error?.originalStatus ?? error?.error?.status;
|
||||
const errData: string =
|
||||
typeof error?.error?.data === "string" ? error.error.data : "";
|
||||
if (httpStatus === 403) {
|
||||
if (errData.includes("disabled by the administrator")) {
|
||||
toast.error(i18n.t("dm_disabled", { ns: "member" }));
|
||||
} else {
|
||||
// 403 means blocked
|
||||
toast.error(i18n.t("send_failed_blocked", { ns: "member", defaultValue: "Send failed, blocked maybe" }));
|
||||
}
|
||||
} else {
|
||||
toast.error(`Send Message Failed ${JSON.stringify(error)}`);
|
||||
toast.error(i18n.t("send_failed", { ns: "member", defaultValue: "Send Message Failed" }));
|
||||
}
|
||||
dispatch(removeContextMessage({ id, mid: ts }));
|
||||
dispatch(removeMessage(ts));
|
||||
|
||||
@@ -29,7 +29,10 @@ const initialState: StoredServer = {
|
||||
loginConfig: null,
|
||||
ext_setting: null,
|
||||
msg_smtp_notify_delay_seconds: 0,
|
||||
msg_smtp_notify_enable: false
|
||||
msg_smtp_notify_enable: false,
|
||||
dm_enable: true,
|
||||
add_friend_enable: true,
|
||||
search_user_enable: true
|
||||
};
|
||||
|
||||
const serverSlice = createSlice({
|
||||
@@ -57,6 +60,9 @@ const serverSlice = createSlice({
|
||||
only_admin_can_create_group = false,
|
||||
loginConfig = state.loginConfig || null,
|
||||
ext_setting = null,
|
||||
dm_enable = true,
|
||||
add_friend_enable = true,
|
||||
search_user_enable = true,
|
||||
...rest
|
||||
} = action.payload || {};
|
||||
return {
|
||||
@@ -73,6 +79,9 @@ const serverSlice = createSlice({
|
||||
chat_layout_mode,
|
||||
loginConfig,
|
||||
ext_setting,
|
||||
dm_enable,
|
||||
add_friend_enable,
|
||||
search_user_enable,
|
||||
...rest
|
||||
};
|
||||
},
|
||||
|
||||
@@ -24,6 +24,11 @@ export default function AddEntriesMenu() {
|
||||
(store) => store.server.only_admin_can_create_group,
|
||||
shallowEqual
|
||||
);
|
||||
const dmEnable = useAppSelector((store) => store.server.dm_enable ?? true, shallowEqual);
|
||||
const searchUserEnable = useAppSelector(
|
||||
(store) => store.server.search_user_enable ?? true,
|
||||
shallowEqual
|
||||
);
|
||||
const [isPrivate, setIsPrivate] = useState(false);
|
||||
const [inviteModalVisible, setInviteModalVisible] = useState(false);
|
||||
const [searchModalVisible, setSearchModalVisible] = useState(false);
|
||||
@@ -67,6 +72,8 @@ export default function AddEntriesMenu() {
|
||||
const iconClass = "w-5 h-5 dark:fill-gray-300";
|
||||
const canPrivateGroup = onlyAdminCreateGroup ? isAdmin : true;
|
||||
const showInvite = isAdmin || !onlyAdminCanInvite;
|
||||
const showNewMsg = isAdmin || dmEnable;
|
||||
const showSearchPeople = isAdmin || searchUserEnable;
|
||||
return (
|
||||
<>
|
||||
<ul className="flex flex-col rounded-xl drop-shadow p-1 select-none text-gray-500 dark:text-gray-300 bg-white dark:bg-black">
|
||||
@@ -83,20 +90,24 @@ export default function AddEntriesMenu() {
|
||||
{t("action.new_private_channel")}
|
||||
</li>
|
||||
)}
|
||||
<li className={itemClass} onClick={toggleUsersModalVisible}>
|
||||
<IconMention className={iconClass} />
|
||||
{t("action.new_msg")}
|
||||
</li>
|
||||
{showNewMsg && (
|
||||
<li className={itemClass} onClick={toggleUsersModalVisible}>
|
||||
<IconMention className={iconClass} />
|
||||
{t("action.new_msg")}
|
||||
</li>
|
||||
)}
|
||||
{showInvite && (
|
||||
<li className={itemClass} onClick={toggleInviteModalVisible}>
|
||||
<IconInvite className={iconClass} />
|
||||
{t("action.invite_people")}
|
||||
</li>
|
||||
)}
|
||||
<li className={itemClass} onClick={toggleSearchModalVisible}>
|
||||
<IconSearch className={iconClass} />
|
||||
{t("action.search_people")}
|
||||
</li>
|
||||
{showSearchPeople && (
|
||||
<li className={itemClass} onClick={toggleSearchModalVisible}>
|
||||
<IconSearch className={iconClass} />
|
||||
{t("action.search_people")}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
{channelModalVisible && <ChannelModal personal={isPrivate} closeModal={handleCloseModal} />}
|
||||
{usersModalVisible && <UsersModal closeModal={toggleUsersModalVisible} />}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// import Tippy from "@tippyjs/react";
|
||||
import { ChangeEvent, FC, FormEvent, useRef, useState } from "react";
|
||||
import { ChangeEvent, FC, FormEvent, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import clsx from "clsx";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import BASE_URL from "@/app/config";
|
||||
import { useSearchUserMutation, useUpdateContactStatusMutation } from "@/app/services/user";
|
||||
@@ -21,8 +21,14 @@ type Props = {
|
||||
type Type = "id" | "email" | "name";
|
||||
|
||||
const SearchUser: FC<Props> = ({ closeModal }) => {
|
||||
const [updateContactStatus, { isLoading: adding }] = useUpdateContactStatusMutation();
|
||||
const [updateContactStatus, { isLoading: adding, error: addContactError }] =
|
||||
useUpdateContactStatusMutation();
|
||||
const usersData = useAppSelector((store) => store.users.byId, shallowEqual);
|
||||
const isAdmin = useAppSelector((store) => store.authData.user?.is_admin, shallowEqual);
|
||||
const addFriendEnable = useAppSelector(
|
||||
(store) => store.server.add_friend_enable ?? true,
|
||||
shallowEqual
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
const navigateTo = useNavigate();
|
||||
const inputRef = useRef(null);
|
||||
@@ -32,7 +38,32 @@ const SearchUser: FC<Props> = ({ closeModal }) => {
|
||||
email: "",
|
||||
name: ""
|
||||
});
|
||||
const [searchUser, { data, isSuccess, isLoading, reset }] = useSearchUserMutation();
|
||||
const [searchUser, { data, isSuccess, isLoading, isError, reset, error: searchError }] =
|
||||
useSearchUserMutation();
|
||||
const isSearchDisabled =
|
||||
!!searchError &&
|
||||
"data" in searchError &&
|
||||
typeof searchError.data === "string" &&
|
||||
(searchError.data as string).includes("disabled by the administrator") &&
|
||||
// text/plain 403 → status='PARSING_ERROR', originalStatus=403
|
||||
// json 403 → status=403
|
||||
(("originalStatus" in searchError && searchError.originalStatus === 403) ||
|
||||
("status" in searchError && searchError.status === 403));
|
||||
useEffect(() => {
|
||||
if (!isSearchDisabled) return;
|
||||
toast.error(t("search_disabled", { ns: "member" }));
|
||||
}, [isSearchDisabled]);
|
||||
useEffect(() => {
|
||||
if (!addContactError) return;
|
||||
const err = addContactError as any;
|
||||
const httpStatus: number = err?.originalStatus ?? err?.status;
|
||||
const errData: string = typeof err?.data === "string" ? err.data : "";
|
||||
toast.error(
|
||||
httpStatus === 403 && errData.includes("disabled by the administrator")
|
||||
? t("add_friend_disabled", { ns: "member" })
|
||||
: t("tip.update_failed", { ns: "common", defaultValue: "Operation failed" })
|
||||
);
|
||||
}, [addContactError]);
|
||||
const handleInput = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const tmp = {
|
||||
[type]: evt.target.value
|
||||
@@ -69,7 +100,8 @@ const SearchUser: FC<Props> = ({ closeModal }) => {
|
||||
const handleChat = async (directChat: boolean) => {
|
||||
if (!data) return;
|
||||
if (!directChat) {
|
||||
await updateContactStatus({ target_uid: data.uid, action: "add" });
|
||||
const result = await updateContactStatus({ target_uid: data.uid, action: "add" });
|
||||
if ("error" in result) return; // toast 由上方 useEffect 处理
|
||||
}
|
||||
closeModal();
|
||||
navigateTo(`/chat/dm/${data.uid}`);
|
||||
@@ -120,7 +152,18 @@ const SearchUser: FC<Props> = ({ closeModal }) => {
|
||||
/>
|
||||
</form>
|
||||
<div className="min-h-[280px] flex-center pb-10">
|
||||
{isSuccess ? (
|
||||
{isError ? (
|
||||
<div className="w-full h-full text-center flex flex-col gap-3 items-center">
|
||||
<span className="text-sm text-gray-800 dark:text-gray-200">
|
||||
{isSearchDisabled
|
||||
? t("search_disabled", { ns: "member" })
|
||||
: t("tip.error", { ns: "common", defaultValue: "Something went wrong" })}
|
||||
</span>
|
||||
<StyledButton className="mini" onClick={resetInput}>
|
||||
Ok
|
||||
</StyledButton>
|
||||
</div>
|
||||
) : isSuccess ? (
|
||||
data ? (
|
||||
<div className="flex flex-col items-center pt-10">
|
||||
<Avatar
|
||||
@@ -139,13 +182,13 @@ const SearchUser: FC<Props> = ({ closeModal }) => {
|
||||
<StyledButton className="mini ghost" onClick={handleSendMsg}>
|
||||
{t("send_msg", { ns: "member" })}
|
||||
</StyledButton>
|
||||
{!inContact && (
|
||||
{!inContact && (isAdmin || addFriendEnable) && (
|
||||
<StyledButton
|
||||
disabled={adding}
|
||||
onClick={handleChat.bind(null, inContact)}
|
||||
className={clsx("mini", inContact && "ghost")}
|
||||
>
|
||||
Add to contact
|
||||
{t("add_to_contact", { ns: "member" })}
|
||||
</StyledButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import toast from "react-hot-toast";
|
||||
import { shallowEqual } from "react-redux";
|
||||
|
||||
import IconAdd from "@/assets/icons/add.person.svg";
|
||||
import IconBlock from "@/assets/icons/block.svg";
|
||||
import { useUpdateContactStatusMutation } from "../../../app/services/user";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { ContactAction } from "../../../types/user";
|
||||
import { shallowEqual } from "react-redux";
|
||||
|
||||
type Props = {
|
||||
uid: number;
|
||||
@@ -13,12 +15,31 @@ type Props = {
|
||||
|
||||
const AddContactTip = (props: Props) => {
|
||||
const { t } = useTranslation("chat");
|
||||
const [updateContactStatus] = useUpdateContactStatusMutation();
|
||||
const { t: tMember } = useTranslation("member");
|
||||
const [updateContactStatus, { error: addContactError }] = useUpdateContactStatusMutation();
|
||||
const enableContact = useAppSelector(
|
||||
(store) => store.server.contact_verification_enable,
|
||||
shallowEqual
|
||||
);
|
||||
const isAdmin = useAppSelector((store) => store.authData.user?.is_admin, shallowEqual);
|
||||
const addFriendEnable = useAppSelector(
|
||||
(store) => store.server.add_friend_enable ?? true,
|
||||
shallowEqual
|
||||
);
|
||||
const targetUser = useAppSelector((store) => store.users.byId[props.uid], shallowEqual);
|
||||
|
||||
useEffect(() => {
|
||||
if (!addContactError) return;
|
||||
const err = addContactError as any;
|
||||
const httpStatus: number = err?.originalStatus ?? err?.status;
|
||||
const errData: string = typeof err?.data === "string" ? err.data : "";
|
||||
toast.error(
|
||||
httpStatus === 403 && errData.includes("disabled by the administrator")
|
||||
? tMember("add_friend_disabled")
|
||||
: tMember("tip.update_failed", { ns: "common", defaultValue: "Operation failed" })
|
||||
);
|
||||
}, [addContactError]);
|
||||
|
||||
const handleContactStatus = (action: ContactAction) => {
|
||||
updateContactStatus({ target_uid: props.uid, action });
|
||||
};
|
||||
@@ -32,7 +53,7 @@ const AddContactTip = (props: Props) => {
|
||||
{blocked ? t("contact_block_tip") : t("contact_tip")}
|
||||
</h3>
|
||||
<ul className="flex gap-4">
|
||||
{!blocked && (
|
||||
{!blocked && (isAdmin || addFriendEnable) && (
|
||||
<li className={itemClass} onClick={handleContactStatus.bind(null, "add")}>
|
||||
<IconAdd className="fill-primary-400" />
|
||||
<span>{t("add_contact")}</span>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { shallowEqual } from "react-redux";
|
||||
|
||||
import SettingBlock from "@/components/SettingBlock";
|
||||
import Toggle from "@/components/styled/Toggle";
|
||||
import { useGetSystemCommonQuery, useUpdateSystemCommonMutation } from "@/app/services/server";
|
||||
import { useAppSelector } from "@/app/store";
|
||||
|
||||
const AddFriendEnable = () => {
|
||||
const { t } = useTranslation("setting", { keyPrefix: "overview.add_friend_enable" });
|
||||
const { t: ct } = useTranslation();
|
||||
const { refetch } = useGetSystemCommonQuery();
|
||||
const addFriendEnable = useAppSelector(
|
||||
(store) => store.server.add_friend_enable ?? true,
|
||||
shallowEqual
|
||||
);
|
||||
const [updateSetting, { isSuccess }] = useUpdateSystemCommonMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
refetch();
|
||||
toast.success(ct("tip.update"));
|
||||
}
|
||||
}, [isSuccess]);
|
||||
|
||||
const handleToggle = () => {
|
||||
updateSetting({ add_friend_enable: !addFriendEnable });
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingBlock
|
||||
title={t("title")}
|
||||
desc={t("desc")}
|
||||
toggler={<Toggle onClick={handleToggle} checked={addFriendEnable} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddFriendEnable;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { shallowEqual } from "react-redux";
|
||||
|
||||
import SettingBlock from "@/components/SettingBlock";
|
||||
import Toggle from "@/components/styled/Toggle";
|
||||
import { useGetSystemCommonQuery, useUpdateSystemCommonMutation } from "@/app/services/server";
|
||||
import { useAppSelector } from "@/app/store";
|
||||
|
||||
const DMEnable = () => {
|
||||
const { t } = useTranslation("setting", { keyPrefix: "overview.dm_enable" });
|
||||
const { t: ct } = useTranslation();
|
||||
const { refetch } = useGetSystemCommonQuery();
|
||||
const dmEnable = useAppSelector((store) => store.server.dm_enable ?? true, shallowEqual);
|
||||
const [updateSetting, { isSuccess }] = useUpdateSystemCommonMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
refetch();
|
||||
toast.success(ct("tip.update"));
|
||||
}
|
||||
}, [isSuccess]);
|
||||
|
||||
const handleToggle = () => {
|
||||
updateSetting({ dm_enable: !dmEnable });
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingBlock
|
||||
title={t("title")}
|
||||
desc={t("desc")}
|
||||
toggler={<Toggle onClick={handleToggle} checked={dmEnable} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DMEnable;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { shallowEqual } from "react-redux";
|
||||
|
||||
import SettingBlock from "@/components/SettingBlock";
|
||||
import Toggle from "@/components/styled/Toggle";
|
||||
import { useGetSystemCommonQuery, useUpdateSystemCommonMutation } from "@/app/services/server";
|
||||
import { useAppSelector } from "@/app/store";
|
||||
|
||||
const SearchUserEnable = () => {
|
||||
const { t } = useTranslation("setting", { keyPrefix: "overview.search_user_enable" });
|
||||
const { t: ct } = useTranslation();
|
||||
const { refetch } = useGetSystemCommonQuery();
|
||||
const searchUserEnable = useAppSelector(
|
||||
(store) => store.server.search_user_enable ?? true,
|
||||
shallowEqual
|
||||
);
|
||||
const [updateSetting, { isSuccess }] = useUpdateSystemCommonMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
refetch();
|
||||
toast.success(ct("tip.update"));
|
||||
}
|
||||
}, [isSuccess]);
|
||||
|
||||
const handleToggle = () => {
|
||||
updateSetting({ search_user_enable: !searchUserEnable });
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingBlock
|
||||
title={t("title")}
|
||||
desc={t("desc")}
|
||||
toggler={<Toggle onClick={handleToggle} checked={searchUserEnable} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchUserEnable;
|
||||
@@ -20,6 +20,9 @@ import WhoCanSignUpSetting from "./WhoCanSignUpSetting";
|
||||
import UserMsgEmailNotify from "./UserMsgEmailNotify";
|
||||
import WhoCanInviteUsers from "./WhoCanInviteUsers";
|
||||
import WebClientAutoUpdate from "./WebClientAutoUpdate";
|
||||
import DMEnable from "./DMEnable";
|
||||
import AddFriendEnable from "./AddFriendEnable";
|
||||
import SearchUserEnable from "./SearchUserEnable";
|
||||
|
||||
export default function Overview() {
|
||||
const { t } = useTranslation("setting");
|
||||
@@ -45,6 +48,9 @@ export default function Overview() {
|
||||
|
||||
<EnableURLPreviewInMsg />
|
||||
<WhoCanInviteUsers />
|
||||
<DMEnable />
|
||||
<AddFriendEnable />
|
||||
<SearchUserEnable />
|
||||
<OnlyAdminCanSeeChannelMembers />
|
||||
{/* 访客模式 */}
|
||||
<GuestMode />
|
||||
|
||||
@@ -19,6 +19,9 @@ export interface SystemCommon {
|
||||
ext_setting: null | string;
|
||||
msg_smtp_notify_enable: boolean;
|
||||
msg_smtp_notify_delay_seconds: number;
|
||||
dm_enable?: boolean;
|
||||
add_friend_enable?: boolean;
|
||||
search_user_enable?: boolean;
|
||||
}
|
||||
export interface GithubAuthConfig {
|
||||
client_id?: string;
|
||||
|
||||
Reference in New Issue
Block a user