Skip to content

feat: Add user presence tracking and online status visibility - #884

Merged
aXenDeveloper merged 9 commits into
canaryfrom
claude/user-presence-system-g531gn
Oct 30, 2026
Merged

aXenDeveloper merged 9 commits into
canaryfrom
claude/user-presence-system-g531gn

Conversation

@aXenDeveloper

Copy link
Copy Markdown
Owner

Description

What?

This PR implements a comprehensive user presence system that tracks when users are online and last seen, with privacy controls to hide online status if desired.

Key features:

  • Presence tracking: Records lastSeenAt timestamp and hideOnlineStatus privacy flag on users
  • Heartbeat mechanism: Frontend sends periodic heartbeats (every 60 seconds) to keep presence updated
  • Batch presence queries: Efficient endpoint to fetch presence for multiple users at once
  • Privacy controls: Users can hide their online status in account settings
  • Permission-based visibility: Staff with can_view_presence permission can see hidden statuses
  • Admin interface: New presence card in user detail view showing online status and last activity
  • Profile integration: Display online status on user profiles with appropriate privacy filtering
  • React Query integration: Query options and hooks for presence data with proper caching

New API endpoints:

  • POST /api/users/presence/heartbeat - Update user presence
  • POST /api/users/presence/batch - Fetch presence for multiple users
  • PATCH /api/users/privacy - Update privacy settings
  • GET /api/admin/users/:id/presence - Admin endpoint for user presence

New UI components:

  • UserPresenceStatus - Displays online/offline status with last seen time
  • Privacy settings in account overview
  • User presence card in admin user detail view

Why?

User presence is a fundamental feature for community platforms, allowing members to see who's online and when others were last active. This implementation provides:

  • Better user engagement by showing activity status
  • Privacy-respecting design with opt-out capability
  • Efficient data fetching with batch queries and caching
  • Admin visibility for moderation purposes
  • Extensible architecture for plugins to display presence information

The heartbeat approach keeps presence fresh without requiring persistent connections, and the batch endpoint prevents N+1 queries when displaying presence for multiple users.

https://claude.ai/code/session_0198jw2ioVCHrx7GaSPcZND3

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T17:06:09.691573Z 65a6202 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

React Doctor found 2 new issues in 2 files · 2 warnings · score 89 / 100 (Great) · 0 fixed · vs canary

2 warnings

src/views/admin/views/core/users/roles/role-form-content.tsx

  • ⚠️ L253 Large component is hard to read and change no-giant-component

src/views/auth/settings/overview/privacy-settings.tsx

  • ⚠️ L50 Loading flag reset outside finally no-loading-flag-reset-outside-finally

Reviewed by React Doctor for commit 65a6202. See inline comments for fixes.

@aXenDeveloper aXenDeveloper changed the title Add user presence tracking and online status visibility feat: Add user presence tracking and online status visibility Oct 30, 2026
@github-actions github-actions Bot added the 💡 Feature A new feature label Oct 30, 2026
@aXenDeveloper
aXenDeveloper force-pushed the claude/user-presence-system-g531gn branch from e61287b to bf25319 Compare October 30, 2026 16:18

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bf25319156

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

export const presenceSessionId = async (tokenHash: string): Promise<string> =>
(await hashSessionToken(`presence:${tokenHash}`)).slice(0, 32);

