Skip to content

Commit c3c64b5

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, 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 `InFresh` so that the existing `\Seen` flag synchronisation mechanism works for them, and mark the last fresh hidden incoming message (reaction) in the chat as seen in `chat::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.
1 parent f85a28d commit c3c64b5

File tree

5 files changed

+121
-25
lines changed

5 files changed

+121
-25
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: 54 additions & 17 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;
@@ -3238,15 +3239,16 @@ pub async fn get_chat_msgs_ex(
32383239
/// Marks all messages in the chat as noticed.
32393240
/// If the given chat-id is the archive-link, marks all messages in all archived chats as noticed.
32403241
pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()> {
3241-
// "WHERE" below uses the index `(state, hidden, chat_id)`, see get_fresh_msg_cnt() for reasoning
3242-
// the additional SELECT statement may speed up things as no write-blocking is needed.
3242+
// `WHERE` statements below use the index `(state, hidden, chat_id)`, that's why we enumerate
3243+
// `hidden` values, see `get_fresh_msg_cnt()` for reasoning.
3244+
// The additional `SELECT` statement may speed up things as no write-blocking is needed.
32433245
if chat_id.is_archived_link() {
32443246
let chat_ids_in_archive = context
32453247
.sql
32463248
.query_map(
32473249
"SELECT DISTINCT(m.chat_id) FROM msgs m
32483250
LEFT JOIN chats c ON m.chat_id=c.id
3249-
WHERE m.state=10 AND m.hidden=0 AND m.chat_id>9 AND c.archived=1",
3251+
WHERE m.state=10 AND m.hidden IN (0,1) AND m.chat_id>9 AND c.archived=1",
32503252
(),
32513253
|row| row.get::<_, ChatId>(0),
32523254
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
@@ -3260,7 +3262,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
32603262
.sql
32613263
.transaction(|transaction| {
32623264
let mut stmt = transaction.prepare(
3263-
"UPDATE msgs SET state=13 WHERE state=10 AND hidden=0 AND chat_id = ?",
3265+
"UPDATE msgs SET state=13 WHERE state=10 AND hidden IN (0,1) AND chat_id=?",
32643266
)?;
32653267
for chat_id_in_archive in &chat_ids_in_archive {
32663268
stmt.execute((chat_id_in_archive,))?;
@@ -3276,22 +3278,56 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
32763278
}
32773279
} else {
32783280
start_chat_ephemeral_timers(context, chat_id).await?;
3279-
3280-
if context
3281-
.sql
3282-
.execute(
3281+
let conn_fn = |conn: &mut rusqlite::Connection| {
3282+
// This is to trigger emitting `MsgsNoticed` on other devices when reactions are noticed
3283+
// locally. We filter out `InNoticed` messages because they are normally a result of
3284+
// `mark_old_messages_as_noticed()` which happens on all devices anyway. Also we limit
3285+
// this to one message because the effect is the same anyway.
3286+
//
3287+
// Even if `message::markseen_msgs()` fails then, in the worst case other devices won't
3288+
// emit `MsgsNoticed` and app notifications won't be removed. The bigger problem is that
3289+
// another device may have more reactions received and not yet seen notifications are
3290+
// removed from it, but the same problem already exists for "usual" messages, so let's
3291+
// not solve it for now.
3292+
let mut stmt = conn.prepare(
3293+
"SELECT id, state FROM msgs
3294+
WHERE (state=? OR state=? OR state=?)
3295+
AND hidden=1
3296+
AND chat_id=?
3297+
ORDER BY id DESC LIMIT 1",
3298+
)?;
3299+
let id_to_markseen = stmt
3300+
.query_row(
3301+
(
3302+
MessageState::InFresh,
3303+
MessageState::InNoticed,
3304+
MessageState::InSeen,
3305+
chat_id,
3306+
),
3307+
|row| {
3308+
let id: MsgId = row.get(0)?;
3309+
let state: MessageState = row.get(1)?;
3310+
Ok((id, state))
3311+
},
3312+
)
3313+
.optional()?
3314+
.filter(|&(_, state)| state == MessageState::InFresh)
3315+
.map(|(id, _)| id);
3316+
let nr_msgs_noticed = conn.execute(
32833317
"UPDATE msgs
3284-
SET state=?
3285-
WHERE state=?
3286-
AND hidden=0
3287-
AND chat_id=?;",
3318+
SET state=?
3319+
WHERE state=? AND hidden IN (0,1) AND chat_id=?",
32883320
(MessageState::InNoticed, MessageState::InFresh, chat_id),
3289-
)
3290-
.await?
3291-
== 0
3292-
{
3321+
)?;
3322+
Ok((nr_msgs_noticed, id_to_markseen))
3323+
};
3324+
let (nr_msgs_noticed, id_to_markseen) = context.sql.call_write(conn_fn).await?;
3325+
if nr_msgs_noticed == 0 {
32933326
return Ok(());
32943327
}
3328+
if let Some(id) = id_to_markseen {
3329+
message::markseen_msgs(context, vec![id]).await?;
3330+
}
32953331
}
32963332

32973333
context.emit_event(EventType::MsgsNoticed(chat_id));
@@ -3332,11 +3368,12 @@ pub(crate) async fn mark_old_messages_as_noticed(
33323368
.transaction(|transaction| {
33333369
let mut changed_chats = Vec::new();
33343370
for (_, msg) in msgs_by_chat {
3371+
// NB: Enumerate `hidden` values to employ the index `(state, hidden, chat_id)`.
33353372
let changed_rows = transaction.execute(
33363373
"UPDATE msgs
33373374
SET state=?
33383375
WHERE state=?
3339-
AND hidden=0
3376+
AND hidden IN (0,1)
33403377
AND chat_id=?
33413378
AND timestamp<=?;",
33423379
(

src/reaction.rs

Lines changed: 16 additions & 2 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};
@@ -664,7 +664,7 @@ Here's my footer -- [email protected]"
664664

665665
let bob_reaction_msg = bob.pop_sent_msg().await;
666666
let alice_reaction_msg = alice.recv_msg_hidden(&bob_reaction_msg).await;
667-
assert_eq!(alice_reaction_msg.state, MessageState::InSeen);
667+
assert_eq!(alice_reaction_msg.state, MessageState::InFresh);
668668
assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2);
669669

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

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+
684698
// Alice reacts to own message.
685699
send_reaction(&alice, alice_msg.sender_msg_id, "👍 😀")
686700
.await

src/receive_imf.rs

Lines changed: 13 additions & 6 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();
@@ -1018,11 +1027,8 @@ async fn add_parts(
10181027
}
10191028
}
10201029

1021-
state = if seen
1022-
|| fetching_existing_messages
1023-
|| is_mdn
1024-
|| is_reaction
1025-
|| chat_id_blocked == Blocked::Yes
1030+
state = if seen || fetching_existing_messages || is_mdn || chat_id_blocked == Blocked::Yes
1031+
// No check for `hidden` because only reactions are such and they should be `InFresh`.
10261032
{
10271033
MessageState::InSeen
10281034
} else {
@@ -1728,6 +1734,7 @@ RETURNING id
17281734
Ok(ReceivedMsg {
17291735
chat_id,
17301736
state,
1737+
hidden,
17311738
sort_timestamp,
17321739
msg_ids: created_db_entries,
17331740
needs_delete_job,

0 commit comments

Comments
 (0)