Skip to content

Commit d5b6b62

Browse files
committed
add online presence rooms
1 parent 6ae7385 commit d5b6b62

5 files changed

Lines changed: 176 additions & 10 deletions

File tree

client/packages/lowcoder/src/comps/comps/chatBoxComponentv2/ChatBoxContext.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createContext, useContext } from "react";
2-
import type { ChatRoom, PendingRoomInvite } from "./store";
2+
import type { ChatRoom, OnlineUser, PendingRoomInvite } from "./store";
33

44
type ChatEventName =
55
| "messageSent"
@@ -27,6 +27,7 @@ export interface ChatBoxContextValue {
2727
currentUserId: string;
2828
currentUserName: string;
2929
typingUsers: any[];
30+
onlineUsers: OnlineUser[];
3031
pendingInvites: PendingRoomInvite[];
3132

3233
// Exposed state

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ const childrenMap = {
105105
rooms: jsonArrayControl([]),
106106
currentRoomId: withDefault(StringControl, ""),
107107
pendingInvites: jsonArrayControl([]),
108+
onlineUsers: jsonArrayControl([]),
108109
showRoomsPanel: withDefault(BoolControl, true),
109110
roomsPanelWidth: withDefault(StringControl, "240px"),
110111
allowRoomCreation: withDefault(BoolControl, true),
@@ -186,6 +187,11 @@ const ChatBoxPropertyView = React.memo((props: { children: any }) => {
186187
tooltip:
187188
"Array of users currently typing. Bind to {{ chatController1.typingUsers }}",
188189
})}
190+
{children.onlineUsers.propertyView({
191+
label: "Online Users",
192+
tooltip:
193+
"Array of online users with presence. Bind to {{ chatController1.onlineUsers }}. Shape: [{ userId, userName, currentRoomId }]",
194+
})}
189195
</Section>
190196

191197
<Section name="Display">
@@ -225,6 +231,7 @@ let ChatBoxV2Tmp = (function () {
225231
const messages = Array.isArray(props.messages) ? props.messages : [];
226232
const rooms = (Array.isArray(props.rooms) ? props.rooms : []) as unknown as ChatRoom[];
227233
const typingUsers = Array.isArray(props.typingUsers) ? props.typingUsers : [];
234+
const onlineUsers = Array.isArray(props.onlineUsers) ? props.onlineUsers : [];
228235
const pendingInvites = (Array.isArray(props.pendingInvites)
229236
? props.pendingInvites
230237
: []) as unknown as PendingRoomInvite[];
@@ -238,6 +245,7 @@ let ChatBoxV2Tmp = (function () {
238245
currentUserId: props.currentUserId,
239246
currentUserName: props.currentUserName,
240247
typingUsers,
248+
onlineUsers: onlineUsers as any,
241249
pendingInvites,
242250

243251
chatTitle: props.chatTitle,

client/packages/lowcoder/src/comps/comps/chatBoxComponentv2/components/ChatBoxView.tsx

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import React, { useState } from "react";
1+
import React, { useMemo, useState } from "react";
22
import {
33
Wrapper,
44
ChatPanelContainer,
55
ChatHeaderBar,
6+
OnlineCountBadge,
7+
OnlineCountDot,
68
} from "../styles";
79
import { MessageList } from "./MessageList";
810
import { InputBar } from "./InputBar";
@@ -21,6 +23,14 @@ export const ChatBoxView = React.memo(() => {
2123
? ctx.currentRoom.name
2224
: ctx.chatTitle.value;
2325

26+
// Count users online in the current room (peers only, not counting self)
27+
const roomOnlineCount = useMemo(() => {
28+
if (!ctx.currentRoomId) return 0;
29+
return ctx.onlineUsers.filter(
30+
(u) => u.currentRoomId === ctx.currentRoomId && u.userId !== ctx.currentUserId,
31+
).length + 1; // +1 for self
32+
}, [ctx.onlineUsers, ctx.currentRoomId, ctx.currentUserId]);
33+
2434
return (
2535
<Wrapper $style={ctx.style} $anim={ctx.animationStyle}>
2636
{/* ── Rooms sidebar ───────────────────────────────────────── */}
@@ -39,13 +49,21 @@ export const ChatBoxView = React.memo(() => {
3949
<ChatPanelContainer>
4050
{ctx.showHeader && (
4151
<ChatHeaderBar>
42-
<div style={{ fontWeight: 600, fontSize: 16 }}>
43-
{headerTitle}
44-
</div>
45-
{ctx.currentRoom?.description && (
46-
<div style={{ fontSize: 12, color: "#888", marginTop: 2 }}>
47-
{ctx.currentRoom.description}
52+
<div>
53+
<div style={{ fontWeight: 600, fontSize: 16 }}>
54+
{headerTitle}
4855
</div>
56+
{ctx.currentRoom?.description && (
57+
<div style={{ fontSize: 12, color: "#888", marginTop: 2 }}>
58+
{ctx.currentRoom.description}
59+
</div>
60+
)}
61+
</div>
62+
{ctx.currentRoomId && roomOnlineCount > 0 && (
63+
<OnlineCountBadge>
64+
<OnlineCountDot />
65+
{roomOnlineCount} online
66+
</OnlineCountBadge>
4967
)}
5068
</ChatHeaderBar>
5169
)}

client/packages/lowcoder/src/comps/comps/chatBoxComponentv2/components/RoomPanel.tsx

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from "react";
1+
import React, { useMemo, useState } from "react";
22
import { Button, Input, Tooltip, Popconfirm } from "antd";
33
import {
44
PlusOutlined,
@@ -9,15 +9,22 @@ import {
99
RobotOutlined,
1010
MailOutlined,
1111
UserAddOutlined,
12+
TeamOutlined,
1213
} from "@ant-design/icons";
13-
import type { ChatRoom } from "../store";
14+
import type { ChatRoom, OnlineUser } from "../store";
1415
import {
1516
RoomPanelContainer,
1617
RoomPanelHeader,
1718
RoomListContainer,
1819
RoomItemStyled,
1920
SearchResultBadge,
2021
LlmRoomBadge,
22+
OnlinePresenceSection,
23+
OnlinePresenceLabel,
24+
OnlineUserItem,
25+
OnlineAvatar,
26+
OnlineDot,
27+
OnlineUserName,
2128
} from "../styles";
2229
import { useChatBox } from "../ChatBoxContext";
2330

@@ -31,17 +38,33 @@ export const RoomPanel = React.memo((props: RoomPanelProps) => {
3138
const {
3239
rooms,
3340
currentRoomId,
41+
currentUserId,
42+
currentUserName,
3443
allowRoomCreation,
3544
allowRoomSearch,
3645
roomsPanelWidth,
3746
pendingInvites,
47+
onlineUsers,
3848
onRoomSwitch,
3949
onRoomJoin,
4050
onRoomLeave,
4151
onInviteAccept,
4252
onInviteDecline,
4353
} = useChatBox();
4454

55+
// Users in the current room (from Pluv presence), plus self
56+
const roomOnlineUsers = useMemo<OnlineUser[]>(() => {
57+
const peers = onlineUsers.filter(
58+
(u) => u.currentRoomId === currentRoomId && u.userId !== currentUserId,
59+
);
60+
const self: OnlineUser = {
61+
userId: currentUserId,
62+
userName: currentUserName,
63+
currentRoomId,
64+
};
65+
return currentRoomId ? [self, ...peers] : peers;
66+
}, [onlineUsers, currentRoomId, currentUserId, currentUserName]);
67+
4568
const [searchQuery, setSearchQuery] = useState("");
4669
const [searchResults, setSearchResults] = useState<ChatRoom[]>([]);
4770
const [isSearchMode, setIsSearchMode] = useState(false);
@@ -308,12 +331,48 @@ export const RoomPanel = React.memo((props: RoomPanelProps) => {
308331
</>
309332
)}
310333
</RoomListContainer>
334+
335+
{/* ── Online Presence ─────────────────────────────────────── */}
336+
{currentRoomId && roomOnlineUsers.length > 0 && (
337+
<OnlinePresenceSection>
338+
<OnlinePresenceLabel>
339+
<TeamOutlined />
340+
Online — {roomOnlineUsers.length}
341+
</OnlinePresenceLabel>
342+
{roomOnlineUsers.map((user) => (
343+
<OnlineUserItem key={user.userId}>
344+
<OnlineAvatar $color={avatarColor(user.userId)}>
345+
{(user.userName || user.userId).slice(0, 1).toUpperCase()}
346+
<OnlineDot />
347+
</OnlineAvatar>
348+
<OnlineUserName title={user.userName}>
349+
{user.userId === currentUserId ? `${user.userName} (You)` : user.userName}
350+
</OnlineUserName>
351+
</OnlineUserItem>
352+
))}
353+
</OnlinePresenceSection>
354+
)}
311355
</RoomPanelContainer>
312356
);
313357
});
314358

315359
RoomPanel.displayName = "RoomPanel";
316360

361+
// ── Avatar color helper ───────────────────────────────────────────────────────
362+
363+
const AVATAR_PALETTE = [
364+
"#1890ff", "#52c41a", "#fa8c16", "#722ed1",
365+
"#eb2f96", "#13c2c2", "#faad14", "#f5222d",
366+
];
367+
368+
function avatarColor(userId: string): string {
369+
let hash = 0;
370+
for (let i = 0; i < userId.length; i++) {
371+
hash = userId.charCodeAt(i) + ((hash << 5) - hash);
372+
}
373+
return AVATAR_PALETTE[Math.abs(hash) % AVATAR_PALETTE.length];
374+
}
375+
317376
// ── Section label ─────────────────────────────────────────────────────────────
318377

319378
const RoomSectionLabel = React.memo(({ label }: { label: string }) => (

client/packages/lowcoder/src/comps/comps/chatBoxComponentv2/styles.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,3 +346,83 @@ export const LlmRoomBadge = styled.span`
346346
padding: 1px 5px;
347347
flex-shrink: 0;
348348
`;
349+
350+
// ── Online Presence styles ──────────────────────────────────────────────────
351+
352+
export const OnlinePresenceSection = styled.div`
353+
border-top: 1px solid #eee;
354+
padding: 8px;
355+
flex-shrink: 0;
356+
`;
357+
358+
export const OnlinePresenceLabel = styled.div`
359+
font-size: 10px;
360+
font-weight: 600;
361+
color: #aaa;
362+
letter-spacing: 0.6px;
363+
text-transform: uppercase;
364+
padding: 4px 2px 6px;
365+
display: flex;
366+
align-items: center;
367+
gap: 6px;
368+
`;
369+
370+
export const OnlineUserItem = styled.div`
371+
display: flex;
372+
align-items: center;
373+
gap: 7px;
374+
padding: 4px 2px;
375+
font-size: 12px;
376+
color: #444;
377+
overflow: hidden;
378+
`;
379+
380+
export const OnlineAvatar = styled.div<{ $color: string }>`
381+
width: 22px;
382+
height: 22px;
383+
border-radius: 50%;
384+
background: ${(p) => p.$color};
385+
color: #fff;
386+
font-size: 10px;
387+
font-weight: 600;
388+
display: flex;
389+
align-items: center;
390+
justify-content: center;
391+
flex-shrink: 0;
392+
position: relative;
393+
`;
394+
395+
export const OnlineDot = styled.span`
396+
position: absolute;
397+
bottom: -1px;
398+
right: -1px;
399+
width: 7px;
400+
height: 7px;
401+
border-radius: 50%;
402+
background: #52c41a;
403+
border: 1.5px solid #fafbfc;
404+
`;
405+
406+
export const OnlineUserName = styled.span`
407+
flex: 1;
408+
overflow: hidden;
409+
text-overflow: ellipsis;
410+
white-space: nowrap;
411+
`;
412+
413+
export const OnlineCountBadge = styled.span`
414+
display: inline-flex;
415+
align-items: center;
416+
gap: 4px;
417+
font-size: 11px;
418+
color: #52c41a;
419+
font-weight: 500;
420+
`;
421+
422+
export const OnlineCountDot = styled.span`
423+
width: 7px;
424+
height: 7px;
425+
border-radius: 50%;
426+
background: #52c41a;
427+
display: inline-block;
428+
`;

0 commit comments

Comments
 (0)