diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a36cf044..1d0faac2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -574,7 +574,7 @@ importers:
version: 0.32.11
zustand:
specifier: ^4.3.6
- version: 4.3.6(immer@9.0.21)(react@18.2.0)
+ version: 4.3.6(immer@9.0.15)(react@18.2.0)
devDependencies:
'@types/crc':
specifier: ^3.4.0
@@ -1556,8 +1556,8 @@ importers:
specifier: workspace:^
version: link:../packages/sdk
tushan:
- specifier: ^0.2.28
- version: 0.2.28(history@5.3.0)(prop-types@15.8.1)(react-hook-form@7.41.5)(ts-node@10.9.1)
+ specifier: ^0.2.32
+ version: 0.2.32(history@5.3.0)(prop-types@15.8.1)(react-hook-form@7.41.5)(ts-node@10.9.1)
vite-express:
specifier: 0.8.0
version: 0.8.0(patch_hash=u6touqej4dt3zxnslnszarl7vq)(express@4.18.2)(vite@4.2.0)
@@ -1875,7 +1875,7 @@ importers:
version: 5.3.6(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)
zustand:
specifier: ^4.3.6
- version: 4.3.6(immer@9.0.21)(react@18.2.0)
+ version: 4.3.6(immer@9.0.15)(react@18.2.0)
server/plugins/com.msgbyte.getui:
dependencies:
@@ -2053,7 +2053,7 @@ importers:
version: 5.3.6(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)
zustand:
specifier: ^4.3.6
- version: 4.3.6(immer@9.0.21)(react@18.2.0)
+ version: 4.3.6(immer@9.0.15)(react@18.2.0)
server/plugins/com.msgbyte.welcome:
dependencies:
@@ -21749,10 +21749,10 @@ packages:
/immer@9.0.15:
resolution: {integrity: sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==}
- dev: false
/immer@9.0.21:
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
+ dev: false
/import-fresh@2.0.0:
resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==}
@@ -34259,8 +34259,8 @@ packages:
domino: 2.1.6
dev: false
- /tushan@0.2.28(history@5.3.0)(prop-types@15.8.1)(react-hook-form@7.41.5)(ts-node@10.9.1):
- resolution: {integrity: sha512-8lYhCgOqvayRHkC6oT44WRzGWIjf0Eh565sjMLmyS5+9K10s/Wb0W1z8mPdx8N+PLDMhA2sBchc9Q3cLjJtA6g==}
+ /tushan@0.2.32(history@5.3.0)(prop-types@15.8.1)(react-hook-form@7.41.5)(ts-node@10.9.1):
+ resolution: {integrity: sha512-yTv1TTUrCSJqzaYxJ1T21pjbxlQnyiEx2bAjB4VDixhrDFMwRugBpy+bZ58imKq1NKv2SZLi8NQlIUW8x432Eg==}
dependencies:
'@arco-design/web-react': 2.49.2(@types/react@18.0.20)(react-dom@18.2.0)(react@18.2.0)
'@tanstack/react-query': 4.29.3(react-dom@18.2.0)(react@18.2.0)
@@ -36412,7 +36412,6 @@ packages:
immer: 9.0.15
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
- dev: false
/zustand@4.3.6(immer@9.0.21)(react@18.2.0):
resolution: {integrity: sha512-6J5zDxjxLE+yukC2XZWf/IyWVKnXT9b9HUv09VJ/bwGCpKNcaTqp7Ws28Xr8jnbvnZcdRaidztAPsXFBIqufiw==}
@@ -36429,6 +36428,7 @@ packages:
immer: 9.0.21
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
+ dev: false
/zwitch@1.0.5:
resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==}
diff --git a/server/admin/package.json b/server/admin/package.json
index 847934e3..d93ec36d 100644
--- a/server/admin/package.json
+++ b/server/admin/package.json
@@ -29,7 +29,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailchat-server-sdk": "workspace:^",
- "tushan": "^0.2.28",
+ "tushan": "^0.2.32",
"vite-express": "0.8.0"
},
"devDependencies": {
diff --git a/server/admin/src/client/App.tsx b/server/admin/src/client/App.tsx
index e07938c0..ef394dbb 100644
--- a/server/admin/src/client/App.tsx
+++ b/server/admin/src/client/App.tsx
@@ -11,6 +11,7 @@ import {
IconEmail,
IconFile,
IconMessage,
+ IconNotification,
IconSettings,
IconStorage,
IconUser,
@@ -27,6 +28,7 @@ import { CacheManager } from './routes/cache';
import { TailchatNetwork } from './routes/network';
import { SocketIOAdmin } from './routes/socketio';
import { SystemConfig } from './routes/system';
+import { SystemNotify } from './routes/system/notify';
const dataProvider = jsonServerProvider('/admin/api', httpClient);
@@ -111,6 +113,10 @@ function App() {
+ }>
+
+
+
}>
diff --git a/server/admin/src/client/i18n.ts b/server/admin/src/client/i18n.ts
index 759bbf3d..4b4e08a2 100644
--- a/server/admin/src/client/i18n.ts
+++ b/server/admin/src/client/i18n.ts
@@ -61,6 +61,18 @@ export const i18n: TushanContextProps['i18n'] = {
'Please be cautious in the production environment, clearing the cache may lead to increased pressure on the database in a short period of time',
cleanBtn: 'Clean Cache',
},
+ 'system-notify': {
+ create: 'Create System Notify',
+ title: 'Title',
+ content: 'Content',
+ scope: 'Notify Scope',
+ allUser: 'All User',
+ allUserTip:
+ 'All users excluding temporary users. Also, if there are many users, it may not be possible to notify all users at once',
+ specifiedUser: 'Specified User',
+ notifySuccess:
+ 'Sent successfully, sent to ${data.userIds.length} users',
+ },
},
},
},
@@ -153,6 +165,9 @@ export const i18n: TushanContextProps['i18n'] = {
cache: {
name: '缓存管理',
},
+ 'system-notify': {
+ name: '系统通知',
+ },
},
custom: {
action: {
@@ -204,6 +219,17 @@ export const i18n: TushanContextProps['i18n'] = {
'生产环境请谨慎操作, 清理缓存可能会导致短时间内数据库压力增加',
cleanBtn: '清理缓存',
},
+ 'system-notify': {
+ create: '创建系统通知',
+ title: '标题',
+ content: '内容',
+ scope: '通知范围',
+ allUser: '所有用户',
+ allUserTip:
+ '所有用户不包含临时用户。另外,如果用户很多,可能会无法立即通知所有用户',
+ specifiedUser: '指定用户',
+ notifySuccess: '发送成功,已发送给 ${count} 名用户',
+ },
},
},
},
diff --git a/server/admin/src/client/routes/system/notify.tsx b/server/admin/src/client/routes/system/notify.tsx
new file mode 100644
index 00000000..c1858d01
--- /dev/null
+++ b/server/admin/src/client/routes/system/notify.tsx
@@ -0,0 +1,141 @@
+import React from 'react';
+import {
+ Button,
+ Input,
+ Form,
+ useTranslation,
+ Typography,
+ Card,
+ Radio,
+ ReferenceFieldEdit,
+ useAsyncRequest,
+ Tooltip,
+ Message,
+} from 'tushan';
+import { IconExclamationCircle } from 'tushan/icon';
+import { MarkdownEditor } from '../../components/MarkdownEditor';
+import { request } from '../../request';
+
+/**
+ * Tailchat 系统通知
+ *
+ * 发送markdown格式的消息到指定用户的收件箱
+ */
+export const SystemNotify: React.FC = React.memo(() => {
+ const { t } = useTranslation();
+ const [form] = Form.useForm();
+ const scope: 'all' | 'specified' = Form.useWatch('scope', form);
+
+ const [{ loading }, handleSubmit] = useAsyncRequest(async (values) => {
+ const { data } = await request.post('/users/system/notify', {
+ scope: values.scope,
+ specifiedUser: values.specifiedUser,
+ title: values.title,
+ content: values.content,
+ });
+
+ Message.success(
+ t('custom.system-notify.notifySuccess', { count: data.userIds.length })
+ );
+ });
+
+ return (
+
+
+ {t('custom.system-notify.create')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('custom.system-notify.allUser')}
+
+
+
+
+
+
+ {t('custom.system-notify.specifiedUser')}
+
+
+
+
+ {scope === 'specified' && (
+
+
+
+ )}
+
+
+
+
+
+
+ );
+});
+SystemNotify.displayName = 'SystemNotify';
+
+export const MarkdownFormInput: React.FC<{
+ value?: string;
+ onChange?: (val: string) => void;
+}> = React.memo((props) => {
+ const value = props.value || '';
+
+ const handleChange = (newValue) => {
+ props.onChange && props.onChange(newValue);
+ };
+
+ return ;
+});
+MarkdownFormInput.displayName = 'MarkdownFormInput';
+
+export const UserSelectedFormInput: React.FC<{
+ value?: string;
+ onChange?: (val: string) => void;
+}> = React.memo((props) => {
+ const value = props.value || '';
+
+ const handleChange = (newValue) => {
+ props.onChange && props.onChange(newValue);
+ };
+
+ /**
+ * Wait for ReferenceMany
+ */
+ return (
+
+ );
+});
+UserSelectedFormInput.displayName = 'UserSelectedFormInput';
diff --git a/server/admin/src/server/router/api.ts b/server/admin/src/server/router/api.ts
index c276cd62..9bd43639 100644
--- a/server/admin/src/server/router/api.ts
+++ b/server/admin/src/server/router/api.ts
@@ -121,6 +121,40 @@ router.post('/user/unban', auth(), async (req, res) => {
ret,
});
});
+router.post('/users/system/notify', auth(), async (req, res) => {
+ const { scope, specifiedUser, title, content } = req.body;
+
+ let userIds = [];
+
+ if (scope === 'all') {
+ const users = await userModel.find(
+ {
+ // false 或 null(正式用户或者老的用户)
+ temporary: {
+ $ne: true,
+ },
+ },
+ {
+ _id: 1,
+ }
+ );
+
+ userIds = users.map((u) => u._id);
+ } else if (scope === 'specified') {
+ userIds = Array.isArray(specifiedUser) ? specifiedUser : [specifiedUser];
+ }
+
+ broker.call('chat.inbox.batchAppend', {
+ userIds,
+ type: 'markdown',
+ payload: {
+ title,
+ content,
+ },
+ });
+
+ res.json({ userIds });
+});
router.use(
'/users',
auth(),