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}