export class UserPresenceModel {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Document the public presence model at its declaration

UserPresenceModel is explicitly introduced as a plugin-facing API in presence-in-plugins.mdx, but its exported class has no JSDoc describing its server-only runtime or showing a minimal import-and-use example. Plugin authors consuming the declaration therefore miss the runtime restriction at the entry point. Add concise class-level JSDoc with that restriction and an @example; the tutorial does not replace the declaration documentation required by AGENTS.md.

AGENTS.md reference: AGENTS.md:L98-L101

Useful? React with 👍 / 👎.

…e anonymously

- `core_users.lastSeenAt` and `core_users.hideOnlineStatus` with a migration
- `UserPresenceModel`: per-session leases in a Redis sorted set (one per
  member, each session expiring on its own), bulk reads in one query and
  one pipeline, throttled forward-only `lastSeenAt` writes; without Redis
  presence falls back to `lastSeenAt` within the lease window
- `CacheModel` lease-set primitives: `renewLease`, `releaseLeases`,
  `countLeases`, `isConfigured`
- API: `POST /users/presence/heartbeat`, `GET /users/{id}/presence`,
  `POST /users/presence/batch`, `PUT /users/me/privacy` and
  `GET /admin/users/{id}/presence`, all `private, no-store`
- `users:can_view_hidden_presence` for admins and moderators; one privacy
  resolver for every read
- sign-out and device revocation end only that session's lease
- client: visibility-aware heartbeat shared across tabs, profile indicator,
  "Browse anonymously" setting, AdminCP presence card and a reusable
  `UserPresenceStatus` component

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198jw2ioVCHrx7GaSPcZND3
- `core_roles.allowBrowseAnonymously` (default true) and
  `core_roles.forceBrowseAnonymously` (default false), edited in the role
  dialog's Profile tab
- one forcing role hides the member; otherwise every role must allow the
  choice. Presence reads resolve it in bulk, so a role change applies to the
  next read
- `PUT /users/me/privacy` answers 403 while the roles decide;
  `GET /users/me/policy` reports `browseAnonymously`
- the settings switch stays visible but disabled, with a tooltip saying why

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198jw2ioVCHrx7GaSPcZND3
…settings

Regenerated on top of canary's latest migration, and formats
`sso-callback-content.tsx` so `pnpm lint` passes again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198jw2ioVCHrx7GaSPcZND3
…ed switch

- `browseAnonymouslyPolicyOf` and `isPresenceHidden` decision tables
- presence reads apply role policy: forcing and disallowing roles, secondary
  roles, and a role change on the next read
- presence reads stay at three queries however many members (was one before
  the roles lookup)
- `PUT /users/me/privacy` answers 403 while the roles decide;
  `GET /users/me/policy` reports `browseAnonymously`
- `PrivacySettings` locks the switch with a reason for forced/disallowed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198jw2ioVCHrx7GaSPcZND3
A disabled switch swallows the pointer, so hovering it never reached the
tooltip trigger around it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198jw2ioVCHrx7GaSPcZND3
…itch

- "Role settings" on User Presence: Allow / Always browse anonymously,
  how roles combine, the disabled switch with its tooltip and the 403
- real screenshots of the role dialog's Profile tab and the locked switch
- `GET /users/me/policy` in the endpoint table; `getMany` now documents its
  three constant queries
- links from Roles and Profile fields

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198jw2ioVCHrx7GaSPcZND3
@aXenDeveloper
aXenDeveloper force-pushed the claude/user-presence-system-g531gn branch from bf25319 to 65a6202 Compare October 30, 2026 17:03
onCheckedChange={async next => {
setIsSaving(true);
const result = await onUpdate({ hideOnlineStatus: next });
setIsSaving(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/no-loading-flag-reset-outside-finally (warning)

This resets a loading/busy flag only on the success path: if the awaited call rejects the reset never runs and the flag stays stuck truthy (a spinner that never stops, a button disabled forever). Move the reset into a finally block, or mirror it on every catch, so it clears on rejection too.

Fix → A trailing setLoading(false) after an await never runs if the awaited call rejects, so the flag stays stuck truthy; reset it in a finally block (or mirror the reset on every catch) so it clears on both paths.

Docs

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 65a6202530

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +104 to +108
/**
* Many members' presence, keyed by user ID - one request per
* {@link PRESENCE_BATCH_MAX_USERS} members, however long the list.
*/
export const userPresencesQueryOptions = ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Document the public batch-presence query's runtime and usage

userPresencesQueryOptions is presented as a plugin-facing entry point in presence-in-plugins.mdx, but its declaration does not document the browser-only constraint, explain that viewerId must identify the current session for privacy-safe caching, or provide a minimal import-and-use @example. Unlike the single-user helper, it does not even warn against warming this query in a loader. Add these constraints and an example to the original declaration so plugin authors consuming its API documentation do not inadvertently place viewer-specific presence in SSR output or the page cache; the tutorial does not replace the required declaration documentation.

AGENTS.md reference: AGENTS.md:L98-L101

Useful? React with 👍 / 👎.

@aXenDeveloper
aXenDeveloper merged commit 7fe69aa into canary Oct 30, 2026
5 checks passed
@aXenDeveloper
aXenDeveloper deleted the claude/user-presence-system-g531gn branch October 30, 2026 17:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💡 Feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants