feat: get contact-id for info messages (#6714)

instead of showing addresses in info message, provide an API to get the
contact-id.

UI can then make the info message tappable and open the contact profile
in scope

the corresponding iOS PR - incl. **screencast** - is at
https://github.com/deltachat/deltachat-ios/pull/2652 ; jsonrpc can come
in a subsequent PR when things are settled on android/ios

the number of parameters in `add_info_msg_with_cmd` gets bigger and
bigger, however, i did not want to refactor this in this PR. it is also
not really adding complexity



closes #6702

---------

Co-authored-by: link2xt <link2xt@testrun.org>
Co-authored-by: Hocuri <hocuri@gmx.de>
This commit is contained in:
bjoern
2025-03-31 18:56:57 +02:00
committed by GitHub
parent e2f9c80cd5
commit 97b0d09ed2
18 changed files with 311 additions and 62 deletions

View File

@@ -4483,6 +4483,11 @@ int dc_msg_is_info (const dc_msg_t* msg);
* UIs can display e.g. an icon based upon the type. * UIs can display e.g. an icon based upon the type.
* *
* Currently, the following types are defined: * Currently, the following types are defined:
* - DC_INFO_GROUP_NAME_CHANGED (2) - "Group name changd from OLD to BY by CONTACT"
* - DC_INFO_GROUP_IMAGE_CHANGED (3) - "Group image changd by CONTACT"
* - DC_INFO_MEMBER_ADDED_TO_GROUP (4) - "Member CONTACT added by OTHER_CONTACT"
* - DC_INFO_MEMBER_REMOVED_FROM_GROUP (5) - "Member CONTACT removed by OTHER_CONTACT"
* - DC_INFO_EPHEMERAL_TIMER_CHANGED (10) - "Disappearing messages CHANGED_TO by CONTACT"
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected" * - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected" * - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet", * - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
@@ -4490,6 +4495,10 @@ int dc_msg_is_info (const dc_msg_t* msg);
* and also offer a way to fix the encryption, eg. by a button offering a QR scan * and also offer a way to fix the encryption, eg. by a button offering a QR scan
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info` * - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
* *
* For the messages that refer to a CONTACT,
* dc_msg_get_info_contact_id() returns the contact ID.
* The UI should open the contact's profile when tapping the info message.
*
* Even when you display an icon, * Even when you display an icon,
* you should still display the text of the informational message using dc_msg_get_text() * you should still display the text of the informational message using dc_msg_get_text()
* *
@@ -4502,6 +4511,29 @@ int dc_msg_is_info (const dc_msg_t* msg);
int dc_msg_get_info_type (const dc_msg_t* msg); int dc_msg_get_info_type (const dc_msg_t* msg);
/**
* Return the contact ID of the profile to open when tapping the info message.
*
* - For DC_INFO_MEMBER_ADDED_TO_GROUP and DC_INFO_MEMBER_REMOVED_FROM_GROUP,
* this is the contact being added/removed.
* The contact that did the adding/removal is usually only a tap away
* (as introducer and/or atop of the memberlist),
* and usually more known anyways.
* - For DC_INFO_GROUP_NAME_CHANGED, DC_INFO_GROUP_IMAGE_CHANGED and DC_INFO_EPHEMERAL_TIMER_CHANGED
* this is the contact who did the change.
*
* No need to check additionally for dc_msg_get_info_type(),
* unless you e.g. want to show the info message in another style.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return If the info message refers to a contact,
* this contact ID or DC_CONTACT_ID_SELF is returned.
* Otherwise 0.
*/
uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
// DC_INFO* uses the same values as SystemMessage in rust-land // DC_INFO* uses the same values as SystemMessage in rust-land
#define DC_INFO_UNKNOWN 0 #define DC_INFO_UNKNOWN 0
#define DC_INFO_GROUP_NAME_CHANGED 2 #define DC_INFO_GROUP_NAME_CHANGED 2

View File

@@ -3730,6 +3730,20 @@ pub unsafe extern "C" fn dc_msg_get_info_type(msg: *mut dc_msg_t) -> libc::c_int
ffi_msg.message.get_info_type() as libc::c_int ffi_msg.message.get_info_type() as libc::c_int
} }
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_info_contact_id(msg: *mut dc_msg_t) -> u32 {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_info_contact_id()");
return 0;
}
let ffi_msg = &*msg;
let context = &*ffi_msg.context;
block_on(ffi_msg.message.get_info_contact_id(context))
.unwrap_or_default()
.map(|id| id.to_u32())
.unwrap_or_default()
}
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn dc_msg_get_webxdc_href(msg: *mut dc_msg_t) -> *mut libc::c_char { pub unsafe extern "C" fn dc_msg_get_webxdc_href(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() { if msg.is_null() {

View File

@@ -89,7 +89,7 @@ def test_qr_securejoin(acfactory, protect):
assert alice_contact_bob_snapshot.is_verified assert alice_contact_bob_snapshot.is_verified
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot() snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr")) assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected == protect assert snapshot.chat.get_basic_snapshot().is_protected == protect
# Test that Bob verified Alice's profile. # Test that Bob verified Alice's profile.
@@ -563,7 +563,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
# ac1 waits for member added message and creates a QR code. # ac1 waits for member added message and creates a QR code.
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot() snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr")) assert snapshot.text == "Member Me added by {}.".format(ac3.get_config("addr"))
ac1_qr_code = snapshot.chat.get_qr_code() ac1_qr_code = snapshot.chat.get_qr_code()
# ac2 verifies ac1 # ac2 verifies ac1
@@ -646,7 +646,7 @@ def test_withdraw_securejoin_qr(acfactory):
alice.clear_all_events() alice.clear_all_events()
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot() snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr")) assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected assert snapshot.chat.get_basic_snapshot().is_protected
bob_chat.leave() bob_chat.leave()

View File

@@ -1346,7 +1346,7 @@ def test_qr_email_capitalization(acfactory, lp):
lp.sec("ac1 joins a verified group via a QR code") lp.sec("ac1 joins a verified group via a QR code")
ac1_chat = ac1.qr_join_chat(qr) ac1_chat = ac1.qr_join_chat(qr)
msg = ac1._evtracker.wait_next_incoming_message() msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr")) assert msg.text == "Member Me added by {}.".format(ac3.get_config("addr"))
assert len(ac1_chat.get_contacts()) == 2 assert len(ac1_chat.get_contacts()) == 2
lp.sec("ac2 joins a verified group via a QR code") lp.sec("ac2 joins a verified group via a QR code")

View File

@@ -582,7 +582,18 @@ impl ChatId {
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled, ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled, ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled,
}; };
add_info_msg_with_cmd(context, self, &text, cmd, timestamp_sort, None, None, None).await?; add_info_msg_with_cmd(
context,
self,
&text,
cmd,
timestamp_sort,
None,
None,
None,
None,
)
.await?;
Ok(()) Ok(())
} }
@@ -1791,6 +1802,7 @@ impl Chat {
Some(now), Some(now),
None, None,
None, None,
None,
) )
.await?; .await?;
context.emit_event(EventType::ChatModified(self.id)); context.emit_event(EventType::ChatModified(self.id));
@@ -3942,6 +3954,8 @@ pub(crate) async fn add_contact_to_chat_ex(
msg.param.set_cmd(SystemMessage::MemberAddedToGroup); msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
msg.param.set(Param::Arg, contact_addr); msg.param.set(Param::Arg, contact_addr);
msg.param.set_int(Param::Arg2, from_handshake.into()); msg.param.set_int(Param::Arg2, from_handshake.into());
msg.param
.set_int(Param::ContactAddedRemoved, contact.id.to_u32() as i32);
send_msg(context, chat_id, &mut msg).await?; send_msg(context, chat_id, &mut msg).await?;
sync = Nosync; sync = Nosync;
@@ -4139,6 +4153,8 @@ pub async fn remove_contact_from_chat(
} }
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup); msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
msg.param.set(Param::Arg, contact.get_addr().to_lowercase()); msg.param.set(Param::Arg, contact.get_addr().to_lowercase());
msg.param
.set(Param::ContactAddedRemoved, contact.id.to_u32() as i32);
let res = send_msg(context, chat_id, &mut msg).await; let res = send_msg(context, chat_id, &mut msg).await;
if contact_id == ContactId::SELF { if contact_id == ContactId::SELF {
res?; res?;
@@ -4737,13 +4753,17 @@ pub(crate) async fn add_info_msg_with_cmd(
timestamp_sent_rcvd: Option<i64>, timestamp_sent_rcvd: Option<i64>,
parent: Option<&Message>, parent: Option<&Message>,
from_id: Option<ContactId>, from_id: Option<ContactId>,
added_removed_id: Option<ContactId>,
) -> Result<MsgId> { ) -> Result<MsgId> {
let rfc724_mid = create_outgoing_rfc724_mid(); let rfc724_mid = create_outgoing_rfc724_mid();
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?; let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
let mut param = Params::new(); let mut param = Params::new();
if cmd != SystemMessage::Unknown { if cmd != SystemMessage::Unknown {
param.set_cmd(cmd) param.set_cmd(cmd);
}
if let Some(contact_id) = added_removed_id {
param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
} }
let row_id = let row_id =
@@ -4791,6 +4811,7 @@ pub(crate) async fn add_info_msg(
None, None,
None, None,
None, None,
None,
) )
.await .await
} }

View File

@@ -1,6 +1,7 @@
use super::*; use super::*;
use crate::chatlist::get_archived_cnt; use crate::chatlist::get_archived_cnt;
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS}; use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
use crate::ephemeral::Timer;
use crate::headerdef::HeaderDef; use crate::headerdef::HeaderDef;
use crate::imex::{has_backup, imex, ImexMode}; use crate::imex::{has_backup, imex, ImexMode};
use crate::message::{delete_msgs, MessengerMessage}; use crate::message::{delete_msgs, MessengerMessage};
@@ -331,11 +332,12 @@ async fn test_member_add_remove() -> Result<()> {
// Alice adds Bob to the chat. // Alice adds Bob to the chat.
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?; add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
let sent = alice.pop_sent_msg().await; let sent = alice.pop_sent_msg().await;
// Locally set name "robert" should not leak. // Locally set name "robert" should not leak.
assert!(!sent.payload.contains("robert")); assert!(!sent.payload.contains("robert"));
assert_eq!( assert_eq!(
sent.load_from_db().await.get_text(), sent.load_from_db().await.get_text(),
"You added member robert (bob@example.net)." "You added member robert."
); );
// Alice removes Bob from the chat. // Alice removes Bob from the chat.
@@ -344,7 +346,7 @@ async fn test_member_add_remove() -> Result<()> {
assert!(!sent.payload.contains("robert")); assert!(!sent.payload.contains("robert"));
assert_eq!( assert_eq!(
sent.load_from_db().await.get_text(), sent.load_from_db().await.get_text(),
"You removed member robert (bob@example.net)." "You removed member robert."
); );
// Alice leaves the chat. // Alice leaves the chat.
@@ -412,7 +414,7 @@ async fn test_parallel_member_remove() -> Result<()> {
// Test that remove message is rewritten. // Test that remove message is rewritten.
assert_eq!( assert_eq!(
bob_received_remove_msg.get_text(), bob_received_remove_msg.get_text(),
"Member Me (bob@example.net) removed by alice@example.org." "Member Me removed by alice@example.org."
); );
Ok(()) Ok(())
@@ -521,6 +523,14 @@ async fn test_modify_chat_multi_device() -> Result<()> {
assert!(a2_msg.is_system_message()); assert!(a2_msg.is_system_message());
assert_eq!(a1_msg.get_info_type(), SystemMessage::GroupNameChanged); assert_eq!(a1_msg.get_info_type(), SystemMessage::GroupNameChanged);
assert_eq!(a2_msg.get_info_type(), SystemMessage::GroupNameChanged); assert_eq!(a2_msg.get_info_type(), SystemMessage::GroupNameChanged);
assert_eq!(
a1_msg.get_info_contact_id(&a1).await?,
Some(ContactId::SELF)
);
assert_eq!(
a2_msg.get_info_contact_id(&a2).await?,
Some(ContactId::SELF)
);
assert_eq!(Chat::load_from_db(&a1, a1_chat_id).await?.name, "bar"); assert_eq!(Chat::load_from_db(&a1, a1_chat_id).await?.name, "bar");
assert_eq!(Chat::load_from_db(&a2, a2_chat_id).await?.name, "bar"); assert_eq!(Chat::load_from_db(&a2, a2_chat_id).await?.name, "bar");
@@ -1574,6 +1584,7 @@ async fn test_add_info_msg_with_cmd() -> Result<()> {
None, None,
None, None,
None, None,
None,
) )
.await?; .await?;
@@ -3332,6 +3343,128 @@ async fn test_do_not_overwrite_draft() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_info_contact_id() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice2 = &tcm.alice().await;
let bob = &tcm.bob().await;
async fn pop_recv_and_check(
alice: &TestContext,
alice2: &TestContext,
bob: &TestContext,
expected_type: SystemMessage,
expected_alice_id: ContactId,
expected_bob_id: ContactId,
) -> Result<()> {
let sent_msg = alice.pop_sent_msg().await;
let msg = Message::load_from_db(alice, sent_msg.sender_msg_id).await?;
assert_eq!(msg.get_info_type(), expected_type);
assert_eq!(
msg.get_info_contact_id(alice).await?,
Some(expected_alice_id)
);
let msg = alice2.recv_msg(&sent_msg).await;
assert_eq!(msg.get_info_type(), expected_type);
assert_eq!(
msg.get_info_contact_id(alice2).await?,
Some(expected_alice_id)
);
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.get_info_type(), expected_type);
assert_eq!(msg.get_info_contact_id(bob).await?, Some(expected_bob_id));
Ok(())
}
// Alice creates group, Bob receives group
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "play", &[bob])
.await;
let sent_msg1 = alice.send_text(alice_chat_id, "moin").await;
let msg = bob.recv_msg(&sent_msg1).await;
let bob_alice_id = msg.from_id;
assert!(!bob_alice_id.is_special());
// Alice does group changes, Bob receives them
set_chat_name(alice, alice_chat_id, "games").await?;
pop_recv_and_check(
alice,
alice2,
bob,
SystemMessage::GroupNameChanged,
ContactId::SELF,
bob_alice_id,
)
.await?;
let file = alice.get_blobdir().join("avatar.png");
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await?;
set_chat_profile_image(alice, alice_chat_id, file.to_str().unwrap()).await?;
pop_recv_and_check(
alice,
alice2,
bob,
SystemMessage::GroupImageChanged,
ContactId::SELF,
bob_alice_id,
)
.await?;
alice_chat_id
.set_ephemeral_timer(alice, Timer::Enabled { duration: 60 })
.await?;
pop_recv_and_check(
alice,
alice2,
bob,
SystemMessage::EphemeralTimerChanged,
ContactId::SELF,
bob_alice_id,
)
.await?;
let fiona_id = alice.add_or_lookup_contact_id(&tcm.fiona().await).await; // contexts are in sync, fiona_id is same everywhere
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
pop_recv_and_check(
alice,
alice2,
bob,
SystemMessage::MemberAddedToGroup,
fiona_id,
fiona_id,
)
.await?;
remove_contact_from_chat(alice, alice_chat_id, fiona_id).await?;
pop_recv_and_check(
alice,
alice2,
bob,
SystemMessage::MemberRemovedFromGroup,
fiona_id,
fiona_id,
)
.await?;
// When fiona_id is deleted, get_info_contact_id() returns None.
// We raw delete in db as Contact::delete() leaves a tombstone (which is great as the tap works longer then)
alice
.sql
.execute("DELETE FROM contacts WHERE id=?", (fiona_id,))
.await?;
let msg = alice.get_last_msg().await;
assert_eq!(msg.get_info_type(), SystemMessage::MemberRemovedFromGroup);
assert!(msg.get_info_contact_id(alice).await?.is_none());
Ok(())
}
/// Test group consistency. /// Test group consistency.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_member_bug() -> Result<()> { async fn test_add_member_bug() -> Result<()> {

View File

@@ -1406,16 +1406,13 @@ impl Contact {
&self.addr &self.addr
} }
/// Get a summary of authorized name and address. /// Get authorized name or address.
///
/// The returned string is either "Name (email@domain.com)" or just
/// "email@domain.com" if the name is unset.
/// ///
/// This string is suitable for sending over email /// This string is suitable for sending over email
/// as it does not leak the locally set name. /// as it does not leak the locally set name.
pub fn get_authname_n_addr(&self) -> String { pub(crate) fn get_authname_or_addr(&self) -> String {
if !self.authname.is_empty() { if !self.authname.is_empty() {
format!("{} ({})", self.authname, self.addr) (&self.authname).into()
} else { } else {
(&self.addr).into() (&self.addr).into()
} }

View File

@@ -936,6 +936,51 @@ impl Message {
self.param.get_cmd() self.param.get_cmd()
} }
/// Return the contact ID of the profile to open when tapping the info message.
pub async fn get_info_contact_id(&self, context: &Context) -> Result<Option<ContactId>> {
match self.param.get_cmd() {
SystemMessage::GroupNameChanged
| SystemMessage::GroupImageChanged
| SystemMessage::EphemeralTimerChanged => {
if self.from_id != ContactId::INFO {
Ok(Some(self.from_id))
} else {
Ok(None)
}
}
SystemMessage::MemberAddedToGroup | SystemMessage::MemberRemovedFromGroup => {
if let Some(contact_i32) = self.param.get_int(Param::ContactAddedRemoved) {
let contact_id = ContactId::new(contact_i32.try_into()?);
if contact_id == ContactId::SELF
|| Contact::real_exists_by_id(context, contact_id).await?
{
Ok(Some(contact_id))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
SystemMessage::AutocryptSetupMessage
| SystemMessage::SecurejoinMessage
| SystemMessage::LocationStreamingEnabled
| SystemMessage::LocationOnly
| SystemMessage::ChatProtectionEnabled
| SystemMessage::ChatProtectionDisabled
| SystemMessage::InvalidUnencryptedMail
| SystemMessage::SecurejoinWait
| SystemMessage::SecurejoinWaitTimeout
| SystemMessage::MultiDeviceSync
| SystemMessage::WebxdcStatusUpdate
| SystemMessage::WebxdcInfoMessage
| SystemMessage::IrohNodeAddr
| SystemMessage::Unknown => Ok(None),
}
}
/// Returns true if the message is a system message. /// Returns true if the message is a system message.
pub fn is_system_message(&self) -> bool { pub fn is_system_message(&self) -> bool {
let cmd = self.param.get_cmd(); let cmd = self.param.get_cmd();

View File

@@ -215,6 +215,9 @@ pub enum Param {
/// For messages: Message text was edited. /// For messages: Message text was edited.
IsEdited = b'L', IsEdited = b'L',
/// For info messages: Contact ID in added or removed to a group.
ContactAddedRemoved = b'5',
} }
/// An object for handling key=value parameter lists. /// An object for handling key=value parameter lists.

View File

@@ -748,6 +748,7 @@ impl Peerstate {
Some(timestamp), Some(timestamp),
None, None,
None, None,
None,
) )
.await?; .await?;
} }

View File

@@ -1451,13 +1451,13 @@ async fn add_parts(
None => better_msg = Some(m), None => better_msg = Some(m),
Some(_) => { Some(_) => {
if !m.is_empty() { if !m.is_empty() {
group_changes.extra_msgs.push((m, is_system_message)) group_changes.extra_msgs.push((m, is_system_message, None))
} }
} }
} }
} }
for (group_changes_msg, cmd) in group_changes.extra_msgs { for (group_changes_msg, cmd, added_removed_id) in group_changes.extra_msgs {
chat::add_info_msg_with_cmd( chat::add_info_msg_with_cmd(
context, context,
chat_id, chat_id,
@@ -1467,6 +1467,7 @@ async fn add_parts(
None, None,
None, None,
None, None,
added_removed_id,
) )
.await?; .await?;
} }
@@ -1550,6 +1551,10 @@ async fn add_parts(
let part_is_empty = let part_is_empty =
typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none(); typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
if let Some(contact_id) = group_changes.added_removed_id {
param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
}
save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden; save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden;
let save_mime_modified = save_mime_modified && parts.peek().is_none(); let save_mime_modified = save_mime_modified && parts.peek().is_none();
@@ -2334,10 +2339,12 @@ struct GroupChangesInfo {
/// Optional: A better message that should replace the original system message. /// Optional: A better message that should replace the original system message.
/// If this is an empty string, the original system message should be trashed. /// If this is an empty string, the original system message should be trashed.
better_msg: Option<String>, better_msg: Option<String>,
/// Added/removed contact `better_msg` refers to.
added_removed_id: Option<ContactId>,
/// If true, the user should not be notified about the group change. /// If true, the user should not be notified about the group change.
silent: bool, silent: bool,
/// A list of additional group changes messages that should be shown in the chat. /// A list of additional group changes messages that should be shown in the chat.
extra_msgs: Vec<(String, SystemMessage)>, extra_msgs: Vec<(String, SystemMessage, Option<ContactId>)>,
} }
/// Apply group member list, name, avatar and protection status changes from the MIME message. /// Apply group member list, name, avatar and protection status changes from the MIME message.
@@ -2654,6 +2661,11 @@ async fn apply_group_changes(
} }
Ok(GroupChangesInfo { Ok(GroupChangesInfo {
better_msg, better_msg,
added_removed_id: if added_id.is_some() {
added_id
} else {
removed_id
},
silent, silent,
extra_msgs: group_changes_msgs, extra_msgs: group_changes_msgs,
}) })
@@ -2665,7 +2677,7 @@ async fn group_changes_msgs(
added_ids: &HashSet<ContactId>, added_ids: &HashSet<ContactId>,
removed_ids: &HashSet<ContactId>, removed_ids: &HashSet<ContactId>,
chat_id: ChatId, chat_id: ChatId,
) -> Result<Vec<(String, SystemMessage)>> { ) -> Result<Vec<(String, SystemMessage, Option<ContactId>)>> {
let mut group_changes_msgs = Vec::new(); let mut group_changes_msgs = Vec::new();
if !added_ids.is_empty() { if !added_ids.is_empty() {
warn!( warn!(
@@ -2686,6 +2698,7 @@ async fn group_changes_msgs(
stock_str::msg_add_member_local(context, contact.get_addr(), ContactId::UNDEFINED) stock_str::msg_add_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
.await, .await,
SystemMessage::MemberAddedToGroup, SystemMessage::MemberAddedToGroup,
Some(contact.id),
)); ));
} }
for contact_id in removed_ids { for contact_id in removed_ids {
@@ -2694,6 +2707,7 @@ async fn group_changes_msgs(
stock_str::msg_del_member_local(context, contact.get_addr(), ContactId::UNDEFINED) stock_str::msg_del_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
.await, .await,
SystemMessage::MemberRemovedFromGroup, SystemMessage::MemberRemovedFromGroup,
Some(contact.id),
)); ));
} }

View File

@@ -126,6 +126,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
Some(ts_start), Some(ts_start),
None, None,
None, None,
None,
) )
.await?; .await?;
chat_id.spawn_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT); chat_id.spawn_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT);

View File

@@ -439,6 +439,7 @@ pub(crate) async fn send_msg_to_smtp(
None, None,
None, None,
None, None,
None,
) )
.await?; .await?;
}; };

View File

@@ -537,14 +537,6 @@ trait StockStringMods: AsRef<str> + Sized {
} }
impl ContactId { impl ContactId {
/// Get contact name and address for stock string, e.g. `Bob (bob@example.net)`
async fn get_stock_name_n_addr(self, context: &Context) -> String {
Contact::get_by_id(context, self)
.await
.map(|contact| contact.get_name_n_addr())
.unwrap_or_else(|_| self.to_string())
}
/// Get contact name, e.g. `Bob`, or `bob@example.net` if no name is set. /// Get contact name, e.g. `Bob`, or `bob@example.net` if no name is set.
async fn get_stock_name(self, context: &Context) -> String { async fn get_stock_name(self, context: &Context) -> String {
Contact::get_by_id(context, self) Contact::get_by_id(context, self)
@@ -613,7 +605,7 @@ pub(crate) async fn msg_grp_name(
.await .await
.replace1(from_group) .replace1(from_group)
.replace2(to_group) .replace2(to_group)
.replace3(&by_contact.get_stock_name_n_addr(context).await) .replace3(&by_contact.get_stock_name(context).await)
} }
} }
@@ -623,7 +615,7 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
} else { } else {
translated(context, StockMessage::MsgGrpImgChangedBy) translated(context, StockMessage::MsgGrpImgChangedBy)
.await .await
.replace1(&by_contact.get_stock_name_n_addr(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
@@ -637,7 +629,7 @@ pub(crate) async fn msg_add_member_remote(context: &Context, added_member_addr:
let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
.await .await
.map(|contact| contact.get_authname_n_addr()) .map(|contact| contact.get_authname_or_addr())
.unwrap_or_else(|_| addr.to_string()), .unwrap_or_else(|_| addr.to_string()),
_ => addr.to_string(), _ => addr.to_string(),
}; };
@@ -659,7 +651,7 @@ pub(crate) async fn msg_add_member_local(
let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
.await .await
.map(|contact| contact.get_name_n_addr()) .map(|contact| contact.get_display_name().to_string())
.unwrap_or_else(|_| addr.to_string()), .unwrap_or_else(|_| addr.to_string()),
_ => addr.to_string(), _ => addr.to_string(),
}; };
@@ -675,7 +667,7 @@ pub(crate) async fn msg_add_member_local(
translated(context, StockMessage::MsgAddMemberBy) translated(context, StockMessage::MsgAddMemberBy)
.await .await
.replace1(whom) .replace1(whom)
.replace2(&by_contact.get_stock_name_n_addr(context).await) .replace2(&by_contact.get_stock_name(context).await)
} }
} }
@@ -688,7 +680,7 @@ pub(crate) async fn msg_del_member_remote(context: &Context, removed_member_addr
let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
.await .await
.map(|contact| contact.get_authname_n_addr()) .map(|contact| contact.get_authname_or_addr())
.unwrap_or_else(|_| addr.to_string()), .unwrap_or_else(|_| addr.to_string()),
_ => addr.to_string(), _ => addr.to_string(),
}; };
@@ -710,7 +702,7 @@ pub(crate) async fn msg_del_member_local(
let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
.await .await
.map(|contact| contact.get_name_n_addr()) .map(|contact| contact.get_display_name().to_string())
.unwrap_or_else(|_| addr.to_string()), .unwrap_or_else(|_| addr.to_string()),
_ => addr.to_string(), _ => addr.to_string(),
}; };
@@ -726,7 +718,7 @@ pub(crate) async fn msg_del_member_local(
translated(context, StockMessage::MsgDelMemberBy) translated(context, StockMessage::MsgDelMemberBy)
.await .await
.replace1(whom) .replace1(whom)
.replace2(&by_contact.get_stock_name_n_addr(context).await) .replace2(&by_contact.get_stock_name(context).await)
} }
} }
@@ -742,7 +734,7 @@ pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactI
} else { } else {
translated(context, StockMessage::MsgGroupLeftBy) translated(context, StockMessage::MsgGroupLeftBy)
.await .await
.replace1(&by_contact.get_stock_name_n_addr(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
@@ -804,7 +796,7 @@ pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId
} else { } else {
translated(context, StockMessage::MsgGrpImgDeletedBy) translated(context, StockMessage::MsgGrpImgDeletedBy)
.await .await
.replace1(&by_contact.get_stock_name_n_addr(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
@@ -821,7 +813,7 @@ pub(crate) async fn secure_join_started(
if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await { if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
translated(context, StockMessage::SecureJoinStarted) translated(context, StockMessage::SecureJoinStarted)
.await .await
.replace1(&contact.get_name_n_addr()) .replace1(contact.get_display_name())
.replace2(contact.get_display_name()) .replace2(contact.get_display_name())
} else { } else {
format!("secure_join_started: unknown contact {inviter_contact_id}") format!("secure_join_started: unknown contact {inviter_contact_id}")
@@ -871,7 +863,7 @@ pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &C
/// Stock string: `%1$s verified.`. /// Stock string: `%1$s verified.`.
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String { pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
let addr = &contact.get_name_n_addr(); let addr = contact.get_display_name();
translated(context, StockMessage::ContactVerified) translated(context, StockMessage::ContactVerified)
.await .await
.replace1(addr) .replace1(addr)
@@ -879,7 +871,7 @@ pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> St
/// Stock string: `Cannot establish guaranteed end-to-end encryption with %1$s`. /// Stock string: `Cannot establish guaranteed end-to-end encryption with %1$s`.
pub(crate) async fn contact_not_verified(context: &Context, contact: &Contact) -> String { pub(crate) async fn contact_not_verified(context: &Context, contact: &Contact) -> String {
let addr = &contact.get_name_n_addr(); let addr = contact.get_display_name();
translated(context, StockMessage::ContactNotVerified) translated(context, StockMessage::ContactNotVerified)
.await .await
.replace1(addr) .replace1(addr)
@@ -936,7 +928,7 @@ pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactI
} else { } else {
translated(context, StockMessage::MsgLocationEnabledBy) translated(context, StockMessage::MsgLocationEnabledBy)
.await .await
.replace1(&contact.get_stock_name_n_addr(context).await) .replace1(&contact.get_stock_name(context).await)
} }
} }
@@ -998,7 +990,7 @@ pub(crate) async fn msg_ephemeral_timer_disabled(
} else { } else {
translated(context, StockMessage::MsgEphemeralTimerDisabledBy) translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
.await .await
.replace1(&by_contact.get_stock_name_n_addr(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
@@ -1016,7 +1008,7 @@ pub(crate) async fn msg_ephemeral_timer_enabled(
translated(context, StockMessage::MsgEphemeralTimerEnabledBy) translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
.await .await
.replace1(timer) .replace1(timer)
.replace2(&by_contact.get_stock_name_n_addr(context).await) .replace2(&by_contact.get_stock_name(context).await)
} }
} }
@@ -1027,7 +1019,7 @@ pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: Co
} else { } else {
translated(context, StockMessage::MsgEphemeralTimerMinuteBy) translated(context, StockMessage::MsgEphemeralTimerMinuteBy)
.await .await
.replace1(&by_contact.get_stock_name_n_addr(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
@@ -1038,7 +1030,7 @@ pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: Cont
} else { } else {
translated(context, StockMessage::MsgEphemeralTimerHourBy) translated(context, StockMessage::MsgEphemeralTimerHourBy)
.await .await
.replace1(&by_contact.get_stock_name_n_addr(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
@@ -1049,7 +1041,7 @@ pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: Conta
} else { } else {
translated(context, StockMessage::MsgEphemeralTimerDayBy) translated(context, StockMessage::MsgEphemeralTimerDayBy)
.await .await
.replace1(&by_contact.get_stock_name_n_addr(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
@@ -1060,7 +1052,7 @@ pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: Cont
} else { } else {
translated(context, StockMessage::MsgEphemeralTimerWeekBy) translated(context, StockMessage::MsgEphemeralTimerWeekBy)
.await .await
.replace1(&by_contact.get_stock_name_n_addr(context).await) .replace1(&by_contact.get_stock_name(context).await)
} }
} }
@@ -1142,7 +1134,7 @@ pub(crate) async fn msg_ephemeral_timer_minutes(
translated(context, StockMessage::MsgEphemeralTimerMinutesBy) translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
.await .await
.replace1(minutes) .replace1(minutes)
.replace2(&by_contact.get_stock_name_n_addr(context).await) .replace2(&by_contact.get_stock_name(context).await)
} }
} }
@@ -1160,7 +1152,7 @@ pub(crate) async fn msg_ephemeral_timer_hours(
translated(context, StockMessage::MsgEphemeralTimerHoursBy) translated(context, StockMessage::MsgEphemeralTimerHoursBy)
.await .await
.replace1(hours) .replace1(hours)
.replace2(&by_contact.get_stock_name_n_addr(context).await) .replace2(&by_contact.get_stock_name(context).await)
} }
} }
@@ -1178,7 +1170,7 @@ pub(crate) async fn msg_ephemeral_timer_days(
translated(context, StockMessage::MsgEphemeralTimerDaysBy) translated(context, StockMessage::MsgEphemeralTimerDaysBy)
.await .await
.replace1(days) .replace1(days)
.replace2(&by_contact.get_stock_name_n_addr(context).await) .replace2(&by_contact.get_stock_name(context).await)
} }
} }
@@ -1196,7 +1188,7 @@ pub(crate) async fn msg_ephemeral_timer_weeks(
translated(context, StockMessage::MsgEphemeralTimerWeeksBy) translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
.await .await
.replace1(weeks) .replace1(weeks)
.replace2(&by_contact.get_stock_name_n_addr(context).await) .replace2(&by_contact.get_stock_name(context).await)
} }
} }

View File

@@ -54,10 +54,7 @@ async fn test_stock_string_repl_str() {
.unwrap(); .unwrap();
let contact = Contact::get_by_id(&t.ctx, contact_id).await.unwrap(); let contact = Contact::get_by_id(&t.ctx, contact_id).await.unwrap();
// uses %1$s substitution // uses %1$s substitution
assert_eq!( assert_eq!(contact_verified(&t, &contact).await, "Someone verified.");
contact_verified(&t, &contact).await,
"Someone (someone@example.org) verified."
);
// We have no string using %1$d to test... // We have no string using %1$d to test...
} }
@@ -95,7 +92,7 @@ async fn test_stock_system_msg_add_member_by_me_with_displayname() {
); );
assert_eq!( assert_eq!(
msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await, msg_add_member_local(&t, "alice@example.org", ContactId::SELF).await,
"You added member Alice (alice@example.org)." "You added member Alice."
); );
} }
@@ -112,7 +109,7 @@ async fn test_stock_system_msg_add_member_by_other_with_displayname() {
}; };
assert_eq!( assert_eq!(
msg_add_member_local(&t, "alice@example.org", contact_id,).await, msg_add_member_local(&t, "alice@example.org", contact_id,).await,
"Member Alice (alice@example.org) added by Bob (bob@example.com)." "Member Alice added by Bob."
); );
} }

View File

@@ -743,10 +743,7 @@ mod tests {
let fiona = &tcm.fiona().await; let fiona = &tcm.fiona().await;
tcm.exec_securejoin_qr(fiona, alice2, &qr).await; tcm.exec_securejoin_qr(fiona, alice2, &qr).await;
let msg = fiona.get_last_msg().await; let msg = fiona.get_last_msg().await;
assert_eq!( assert_eq!(msg.text, "Member Me added by alice@example.org.");
msg.text,
"Member Me (fiona@example.net) added by alice@example.org."
);
Ok(()) Ok(())
} }
} }

View File

@@ -388,6 +388,7 @@ impl Context {
None, None,
Some(&instance), Some(&instance),
Some(from_id), Some(from_id),
None,
) )
.await?; .await?;
} }

View File

@@ -5,5 +5,5 @@ Msg#11: info (Contact#Contact#Info): alice@example.org invited you to join this
Waiting for the device of alice@example.org to reply… [NOTICED][INFO] Waiting for the device of alice@example.org to reply… [NOTICED][INFO]
Msg#13: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO] Msg#13: info (Contact#Contact#Info): alice@example.org replied, waiting for being added to the group… [NOTICED][INFO]
Msg#17: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️] Msg#17: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#18🔒: (Contact#Contact#10): Member Me (fiona@example.net) added by alice@example.org. [FRESH][INFO] Msg#18🔒: (Contact#Contact#10): Member Me added by alice@example.org. [FRESH][INFO]
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------