diff --git a/CHANGELOG.md b/CHANGELOG.md index fa98e0fc9..f832e1a9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ and `dc_event_emitter_unref()` should be used instead of `dc_accounts_event_emitter_unref`. - add `dc_contact_was_seen_recently()` #3560 -- jsonrpc: add functions: #3586, #3587 +- Fix get_connectivity_html and get_encrinfo futures not being Send. See rust-lang/rust#101650 for more information +- jsonrpc: add functions: #3586, #3587, #3590 - `deleteChat()` - `getChatEncryptionInfo()` - `getChatSecurejoinQrCodeSvg()` @@ -20,10 +21,27 @@ - `addContactToChat()` - `deleteMessages()` - `getMessageInfo()` + - `getBasicChatInfo()` + - `marknoticedChat()` + - `getFirstUnreadMessageOfChat()` + - `markseenMsgs()` + - `forwardMessages()` + - `removeDraft()` + - `getDraft()` + - `miscSendMsg()` + - `miscSetDraft()` + - `maybeNetwork()` + - `getConnectivity()` + - `getContactEncryptionInfo()` + - `getConnectivityHtml()` - jsonrpc: add `is_broadcast` property to `ChatListItemFetchResult` #3584 - jsonrpc: add `was_seen_recently` property to `ChatListItemFetchResult`, `FullChat` and `Contact` #3584 - jsonrpc: add `webxdc_info` property to `Message` #3588 - python: move `get_dc_event_name()` from `deltachat` to `deltachat.events` #3564 +- jsonrpc: add `webxdc_info`, `parent_id` and `download_state` property to `Message` #3588, #3590 +- jsonrpc: add `BasicChat` object as a leaner alternative to `FullChat` #3590 +- jsonrpc: add `last_seen` property to `Contact` #3590 +- breaking! jsonrpc: replace `Message.quoted_text` and `Message.quoted_message_id` with `Message.quote` #3590 ### Changes - order contact lists by "last seen"; diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index 50717f592..71f97316d 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -1,11 +1,14 @@ use anyhow::{anyhow, bail, Context, Result}; use deltachat::{ - chat::{add_contact_to_chat, get_chat_media, get_chat_msgs, remove_contact_from_chat, ChatId}, + chat::{ + self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, marknoticed_chat, + remove_contact_from_chat, Chat, ChatId, ChatItem, + }, chatlist::Chatlist, config::Config, contact::{may_be_valid_addr, Contact, ContactId}, context::get_info, - message::{delete_msgs, get_msg_info, Message, MsgId, Viewtype}, + message::{delete_msgs, get_msg_info, markseen_msgs, Message, MessageState, MsgId, Viewtype}, provider::get_provider_info, qr, qr_code_generator::get_securejoin_qr_svg, @@ -34,7 +37,10 @@ use types::message::MessageObject; use types::provider_info::ProviderInfo; use types::webxdc::WebxdcMessageInfo; -use self::types::message::MessageViewtype; +use self::types::{ + chat::{BasicChat, MuteDuration}, + message::MessageViewtype, +}; #[derive(Clone, Debug)] pub struct CommandApi { @@ -368,6 +374,13 @@ impl CommandApi { FullChat::try_from_dc_chat_id(&ctx, chat_id).await } + /// get basic info about a chat, + /// use chatlist_get_full_chat_by_id() instead if you need more information + async fn get_basic_chat_info(&self, account_id: u32, chat_id: u32) -> Result { + let ctx = self.get_context(account_id).await?; + BasicChat::try_from_dc_chat_id(&ctx, chat_id).await + } + async fn accept_chat(&self, account_id: u32, chat_id: u32) -> Result<()> { let ctx = self.get_context(account_id).await?; ChatId::new(chat_id).accept(&ctx).await @@ -427,6 +440,8 @@ impl CommandApi { /// If not set, the Setup-Contact protocol is offered in the QR code. /// See https://countermitm.readthedocs.io/en/latest/new.html /// for details about both protocols. + /// + /// return format: `[code, svg]` // TODO fix doc comment after adding dc_join_securejoin async fn get_chat_securejoin_qr_code_svg( &self, @@ -495,10 +510,101 @@ impl CommandApi { Ok(message_id.to_u32()) } + /// Mark all messages in a chat as _noticed_. + /// _Noticed_ messages are no longer _fresh_ and do not count as being unseen + /// but are still waiting for being marked as "seen" using markseen_msgs() + /// (IMAP/MDNs is not done for noticed messages). + /// + /// Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED. + /// See also markseen_msgs(). + async fn marknoticed_chat(&self, account_id: u32, chat_id: u32) -> Result<()> { + let ctx = self.get_context(account_id).await?; + marknoticed_chat(&ctx, ChatId::new(chat_id)).await + } + + async fn get_first_unread_message_of_chat( + &self, + account_id: u32, + chat_id: u32, + ) -> Result> { + let ctx = self.get_context(account_id).await?; + + // TODO: implement this in core with an SQL query, that will be way faster + let messages = get_chat_msgs(&ctx, ChatId::new(chat_id), 0).await?; + let mut first_unread_message_id = None; + for item in messages.into_iter().rev() { + if let ChatItem::Message { msg_id } = item { + match msg_id.get_state(&ctx).await? { + MessageState::InSeen => break, + MessageState::InFresh | MessageState::InNoticed => { + first_unread_message_id = Some(msg_id) + } + _ => continue, + } + } + } + Ok(first_unread_message_id.map(|id| id.to_u32())) + } + + /// Set mute duration of a chat. + /// + /// The UI can then call is_chat_muted() when receiving a new message + /// to decide whether it should trigger an notification. + /// + /// Muted chats should not sound or vibrate + /// and should not show a visual notification in the system area. + /// Moreover, muted chats should be excluded from global badge counter + /// (get_fresh_msgs() skips muted chats therefore) + /// and the in-app, per-chat badge counter should use a less obtrusive color. + /// + /// Sends out #DC_EVENT_CHAT_MODIFIED. + async fn set_chat_mute_duration( + &self, + account_id: u32, + chat_id: u32, + duration: MuteDuration, + ) -> Result<()> { + let ctx = self.get_context(account_id).await?; + chat::set_muted(&ctx, ChatId::new(chat_id), duration.try_into_core_type()?).await + } + + /// Check whether the chat is currently muted (can be changed by set_chat_mute_duration()). + /// + /// This is available as a standalone function outside of fullchat, because it might be only needed for notification + async fn is_chat_muted(&self, account_id: u32, chat_id: u32) -> Result { + let ctx = self.get_context(account_id).await?; + Ok(Chat::load_from_db(&ctx, ChatId::new(chat_id)) + .await? + .is_muted()) + } + // --------------------------------------------- // message list // --------------------------------------------- + /// Mark messages as presented to the user. + /// Typically, UIs call this function on scrolling through the message list, + /// when the messages are presented at least for a little moment. + /// The concrete action depends on the type of the chat and on the users settings + /// (dc_msgs_presented() may be a better name therefore, but well. :) + /// + /// - For normal chats, the IMAP state is updated, MDN is sent + /// (if set_config()-options `mdns_enabled` is set) + /// and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions. + /// + /// - For contact requests, no IMAP or MDNs is done + /// and the internal state is not changed therefore. + /// See also marknoticed_chat(). + /// + /// Moreover, timer is started for incoming ephemeral messages. + /// This also happens for contact requests chats. + /// + /// One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat. + async fn markseen_msgs(&self, account_id: u32, msg_ids: Vec) -> Result<()> { + let ctx = self.get_context(account_id).await?; + markseen_msgs(&ctx, msg_ids.into_iter().map(MsgId::new).collect()).await + } + async fn message_list_get_message_ids( &self, account_id: u32, @@ -687,6 +793,19 @@ impl CommandApi { } Ok(contacts) } + + /// Get encryption info for a contact. + /// Get a multi-line encryption info, containing your fingerprint and the + /// fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification. + async fn get_contact_encryption_info( + &self, + account_id: u32, + contact_id: u32, + ) -> Result { + let ctx = self.get_context(account_id).await?; + Contact::get_encrinfo(&ctx, ContactId::new(contact_id)).await + } + // --------------------------------------------- // chat // --------------------------------------------- @@ -722,6 +841,50 @@ impl CommandApi { Ok(media.iter().map(|msg_id| msg_id.to_u32()).collect()) } + // --------------------------------------------- + // connectivity + // --------------------------------------------- + + /// Indicate that the network likely has come back. + /// or just that the network conditions might have changed + async fn maybe_network(&self) -> Result<()> { + self.accounts.read().await.maybe_network().await; + Ok(()) + } + + /// Get the current connectivity, i.e. whether the device is connected to the IMAP server. + /// One of: + /// - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot + /// - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot + /// - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel + /// - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot + /// + /// We don't use exact values but ranges here so that we can split up + /// states into multiple states in the future. + /// + /// Meant as a rough overview that can be shown + /// e.g. in the title of the main screen. + /// + /// If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted. + async fn get_connectivity(&self, account_id: u32) -> Result { + let ctx = self.get_context(account_id).await?; + Ok(ctx.get_connectivity().await as u32) + } + + /// Get an overview of the current connectivity, and possibly more statistics. + /// Meant to give the user more insight about the current status than + /// the basic connectivity info returned by get_connectivity(); show this + /// e.g., if the user taps on said basic connectivity info. + /// + /// If this page changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted. + /// + /// This comes as an HTML from the core so that we can easily improve it + /// and the improvement instantly reaches all UIs. + async fn get_connectivity_html(&self, account_id: u32) -> Result { + let ctx = self.get_context(account_id).await?; + ctx.get_connectivity_html().await + } + // --------------------------------------------- // webxdc // --------------------------------------------- @@ -762,6 +925,45 @@ impl CommandApi { WebxdcMessageInfo::get_for_message(&ctx, MsgId::new(instance_msg_id)).await } + /// Forward messages to another chat. + /// + /// All types of messages can be forwarded, + /// however, they will be flagged as such (dc_msg_is_forwarded() is set). + /// + /// Original sender, info-state and webxdc updates are not forwarded on purpose. + async fn forward_messages( + &self, + account_id: u32, + message_ids: Vec, + chat_id: u32, + ) -> Result<()> { + let ctx = self.get_context(account_id).await?; + let message_ids: Vec = message_ids.into_iter().map(MsgId::new).collect(); + forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await + } + + // --------------------------------------------- + // functions for the composer + // the composer is the message input field + // --------------------------------------------- + + async fn remove_draft(&self, account_id: u32, chat_id: u32) -> Result<()> { + let ctx = self.get_context(account_id).await?; + ChatId::new(chat_id).set_draft(&ctx, None).await + } + + /// Get draft for a chat, if any. + async fn get_draft(&self, account_id: u32, chat_id: u32) -> Result> { + let ctx = self.get_context(account_id).await?; + if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? { + Ok(Some( + MessageObject::from_msg_id(&ctx, draft.get_id()).await?, + )) + } else { + Ok(None) + } + } + // --------------------------------------------- // misc prototyping functions // that might get removed later again @@ -782,6 +984,91 @@ impl CommandApi { let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?; Ok(message_id.to_u32()) } + + // mimics the old desktop call, will get replaced with something better in the composer rewrite, + // the better version will just be sending the current draft, though there will be probably something similar with more options to this for the corner cases like setting a marker on the map + async fn misc_send_msg( + &self, + account_id: u32, + chat_id: u32, + text: Option, + file: Option, + location: Option<(f64, f64)>, + quoted_message_id: Option, + ) -> Result<(u32, MessageObject)> { + let ctx = self.get_context(account_id).await?; + let mut message = Message::new(if file.is_some() { + Viewtype::File + } else { + Viewtype::Text + }); + if text.is_some() { + message.set_text(text); + } + if let Some(file) = file { + message.set_file(file, None); + } + if let Some((latitude, longitude)) = location { + message.set_location(latitude, longitude); + } + if let Some(id) = quoted_message_id { + message + .set_quote( + &ctx, + Some( + &Message::load_from_db(&ctx, MsgId::new(id)) + .await + .context("message to quote could not be loaded")?, + ), + ) + .await?; + } + let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message) + .await? + .to_u32(); + let message = MessageObject::from_message_id(&ctx, msg_id).await?; + Ok((msg_id, message)) + } + + // mimics the old desktop call, will get replaced with something better in the composer rewrite, + // the better version should support: + // - changing viewtype to enable/disable compression + // - keeping same message id as long as attachment does not change for webxdc messages + async fn misc_set_draft( + &self, + account_id: u32, + chat_id: u32, + text: Option, + file: Option, + quoted_message_id: Option, + ) -> Result<()> { + let ctx = self.get_context(account_id).await?; + let mut draft = Message::new(if file.is_some() { + Viewtype::File + } else { + Viewtype::Text + }); + if text.is_some() { + draft.set_text(text); + } + if let Some(file) = file { + draft.set_file(file, None); + } + if let Some(id) = quoted_message_id { + draft + .set_quote( + &ctx, + Some( + &Message::load_from_db(&ctx, MsgId::new(id)) + .await + .context("message to quote could not be loaded")?, + ), + ) + .await?; + } + + ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await + } } // Helper functions (to prevent code duplication) diff --git a/deltachat-jsonrpc/src/api/types/chat.rs b/deltachat-jsonrpc/src/api/types/chat.rs index d2945babd..a94eb5de1 100644 --- a/deltachat-jsonrpc/src/api/types/chat.rs +++ b/deltachat-jsonrpc/src/api/types/chat.rs @@ -1,11 +1,13 @@ -use anyhow::{anyhow, Result}; -use deltachat::chat::get_chat_contacts; +use std::time::{Duration, SystemTime}; + +use anyhow::{anyhow, bail, Result}; +use deltachat::chat::{self, get_chat_contacts}; use deltachat::chat::{Chat, ChatId}; use deltachat::constants::Chattype; use deltachat::contact::{Contact, ContactId}; use deltachat::context::Context; use num_traits::cast::ToPrimitive; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use typescript_type_def::TypeDef; use super::color_int_to_hex_string; @@ -83,7 +85,7 @@ impl FullChat { name: chat.name.clone(), is_protected: chat.is_protected(), profile_image, //BLOBS ? - archived: chat.get_visibility() == deltachat::chat::ChatVisibility::Archived, + archived: chat.get_visibility() == chat::ChatVisibility::Archived, chat_type: chat .get_type() .to_u32() @@ -104,3 +106,86 @@ impl FullChat { }) } } + +/// cheaper version of fullchat, omits: +/// - contacts +/// - contact_ids +/// - fresh_message_counter +/// - ephemeral_timer +/// - self_in_group +/// - was_seen_recently +/// - can_send +/// +/// used when you only need the basic metadata of a chat like type, name, profile picture +#[derive(Serialize, TypeDef)] +#[serde(rename_all = "camelCase")] +pub struct BasicChat { + id: u32, + name: String, + is_protected: bool, + profile_image: Option, //BLOBS ? + archived: bool, + chat_type: u32, + is_unpromoted: bool, + is_self_talk: bool, + color: String, + is_contact_request: bool, + is_device_chat: bool, + is_muted: bool, +} + +impl BasicChat { + pub async fn try_from_dc_chat_id(context: &Context, chat_id: u32) -> Result { + let rust_chat_id = ChatId::new(chat_id); + let chat = Chat::load_from_db(context, rust_chat_id).await?; + + let profile_image = match chat.get_profile_image(context).await? { + Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()), + None => None, + }; + let color = color_int_to_hex_string(chat.get_color(context).await?); + + Ok(BasicChat { + id: chat_id, + name: chat.name.clone(), + is_protected: chat.is_protected(), + profile_image, //BLOBS ? + archived: chat.get_visibility() == chat::ChatVisibility::Archived, + chat_type: chat + .get_type() + .to_u32() + .ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap? + is_unpromoted: chat.is_unpromoted(), + is_self_talk: chat.is_self_talk(), + color, + is_contact_request: chat.is_contact_request(), + is_device_chat: chat.is_device_talk(), + is_muted: chat.is_muted(), + }) + } +} + +#[derive(Clone, Serialize, Deserialize, TypeDef)] +pub enum MuteDuration { + NotMuted, + Forever, + Until(i64), +} + +impl MuteDuration { + pub fn try_into_core_type(self) -> Result { + match self { + MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted), + MuteDuration::Forever => Ok(chat::MuteDuration::Forever), + MuteDuration::Until(n) => { + if n <= 0 { + bail!("failed to read mute duration") + } + + Ok(SystemTime::now() + .checked_add(Duration::from_secs(n as u64)) + .map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until)) + } + } + } +} diff --git a/deltachat-jsonrpc/src/api/types/contact.rs b/deltachat-jsonrpc/src/api/types/contact.rs index d5c9ab2a8..4ed4cf435 100644 --- a/deltachat-jsonrpc/src/api/types/contact.rs +++ b/deltachat-jsonrpc/src/api/types/contact.rs @@ -20,6 +20,8 @@ pub struct ContactObject { name_and_addr: String, is_blocked: bool, is_verified: bool, + /// the contact's last seen timestamp + last_seen: i64, was_seen_recently: bool, } @@ -46,6 +48,7 @@ impl ContactObject { name_and_addr: contact.get_name_n_addr(), is_blocked: contact.is_blocked(), is_verified, + last_seen: contact.last_seen(), was_seen_recently: contact.was_seen_recently(), }) } diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index 04493ee4d..62c867c39 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Result}; use deltachat::contact::Contact; use deltachat::context::Context; +use deltachat::download; use deltachat::message::Message; use deltachat::message::MsgId; use deltachat::message::Viewtype; @@ -9,6 +10,7 @@ use serde::Deserialize; use serde::Serialize; use typescript_type_def::TypeDef; +use super::color_int_to_hex_string; use super::contact::ContactObject; use super::webxdc::WebxdcMessageInfo; @@ -18,8 +20,9 @@ pub struct MessageObject { id: u32, chat_id: u32, from_id: u32, - quoted_text: Option, - quoted_message_id: Option, + quote: Option, + parent_id: Option, + text: Option, has_location: bool, has_html: bool, @@ -56,17 +59,36 @@ pub struct MessageObject { file_name: Option, webxdc_info: Option, + + download_state: DownloadState, +} + +#[derive(Serialize, TypeDef)] +#[serde(tag = "kind")] +enum MessageQuote { + JustText { + text: String, + }, + #[serde(rename_all = "camelCase")] + WithMessage { + text: String, + message_id: u32, + author_display_name: String, + author_display_color: String, + override_sender_name: Option, + image: Option, + is_forwarded: bool, + }, } impl MessageObject { pub async fn from_message_id(context: &Context, message_id: u32) -> Result { let msg_id = MsgId::new(message_id); - let message = Message::load_from_db(context, msg_id).await?; + Self::from_msg_id(context, msg_id).await + } - let quoted_message_id = message - .quoted_message(context) - .await? - .map(|m| m.get_id().to_u32()); + pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result { + let message = Message::load_from_db(context, msg_id).await?; let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?; let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?; @@ -79,12 +101,45 @@ impl MessageObject { None }; + let parent_id = message.parent(context).await?.map(|m| m.get_id().to_u32()); + + let download_state = message.download_state().into(); + + let quote = if let Some(quoted_text) = message.quoted_text() { + match message.quoted_message(context).await? { + Some(quote) => { + let quote_author = Contact::load_from_db(context, quote.get_from_id()).await?; + Some(MessageQuote::WithMessage { + text: quoted_text, + message_id: quote.get_id().to_u32(), + author_display_name: quote_author.get_display_name().to_owned(), + author_display_color: color_int_to_hex_string(quote_author.get_color()), + override_sender_name: quote.get_override_sender_name(), + image: if quote.get_viewtype() == Viewtype::Image + || quote.get_viewtype() == Viewtype::Gif + { + match quote.get_file(context) { + Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()), + None => None, + } + } else { + None + }, + is_forwarded: quote.is_forwarded(), + }) + } + None => Some(MessageQuote::JustText { text: quoted_text }), + } + } else { + None + }; + Ok(MessageObject { - id: message_id, + id: msg_id.to_u32(), chat_id: message.get_chat_id().to_u32(), from_id: message.get_from_id().to_u32(), - quoted_text: message.quoted_text(), - quoted_message_id, + quote, + parent_id, text: message.get_text(), has_location: message.has_location(), has_html: message.has_html(), @@ -131,6 +186,8 @@ impl MessageObject { file_bytes, file_name: message.get_filename(), webxdc_info, + + download_state, }) } } @@ -210,3 +267,22 @@ impl From for Viewtype { } } } + +#[derive(Serialize, TypeDef)] +pub enum DownloadState { + Done, + Available, + Failure, + InProgress, +} + +impl From for DownloadState { + fn from(state: download::DownloadState) -> Self { + match state { + download::DownloadState::Done => DownloadState::Done, + download::DownloadState::Available => DownloadState::Available, + download::DownloadState::Failure => DownloadState::Failure, + download::DownloadState::InProgress => DownloadState::InProgress, + } + } +} diff --git a/deltachat-jsonrpc/typescript/generated/client.ts b/deltachat-jsonrpc/typescript/generated/client.ts index bb5b9a65e..2b4667c49 100644 --- a/deltachat-jsonrpc/typescript/generated/client.ts +++ b/deltachat-jsonrpc/typescript/generated/client.ts @@ -204,6 +204,14 @@ export class RawClient { return (this._transport.request('chatlist_get_full_chat_by_id', [accountId, chatId] as RPC.Params)) as Promise; } + /** + * get basic info about a chat, + * use chatlist_get_full_chat_by_id() instead if you need more information + */ + public getBasicChatInfo(accountId: T.U32, chatId: T.U32): Promise { + return (this._transport.request('get_basic_chat_info', [accountId, chatId] as RPC.Params)) as Promise; + } + public acceptChat(accountId: T.U32, chatId: T.U32): Promise { return (this._transport.request('accept_chat', [accountId, chatId] as RPC.Params)) as Promise; @@ -265,6 +273,8 @@ export class RawClient { * If not set, the Setup-Contact protocol is offered in the QR code. * See https://countermitm.readthedocs.io/en/latest/new.html * for details about both protocols. + * + * return format: `[code, svg]` */ public getChatSecurejoinQrCodeSvg(accountId: T.U32, chatId: (T.U32|null)): Promise<[string,string]> { return (this._transport.request('get_chat_securejoin_qr_code_svg', [accountId, chatId] as RPC.Params)) as Promise<[string,string]>; @@ -306,6 +316,75 @@ export class RawClient { return (this._transport.request('add_device_message', [accountId, label, text] as RPC.Params)) as Promise; } + /** + * Mark all messages in a chat as _noticed_. + * _Noticed_ messages are no longer _fresh_ and do not count as being unseen + * but are still waiting for being marked as "seen" using markseen_msgs() + * (IMAP/MDNs is not done for noticed messages). + * + * Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED. + * See also markseen_msgs(). + */ + public marknoticedChat(accountId: T.U32, chatId: T.U32): Promise { + return (this._transport.request('marknoticed_chat', [accountId, chatId] as RPC.Params)) as Promise; + } + + + public getFirstUnreadMessageOfChat(accountId: T.U32, chatId: T.U32): Promise<(T.U32|null)> { + return (this._transport.request('get_first_unread_message_of_chat', [accountId, chatId] as RPC.Params)) as Promise<(T.U32|null)>; + } + + /** + * Set mute duration of a chat. + * + * The UI can then call is_chat_muted() when receiving a new message + * to decide whether it should trigger an notification. + * + * Muted chats should not sound or vibrate + * and should not show a visual notification in the system area. + * Moreover, muted chats should be excluded from global badge counter + * (get_fresh_msgs() skips muted chats therefore) + * and the in-app, per-chat badge counter should use a less obtrusive color. + * + * Sends out #DC_EVENT_CHAT_MODIFIED. + */ + public setChatMuteDuration(accountId: T.U32, chatId: T.U32, duration: T.MuteDuration): Promise { + return (this._transport.request('set_chat_mute_duration', [accountId, chatId, duration] as RPC.Params)) as Promise; + } + + /** + * Check whether the chat is currently muted (can be changed by set_chat_mute_duration()). + * + * This is available as a standalone function outside of fullchat, because it might be only needed for notification + */ + public isChatMuted(accountId: T.U32, chatId: T.U32): Promise { + return (this._transport.request('is_chat_muted', [accountId, chatId] as RPC.Params)) as Promise; + } + + /** + * Mark messages as presented to the user. + * Typically, UIs call this function on scrolling through the message list, + * when the messages are presented at least for a little moment. + * The concrete action depends on the type of the chat and on the users settings + * (dc_msgs_presented() may be a better name therefore, but well. :) + * + * - For normal chats, the IMAP state is updated, MDN is sent + * (if set_config()-options `mdns_enabled` is set) + * and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions. + * + * - For contact requests, no IMAP or MDNs is done + * and the internal state is not changed therefore. + * See also marknoticed_chat(). + * + * Moreover, timer is started for incoming ephemeral messages. + * This also happens for contact requests chats. + * + * One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat. + */ + public markseenMsgs(accountId: T.U32, msgIds: (T.U32)[]): Promise { + return (this._transport.request('markseen_msgs', [accountId, msgIds] as RPC.Params)) as Promise; + } + public messageListGetMessageIds(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.U32)[]> { return (this._transport.request('message_list_get_message_ids', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.U32)[]>; @@ -396,6 +475,15 @@ export class RawClient { return (this._transport.request('contacts_get_contacts_by_ids', [accountId, ids] as RPC.Params)) as Promise>; } + /** + * Get encryption info for a contact. + * Get a multi-line encryption info, containing your fingerprint and the + * fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification. + */ + public getContactEncryptionInfo(accountId: T.U32, contactId: T.U32): Promise { + return (this._transport.request('get_contact_encryption_info', [accountId, contactId] as RPC.Params)) as Promise; + } + /** * Returns all message IDs of the given types in a chat. * Typically used to show a gallery. @@ -411,6 +499,49 @@ export class RawClient { return (this._transport.request('chat_get_media', [accountId, chatId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<(T.U32)[]>; } + /** + * Indicate that the network likely has come back. + * or just that the network conditions might have changed + */ + public maybeNetwork(): Promise { + return (this._transport.request('maybe_network', [] as RPC.Params)) as Promise; + } + + /** + * Get the current connectivity, i.e. whether the device is connected to the IMAP server. + * One of: + * - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot + * - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot + * - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel + * - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot + * + * We don't use exact values but ranges here so that we can split up + * states into multiple states in the future. + * + * Meant as a rough overview that can be shown + * e.g. in the title of the main screen. + * + * If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted. + */ + public getConnectivity(accountId: T.U32): Promise { + return (this._transport.request('get_connectivity', [accountId] as RPC.Params)) as Promise; + } + + /** + * Get an overview of the current connectivity, and possibly more statistics. + * Meant to give the user more insight about the current status than + * the basic connectivity info returned by get_connectivity(); show this + * e.g., if the user taps on said basic connectivity info. + * + * If this page changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted. + * + * This comes as an HTML from the core so that we can easily improve it + * and the improvement instantly reaches all UIs. + */ + public getConnectivityHtml(accountId: T.U32): Promise { + return (this._transport.request('get_connectivity_html', [accountId] as RPC.Params)) as Promise; + } + public webxdcSendStatusUpdate(accountId: T.U32, instanceMsgId: T.U32, updateStr: string, description: string): Promise { return (this._transport.request('webxdc_send_status_update', [accountId, instanceMsgId, updateStr, description] as RPC.Params)) as Promise; @@ -428,6 +559,30 @@ export class RawClient { return (this._transport.request('message_get_webxdc_info', [accountId, instanceMsgId] as RPC.Params)) as Promise; } + /** + * Forward messages to another chat. + * + * All types of messages can be forwarded, + * however, they will be flagged as such (dc_msg_is_forwarded() is set). + * + * Original sender, info-state and webxdc updates are not forwarded on purpose. + */ + public forwardMessages(accountId: T.U32, messageIds: (T.U32)[], chatId: T.U32): Promise { + return (this._transport.request('forward_messages', [accountId, messageIds, chatId] as RPC.Params)) as Promise; + } + + + public removeDraft(accountId: T.U32, chatId: T.U32): Promise { + return (this._transport.request('remove_draft', [accountId, chatId] as RPC.Params)) as Promise; + } + + /** + * Get draft for a chat, if any. + */ + public getDraft(accountId: T.U32, chatId: T.U32): Promise<(T.Message|null)> { + return (this._transport.request('get_draft', [accountId, chatId] as RPC.Params)) as Promise<(T.Message|null)>; + } + /** * Returns the messageid of the sent message */ @@ -436,4 +591,14 @@ export class RawClient { } + public miscSendMsg(accountId: T.U32, chatId: T.U32, text: (string|null), file: (string|null), location: ([T.F64,T.F64]|null), quotedMessageId: (T.U32|null)): Promise<[T.U32,T.Message]> { + return (this._transport.request('misc_send_msg', [accountId, chatId, text, file, location, quotedMessageId] as RPC.Params)) as Promise<[T.U32,T.Message]>; + } + + + public miscSetDraft(accountId: T.U32, chatId: T.U32, text: (string|null), file: (string|null), quotedMessageId: (T.U32|null)): Promise { + return (this._transport.request('misc_set_draft', [accountId, chatId, text, file, quotedMessageId] as RPC.Params)) as Promise; + } + + } diff --git a/deltachat-jsonrpc/typescript/generated/types.ts b/deltachat-jsonrpc/typescript/generated/types.ts index fa5e4e538..23c90f0ac 100644 --- a/deltachat-jsonrpc/typescript/generated/types.ts +++ b/deltachat-jsonrpc/typescript/generated/types.ts @@ -16,8 +16,41 @@ export type ChatListItemFetchResult=(({"type":"ChatListItem";}&{"id":U32;"name": * contact id if this is a dm chat (for view profile entry in context menu) */ "dmChatContact":(U32|null);"wasSeenRecently":boolean;})|{"type":"ArchiveLink";}|({"type":"Error";}&{"id":U32;"error":string;})); -export type Contact={"address":string;"color":string;"authName":string;"status":string;"displayName":string;"id":U32;"name":string;"profileImage":(string|null);"nameAndAddr":string;"isBlocked":boolean;"isVerified":boolean;"wasSeenRecently":boolean;}; +export type Contact={"address":string;"color":string;"authName":string;"status":string;"displayName":string;"id":U32;"name":string;"profileImage":(string|null);"nameAndAddr":string;"isBlocked":boolean;"isVerified":boolean; +/** + * the contact's last seen timestamp + */ +"lastSeen":I64;"wasSeenRecently":boolean;}; export type FullChat={"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"contacts":(Contact)[];"contactIds":(U32)[];"color":string;"freshMessageCounter":Usize;"isContactRequest":boolean;"isDeviceChat":boolean;"selfInGroup":boolean;"isMuted":boolean;"ephemeralTimer":U32;"canSend":boolean;"wasSeenRecently":boolean;}; + +/** + * cheaper version of fullchat, omits: + * - contacts + * - contact_ids + * - fresh_message_counter + * - ephemeral_timer + * - self_in_group + * - was_seen_recently + * - can_send + * + * used when you only need the basic metadata of a chat like type, name, profile picture + */ +export type BasicChat= +/** + * cheaper version of fullchat, omits: + * - contacts + * - contact_ids + * - fresh_message_counter + * - ephemeral_timer + * - self_in_group + * - was_seen_recently + * - can_send + * + * used when you only need the basic metadata of a chat like type, name, profile picture + */ +{"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"color":string;"isContactRequest":boolean;"isDeviceChat":boolean;"isMuted":boolean;}; +export type MuteDuration=("NotMuted"|"Forever"|{"Until":I64;}); +export type MessageQuote=(({"kind":"JustText";}&{"text":string;})|({"kind":"WithMessage";}&{"text":string;"messageId":U32;"authorDisplayName":string;"authorDisplayColor":string;"overrideSenderName":(string|null);"image":(string|null);"isForwarded":boolean;})); export type Viewtype=("Unknown"| /** * Text message. @@ -102,5 +135,7 @@ export type WebxdcMessageInfo={ * True if full internet access should be granted to the app. */ "internetAccess":boolean;}; -export type Message={"id":U32;"chatId":U32;"fromId":U32;"quotedText":(string|null);"quotedMessageId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);}; -export type __AllTyps=[string,boolean,Record,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],U32,Account,U32,string,(ProviderInfo|null),U32,boolean,U32,Record,U32,string,(string|null),null,U32,Record,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record,U32,null,U32,null,U32,(U32)[],U32,U32,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record,U32,U32,FullChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,string,string,U32,U32,U32,U32,(U32)[],U32,U32,Message,U32,(U32)[],Record,U32,(U32)[],null,U32,U32,string,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record,U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,string,U32,U32]; +export type DownloadState=("Done"|"Available"|"Failure"|"InProgress"); +export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;}; +export type F64=number; +export type __AllTyps=[string,boolean,Record,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],U32,Account,U32,string,(ProviderInfo|null),U32,boolean,U32,Record,U32,string,(string|null),null,U32,Record,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record,U32,null,U32,null,U32,(U32)[],U32,U32,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,Message,U32,(U32)[],Record,U32,(U32)[],null,U32,U32,string,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record,U32,U32,string,U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],null,U32,U32,U32,string,U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,null,U32,U32,(Message|null),U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null]; diff --git a/src/contact.rs b/src/contact.rs index 66b25b7cd..079b015b0 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -896,11 +896,8 @@ impl Contact { EncryptPreference::Reset => stock_str::encr_none(context).await, }; - ret += &format!( - "{}.\n{}:", - stock_message, - stock_str::finger_prints(context).await - ); + let finger_prints = stock_str::finger_prints(context).await; + ret += &format!("{}.\n{}:", stock_message, finger_prints); let fingerprint_self = SignedPublicKey::load_self(context) .await? diff --git a/src/scheduler/connectivity.rs b/src/scheduler/connectivity.rs index 4fe16c90b..880c2760a 100644 --- a/src/scheduler/connectivity.rs +++ b/src/scheduler/connectivity.rs @@ -390,7 +390,8 @@ impl Context { // ============================================================================================= let watched_folders = get_watched_folder_configs(self).await?; - ret += &format!("

