Skip to content

feat(messages): implement real-time user presence and improve messagi…#144

Merged
zeemscript merged 1 commit into
Deen-Bridge:devfrom
IamOluwatoyin:features/presence-system
Jul 24, 2026
Merged

feat(messages): implement real-time user presence and improve messagi…#144
zeemscript merged 1 commit into
Deen-Bridge:devfrom
IamOluwatoyin:features/presence-system

Conversation

@IamOluwatoyin

@IamOluwatoyin IamOluwatoyin commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

…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 " 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

Close #87

Summary by CodeRabbit

  • New Features
    • Added real-time online presence tracking across the dashboard.
    • Conversation lists now display online indicators for active participants.
    • Chat headers show “Active now” or the participant’s last-seen time.
    • Presence updates respond to page visibility changes and leaving the app.
  • Bug Fixes
    • Message-send failures now appear as in-app error notifications instead of browser alerts.

…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
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

@IamOluwatoyin is attempting to deploy a commit to the Deen Bridge Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The 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.

Changes

Presence tracking

Layer / File(s) Summary
Dashboard presence lifecycle
components/providers/PresenceProvider.jsx, app/dashboard/layout.jsx
Adds a dashboard-level provider that marks authenticated users online, refreshes presence every 45 seconds, handles visibility and unload events, and cleans up listeners and intervals.
Presence subscription layer
lib/actions/messages/listen-to-presence.js, hooks/usePresence.js
Adds Firestore snapshot subscriptions that expose normalized online and lastSeen values through a React hook.
Messaging presence display
app/dashboard/messages/[room]/page.jsx, components/molecules/dashboard/messages/MessagesHeadSideList.jsx
Displays participant presence in chat headers and conversation rows, and replaces send-failure alerts with toast errors.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: zeemscript

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
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Most presence and toast requirements are met, but the summary does not show stale lastSeen being treated as offline. Add a read-time staleness threshold for presence so users are marked offline when lastSeen is too old, even if cleanup never ran.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: real-time presence for messages plus messaging UX improvements.
Out of Scope Changes check ✅ Passed The changes stay focused on message presence, conversation indicators, and send-failure UX, with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8191b42 and f3a5491.

📒 Files selected for processing (6)
  • app/dashboard/layout.jsx
  • app/dashboard/messages/[room]/page.jsx
  • components/molecules/dashboard/messages/MessagesHeadSideList.jsx
  • components/providers/PresenceProvider.jsx
  • hooks/usePresence.js
  • lib/actions/messages/listen-to-presence.js

Comment on lines +15 to +27
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();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment thread hooks/usePresence.js
Comment on lines +8 to +12
useEffect(() => {
if (!userId) return;
const unsubscribe = listenToPresence(userId, setPresence);
return () => unsubscribe();
}, [userId]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment on lines +6 to +16
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 });
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

@zeemscript
zeemscript merged commit a94989c into Deen-Bridge:dev Jul 24, 2026
2 of 3 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jul 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants