Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
63b1994
feat: allow channel subscriber reactions
r10s Jul 23, 2026
e0bcc96
add tables to handle broadcast reactions
r10s Jul 23, 2026
1bd8142
schedule broadcasting reactions
r10s Jul 23, 2026
fa28146
send broadcast reactions
r10s Jul 23, 2026
bd61c6e
move broadcast related functions to a dedicated file
r10s Jul 24, 2026
73dbdb0
broadcast reactions using ChatReactions: header
r10s Jul 24, 2026
9333913
fix CONFLICT clause
r10s Jul 24, 2026
4ce1377
create basic test for sending reactions to channels
r10s Jul 24, 2026
f98e89d
use 'broadcast reactions' everywhere
r10s Jul 24, 2026
13dd756
apply received broadcasted reactions
r10s Jul 24, 2026
8e4dfe7
save one db call
r10s Jul 24, 2026
fada3c3
move reaction&count logic from jsonrpc bindungs to core
r10s Jul 25, 2026
8ce92af
move function needed for testing only to test module
r10s Jul 25, 2026
2b366b6
use new frequencies field
r10s Jul 25, 2026
0cde407
streamline get_frequencies
r10s Jul 25, 2026
32314be
let get_msg_reactions() also return broadcasted reactions
r10s Jul 26, 2026
061860a
refine broadcasted reactions by local by-contact state
r10s Jul 27, 2026
85d6a97
clearer internal naming and documentation
r10s Jul 27, 2026
8f743ad
remove unneeded cloning
r10s Jul 27, 2026
12fbb81
replace low-level sql tests by higher-level functionality tests
r10s Jul 27, 2026
f6e8f57
remove superfluous error return
r10s Jul 27, 2026
cfd6f97
fix typo and docs
r10s Jul 27, 2026
019d528
emit 'chatlist changed' event
r10s Jul 27, 2026
954252b
simplify reaction refining and make it more robust
r10s Jul 27, 2026
e053861
factor out sorting by frequencies
r10s Jul 27, 2026
3a3cdf0
simplify reaction refining
r10s Jul 27, 2026
4789f11
add test for test_refine_broadcast_reactions()
r10s Jul 27, 2026
7640a4d
increase self counter so one gets an immediate feedback on reacting
r10s Jul 27, 2026
fff4333
factor out save_broadcast_reactions(), clearer names for the wire obj…
r10s Jul 28, 2026
3243be1
revert increasing self-counter for immediate feedback
r10s Jul 28, 2026
dce023a
let load_broadasted_reactions() return empty array; this makes modifi…
r10s Jul 28, 2026
3303f70
modify broadcast reactions on SELF-reaction
r10s Jul 28, 2026
ae96959
mute outgoing broadcast channels; receiving potentially lots of notif…
r10s Jul 28, 2026
21f00c0
make clippy happy
r10s Jul 28, 2026
69027ef
Update src/reaction/broadcast_reactions.rs
r10s Aug 3, 2026
86ef977
do not get stuck if the clock was set to the future for a moment, sav…
r10s Aug 3, 2026
2a00d88
Update src/param.rs
r10s Aug 3, 2026
ba6ba49
add doc comment for Wire format
r10s Aug 3, 2026
ba3e386
use Chat-Broadcast-Reactions: header to stay in the 'Chat-' name space
r10s Aug 3, 2026
9a64ee5
add test_broadcast_muted()
r10s Aug 3, 2026
5343760
log error if loading chat for correct message fails
r10s Aug 3, 2026
7c92e7d
make deletion of reactions-to-broadcast more robust
r10s Aug 3, 2026
5d6e54c
make clippy happy
r10s Aug 3, 2026
70e3870
add a test for the broadcast wire format
r10s Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 12 additions & 19 deletions deltachat-jsonrpc/src/api/types/reactions.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use std::collections::BTreeMap;

