leptos_use/use_idle.rs
1use crate::core::now;
2use crate::filter_builder_methods;
3use crate::utils::{DebounceOptions, FilterOptions, ThrottleOptions};
4use default_struct_builder::DefaultBuilder;
5use leptos::prelude::*;
6use leptos::reactive::wrappers::read::Signal;
7
8/// Tracks whether the user is being inactive.
9///
10/// ## Demo
11///
12/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_idle)
13///
14/// ## Usage
15///
16/// ```
17/// # use leptos::prelude::*;
18/// # use leptos::logging::log;
19/// # use leptos_use::{use_idle, UseIdleReturn};
20/// #
21/// # #[component]
22/// # fn Demo() -> impl IntoView {
23/// let UseIdleReturn {
24/// idle, last_active, ..
25/// } = use_idle(5 * 60 * 1000); // 5 minutes
26///
27/// log!("{}", idle.get()); // true or false
28/// #
29/// # view! { }
30/// # }
31/// ```
32///
33/// Programatically resetting:
34///
35/// ```
36/// # use std::time::Duration;
37/// use leptos::prelude::*;
38/// # use leptos::logging::log;
39/// # use leptos_use::{use_idle, UseIdleReturn};
40/// #
41/// # #[component]
42/// # fn Demo() -> impl IntoView {
43/// let UseIdleReturn {
44/// idle, last_active, reset
45/// } = use_idle(5 * 60 * 1000); // 5 minutes
46///
47/// reset(); // restarts the idle timer. Does not change the `last_active` value.
48/// #
49/// # view! { }
50/// # }
51/// ```
52///
53/// ## SendWrapped Return
54///
55/// The returned closure `reset` is a sendwrapped function. It can
56/// only be called from the same thread that called `use_idle`.
57///
58/// ## Server-Side Rendering
59///
60/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
61///
62/// On the server this will always return static signals
63///
64/// ```ignore
65/// UseIdleReturn{
66/// idle: Signal(initial_state),
67/// last_active: Signal(now),
68/// reset: || {}
69/// }
70/// ```
71pub fn use_idle(timeout: u64) -> UseIdleReturn<impl Fn() + Clone + Send + Sync> {
72 use_idle_with_options(timeout, UseIdleOptions::default())
73}
74
75/// Version of [`use_idle`] that takes a `UseIdleOptions`. See [`use_idle`] for how to use.
76pub fn use_idle_with_options(
77 timeout: u64,
78 options: UseIdleOptions,
79) -> UseIdleReturn<impl Fn() + Clone + Send + Sync> {
80 let UseIdleOptions {
81 events,
82 listen_for_visibility_change,
83 initial_state,
84 filter,
85 } = options;
86
87 let (idle, set_idle) = signal(initial_state);
88 let (last_active, set_last_active) = signal(now());
89
90 let reset;
91
92 #[cfg(feature = "ssr")]
93 {
94 reset = || ();
95 let _ = timeout;
96 let _ = events;
97 let _ = listen_for_visibility_change;
98 let _ = filter;
99 let _ = set_last_active;
100 let _ = set_idle;
101 }
102
103 #[cfg(not(feature = "ssr"))]
104 {
105 use crate::utils::create_filter_wrapper;
106 use crate::{
107 UseEventListenerOptions, sendwrap_fn, use_document, use_event_listener_with_options,
108 };
109 use leptos::ev::{Custom, visibilitychange};
110 use leptos::leptos_dom::helpers::TimeoutHandle;
111 use std::cell::Cell;
112 use std::rc::Rc;
113 use std::time::Duration;
114
115 let timer = Rc::new(Cell::new(None::<TimeoutHandle>));
116
117 reset = {
118 let timer = Rc::clone(&timer);
119
120 sendwrap_fn!(move || {
121 set_idle.set(false);
122 if let Some(timer) = timer.replace(
123 set_timeout_with_handle(
124 move || set_idle.set(true),
125 Duration::from_millis(timeout.min(i32::MAX as u64)),
126 )
127 .ok(),
128 ) {
129 timer.clear();
130 }
131 })
132 };
133
134 let on_event = {
135 let reset = reset.clone();
136
137 let filtered_callback = create_filter_wrapper(filter.filter_fn(), move || {
138 set_last_active.set(js_sys::Date::now());
139 reset();
140 });
141
142 move |_: web_sys::Event| {
143 filtered_callback();
144 }
145 };
146
147 let listener_options = UseEventListenerOptions::default().passive(true);
148 for event in events {
149 let _ = use_event_listener_with_options(
150 use_document(),
151 Custom::new(event),
152 on_event.clone(),
153 listener_options,
154 );
155 }
156
157 if listen_for_visibility_change {
158 let on_event = on_event.clone();
159
160 let _ = use_event_listener_with_options(
161 use_document(),
162 visibilitychange,
163 move |evt| {
164 if !document().hidden() {
165 on_event(evt);
166 }
167 },
168 listener_options,
169 );
170 }
171
172 reset.clone()();
173 }
174
175 UseIdleReturn {
176 idle: idle.into(),
177 last_active: last_active.into(),
178 reset,
179 }
180}
181
182/// Options for [`use_idle_with_options`].
183#[derive(DefaultBuilder)]
184pub struct UseIdleOptions {
185 /// Event names to listen to for detected user activity.
186 /// Default: `vec!["mousemove", "mousedown", "resize", "keydown", "touchstart", "wheel"]`.
187 events: Vec<String>,
188
189 /// Whether to listen for document visibility change.
190 /// Defaults to `true`.
191 listen_for_visibility_change: bool,
192
193 /// Initial state of the returned `idle`.
194 /// Defaults to `false`.
195 initial_state: bool,
196
197 /// Allows to debounce or throttle the event listener that is called for
198 /// every event (from `events`). Defaults to a throttle by 50ms.
199 filter: FilterOptions,
200}
201
202impl Default for UseIdleOptions {
203 fn default() -> Self {
204 Self {
205 events: vec![
206 "mousemove".to_string(),
207 "mousedown".to_string(),
208 "resize".to_string(),
209 "keydown".to_string(),
210 "touchstart".to_string(),
211 "wheel".to_string(),
212 ],
213 listen_for_visibility_change: true,
214 initial_state: false,
215 filter: FilterOptions::throttle(50.0),
216 }
217 }
218}
219
220impl UseIdleOptions {
221 filter_builder_methods!(
222 /// the event listener
223 filter
224 );
225}
226
227/// Return type of [`use_idle`].
228pub struct UseIdleReturn<F>
229where
230 F: Fn() + Clone + Send + Sync,
231{
232 /// Wether the use has been inactive for at least `timeout` milliseconds.
233 pub idle: Signal<bool>,
234
235 /// Timestamp of last user activity.
236 pub last_active: Signal<f64>,
237
238 /// Reset function. Sets the idle state to `false`.
239 pub reset: F,
240}