Skip to content

Commit 25c7ab4

Browse files
committed
feat: Enhance collaboration UI and WebSocket resilience
- Add ParticipantAvatars component with overflow handling - Implement WebSocket automatic reconnection - Update shape synchronization for newly joined users - Improve real-time collaboration user experience
1 parent 93e8db0 commit 25c7ab4

File tree

10 files changed

+166
-73
lines changed

10 files changed

+166
-73
lines changed

apps/collabydraw/actions/shape.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import client from "@repo/db/client";
55
import { JoinRoomSchema } from "@repo/common/types";
66
import { getServerSession } from "next-auth";
77
import { authOptions } from "@/utils/auth";
8+
import { Shape } from "@/types/canvas";
89

910
export async function getShapes(data: { roomName: string }) {
1011
try {
@@ -26,7 +27,7 @@ export async function getShapes(data: { roomName: string }) {
2627
return { success: true, shapes: [] };
2728
}
2829

29-
const shapes = shapesResponse.map((x) => JSON.parse(x.message));
30+
const shapes: Shape[] = shapesResponse.map((x) => JSON.parse(x.message));
3031

3132
return { success: true, shapes };
3233
} catch (error) {

apps/collabydraw/app/(canvas)/canvas/[roomName]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default async function CanvasPage({ params }: { params: Promise<{ roomNam
2020
const user = session?.user;
2121
if (!user || !user.id) {
2222
console.error('User from session not found.');
23-
redirect(`/`);
23+
redirect('/auth/signin?callbackUrl=' + encodeURIComponent(`/canvas/${decodedParam}`));
2424
}
2525
console.log('CanvasPage loaded')
2626

apps/collabydraw/components/CollaborationStartBtn.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,24 @@ import { CollaborationAdDialog } from "./CollaborationAdDialog";
1010
import { cn } from "@/lib/utils";
1111
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
1212
import { RoomParticipants } from "@repo/common/types";
13+
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
1314

1415
export default function CollaborationStartBtn({ slug, participants, onCloseRoom }: { slug?: string, participants?: RoomParticipants[], onCloseRoom?: () => void; }) {
1516
const pathname = usePathname();
1617
const [isOpen, setIsOpen] = useState(false);
1718
const { data: session } = useSession();
1819
const roomSlug = slug;
1920
const decodedPathname = decodeURIComponent(pathname);
21+
const displayParticipants = participants?.slice(0, 3);
22+
const remainingParticipants = participants?.slice(3);
2023

2124
return (
2225
<div className="Start_Room_Session transition-transform duration-500 ease-in-out flex items-center justify-end">
2326
<div className="UserList__wrapper flex w-full justify-end items-center">
2427
<div className="UserList p-1 flex flex-wrap justify-end items-center gap-[.625rem]">
2528
<TooltipProvider delayDuration={0}>
2629
<div className="flex space-x-2">
27-
{participants?.map((participant) => (
30+
{displayParticipants?.map((participant) => (
2831
<Tooltip key={participant.userId}>
2932
<TooltipTrigger asChild>
3033
<div style={{ backgroundColor: getClientColor(participant) }} className={`w-7 h-7 rounded-full flex items-center justify-center cursor-pointer`}>
@@ -38,6 +41,40 @@ export default function CollaborationStartBtn({ slug, participants, onCloseRoom
3841
</TooltipContent>
3942
</Tooltip>
4043
))}
44+
45+
{remainingParticipants && remainingParticipants.length > 0 && (
46+
<Popover>
47+
<PopoverTrigger asChild>
48+
<div
49+
className="w-7 h-7 rounded-full bg-gray-200 flex items-center justify-center
50+
cursor-pointer hover:bg-gray-400 transition-colors"
51+
>
52+
<span className="text-xs font-bold text-gray-900">
53+
+{remainingParticipants?.length}
54+
</span>
55+
</div>
56+
</PopoverTrigger>
57+
<PopoverContent className="w-full max-h-[200px] h-full overflow-auto p-4 mt-3 rounded-lg">
58+
<div className="space-y-2">
59+
<h4 className="text-sm font-medium font-assistant">Additional Participants</h4>
60+
{remainingParticipants?.map((participant) => (
61+
<div
62+
key={participant.userId}
63+
className="cursor-pointer select-none flex items-center space-x-2 h-8 w-full justify-start gap-2 rounded-md px-3 text-sm font-medium transition-colors text-color-on-surface hover:text-color-on-surface bg-transparent hover:bg-button-hover-bg focus-visible:shadow-brand-color-shadow focus-visible:outline-none focus-visible:ring-0 active:bg-button-hover-bg active:border active:border-brand-active dark:hover:bg-w-button-hover-bg"
64+
>
65+
<div style={{ backgroundColor: getClientColor(participant) }} className={`w-7 h-7 rounded-full flex items-center justify-center cursor-pointer`}>
66+
<span className="text-sm font-bold text-gray-900 dark:text-gray-900">
67+
{participant.userName.charAt(0).toUpperCase()}
68+
</span>
69+
</div>
70+
71+
<span className="text-sm text-color-on-surface">{participant.userName}</span>
72+
</div>
73+
))}
74+
</div>
75+
</PopoverContent>
76+
</Popover>
77+
)}
4178
</div>
4279
</TooltipProvider>
4380
</div>

apps/collabydraw/components/RoomSharingDialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function RoomSharingDialog({ open, onOpenChange, link, onCloseRoom }: { o
4040

4141
<div className="space-y-6">
4242
<div className="text-text-primary-color">
43-
<p className="font-semibold mb-2">Linkkkkkkkkkkkkkkkkkkkkk</p>
43+
<p className="font-semibold mb-2">Link</p>
4444
<div className="flex items-center gap-2">
4545
<div className="flex-1 bg-collaby-textfield border border-collaby-textfield rounded-md px-3 py-2 text-text-primary-color overflow-hidden text-ellipsis">
4646
{roomLink}

apps/collabydraw/components/canvas/CanvasSheet.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export default function CanvasSheet({ roomName, roomId, userId, userName, token
5151
const {
5252
isConnected,
5353
messages,
54+
existingMsgs,
5455
sendMessage,
5556
participants
5657
} = useWebSocket(paramsRef.current.roomId, paramsRef.current.roomName, paramsRef.current.userId, paramsRef.current.userName, paramsRef.current.token);
@@ -197,9 +198,18 @@ export default function CanvasSheet({ roomName, roomId, userId, userName, token
197198
console.log('E8')
198199
}, [messages, canvasState.game, processMessages]);
199200

201+
useEffect(() => {
202+
if (existingMsgs?.message && canvasState.game) {
203+
console.log('Updating shapes with existing messages:', existingMsgs.message);
204+
canvasState.game.updateShapes(existingMsgs.message);
205+
}
206+
console.log('E9');
207+
}, [existingMsgs, canvasState.game]);
208+
209+
200210
const toggleSidebar = useCallback(() => {
201211
setCanvasState(prev => ({ ...prev, sidebarOpen: !prev.sidebarOpen }));
202-
console.log('E9')
212+
console.log('E10')
203213
}, []);
204214

205215
if (isLoading) {

apps/collabydraw/draw/Game.ts

Lines changed: 36 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
import { SelectionManager } from "./SelectionManager";
99
import { v4 as uuidv4 } from "uuid";
1010
import { WS_DATA_TYPE } from "@repo/common/types";
11-
import { getShapes } from "@/actions/shape";
1211

1312
const CORNER_RADIUS_FACTOR = 20;
1413
const RECT_CORNER_RADIUS_FACTOR = CORNER_RADIUS_FACTOR;
@@ -34,7 +33,7 @@ export class Game {
3433
private roomId: string | null;
3534
private canvasBgColor: string;
3635
private sendMessage: ((data: string) => void) | null;
37-
private existingShape: Shape[];
36+
private existingShapes: Shape[];
3837
private clicked: boolean;
3938
private roomName: string | null;
4039
private activeTool: ToolType = "grab";
@@ -70,7 +69,7 @@ export class Game {
7069
this.roomId = roomId;
7170
this.sendMessage = sendMessage;
7271
this.clicked = false;
73-
this.existingShape = [];
72+
this.existingShapes = [];
7473
this.canvas.width = document.body.clientWidth;
7574
this.canvas.height = document.body.clientHeight;
7675
this.onScaleChangeCallback = onScaleChangeCallback;
@@ -84,7 +83,7 @@ export class Game {
8483
if (this.isStandalone) {
8584
localStorage.setItem(
8685
LOCALSTORAGE_CANVAS_KEY,
87-
JSON.stringify(this.existingShape)
86+
JSON.stringify(this.existingShapes)
8887
);
8988
}
9089
});
@@ -96,38 +95,11 @@ export class Game {
9695
const storedShapes = localStorage.getItem(LOCALSTORAGE_CANVAS_KEY);
9796
if (storedShapes) {
9897
const parsedShapes = JSON.parse(storedShapes);
99-
this.existingShape = [...this.existingShape, ...parsedShapes];
98+
this.existingShapes = [...this.existingShapes, ...parsedShapes];
10099
}
101100
} catch (e) {
102101
console.error("Error loading shapes from localStorage:", e);
103102
}
104-
} else if (!this.isStandalone && this.roomName) {
105-
try {
106-
const getShapesResult = await getShapes({ roomName: this.roomName });
107-
108-
if (getShapesResult.success && getShapesResult.shapes?.length) {
109-
getShapesResult.shapes.forEach((shape: Shape, index: number) => {
110-
try {
111-
const alreadyExists = this.existingShape.some(
112-
(s) => s.id === shape.id
113-
);
114-
115-
if (!alreadyExists) {
116-
this.existingShape.push(shape);
117-
console.log("init(): Pushing shape from getShapesResult");
118-
} else {
119-
console.log(`Shape ${shape.id} already exists. Skipping.`);
120-
}
121-
} catch (e) {
122-
console.error(`Error processing shape ${index}:`, e);
123-
}
124-
});
125-
} else if (!getShapesResult.success) {
126-
console.error("Error fetching room: " + getShapesResult.error);
127-
}
128-
} catch (error) {
129-
console.error("Error in init:", error);
130-
}
131103
}
132104
this.clearCanvas();
133105
}
@@ -166,7 +138,20 @@ export class Game {
166138
}
167139

168140
updateShapes(shapes: Shape[]) {
169-
this.existingShape = shapes;
141+
shapes.forEach((newShape) => {
142+
const existingIndex = this.existingShapes.findIndex(
143+
(s) => s.id === newShape.id
144+
);
145+
if (existingIndex !== -1) {
146+
this.existingShapes[existingIndex] = {
147+
...this.existingShapes[existingIndex],
148+
...newShape,
149+
};
150+
} else {
151+
this.existingShapes.push(newShape);
152+
}
153+
});
154+
170155
this.clearCanvas();
171156
}
172157

@@ -205,7 +190,7 @@ export class Game {
205190
this.canvas.height / this.scale
206191
);
207192

208-
this.existingShape.map((shape: Shape) => {
193+
this.existingShapes.map((shape: Shape) => {
209194
if (shape.type === "rectangle") {
210195
this.drawRect(
211196
shape.x,
@@ -299,15 +284,15 @@ export class Game {
299284
) {
300285
const selectedShape = this.selectionManager.getSelectedShape();
301286
if (selectedShape) {
302-
const index = this.existingShape.findIndex(
287+
const index = this.existingShapes.findIndex(
303288
(shape) => shape.id === selectedShape.id
304289
);
305-
this.existingShape[index] = selectedShape;
290+
this.existingShapes[index] = selectedShape;
306291
if (index !== -1) {
307292
if (this.isStandalone) {
308293
localStorage.setItem(
309294
LOCALSTORAGE_CANVAS_KEY,
310-
JSON.stringify(this.existingShape)
295+
JSON.stringify(this.existingShapes)
311296
);
312297
} else if (this.sendMessage && this.roomId) {
313298
this.sendMessage(
@@ -332,7 +317,7 @@ export class Game {
332317
if (this.selectedShape) {
333318
localStorage.setItem(
334319
LOCALSTORAGE_CANVAS_KEY,
335-
JSON.stringify(this.existingShape)
320+
JSON.stringify(this.existingShapes)
336321
);
337322
}
338323

@@ -420,7 +405,8 @@ export class Game {
420405
break;
421406

422407
case "pen":
423-
const currentShape = this.existingShape[this.existingShape.length - 1];
408+
const currentShape =
409+
this.existingShapes[this.existingShapes.length - 1];
424410
if (currentShape?.type === "pen") {
425411
shape = {
426412
id: uuidv4(),
@@ -442,13 +428,13 @@ export class Game {
442428
return;
443429
}
444430

445-
this.existingShape.push(shape);
431+
this.existingShapes.push(shape);
446432

447433
if (this.isStandalone) {
448434
try {
449435
localStorage.setItem(
450436
LOCALSTORAGE_CANVAS_KEY,
451-
JSON.stringify(this.existingShape)
437+
JSON.stringify(this.existingShapes)
452438
);
453439
} catch (e) {
454440
console.error("Error saving shapes to localStorage:", e);
@@ -511,8 +497,8 @@ export class Game {
511497
return;
512498
}
513499
}
514-
for (let i = this.existingShape.length - 1; i >= 0; i--) {
515-
const shape = this.existingShape[i];
500+
for (let i = this.existingShapes.length - 1; i >= 0; i--) {
501+
const shape = this.existingShapes[i];
516502

517503
if (this.selectionManager.isPointInShape(x, y, shape)) {
518504
this.selectedShape = shape;
@@ -533,7 +519,7 @@ export class Game {
533519
this.startY = y;
534520

535521
if (this.activeTool === "pen") {
536-
this.existingShape.push({
522+
this.existingShapes.push({
537523
id: uuidv4(),
538524
type: "pen",
539525
points: [{ x, y }],
@@ -652,7 +638,7 @@ export class Game {
652638

653639
case "pen":
654640
const currentShape =
655-
this.existingShape[this.existingShape.length - 1];
641+
this.existingShapes[this.existingShapes.length - 1];
656642
if (currentShape?.type === "pen") {
657643
currentShape.points.push({ x, y });
658644
this.drawPencil(
@@ -1045,20 +1031,20 @@ export class Game {
10451031
}
10461032

10471033
eraser(x: number, y: number) {
1048-
const shapeIndex = this.existingShape.findIndex((shape) =>
1034+
const shapeIndex = this.existingShapes.findIndex((shape) =>
10491035
this.isPointInShape(x, y, shape)
10501036
);
10511037

10521038
if (shapeIndex !== -1) {
1053-
const erasedShape = this.existingShape[shapeIndex];
1054-
this.existingShape.splice(shapeIndex, 1);
1039+
const erasedShape = this.existingShapes[shapeIndex];
1040+
this.existingShapes.splice(shapeIndex, 1);
10551041
this.clearCanvas();
10561042

10571043
if (this.isStandalone) {
10581044
try {
10591045
localStorage.setItem(
10601046
LOCALSTORAGE_CANVAS_KEY,
1061-
JSON.stringify(this.existingShape)
1047+
JSON.stringify(this.existingShapes)
10621048
);
10631049
} catch (e) {
10641050
console.error("Error saving shapes to localStorage:", e);
@@ -1103,7 +1089,7 @@ export class Game {
11031089
}
11041090

11051091
clearAllShapes() {
1106-
this.existingShape = [];
1092+
this.existingShapes = [];
11071093
this.clearCanvas();
11081094
if (this.isStandalone) {
11091095
localStorage.removeItem(LOCALSTORAGE_CANVAS_KEY);

0 commit comments

Comments
 (0)