Skip to main content

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}