layer_client/typing_guard.rs
1// Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4// NOTE:
5// The "Layer" project is no longer maintained or supported.
6// Its original purpose for personal SDK/APK experimentation and learning
7// has been fulfilled.
8//
9// Please use Ferogram instead:
10// https://github.com/ankit-chaubey/ferogram
11// Ferogram will receive future updates and development, although progress
12// may be slower.
13//
14// Ferogram is an async Telegram MTProto client library written in Rust.
15// Its implementation follows the behaviour of the official Telegram clients,
16// particularly Telegram Desktop and TDLib, and aims to provide a clean and
17// modern async interface for building Telegram clients and tools.
18
19//! RAII typing indicator guard.
20//!
21//! [`TypingGuard`] automatically cancels the "typing…" chat action when
22//! dropped, eliminating the need to remember to call `send_chat_action`
23//! with `SendMessageAction::SendMessageCancelAction` manually.
24//!
25//! # Example
26//! ```rust,no_run
27//! use layer_client::{Client, TypingGuard};
28//! use layer_tl_types as tl;
29//!
30//! async fn handle(client: Client, peer: tl::enums::Peer) {
31//! // Typing indicator is sent immediately and auto-cancelled on drop.
32//! let _typing = TypingGuard::start(&client, peer.clone(),
33//! tl::enums::SendMessageAction::SendMessageTypingAction).await.unwrap();
34//!
35//! do_expensive_work().await;
36//! // `_typing` is dropped here: Telegram sees the typing stop.
37//! }
38//! # async fn do_expensive_work() {}
39//! ```
40
41use crate::{Client, InvocationError, PeerRef};
42use layer_tl_types as tl;
43use std::sync::Arc;
44use std::time::Duration;
45use tokio::sync::Notify;
46use tokio::task::JoinHandle;
47
48// TypingGuard
49
50/// Scoped typing indicator. Keeps the action alive by re-sending it every
51/// ~4 seconds (Telegram drops the indicator after ~5 s).
52///
53/// Drop this guard to cancel the action immediately.
54pub struct TypingGuard {
55 stop: Arc<Notify>,
56 task: Option<JoinHandle<()>>,
57}
58
59impl TypingGuard {
60 /// Send `action` to `peer` and keep repeating it until the guard is dropped.
61 pub async fn start(
62 client: &Client,
63 peer: impl Into<PeerRef>,
64 action: tl::enums::SendMessageAction,
65 ) -> Result<Self, InvocationError> {
66 let peer = peer.into().resolve(client).await?;
67 Self::start_ex(client, peer, action, None, Duration::from_secs(4)).await
68 }
69
70 /// Like [`start`](Self::start) but also accepts a forum **topic id**
71 /// (`top_msg_id`) and a custom **repeat delay**.
72 ///
73 /// # Arguments
74 /// * `topic_id` : `Some(msg_id)` for a forum topic thread; `None` for
75 /// the main chat.
76 /// * `repeat_delay`: How often to re-send the action to keep it alive.
77 /// Telegram drops the indicator after ~5 s; ≤ 4 s is
78 /// recommended.
79 pub async fn start_ex(
80 client: &Client,
81 peer: tl::enums::Peer,
82 action: tl::enums::SendMessageAction,
83 topic_id: Option<i32>,
84 repeat_delay: Duration,
85 ) -> Result<Self, InvocationError> {
86 // Send once immediately so the indicator appears without delay.
87 client
88 .send_chat_action_ex(peer.clone(), action.clone(), topic_id)
89 .await?;
90
91 let stop = Arc::new(Notify::new());
92 let stop2 = stop.clone();
93 let client = client.clone();
94
95 let task = tokio::spawn(async move {
96 loop {
97 tokio::select! {
98 _ = tokio::time::sleep(repeat_delay) => {
99 if let Err(e) = client.send_chat_action_ex(peer.clone(), action.clone(), topic_id).await {
100 tracing::warn!("[typing_guard] Failed to refresh typing action: {e}");
101 break;
102 }
103 }
104 _ = stop2.notified() => break,
105 }
106 }
107 // Cancel the action
108 let cancel = tl::enums::SendMessageAction::SendMessageCancelAction;
109 let _ = client
110 .send_chat_action_ex(peer.clone(), cancel, topic_id)
111 .await;
112 });
113
114 Ok(Self {
115 stop,
116 task: Some(task),
117 })
118 }
119
120 /// Cancel the typing indicator immediately without waiting for the drop.
121 pub fn cancel(&mut self) {
122 self.stop.notify_one();
123 }
124}
125
126impl Drop for TypingGuard {
127 fn drop(&mut self) {
128 self.stop.notify_one();
129 if let Some(t) = self.task.take() {
130 t.abort();
131 }
132 }
133}
134
135// Client extension
136
137impl Client {
138 /// Start a scoped typing indicator that auto-cancels when dropped.
139 ///
140 /// This is a convenience wrapper around [`TypingGuard::start`].
141 pub async fn typing(&self, peer: impl Into<PeerRef>) -> Result<TypingGuard, InvocationError> {
142 TypingGuard::start(
143 self,
144 peer,
145 tl::enums::SendMessageAction::SendMessageTypingAction,
146 )
147 .await
148 }
149
150 /// Start a scoped typing indicator in a **forum topic** thread.
151 ///
152 /// `topic_id` is the `top_msg_id` of the forum topic.
153 pub async fn typing_in_topic(
154 &self,
155 peer: impl Into<PeerRef>,
156 topic_id: i32,
157 ) -> Result<TypingGuard, InvocationError> {
158 let peer = peer.into().resolve(self).await?;
159 TypingGuard::start_ex(
160 self,
161 peer,
162 tl::enums::SendMessageAction::SendMessageTypingAction,
163 Some(topic_id),
164 std::time::Duration::from_secs(4),
165 )
166 .await
167 }
168
169 /// Start a scoped "uploading document" action that auto-cancels when dropped.
170 pub async fn uploading_document(
171 &self,
172 peer: impl Into<PeerRef>,
173 ) -> Result<TypingGuard, InvocationError> {
174 TypingGuard::start(
175 self,
176 peer,
177 tl::enums::SendMessageAction::SendMessageUploadDocumentAction(
178 tl::types::SendMessageUploadDocumentAction { progress: 0 },
179 ),
180 )
181 .await
182 }
183
184 /// Start a scoped "recording video" action that auto-cancels when dropped.
185 pub async fn recording_video(
186 &self,
187 peer: impl Into<PeerRef>,
188 ) -> Result<TypingGuard, InvocationError> {
189 TypingGuard::start(
190 self,
191 peer,
192 tl::enums::SendMessageAction::SendMessageRecordVideoAction,
193 )
194 .await
195 }
196
197 /// Send a chat action with optional forum topic support (internal helper).
198 pub(crate) async fn send_chat_action_ex(
199 &self,
200 peer: tl::enums::Peer,
201 action: tl::enums::SendMessageAction,
202 topic_id: Option<i32>,
203 ) -> Result<(), InvocationError> {
204 let input_peer = self.inner.peer_cache.read().await.peer_to_input(&peer);
205 let req = tl::functions::messages::SetTyping {
206 peer: input_peer,
207 top_msg_id: topic_id,
208 action,
209 };
210 self.rpc_write(&req).await
211 }
212}