diff --git a/client/shared/api/socket.ts b/client/shared/api/socket.ts index 3e78bff9ab2..c78081b3dcd 100644 --- a/client/shared/api/socket.ts +++ b/client/shared/api/socket.ts @@ -55,13 +55,13 @@ export class AppSocket { if (resp.result === true) { resolve(resp.data); } else if (resp.result === false) { - reject( - new SocketEventError( - `[${eventName}]: ${resp.message}: \ndata: ${JSON.stringify( - eventData - )}` - ) + const error = new SocketEventError( + `[${eventName}]: ${resp.message}: \ndata: ${JSON.stringify( + eventData + )}` ); + console.error(error); + reject(error); } }); }); diff --git a/client/web/src/App.tsx b/client/web/src/App.tsx index 0cc9a45ac0f..51ca2a9d0b0 100644 --- a/client/web/src/App.tsx +++ b/client/web/src/App.tsx @@ -59,6 +59,14 @@ const InviteRoute = Loadable( ) ); +const PreviewRoute = Loadable( + () => + import( + /* webpackChunkName: 'preview' */ /* webpackPreload: true */ + './routes/Preview' + ) +); + export const TcAntdProvider: React.FC = React.memo( (props) => { const { value: locale } = useAsync(async (): Promise => { @@ -168,6 +176,7 @@ export const App: React.FC = React.memo(() => { } /> } /> } /> + } /> = React.memo((props) => { + useSocket(props.groupId); + + return null; +}); +GroupPreview.displayName = 'GroupPreview'; diff --git a/client/web/src/components/GroupPreview/MessageList.tsx b/client/web/src/components/GroupPreview/MessageList.tsx new file mode 100644 index 00000000000..d5c78203991 --- /dev/null +++ b/client/web/src/components/GroupPreview/MessageList.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { ChatMessageList } from '../ChatBox/ChatMessageList'; + +interface GroupPreviewMessageListProps { + groupId: string; + converseId: string; +} +export const GroupPreviewMessageList: React.FC = + React.memo(() => { + return ( + {}} + /> + ); + }); +GroupPreviewMessageList.displayName = 'GroupPreviewMessageList'; diff --git a/client/web/src/components/GroupPreview/README.md b/client/web/src/components/GroupPreview/README.md new file mode 100644 index 00000000000..966c1e983fe --- /dev/null +++ b/client/web/src/components/GroupPreview/README.md @@ -0,0 +1,3 @@ +Preview Group Message like Discord. + +NOTICE: GroupPreview should has independent context because its should can run in non-main page. diff --git a/client/web/src/components/GroupPreview/index.tsx b/client/web/src/components/GroupPreview/index.tsx new file mode 100644 index 00000000000..854383293b2 --- /dev/null +++ b/client/web/src/components/GroupPreview/index.tsx @@ -0,0 +1 @@ +export { GroupPreview } from './GroupPreview'; diff --git a/client/web/src/components/GroupPreview/store.ts b/client/web/src/components/GroupPreview/store.ts new file mode 100644 index 00000000000..156be958c35 --- /dev/null +++ b/client/web/src/components/GroupPreview/store.ts @@ -0,0 +1,29 @@ +import { create } from 'zustand'; +import type { model } from 'tailchat-shared'; + +interface ChatConverseState extends model.converse.ChatConverseInfo { + messages: model.message.LocalChatMessage[]; +} + +interface GroupPreviewState { + groupInfo: model.group.GroupInfo | null; + converses: Record; +} + +function getDefaultState() { + return { + groupInfo: null, + converses: {}, + }; +} + +export const useGroupPreviewStore = create((get) => ({ + ...getDefaultState(), +})); + +/** + * 重置状态 + */ +export function resetGroupPreviewState() { + useGroupPreviewStore.setState(getDefaultState()); +} diff --git a/client/web/src/components/GroupPreview/useSocket.tsx b/client/web/src/components/GroupPreview/useSocket.tsx new file mode 100644 index 00000000000..e91dddf604f --- /dev/null +++ b/client/web/src/components/GroupPreview/useSocket.tsx @@ -0,0 +1,41 @@ +import { useSocketContext } from '@/context/SocketContext'; +import { useEffect } from 'react'; +import { GroupInfo, GroupPanelType } from 'tailchat-shared'; +import { resetGroupPreviewState, useGroupPreviewStore } from './store'; + +export function useSocket(groupId: string) { + const socket = useSocketContext(); + + useEffect(() => { + socket.request('group.preview.joinGroupRooms', { + groupId, + }); + + socket + .request('group.preview.getGroupInfo', { + groupId, + }) + .then((groupInfo) => { + console.log('groupInfo', groupInfo); + useGroupPreviewStore.setState({ + groupInfo, + }); + + if (Array.isArray(groupInfo.panels)) { + const textPanels = groupInfo.panels.map( + (p) => p.type === GroupPanelType.TEXT + ); + + // TODO + } + }); + + return () => { + socket.request('group.preview.leaveGroupRooms', { + groupId, + }); + + resetGroupPreviewState(); + }; + }, [groupId]); +} diff --git a/client/web/src/routes/Preview/index.tsx b/client/web/src/routes/Preview/index.tsx new file mode 100644 index 00000000000..36a85a0e945 --- /dev/null +++ b/client/web/src/routes/Preview/index.tsx @@ -0,0 +1,34 @@ +import { GroupPreview } from '@/components/GroupPreview'; +import { NotFound } from '@/components/NotFound'; +import React from 'react'; +import { Route, Routes, useParams } from 'react-router'; +import { t } from 'tailchat-shared'; +import { MainProvider } from '../Main/Provider'; + +const PreviewRoute: React.FC = React.memo(() => { + return ( + + + } /> + + + + + ); +}); +PreviewRoute.displayName = 'PreviewRoute'; + +const GroupPreviewRoute: React.FC = React.memo(() => { + const { groupId } = useParams<{ + groupId: string; + }>(); + + if (!groupId) { + return ; + } + + return ; +}); +GroupPreviewRoute.displayName = 'GroupPreviewRoute'; + +export default PreviewRoute; diff --git a/server/services/core/group/group.service.ts b/server/services/core/group/group.service.ts index 0b2b5a5949d..7d8bcc26a95 100644 --- a/server/services/core/group/group.service.ts +++ b/server/services/core/group/group.service.ts @@ -25,6 +25,7 @@ import { db, } from 'tailchat-server-sdk'; import moment from 'moment'; +import type { GroupStruct } from 'tailchat-server-sdk'; interface GroupService extends TcService, @@ -48,6 +49,7 @@ class GroupService extends TcService { 'getJoinedGroupAndPanelIds', this.getJoinedGroupAndPanelIds ); + this.registerAction('getGroupSocketRooms', this.getGroupSocketRooms); this.registerAction('getGroupBasicInfo', this.getGroupBasicInfo, { params: { groupId: 'string', @@ -234,7 +236,7 @@ class GroupService extends TcService { * * 订阅即加入socket房间 */ - private getSubscribedGroupPanelIds(group: Group): { + private getSubscribedGroupPanelIds(group: GroupStruct): { textPanelIds: string[]; subscribeFeaturePanelIds: string[]; } { @@ -254,7 +256,7 @@ class GroupService extends TcService { * 获取群组所有的文字面板id列表 * 用于加入房间 */ - private getGroupTextPanelIds(group: Group): string[] { + private getGroupTextPanelIds(group: GroupStruct): string[] { // TODO: 先无视权限, 把所有的信息全部显示 const textPanelIds = group.panels .filter((p) => p.type === GroupPanelType.TEXT) @@ -268,7 +270,7 @@ class GroupService extends TcService { * @param group */ private getGroupPanelIdsWithFeature( - group: Group, + group: GroupStruct, feature: PanelFeature ): string[] { const featureAllPanelNames = this.getPanelNamesWithFeature(feature); @@ -301,12 +303,14 @@ class GroupService extends TcService { throw new NoPermissionError(t('创建群组功能已被管理员禁用')); } - const group = await this.adapter.model.createGroup({ + const doc = await this.adapter.model.createGroup({ name, panels, owner: userId, }); + const group = await this.transformDocuments(ctx, {}, doc); + const { textPanelIds, subscribeFeaturePanelIds } = this.getSubscribedGroupPanelIds(group); @@ -315,10 +319,10 @@ class GroupService extends TcService { userId ); - return this.transformDocuments(ctx, {}, group); + return group; } - async getUserGroups(ctx: TcContext): Promise { + async getUserGroups(ctx: TcContext): Promise { const userId = ctx.meta.userId; const groups = await this.adapter.model.getUserGroups(userId); @@ -351,6 +355,20 @@ class GroupService extends TcService { }; } + /** + * 获取所有订阅的群组面板列表 + */ + async getGroupSocketRooms(ctx: TcContext<{ groupId: string }>): Promise<{ + textPanelIds: string[]; + subscribeFeaturePanelIds: string[]; + }> { + const groupId = ctx.params.groupId; + + const group = await call(ctx).getGroupInfo(groupId); + + return this.getSubscribedGroupPanelIds(group); + } + /** * 获取群组基本信息 */ @@ -550,7 +568,7 @@ class GroupService extends TcService { ) .exec(); - const group: Group = await this.transformDocuments(ctx, {}, doc); + const group: GroupStruct = await this.transformDocuments(ctx, {}, doc); this.notifyGroupInfoUpdate(ctx, group); // 推送变更 this.unicastNotify(ctx, userId, 'add', group); @@ -1241,13 +1259,15 @@ class GroupService extends TcService { */ private async notifyGroupInfoUpdate( ctx: TcContext, - group: Group - ): Promise { + group: Group | GroupStruct + ): Promise { const groupId = String(group._id); - let json = group; + let json: GroupStruct; if (_.isPlainObject(group) === false) { // 当传入的数据为group doc时 json = await this.transformDocuments(ctx, {}, group); + } else { + json = group as any; } this.cleanGroupInfoCache(groupId); diff --git a/server/services/core/group/preview.service.ts b/server/services/core/group/preview.service.ts new file mode 100644 index 00000000000..2338c072f23 --- /dev/null +++ b/server/services/core/group/preview.service.ts @@ -0,0 +1,78 @@ +import { TcService, TcContext, call } from 'tailchat-server-sdk'; + +class GroupPreviewService extends TcService { + get serviceName(): string { + return 'group.preview'; + } + + onInit(): void { + /** + * TODO: 这里的action都应当判断一下群组是否支持预览 + */ + this.registerAction('joinGroupRooms', this.joinGroupRooms, { + params: { + groupId: 'string', + }, + }); + this.registerAction('leaveGroupRooms', this.leaveGroupRooms, { + params: { + groupId: 'string', + }, + }); + this.registerAction('getGroupInfo', this.getGroupInfo, { + params: { + groupId: 'string', + }, + }); + } + + async joinGroupRooms(ctx: TcContext<{ groupId: string }>) { + const groupId = ctx.params.groupId; + + const { textPanelIds, subscribeFeaturePanelIds } = await ctx.call< + { + textPanelIds: string[]; + subscribeFeaturePanelIds: string[]; + }, + { groupId: string } + >('group.getGroupSocketRooms', { + groupId, + }); + + await call(ctx).joinSocketIORoom([ + groupId, + ...textPanelIds, + ...subscribeFeaturePanelIds, + ]); + } + + async leaveGroupRooms(ctx: TcContext<{ groupId: string }>) { + const groupId = ctx.params.groupId; + + const { textPanelIds, subscribeFeaturePanelIds } = await ctx.call< + { + textPanelIds: string[]; + subscribeFeaturePanelIds: string[]; + }, + { groupId: string } + >('group.getGroupSocketRooms', { + groupId, + }); + + await call(ctx).leaveSocketIORoom([ + groupId, + ...textPanelIds, + ...subscribeFeaturePanelIds, + ]); + } + + async getGroupInfo(ctx: TcContext<{ groupId: string }>) { + const groupId = ctx.params.groupId; + + const groupInfo = await call(ctx).getGroupInfo(groupId); + + return groupInfo; + } +} + +export default GroupPreviewService;