diff --git a/src/content/container.ts b/src/content/container.ts index c5eaf2d8f..b964fd2f9 100644 --- a/src/content/container.ts +++ b/src/content/container.ts @@ -9,6 +9,7 @@ import { type ContentToBackgroundMessage, MessageManager, } from '@/shared/messages'; +import { IdleDetection } from './services/idleDetection'; export interface Cradle { logger: Logger; @@ -18,6 +19,7 @@ export interface Cradle { message: MessageManager; monetizationLinkManager: MonetizationLinkManager; frameManager: FrameManager; + idleDetection: IdleDetection; contentScript: ContentScript; } @@ -44,6 +46,11 @@ export const configureContainer = () => { .inject(() => ({ logger: logger.getLogger('content-script:tagManager'), })), + idleDetection: asClass(IdleDetection) + .singleton() + .inject(() => ({ + logger: logger.getLogger('content-script:idleDetection'), + })), contentScript: asClass(ContentScript) .singleton() .inject(() => ({ diff --git a/src/content/services/idleDetection.ts b/src/content/services/idleDetection.ts new file mode 100644 index 000000000..a3f03fc1d --- /dev/null +++ b/src/content/services/idleDetection.ts @@ -0,0 +1,68 @@ +import { Cradle } from '@/content/container'; + +export class IdleDetection { + private document: Cradle['document']; + private logger: Cradle['logger']; + // Pass this to the class to make e2e testing easier instead of declaring + // it here. + private isIdle: boolean = false; + private readonly idleTimeout: number = 10000; + + constructor({ document, logger }: Cradle) { + Object.assign(this, { + document, + logger, + }); + } + + detectUserInactivity() { + let lastActive = Date.now(); + let timeoutId: ReturnType | undefined = undefined; + + const onInactivityTimeoutReached = () => { + const now = Date.now(); + const timeLeft = lastActive + this.idleTimeout - now; + + // There is probably an edge case when the timeout is reached and the user + // will move the mouse at the same time? + if (timeLeft <= 0) { + // Send `STOP_MONETIZATION`. + this.logger.debug( + `No activity from the user - stopping monetization` + + ` Last active: ${new Date(lastActive).toLocaleString()}`, + ); + this.isIdle = true; + } + }; + + const activityListener = () => { + lastActive = Date.now(); + if (this.isIdle) { + this.logger.debug('Detected user activity - resuming monetization'); + // Send `RESUME_MONETIZATION` + this.isIdle = false; + clearTimeout(timeoutId); + timeoutId = setTimeout(onInactivityTimeoutReached, this.idleTimeout); + } + }; + + timeoutId = setTimeout(onInactivityTimeoutReached, this.idleTimeout); + + this.registerDocumentEventListeners(activityListener); + this.logger.debug('Started listening for user activity'); + + // We should probably have a cleanup when the document is not focused? + } + + private registerDocumentEventListeners(fn: () => void) { + // Additional events that we might want to register: + // - mousedown + // - touchstart (not relevant at the moment) + // - touchmove (not relevant at the moment) + // - click + // - keydown + // - scroll (will this bubble when scrolling inside a scrollable element?) + // - wheel + this.document.addEventListener('mousemove', fn); + } +} diff --git a/src/content/services/monetizationLinkManager.ts b/src/content/services/monetizationLinkManager.ts index 2a8ebcd43..07316f8ce 100644 --- a/src/content/services/monetizationLinkManager.ts +++ b/src/content/services/monetizationLinkManager.ts @@ -17,6 +17,7 @@ export class MonetizationLinkManager extends EventEmitter { private document: Cradle['document']; private logger: Cradle['logger']; private message: Cradle['message']; + private idleDetection: Cradle['idleDetection']; private isTopFrame: boolean; private isFirstLevelFrame: boolean; @@ -29,13 +30,14 @@ export class MonetizationLinkManager extends EventEmitter { { walletAddress: WalletAddress; requestId: string } >(); - constructor({ window, document, logger, message }: Cradle) { + constructor({ window, document, logger, message, idleDetection }: Cradle) { super(); Object.assign(this, { window, document, logger, message, + idleDetection, }); this.documentObserver = new MutationObserver((records) => @@ -106,6 +108,8 @@ export class MonetizationLinkManager extends EventEmitter { 'visibilitychange', this.onDocumentVisibilityChange, ); + // I feel like this should be moved in the main service? + this.idleDetection.detectUserInactivity(); this.onFocus(); this.window.addEventListener('focus', this.onFocus);