mirror of
https://github.com/chatmail/core.git
synced 2026-04-23 00:16:34 +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:
129
src/chat.rs
129
src/chat.rs
@@ -123,6 +123,9 @@ pub(crate) enum CantSendReason {
|
||||
/// Mailing list without known List-Post header.
|
||||
ReadOnlyMailingList,
|
||||
|
||||
/// Incoming broadcast channel where the user can't send messages.
|
||||
InBroadcast,
|
||||
|
||||
/// Not a member of the chat.
|
||||
NotAMember,
|
||||
|
||||
@@ -146,6 +149,9 @@ impl fmt::Display for CantSendReason {
|
||||
Self::ReadOnlyMailingList => {
|
||||
write!(f, "mailing list does not have a know post address")
|
||||
}
|
||||
Self::InBroadcast => {
|
||||
write!(f, "Broadcast channel is read-only")
|
||||
}
|
||||
Self::NotAMember => write!(f, "not a member of the chat"),
|
||||
Self::MissingKey => write!(f, "key is missing"),
|
||||
}
|
||||
@@ -395,7 +401,7 @@ impl ChatId {
|
||||
let mut delete = false;
|
||||
|
||||
match chat.typ {
|
||||
Chattype::Broadcast => {
|
||||
Chattype::OutBroadcast => {
|
||||
bail!("Can't block chat of type {:?}", chat.typ)
|
||||
}
|
||||
Chattype::Single => {
|
||||
@@ -413,7 +419,7 @@ impl ChatId {
|
||||
info!(context, "Can't block groups yet, deleting the chat.");
|
||||
delete = true;
|
||||
}
|
||||
Chattype::Mailinglist => {
|
||||
Chattype::Mailinglist | Chattype::InBroadcast => {
|
||||
if self.set_blocked(context, Blocked::Yes).await? {
|
||||
context.emit_event(EventType::ChatModified(self));
|
||||
}
|
||||
@@ -479,7 +485,7 @@ impl ChatId {
|
||||
.inner_set_protection(context, ProtectionStatus::Unprotected)
|
||||
.await?;
|
||||
}
|
||||
Chattype::Single | Chattype::Group | Chattype::Broadcast => {
|
||||
Chattype::Single | Chattype::Group | Chattype::OutBroadcast | Chattype::InBroadcast => {
|
||||
// User has "created a chat" with all these contacts.
|
||||
//
|
||||
// Previously accepting a chat literally created a chat because unaccepted chats
|
||||
@@ -529,7 +535,10 @@ impl ChatId {
|
||||
|
||||
match protect {
|
||||
ProtectionStatus::Protected => match chat.typ {
|
||||
Chattype::Single | Chattype::Group | Chattype::Broadcast => {}
|
||||
Chattype::Single
|
||||
| Chattype::Group
|
||||
| Chattype::OutBroadcast
|
||||
| Chattype::InBroadcast => {}
|
||||
Chattype::Mailinglist => bail!("Cannot protect mailing lists"),
|
||||
},
|
||||
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {}
|
||||
@@ -1659,6 +1668,12 @@ impl Chat {
|
||||
return Ok(Some(reason));
|
||||
}
|
||||
}
|
||||
if self.typ == Chattype::InBroadcast {
|
||||
let reason = InBroadcast;
|
||||
if !skip_fn(&reason) {
|
||||
return Ok(Some(reason));
|
||||
}
|
||||
}
|
||||
|
||||
// Do potentially slow checks last and after calls to `skip_fn` which should be fast.
|
||||
let reason = NotAMember;
|
||||
@@ -1692,8 +1707,9 @@ impl Chat {
|
||||
/// The function does not check if the chat type allows editing of concrete elements.
|
||||
pub(crate) async fn is_self_in_chat(&self, context: &Context) -> Result<bool> {
|
||||
match self.typ {
|
||||
Chattype::Single | Chattype::Broadcast | Chattype::Mailinglist => Ok(true),
|
||||
Chattype::Single | Chattype::OutBroadcast | Chattype::Mailinglist => Ok(true),
|
||||
Chattype::Group => is_contact_in_chat(context, self.id, ContactId::SELF).await,
|
||||
Chattype::InBroadcast => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1758,8 +1774,6 @@ impl Chat {
|
||||
if !image_rel.is_empty() {
|
||||
return Ok(Some(get_abs_path(context, Path::new(&image_rel))));
|
||||
}
|
||||
} else if self.typ == Chattype::Broadcast {
|
||||
return Ok(Some(get_broadcast_icon(context).await?));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -1872,7 +1886,7 @@ impl Chat {
|
||||
!self.grpid.is_empty()
|
||||
}
|
||||
Chattype::Mailinglist => false,
|
||||
Chattype::Broadcast => true,
|
||||
Chattype::OutBroadcast | Chattype::InBroadcast => true,
|
||||
};
|
||||
Ok(is_encrypted)
|
||||
}
|
||||
@@ -1970,7 +1984,7 @@ impl Chat {
|
||||
);
|
||||
bail!("Cannot set message, contact for {} not found.", self.id);
|
||||
}
|
||||
} else if self.typ == Chattype::Group
|
||||
} else if matches!(self.typ, Chattype::Group | Chattype::OutBroadcast)
|
||||
&& self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
|
||||
{
|
||||
msg.param.set_int(Param::AttachGroupImage, 1);
|
||||
@@ -2291,7 +2305,10 @@ impl Chat {
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
Chattype::Broadcast | Chattype::Group | Chattype::Mailinglist => {
|
||||
Chattype::OutBroadcast
|
||||
| Chattype::InBroadcast
|
||||
| Chattype::Group
|
||||
| Chattype::Mailinglist => {
|
||||
if !self.grpid.is_empty() {
|
||||
return Ok(Some(SyncId::Grpid(self.grpid.clone())));
|
||||
}
|
||||
@@ -2465,15 +2482,6 @@ pub(crate) async fn get_device_icon(context: &Context) -> Result<PathBuf> {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_broadcast_icon(context: &Context) -> Result<PathBuf> {
|
||||
get_asset_icon(
|
||||
context,
|
||||
"icon-broadcast",
|
||||
include_bytes!("../assets/icon-broadcast.png"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_archive_icon(context: &Context) -> Result<PathBuf> {
|
||||
get_asset_icon(
|
||||
context,
|
||||
@@ -3614,37 +3622,27 @@ pub async fn create_group_chat(
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
/// Finds an unused name for a new broadcast list.
|
||||
async fn find_unused_broadcast_list_name(context: &Context) -> Result<String> {
|
||||
let base_name = stock_str::broadcast_list(context).await;
|
||||
for attempt in 1..1000 {
|
||||
let better_name = if attempt > 1 {
|
||||
format!("{base_name} {attempt}")
|
||||
} else {
|
||||
base_name.clone()
|
||||
};
|
||||
if !context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*) FROM chats WHERE type=? AND name=?;",
|
||||
(Chattype::Broadcast, &better_name),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(better_name);
|
||||
}
|
||||
}
|
||||
Ok(base_name)
|
||||
}
|
||||
|
||||
/// Creates a new broadcast list.
|
||||
pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
|
||||
let chat_name = find_unused_broadcast_list_name(context).await?;
|
||||
/// Create a new **broadcast channel**
|
||||
/// (called "Channel" in the UI).
|
||||
///
|
||||
/// Broadcast channels are similar to groups on the sending device,
|
||||
/// however, recipients get the messages in a read-only chat
|
||||
/// and will not see who the other members are.
|
||||
///
|
||||
/// Called `broadcast` here rather than `channel`,
|
||||
/// because the word "channel" already appears a lot in the code,
|
||||
/// which would make it hard to grep for it.
|
||||
///
|
||||
/// After creation, the chat contains no recipients and is in _unpromoted_ state;
|
||||
/// see [`create_group_chat`] for more information on the unpromoted state.
|
||||
///
|
||||
/// Returns the created chat's id.
|
||||
pub async fn create_broadcast(context: &Context, chat_name: String) -> Result<ChatId> {
|
||||
let grpid = create_id();
|
||||
create_broadcast_list_ex(context, Sync, grpid, chat_name).await
|
||||
create_broadcast_ex(context, Sync, grpid, chat_name).await
|
||||
}
|
||||
|
||||
pub(crate) async fn create_broadcast_list_ex(
|
||||
pub(crate) async fn create_broadcast_ex(
|
||||
context: &Context,
|
||||
sync: sync::Sync,
|
||||
grpid: String,
|
||||
@@ -3659,7 +3657,7 @@ pub(crate) async fn create_broadcast_list_ex(
|
||||
if cnt == 1 {
|
||||
return Ok(t.query_row(
|
||||
"SELECT id FROM chats WHERE grpid=? AND type=?",
|
||||
(grpid, Chattype::Broadcast),
|
||||
(grpid, Chattype::OutBroadcast),
|
||||
|row| {
|
||||
let id: isize = row.get(0)?;
|
||||
Ok(id)
|
||||
@@ -3671,7 +3669,7 @@ pub(crate) async fn create_broadcast_list_ex(
|
||||
(type, name, grpid, param, created_timestamp) \
|
||||
VALUES(?, ?, ?, \'U=1\', ?);",
|
||||
(
|
||||
Chattype::Broadcast,
|
||||
Chattype::OutBroadcast,
|
||||
&chat_name,
|
||||
&grpid,
|
||||
create_smeared_timestamp(context),
|
||||
@@ -3809,7 +3807,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
// this also makes sure, no contacts are added to special or normal chats
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
ensure!(
|
||||
chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast,
|
||||
chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast,
|
||||
"{} is not a group/broadcast where one can add members",
|
||||
chat_id
|
||||
);
|
||||
@@ -3820,8 +3818,8 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
);
|
||||
ensure!(!chat.is_mailing_list(), "Mailing lists can't be changed");
|
||||
ensure!(
|
||||
chat.typ != Chattype::Broadcast || contact_id != ContactId::SELF,
|
||||
"Cannot add SELF to broadcast."
|
||||
chat.typ != Chattype::OutBroadcast || contact_id != ContactId::SELF,
|
||||
"Cannot add SELF to broadcast channel."
|
||||
);
|
||||
ensure!(
|
||||
chat.is_encrypted(context).await? == contact.is_key_contact(),
|
||||
@@ -4040,7 +4038,7 @@ pub async fn remove_contact_from_chat(
|
||||
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::Broadcast {
|
||||
if chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast {
|
||||
if !chat.is_self_in_chat(context).await? {
|
||||
let err_msg = format!(
|
||||
"Cannot remove contact {contact_id} from chat {chat_id}: self not in group."
|
||||
@@ -4148,7 +4146,7 @@ async fn rename_ex(
|
||||
|
||||
if chat.typ == Chattype::Group
|
||||
|| chat.typ == Chattype::Mailinglist
|
||||
|| chat.typ == Chattype::Broadcast
|
||||
|| chat.typ == Chattype::OutBroadcast
|
||||
{
|
||||
if chat.name == new_name {
|
||||
success = true;
|
||||
@@ -4166,7 +4164,6 @@ async fn rename_ex(
|
||||
.await?;
|
||||
if chat.is_promoted()
|
||||
&& !chat.is_mailing_list()
|
||||
&& chat.typ != Chattype::Broadcast
|
||||
&& sanitize_single_line(&chat.name) != new_name
|
||||
{
|
||||
msg.viewtype = Viewtype::Text;
|
||||
@@ -4212,15 +4209,15 @@ pub async fn set_chat_profile_image(
|
||||
ensure!(!chat_id.is_special(), "Invalid chat ID");
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
ensure!(
|
||||
chat.typ == Chattype::Group,
|
||||
"Can only set profile image for group chats"
|
||||
chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast,
|
||||
"Can only set profile image for groups / broadcasts"
|
||||
);
|
||||
ensure!(
|
||||
!chat.grpid.is_empty(),
|
||||
"Cannot set profile image for ad hoc groups"
|
||||
);
|
||||
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
|
||||
if !is_contact_in_chat(context, chat_id, ContactId::SELF).await? {
|
||||
if !chat.is_self_in_chat(context).await? {
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
"Cannot set chat profile image; self not in group.".into(),
|
||||
));
|
||||
@@ -4287,10 +4284,6 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
bail!("cannot forward drafts.");
|
||||
}
|
||||
|
||||
// we tested a sort of broadcast
|
||||
// by not marking own forwarded messages as such,
|
||||
// however, this turned out to be to confusing and unclear.
|
||||
|
||||
if msg.get_viewtype() != Viewtype::Sticker {
|
||||
msg.param
|
||||
.set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
|
||||
@@ -4787,7 +4780,7 @@ async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String])
|
||||
"Cannot add address-contacts to encrypted chat {id}"
|
||||
);
|
||||
ensure!(
|
||||
chat.typ == Chattype::Broadcast,
|
||||
chat.typ == Chattype::OutBroadcast,
|
||||
"{id} is not a broadcast list",
|
||||
);
|
||||
let mut contacts = HashSet::new();
|
||||
@@ -4808,7 +4801,7 @@ async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String])
|
||||
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?;
|
||||
|
||||
// We do not care about `add_timestamp` column
|
||||
// because timestamps are not used for broadcast lists.
|
||||
// because timestamps are not used for broadcast channels.
|
||||
let mut statement = transaction
|
||||
.prepare("INSERT INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)")?;
|
||||
for contact_id in &contacts {
|
||||
@@ -4835,7 +4828,7 @@ async fn set_contacts_by_fingerprints(
|
||||
"Cannot add key-contacts to unencrypted chat {id}"
|
||||
);
|
||||
ensure!(
|
||||
chat.typ == Chattype::Broadcast,
|
||||
chat.typ == Chattype::OutBroadcast,
|
||||
"{id} is not a broadcast list",
|
||||
);
|
||||
let mut contacts = HashSet::new();
|
||||
@@ -4857,7 +4850,7 @@ async fn set_contacts_by_fingerprints(
|
||||
transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?;
|
||||
|
||||
// We do not care about `add_timestamp` column
|
||||
// because timestamps are not used for broadcast lists.
|
||||
// because timestamps are not used for broadcast channels.
|
||||
let mut statement = transaction
|
||||
.prepare("INSERT INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)")?;
|
||||
for contact_id in &contacts {
|
||||
@@ -4895,7 +4888,7 @@ pub(crate) enum SyncAction {
|
||||
Accept,
|
||||
SetVisibility(ChatVisibility),
|
||||
SetMuted(MuteDuration),
|
||||
/// Create broadcast list with the given name.
|
||||
/// Create broadcast channel with the given name.
|
||||
CreateBroadcast(String),
|
||||
Rename(String),
|
||||
/// Set chat contacts by their addresses.
|
||||
@@ -4960,7 +4953,7 @@ impl Context {
|
||||
}
|
||||
SyncId::Grpid(grpid) => {
|
||||
if let SyncAction::CreateBroadcast(name) = action {
|
||||
create_broadcast_list_ex(self, Nosync, grpid.clone(), name.clone()).await?;
|
||||
create_broadcast_ex(self, Nosync, grpid.clone(), name.clone()).await?;
|
||||
return Ok(());
|
||||
}
|
||||
get_chat_id_by_grpid(self, grpid)
|
||||
|
||||
Reference in New Issue
Block a user