Skip to content

Commit f7bb165

Browse files
added chat controller component to expose chat data/methods
1 parent 3e5ef05 commit f7bb165

File tree

11 files changed

+1059
-351
lines changed

11 files changed

+1059
-351
lines changed

client/packages/lowcoder/src/comps/comps/chatBoxComponent/chatBoxComp.tsx

Lines changed: 286 additions & 335 deletions
Large diffs are not rendered by default.

client/packages/lowcoder/src/comps/comps/chatBoxComponent/chatControllerComp.tsx

Lines changed: 487 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { AutoHeightControl } from "@lowcoder-ee/comps/controls/autoHeightControl";
2+
import { BoolControl } from "@lowcoder-ee/comps/controls/boolControl";
3+
import { StringControl } from "@lowcoder-ee/comps/controls/codeControl";
4+
import { stringExposingStateControl } from "@lowcoder-ee/comps/controls/codeStateControl";
5+
import { dropdownControl } from "@lowcoder-ee/comps/controls/dropdownControl";
6+
import { clickEvent, doubleClickEvent, eventHandlerControl } from "@lowcoder-ee/comps/controls/eventHandlerControl";
7+
import { styleControl } from "@lowcoder-ee/comps/controls/styleControl";
8+
import { AnimationStyle, TextStyle } from "@lowcoder-ee/comps/controls/styleControlConstants";
9+
import { EditorContext } from "@lowcoder-ee/comps/editorState";
10+
import { withDefault } from "@lowcoder-ee/comps/generators/simpleGenerators";
11+
import { NewChildren } from "@lowcoder-ee/comps/generators/uiCompBuilder";
12+
import { hiddenPropertyView } from "@lowcoder-ee/comps/utils/propertyUtils";
13+
import { RecordConstructorToComp } from "lowcoder-core";
14+
import { ScrollBar, Section, sectionNames } from "lowcoder-design";
15+
import React, { useContext, useMemo } from "react";
16+
import { trans } from "i18n";
17+
18+
// Event options for the chat component
19+
const EventOptions = [clickEvent, doubleClickEvent] as const;
20+
21+
// Define the component's children map
22+
export const chatCompChildrenMap = {
23+
chatName: stringExposingStateControl("chatName", "Chat Room"),
24+
userId: stringExposingStateControl("userId", "user_1"),
25+
userName: stringExposingStateControl("userName", "User"),
26+
applicationId: stringExposingStateControl("applicationId", "lowcoder_app"),
27+
roomId: stringExposingStateControl("roomId", "general"),
28+
mode: dropdownControl([
29+
{ label: "🌐 Collaborative (Real-time)", value: "collaborative" },
30+
{ label: "🔀 Hybrid (Local + Real-time)", value: "hybrid" },
31+
{ label: "📱 Local Only", value: "local" }
32+
], "collaborative"),
33+
34+
// Room Management Configuration
35+
allowRoomCreation: withDefault(BoolControl, true),
36+
allowRoomJoining: withDefault(BoolControl, true),
37+
roomPermissionMode: dropdownControl([
38+
{ label: "🌐 Open (Anyone can join public rooms)", value: "open" },
39+
{ label: "🔐 Invite Only (Admin invitation required)", value: "invite" },
40+
{ label: "👤 Admin Only (Only admins can manage)", value: "admin" }
41+
], "open"),
42+
showAvailableRooms: withDefault(BoolControl, true),
43+
maxRoomsDisplay: withDefault(StringControl, "10"),
44+
45+
// UI Configuration
46+
leftPanelWidth: withDefault(StringControl, "200px"),
47+
showRooms: withDefault(BoolControl, true),
48+
autoHeight: AutoHeightControl,
49+
onEvent: eventHandlerControl(EventOptions),
50+
style: styleControl(TextStyle, 'style'),
51+
animationStyle: styleControl(AnimationStyle, 'animationStyle'),
52+
};
53+
54+
export type ChatCompChildrenType = NewChildren<RecordConstructorToComp<typeof chatCompChildrenMap>>;
55+
56+
// Property view component
57+
export const ChatPropertyView = React.memo((props: {
58+
children: ChatCompChildrenType
59+
}) => {
60+
const editorContext = useContext(EditorContext);
61+
const editorModeStatus = useMemo(() => editorContext.editorModeStatus, [editorContext.editorModeStatus]);
62+
63+
const basicSection = useMemo(() => (
64+
<Section name={sectionNames.basic}>
65+
{props.children.chatName.propertyView({
66+
label: "Chat Name",
67+
tooltip: "Name displayed in the chat header"
68+
})}
69+
{props.children.userId.propertyView({
70+
label: "User ID",
71+
tooltip: "Unique identifier for the current user"
72+
})}
73+
{props.children.userName.propertyView({
74+
label: "User Name",
75+
tooltip: "Display name for the current user"
76+
})}
77+
{props.children.applicationId.propertyView({
78+
label: "Application ID",
79+
tooltip: "Unique identifier for this Lowcoder application - all chat components with the same Application ID can discover each other's rooms"
80+
})}
81+
{props.children.roomId.propertyView({
82+
label: "Initial Room",
83+
tooltip: "Default room to join when the component loads (within the application scope)"
84+
})}
85+
{props.children.mode.propertyView({
86+
label: "Sync Mode",
87+
tooltip: "Choose how messages are synchronized: Collaborative (real-time), Hybrid (local + real-time), or Local only"
88+
})}
89+
</Section>
90+
), [props.children]);
91+
92+
const roomManagementSection = useMemo(() => (
93+
<Section name="Room Management">
94+
{props.children.allowRoomCreation.propertyView({
95+
label: "Allow Room Creation",
96+
tooltip: "Allow users to create new chat rooms"
97+
})}
98+
{props.children.allowRoomJoining.propertyView({
99+
label: "Allow Room Joining",
100+
tooltip: "Allow users to join existing rooms"
101+
})}
102+
{props.children.roomPermissionMode.propertyView({
103+
label: "Permission Mode",
104+
tooltip: "Control how users can join rooms"
105+
})}
106+
{props.children.showAvailableRooms.propertyView({
107+
label: "Show Available Rooms",
108+
tooltip: "Display list of available rooms to join"
109+
})}
110+
{props.children.maxRoomsDisplay.propertyView({
111+
label: "Max Rooms to Display",
112+
tooltip: "Maximum number of rooms to show in the list"
113+
})}
114+
</Section>
115+
), [props.children]);
116+
117+
const interactionSection = useMemo(() =>
118+
["logic", "both"].includes(editorModeStatus) && (
119+
<Section name={sectionNames.interaction}>
120+
{hiddenPropertyView(props.children)}
121+
{props.children.onEvent.getPropertyView()}
122+
</Section>
123+
), [editorModeStatus, props.children]);
124+
125+
const layoutSection = useMemo(() =>
126+
["layout", "both"].includes(editorModeStatus) && (
127+
<>
128+
<Section name={sectionNames.layout}>
129+
{props.children.autoHeight.getPropertyView()}
130+
{props.children.leftPanelWidth.propertyView({
131+
label: "Left Panel Width",
132+
tooltip: "Width of the rooms/people panel (e.g., 300px, 25%)"
133+
})}
134+
{props.children.showRooms.propertyView({
135+
label: "Show Rooms"
136+
})}
137+
</Section>
138+
<Section name={sectionNames.style}>
139+
{props.children.style.getPropertyView()}
140+
</Section>
141+
<Section name={sectionNames.animationStyle} hasTooltip={true}>
142+
{props.children.animationStyle.getPropertyView()}
143+
</Section>
144+
</>
145+
), [editorModeStatus, props.children]);
146+
147+
return (
148+
<>
149+
{basicSection}
150+
{roomManagementSection}
151+
{interactionSection}
152+
{layoutSection}
153+
</>
154+
);
155+
});

