Skip to content

Commit 38b08fa

Browse files
authored
feat: When reactions are seen, remove notification from second device (#6480)
Instead of being trashed, the message containing a reaction remains in the chat, hidden and InFresh. When the chat is opened, it will be marked as Seen on the server, so that a second device removes the notifications for the reaction. Close #6210. Also, this adds a benchmark.
1 parent a49dfec commit 38b08fa

File tree

7 files changed

+226
-32
lines changed

7 files changed

+226
-32
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ harness = false
158158
name = "get_chat_msgs"
159159
harness = false
160160

161+
[[bench]]
162+
name = "marknoticed_chat"
163+
harness = false
164+
161165
[[bench]]
162166
name = "get_chatlist"
163167
harness = false

benches/marknoticed_chat.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#![recursion_limit = "256"]
2+
use std::path::Path;
3+
4+
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
5+
use deltachat::chat::{self, ChatId};
6+
use deltachat::chatlist::Chatlist;
7+
use deltachat::context::Context;
8+
use deltachat::stock_str::StockStrings;
9+
use deltachat::Events;
10+
use futures_lite::future::block_on;
11+
use tempfile::tempdir;
12+
13+
async fn marknoticed_chat_benchmark(context: &Context, chats: &[ChatId]) {
14+
for c in chats.iter().take(20) {
15+
chat::marknoticed_chat(context, *c).await.unwrap();
16+
}
17+
}
18+
19+
fn criterion_benchmark(c: &mut Criterion) {
20+
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
21+
// messages, such as your primary account.
22+
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
23+
let rt = tokio::runtime::Runtime::new().unwrap();
24+
25+
let chats: Vec<_> = rt.block_on(async {
26+
let context = Context::new(Path::new(&path), 100, Events::new(), StockStrings::new())
27+
.await
28+
.unwrap();
29+
let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap();
30+
let len = chatlist.len();
31+
(1..len).map(|i| chatlist.get_chat_id(i).unwrap()).collect()
32+
});
33+
34+
// This mainly tests the performance of marknoticed_chat()
35+
// when nothing has to be done
36+
c.bench_function(
37+
"chat::marknoticed_chat (mark 20 chats as noticed repeatedly)",
38+
|b| {
39+
let dir = tempdir().unwrap();
40+
let dir = dir.path();
41+
let new_db = dir.join("dc.db");
42+
std::fs::copy(&path, &new_db).unwrap();
43+
44+
let context = block_on(async {
45+
Context::new(Path::new(&new_db), 100, Events::new(), StockStrings::new())
46+
.await
47+
.unwrap()
48+
});
49+
50+
b.to_async(&rt)
51+
.iter(|| marknoticed_chat_benchmark(&context, black_box(&chats)))
52+
},
53+
);
54+
55+
// If the first 20 chats contain fresh messages or reactions,
56+
// this tests the performance of marking them as noticed.
57+
c.bench_function(
58+
"chat::marknoticed_chat (mark 20 chats as noticed, resetting after every iteration)",
59+
|b| {
60+
b.to_async(&rt).iter_batched(
61+
|| {
62+
let dir = tempdir().unwrap();
63+
let new_db = dir.path().join("dc.db");
64+
std::fs::copy(&path, &new_db).unwrap();
65+
66+
let context = block_on(async {
67+
Context::new(
68+
Path::new(&new_db),
69+
100,
70+
Events::new(),
71+
StockStrings::new(),
72+
)
73+
.await
74+
.unwrap()
75+
});
76+
(dir, context)
77+
},
78+
|(_dir, context)| {
79+
let chats = &chats;
80+
async move {
81+
marknoticed_chat_benchmark(black_box(&context), black_box(chats)).await
82+
}
83+
},
84+
BatchSize::PerIteration,
85+
);
86+
},
87+
);
88+
} else {
89+
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
90+
}
91+
}
92+
93+
criterion_group!(benches, criterion_benchmark);
94+
criterion_main!(benches);

deltachat-rpc-client/tests/test_something.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,44 @@ def test_message(acfactory) -> None:
287287
assert reactions == snapshot.reactions
288288

289289

