Skip to content

Commit f7473d6

Browse files
committed
notif sockets configure
1 parent deb3d6f commit f7473d6

8 files changed

Lines changed: 146 additions & 100 deletions

File tree

src/app/(app)/notifications/page.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"use client";
2-
import { useState, useEffect } from 'react';
2+
import { useState, useEffect, useRef } from 'react';
33
import { usePageReady } from "@/components/RouteChangeLoader";
44
import { Bell, CheckCheck, Check } from 'lucide-react';
55
import { getUser } from '@/api';
@@ -40,7 +40,8 @@ export default function NotificationsPage() {
4040
const [notifications, setNotifications] = useState<Notification[]>([]);
4141
const [loading, setLoading] = useState(true);
4242
const [filter, setFilter] = useState<"all" | "unread">("all");
43-
const { markAsRead, markAllAsRead } = useNotifications();
43+
const { notifications: realtimeNotifications, unreadCount: realtimeUnreadCount, markAsRead, markAllAsRead } = useNotifications();
44+
const lastRealtimeNotificationCountRef = useRef(0);
4445
const [toast, setToast] = useState<{
4546
message: string;
4647
type: "info" | "success" | "error";
@@ -51,6 +52,13 @@ export default function NotificationsPage() {
5152
loadNotifications();
5253
}, []);
5354

55+
useEffect(() => {
56+
if (realtimeNotifications.length > lastRealtimeNotificationCountRef.current) {
57+
void loadNotifications();
58+
}
59+
lastRealtimeNotificationCountRef.current = realtimeNotifications.length;
60+
}, [realtimeNotifications.length]);
61+
5462
const loadNotifications = async () => {
5563
try {
5664
setLoading(true);
@@ -141,7 +149,10 @@ export default function NotificationsPage() {
141149
? notifications.filter((n) => !n.is_read)
142150
: notifications;
143151

144-
const unreadCount = notifications.filter((n) => !n.is_read).length;
152+
const unreadCount = Math.max(
153+
notifications.filter((n) => !n.is_read).length,
154+
realtimeUnreadCount
155+
);
145156

146157

147158

src/components/MentionNotifications.tsx

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
"use client";
22

3-
import React, { useState, useEffect, useCallback } from 'react';
3+
import React, { useState, useEffect } from 'react';
44
import { Bell, X, Check, User, Users, AtSign } from 'lucide-react';
5-
import { useMentionNotifications } from '@/hooks/useMentionNotifications';
65

76
interface MentionNotification {
87
id: string;
@@ -39,16 +38,9 @@ export default function MentionNotifications({
3938
const [mentions, setMentions] = useState<MentionNotification[]>([]);
4039
const [loading, setLoading] = useState(false);
4140
const [filter, setFilter] = useState<'all' | 'unread'>('unread');
42-
const {
43-
fetchUnreadCount,
44-
markMentionAsRead,
45-
markAllMentionsAsRead,
46-
socket,
47-
unreadMentionsCount,
48-
} = useMentionNotifications();
4941

5042
// Fetch mentions
51-
const fetchMentions = useCallback(async () => {
43+
const fetchMentions = async () => {
5244
setLoading(true);
5345
try {
5446
const response = await fetch(`/api/mentions?unreadOnly=${filter === 'unread'}`, {
@@ -64,7 +56,7 @@ export default function MentionNotifications({
6456
} finally {
6557
setLoading(false);
6658
}
67-
}, [filter]);
59+
};
6860

6961
// Mark mention as read
7062
const markAsRead = async (mentionId: string) => {
@@ -82,7 +74,6 @@ export default function MentionNotifications({
8274
: mention
8375
)
8476
);
85-
await markMentionAsRead();
8677
}
8778
} catch (error) {
8879
console.error('Failed to mark mention as read:', error);
@@ -96,8 +87,6 @@ export default function MentionNotifications({
9687
for (const mention of unreadMentions) {
9788
await markAsRead(mention.id);
9889
}
99-
100-
await markAllMentionsAsRead();
10190
};
10291

10392

@@ -110,28 +99,8 @@ export default function MentionNotifications({
11099
useEffect(() => {
111100
if (isOpen) {
112101
fetchMentions();
113-
void fetchUnreadCount();
114102
}
115-
}, [isOpen, filter, fetchMentions, fetchUnreadCount]);
116-
117-
useEffect(() => {
118-
if (!socket) return;
119-
120-
const refreshMentions = () => {
121-
if (isOpen) {
122-
fetchMentions();
123-
void fetchUnreadCount();
124-
}
125-
};
126-
127-
socket.on('mention_notification', refreshMentions);
128-
socket.on('mention_marked_read', refreshMentions);
129-
130-
return () => {
131-
socket.off('mention_notification', refreshMentions);
132-
socket.off('mention_marked_read', refreshMentions);
133-
};
134-
}, [socket, isOpen, fetchMentions, fetchUnreadCount]);
103+
}, [isOpen, filter]);
135104

136105
const getMentionIcon = (type: string) => {
137106
switch (type) {
@@ -166,8 +135,10 @@ export default function MentionNotifications({
166135
<div className="flex items-center space-x-3">
167136
<Bell className="text-blue-400" size={24} />
168137
<h2 className="text-xl font-semibold text-white">Mentions</h2>
169-
{unreadMentionsCount > 0 && (
170-
<span className="h-2.5 w-2.5 rounded-full bg-red-500" />
138+
{mentions.filter(m => !m.is_read).length > 0 && (
139+
<span className="bg-red-500 text-white text-xs px-2 py-1 rounded-full">
140+
{mentions.filter(m => !m.is_read).length}
141+
</span>
171142
)}
172143
</div>
173144

src/components/MessageBubble.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({
130130
{/* Message Bubble */}
131131
<div
132132
className={`
133-
px-4 py-2.5 w-fit max-w-full
133+
px-4 py-2.5 w-fit max-w-72
134134
${bubbleStyles}
135135
rounded-lg
136136
${

src/components/NotificationBell.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,12 @@ export default function NotificationBell({ className = "", onNavigateToMessage }
6161
title="Notifications"
6262
>
6363
<Bell size={20} />
64+
65+
{/* Unread Count Badge */}
6466
{unreadCount > 0 && (
65-
<span className="absolute -top-0.5 -right-0.5 h-2.5 w-2.5 rounded-full bg-red-500 ring-2 ring-[#111214]" />
67+
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full min-w-[18px] h-[18px] flex items-center justify-center px-1">
68+
{unreadCount > 99 ? '99+' : unreadCount}
69+
</span>
6670
)}
6771
</button>
6872

src/components/Sidebar.tsx

Lines changed: 6 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import { useEffect, useState } from "react";
2424
import { useNotifications } from "../hooks/useNotifications";
2525
import { useFriendNotifications } from "../contexts/FriendNotificationContext";
2626
import { useMessageNotifications } from "../contexts/MessageNotificationContext";
27-
import { createAuthSocket } from "@/socket";
2827

2928
const navItems = [
3029
{ label: "Servers", icon: Users, path: "/servers" },
@@ -42,7 +41,7 @@ export default function Sidebar() {
4241
const [error, setError] = useState<string | null>(null);
4342

4443
// ✅ Get values directly from hooks (no refreshCount needed)
45-
const { unreadCount, setUnreadCount } = useNotifications();
44+
const { unreadCount } = useNotifications();
4645
const { friendRequestCount, refreshCount: refreshFriendCount } =
4746
useFriendNotifications();
4847
const { unreadMessageCount, refreshCount: refreshMessageCount } =
@@ -58,51 +57,6 @@ export default function Sidebar() {
5857
// Notifications page will handle its own refresh
5958
};
6059

61-
// ✅ WebSocket-based real-time updates
62-
useEffect(() => {
63-
if (!user?.id) return;
64-
65-
const socket = createAuthSocket(user.id);
66-
67-
// Listen for real-time notification events
68-
socket.on("new_notification", (data?: { count?: number }) => {
69-
console.log("📬 New notification received", data);
70-
71-
// If backend sends the new count, use it
72-
if (data?.count !== undefined) {
73-
setUnreadCount(data.count);
74-
} else {
75-
// Otherwise increment locally
76-
setUnreadCount((prev) => prev + 1);
77-
}
78-
});
79-
80-
socket.on("new_message", () => {
81-
console.log("💬 New message received");
82-
// Message notifications context will handle its own update
83-
refreshMessageCount?.();
84-
});
85-
86-
socket.on("friend_request", () => {
87-
console.log("👋 New friend request received");
88-
refreshFriendCount?.();
89-
});
90-
91-
socket.on("friend_request_accepted", () => {
92-
console.log("✅ Friend request accepted");
93-
refreshFriendCount?.();
94-
});
95-
96-
// Cleanup on unmount
97-
return () => {
98-
socket.off("new_notification");
99-
socket.off("new_message");
100-
socket.off("friend_request");
101-
socket.off("friend_request_accepted");
102-
socket.disconnect();
103-
};
104-
}, [user?.id, setUnreadCount, refreshFriendCount, refreshMessageCount]);
105-
10660
// ✅ Refresh counts when window regains focus
10761
useEffect(() => {
10862
const handleFocus = async () => {
@@ -164,12 +118,12 @@ export default function Sidebar() {
164118
>
165119
{/* Background */}
166120
<div
167-
className="absolute inset-0 z-0 bg-no-repeat bg-cover opacity-90 border-r border-gray-800 pointer-events-none"
168-
/>
121+
className="absolute inset-0 z-0 bg-no-repeat bg-cover opacity-90 border-r border-gray-800"
122+
/>
169123

170124

171125
{/* Content */}
172-
<div className="relative flex flex-col h-full justify-between">
126+
<div className="relative z-10 flex flex-col h-full justify-between">
173127
{/* Top Section */}
174128
<div>
175129
<div className="flex items-center justify-between p-4">
@@ -199,7 +153,6 @@ export default function Sidebar() {
199153
{navItems.map((item) => {
200154
const isActive = pathname === item.path;
201155
let notificationCount = 0;
202-
const showDotOnly = item.label === "Notifications";
203156
if (item.label === "Notifications") {
204157
notificationCount = unreadCount;
205158
} else if (item.label === "Messages") {
@@ -224,9 +177,7 @@ export default function Sidebar() {
224177
<div className="relative">
225178
<item.icon className="w-5 h-5" />
226179
{/* Show notification badge with animation */}
227-
{notificationCount > 0 && showDotOnly ? (
228-
<span className="absolute -top-1 -right-1 h-2.5 w-2.5 rounded-full bg-red-500 ring-2 ring-[#111214] animate-pulse" />
229-
) : notificationCount > 0 && (
180+
{notificationCount > 0 && (
230181
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full min-w-[16px] h-[16px] flex items-center justify-center px-1 font-bold animate-pulse">
231182
{notificationCount > 99 ? "99+" : notificationCount}
232183
</span>
@@ -239,7 +190,7 @@ export default function Sidebar() {
239190
{collapsed && (
240191
<div className="absolute left-full top-1/2 -translate-y-1/2 ml-2 z-20 px-3 py-1 text-sm text-white bg-black rounded shadow-lg opacity-0 group-hover:opacity-100 transition">
241192
{item.label}
242-
{notificationCount > 0 && !showDotOnly && ` (${notificationCount})`}
193+
{notificationCount > 0 && ` (${notificationCount})`}
243194
</div>
244195
)}
245196
</div>

src/contexts/FriendNotificationContext.tsx

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"use client";
2-
import { createContext, useContext, useEffect, useState, ReactNode, useCallback, useMemo } from 'react';
3-
import { fetchFriendRequests } from '@/api';
2+
import { createContext, useContext, useEffect, useState, ReactNode, useCallback, useMemo, useRef } from 'react';
3+
import { Socket } from 'socket.io-client';
4+
import { fetchFriendRequests, getUser } from '@/api';
5+
import { createAuthSocket } from '@/socket';
46

57
interface FriendNotificationContextType {
68
friendRequestCount: number;
@@ -17,8 +19,12 @@ const FriendNotificationContext = createContext<FriendNotificationContextType>({
1719
export function FriendNotificationProvider({ children }: { children: ReactNode }) {
1820
const [friendRequestCount, setFriendRequestCount] = useState(0);
1921
const [loading, setLoading] = useState(true);
22+
const socketRef = useRef<Socket | null>(null);
23+
const refreshInFlightRef = useRef(false);
2024

2125
const refreshCount = useCallback(async () => {
26+
if (refreshInFlightRef.current) return;
27+
refreshInFlightRef.current = true;
2228
try {
2329
const requests = await fetchFriendRequests();
2430
setFriendRequestCount(requests.length);
@@ -28,6 +34,7 @@ export function FriendNotificationProvider({ children }: { children: ReactNode }
2834
setFriendRequestCount(0);
2935
} finally {
3036
setLoading(false);
37+
refreshInFlightRef.current = false;
3138
}
3239
}, []);
3340

@@ -36,6 +43,46 @@ export function FriendNotificationProvider({ children }: { children: ReactNode }
3643
refreshCount();
3744
}, [refreshCount]);
3845

46+
useEffect(() => {
47+
let mounted = true;
48+
let cleanupSocket: (() => void) | null = null;
49+
50+
const setupSocket = async () => {
51+
try {
52+
const user = await getUser();
53+
if (!mounted || !user?.id) return;
54+
55+
const socket = createAuthSocket(user.id);
56+
socketRef.current = socket;
57+
58+
const handleFriendEvent = () => {
59+
void refreshCount();
60+
};
61+
62+
socket.on('friend_request', handleFriendEvent);
63+
socket.on('friend_request_accepted', handleFriendEvent);
64+
65+
cleanupSocket = () => {
66+
socket.off('friend_request', handleFriendEvent);
67+
socket.off('friend_request_accepted', handleFriendEvent);
68+
socket.disconnect();
69+
if (socketRef.current === socket) {
70+
socketRef.current = null;
71+
}
72+
};
73+
} catch (error) {
74+
console.error('Failed to initialize friend notification socket:', error);
75+
}
76+
};
77+
78+
void setupSocket();
79+
80+
return () => {
81+
mounted = false;
82+
cleanupSocket?.();
83+
};
84+
}, [refreshCount]);
85+
3986
const contextValue = useMemo(
4087
() => ({ friendRequestCount, loading, refreshCount }),
4188
[friendRequestCount, loading, refreshCount]

0 commit comments

Comments
 (0)