Skip to content

Commit 45e1a17

Browse files
committed
feat: expose the room's collaborator count as JS hooks
presence-heartbeat-collaborators (#451) only reached presence-ping.js's own tick handler. wp.hooks lets external code, like Gutenberg's sync poll loop, react to a room's editor count instead of polling for it.
1 parent 1e45a95 commit 45e1a17

5 files changed

Lines changed: 417 additions & 14 deletions

File tree

‎README.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,25 @@ add_action( 'wp_presence_collaboration_ended', function( $room, $entries ) {
206206
}, 10, 2 );
207207
```
208208

209+
### JS Actions
210+
Fired through `wp.hooks`, not PHP. `presence-ping.js` is the only thing that computes these, so a consumer has to listen rather than poll for them.
211+
212+
#### `presence-api.watchingRoom`
213+
Fires once, synchronously, before Heartbeat's first tick. Only on a post-edit screen for a post type with `presence` support — lets a listener tell "presence-api isn't here" apart from "here, no tick yet."
214+
```js
215+
wp.hooks.addAction( 'presence-api.watchingRoom', 'my-plugin', ( room ) => {
216+
// room is 'postType/{type}:{id}'
217+
} );
218+
```
219+
220+
#### `presence-api.collaboratorsChanged`
221+
Fires on the same 1-to-2+ and 2+-to-1 edges as `wp_presence_collaboration_started`/`_ended`, not on every tick.
222+
```js
223+
wp.hooks.addAction( 'presence-api.collaboratorsChanged', 'my-plugin', ( room, count ) => {
224+
// count > 1 means someone besides you is in the room.
225+
} );
226+
```
227+
209228
</details>
210229

211230
## REST API
@@ -265,6 +284,8 @@ No room mapping sits between the two: both use `postType/{type}:{id}`, the gramm
265284

266285
That leaves the split: awareness and cursors inside the editor go through sync-storage; room membership everywhere else in wp-admin — plus the post-lock bridge below — stays this plugin's.
267286

287+
The JS actions above exist for that same relationship. A consumer like Gutenberg's sync poll loop ([presence-api#444](https://github.com/WordPress/presence-api/issues/444)) can wait for `presence-api.collaboratorsChanged` instead of polling to find out whether anyone else is in the room.
288+
268289
## Post-lock bridge
269290

270291
Creates presence entries alongside `_edit_lock` postmeta when a post lock is refreshed via Heartbeat. Both systems coexist.

‎assets/js/presence-ping.js‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
( function ( $ ) {
2-
if ( typeof wp === 'undefined' || typeof wp.heartbeat === 'undefined' ) {
2+
if (
3+
typeof wp === 'undefined' ||
4+
typeof wp.heartbeat === 'undefined' ||
5+
typeof wp.hooks === 'undefined'
6+
) {
37
return;
48
}
59

610
const config = window.wpPresenceConfig || {};
711
const entries = Array.isArray( config.entries ) ? config.entries : [];
812
const frontContext = config.frontContext || null;
913
const editorPostId = parseInt( config.editorPostId, 10 ) || 0;
14+
const editorRoom = config.editorRoom || '';
1015
const restUrl = config.restUrl || '';
1116
const nonce = config.nonce || '';
1217
const idleTicks = parseInt( config.idleTicks, 10 ) || 0;
@@ -15,12 +20,22 @@
1520
const backoffEnabled = idleTicks > 0 && idleInterval > 0;
1621
const TTL_SAFETY_MARGIN = 15;
1722

23+
// Fired synchronously, ahead of Heartbeat's first tick, so a listener can
24+
// tell "presence-api isn't here" apart from "here, no tick yet."
25+
if ( editorRoom ) {
26+
wp.hooks.doAction( 'presence-api.watchingRoom', editorRoom );
27+
}
28+
1829
// Guards against duplicate leave() invocations.
1930
let hasLeft = false;
2031

2132
let unchangedTicks = 0;
2233
let lastOnlineHash = '';
2334
let normalInterval = null;
35+
// Matches wp_presence_check_collaboration_threshold()'s own default: with
36+
// nothing observed yet, assume solo, so a fresh room's first tick at count
37+
// 1 isn't mistaken for a 2+-to-1 edge.
38+
let hasCollaborators = false;
2439

2540
// Reads the interval lazily (not at page load) so it reflects whatever
2641
// another script, e.g. post.js's lock-refresh interval, already set.
@@ -114,6 +129,33 @@
114129
}
115130
} );
116131

132+
if ( editorRoom ) {
133+
$( document ).on( 'heartbeat-tick', function ( event, data ) {
134+
if (
135+
! Object.prototype.hasOwnProperty.call(
136+
data,
137+
'presence-heartbeat-collaborators'
138+
)
139+
) {
140+
return;
141+
}
142+
143+
const count = data[ 'presence-heartbeat-collaborators' ];
144+
const nowHasCollaborators = count > 1;
145+
146+
if ( nowHasCollaborators === hasCollaborators ) {
147+
return;
148+
}
149+
150+
hasCollaborators = nowHasCollaborators;
151+
wp.hooks.doAction(
152+
'presence-api.collaboratorsChanged',
153+
editorRoom,
154+
count
155+
);
156+
} );
157+
}
158+
117159
if ( backoffEnabled ) {
118160
// Reuses the Who's Online widget's hash exchange on every screen,
119161
// not just the Dashboard, to learn whether the room changed.

‎includes/heartbeat.php‎

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ function wp_presence_enqueue_heartbeat_ping() {
126126

127127
// On the post-edit screen, also occupy the per-post room.
128128
$editor_post_id = 0;
129+
$editor_room = '';
129130
if ( is_admin() && function_exists( 'get_current_screen' ) ) {
130131
$screen = get_current_screen();
131132
if ( $screen && 'post' === $screen->base ) {
@@ -134,6 +135,7 @@ function wp_presence_enqueue_heartbeat_ping() {
134135
$room = wp_presence_post_room( $post->ID );
135136
if ( $room ) {
136137
$editor_post_id = $post->ID;
138+
$editor_room = $room;
137139
$entries[] = array(
138140
'room' => $room,
139141
'client_id' => 'editor-' . $user_id,
@@ -160,24 +162,24 @@ function wp_presence_enqueue_heartbeat_ping() {
160162
}
161163
wp_set_presence( wp_presence_admin_room(), 'user-' . $user_id, $admin_state, $user_id );
162164

163-
if ( $editor_post_id ) {
164-
$editor_room = wp_presence_post_room( $editor_post_id );
165-
if ( $editor_room ) {
166-
// No tick has carried a lock refresh yet. connectNow() on load makes
167-
// that a single request, not a visible state.
168-
wp_set_presence(
169-
$editor_room,
170-
'editor-' . $user_id,
171-
wp_presence_editor_state( $screen_id, false ),
172-
$user_id
173-
);
174-
}
165+
if ( $editor_room ) {
166+
// No tick has carried a lock refresh yet. connectNow() on load makes
167+
// that a single request, not a visible state.
168+
wp_set_presence(
169+
$editor_room,
170+
'editor-' . $user_id,
171+
wp_presence_editor_state( $screen_id, false ),
172+
$user_id
173+
);
175174
}
176175

177176
$config = array(
178177
'entries' => $entries,
179178
'frontContext' => $front_context,
180179
'editorPostId' => $editor_post_id,
180+
// Lets presence-ping.js fire `presence-api.watchingRoom` without
181+
// duplicating the postType/{type}:{id} grammar client-side.
182+
'editorRoom' => $editor_room,
181183
'restUrl' => esc_url_raw( rest_url( 'wp-presence/v1/presence' ) ),
182184
'nonce' => wp_create_nonce( 'wp_rest' ),
183185
'idleTicks' => wp_presence_get_heartbeat_idle_ticks(),
@@ -196,7 +198,7 @@ function wp_presence_enqueue_heartbeat_ping() {
196198
wp_enqueue_script(
197199
'wp-presence-ping',
198200
WP_PRESENCE_PLUGIN_URL . 'assets/js/presence-ping.js',
199-
array( 'jquery', 'heartbeat', 'wp-presence-tab-coordinator' ),
201+
array( 'jquery', 'heartbeat', 'wp-hooks', 'wp-presence-tab-coordinator' ),
200202
WP_PRESENCE_VERSION,
201203
true
202204
);

0 commit comments

Comments
 (0)