FEATURE: respect user presence when chat is on screen (#36652)

Handle, timeout of user so we stop reading and scrolling when user is in
background or no longer active

---------

Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Co-authored-by: David Taylor <david@taylorhq.com>
This commit is contained in:
Sam
2025-12-16 17:23:32 +11:00
committed by GitHub
co-authored by Joffrey JAFFEUX David Taylor
parent 5e46fd35f9
commit d85becccef
10 changed files with 947 additions and 86 deletions
@@ -75,7 +75,7 @@ export default {
// present
// When 20 minutes pass we stop long polling due to "shouldLongPollCallback".
onPresenceChange({
unseenTime: LONG_POLL_AFTER_UNSEEN_TIME,
userUnseenTime: LONG_POLL_AFTER_UNSEEN_TIME,
callback: (present) => {
if (present && messageBus.onVisibilityChange) {
messageBus.onVisibilityChange();
+29 -10
View File
@@ -7,11 +7,10 @@ const DEFAULT_BROWSER_HIDDEN_MS = 0;
let browserHiddenAt = null;
let lastUserActivity = Date.now();
let userSeenJustNow = false;
let callbackWaitingForPresence = false;
let testPresence = true;
let debounceUpdateDateTimeout = null;
// 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
@@ -52,17 +51,22 @@ export function onPresenceChange({
if (userUnseenTime < DEFAULT_USER_UNSEEN_MS) {
throw `userUnseenTime must be at least ${DEFAULT_USER_UNSEEN_MS}`;
}
if (browserHiddenTime < 0) {
throw "browserHiddenTime must be non-negative";
}
callbacks.push({
userUnseenTime,
browserHiddenTime,
lastState: true,
lastState: userPresent({ userUnseenTime, browserHiddenTime }),
callback,
});
}
export function removeOnPresenceChange(callback) {
const i = callbacks.findIndex((c) => c.callback === callback);
callbacks.splice(i, 1);
if (i > -1) {
callbacks.splice(i, 1);
}
}
function processChanges() {
@@ -71,11 +75,6 @@ function processChanges() {
browserHiddenAt = browserHidden ? Date.now() : null;
}
if (userSeenJustNow) {
lastUserActivity = Date.now();
userSeenJustNow = false;
}
callbackWaitingForPresence = false;
for (const callback of callbacks) {
const currentState = userPresent({
@@ -86,6 +85,9 @@ function processChanges() {
if (callback.lastState !== currentState) {
try {
callback.callback(currentState);
} catch (e) {
// eslint-disable-next-line no-console
console.error("Error in presence change callback:", e);
} finally {
callback.lastState = currentState;
}
@@ -98,9 +100,25 @@ function processChanges() {
}
export function seenUser() {
userSeenJustNow = true;
// a boolean check is going to be 10x to 80x faster than Date.now()
// scroll, touchmove, click, keydown can all happen very frequently
// this debounces to de-risk
if (callbackWaitingForPresence) {
if (debounceUpdateDateTimeout) {
clearTimeout(debounceUpdateDateTimeout);
debounceUpdateDateTimeout = null;
}
// we are in the background waiting for presence, do this right away
lastUserActivity = Date.now();
processChanges();
} else {
// app is in foreground, debounce updates to lastUserActivity
if (!debounceUpdateDateTimeout) {
debounceUpdateDateTimeout = setTimeout(() => {
debounceUpdateDateTimeout = null;
lastUserActivity = Date.now();
}, 1000);
}
}
}
@@ -127,6 +145,7 @@ 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 });
document.addEventListener("keydown", seenUser, { passive: true });
window.addEventListener("scroll", seenUser, { passive: true });
window.addEventListener("focus", seenUser, { passive: true });