Skip to main content
The Chatstate struct provides methods for sending typing indicators and recording notifications to recipients.

Access

Access chatstate operations through the client:
let chatstate = client.chatstate();

Methods

send

Send a chat state update to a recipient.
pub async fn send(
    &self,
    to: &Jid,
    state: ChatStateType,
) -> Result<(), ClientError>
Parameters:
  • to - Recipient JID (user or group)
  • state: ChatStateType - Type of chat state to send
Returns:
  • Result<(), ClientError> - Success or client error
Example:
use whatsapp_rust::features::chatstate::ChatStateType;

let recipient: Jid = "15551234567@s.whatsapp.net".parse()?;

// Send typing indicator
client.chatstate().send(&recipient, ChatStateType::Composing).await?;

// Send recording indicator
client.chatstate().send(&recipient, ChatStateType::Recording).await?;

// Send paused (stopped typing)
client.chatstate().send(&recipient, ChatStateType::Paused).await?;

send_composing

Convenience method to send typing indicator.
pub async fn send_composing(&self, to: &Jid) -> Result<(), ClientError>
Example:
let recipient: Jid = "15551234567@s.whatsapp.net".parse()?;
client.chatstate().send_composing(&recipient).await?;

send_recording

Convenience method to send audio recording indicator.
pub async fn send_recording(&self, to: &Jid) -> Result<(), ClientError>
Example:
let recipient: Jid = "15551234567@s.whatsapp.net".parse()?;
client.chatstate().send_recording(&recipient).await?;
println!("Showing 'recording audio' indicator");

send_paused

Convenience method to send paused/stopped typing indicator.
pub async fn send_paused(&self, to: &Jid) -> Result<(), ClientError>
Example:
let recipient: Jid = "15551234567@s.whatsapp.net".parse()?;
client.chatstate().send_paused(&recipient).await?;
println!("Cleared typing indicator");

Receiving chat state events

ChatStateEvent

Incoming chatstate stanzas (typing indicators from other users) are dispatched as ChatStateEvent values. Register a handler with register_chatstate_handler to receive them.
#[derive(Debug, Clone)]
pub struct ChatStateEvent {
    /// The chat where the event occurred (user JID for 1:1, group JID for groups)
    pub chat: Jid,
    /// For group chats, the participant who triggered the event
    pub participant: Option<Jid>,
    /// The chat state (typing, recording_audio, or idle)
    pub state: ReceivedChatState,
}
chat
Jid
The chat where the event occurred. For direct messages this is the sender’s JID; for groups this is the group JID.
participant
Option<Jid>
For group chats, the participant who triggered the event. None for 1:1 chats.
state
ReceivedChatState
The parsed chat state — see ReceivedChatState below.
Example:
use whatsapp_rust::ChatStateEvent;
use std::sync::Arc;

client.register_chatstate_handler(Arc::new(|event: ChatStateEvent| {
    match event.state {
        ReceivedChatState::Typing => {
            println!("{} is typing in {}", event.participant.as_ref().unwrap_or(&event.chat), event.chat);
        }
        ReceivedChatState::RecordingAudio => {
            println!("{} is recording audio", event.chat);
        }
        ReceivedChatState::Idle => {
            println!("{} stopped typing", event.chat);
        }
        _ => {}
    }
})).await;

ReceivedChatState

The state values for incoming chatstate events, aligned with WhatsApp Web’s WAChatState constants.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReceivedChatState {
    Typing,        // User is typing text
    RecordingAudio, // User is recording a voice message
    Idle,          // User stopped typing/recording (default)
}
ReceivedChatState is #[non_exhaustive], so match statements should include a wildcard arm. Wire format mapping:
XML stanzaReceivedChatState
<composing/>Typing
<composing media="audio"/>RecordingAudio
<paused/>Idle

Sending chat state updates

ChatStateType Enum

#[non_exhaustive]
pub enum ChatStateType {
    Composing,  // Typing text
    Recording,  // Recording audio
    Paused,     // Stopped typing/recording
}
ChatStateType is #[non_exhaustive], so match statements should include a wildcard arm to handle future variants. Methods:
  • as_str() - Returns "composing", "recording", or "paused"
String conversion:
use whatsapp_rust::features::chatstate::ChatStateType;

let state = ChatStateType::Composing;
assert_eq!(state.as_str(), "composing");
assert_eq!(state.to_string(), "composing");

// Parse from string
let parsed = ChatStateType::try_from("recording")?;
assert_eq!(parsed, ChatStateType::Recording);

Wire Format

Composing (Typing)

<chatstate to="15551234567@s.whatsapp.net">
  <composing/>
</chatstate>

Recording (Voice)

<chatstate to="15551234567@s.whatsapp.net">
  <composing media="audio"/>
</chatstate>
Note: Recording uses <composing media="audio"> rather than a separate tag.

Paused (Stopped)

