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