api!: Contact::get_color(): Preserve address-based color hue for SELF

4010c60e7b "feat: use key fingerprints for color generation" changes
colors for contacts including SELF. Even if an avatar is set, the self-color is visible in
e.g. replies to outgoing messages.

This adds `Config::Selfcolor` and sets it in a migration. This doesn't preserve the old
address-based color accurately because the old code generating colors is already dropped, so this
only preserves the color angle (hue).
This commit is contained in:
iequidoo
2025-08-27 07:20:52 -03:00
parent 0bbd910883
commit 0dab21007d
13 changed files with 66 additions and 15 deletions

View File

@@ -4311,7 +4311,8 @@ pub unsafe extern "C" fn dc_contact_get_color(contact: *mut dc_contact_t) -> u32
return 0;
}
let ffi_contact = &*contact;
ffi_contact.contact.get_color()
let ctx = &*ffi_contact.context;
block_on(ffi_contact.contact.get_color(ctx)).unwrap_or_log_default(ctx, "Failed get_color")
}
#[no_mangle]

View File

@@ -32,7 +32,10 @@ impl Account {
let addr = ctx.get_config(Config::Addr).await?;
let profile_image = ctx.get_config(Config::Selfavatar).await?;
let color = color_int_to_hex_string(
Contact::get_by_id(ctx, ContactId::SELF).await?.get_color(),
Contact::get_by_id(ctx, ContactId::SELF)
.await?
.get_color(ctx)
.await?,
);
let private_tag = ctx.get_config(Config::PrivateTag).await?;
Ok(Account::Configured {

View File

@@ -97,7 +97,7 @@ impl ContactObject {
Ok(ContactObject {
address: contact.get_addr().to_owned(),
color: color_int_to_hex_string(contact.get_color()),
color: color_int_to_hex_string(contact.get_color(context).await?),
auth_name: contact.get_authname().to_owned(),
status: contact.get_status().to_owned(),
display_name: contact.get_display_name().to_owned(),

View File

@@ -162,7 +162,9 @@ impl MessageObject {
message_id: quote.get_id().to_u32(),
chat_id: quote.get_chat_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()),
author_display_color: color_int_to_hex_string(
quote_author.get_color(context).await?,
),
override_sender_name: quote.get_override_sender_name(),
image: if quote.get_viewtype() == Viewtype::Image
|| quote.get_viewtype() == Viewtype::Gif
@@ -581,7 +583,7 @@ impl MessageSearchResult {
id: msg_id.to_u32(),
author_profile_image: profile_image,
author_name,
author_color: color_int_to_hex_string(sender.get_color()),
author_color: color_int_to_hex_string(sender.get_color(context).await?),
author_id: sender.id.to_u32(),
chat_id: chat.id.to_u32(),
chat_name: chat.get_name().to_owned(),

View File

@@ -1807,7 +1807,7 @@ impl Chat {
let contacts = get_chat_contacts(context, self.id).await?;
if let Some(contact_id) = contacts.first() {
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
color = contact.get_color();
color = contact.get_color(context).await?;
}
}
} else if !self.grpid.is_empty() {

View File

@@ -136,6 +136,9 @@ pub enum Config {
/// Own name to use in the `From:` field when sending messages.
Displayname,
/// Own color to use in the avatar placeholder and replies to outgoing messages.
Selfcolor,
/// Own status to display, sent in message footer.
Selfstatus,
@@ -474,6 +477,7 @@ impl Config {
| Self::MvboxMove
| Self::ShowEmails
| Self::Selfavatar
| Self::Selfcolor
| Self::Selfstatus,
)
}
@@ -833,7 +837,7 @@ impl Context {
}
if matches!(
key,
Config::Displayname | Config::Selfavatar | Config::PrivateTag
Config::Displayname | Config::Selfavatar | Config::Selfcolor | Config::PrivateTag
) {
self.emit_event(EventType::AccountsItemChanged);
}

View File

@@ -245,6 +245,7 @@ async fn test_sync() -> Result<()> {
Ok(())
}
test_config_str(&alice0, &alice1, Config::Displayname, "Alice Sync").await?;
test_config_str(&alice0, &alice1, Config::Selfcolor, "255").await?;
test_config_str(&alice0, &alice1, Config::Selfstatus, "My status").await?;
assert!(alice0.get_config(Config::Selfavatar).await?.is_none());

View File

@@ -1579,11 +1579,17 @@ impl Contact {
/// or email address (for address-contacts) and can be used
/// for an fallback avatar with white initials
/// as well as for headlines in bubbles of group chats.
pub fn get_color(&self) -> u32 {
pub async fn get_color(&self, context: &Context) -> Result<u32> {
if self.id == ContactId::SELF {
if let Some(v) = context.get_config_opt_parsed(Config::Selfcolor).await? {
return Ok(v);
}
}
if let Some(fingerprint) = self.fingerprint() {
str_to_color(&fingerprint.hex())
Ok(str_to_color(&fingerprint.hex()))
} else {
str_to_color(&self.addr.to_lowercase())
Ok(str_to_color(&self.addr.to_lowercase()))
}
}

View File

@@ -758,17 +758,26 @@ async fn test_lookup_id_by_addr() {
async fn test_contact_get_color() -> Result<()> {
let t = TestContext::new().await;
let contact_id = Contact::create(&t, "name", "name@example.net").await?;
let color1 = Contact::get_by_id(&t, contact_id).await?.get_color();
let color1 = Contact::get_by_id(&t, contact_id)
.await?
.get_color(&t)
.await?;
assert_eq!(color1, 0x4947dc);
let t = TestContext::new().await;
let contact_id = Contact::create(&t, "prename name", "name@example.net").await?;
let color2 = Contact::get_by_id(&t, contact_id).await?.get_color();
let color2 = Contact::get_by_id(&t, contact_id)
.await?
.get_color(&t)
.await?;
assert_eq!(color2, color1);
let t = TestContext::new().await;
let contact_id = Contact::create(&t, "Name", "nAme@exAmple.NET").await?;
let color3 = Contact::get_by_id(&t, contact_id).await?.get_color();
let color3 = Contact::get_by_id(&t, contact_id)
.await?
.get_color(&t)
.await?;
assert_eq!(color3, color1);
Ok(())
}

View File

@@ -277,6 +277,7 @@ async fn test_get_info_completeness() {
"mail_security",
"notify_about_wrong_pw",
"self_reporting_id",
"selfcolor",
"selfstatus",
"send_server",
"send_user",

View File

@@ -161,7 +161,7 @@ async fn self_info(context: &Context) -> Result<(Option<Vec<u8>>, String, String
None => contact.get_addr().to_string(),
};
let addr = contact.get_addr().to_string();
let color = color_int_to_hex_string(contact.get_color());
let color = color_int_to_hex_string(contact.get_color(context).await?);
Ok((avatar, displayname, addr, color))
}

View File

@@ -1261,6 +1261,30 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
.await?;
}
inc_and_check(&mut migration_version, 134)?;
if dbversion < migration_version {
let trans_fn = |t: &mut rusqlite::Transaction| {
let Some(addr): Option<String> = t
.query_row(
"SELECT value FROM config WHERE keyname='configured_addr'",
(),
|row| row.get(0),
)
.optional()?
else {
return Ok(());
};
let color = crate::color::str_to_color(&addr.to_lowercase());
t.execute(
"INSERT OR IGNORE INTO config (keyname, value) VALUES ('selfcolor', ?)",
(color,),
)?;
Ok(())
};
sql.execute_migration_transaction(trans_fn, migration_version)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?

View File

@@ -112,7 +112,7 @@ pub(crate) async fn intercept_get_updates(
hash_map::Entry::Vacant(e) => {
let contact = Contact::get_by_id(context, location.contact_id).await?;
let name = contact.get_display_name().to_string();
let color = color_int_to_hex_string(contact.get_color());
let color = color_int_to_hex_string(contact.get_color(context).await?);
e.insert((name, color)).clone()
}
hash_map::Entry::Occupied(e) => e.get().clone(),