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        Self::start_ex(client, peer, action, None, Duration::from_secs(4)).await
49    }
50
51    /// Like [`start`](Self::start) but also accepts a forum **topic id**
52    /// (`top_msg_id`) and a custom **repeat delay**.
53    ///
54    /// # Arguments
55    /// * `topic_id`     — `Some(msg_id)` for a forum topic thread; `None` for
56    ///   the main chat.
57    /// * `repeat_delay` — How often to re-send the action to keep it alive.
58    ///   Telegram drops the indicator after ~5 s; ≤ 4 s is
59    ///   recommended.
60    pub async fn start_ex(
61        client:       &Client,
62        peer:         tl::enums::Peer,
63        action:       tl::enums::SendMessageAction,
64        topic_id:     Option<i32>,
65        repeat_delay: Duration,
66    ) -> Result<Self, InvocationError> {
67        // Send once immediately so the indicator appears without delay.
68        client.send_chat_action_ex(peer.clone(), action.clone(), topic_id).await?;
69
70        let stop   = Arc::new(Notify::new());
71        let stop2  = stop.clone();
72        let client = client.clone();
73
74        let task = tokio::spawn(async move {
75            loop {
76                tokio::select! {
77                    _ = tokio::time::sleep(repeat_delay) => {
78                        if let Err(e) = client.send_chat_action_ex(peer.clone(), action.clone(), topic_id).await {
79                            log::warn!("[typing_guard] Failed to refresh typing action: {e}");
80                            break;
81                        }
82                    }
83                    _ = stop2.notified() => break,
84                }
85            }
86            // Cancel the action
87            let cancel = tl::enums::SendMessageAction::SendMessageCancelAction;
88            let _ = client.send_chat_action_ex(peer.clone(), cancel, topic_id).await;
89        });
90
91        Ok(Self { stop, task: Some(task) })
92    }
93
94    /// Cancel the typing indicator immediately without waiting for the drop.
95    pub fn cancel(&mut self) {
96        self.stop.notify_one();
97    }
98}
99
100impl Drop for TypingGuard {
101    fn drop(&mut self) {
102        self.stop.notify_one();
103        if let Some(t) = self.task.take() {
104            t.abort();
105        }
106    }
107}
108
109// ─── Client extension ─────────────────────────────────────────────────────────
110
111impl Client {
112    /// Start a scoped typing indicator that auto-cancels when dropped.
113    ///
114    /// This is a convenience wrapper around [`TypingGuard::start`].
115    pub async fn typing(
116        &self,
117        peer: tl::enums::Peer,
118    ) -> Result<TypingGuard, InvocationError> {
119        TypingGuard::start(self, peer, tl::enums::SendMessageAction::SendMessageTypingAction).await
120    }
121
122    /// Start a scoped typing indicator in a **forum topic** thread.
123    ///
124    /// `topic_id` is the `top_msg_id` of the forum topic.
125    pub async fn typing_in_topic(
126        &self,
127        peer:     tl::enums::Peer,
128        topic_id: i32,
129    ) -> Result<TypingGuard, InvocationError> {
130        TypingGuard::start_ex(
131            self, peer,
132            tl::enums::SendMessageAction::SendMessageTypingAction,
133            Some(topic_id),
134            std::time::Duration::from_secs(4),
135        ).await
136    }
137
138    /// Start a scoped "uploading document" action that auto-cancels when dropped.
139    pub async fn uploading_document(
140        &self,
141        peer: tl::enums::Peer,
142    ) -> Result<TypingGuard, InvocationError> {
143        TypingGuard::start(self, peer, tl::enums::SendMessageAction::SendMessageUploadDocumentAction(
144            tl::types::SendMessageUploadDocumentAction { progress: 0 }
145        )).await
146    }
147
148    /// Start a scoped "recording video" action that auto-cancels when dropped.
149    pub async fn recording_video(
150        &self,
151        peer: tl::enums::Peer,
152    ) -> Result<TypingGuard, InvocationError> {
153        TypingGuard::start(self, peer, tl::enums::SendMessageAction::SendMessageRecordVideoAction).await
154    }
155
156    /// Send a chat action with optional forum topic support (internal helper).
157    pub(crate) async fn send_chat_action_ex(
158        &self,
159        peer:     tl::enums::Peer,
160        action:   tl::enums::SendMessageAction,
161        topic_id: Option<i32>,
162    ) -> Result<(), InvocationError> {
163        let input_peer = self.inner.peer_cache.read().await.peer_to_input(&peer);
164        let req = tl::functions::messages::SetTyping {
165            peer: input_peer,
166            top_msg_id: topic_id,
167            action,
168        };
169        self.rpc_write(&req).await
170    }
171}