> ## Documentation Index
> Fetch the complete documentation index at: https://whatsapp-rust.jlucaso.com/llms.txt
> Use this file to discover all available pages before exploring further.

# June 10, 2026 — Encrypted CAG reactions and channel comments

> Reactions to Community Announcement Group posts are now encrypted automatically. A new comments() API lets you send and receive encrypted threaded replies on CAG channel posts.

## New features

**Encrypted reactions in Community Announcement Groups**

`Client::send_reaction` now transparently detects Community Announcement Groups (CAG — the default announcement subgroup of a community) and encrypts the reaction before sending. No API change is needed; the same call works for DMs, regular groups, and CAGs:

```rust theme={null}
// Works identically for DMs, regular groups, and CAGs.
client.send_reaction(&cag_jid, target_key, "🔥").await?;
```

For a CAG chat the library checks `GroupInfo::is_community_announce` (cached from group metadata). When true, the reaction emoji and timestamp are encrypted with the target post's `messageSecret` under the `"Enc Reaction"` HKDF use-case and shipped as an `enc_reaction_message` envelope — matching WA Web's `WAWebReactionEncryptMsgData` flow. If the parent secret was not captured (message received before session started, or `msg_secret_policy` disabled without a resolver) the call fails with a descriptive error rather than silently emitting a plaintext reaction the channel would reject.

Incoming encrypted reactions are decrypted transparently by the receive path and dispatched in the same plaintext `reaction_message` shape as an ordinary group reaction. The `key` field is filled from the envelope's `target_message_key` so event handlers require no changes.

***

**Channel comments via `client.comments()`**

A new `Comments` feature handle lets you post encrypted threaded replies under a CAG post. Comments require the parent post's `messageSecret` (captured during receive) and are authored under the LID identity, matching WA Web's `getMeLidUserOrThrow`:

```rust theme={null}
let parent_key = wa::MessageKey {
    remote_jid: Some(cag_jid.to_string()),
    from_me: Some(false),
    id: Some("3EB0POSTID".to_string()),
    // Must be the post author so receivers can derive the HKDF key.
    participant: Some(post_author_jid.to_string()),
};

// Text comment (extended_text_message body)
let result = client.comments()
    .send_text(&cag_jid, parent_key.clone(), "Great post!")
    .await?;

// Arbitrary message body
let body = wa::Message {
    extended_text_message: Some(Box::new(wa::message::ExtendedTextMessage {
        text: Some("Great post!".to_string()),
        ..Default::default()
    })),
    ..Default::default()
};
let result = client.comments()
    .send_message(&cag_jid, parent_key, body)
    .await?;
```

The comment carries a fresh `messageSecret` of its own so it can receive encrypted reactions. Both the comment body secret and the parent-post secret are persisted under the correct ids.

Incoming encrypted comments are decrypted transparently. The decrypted body is dispatched as a normal `Event::Message`. The parent post key surfaces on the new `MessageInfo::comment_target` field, since the inner `Message` proto has no slot for the threading link:

```rust theme={null}
Event::Message(msg, info) => {
    if let Some(parent_key) = &info.comment_target {
        println!("Comment on post: {:?}", parent_key.id);
        if let Some(text) = msg.text_content() {
            println!("Comment text: {}", text);
        }
    }
}
```

The comment's own `messageSecret` (from the outer envelope) is persisted under the comment's id and sender so that future encrypted reactions to the comment can be decrypted.

## Breaking changes

Pre-1.0, additive surface. The following structs and enums gain new fields/variants. If you construct them with exhaustive struct-literal syntax (without `..Default::default()`), add the new fields:

**`MessageInfo`** gains `comment_target: Option<wa::MessageKey>`:

```rust theme={null}
// Add the field or use ..Default::default():
let info = MessageInfo {
    id: "...".to_string(),
    // ...
    peer_recipient_pn: None,
    comment_target: None,   // new
    bcl_participants: vec![],
};
```

**`GroupInfo`** gains `is_community_announce: Option<bool>`. Blobs persisted before this field was added will deserialize with `None` ("unknown") and trigger one metadata re-query when the value is first needed. No migration is required — existing serialized blobs remain valid.

**`SecretEncKind`** gains two variants that must be covered in exhaustive `match` arms:

* `SecretEncKind::EncReaction` — an `enc_reaction_message` envelope
* `SecretEncKind::EncComment` — an `enc_comment_message` envelope
