logicaffeine_web/ui/hooks/
use_inactivity_timer.rs1use dioxus::prelude::*;
8
9pub const DEFAULT_INACTIVITY_THRESHOLD_MS: u64 = 5000;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
14pub struct InactivityState {
15 pub is_inactive: bool,
17 pub inactive_duration_ms: u64,
19}
20
21impl Default for InactivityState {
22 fn default() -> Self {
23 Self {
24 is_inactive: false,
25 inactive_duration_ms: 0,
26 }
27 }
28}
29
30#[cfg(target_arch = "wasm32")]
51pub fn use_inactivity_timer(
52 threshold_ms: u64,
53 on_inactive: impl Fn() + 'static,
54) -> (Signal<InactivityState>, impl FnMut()) {
55 use gloo_timers::callback::Interval;
56
57 let mut state = use_signal(InactivityState::default);
58 let mut last_activity_time = use_signal(|| js_sys::Date::now());
59 let mut callback_triggered = use_signal(|| false);
60
61 let on_inactive = std::rc::Rc::new(on_inactive);
63 let on_inactive_clone = on_inactive.clone();
64
65 use_effect(move || {
67 let on_inactive = on_inactive_clone.clone();
68
69 let interval = Interval::new(1000, move || {
70 let now = js_sys::Date::now();
71 let last = *last_activity_time.read();
72 let elapsed_ms = (now - last) as u64;
73
74 let is_inactive = elapsed_ms >= threshold_ms;
75
76 state.set(InactivityState {
78 is_inactive,
79 inactive_duration_ms: elapsed_ms,
80 });
81
82 if is_inactive && !*callback_triggered.read() {
84 callback_triggered.set(true);
85 on_inactive();
86 }
87 });
88
89 std::mem::forget(interval);
91 });
92
93 let reset_activity = move || {
95 #[cfg(target_arch = "wasm32")]
96 {
97 last_activity_time.set(js_sys::Date::now());
98 }
99 callback_triggered.set(false);
100 state.set(InactivityState::default());
101 };
102
103 (state, reset_activity)
104}
105
106#[cfg(not(target_arch = "wasm32"))]
108pub fn use_inactivity_timer(
109 _threshold_ms: u64,
110 _on_inactive: impl Fn() + 'static,
111) -> (Signal<InactivityState>, impl FnMut()) {
112 let state = use_signal(InactivityState::default);
113 let reset_activity = || {};
114 (state, reset_activity)
115}
116
117#[cfg(target_arch = "wasm32")]
119pub fn use_is_inactive(threshold_ms: u64) -> Signal<bool> {
120 use gloo_timers::callback::Interval;
121
122 let mut is_inactive = use_signal(|| false);
123 let mut last_activity_time = use_signal(|| js_sys::Date::now());
124
125 use_effect(move || {
126 let interval = Interval::new(1000, move || {
127 let now = js_sys::Date::now();
128 let last = *last_activity_time.read();
129 let elapsed_ms = (now - last) as u64;
130
131 is_inactive.set(elapsed_ms >= threshold_ms);
132 });
133
134 std::mem::forget(interval);
136 });
137
138 is_inactive
139}
140
141#[cfg(not(target_arch = "wasm32"))]
142pub fn use_is_inactive(_threshold_ms: u64) -> Signal<bool> {
143 use_signal(|| false)
144}
145
146#[cfg(target_arch = "wasm32")]
149pub fn use_activity_resetter() -> (Signal<f64>, impl FnMut()) {
150 let last_activity = use_signal(|| js_sys::Date::now());
151
152 let reset = {
153 let mut last_activity = last_activity;
154 move || {
155 last_activity.set(js_sys::Date::now());
156 }
157 };
158
159 (last_activity, reset)
160}
161
162#[cfg(not(target_arch = "wasm32"))]
163pub fn use_activity_resetter() -> (Signal<f64>, impl FnMut()) {
164 let last_activity = use_signal(|| 0.0);
165 let reset = || {};
166 (last_activity, reset)
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn test_inactivity_state_default() {
175 let state = InactivityState::default();
176 assert!(!state.is_inactive);
177 assert_eq!(state.inactive_duration_ms, 0);
178 }
179
180 #[test]
181 fn test_default_threshold() {
182 assert_eq!(DEFAULT_INACTIVITY_THRESHOLD_MS, 5000);
183 }
184}