This commit is contained in:
2026-04-25 16:36:34 +08:00
commit db90e7579b
1876 changed files with 189777 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
const path = require('path');
module.exports = {
externalDeps: [
'react',
'styled-components',
'zustand',
'zustand/middleware/immer',
],
pluginRoot: path.resolve(__dirname, './web'),
outDir: path.resolve(__dirname, '../../public'),
};

View File

@@ -0,0 +1,68 @@
import { db } from 'tailchat-server-sdk';
const { getModelForClass, prop, TimeStamps, modelOptions } = db;
import type { Types } from 'mongoose';
import { nanoid } from 'nanoid';
class GroupTopicComment extends TimeStamps {
@prop({
default: () => nanoid(8),
})
id: string;
@prop()
content: string;
@prop()
author: string;
/**
* 回复他人评论的id
*/
@prop()
replyCommentId?: string;
}
@modelOptions({
options: {
customName: 'p_topic',
},
})
export class GroupTopic extends TimeStamps implements db.Base {
_id: Types.ObjectId;
id: string;
@prop()
content: string;
@prop()
author: string;
@prop()
groupId: string;
/**
* 会话面板id
*/
@prop()
panelId: string;
@prop({
type: () => GroupTopicComment,
default: [],
})
comments: GroupTopicComment[];
/**
* 话题的其他数据
*/
@prop()
meta?: object;
}
export type GroupTopicDocument = db.DocumentType<GroupTopic>;
const model = getModelForClass(GroupTopic);
export type GroupTopicModel = typeof model;
export default model;

View File

@@ -0,0 +1,25 @@
{
"name": "tailchat-plugin-topic",
"version": "1.0.0",
"main": "index.js",
"author": "moonrailgun",
"description": "为群组提供话题功能",
"license": "MIT",
"private": true,
"scripts": {
"build:web": "ministar buildPlugin all",
"build:web:watch": "ministar watchPlugin all"
},
"devDependencies": {
"@types/react": "18.0.20",
"less": "^4.1.3",
"mini-star": "*",
"rollup-plugin-inject-process-env": "^1.3.1",
"rollup-plugin-less": "^1.1.3"
},
"dependencies": {
"lodash": "^4.17.21",
"nanoid": "^3.1.23",
"tailchat-server-sdk": "*"
}
}

View File

