Repository navigation
feat: Add user presence tracking and online status visibility - #884
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
React Doctor found 2 new issues in 2 files · 2 warnings · score 89 / 100 (Great) · 0 fixed · vs 2 warnings
Reviewed by React Doctor for commit |
e61287b to
bf25319
Compare
There was a problem hiding this comment.
💡 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 { |
There was a problem hiding this comment.
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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198jw2ioVCHrx7GaSPcZND3
…eartbeat Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198jw2ioVCHrx7GaSPcZND3
… fixture 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
bf25319 to
65a6202
Compare
| onCheckedChange={async next => { | ||
| setIsSaving(true); | ||
| const result = await onUpdate({ hideOnlineStatus: next }); | ||
| setIsSaving(false); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| /** | ||
| * Many members' presence, keyed by user ID - one request per | ||
| * {@link PRESENCE_BATCH_MAX_USERS} members, however long the list. | ||
| */ | ||
| export const userPresencesQueryOptions = ({ |
There was a problem hiding this comment.
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 👍 / 👎.
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:
lastSeenAttimestamp andhideOnlineStatusprivacy flag on userscan_view_presencepermission can see hidden statusesNew API endpoints:
POST /api/users/presence/heartbeat- Update user presencePOST /api/users/presence/batch- Fetch presence for multiple usersPATCH /api/users/privacy- Update privacy settingsGET /api/admin/users/:id/presence- Admin endpoint for user presenceNew UI components:
UserPresenceStatus- Displays online/offline status with last seen timeWhy?
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:
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