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,
108 use_event_listener_with_options,
109 };
110 use leptos::ev::{Custom, visibilitychange};
111 use leptos::leptos_dom::helpers::TimeoutHandle;
112 use std::cell::Cell;
113 use std::rc::Rc;
114 use std::time::Duration;
115
116 let timer = Rc::new(Cell::new(None::<TimeoutHandle>));
117
118 reset = {
119 let timer = Rc::clone(&timer);
120
121 sendwrap_fn!(move || {
122 set_idle.set(false);
123 if let Some(timer) = timer.replace(
124 set_timeout_with_handle(
125 move || set_idle.set(true),
126 Duration::from_millis(timeout),
127 )
128 .ok(),
129 ) {
130 timer.clear();
131 }
132 })
133 };
134
135 let on_event = {
136 let reset = reset.clone();
137
138 let filtered_callback = create_filter_wrapper(filter.filter_fn(), move || {
139 set_last_active.set(js_sys::Date::now());
140 reset();
141 });
142
143 move |_: web_sys::Event| {
144 filtered_callback();
145 }
146 };
147
148 let listener_options = UseEventListenerOptions::default().passive(true);
149 for event in events {
150 let _ = use_event_listener_with_options(
151 use_document(),
152 Custom::new(event),
153 on_event.clone(),
154 listener_options,
155 );
156 }
157
158 if listen_for_visibility_change {
159 let on_event = on_event.clone();
160
161 let _ = use_event_listener(use_document(), visibilitychange, move |evt| {
162 if !document().hidden() {
163 on_event(evt);
164 }
165 });
166 }
167
168 reset.clone()();
169 }
170
171 UseIdleReturn {
172 idle: idle.into(),
173 last_active: last_active.into(),
174 reset,
175 }
176}
177
178/// Options for [`use_idle_with_options`].
179#[derive(DefaultBuilder)]
180pub struct UseIdleOptions {
181 /// Event names to listen to for detected user activity.
182 /// Default: `vec!["mousemove", "mousedown", "resize", "keydown", "touchstart", "wheel"]`.
183 events: Vec<String>,
184
185 /// Whether to listen for document visibility change.
186 /// Defaults to `true`.
187 listen_for_visibility_change: bool,
188
189 /// Initial state of the returned `idle`.
190 /// Defaults to `false`.
191 initial_state: bool,
192
193 /// Allows to debounce or throttle the event listener that is called for
194 /// every event (from `events`). Defaults to a throttle by 50ms.
195 filter: FilterOptions,
196}
197
198impl Default for UseIdleOptions {
199 fn default() -> Self {
200 Self {
201 events: vec![
202 "mousemove".to_string(),
203 "mousedown".to_string(),
204 "resize".to_string(),
205 "keydown".to_string(),
206 "touchstart".to_string(),
207 "wheel".to_string(),
208 ],
209 listen_for_visibility_change: true,
210 initial_state: false,
211 filter: FilterOptions::throttle(50.0),
212 }
213 }
214}
215
216impl UseIdleOptions {
217 filter_builder_methods!(
218 /// the event listener
219 filter
220 );
221}
222
223/// Return type of [`use_idle`].
224pub struct UseIdleReturn<F>
225where
226 F: Fn() + Clone + Send + Sync,
227{
228 /// Wether the use has been inactive for at least `timeout` milliseconds.
229 pub idle: Signal<bool>,
230
231 /// Timestamp of last user activity.
232 pub last_active: Signal<f64>,
233
234 /// Reset function. Sets the idle state to `false`.
235 pub reset: F,
236}