@@ -0,0 +1,251 @@
import _ from 'lodash';
import { TcService, TcDbService, TcContext, call } from 'tailchat-server-sdk';
import type { GroupTopicDocument, GroupTopicModel } from '../models/topic';
/**
* 群组话题
*/
interface GroupTopicService
extends TcService,
TcDbService<GroupTopicDocument, GroupTopicModel> {}
class GroupTopicService extends TcService {
get serviceName(): string {
return 'plugin:com.msgbyte.topic';
}
onInit(): void {
this.registerLocalDb(require('../models/topic').default);
this.registerAction('list', this.list, {
params: {
groupId: 'string',
panelId: 'string',
page: { type: 'number', optional: true },
size: { type: 'number', optional: true },
},
});
this.registerAction('create', this.create, {
params: {
groupId: 'string',
panelId: 'string',
content: 'string',
meta: { type: 'object', optional: true },
},
});
this.registerAction('createComment', this.createComment, {
params: {
groupId: 'string',
panelId: 'string',
topicId: 'string',
content: 'string',
replyCommentId: { type: 'string', optional: true },
},
});
this.registerAction('delete', this.delete, {
params: {
groupId: 'string',
panelId: 'string',
topicId: 'string',
},
});
}
protected onInited(): void {
this.setPanelFeature('com.msgbyte.topic/grouppanel', ['subscribe']);
}
/**
* 获取所有Topic
*/
async list(
ctx: TcContext<{
groupId: string;
panelId: string;
page?: number;
size?: number;
}>
) {
const { groupId, panelId, page = 1, size = 20 } = ctx.params;
const userId = ctx.meta.userId;
const t = ctx.meta.t;
// 鉴权
const group = await call(ctx).getGroupInfo(groupId);
const isMember = group.members.some((member) => {
return String(member.userId) === userId;
});
if (!isMember) {
throw new Error(t('不是该群组成员'));
}
const topic = await this.adapter.model
.find({
groupId,
panelId,
})
.limit(size)
.skip((page - 1) * 20)
.sort({ _id: 'desc' })
.exec();
const json = await this.transformDocuments(ctx, {}, topic);
return json;
}
/**
* 创建一条Topic
*/
async create(
ctx: TcContext<{
groupId: string;
panelId: string;
content: string;
meta?: object;
}>
) {
const { groupId, panelId, content, meta } = ctx.params;
const userId = ctx.meta.userId;
const t = ctx.meta.t;
// 鉴权
const group = await call(ctx).getGroupInfo(groupId);
const isMember = group.members.some((member) => member.userId === userId);
if (!isMember) {
throw new Error(t('不是该群组成员'));
}
const targetPanel = group.panels.find((p) => p.id === panelId);
if (!targetPanel) {
throw new Error(t('面板不存在'));
}
const topic = await this.adapter.model.create({
groupId,
panelId,
content,
meta,
author: userId,
comment: [],
});
const json = await this.transformDocuments(ctx, {}, topic);
this.roomcastNotify(ctx, panelId, 'create', json);
return json;
}
/**
* 回复话题
*/
async createComment(
ctx: TcContext<{
groupId: string;
panelId: string;
topicId: string;
content: string;
replyCommentId?: string;
}>
) {
const { groupId, panelId, topicId, content, replyCommentId } = ctx.params;
const userId = ctx.meta.userId;
const t = ctx.meta.t;
// 鉴权
const group = await call(ctx).getGroupInfo(groupId);
const isMember = group.members.some((member) => member.userId === userId);
if (!isMember) {
throw new Error(t('不是该群组成员'));
}
const targetPanel = group.panels.find((p) => p.id === panelId);
if (!targetPanel) {
throw new Error(t('面板不存在'));
}
const topic = await this.adapter.model.findOneAndUpdate(
{
_id: topicId,
groupId,
panelId,
},
{
$push: {
comments: {
content,
author: userId,
replyCommentId,
},
},
},
{ new: true }
);
const json = await this.transformDocuments(ctx, {}, topic);
this.roomcastNotify(ctx, panelId, 'createComment', json);
// 向所有参与者都添加收件箱消息
const memberIds = _.uniq([
topic.author,
...topic.comments.map((c) => c.author),
]);
await Promise.all(
memberIds.map((memberId) =>
call(ctx).appendInbox(
'plugin:com.msgbyte.topic.comment',
json,
String(memberId)
)
)
);
return true;
}
/**
* 删除话题
*/
async delete(
ctx: TcContext<{
groupId: string;
panelId: string;
topicId: string;
}>
) {
const { groupId, panelId, topicId } = ctx.params;
const userId = ctx.meta.userId;
const t = ctx.meta.t;
// 鉴权
const group = await call(ctx).getGroupInfo(groupId);
const isMember = group.members.some((member) => member.userId === userId);
if (!isMember) {
throw new Error(t('不是该群组成员'));
}
if (String(group.owner) !== userId) {
throw new Error(t('仅群组所有者有权限删除话题'));
}
const result = await this.adapter.model.deleteOne({
_id: topicId,
groupId,
panelId,
});
this.roomcastNotify(ctx, panelId, 'delete', {
groupId,
panelId,
topicId,
});
return result.deletedCount > 0;
}
}
export default GroupTopicService;

View File

@@ -0,0 +1,11 @@
{
"label": "Topic",
"label.zh-CN": "群组话题",
"name": "com.msgbyte.topic",
"url": "{BACKEND}/plugins/com.msgbyte.topic/index.js",
"version": "0.0.0",
"author": "moonrailgun",
"description": "Provide topic feature for groups",
"description.zh-CN": "为群组提供话题功能",
"requireRestart": true
}

View File

@@ -0,0 +1,20 @@
{
"name": "@plugins/com.msgbyte.topic",
"main": "src/index.tsx",
"version": "0.0.0",
"description": "为群组提供话题功能",
"private": true,
"scripts": {
"sync:declaration": "tailchat declaration github"
},
"devDependencies": {
"@types/styled-components": "^5.1.26",
"@types/lodash": "^4.14.191",
"react": "18.2.0",
"styled-components": "^5.3.6",
"zustand": "^4.3.6"
},
"dependencies": {
"lodash": "^4.17.21"
}
}

View File

