mirror of
https://github.com/chatmail/core.git
synced 2026-04-20 15:06:30 +03:00
feat: Show broadcast channels in their own, proper "Channel" chat (#6901)
Part of #6884 ---- - [x] Add new chat type `InBroadcastChannel` and `OutBroadcastChannel` for incoming / outgoing channels, where the former is similar to a `Mailinglist` and the latter is similar to a `Broadcast` (which is removed) - Consideration for naming: `InChannel`/`OutChannel` (without "broadcast") would be shorter, but less greppable because we already have a lot of occurences of `channel` in the code. Consistently calling them `BcChannel`/`bc_channel` in the code would be both short and greppable, but a bit arcane when reading it at first. Opinions are welcome; if I hear none, I'll keep with `BroadcastChannel`. - [x] api: Add create_broadcast_channel(), deprecate create_broadcast_list() (or `create_channel()` / `create_bc_channel()` if we decide to switch) - Adjust code comments to match the new behavior. - [x] Ask Desktop developers what they use `is_broadcast` field for, and whether it should be true for both outgoing & incoming channels (or look it up myself) - I added `is_out_broadcast_channel`, and deprecated `is_broadcast`, for now - [x] When the user changes the broadcast channel name, immediately show this change on receiving devices - [x] Allow to change brodacast channel avatar, and immediately apply it on the receiving device - [x] Make it possible to block InBroadcastChannel - [x] Make it possible to set the avatar of an OutgoingChannel, and apply it on the receiving side - [x] DECIDE whether we still want to use the broadcast icon as the default icon or whether we want to use the letter-in-a-circle - We decided to use the letter-in-a-circle for now, because it's easier to implement, and I need to stay in the time plan - [x] chat.rs: Return an error if the user tries to modify a `InBroadcastChannel` - [x] Add automated regression tests - [x] Grep for `broadcast` and see whether there is any other work I need to do - [x] Bug: Don't show `~` in front of the sender's same in broadcast lists ---- Note that I removed the following guard: ```rust if !new_chat_contacts.contains(&ContactId::SELF) { warn!( context, "Received group avatar update for group chat {} we are not a member of.", chat.id ); } else if !new_chat_contacts.contains(&from_id) { warn!( context, "Contact {from_id} attempts to modify group chat {} avatar without being a member.", chat.id, ); } else [...] ``` i.e. with this change, non-members will be able to modify the avatar. Things were slightly easier this way, and I think that this is in line with non-members being able to modify the group name and memberlist (they need to know the Group-Chat-Id, anyway), but I can also change it back.
This commit is contained in:
@@ -6,7 +6,10 @@ use crate::headerdef::HeaderDef;
|
||||
use crate::imex::{ImexMode, has_backup, imex};
|
||||
use crate::message::{MessengerMessage, delete_msgs};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager, TimeShiftFalsePositiveNote, sync};
|
||||
use crate::test_utils::{
|
||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
|
||||
TimeShiftFalsePositiveNote, sync,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
use strum::IntoEnumIterator;
|
||||
use tokio::fs;
|
||||
@@ -2259,7 +2262,7 @@ async fn test_only_minimal_data_are_forwarded() -> Result<()> {
|
||||
let single_id = ChatId::create_for_contact(&bob, charlie_id).await?;
|
||||
let group_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "group2").await?;
|
||||
add_contact_to_chat(&bob, group_id, charlie_id).await?;
|
||||
let broadcast_id = create_broadcast_list(&bob).await?;
|
||||
let broadcast_id = create_broadcast(&bob, "Channel".to_string()).await?;
|
||||
add_contact_to_chat(&bob, broadcast_id, charlie_id).await?;
|
||||
for chat_id in &[single_id, group_id, broadcast_id] {
|
||||
forward_msgs(&bob, &[orig_msg.id], *chat_id).await?;
|
||||
@@ -2619,8 +2622,8 @@ async fn test_broadcast() -> Result<()> {
|
||||
let msg = alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
// test broadcast list
|
||||
let broadcast_id = create_broadcast_list(&alice).await?;
|
||||
// test broadcast channel
|
||||
let broadcast_id = create_broadcast(&alice, "Channel".to_string()).await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
broadcast_id,
|
||||
@@ -2629,11 +2632,11 @@ async fn test_broadcast() -> Result<()> {
|
||||
.await?;
|
||||
let fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
|
||||
add_contact_to_chat(&alice, broadcast_id, fiona_contact_id).await?;
|
||||
set_chat_name(&alice, broadcast_id, "Broadcast list").await?;
|
||||
set_chat_name(&alice, broadcast_id, "Broadcast channel").await?;
|
||||
{
|
||||
let chat = Chat::load_from_db(&alice, broadcast_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Broadcast);
|
||||
assert_eq!(chat.name, "Broadcast list");
|
||||
assert_eq!(chat.typ, Chattype::OutBroadcast);
|
||||
assert_eq!(chat.name, "Broadcast channel");
|
||||
assert!(!chat.is_self_talk());
|
||||
|
||||
send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?;
|
||||
@@ -2649,12 +2652,13 @@ async fn test_broadcast() -> Result<()> {
|
||||
assert!(!msg.header_exists(HeaderDef::AutocryptGossip));
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.get_text(), "ola!");
|
||||
assert_eq!(msg.subject, "Broadcast list");
|
||||
assert_eq!(msg.subject, "Broadcast channel");
|
||||
assert!(msg.get_showpadlock());
|
||||
assert!(msg.get_override_sender_name().is_none());
|
||||
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Mailinglist);
|
||||
assert_eq!(chat.typ, Chattype::InBroadcast);
|
||||
assert_ne!(chat.id, chat_bob.id);
|
||||
assert_eq!(chat.name, "Broadcast list");
|
||||
assert_eq!(chat.name, "Broadcast channel");
|
||||
assert!(!chat.is_self_talk());
|
||||
}
|
||||
|
||||
@@ -2672,6 +2676,14 @@ async fn test_broadcast() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// - Alice has multiple devices
|
||||
/// - Alice creates a broadcast and sends a message into it
|
||||
/// - Alice's second device sees the broadcast
|
||||
/// - Alice adds Bob to the broadcast
|
||||
/// - Synchronization is only implemented via sync messages for now,
|
||||
/// which are not enabled in tests by default,
|
||||
/// so, Alice's second device doesn't see the change yet.
|
||||
/// `test_sync_broadcast()` tests that synchronization works via sync messages.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcast_multidev() -> Result<()> {
|
||||
let alices = [
|
||||
@@ -2681,9 +2693,9 @@ async fn test_broadcast_multidev() -> Result<()> {
|
||||
let bob = TestContext::new_bob().await;
|
||||
let a1b_contact_id = alices[1].add_or_lookup_contact(&bob).await.id;
|
||||
|
||||
let a0_broadcast_id = create_broadcast_list(&alices[0]).await?;
|
||||
let a0_broadcast_id = create_broadcast(&alices[0], "Channel".to_string()).await?;
|
||||
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
|
||||
set_chat_name(&alices[0], a0_broadcast_id, "Broadcast list 42").await?;
|
||||
set_chat_name(&alices[0], a0_broadcast_id, "Broadcast channel 42").await?;
|
||||
let sent_msg = alices[0].send_text(a0_broadcast_id, "hi").await;
|
||||
let msg = alices[1].recv_msg(&sent_msg).await;
|
||||
let a1_broadcast_id = get_chat_id_by_grpid(&alices[1], &a0_broadcast_chat.grpid)
|
||||
@@ -2692,8 +2704,8 @@ async fn test_broadcast_multidev() -> Result<()> {
|
||||
.0;
|
||||
assert_eq!(msg.chat_id, a1_broadcast_id);
|
||||
let a1_broadcast_chat = Chat::load_from_db(&alices[1], a1_broadcast_id).await?;
|
||||
assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast);
|
||||
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
|
||||
assert_eq!(a1_broadcast_chat.get_type(), Chattype::OutBroadcast);
|
||||
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast channel 42");
|
||||
assert!(
|
||||
get_chat_contacts(&alices[1], a1_broadcast_id)
|
||||
.await?
|
||||
@@ -2701,13 +2713,13 @@ async fn test_broadcast_multidev() -> Result<()> {
|
||||
);
|
||||
|
||||
add_contact_to_chat(&alices[1], a1_broadcast_id, a1b_contact_id).await?;
|
||||
set_chat_name(&alices[1], a1_broadcast_id, "Broadcast list 43").await?;
|
||||
set_chat_name(&alices[1], a1_broadcast_id, "Broadcast channel 43").await?;
|
||||
let sent_msg = alices[1].send_text(a1_broadcast_id, "hi").await;
|
||||
let msg = alices[0].recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, a0_broadcast_id);
|
||||
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
|
||||
assert_eq!(a0_broadcast_chat.get_type(), Chattype::Broadcast);
|
||||
assert_eq!(a0_broadcast_chat.get_name(), "Broadcast list 42");
|
||||
assert_eq!(a0_broadcast_chat.get_type(), Chattype::OutBroadcast);
|
||||
assert_eq!(a0_broadcast_chat.get_name(), "Broadcast channel 42");
|
||||
assert!(
|
||||
get_chat_contacts(&alices[0], a0_broadcast_id)
|
||||
.await?
|
||||
@@ -2717,6 +2729,161 @@ async fn test_broadcast_multidev() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// - Create a broadcast channel
|
||||
/// - Send a message into it in order to promote it
|
||||
/// - Add a contact
|
||||
/// - Rename it
|
||||
/// - the change should be visible on the receiver's side immediately
|
||||
/// - Change the avatar
|
||||
/// - The change should be visible on the receiver's side immediately
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_broadcasts_name_and_avatar() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
alice.set_config(Config::Displayname, Some("Alice")).await?;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
|
||||
tcm.section("Create a broadcast channel");
|
||||
let alice_chat_id = create_broadcast(alice, "My Channel".to_string()).await?;
|
||||
let alice_chat = Chat::load_from_db(alice, alice_chat_id).await?;
|
||||
assert_eq!(alice_chat.typ, Chattype::OutBroadcast);
|
||||
|
||||
let alice_chat = Chat::load_from_db(alice, alice_chat_id).await?;
|
||||
assert_eq!(alice_chat.is_promoted(), false);
|
||||
let sent = alice.send_text(alice_chat_id, "Hi nobody").await;
|
||||
let alice_chat = Chat::load_from_db(alice, alice_chat_id).await?;
|
||||
assert_eq!(alice_chat.is_promoted(), true);
|
||||
assert_eq!(sent.recipients, "alice@example.org");
|
||||
|
||||
tcm.section("Add a contact to the chat and send a message");
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let sent = alice.send_text(alice_chat_id, "Hi somebody").await;
|
||||
|
||||
assert_eq!(sent.recipients, "bob@example.net alice@example.org");
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
assert!(rcvd.get_override_sender_name().is_none());
|
||||
assert_eq!(rcvd.text, "Hi somebody");
|
||||
let bob_chat = Chat::load_from_db(bob, rcvd.chat_id).await?;
|
||||
assert_eq!(bob_chat.typ, Chattype::InBroadcast);
|
||||
assert_eq!(bob_chat.name, "My Channel");
|
||||
assert_eq!(bob_chat.get_profile_image(bob).await?, None);
|
||||
|
||||
tcm.section("Change broadcast channel name, and check that receivers see it");
|
||||
set_chat_name(alice, alice_chat_id, "New Channel name").await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
assert!(rcvd.get_override_sender_name().is_none());
|
||||
assert_eq!(rcvd.get_info_type(), SystemMessage::GroupNameChanged);
|
||||
assert_eq!(
|
||||
rcvd.text,
|
||||
r#"Group name changed from "My Channel" to "New Channel name" by Alice."#
|
||||
);
|
||||
let bob_chat = Chat::load_from_db(bob, bob_chat.id).await?;
|
||||
assert_eq!(bob_chat.name, "New Channel name");
|
||||
|
||||
tcm.section("Set a broadcast channel avatar, and check that receivers see it");
|
||||
let file = alice.get_blobdir().join("avatar.png");
|
||||
tokio::fs::write(&file, AVATAR_64x64_BYTES).await?;
|
||||
set_chat_profile_image(alice, alice_chat_id, file.to_str().unwrap()).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
let bob_chat = Chat::load_from_db(bob, bob_chat.id).await?;
|
||||
assert_eq!(bob_chat.get_profile_image(bob).await?, None);
|
||||
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
assert!(rcvd.get_override_sender_name().is_none());
|
||||
assert_eq!(rcvd.get_info_type(), SystemMessage::GroupImageChanged);
|
||||
assert_eq!(rcvd.text, "Group image changed by Alice.");
|
||||
assert_eq!(rcvd.chat_id, bob_chat.id);
|
||||
|
||||
let bob_chat = Chat::load_from_db(bob, bob_chat.id).await?;
|
||||
let avatar = bob_chat.get_profile_image(bob).await?.unwrap();
|
||||
assert_eq!(
|
||||
avatar.file_name().unwrap().to_str().unwrap(),
|
||||
AVATAR_64x64_DEDUPLICATED
|
||||
);
|
||||
|
||||
tcm.section("Check that Bob can't modify the broadcast channel");
|
||||
set_chat_profile_image(bob, bob_chat.id, file.to_str().unwrap())
|
||||
.await
|
||||
.unwrap_err();
|
||||
set_chat_name(bob, bob_chat.id, "Bob Channel name")
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// - Create a broadcast channel
|
||||
/// - Block it
|
||||
/// - Check that the broadcast channel appears in the list of blocked contacts
|
||||
/// - A message is sent into the broadcast channel, but it is blocked
|
||||
/// - Unblock it
|
||||
/// - Receive a message again in the now-unblocked broadcast channel
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_block_broadcast() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
|
||||
tcm.section("Create a broadcast channel with Bob, and send a message");
|
||||
let alice_chat_id = create_broadcast(alice, "My Channel".to_string()).await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let sent = alice.send_text(alice_chat_id, "Hi somebody").await;
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
|
||||
let chats = Chatlist::try_load(bob, DC_GCL_NO_SPECIALS, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0)?, rcvd.chat_id);
|
||||
|
||||
assert_eq!(rcvd.chat_blocked, Blocked::Request);
|
||||
let blocked = Contact::get_all_blocked(bob).await.unwrap();
|
||||
assert_eq!(blocked.len(), 0);
|
||||
|
||||
tcm.section("Bob blocks the chat");
|
||||
rcvd.chat_id.block(bob).await?;
|
||||
let chat = Chat::load_from_db(bob, rcvd.chat_id).await?;
|
||||
assert_eq!(chat.blocked, Blocked::Yes);
|
||||
let blocked = Contact::get_all_blocked(bob).await.unwrap();
|
||||
assert_eq!(blocked.len(), 1);
|
||||
let blocked = Contact::get_by_id(bob, blocked[0]).await?;
|
||||
assert!(blocked.is_key_contact());
|
||||
assert_eq!(blocked.origin, Origin::MailinglistAddress);
|
||||
assert_eq!(blocked.get_name(), "My Channel");
|
||||
|
||||
let sent = alice.send_text(alice_chat_id, "Second message").await;
|
||||
let rcvd2 = bob.recv_msg(&sent).await;
|
||||
assert_eq!(rcvd2.chat_id, rcvd.chat_id);
|
||||
assert_eq!(rcvd2.chat_blocked, Blocked::Yes);
|
||||
|
||||
let chats = Chatlist::try_load(bob, DC_GCL_NO_SPECIALS, None, None).await?;
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
tcm.section("Bob unblocks the chat");
|
||||
Contact::unblock(bob, blocked.id).await?;
|
||||
|
||||
let sent = alice.send_text(alice_chat_id, "Third message").await;
|
||||
let rcvd3 = bob.recv_msg(&sent).await;
|
||||
assert_eq!(rcvd3.chat_id, rcvd.chat_id);
|
||||
assert_eq!(rcvd3.chat_blocked, Blocked::Not);
|
||||
|
||||
let blocked = Contact::get_all_blocked(bob).await.unwrap();
|
||||
assert_eq!(blocked.len(), 0);
|
||||
|
||||
let chats = Chatlist::try_load(bob, DC_GCL_NO_SPECIALS, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(chats.get_chat_id(0)?, rcvd.chat_id);
|
||||
|
||||
let chat = Chat::load_from_db(bob, rcvd3.chat_id).await?;
|
||||
assert_eq!(chat.blocked, Blocked::Not);
|
||||
assert_eq!(chat.name, "My Channel");
|
||||
assert_eq!(chat.typ, Chattype::InBroadcast);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_for_contact_with_blocked() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
@@ -3403,6 +3570,7 @@ async fn test_sync_muted() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that synchronizing broadcast channels via sync-messages works
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_broadcast() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -3414,7 +3582,7 @@ async fn test_sync_broadcast() -> Result<()> {
|
||||
let bob = &tcm.bob().await;
|
||||
let a0b_contact_id = alice0.add_or_lookup_contact(bob).await.id;
|
||||
|
||||
let a0_broadcast_id = create_broadcast_list(alice0).await?;
|
||||
let a0_broadcast_id = create_broadcast(alice0, "Channel".to_string()).await?;
|
||||
sync(alice0, alice1).await;
|
||||
let a0_broadcast_chat = Chat::load_from_db(alice0, a0_broadcast_id).await?;
|
||||
let a1_broadcast_id = get_chat_id_by_grpid(alice1, &a0_broadcast_chat.grpid)
|
||||
@@ -3422,7 +3590,7 @@ async fn test_sync_broadcast() -> Result<()> {
|
||||
.unwrap()
|
||||
.0;
|
||||
let a1_broadcast_chat = Chat::load_from_db(alice1, a1_broadcast_id).await?;
|
||||
assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast);
|
||||
assert_eq!(a1_broadcast_chat.get_type(), Chattype::OutBroadcast);
|
||||
assert_eq!(a1_broadcast_chat.get_name(), a0_broadcast_chat.get_name());
|
||||
assert!(get_chat_contacts(alice1, a1_broadcast_id).await?.is_empty());
|
||||
add_contact_to_chat(alice0, a0_broadcast_id, a0b_contact_id).await?;
|
||||
@@ -3440,7 +3608,7 @@ async fn test_sync_broadcast() -> Result<()> {
|
||||
let sent_msg = alice1.send_text(a1_broadcast_id, "hi").await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
let chat = Chat::load_from_db(bob, msg.chat_id).await?;
|
||||
assert_eq!(chat.get_type(), Chattype::Mailinglist);
|
||||
assert_eq!(chat.get_type(), Chattype::InBroadcast);
|
||||
let msg = alice0.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, a0_broadcast_id);
|
||||
remove_contact_from_chat(alice0, a0_broadcast_id, a0b_contact_id).await?;
|
||||
@@ -3465,18 +3633,18 @@ async fn test_sync_name() -> Result<()> {
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
let a0_broadcast_id = create_broadcast_list(alice0).await?;
|
||||
let a0_broadcast_id = create_broadcast(alice0, "Channel".to_string()).await?;
|
||||
sync(alice0, alice1).await;
|
||||
let a0_broadcast_chat = Chat::load_from_db(alice0, a0_broadcast_id).await?;
|
||||
set_chat_name(alice0, a0_broadcast_id, "Broadcast list 42").await?;
|
||||
set_chat_name(alice0, a0_broadcast_id, "Broadcast channel 42").await?;
|
||||
sync(alice0, alice1).await;
|
||||
let a1_broadcast_id = get_chat_id_by_grpid(alice1, &a0_broadcast_chat.grpid)
|
||||
.await?
|
||||
.unwrap()
|
||||
.0;
|
||||
let a1_broadcast_chat = Chat::load_from_db(alice1, a1_broadcast_id).await?;
|
||||
assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast);
|
||||
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
|
||||
assert_eq!(a1_broadcast_chat.get_type(), Chattype::OutBroadcast);
|
||||
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast channel 42");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user