11/**
22 * useReadingSession — reading session state machine hook
3+ *
4+ * Cross-platform: event listeners are provided by a SessionEventSource adapter.
5+ * - Web: uses window/document event listeners (default)
6+ * - React Native: inject an AppState-based adapter
37 */
48import { type SessionEvent , createSessionDetector } from "../reader/session-detector" ;
59import { useReadingSessionStore } from "../stores/reading-session-store" ;
@@ -9,6 +13,50 @@ import { useCallback, useEffect, useRef } from "react";
913// Save session every 5 minutes
1014const AUTO_SAVE_INTERVAL = 5 * 60 * 1000 ;
1115
16+ /**
17+ * Platform adapter for user activity / visibility / unload events.
18+ * Each platform provides its own implementation.
19+ */
20+ export interface SessionEventSource {
21+ /** Subscribe to user activity events. Returns unsubscribe function. */
22+ subscribeActivity ( callback : ( ) => void ) : ( ) => void ;
23+ /** Subscribe to visibility changes. Returns unsubscribe function. */
24+ subscribeVisibility ( callback : ( visible : boolean ) => void ) : ( ) => void ;
25+ /** Subscribe to app close / beforeunload. Returns unsubscribe function. */
26+ subscribeBeforeUnload ( callback : ( ) => void ) : ( ) => void ;
27+ }
28+
29+ /** Default Web implementation using window/document events */
30+ export const webSessionEventSource : SessionEventSource = {
31+ subscribeActivity ( callback ) {
32+ const events = [ "mousemove" , "keydown" , "scroll" , "click" , "touchstart" ] as const ;
33+ for ( const evt of events ) {
34+ window . addEventListener ( evt , callback ) ;
35+ }
36+ return ( ) => {
37+ for ( const evt of events ) {
38+ window . removeEventListener ( evt , callback ) ;
39+ }
40+ } ;
41+ } ,
42+ subscribeVisibility ( callback ) {
43+ const handler = ( ) => callback ( ! document . hidden ) ;
44+ document . addEventListener ( "visibilitychange" , handler ) ;
45+ return ( ) => document . removeEventListener ( "visibilitychange" , handler ) ;
46+ } ,
47+ subscribeBeforeUnload ( callback ) {
48+ window . addEventListener ( "beforeunload" , callback ) ;
49+ return ( ) => window . removeEventListener ( "beforeunload" , callback ) ;
50+ } ,
51+ } ;
52+
53+ /** Global override — set by platforms that cannot use web events (e.g. React Native) */
54+ let _sessionEventSource : SessionEventSource = webSessionEventSource ;
55+
56+ export function setSessionEventSource ( source : SessionEventSource ) : void {
57+ _sessionEventSource = source ;
58+ }
59+
1260export function useReadingSession ( bookId : string | null , tabId ?: string ) {
1361 const { startSession, pauseSession, resumeSession, stopSession, updateActiveTime, saveCurrentSession } =
1462 useReadingSessionStore ( ) ;
@@ -56,24 +104,19 @@ export function useReadingSession(bookId: string | null, tabId?: string) {
56104 useEffect ( ( ) => {
57105 if ( ! bookId ) return ;
58106
107+ const source = _sessionEventSource ;
108+
59109 const onActivity = ( ) => {
60110 if ( useAppStore . getState ( ) . activeTabId === tabId || ! tabId ) {
61111 sendEvent ( { type : "activity" } ) ;
62112 }
63113 } ;
64- const onVisibility = ( ) => sendEvent ( { type : "visibility" , visible : ! document . hidden } ) ;
65-
66- const onBeforeUnload = ( ) => {
67- stopSession ( ) ;
68- } ;
69114
70- window . addEventListener ( "mousemove" , onActivity ) ;
71- window . addEventListener ( "keydown" , onActivity ) ;
72- window . addEventListener ( "scroll" , onActivity ) ;
73- window . addEventListener ( "click" , onActivity ) ;
74- window . addEventListener ( "touchstart" , onActivity ) ;
75- document . addEventListener ( "visibilitychange" , onVisibility ) ;
76- window . addEventListener ( "beforeunload" , onBeforeUnload ) ;
115+ const unsubActivity = source . subscribeActivity ( onActivity ) ;
116+ const unsubVisibility = source . subscribeVisibility ( ( visible ) =>
117+ sendEvent ( { type : "visibility" , visible } ) ,
118+ ) ;
119+ const unsubUnload = source . subscribeBeforeUnload ( ( ) => stopSession ( ) ) ;
77120
78121 sendEvent ( { type : "activity" } ) ;
79122
@@ -98,13 +141,9 @@ export function useReadingSession(bookId: string | null, tabId?: string) {
98141 } , 1000 ) ;
99142
100143 return ( ) => {
101- window . removeEventListener ( "mousemove" , onActivity ) ;
102- window . removeEventListener ( "keydown" , onActivity ) ;
103- window . removeEventListener ( "scroll" , onActivity ) ;
104- window . removeEventListener ( "click" , onActivity ) ;
105- window . removeEventListener ( "touchstart" , onActivity ) ;
106- document . removeEventListener ( "visibilitychange" , onVisibility ) ;
107- window . removeEventListener ( "beforeunload" , onBeforeUnload ) ;
144+ unsubActivity ( ) ;
145+ unsubVisibility ( ) ;
146+ unsubUnload ( ) ;
108147 clearInterval ( timer ) ;
109148 sendEvent ( { type : "close" } ) ;
110149 } ;
0 commit comments