@@ -0,0 +1,172 @@
import React, { useReducer, useState } from 'react';
import {
getMessageRender,
showMessageTime,
showSuccessToasts,
useAsyncRequest,
useCurrentUserInfo,
useGroupInfo,
} from '@capital/common';
import {
IconBtn,
TextArea,
UserName,
UserAvatar,
MessageAckContainer,
Popconfirm,
} from '@capital/component';
import styled from 'styled-components';
import type { GroupTopic } from '../types';
import { Translate } from '../translate';
import { request } from '../request';
import { TopicComments } from './TopicComments';
const Root = styled.div`
background-color: rgba(0, 0, 0, 0.05);
padding: 10px;
border-radius: 3px;
margin: 10px;
width: auto;
display: flex;
.dark & {
background-color: rgba(0, 0, 0, 0.25);
}
.left {
margin-right: 10px;
}
.right {
flex: 1;
user-select: text;
.header {
display: flex;
line-height: 32px;
.name {
margin-right: 4px;
}
.date {
opacity: 0.6;
}
}
.body {
.content {
margin-top: 6px;
margin-bottom: 6px;
}
}
.footer {
display: flex;
gap: 4px;
}
}
`;
const ReplyBox = styled.div`
padding: 10px;
margin-top: 10px;
background-color: transparent;
.dark & {
background-color: rgba(0, 0, 0, 0.25);
}
`;
export const TopicCard: React.FC<{
topic: GroupTopic;
}> = React.memo((props) => {
const topic: Partial<GroupTopic> = props.topic ?? {};
const [showReply, toggleShowReply] = useReducer((state) => !state, false);
const [comment, setComment] = useState('');
const groupInfo = useGroupInfo(topic.groupId);
const groupOwnerId = groupInfo?.owner;
const userId = useCurrentUserInfo()._id;
const [{ loading }, handleComment] = useAsyncRequest(async () => {
await request.post('createComment', {
groupId: topic.groupId,
panelId: topic.panelId,
topicId: topic._id,
content: comment,
});
setComment('');
toggleShowReply();
showSuccessToasts();
}, [topic.groupId, topic.panelId, topic._id, comment]);
const [, handleDeleteTopic] = useAsyncRequest(async () => {
await request.post('delete', {
groupId: topic.groupId,
panelId: topic.panelId,
topicId: topic._id,
});
}, []);
return (
<MessageAckContainer converseId={topic.panelId} messageId={topic._id}>
<Root>
<div className="left">
<UserAvatar userId={topic.author} />
</div>
<div className="right">
<div className="header">
<div className="name">
<UserName userId={topic.author} />
</div>
<div className="date">{showMessageTime(topic.createdAt)}</div>
</div>
<div className="body">
<div className="content">{getMessageRender(topic.content)}</div>
{Array.isArray(topic.comments) && topic.comments.length > 0 && (
<TopicComments comments={topic.comments} />
)}
</div>
<div className="footer">
<IconBtn
title={Translate.reply}
icon="mdi:message-reply-text-outline"
onClick={toggleShowReply}
/>
{userId === groupOwnerId && (
<Popconfirm
title={Translate.topicDeleteConfimTip}
onConfirm={handleDeleteTopic}
>
<IconBtn title={Translate.delete} icon="mdi:delete-outline" />
</Popconfirm>
)}
</div>
{showReply && (
<ReplyBox>
<TextArea
autoFocus
placeholder={Translate.replyThisTopic}
disabled={loading}
value={comment}
row={2}
maxLength={1000}
showCount={true}
onChange={(e) => setComment(e.target.value)}
onPressEnter={handleComment}
/>
</ReplyBox>
)}
</div>
</Root>
</MessageAckContainer>
);
});
TopicCard.displayName = 'TopicCard';

View File

@@ -0,0 +1,83 @@
import { getMessageRender } from '@capital/common';
import { UserAvatar, UserName } from '@capital/component';
import React, { useState } from 'react';
import styled from 'styled-components';
import type { GroupTopicComment } from '../types';
import _takeRight from 'lodash/takeRight';
import { Translate } from '../translate';
const Root = styled.div`
padding: 10px;
margin-bottom: 6px;
border-radius: 3px;
background-color: rgba(0, 0, 0, 0.05);
.dark & {
background-color: rgba(0, 0, 0, 0.25);
}
.show-more {
font-size: 12px;
cursor: pointer;
text-align: center;
&:hover {
color: #40a9ff;
}
}
.comment-item {
display: flex;
margin-bottom: 10px;
.left {
margin-right: 4px;
}
.right {
.username {
font-weight: bold;
line-height: 24px;
}
}
}
`;
/**
* 话题评论
*/
export const TopicComments: React.FC<{
comments: GroupTopicComment[];
}> = React.memo((props) => {
const [showAllComment, setShowAllComment] = useState(false);
const comments = showAllComment
? props.comments
: _takeRight(props.comments, 2);
return (
<Root>
{props.comments.length > 2 && !showAllComment && (
<div className="show-more" onClick={() => setShowAllComment(true)}>
{Translate.loadMore}...
</div>
)}
{comments.map((comment) => (
<div key={comment.id} className="comment-item">
<div className="left">
<UserAvatar userId={comment.author} size={24} />
</div>
<div className="right">
<div className="username">
<UserName userId={comment.author} />
</div>
<div>{getMessageRender(comment.content)}</div>
</div>
</div>
))}
</Root>
);
});
TopicComments.displayName = 'TopicComments';

