Skip to main content

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}