feat(messages): implement real-time user presence and improve messagi…#144
Conversation
…ng UX - Add Firestore presence listener (`listen-to-presence`) following the existing typing subscription pattern - Create reusable `usePresence` hook for subscribing to user online status and last seen updates - Introduce `PresenceProvider` to manage presence lifecycle across the dashboard - Mount `PresenceProvider` in the dashboard layout to automatically track authenticated user presence - Implement presence updates by: - Marking users online when a dashboard session starts - Sending heartbeat updates every 45 seconds to keep presence fresh - Marking users offline on tab close and visibility changes - Keeping `lastSeen` updated for accurate offline status - Replace hardcoded conversation header presence with live Firestore data - Display "Active now" with a green indicator for online users - Show "Last seen <relative time>" for offline users using the stored `lastSeen` timestamp - Add real-time presence indicators to conversation rows in `MessagesHeadSideList` - Display online status consistently across both the conversation list and active chat header - Improve message send failure handling by: - Replacing blocking `alert()` dialogs with `toast.error()` - Preserving unsent message text so users can retry without losing their draft Implementation details: - Added `listen-to-presence.js` for Firestore presence subscriptions - Added reusable `usePresence` hook - Created `PresenceProvider` for application-wide presence management - Updated dashboard layout to initialise presence tracking - Refactored messages page to consume live presence state - Enhanced conversation list with real-time online indicators Verification: - Real-time presence updates reflected in conversation header and list - Online status maintained through heartbeat updates - Offline status derived from `lastSeen` after session ends - Send failures now surface as non-blocking toast notifications while preserving message drafts - Lint checks pass successfully
|
@IamOluwatoyin is attempting to deploy a commit to the Deen Bridge Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughThe dashboard now provides authenticated presence tracking through Firestore. Messaging views subscribe to participant presence, render online or last-seen status, display conversation-list indicators, and use toast notifications for send failures. ChangesPresence tracking
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant UserSession
participant PresenceProvider
participant Firestore
participant MessagesPage
UserSession->>PresenceProvider: provide authenticated user
PresenceProvider->>Firestore: write online presence and heartbeat
MessagesPage->>Firestore: subscribe to participant presence
Firestore-->>MessagesPage: return online and lastSeen state
MessagesPage-->>MessagesPage: render active or last-seen status
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/providers/PresenceProvider.jsx`:
- Around line 15-27: Update markOnline in PresenceProvider so it only calls
setUserOnline(user._id, true) when document.visibilityState is "visible";
preserve the existing markOffline behavior and interval setup so hidden tabs
remain offline between visibility events.
In `@hooks/usePresence.js`:
- Around line 8-12: Update the useEffect in usePresence to reset presence to {
online: false, lastSeen: null } before the !userId early return and before
subscribing to a new user. Preserve the existing listenToPresence subscription
and cleanup behavior for valid userId values.
In `@lib/actions/messages/listen-to-presence.js`:
- Around line 6-16: Update the onSnapshot subscription in listen-to-presence.js
to treat online records as stale when lastSeen exceeds the threshold coordinated
with the 45-second heartbeat. Derive freshness from lastSeen, schedule or
reschedule an expiry callback for each subscription that reports { online:
false, lastSeen: null }, and ensure the timer is cleared when the returned
unsubscribe function is invoked.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 2046825a-d998-44cf-95ad-ca55cbdf6ef9
📒 Files selected for processing (6)
app/dashboard/layout.jsxapp/dashboard/messages/[room]/page.jsxcomponents/molecules/dashboard/messages/MessagesHeadSideList.jsxcomponents/providers/PresenceProvider.jsxhooks/usePresence.jslib/actions/messages/listen-to-presence.js
| const markOnline = () => setUserOnline(user._id, true); | ||
| const markOffline = () => setUserOnline(user._id, false); | ||
|
|
||
| markOnline(); | ||
|
|
||
| intervalRef.current = setInterval(markOnline, HEARTBEAT_INTERVAL); | ||
|
|
||
| const handleVisibility = () => { | ||
| if (document.visibilityState === "hidden") { | ||
| markOffline(); | ||
| } else { | ||
| markOnline(); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Keep hidden tabs offline between visibility events.
After a hidden tab is marked offline, the interval at line 20 still invokes markOnline every 45 seconds. The user is therefore shown online again while the dashboard remains backgrounded. Make markOnline a no-op unless document.visibilityState === "visible".
Proposed fix
- const markOnline = () => setUserOnline(user._id, true);
+ const markOnline = () => {
+ if (document.visibilityState !== "visible") return;
+ return setUserOnline(user._id, true);
+ };📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const markOnline = () => setUserOnline(user._id, true); | |
| const markOffline = () => setUserOnline(user._id, false); | |
| markOnline(); | |
| intervalRef.current = setInterval(markOnline, HEARTBEAT_INTERVAL); | |
| const handleVisibility = () => { | |
| if (document.visibilityState === "hidden") { | |
| markOffline(); | |
| } else { | |
| markOnline(); | |
| } | |
| const markOnline = () => { | |
| if (document.visibilityState !== "visible") return; | |
| return setUserOnline(user._id, true); | |
| }; | |
| const markOffline = () => setUserOnline(user._id, false); | |
| markOnline(); | |
| intervalRef.current = setInterval(markOnline, HEARTBEAT_INTERVAL); | |
| const handleVisibility = () => { | |
| if (document.visibilityState === "hidden") { | |
| markOffline(); | |
| } else { | |
| markOnline(); | |
| } |
🧰 Tools
🪛 ast-grep (0.44.1)
[warning] 19-19: Avoid using the initial state variable in setState
Context: setInterval(markOnline, HEARTBEAT_INTERVAL)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(setstate-same-var)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/providers/PresenceProvider.jsx` around lines 15 - 27, Update
markOnline in PresenceProvider so it only calls setUserOnline(user._id, true)
when document.visibilityState is "visible"; preserve the existing markOffline
behavior and interval setup so hidden tabs remain offline between visibility
events.
| useEffect(() => { | ||
| if (!userId) return; | ||
| const unsubscribe = listenToPresence(userId, setPresence); | ||
| return () => unsubscribe(); | ||
| }, [userId]); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reset presence before changing subscriptions.
When userId becomes null or switches to another participant, this effect retains the prior user's status until a new snapshot arrives. Reset to { online: false, lastSeen: null } before the early return/subscription so a previous conversation’s presence is never shown for the next one.
Proposed fix
useEffect(() => {
+ setPresence({ online: false, lastSeen: null });
if (!userId) return;
const unsubscribe = listenToPresence(userId, setPresence);
return () => unsubscribe();
}, [userId]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| if (!userId) return; | |
| const unsubscribe = listenToPresence(userId, setPresence); | |
| return () => unsubscribe(); | |
| }, [userId]); | |
| useEffect(() => { | |
| setPresence({ online: false, lastSeen: null }); | |
| if (!userId) return; | |
| const unsubscribe = listenToPresence(userId, setPresence); | |
| return () => unsubscribe(); | |
| }, [userId]); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@hooks/usePresence.js` around lines 8 - 12, Update the useEffect in
usePresence to reset presence to { online: false, lastSeen: null } before the
!userId early return and before subscribing to a new user. Preserve the existing
listenToPresence subscription and cleanup behavior for valid userId values.
| return onSnapshot(userRef, (snapshot) => { | ||
| if (snapshot.exists()) { | ||
| const data = snapshot.data(); | ||
| callback({ | ||
| online: data.online ?? false, | ||
| lastSeen: data.lastSeen ?? null, | ||
| }); | ||
| } else { | ||
| callback({ online: false, lastSeen: null }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Expire stale online records locally.
A disconnected client can leave online: true behind. Since this listener only reacts to Firestore snapshots, it will continue reporting that user as online forever when no later write arrives. Derive freshness from lastSeen and schedule an expiry callback per subscription (cleared by the returned unsubscribe), using a threshold coordinated with the 45-second heartbeat. This shared fix covers both the room header and conversation-list indicators.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/actions/messages/listen-to-presence.js` around lines 6 - 16, Update the
onSnapshot subscription in listen-to-presence.js to treat online records as
stale when lastSeen exceeds the threshold coordinated with the 45-second
heartbeat. Derive freshness from lastSeen, schedule or reschedule an expiry
callback for each subscription that reports { online: false, lastSeen: null },
and ensure the timer is cleared when the returned unsubscribe function is
invoked.
…ng UX
Add Firestore presence listener (
listen-to-presence) following the existing typing subscription patternCreate reusable
usePresencehook for subscribing to user online status and last seen updatesIntroduce
PresenceProviderto manage presence lifecycle across the dashboardMount
PresenceProviderin the dashboard layout to automatically track authenticated user presenceImplement presence updates by:
lastSeenupdated for accurate offline statusReplace hardcoded conversation header presence with live Firestore data
Display "Active now" with a green indicator for online users
Show "Last seen " for offline users using the stored
lastSeentimestampAdd real-time presence indicators to conversation rows in
MessagesHeadSideListDisplay online status consistently across both the conversation list and active chat header
Improve message send failure handling by:
alert()dialogs withtoast.error()Implementation details:
listen-to-presence.jsfor Firestore presence subscriptionsusePresencehookPresenceProviderfor application-wide presence managementVerification:
lastSeenafter session endsClose #87
Summary by CodeRabbit