View File

@@ -0,0 +1,37 @@
import { useAsyncRequest } from '@capital/common';
import { Button, ModalWrapper, TextArea } from '@capital/component';
import React, { useState } from 'react';
import styled from 'styled-components';
import { Translate } from '../../translate';
const Footer = styled.div({
textAlign: 'right',
paddingTop: 10,
});
export const TopicCreate: React.FC<{
onCreate: (text: string) => Promise<void>;
}> = React.memo((props) => {
const [text, setText] = useState('');
const [{ loading }, handleCreate] = useAsyncRequest(async () => {
await props.onCreate(text);
}, [text]);
return (
<ModalWrapper title={Translate.createBtn}>
<TextArea
autoFocus
value={text}
onChange={(e) => setText(e.target.value)}
/>
<Footer>
<Button type="primary" loading={loading} onClick={handleCreate}>
{Translate.createBtn}
</Button>
</Footer>
</ModalWrapper>
);
});
TopicCreate.displayName = 'TopicCreate';

View File

@@ -0,0 +1,202 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { TopicCard } from '../components/TopicCard';
import {
showSuccessToasts,
useAsyncRequest,
useGlobalSocketEvent,
useGroupPanelContext,
} from '@capital/common';
import {
Button,
Empty,
IconBtn,
openModal,
closeModal,
LoadingOnFirst,
} from '@capital/component';
import { request } from '../request';
import { Translate } from '../translate';
import { TopicCreate } from '../components/modals/TopicCreate';
import styled from 'styled-components';
import { useTopicStore } from '../store';
import type { GroupTopic } from '../types';
const Root = styled(LoadingOnFirst)({
display: 'flex',
flexDirection: 'column',
width: '100%',
position: 'relative',
paddingTop: 10,
paddingBottom: 10,
'.ant-empty': {
paddingTop: 80,
},
'.list': {
height: '100%',
overflow: 'auto',
},
'.create-btn': {
position: 'absolute',
right: 20,
bottom: 20,
'> .anticon': {
fontSize: 24,
},
},
});
const PAGE_SIZE = 20;
const GroupTopicPanelRender: React.FC = React.memo(() => {
const panelInfo = useGroupPanelContext();
const { panelId, groupId } = panelInfo;
const {
topicMap,
addTopicPanel,
addTopicItem,
deleteTopicItem,
updateTopicItem,
resetTopicPanel,
} = useTopicStore();
const topicList = topicMap[panelId];
const currentPageRef = useRef(1);
const [hasMore, setHasMore] = useState(true);
const [{ loading }, fetch] = useAsyncRequest(
async (page = 1) => {
if (!groupId || !panelId) {
return [];
}
const { data: list } = await request.post('list', {
groupId,
panelId,
page,
size: PAGE_SIZE,
});
if (Array.isArray(list)) {
addTopicPanel(panelId, list);
if (list.length !== PAGE_SIZE) {
// 没有更多了
setHasMore(false);
}
}
},
[groupId, panelId, addTopicPanel]
);
useEffect(() => {
/**
* 加载的时候获取列表
*/
fetch();
return () => {
// 因为监听更新只在当前激活的面板中监听,还没添加到全局,因此为了保持面板状态需要清理面板状态
// TODO: 增加群组级别的更新监听新增后可以移除
resetTopicPanel(panelId);
};
}, []);
const handleLoadMore = useCallback(() => {
currentPageRef.current += 1;
fetch(currentPageRef.current);
}, [fetch]);
useGlobalSocketEvent(
'plugin:com.msgbyte.topic.create',
(topic: GroupTopic) => {
/**
* 仅处理当前面板的话题更新
*/
if (topic.panelId === panelId) {
addTopicItem(panelId, topic);
}
}
);
useGlobalSocketEvent(
'plugin:com.msgbyte.topic.delete',
(info: { panelId: string; topicId: string }) => {
/**
* 仅处理当前面板的话题更新
*/
if (info.panelId === panelId) {
deleteTopicItem(panelId, info.topicId);
}
}
);
useGlobalSocketEvent(
'plugin:com.msgbyte.topic.createComment',
(topic: GroupTopic) => {
/**
* 仅处理当前面板的话题更新
*/
if (topic.panelId === panelId) {
updateTopicItem(panelId, topic);
}
}
);
const handleCreateTopic = useCallback(() => {
const key = openModal(
<TopicCreate
onCreate={async (text) => {
await request.post('create', {
groupId,
panelId,
content: text,
});
showSuccessToasts();
closeModal(key);
}}
/>
);
}, [panelInfo, fetch]);
return (
<Root spinning={loading}>
{Array.isArray(topicList) && topicList.length > 0 ? (
<div className="list">
{topicList.map((item, i) => (
<TopicCard key={i} topic={item} />
))}
{hasMore ? (
<Button type="link" disabled={loading} onClick={handleLoadMore}>
{loading ? Translate.loading : Translate.loadMore}
</Button>
) : (
<Button type="link" disabled={true} onClick={handleLoadMore}>
{Translate.noMore}
</Button>
)}
</div>
) : (
<Empty description={Translate.noTopic}>
<Button type="primary" onClick={handleCreateTopic}>
{Translate.createBtn}
</Button>
</Empty>
)}
<IconBtn
className="create-btn"
size="large"
icon="mdi:plus"
title={Translate.createBtn}
onClick={handleCreateTopic}
/>
</Root>
);
});
GroupTopicPanelRender.displayName = 'GroupTopicPanelRender';
export default GroupTopicPanelRender;

