diff --git a/assets/icon-device.png b/assets/icon-device.png new file mode 100644 index 0000000000..ff2d545bf5 Binary files /dev/null and b/assets/icon-device.png differ diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 56ceefad00..60404603b2 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1098,6 +1098,27 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch void dc_set_draft (dc_context_t* context, uint32_t chat_id, dc_msg_t* msg); +/** + * Add a message to the device-chat. + * Device-messages usually contain update information + * and some hints that are added during the program runs, multi-device etc. + * + * Device-messages may be added from the core, + * however, with this function, this can be done from the ui as well. + * If needed, the device-chat is created before. + * + * Sends the event #DC_EVENT_MSGS_CHANGED on success. + * To check, if a given chat is a device-chat, see dc_chat_is_device_talk() + * + * @memberof dc_context_t + * @param context The context as created by dc_context_new(). + * @param msg Message to be added to the device-chat. + * The message appears to the user as an incoming message. + * @return The ID of the added message. + */ +uint32_t dc_add_device_msg (dc_context_t* context, dc_msg_t* msg); + + /** * Get draft for a chat, if any. * See dc_set_draft() for more details about drafts. @@ -2715,6 +2736,39 @@ int dc_chat_is_unpromoted (const dc_chat_t* chat); int dc_chat_is_self_talk (const dc_chat_t* chat); +/** + * Check if a chat is a device-talk. + * Device-talks contain update information + * and some hints that are added during the program runs, multi-device etc. + * + * From the ui view, device-talks are not very special, + * the user can delete and forward messages, archive the chat, set notifications etc. + * + * Messages may be added from the core to the device chat, + * so the chat just pops up as usual. + * However, if needed the ui can also add messages using dc_add_device_msg() + * + * @memberof dc_chat_t + * @param chat The chat object. + * @return 1=chat is device-talk, 0=chat is no device-talk + */ +int dc_chat_is_device_talk (const dc_chat_t* chat); + + +/** + * Check if messages can be sent to a give chat. + * This is not true eg. for the deaddrop or for the device-talk, cmp. dc_chat_is_device_talk(). + * + * Calling dc_send_msg() for these chats will fail + * and the ui may decide to hide input controls therefore. + * + * @memberof dc_chat_t + * @param chat The chat object. + * @return 1=chat is writable, 0=chat is not writable + */ +int dc_chat_can_send (const dc_chat_t* chat); + + /** * Check if a chat is verified. Verified chats contain only verified members * and encryption is alwasy enabled. Verified chats are created using @@ -3370,7 +3424,8 @@ void dc_msg_latefiling_mediasize (dc_msg_t* msg, int width, int hei #define DC_CONTACT_ID_SELF 1 -#define DC_CONTACT_ID_DEVICE 2 +#define DC_CONTACT_ID_INFO 2 // centered messages as "member added", used in all chats +#define DC_CONTACT_ID_DEVICE 5 // messages "update info" in the device-chat #define DC_CONTACT_ID_LAST_SPECIAL 9 diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index dc3019a444..6c52f09ca4 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -811,6 +811,23 @@ pub unsafe extern "C" fn dc_set_draft( .unwrap_or(()) } +#[no_mangle] +pub unsafe extern "C" fn dc_add_device_msg(context: *mut dc_context_t, msg: *mut dc_msg_t) -> u32 { + if context.is_null() || msg.is_null() { + eprintln!("ignoring careless call to dc_add_device_msg()"); + return 0; + } + let ffi_context = &mut *context; + let ffi_msg = &mut *msg; + ffi_context + .with_inner(|ctx| { + chat::add_device_msg(ctx, &mut ffi_msg.message) + .unwrap_or_log_default(ctx, "Failed to add device message") + }) + .map(|msg_id| msg_id.to_u32()) + .unwrap_or(0) +} + #[no_mangle] pub unsafe extern "C" fn dc_get_draft(context: *mut dc_context_t, chat_id: u32) -> *mut dc_msg_t { if context.is_null() { @@ -2264,6 +2281,26 @@ pub unsafe extern "C" fn dc_chat_is_self_talk(chat: *mut dc_chat_t) -> libc::c_i ffi_chat.chat.is_self_talk() as libc::c_int } +#[no_mangle] +pub unsafe extern "C" fn dc_chat_is_device_talk(chat: *mut dc_chat_t) -> libc::c_int { + if chat.is_null() { + eprintln!("ignoring careless call to dc_chat_is_device_talk()"); + return 0; + } + let ffi_chat = &*chat; + ffi_chat.chat.is_device_talk() as libc::c_int +} + +#[no_mangle] +pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int { + if chat.is_null() { + eprintln!("ignoring careless call to dc_chat_can_send()"); + return 0; + } + let ffi_chat = &*chat; + ffi_chat.chat.can_send() as libc::c_int +} + #[no_mangle] pub unsafe extern "C" fn dc_chat_is_verified(chat: *mut dc_chat_t) -> libc::c_int { if chat.is_null() { diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index 0796b159d0..aeb6c4fb15 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -379,6 +379,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E sendimage []\n\ sendfile []\n\ draft []\n\ + devicemsg \n\ listmedia\n\ archive \n\ unarchive \n\ @@ -521,13 +522,12 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E for i in (0..cnt).rev() { let chat = Chat::load_from_db(context, chatlist.get_chat_id(i))?; - let temp_name = chat.get_name(); info!( context, "{}#{}: {} [{} fresh]", chat_prefix(&chat), chat.get_id(), - temp_name, + chat.get_name(), chat::get_fresh_msg_cnt(context, chat.get_id()), ); let lot = chatlist.get_summary(context, i, Some(&chat)); @@ -586,25 +586,33 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E let msglist = chat::get_chat_msgs(context, sel_chat.get_id(), 0x1, None); let members = chat::get_chat_contacts(context, sel_chat.id); - let temp2 = if sel_chat.get_type() == Chattype::Single && members.len() >= 1 { + let subtitle = if sel_chat.is_device_talk() { + "device-talk".to_string() + } else if sel_chat.get_type() == Chattype::Single && members.len() >= 1 { let contact = Contact::get_by_id(context, members[0])?; contact.get_addr().to_string() } else { format!("{} member(s)", members.len()) }; - let temp_name = sel_chat.get_name(); info!( context, - "{}#{}: {} [{}]{}", + "{}#{}: {} [{}]{}{}", chat_prefix(sel_chat), sel_chat.get_id(), - temp_name, - temp2, + sel_chat.get_name(), + subtitle, if sel_chat.is_sending_locations() { "📍" } else { "" }, + match sel_chat.get_profile_image(context) { + Some(icon) => match icon.to_str() { + Some(icon) => format!(" Icon: {}", icon), + _ => " Icon: Err".to_string(), + }, + _ => "".to_string(), + }, ); log_msglist(context, &msglist)?; if let Some(draft) = chat::get_draft(context, sel_chat.get_id())? { @@ -822,6 +830,15 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E println!("Draft deleted."); } } + "devicemsg" => { + ensure!( + !arg1.is_empty(), + "Please specify text to add as device message." + ); + let mut msg = Message::new(Viewtype::Text); + msg.set_text(Some(arg1.to_string())); + chat::add_device_msg(context, &mut msg)?; + } "listmedia" => { ensure!(sel_chat.is_some(), "No chat selected."); diff --git a/python/src/deltachat/const.py b/python/src/deltachat/const.py index ecfd5a86ba..ff22addb28 100644 --- a/python/src/deltachat/const.py +++ b/python/src/deltachat/const.py @@ -47,7 +47,8 @@ DC_STATE_OUT_DELIVERED = 26 DC_STATE_OUT_MDN_RCVD = 28 DC_CONTACT_ID_SELF = 1 -DC_CONTACT_ID_DEVICE = 2 +DC_CONTACT_ID_INFO = 2 +DC_CONTACT_ID_DEVICE = 5 DC_CONTACT_ID_LAST_SPECIAL = 9 DC_MSG_TEXT = 10 DC_MSG_IMAGE = 20 diff --git a/src/chat.rs b/src/chat.rs index 977b7249c3..6accc8ef62 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -97,6 +97,8 @@ impl Chat { if chat.param.exists(Param::Selftalk) { chat.name = context.stock_str(StockMessage::SelfMsg).into(); + } else if chat.param.exists(Param::Devicetalk) { + chat.name = context.stock_str(StockMessage::DeviceMessages).into(); } } } @@ -109,6 +111,14 @@ impl Chat { self.param.exists(Param::Selftalk) } + pub fn is_device_talk(&self) -> bool { + self.param.exists(Param::Devicetalk) + } + + pub fn can_send(&self) -> bool { + self.id > DC_CHAT_ID_LAST_SPECIAL && !self.is_device_talk() + } + pub fn update_param(&mut self, context: &Context) -> Result<(), Error> { sql::execute( context, @@ -582,6 +592,12 @@ pub fn set_blocking(context: &Context, chat_id: u32, new_blocking: Blocked) -> b .is_ok() } +fn copy_device_icon_to_blobs(context: &Context) -> Result { + let icon = include_bytes!("../assets/icon-device.png"); + let blob = BlobObject::create(context, "icon-device.png".to_string(), icon)?; + Ok(blob.as_name().to_string()) +} + pub fn create_or_lookup_by_contact_id( context: &Context, contact_id: u32, @@ -605,7 +621,14 @@ pub fn create_or_lookup_by_contact_id( "INSERT INTO chats (type, name, param, blocked, grpid) VALUES({}, '{}', '{}', {}, '{}')", 100, chat_name, - if contact_id == DC_CONTACT_ID_SELF as u32 { "K=1" } else { "" }, + match contact_id { + DC_CONTACT_ID_SELF => "K=1".to_string(), // K = Param::Selftalk + DC_CONTACT_ID_DEVICE => { + let icon = copy_device_icon_to_blobs(context)?; + format!("D=1\ni={}", icon) // D = Param::Devicetalk, i = Param::ProfileImage + }, + _ => "".to_string() + }, create_blocked as u8, contact.get_addr(), ), @@ -677,8 +700,7 @@ pub fn msgtype_has_file(msgtype: Viewtype) -> bool { } } -fn prepare_msg_common(context: &Context, chat_id: u32, msg: &mut Message) -> Result { - msg.id = MsgId::new_unset(); +fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<(), Error> { if msg.type_0 == Viewtype::Text { // the caller should check if the message text is empty } else if msgtype_has_file(msg.type_0) { @@ -714,10 +736,16 @@ fn prepare_msg_common(context: &Context, chat_id: u32, msg: &mut Message) -> Res } else { bail!("Cannot send messages of type #{}.", msg.type_0); } + Ok(()) +} +fn prepare_msg_common(context: &Context, chat_id: u32, msg: &mut Message) -> Result { + msg.id = MsgId::new_unset(); + prepare_msg_blob(context, msg)?; unarchive(context, chat_id)?; let mut chat = Chat::load_from_db(context, chat_id)?; + ensure!(chat.can_send(), "cannot send to chat #{}", chat_id); // The OutPreparing state is set by dc_prepare_msg() before it // calls this function and the message is left in the OutPreparing @@ -994,7 +1022,7 @@ pub fn get_chat_msgs( " LEFT JOIN contacts", " ON m.from_id=contacts.id", " WHERE m.from_id!=1", // 1=DC_CONTACT_ID_SELF - " AND m.from_id!=2", // 2=DC_CONTACT_ID_DEVICE + " AND m.from_id!=2", // 2=DC_CONTACT_ID_INFO " AND m.hidden=0", " AND chats.blocked=2", " AND contacts.blocked=0", @@ -1898,15 +1926,46 @@ pub fn get_chat_id_by_grpid(context: &Context, grpid: impl AsRef) -> (u32, .unwrap_or((0, false, Blocked::Not)) } -pub fn add_device_msg(context: &Context, chat_id: u32, text: impl AsRef) { +pub fn add_device_msg(context: &Context, msg: &mut Message) -> Result { + let (chat_id, _blocked) = + create_or_lookup_by_contact_id(context, DC_CONTACT_ID_DEVICE, Blocked::Not)?; + let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device"); + + prepare_msg_blob(context, msg)?; + unarchive(context, chat_id)?; + + context.sql.execute( + "INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,param,rfc724_mid) \ + VALUES (?,?,?, ?,?,?, ?,?,?);", + params![ + chat_id, + DC_CONTACT_ID_DEVICE, + DC_CONTACT_ID_SELF, + dc_create_smeared_timestamp(context), + msg.type_0, + MessageState::InFresh, + msg.text.as_ref().map_or("", String::as_str), + msg.param.to_string(), + rfc724_mid, + ], + )?; + + let row_id = sql::get_rowid(context, &context.sql, "msgs", "rfc724_mid", &rfc724_mid); + let msg_id = MsgId::new(row_id); + context.call_cb(Event::IncomingMsg { chat_id, msg_id }); + + Ok(msg_id) +} + +pub fn add_info_msg(context: &Context, chat_id: u32, text: impl AsRef) { let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device"); if context.sql.execute( "INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,rfc724_mid) VALUES (?,?,?, ?,?,?, ?,?);", params![ chat_id as i32, - DC_CONTACT_ID_DEVICE, - DC_CONTACT_ID_DEVICE, + DC_CONTACT_ID_INFO, + DC_CONTACT_ID_INFO, dc_create_smeared_timestamp(context), Viewtype::Text, MessageState::InNoticed, diff --git a/src/constants.rs b/src/constants.rs index f6d752ff3b..68783837f5 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -130,7 +130,8 @@ const DC_MAX_GET_INFO_LEN: usize = 100000; pub const DC_CONTACT_ID_UNDEFINED: u32 = 0; pub const DC_CONTACT_ID_SELF: u32 = 1; -pub const DC_CONTACT_ID_DEVICE: u32 = 2; +pub const DC_CONTACT_ID_INFO: u32 = 2; +pub const DC_CONTACT_ID_DEVICE: u32 = 5; pub const DC_CONTACT_ID_LAST_SPECIAL: u32 = 9; pub const DC_CREATE_MVBOX: usize = 1; diff --git a/src/contact.rs b/src/contact.rs index f0c765c4c9..e0d8821287 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -153,7 +153,16 @@ impl Contact { blocked: false, origin: Origin::Unknown, }; - + return Ok(contact); + } else if contact_id == DC_CONTACT_ID_DEVICE { + let contact = Contact { + id: contact_id, + name: context.stock_str(StockMessage::DeviceMessages).into(), + authname: "".into(), + addr: "device@localhost".into(), + blocked: false, + origin: Origin::Unknown, + }; return Ok(contact); } diff --git a/src/location.rs b/src/location.rs index 14ef83dda0..20527f06a7 100644 --- a/src/location.rs +++ b/src/location.rs @@ -219,7 +219,7 @@ pub fn send_locations_to_chat(context: &Context, chat_id: u32, seconds: i64) { } else if 0 == seconds && is_sending_locations_before { let stock_str = context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0); - chat::add_device_msg(context, chat_id, stock_str); + chat::add_info_msg(context, chat_id, stock_str); } context.call_cb(Event::ChatModified(chat_id)); if 0 != seconds { @@ -651,7 +651,7 @@ pub fn job_do_DC_JOB_MAYBE_SEND_LOC_ENDED(context: &Context, job: &mut Job) { params![chat_id as i32], ).is_ok() { let stock_str = context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0); - chat::add_device_msg(context, chat_id, stock_str); + chat::add_info_msg(context, chat_id, stock_str); context.call_cb(Event::ChatModified(chat_id)); } } diff --git a/src/message.rs b/src/message.rs index b2988d9107..bf4cdfecd7 100644 --- a/src/message.rs +++ b/src/message.rs @@ -476,8 +476,8 @@ impl Message { pub fn is_info(&self) -> bool { let cmd = self.param.get_cmd(); - self.from_id == DC_CONTACT_ID_DEVICE as libc::c_uint - || self.to_id == DC_CONTACT_ID_DEVICE as libc::c_uint + self.from_id == DC_CONTACT_ID_INFO as libc::c_uint + || self.to_id == DC_CONTACT_ID_INFO as libc::c_uint || cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage } @@ -714,7 +714,7 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String { ret += "\n"; } - if msg.from_id == DC_CONTACT_ID_DEVICE || msg.to_id == DC_CONTACT_ID_DEVICE { + if msg.from_id == DC_CONTACT_ID_INFO || msg.to_id == DC_CONTACT_ID_INFO { // device-internal message, no further details needed return ret; } diff --git a/src/param.rs b/src/param.rs index 9fcdf0db3c..524fcedf66 100644 --- a/src/param.rs +++ b/src/param.rs @@ -76,6 +76,8 @@ pub enum Param { ProfileImage = b'i', // For Chats Selftalk = b'K', + // For Chats + Devicetalk = b'D', // For QR Auth = b's', // For QR diff --git a/src/qr.rs b/src/qr.rs index e53c71bf48..72f2b2d65f 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -150,7 +150,7 @@ fn decode_openpgp(context: &Context, qr: &str) -> Lot { let (id, _) = chat::create_or_lookup_by_contact_id(context, lot.id, Blocked::Deaddrop) .unwrap_or_default(); - chat::add_device_msg( + chat::add_info_msg( context, id, format!("{} verified.", peerstate.addr.unwrap_or_default()), diff --git a/src/securejoin.rs b/src/securejoin.rs index f8f8dc74de..783f6f3d14 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -638,7 +638,7 @@ fn secure_connection_established(context: &Context, contact_chat_id: u32) { "?" }; let msg = context.stock_string_repl_str(StockMessage::ContactVerified, addr); - chat::add_device_msg(context, contact_chat_id, msg); + chat::add_info_msg(context, contact_chat_id, msg); emit_event!(context, Event::ChatModified(contact_chat_id)); } @@ -654,7 +654,7 @@ fn could_not_establish_secure_connection(context: &Context, contact_chat_id: u32 }, ); - chat::add_device_msg(context, contact_chat_id, &msg); + chat::add_info_msg(context, contact_chat_id, &msg); error!(context, "{} ({})", &msg, details); } @@ -735,7 +735,7 @@ pub fn handle_degrade_event(context: &Context, peerstate: &Peerstate) -> Result< }; let msg = context.stock_string_repl_str(StockMessage::ContactSetupChanged, peeraddr); - chat::add_device_msg(context, contact_chat_id, msg); + chat::add_info_msg(context, contact_chat_id, msg); emit_event!(context, Event::ChatModified(contact_chat_id)); } } diff --git a/src/sql.rs b/src/sql.rs index 6cac599fae..16b1a39247 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -385,8 +385,8 @@ fn open( )?; sql.execute( "INSERT INTO contacts (id,name,origin) VALUES \ - (1,'self',262144), (2,'device',262144), (3,'rsvd',262144), \ - (4,'rsvd',262144), (5,'rsvd',262144), (6,'rsvd',262144), \ + (1,'self',262144), (2,'info',262144), (3,'rsvd',262144), \ + (4,'rsvd',262144), (5,'device',262144), (6,'rsvd',262144), \ (7,'rsvd',262144), (8,'rsvd',262144), (9,'rsvd',262144);", params![], )?; diff --git a/src/stock.rs b/src/stock.rs index af24bac01b..f5d27f0edd 100644 --- a/src/stock.rs +++ b/src/stock.rs @@ -110,6 +110,8 @@ pub enum StockMessage { Location = 66, #[strum(props(fallback = "Sticker"))] Sticker = 67, + #[strum(props(fallback = "Device Messages"))] + DeviceMessages = 68, } impl StockMessage {