Skip to content

Commit adad045

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 822a99e commit adad045

7 files changed

Lines changed: 148 additions & 66 deletions

File tree

deltachat-ffi/deltachat.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2104,9 +2104,12 @@ int dc_resend_msgs (dc_context_t* context, const uint3
21042104

21052105

21062106
/**
2107-
* Mark messages as presented to the user.
2107+
* Mark messages and reactions to them as presented to the user.
21082108
* Typically, UIs call this function on scrolling through the message list,
21092109
* when the messages are presented at least for a little moment.
2110+
* UIs should pass all messages to this function, incl. outgoing and info ones, as this is used also
2111+
* for synchronization and to track last position.
2112+
* This should also be called when a reaction for a message being in view arrives.
21102113
* The concrete action depends on the type of the chat and on the users settings
21112114
* (dc_msgs_presented() may be a better name therefore, but well. :)
21122115
*

deltachat-jsonrpc/src/api.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,6 +1301,9 @@ impl CommandApi {
13011301
/// Mark messages as presented to the user.
13021302
/// Typically, UIs call this function on scrolling through the message list,
13031303
/// when the messages are presented at least for a little moment.
1304+
/// UIs should pass all messages to this function, incl. outgoing and info ones, as this is used
1305+
/// also for synchronization and to track last position.
1306+
/// This should also be called when a reaction for a message being in view arrives.
13041307
/// The concrete action depends on the type of the chat and on the users settings
13051308
/// (dc_msgs_presented() may be a better name therefore, but well. :)
13061309
///

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: 64 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1829,7 +1829,10 @@ 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.
1833+
/// NB: UIs should pass all messages to this function, incl. outgoing and info ones, as this is used
1834+
/// also for synchronization and to track last position. This should be called when a message comes
1835+
/// into view or when a reaction for a message being in view arrives.
18331836
pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()> {
18341837
if msg_ids.is_empty() {
18351838
return Ok(());
@@ -1843,10 +1846,18 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
18431846

18441847
let mut msgs = Vec::with_capacity(msg_ids.len());
18451848
for &id in &msg_ids {
1846-
if let Some(msg) = context
1849+
let Some(rfc724_mid): Option<String> = context
18471850
.sql
1848-
.query_row_optional(
1851+
.query_get_value("SELECT rfc724_mid FROM msgs WHERE id=?", (id,))
1852+
.await?
1853+
else {
1854+
continue;
1855+
};
1856+
context
1857+
.sql
1858+
.query_map(
18491859
"SELECT
1860+
m.id AS id,
18501861
m.chat_id AS chat_id,
18511862
m.state AS state,
18521863
m.ephemeral_timer AS ephemeral_timer,
@@ -1857,9 +1868,11 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
18571868
c.archived AS archived,
18581869
c.blocked AS blocked
18591870
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
1860-
WHERE m.id=? AND m.chat_id>9",
1861-
(id,),
1871+
WHERE (m.id=? OR m.mime_in_reply_to=? AND m.hidden=1)
1872+
AND m.chat_id>9 AND ?<=m.state AND m.state<?",
1873+
(id, rfc724_mid, MessageState::InFresh, MessageState::InSeen),
18621874
|row| {
1875+
let id: MsgId = row.get("id")?;
18631876
let chat_id: ChatId = row.get("chat_id")?;
18641877
let state: MessageState = row.get("state")?;
18651878
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
@@ -1884,11 +1897,14 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
18841897
ephemeral_timer,
18851898
))
18861899
},
1900+
|rows| {
1901+
for row in rows {
1902+
msgs.push(row?);
1903+
}
1904+
Ok(())
1905+
},
18871906
)
1888-
.await?
1889-
{
1890-
msgs.push(msg);
1891-
}
1907+
.await?;
18921908
}
18931909

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

src/reaction.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,4 +1108,37 @@ Content-Transfer-Encoding: 7bit\r
11081108

11091109
Ok(())
11101110
}
1111+
1112+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1113+
async fn test_markseen_referenced_msg() -> Result<()> {
1114+
let mut tcm = TestContextManager::new();
1115+
let alice = &tcm.alice().await;
1116+
let bob = &tcm.bob().await;
1117+
let chat_id = alice.create_chat(bob).await.id;
1118+
1119+
let alice_msg_id = send_text_msg(alice, chat_id, "foo".to_string()).await?;
1120+
let sent_msg = alice.pop_sent_msg().await;
1121+
let bob_msg = bob.recv_msg(&sent_msg).await;
1122+
bob_msg.chat_id.accept(bob).await?;
1123+
1124+
send_reaction(bob, bob_msg.id, "👀").await?;
1125+
let sent_msg = bob.pop_sent_msg().await;
1126+
let alice_reaction = alice.recv_msg_hidden(&sent_msg).await;
1127+
assert_eq!(alice_reaction.state, MessageState::InFresh);
1128+
1129+
markseen_msgs(alice, vec![alice_msg_id]).await?;
1130+
let alice_reaction = Message::load_from_db(alice, alice_reaction.id).await?;
1131+
assert_eq!(alice_reaction.state, MessageState::InSeen);
1132+
assert_eq!(
1133+
alice
1134+
.sql
1135+
.count(
1136+
"SELECT COUNT(*) FROM smtp_mdns WHERE from_id=?",
1137+
(ContactId::SELF,)
1138+
)
1139+
.await?,
1140+
1
1141+
);
1142+
Ok(())
1143+
}
11111144
}

src/receive_imf.rs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ use crate::message::{
3535
rfc724_mid_exists,
3636
};
3737
use crate::mimeparser::{
38-
AvatarAction, GossipedKey, MimeMessage, PreMessageMode, SystemMessage, parse_message_ids,
38+
AvatarAction, GossipedKey, MimeMessage, PreMessageMode, SystemMessage, parse_message_id,
39+
parse_message_ids,
3940
};
4041
use crate::param::{Param, Params};
4142
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub, iroh_topic_from_str};
@@ -1023,7 +1024,7 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
10231024
"
10241025
UPDATE msgs SET state=? WHERE
10251026
state=? AND
1026-
hidden=0 AND
1027+
(hidden=0 OR hidden=1) AND
10271028
chat_id=? AND
10281029
timestamp<?",
10291030
(
@@ -1035,7 +1036,18 @@ UPDATE msgs SET state=? WHERE
10351036
)
10361037
.await
10371038
.context("UPDATE msgs.state")?;
1038-
if chat_id.get_fresh_msg_cnt(context).await? == 0 {
1039+
let n_fresh_msgs = context
1040+
.sql
1041+
.count(
1042+
"
1043+
SELECT COUNT(*) FROM msgs WHERE
1044+
state=? AND
1045+
(hidden=0 OR hidden=1) AND
1046+
chat_id=?",
1047+
(MessageState::InFresh, chat_id),
1048+
)
1049+
.await?;
1050+
if n_fresh_msgs == 0 {
10391051
// Removes all notifications for the chat in UIs.
10401052
context.emit_event(EventType::MsgsNoticed(chat_id));
10411053
} else {
@@ -1993,9 +2005,13 @@ async fn add_parts(
19932005
)
19942006
.await?;
19952007

1996-
let mime_in_reply_to = mime_parser
1997-
.get_header(HeaderDef::InReplyTo)
1998-
.unwrap_or_default();
2008+
let mime_in_reply_to = match mime_parser.get_header(HeaderDef::InReplyTo) {
2009+
Some(in_reply_to) => parse_message_id(in_reply_to)
2010+
.log_err(context)
2011+
.ok()
2012+
.unwrap_or_default(),
2013+
None => "".to_string(),
2014+
};
19992015
let mime_references = mime_parser
20002016
.get_header(HeaderDef::References)
20012017
.unwrap_or_default();
@@ -2122,7 +2138,7 @@ async fn add_parts(
21222138
let is_incoming_fresh = mime_parser.incoming && !seen;
21232139
set_msg_reaction(
21242140
context,
2125-
mime_in_reply_to,
2141+
&mime_in_reply_to,
21262142
chat_id,
21272143
from_id,
21282144
sort_timestamp,
@@ -2264,7 +2280,7 @@ RETURNING id
22642280
} else {
22652281
Vec::new()
22662282
},
2267-
if trash { "" } else { mime_in_reply_to },
2283+
if trash { "" } else { &mime_in_reply_to },
22682284
if trash { "" } else { mime_references },
22692285
!trash && save_mime_modified,
22702286
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
@@ -2343,6 +2343,15 @@ ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
23432343
.await?;
23442344
}
23452345

2346+
inc_and_check(&mut migration_version, 149)?;
2347+
if dbversion < migration_version {
2348+
sql.execute_migration(
2349+
"CREATE INDEX msgs_index10 ON msgs (mime_in_reply_to)",
2350+
migration_version,
2351+
)
2352+
.await?;
2353+
}
2354+
23462355
let new_version = sql
23472356
.get_raw_config_int(VERSION_CFG)
23482357
.await?

0 commit comments

Comments
 (0)