View File

@@ -0,0 +1,27 @@
import React from 'react';
import { TopicCard } from '../components/TopicCard';
import { Problem, JumpToGroupPanelButton } from '@capital/component';
import { Translate } from '../translate';
export const TopicInboxItem: React.FC<{ inboxItem: any }> = React.memo(
(props) => {
const payload = props.inboxItem.payload;
if (!payload) {
return <Problem text={Translate.topicDataError} />;
}
return (
<div style={{ width: '100%' }}>
<div style={{ height: '100%', overflow: 'auto', paddingBottom: 50 }}>
<TopicCard topic={payload} />
</div>
<JumpToGroupPanelButton
groupId={payload.groupId}
panelId={payload.panelId}
/>
</div>
);
}
);
TopicInboxItem.displayName = 'TopicInboxItem';

View File

@@ -0,0 +1,30 @@
import {
getMessageRender,
regGroupPanel,
regPluginInboxItemMap,
} from '@capital/common';
import { Loadable } from '@capital/component';
import { Translate } from './translate';
const PLUGIN_NAME = 'com.msgbyte.topic';
regGroupPanel({
name: `${PLUGIN_NAME}/grouppanel`,
label: Translate.topicpanel,
provider: PLUGIN_NAME,
render: Loadable(() => import('./group/GroupTopicPanelRender')),
feature: ['subscribe', 'ack'],
});
regPluginInboxItemMap('plugin:com.msgbyte.topic.comment', {
source: Translate.topicpanel,
getPreview: (item) => {
return {
title: Translate.topicpanel,
desc: getMessageRender(item?.payload?.content ?? ''),
};
},
render: Loadable(() =>
import('./inbox/TopicInboxItem').then((module) => module.TopicInboxItem)
),
});

View File

@@ -0,0 +1,3 @@
import { createPluginRequest } from '@capital/common';
export const request = createPluginRequest('com.msgbyte.topic');

View File

