Skip to content

Commit 3aa1e03

Browse files
committed
feat: Mark reactions as IMAP-seen in marknoticed_chat() (#6210)
When a reaction notification is shown in the UIs, there's an option "Mark Read", but the UIs are unaware of reactions message ids, moreover there are no `msgs` rows for incoming reactions in the core, so the UIs just call `marknoticed_chat()` in this case. We don't want to introduce reactions message ids to the UIs (at least currently), but let's make received reactions usual messages, just hidden and `InFresh`, so that the existing `\Seen` flag synchronisation mechanism works for them, and mark the last fresh hidden incoming message in the chat as seen in `marknoticed_chat()` to trigger emitting `MsgsNoticed` on other devices. There's a problem though that another device may have more reactions received and not yet seen notifications are removed from it when handling `MsgsNoticed`, but the same problem already exists for "usual" messages, so let's not solve it for now. It's interesting that sent out reactions are already hidden messages, so this change mostly just unifies things.
1 parent b74ff27 commit 3aa1e03

File tree

6 files changed

+153
-34
lines changed

6 files changed

+153
-34
lines changed

deltachat-rpc-client/src/deltachat_rpc_client/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class EventType(str, Enum):
3939
ERROR_SELF_NOT_IN_GROUP = "ErrorSelfNotInGroup"
4040
MSGS_CHANGED = "MsgsChanged"
4141
REACTIONS_CHANGED = "ReactionsChanged"
42+
INCOMING_REACTION = "IncomingReaction"
4243
INCOMING_MSG = "IncomingMsg"
4344
INCOMING_MSG_BUNCH = "IncomingMsgBunch"
4445
MSGS_NOTICED = "MsgsNoticed"

deltachat-rpc-client/tests/test_something.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,43 @@ def test_message(acfactory) -> None:
285285
assert reactions == snapshot.reactions
286286

287287

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

src/chat.rs

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use crate::mimeparser::SystemMessage;
3939
use crate::param::{Param, Params};
4040
use crate::peerstate::Peerstate;
4141
use crate::receive_imf::ReceivedMsg;
42+
use crate::rusqlite::OptionalExtension;
4243
use crate::securejoin::BobState;
4344
use crate::smtp::send_msg_to_smtp;
4445
use crate::sql;
@@ -3255,10 +3256,10 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
32553256
.query_map(
32563257
"SELECT DISTINCT(m.chat_id) FROM msgs m
32573258
LEFT JOIN chats c ON m.chat_id=c.id
3258-
WHERE m.state=10 AND m.hidden=0 AND m.chat_id>9 AND c.blocked=0 AND c.archived=1",
3259-
(),
3259+
WHERE m.state=10 AND m.chat_id>9 AND c.blocked=0 AND c.archived=1",
3260+
(),
32603261
|row| row.get::<_, ChatId>(0),
3261-
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into)
3262+
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
32623263
)
32633264
.await?;
32643265
if chat_ids_in_archive.is_empty() {
@@ -3269,7 +3270,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
32693270
.sql
32703271
.execute(
32713272
&format!(
3272-
"UPDATE msgs SET state=13 WHERE state=10 AND hidden=0 AND chat_id IN ({});",
3273+
"UPDATE msgs SET state=13 WHERE state=10 AND chat_id IN ({});",
32733274
sql::repeat_vars(chat_ids_in_archive.len())
32743275
),
32753276
rusqlite::params_from_iter(&chat_ids_in_archive),
@@ -3279,20 +3280,61 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
32793280
context.emit_event(EventType::MsgsNoticed(chat_id_in_archive));
32803281
chatlist_events::emit_chatlist_item_changed(context, chat_id_in_archive);
32813282
}
3282-
} else if context
3283-
.sql
3284-
.execute(
3285-
"UPDATE msgs
3286-
SET state=?
3287-
WHERE state=?
3288-
AND hidden=0
3289-
AND chat_id=?;",
3290-
(MessageState::InNoticed, MessageState::InFresh, chat_id),
3291-
)
3292-
.await?
3293-
== 0
3294-
{
3295-
return Ok(());
3283+
} else {
3284+
let conn_fn = |conn: &mut rusqlite::Connection| {
3285+
// This is to trigger emitting `MsgsNoticed` on other devices when reactions are noticed
3286+
// locally. We filter out `InNoticed` messages because they are normally a result of
3287+
// `mark_old_messages_as_noticed()` which happens on all devices anyway. Also we limit
3288+
// this to one message because the effect is the same anyway.
3289+
//
3290+
// Even if `message::markseen_msgs()` fails then, in the worst case other devices won't
3291+
// emit `MsgsNoticed` and app notifications won't be removed. The bigger problem is that
3292+
// another device may have more reactions received and not yet seen notifications are
3293+
// removed from it, but the same problem already exists for "usual" messages, so let's
3294+
// not solve it for now.
3295+
let mut stmt = conn.prepare(
3296+
"SELECT id, state FROM msgs
3297+
WHERE (state=? OR state=? OR state=?)
3298+
AND hidden=1
3299+
AND chat_id=?
3300+
ORDER BY id DESC LIMIT 1",
3301+
)?;
3302+
let id_to_markseen = stmt
3303+
.query_row(
3304+
(
3305+
MessageState::InFresh,
3306+
MessageState::InNoticed,
3307+
MessageState::InSeen,
3308+
chat_id,
3309+
),
3310+
|row| {
3311+
let id: MsgId = row.get(0)?;
3312+
let state: MessageState = row.get(1)?;
3313+
Ok((id, state))
3314+
},
3315+
)
3316+
.optional()?
3317+
.filter(|&(_, state)| state == MessageState::InFresh)
3318+
.map(|(id, _)| id);
3319+
3320+
// NB: Enumerate `hidden` values to employ `msgs_index7`.
3321+
let nr_msgs_noticed = conn.execute(
3322+
"UPDATE msgs
3323+
SET state=?
3324+
WHERE state=?
3325+
AND (hidden=0 OR hidden=1)
3326+
AND chat_id=?",
3327+
(MessageState::InNoticed, MessageState::InFresh, chat_id),
3328+
)?;
3329+
Ok((nr_msgs_noticed, id_to_markseen))
3330+
};
3331+
let (nr_msgs_noticed, id_to_markseen) = context.sql.call_write(conn_fn).await?;
3332+
if nr_msgs_noticed == 0 {
3333+
return Ok(());
3334+
}
3335+
if let Some(id) = id_to_markseen {
3336+
message::markseen_msgs(context, vec![id]).await?;
3337+
}
32963338
}
32973339