client/packages/lowcoder/src/comps/comps/chatBoxComponent/hooks/useChatManager.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface UseChatManagerReturn {
4848
joinRoom: (roomId: string) => Promise<boolean>;
4949
leaveRoom: (roomId: string) => Promise<boolean>;
5050
canUserJoinRoom: (roomId: string) => Promise<boolean>;
51+
getRoomParticipants: (roomId: string) => Promise<Array<{ id: string; name: string }>>;
5152

5253
// Manager access (for advanced use)
5354
manager: HybridChatManager | null;
@@ -95,7 +96,7 @@ export function useChatManager(config: UseChatManagerConfig): UseChatManagerRetu
9596
// 🧪 TEST: Add collaborative config to enable YjsPluvProvider for testing
9697
// This enables testing of the Yjs document structure (Step 1)
9798
collaborative: {
98-
serverUrl: 'ws://localhost:3001', // Placeholder - not used in Step 1
99+
serverUrl: 'ws://localhost:3005', // Placeholder - not used in Step 1
99100
roomId: config.roomId,
100101
authToken: undefined,
101102
autoConnect: true,
@@ -353,7 +354,7 @@ export function useChatManager(config: UseChatManagerConfig): UseChatManagerRetu
353354
}
354355

355356
try {
356-
const result = await manager.createRoom({
357+
const result = await manager.createRoom({
357358
name,
358359
type,
359360
participants: [config.userId],
@@ -558,6 +559,23 @@ export function useChatManager(config: UseChatManagerConfig): UseChatManagerRetu
558559
return false;
559560
}
560561
}, [config.userId]);
562+
563+
const getRoomParticipants = useCallback(async (roomId: string): Promise<Array<{ id: string; name: string }>> => {
564+
const manager = managerRef.current;
565+
if (!manager) return [];
566+
567+
try {
568+
const result = await manager.getRoomParticipants(roomId);
569+
if (result.success) {
570+
return result.data!;
571+
}
572+
setError(result.error || 'Failed to get room participants');
573+
return [];
574+
} catch (error) {
575+
setError(error instanceof Error ? error.message : 'Failed to get room participants');
576+
return [];
577+
}
578+
}, []);
561579

562580
return {
563581
// Connection state
@@ -588,6 +606,7 @@ export function useChatManager(config: UseChatManagerConfig): UseChatManagerRetu
588606
joinRoom,
589607
leaveRoom,
590608
canUserJoinRoom,
609+
getRoomParticipants,
591610

592611
// Manager access
593612
manager: managerRef.current,

client/packages/lowcoder/src/comps/comps/chatBoxComponent/managers/HybridChatManager.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,64 @@ export class HybridChatManager {
394394
console.log('[HybridChatManager] 🔐 Checking if user can join room:', { roomId, userId });
395395
return this.getActiveProvider().canUserJoinRoom(roomId, userId);
396396
}
397+
398+
async getRoomParticipants(roomId: string): Promise<OperationResult<Array<{ id: string; name: string }>>> {
399+
console.log('[HybridChatManager] 👥 Getting room participants:', { roomId });
400+
401+
try {
402+
// First get the room to access participants
403+
const roomResult = await this.getRoom(roomId);
404+
if (!roomResult.success || !roomResult.data) {
405+
return {
406+
success: false,
407+
error: roomResult.error || 'Room not found',
408+
timestamp: Date.now()
409+
};
410+
}
411+
412+
const room = roomResult.data;
413+
const participants = room.participants || [];
414+
415+
// Get participant details by looking at recent messages to extract user names
416+
const messagesResult = await this.getMessages(roomId, 100); // Get recent messages
417+
if (!messagesResult.success) {
418+
// If we can't get messages, return participants with just IDs
419+
return {
420+
success: true,
421+
data: participants.map(id => ({ id, name: id })), // Fallback to ID as name
422+
timestamp: Date.now()
423+
};
424+
}
425+
426+
// Create a map of userId -> userName from messages
427+
const userMap = new Map<string, string>();
428+
messagesResult.data?.forEach(message => {
429+
if (message.authorId && message.authorName) {
430+
userMap.set(message.authorId, message.authorName);
431+
}
432+
});
433+
434+
// Build participant list with names
435+
const participantsWithNames = participants.map(participantId => ({
436+
id: participantId,
437+
name: userMap.get(participantId) || participantId // Fallback to ID if name not found
438+
}));
439+
440+
return {
441+
success: true,
442+
data: participantsWithNames,
443+
timestamp: Date.now()
444+
};
445+
446+
} catch (error) {
447+
console.error('[HybridChatManager] Error getting room participants:', error);
448+
return {
449+
success: false,
450+
error: error instanceof Error ? error.message : 'Failed to get room participants',
451+
timestamp: Date.now()
452+
};
453+
}
454+
}
397455

398456
// Message operations (delegated to active provider)
399457
async sendMessage(message: Omit<UnifiedMessage, 'id' | 'timestamp' | 'status'>): Promise<OperationResult<UnifiedMessage>> {

client/packages/lowcoder/src/comps/comps/chatBoxComponent/providers/YjsPluvProvider.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class YjsPluvProvider extends BaseChatDataProvider implements ChatDataPro
5858
ydoc = new Y.Doc();
5959
YjsPluvProvider.globalDocs.set(docId, ydoc);
6060
YjsPluvProvider.docRefCounts.set(docId, 1);
61-
const wsUrl = config.realtime.serverUrl || 'ws://localhost:3001';
61+
const wsUrl = config.realtime.serverUrl || 'ws://localhost:3005';
6262
wsProvider = new WebsocketProvider(wsUrl, docId, ydoc, {
6363
connect: true,
6464
params: { room: docId }
@@ -80,28 +80,29 @@ export class YjsPluvProvider extends BaseChatDataProvider implements ChatDataPro
8080
this.messagesMap.observe(this.messagesObserver);
8181
this.roomsMap.observe(this.roomsObserver);
8282
this.typingMap.observe(this.typingObserver);
83+
84+
// Set connection state immediately to allow local operations
85+
this.setConnectionState('connected');
86+
8387
if (this.wsProvider) {
8488
this.wsProvider.off('status', this.handleWSStatus);
8589
this.wsProvider.off('sync', this.handleWSSync);
8690
this.wsProvider.on('status', this.handleWSStatus.bind(this));
8791
this.wsProvider.on('sync', this.handleWSSync.bind(this));
88-
const currentStatus = this.wsProvider.wsconnected ? 'connected' :
89-
this.wsProvider.wsconnecting ? 'connecting' : 'disconnected';
90-
this.setConnectionState(currentStatus as ConnectionState);
92+
93+
// Update connection state based on WebSocket status
9194
if (this.wsProvider.wsconnected) {
9295
this.setConnectionState('connected');
9396
} else if (this.wsProvider.wsconnecting) {
9497
this.setConnectionState('connecting');
95-
} else {
96-
this.setConnectionState('connecting');
9798
}
9899
}
99-
if (this.connectionState !== 'connected') {
100-
this.setConnectionState('connected');
101-
}
100+
101+
console.log('[YjsPluvProvider] ✅ Connected successfully with docId:', docId);
102102
return this.createSuccessResult(undefined);
103103
} catch (error) {
104104
this.setConnectionState('failed');
105+
console.error('[YjsPluvProvider] ❌ Connection failed:', error);
105106
return this.handleError(error, 'connect');
106107
}
107108
}
@@ -347,9 +348,16 @@ export class YjsPluvProvider extends BaseChatDataProvider implements ChatDataPro
347348

348349
async getAvailableRooms(userId: string, filter?: RoomListFilter): Promise<OperationResult<UnifiedRoom[]>> {
349350
try {
351+
console.log('[YjsPluvProvider] 🔍 Getting available rooms for user:', userId);
352+
console.log('[YjsPluvProvider] 📊 Connection state:', this.connectionState);
353+
console.log('[YjsPluvProvider] 📄 Yjs doc available:', !!this.ydoc);
354+
console.log('[YjsPluvProvider] 🗺️ Rooms map available:', !!this.roomsMap);
355+
350356
await this.ensureConnected();
351357
const allRooms = Array.from(this.roomsMap!.values());
352358

359+
console.log('[YjsPluvProvider] 📋 Total rooms found:', allRooms.length);
360+
353361
let filteredRooms = allRooms.filter(room => {
354362
if (!room.isActive) return false;
355363
if (filter?.type && room.type !== filter.type) return false;
@@ -361,8 +369,10 @@ export class YjsPluvProvider extends BaseChatDataProvider implements ChatDataPro
361369
return true;
362370
});
363371

372+
console.log('[YjsPluvProvider] ✅ Filtered rooms:', filteredRooms.length);
364373
return this.createSuccessResult(filteredRooms);
365374
} catch (error) {
375+
console.error('[YjsPluvProvider] ❌ Error in getAvailableRooms:', error);
366376
return this.handleError(error, 'getAvailableRooms');
367377
}
368378
}
@@ -881,8 +891,13 @@ export class YjsPluvProvider extends BaseChatDataProvider implements ChatDataPro
881891
}
882892

883893
private async ensureConnected(): Promise<void> {
884-
if (!this.ydoc || this.connectionState !== 'connected') {
885-
throw new Error('YjsPluvProvider is not connected');
894+
if (!this.ydoc) {
895+
throw new Error('YjsPluvProvider is not connected - no Yjs document available');
896+
}
897+
898+
// Allow operations even if WebSocket is still connecting, as Yjs works locally
899+
if (this.connectionState === 'failed' || this.connectionState === 'disconnected') {
900+
throw new Error('YjsPluvProvider is not connected - connection state: ' + this.connectionState);
886901
}
887902
}
888-
}
903+
}

client/packages/lowcoder/src/comps/hooks/hookComp.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { ThemeComp } from "./themeComp";
3737
import UrlParamsHookComp from "./UrlParamsHookComp";
3838
import { UtilsComp } from "./utilsComp";
3939
import { ScreenInfoHookComp } from "./screenInfoComp";
40+
import { ChatControllerComp } from "../comps/chatBoxComponent/chatControllerComp";
4041

4142
window._ = _;
4243
window.dayjs = dayjs;
@@ -118,6 +119,7 @@ const HookMap: HookCompMapRawType = {
118119
urlParams: UrlParamsHookComp,
119120
drawer: DrawerComp,
120121
theme: ThemeComp,
122+
chatController: ChatControllerComp,
121123
};
122124

123125
export const HookTmpComp = withTypeAndChildren(HookMap, "title", {
@@ -155,7 +157,8 @@ function SelectHookView(props: {
155157
if (
156158
(props.compType !== "modal" &&
157159
props.compType !== "drawer" &&
158-
props.compType !== "meeting") ||
160+
props.compType !== "meeting" &&
161+
props.compType !== "chatController") ||
159162
!selectedComp ||
160163
(editorState.selectSource !== "addComp" &&
161164
editorState.selectSource !== "leftPanel")

0 commit comments

Comments
 (0)