feat: more setting of ext

This commit is contained in:
Tristan Yang
2025-02-10 17:15:16 +08:00
parent 82761ee73e
commit 0972c87842
13 changed files with 87 additions and 14 deletions
+1
View File
@@ -42,6 +42,7 @@
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^3.0.2",
"linkify-plugin-mention": "^4.2.0", "linkify-plugin-mention": "^4.2.0",
"linkify-react": "^4.2.0", "linkify-react": "^4.2.0",
"linkify-string": "^4.2.0",
"linkifyjs": "^4.2.0", "linkifyjs": "^4.2.0",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"localforage-setitems": "^1.4.0", "localforage-setitems": "^1.4.0",
+12
View File
@@ -125,6 +125,9 @@ importers:
linkify-react: linkify-react:
specifier: ^4.2.0 specifier: ^4.2.0
version: 4.2.0(linkifyjs@4.2.0)(react@19.0.0) version: 4.2.0(linkifyjs@4.2.0)(react@19.0.0)
linkify-string:
specifier: ^4.2.0
version: 4.2.0(linkifyjs@4.2.0)
linkifyjs: linkifyjs:
specifier: ^4.2.0 specifier: ^4.2.0
version: 4.2.0 version: 4.2.0
@@ -3909,6 +3912,11 @@ packages:
linkifyjs: ^4.0.0 linkifyjs: ^4.0.0
react: '>= 15.0.0' react: '>= 15.0.0'
linkify-string@4.2.0:
resolution: {integrity: sha512-LqOKk0+RlqibFkxjPAGOL7Mfssqj6/SdG9QWGvzeVDpoWXhaw9OXxseCtFanjIl7C6UyTTZizhyGr4IWLfijiw==}
peerDependencies:
linkifyjs: ^4.0.0
linkifyjs@4.2.0: linkifyjs@4.2.0:
resolution: {integrity: sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw==} resolution: {integrity: sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw==}
@@ -10109,6 +10117,10 @@ snapshots:
linkifyjs: 4.2.0 linkifyjs: 4.2.0
react: 19.0.0 react: 19.0.0
linkify-string@4.2.0(linkifyjs@4.2.0):
dependencies:
linkifyjs: 4.2.0
linkifyjs@4.2.0: {} linkifyjs@4.2.0: {}
loader-runner@4.3.0: {} loader-runner@4.3.0: {}
+7 -1
View File
@@ -33,7 +33,7 @@
"id": "Channel ID", "id": "Channel ID",
"name": "Channel Name", "name": "Channel Name",
"topic": "Channel Topic", "topic": "Channel Topic",
"topic_placeholder": "Let everyone know how to use this channel.", "topic_placeholder": "Let everyone know how to use this channel.(markdown supported)",
"visibility": "Channel Visibility", "visibility": "Channel Visibility",
"public": "Public", "public": "Public",
"private": "Private", "private": "Private",
@@ -86,6 +86,12 @@
"enable": "Enable", "enable": "Enable",
"disable": "Disable" "disable": "Disable"
}, },
"admin_only_can_invite": {
"title": "Only Admins Can Invite Users",
"desc": "If enabled, only admins can invite new members.",
"enable": "Enable",
"disable": "Disable"
},
"enable_msg_url_preview": { "enable_msg_url_preview": {
"title": "Enable URL Preview in Message", "title": "Enable URL Preview in Message",
"desc": "If enabled, url in message will show preview content.", "desc": "If enabled, url in message will show preview content.",
+1 -1
View File
@@ -32,7 +32,7 @@
"delete_desc": "Are you sure want to delete this channel?", "delete_desc": "Are you sure want to delete this channel?",
"name": "Channel Name", "name": "Channel Name",
"topic": "Channel Topic", "topic": "Channel Topic",
"topic_placeholder": "Let everyone know how to use this channel.", "topic_placeholder": "Let everyone know how to use this channel.(markdown supported)",
"visibility": "Channel Visibility", "visibility": "Channel Visibility",
"public": "Public", "public": "Public",
"private": "Private" "private": "Private"
+7 -1
View File
@@ -33,7 +33,7 @@
"id": "频道 ID", "id": "频道 ID",
"name": "频道名", "name": "频道名",
"topic": "频道主题", "topic": "频道主题",
"topic_placeholder": "让别人知道这个频道是用来做啥的", "topic_placeholder": "让别人知道这个频道是用来做啥的(支持 markdown",
"visibility": "可见性", "visibility": "可见性",
"public": "公开", "public": "公开",
"private": "私密", "private": "私密",
@@ -85,6 +85,12 @@
"enable": "开启", "enable": "开启",
"disable": "关闭" "disable": "关闭"
}, },
"admin_only_can_invite": {
"title": "只有管理员可以邀请新用户",
"desc": "如果开启,只有管理员才能邀请新用户",
"enable": "开启",
"disable": "关闭"
},
"enable_msg_url_preview": { "enable_msg_url_preview": {
"title": "开启消息网址预览", "title": "开启消息网址预览",
"desc": "如果开启,消息中的网址会显示预览内容", "desc": "如果开启,消息中的网址会显示预览内容",
+1
View File
@@ -208,6 +208,7 @@ export const getEmailNotifyDelayList = () => [
value: 12 * 60 * 60, value: 12 * 60 * 60,
}, },
]; ];
export const KEY_ADMIN_ONLY_INVITE = `admin_only_can_invite`;
export const KEY_ADMIN_SEE_CHANNEL_MEMBERS = `only_admin_can_see_channel_members`; export const KEY_ADMIN_SEE_CHANNEL_MEMBERS = `only_admin_can_see_channel_members`;
export const KEY_MSG_URL_PREVIEW = `enable_msg_url_preview`; export const KEY_MSG_URL_PREVIEW = `enable_msg_url_preview`;
export default BASE_URL; export default BASE_URL;
+11 -4
View File
@@ -12,9 +12,13 @@ import InviteModal from "./InviteModal";
import SearchUser from "./SearchUser"; import SearchUser from "./SearchUser";
import UsersModal from "./UsersModal"; import UsersModal from "./UsersModal";
import { shallowEqual } from "react-redux"; import { shallowEqual } from "react-redux";
import useServerExtSetting from "../hooks/useServerExtSetting";
import { KEY_ADMIN_ONLY_INVITE } from "../app/config";
export default function AddEntriesMenu() { export default function AddEntriesMenu() {
const { t } = useTranslation(); const { t } = useTranslation();
const { getExtSetting } = useServerExtSetting();
const onlyAdminCanInvite = getExtSetting(KEY_ADMIN_ONLY_INVITE);
const isAdmin = useAppSelector((store) => store.authData.user?.is_admin, shallowEqual); const isAdmin = useAppSelector((store) => store.authData.user?.is_admin, shallowEqual);
const onlyAdminCreateGroup = useAppSelector( const onlyAdminCreateGroup = useAppSelector(
(store) => store.server.only_admin_can_create_group, (store) => store.server.only_admin_can_create_group,
@@ -62,6 +66,7 @@ export default function AddEntriesMenu() {
"rounded flex items-center gap-2 text-sm font-semibold cursor-pointer px-2 py-2.5 md:hover:bg-gray-800/20 md:dark:hover:bg-gray-200/20"; "rounded flex items-center gap-2 text-sm font-semibold cursor-pointer px-2 py-2.5 md:hover:bg-gray-800/20 md:dark:hover:bg-gray-200/20";
const iconClass = "w-5 h-5 dark:fill-gray-300"; const iconClass = "w-5 h-5 dark:fill-gray-300";
const canPrivateGroup = onlyAdminCreateGroup ? isAdmin : true; const canPrivateGroup = onlyAdminCreateGroup ? isAdmin : true;
const showInvite = isAdmin || !onlyAdminCanInvite;
return ( 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"> <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">
@@ -82,10 +87,12 @@ export default function AddEntriesMenu() {
<IconMention className={iconClass} /> <IconMention className={iconClass} />
{t("action.new_msg")} {t("action.new_msg")}
</li> </li>
<li className={itemClass} onClick={toggleInviteModalVisible}> {showInvite && (
<IconInvite className={iconClass} /> <li className={itemClass} onClick={toggleInviteModalVisible}>
{t("action.invite_people")} <IconInvite className={iconClass} />
</li> {t("action.invite_people")}
</li>
)}
<li className={itemClass} onClick={toggleSearchModalVisible}> <li className={itemClass} onClick={toggleSearchModalVisible}>
<IconSearch className={iconClass} /> <IconSearch className={iconClass} />
{t("action.search_people")} {t("action.search_people")}
+4 -2
View File
@@ -16,9 +16,10 @@ import { isDarkMode } from "../utils";
import ImagePreview from "./ImagePreview"; import ImagePreview from "./ImagePreview";
interface IProps { interface IProps {
cleanMode?: boolean;
content: string; content: string;
} }
const MarkdownRender: FC<IProps> = ({ content }) => { const MarkdownRender: FC<IProps> = ({ content, cleanMode = false }) => {
const mdContainer = useRef<HTMLDivElement | null>(null); const mdContainer = useRef<HTMLDivElement | null>(null);
useEffect(() => { useEffect(() => {
if (mdContainer.current) { if (mdContainer.current) {
@@ -28,7 +29,8 @@ const MarkdownRender: FC<IProps> = ({ content }) => {
}); });
} }
}, [mdContainer]); }, [mdContainer]);
if (cleanMode)
return <Viewer initialValue={content} theme={isDarkMode() ? "dark" : "light"}></Viewer>;
return ( return (
<> <>
<ImagePreview container={mdContainer.current} context="markdown" /> <ImagePreview container={mdContainer.current} context="markdown" />
+4 -4
View File
@@ -1,15 +1,15 @@
import { FC } from "react";
import { useAppSelector } from "@/app/store"; import { useAppSelector } from "@/app/store";
import { useGetServerQuery } from "@/app/services/server"; import { useGetServerQuery } from "@/app/services/server";
import { shallowEqual } from "react-redux"; import { shallowEqual } from "react-redux";
type Props = {}; const Meta = () => {
const Meta: FC<Props> = () => {
useGetServerQuery(); useGetServerQuery();
const { name, logo } = useAppSelector((store) => store.server, shallowEqual); const { name, logo } = useAppSelector((store) => store.server, shallowEqual);
console.log({name});
if(!name) return null
return ( return (
<> <>
{name && <title>{name} Web App</title>} {name && <title>{`${name} Web App`}</title>}
{logo && <link rel="icon" href={`${logo}?ts=${new Date().getTime()}`} />} {logo && <link rel="icon" href={`${logo}?ts=${new Date().getTime()}`} />}
</> </>
); );
@@ -6,6 +6,7 @@ import clsx from "clsx";
import { useAppSelector } from "@/app/store"; import { useAppSelector } from "@/app/store";
import EditIcon from "@/assets/icons/edit.svg"; import EditIcon from "@/assets/icons/edit.svg";
import { shallowEqual } from "react-redux"; import { shallowEqual } from "react-redux";
import linkifyStr from "linkify-string";
type ChannelHeaderProps = { type ChannelHeaderProps = {
cid: number; cid: number;
@@ -15,12 +16,18 @@ const ChannelHeader = ({ cid }: ChannelHeaderProps) => {
const { t } = useTranslation("chat"); const { t } = useTranslation("chat");
const isAdmin = useAppSelector((store) => store.authData.user?.is_admin, shallowEqual); const isAdmin = useAppSelector((store) => store.authData.user?.is_admin, shallowEqual);
const data = useAppSelector((store) => store.channels.byId[cid], shallowEqual); const data = useAppSelector((store) => store.channels.byId[cid], shallowEqual);
const desc = data.description
? linkifyStr(data.description, { target: "_blank" })
: t("welcome_desc", { name: data?.name });
return ( return (
<div className="pt-14 px-1 md:px-0 flex flex-col items-start gap-2"> <div className="pt-14 px-1 md:px-0 flex flex-col items-start gap-2">
<h2 className="font-bold text-4xl dark:text-white"> <h2 className="font-bold text-4xl dark:text-white">
{t("welcome_channel", { name: data?.name })} {t("welcome_channel", { name: data?.name })}
</h2> </h2>
<p className="text-gray-600 dark:text-gray-300">{t("welcome_desc", { name: data?.name })} </p> <p
className="text-gray-600 dark:text-gray-300 [&>a]:text-blue-500 [&>a]:hover:underline"
dangerouslySetInnerHTML={{ __html: desc }}
/>
{isAdmin && ( {isAdmin && (
<NavLink <NavLink
to={`/setting/channel/${cid}/overview?f=${pathname}`} to={`/setting/channel/${cid}/overview?f=${pathname}`}
@@ -0,0 +1,28 @@
import SettingBlock from "@/components/SettingBlock";
import { useTranslation } from "react-i18next";
import ServerVersionChecker from "@/components/ServerVersionChecker";
import { KEY_ADMIN_ONLY_INVITE } from "@/app/config";
import useServerExtSetting from "@/hooks/useServerExtSetting";
import Toggle from "@/components/styled/Toggle";
type Props = {};
const WhoCanInviteUsers = ({}: Props) => {
const { updateExtSetting, getExtSetting } = useServerExtSetting();
const { t } = useTranslation("setting");
const adminOnlyCanInvite = getExtSetting(KEY_ADMIN_ONLY_INVITE);
const handleToggle = () => {
updateExtSetting(KEY_ADMIN_ONLY_INVITE, !adminOnlyCanInvite);
};
return (
<ServerVersionChecker empty version="0.3.50">
<SettingBlock
title={t("overview.admin_only_can_invite.title")}
desc={t("overview.admin_only_can_invite.desc")}
toggler={<Toggle onClick={handleToggle} checked={adminOnlyCanInvite} />}
></SettingBlock>
</ServerVersionChecker>
);
};
export default WhoCanInviteUsers;
+2
View File
@@ -18,6 +18,7 @@ import ServerMsgNotify from "./ServerMsgEmailNotify";
import GuestMode from "./GuestMode"; import GuestMode from "./GuestMode";
import WhoCanSignUpSetting from "./WhoCanSignUpSetting"; import WhoCanSignUpSetting from "./WhoCanSignUpSetting";
import UserMsgEmailNotify from "./UserMsgEmailNotify"; import UserMsgEmailNotify from "./UserMsgEmailNotify";
import WhoCanInviteUsers from "./WhoCanInviteUsers";
export default function Overview() { export default function Overview() {
const { t } = useTranslation("setting"); const { t } = useTranslation("setting");
@@ -42,6 +43,7 @@ export default function Overview() {
{/* 只有 admin 能创建群组 */} {/* 只有 admin 能创建群组 */}
<EnableURLPreviewInMsg /> <EnableURLPreviewInMsg />
<WhoCanInviteUsers />
<OnlyAdminCanSeeChannelMembers /> <OnlyAdminCanSeeChannelMembers />
{/* 访客模式 */} {/* 访客模式 */}
<GuestMode /> <GuestMode />
+1
View File
@@ -15,6 +15,7 @@ export interface SystemCommon {
chat_layout_mode?: ChatLayout; chat_layout_mode?: ChatLayout;
max_file_expiry_mode?: MessageExpireMode; max_file_expiry_mode?: MessageExpireMode;
only_admin_can_create_group: boolean; only_admin_can_create_group: boolean;
who_can_invite_users: boolean;
ext_setting: null | string; ext_setting: null | string;
msg_smtp_notify_enable: boolean; msg_smtp_notify_enable: boolean;
msg_smtp_notify_delay_seconds: number; msg_smtp_notify_delay_seconds: number;