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