From b23ca26908804cdef08261508e8029989c79b73f Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Sun, 28 Jul 2019 11:32:48 +0200 Subject: [PATCH] refactor(chatlist): rustify --- deltachat-ffi/src/lib.rs | 57 ++++-- examples/repl/cmdline.rs | 24 ++- examples/simple.rs | 9 +- src/chatlist.rs | 336 +++++++++++++++++++++++++++++++++ src/dc_chat.rs | 2 +- src/dc_chatlist.rs | 390 --------------------------------------- src/dc_imex.rs | 3 +- src/dc_lot.rs | 2 +- src/dc_tools.rs | 4 +- src/error.rs | 66 +++++++ src/lib.rs | 5 +- 11 files changed, 465 insertions(+), 433 deletions(-) create mode 100644 src/chatlist.rs delete mode 100644 src/dc_chatlist.rs diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 3918b56c6..7a5e77ba5 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -281,11 +281,20 @@ pub unsafe extern "C" fn dc_get_chatlist<'a>( flags: libc::c_int, query_str: *mut libc::c_char, query_id: u32, -) -> *mut dc_chatlist::dc_chatlist_t<'a> { +) -> *mut dc_chatlist_t<'a> { assert!(!context.is_null()); let context = &*context; - dc_chatlist::dc_get_chatlist(context, flags, query_str, query_id) + let qs = if query_str.is_null() { + None + } else { + Some(dc_tools::as_str(query_str)) + }; + let qi = if query_id == 0 { None } else { Some(query_id) }; + match chatlist::Chatlist::try_load(context, flags as usize, qs, qi) { + Ok(list) => Box::into_raw(Box::new(list)), + Err(_) => std::ptr::null_mut(), + } } #[no_mangle] @@ -1075,51 +1084,65 @@ pub unsafe fn dc_array_is_independent( // dc_chatlist_t #[no_mangle] -pub type dc_chatlist_t<'a> = dc_chatlist::dc_chatlist_t<'a>; +pub type dc_chatlist_t<'a> = chatlist::Chatlist<'a>; #[no_mangle] -pub unsafe extern "C" fn dc_chatlist_unref(chatlist: *mut dc_chatlist::dc_chatlist_t) { - dc_chatlist::dc_chatlist_unref(chatlist) +pub unsafe extern "C" fn dc_chatlist_unref(chatlist: *mut dc_chatlist_t) { + assert!(!chatlist.is_null()); + + Box::from_raw(chatlist); } #[no_mangle] -pub unsafe extern "C" fn dc_chatlist_get_cnt( - chatlist: *mut dc_chatlist::dc_chatlist_t, -) -> libc::size_t { - dc_chatlist::dc_chatlist_get_cnt(chatlist) +pub unsafe extern "C" fn dc_chatlist_get_cnt(chatlist: *mut dc_chatlist_t) -> libc::size_t { + assert!(!chatlist.is_null()); + + let list = &*chatlist; + list.len() as libc::size_t } #[no_mangle] pub unsafe extern "C" fn dc_chatlist_get_chat_id( - chatlist: *mut dc_chatlist::dc_chatlist_t, + chatlist: *mut dc_chatlist_t, index: libc::size_t, ) -> u32 { - dc_chatlist::dc_chatlist_get_chat_id(chatlist, index) + assert!(!chatlist.is_null()); + + let list = &*chatlist; + list.get_chat_id(index as usize) } #[no_mangle] pub unsafe extern "C" fn dc_chatlist_get_msg_id( - chatlist: *mut dc_chatlist::dc_chatlist_t, + chatlist: *mut dc_chatlist_t, index: libc::size_t, ) -> u32 { - dc_chatlist::dc_chatlist_get_msg_id(chatlist, index) + assert!(!chatlist.is_null()); + + let list = &*chatlist; + list.get_msg_id(index as usize) } #[no_mangle] pub unsafe extern "C" fn dc_chatlist_get_summary<'a>( - chatlist: *mut dc_chatlist::dc_chatlist_t<'a>, + chatlist: *mut dc_chatlist_t<'a>, index: libc::size_t, chat: *mut dc_chat_t<'a>, ) -> *mut dc_lot::dc_lot_t { - dc_chatlist::dc_chatlist_get_summary(chatlist, index, chat) + assert!(!chatlist.is_null()); + + let list = &*chatlist; + list.get_summary(index as usize, chat) } #[no_mangle] pub unsafe extern "C" fn dc_chatlist_get_context( - chatlist: *mut dc_chatlist::dc_chatlist_t, + chatlist: *mut dc_chatlist_t, ) -> *const dc_context_t { assert!(!chatlist.is_null()); - (*chatlist).context as *const _ + let list = &*chatlist; + + list.get_context() as *const _ } // dc_chat_t diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index 7a5ae1c22..9e59b08b8 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -1,11 +1,11 @@ use std::str::FromStr; +use deltachat::chatlist::*; use deltachat::config; use deltachat::constants::*; use deltachat::context::*; use deltachat::dc_array::*; use deltachat::dc_chat::*; -use deltachat::dc_chatlist::*; use deltachat::dc_configure::*; use deltachat::dc_contact::*; use deltachat::dc_imex::*; @@ -630,11 +630,10 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E } "listchats" | "listarchived" | "chats" => { let listflags = if arg0 == "listarchived" { 0x01 } else { 0 }; - let chatlist = dc_get_chatlist(context, listflags, arg1_c, 0 as uint32_t); - ensure!(!chatlist.is_null(), "Failed to retrieve chatlist"); + let chatlist = Chatlist::try_load(context, listflags, Some(arg1), None)?; - let mut i: libc::c_int; - let cnt = dc_chatlist_get_cnt(chatlist) as libc::c_int; + let mut i: usize; + let cnt = chatlist.len(); if cnt > 0 { info!( context, 0, @@ -643,8 +642,8 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E i = cnt - 1; - while i >= 0 { - let chat = dc_get_chat(context, dc_chatlist_get_chat_id(chatlist, i as size_t)); + while i > 0 { + let chat = dc_get_chat(context, chatlist.get_chat_id(i)); let temp_subtitle = dc_chat_get_subtitle(chat); let temp_name = dc_chat_get_name(chat); info!( @@ -659,7 +658,7 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E ); free(temp_subtitle as *mut libc::c_void); free(temp_name as *mut libc::c_void); - let lot = dc_chatlist_get_summary(chatlist, i as size_t, chat); + let lot = chatlist.get_summary(i, chat); let statestr = if 0 != dc_chat_get_archived(chat) { " [Archived]" } else { @@ -706,7 +705,6 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E info!(context, 0, "Location streaming enabled."); } println!("{} chats", cnt); - dc_chatlist_unref(chatlist); } "chat" => { if sel_chat.is_null() && arg1.is_empty() { @@ -1136,8 +1134,8 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E res += as_str(encrinfo); free(encrinfo as *mut libc::c_void); - let chatlist = dc_get_chatlist(context, 0, 0 as *const libc::c_char, contact_id); - let chatlist_cnt = dc_chatlist_get_cnt(chatlist) as libc::c_int; + let chatlist = Chatlist::try_load(context, 0, None, Some(contact_id))?; + let chatlist_cnt = chatlist.len(); if chatlist_cnt > 0 { res += &format!( "\n\n{} chats shared with Contact#{}: ", @@ -1147,12 +1145,12 @@ pub unsafe fn dc_cmdline(context: &Context, line: &str) -> Result<(), failure::E if 0 != i { res += ", "; } - let chat = dc_get_chat(context, dc_chatlist_get_chat_id(chatlist, i as size_t)); + let chat = dc_get_chat(context, chatlist.get_chat_id(i)); res += &format!("{}#{}", chat_prefix(chat), dc_chat_get_id(chat)); dc_chat_unref(chat); } } - dc_chatlist_unref(chatlist); + println!("{}", res); } "delcontact" => { diff --git a/examples/simple.rs b/examples/simple.rs index 643ed5624..4a9b0c1fc 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -5,11 +5,11 @@ use std::sync::{Arc, RwLock}; use std::{thread, time}; use tempfile::tempdir; +use deltachat::chatlist::*; use deltachat::config; use deltachat::constants::Event; use deltachat::context::*; use deltachat::dc_chat::*; -use deltachat::dc_chatlist::*; use deltachat::dc_configure::*; use deltachat::dc_contact::*; use deltachat::dc_job::{ @@ -101,10 +101,10 @@ fn main() { dc_send_text_msg(&ctx, chat_id, msg_text.as_ptr()); println!("fetching chats.."); - let chats = dc_get_chatlist(&ctx, 0, std::ptr::null(), 0); + let chats = Chatlist::try_load(&ctx, 0, None, None).unwrap(); - for i in 0..dc_chatlist_get_cnt(chats) { - let summary = dc_chatlist_get_summary(chats, 0, std::ptr::null_mut()); + for i in 0..chats.len() { + let summary = chats.get_summary(0, std::ptr::null_mut()); let text1 = dc_lot_get_text1(summary); let text2 = dc_lot_get_text2(summary); @@ -121,7 +121,6 @@ fn main() { println!("chat: {} - {:?} - {:?}", i, text1_s, text2_s,); dc_lot_unref(summary); } - dc_chatlist_unref(chats); thread::sleep(duration); diff --git a/src/chatlist.rs b/src/chatlist.rs new file mode 100644 index 000000000..112363098 --- /dev/null +++ b/src/chatlist.rs @@ -0,0 +1,336 @@ +use crate::constants::*; +use crate::context::*; +use crate::dc_chat::*; +use crate::dc_contact::*; +use crate::dc_lot::*; +use crate::dc_msg::*; +use crate::dc_tools::*; +use crate::error::Result; +use crate::stock::StockMessage; + +/// An object representing a single chatlist in memory. +/// +/// Chatlist objects contain chat IDs and, if possible, message IDs belonging to them. +/// The chatlist object is not updated; if you want an update, you have to recreate the object. +/// +/// For a **typical chat overview**, the idea is to get the list of all chats via dc_get_chatlist() +/// without any listflags (see below) and to implement a "virtual list" or so +/// (the count of chats is known by chatlist.len()). +/// +/// Only for the items that are in view (the list may have several hundreds chats), +/// the UI should call chatlist.get_summary() then. +/// chatlist.get_summary() provides all elements needed for painting the item. +/// +/// On a click of such an item, the UI should change to the chat view +/// and get all messages from this view via dc_get_chat_msgs(). +/// Again, a "virtual list" is created (the count of messages is known) +/// and for each messages that is scrolled into view, dc_get_msg() is called then. +/// +/// Why no listflags? +/// Without listflags, dc_get_chatlist() adds the deaddrop and the archive "link" automatically as needed. +/// The UI can just render these items differently then. Although the deaddrop link is currently always the +/// first entry and only present on new messages, there is the rough idea that it can be optionally always +/// present and sorted into the list by date. Rendering the deaddrop in the described way +/// would not add extra work in the UI then. +pub struct Chatlist<'a> { + context: &'a Context, + /// Stores pairs of `chat_id, message_id` + ids: Vec<(u32, u32)>, +} + +impl<'a> Chatlist<'a> { + pub fn get_context(&self) -> &Context { + self.context + } + + /// Get a list of chats. + /// The list can be filtered by query parameters. + /// + /// The list is already sorted and starts with the most recent chat in use. + /// The sorting takes care of invalid sending dates, drafts and chats without messages. + /// Clients should not try to re-sort the list as this would be an expensive action + /// and would result in inconsistencies between clients. + /// + /// To get information about each entry, use eg. chatlist.get_summary(). + /// + /// By default, the function adds some special entries to the list. + /// These special entries can be identified by the ID returned by chatlist.get_chat_id(): + /// - DC_CHAT_ID_DEADDROP (1) - this special chat is present if there are + /// messages from addresses that have no relationship to the configured account. + /// The last of these messages is represented by DC_CHAT_ID_DEADDROP and you can retrieve details + /// about it with chatlist.get_msg_id(). Typically, the UI asks the user "Do you want to chat with NAME?" + /// and offers the options "Yes" (call dc_create_chat_by_msg_id()), "Never" (call dc_block_contact()) + /// or "Not now". + /// The UI can also offer a "Close" button that calls dc_marknoticed_contact() then. + /// - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has + /// archived _any_ chat using dc_archive_chat(). The UI should show a link as + /// "Show archived chats", if the user clicks this item, the UI should show a + /// list of all archived chats that can be created by this function hen using + /// the DC_GCL_ARCHIVED_ONLY flag. + /// - DC_CHAT_ID_ALLDONE_HINT (7) - this special chat is present + /// if DC_GCL_ADD_ALLDONE_HINT is added to listflags + /// and if there are only archived chats. + /// + /// The `listflags` is a combination of flags: + /// - if the flag DC_GCL_ARCHIVED_ONLY is set, only archived chats are returned. + /// if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and + /// the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are _any_ archived + /// chats + /// - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added + /// to the list (may be used eg. for selecting chats on forwarding, the flag is + /// not needed when DC_GCL_ARCHIVED_ONLY is already set) + /// - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT + /// is added as needed. + /// `query`: An optional query for filtering the list. Only chats matching this query + /// are returned. + /// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID + /// are returned. + pub fn try_load( + context: &'a Context, + listflags: usize, + query: Option<&str>, + query_contact_id: Option, + ) -> Result { + let mut add_archived_link_item = 0; + + // select with left join and minimum: + // - the inner select must use `hidden` and _not_ `m.hidden` + // which would refer the outer select and take a lot of time + // - `GROUP BY` is needed several messages may have the same timestamp + // - the list starts with the newest chats + // nb: the query currently shows messages from blocked contacts in groups. + // however, for normal-groups, this is okay as the message is also returned by dc_get_chat_msgs() + // (otherwise it would be hard to follow conversations, wa and tg do the same) + // for the deaddrop, however, they should really be hidden, however, _currently_ the deaddrop is not + // shown at all permanent in the chatlist. + + let process_row = |row: &rusqlite::Row| { + let chat_id: i32 = row.get(0)?; + // TODO: verify that it is okay for this to be Null + let msg_id: i32 = row.get(1).unwrap_or_default(); + + Ok((chat_id as u32, msg_id as u32)) + }; + + let process_rows = |rows: rusqlite::MappedRows<_>| { + rows.collect::, _>>() + .map_err(Into::into) + }; + + // nb: the query currently shows messages from blocked contacts in groups. + // however, for normal-groups, this is okay as the message is also returned by dc_get_chat_msgs() + // (otherwise it would be hard to follow conversations, wa and tg do the same) + // for the deaddrop, however, they should really be hidden, however, _currently_ the deaddrop is not + // shown at all permanent in the chatlist. + + let mut ids = if let Some(query_contact_id) = query_contact_id { + // show chats shared with a given contact + context.sql.query_map( + "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m \ + ON c.id=m.chat_id \ + AND m.timestamp=( SELECT MAX(timestamp) \ + FROM msgs WHERE chat_id=c.id \ + AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \ + AND c.blocked=0 AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?) \ + GROUP BY c.id ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;", + params![query_contact_id as i32], + process_row, + process_rows, + )? + } else if 0 != listflags & DC_GCL_ARCHIVED_ONLY { + // show archived chats + context.sql.query_map( + "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m \ + ON c.id=m.chat_id \ + AND m.timestamp=( SELECT MAX(timestamp) \ + FROM msgs WHERE chat_id=c.id \ + AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \ + AND c.blocked=0 AND c.archived=1 GROUP BY c.id \ + ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;", + params![], + process_row, + process_rows, + )? + } else if let Some(query) = query { + let query = query.trim().to_string(); + ensure!(!query.is_empty(), "missing query"); + + let strLikeCmd = format!("%{}%", query); + context.sql.query_map( + "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m \ + ON c.id=m.chat_id \ + AND m.timestamp=( SELECT MAX(timestamp) \ + FROM msgs WHERE chat_id=c.id \ + AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \ + AND c.blocked=0 AND c.name LIKE ? \ + GROUP BY c.id ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;", + params![strLikeCmd], + process_row, + process_rows, + )? + } else { + // show normal chatlist + let mut ids = context.sql.query_map( + "SELECT c.id, m.id FROM chats c \ + LEFT JOIN msgs m \ + ON c.id=m.chat_id \ + AND m.timestamp=( SELECT MAX(timestamp) \ + FROM msgs WHERE chat_id=c.id \ + AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \ + AND c.blocked=0 AND c.archived=0 \ + GROUP BY c.id \ + ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;", + params![], + process_row, + process_rows, + )?; + if 0 == listflags & DC_GCL_NO_SPECIALS { + let last_deaddrop_fresh_msg_id = get_last_deaddrop_fresh_msg(context); + if last_deaddrop_fresh_msg_id > 0 { + ids.push((1, last_deaddrop_fresh_msg_id)); + } + add_archived_link_item = 1; + } + ids + }; + + if 0 != add_archived_link_item && dc_get_archived_cnt(context) > 0 { + if ids.is_empty() && 0 != listflags & DC_GCL_ADD_ALLDONE_HINT { + ids.push((DC_CHAT_ID_ALLDONE_HINT as u32, 0)); + } + ids.push((DC_CHAT_ID_ARCHIVED_LINK as u32, 0)); + } + + Ok(Chatlist { context, ids }) + } + + /// Find out the number of chats. + pub fn len(&self) -> usize { + self.ids.len() + } + + pub fn is_empty(&self) -> bool { + self.ids.is_empty() + } + + /// Get a single chat ID of a chatlist. + /// + /// To get the message object from the message ID, use dc_get_chat(). + pub fn get_chat_id(&self, index: usize) -> u32 { + if index >= self.ids.len() { + return 0; + } + self.ids[index].0 + } + + /// Get a single message ID of a chatlist. + /// + /// To get the message object from the message ID, use dc_get_msg(). + pub fn get_msg_id(&self, index: usize) -> u32 { + if index >= self.ids.len() { + return 0; + } + + self.ids[index].1 + } + + /// Get a summary for a chatlist index. + /// + /// The summary is returned by a dc_lot_t object with the following fields: + /// + /// - dc_lot_t::text1: contains the username or the strings "Me", "Draft" and so on. + /// The string may be colored by having a look at text1_meaning. + /// If there is no such name or it should not be displayed, the element is NULL. + /// - dc_lot_t::text1_meaning: one of DC_TEXT1_USERNAME, DC_TEXT1_SELF or DC_TEXT1_DRAFT. + /// Typically used to show dc_lot_t::text1 with different colors. 0 if not applicable. + /// - dc_lot_t::text2: contains an excerpt of the message text or strings as + /// "No messages". May be NULL of there is no such text (eg. for the archive link) + /// - dc_lot_t::timestamp: the timestamp of the message. 0 if not applicable. + /// - dc_lot_t::state: The state of the message as one of the DC_STATE_* constants (see #dc_msg_get_state()). + // 0 if not applicable. + pub unsafe fn get_summary(&self, index: usize, mut chat: *mut Chat<'a>) -> *mut dc_lot_t { + // The summary is created by the chat, not by the last message. + // This is because we may want to display drafts here or stuff as + // "is typing". + // Also, sth. as "No messages" would not work if the summary comes from a message. + + let mut ret = dc_lot_new(); + if index >= self.ids.len() { + (*ret).text2 = to_cstring("ErrBadChatlistIndex"); + return ret; + } + + let lastmsg_id = self.ids[index].1; + let mut lastcontact = 0 as *mut dc_contact_t; + + if chat.is_null() { + chat = dc_chat_new(self.context); + let chat_to_delete = chat; + if !dc_chat_load_from_db(chat, self.ids[index].0) { + (*ret).text2 = to_cstring("ErrCannotReadChat"); + dc_chat_unref(chat_to_delete); + + return ret; + } + } + + let lastmsg = if 0 != lastmsg_id { + let lastmsg = dc_msg_new_untyped(self.context); + dc_msg_load_from_db(lastmsg, self.context, lastmsg_id); + + if (*lastmsg).from_id != 1 as libc::c_uint + && ((*chat).type_0 == DC_CHAT_TYPE_GROUP + || (*chat).type_0 == DC_CHAT_TYPE_VERIFIED_GROUP) + { + lastcontact = dc_contact_new(self.context); + dc_contact_load_from_db(lastcontact, &self.context.sql, (*lastmsg).from_id); + } + lastmsg + } else { + std::ptr::null_mut() + }; + + if (*chat).id == DC_CHAT_ID_ARCHIVED_LINK as u32 { + (*ret).text2 = dc_strdup(0 as *const libc::c_char) + } else if lastmsg.is_null() || (*lastmsg).from_id == DC_CONTACT_ID_SELF as u32 { + (*ret).text2 = to_cstring(self.context.stock_str(StockMessage::NoMessages)); + } else { + dc_lot_fill(ret, lastmsg, chat, lastcontact, self.context); + } + + dc_msg_unref(lastmsg); + dc_contact_unref(lastcontact); + + ret + } +} + +pub fn dc_get_archived_cnt(context: &Context) -> u32 { + context + .sql + .query_row_col( + context, + "SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;", + params![], + 0, + ) + .unwrap_or_default() +} + +fn get_last_deaddrop_fresh_msg(context: &Context) -> u32 { + // We have an index over the state-column, this should be sufficient as there are typically + // only few fresh messages. + context + .sql + .query_row_col( + context, + "SELECT m.id FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \ + WHERE m.state=10 \ + AND m.hidden=0 \ + AND c.blocked=2 \ + ORDER BY m.timestamp DESC, m.id DESC;", + params![], + 0, + ) + .unwrap_or_default() +} diff --git a/src/dc_chat.rs b/src/dc_chat.rs index 8f7ac8ddf..ca6a77021 100644 --- a/src/dc_chat.rs +++ b/src/dc_chat.rs @@ -1,9 +1,9 @@ use std::ffi::CString; +use crate::chatlist::*; use crate::constants::*; use crate::context::Context; use crate::dc_array::*; -use crate::dc_chatlist::*; use crate::dc_contact::*; use crate::dc_job::*; use crate::dc_msg::*; diff --git a/src/dc_chatlist.rs b/src/dc_chatlist.rs deleted file mode 100644 index 15df6aad3..000000000 --- a/src/dc_chatlist.rs +++ /dev/null @@ -1,390 +0,0 @@ -use crate::context::*; -use crate::dc_array::*; -use crate::dc_chat::*; -use crate::dc_contact::*; -use crate::dc_lot::*; -use crate::dc_msg::*; -use crate::dc_tools::*; -use crate::stock::StockMessage; -use crate::types::*; -use crate::x::*; - -/* * the structure behind dc_chatlist_t */ -#[derive(Copy, Clone)] -#[repr(C)] -pub struct dc_chatlist_t<'a> { - pub magic: uint32_t, - pub context: &'a Context, - pub cnt: size_t, - pub chatNlastmsg_ids: *mut dc_array_t, -} - -// handle chatlists -pub unsafe fn dc_get_chatlist<'a>( - context: &'a Context, - listflags: libc::c_int, - query_str: *const libc::c_char, - query_id: uint32_t, -) -> *mut dc_chatlist_t<'a> { - let obj = dc_chatlist_new(context); - - if 0 != dc_chatlist_load_from_db(obj, listflags, query_str, query_id) { - return obj; - } - - dc_chatlist_unref(obj); - return 0 as *mut dc_chatlist_t; -} - -/** - * @class dc_chatlist_t - * - * An object representing a single chatlist in memory. - * Chatlist objects contain chat IDs - * and, if possible, message IDs belonging to them. - * The chatlist object is not updated; - * if you want an update, you have to recreate the object. - * - * For a **typical chat overview**, - * the idea is to get the list of all chats via dc_get_chatlist() - * without any listflags (see below) - * and to implement a "virtual list" or so - * (the count of chats is known by dc_chatlist_get_cnt()). - * - * Only for the items that are in view - * (the list may have several hundreds chats), - * the UI should call dc_chatlist_get_summary() then. - * dc_chatlist_get_summary() provides all elements needed for painting the item. - * - * On a click of such an item, - * the UI should change to the chat view - * and get all messages from this view via dc_get_chat_msgs(). - * Again, a "virtual list" is created - * (the count of messages is known) - * and for each messages that is scrolled into view, dc_get_msg() is called then. - * - * Why no listflags? - * Without listflags, dc_get_chatlist() adds the deaddrop - * and the archive "link" automatically as needed. - * The UI can just render these items differently then. - * Although the deaddrop link is currently always the first entry - * and only present on new messages, - * there is the rough idea that it can be optionally always present - * and sorted into the list by date. - * Rendering the deaddrop in the described way - * would not add extra work in the UI then. - */ -pub unsafe fn dc_chatlist_new(context: &Context) -> *mut dc_chatlist_t { - let mut chatlist: *mut dc_chatlist_t; - chatlist = calloc(1, ::std::mem::size_of::()) as *mut dc_chatlist_t; - assert!(!chatlist.is_null()); - - (*chatlist).magic = 0xc4a71157u32; - (*chatlist).context = context; - (*chatlist).chatNlastmsg_ids = dc_array_new(128i32 as size_t); - assert!(!(*chatlist).chatNlastmsg_ids.is_null()); - chatlist -} - -pub unsafe fn dc_chatlist_unref(mut chatlist: *mut dc_chatlist_t) { - if chatlist.is_null() || (*chatlist).magic != 0xc4a71157u32 { - return; - } - dc_chatlist_empty(chatlist); - dc_array_unref((*chatlist).chatNlastmsg_ids); - (*chatlist).magic = 0i32 as uint32_t; - free(chatlist as *mut libc::c_void); -} - -pub unsafe fn dc_chatlist_empty(mut chatlist: *mut dc_chatlist_t) { - if chatlist.is_null() || (*chatlist).magic != 0xc4a71157u32 { - return; - } - (*chatlist).cnt = 0i32 as size_t; - dc_array_empty((*chatlist).chatNlastmsg_ids); -} - -/** - * Load a chatlist from the database to the chatlist object. - * - * @private @memberof dc_chatlist_t - */ -// TODO should return bool /rtn -unsafe fn dc_chatlist_load_from_db( - mut chatlist: *mut dc_chatlist_t, - listflags: libc::c_int, - query__: *const libc::c_char, - query_contact_id: u32, -) -> libc::c_int { - if chatlist.is_null() || (*chatlist).magic != 0xc4a71157u32 { - return 0; - } - dc_chatlist_empty(chatlist); - - let mut add_archived_link_item = 0; - - // select with left join and minimum: - // - the inner select must use `hidden` and _not_ `m.hidden` - // which would refer the outer select and take a lot of time - // - `GROUP BY` is needed several messages may have the same timestamp - // - the list starts with the newest chats - // nb: the query currently shows messages from blocked contacts in groups. - // however, for normal-groups, this is okay as the message is also returned by dc_get_chat_msgs() - // (otherwise it would be hard to follow conversations, wa and tg do the same) - // for the deaddrop, however, they should really be hidden, however, _currently_ the deaddrop is not - // shown at all permanent in the chatlist. - - let process_row = |row: &rusqlite::Row| { - let chat_id: i32 = row.get(0)?; - // TODO: verify that it is okay for this to be Null - let msg_id: i32 = row.get(1).unwrap_or_default(); - - Ok((chat_id, msg_id)) - }; - - let process_rows = |rows: rusqlite::MappedRows<_>| { - for row in rows { - let (id1, id2) = row?; - - dc_array_add_id((*chatlist).chatNlastmsg_ids, id1 as u32); - dc_array_add_id((*chatlist).chatNlastmsg_ids, id2 as u32); - } - Ok(()) - }; - - // nb: the query currently shows messages from blocked contacts in groups. - // however, for normal-groups, this is okay as the message is also returned by dc_get_chat_msgs() - // (otherwise it would be hard to follow conversations, wa and tg do the same) - // for the deaddrop, however, they should really be hidden, however, _currently_ the deaddrop is not - // shown at all permanent in the chatlist. - - let success = if query_contact_id != 0 { - // show chats shared with a given contact - (*chatlist).context.sql.query_map( - "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m \ - ON c.id=m.chat_id \ - AND m.timestamp=( SELECT MAX(timestamp) \ - FROM msgs WHERE chat_id=c.id \ - AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \ - AND c.blocked=0 AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?) \ - GROUP BY c.id ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;", - params![query_contact_id as i32], - process_row, - process_rows, - ) - } else if 0 != listflags & 0x1 { - // show archived chats - (*chatlist).context.sql.query_map( - "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m \ - ON c.id=m.chat_id \ - AND m.timestamp=( SELECT MAX(timestamp) \ - FROM msgs WHERE chat_id=c.id \ - AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \ - AND c.blocked=0 AND c.archived=1 GROUP BY c.id \ - ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;", - params![], - process_row, - process_rows, - ) - } else if query__.is_null() { - // show normal chatlist - if 0 == listflags & 0x2 { - let last_deaddrop_fresh_msg_id = get_last_deaddrop_fresh_msg((*chatlist).context); - if last_deaddrop_fresh_msg_id > 0 { - dc_array_add_id((*chatlist).chatNlastmsg_ids, 1); - dc_array_add_id((*chatlist).chatNlastmsg_ids, last_deaddrop_fresh_msg_id); - } - add_archived_link_item = 1; - } - (*chatlist).context.sql.query_map( - "SELECT c.id, m.id FROM chats c \ - LEFT JOIN msgs m \ - ON c.id=m.chat_id \ - AND m.timestamp=( SELECT MAX(timestamp) \ - FROM msgs WHERE chat_id=c.id \ - AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \ - AND c.blocked=0 AND c.archived=0 \ - GROUP BY c.id \ - ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;", - params![], - process_row, - process_rows, - ) - } else { - let query = to_string(query__).trim().to_string(); - if query.is_empty() { - return 1; - } else { - let strLikeCmd = format!("%{}%", query); - (*chatlist).context.sql.query_map( - "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m \ - ON c.id=m.chat_id \ - AND m.timestamp=( SELECT MAX(timestamp) \ - FROM msgs WHERE chat_id=c.id \ - AND (hidden=0 OR (hidden=1 AND state=19))) WHERE c.id>9 \ - AND c.blocked=0 AND c.name LIKE ? \ - GROUP BY c.id ORDER BY IFNULL(m.timestamp,0) DESC, m.id DESC;", - params![strLikeCmd], - process_row, - process_rows, - ) - } - }; - - if 0 != add_archived_link_item && dc_get_archived_cnt((*chatlist).context) > 0 { - if dc_array_get_cnt((*chatlist).chatNlastmsg_ids) == 0 && 0 != listflags & 0x4 { - dc_array_add_id((*chatlist).chatNlastmsg_ids, 7); - dc_array_add_id((*chatlist).chatNlastmsg_ids, 0); - } - dc_array_add_id((*chatlist).chatNlastmsg_ids, 6); - dc_array_add_id((*chatlist).chatNlastmsg_ids, 0); - } - (*chatlist).cnt = dc_array_get_cnt((*chatlist).chatNlastmsg_ids) / 2; - - match success { - Ok(_) => 1, - Err(err) => { - error!( - (*chatlist).context, - 0, "chatlist: failed to load from database: {:?}", err - ); - 0 - } - } -} - -// Context functions to work with chatlist -pub fn dc_get_archived_cnt(context: &Context) -> libc::c_int { - context - .sql - .query_row_col( - context, - "SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;", - params![], - 0, - ) - .unwrap_or_default() -} - -fn get_last_deaddrop_fresh_msg(context: &Context) -> u32 { - // we have an index over the state-column, this should be sufficient as there are typically only few fresh messages - context - .sql - .query_row_col( - context, - "SELECT m.id FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \ - WHERE m.state=10 \ - AND m.hidden=0 \ - AND c.blocked=2 \ - ORDER BY m.timestamp DESC, m.id DESC;", - params![], - 0, - ) - .unwrap_or_default() -} - -pub unsafe fn dc_chatlist_get_cnt(chatlist: *const dc_chatlist_t) -> size_t { - if chatlist.is_null() || (*chatlist).magic != 0xc4a71157u32 { - return 0i32 as size_t; - } - (*chatlist).cnt -} - -pub unsafe fn dc_chatlist_get_chat_id(chatlist: *const dc_chatlist_t, index: size_t) -> uint32_t { - if chatlist.is_null() - || (*chatlist).magic != 0xc4a71157u32 - || (*chatlist).chatNlastmsg_ids.is_null() - || index >= (*chatlist).cnt - { - return 0i32 as uint32_t; - } - dc_array_get_id((*chatlist).chatNlastmsg_ids, index.wrapping_mul(2)) -} - -pub unsafe fn dc_chatlist_get_msg_id(chatlist: *const dc_chatlist_t, index: size_t) -> uint32_t { - if chatlist.is_null() - || (*chatlist).magic != 0xc4a71157u32 - || (*chatlist).chatNlastmsg_ids.is_null() - || index >= (*chatlist).cnt - { - return 0i32 as uint32_t; - } - dc_array_get_id( - (*chatlist).chatNlastmsg_ids, - index.wrapping_mul(2).wrapping_add(1), - ) -} - -pub unsafe fn dc_chatlist_get_summary<'a>( - chatlist: *const dc_chatlist_t<'a>, - index: size_t, - mut chat: *mut Chat<'a>, -) -> *mut dc_lot_t { - let current_block: u64; - /* The summary is created by the chat, not by the last message. - This is because we may want to display drafts here or stuff as - "is typing". - Also, sth. as "No messages" would not work if the summary comes from a - message. */ - /* the function never returns NULL */ - let mut ret: *mut dc_lot_t = dc_lot_new(); - let lastmsg_id: uint32_t; - let mut lastmsg: *mut dc_msg_t = 0 as *mut dc_msg_t; - let mut lastcontact: *mut dc_contact_t = 0 as *mut dc_contact_t; - let mut chat_to_delete: *mut Chat = 0 as *mut Chat; - if chatlist.is_null() || (*chatlist).magic != 0xc4a71157u32 || index >= (*chatlist).cnt { - (*ret).text2 = dc_strdup(b"ErrBadChatlistIndex\x00" as *const u8 as *const libc::c_char) - } else { - lastmsg_id = dc_array_get_id( - (*chatlist).chatNlastmsg_ids, - index.wrapping_mul(2).wrapping_add(1), - ); - if chat.is_null() { - chat = dc_chat_new((*chatlist).context); - chat_to_delete = chat; - if !dc_chat_load_from_db( - chat, - dc_array_get_id((*chatlist).chatNlastmsg_ids, index.wrapping_mul(2)), - ) { - (*ret).text2 = - dc_strdup(b"ErrCannotReadChat\x00" as *const u8 as *const libc::c_char); - current_block = 3777403817673069519; - } else { - current_block = 7651349459974463963; - } - } else { - current_block = 7651349459974463963; - } - match current_block { - 3777403817673069519 => {} - _ => { - if 0 != lastmsg_id { - lastmsg = dc_msg_new_untyped((*chatlist).context); - dc_msg_load_from_db(lastmsg, (*chatlist).context, lastmsg_id); - if (*lastmsg).from_id != 1i32 as libc::c_uint - && ((*chat).type_0 == 120i32 || (*chat).type_0 == 130i32) - { - lastcontact = dc_contact_new((*chatlist).context); - dc_contact_load_from_db( - lastcontact, - &(*chatlist).context.sql, - (*lastmsg).from_id, - ); - } - } - if (*chat).id == 6i32 as libc::c_uint { - (*ret).text2 = dc_strdup(0 as *const libc::c_char) - } else if lastmsg.is_null() || (*lastmsg).from_id == 0i32 as libc::c_uint { - (*ret).text2 = - to_cstring((*chatlist).context.stock_str(StockMessage::NoMessages)); - } else { - dc_lot_fill(ret, lastmsg, chat, lastcontact, (*chatlist).context); - } - } - } - } - dc_msg_unref(lastmsg); - dc_contact_unref(lastcontact); - dc_chat_unref(chat_to_delete); - ret -} diff --git a/src/dc_imex.rs b/src/dc_imex.rs index b2bf84452..0a0ff19a8 100644 --- a/src/dc_imex.rs +++ b/src/dc_imex.rs @@ -1,6 +1,5 @@ use std::ffi::CString; -use failure::format_err; use mmime::mailmime_content::*; use mmime::mmapstring::*; use mmime::other::*; @@ -864,7 +863,7 @@ unsafe fn import_backup(context: &Context, backup_to_import: *const libc::c_char } if !loop_success { - return Err(format_err!("fail").into()); + return Err(format_err!("fail")); } Ok(()) }, diff --git a/src/dc_lot.rs b/src/dc_lot.rs index 69c517a1d..a7889f0ac 100644 --- a/src/dc_lot.rs +++ b/src/dc_lot.rs @@ -29,7 +29,7 @@ pub struct dc_lot_t { * An object containing a set of values. * The meaning of the values is defined by the function returning the object. * Lot objects are created - * eg. by dc_chatlist_get_summary() or dc_msg_get_summary(). + * eg. by chatlist.get_summary() or dc_msg_get_summary(). * * NB: _Lot_ is used in the meaning _heap_ here. */ diff --git a/src/dc_tools.rs b/src/dc_tools.rs index 11fc46717..dda2b2584 100644 --- a/src/dc_tools.rs +++ b/src/dc_tools.rs @@ -4,12 +4,12 @@ use std::fs; use std::time::SystemTime; use chrono::{Local, TimeZone}; -use failure::format_err; use mmime::mailimf_types::*; use rand::{thread_rng, Rng}; use crate::context::Context; use crate::dc_array::*; +use crate::error::Error; use crate::types::*; use crate::x::*; @@ -1567,7 +1567,7 @@ pub fn as_str<'a>(s: *const libc::c_char) -> &'a str { as_str_safe(s).unwrap_or_else(|err| panic!("{}", err)) } -pub fn as_str_safe<'a>(s: *const libc::c_char) -> Result<&'a str, failure::Error> { +pub fn as_str_safe<'a>(s: *const libc::c_char) -> Result<&'a str, Error> { assert!(!s.is_null(), "cannot be used on null pointers"); let cstr = unsafe { CStr::from_ptr(s) }; diff --git a/src/error.rs b/src/error.rs index e8ac6f154..8f7855a4e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,6 +16,8 @@ pub enum Error { SqlFailedToOpen, #[fail(display = "{:?}", _0)] Io(std::io::Error), + #[fail(display = "{:?}", _0)] + Message(String), } pub type Result = std::result::Result; @@ -43,3 +45,67 @@ impl From for Error { Error::Io(err) } } + +#[macro_export] +macro_rules! bail { + ($e:expr) => { + return Err($crate::error::Error::Message($e.to_string())); + }; + ($fmt:expr, $($arg:tt)+) => { + return Err($crate::error::Error::Message(format!($fmt, $($arg)+))); + }; +} + +#[macro_export] +macro_rules! format_err { + ($e:expr) => { + $crate::error::Error::Message($e.to_string()); + }; + ($fmt:expr, $($arg:tt)+) => { + $crate::error::Error::Message(format!($fmt, $($arg)+)); + }; +} + +#[macro_export(local_inner_macros)] +macro_rules! ensure { + ($cond:expr, $e:expr) => { + if !($cond) { + bail!($e); + } + }; + ($cond:expr, $fmt:expr, $($arg:tt)+) => { + if !($cond) { + bail!($fmt, $($arg)+); + } + }; +} + +#[macro_export] +macro_rules! ensure_eq { + ($left:expr, $right:expr) => ({ + match (&$left, &$right) { + (left_val, right_val) => { + if !(*left_val == *right_val) { + bail!(r#"assertion failed: `(left == right)` + left: `{:?}`, + right: `{:?}`"#, left_val, right_val) + } + } + } + }); + ($left:expr, $right:expr,) => ({ + ensure_eq!($left, $right) + }); + ($left:expr, $right:expr, $($arg:tt)+) => ({ + match (&($left), &($right)) { + (left_val, right_val) => { + if !(*left_val == *right_val) { + bail!(r#"assertion failed: `(left == right)` + left: `{:?}`, + right: `{:?}`: {}"#, left_val, right_val, + format_args!($($arg)+)) + } + } + } + }); +} diff --git a/src/lib.rs b/src/lib.rs index 82e9173d6..465577ca1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,12 +19,14 @@ extern crate rusqlite; #[macro_use] mod log; +#[macro_use] +pub mod error; pub mod aheader; +pub mod chatlist; pub mod config; pub mod constants; pub mod context; -pub mod error; pub mod imap; pub mod key; pub mod keyhistory; @@ -40,7 +42,6 @@ pub mod x; pub mod dc_array; pub mod dc_chat; -pub mod dc_chatlist; pub mod dc_configure; pub mod dc_contact; pub mod dc_dehtml;