290+
def test_reaction_seen_on_another_dev(acfactory, tmp_path) -> None:
291+
alice, bob = acfactory.get_online_accounts(2)
292+
alice.export_backup(tmp_path)
293+
files = list(tmp_path.glob("*.tar"))
294+
alice2 = acfactory.get_unconfigured_account()
295+
alice2.import_backup(files[0])
296+
alice2.start_io()
297+
298+
bob_addr = bob.get_config("addr")
299+
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
300+
alice_chat_bob = alice_contact_bob.create_chat()
301+
alice_chat_bob.send_text("Hello!")
302+
303+
event = bob.wait_for_incoming_msg_event()
304+
msg_id = event.msg_id
305+
306+
message = bob.get_message_by_id(msg_id)
307+
snapshot = message.get_snapshot()
308+
snapshot.chat.accept()
309+
message.send_reaction("😎")
310+
for a in [alice, alice2]:
311+
while True:
312+
event = a.wait_for_event()
313+
if event.kind == EventType.INCOMING_REACTION:
314+
break
315+
316+
alice2.clear_all_events()
317+
alice_chat_bob.mark_noticed()
318+
while True:
319+
event = alice2.wait_for_event()
320+
if event.kind == EventType.MSGS_NOTICED:
321+
chat_id = event.chat_id
322+
break
323+
alice2_contact_bob = alice2.get_contact_by_addr(bob_addr)
324+
alice2_chat_bob = alice2_contact_bob.create_chat()
325+
assert chat_id == alice2_chat_bob.id
326+
327+
290328
def test_is_bot(acfactory) -> None:
291329
"""Test that we can recognize messages submitted by bots."""
292330
alice, bob = acfactory.get_online_accounts(2)

