mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 09:26:29 +03:00
feat: Group and broadcast channel descriptions (#7829)
fix https://github.com/chatmail/core/issues/7766 Implementation notes: - Descriptions are only sent with member additions, when the description is changed, and when promoting a previously-unpromoted group, in order not to waste bandwith. - Descriptions are not loaded everytime a chat object is loaded, because they are only needed for the profile. Instead, they are in their own table, and can be loaded with their own JsonRPC call. --------- Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
This commit is contained in:
@@ -1854,15 +1854,16 @@ int dc_remove_contact_from_chat (dc_context_t* context, uint32_t ch
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set group name.
|
* Set the name of a group or broadcast channel.
|
||||||
*
|
*
|
||||||
* If the group is already _promoted_ (any message was sent to the group),
|
* If the group is already _promoted_ (any message was sent to the group),
|
||||||
* all group members are informed by a special status message that is sent automatically by this function.
|
* or if this is a brodacast channel,
|
||||||
|
* all members are informed by a special status message that is sent automatically by this function.
|
||||||
*
|
*
|
||||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||||
*
|
*
|
||||||
* @memberof dc_context_t
|
* @memberof dc_context_t
|
||||||
* @param chat_id The chat ID to set the name for. Must be a group chat.
|
* @param chat_id The chat ID to set the name for. Must be a group chat or broadcast channel.
|
||||||
* @param name New name of the group.
|
* @param name New name of the group.
|
||||||
* @param context The context object.
|
* @param context The context object.
|
||||||
* @return 1=success, 0=error
|
* @return 1=success, 0=error
|
||||||
@@ -1889,10 +1890,11 @@ int dc_set_chat_name (dc_context_t* context, uint32_t ch
|
|||||||
int dc_set_chat_ephemeral_timer (dc_context_t* context, uint32_t chat_id, uint32_t timer);
|
int dc_set_chat_ephemeral_timer (dc_context_t* context, uint32_t chat_id, uint32_t timer);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set group profile image.
|
* Set group or broadcast channel profile image.
|
||||||
*
|
*
|
||||||
* If the group is already _promoted_ (any message was sent to the group),
|
* If the group is already _promoted_ (any message was sent to the group),
|
||||||
* all group members are informed by a special status message that is sent automatically by this function.
|
* or if this is a brodacast channel,
|
||||||
|
* all members are informed by a special status message that is sent automatically by this function.
|
||||||
*
|
*
|
||||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||||
*
|
*
|
||||||
@@ -1900,7 +1902,7 @@ int dc_set_chat_ephemeral_timer (dc_context_t* context, uint32_t chat_id, uint32
|
|||||||
*
|
*
|
||||||
* @memberof dc_context_t
|
* @memberof dc_context_t
|
||||||
* @param context The context object.
|
* @param context The context object.
|
||||||
* @param chat_id The chat ID to set the image for.
|
* @param chat_id The chat ID to set the image for. Must be a group chat or broadcast channel.
|
||||||
* @param image Full path of the image to use as the group image. The image will immediately be copied to the
|
* @param image Full path of the image to use as the group image. The image will immediately be copied to the
|
||||||
* `blobdir`; the original image will not be needed anymore.
|
* `blobdir`; the original image will not be needed anymore.
|
||||||
* If you pass NULL here, the group image is deleted (for promoted groups, all members are informed about
|
* If you pass NULL here, the group image is deleted (for promoted groups, all members are informed about
|
||||||
|
|||||||
@@ -1068,7 +1068,8 @@ impl CommandApi {
|
|||||||
/// Set group name.
|
/// Set group name.
|
||||||
///
|
///
|
||||||
/// If the group is already _promoted_ (any message was sent to the group),
|
/// If the group is already _promoted_ (any message was sent to the group),
|
||||||
/// all group members are informed by a special status message that is sent automatically by this function.
|
/// or if this is a brodacast channel,
|
||||||
|
/// all members are informed by a special status message that is sent automatically by this function.
|
||||||
///
|
///
|
||||||
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||||
async fn set_chat_name(&self, account_id: u32, chat_id: u32, new_name: String) -> Result<()> {
|
async fn set_chat_name(&self, account_id: u32, chat_id: u32, new_name: String) -> Result<()> {
|
||||||
@@ -1076,10 +1077,39 @@ impl CommandApi {
|
|||||||
chat::set_chat_name(&ctx, ChatId::new(chat_id), &new_name).await
|
chat::set_chat_name(&ctx, ChatId::new(chat_id), &new_name).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set group or broadcast channel description.
|
||||||
|
///
|
||||||
|
/// If the group is already _promoted_ (any message was sent to the group),
|
||||||
|
/// or if this is a brodacast channel,
|
||||||
|
/// all members are informed by a special status message that is sent automatically by this function.
|
||||||
|
///
|
||||||
|
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||||
|
///
|
||||||
|
/// See also [`Self::get_chat_description`] / `getChatDescription()`.
|
||||||
|
async fn set_chat_description(
|
||||||
|
&self,
|
||||||
|
account_id: u32,
|
||||||
|
chat_id: u32,
|
||||||
|
description: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
chat::set_chat_description(&ctx, ChatId::new(chat_id), &description).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the chat description from the database.
|
||||||
|
///
|
||||||
|
/// UIs show this in the profile page of the chat,
|
||||||
|
/// it is settable by [`Self::set_chat_description`] / `setChatDescription()`.
|
||||||
|
async fn get_chat_description(&self, account_id: u32, chat_id: u32) -> Result<String> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
chat::get_chat_description(&ctx, ChatId::new(chat_id)).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Set group profile image.
|
/// Set group profile image.
|
||||||
///
|
///
|
||||||
/// If the group is already _promoted_ (any message was sent to the group),
|
/// If the group is already _promoted_ (any message was sent to the group),
|
||||||
/// all group members are informed by a special status message that is sent automatically by this function.
|
/// or if this is a brodacast channel,
|
||||||
|
/// all members are informed by a special status message that is sent automatically by this function.
|
||||||
///
|
///
|
||||||
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -388,6 +388,7 @@ impl From<download::DownloadState> for DownloadState {
|
|||||||
pub enum SystemMessageType {
|
pub enum SystemMessageType {
|
||||||
Unknown,
|
Unknown,
|
||||||
GroupNameChanged,
|
GroupNameChanged,
|
||||||
|
GroupDescriptionChanged,
|
||||||
GroupImageChanged,
|
GroupImageChanged,
|
||||||
MemberAddedToGroup,
|
MemberAddedToGroup,
|
||||||
MemberRemovedFromGroup,
|
MemberRemovedFromGroup,
|
||||||
@@ -440,6 +441,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
|||||||
match system_message_type {
|
match system_message_type {
|
||||||
SystemMessage::Unknown => SystemMessageType::Unknown,
|
SystemMessage::Unknown => SystemMessageType::Unknown,
|
||||||
SystemMessage::GroupNameChanged => SystemMessageType::GroupNameChanged,
|
SystemMessage::GroupNameChanged => SystemMessageType::GroupNameChanged,
|
||||||
|
SystemMessage::GroupDescriptionChanged => SystemMessageType::GroupDescriptionChanged,
|
||||||
SystemMessage::GroupImageChanged => SystemMessageType::GroupImageChanged,
|
SystemMessage::GroupImageChanged => SystemMessageType::GroupImageChanged,
|
||||||
SystemMessage::MemberAddedToGroup => SystemMessageType::MemberAddedToGroup,
|
SystemMessage::MemberAddedToGroup => SystemMessageType::MemberAddedToGroup,
|
||||||
SystemMessage::MemberRemovedFromGroup => SystemMessageType::MemberRemovedFromGroup,
|
SystemMessage::MemberRemovedFromGroup => SystemMessageType::MemberRemovedFromGroup,
|
||||||
|
|||||||
@@ -343,6 +343,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
addmember <contact-id>\n\
|
addmember <contact-id>\n\
|
||||||
removemember <contact-id>\n\
|
removemember <contact-id>\n\
|
||||||
groupname <name>\n\
|
groupname <name>\n\
|
||||||
|
groupdescription <description>\n\
|
||||||
groupimage <image>\n\
|
groupimage <image>\n\
|
||||||
chatinfo\n\
|
chatinfo\n\
|
||||||
sendlocations <seconds>\n\
|
sendlocations <seconds>\n\
|
||||||
@@ -770,6 +771,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
|
|
||||||
println!("Chat name set");
|
println!("Chat name set");
|
||||||
}
|
}
|
||||||
|
"groupdescription" => {
|
||||||
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
|
ensure!(!arg1.is_empty(), "Argument <description> missing.");
|
||||||
|
chat::set_chat_description(&context, sel_chat.as_ref().unwrap().get_id(), arg1).await?;
|
||||||
|
|
||||||
|
println!("Chat description set");
|
||||||
|
}
|
||||||
"groupimage" => {
|
"groupimage" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
ensure!(!arg1.is_empty(), "Argument <image> missing.");
|
ensure!(!arg1.is_empty(), "Argument <image> missing.");
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ const DB_COMMANDS: [&str; 11] = [
|
|||||||
"housekeeping",
|
"housekeeping",
|
||||||
];
|
];
|
||||||
|
|
||||||
const CHAT_COMMANDS: [&str; 39] = [
|
const CHAT_COMMANDS: [&str; 40] = [
|
||||||
"listchats",
|
"listchats",
|
||||||
"listarchived",
|
"listarchived",
|
||||||
"start-realtime",
|
"start-realtime",
|
||||||
@@ -192,6 +192,7 @@ const CHAT_COMMANDS: [&str; 39] = [
|
|||||||
"addmember",
|
"addmember",
|
||||||
"removemember",
|
"removemember",
|
||||||
"groupname",
|
"groupname",
|
||||||
|
"groupdescription",
|
||||||
"groupimage",
|
"groupimage",
|
||||||
"chatinfo",
|
"chatinfo",
|
||||||
"sendlocations",
|
"sendlocations",
|
||||||
|
|||||||
128
src/chat.rs
128
src/chat.rs
@@ -1763,10 +1763,11 @@ impl Chat {
|
|||||||
} else if matches!(self.typ, Chattype::Group | Chattype::OutBroadcast)
|
} else if matches!(self.typ, Chattype::Group | Chattype::OutBroadcast)
|
||||||
&& self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
|
&& self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
|
||||||
{
|
{
|
||||||
msg.param.set_int(Param::AttachGroupImage, 1);
|
msg.param.set_int(Param::AttachChatAvatarAndDescription, 1);
|
||||||
self.param
|
self.param
|
||||||
.remove(Param::Unpromoted)
|
.remove(Param::Unpromoted)
|
||||||
.set_i64(Param::GroupNameTimestamp, msg.timestamp_sort);
|
.set_i64(Param::GroupNameTimestamp, msg.timestamp_sort)
|
||||||
|
.set_i64(Param::GroupDescriptionTimestamp, msg.timestamp_sort);
|
||||||
self.update_param(context).await?;
|
self.update_param(context).await?;
|
||||||
// TODO: Remove this compat code needed because Core <= v1.143:
|
// TODO: Remove this compat code needed because Core <= v1.143:
|
||||||
// - doesn't accept synchronization of QR code tokens for unpromoted groups, so we also
|
// - doesn't accept synchronization of QR code tokens for unpromoted groups, so we also
|
||||||
@@ -2807,9 +2808,18 @@ async fn render_mime_message_and_pre_message(
|
|||||||
///
|
///
|
||||||
/// The caller has to interrupt SMTP loop or otherwise process new rows.
|
/// The caller has to interrupt SMTP loop or otherwise process new rows.
|
||||||
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
|
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
|
||||||
if msg.param.get_cmd() == SystemMessage::GroupNameChanged {
|
let cmd = msg.param.get_cmd();
|
||||||
|
if cmd == SystemMessage::GroupNameChanged || cmd == SystemMessage::GroupDescriptionChanged {
|
||||||
msg.chat_id
|
msg.chat_id
|
||||||
.update_timestamp(context, Param::GroupNameTimestamp, msg.timestamp_sort)
|
.update_timestamp(
|
||||||
|
context,
|
||||||
|
if cmd == SystemMessage::GroupNameChanged {
|
||||||
|
Param::GroupNameTimestamp
|
||||||
|
} else {
|
||||||
|
Param::GroupDescriptionTimestamp
|
||||||
|
},
|
||||||
|
msg.timestamp_sort,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3882,9 +3892,11 @@ pub(crate) async fn add_contact_to_chat_ex(
|
|||||||
|
|
||||||
let sync_qr_code_tokens;
|
let sync_qr_code_tokens;
|
||||||
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
|
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
|
||||||
|
let smeared_time = smeared_time(context);
|
||||||
chat.param
|
chat.param
|
||||||
.remove(Param::Unpromoted)
|
.remove(Param::Unpromoted)
|
||||||
.set_i64(Param::GroupNameTimestamp, smeared_time(context));
|
.set_i64(Param::GroupNameTimestamp, smeared_time)
|
||||||
|
.set_i64(Param::GroupDescriptionTimestamp, smeared_time);
|
||||||
chat.update_param(context).await?;
|
chat.update_param(context).await?;
|
||||||
sync_qr_code_tokens = true;
|
sync_qr_code_tokens = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -4187,7 +4199,107 @@ async fn send_member_removal_msg(
|
|||||||
send_msg(context, chat.id, &mut msg).await
|
send_msg(context, chat.id, &mut msg).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets group or mailing list chat name.
|
/// Set group or broadcast channel description.
|
||||||
|
///
|
||||||
|
/// If the group is already _promoted_ (any message was sent to the group),
|
||||||
|
/// or if this is a brodacast channel,
|
||||||
|
/// all members are informed by a special status message that is sent automatically by this function.
|
||||||
|
///
|
||||||
|
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||||
|
///
|
||||||
|
/// See also [`get_chat_description`]
|
||||||
|
pub async fn set_chat_description(
|
||||||
|
context: &Context,
|
||||||
|
chat_id: ChatId,
|
||||||
|
new_description: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
set_chat_description_ex(context, Sync, chat_id, new_description).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_chat_description_ex(
|
||||||
|
context: &Context,
|
||||||
|
mut sync: sync::Sync,
|
||||||
|
chat_id: ChatId,
|
||||||
|
new_description: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let new_description = sanitize_bidi_characters(new_description.trim());
|
||||||
|
|
||||||
|
ensure!(!chat_id.is_special(), "Invalid chat ID");
|
||||||
|
|
||||||
|
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||||
|
ensure!(
|
||||||
|
chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast,
|
||||||
|
"Can only set description for groups / broadcasts"
|
||||||
|
);
|
||||||
|
ensure!(
|
||||||
|
!chat.grpid.is_empty(),
|
||||||
|
"Cannot set description for ad hoc groups"
|
||||||
|
);
|
||||||
|
if !chat.is_self_in_chat(context).await? {
|
||||||
|
context.emit_event(EventType::ErrorSelfNotInGroup(
|
||||||
|
"Cannot set chat description; self not in group".into(),
|
||||||
|
));
|
||||||
|
bail!("Cannot set chat description; self not in group");
|
||||||
|
}
|
||||||
|
|
||||||
|
let affected_rows = context
|
||||||
|
.sql
|
||||||
|
.execute(
|
||||||
|
"INSERT INTO chats_descriptions(chat_id, description) VALUES(?, ?)
|
||||||
|
ON CONFLICT(chat_id) DO UPDATE
|
||||||
|
SET description=excluded.description WHERE description<>excluded.description",
|
||||||
|
(chat_id, &new_description),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if affected_rows == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if chat.is_promoted() {
|
||||||
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
|
msg.text = stock_str::msg_chat_description_changed(context, ContactId::SELF).await;
|
||||||
|
msg.param.set_cmd(SystemMessage::GroupDescriptionChanged);
|
||||||
|
|
||||||
|
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||||
|
context.emit_msgs_changed(chat_id, msg.id);
|
||||||
|
sync = Nosync;
|
||||||
|
}
|
||||||
|
context.emit_event(EventType::ChatModified(chat_id));
|
||||||
|
|
||||||
|
if sync.into() {
|
||||||
|
chat.sync(context, SyncAction::SetDescription(new_description))
|
||||||
|
.await
|
||||||
|
.log_err(context)
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the chat description from the database.
|
||||||
|
///
|
||||||
|
/// UIs show this in the profile page of the chat,
|
||||||
|
/// it is settable by [`set_chat_description`]
|
||||||
|
pub async fn get_chat_description(context: &Context, chat_id: ChatId) -> Result<String> {
|
||||||
|
let description = context
|
||||||
|
.sql
|
||||||
|
.query_get_value(
|
||||||
|
"SELECT description FROM chats_descriptions WHERE chat_id=?",
|
||||||
|
(chat_id,),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.unwrap_or_default();
|
||||||
|
Ok(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets group, mailing list, or broadcast channel chat name.
|
||||||
|
///
|
||||||
|
/// If the group is already _promoted_ (any message was sent to the group),
|
||||||
|
/// or if this is a brodacast channel,
|
||||||
|
/// all members are informed by a special status message that is sent automatically by this function.
|
||||||
|
///
|
||||||
|
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||||
pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -> Result<()> {
|
pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -> Result<()> {
|
||||||
rename_ex(context, Sync, chat_id, new_name).await
|
rename_ex(context, Sync, chat_id, new_name).await
|
||||||
}
|
}
|
||||||
@@ -5015,6 +5127,7 @@ pub(crate) enum SyncAction {
|
|||||||
///
|
///
|
||||||
/// The list is a list of pairs of fingerprint and address.
|
/// The list is a list of pairs of fingerprint and address.
|
||||||
SetPgpContacts(Vec<(String, String)>),
|
SetPgpContacts(Vec<(String, String)>),
|
||||||
|
SetDescription(String),
|
||||||
Delete,
|
Delete,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5113,6 +5226,9 @@ impl Context {
|
|||||||
Err(anyhow!("sync_alter_chat({id:?}, {action:?}): Bad request."))
|
Err(anyhow!("sync_alter_chat({id:?}, {action:?}): Bad request."))
|
||||||
}
|
}
|
||||||
SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await,
|
SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await,
|
||||||
|
SyncAction::SetDescription(to) => {
|
||||||
|
set_chat_description_ex(self, Nosync, chat_id, to).await
|
||||||
|
}
|
||||||
SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
|
SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
|
||||||
SyncAction::SetPgpContacts(fingerprint_addrs) => {
|
SyncAction::SetPgpContacts(fingerprint_addrs) => {
|
||||||
set_contacts_by_fingerprints(self, chat_id, fingerprint_addrs).await
|
set_contacts_by_fingerprints(self, chat_id, fingerprint_addrs).await
|
||||||
|
|||||||
@@ -3156,6 +3156,119 @@ async fn test_broadcasts_name_and_avatar() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_chat_description_basic() {
|
||||||
|
test_chat_description("", false).await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_chat_description_unpromoted_description() {
|
||||||
|
test_chat_description("Unpromoted description in the beginning", false)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_chat_description_qr() {
|
||||||
|
test_chat_description("", true).await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_chat_description_unpromoted_description_qr() {
|
||||||
|
test_chat_description("Unpromoted description in the beginning", true)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_chat_description(initial_description: &str, join_via_qr: bool) -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = &tcm.alice().await;
|
||||||
|
let alice2 = &tcm.alice().await;
|
||||||
|
let bob = &tcm.bob().await;
|
||||||
|
|
||||||
|
alice.set_config_bool(Config::SyncMsgs, true).await?;
|
||||||
|
alice2.set_config_bool(Config::SyncMsgs, true).await?;
|
||||||
|
|
||||||
|
tcm.section("Create a group chat, and add Bob");
|
||||||
|
let alice_chat_id = create_group(alice, "My Group").await?;
|
||||||
|
|
||||||
|
if !initial_description.is_empty() {
|
||||||
|
set_chat_description(alice, alice_chat_id, initial_description).await?;
|
||||||
|
}
|
||||||
|
sync(alice, alice2).await;
|
||||||
|
|
||||||
|
let alice2_chat_id = get_chat_id_by_grpid(
|
||||||
|
alice2,
|
||||||
|
&Chat::load_from_db(alice, alice_chat_id).await?.grpid,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
assert_eq!(
|
||||||
|
get_chat_description(alice2, alice2_chat_id).await?,
|
||||||
|
initial_description
|
||||||
|
);
|
||||||
|
|
||||||
|
let bob_chat_id = if join_via_qr {
|
||||||
|
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await.unwrap();
|
||||||
|
tcm.exec_securejoin_qr(bob, alice, &qr).await
|
||||||
|
} else {
|
||||||
|
let alice_bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||||
|
add_contact_to_chat(alice, alice_chat_id, alice_bob_id).await?;
|
||||||
|
let sent = alice.send_text(alice_chat_id, "promoting the group").await;
|
||||||
|
bob.recv_msg(&sent).await.chat_id
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
get_chat_description(bob, bob_chat_id).await?,
|
||||||
|
initial_description
|
||||||
|
);
|
||||||
|
|
||||||
|
for description in ["This is a cool group", "", "ä ẟ 😂"] {
|
||||||
|
tcm.section(&format!(
|
||||||
|
"Alice sets the chat description to '{description}'"
|
||||||
|
));
|
||||||
|
set_chat_description(alice, alice_chat_id, description).await?;
|
||||||
|
let sent = alice.pop_sent_msg().await;
|
||||||
|
assert_eq!(
|
||||||
|
sent.load_from_db().await.text,
|
||||||
|
"You changed the chat description."
|
||||||
|
);
|
||||||
|
|
||||||
|
tcm.section("Bob receives the description change");
|
||||||
|
let rcvd = bob.recv_msg(&sent).await;
|
||||||
|
assert_eq!(rcvd.get_info_type(), SystemMessage::GroupDescriptionChanged);
|
||||||
|
assert_eq!(rcvd.text, "Chat description changed by alice@example.org.");
|
||||||
|
|
||||||
|
assert_eq!(get_chat_description(bob, rcvd.chat_id).await?, description);
|
||||||
|
|
||||||
|
tcm.section("Check Alice's second device");
|
||||||
|
alice2.recv_msg(&sent).await;
|
||||||
|
let alice2_chat_id = get_chat_id_by_grpid(
|
||||||
|
alice2,
|
||||||
|
&Chat::load_from_db(alice, alice_chat_id).await?.grpid,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_chat_description(alice2, alice2_chat_id).await?,
|
||||||
|
description
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tcm.section("Alice calls set_chat_description() without actually changing the description");
|
||||||
|
set_chat_description(alice, alice_chat_id, "ä ẟ 😂").await?;
|
||||||
|
assert!(
|
||||||
|
alice
|
||||||
|
.pop_sent_msg_opt(Duration::from_secs(0))
|
||||||
|
.await
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Tests that directly after broadcast-securejoin,
|
/// Tests that directly after broadcast-securejoin,
|
||||||
/// the brodacast is shown correctly on both devices.
|
/// the brodacast is shown correctly on both devices.
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ pub enum HeaderDef {
|
|||||||
ChatGroupName,
|
ChatGroupName,
|
||||||
ChatGroupNameChanged,
|
ChatGroupNameChanged,
|
||||||
ChatGroupNameTimestamp,
|
ChatGroupNameTimestamp,
|
||||||
|
ChatGroupDescription,
|
||||||
|
ChatGroupDescriptionChanged,
|
||||||
|
ChatGroupDescriptionTimestamp,
|
||||||
ChatVerified,
|
ChatVerified,
|
||||||
ChatGroupAvatar,
|
ChatGroupAvatar,
|
||||||
ChatUserAvatar,
|
ChatUserAvatar,
|
||||||
|
|||||||
@@ -1005,6 +1005,7 @@ impl Message {
|
|||||||
pub async fn get_info_contact_id(&self, context: &Context) -> Result<Option<ContactId>> {
|
pub async fn get_info_contact_id(&self, context: &Context) -> Result<Option<ContactId>> {
|
||||||
match self.param.get_cmd() {
|
match self.param.get_cmd() {
|
||||||
SystemMessage::GroupNameChanged
|
SystemMessage::GroupNameChanged
|
||||||
|
| SystemMessage::GroupDescriptionChanged
|
||||||
| SystemMessage::GroupImageChanged
|
| SystemMessage::GroupImageChanged
|
||||||
| SystemMessage::EphemeralTimerChanged => {
|
| SystemMessage::EphemeralTimerChanged => {
|
||||||
if self.from_id != ContactId::INFO {
|
if self.from_id != ContactId::INFO {
|
||||||
|
|||||||
@@ -663,7 +663,7 @@ impl MimeFactory {
|
|||||||
|
|
||||||
if msg
|
if msg
|
||||||
.param
|
.param
|
||||||
.get_bool(Param::AttachGroupImage)
|
.get_bool(Param::AttachChatAvatarAndDescription)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
{
|
{
|
||||||
return chat.param.get(Param::ProfileImage).map(Into::into);
|
return chat.param.get(Param::ProfileImage).map(Into::into);
|
||||||
@@ -1669,6 +1669,12 @@ impl MimeFactory {
|
|||||||
mail_builder::headers::text::Text::new(old_name).into(),
|
mail_builder::headers::text::Text::new(old_name).into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
SystemMessage::GroupDescriptionChanged => {
|
||||||
|
headers.push((
|
||||||
|
"Chat-Group-Description-Changed",
|
||||||
|
mail_builder::headers::text::Text::new("").into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
SystemMessage::GroupImageChanged => {
|
SystemMessage::GroupImageChanged => {
|
||||||
headers.push((
|
headers.push((
|
||||||
"Chat-Content",
|
"Chat-Content",
|
||||||
@@ -1683,6 +1689,26 @@ impl MimeFactory {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if command == SystemMessage::GroupDescriptionChanged
|
||||||
|
|| command == SystemMessage::MemberAddedToGroup
|
||||||
|
|| msg
|
||||||
|
.param
|
||||||
|
.get_bool(Param::AttachChatAvatarAndDescription)
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
let description = chat::get_chat_description(context, chat.id).await?;
|
||||||
|
headers.push((
|
||||||
|
"Chat-Group-Description",
|
||||||
|
mail_builder::headers::text::Text::new(description.clone()).into(),
|
||||||
|
));
|
||||||
|
if let Some(ts) = chat.param.get_i64(Param::GroupDescriptionTimestamp) {
|
||||||
|
headers.push((
|
||||||
|
"Chat-Group-Description-Timestamp",
|
||||||
|
mail_builder::headers::text::Text::new(ts.to_string()).into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match command {
|
match command {
|
||||||
|
|||||||
@@ -186,10 +186,10 @@ pub enum SystemMessage {
|
|||||||
#[default]
|
#[default]
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
|
|
||||||
/// Group name changed.
|
/// Group or broadcast channel name changed.
|
||||||
GroupNameChanged = 2,
|
GroupNameChanged = 2,
|
||||||
|
|
||||||
/// Group avatar changed.
|
/// Group or broadcast channel avatar changed.
|
||||||
GroupImageChanged = 3,
|
GroupImageChanged = 3,
|
||||||
|
|
||||||
/// Member was added to the group.
|
/// Member was added to the group.
|
||||||
@@ -254,6 +254,9 @@ pub enum SystemMessage {
|
|||||||
|
|
||||||
/// Message indicating that a call was ended.
|
/// Message indicating that a call was ended.
|
||||||
CallEnded = 67,
|
CallEnded = 67,
|
||||||
|
|
||||||
|
/// Group or broadcast channel description changed.
|
||||||
|
GroupDescriptionChanged = 70,
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
|
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
|
||||||
@@ -773,6 +776,11 @@ impl MimeMessage {
|
|||||||
self.is_system_message = SystemMessage::MemberAddedToGroup;
|
self.is_system_message = SystemMessage::MemberAddedToGroup;
|
||||||
} else if self.get_header(HeaderDef::ChatGroupNameChanged).is_some() {
|
} else if self.get_header(HeaderDef::ChatGroupNameChanged).is_some() {
|
||||||
self.is_system_message = SystemMessage::GroupNameChanged;
|
self.is_system_message = SystemMessage::GroupNameChanged;
|
||||||
|
} else if self
|
||||||
|
.get_header(HeaderDef::ChatGroupDescriptionChanged)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
self.is_system_message = SystemMessage::GroupDescriptionChanged;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ pub enum Param {
|
|||||||
Arg4 = b'H',
|
Arg4 = b'H',
|
||||||
|
|
||||||
/// For Messages
|
/// For Messages
|
||||||
AttachGroupImage = b'A',
|
AttachChatAvatarAndDescription = b'A',
|
||||||
|
|
||||||
/// For Messages
|
/// For Messages
|
||||||
WebrtcRoom = b'V',
|
WebrtcRoom = b'V',
|
||||||
@@ -219,6 +219,9 @@ pub enum Param {
|
|||||||
/// For Chats: timestamp of group name update.
|
/// For Chats: timestamp of group name update.
|
||||||
GroupNameTimestamp = b'g',
|
GroupNameTimestamp = b'g',
|
||||||
|
|
||||||
|
/// For Chats: timestamp of chat description update.
|
||||||
|
GroupDescriptionTimestamp = b'6',
|
||||||
|
|
||||||
/// For Chats: timestamp of member list update.
|
/// For Chats: timestamp of member list update.
|
||||||
MemberListTimestamp = b'k',
|
MemberListTimestamp = b'k',
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ use std::sync::LazyLock;
|
|||||||
use anyhow::{Context as _, Result, ensure};
|
use anyhow::{Context as _, Result, ensure};
|
||||||
use data_encoding::BASE32_NOPAD;
|
use data_encoding::BASE32_NOPAD;
|
||||||
use deltachat_contact_tools::{
|
use deltachat_contact_tools::{
|
||||||
ContactAddress, addr_cmp, addr_normalize, may_be_valid_addr, sanitize_single_line,
|
ContactAddress, addr_cmp, addr_normalize, may_be_valid_addr, sanitize_bidi_characters,
|
||||||
|
sanitize_single_line,
|
||||||
};
|
};
|
||||||
use iroh_gossip::proto::TopicId;
|
use iroh_gossip::proto::TopicId;
|
||||||
use mailparse::SingleInfo;
|
use mailparse::SingleInfo;
|
||||||
@@ -3101,7 +3102,7 @@ async fn apply_group_changes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if is_from_in_chat {
|
if is_from_in_chat {
|
||||||
apply_chat_name_and_avatar_changes(
|
apply_chat_name_avatar_and_description_changes(
|
||||||
context,
|
context,
|
||||||
mime_parser,
|
mime_parser,
|
||||||
from_id,
|
from_id,
|
||||||
@@ -3280,7 +3281,7 @@ async fn apply_group_changes(
|
|||||||
///
|
///
|
||||||
/// - `send_event_chat_modified` is set to `true` if ChatModified event should be sent
|
/// - `send_event_chat_modified` is set to `true` if ChatModified event should be sent
|
||||||
/// - `better_msg` is filled with an info message about name change, if necessary
|
/// - `better_msg` is filled with an info message about name change, if necessary
|
||||||
async fn apply_chat_name_and_avatar_changes(
|
async fn apply_chat_name_avatar_and_description_changes(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
mime_parser: &MimeMessage,
|
mime_parser: &MimeMessage,
|
||||||
from_id: ContactId,
|
from_id: ContactId,
|
||||||
@@ -3339,6 +3340,51 @@ async fn apply_chat_name_and_avatar_changes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Apply chat description changes ==========
|
||||||
|
|
||||||
|
if let Some(new_description) = mime_parser
|
||||||
|
.get_header(HeaderDef::ChatGroupDescription)
|
||||||
|
.map(|d| d.trim())
|
||||||
|
{
|
||||||
|
let new_description = sanitize_bidi_characters(new_description.trim());
|
||||||
|
let old_description = chat::get_chat_description(context, chat.id).await?;
|
||||||
|
|
||||||
|
let old_timestamp = chat
|
||||||
|
.param
|
||||||
|
.get_i64(Param::GroupDescriptionTimestamp)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let timestamp_in_header = mime_parser
|
||||||
|
.get_header(HeaderDef::ChatGroupDescriptionTimestamp)
|
||||||
|
.and_then(|s| s.parse::<i64>().ok());
|
||||||
|
|
||||||
|
let new_timestamp = timestamp_in_header.unwrap_or(mime_parser.timestamp_sent);
|
||||||
|
// To provide consistency, compare descriptions if timestamps are equal.
|
||||||
|
if (old_timestamp, &old_description) < (new_timestamp, &new_description)
|
||||||
|
&& chat
|
||||||
|
.id
|
||||||
|
.update_timestamp(context, Param::GroupDescriptionTimestamp, new_timestamp)
|
||||||
|
.await?
|
||||||
|
&& new_description != old_description
|
||||||
|
{
|
||||||
|
info!(context, "Updating description for chat {}.", chat.id);
|
||||||
|
context
|
||||||
|
.sql
|
||||||
|
.execute(
|
||||||
|
"INSERT OR REPLACE INTO chats_descriptions(chat_id, description) VALUES(?, ?)",
|
||||||
|
(chat.id, &new_description),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
*send_event_chat_modified = true;
|
||||||
|
}
|
||||||
|
if mime_parser
|
||||||
|
.get_header(HeaderDef::ChatGroupDescriptionChanged)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
better_msg
|
||||||
|
.get_or_insert(stock_str::msg_chat_description_changed(context, from_id).await);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Apply chat avatar changes ==========
|
// ========== Apply chat avatar changes ==========
|
||||||
|
|
||||||
if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg)
|
if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg)
|
||||||
@@ -3667,7 +3713,7 @@ async fn apply_out_broadcast_changes(
|
|||||||
let mut better_msg = None;
|
let mut better_msg = None;
|
||||||
|
|
||||||
if from_id == ContactId::SELF {
|
if from_id == ContactId::SELF {
|
||||||
apply_chat_name_and_avatar_changes(
|
apply_chat_name_avatar_and_description_changes(
|
||||||
context,
|
context,
|
||||||
mime_parser,
|
mime_parser,
|
||||||
from_id,
|
from_id,
|
||||||
@@ -3757,7 +3803,7 @@ async fn apply_in_broadcast_changes(
|
|||||||
let mut send_event_chat_modified = false;
|
let mut send_event_chat_modified = false;
|
||||||
let mut better_msg = None;
|
let mut better_msg = None;
|
||||||
|
|
||||||
apply_chat_name_and_avatar_changes(
|
apply_chat_name_avatar_and_description_changes(
|
||||||
context,
|
context,
|
||||||
mime_parser,
|
mime_parser,
|
||||||
from_id,
|
from_id,
|
||||||
|
|||||||
@@ -1558,6 +1558,15 @@ ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inc_and_check(&mut migration_version, 147)?;
|
||||||
|
if dbversion < migration_version {
|
||||||
|
sql.execute_migration(
|
||||||
|
"CREATE TABLE chats_descriptions (chat_id INTEGER PRIMARY KEY AUTOINCREMENT, description TEXT NOT NULL DEFAULT '') STRICT;",
|
||||||
|
migration_version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
let new_version = sql
|
let new_version = sql
|
||||||
.get_raw_config_int(VERSION_CFG)
|
.get_raw_config_int(VERSION_CFG)
|
||||||
.await?
|
.await?
|
||||||
|
|||||||
@@ -423,6 +423,12 @@ https://delta.chat/donate"))]
|
|||||||
|
|
||||||
#[strum(props(fallback = "Incoming video call"))]
|
#[strum(props(fallback = "Incoming video call"))]
|
||||||
IncomingVideoCall = 235,
|
IncomingVideoCall = 235,
|
||||||
|
|
||||||
|
#[strum(props(fallback = "You changed the chat description."))]
|
||||||
|
MsgYouChangedDescription = 240,
|
||||||
|
|
||||||
|
#[strum(props(fallback = "Chat description changed by %1$s."))]
|
||||||
|
MsgChatDescriptionChangedBy = 241,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StockMessage {
|
impl StockMessage {
|
||||||
@@ -601,6 +607,19 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn msg_chat_description_changed(
|
||||||
|
context: &Context,
|
||||||
|
by_contact: ContactId,
|
||||||
|
) -> String {
|
||||||
|
if by_contact == ContactId::SELF {
|
||||||
|
translated(context, StockMessage::MsgYouChangedDescription).await
|
||||||
|
} else {
|
||||||
|
translated(context, StockMessage::MsgChatDescriptionChangedBy)
|
||||||
|
.await
|
||||||
|
.replace1(&by_contact.get_stock_name(context).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Stock string: `You added member %1$s.` or `Member %1$s added by %2$s.`.
|
/// Stock string: `You added member %1$s.` or `Member %1$s added by %2$s.`.
|
||||||
///
|
///
|
||||||
/// The `added_member_addr` parameter should be an email address and is looked up in the
|
/// The `added_member_addr` parameter should be an email address and is looked up in the
|
||||||
|
|||||||
Reference in New Issue
Block a user