diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index aed6b6e8ed..2ab5b40085 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -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), - * 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. * * @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 context The context object. * @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); /** - * Set group profile image. + * Set group or broadcast channel profile image. * * 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. * @@ -1900,7 +1902,7 @@ int dc_set_chat_ephemeral_timer (dc_context_t* context, uint32_t chat_id, uint32 * * @memberof dc_context_t * @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 * `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 diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index 4f59dd6428..19c0c1b2f1 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -1068,7 +1068,8 @@ impl CommandApi { /// Set group name. /// /// 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. 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 } + /// 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 { + let ctx = self.get_context(account_id).await?; + chat::get_chat_description(&ctx, ChatId::new(chat_id)).await + } + /// Set group profile image. /// /// 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. /// diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index 384d6ee212..3a66a0f98a 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -388,6 +388,7 @@ impl From for DownloadState { pub enum SystemMessageType { Unknown, GroupNameChanged, + GroupDescriptionChanged, GroupImageChanged, MemberAddedToGroup, MemberRemovedFromGroup, @@ -440,6 +441,7 @@ impl From for SystemMessageType { match system_message_type { SystemMessage::Unknown => SystemMessageType::Unknown, SystemMessage::GroupNameChanged => SystemMessageType::GroupNameChanged, + SystemMessage::GroupDescriptionChanged => SystemMessageType::GroupDescriptionChanged, SystemMessage::GroupImageChanged => SystemMessageType::GroupImageChanged, SystemMessage::MemberAddedToGroup => SystemMessageType::MemberAddedToGroup, SystemMessage::MemberRemovedFromGroup => SystemMessageType::MemberRemovedFromGroup, diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs index 1eecbe1464..4dd13f94dd 100644 --- a/deltachat-repl/src/cmdline.rs +++ b/deltachat-repl/src/cmdline.rs @@ -343,6 +343,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu addmember \n\ removemember \n\ groupname \n\ + groupdescription \n\ groupimage \n\ chatinfo\n\ sendlocations \n\ @@ -770,6 +771,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu println!("Chat name set"); } + "groupdescription" => { + ensure!(sel_chat.is_some(), "No chat selected."); + ensure!(!arg1.is_empty(), "Argument missing."); + chat::set_chat_description(&context, sel_chat.as_ref().unwrap().get_id(), arg1).await?; + + println!("Chat description set"); + } "groupimage" => { ensure!(sel_chat.is_some(), "No chat selected."); ensure!(!arg1.is_empty(), "Argument missing."); diff --git a/deltachat-repl/src/main.rs b/deltachat-repl/src/main.rs index 518ea8deea..8740678d61 100644 --- a/deltachat-repl/src/main.rs +++ b/deltachat-repl/src/main.rs @@ -179,7 +179,7 @@ const DB_COMMANDS: [&str; 11] = [ "housekeeping", ]; -const CHAT_COMMANDS: [&str; 39] = [ +const CHAT_COMMANDS: [&str; 40] = [ "listchats", "listarchived", "start-realtime", @@ -192,6 +192,7 @@ const CHAT_COMMANDS: [&str; 39] = [ "addmember", "removemember", "groupname", + "groupdescription", "groupimage", "chatinfo", "sendlocations", diff --git a/src/chat.rs b/src/chat.rs index 7023b03adf..06cf1712ea 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1763,10 +1763,11 @@ impl Chat { } 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); + msg.param.set_int(Param::AttachChatAvatarAndDescription, 1); self.param .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?; // TODO: Remove this compat code needed because Core <= v1.143: // - 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. pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result> { - if msg.param.get_cmd() == SystemMessage::GroupNameChanged { + let cmd = msg.param.get_cmd(); + if cmd == SystemMessage::GroupNameChanged || cmd == SystemMessage::GroupDescriptionChanged { 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?; } @@ -3882,9 +3892,11 @@ pub(crate) async fn add_contact_to_chat_ex( let sync_qr_code_tokens; if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 { + let smeared_time = smeared_time(context); chat.param .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?; sync_qr_code_tokens = true; } else { @@ -4187,7 +4199,107 @@ async fn send_member_removal_msg( 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 { + 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<()> { 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. SetPgpContacts(Vec<(String, String)>), + SetDescription(String), Delete, } @@ -5113,6 +5226,9 @@ impl Context { Err(anyhow!("sync_alter_chat({id:?}, {action:?}): Bad request.")) } 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::SetPgpContacts(fingerprint_addrs) => { set_contacts_by_fingerprints(self, chat_id, fingerprint_addrs).await diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 5befc53cff..e5e5a05407 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -3156,6 +3156,119 @@ async fn test_broadcasts_name_and_avatar() -> Result<()> { 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, /// the brodacast is shown correctly on both devices. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/src/headerdef.rs b/src/headerdef.rs index 979cb8749f..166815f0f0 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -60,6 +60,9 @@ pub enum HeaderDef { ChatGroupName, ChatGroupNameChanged, ChatGroupNameTimestamp, + ChatGroupDescription, + ChatGroupDescriptionChanged, + ChatGroupDescriptionTimestamp, ChatVerified, ChatGroupAvatar, ChatUserAvatar, diff --git a/src/message.rs b/src/message.rs index 0676daf7d8..65c591361a 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1005,6 +1005,7 @@ impl Message { pub async fn get_info_contact_id(&self, context: &Context) -> Result> { match self.param.get_cmd() { SystemMessage::GroupNameChanged + | SystemMessage::GroupDescriptionChanged | SystemMessage::GroupImageChanged | SystemMessage::EphemeralTimerChanged => { if self.from_id != ContactId::INFO { diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 6ff3375336..71fdf0d7df 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -663,7 +663,7 @@ impl MimeFactory { if msg .param - .get_bool(Param::AttachGroupImage) + .get_bool(Param::AttachChatAvatarAndDescription) .unwrap_or_default() { return chat.param.get(Param::ProfileImage).map(Into::into); @@ -1669,6 +1669,12 @@ impl MimeFactory { 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 => { headers.push(( "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 { diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 4de47dd9a4..a04b5621fc 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -186,10 +186,10 @@ pub enum SystemMessage { #[default] Unknown = 0, - /// Group name changed. + /// Group or broadcast channel name changed. GroupNameChanged = 2, - /// Group avatar changed. + /// Group or broadcast channel avatar changed. GroupImageChanged = 3, /// Member was added to the group. @@ -254,6 +254,9 @@ pub enum SystemMessage { /// Message indicating that a call was ended. CallEnded = 67, + + /// Group or broadcast channel description changed. + GroupDescriptionChanged = 70, } const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup"; @@ -773,6 +776,11 @@ impl MimeMessage { self.is_system_message = SystemMessage::MemberAddedToGroup; } else if self.get_header(HeaderDef::ChatGroupNameChanged).is_some() { self.is_system_message = SystemMessage::GroupNameChanged; + } else if self + .get_header(HeaderDef::ChatGroupDescriptionChanged) + .is_some() + { + self.is_system_message = SystemMessage::GroupDescriptionChanged; } } diff --git a/src/param.rs b/src/param.rs index 30c4f008a7..dfd48d31fc 100644 --- a/src/param.rs +++ b/src/param.rs @@ -140,7 +140,7 @@ pub enum Param { Arg4 = b'H', /// For Messages - AttachGroupImage = b'A', + AttachChatAvatarAndDescription = b'A', /// For Messages WebrtcRoom = b'V', @@ -219,6 +219,9 @@ pub enum Param { /// For Chats: timestamp of group name update. GroupNameTimestamp = b'g', + /// For Chats: timestamp of chat description update. + GroupDescriptionTimestamp = b'6', + /// For Chats: timestamp of member list update. MemberListTimestamp = b'k', diff --git a/src/receive_imf.rs b/src/receive_imf.rs index ff1c2ed570..906489c9b1 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -8,7 +8,8 @@ use std::sync::LazyLock; use anyhow::{Context as _, Result, ensure}; use data_encoding::BASE32_NOPAD; 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 mailparse::SingleInfo; @@ -3101,7 +3102,7 @@ async fn apply_group_changes( } if is_from_in_chat { - apply_chat_name_and_avatar_changes( + apply_chat_name_avatar_and_description_changes( context, mime_parser, 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 /// - `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, mime_parser: &MimeMessage, 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::().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 ========== 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; if from_id == ContactId::SELF { - apply_chat_name_and_avatar_changes( + apply_chat_name_avatar_and_description_changes( context, mime_parser, from_id, @@ -3757,7 +3803,7 @@ async fn apply_in_broadcast_changes( let mut send_event_chat_modified = false; let mut better_msg = None; - apply_chat_name_and_avatar_changes( + apply_chat_name_avatar_and_description_changes( context, mime_parser, from_id, diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index b1cd08f667..11a2fb48b8 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1558,6 +1558,15 @@ ALTER TABLE contacts ADD COLUMN name_normalized TEXT; .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 .get_raw_config_int(VERSION_CFG) .await? diff --git a/src/stock_str.rs b/src/stock_str.rs index c38b766911..b2830955ea 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -423,6 +423,12 @@ https://delta.chat/donate"))] #[strum(props(fallback = "Incoming video call"))] 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 { @@ -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.`. /// /// The `added_member_addr` parameter should be an email address and is looked up in the