src/chat.rs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ use tokio::task;
2020
use crate::aheader::EncryptPreference;
2121
use crate::blob::BlobObject;
2222
use crate::chatlist::Chatlist;
23-
use crate::chatlist_events;
2423
use crate::color::str_to_color;
2524
use crate::config::Config;
2625
use crate::constants::{
@@ -51,6 +50,7 @@ use crate::tools::{
5150
truncate_msg_text, IsNoneOrEmpty, SystemTime,
5251
};
5352
use crate::webxdc::StatusUpdateSerial;
53+
use crate::{chatlist_events, imap};
5454

5555
/// An chat item, such as a message or a marker.
5656
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -3396,7 +3396,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
33963396
} else {
33973397
start_chat_ephemeral_timers(context, chat_id).await?;
33983398

3399-
if context
3399+
let noticed_msgs_count = context
34003400
.sql
34013401
.execute(
34023402
"UPDATE msgs
@@ -3406,9 +3406,36 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
34063406
AND chat_id=?;",
34073407
(MessageState::InNoticed, MessageState::InFresh, chat_id),
34083408
)
3409-
.await?
3410-
== 0
3411-
{
3409+
.await?;
3410+
3411+
// This is to trigger emitting `MsgsNoticed` on other devices when reactions are noticed
3412+
// locally (i.e. when the chat was opened locally).
3413+
let hidden_messages = context
3414+
.sql
3415+
.query_map(
3416+
"SELECT id, rfc724_mid FROM msgs
3417+
WHERE state=?
3418+
AND hidden=1
3419+
AND chat_id=?
3420+
ORDER BY id LIMIT 100", // LIMIT to 100 in order to avoid blocking the UI too long, usually there will be less than 100 messages anyway
3421+
(MessageState::InFresh, chat_id), // No need to check for InNoticed messages, because reactions are never InNoticed
3422+
|row| {
3423+
let msg_id: MsgId = row.get(0)?;
3424+
let rfc724_mid: String = row.get(1)?;
3425+
Ok((msg_id, rfc724_mid))
3426+
},
3427+
|rows| {
3428+
rows.collect::<std::result::Result<Vec<_>, _>>()
3429+
.map_err(Into::into)
3430+
},
3431+
)
3432+
.await?;
3433+
for (msg_id, rfc724_mid) in &hidden_messages {
3434+
message::update_msg_state(context, *msg_id, MessageState::InSeen).await?;
3435+
imap::markseen_on_imap_table(context, rfc724_mid).await?;
3436+
}
3437+
3438+
if noticed_msgs_count == 0 {
34123439
return Ok(());
34133440
}
34143441
}

src/reaction.rs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ mod tests {
398398
use deltachat_contact_tools::ContactAddress;
399399

400400
use super::*;
401-
use crate::chat::{forward_msgs, get_chat_msgs, send_text_msg};
401+
use crate::chat::{forward_msgs, get_chat_msgs, marknoticed_chat, send_text_msg};
402402
use crate::chatlist::Chatlist;
403403
use crate::config::Config;
404404
use crate::contact::{Contact, Origin};
@@ -623,7 +623,9 @@ Here's my footer -- [email protected]"
623623
.get_matching_opt(t, |evt| {
624624
matches!(
625625
evt,
626-
EventType::IncomingReaction { .. } | EventType::IncomingMsg { .. }
626+
EventType::IncomingReaction { .. }
627+
| EventType::IncomingMsg { .. }
628+
| EventType::MsgsChanged { .. }
627629
)
628630
})
629631
.await;
@@ -667,7 +669,8 @@ Here's my footer -- [email protected]"
667669
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
668670

669671
let bob_reaction_msg = bob.pop_sent_msg().await;
670-
alice.recv_msg_trash(&bob_reaction_msg).await;
672+
let alice_reaction_msg = alice.recv_msg_hidden(&bob_reaction_msg).await;
673+
assert_eq!(alice_reaction_msg.state, MessageState::InFresh);
671674
assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2);
672675

673676
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
@@ -691,6 +694,20 @@ Here's my footer -- [email protected]"
691694
.await?;
692695
expect_no_unwanted_events(&alice).await;
693696

697+
marknoticed_chat(&alice, chat_alice.id).await?;
698+
assert_eq!(
699+
alice_reaction_msg.id.get_state(&alice).await?,
700+
MessageState::InSeen
701+
);
702+
// Reactions don't request MDNs.
703+
assert_eq!(
704+
alice
705+
.sql
706+
.count("SELECT COUNT(*) FROM smtp_mdns", ())
707+
.await?,
708+
0
709+
);
710+
694711
// Alice reacts to own message.
695712
send_reaction(&alice, alice_msg.sender_msg_id, "👍 😀")
696713
.await
@@ -730,7 +747,7 @@ Here's my footer -- [email protected]"
730747
bob_msg1.chat_id.accept(&bob).await?;
731748
send_reaction(&bob, bob_msg1.id, "👍").await?;
732749
let bob_send_reaction = bob.pop_sent_msg().await;
733-
alice.recv_msg_trash(&bob_send_reaction).await;
750+
alice.recv_msg_hidden(&bob_send_reaction).await;
734751
expect_incoming_reactions_event(
735752
&alice,
736753
alice_chat.id,
@@ -899,7 +916,7 @@ Here's my footer -- [email protected]"
899916
let bob_reaction_msg = bob.pop_sent_msg().await;
900917

901918
// Alice receives a reaction.
902-
alice.recv_msg_trash(&bob_reaction_msg).await;
919+
alice.recv_msg_hidden(&bob_reaction_msg).await;
903920

904921
let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
905922
assert_eq!(reactions.to_string(), "👍1");
@@ -951,7 +968,7 @@ Here's my footer -- [email protected]"
951968
{
952969
send_reaction(&alice2, alice2_msg.id, "👍").await?;
953970
let msg = alice2.pop_sent_msg().await;
954-
alice1.recv_msg_trash(&msg).await;
971+
alice1.recv_msg_hidden(&msg).await;
955972
}
956973

957974
// Check that the status is still the same.
@@ -973,7 +990,7 @@ Here's my footer -- [email protected]"
973990
let alice1_msg = alice1.recv_msg(&alice0.pop_sent_msg().await).await;
974991

975992
send_reaction(&alice0, alice0_msg_id, "👀").await?;
976-
alice1.recv_msg_trash(&alice0.pop_sent_msg().await).await;
993+
alice1.recv_msg_hidden(&alice0.pop_sent_msg().await).await;
977994

978995
expect_reactions_changed_event(&alice0, chat_id, alice0_msg_id, ContactId::SELF).await?;
979996
expect_reactions_changed_event(&alice1, alice1_msg.chat_id, alice1_msg.id, ContactId::SELF)

0 commit comments

Comments
 (0)