Skip to content

Commit 2d25772

Browse files
committed
fix: markseen_msgs(): Mark reactions to specified messages as seen too (#7884)
This allows to remove notifications for reactions from other devices. NB: UIs should pass all messages to markseen_msgs(), incl. outgoing ones. markseen_msgs() should be called when a message comes into view or when a reaction for a message being in view arrives. Also don't emit `MsgsNoticed` from receive_imf_inner() if the chat still contains fresh hidden messages, i.e. include reactions into this logic, to avoid removing notifications for reactions until they are seen on another device.
1 parent e1e8407 commit 2d25772

File tree

5 files changed

+135
-62
lines changed

5 files changed

+135
-62
lines changed

deltachat-rpc-client/tests/test_something.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -413,27 +413,32 @@ def test_dont_move_sync_msgs(acfactory, direct_imap):
413413
time.sleep(1)
414414

415415

416-
def test_reaction_seen_on_another_dev(acfactory) -> None:
416+
@pytest.mark.parametrize("chat_noticed", [False, True])
417+
def test_reaction_seen_on_another_dev(acfactory, chat_noticed) -> None:
417418
alice, bob = acfactory.get_online_accounts(2)
418419
alice2 = alice.clone()
419420
alice2.start_io()
420421

421422
alice_contact_bob = alice.create_contact(bob, "Bob")
422423
alice_chat_bob = alice_contact_bob.create_chat()
423-
alice_chat_bob.send_text("Hello!")
424+
alice_msg = alice_chat_bob.send_text("Hello!")
424425

425426
event = bob.wait_for_incoming_msg_event()
426427
msg_id = event.msg_id
427428

428-
message = bob.get_message_by_id(msg_id)
429-
snapshot = message.get_snapshot()
429+
bob_msg = bob.get_message_by_id(msg_id)
430+
snapshot = bob_msg.get_snapshot()
430431
snapshot.chat.accept()
431-
message.send_reaction("😎")
432+
bob_msg.send_reaction("😎")
432433
for a in [alice, alice2]:
433434
a.wait_for_event(EventType.INCOMING_REACTION)
434435

435436
alice2.clear_all_events()
436-
alice_chat_bob.mark_noticed()
437+
if chat_noticed:
438+
alice_chat_bob.mark_noticed()
439+
else:
440+
# UIs are supposed to mark messages being in view as seen, not reactions themselves.
441+
alice_msg.mark_seen()
437442
chat_id = alice2.wait_for_event(EventType.MSGS_NOTICED).chat_id
438443
alice2_chat_bob = alice2.create_chat(bob)
439444
assert chat_id == alice2_chat_bob.id

src/message.rs

Lines changed: 60 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1829,7 +1829,7 @@ pub async fn delete_msgs_ex(
18291829
Ok(())
18301830
}
18311831

1832-
/// Marks requested messages as seen.
1832+
/// Marks requested messages and reactions to them as seen.
18331833
pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()> {
18341834
if msg_ids.is_empty() {
18351835
return Ok(());
@@ -1843,10 +1843,18 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
18431843

18441844
let mut msgs = Vec::with_capacity(msg_ids.len());
18451845
for &id in &msg_ids {
1846-
if let Some(msg) = context
1846+
let Some(rfc724_mid): Option<String> = context
18471847
.sql
1848-
.query_row_optional(
1848+
.query_get_value("SELECT rfc724_mid FROM msgs WHERE id=?", (id,))
1849+
.await?
1850+
else {
1851+
continue;
1852+
};
1853+
context
1854+
.sql
1855+
.query_map(
18491856
"SELECT
1857+
m.id AS id,
18501858
m.chat_id AS chat_id,
18511859
m.state AS state,
18521860
m.ephemeral_timer AS ephemeral_timer,
@@ -1857,9 +1865,11 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
18571865
c.archived AS archived,
18581866
c.blocked AS blocked
18591867
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
1860-
WHERE m.id=? AND m.chat_id>9",
1861-
(id,),
1868+
WHERE (m.id=? OR m.mime_in_reply_to=? AND m.hidden=1)
1869+
AND m.chat_id>9 AND ?<=m.state AND m.state<?",
1870+
(id, rfc724_mid, MessageState::InFresh, MessageState::InSeen),
18621871
|row| {
1872+
let id: MsgId = row.get("id")?;
18631873
let chat_id: ChatId = row.get("chat_id")?;
18641874
let state: MessageState = row.get("state")?;
18651875
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
@@ -1884,11 +1894,14 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
18841894
ephemeral_timer,
18851895
))
18861896
},
1897+
|rows| {
1898+
for row in rows {
1899+
msgs.push(row?);
1900+
}
1901+
Ok(())
1902+
},
18871903
)
1888-
.await?
1889-
{
1890-
msgs.push(msg);
1891-
}
1904+
.await?;
18921905
}
18931906

18941907
if msgs
@@ -1917,47 +1930,44 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
19171930
_curr_ephemeral_timer,
19181931
) in msgs
19191932
{
1920-
if curr_state == MessageState::InFresh || curr_state == MessageState::InNoticed {
1921-
update_msg_state(context, id, MessageState::InSeen).await?;
1922-
info!(context, "Seen message {}.", id);
1923-
1924-
markseen_on_imap_table(context, &curr_rfc724_mid).await?;
1925-
1926-
// Read receipts for system messages are never sent to contacts.
1927-
// These messages have no place to display received read receipt
1928-
// anyway. And since their text is locally generated,
1929-
// quoting them is dangerous as it may contain contact names. E.g., for original message
1930-
// "Group left by me", a read receipt will quote "Group left by <name>", and the name can
1931-
// be a display name stored in address book rather than the name sent in the From field by
1932-
// the user.
1933-
//
1934-
// We also don't send read receipts for contact requests.
1935-
// Read receipts will not be sent even after accepting the chat.
1936-
let to_id = if curr_blocked == Blocked::Not
1937-
&& curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
1938-
&& curr_param.get_cmd() == SystemMessage::Unknown
1939-
&& context.should_send_mdns().await?
1940-
{
1941-
Some(curr_from_id)
1942-
} else if context.get_config_bool(Config::BccSelf).await? {
1943-
Some(ContactId::SELF)
1944-
} else {
1945-
None
1946-
};
1947-
if let Some(to_id) = to_id {
1948-
context
1949-
.sql
1950-
.execute(
1951-
"INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
1952-
(id, to_id, curr_rfc724_mid),
1953-
)
1954-
.await
1955-
.context("failed to insert into smtp_mdns")?;
1956-
context.scheduler.interrupt_smtp().await;
1957-
}
1958-
if !curr_hidden {
1959-
updated_chat_ids.insert(curr_chat_id);
1960-
}
1933+
update_msg_state(context, id, MessageState::InSeen).await?;
1934+
info!(context, "Seen message {}.", id);
1935+
1936+
markseen_on_imap_table(context, &curr_rfc724_mid).await?;
1937+
1938+
// Read receipts for system messages are never sent to contacts. These messages have no
1939+
// place to display received read receipt anyway. And since their text is locally generated,
1940+
// quoting them is dangerous as it may contain contact names. E.g., for original message
1941+
// "Group left by me", a read receipt will quote "Group left by <name>", and the name can be
1942+
// a display name stored in address book rather than the name sent in the From field by the
1943+
// user.
1944+
//
1945+
// We also don't send read receipts for contact requests. Read receipts will not be sent
1946+
// even after accepting the chat.
1947+
let to_id = if curr_blocked == Blocked::Not
1948+
&& curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
1949+
&& curr_param.get_cmd() == SystemMessage::Unknown
1950+
&& context.should_send_mdns().await?
1951+
{
1952+
Some(curr_from_id)
1953+
} else if context.get_config_bool(Config::BccSelf).await? {
1954+
Some(ContactId::SELF)
1955+
} else {
1956+
None
1957+
};
1958+
if let Some(to_id) = to_id {
1959+
context
1960+
.sql
1961+
.execute(
1962+
"INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
1963+
(id, to_id, curr_rfc724_mid),
1964+
)
1965+
.await
1966+
.context("failed to insert into smtp_mdns")?;
1967+
context.scheduler.interrupt_smtp().await;
1968+
}
1969+
if !curr_hidden {
1970+
updated_chat_ids.insert(curr_chat_id);
19611971
}
19621972
archived_chats_maybe_noticed |= curr_state == MessageState::InFresh
19631973
&& !curr_hidden

src/reaction.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ mod tests {
393393
use crate::chatlist::Chatlist;
394394
use crate::config::Config;
395395
use crate::contact::{Contact, Origin};
396-
use crate::message::{MessageState, Viewtype, delete_msgs};
396+
use crate::message::{MessageState, Viewtype, delete_msgs, markseen_msgs};
397397
use crate::receive_imf::receive_imf;
398398
use crate::sql::housekeeping;
399399
use crate::test_utils::E2EE_INFO_MSGS;
@@ -956,4 +956,37 @@ Content-Disposition: reaction\n\
956956
}
957957
Ok(())
958958
}
959+
960+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
961+
async fn test_markseen_referenced_msg() -> Result<()> {
962+
let mut tcm = TestContextManager::new();
963+
let alice = &tcm.alice().await;
964+
let bob = &tcm.bob().await;
965+
let chat_id = alice.create_chat(bob).await.id;
966+
967+
let alice_msg_id = send_text_msg(alice, chat_id, "foo".to_string()).await?;
968+
let sent_msg = alice.pop_sent_msg().await;
969+
let bob_msg = bob.recv_msg(&sent_msg).await;
970+
bob_msg.chat_id.accept(bob).await?;
971+
972+
send_reaction(bob, bob_msg.id, "👀").await?;
973+
let sent_msg = bob.pop_sent_msg().await;
974+
let alice_reaction = alice.recv_msg_hidden(&sent_msg).await;
975+
assert_eq!(alice_reaction.state, MessageState::InFresh);
976+
977+
markseen_msgs(alice, vec![alice_msg_id]).await?;
978+
let alice_reaction = Message::load_from_db(alice, alice_reaction.id).await?;
979+
assert_eq!(alice_reaction.state, MessageState::InSeen);
980+
assert_eq!(
981+
alice
982+
.sql
983+
.count(
984+
"SELECT COUNT(*) FROM smtp_mdns WHERE from_id=?",
985+
(ContactId::SELF,)
986+
)
987+
.await?,
988+
1
989+
);
990+
Ok(())
991+
}
959992
}

src/receive_imf.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ use crate::message::{
3434
self, Message, MessageState, MessengerMessage, MsgId, Viewtype, rfc724_mid_exists,
3535
};
3636
use crate::mimeparser::{
37-
AvatarAction, GossipedKey, MimeMessage, PreMessageMode, SystemMessage, parse_message_ids,
37+
AvatarAction, GossipedKey, MimeMessage, PreMessageMode, SystemMessage, parse_message_id,
38+
parse_message_ids,
3839
};
3940
use crate::param::{Param, Params};
4041
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub, iroh_topic_from_str};
@@ -1026,7 +1027,7 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
10261027
"
10271028
UPDATE msgs SET state=? WHERE
10281029
state=? AND
1029-
hidden=0 AND
1030+
(hidden=0 OR hidden=1) AND
10301031
chat_id=? AND
10311032
timestamp<?",
10321033
(
@@ -1038,7 +1039,18 @@ UPDATE msgs SET state=? WHERE
10381039
)
10391040
.await
10401041
.context("UPDATE msgs.state")?;
1041-
if chat_id.get_fresh_msg_cnt(context).await? == 0 {
1042+
let n_fresh_msgs = context
1043+
.sql
1044+
.count(
1045+
"
1046+
SELECT COUNT(*) FROM msgs WHERE
1047+
state=? AND
1048+
(hidden=0 OR hidden=1) AND
1049+
chat_id=?",
1050+
(MessageState::InFresh, chat_id),
1051+
)
1052+
.await?;
1053+
if n_fresh_msgs == 0 {
10421054
// Removes all notifications for the chat in UIs.
10431055
context.emit_event(EventType::MsgsNoticed(chat_id));
10441056
} else {
@@ -1999,6 +2011,10 @@ async fn add_parts(
19992011
let mime_in_reply_to = mime_parser
20002012
.get_header(HeaderDef::InReplyTo)
20012013
.unwrap_or_default();
2014+
let mime_in_reply_to = parse_message_id(&mime_in_reply_to)
2015+
.log_err(context)
2016+
.ok()
2017+
.unwrap_or_default();
20022018
let mime_references = mime_parser
20032019
.get_header(HeaderDef::References)
20042020
.unwrap_or_default();
@@ -2125,7 +2141,7 @@ async fn add_parts(
21252141
let is_incoming_fresh = mime_parser.incoming && !seen;
21262142
set_msg_reaction(
21272143
context,
2128-
mime_in_reply_to,
2144+
&mime_in_reply_to,
21292145
chat_id,
21302146
from_id,
21312147
sort_timestamp,
@@ -2267,7 +2283,7 @@ RETURNING id
22672283
} else {
22682284
Vec::new()
22692285
},
2270-
if trash { "" } else { mime_in_reply_to },
2286+
if trash { "" } else { &mime_in_reply_to },
22712287
if trash { "" } else { mime_references },
22722288
!trash && save_mime_modified,
22732289
if trash { "" } else { part.error.as_deref().unwrap_or_default() },

src/sql/migrations.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2323,6 +2323,15 @@ ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
23232323
.await?;
23242324
}
23252325

2326+
inc_and_check(&mut migration_version, 148)?;
2327+
if dbversion < migration_version {
2328+
sql.execute_migration(
2329+
"CREATE INDEX msgs_index10 ON msgs (mime_in_reply_to)",
2330+
migration_version,
2331+
)
2332+
.await?;
2333+
}
2334+
23262335
let new_version = sql
23272336
.get_raw_config_int(VERSION_CFG)
23282337
.await?

0 commit comments

Comments
 (0)