diff --git a/CHANGELOG.md b/CHANGELOG.md index ba96c12..01f399f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Avatars for contacts and groups. + ### Fixed - Persistent typing messages diff --git a/Cargo.lock b/Cargo.lock index 4bed1e0..352a01a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2073,7 +2073,7 @@ dependencies = [ [[package]] name = "libsignal-service" version = "0.1.0" -source = "git+https://github.com/whisperfish/libsignal-service-rs?rev=a2e7540a71866a62028ad0205574a5feb0e717ec#a2e7540a71866a62028ad0205574a5feb0e717ec" +source = "git+https://github.com/whisperfish/libsignal-service-rs?rev=1a8cdf81#1a8cdf81e66477077bd130cdd0313762e3d5e3c8" dependencies = [ "aes", "aes-gcm", @@ -2108,7 +2108,7 @@ dependencies = [ [[package]] name = "libsignal-service-hyper" version = "0.1.0" -source = "git+https://github.com/whisperfish/libsignal-service-rs?rev=a2e7540a71866a62028ad0205574a5feb0e717ec#a2e7540a71866a62028ad0205574a5feb0e717ec" +source = "git+https://github.com/whisperfish/libsignal-service-rs?rev=1a8cdf81#1a8cdf81e66477077bd130cdd0313762e3d5e3c8" dependencies = [ "async-trait", "async-tungstenite", @@ -2881,7 +2881,7 @@ checksum = "94e851c7654eed9e68d7d27164c454961a616cf8c203d500607ef22c737b51bb" [[package]] name = "presage" version = "0.6.0-dev" -source = "git+https://github.com/Schmiddiii/presage?rev=75011227faac60773bb1422d365ecc7fed95484a#75011227faac60773bb1422d365ecc7fed95484a" +source = "git+https://github.com/Schmiddiii/presage?rev=03ffb349a125518fdc6087ebfb6a5d82a1600da6#03ffb349a125518fdc6087ebfb6a5d82a1600da6" dependencies = [ "base64", "futures", @@ -2900,7 +2900,7 @@ dependencies = [ [[package]] name = "presage-store-cipher" version = "0.1.0" -source = "git+https://github.com/Schmiddiii/presage?rev=75011227faac60773bb1422d365ecc7fed95484a#75011227faac60773bb1422d365ecc7fed95484a" +source = "git+https://github.com/Schmiddiii/presage?rev=03ffb349a125518fdc6087ebfb6a5d82a1600da6#03ffb349a125518fdc6087ebfb6a5d82a1600da6" dependencies = [ "blake3", "chacha20poly1305", @@ -2917,7 +2917,7 @@ dependencies = [ [[package]] name = "presage-store-sled" version = "0.6.0-dev" -source = "git+https://github.com/Schmiddiii/presage?rev=75011227faac60773bb1422d365ecc7fed95484a#75011227faac60773bb1422d365ecc7fed95484a" +source = "git+https://github.com/Schmiddiii/presage?rev=03ffb349a125518fdc6087ebfb6a5d82a1600da6#03ffb349a125518fdc6087ebfb6a5d82a1600da6" dependencies = [ "async-trait", "base64", diff --git a/Cargo.toml b/Cargo.toml index 1b2afa4..2b41c4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,9 +30,9 @@ tokio = { version = "1.35.1", features = ["rt", "macros", "rt-multi-thread", "ti regex = "1.10.3" lazy_static = "1.4.0" -presage = { git = "https://github.com/Schmiddiii/presage", rev = "75011227faac60773bb1422d365ecc7fed95484a" } -presage-store-sled = { git = "https://github.com/Schmiddiii/presage", rev = "75011227faac60773bb1422d365ecc7fed95484a" } -libsignal-service = { git = "https://github.com/whisperfish/libsignal-service-rs", rev = "a2e7540a71866a62028ad0205574a5feb0e717ec" } +presage = { git = "https://github.com/Schmiddiii/presage", rev = "03ffb349a125518fdc6087ebfb6a5d82a1600da6" } +presage-store-sled = { git = "https://github.com/Schmiddiii/presage", rev = "03ffb349a125518fdc6087ebfb6a5d82a1600da6" } +libsignal-service = { git = "https://github.com/whisperfish/libsignal-service-rs", rev = "1a8cdf81" } async-trait = "0.1.77" sled = "0.34.7" diff --git a/build-aux/de.schmidhuberj.Flare.Devel.json b/build-aux/de.schmidhuberj.Flare.Devel.json index 5eb1a34..0303720 100644 --- a/build-aux/de.schmidhuberj.Flare.Devel.json +++ b/build-aux/de.schmidhuberj.Flare.Devel.json @@ -56,6 +56,25 @@ } ] }, + { + "name": "protobuf", + "buildsystem": "autotools", + "config-opts": [ + "DIST_LANG=cpp" + ], + "cleanup": [ + "/bin/protoc*", + "/lib/libprotoc*", + "/lib/libprotobuf-lite*" + ], + "sources": [ + { + "type": "archive", + "url": "https://github.com/protocolbuffers/protobuf/releases/download/v3.17.3/protobuf-all-3.17.3.tar.gz", + "sha256": "77ad26d3f65222fd96ccc18b055632b0bfedf295cb748b712a98ba1ac0b704b2" + } + ] + }, { "name": "flare", "buildsystem": "meson", diff --git a/data/resources/ui/channel_item.blp b/data/resources/ui/channel_item.blp index e90f511..69481db 100644 --- a/data/resources/ui/channel_item.blp +++ b/data/resources/ui/channel_item.blp @@ -13,6 +13,7 @@ template $FlChannelItem: Box { // Avatar Adw.Avatar { text: bind template.channel as <$FlChannel>.title; + custom-image: bind template.channel as <$FlChannel>.avatar; icon-name: "user-bookmarks-symbolic"; show-initials: bind $not(template.channel as <$FlChannel>.is-self) as ; size: 44; diff --git a/data/resources/ui/message_item.blp b/data/resources/ui/message_item.blp index 57a5876..9b39edf 100644 --- a/data/resources/ui/message_item.blp +++ b/data/resources/ui/message_item.blp @@ -68,6 +68,7 @@ template $FlMessageItem: $ContextMenuBin { ] text: bind template.message as <$FlTextMessage>.sender as <$FlContact>.title; + custom-image: bind template.message as <$FlTextMessage>.sender as <$FlContact>.avatar; show-initials: true; valign: start; size: 32; diff --git a/src/backend/channel.rs b/src/backend/channel.rs index 1c02217..fc3f2da 100644 --- a/src/backend/channel.rs +++ b/src/backend/channel.rs @@ -5,8 +5,9 @@ use std::{ }; use gdk::{ - glib::clone, + glib::{clone, Bytes}, prelude::{ObjectExt, SettingsExt}, + Paintable, Texture, }; use gio::subclass::prelude::ObjectSubclassIsExt; use glib::{Cast, Object}; @@ -107,6 +108,7 @@ impl Channel { .group_context .swap(&RefCell::new(Some(group_context_v2.clone()))); s.initialize_participants().await; + s.initialize_avatar().await; s } @@ -488,6 +490,33 @@ impl Channel { } } + async fn initialize_avatar(&self) { + // TODO: Do in background. + let Some(context) = self.group_context() else { + return; + }; + + let Some(avatar) = self + .manager() + .retrieve_group_avatar(context) + .await + .ok() + .flatten() + .and_then(|b| Texture::from_bytes(&Bytes::from_owned(b)).ok()) + else { + log::debug!("Failed to fetch group avatar; it may not have a profile picture set",); + return; + }; + + self.imp().group_avatar.replace(Some(avatar.into())); + + self.notify("avatar"); + } + + pub fn avatar(&self) -> Option { + self.property("avatar") + } + pub fn phone_number(&self) -> Option { self.property("phone-number") } @@ -530,6 +559,7 @@ mod imp { glib::{ParamSpecBoolean, ParamSpecUInt}, prelude::*, subclass::prelude::*, + Paintable, }; use glib::{ once_cell::sync::Lazy, subclass::Signal, ParamSpec, ParamSpecObject, ParamSpecString, Value, @@ -551,6 +581,8 @@ mod imp { pub(super) group: RefCell>, pub(super) group_context: RefCell>, + pub(super) group_avatar: RefCell>, + pub(super) participants: RefCell>, pub(super) manager: RefCell>, @@ -606,6 +638,9 @@ mod imp { ParamSpecObject::builder::("last-message") .read_only() .build(), + ParamSpecObject::builder::("avatar") + .read_only() + .build(), ParamSpecString::builder("title").read_only().build(), ParamSpecBoolean::builder("is-contact").read_only().build(), ParamSpecBoolean::builder("is-self").read_only().build(), @@ -639,6 +674,13 @@ mod imp { .unwrap_or(false) }) .to_value(), + "avatar" => { + if let Some(contact) = self.contact.borrow().as_ref() { + contact.property("avatar") + } else { + self.group_avatar.borrow().as_ref().to_value() + } + } "title" => { let title = if let Some(group) = self.group.borrow().as_ref() { group.title.clone() diff --git a/src/backend/contact.rs b/src/backend/contact.rs index 37c4257..5f33b6a 100644 --- a/src/backend/contact.rs +++ b/src/backend/contact.rs @@ -1,6 +1,9 @@ use std::cell::RefCell; -use gdk::glib::clone; +use gdk::{ + glib::{clone, Bytes}, + Paintable, Texture, +}; use gio::subclass::prelude::ObjectSubclassIsExt; use glib::{Object, ObjectExt}; use gtk::{gio, glib}; @@ -102,9 +105,32 @@ impl Contact { let profile = manager.retrieve_profile_by_uuid(uuid, key).await.ok(); obj.profile.replace(profile); self.notify("title"); + let Some(avatar) = manager + .retrieve_profile_avatar_by_uuid(uuid, key) + .await + .ok() + .flatten() + .and_then(|b| Texture::from_bytes(&Bytes::from_owned(b)).ok()) + else { + log::debug!( + "Failed to fetch avatar for {}; they may not have a profile picture set", + self.title() + ); + return; + }; + + self.set_avatar(avatar); } } + pub fn avatar(&self) -> Option { + self.property("avatar") + } + + fn set_avatar(&self, image: Texture) { + self.set_property("avatar", image); + } + pub fn uuid(&self) -> Uuid { if let Some(a) = self.address() { a.uuid @@ -157,6 +183,7 @@ impl Contact { mod imp { use std::cell::RefCell; + use gdk::Paintable; use gdk::{prelude::*, subclass::prelude::*}; use glib::{ once_cell::sync::Lazy, ParamSpec, ParamSpecBoolean, ParamSpecObject, ParamSpecString, Value, @@ -175,6 +202,8 @@ mod imp { pub(super) uuid: RefCell>, pub(super) profile: RefCell>, + pub(crate) avatar: RefCell>, + manager: RefCell>, channel: RefCell>, } @@ -193,6 +222,7 @@ mod imp { .construct_only() .build(), ParamSpecObject::builder::("channel").build(), + ParamSpecObject::builder::("avatar").build(), ParamSpecBoolean::builder("is-self").read_only().build(), ParamSpecString::builder("title").read_only().build(), ] @@ -204,6 +234,7 @@ mod imp { match pspec.name() { "manager" => self.manager.borrow().as_ref().to_value(), "channel" => self.channel.borrow().as_ref().to_value(), + "avatar" => self.avatar.borrow().as_ref().to_value(), "is-self" => { if let Some(contact) = self.contact.borrow().as_ref() { (contact.uuid == self.manager.borrow().as_ref().unwrap().uuid()).to_value() @@ -275,6 +306,13 @@ mod imp { self.channel.replace(obj); } + "avatar" => { + let obj = value + .get::>() + .expect("Property `avatar` of `Contact` has to be of type `Paintable`"); + + self.avatar.replace(obj); + } _ => unimplemented!(), } } diff --git a/src/backend/manager.rs b/src/backend/manager.rs index f198cc5..24dec72 100644 --- a/src/backend/manager.rs +++ b/src/backend/manager.rs @@ -551,6 +551,29 @@ impl Manager { log::trace!("`Manager::retrieve_profile_by_uuid` finished"); r } + pub(super) async fn retrieve_profile_avatar_by_uuid( + &self, + uuid: Uuid, + profile_key: ProfileKey, + ) -> Result>, PresageError> { + log::trace!("`Manager::retrieve_profile_avatar_by_uuid` start"); + let r = self + .internal() + .retrieve_profile_avatar_by_uuid(uuid, profile_key) + .await; + log::trace!("`Manager::retrieve_profile_avatar_by_uuid` finished"); + r + } + + pub(super) async fn retrieve_group_avatar( + &self, + context: GroupContextV2, + ) -> Result>, PresageError> { + log::trace!("`Manager::retrieve_group_avatar` start"); + let r = self.internal().retrieve_group_avatar(context).await; + log::trace!("`Manager::retrieve_group_avatar` finished"); + r + } pub(super) async fn send_message( &self, diff --git a/src/backend/manager_thread.rs b/src/backend/manager_thread.rs index aca4771..65d5192 100644 --- a/src/backend/manager_thread.rs +++ b/src/backend/manager_thread.rs @@ -6,7 +6,7 @@ use libsignal_service::{ content::ContentBody, groups_v2::Group, prelude::{phonenumber, Content, ProfileKey, Uuid}, - proto::{AttachmentPointer, DataMessage}, + proto::{AttachmentPointer, DataMessage, GroupContextV2}, push_service::DeviceInfo, sender::{AttachmentSpec, AttachmentUploadError}, Profile, ServiceAddress, @@ -70,6 +70,15 @@ enum Command { UnlinkSecondary(i64, oneshot::Sender>), LinkedDevices(oneshot::Sender, Error>>), RequestContacts(oneshot::Sender>), + RetrieveProfileAvatarByUuid( + Uuid, + ProfileKey, + oneshot::Sender>, Error>>, + ), + RetrieveGroupAvatar( + GroupContextV2, + oneshot::Sender>, Error>>, + ), } #[derive(Debug)] @@ -390,6 +399,35 @@ impl ManagerThread { .expect("Command sending failed"); receiver.await.expect("Callback receiving failed") } + + pub async fn retrieve_profile_avatar_by_uuid( + &self, + uuid: Uuid, + profile_key: ProfileKey, + ) -> Result>, Error> { + let (sender, receiver) = oneshot::channel(); + self.command_sender + .send(Command::RetrieveProfileAvatarByUuid( + uuid, + profile_key, + sender, + )) + .await + .expect("Command sending failed"); + receiver.await.expect("Callback receiving failed") + } + + pub async fn retrieve_group_avatar( + &self, + context: GroupContextV2, + ) -> Result>, Error> { + let (sender, receiver) = oneshot::channel(); + self.command_sender + .send(Command::RetrieveGroupAvatar(context, sender)) + .await + .expect("Command sending failed"); + receiver.await.expect("Callback receiving failed") + } } async fn setup_manager( @@ -601,5 +639,15 @@ async fn handle_command(manager: &mut Manager, command: Comma Command::RequestContacts(callback) => callback .send(manager.request_contacts().await) .expect("Callback sending failed"), + Command::RetrieveProfileAvatarByUuid(uuid, profile_key, callback) => callback + .send( + manager + .retrieve_profile_avatar_by_uuid(uuid, profile_key) + .await, + ) + .expect("Callback sending failed"), + Command::RetrieveGroupAvatar(context, callback) => callback + .send(manager.retrieve_group_avatar(context).await) + .expect("Callback sending failed"), } } diff --git a/src/gui/channel_info_dialog.rs b/src/gui/channel_info_dialog.rs index 2e4e439..98a5b31 100644 --- a/src/gui/channel_info_dialog.rs +++ b/src/gui/channel_info_dialog.rs @@ -72,6 +72,7 @@ pub mod imp { let channel = binding.as_ref().expect("channel to be set at setup"); self.avatar.set_text(Some(&channel.title())); self.avatar.set_show_initials(!channel.is_self()); + self.avatar.set_custom_image(channel.avatar().as_ref()); let phone = channel.phone_number(); self.row_phone.set_visible(phone.is_some());