32983340
context.emit_event(EventType::MsgsNoticed(chat_id));
@@ -3337,7 +3379,6 @@ pub(crate) async fn mark_old_messages_as_noticed(
33373379
"UPDATE msgs
33383380
SET state=?
33393381
WHERE state=?
3340-
AND hidden=0
33413382
AND chat_id=?
33423383
AND timestamp<=?;",
33433384
(

src/reaction.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ mod tests {
397397
use deltachat_contact_tools::ContactAddress;
398398

399399
use super::*;
400-
use crate::chat::{forward_msgs, get_chat_msgs, send_text_msg};
400+
use crate::chat::{forward_msgs, get_chat_msgs, marknoticed_chat, send_text_msg};
401401
use crate::chatlist::Chatlist;
402402
use crate::config::Config;
403403
use crate::contact::{Contact, Origin};
@@ -663,7 +663,8 @@ Here's my footer -- [email protected]"
663663
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
664664

665665
let bob_reaction_msg = bob.pop_sent_msg().await;
666-
alice.recv_msg_trash(&bob_reaction_msg).await;
666+
let alice_reaction_msg = alice.recv_msg_hidden(&bob_reaction_msg).await;
667+
assert_eq!(alice_reaction_msg.state, MessageState::InFresh);
667668
assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2);
668669

669670
let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
@@ -680,6 +681,20 @@ Here's my footer -- [email protected]"
680681
expect_incoming_reactions_event(&alice, alice_msg.sender_msg_id, *bob_id, "👍").await?;
681682
expect_no_unwanted_events(&alice).await;
682683

684+
marknoticed_chat(&alice, chat_alice.id).await?;
685+
assert_eq!(
686+
alice_reaction_msg.id.get_state(&alice).await?,
687+
MessageState::InSeen
688+
);
689+
// Reactions don't request MDNs.
690+
assert_eq!(
691+
alice
692+
.sql
693+
.count("SELECT COUNT(*) FROM smtp_mdns", ())
694+
.await?,
695+
0
696+
);
697+
683698
// Alice reacts to own message.
684699
send_reaction(&alice, alice_msg.sender_msg_id, "👍 😀")
685700
.await
@@ -719,7 +734,7 @@ Here's my footer -- [email protected]"
719734
bob_msg1.chat_id.accept(&bob).await?;
720735
send_reaction(&bob, bob_msg1.id, "👍").await?;
721736
let bob_send_reaction = bob.pop_sent_msg().await;
722-
alice.recv_msg_trash(&bob_send_reaction).await;
737+
alice.recv_msg_hidden(&bob_send_reaction).await;
723738
expect_incoming_reactions_event(&alice, alice_msg1.sender_msg_id, alice_bob_id, "👍")
724739
.await?;
725740
expect_no_unwanted_events(&alice).await;
@@ -882,7 +897,7 @@ Here's my footer -- [email protected]"
882897
let bob_reaction_msg = bob.pop_sent_msg().await;
883898

884899
// Alice receives a reaction.
885-
alice.recv_msg_trash(&bob_reaction_msg).await;
900+
alice.recv_msg_hidden(&bob_reaction_msg).await;
886901

887902
let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
888903
assert_eq!(reactions.to_string(), "👍1");
@@ -934,7 +949,7 @@ Here's my footer -- [email protected]"
934949
{
935950
send_reaction(&alice2, alice2_msg.id, "👍").await?;
936951
let msg = alice2.pop_sent_msg().await;
937-
alice1.recv_msg_trash(&msg).await;
952+
alice1.recv_msg_hidden(&msg).await;
938953
}
939954

940955
// Check that the status is still the same.
@@ -956,7 +971,7 @@ Here's my footer -- [email protected]"
956971
let alice1_msg = alice1.recv_msg(&alice0.pop_sent_msg().await).await;
957972

958973
send_reaction(&alice0, alice0_msg_id, "👀").await?;
959-
alice1.recv_msg_trash(&alice0.pop_sent_msg().await).await;
974+
alice1.recv_msg_hidden(&alice0.pop_sent_msg().await).await;
960975

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

src/receive_imf.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ pub struct ReceivedMsg {
5454
/// Received message state.
5555
pub state: MessageState,
5656

57+
/// Whether the message is hidden.
58+
pub hidden: bool,
59+
5760
/// Message timestamp for sorting.
5861
pub sort_timestamp: i64,
5962

@@ -192,6 +195,7 @@ pub(crate) async fn receive_imf_inner(
192195
return Ok(Some(ReceivedMsg {
193196
chat_id: DC_CHAT_ID_TRASH,
194197
state: MessageState::Undefined,
198+
hidden: false,
195199
sort_timestamp: 0,
196200
msg_ids,
197201
needs_delete_job: false,
@@ -373,6 +377,7 @@ pub(crate) async fn receive_imf_inner(
373377
received_msg = Some(ReceivedMsg {
374378
chat_id: DC_CHAT_ID_TRASH,
375379
state: MessageState::InSeen,
380+
hidden: false,
376381
sort_timestamp: mime_parser.timestamp_sent,
377382
msg_ids: vec![msg_id],
378383
needs_delete_job: res == securejoin::HandshakeMessage::Done,
@@ -611,7 +616,11 @@ pub(crate) async fn receive_imf_inner(
611616
} else if !chat_id.is_trash() {
612617
let fresh = received_msg.state == MessageState::InFresh;
613618
for msg_id in &received_msg.msg_ids {
614-
chat_id.emit_msg_event(context, *msg_id, mime_parser.incoming && fresh);
619+
chat_id.emit_msg_event(
620+
context,
621+
*msg_id,
622+
mime_parser.incoming && fresh && !received_msg.hidden,
623+
);
615624
}
616625
}
617626
context.new_msgs_notify.notify_one();
@@ -760,7 +769,7 @@ async fn add_parts(
760769
// (of course, the user can add other chats manually later)
761770
let to_id: ContactId;
762771
let state: MessageState;
763-
let mut hidden = false;
772+
let mut hidden = is_reaction;
764773
let mut needs_delete_job = false;
765774
let mut restore_protection = false;
766775

@@ -1021,7 +1030,9 @@ async fn add_parts(
10211030
state = if seen
10221031
|| fetching_existing_messages
10231032
|| is_mdn
1024-
|| is_reaction
1033+
// Currently only reactions are hidden, so this check is just in case we have other
1034+
// hidden messages in the future to make `chat::marknoticed_chat()` no-op for them.
1035+
|| (hidden && !is_reaction)
10251036
|| chat_id_blocked == Blocked::Yes
10261037
{
10271038
MessageState::InSeen
@@ -1232,7 +1243,7 @@ async fn add_parts(
12321243
}
12331244

12341245
let orig_chat_id = chat_id;
1235-
let mut chat_id = if is_mdn || is_reaction {
1246+
let mut chat_id = if is_mdn {
12361247
DC_CHAT_ID_TRASH
12371248
} else {
12381249
chat_id.unwrap_or_else(|| {
@@ -1597,10 +1608,10 @@ RETURNING id
15971608
state,
15981609
is_dc_message,
15991610
if trash { "" } else { msg },
1600-
if trash { None } else { message::normalize_text(msg) },
1601-
if trash { "" } else { &subject },
1611+
if trash || hidden { None } else { message::normalize_text(msg) },
1612+
if trash || hidden { "" } else { &subject },
16021613
// txt_raw might contain invalid utf8
1603-
if trash { "" } else { &txt_raw },
1614+
if trash || hidden { "" } else { &txt_raw },
16041615
if trash {
16051616
"".to_string()
16061617
} else {
@@ -1616,7 +1627,7 @@ RETURNING id
16161627
mime_in_reply_to,
16171628
mime_references,
16181629
save_mime_modified,
1619-
part.error.as_deref().unwrap_or_default(),
1630+
if trash || hidden { "" } else { part.error.as_deref().unwrap_or_default() },
16201631
ephemeral_timer,
16211632
ephemeral_timestamp,
16221633
if is_partial_download.is_some() {
@@ -1626,7 +1637,7 @@ RETURNING id
16261637
} else {
16271638
DownloadState::Done
16281639
},
1629-
mime_parser.hop_info
1640+
if trash || hidden { "" } else { &mime_parser.hop_info },
16301641
],
16311642
|row| {
16321643
let msg_id: MsgId = row.get(0)?;
@@ -1733,6 +1744,7 @@ RETURNING id
17331744
Ok(ReceivedMsg {
17341745
chat_id,
17351746
state,
1747+
hidden,
17361748
sort_timestamp,
17371749
msg_ids: created_db_entries,
17381750
needs_delete_job,

src/test_utils.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,19 @@ impl TestContext {
606606
msg
607607
}
608608

609+
/// Receive a message using the `receive_imf()` pipeline. Panics if it's not hidden.
610+
pub async fn recv_msg_hidden(&self, msg: &SentMessage<'_>) -> Message {
611+
let received = self
612+
.recv_msg_opt(msg)
613+
.await
614+
.expect("receive_imf() seems not to have added a new message to the db");
615+
let msg = Message::load_from_db(self, *received.msg_ids.last().unwrap())
616+
.await
617+
.unwrap();
618+
assert!(msg.hidden);
619+
msg
620+
}
621+
609622
/// Receive a message using the `receive_imf()` pipeline. This is similar
610623
/// to `recv_msg()`, but doesn't assume that the message is shown in the chat.
611624
pub async fn recv_msg_opt(

0 commit comments

Comments
 (0)