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.lock().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}