mirror of
https://github.com/chatmail/core.git
synced 2026-04-28 19:06:35 +03:00
broadcasts (#2707)
* add Chattype::Broadcast and create_broadcast_list() * do not disclose recipients for broadcasts * allow sending/add-/remove-member for broadcast * set broadcast subject same as for one-to-one chats * broadcast-recipient-list does not include SELF * use special icon for broadcast groups * generate initial broadcast names * make clippy happy * send BCC message unencrypted to avoid unexpected disclosing; encryption is opportunistic anyway. if we have 'protected chats' at some point, we can think that over. * reword 'To:'-group * simplify can-send-check * add broadcast tests * tweak comments * Update deltachat-ffi/deltachat.h Co-authored-by: Hocuri <hocuri@gmx.de> * change name of can_edit() to is_self_in_chat() Co-authored-by: Hocuri <hocuri@gmx.de>
This commit is contained in:
238
src/chat.rs
238
src/chat.rs
@@ -271,7 +271,9 @@ impl ChatId {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
|
||||
match chat.typ {
|
||||
Chattype::Undefined => bail!("Can't block chat of undefined chattype"),
|
||||
Chattype::Undefined | Chattype::Broadcast => {
|
||||
bail!("Can't block chat of type {:?}", chat.typ)
|
||||
}
|
||||
Chattype::Single => {
|
||||
for contact_id in get_chat_contacts(context, self).await? {
|
||||
if contact_id != DC_CONTACT_ID_SELF {
|
||||
@@ -311,7 +313,7 @@ impl ChatId {
|
||||
|
||||
match chat.typ {
|
||||
Chattype::Undefined => bail!("Can't accept chat of undefined chattype"),
|
||||
Chattype::Single | Chattype::Group => {
|
||||
Chattype::Single | Chattype::Group | Chattype::Broadcast => {
|
||||
// User has "created a chat" with all these contacts.
|
||||
//
|
||||
// Previously accepting a chat literally created a chat because unaccepted chats
|
||||
@@ -355,7 +357,7 @@ impl ChatId {
|
||||
|
||||
match protect {
|
||||
ProtectionStatus::Protected => match chat.typ {
|
||||
Chattype::Single | Chattype::Group => {
|
||||
Chattype::Single | Chattype::Group | Chattype::Broadcast => {
|
||||
let contact_ids = get_chat_contacts(context, self).await?;
|
||||
for contact_id in contact_ids.into_iter() {
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
@@ -980,8 +982,18 @@ impl Chat {
|
||||
&& !self.is_device_talk()
|
||||
&& !self.is_mailing_list()
|
||||
&& !self.is_contact_request()
|
||||
&& (self.typ == Chattype::Single
|
||||
|| is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF).await?))
|
||||
&& self.is_self_in_chat(context).await?)
|
||||
}
|
||||
|
||||
/// Checks if the user is part of a chat
|
||||
/// and has basically the permissions to edit the chat therefore.
|
||||
/// 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::Group => is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF).await,
|
||||
Chattype::Undefined => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_param(&mut self, context: &Context) -> Result<()> {
|
||||
@@ -1023,8 +1035,11 @@ impl Chat {
|
||||
return contact.get_profile_image(context).await;
|
||||
}
|
||||
}
|
||||
} else if self.typ == Chattype::Broadcast {
|
||||
if let Ok(image_rel) = get_broadcast_icon(context).await {
|
||||
return Ok(Some(dc_get_abs_path(context, image_rel)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -1126,18 +1141,18 @@ impl Chat {
|
||||
let mut to_id = 0;
|
||||
let mut location_id = 0;
|
||||
|
||||
if !(self.typ == Chattype::Single || self.typ == Chattype::Group) {
|
||||
error!(context, "Cannot send to chat type #{}.", self.typ,);
|
||||
bail!("Cannot set to chat type #{}", self.typ);
|
||||
}
|
||||
|
||||
if self.typ == Chattype::Group
|
||||
&& !is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF).await?
|
||||
{
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
"Cannot send message; self not in group.".into(),
|
||||
));
|
||||
bail!("Cannot set message; self not in group.");
|
||||
if !self.can_send(context).await? {
|
||||
if self.typ == Chattype::Group
|
||||
&& !is_contact_in_chat(context, self.id, DC_CONTACT_ID_SELF).await?
|
||||
{
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
"Cannot send message; self not in group.".into(),
|
||||
));
|
||||
bail!("Cannot set message; self not in group.");
|
||||
} else {
|
||||
error!(context, "Cannot send to chat type #{}.", self.typ,);
|
||||
bail!("Cannot send to chat type #{}", self.typ);
|
||||
}
|
||||
}
|
||||
|
||||
let from = context
|
||||
@@ -1461,6 +1476,21 @@ pub(crate) async fn update_device_icon(context: &Context) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_broadcast_icon(context: &Context) -> Result<String> {
|
||||
if let Some(icon) = context.sql.get_raw_config("icon-broadcast").await? {
|
||||
return Ok(icon);
|
||||
}
|
||||
|
||||
let icon = include_bytes!("../assets/icon-broadcast.png");
|
||||
let blob = BlobObject::create(context, "icon-broadcast.png", icon).await?;
|
||||
let icon = blob.as_name().to_string();
|
||||
context
|
||||
.sql
|
||||
.set_raw_config("icon-broadcast", Some(&icon))
|
||||
.await?;
|
||||
Ok(icon)
|
||||
}
|
||||
|
||||
async fn update_special_chat_name(context: &Context, contact_id: u32, name: String) -> Result<()> {
|
||||
if let Some(chat_id) = ChatId::lookup_by_contact(context, contact_id).await? {
|
||||
// the `!= name` condition avoids unneeded writes
|
||||
@@ -2221,6 +2251,56 @@ 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=?;",
|
||||
paramsv![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?;
|
||||
let grpid = dc_create_id();
|
||||
let row_id = context
|
||||
.sql
|
||||
.insert(
|
||||
"INSERT INTO chats
|
||||
(type, name, grpid, param, created_timestamp)
|
||||
VALUES(?, ?, ?, \'U=1\', ?);",
|
||||
paramsv![
|
||||
Chattype::Broadcast,
|
||||
chat_name,
|
||||
grpid,
|
||||
dc_create_smeared_timestamp(context).await,
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
let chat_id = ChatId::new(u32::try_from(row_id)?);
|
||||
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
msg_id: MsgId::new(0),
|
||||
chat_id: ChatId::new(0),
|
||||
});
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
/// Adds a contact to the `chats_contacts` table.
|
||||
pub(crate) async fn add_to_chat_contacts_table(
|
||||
context: &Context,
|
||||
@@ -2278,8 +2358,8 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
/*this also makes sure, not contacts are added to special or normal chats*/
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
ensure!(
|
||||
chat.typ == Chattype::Group,
|
||||
"{} is not a group where one can add members",
|
||||
chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast,
|
||||
"{} is not a group/broadcast where one can add members",
|
||||
chat_id
|
||||
);
|
||||
ensure!(
|
||||
@@ -2288,14 +2368,18 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
contact_id
|
||||
);
|
||||
ensure!(!chat.is_mailing_list(), "Mailing lists can't be changed");
|
||||
ensure!(
|
||||
chat.typ != Chattype::Broadcast || contact_id != DC_CONTACT_ID_SELF,
|
||||
"Cannot add SELF to broadcast."
|
||||
);
|
||||
|
||||
if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await? {
|
||||
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
|
||||
if !chat.is_self_in_chat(context).await? {
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
"Cannot add contact to group; self not in group.".into(),
|
||||
));
|
||||
bail!("can not add contact because our account is not part of it");
|
||||
}
|
||||
|
||||
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
|
||||
chat.param.remove(Param::Unpromoted);
|
||||
chat.update_param(context).await?;
|
||||
@@ -2334,7 +2418,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
}
|
||||
add_to_chat_contacts_table(context, chat_id, contact_id).await?;
|
||||
}
|
||||
if chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 0 {
|
||||
if chat.typ == Chattype::Group && chat.is_promoted() {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
|
||||
msg.text = Some(
|
||||
@@ -2499,14 +2583,14 @@ pub async fn remove_contact_from_chat(
|
||||
/* we do not check if "contact_id" exists but just delete all records with the id from chats_contacts */
|
||||
/* this allows to delete pending references to deleted contacts. Of course, this should _not_ happen. */
|
||||
if let Ok(chat) = Chat::load_from_db(context, chat_id).await {
|
||||
if chat.typ == Chattype::Group {
|
||||
if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await? {
|
||||
if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
|
||||
if !chat.is_self_in_chat(context).await? {
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
"Cannot remove contact from chat; self not in group.".into(),
|
||||
));
|
||||
} else {
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
|
||||
if chat.is_promoted() {
|
||||
if chat.typ == Chattype::Group && chat.is_promoted() {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
if contact.id == DC_CONTACT_ID_SELF {
|
||||
set_group_explicitly_left(context, &chat.grpid).await?;
|
||||
@@ -2593,48 +2677,47 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
let mut msg = Message::default();
|
||||
|
||||
if chat.typ == Chattype::Group || chat.typ == Chattype::Mailinglist {
|
||||
if chat.typ == Chattype::Group
|
||||
|| chat.typ == Chattype::Mailinglist
|
||||
|| chat.typ == Chattype::Broadcast
|
||||
{
|
||||
if chat.name == new_name {
|
||||
success = true;
|
||||
} else if !is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF).await? {
|
||||
} else if !chat.is_self_in_chat(context).await? {
|
||||
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||
"Cannot set chat name; self not in group".into(),
|
||||
));
|
||||
} else {
|
||||
/* we should respect this - whatever we send to the group, it gets discarded anyway! */
|
||||
if context
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET name=? WHERE id=?;",
|
||||
paramsv![new_name.to_string(), chat_id],
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
if chat.is_promoted() && !chat.is_mailing_list() {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
msg.text = Some(
|
||||
stock_str::msg_grp_name(
|
||||
context,
|
||||
&chat.name,
|
||||
&new_name,
|
||||
DC_CONTACT_ID_SELF as u32,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
msg.param.set_cmd(SystemMessage::GroupNameChanged);
|
||||
if !chat.name.is_empty() {
|
||||
msg.param.set(Param::Arg, &chat.name);
|
||||
}
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
.await?;
|
||||
if chat.is_promoted() && !chat.is_mailing_list() && chat.typ != Chattype::Broadcast {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
msg.text = Some(
|
||||
stock_str::msg_grp_name(
|
||||
context,
|
||||
&chat.name,
|
||||
&new_name,
|
||||
DC_CONTACT_ID_SELF as u32,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
msg.param.set_cmd(SystemMessage::GroupNameChanged);
|
||||
if !chat.name.is_empty() {
|
||||
msg.param.set(Param::Arg, &chat.name);
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
success = true;
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4243,4 +4326,49 @@ mod tests {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_broadcast() -> Result<()> {
|
||||
// create two context, send two messages so both know the other
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await;
|
||||
send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
|
||||
let chat_bob = bob.create_chat(&alice).await;
|
||||
send_text_msg(&bob, chat_bob.id, "ho!".to_string()).await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
// test broadcast list
|
||||
let broadcast_id = create_broadcast_list(&alice).await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
broadcast_id,
|
||||
get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(),
|
||||
)
|
||||
.await?;
|
||||
let chat = Chat::load_from_db(&alice, broadcast_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Broadcast);
|
||||
assert_eq!(chat.name, stock_str::broadcast_list(&alice).await);
|
||||
assert!(!chat.is_self_talk());
|
||||
|
||||
send_text_msg(&alice, broadcast_id, "ola!".to_string()).await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.chat_id, chat.id);
|
||||
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert_eq!(msg.get_text(), Some("ola!".to_string()));
|
||||
assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
|
||||
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Single);
|
||||
assert_eq!(chat.id, chat_bob.id);
|
||||
assert!(!chat.is_self_talk());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,7 +329,7 @@ impl Chatlist {
|
||||
(Some(lastmsg), None)
|
||||
} else {
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Mailinglist => {
|
||||
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
|
||||
let lastcontact = Contact::load_from_db(context, lastmsg.from_id).await?;
|
||||
(Some(lastmsg), Some(lastcontact))
|
||||
}
|
||||
|
||||
@@ -153,6 +153,7 @@ pub enum Chattype {
|
||||
Single = 100,
|
||||
Group = 120,
|
||||
Mailinglist = 140,
|
||||
Broadcast = 160,
|
||||
}
|
||||
|
||||
impl Default for Chattype {
|
||||
@@ -348,6 +349,7 @@ mod tests {
|
||||
assert_eq!(Chattype::Single, Chattype::from_i32(100).unwrap());
|
||||
assert_eq!(Chattype::Group, Chattype::from_i32(120).unwrap());
|
||||
assert_eq!(Chattype::Mailinglist, Chattype::from_i32(140).unwrap());
|
||||
assert_eq!(Chattype::Broadcast, Chattype::from_i32(160).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -604,7 +604,7 @@ impl Message {
|
||||
|
||||
let contact = if self.from_id != DC_CONTACT_ID_SELF {
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Mailinglist => {
|
||||
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
|
||||
Some(Contact::get_by_id(context, self.from_id).await?)
|
||||
}
|
||||
Chattype::Single | Chattype::Undefined => None,
|
||||
@@ -1599,7 +1599,7 @@ async fn ndn_maybe_add_info_msg(
|
||||
chat_type: Chattype,
|
||||
) -> Result<()> {
|
||||
match chat_type {
|
||||
Chattype::Group => {
|
||||
Chattype::Group | Chattype::Broadcast => {
|
||||
if let Some(failed_recipient) = &failed.failed_recipient {
|
||||
let contact_id =
|
||||
Contact::lookup_id_by_addr(context, failed_recipient, Origin::Unknown)
|
||||
|
||||
@@ -316,6 +316,10 @@ impl<'a> MimeFactory<'a> {
|
||||
Loaded::Message { chat } => {
|
||||
if chat.is_protected() {
|
||||
false
|
||||
} else if chat.typ == Chattype::Broadcast {
|
||||
// encryption may disclose recipients;
|
||||
// this is probably a worse issue than not opportunistically (!) encrypting
|
||||
true
|
||||
} else {
|
||||
self.msg
|
||||
.param
|
||||
@@ -393,8 +397,6 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
|
||||
if chat.typ == Chattype::Group && quoted_msg_subject.is_none_or_empty() {
|
||||
// If we have a `quoted_msg_subject`, we use the subject of the quoted message
|
||||
// instead of the group name
|
||||
let re = if self.in_reply_to.is_empty() {
|
||||
""
|
||||
} else {
|
||||
@@ -403,22 +405,22 @@ impl<'a> MimeFactory<'a> {
|
||||
return Ok(format!("{}{}", re, chat.name));
|
||||
}
|
||||
|
||||
let parent_subject = if quoted_msg_subject.is_none_or_empty() {
|
||||
chat.param.get(Param::LastSubject)
|
||||
} else {
|
||||
quoted_msg_subject.as_deref()
|
||||
};
|
||||
|
||||
if let Some(last_subject) = parent_subject {
|
||||
format!("Re: {}", remove_subject_prefix(last_subject))
|
||||
} else {
|
||||
let self_name = match context.get_config(Config::Displayname).await? {
|
||||
Some(name) => name,
|
||||
None => context.get_config(Config::Addr).await?.unwrap_or_default(),
|
||||
if chat.typ != Chattype::Broadcast {
|
||||
let parent_subject = if quoted_msg_subject.is_none_or_empty() {
|
||||
chat.param.get(Param::LastSubject)
|
||||
} else {
|
||||
quoted_msg_subject.as_deref()
|
||||
};
|
||||
|
||||
stock_str::subject_for_new_contact(context, self_name).await
|
||||
if let Some(last_subject) = parent_subject {
|
||||
return Ok(format!("Re: {}", remove_subject_prefix(last_subject)));
|
||||
}
|
||||
}
|
||||
|
||||
let self_name = match context.get_config(Config::Displayname).await? {
|
||||
Some(name) => name,
|
||||
None => context.get_config(Config::Addr).await?.unwrap_or_default(),
|
||||
};
|
||||
stock_str::subject_for_new_contact(context, self_name).await
|
||||
}
|
||||
Loaded::Mdn { .. } => stock_str::read_rcpt(context).await,
|
||||
};
|
||||
@@ -555,9 +557,21 @@ impl<'a> MimeFactory<'a> {
|
||||
render_rfc724_mid(&rfc724_mid),
|
||||
));
|
||||
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new_with_value("To".into(), to).unwrap());
|
||||
let undisclosed_recipients = match &self.loaded {
|
||||
Loaded::Message { chat } => chat.typ == Chattype::Broadcast,
|
||||
Loaded::Mdn { .. } => false,
|
||||
};
|
||||
|
||||
if undisclosed_recipients {
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new("To".into(), "hidden-recipients: ;".to_string()));
|
||||
} else {
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new_with_value("To".into(), to).unwrap());
|
||||
}
|
||||
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new_with_value("From".into(), vec![from]).unwrap());
|
||||
|
||||
@@ -311,6 +311,9 @@ pub enum StockMessage {
|
||||
|
||||
#[strum(props(fallback = "Messages"))]
|
||||
Messages = 114,
|
||||
|
||||
#[strum(props(fallback = "Broadcast List"))]
|
||||
BroadcastList = 115,
|
||||
}
|
||||
|
||||
impl StockMessage {
|
||||
@@ -985,6 +988,12 @@ pub(crate) async fn messages(context: &Context) -> String {
|
||||
translated(context, StockMessage::Messages).await
|
||||
}
|
||||
|
||||
/// Stock string: `Broadcast List`.
|
||||
/// Used as the default name for broadcast lists; a number may be added.
|
||||
pub(crate) async fn broadcast_list(context: &Context) -> String {
|
||||
translated(context, StockMessage::BroadcastList).await
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Set the stock string for the [StockMessage].
|
||||
///
|
||||
|
||||
@@ -72,7 +72,7 @@ impl Summary {
|
||||
}
|
||||
} else {
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Mailinglist => {
|
||||
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
|
||||
if msg.is_info() || contact.is_none() {
|
||||
None
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user