@@ -0,0 +1,68 @@
import create from 'zustand';
import { immer } from 'zustand/middleware/immer';
import type { GroupTopic } from './types';
interface TopicPanelMap {
[panelId: string]: GroupTopic[];
}
interface TopicStoreState {
topicMap: TopicPanelMap;
addTopicPanel: (panelId: string, topicList: GroupTopic[]) => void;
addTopicItem: (panelId: string, topic: GroupTopic) => void;
deleteTopicItem: (panelId: string, topicId: string) => void;
updateTopicItem: (panelId: string, topic: GroupTopic) => void;
resetTopicPanel: (panelId: string) => void;
}
export const useTopicStore = create<
TopicStoreState,
[['zustand/immer', never]]
>(
immer((set) => ({
topicMap: {},
addTopicPanel: (panelId, topicList) => {
set((state) => {
if (state.topicMap[panelId]) {
state.topicMap[panelId].push(...topicList);
} else {
state.topicMap[panelId] = topicList;
}
});
},
addTopicItem: (panelId, topic) => {
set((state) => {
if (state.topicMap[panelId]) {
state.topicMap[panelId].unshift(topic);
}
});
},
deleteTopicItem: (panelId, topicId) => {
set((state) => {
if (state.topicMap[panelId]) {
state.topicMap[panelId] = state.topicMap[panelId].filter(
(item) => item._id !== topicId
);
}
});
},
updateTopicItem: (panelId, topic) => {
set((state) => {
if (state.topicMap[panelId]) {
const findedTopicIndex = state.topicMap[panelId].findIndex(
(t) => t._id === topic._id
);
if (findedTopicIndex >= 0) {
state.topicMap[panelId][findedTopicIndex] = topic;
}
}
});
},
resetTopicPanel: (panelId) => {
set((state) => {
delete state.topicMap[panelId];
});
},
}))
);

View File

@@ -0,0 +1,33 @@
import { localTrans } from '@capital/common';
export const Translate = {
topicpanel: localTrans({ 'zh-CN': '话题面板', 'en-US': 'Topic Panel' }),
noTopic: localTrans({ 'zh-CN': '暂无话题', 'en-US': 'No Topic' }),
createBtn: localTrans({ 'zh-CN': '创建话题', 'en-US': 'Create Topic' }),
reply: localTrans({ 'zh-CN': '回复', 'en-US': 'Reply' }),
delete: localTrans({ 'zh-CN': '删除', 'en-US': 'Delete' }),
replyThisTopic: localTrans({
'zh-CN': '回复该话题',
'en-US': 'Reply this topic',
}),
loadMore: localTrans({
'zh-CN': '加载更多',
'en-US': 'Load More',
}),
noMore: localTrans({
'zh-CN': '没有更多了',
'en-US': 'No More',
}),
loading: localTrans({
'zh-CN': '加载中...',
'en-US': 'Loading...',
}),
topicDataError: localTrans({
'zh-CN': '话题信息异常',
'en-US': 'Topic Data Error',
}),
topicDeleteConfimTip: localTrans({
'zh-CN': '你确定要删除该话题么',
'en-US': 'Are you sure you want to delete this topic?',
}),
};

View File

@@ -0,0 +1,16 @@
export interface GroupTopicComment {
author: string;
content: string;
id: string;
}
export interface GroupTopic {
_id: string;
author: string;
comments: GroupTopicComment[];
content: string;
createdAt: string;
groupId: string;
panelId: string;
updatedAt: string;
}

View File

@@ -0,0 +1,7 @@
{
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react",
"importsNotUsedAsValues": "error"
}
}

View File

