Skip to content

Instantly share code, notes, and snippets.

@sethrubenstein
Last active April 16, 2026 17:46
Show Gist options
  • Select an option

  • Save sethrubenstein/44f3d91e4bc7c796cd08e31dd53e9ae2 to your computer and use it in GitHub Desktop.

Select an option

Save sethrubenstein/44f3d91e4bc7c796cd08e31dd53e9ae2 to your computer and use it in GitHub Desktop.
Using the https://github.com/WordPress/presence-api Presence API this hook will return a flag for if users are detected editing an entity and what users are present.
/**
* WordPress Dependencies
*/
import { useState, useEffect, useRef, useCallback } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
/**
* Default interval (ms) for Presence API REST fetches.
* Matches the default WordPress Heartbeat interval (15 s).
*/
const DEFAULT_INTERVAL_MS = 15000;
/**
* Polls the WordPress Presence API for occupants of a room and resolves display names.
*
* When `room` is null or undefined, no requests run and the hook reports empty presence
* (nothing to observe).
*
* @param {string|null|undefined} room Presence room id (e.g. `postType/chart:123`).
* @param {Object} [options]
* @param {boolean} [options.includeSelf=false] When false, the current user is
* excluded from `users`.
* @param {number} [options.interval=15000] Fetch interval in ms.
*
* @return {{
* isPresent: boolean,
* users: Array<{ userId: number, displayName: string }>
* }}
*/
export default function usePresenceFelt(room, options = {}) {
const { includeSelf = false, interval = DEFAULT_INTERVAL_MS } = options;
const [userIds, setUserIds] = useState([]);
const currentUserId = useSelect(
(select) => select('core').getCurrentUser()?.id,
[]
);
const userNameCache = useRef({});
const prevUserIdKey = useRef('');
const intervalRef = useRef(null);
const fetchPresence = useCallback(async () => {
if (!room) {
return;
}
try {
const entries = await apiFetch({
path: `/wp-presence/v1/presence?room=${encodeURIComponent(
room
)}`,
});
if (!Array.isArray(entries)) {
return;
}
const presentUserIds = entries
.map((e) => Number(e.user_id))
.filter((id) => id > 0)
.filter((id, idx, arr) => arr.indexOf(id) === idx)
.filter((id) => includeSelf || id !== currentUserId);
const idKey = [...presentUserIds].sort().join(',');
if (idKey !== prevUserIdKey.current) {
prevUserIdKey.current = idKey;
const missing = presentUserIds.filter(
(id) => !userNameCache.current[id]
);
if (missing.length) {
await Promise.all(
missing.map(async (userId) => {
try {
const user = await apiFetch({
path: `/wp/v2/users/${userId}`,
});
userNameCache.current[userId] = user.name;
} catch {
userNameCache.current[
userId
] = `User ${userId}`;
}
})
);
}
}
setUserIds(presentUserIds);
} catch {
setUserIds([]);
}
}, [room, currentUserId, includeSelf]);
useEffect(() => {
if (!room) {
setUserIds([]);
prevUserIdKey.current = '';
return;
}
fetchPresence();
intervalRef.current = setInterval(fetchPresence, interval);
return () => {
clearInterval(intervalRef.current);
};
}, [room, interval, fetchPresence]);
const users = userIds.map((userId) => ({
userId,
displayName: userNameCache.current[userId] || `User ${userId}`,
}));
return {
isPresent: users.length > 0,
users,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment