Skip to content

Skip presence ping for hidden tabs and refresh on visibility#20

Merged
josephfusco merged 5 commits into
mainfrom
feat/16-visibility-aware-presence
May 15, 2026
Merged

Skip presence ping for hidden tabs and refresh on visibility#20
josephfusco merged 5 commits into
mainfrom
feat/16-visibility-aware-presence

Conversation

@josephfusco

Copy link
Copy Markdown
Collaborator

Closes #16.

A user with a background tab on a presence-enabled page was still reported as actively present.

Change

Two small additions to the inline heartbeat script:

  1. In the heartbeat-send handler, return early when document.visibilityState === 'hidden'. The presence-ping is omitted, and the existing 60 s TTL plus the cron sweep clean up the stale entry naturally.
  2. On visibilitychange to 'visible', call wp.heartbeat.connectNow() so the user is re-established in the room within milliseconds rather than waiting for the next heartbeat interval.

This is intentionally a soft fix (no explicit DELETE) so it can land independently of any pagehide-based cleanup. If we later add pagehide DELETEs (see #15 / #19), a small follow-up could fire the same leave() on hidden as well; out of scope here to keep this diff minimal.

Files

  • includes/heartbeat.php

Verification

With wp-env running, log in as editor in window A and as admin in window B. From the admin window, watch the Who's Online widget. In window A, switch to a different tab — within roughly a heartbeat interval the editor's screen should stop updating. Switch back to the admin tab and the editor's presence should refresh on the next tick.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the Presence API Heartbeat inline script to better reflect user activity by factoring in Page Visibility, so background tabs do not keep a user marked as actively present and visibility restoration refreshes presence immediately.

Changes:

  • Skip sending the presence-ping payload on heartbeat-send when document.visibilityState === 'hidden'.
  • On visibilitychange back to 'visible', trigger an immediate Heartbeat tick via wp.heartbeat.connectNow() to re-establish presence without waiting for the next interval.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread includes/heartbeat.php Outdated
Comment thread includes/heartbeat.php Outdated
Comment thread includes/heartbeat.php Outdated
Comment thread includes/heartbeat.php Outdated
josephfusco added a commit that referenced this pull request May 14, 2026
Page Visibility is universally supported across all browsers WP
targets; the typeof string-check was belt-and-braces. Per Weston's
review on PR #20.
josephfusco added a commit that referenced this pull request May 14, 2026
The previous commit's visibility guard only skipped the main
presence-ping. The editor heartbeat handler — registered separately by
wp_presence_enqueue_editor_ping() — kept refreshing presence-editor-ping
in the per-post room, so a backgrounded post-edit tab still appeared
as actively editing in the Active Posts widget.

Apply the same hidden-state early-return inside the editor handler.

Also softens the comment wording on the main guard to refer to the
'default TTL' rather than '60s TTL', since the TTL is filterable via
wp_presence_default_ttl.

Per Copilot's review on PR #20.
josephfusco added a commit that referenced this pull request May 14, 2026
Three e2e tests, all in tests/e2e/presence-visibility.test.js:

- heartbeat-send omits presence-ping while document is hidden;
  on visibility restore, the ping reappears with a screen field.
- heartbeat-send omits presence-editor-ping while the post-edit
  tab is hidden; on visibility restore, it returns with the
  correct post id.
- A visibilitychange to 'visible' triggers an immediate
  wp.heartbeat.connectNow() call (counted via a method wrapper).

Playwright headless doesn't fire real visibilitychange when a tab
is backgrounded, so the tests stub document.visibilityState via
Object.defineProperty and dispatch the event manually. The plugin's
heartbeat-send handler runs before the test's one-shot listener, so
the data object reflects whatever the guard did (or didn't do).

Per Copilot's review on PR #20.
josephfusco added a commit that referenced this pull request May 14, 2026
- includes/heartbeat.php: use optional chaining on the connectNow
  guard inside the visibilitychange handler. Broaden the comment so
  it acknowledges that 'hidden' covers more than tab backgrounding
  (minimized windows, app switching).
- tests/e2e/presence-visibility.test.js: replace the magic
  waitForTimeout(1000) warm-up with an explicit waitForFunction on
  wp.heartbeat. Clean up created posts via afterEach. Tighten the
  visibility-restore test by snapshotting the connectNow call counter
  before flipping visible and asserting the delta is at least one,
  rather than checking the absolute counter.
@josephfusco josephfusco marked this pull request as ready for review May 14, 2026 17:41
Fixes #16.

A user with a background tab on a presence-enabled page was still
reported as actively present. Two changes:

1. In the heartbeat-send handler, return early when
   document.visibilityState === 'hidden' so the presence-ping is
   omitted. The existing 60s TTL cleans up the stale entry naturally.

2. On visibilitychange to 'visible', call wp.heartbeat.connectNow() so
   the user is re-established in the room within milliseconds rather
   than waiting for the next heartbeat interval.

This is intentionally a soft fix (no explicit DELETE) so it can land
independently of any pagehide-based cleanup.
Page Visibility is universally supported across all browsers WP
targets; the typeof string-check was belt-and-braces. Per Weston's
review on PR #20.
The previous commit's visibility guard only skipped the main
presence-ping. The editor heartbeat handler — registered separately by
wp_presence_enqueue_editor_ping() — kept refreshing presence-editor-ping
in the per-post room, so a backgrounded post-edit tab still appeared
as actively editing in the Active Posts widget.

Apply the same hidden-state early-return inside the editor handler.

Also softens the comment wording on the main guard to refer to the
'default TTL' rather than '60s TTL', since the TTL is filterable via
wp_presence_default_ttl.

Per Copilot's review on PR #20.
Three e2e tests, all in tests/e2e/presence-visibility.test.js:

- heartbeat-send omits presence-ping while document is hidden;
  on visibility restore, the ping reappears with a screen field.
- heartbeat-send omits presence-editor-ping while the post-edit
  tab is hidden; on visibility restore, it returns with the
  correct post id.
- A visibilitychange to 'visible' triggers an immediate
  wp.heartbeat.connectNow() call (counted via a method wrapper).

Playwright headless doesn't fire real visibilitychange when a tab
is backgrounded, so the tests stub document.visibilityState via
Object.defineProperty and dispatch the event manually. The plugin's
heartbeat-send handler runs before the test's one-shot listener, so
the data object reflects whatever the guard did (or didn't do).

Per Copilot's review on PR #20.
- includes/heartbeat.php: use optional chaining on the connectNow
  guard inside the visibilitychange handler. Broaden the comment so
  it acknowledges that 'hidden' covers more than tab backgrounding
  (minimized windows, app switching).
- tests/e2e/presence-visibility.test.js: replace the magic
  waitForTimeout(1000) warm-up with an explicit waitForFunction on
  wp.heartbeat. Clean up created posts via afterEach. Tighten the
  visibility-restore test by snapshotting the connectNow call counter
  before flipping visible and asserting the delta is at least one,
  rather than checking the absolute counter.
@josephfusco josephfusco force-pushed the feat/16-visibility-aware-presence branch from 4e17888 to 3fbba1a Compare May 15, 2026 12:01
@josephfusco josephfusco merged commit acf2558 into main May 15, 2026
4 checks passed
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.

Page visibility should be factored into presence

3 participants