mirror of
https://github.com/chatmail/core.git
synced 2026-09-22 04:58:47 +03:00
api!: replace was_seen_recently by freshness in contact object
BREAKING CHANGE: use contact's `freshness` instead of `seen_recently`
This commit is contained in:
@@ -4912,6 +4912,38 @@ uint32_t dc_msg_get_saved_msg_id (const dc_msg_t* msg);
|
||||
int dc_msg_is_pinned (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_FRESHNESS DC_FRESHNESS
|
||||
*
|
||||
* These constants describe the freshness of a contact,
|
||||
* as returned by dc_contact_get_freshness().
|
||||
*
|
||||
* @addtogroup DC_FRESHNESS
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contact shall not be highlighted, e.g. neither shown with a "seen recently" dot
|
||||
* nor with a "not seen for a long time" hint.
|
||||
*/
|
||||
#define DC_FRESHNESS_NORMAL 0
|
||||
|
||||
/**
|
||||
* Contact was seen recently, the UI shall highlight it e.g. with a little green dot on the avatar.
|
||||
*/
|
||||
#define DC_FRESHNESS_RECENTLY_SEEN 1
|
||||
|
||||
/**
|
||||
* Contact was not seen for a long time, the UI shall highlight it e.g. with a string
|
||||
* below the contact name (e.g. "Seen 2 months ago").
|
||||
*/
|
||||
#define DC_FRESHNESS_OLD 2
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_contact_t
|
||||
*
|
||||
@@ -5084,18 +5116,18 @@ int64_t dc_contact_get_last_seen (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the contact was seen recently.
|
||||
* Get the contact's freshness.
|
||||
*
|
||||
* The UI shall hightlight contacts that are recently seen by a little green dot on the avatar
|
||||
* and contacts that were not seen for a long time by a string below the contact name (e.g. "Seen 2 months ago")
|
||||
*
|
||||
* The UI may highlight these contacts,
|
||||
* eg. draw a little green dot on the avatars of the users recently seen.
|
||||
* DC_CONTACT_ID_SELF and other special contact IDs are defined as never seen recently (they should not get a dot).
|
||||
* To get the time a contact was seen, use dc_contact_get_last_seen().
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return 1=contact seen recently, 0=contact not seen recently.
|
||||
* @return One of the @ref DC_FRESHNESS constants.
|
||||
*/
|
||||
int dc_contact_was_seen_recently (const dc_contact_t* contact);
|
||||
int dc_contact_get_freshness (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -4034,13 +4034,13 @@ pub unsafe extern "C" fn dc_contact_get_last_seen(contact: *mut dc_contact_t) ->
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_contact_was_seen_recently(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
pub unsafe extern "C" fn dc_contact_get_freshness(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_was_seen_recently()");
|
||||
eprintln!("ignoring careless call to dc_contact_get_freshness()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_contact = unsafe { &*contact };
|
||||
ffi_contact.contact.was_seen_recently() as libc::c_int
|
||||
u32::from(ffi_contact.contact.get_freshness()) as libc::c_int
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
|
||||
@@ -9,6 +9,8 @@ use deltachat::context::Context;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use crate::api::types::contact::ContactFreshness;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
@@ -69,7 +71,7 @@ pub struct FullChat {
|
||||
is_muted: bool,
|
||||
ephemeral_timer: u32,
|
||||
can_send: bool,
|
||||
was_seen_recently: bool,
|
||||
freshness: ContactFreshness,
|
||||
mailing_list_address: Option<String>,
|
||||
}
|
||||
|
||||
@@ -92,16 +94,17 @@ impl FullChat {
|
||||
|
||||
let can_send = chat.can_send(context).await?;
|
||||
|
||||
let was_seen_recently = if chat.get_type() == Chattype::Single {
|
||||
let freshness = if chat.get_type() == Chattype::Single {
|
||||
match contact_ids.first() {
|
||||
Some(contact) => Contact::get_by_id(context, *contact)
|
||||
.await
|
||||
.context("failed to load contact for was_seen_recently")?
|
||||
.was_seen_recently(),
|
||||
None => false,
|
||||
.context("failed to load contact for get_freshness")?
|
||||
.get_freshness()
|
||||
.into(),
|
||||
None => ContactFreshness::Normal,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
ContactFreshness::Normal
|
||||
};
|
||||
|
||||
let mailing_list_address = chat.get_mailinglist_addr().map(|s| s.to_string());
|
||||
@@ -126,7 +129,7 @@ impl FullChat {
|
||||
is_muted: chat.is_muted(),
|
||||
ephemeral_timer,
|
||||
can_send,
|
||||
was_seen_recently,
|
||||
freshness,
|
||||
mailing_list_address,
|
||||
})
|
||||
}
|
||||
@@ -137,7 +140,7 @@ impl FullChat {
|
||||
/// - fresh_message_counter
|
||||
/// - ephemeral_timer
|
||||
/// - self_in_group
|
||||
/// - was_seen_recently
|
||||
/// - freshness
|
||||
/// - can_send
|
||||
///
|
||||
/// used when you only need the basic metadata of a chat like type, name, profile picture
|
||||
|
||||
@@ -11,6 +11,8 @@ use num_traits::cast::ToPrimitive;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use crate::api::types::contact::ContactFreshness;
|
||||
|
||||
use super::chat::JsonrpcChatType;
|
||||
use super::color_int_to_hex_string;
|
||||
use super::message::MessageViewtype;
|
||||
@@ -68,7 +70,7 @@ pub enum ChatListItemFetchResult {
|
||||
is_contact_request: bool,
|
||||
/// contact id if this is a dm chat (for view profile entry in context menu)
|
||||
dm_chat_contact: Option<u32>,
|
||||
was_seen_recently: bool,
|
||||
freshness: ContactFreshness,
|
||||
last_message_type: Option<MessageViewtype>,
|
||||
last_message_id: Option<u32>,
|
||||
},
|
||||
@@ -127,22 +129,20 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
None => (None, None),
|
||||
};
|
||||
|
||||
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
|
||||
let (dm_chat_contact, freshness) = if chat.get_type() == Chattype::Single {
|
||||
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
||||
let contact = chat_contacts.first();
|
||||
let was_seen_recently = match contact {
|
||||
let freshness = match contact {
|
||||
Some(contact) => Contact::get_by_id(ctx, *contact)
|
||||
.await
|
||||
.context("contact")?
|
||||
.was_seen_recently(),
|
||||
None => false,
|
||||
.get_freshness()
|
||||
.into(),
|
||||
None => ContactFreshness::Normal,
|
||||
};
|
||||
(
|
||||
contact.map(|contact_id| contact_id.to_u32()),
|
||||
was_seen_recently,
|
||||
)
|
||||
(contact.map(|contact_id| contact_id.to_u32()), freshness)
|
||||
} else {
|
||||
(None, false)
|
||||
(None, ContactFreshness::Normal)
|
||||
};
|
||||
|
||||
let color = color_int_to_hex_string(chat.get_color(ctx).await?);
|
||||
@@ -170,7 +170,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
is_muted: chat.is_muted(),
|
||||
is_contact_request: chat.is_contact_request(),
|
||||
dm_chat_contact,
|
||||
was_seen_recently,
|
||||
freshness,
|
||||
last_message_type: message_type,
|
||||
last_message_id: last_msgid.map(|id| id.to_u32()),
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use deltachat::contact;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::key::{DcKey, SignedPublicKey};
|
||||
use serde::Serialize;
|
||||
@@ -6,6 +7,27 @@ use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
|
||||
/// Freshness of a contact, based on when it was last seen.
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
pub enum ContactFreshness {
|
||||
/// Contact shall not be highlighted.
|
||||
Normal,
|
||||
/// Contact was seen recently.
|
||||
RecentlySeen,
|
||||
/// Contact was not seen for a long time.
|
||||
Old,
|
||||
}
|
||||
|
||||
impl From<contact::Freshness> for ContactFreshness {
|
||||
fn from(freshness: contact::Freshness) -> Self {
|
||||
match freshness {
|
||||
contact::Freshness::Normal => ContactFreshness::Normal,
|
||||
contact::Freshness::RecentlySeen => ContactFreshness::RecentlySeen,
|
||||
contact::Freshness::Old => ContactFreshness::Old,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Contact", rename_all = "camelCase")]
|
||||
pub struct ContactObject {
|
||||
@@ -32,7 +54,7 @@ pub struct ContactObject {
|
||||
|
||||
/// the contact's last seen timestamp
|
||||
last_seen: i64,
|
||||
was_seen_recently: bool,
|
||||
freshness: ContactFreshness,
|
||||
|
||||
/// If the contact is a bot.
|
||||
is_bot: bool,
|
||||
@@ -60,7 +82,7 @@ impl ContactObject {
|
||||
is_key_contact: contact.is_key_contact(),
|
||||
e2ee_avail: contact.e2ee_avail(context).await?,
|
||||
last_seen: contact.last_seen(),
|
||||
was_seen_recently: contact.was_seen_recently(),
|
||||
freshness: contact.get_freshness().into(),
|
||||
is_bot: contact.is_bot(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -40,9 +40,33 @@ use crate::sync::{self, Sync::*};
|
||||
use crate::tools::{SystemTime, duration_to_str, get_abs_path, normalize_text, time, to_lowercase};
|
||||
use crate::{chat, chatlist_events, ensure_and_debug_assert, stock_str};
|
||||
|
||||
/// Time during which a contact is considered as seen recently.
|
||||
/// If the contact's "last seen" is newer, the contact freshness is set to "recently seen".
|
||||
const SEEN_RECENTLY_SECONDS: i64 = 600;
|
||||
|
||||
/// If the contact's "last seen" is older, the contact freshness is set to "old".
|
||||
const CONTACT_OLD_SECONDS: i64 = 60 * 24 * 60 * 60;
|
||||
|
||||
/// Freshness of a contact, based on when it was last seen.
|
||||
///
|
||||
/// Used by the UI to highlight contacts:
|
||||
/// recently seen contacts get a little green dot on the avatar,
|
||||
/// contacts not seen for a long time get a string below the name (e.g. "Seen 2 months ago").
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Freshness {
|
||||
/// Contact shall not be highlighted.
|
||||
Normal = 0,
|
||||
/// Contact was seen recently.
|
||||
RecentlySeen = 1,
|
||||
/// Contact was not seen for a long time.
|
||||
Old = 2,
|
||||
}
|
||||
|
||||
impl From<Freshness> for u32 {
|
||||
fn from(freshness: Freshness) -> Self {
|
||||
freshness as u32
|
||||
}
|
||||
}
|
||||
|
||||
/// Contact ID, including reserved IDs.
|
||||
///
|
||||
/// Some contact IDs are reserved to identify special contacts. This
|
||||
@@ -726,9 +750,22 @@ impl Contact {
|
||||
}
|
||||
|
||||
/// Returns `true` if this contact was seen recently.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub fn was_seen_recently(&self) -> bool {
|
||||
time() - self.last_seen <= SEEN_RECENTLY_SECONDS
|
||||
pub fn get_freshness(&self) -> Freshness {
|
||||
if self.id.is_special() {
|
||||
return Freshness::Normal;
|
||||
}
|
||||
|
||||
let is_old = time().saturating_sub(self.last_seen) > CONTACT_OLD_SECONDS;
|
||||
if is_old || self.last_seen <= 0 {
|
||||
return Freshness::Old;
|
||||
}
|
||||
|
||||
let seen_recently = time().saturating_sub(self.last_seen) <= SEEN_RECENTLY_SECONDS;
|
||||
if seen_recently {
|
||||
return Freshness::RecentlySeen;
|
||||
}
|
||||
|
||||
Freshness::Normal
|
||||
}
|
||||
|
||||
/// Check if a contact is blocked.
|
||||
|
||||
@@ -1048,7 +1048,7 @@ async fn test_last_seen() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_was_seen_recently() -> Result<()> {
|
||||
async fn test_contact_freshness() -> Result<()> {
|
||||
let _n = TimeShiftFalsePositiveNote;
|
||||
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -1061,15 +1061,15 @@ async fn test_was_seen_recently() -> Result<()> {
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let contacts = chat::get_chat_contacts(&bob, chat.id).await?;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(!contact.was_seen_recently());
|
||||
assert_eq!(contact.get_freshness(), Freshness::Old);
|
||||
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
|
||||
assert!(contact.was_seen_recently());
|
||||
assert_eq!(contact.get_freshness(), Freshness::RecentlySeen);
|
||||
|
||||
let self_contact = Contact::get_by_id(&bob, ContactId::SELF).await?;
|
||||
assert!(!self_contact.was_seen_recently());
|
||||
assert_eq!(self_contact.get_freshness(), Freshness::Normal);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1087,11 +1087,11 @@ async fn test_was_seen_recently_event() -> Result<()> {
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent_msg = alice.send_text(chat.id, "moin").await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(!contact.was_seen_recently());
|
||||
assert_ne!(contact.get_freshness(), Freshness::RecentlySeen);
|
||||
bob.evtracker.clear_events();
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(contact.was_seen_recently());
|
||||
assert_eq!(contact.get_freshness(), Freshness::RecentlySeen);
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
|
||||
.await;
|
||||
@@ -1099,12 +1099,12 @@ async fn test_was_seen_recently_event() -> Result<()> {
|
||||
.interrupt(contact.id, contact.last_seen)
|
||||
.await;
|
||||
|
||||
// Wait for `was_seen_recently()` to turn off.
|
||||
// Wait for "seen recently" to turn off.
|
||||
bob.evtracker.clear_events();
|
||||
SystemTime::shift(Duration::from_secs(SEEN_RECENTLY_SECONDS as u64 * 2));
|
||||
recently_seen_loop.interrupt(ContactId::UNDEFINED, 0).await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(!contact.was_seen_recently());
|
||||
assert_eq!(contact.get_freshness(), Freshness::Normal);
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
|
||||
.await;
|
||||
|
||||
Reference in New Issue
Block a user