|
|
|
@@ -1,849 +0,0 @@
|
|
|
|
|
# 📧 Email WebSocket Integration Guide for React
|
|
|
|
|
|
|
|
|
|
This guide will help you integrate real-time email notifications into your React application using WebSocket connections.
|
|
|
|
|
|
|
|
|
|
## 🚀 Quick Start
|
|
|
|
|
|
|
|
|
|
### Installation
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
npm install socket.io-client
|
|
|
|
|
# or
|
|
|
|
|
yarn add socket.io-client
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Basic Setup
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
import io from "socket.io-client";
|
|
|
|
|
|
|
|
|
|
const socket = io("ws://localhost:3000/email", {
|
|
|
|
|
auth: {
|
|
|
|
|
token: localStorage.getItem("jwt-token"), // Your JWT authentication token
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 🔌 Connection Setup
|
|
|
|
|
|
|
|
|
|
### 1. Basic Connection Hook
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// hooks/useEmailWebSocket.ts
|
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
import io, { Socket } from "socket.io-client";
|
|
|
|
|
|
|
|
|
|
interface UseEmailWebSocketOptions {
|
|
|
|
|
token: string;
|
|
|
|
|
serverUrl?: string;
|
|
|
|
|
onConnect?: () => void;
|
|
|
|
|
onDisconnect?: () => void;
|
|
|
|
|
onError?: (error: string) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const useEmailWebSocket = ({ token, serverUrl = "ws://localhost:3000/email", onConnect, onDisconnect, onError }: UseEmailWebSocketOptions) => {
|
|
|
|
|
const [socket, setSocket] = useState<Socket | null>(null);
|
|
|
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!token) return;
|
|
|
|
|
|
|
|
|
|
const newSocket = io(serverUrl, {
|
|
|
|
|
auth: { token },
|
|
|
|
|
transports: ["websocket", "polling"],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Connection events
|
|
|
|
|
newSocket.on("connect", () => {
|
|
|
|
|
console.log("✅ Connected to email WebSocket");
|
|
|
|
|
setIsConnected(true);
|
|
|
|
|
onConnect?.();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
newSocket.on("disconnect", () => {
|
|
|
|
|
console.log("❌ Disconnected from email WebSocket");
|
|
|
|
|
setIsConnected(false);
|
|
|
|
|
onDisconnect?.();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
newSocket.on("connection_established", (data) => {
|
|
|
|
|
console.log("🎉 Email WebSocket connection established:", data);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
newSocket.on("connection_error", (error) => {
|
|
|
|
|
console.error("💥 Email WebSocket connection error:", error);
|
|
|
|
|
onError?.(error.error);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Health monitoring
|
|
|
|
|
newSocket.on("ping", () => {
|
|
|
|
|
newSocket.emit("pong", { timestamp: new Date().toISOString() });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setSocket(newSocket);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
newSocket.close();
|
|
|
|
|
setSocket(null);
|
|
|
|
|
};
|
|
|
|
|
}, [token, serverUrl]);
|
|
|
|
|
|
|
|
|
|
return { socket, isConnected };
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 2. Email Events Hook
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// hooks/useEmailEvents.ts
|
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
import { Socket } from "socket.io-client";
|
|
|
|
|
|
|
|
|
|
// TypeScript interfaces (copy from your backend)
|
|
|
|
|
export interface EmailPayload {
|
|
|
|
|
messageId: number;
|
|
|
|
|
userId: string;
|
|
|
|
|
from: {
|
|
|
|
|
name?: string;
|
|
|
|
|
address: string;
|
|
|
|
|
};
|
|
|
|
|
to: Array<{
|
|
|
|
|
name?: string;
|
|
|
|
|
address: string;
|
|
|
|
|
}>;
|
|
|
|
|
subject: string;
|
|
|
|
|
preview?: string;
|
|
|
|
|
hasAttachments: boolean;
|
|
|
|
|
timestamp: string;
|
|
|
|
|
mailboxId: string;
|
|
|
|
|
mailboxName: string;
|
|
|
|
|
isRead: boolean;
|
|
|
|
|
isFlagged: boolean;
|
|
|
|
|
size: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface EmailStatusPayload {
|
|
|
|
|
messageId: number;
|
|
|
|
|
userId: string;
|
|
|
|
|
mailboxId: string;
|
|
|
|
|
timestamp: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface UnreadCountPayload {
|
|
|
|
|
userId: string;
|
|
|
|
|
totalUnread: number;
|
|
|
|
|
mailboxCounts: Record<string, number>;
|
|
|
|
|
timestamp: string;
|
|
|
|
|
count: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface EmailEventHandlers {
|
|
|
|
|
onNewEmail?: (email: EmailPayload) => void;
|
|
|
|
|
onEmailRead?: (data: EmailStatusPayload) => void;
|
|
|
|
|
onEmailDeleted?: (data: EmailStatusPayload) => void;
|
|
|
|
|
onEmailSent?: (data: any) => void;
|
|
|
|
|
onUnreadCountUpdated?: (data: UnreadCountPayload) => void;
|
|
|
|
|
onMailboxUpdated?: (data: any) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const useEmailEvents = (socket: Socket | null, handlers: EmailEventHandlers) => {
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!socket) return;
|
|
|
|
|
|
|
|
|
|
// Register event handlers
|
|
|
|
|
if (handlers.onNewEmail) {
|
|
|
|
|
socket.on("new_email", handlers.onNewEmail);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (handlers.onEmailRead) {
|
|
|
|
|
socket.on("email_read", handlers.onEmailRead);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (handlers.onEmailDeleted) {
|
|
|
|
|
socket.on("email_deleted", handlers.onEmailDeleted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (handlers.onEmailSent) {
|
|
|
|
|
socket.on("email_sent", handlers.onEmailSent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (handlers.onUnreadCountUpdated) {
|
|
|
|
|
socket.on("unread_count_updated", handlers.onUnreadCountUpdated);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (handlers.onMailboxUpdated) {
|
|
|
|
|
socket.on("mailbox_updated", handlers.onMailboxUpdated);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cleanup on unmount
|
|
|
|
|
return () => {
|
|
|
|
|
socket.off("new_email");
|
|
|
|
|
socket.off("email_read");
|
|
|
|
|
socket.off("email_deleted");
|
|
|
|
|
socket.off("email_sent");
|
|
|
|
|
socket.off("unread_count_updated");
|
|
|
|
|
socket.off("mailbox_updated");
|
|
|
|
|
};
|
|
|
|
|
}, [socket, handlers]);
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 📱 Complete React Integration
|
|
|
|
|
|
|
|
|
|
### 1. Email Notification Component
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// components/EmailNotifications.tsx
|
|
|
|
|
import React, { useState, useCallback } from 'react';
|
|
|
|
|
import { useEmailWebSocket, useEmailEvents, EmailPayload, UnreadCountPayload } from '../hooks';
|
|
|
|
|
|
|
|
|
|
interface EmailNotificationsProps {
|
|
|
|
|
userToken: string;
|
|
|
|
|
onEmailClick?: (email: EmailPayload) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const EmailNotifications: React.FC<EmailNotificationsProps> = ({
|
|
|
|
|
userToken,
|
|
|
|
|
onEmailClick
|
|
|
|
|
}) => {
|
|
|
|
|
const [notifications, setNotifications] = useState<EmailPayload[]>([]);
|
|
|
|
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
|
|
|
const [isVisible, setIsVisible] = useState(false);
|
|
|
|
|
|
|
|
|
|
const { socket, isConnected } = useEmailWebSocket({
|
|
|
|
|
token: userToken,
|
|
|
|
|
onConnect: () => console.log('Email notifications connected'),
|
|
|
|
|
onError: (error) => console.error('WebSocket error:', error),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const handleNewEmail = useCallback((email: EmailPayload) => {
|
|
|
|
|
console.log('📧 New email received:', email);
|
|
|
|
|
|
|
|
|
|
// Add to notifications list
|
|
|
|
|
setNotifications(prev => [email, ...prev.slice(0, 9)]); // Keep only 10 latest
|
|
|
|
|
|
|
|
|
|
// Show browser notification if permission granted
|
|
|
|
|
if (Notification.permission === 'granted') {
|
|
|
|
|
new Notification(`📧 New Email: ${email.subject}`, {
|
|
|
|
|
body: `From: ${email.from.address}\n${email.preview}`,
|
|
|
|
|
icon: '/email-icon.png',
|
|
|
|
|
tag: `email-${email.messageId}`, // Prevent duplicates
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Play notification sound (optional)
|
|
|
|
|
const audio = new Audio('/notification.mp3');
|
|
|
|
|
audio.play().catch(() => {}); // Ignore if audio fails
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleUnreadCountUpdate = useCallback((data: UnreadCountPayload) => {
|
|
|
|
|
setUnreadCount(data.count);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleEmailRead = useCallback((data: any) => {
|
|
|
|
|
// Remove from notifications if it was there
|
|
|
|
|
setNotifications(prev =>
|
|
|
|
|
prev.filter(notification => notification.messageId !== data.messageId)
|
|
|
|
|
);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEmailEvents(socket, {
|
|
|
|
|
onNewEmail: handleNewEmail,
|
|
|
|
|
onUnreadCountUpdated: handleUnreadCountUpdate,
|
|
|
|
|
onEmailRead: handleEmailRead,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Request notification permission on mount
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (Notification.permission === 'default') {
|
|
|
|
|
Notification.requestPermission();
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="email-notifications">
|
|
|
|
|
{/* Connection Status */}
|
|
|
|
|
<div className={`connection-status ${isConnected ? 'connected' : 'disconnected'}`}>
|
|
|
|
|
<span className="status-dot"></span>
|
|
|
|
|
{isConnected ? 'Connected' : 'Disconnected'}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Notification Bell */}
|
|
|
|
|
<div
|
|
|
|
|
className="notification-bell"
|
|
|
|
|
onClick={() => setIsVisible(!isVisible)}
|
|
|
|
|
>
|
|
|
|
|
🔔
|
|
|
|
|
{unreadCount > 0 && (
|
|
|
|
|
<span className="badge">{unreadCount > 99 ? '99+' : unreadCount}</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Notifications Dropdown */}
|
|
|
|
|
{isVisible && (
|
|
|
|
|
<div className="notifications-dropdown">
|
|
|
|
|
<div className="notifications-header">
|
|
|
|
|
<h3>📧 Email Notifications</h3>
|
|
|
|
|
<span>{unreadCount} unread</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="notifications-list">
|
|
|
|
|
{notifications.length === 0 ? (
|
|
|
|
|
<div className="no-notifications">No new emails</div>
|
|
|
|
|
) : (
|
|
|
|
|
notifications.map((email) => (
|
|
|
|
|
<div
|
|
|
|
|
key={email.messageId}
|
|
|
|
|
className="notification-item"
|
|
|
|
|
onClick={() => onEmailClick?.(email)}
|
|
|
|
|
>
|
|
|
|
|
<div className="sender">
|
|
|
|
|
<strong>{email.from.name || email.from.address}</strong>
|
|
|
|
|
{email.hasAttachments && <span className="attachment-icon">📎</span>}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="subject">{email.subject}</div>
|
|
|
|
|
<div className="preview">{email.preview}</div>
|
|
|
|
|
<div className="timestamp">
|
|
|
|
|
{new Date(email.timestamp).toLocaleString()}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 2. Email Dashboard Component
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// components/EmailDashboard.tsx
|
|
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
import { useEmailWebSocket, useEmailEvents } from '../hooks';
|
|
|
|
|
|
|
|
|
|
interface EmailDashboardProps {
|
|
|
|
|
userToken: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const EmailDashboard: React.FC<EmailDashboardProps> = ({ userToken }) => {
|
|
|
|
|
const [emailStats, setEmailStats] = useState({
|
|
|
|
|
totalUnread: 0,
|
|
|
|
|
newToday: 0,
|
|
|
|
|
sentToday: 0,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { socket, isConnected } = useEmailWebSocket({
|
|
|
|
|
token: userToken,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
useEmailEvents(socket, {
|
|
|
|
|
onNewEmail: (email) => {
|
|
|
|
|
setEmailStats(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
totalUnread: prev.totalUnread + 1,
|
|
|
|
|
newToday: prev.newToday + 1,
|
|
|
|
|
}));
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
onEmailSent: () => {
|
|
|
|
|
setEmailStats(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
sentToday: prev.sentToday + 1,
|
|
|
|
|
}));
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
onUnreadCountUpdated: (data) => {
|
|
|
|
|
setEmailStats(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
totalUnread: data.count,
|
|
|
|
|
}));
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="email-dashboard">
|
|
|
|
|
<h2>📧 Email Dashboard</h2>
|
|
|
|
|
|
|
|
|
|
<div className="stats-grid">
|
|
|
|
|
<div className="stat-card">
|
|
|
|
|
<h3>{emailStats.totalUnread}</h3>
|
|
|
|
|
<p>Unread Emails</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="stat-card">
|
|
|
|
|
<h3>{emailStats.newToday}</h3>
|
|
|
|
|
<p>Received Today</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="stat-card">
|
|
|
|
|
<h3>{emailStats.sentToday}</h3>
|
|
|
|
|
<p>Sent Today</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="stat-card">
|
|
|
|
|
<h3 className={isConnected ? 'connected' : 'disconnected'}>
|
|
|
|
|
{isConnected ? '🟢' : '🔴'}
|
|
|
|
|
</h3>
|
|
|
|
|
<p>{isConnected ? 'Connected' : 'Disconnected'}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3. Context Provider (Advanced)
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// context/EmailWebSocketContext.tsx
|
|
|
|
|
import React, { createContext, useContext, ReactNode } from 'react';
|
|
|
|
|
import { useEmailWebSocket } from '../hooks/useEmailWebSocket';
|
|
|
|
|
import { Socket } from 'socket.io-client';
|
|
|
|
|
|
|
|
|
|
interface EmailWebSocketContextValue {
|
|
|
|
|
socket: Socket | null;
|
|
|
|
|
isConnected: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const EmailWebSocketContext = createContext<EmailWebSocketContextValue | null>(null);
|
|
|
|
|
|
|
|
|
|
interface EmailWebSocketProviderProps {
|
|
|
|
|
children: ReactNode;
|
|
|
|
|
token: string;
|
|
|
|
|
serverUrl?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const EmailWebSocketProvider: React.FC<EmailWebSocketProviderProps> = ({
|
|
|
|
|
children,
|
|
|
|
|
token,
|
|
|
|
|
serverUrl,
|
|
|
|
|
}) => {
|
|
|
|
|
const { socket, isConnected } = useEmailWebSocket({ token, serverUrl });
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<EmailWebSocketContext.Provider value={{ socket, isConnected }}>
|
|
|
|
|
{children}
|
|
|
|
|
</EmailWebSocketContext.Provider>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const useEmailWebSocketContext = () => {
|
|
|
|
|
const context = useContext(EmailWebSocketContext);
|
|
|
|
|
if (!context) {
|
|
|
|
|
throw new Error('useEmailWebSocketContext must be used within EmailWebSocketProvider');
|
|
|
|
|
}
|
|
|
|
|
return context;
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 🎨 CSS Styles
|
|
|
|
|
|
|
|
|
|
```css
|
|
|
|
|
/* styles/email-notifications.css */
|
|
|
|
|
.email-notifications {
|
|
|
|
|
position: relative;
|
|
|
|
|
display: inline-block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.connection-status {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: -8px;
|
|
|
|
|
right: -8px;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.connection-status.connected {
|
|
|
|
|
background: #d4edda;
|
|
|
|
|
color: #155724;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.connection-status.disconnected {
|
|
|
|
|
background: #f8d7da;
|
|
|
|
|
color: #721c24;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-dot {
|
|
|
|
|
width: 6px;
|
|
|
|
|
height: 6px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: currentColor;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notification-bell {
|
|
|
|
|
position: relative;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notification-bell:hover {
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.badge {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
background: #dc3545;
|
|
|
|
|
color: white;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
min-width: 18px;
|
|
|
|
|
height: 18px;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notifications-dropdown {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 100%;
|
|
|
|
|
right: 0;
|
|
|
|
|
width: 350px;
|
|
|
|
|
max-height: 400px;
|
|
|
|
|
background: white;
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notifications-header {
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
border-bottom: 1px solid #dee2e6;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notifications-list {
|
|
|
|
|
max-height: 300px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notification-item {
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
border-bottom: 1px solid #eee;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notification-item:hover {
|
|
|
|
|
background-color: #f8f9fa;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notification-item:last-child {
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sender {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.attachment-icon {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.subject {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview {
|
|
|
|
|
color: #666;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
display: -webkit-box;
|
|
|
|
|
-webkit-line-clamp: 2;
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.timestamp {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #999;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.no-notifications {
|
|
|
|
|
padding: 20px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
color: #666;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Dashboard Styles */
|
|
|
|
|
.email-dashboard {
|
|
|
|
|
padding: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stats-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
|
|
gap: 16px;
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
background: white;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-card h3 {
|
|
|
|
|
margin: 0 0 8px 0;
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-card h3.connected {
|
|
|
|
|
color: #28a745;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-card h3.disconnected {
|
|
|
|
|
color: #dc3545;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-card p {
|
|
|
|
|
margin: 0;
|
|
|
|
|
color: #666;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 🧪 Testing & Manual Triggers
|
|
|
|
|
|
|
|
|
|
### Manual Email Check
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// API call to manually trigger email check
|
|
|
|
|
const triggerEmailCheck = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch("/api/email/gateway/check-emails", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${userToken}`,
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
console.log("✅ Manual email check triggered:", result);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("❌ Failed to trigger email check:", error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Send Test Notification
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
const sendTestNotification = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch("/api/email/gateway/test-notification", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${userToken}`,
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
message: "Test notification from React app!",
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
console.log("✅ Test notification sent:", result);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("❌ Failed to send test notification:", error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 🔧 Complete App Integration
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// App.tsx - Complete example
|
|
|
|
|
import React from 'react';
|
|
|
|
|
import { EmailWebSocketProvider } from './context/EmailWebSocketContext';
|
|
|
|
|
import { EmailNotifications } from './components/EmailNotifications';
|
|
|
|
|
import { EmailDashboard } from './components/EmailDashboard';
|
|
|
|
|
|
|
|
|
|
function App() {
|
|
|
|
|
const userToken = localStorage.getItem('jwt-token') || '';
|
|
|
|
|
|
|
|
|
|
const handleEmailClick = (email: any) => {
|
|
|
|
|
console.log('Email clicked:', email);
|
|
|
|
|
// Navigate to email detail page
|
|
|
|
|
// router.push(`/emails/${email.messageId}`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!userToken) {
|
|
|
|
|
return <div>Please login to access email notifications</div>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<EmailWebSocketProvider token={userToken}>
|
|
|
|
|
<div className="app">
|
|
|
|
|
<header>
|
|
|
|
|
<h1>📧 Email App</h1>
|
|
|
|
|
<EmailNotifications
|
|
|
|
|
userToken={userToken}
|
|
|
|
|
onEmailClick={handleEmailClick}
|
|
|
|
|
/>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<main>
|
|
|
|
|
<EmailDashboard userToken={userToken} />
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
</EmailWebSocketProvider>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default App;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 🚨 Error Handling & Edge Cases
|
|
|
|
|
|
|
|
|
|
### Connection Recovery
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
const useConnectionRecovery = (socket: Socket | null) => {
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!socket) return;
|
|
|
|
|
|
|
|
|
|
const handleReconnect = () => {
|
|
|
|
|
console.log("🔄 WebSocket reconnecting...");
|
|
|
|
|
// Optionally refresh data or show reconnecting state
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleReconnectError = () => {
|
|
|
|
|
console.error("❌ Failed to reconnect");
|
|
|
|
|
// Show user notification or retry logic
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
socket.on("reconnect", handleReconnect);
|
|
|
|
|
socket.on("reconnect_error", handleReconnectError);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
socket.off("reconnect", handleReconnect);
|
|
|
|
|
socket.off("reconnect_error", handleReconnectError);
|
|
|
|
|
};
|
|
|
|
|
}, [socket]);
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Token Refresh
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
const useTokenRefresh = (socket: Socket | null) => {
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const refreshToken = () => {
|
|
|
|
|
const newToken = localStorage.getItem("jwt-token");
|
|
|
|
|
if (socket && newToken) {
|
|
|
|
|
socket.auth = { token: newToken };
|
|
|
|
|
socket.disconnect();
|
|
|
|
|
socket.connect();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Listen for token refresh events
|
|
|
|
|
window.addEventListener("token-refreshed", refreshToken);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener("token-refreshed", refreshToken);
|
|
|
|
|
};
|
|
|
|
|
}, [socket]);
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 📝 Environment Configuration
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// config/websocket.ts
|
|
|
|
|
export const WEBSOCKET_CONFIG = {
|
|
|
|
|
development: {
|
|
|
|
|
serverUrl: "ws://localhost:3000/email",
|
|
|
|
|
reconnectionAttempts: 5,
|
|
|
|
|
timeout: 20000,
|
|
|
|
|
},
|
|
|
|
|
production: {
|
|
|
|
|
serverUrl: "wss://your-domain.com/email",
|
|
|
|
|
reconnectionAttempts: 10,
|
|
|
|
|
timeout: 30000,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getWebSocketConfig = () => {
|
|
|
|
|
const env = process.env.NODE_ENV || "development";
|
|
|
|
|
return WEBSOCKET_CONFIG[env as keyof typeof WEBSOCKET_CONFIG];
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 🎯 Best Practices
|
|
|
|
|
|
|
|
|
|
1. **🔐 Security**: Always use HTTPS/WSS in production
|
|
|
|
|
2. **🔄 Reconnection**: Implement proper reconnection logic
|
|
|
|
|
3. **🎯 Performance**: Limit notification history (max 10-50 items)
|
|
|
|
|
4. **📱 UX**: Request notification permissions gracefully
|
|
|
|
|
5. **🔊 Audio**: Make notification sounds optional
|
|
|
|
|
6. **💾 Persistence**: Consider storing notifications in localStorage
|
|
|
|
|
7. **🎨 Theming**: Support light/dark mode for notifications
|
|
|
|
|
8. **♿ Accessibility**: Add proper ARIA labels and keyboard navigation
|
|
|
|
|
|
|
|
|
|
## 🐛 Troubleshooting
|
|
|
|
|
|
|
|
|
|
**Connection Issues:**
|
|
|
|
|
|
|
|
|
|
- Check JWT token validity
|
|
|
|
|
- Verify server URL and port
|
|
|
|
|
- Check browser console for errors
|
|
|
|
|
- Ensure CORS is properly configured
|
|
|
|
|
|
|
|
|
|
**Missing Notifications:**
|
|
|
|
|
|
|
|
|
|
- Verify WebSocket event names match backend
|
|
|
|
|
- Check browser notification permissions
|
|
|
|
|
- Confirm user is authenticated properly
|
|
|
|
|
- Test with manual triggers
|
|
|
|
|
|
|
|
|
|
**Performance Issues:**
|
|
|
|
|
|
|
|
|
|
- Limit notification history
|
|
|
|
|
- Debounce rapid events
|
|
|
|
|
- Use React.memo for components
|
|
|
|
|
- Implement virtual scrolling for long lists
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 🚀 Ready to Go!
|
|
|
|
|
|
|
|
|
|
Your React app is now ready to receive real-time email notifications! The WebSocket connection will automatically:
|
|
|
|
|
|
|
|
|
|
- ✅ Authenticate users with JWT tokens
|
|
|
|
|
- ✅ Receive new email notifications instantly
|
|
|
|
|
- ✅ Update unread counts in real-time
|
|
|
|
|
- ✅ Handle connection recovery automatically
|
|
|
|
|
- ✅ Show browser notifications (with permission)
|
|
|
|
|
- ✅ Provide a beautiful UI for email notifications
|
|
|
|
|
|
|
|
|
|
Happy coding! 🎉
|