{}

    ", stock_str::incoming_messages(self).await); + let incoming_messages = stock_str::incoming_messages(self).await; + ret += &format!("

    {}

      ", incoming_messages); for (folder, state) in &folders_states { let mut folder_added = false; @@ -432,10 +433,8 @@ impl Context { // Your last message was sent successfully // ============================================================================================= - ret += &format!( - "

      {}

      • ", - stock_str::outgoing_messages(self).await - ); + let outgoing_messages = stock_str::outgoing_messages(self).await; + ret += &format!("

        {}

        • ", outgoing_messages); let detailed = smtp.get_detailed().await; ret += &*detailed.to_icon(); ret += " "; @@ -450,10 +449,8 @@ impl Context { // ============================================================================================= let domain = tools::EmailAddress::new(&self.get_primary_self_addr().await?)?.domain; - ret += &format!( - "

          {}

            ", - stock_str::storage_on_domain(self, domain).await - ); + let storage_on_domain = stock_str::storage_on_domain(self, domain).await; + ret += &format!("

            {}

              ", storage_on_domain); let quota = self.quota.read().await; if let Some(quota) = &*quota { match "a.recent { @@ -473,30 +470,23 @@ impl Context { info!(self, "connectivity: root name hidden: \"{}\"", root_name); } + let messages = stock_str::messages(self).await; + let part_of_total_used = stock_str::part_of_total_used( + self, + resource.usage.to_string(), + resource.limit.to_string(), + ) + .await; ret += &match &resource.name { Atom(resource_name) => { format!( "{}: {}", &*escaper::encode_minimal(resource_name), - stock_str::part_of_total_used( - self, - resource.usage.to_string(), - resource.limit.to_string() - ) - .await, + part_of_total_used ) } Message => { - format!( - "{}: {}", - stock_str::messages(self).await, - stock_str::part_of_total_used( - self, - resource.usage.to_string(), - resource.limit.to_string() - ) - .await, - ) + format!("{}: {}", part_of_total_used, messages) } Storage => { // do not use a special title needed for "Storage": @@ -538,7 +528,8 @@ impl Context { self.schedule_quota_update().await?; } } else { - ret += &format!("
            • {}
            • ", stock_str::not_connected(self).await); + let not_connected = stock_str::not_connected(self).await; + ret += &format!("
            • {}
            • ", not_connected); self.schedule_quota_update().await?; } ret += "
            ";