mirror of
https://github.com/discourse/discourse.git
synced 2024-12-01 21:19:41 -06:00
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`
This commit is contained in:
parent
6e2d4a14ac
commit
fd93d6f955
@ -46,7 +46,7 @@ export default {
|
||||
|
||||
messageBus.alwaysLongPoll = !isProduction();
|
||||
messageBus.shouldLongPollCallback = () =>
|
||||
userPresent(LONG_POLL_AFTER_UNSEEN_TIME);
|
||||
userPresent({ userUnseenTime: LONG_POLL_AFTER_UNSEEN_TIME });
|
||||
|
||||
// we do not want to start anything till document is complete
|
||||
messageBus.stop();
|
||||
@ -56,7 +56,11 @@ export default {
|
||||
// When 20 minutes pass we stop long polling due to "shouldLongPollCallback".
|
||||
onPresenceChange({
|
||||
unseenTime: LONG_POLL_AFTER_UNSEEN_TIME,
|
||||
callback: () => document.dispatchEvent(new Event("visibilitychange")),
|
||||
callback: (present) => {
|
||||
if (present && messageBus.onVisibilityChange) {
|
||||
messageBus.onVisibilityChange();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (siteSettings.login_required && !user) {
|
||||
|
@ -1,15 +0,0 @@
|
||||
// for android we test webkit
|
||||
let hiddenProperty =
|
||||
document.hidden !== undefined
|
||||
? "hidden"
|
||||
: document.webkitHidden !== undefined
|
||||
? "webkitHidden"
|
||||
: undefined;
|
||||
|
||||
export default function () {
|
||||
if (hiddenProperty !== undefined) {
|
||||
return !document[hiddenProperty];
|
||||
} else {
|
||||
return document && document.hasFocus;
|
||||
}
|
||||
}
|
@ -1,68 +1,138 @@
|
||||
// for android we test webkit
|
||||
const hiddenProperty =
|
||||
document.hidden !== undefined
|
||||
? "hidden"
|
||||
: document.webkitHidden !== undefined
|
||||
? "webkitHidden"
|
||||
: undefined;
|
||||
|
||||
const MAX_UNSEEN_TIME = 60000;
|
||||
|
||||
let seenUserTime = Date.now();
|
||||
|
||||
export default function (maxUnseenTime) {
|
||||
maxUnseenTime = maxUnseenTime === undefined ? MAX_UNSEEN_TIME : maxUnseenTime;
|
||||
const now = Date.now();
|
||||
|
||||
if (seenUserTime + maxUnseenTime < now) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hiddenProperty !== undefined) {
|
||||
return !document[hiddenProperty];
|
||||
} else {
|
||||
return document && document.hasFocus;
|
||||
}
|
||||
}
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
|
||||
const callbacks = [];
|
||||
|
||||
const MIN_DELTA = 60000;
|
||||
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() {
|
||||
let lastSeenTime = seenUserTime;
|
||||
seenUserTime = Date.now();
|
||||
let delta = seenUserTime - lastSeenTime;
|
||||
|
||||
if (lastSeenTime && delta > MIN_DELTA) {
|
||||
callbacks.forEach((info) => {
|
||||
if (delta > info.unseenTime) {
|
||||
info.callback();
|
||||
}
|
||||
});
|
||||
userSeenJustNow = true;
|
||||
if (callbackWaitingForPresence) {
|
||||
processChanges();
|
||||
}
|
||||
}
|
||||
|
||||
// register a callback for cases where presence changed
|
||||
export function onPresenceChange({ unseenTime, callback }) {
|
||||
if (unseenTime < MIN_DELTA) {
|
||||
throw "unseenTime is too short";
|
||||
export function visibilityChanged() {
|
||||
if (document.hidden) {
|
||||
processChanges();
|
||||
} else {
|
||||
seenUser();
|
||||
}
|
||||
callbacks.push({ unseenTime, callback });
|
||||
}
|
||||
|
||||
// We could piggieback on the Scroll mixin, but it is not applied
|
||||
// consistently to all pages
|
||||
//
|
||||
// We try to keep this as cheap as possible by performing absolute minimal
|
||||
// amount of work when the event handler is fired
|
||||
//
|
||||
// An alternative would be to use a timer that looks at the scroll position
|
||||
// however this will not work as message bus can issue page updates and scroll
|
||||
// page around when user is not present
|
||||
//
|
||||
// We avoid tracking mouse move which would be very expensive
|
||||
export function setTestPresence(value) {
|
||||
if (!isTesting()) {
|
||||
throw "Only available in test mode";
|
||||
}
|
||||
testPresence = value;
|
||||
}
|
||||
|
||||
$(document).bind("touchmove.discourse-track-presence", seenUser);
|
||||
$(document).bind("click.discourse-track-presence", seenUser);
|
||||
$(window).bind("scroll.discourse-track-presence", seenUser);
|
||||
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);
|
||||
}
|
||||
|
@ -54,6 +54,10 @@ import { resetLastEditNotificationClick } from "discourse/models/post-stream";
|
||||
import { clearAuthMethods } from "discourse/models/login-method";
|
||||
import { clearTopicFooterDropdowns } from "discourse/lib/register-topic-footer-dropdown";
|
||||
import { clearTopicFooterButtons } from "discourse/lib/register-topic-footer-button";
|
||||
import {
|
||||
clearPresenceCallbacks,
|
||||
setTestPresence,
|
||||
} from "discourse/lib/user-presence";
|
||||
|
||||
const LEGACY_ENV = !setupApplicationTest;
|
||||
|
||||
@ -297,6 +301,10 @@ export function acceptance(name, optionsOrCallback) {
|
||||
clearTopicFooterButtons();
|
||||
resetLastEditNotificationClick();
|
||||
clearAuthMethods();
|
||||
setTestPresence(true);
|
||||
if (!LEGACY_ENV) {
|
||||
clearPresenceCallbacks();
|
||||
}
|
||||
|
||||
app._runInitializer("instanceInitializers", (_, initializer) => {
|
||||
initializer.teardown?.();
|
||||
|
Loading…
Reference in New Issue
Block a user