<chatstate to="15551234567@s.whatsapp.net">
  <paused/>
</chatstate>

Usage Patterns

Typing Indicator Lifecycle

use whatsapp_rust::features::chatstate::ChatStateType;

let recipient: Jid = "15551234567@s.whatsapp.net".parse()?;

// User starts typing
client.chatstate().send_composing(&recipient).await?;

// User is typing... (you may want to throttle these)
std::thread::sleep(std::time::Duration::from_secs(2));

// User stopped typing (optional, will auto-clear)
client.chatstate().send_paused(&recipient).await?;

// Send the actual message
let message = wa::Message {
    conversation: Some("Hello!".to_string()),
    ..Default::default()
};
client.send_message(recipient.clone(), message).await?;

Recording Audio

let recipient: Jid = "15551234567@s.whatsapp.net".parse()?;

// Start recording
client.chatstate().send_recording(&recipient).await?;

// Record audio...
let audio_data = record_audio();

// Stop indicator (optional)
client.chatstate().send_paused(&recipient).await?;

// Upload and send audio message
let upload = client.upload(audio_data, MediaType::Audio, Default::default()).await?;
let message = wa::Message {
    audio_message: Some(Box::new(wa::message::AudioMessage {
        url: Some(upload.url),
        direct_path: Some(upload.direct_path),
        media_key: Some(upload.media_key_vec()),
        file_enc_sha256: Some(upload.file_enc_sha256_vec()),
        file_sha256: Some(upload.file_sha256_vec()),
        file_length: Some(upload.file_length),
        mimetype: Some("audio/ogg; codecs=opus".to_string()),
        ptt: Some(true),
        ..Default::default()
    })),
    ..Default::default()
};
client.send_message(recipient.clone(), message).await?;

Throttling

To avoid spamming chat state updates:
use std::time::{Duration, Instant};

struct ChatStateThrottler {
    last_sent: Option<Instant>,
    interval: Duration,
}

impl ChatStateThrottler {
    fn new() -> Self {
        Self {
            last_sent: None,
            interval: Duration::from_secs(3),
        }
    }
    
    fn should_send(&mut self) -> bool {
        if let Some(last) = self.last_sent {
            if last.elapsed() < self.interval {
                return false;
            }
        }
        self.last_sent = Some(Instant::now());
        true
    }
}

// Usage
let mut throttler = ChatStateThrottler::new();
let recipient: Jid = "15551234567@s.whatsapp.net".parse()?;

// In your typing event handler
if throttler.should_send() {
    client.chatstate().send_composing(&recipient).await?;
}

Group Chats

Chat state indicators work in group chats as well:
let group_jid: Jid = "123456789@g.us".parse()?;

// Show typing in group
client.chatstate().send_composing(&group_jid).await?;

// Send message
let message = wa::Message {
    conversation: Some("Hello everyone!".to_string()),
    ..Default::default()
};
client.send_message(group_jid.clone(), message).await?;

Error Handling

All methods return Result<(), ClientError>. Common errors:
  • Not connected: Client not connected to WhatsApp
  • Invalid JID: Malformed recipient JID
  • Network errors: Connection issues
use whatsapp_rust::client::ClientError;

let recipient: Jid = "15551234567@s.whatsapp.net".parse()?;

match client.chatstate().send_composing(&recipient).await {
    Ok(_) => println!("Sent typing indicator"),
    Err(ClientError::NotConnected) => {
        eprintln!("Not connected to WhatsApp");
    }
    Err(e) => eprintln!("Error: {}", e),
}

Best Practices

  1. Throttle updates: Don’t send chat state updates more than once every 2-3 seconds
  2. Clear on send: Send Paused before sending the actual message
  3. Auto-timeout: Consider auto-clearing typing indicators after 10-15 seconds of inactivity
  4. Don’t spam: Only send when state actually changes
  5. Groups: Be mindful that all group members will see the indicator

Complete Example

use whatsapp_rust::features::chatstate::ChatStateType;
use std::time::Duration;

let recipient: Jid = "15551234567@s.whatsapp.net".parse()?;

// Simulate user typing
println!("User starts typing...");
client.chatstate().send_composing(&recipient).await?;

// Simulate typing delay
tokio::time::sleep(Duration::from_secs(2)).await;

// User still typing (throttled)
println!("Still typing...");

// User finishes and sends message
client.chatstate().send_paused(&recipient).await?;
let message = wa::Message {
    conversation: Some("Hello there!".to_string()),
    ..Default::default()
};
client.send_message(recipient.clone(), message).await?;
println!("Message sent");

// Or simulate voice recording
println!("\nUser starts recording...");
client.chatstate().send_recording(&recipient).await?;

tokio::time::sleep(Duration::from_secs(3)).await;

client.chatstate().send_paused(&recipient).await?;
println!("Recording stopped (would send audio here)");