feat: add socket context and notifications listener for real-time event handling

- Introduced `SocketProvider` to manage WebSocket connection and context across the app.
- Added `NotificationsListener` component to handle real-time notifications and display feedback via `toast`.
- Enabled event subscriptions for projects, groups, collaborators, and user actions.
This commit is contained in:
Mathis H (Avnyr) 2025-05-16 16:41:55 +02:00
parent d7255444f5
commit ad6ef4c907
2 changed files with 255 additions and 0 deletions

View File

@ -0,0 +1,63 @@
import { useEffect, useState } from "react";
import { useSocket } from "@/lib/socket-context";
import { toast } from "sonner";
/**
* Notification component that listens for real-time notifications
* and displays them using toast notifications.
*/
export function NotificationsListener() {
const { onNotification, isConnected } = useSocket();
const [initialized, setInitialized] = useState(false);
useEffect(() => {
if (!isConnected) return;
// Set up notification listener
const unsubscribe = onNotification((data) => {
// Display notification based on type
switch (data.type) {
case "project_invitation":
toast.info(data.message, {
description: `You've been invited to collaborate on ${data.projectName}`,
action: {
label: "View Project",
onClick: () => window.location.href = `/projects/${data.projectId}`,
},
});
break;
case "group_update":
toast.info(data.message, {
description: data.description,
action: data.projectId && {
label: "View Groups",
onClick: () => window.location.href = `/projects/${data.projectId}/groups`,
},
});
break;
case "person_added":
toast.success(data.message, {
description: data.description,
});
break;
case "person_removed":
toast.info(data.message, {
description: data.description,
});
break;
default:
toast.info(data.message);
}
});
setInitialized(true);
// Clean up on unmount
return () => {
unsubscribe();
};
}, [isConnected, onNotification]);
// This component doesn't render anything visible
return null;
}

View File

@ -0,0 +1,192 @@
"use client";
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
import { io, Socket } from "socket.io-client";
import { useAuth } from "./auth-context";
// Define the SocketContext type
interface SocketContextType {
socket: Socket | null;
isConnected: boolean;
joinProject: (projectId: string) => void;
leaveProject: (projectId: string) => void;
// Event listeners
onProjectUpdated: (callback: (data: any) => void) => () => void;
onCollaboratorAdded: (callback: (data: any) => void) => () => void;
onGroupCreated: (callback: (data: any) => void) => () => void;
onGroupUpdated: (callback: (data: any) => void) => () => void;
onPersonAddedToGroup: (callback: (data: any) => void) => () => void;
onPersonRemovedFromGroup: (callback: (data: any) => void) => () => void;
onNotification: (callback: (data: any) => void) => () => void;
}
// Create the SocketContext
const SocketContext = createContext<SocketContextType | undefined>(undefined);
// Create a provider component
export function SocketProvider({ children }: { children: ReactNode }) {
const [socket, setSocket] = useState<Socket | null>(null);
const [isConnected, setIsConnected] = useState<boolean>(false);
const { user, isAuthenticated } = useAuth();
// Initialize socket connection when user is authenticated
useEffect(() => {
if (!isAuthenticated || !user) {
return;
}
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
// Create socket connection
const socketInstance = io(API_URL, {
withCredentials: true,
query: {
userId: user.id,
},
});
// Set up event listeners
socketInstance.on('connect', () => {
console.log('Socket connected');
setIsConnected(true);
});
socketInstance.on('disconnect', () => {
console.log('Socket disconnected');
setIsConnected(false);
});
socketInstance.on('connect_error', (error) => {
console.error('Socket connection error:', error);
setIsConnected(false);
});
// Save socket instance
setSocket(socketInstance);
// Clean up on unmount
return () => {
socketInstance.disconnect();
setSocket(null);
setIsConnected(false);
};
}, [isAuthenticated, user]);
// Join a project room
const joinProject = (projectId: string) => {
if (socket && isConnected) {
socket.emit('project:join', projectId);
}
};
// Leave a project room
const leaveProject = (projectId: string) => {
if (socket && isConnected) {
socket.emit('project:leave', projectId);
}
};
// Event listeners with cleanup
const onProjectUpdated = (callback: (data: any) => void) => {
if (socket) {
socket.on('project:updated', callback);
}
return () => {
if (socket) {
socket.off('project:updated', callback);
}
};
};
const onCollaboratorAdded = (callback: (data: any) => void) => {
if (socket) {
socket.on('project:collaboratorAdded', callback);
}
return () => {
if (socket) {
socket.off('project:collaboratorAdded', callback);
}
};
};
const onGroupCreated = (callback: (data: any) => void) => {
if (socket) {
socket.on('group:created', callback);
}
return () => {
if (socket) {
socket.off('group:created', callback);
}
};
};
const onGroupUpdated = (callback: (data: any) => void) => {
if (socket) {
socket.on('group:updated', callback);
}
return () => {
if (socket) {
socket.off('group:updated', callback);
}
};
};
const onPersonAddedToGroup = (callback: (data: any) => void) => {
if (socket) {
socket.on('group:personAdded', callback);
}
return () => {
if (socket) {
socket.off('group:personAdded', callback);
}
};
};
const onPersonRemovedFromGroup = (callback: (data: any) => void) => {
if (socket) {
socket.on('group:personRemoved', callback);
}
return () => {
if (socket) {
socket.off('group:personRemoved', callback);
}
};
};
const onNotification = (callback: (data: any) => void) => {
if (socket) {
socket.on('notification:new', callback);
}
return () => {
if (socket) {
socket.off('notification:new', callback);
}
};
};
// Create the context value
const value = {
socket,
isConnected,
joinProject,
leaveProject,
onProjectUpdated,
onCollaboratorAdded,
onGroupCreated,
onGroupUpdated,
onPersonAddedToGroup,
onPersonRemovedFromGroup,
onNotification,
};
return <SocketContext.Provider value={value}>{children}</SocketContext.Provider>;
}
// Create a hook to use the SocketContext
export function useSocket() {
const context = useContext(SocketContext);
if (context === undefined) {
throw new Error("useSocket must be used within a SocketProvider");
}
return context;
}