use deltachat::contact::ContactId;
use deltachat::reaction::Reactions;
use serde::Serialize;
use typescript_type_def::TypeDef;
Expand Down Expand Up @@ -34,30 +33,24 @@ pub struct JsonrpcReactions {
impl From<Reactions> for JsonrpcReactions {
fn from(reactions: Reactions) -> Self {
let reactions_by_contact: BTreeMap<u32, Vec<String>> = reactions
.by_contact
.iter()
.map(|(key, value)| (key.to_u32(), vec![value.as_str().to_string()]))
.collect();
let self_reaction = reactions_by_contact.get(&ContactId::SELF.to_u32());

let mut reactions_v = Vec::new();
for (emoji, count) in reactions.emoji_sorted_by_frequency() {
let is_from_self = if let Some(self_reaction) = self_reaction {
self_reaction.contains(&emoji)
} else {
false
};

let reaction = JsonrpcReaction {
emoji,
count,
is_from_self,
};
reactions_v.push(reaction)
}

let reactions = reactions
.frequencies
.into_iter()
.map(|entry| JsonrpcReaction {
emoji: entry.reaction.as_str().to_string(),
count: entry.count,
is_from_self: entry.is_from_self,
})
.collect();

JsonrpcReactions {
reactions_by_contact,
reactions: reactions_v,
reactions,
}
}
}
14 changes: 8 additions & 6 deletions src/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2685,10 +2685,11 @@ async fn prepare_send_msg(
// from the chat.
CantSendReason::NotAMember => msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup,
CantSendReason::InBroadcast => {
matches!(
msg.param.get_cmd(),
SystemMessage::MemberRemovedFromGroup | SystemMessage::SecurejoinMessage
)
msg.param.get_int(Param::Reaction).unwrap_or_default() != 0
|| matches!(
msg.param.get_cmd(),
SystemMessage::MemberRemovedFromGroup | SystemMessage::SecurejoinMessage
)
}
CantSendReason::MissingKey => msg
.param
Expand Down Expand Up @@ -3677,14 +3678,15 @@ pub(crate) async fn create_out_broadcast_ex(

t.execute(
"INSERT INTO chats
(type, name, name_normalized, grpid, created_timestamp, param)
VALUES(?, ?, ?, ?, ?, ?)",
(type, name, name_normalized, grpid, created_timestamp, muted_until, param)
VALUES(?, ?, ?, ?, ?, ?, ?)",
(
Chattype::OutBroadcast,
&chat_name,
normalize_text(&chat_name),
&grpid,
timestamp,
MuteDuration::Forever,
Comment thread
r10s marked this conversation as resolved.
params.to_string(),
),
)?;
Expand Down
23 changes: 23 additions & 0 deletions src/chat/chat_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3055,6 +3055,29 @@ async fn test_broadcast_change_name() -> Result<()> {
Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_broadcast_muted() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;

// Alice's new outgoing broadcast channel is muted after creation:
// Channel owners can only get reaction notifications; they are usually not of much interest.
let alice_chat_id = create_broadcast(alice, "Channel".to_string()).await?;
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
let alice_chat = Chat::load_from_db(alice, alice_chat_id).await?;
assert!(alice_chat.is_muted());

// Bob joins the channel, for him, it is not muted:
// For channel subscribers, new messages to newly subscribed channels are often interesting.
let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &qr).await;
bob_chat_id.accept(bob).await?;
let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?;
assert!(!bob_chat.is_muted());

Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_broadcast_resend_to_new_member() -> Result<()> {
let mut tcm = TestContextManager::new();
Expand Down
3 changes: 3 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,9 @@ pub enum Config {
/// Timestamp of the last time housekeeping was run
LastHousekeeping,

/// Timestamp of the last time accumulated broadcast channel reactions were sent
LastReactionsBroadcast,

/// Timestamp of the last `CantDecryptOutgoingMsgs` notification.
LastCantDecryptOutgoingMsgs,

Expand Down
6 changes: 6 additions & 0 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,12 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"last_reactions_broadcast",
self.get_config_int(Config::LastReactionsBroadcast)
.await?
.to_string(),
);
res.insert(
"last_cant_decrypt_outgoing_msgs",
self.get_config_int(Config::LastCantDecryptOutgoingMsgs)
Expand Down
2 changes: 1 addition & 1 deletion src/events/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ pub enum EventType {
/// ID of the message for which reactions were changed.
msg_id: MsgId,

/// ID of the contact whose reaction set is changed.
/// ID of the contact whose reaction set is changed. May be 0 eg. in case of broadcasted reactions.
contact_id: ContactId,
},

Expand Down
4 changes: 4 additions & 0 deletions src/headerdef.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ pub enum HeaderDef {
/// This is an unprotected header.
ChatIsPostMessage,

/// Broadcasted reactions for this or other chat messages.
/// See broadcast_reactions.rs for the wire format.
ChatBroadcastReactions,

/// [Autocrypt](https://autocrypt.org/) header.
Autocrypt,
AutocryptGossip,
Expand Down
7 changes: 7 additions & 0 deletions src/mimefactory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1698,6 +1698,13 @@ impl MimeFactory {
))
}

if let Some(broadcast_reactions) = msg.param.get(Param::BroadcastReactions) {
headers.push((
"Chat-Broadcast-Reactions",
mail_builder::headers::raw::Raw::new(b_encode(broadcast_reactions)).into(),
));
}

if msg.viewtype == Viewtype::Voice
|| msg.viewtype == Viewtype::Audio
|| msg.viewtype == Viewtype::Video
Expand Down
12 changes: 12 additions & 0 deletions src/mimeparser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ pub(crate) struct MimeMessage {
pub(crate) mdn_reports: Vec<Report>,
pub(crate) delivery_report: Option<DeliveryReport>,

/// Parsed `Chat-Broadcast-Reactions` header, if any:
/// accumulated reaction updates sent by a broadcast channel owner.
pub(crate) broadcast_reactions: Option<String>,

/// Standard USENET signature, if any.
///
/// `None` means no text part was received, empty string means a text part without a footer is
Expand Down Expand Up @@ -657,6 +661,7 @@ impl MimeMessage {
user_avatar: None,
group_avatar: None,
delivery_report: None,
broadcast_reactions: None,
footer: None,
is_mime_modified: false,
decoded_data: Vec::new(),
Expand Down Expand Up @@ -793,6 +798,12 @@ impl MimeMessage {
}
}

fn parse_broadcast_reactions_header(&mut self) {
self.broadcast_reactions = self
.get_header(HeaderDef::ChatBroadcastReactions)
.map(|s| s.to_string());
}

/// Squashes mutitpart chat messages with attachment into single-part messages.
///
/// Delta Chat sends attachments, such as images, in two-part messages, with the first message
Expand Down Expand Up @@ -874,6 +885,7 @@ impl MimeMessage {
self.parse_system_message_headers();
self.parse_avatar_headers(context)?;
self.parse_videochat_headers();
self.parse_broadcast_reactions_header();
if self.delivery_report.is_none() {
self.squash_attachment_parts();
}
Expand Down
5 changes: 4 additions & 1 deletion src/param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,12 @@ pub enum Param {
/// For Messages
WantsMdn = b'r',

/// For Messages: the message is a reaction.
/// For Messages: Render message as a RFC 9078 reaction.
Reaction = b'x',

/// For Messages: Additional reactions that go to the `Chat-Broadcast-Reactions:` header
BroadcastReactions = b'X',

/// For Chats: the timestamp of the last reaction.
LastReactionTimestamp = b'y',

Expand Down
Loading
Loading