This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.
osr-discourse-src/app/assets/javascripts/discourse/app/lib/user-presence.js
David Taylor fd93d6f955
DEV: lib/user-presence improvements (#15046)
- 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`
2021-11-25 12:07:07 +00:00

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);
}