mirror of
https://github.com/chatmail/core.git
synced 2026-04-27 18:36:30 +03:00
feat: Make it possible to leave broadcast channels (#6984)
Part of #6884.
The channel owner will not be notified in any way that you left, they
will only see that there is one member less.
For the translated stock strings, this is what we agreed on in the
group:
- Add a new "Leave Channel" stock string (will need to be done in UIs)
- Reword the existing "Are you sure you want to leave this group?"
string to "Are you sure you want to leave?" (the options are "Cancel"
and "Leave Group" / "Leave Channel", so it's clear what you are leaving)
(will need to be done in the deltachat-android repo, other UIs will pick
it up automatically)
- Reword the existing "You left the group." string to "You left". (done
here, I will adapt the strings in deltachat-android, too)
I adapted DC Android by pushing
6df2740884
to https://github.com/deltachat/deltachat-android/pull/3783.
---------
Co-authored-by: l <link2xt@testrun.org>
This commit is contained in:
52
src/chat.rs
52
src/chat.rs
@@ -2905,10 +2905,12 @@ async fn prepare_send_msg(
|
||||
// If the chat is a contact request, let the user accept it later.
|
||||
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||
}
|
||||
// Allow to send "Member removed" messages so we can leave the group.
|
||||
// Allow to send "Member removed" messages so we can leave the group/broadcast.
|
||||
// Necessary checks should be made anyway before removing contact
|
||||
// from the chat.
|
||||
CantSendReason::NotAMember => msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup,
|
||||
CantSendReason::NotAMember | CantSendReason::InBroadcast => {
|
||||
msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup
|
||||
}
|
||||
CantSendReason::MissingKey => msg
|
||||
.param
|
||||
.get_bool(Param::ForcePlaintext)
|
||||
@@ -4079,8 +4081,6 @@ pub async fn remove_contact_from_chat(
|
||||
"Cannot remove special contact"
|
||||
);
|
||||
|
||||
let mut msg = Message::new(Viewtype::default());
|
||||
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast {
|
||||
if !chat.is_self_in_chat(context).await? {
|
||||
@@ -4110,19 +4110,10 @@ pub async fn remove_contact_from_chat(
|
||||
// in case of the database becoming inconsistent due to a bug.
|
||||
if let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? {
|
||||
if chat.typ == Chattype::Group && chat.is_promoted() {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
if contact_id == ContactId::SELF {
|
||||
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
|
||||
} else {
|
||||
msg.text =
|
||||
stock_str::msg_del_member_local(context, contact_id, ContactId::SELF)
|
||||
.await;
|
||||
}
|
||||
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
|
||||
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 addr = contact.get_addr();
|
||||
|
||||
let res = send_member_removal_msg(context, chat_id, contact_id, addr).await;
|
||||
|
||||
if contact_id == ContactId::SELF {
|
||||
res?;
|
||||
set_group_explicitly_left(context, &chat.grpid).await?;
|
||||
@@ -4141,6 +4132,11 @@ pub async fn remove_contact_from_chat(
|
||||
chat.sync_contacts(context).await.log_err(context).ok();
|
||||
}
|
||||
}
|
||||
} else if chat.typ == Chattype::InBroadcast && contact_id == ContactId::SELF {
|
||||
// For incoming broadcast channels, it's not possible to remove members,
|
||||
// but it's possible to leave:
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
send_member_removal_msg(context, chat_id, contact_id, &self_addr).await?;
|
||||
} else {
|
||||
bail!("Cannot remove members from non-group chats.");
|
||||
}
|
||||
@@ -4148,6 +4144,28 @@ pub async fn remove_contact_from_chat(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_member_removal_msg(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
contact_id: ContactId,
|
||||
addr: &str,
|
||||
) -> Result<MsgId> {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
|
||||
if contact_id == ContactId::SELF {
|
||||
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
|
||||
} else {
|
||||
msg.text = stock_str::msg_del_member_local(context, contact_id, ContactId::SELF).await;
|
||||
}
|
||||
|
||||
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
|
||||
msg.param.set(Param::Arg, addr.to_lowercase());
|
||||
msg.param
|
||||
.set(Param::ContactAddedRemoved, contact_id.to_u32());
|
||||
|
||||
send_msg(context, chat_id, &mut msg).await
|
||||
}
|
||||
|
||||
async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()> {
|
||||
if !is_group_explicitly_left(context, grpid).await? {
|
||||
context
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::ephemeral::Timer;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::imex::{ImexMode, has_backup, imex};
|
||||
use crate::message::{MessengerMessage, delete_msgs};
|
||||
use crate::mimeparser;
|
||||
use crate::mimeparser::{self, MimeMessage};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{
|
||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
|
||||
@@ -374,7 +374,10 @@ async fn test_member_add_remove() -> Result<()> {
|
||||
// Alice leaves the chat.
|
||||
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert_eq!(sent.load_from_db().await.get_text(), "You left the group.");
|
||||
assert_eq!(
|
||||
sent.load_from_db().await.get_text(),
|
||||
stock_str::msg_group_left_local(&alice, ContactId::SELF).await
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2931,6 +2934,108 @@ async fn test_broadcast_channel_protected_listid() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that if Bob leaves a broadcast channel,
|
||||
/// Alice (the channel owner) won't see him as a member anymore,
|
||||
/// but won't be notified about this in any way.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_leave_broadcast() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
tcm.section("Alice creates broadcast channel with Bob.");
|
||||
let alice_chat_id = create_broadcast(alice, "foo".to_string()).await?;
|
||||
let bob_contact = alice.add_or_lookup_contact(bob).await.id;
|
||||
add_contact_to_chat(alice, alice_chat_id, bob_contact).await?;
|
||||
|
||||
tcm.section("Alice sends first message to broadcast.");
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
|
||||
let bob_msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 1);
|
||||
|
||||
// Clear events so that we can later check
|
||||
// that the 'Broadcast channel left' message didn't trigger IncomingMsg:
|
||||
alice.evtracker.clear_events();
|
||||
|
||||
// Shift the time so that we can later check the "Broadcast channel left" message's timestamp:
|
||||
SystemTime::shift(Duration::from_secs(60));
|
||||
|
||||
tcm.section("Bob leaves the broadcast channel.");
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
remove_contact_from_chat(bob, bob_chat_id, ContactId::SELF).await?;
|
||||
|
||||
let leave_msg = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&leave_msg).await;
|
||||
|
||||
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 0);
|
||||
|
||||
alice.emit_event(EventType::Test);
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|ev| match ev {
|
||||
EventType::Test => true,
|
||||
EventType::IncomingMsg { .. } => {
|
||||
panic!("'Broadcast channel left' message should be silent")
|
||||
}
|
||||
EventType::MsgsNoticed(..) => {
|
||||
panic!("'Broadcast channel left' message shouldn't clear notifications")
|
||||
}
|
||||
EventType::MsgsChanged { .. } => {
|
||||
panic!("Broadcast channels should be left silently, without any message");
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that if Bob leaves a broadcast channel with one device,
|
||||
/// the other device shows a correct info message "You left.".
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_leave_broadcast_multidevice() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob0 = &tcm.bob().await;
|
||||
let bob1 = &tcm.bob().await;
|
||||
|
||||
tcm.section("Alice creates broadcast channel with Bob.");
|
||||
let alice_chat_id = create_broadcast(alice, "foo".to_string()).await?;
|
||||
let bob_contact = alice.add_or_lookup_contact(bob0).await.id;
|
||||
add_contact_to_chat(alice, alice_chat_id, bob_contact).await?;
|
||||
|
||||
tcm.section("Alice sends first message to broadcast.");
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
|
||||
let bob0_hello = bob0.recv_msg(&sent_msg).await;
|
||||
let bob1_hello = bob1.recv_msg(&sent_msg).await;
|
||||
|
||||
tcm.section("Bob leaves the broadcast channel with his first device.");
|
||||
let bob_chat_id = bob0_hello.chat_id;
|
||||
bob_chat_id.accept(bob0).await?;
|
||||
remove_contact_from_chat(bob0, bob_chat_id, ContactId::SELF).await?;
|
||||
|
||||
let leave_msg = bob0.pop_sent_msg().await;
|
||||
let parsed = MimeMessage::from_bytes(bob1, leave_msg.payload().as_bytes(), None).await?;
|
||||
assert_eq!(
|
||||
parsed.parts[0].msg,
|
||||
stock_str::msg_group_left_remote(bob0).await
|
||||
);
|
||||
|
||||
let rcvd = bob1.recv_msg(&leave_msg).await;
|
||||
|
||||
assert_eq!(rcvd.chat_id, bob1_hello.chat_id);
|
||||
assert!(rcvd.is_info());
|
||||
assert_eq!(rcvd.get_info_type(), SystemMessage::MemberRemovedFromGroup);
|
||||
assert_eq!(
|
||||
rcvd.text,
|
||||
stock_str::msg_group_left_local(bob1, ContactId::SELF).await
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_for_contact_with_blocked() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
@@ -789,7 +789,7 @@ impl MimeFactory {
|
||||
}
|
||||
|
||||
if let Loaded::Message { chat, .. } = &self.loaded {
|
||||
if chat.typ == Chattype::OutBroadcast {
|
||||
if chat.typ == Chattype::OutBroadcast || chat.typ == Chattype::InBroadcast {
|
||||
headers.push((
|
||||
"List-ID",
|
||||
mail_builder::headers::text::Text::new(format!(
|
||||
@@ -1319,7 +1319,10 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
|
||||
if chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast {
|
||||
if chat.typ == Chattype::Group
|
||||
|| chat.typ == Chattype::OutBroadcast
|
||||
|| chat.typ == Chattype::InBroadcast
|
||||
{
|
||||
headers.push((
|
||||
"Chat-Group-Name",
|
||||
mail_builder::headers::text::Text::new(chat.name.to_string()).into(),
|
||||
|
||||
@@ -14,7 +14,9 @@ use mailparse::SingleInfo;
|
||||
use num_traits::FromPrimitive;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::chat::{
|
||||
self, Chat, ChatId, ChatIdBlocked, ProtectionStatus, remove_from_chat_contacts_table,
|
||||
};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
|
||||
use crate::contact::{Contact, ContactId, Origin, mark_contact_id_as_verified};
|
||||
@@ -1687,7 +1689,9 @@ async fn add_parts(
|
||||
_ if chat.id.is_special() => GroupChangesInfo::default(),
|
||||
Chattype::Single => GroupChangesInfo::default(),
|
||||
Chattype::Mailinglist => GroupChangesInfo::default(),
|
||||
Chattype::OutBroadcast => GroupChangesInfo::default(),
|
||||
Chattype::OutBroadcast => {
|
||||
apply_out_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
|
||||
}
|
||||
Chattype::Group => {
|
||||
apply_group_changes(
|
||||
context,
|
||||
@@ -1701,7 +1705,7 @@ async fn add_parts(
|
||||
.await?
|
||||
}
|
||||
Chattype::InBroadcast => {
|
||||
apply_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
|
||||
apply_in_broadcast_changes(context, mime_parser, &mut chat, from_id).await?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3438,7 +3442,30 @@ async fn apply_mailinglist_changes(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn apply_broadcast_changes(
|
||||
async fn apply_out_broadcast_changes(
|
||||
context: &Context,
|
||||
mime_parser: &MimeMessage,
|
||||
chat: &mut Chat,
|
||||
from_id: ContactId,
|
||||
) -> Result<GroupChangesInfo> {
|
||||
ensure!(chat.typ == Chattype::OutBroadcast);
|
||||
|
||||
if let Some(_removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
|
||||
// The sender of the message left the broadcast channel
|
||||
remove_from_chat_contacts_table(context, chat.id, from_id).await?;
|
||||
|
||||
return Ok(GroupChangesInfo {
|
||||
better_msg: Some("".to_string()),
|
||||
added_removed_id: None,
|
||||
silent: true,
|
||||
extra_msgs: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
Ok(GroupChangesInfo::default())
|
||||
}
|
||||
|
||||
async fn apply_in_broadcast_changes(
|
||||
context: &Context,
|
||||
mime_parser: &MimeMessage,
|
||||
chat: &mut Chat,
|
||||
@@ -3459,6 +3486,15 @@ async fn apply_broadcast_changes(
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(_removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
|
||||
// The only member added/removed message that is ever sent is "I left.",
|
||||
// so, this is the only case we need to handle here
|
||||
if from_id == ContactId::SELF {
|
||||
better_msg
|
||||
.get_or_insert(stock_str::msg_group_left_local(context, ContactId::SELF).await);
|
||||
}
|
||||
}
|
||||
|
||||
if send_event_chat_modified {
|
||||
context.emit_event(EventType::ChatModified(chat.id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat.id);
|
||||
|
||||
@@ -285,7 +285,7 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Member %1$s removed by %2$s."))]
|
||||
MsgDelMemberBy = 131,
|
||||
|
||||
#[strum(props(fallback = "You left the group."))]
|
||||
#[strum(props(fallback = "You left."))]
|
||||
MsgYouLeftGroup = 132,
|
||||
|
||||
#[strum(props(fallback = "Group left by %1$s."))]
|
||||
@@ -685,7 +685,7 @@ pub(crate) async fn msg_group_left_remote(context: &Context) -> String {
|
||||
translated(context, StockMessage::MsgILeftGroup).await
|
||||
}
|
||||
|
||||
/// Stock string: `You left the group.` or `Group left by %1$s.`.
|
||||
/// Stock string: `You left.` or `Group left by %1$s.`.
|
||||
pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouLeftGroup).await
|
||||
|
||||
Reference in New Issue
Block a user