Skip to content

Commit ac5076b

Browse files
committed
feat: Room participants count fix: synchronize WebSocket participants across clients
1 parent 548c2c3 commit ac5076b

File tree

9 files changed

+174
-59
lines changed

9 files changed

+174
-59
lines changed

apps/collabydraw/components/CollaborationButton.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
import { useState } from "react";
44
import { CollaborationAdDialog } from "./CollaborationAdDialog";
5+
import CollaborationStartdDialog from "./CollaborationStartdDialog";
6+
import { useSession } from "next-auth/react";
57

68
export function CollaborationButton() {
79
const [isOpen, setIsOpen] = useState(false);
10+
const { data: session } = useSession();
811

912
const handleCollaborationClick = () => {
1013
setIsOpen(true);
@@ -26,7 +29,12 @@ export function CollaborationButton() {
2629
</div>
2730
<div className="welcome-screen-menu-item__text">Live collaboration...</div>
2831
</button>
29-
<CollaborationAdDialog open={isOpen} onOpenChange={setIsOpen} />
32+
33+
{session?.user && session?.user.id ? (
34+
<CollaborationStartdDialog open={isOpen} onOpenChange={setIsOpen} />
35+
) : (
36+
<CollaborationAdDialog open={isOpen} onOpenChange={setIsOpen} />
37+
)}
3038
</>
3139
);
3240
}

apps/collabydraw/components/CollaborationStartBtn.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { useSession } from "next-auth/react";
77
import { RoomSharingDialog } from "./RoomSharingDialog";
88
import { usePathname } from "next/navigation";
99
import { CollaborationAdDialog } from "./CollaborationAdDialog";
10+
import { cn } from "@/lib/utils";
1011

11-
export default function CollaborationStartBtn({ slug }: { slug?: string }) {
12+
export default function CollaborationStartBtn({ slug, participantsCount }: { slug?: string, participantsCount?: number }) {
1213
const pathname = usePathname();
1314
const [isOpen, setIsOpen] = useState(false);
1415
const { data: session } = useSession();
@@ -18,8 +19,10 @@ export default function CollaborationStartBtn({ slug }: { slug?: string }) {
1819
return (
1920
<div className="Start_Room_Session transition-transform duration-500 ease-in-out flex items-center justify-end">
2021
<Button type="button" onClick={() => setIsOpen(true)}
21-
className="excalidraw-button collab-button relative w-auto py-3 px-4 rounded-md text-[.875rem] font-semibold shadow-none bg-color-primary hover:bg-brand-hover active:bg-brand-active active:scale-[.98]"
22-
title="Live collaboration...">Share</Button>
22+
className={cn("excalidraw-button collab-button relative w-auto py-3 px-4 rounded-md text-[.875rem] font-semibold shadow-none active:scale-[.98]", roomSlug ? "bg-[#0fb884] dark:bg-[#0fb884] hover:bg-[#0fb884]" : "bg-color-primary hover:bg-brand-hover active:bg-brand-active")}
23+
title="Live collaboration...">Share {roomSlug && participantsCount && participantsCount > 0 && (
24+
<div className="CollabButton-collaborators text-[.6rem] text-[#2b8a3e] bg-[#b2f2bb] font-bold font-assistant rounded-[50%] p-1 min-w-4 min-h-4 w-4 h-4 flex items-center justify-center absolute bottom-[-5px] right-[-5px]">{participantsCount}</div>
25+
)}</Button>
2326

2427
{session?.user && session?.user.id ? (
2528
roomSlug ? (
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useSession } from "next-auth/react";
2+
import Link from "next/link";
3+
4+
export default function SignupWelcomeButton() {
5+
const { data: session } = useSession();
6+
return (
7+
<>
8+
{
9+
session?.user.id ? (
10+
<></>
11+
) : (
12+
<Link className="welcome-screen-menu-item " href="/auth/signup" target="_blank" rel="noreferrer">
13+
<div className="welcome-screen-menu-item__icon">
14+
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 24 24" className="" fill="none" strokeWidth="2" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round">
15+
<g strokeWidth="1.5">
16+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
17+
<path d="M15 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"></path>
18+
<path d="M21 12h-13l3 -3"></path>
19+
<path d="M11 15l-3 -3"></path>
20+
</g>
21+
</svg>
22+
</div>
23+
<div className="welcome-screen-menu-item__text">Sign up</div>
24+
</Link >
25+
)
26+
}
27+
</>
28+
)
29+
}

apps/collabydraw/components/UserRoomsListDialog.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { getUserRooms, joinRoom, deleteRoom } from "@/actions/room";
88
import { useRouter } from "next/navigation";
99
import { useEffect, useState, useTransition } from "react";
1010
import { Skeleton } from "./ui/skeleton";
11+
import CollaborationStartdDialog from "./CollaborationStartdDialog";
1112

1213
type RoomType = {
1314
id: number;
@@ -27,6 +28,7 @@ export function UserRoomsListDialog({
2728
onOpenChange,
2829
isMobile = false
2930
}: UserRoomsListDialogProps) {
31+
const [isOpen, setIsOpen] = useState(false);
3032
const [isPending, startTransition] = useTransition();
3133
const [rooms, setRooms] = useState<RoomType[]>([]);
3234
const [isLoading, setIsLoading] = useState(false);
@@ -185,14 +187,11 @@ export function UserRoomsListDialog({
185187
))
186188
) : (
187189
<div className="text-center py-8 text-gray-400">
188-
<p>No rooms found.</p>
189-
<Button
190-
onClick={() => router.push('/create-room')}
191-
className="mt-4 bg-blue-500 hover:bg-blue-600 text-white"
192-
size="sm"
193-
>
194-
Create a Room
195-
</Button>
190+
<p className="mb-4">No rooms found.</p>
191+
<Button type="button" onClick={() => { setIsOpen(true); }}
192+
className="excalidraw-button collab-button relative w-auto py-3 px-4 rounded-md text-[.875rem] font-semibold shadow-none bg-color-primary hover:bg-brand-hover active:bg-brand-active active:scale-[.98]"
193+
title="Live collaboration...">Create a Room</Button>
194+
<CollaborationStartdDialog open={isOpen} onOpenChange={setIsOpen} />
196195
</div>
197196
)}
198197
</div>

apps/collabydraw/components/canvas/CanvasSheet.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function CanvasSheet({ roomName, roomId, userId, userName }: { roomName:
4040
const [canvasColor, setCanvasColor] = useState<string>(canvasBgLight[0]);
4141
const canvasColorRef = useRef(canvasColor);
4242

43-
const { isConnected, messages, sendMessage } = useWebSocket(
43+
const { isConnected, messages, sendMessage, participants } = useWebSocket(
4444
roomId,
4545
roomName,
4646
userId,
@@ -77,6 +77,7 @@ export function CanvasSheet({ roomName, roomId, userId, userName }: { roomName:
7777
messages.forEach((message) => {
7878
try {
7979
const data = JSON.parse(message.content);
80+
console.log('ws msg data = ', data)
8081
if (data.type === "draw") {
8182
const shape = JSON.parse(data.data).shape;
8283
setExistingShapes((prevShapes) => [...prevShapes, shape]);
@@ -96,6 +97,14 @@ export function CanvasSheet({ roomName, roomId, userId, userName }: { roomName:
9697
}
9798
}, [messages]);
9899

100+
useEffect(() => {
101+
try {
102+
console.log('participants = ', participants)
103+
} catch (e) {
104+
console.error("Error processing messages:", e);
105+
}
106+
}, [participants]);
107+
99108
useEffect(() => {
100109
game?.setTool(activeTool)
101110
game?.setStrokeWidth(strokeWidth)
@@ -298,7 +307,7 @@ export function CanvasSheet({ roomName, roomId, userId, userName }: { roomName:
298307
onToolSelect={setActiveTool}
299308
/>
300309
{matches && (
301-
<CollaborationStart slug={roomName} />
310+
<CollaborationStart participantsCount={participants.length} slug={roomName} />
302311
)}
303312
</div>
304313

@@ -323,6 +332,11 @@ export function CanvasSheet({ roomName, roomId, userId, userName }: { roomName:
323332
bgFill={bgFill}
324333
setBgFill={setBgFill}
325334

335+
strokeEdge={strokeEdge}
336+
setStrokeEdge={setStrokeEdge}
337+
strokeStyle={strokeStyle}
338+
setStrokeStyle={setStrokeStyle}
339+
326340
roomName={roomName}
327341
/>
328342
)}

apps/collabydraw/components/welcome-screen.tsx

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import Link from "next/link"
21
import { CollaborationButton } from "./CollaborationButton"
2+
import SignupWelcomeButton from "./SignupWelcomeButton";
33

44
export function MainMenuWelcome() {
55
return (
@@ -50,36 +50,9 @@ export function HomeWelcome() {
5050
<div className="welcome-screen-menu-item__shortcut">Ctrl+O</div>
5151
</button>
5252

53-
{/* <button type="button" className="welcome-screen-menu-item">
54-
<div className="welcome-screen-menu-item__icon">
55-
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 24 24" className="" fill="none" strokeWidth="2" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round">
56-
<g strokeWidth="1.5">
57-
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
58-
<circle cx="12" cy="12" r="9"></circle>
59-
<line x1="12" y1="17" x2="12" y2="17.01"></line>
60-
<path d="M12 13.5a1.5 1.5 0 0 1 1 -1.5a2.6 2.6 0 1 0 -3 -4"></path>
61-
</g>
62-
</svg>
63-
</div>
64-
<div className="welcome-screen-menu-item__text">Help</div>
65-
<div className="welcome-screen-menu-item__shortcut">?</div>
66-
</button> */}
67-
6853
<CollaborationButton />
69-
<Link className="welcome-screen-menu-item " href="/auth/signup" target="_blank" rel="noreferrer">
70-
<div className="welcome-screen-menu-item__icon">
71-
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 24 24" className="" fill="none" strokeWidth="2" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round">
72-
<g strokeWidth="1.5">
73-
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
74-
<path d="M15 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"></path>
75-
<path d="M21 12h-13l3 -3"></path>
76-
<path d="M11 15l-3 -3"></path>
77-
</g>
78-
</svg>
79-
</div>
80-
<div className="welcome-screen-menu-item__text">Sign up</div>
81-
</Link>
82-
</div>
54+
<SignupWelcomeButton />
55+
</div >
8356
</div >
8457
</>
8558
)

apps/collabydraw/hooks/useWebSocket.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,29 @@ export function useWebSocket(roomId: string, roomName: string, userId: string, u
4545
const handleMessage = (event: MessageEvent) => {
4646
try {
4747
const data: WebSocketMessage = JSON.parse(event.data);
48+
if (data.participants) {
49+
setParticipants(data.participants);
50+
}
4851
switch (data.type) {
4952
case WS_DATA_TYPE.USER_JOINED:
50-
setParticipants(prev => {
51-
const exists = prev.some(p => p.userId === data.userId);
52-
if (!exists && data.userId && data.userName) {
53-
return [...prev, {
54-
userId: data.userId,
55-
userName: data.userName
56-
}];
57-
}
58-
return prev;
59-
});
53+
if (!data.participants && data.userId && data.userName) {
54+
setParticipants(prev => {
55+
const exists = prev.some(p => p.userId === data.userId);
56+
if (!exists && data.userId && data.userName) {
57+
return [...prev, {
58+
userId: data.userId,
59+
userName: data.userName
60+
}];
61+
}
62+
return prev;
63+
});
64+
}
6065
break;
6166

6267
case WS_DATA_TYPE.USER_LEFT:
68+
// if (!data.participants) {
69+
// setParticipants(prev => prev.filter(user => user.userId !== data.userId));
70+
// }
6371
setParticipants(prev => prev.filter(user => user.userId !== data.userId));
6472
break;
6573

apps/ws/src/index.ts

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,41 @@ wss.on("connection", function connection(ws, req) {
6363
}
6464

6565
const userId = req.user.id;
66-
const user: User = {
66+
let user: User = {
6767
userId,
6868
userName: userId,
6969
ws,
7070
rooms: [],
7171
};
72+
73+
const existingUserIndex = users.findIndex((u) => u.userId === userId);
74+
if (existingUserIndex !== -1) {
75+
console.log(`User ${userId} reconnecting - cleaning up old connection`);
76+
const existingUser = users[existingUserIndex];
77+
if (!existingUser) {
78+
console.error("Error: Existing User not found.");
79+
return;
80+
}
81+
// Keep the rooms they were in
82+
const existingRooms = [...existingUser.rooms];
83+
// Remove old connection
84+
users.splice(existingUserIndex, 1);
85+
// Add back with new connection but keep rooms
86+
user = {
87+
userId,
88+
userName: existingUser.userName || userId,
89+
ws,
90+
rooms: existingRooms,
91+
};
92+
} else {
93+
user = {
94+
userId,
95+
userName: userId,
96+
ws,
97+
rooms: [],
98+
};
99+
}
100+
72101
users.push(user);
73102

74103
console.log(`User ${userId} connected`);
@@ -97,8 +126,40 @@ wss.on("connection", function connection(ws, req) {
97126
console.error("No roomId provided for JOIN message");
98127
return;
99128
}
129+
130+
if (!user.rooms.includes(parsedData.roomId)) {
131+
user.rooms.push(parsedData.roomId);
132+
}
133+
134+
const uniqueParticipantsMap = new Map();
135+
users
136+
.filter(
137+
(u) => u.rooms.includes(parsedData.roomId!)
138+
)
139+
.forEach((u) =>
140+
uniqueParticipantsMap.set(u.userId, {
141+
userId: u.userId,
142+
userName: u.userName,
143+
})
144+
);
145+
146+
const currentParticipants = Array.from(
147+
uniqueParticipantsMap.values()
148+
);
149+
150+
ws.send(
151+
JSON.stringify({
152+
type: WS_DATA_TYPE.USER_JOINED,
153+
userId: user.userId,
154+
roomId: parsedData.roomId,
155+
userName: parsedData.userName,
156+
timestamp: new Date().toISOString(),
157+
participants: currentParticipants,
158+
})
159+
);
160+
100161
console.log(`User ${userId} joining room ${parsedData.roomId}`);
101-
user.rooms.push(parsedData.roomId);
162+
102163
broadcastToRoom(
103164
parsedData.roomId,
104165
{
@@ -107,8 +168,10 @@ wss.on("connection", function connection(ws, req) {
107168
roomId: parsedData.roomId,
108169
userName: parsedData.userName,
109170
timestamp: new Date().toISOString(),
171+
participants: currentParticipants,
110172
},
111-
[]
173+
[user.userId],
174+
false
112175
);
113176
break;
114177

@@ -124,7 +187,8 @@ wss.on("connection", function connection(ws, req) {
124187
userName: user.userName,
125188
roomId: parsedData.roomId,
126189
},
127-
[user.userId]
190+
[user.userId],
191+
true
128192
);
129193
break;
130194

@@ -266,8 +330,23 @@ wss.on("connection", function connection(ws, req) {
266330
function broadcastToRoom(
267331
roomId: string,
268332
message: WebSocketMessage,
269-
excludeUsers: string[] = []
333+
excludeUsers: string[] = [],
334+
includeParticipants: boolean = false
270335
) {
336+
if (includeParticipants && !message.participants) {
337+
const uniqueParticipantsMap = new Map();
338+
users
339+
.filter((u) => u.rooms.includes(roomId))
340+
.forEach((u) =>
341+
uniqueParticipantsMap.set(u.userId, {
342+
userId: u.userId,
343+
userName: u.userName,
344+
})
345+
);
346+
347+
const currentParticipants = Array.from(uniqueParticipantsMap.values());
348+
message.participants = currentParticipants;
349+
}
271350
users.forEach((u) => {
272351
if (u.rooms.includes(roomId) && !excludeUsers.includes(u.userId)) {
273352
u.ws.send(JSON.stringify(message));

0 commit comments

Comments
 (0)