diff --git a/src/auth/AuthContext.jsx b/src/auth/AuthContext.jsx index e85a1c9..15f64ca 100644 --- a/src/auth/AuthContext.jsx +++ b/src/auth/AuthContext.jsx @@ -155,6 +155,7 @@ async function me(accessToken) { status: res.status, json, text, + userId: u && typeof u.user_id === "number" ? u.user_id : null, username: (u && (u.username || u.user || u.preferred_username)) || "", emailVerified, avatarUrl: u && typeof u.avatar_url === "string" ? u.avatar_url : "", @@ -212,7 +213,7 @@ function nowSec() { export function AuthProvider({ children }) { const [status, setStatus] = useState("loading"); // loading | anon | auth - const [user, setUser] = useState(null); // { username } + const [user, setUser] = useState(null); // { username, userId } const [tokens, setTokens] = useState(null); // { access_token, refresh_token, ... } const [avatarUrl, setAvatarUrl] = useState(""); const [profileLoaded, setProfileLoaded] = useState(false); @@ -261,7 +262,11 @@ export function AuthProvider({ children }) { setAvatarUrl(mr.avatarUrl); } if (mr.username) { - setUser({ username: mr.username }); + setUser((prev) => ({ + ...prev, + username: mr.username, + userId: mr.userId || prev?.userId || null, + })); } if (typeof mr.needsUsername === "boolean") { setNeedsUsername(mr.needsUsername); @@ -742,6 +747,7 @@ export function AuthProvider({ children }) { const loading = status === "loading"; const authenticated = status === "auth"; const username = user?.username || ""; + const userId = user?.userId || null; const token = tokens?.access_token || ""; return { @@ -758,6 +764,7 @@ export function AuthProvider({ children }) { loading, authenticated, username, + userId, token, lastError: error || "", diff --git a/src/components/Chat/ChatContainer.jsx b/src/components/Chat/ChatContainer.jsx index f456bfa..1252c9b 100644 --- a/src/components/Chat/ChatContainer.jsx +++ b/src/components/Chat/ChatContainer.jsx @@ -101,6 +101,9 @@ export default function ChatContainer() { } }, []); + // Reference to markAsRead function (will be set after useChat) + const markAsReadRef = useRef(null); + // Handle new incoming message const handleNewMessage = useCallback((msg) => { const senderName = msg.sender_name; @@ -110,18 +113,24 @@ export default function ChatContainer() { const isChatMinimized = minimizedChatsRef.current[senderName]; const isDocumentFocused = document.hasFocus(); - // Update unread count if chat is not visible - if (!isChatOpen || isChatMinimized || !isDocumentFocused) { + // Chat is visible = open + not minimized + document focused + const isChatVisible = isChatOpen && !isChatMinimized && isDocumentFocused; + + if (isChatVisible) { + // Chat is open and user can see the message - mark as read immediately + if (markAsReadRef.current) { + markAsReadRef.current(senderName); + } + } else { + // Update unread count if chat is not visible setUnreadCounts((prev) => ({ ...prev, [senderName]: (prev[senderName] || 0) + 1, })); } - // Show notification if: - // - Chat is not open, or is minimized, or document is not focused - // - Notification permission is granted - const shouldNotify = !isChatOpen || isChatMinimized || !isDocumentFocused; + // Show notification if chat is not visible + const shouldNotify = !isChatVisible; if (shouldNotify) { // Play notification sound @@ -168,6 +177,11 @@ export default function ChatContainer() { onNewMessage: handleNewMessage, }); + // Keep markAsReadRef updated so handleNewMessage can use it + useEffect(() => { + markAsReadRef.current = markAsRead; + }, [markAsRead]); + // Load friends when authenticated useEffect(() => { if (!authenticated) { @@ -243,22 +257,28 @@ export default function ChatContainer() { // Refresh conversations to update unread counts/badges const convs = await fetchConversations(); if (convs && convs.length > 0) { - const newUnread = {}; + // Build new unread counts from server (source of truth) + const serverUnread = {}; convs.forEach((c) => { - if (c.unread_count > 0) { - newUnread[c.username] = c.unread_count; - } + // Use case-insensitive key matching + serverUnread[c.username.toLowerCase()] = { username: c.username, count: c.unread_count || 0 }; }); + setUnreadCounts((prev) => { - // Merge but don't overwrite zeros (already read in this session) - const merged = { ...prev }; - Object.entries(newUnread).forEach(([user, count]) => { - // Only update if we don't have a local zero (user already read it) - if (merged[user] === undefined || merged[user] > 0) { - merged[user] = count; - } + const updated = {}; + // For each conversation from server, use server count + // unless the chat is currently open and not minimized (user is viewing it) + Object.values(serverUnread).forEach(({ username, count }) => { + const key = username; + const isOpenAndVisible = activeChatsRef.current.some( + (c) => c.username.toLowerCase() === username.toLowerCase() + ) && !minimizedChatsRef.current[username]; + + // If chat is open and visible, keep it at 0 (user can see messages) + // Otherwise use server count + updated[key] = isOpenAndVisible ? 0 : count; }); - return merged; + return updated; }); } } @@ -388,6 +408,10 @@ export default function ChatContainer() { return; } + // Refresh conversations when opening to get latest activity for sorting + if (!showContacts) { + fetchConversations(); + } setShowContacts(!showContacts); }; @@ -468,7 +492,22 @@ export default function ChatContainer() { ) : (