Skip to main content

layer_client/
typing_guard.rs

1//! RAII typing indicator guard.
2//!
3//! [`TypingGuard`] automatically cancels the "typing…" chat action when
4//! dropped, eliminating the need to remember to call `send_chat_action`
5//! with `SendMessageAction::SendMessageCancelAction` manually.
6//!
7//! # Example
8//! ```rust,no_run
9//! use layer_client::{Client, TypingGuard};
10//! use layer_tl_types as tl;
11//!
12//! async fn handle(client: Client, peer: tl::enums::Peer) {
13//!     // Typing indicator is sent immediately and auto-cancelled on drop.
14//!     let _typing = TypingGuard::start(&client, peer.clone(),
15//!         tl::enums::SendMessageAction::SendMessageTypingAction).await.unwrap();
16//!
17//!     do_expensive_work().await;
18//!     // `_typing` is dropped here — Telegram sees the typing stop.
19//! }
20//! # async fn do_expensive_work() {}
21//! ```
22
23use std::sync::Arc;
24use std::time::Duration;
25use tokio::sync::Notify;
26use tokio::task::JoinHandle;
27use layer_tl_types as tl;
28use crate::{Client, InvocationError};
29
30// ─── TypingGuard ─────────────────────────────────────────────────────────────
31
32/// Scoped typing indicator.  Keeps the action alive by re-sending it every
33/// ~4 seconds (Telegram drops the indicator after ~5 s).
34///
35/// Drop this guard to cancel the action immediately.
36pub struct TypingGuard {
37    stop: Arc<Notify>,
38    task: Option<JoinHandle<()>>,
39}
40
41impl TypingGuard {
42    /// Send `action` to `peer` and keep repeating it until the guard is dropped.
43    pub async fn start(
44        client: &Client,
45        peer:   tl::enums::Peer,
46        action: tl::enums::SendMessageAction,
47    ) -> Result<Self, InvocationError> {
48        // Send once immediately so the indicator appears without delay.
49        client.send_chat_action(peer.clone(), action.clone()).await?;
50
51        let stop   = Arc::new(Notify::new());
52        let stop2  = stop.clone();
53        let client = client.clone();
54
55        let task = tokio::spawn(async move {
56            loop {
57                tokio::select! {
58                    _ = tokio::time::sleep(Duration::from_secs(4)) => {
59                        if let Err(e) = client.send_chat_action(peer.clone(), action.clone()).await {
60                            log::warn!("[typing_guard] Failed to refresh typing action: {e}");
61                            break;
62                        }
63                    }
64                    _ = stop2.notified() => break,
65                }
66            }
67            // Cancel the action
68            let cancel = tl::enums::SendMessageAction::SendMessageCancelAction;
69            let _ = client.send_chat_action(peer.clone(), cancel).await;
70        });
71
72        Ok(Self { stop, task: Some(task) })
73    }
74
75    /// Cancel the typing indicator immediately without waiting for the drop.
76    pub fn cancel(&mut self) {
77        self.stop.notify_one();
78    }
79}
80
81impl Drop for TypingGuard {
82    fn drop(&mut self) {
83        self.stop.notify_one();
84        // Detach the task — it will see the notify, send a cancel action, then exit.
85        // We don't abort it because we want the cancel action to reach Telegram.
86        if let Some(t) = self.task.take() {
87            t.abort(); // abort is fine since notify already fired — cancel fires in select
88        }
89    }
90}
91
92// ─── Client extension ─────────────────────────────────────────────────────────
93
94impl Client {
95    /// Start a scoped typing indicator that auto-cancels when dropped.
96    ///
97    /// This is a convenience wrapper around [`TypingGuard::start`].
98    pub async fn typing(
99        &self,
100        peer: tl::enums::Peer,
101    ) -> Result<TypingGuard, InvocationError> {
102        TypingGuard::start(self, peer, tl::enums::SendMessageAction::SendMessageTypingAction).await
103    }
104
105    /// Start a scoped "uploading document" action that auto-cancels when dropped.
106    pub async fn uploading_document(
107        &self,
108        peer: tl::enums::Peer,
109    ) -> Result<TypingGuard, InvocationError> {
110        TypingGuard::start(self, peer, tl::enums::SendMessageAction::SendMessageUploadDocumentAction(
111            tl::types::SendMessageUploadDocumentAction { progress: 0 }
112        )).await
113    }
114
115    /// Start a scoped "recording video" action that auto-cancels when dropped.
116    pub async fn recording_video(
117        &self,
118        peer: tl::enums::Peer,
119    ) -> Result<TypingGuard, InvocationError> {
120        TypingGuard::start(self, peer, tl::enums::SendMessageAction::SendMessageRecordVideoAction).await
121    }
122}