@@ -0,0 +1,516 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/// <reference types="react" />
/**
* 该文件由 Tailchat 自动生成
* 用于插件的类型声明
* 生成命令: pnpm run plugins:declaration:generate
*/
/**
* Tailchat 通用
*/
declare module '@capital/common' {
export const useGroupPanelParams: any;
/**
* 打开模态框
* @deprecated 请从 @capital/component 引入
*/
export const openModal: (
content: React.ReactNode,
props?: {
/**
* 是否显示右上角的关闭按钮
* @default false
*/
closable?: boolean;
/**
* 遮罩层是否可关闭
*/
maskClosable?: boolean;
/**
* 关闭modal的回调
*/
onCloseModal?: () => void;
}
) => number;
/**
* @deprecated 请从 @capital/component 引入
*/
export const closeModal: any;
/**
* @deprecated 请从 @capital/component 引入
*/
export const ModalWrapper: any;
/**
* @deprecated 请从 @capital/component 引入
*/
export const useModalContext: any;
/**
* @deprecated 请从 @capital/component 引入
*/
export const openConfirmModal: any;
/**
* @deprecated 请从 @capital/component 引入
*/
export const openReconfirmModal: any;
/**
* @deprecated 请从 @capital/component 引入
*/
export const Loadable: any;
export const getGlobalState: any;
export const useGlobalSocketEvent: <T>(
eventName: string,
callback: (data: T) => void
) => void;
export const getJWTUserInfo: () => Promise<{
_id?: string;
nickname?: string;
email?: string;
avatar?: string;
}>;
export const dataUrlToFile: any;
export const urlSearchStringify: any;
export const urlSearchParse: any;
export const appendUrlSearch: any;
export const getServiceWorkerRegistration: any;
export const getServiceUrl: () => string;
export const getCachedUserInfo: (
userId: string,
refetch?: boolean
) => Promise<{
_id: string;
email: string;
nickname: string;
discriminator: string;
avatar: string | null;
temporary: boolean;
}>;
export const getCachedConverseInfo: any;
export const getCachedBaseGroupInfo: any;
export const getCachedUserSettings: any;
/**
* 本地翻译
* @example
* localTrans({'zh-CN': '你好', 'en-US': 'Hello'});
*
* @param trans 翻译对象
*/
export const localTrans: (trans: Record<'zh-CN' | 'en-US', string>) => string;
export const getLanguage: any;
export const sharedEvent: any;
export const useAsync: <T extends (...args: any[]) => Promise<any>>(
fn: T,
deps?: React.DependencyList
) => { loading: boolean; value?: any; error?: Error };
export const useAsyncFn: <T extends (...args: any[]) => Promise<any>>(
fn: T,
deps?: React.DependencyList
) => [{ loading: boolean; value?: any; error?: Error }, T];
export const useAsyncRefresh: <T extends (...args: any[]) => Promise<any>>(
fn: T,
deps?: React.DependencyList
) => { loading: boolean; value?: any; error?: Error; refresh: () => void };
export const useAsyncRequest: <T extends (...args: any[]) => Promise<any>>(
fn: T,
deps?: React.DependencyList
) => [{ loading: boolean; value?: any }, T];
export const uploadFile: any;
export const showToasts: (
message: string,
type?: 'info' | 'success' | 'error' | 'warning'
) => void;
export const showSuccessToasts: any;
export const showErrorToasts: (error: any) => void;
export const fetchAvailableServices: any;
export const isValidStr: (str: any) => str is string;
export const useGroupPanelInfo: any;
export const sendMessage: any;
export const showMessageTime: any;
export const joinArray: any;
export const navigate: any;
export const useLocation: any;
export const useNavigate: any;
/**
* @deprecated please use createMetaFormSchema from @capital/component
*/
export const createFastFormSchema: any;
/**
* @deprecated please use metaFormFieldSchema from @capital/component
*/
export const fieldSchema: any;
export const useCurrentUserInfo: any;
export const createPluginRequest: (pluginName: string) => {
get: (actionName: string, config?: any) => Promise<any>;
post: (actionName: string, data?: any, config?: any) => Promise<any>;
};
export const postRequest: any;
export const pluginCustomPanel: any;
export const regCustomPanel: any;
export const pluginGroupPanel: any;
export const regGroupPanel: any;
export const messageInterpreter: {
name?: string;
explainMessage: (message: string) => React.ReactNode;
}[];
export const regMessageInterpreter: (interpreter: {
name?: string;
explainMessage: (message: string) => React.ReactNode;
}) => void;
export const getMessageRender: (message: string) => React.ReactNode;
export const regMessageRender: (
render: (message: string) => React.ReactNode
) => void;
export const getMessageTextDecorators: any;
export const regMessageTextDecorators: any;
export const ChatInputActionContextProps: any;
export const pluginChatInputActions: any;
export const regChatInputAction: any;
export const regSocketEventListener: (item: {
eventName: string;
eventFn: (...args: any[]) => void;
}) => void;
export const pluginColorScheme: any;
export const regPluginColorScheme: any;
export const pluginInspectServices: any;
export const regInspectService: any;
export const pluginMessageExtraParsers: any;
export const regMessageExtraParser: any;
export const pluginRootRoute: any;
export const regPluginRootRoute: any;
export const pluginPanelActions: any;
export const regPluginPanelAction: (
action:
| {
name: string;
label: string;
icon: string;
position: 'group';
onClick: (ctx: { groupId: string; panelId: string }) => void;
}
| {
name: string;
label: string;
icon: string;
position: 'dm';
onClick: (ctx: { converseId: string }) => void;
}
) => void;
export const pluginPermission: any;
export const regPluginPermission: (permission: {
/**
* 权限唯一key, 用于写入数据库
* 如果为插件则权限点应当符合命名规范, 如: plugin.com.msgbyte.github.manage
*/
key: string;
/**
* 权限点显示名称
*/
title: string;
/**
* 权限描述
*/
desc: string;
/**
* 是否默认开启
*/
default: boolean;
/**
* 是否依赖其他权限点
*/
required?: string[];
}) => void;
export const pluginGroupPanelBadges: any;
export const regGroupPanelBadge: any;
export const pluginGroupTextPanelExtraMenus: any;
export const regPluginGroupTextPanelExtraMenu: any;
export const pluginUserExtraInfo: any;
export const regUserExtraInfo: any;
export const pluginSettings: any;
export const regPluginSettings: any;
export const pluginInboxItemMap: any;
export const regPluginInboxItemMap: any;
export const useGroupIdContext: () => string;
export const useGroupPanelContext: () => {
groupId: string;
panelId: string;
} | null;
export const useSocketContext: any;
}
/**
* Tailchat 组件
*/
declare module '@capital/component' {
export const Button: any;
export const Checkbox: any;
export const Input: any;
export const Divider: any;
export const Space: any;
export const Menu: any;
export const Table: any;
export const Switch: any;
export const Tooltip: any;
/**
* @link https://ant.design/components/notification-cn/
*/
export const notification: any;
export const Empty: React.FC<
React.PropsWithChildren<{
prefixCls?: string;
className?: string;
style?: React.CSSProperties;
imageStyle?: React.CSSProperties;
image?: React.ReactNode;
description?: React.ReactNode;
}>
>;
export const TextArea: any;
export const Avatar: any;
export const SensitiveText: React.FC<{ className?: string; text: string }>;
export const Icon: React.FC<{ icon: string } & React.SVGProps<SVGSVGElement>>;
export const CopyableText: React.FC<{
className?: string;
style?: React.CSSProperties;
config?:
| boolean
| {
text?: string;
onCopy?: (event?: React.MouseEvent<HTMLDivElement>) => void;
icon?: React.ReactNode;
tooltips?: boolean | React.ReactNode;
format?: 'text/plain' | 'text/html';
};
}>;
export const WebFastForm: any;
export const WebMetaForm: any;
export const createMetaFormSchema: any;
export const metaFormFieldSchema: any;
export const Link: any;
export const MessageAckContainer: any;
export const Image: any;
export const IconBtn: React.FC<{
icon: string;
className?: string;
iconClassName?: string;
size?: 'small' | 'middle' | 'large';
shape?: 'circle' | 'square';
title?: string;
danger?: boolean;
active?: boolean;
disabled?: boolean;
onClick?: React.MouseEventHandler<HTMLElement>;
}>;
export const PillTabs: any;
export const PillTabPane: any;
export const LoadingSpinner: React.FC<{ tip?: string }>;
export const FullModalField: any;
export const DefaultFullModalInputEditorRender: any;
export const DefaultFullModalTextAreaEditorRender: any;
export const openModal: (
content: React.ReactNode,
props?: {
/**
* 是否显示右上角的关闭按钮
* @default false
*/
closable?: boolean;
/**
* 遮罩层是否可关闭
*/
maskClosable?: boolean;
/**
* 关闭modal的回调
*/
onCloseModal?: () => void;
}
) => number;
export const closeModal: any;
export const ModalWrapper: any;
export const useModalContext: any;
export const openConfirmModal: any;
export const openReconfirmModal: any;
export const Loadable: any;
export const Loading: React.FC<{
spinning: boolean;
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
}>;
export const LoadingOnFirst: React.FC<{
spinning: boolean;
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
}>;
export const SidebarView: any;
export const GroupPanelSelector: any;
export const Emoji: any;
export const PortalAdd: any;
export const PortalRemove: any;
export const ErrorBoundary: any;
export const UserAvatar: React.FC<{
userId: string;
className?: string;
style?: React.CSSProperties;
size?: 'large' | 'small' | 'default' | number;
}>;
export const UserName: React.FC<{
userId: string;
className?: string;
style?: React.CSSProperties;
}>;
export const Markdown: any;
export const Webview: any;
export const WebviewKeepAlive: any;
export const Card: any;
export const Problem: any;
export const JumpToButton: any;
export const JumpToGroupPanelButton: any;
export const JumpToConverseButton: any;
}