- Remove JQuery - Remove legacy `document.webkitHidden` support. None of our currently supported browsers need this - Use `passive` event listeners. These allows the browser to process the events first, before passing control to us - Add a new `unseenTime` parameter. This allows consumers to request a delay before being notified about the browser going into the background - Add a method for removing a callback - Fire the callback when presence changes in either direction. Previously it would only fire when the user becomes present after a period of inactivity. - Ensure callbacks are only called once for each state change. Previously they would be called every 60s, regardless of the value - Listen to the `visibilitychanged` and `focus` events, treating them as equivalent to user action. This will make messagebus re-activate more quickly when switching back to a stale tab - Add test helpers - Delete the unused `discourse/lib/page-visible` module. - Call message-bus's onVisibilityChange API directly, rather than dispatching a fake event on the `document`
139 lines
3.6 KiB
JavaScript
139 lines
3.6 KiB
JavaScript
import { isTesting } from "discourse-common/config/environment";
|
|
|
|
const callbacks = [];
|
|
|
|
const DEFAULT_USER_UNSEEN_MS = 60000;
|
|
const DEFAULT_BROWSER_HIDDEN_MS = 0;
|
|
|
|
let browserHiddenAt = null;
|
|
let lastUserActivity = Date.now();
|
|
let userSeenJustNow = false;
|
|
|
|
let callbackWaitingForPresence = false;
|
|
|
|
let testPresence = true;
|
|
|
|
// Check whether the document is currently visible, and the user is actively using the site
|
|
// Will return false if the browser went into the background more than `browserHiddenTime` milliseconds ago
|
|
// Will also return false if there has been no user activty for more than `userUnseenTime` milliseconds
|
|
// Otherwise, will return true
|
|
export default function userPresent({
|
|
browserHiddenTime = DEFAULT_BROWSER_HIDDEN_MS,
|
|
userUnseenTime = DEFAULT_USER_UNSEEN_MS,
|
|
} = {}) {
|
|
if (isTesting()) {
|
|
return testPresence;
|
|
}
|
|
|
|
if (browserHiddenAt) {
|
|
const timeSinceBrowserHidden = Date.now() - browserHiddenAt;
|
|
if (timeSinceBrowserHidden >= browserHiddenTime) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const timeSinceUserActivity = Date.now() - lastUserActivity;
|
|
if (timeSinceUserActivity >= userUnseenTime) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Register a callback to be triggered when the value of `userPresent()` changes.
|
|
// userUnseenTime and browserHiddenTime work the same as for `userPresent()`
|
|
// 'not present' callbacks may lag by up to 10s, depending on the reason
|
|
// 'now present' callbacks should be almost instantaneous
|
|
export function onPresenceChange({
|
|
userUnseenTime = DEFAULT_USER_UNSEEN_MS,
|
|
browserHiddenTime = DEFAULT_BROWSER_HIDDEN_MS,
|
|
callback,
|
|
} = {}) {
|
|
if (userUnseenTime < DEFAULT_USER_UNSEEN_MS) {
|
|
throw `userUnseenTime must be at least ${DEFAULT_USER_UNSEEN_MS}`;
|
|
}
|
|
callbacks.push({
|
|
userUnseenTime,
|
|
browserHiddenTime,
|
|
lastState: true,
|
|
callback,
|
|
});
|
|
}
|
|
|
|
export function removeOnPresenceChange(callback) {
|
|
const i = callbacks.findIndex((c) => c.callback === callback);
|
|
callbacks.splice(i, 1);
|
|
}
|
|
|
|
function processChanges() {
|
|
const browserHidden = document.hidden;
|
|
if (!!browserHiddenAt !== browserHidden) {
|
|
browserHiddenAt = browserHidden ? Date.now() : null;
|
|
}
|
|
|
|
if (userSeenJustNow) {
|
|
lastUserActivity = Date.now();
|
|
userSeenJustNow = false;
|
|
}
|
|
|
|
callbackWaitingForPresence = false;
|
|
for (const callback of callbacks) {
|
|
const currentState = userPresent({
|
|
userUnseenTime: callback.userUnseenTime,
|
|
browserHiddenTime: callback.browserHiddenTime,
|
|
});
|
|
|
|
if (callback.lastState !== currentState) {
|
|
try {
|
|
callback.callback(currentState);
|
|
} finally {
|
|
callback.lastState = currentState;
|
|
}
|
|
}
|
|
|
|
if (!currentState) {
|
|
callbackWaitingForPresence = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function seenUser() {
|
|
userSeenJustNow = true;
|
|
if (callbackWaitingForPresence) {
|
|
processChanges();
|
|
}
|
|
}
|
|
|
|
export function visibilityChanged() {
|
|
if (document.hidden) {
|
|
processChanges();
|
|
} else {
|
|
seenUser();
|
|
}
|
|
}
|
|
|
|
export function setTestPresence(value) {
|
|
if (!isTesting()) {
|
|
throw "Only available in test mode";
|
|
}
|
|
testPresence = value;
|
|
}
|
|
|
|
export function clearPresenceCallbacks() {
|
|
callbacks.splice(0, callbacks.length);
|
|
}
|
|
|
|
if (!isTesting()) {
|
|
// Some of these events occur very frequently. Therefore seenUser() is as fast as possible.
|
|
document.addEventListener("touchmove", seenUser, { passive: true });
|
|
document.addEventListener("click", seenUser, { passive: true });
|
|
window.addEventListener("scroll", seenUser, { passive: true });
|
|
window.addEventListener("focus", seenUser, { passive: true });
|
|
|
|
document.addEventListener("visibilitychange", visibilityChanged, {
|
|
passive: true,
|
|
});
|
|
|
|
setInterval(processChanges, 10000);
|
|
}
|