diff --git a/data/resources/style.css b/data/resources/style.css index 9400328f7..373ebe018 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -349,6 +349,10 @@ messagereply label.message { color: @window_fg_color; } +messagebubble.outgoing messagereply label.message { + color: currentColor; +} + messagesticker { border-spacing: 6px; } diff --git a/src/model/chat.rs b/src/model/chat.rs index 9ce5e749a..160291470 100644 --- a/src/model/chat.rs +++ b/src/model/chat.rs @@ -89,6 +89,8 @@ mod imp { #[property(get)] pub(super) title: RefCell, #[property(get)] + pub(super) theme_name: RefCell, + #[property(get)] pub(super) avatar: RefCell>, #[property(get)] pub(super) last_read_outbox_message_id: Cell, @@ -173,6 +175,7 @@ impl Chat { imp.block_list .replace(td_chat.block_list.map(model::BoxedBlockList)); imp.title.replace(td_chat.title); + imp.theme_name.replace(td_chat.theme_name); imp.avatar.replace(td_chat.photo.map(model::Avatar::from)); imp.last_read_outbox_message_id .set(td_chat.last_read_outbox_message_id); @@ -232,6 +235,7 @@ impl Chat { self.set_last_read_outbox_message_id(update.last_read_outbox_message_id); } ChatTitle(update) => self.set_title(update.title), + ChatTheme(update) => self.set_theme_name(update.theme_name), ChatUnreadMentionCount(update) => { self.set_unread_mention_count(update.unread_mention_count) } @@ -322,6 +326,19 @@ impl Chat { self.notify_title(); } + fn set_theme_name(&self, theme_name: String) { + if self.theme_name() == theme_name { + return; + } + self.imp().theme_name.replace(theme_name); + self.notify_theme_name(); + } + + pub(crate) fn chat_theme(&self) -> Option { + self.session_() + .find_chat_theme(&self.imp().theme_name.borrow()) + } + fn set_avatar(&self, avatar: Option) { if self.avatar() == avatar { return; diff --git a/src/model/client_state_session.rs b/src/model/client_state_session.rs index 6da0ca517..2664b766d 100644 --- a/src/model/client_state_session.rs +++ b/src/model/client_state_session.rs @@ -8,6 +8,8 @@ use glib::prelude::*; use glib::subclass::prelude::*; use glib::Properties; use gtk::glib; +use gtk::glib::subclass::Signal; +use once_cell::sync::Lazy; use crate::model; use crate::utils; @@ -20,6 +22,7 @@ mod imp { pub(crate) struct ClientStateSession { pub(super) filter_chat_lists: RefCell>, pub(super) chats: RefCell>, + pub(super) chat_themes: RefCell>, pub(super) users: RefCell>, pub(super) basic_groups: RefCell>, pub(super) supergroups: RefCell>, @@ -52,6 +55,12 @@ mod imp { } impl ObjectImpl for ClientStateSession { + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = + Lazy::new(|| vec![Signal::builder("update-chat-themes").build()]); + SIGNALS.as_ref() + } + fn properties() -> &'static [glib::ParamSpec] { Self::derived_properties() } @@ -388,6 +397,25 @@ impl ClientStateSession { } } + pub(crate) fn find_chat_theme(&self, name: &str) -> Option { + self.imp() + .chat_themes + .borrow() + .iter() + .find(|theme| theme.name == name) + .cloned() + } + + pub(crate) fn connect_update_chat_themes(&self, callback: F) -> glib::SignalHandlerId + where + F: Fn() + 'static, + { + self.connect_local("update-chat-themes", true, move |_| { + callback(); + None + }) + } + pub(crate) fn handle_update(&self, update: tdlib::enums::Update) { use tdlib::enums::Update::*; @@ -400,6 +428,7 @@ impl ClientStateSession { } ChatTitle(ref data) => self.chat(data.chat_id).handle_update(update), ChatPhoto(ref data) => self.chat(data.chat_id).handle_update(update), + ChatTheme(ref data) => self.chat(data.chat_id).handle_update(update), ChatPermissions(ref data) => self.chat(data.chat_id).handle_update(update), ChatLastMessage(ref data) => { let chat = self.chat(data.chat_id); @@ -502,6 +531,10 @@ impl ClientStateSession { UserStatus(data) => { self.user(data.user_id).update_status(data.status); } + ChatThemes(chat_themes) => { + self.imp().chat_themes.replace(chat_themes.chat_themes); + self.emit_by_name::<()>("update-chat-themes", &[]); + } _ => {} } } diff --git a/src/session/mod.rs b/src/session/mod.rs new file mode 100644 index 000000000..45fdf7567 --- /dev/null +++ b/src/session/mod.rs @@ -0,0 +1,680 @@ +mod contacts_window; +mod content; +mod preferences_window; +mod sidebar; + +use std::cell::Cell; +use std::cell::RefCell; +use std::collections::hash_map::Entry; +use std::collections::hash_map::HashMap; + +use adw::subclass::prelude::BinImpl; +use glib::clone; +use glib::subclass::Signal; +use glib::Sender; +use gtk::glib; +use gtk::glib::WeakRef; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; +use once_cell::sync::Lazy; +use once_cell::sync::OnceCell; +use tdlib::enums; +use tdlib::enums::ChatList as TdChatList; +use tdlib::enums::NotificationSettingsScope; +use tdlib::enums::Update; +use tdlib::functions; +use tdlib::types::ChatPosition as TdChatPosition; +use tdlib::types::Error as TdError; +use tdlib::types::File; + +use self::contacts_window::ContactsWindow; +use self::content::Content; +use self::preferences_window::PreferencesWindow; +use self::sidebar::Sidebar; +use crate::session_manager::DatabaseInfo; +use crate::tdlib::BasicGroup; +use crate::tdlib::BoxedScopeNotificationSettings; +use crate::tdlib::Chat; +use crate::tdlib::ChatList; +use crate::tdlib::SecretChat; +use crate::tdlib::Supergroup; +use crate::tdlib::User; +use crate::utils::log_out; +use crate::utils::spawn; + +#[derive(Clone, Debug, glib::Boxed)] +#[boxed_type(name = "BoxedDatabaseInfo")] +pub(crate) struct BoxedDatabaseInfo(pub(crate) DatabaseInfo); + +mod imp { + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/app/drey/paper-plane/ui/session.ui")] + pub(crate) struct Session { + pub(super) client_id: Cell, + pub(super) database_info: OnceCell, + pub(super) me: WeakRef, + pub(super) main_chat_list: OnceCell, + pub(super) archive_chat_list: OnceCell, + pub(super) folder_chat_lists: RefCell>, + pub(super) chats: RefCell>, + pub(super) chat_themes: RefCell>, + pub(super) users: RefCell>, + pub(super) basic_groups: RefCell>, + pub(super) supergroups: RefCell>, + pub(super) secret_chats: RefCell>, + pub(super) private_chats_notification_settings: + RefCell>, + pub(super) group_chats_notification_settings: + RefCell>, + pub(super) channel_chats_notification_settings: + RefCell>, + pub(super) downloading_files: RefCell>>>, + #[template_child] + pub(super) leaflet: TemplateChild, + #[template_child] + pub(super) sidebar: TemplateChild, + #[template_child] + pub(super) content: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for Session { + const NAME: &'static str = "Session"; + type Type = super::Session; + type ParentType = adw::Bin; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + + klass.install_action("content.go-back", None, move |widget, _, _| { + widget + .imp() + .leaflet + .navigate(adw::NavigationDirection::Back); + }); + klass.install_action_async("session.log-out", None, |widget, _, _| async move { + log_out(widget.client_id()).await; + }); + klass.install_action("session.show-preferences", None, move |widget, _, _| { + let parent_window = widget.root().and_then(|r| r.downcast().ok()); + let preferences = PreferencesWindow::new(parent_window.as_ref(), widget); + preferences.present(); + }); + klass.install_action("session.show-contacts", None, move |widget, _, _| { + let parent = widget.root().and_then(|r| r.downcast().ok()); + let contacts = ContactsWindow::new(parent.as_ref(), widget.clone()); + + contacts.connect_contact_activated(clone!(@weak widget => move |_, user_id| { + widget.select_chat(user_id); + })); + + contacts.present(); + }); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for Session { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![ + glib::ParamSpecInt::builder("client-id") + .construct_only() + .build(), + glib::ParamSpecBoxed::builder::("database-info") + .construct_only() + .build(), + glib::ParamSpecObject::builder::("me") + .read_only() + .build(), + glib::ParamSpecObject::builder::("main-chat-list") + .read_only() + .build(), + glib::ParamSpecBoxed::builder::( + "private-chats-notification-settings", + ) + .read_only() + .build(), + glib::ParamSpecBoxed::builder::( + "group-chats-notification-settings", + ) + .read_only() + .build(), + glib::ParamSpecBoxed::builder::( + "channel-chats-notification-settings", + ) + .read_only() + .build(), + ] + }); + PROPERTIES.as_ref() + } + + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = + Lazy::new(|| vec![Signal::builder("update-chat-themes").build()]); + SIGNALS.as_ref() + } + + fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + match pspec.name() { + "client-id" => { + let client_id = value.get().unwrap(); + self.client_id.set(client_id); + } + "database-info" => { + let database_info = value.get().unwrap(); + self.database_info.set(database_info).unwrap(); + } + _ => unimplemented!(), + } + } + + fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + let obj = self.obj(); + + match pspec.name() { + "client-id" => obj.client_id().to_value(), + "database-info" => obj.database_info().to_value(), + "me" => self.me.upgrade().to_value(), + "main-chat-list" => obj.main_chat_list().to_value(), + "private-chats-notification-settings" => { + obj.private_chats_notification_settings().to_value() + } + "group-chats-notification-settings" => { + obj.group_chats_notification_settings().to_value() + } + "channel-chats-notification-settings" => { + obj.channel_chats_notification_settings().to_value() + } + _ => unimplemented!(), + } + } + + fn constructed(&self) { + self.parent_constructed(); + + let obj = self.obj(); + self.sidebar + .connect_chat_selected(clone!(@weak obj => move |_| { + obj.imp().leaflet.navigate(adw::NavigationDirection::Forward); + })); + } + } + + impl WidgetImpl for Session {} + impl BinImpl for Session {} +} + +glib::wrapper! { + pub(crate) struct Session(ObjectSubclass) + @extends gtk::Widget, adw::Bin; +} + +impl Session { + pub(crate) fn new(client_id: i32, database_info: DatabaseInfo) -> Self { + glib::Object::builder() + .property("client-id", client_id) + .property("database-info", BoxedDatabaseInfo(database_info)) + .build() + } + + pub(crate) fn handle_update(&self, update: Update) { + match update { + Update::NewChat(data) => { + // No need to update the chat positions here, tdlib sends + // the correct chat positions in other updates later + let chat = Chat::new(data.chat, self); + self.imp().chats.borrow_mut().insert(chat.id(), chat); + } + Update::ChatTitle(ref data) => self.chat(data.chat_id).handle_update(update), + Update::ChatPhoto(ref data) => self.chat(data.chat_id).handle_update(update), + Update::ChatTheme(ref data) => self.chat(data.chat_id).handle_update(update), + Update::ChatPermissions(ref data) => self.chat(data.chat_id).handle_update(update), + Update::ChatLastMessage(ref data) => { + let chat = self.chat(data.chat_id); + for position in &data.positions { + self.handle_chat_position_update(&chat, position); + } + chat.handle_update(update); + } + Update::ChatPosition(ref data) => { + self.handle_chat_position_update(&self.chat(data.chat_id), &data.position) + } + Update::ChatReadInbox(ref data) => self.chat(data.chat_id).handle_update(update), + Update::ChatReadOutbox(ref data) => self.chat(data.chat_id).handle_update(update), + Update::ChatDraftMessage(ref data) => { + let chat = self.chat(data.chat_id); + for position in &data.positions { + self.handle_chat_position_update(&chat, position); + } + chat.handle_update(update); + } + Update::ChatNotificationSettings(ref data) => { + self.chat(data.chat_id).handle_update(update) + } + Update::ChatUnreadMentionCount(ref data) => { + self.chat(data.chat_id).handle_update(update) + } + Update::ChatIsBlocked(ref data) => self.chat(data.chat_id).handle_update(update), + Update::ChatIsMarkedAsUnread(ref data) => self.chat(data.chat_id).handle_update(update), + Update::DeleteMessages(ref data) => self.chat(data.chat_id).handle_update(update), + Update::ChatAction(ref data) => self.chat(data.chat_id).handle_update(update), + Update::MessageContent(ref data) => self.chat(data.chat_id).handle_update(update), + Update::MessageEdited(ref data) => self.chat(data.chat_id).handle_update(update), + Update::MessageInteractionInfo(ref data) => { + self.chat(data.chat_id).handle_update(update) + } + Update::MessageMentionRead(ref data) => self.chat(data.chat_id).handle_update(update), + Update::MessageSendSucceeded(ref data) => { + self.chat(data.message.chat_id).handle_update(update) + } + Update::NewMessage(ref data) => self.chat(data.message.chat_id).handle_update(update), + Update::BasicGroup(data) => { + let mut basic_groups = self.imp().basic_groups.borrow_mut(); + match basic_groups.entry(data.basic_group.id) { + Entry::Occupied(entry) => entry.get().update(data.basic_group), + Entry::Vacant(entry) => { + entry.insert(BasicGroup::from_td_object(data.basic_group)); + } + } + } + Update::File(update) => { + self.handle_file_update(update.file); + } + Update::ScopeNotificationSettings(update) => { + let settings = Some(BoxedScopeNotificationSettings(update.notification_settings)); + match update.scope { + NotificationSettingsScope::PrivateChats => { + self.set_private_chats_notification_settings(settings); + } + NotificationSettingsScope::GroupChats => { + self.set_group_chats_notification_settings(settings); + } + NotificationSettingsScope::ChannelChats => { + self.set_channel_chats_notification_settings(settings); + } + } + } + Update::SecretChat(data) => { + let mut secret_chats = self.imp().secret_chats.borrow_mut(); + match secret_chats.entry(data.secret_chat.id) { + Entry::Occupied(entry) => entry.get().update(data.secret_chat), + Entry::Vacant(entry) => { + let user = self.user(data.secret_chat.user_id); + entry.insert(SecretChat::from_td_object(data.secret_chat, user)); + } + } + } + Update::Supergroup(data) => { + let mut supergroups = self.imp().supergroups.borrow_mut(); + match supergroups.entry(data.supergroup.id) { + Entry::Occupied(entry) => entry.get().update(data.supergroup), + Entry::Vacant(entry) => { + entry.insert(Supergroup::from_td_object(data.supergroup)); + } + } + } + Update::UnreadMessageCount(data) => match data.chat_list { + TdChatList::Main => { + self.main_chat_list() + .update_unread_message_count(data.unread_count); + } + TdChatList::Archive => { + self.archive_chat_list() + .update_unread_message_count(data.unread_count); + } + TdChatList::Folder(data_) => { + self.folder_chat_list(data_.chat_folder_id) + .update_unread_message_count(data.unread_count); + } + }, + Update::User(data) => { + let mut users = self.imp().users.borrow_mut(); + match users.entry(data.user.id) { + Entry::Occupied(entry) => entry.get().update(data.user), + Entry::Vacant(entry) => { + entry.insert(User::from_td_object(data.user, self)); + } + } + } + Update::UserStatus(data) => { + self.user(data.user_id).update_status(data.status); + } + Update::ChatThemes(chat_themes) => { + self.imp().chat_themes.replace(chat_themes.chat_themes); + self.emit_by_name::<()>("update-chat-themes", &[]); + } + _ => {} + } + } + + /// Returns the `Chat` of the specified id, if present. + pub(crate) fn try_chat(&self, chat_id: i64) -> Option { + self.imp().chats.borrow().get(&chat_id).cloned() + } + + /// Returns the `Chat` of the specified id. Panics if the chat is not present. + /// + /// Note that TDLib guarantees that types are always returned before their ids, + /// so if you use an id returned by TDLib, it should be expected that the + /// relative `Chat` exists in the list. + pub(crate) fn chat(&self, chat_id: i64) -> Chat { + self.try_chat(chat_id).expect("Failed to get expected Chat") + } + + /// Returns the `User` of the specified id. Panics if the user is not present. + /// + /// Note that TDLib guarantees that types are always returned before their ids, + /// so if you use an id returned by TDLib, it should be expected that the + /// relative `User` exists in the list. + pub(crate) fn user(&self, user_id: i64) -> User { + self.imp() + .users + .borrow() + .get(&user_id) + .expect("Failed to get expected User") + .clone() + } + + /// Returns the `BasicGroup` of the specified id. Panics if the basic group is not present. + /// + /// Note that TDLib guarantees that types are always returned before their ids, + /// so if you use an id returned by TDLib, it should be expected that the + /// relative `BasicGroup` exists in the list. + pub(crate) fn basic_group(&self, basic_group_id: i64) -> BasicGroup { + self.imp() + .basic_groups + .borrow() + .get(&basic_group_id) + .expect("Failed to get expected BasicGroup") + .clone() + } + + /// Returns the `Supergroup` of the specified id. Panics if the supergroup is not present. + /// + /// Note that TDLib guarantees that types are always returned before their ids, + /// so if you use an id returned by TDLib, it should be expected that the + /// relative `Supergroup` exists in the list. + pub(crate) fn supergroup(&self, supergroup_id: i64) -> Supergroup { + self.imp() + .supergroups + .borrow() + .get(&supergroup_id) + .expect("Failed to get expected Supergroup") + .clone() + } + + /// Returns the `SecretChat` of the specified id. Panics if the secret chat is not present. + /// + /// Note that TDLib guarantees that types are always returned before their ids, + /// so if you use an id returned by TDLib, it should be expected that the + /// relative `SecretChat` exists in the list. + pub(crate) fn secret_chat(&self, secret_chat_id: i32) -> SecretChat { + self.imp() + .secret_chats + .borrow() + .get(&secret_chat_id) + .expect("Failed to get expected SecretChat") + .clone() + } + + /// Returns the main chat list. + pub(crate) fn main_chat_list(&self) -> &ChatList { + self.imp().main_chat_list.get_or_init(ChatList::new) + } + + /// Returns the list of archived chats. + pub(crate) fn archive_chat_list(&self) -> &ChatList { + self.imp().archive_chat_list.get_or_init(ChatList::new) + } + + /// Returns the folder chat list of the specified id. + pub(crate) fn folder_chat_list(&self, chat_folder_id: i32) -> ChatList { + self.imp() + .folder_chat_lists + .borrow_mut() + .entry(chat_folder_id) + .or_insert_with(ChatList::new) + .clone() + } + + /// Fetches the contacts of the user. + pub(crate) async fn fetch_contacts(&self) -> Result, TdError> { + let client_id = self.imp().client_id.get(); + let result = functions::get_contacts(client_id).await; + + result.map(|data| { + let tdlib::enums::Users::Users(users) = data; + users.user_ids.into_iter().map(|id| self.user(id)).collect() + }) + } + + /// Downloads a file of the specified id. This will only return when the file + /// downloading has completed or has failed. + pub(crate) async fn download_file(&self, file_id: i32) -> Result { + let client_id = self.client_id(); + let result = functions::download_file(file_id, 5, 0, 0, true, client_id).await; + + result.map(|data| { + let tdlib::enums::File::File(file) = data; + file + }) + } + + /// Downloads a file of the specified id and calls a closure every time there's an update + /// about the progress or when the download has completed. + pub(crate) fn download_file_with_updates(&self, file_id: i32, f: F) { + let (sender, receiver) = glib::MainContext::channel::(glib::PRIORITY_DEFAULT); + receiver.attach(None, move |file| { + let is_downloading_active = file.local.is_downloading_active; + f(file); + glib::Continue(is_downloading_active) + }); + + let mut downloading_files = self.imp().downloading_files.borrow_mut(); + match downloading_files.entry(file_id) { + Entry::Occupied(mut entry) => { + entry.get_mut().push(sender); + } + Entry::Vacant(entry) => { + entry.insert(vec![sender]); + + let client_id = self.client_id(); + spawn(clone!(@weak self as obj => async move { + let result = functions::download_file(file_id, 5, 0, 0, false, client_id).await; + match result { + Ok(enums::File::File(file)) => { + obj.handle_file_update(file); + } + Err(e) => { + log::warn!("Error downloading a file: {:?}", e); + } + } + })); + } + } + } + + pub(crate) fn cancel_download_file(&self, file_id: i32) { + let client_id = self.client_id(); + spawn(async move { + if let Err(e) = functions::cancel_download_file(file_id, false, client_id).await { + log::warn!("Error canceling a file: {:?}", e); + } + }); + } + + pub(crate) fn select_chat(&self, chat_id: i64) { + match self.try_chat(chat_id) { + Some(chat) => self.imp().sidebar.select_chat(chat), + None => spawn(clone!(@weak self as obj => async move { + match functions::create_private_chat(chat_id, true, obj.client_id()).await { + Ok(enums::Chat::Chat(data)) => obj.imp().sidebar.select_chat(obj.chat(data.id)), + Err(e) => log::warn!("Failed to create private chat: {:?}", e), + } + })), + } + } + + pub(crate) fn find_chat_theme(&self, name: &str) -> Option { + self.imp() + .chat_themes + .borrow() + .iter() + .find(|theme| theme.name == name) + .cloned() + } + + pub(crate) fn connect_update_chat_themes(&self, callback: F) -> glib::SignalHandlerId + where + F: Fn() + 'static, + { + self.connect_local("update-chat-themes", true, move |_| { + callback(); + None + }) + } + + pub(crate) fn handle_paste_action(&self) { + self.imp().content.handle_paste_action(); + } + + pub(crate) fn begin_chats_search(&self) { + let imp = self.imp(); + imp.leaflet.navigate(adw::NavigationDirection::Back); + imp.sidebar.begin_chats_search(); + } + + fn handle_chat_position_update(&self, chat: &Chat, position: &TdChatPosition) { + match &position.list { + TdChatList::Main => { + self.main_chat_list().update_chat_position(chat, position); + } + TdChatList::Archive => { + self.archive_chat_list() + .update_chat_position(chat, position); + } + TdChatList::Folder(data) => { + self.folder_chat_list(data.chat_folder_id) + .update_chat_position(chat, position); + } + } + } + + fn handle_file_update(&self, file: File) { + let mut downloading_files = self.imp().downloading_files.borrow_mut(); + if let Entry::Occupied(mut entry) = downloading_files.entry(file.id) { + // Keep only the senders with which it was possible to send successfully. + // It is indeed possible that the object that created the sender and receiver and + // attached it to the default main context has been disposed in the meantime. + // This is problematic if it is now tried to upgrade a weak reference of this object in + // the receiver closure. + // It will either panic directly if `@default-panic` is used or the sender will return + // an error in the `SyncSender::send()` function if + // `default-return glib::Continue(false)` is used. In the latter case, the Receiver + // will be detached from the main context, which will cause the sending to fail. + entry + .get_mut() + .retain(|sender| sender.send(file.clone()).is_ok()); + + if !file.local.is_downloading_active || entry.get().is_empty() { + entry.remove(); + } + } + } + + pub(crate) fn client_id(&self) -> i32 { + self.imp().client_id.get() + } + + pub(crate) fn database_info(&self) -> &BoxedDatabaseInfo { + self.imp().database_info.get().unwrap() + } + + pub(crate) fn me(&self) -> User { + self.imp().me.upgrade().unwrap() + } + + pub(crate) fn set_me(&self, me: &User) { + let imp = self.imp(); + assert!(imp.me.upgrade().is_none()); + imp.me.set(Some(me)); + self.notify("me"); + } + + fn private_chats_notification_settings(&self) -> Option { + self.imp() + .private_chats_notification_settings + .borrow() + .clone() + } + + fn set_private_chats_notification_settings( + &self, + settings: Option, + ) { + if self.private_chats_notification_settings() == settings { + return; + } + self.imp() + .private_chats_notification_settings + .replace(settings); + self.notify("private-chats-notification-settings") + } + + fn group_chats_notification_settings(&self) -> Option { + self.imp() + .group_chats_notification_settings + .borrow() + .clone() + } + + fn set_group_chats_notification_settings( + &self, + settings: Option, + ) { + if self.group_chats_notification_settings() == settings { + return; + } + self.imp() + .group_chats_notification_settings + .replace(settings); + self.notify("group-chats-notification-settings") + } + + fn channel_chats_notification_settings(&self) -> Option { + self.imp() + .channel_chats_notification_settings + .borrow() + .clone() + } + + fn set_channel_chats_notification_settings( + &self, + settings: Option, + ) { + if self.channel_chats_notification_settings() == settings { + return; + } + self.imp() + .channel_chats_notification_settings + .replace(settings); + self.notify("channel-chats-notification-settings") + } + + pub(crate) fn fetch_chats(&self) { + let client_id = self.imp().client_id.get(); + self.main_chat_list().fetch(client_id); + } + + pub(crate) fn set_sessions(&self, sessions: gtk::SelectionModel) { + self.imp().sidebar.set_sessions(sessions, self); + } +} diff --git a/src/tdlib/chat.rs b/src/tdlib/chat.rs new file mode 100644 index 000000000..db4c62594 --- /dev/null +++ b/src/tdlib/chat.rs @@ -0,0 +1,621 @@ +use std::cell::Cell; +use std::cell::RefCell; +use std::collections::HashMap; + +use glib::subclass::Signal; +use glib::WeakRef; +use gtk::glib; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use once_cell::sync::Lazy; +use once_cell::unsync::OnceCell; +use tdlib::enums::ChatType as TdChatType; +use tdlib::enums::Update; +use tdlib::functions; +use tdlib::types; +use tdlib::types::Chat as TelegramChat; + +use crate::tdlib::Avatar; +use crate::tdlib::BasicGroup; +use crate::tdlib::BoxedChatNotificationSettings; +use crate::tdlib::BoxedChatPermissions; +use crate::tdlib::BoxedDraftMessage; +use crate::tdlib::ChatActionList; +use crate::tdlib::Message; +use crate::tdlib::SecretChat; +use crate::tdlib::Supergroup; +use crate::tdlib::User; +use crate::Session; + +#[derive(Clone, Debug, glib::Boxed)] +#[boxed_type(name = "ChatType")] +pub(crate) enum ChatType { + Private(User), + BasicGroup(BasicGroup), + Supergroup(Supergroup), + Secret(SecretChat), +} + +impl ChatType { + pub(crate) fn from_td_object(_type: &TdChatType, session: &Session) -> Self { + match _type { + TdChatType::Private(data) => { + let user = session.user(data.user_id); + Self::Private(user) + } + TdChatType::BasicGroup(data) => { + let basic_group = session.basic_group(data.basic_group_id); + Self::BasicGroup(basic_group) + } + TdChatType::Supergroup(data) => { + let supergroup = session.supergroup(data.supergroup_id); + Self::Supergroup(supergroup) + } + TdChatType::Secret(data) => { + let secret_chat = session.secret_chat(data.secret_chat_id); + Self::Secret(secret_chat) + } + } + } + + pub(crate) fn user(&self) -> Option<&User> { + Some(match self { + ChatType::Private(user) => user, + ChatType::Secret(secret_chat) => secret_chat.user(), + _ => return None, + }) + } + + pub(crate) fn basic_group(&self) -> Option<&BasicGroup> { + Some(match self { + ChatType::BasicGroup(basic_group) => basic_group, + _ => return None, + }) + } + + pub(crate) fn supergroup(&self) -> Option<&Supergroup> { + Some(match self { + ChatType::Supergroup(supergroup) => supergroup, + _ => return None, + }) + } +} + +mod imp { + use super::*; + + #[derive(Debug, Default)] + pub(crate) struct Chat { + pub(super) id: Cell, + pub(super) type_: OnceCell, + pub(super) is_blocked: Cell, + pub(super) title: RefCell, + pub(super) theme_name: RefCell, + pub(super) avatar: RefCell>, + pub(super) last_read_outbox_message_id: Cell, + pub(super) is_marked_as_unread: Cell, + pub(super) last_message: RefCell>, + pub(super) unread_mention_count: Cell, + pub(super) unread_count: Cell, + pub(super) draft_message: RefCell>, + pub(super) notification_settings: RefCell>, + pub(super) actions: OnceCell, + pub(super) session: WeakRef, + pub(super) permissions: RefCell>, + pub(super) messages: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for Chat { + const NAME: &'static str = "Chat"; + type Type = super::Chat; + } + + impl ObjectImpl for Chat { + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = Lazy::new(|| { + vec![ + Signal::builder("new-message") + .param_types([Message::static_type()]) + .build(), + Signal::builder("deleted-message") + .param_types([Message::static_type()]) + .build(), + ] + }); + SIGNALS.as_ref() + } + + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![ + glib::ParamSpecInt64::builder("id").read_only().build(), + glib::ParamSpecBoxed::builder::("type") + .read_only() + .build(), + glib::ParamSpecBoolean::builder("is-blocked") + .read_only() + .build(), + glib::ParamSpecString::builder("title").read_only().build(), + glib::ParamSpecString::builder("theme-name") + .read_only() + .build(), + glib::ParamSpecBoxed::builder::("avatar") + .read_only() + .build(), + glib::ParamSpecInt64::builder("last-read-outbox-message-id") + .read_only() + .build(), + glib::ParamSpecBoolean::builder("is-marked-as-unread") + .read_only() + .build(), + glib::ParamSpecObject::builder::("last-message") + .read_only() + .build(), + glib::ParamSpecInt::builder("unread-mention-count") + .read_only() + .build(), + glib::ParamSpecInt::builder("unread-count") + .read_only() + .build(), + glib::ParamSpecBoxed::builder::("draft-message") + .read_only() + .build(), + glib::ParamSpecBoxed::builder::( + "notification-settings", + ) + .read_only() + .build(), + glib::ParamSpecObject::builder::("actions") + .read_only() + .build(), + glib::ParamSpecBoxed::builder::("permissions") + .read_only() + .build(), + glib::ParamSpecObject::builder::("session") + .read_only() + .build(), + ] + }); + PROPERTIES.as_ref() + } + + fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + let obj = self.obj(); + + match pspec.name() { + "id" => obj.id().to_value(), + "type" => obj.type_().to_value(), + "is-blocked" => obj.is_blocked().to_value(), + "title" => obj.title().to_value(), + "theme-name" => obj.theme_name().to_value(), + "avatar" => obj.avatar().to_value(), + "last-read-outbox-message-id" => obj.last_read_outbox_message_id().to_value(), + "is-marked-as-unread" => obj.is_marked_as_unread().to_value(), + "last-message" => obj.last_message().to_value(), + "unread-mention-count" => obj.unread_mention_count().to_value(), + "unread-count" => obj.unread_count().to_value(), + "draft-message" => obj.draft_message().to_value(), + "notification-settings" => obj.notification_settings().to_value(), + "actions" => obj.actions().to_value(), + "permissions" => obj.permissions().to_value(), + "session" => obj.session().to_value(), + _ => unimplemented!(), + } + } + } +} + +glib::wrapper! { + pub(crate) struct Chat(ObjectSubclass); +} + +impl Chat { + pub(crate) fn new(td_chat: TelegramChat, session: &Session) -> Self { + let chat: Chat = glib::Object::new(); + let imp = chat.imp(); + + let type_ = ChatType::from_td_object(&td_chat.r#type, session); + let avatar = td_chat.photo.map(Avatar::from); + let last_message = td_chat.last_message.map(|m| Message::new(m, &chat)); + let draft_message = td_chat.draft_message.map(BoxedDraftMessage); + let notification_settings = BoxedChatNotificationSettings(td_chat.notification_settings); + let permissions = BoxedChatPermissions(td_chat.permissions); + + imp.id.set(td_chat.id); + imp.type_.set(type_).unwrap(); + imp.is_blocked.set(td_chat.is_blocked); + imp.title.replace(td_chat.title); + imp.theme_name.replace(td_chat.theme_name); + imp.avatar.replace(avatar); + imp.last_read_outbox_message_id + .set(td_chat.last_read_outbox_message_id); + imp.is_marked_as_unread.set(td_chat.is_marked_as_unread); + imp.last_message.replace(last_message); + imp.unread_mention_count.set(td_chat.unread_mention_count); + imp.unread_count.set(td_chat.unread_count); + imp.draft_message.replace(draft_message); + imp.notification_settings + .replace(Some(notification_settings)); + imp.session.set(Some(session)); + imp.permissions.replace(Some(permissions)); + + chat + } + + pub(crate) fn handle_update(&self, update: Update) { + use Update::*; + let imp = self.imp(); + + match update { + ChatAction(update) => { + self.actions().handle_update(update); + // TODO: Remove this at some point. Widgets should use the `items-changed` signal + // for updating their state in the future. + self.notify("actions"); + } + ChatDraftMessage(update) => { + self.set_draft_message(update.draft_message.map(BoxedDraftMessage)); + } + ChatIsBlocked(update) => self.set_is_blocked(update.is_blocked), + ChatIsMarkedAsUnread(update) => self.set_marked_as_unread(update.is_marked_as_unread), + ChatLastMessage(update) => { + self.set_last_message(update.last_message.map(|m| Message::new(m, self))); + } + ChatNotificationSettings(update) => { + self.set_notification_settings(BoxedChatNotificationSettings( + update.notification_settings, + )); + } + ChatPermissions(update) => { + self.set_permissions(BoxedChatPermissions(update.permissions)) + } + ChatPhoto(update) => self.set_avatar(update.photo.map(Into::into)), + ChatReadInbox(update) => self.set_unread_count(update.unread_count), + ChatReadOutbox(update) => { + self.set_last_read_outbox_message_id(update.last_read_outbox_message_id); + } + ChatTitle(update) => self.set_title(update.title), + ChatTheme(update) => self.set_theme_name(update.theme_name), + ChatUnreadMentionCount(update) => { + self.set_unread_mention_count(update.unread_mention_count) + } + DeleteMessages(data) => { + // FIXME: This should be removed after we notify opened and closed chats to TDLib + // See discussion here: https://t.me/tdlibchat/65304 + if !data.from_cache { + let mut messages = imp.messages.borrow_mut(); + let deleted_messages: Vec = data + .message_ids + .into_iter() + .filter_map(|id| messages.remove(&id)) + .collect(); + + drop(messages); + for message in deleted_messages { + self.emit_by_name::<()>("deleted-message", &[&message]); + } + } + } + MessageContent(ref data) => { + if let Some(message) = self.message(data.message_id) { + message.handle_update(update); + } + } + MessageEdited(ref data) => { + if let Some(message) = self.message(data.message_id) { + message.handle_update(update); + } + } + MessageInteractionInfo(ref data) => { + if let Some(message) = self.message(data.message_id) { + message.handle_update(update); + } + } + MessageSendSucceeded(data) => { + let mut messages = imp.messages.borrow_mut(); + let old_message = messages.remove(&data.old_message_id); + + let message_id = data.message.id; + let message = Message::new(data.message, self); + messages.insert(message_id, message.clone()); + + drop(messages); + self.emit_by_name::<()>("deleted-message", &[&old_message]); + self.emit_by_name::<()>("new-message", &[&message]); + } + NewMessage(data) => { + let message_id = data.message.id; + let message = Message::new(data.message, self); + imp.messages + .borrow_mut() + .insert(message_id, message.clone()); + + self.emit_by_name::<()>("new-message", &[&message]); + } + MessageMentionRead(update) => { + self.set_unread_mention_count(update.unread_mention_count) + } + _ => {} + } + } + + pub(crate) fn id(&self) -> i64 { + self.imp().id.get() + } + + pub(crate) fn type_(&self) -> &ChatType { + self.imp().type_.get().unwrap() + } + + pub(crate) fn is_blocked(&self) -> bool { + self.imp().is_blocked.get() + } + + fn set_is_blocked(&self, is_blocked: bool) { + if self.is_blocked() == is_blocked { + return; + } + self.imp().is_blocked.replace(is_blocked); + self.notify("is-blocked"); + } + + pub(crate) fn title(&self) -> String { + self.imp().title.borrow().to_owned() + } + + fn set_title(&self, title: String) { + if self.title() == title { + return; + } + self.imp().title.replace(title); + self.notify("title"); + } + + pub(crate) fn theme_name(&self) -> String { + self.imp().theme_name.borrow().to_owned() + } + + fn set_theme_name(&self, theme_name: String) { + if self.title() == theme_name { + return; + } + self.imp().theme_name.replace(theme_name); + self.notify("theme-name"); + } + + pub(crate) fn chat_theme(&self) -> Option { + self.session() + .find_chat_theme(&self.imp().theme_name.borrow()) + } + + pub(crate) fn avatar(&self) -> Option { + self.imp().avatar.borrow().to_owned() + } + + fn set_avatar(&self, avatar: Option) { + if self.avatar() == avatar { + return; + } + self.imp().avatar.replace(avatar); + self.notify("avatar"); + } + + pub(crate) fn last_read_outbox_message_id(&self) -> i64 { + self.imp().last_read_outbox_message_id.get() + } + + fn set_last_read_outbox_message_id(&self, last_read_outbox_message_id: i64) { + if self.last_read_outbox_message_id() == last_read_outbox_message_id { + return; + } + self.imp() + .last_read_outbox_message_id + .set(last_read_outbox_message_id); + self.notify("last-read-outbox-message-id"); + } + + pub(crate) fn is_marked_as_unread(&self) -> bool { + self.imp().is_marked_as_unread.get() + } + + fn set_marked_as_unread(&self, is_marked_as_unread: bool) { + if self.is_marked_as_unread() == is_marked_as_unread { + return; + } + self.imp().is_marked_as_unread.set(is_marked_as_unread); + self.notify("is-marked-as-unread"); + } + + pub(crate) fn last_message(&self) -> Option { + self.imp().last_message.borrow().to_owned() + } + + fn set_last_message(&self, last_message: Option) { + if self.last_message() == last_message { + return; + } + self.imp().last_message.replace(last_message); + self.notify("last-message"); + } + + pub(crate) fn unread_mention_count(&self) -> i32 { + self.imp().unread_mention_count.get() + } + + fn set_unread_mention_count(&self, unread_mention_count: i32) { + if self.unread_mention_count() == unread_mention_count { + return; + } + self.imp().unread_mention_count.set(unread_mention_count); + self.notify("unread-mention-count"); + } + + pub(crate) fn unread_count(&self) -> i32 { + self.imp().unread_count.get() + } + + fn set_unread_count(&self, unread_count: i32) { + if self.unread_count() == unread_count { + return; + } + self.imp().unread_count.set(unread_count); + self.notify("unread-count"); + } + + pub(crate) fn draft_message(&self) -> Option { + self.imp().draft_message.borrow().to_owned() + } + + fn set_draft_message(&self, draft_message: Option) { + if self.draft_message() == draft_message { + return; + } + self.imp().draft_message.replace(draft_message); + self.notify("draft-message"); + } + + pub(crate) fn notification_settings(&self) -> BoxedChatNotificationSettings { + self.imp() + .notification_settings + .borrow() + .as_ref() + .unwrap() + .to_owned() + } + + fn set_notification_settings(&self, notification_settings: BoxedChatNotificationSettings) { + if self.imp().notification_settings.borrow().as_ref() == Some(¬ification_settings) { + return; + } + self.imp() + .notification_settings + .replace(Some(notification_settings)); + self.notify("notification-settings"); + } + + pub(crate) fn actions(&self) -> &ChatActionList { + self.imp() + .actions + .get_or_init(|| ChatActionList::from(self)) + } + + pub(crate) fn session(&self) -> Session { + self.imp().session.upgrade().unwrap() + } + + pub(crate) fn is_own_chat(&self) -> bool { + self.type_().user() == Some(&self.session().me()) + } + + pub(crate) fn permissions(&self) -> BoxedChatPermissions { + self.imp().permissions.borrow().to_owned().unwrap() + } + + fn set_permissions(&self, permissions: BoxedChatPermissions) { + if self.imp().permissions.borrow().as_ref() == Some(&permissions) { + return; + } + self.imp().permissions.replace(Some(permissions)); + self.notify("permissions"); + } + + pub(crate) fn connect_new_message( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_local("new-message", true, move |values| { + let obj = values[0].get().unwrap(); + let message = values[1].get().unwrap(); + f(obj, message); + None + }) + } + + pub(crate) fn connect_deleted_message( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_local("deleted-message", true, move |values| { + let obj = values[0].get().unwrap(); + let message = values[1].get().unwrap(); + f(obj, message); + None + }) + } + + /// Returns the `Message` of the specified id, if present in the cache. + pub(crate) fn message(&self, message_id: i64) -> Option { + self.imp().messages.borrow().get(&message_id).cloned() + } + + /// Returns the `Message` of the specified id, if present in the cache. Otherwise it + /// fetches it from the server and then it returns the result. + pub(crate) async fn fetch_message(&self, message_id: i64) -> Result { + if let Some(message) = self.message(message_id) { + return Ok(message); + } + + let client_id = self.session().client_id(); + let result = functions::get_message(self.id(), message_id, client_id).await; + + result.map(|r| { + let tdlib::enums::Message::Message(message) = r; + + self.imp() + .messages + .borrow_mut() + .entry(message_id) + .or_insert_with(|| Message::new(message, self)) + .clone() + }) + } + + pub(crate) async fn get_chat_history( + &self, + from_message_id: i64, + limit: i32, + ) -> Result, types::Error> { + let client_id = self.session().client_id(); + let result = + functions::get_chat_history(self.id(), from_message_id, 0, limit, false, client_id) + .await; + + let tdlib::enums::Messages::Messages(data) = result?; + + let mut messages = self.imp().messages.borrow_mut(); + let loaded_messages: Vec = data + .messages + .into_iter() + .flatten() + .map(|m| Message::new(m, self)) + .collect(); + + for message in &loaded_messages { + messages.insert(message.id(), message.clone()); + } + + Ok(loaded_messages) + } + + pub(crate) async fn mark_as_read(&self) -> Result<(), types::Error> { + if let Some(message) = self.last_message() { + functions::view_messages( + self.id(), + vec![message.id()], + None, + true, + self.session().client_id(), + ) + .await?; + } + + functions::toggle_chat_is_marked_as_unread(self.id(), false, self.session().client_id()) + .await + } + + pub(crate) async fn mark_as_unread(&self) -> Result<(), types::Error> { + functions::toggle_chat_is_marked_as_unread(self.id(), true, self.session().client_id()) + .await + } +} diff --git a/src/ui/session/content/background.rs b/src/ui/session/content/background.rs index cd1f91165..d6321ed65 100644 --- a/src/ui/session/content/background.rs +++ b/src/ui/session/content/background.rs @@ -21,15 +21,20 @@ uniform vec3 color1; uniform vec3 color2; uniform vec3 color3; uniform vec3 color4; -uniform vec2 p1; -uniform vec2 p2; -uniform vec2 p3; -uniform vec2 p4; +uniform vec4 p12; +uniform vec4 p34; +uniform vec4 gradient_bounds; void mainImage(out vec4 fragColor, in vec2 fragCoord, in vec2 resolution, in vec2 uv) { + vec2 p1 = p12.xy; + vec2 p2 = p12.zw; + vec2 p3 = p34.xy; + vec2 p4 = p34.zw; + + uv = (fragCoord - gradient_bounds.xy) / gradient_bounds.zw; uv.y = 1.0 - uv.y; float dp1 = distance(uv, p1); @@ -55,7 +60,10 @@ mod imp { #[derive(Default)] pub(crate) struct Background { - pub(super) gradient_texture: RefCell>, + pub(super) chat_theme: RefCell>, + + pub(super) background_texture: RefCell>, + pub(super) last_size: Cell<(f32, f32)>, pub(super) shader: RefCell>, @@ -67,7 +75,8 @@ mod imp { pub(super) dark: Cell, - pub(super) colors: RefCell>, + pub(super) bg_colors: RefCell>, + pub(super) message_colors: RefCell>, } #[glib::object_subclass] @@ -88,10 +97,10 @@ mod imp { self.pattern.set(pattern).unwrap(); let style_manager = adw::StyleManager::default(); - obj.set_theme(hard_coded_themes(style_manager.is_dark())); + obj.refresh_theme(style_manager.is_dark()); style_manager.connect_dark_notify(clone!(@weak obj => move |style_manager| { - obj.set_theme(hard_coded_themes(style_manager.is_dark())) + obj.refresh_theme(style_manager.is_dark()); })); if style_manager.is_high_contrast() { @@ -108,7 +117,7 @@ mod imp { let target = adw::CallbackAnimationTarget::new(clone!(@weak obj => move |progress| { let imp = obj.imp(); - imp.gradient_texture.take(); + imp.background_texture.take(); let progress = progress as f32; if progress >= 1.0 { imp.progress.set(0.0); @@ -177,18 +186,19 @@ mod imp { size_changed: bool, ) { if self.progress.get() == 0.0 { - let texture = match self.gradient_texture.take() { + let texture = match self.background_texture.take() { Some(texture) if !size_changed => texture, _ => { let renderer = self.obj().native().unwrap().renderer(); - renderer.render_texture(self.gradient_shader_node(bounds), Some(bounds)) + + renderer.render_texture(self.obj().bg_node(bounds, bounds), Some(bounds)) } }; snapshot.append_texture(&texture, bounds); - self.gradient_texture.replace(Some(texture)); + self.background_texture.replace(Some(texture)); } else { - snapshot.append_node(self.gradient_shader_node(bounds)); + snapshot.append_node(&self.obj().bg_node(bounds, bounds)); } } @@ -226,7 +236,46 @@ mod imp { } } - fn gradient_shader_node(&self, bounds: &graphene::Rect) -> gsk::GLShaderNode { + pub(super) fn fill_node( + &self, + bounds: &graphene::Rect, + gradient_bounds: &graphene::Rect, + colors: &[graphene::Vec3], + ) -> gsk::RenderNode { + match colors.len() { + 1 => gsk::ColorNode::new(&vec3_to_rgba(&colors[0]), bounds).upcast(), + 2 => gsk::LinearGradientNode::new( + bounds, + &gradient_bounds.top_left(), + &gradient_bounds.bottom_left(), + &[ + gsk::ColorStop::new(0.0, vec3_to_rgba(&colors[0])), + gsk::ColorStop::new(1.0, vec3_to_rgba(&colors[1])), + ], + ) + .upcast(), + 3 => { + log::error!("Three color gradients aren't supported yet"); + + let mut colors = colors.to_vec(); + colors.push(colors[2]); + + self.gradient_shader_node(bounds, gradient_bounds, &colors) + .upcast() + } + 4 => self + .gradient_shader_node(bounds, gradient_bounds, colors) + .upcast(), + _ => unreachable!("Unsupported color count"), + } + } + + pub(super) fn gradient_shader_node( + &self, + bounds: &graphene::Rect, + gradient_bounds: &graphene::Rect, + colors: &[graphene::Vec3], + ) -> gsk::GLShaderNode { let Some(gradient_shader) = &*self.shader.borrow() else { unreachable!() }; @@ -236,10 +285,8 @@ mod imp { let progress = self.progress.get(); let phase = self.phase.get() as usize; - let colors = self.colors.borrow(); - - let &[c1, c2, c3, c4] = &colors[..] else { - unimplemented!("Unexpected color count"); + let &[c1, c2, c3, c4] = colors else { + unimplemented!("Unexpected color count") }; args_builder.set_vec3(0, &c1); @@ -247,16 +294,25 @@ mod imp { args_builder.set_vec3(2, &c3); args_builder.set_vec3(3, &c4); - let [p1, p2, p3, p4] = Self::calculate_positions(progress, phase); - args_builder.set_vec2(4, &p1); - args_builder.set_vec2(5, &p2); - args_builder.set_vec2(6, &p3); - args_builder.set_vec2(7, &p4); + let [p12, p34] = Self::calculate_positions(progress, phase); + args_builder.set_vec4(4, &p12); + args_builder.set_vec4(5, &p34); + + let gradient_bounds = { + graphene::Vec4::new( + gradient_bounds.x(), + gradient_bounds.y(), + gradient_bounds.width(), + gradient_bounds.height(), + ) + }; + + args_builder.set_vec4(6, &gradient_bounds); gsk::GLShaderNode::new(gradient_shader, bounds, &args_builder.to_args(), &[]) } - fn calculate_positions(progress: f32, phase: usize) -> [graphene::Vec2; 4] { + fn calculate_positions(progress: f32, phase: usize) -> [graphene::Vec4; 2] { static POSITIONS: [(f32, f32); 8] = [ (0.80, 0.10), (0.60, 0.20), @@ -268,7 +324,7 @@ mod imp { (0.75, 0.40), ]; - let mut points = [graphene::Vec2::new(0.0, 0.0); 4]; + let mut points = [(0.0, 0.0); 4]; for i in 0..4 { let start = POSITIONS[(i * 2 + phase) % 8]; @@ -281,10 +337,18 @@ mod imp { let x = interpolate(start.0, end.0, progress); let y = interpolate(start.1, end.1, progress); - points[i] = graphene::Vec2::new(x, y); + points[i] = (x, y); } - points + let points: Vec<_> = points + .chunks(2) + .map(|p| { + let [(x1, y1), (x2, y2)]: [(f32, f32); 2] = p.try_into().unwrap(); + graphene::Vec4::from_float([x1, y1, x2, y2]) + }) + .collect(); + + points.try_into().unwrap() } } } @@ -299,50 +363,43 @@ impl Background { glib::Object::new() } - pub(crate) fn set_theme(&self, theme: tdlib::types::ThemeSettings) { - let Some(background) = theme.background else { + pub(crate) fn set_chat_theme(&self, theme: Option) { + self.imp().chat_theme.replace(theme); + self.refresh_theme(adw::StyleManager::default().is_dark()); + } + + pub(crate) fn set_theme(&self, theme: &tdlib::types::ThemeSettings) { + let Some(background) = &theme.background else { return; }; let imp = self.imp(); imp.dark.set(background.is_dark); - let fill = match background.r#type { - tdlib::enums::BackgroundType::Pattern(pattern) => pattern.fill, - tdlib::enums::BackgroundType::Fill(fill) => fill.fill, + let bg_fill = match &background.r#type { + tdlib::enums::BackgroundType::Pattern(pattern) => &pattern.fill, + tdlib::enums::BackgroundType::Fill(fill) => &fill.fill, tdlib::enums::BackgroundType::Wallpaper(_) => { unimplemented!("Wallpaper chat background") } }; - match fill { - tdlib::enums::BackgroundFill::FreeformGradient(gradient) => { - if gradient.colors.len() != 4 { - unimplemented!("Unsupported gradient colors count"); - } - - let colors = gradient - .colors - .into_iter() - .map(|int_color| { - let r = (int_color >> 16) & 0xFF; - let g = (int_color >> 8) & 0xFF; - let b = int_color & 0xFF; + imp.bg_colors.replace(fill_colors(bg_fill)); + imp.message_colors + .replace(fill_colors(&theme.outgoing_message_fill)); - graphene::Vec3::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0) - }) - .collect(); - - imp.colors.replace(colors); - } - _ => unimplemented!("Background fill"), - } - - imp.gradient_texture.take(); + imp.background_texture.take(); self.queue_draw(); } pub(crate) fn animate(&self) { + let nothing_to_animate = self.imp().bg_colors.borrow().len() <= 2 + && self.imp().message_colors.borrow().len() <= 2; + + if nothing_to_animate { + return; + } + let animation = self.imp().animation.get().unwrap(); let val = animation.value(); @@ -351,6 +408,29 @@ impl Background { } } + pub fn subscribe_to_redraw(&self, child: >k::Widget) { + let animation = self.imp().animation.get().unwrap(); + animation.connect_value_notify(clone!(@weak child => move |_| child.queue_draw())); + } + + pub fn bg_node( + &self, + bounds: &graphene::Rect, + gradient_bounds: &graphene::Rect, + ) -> gsk::RenderNode { + self.imp() + .fill_node(bounds, gradient_bounds, &self.imp().bg_colors.borrow()) + } + + pub fn message_bg_node( + &self, + bounds: &graphene::Rect, + gradient_bounds: &graphene::Rect, + ) -> gsk::RenderNode { + self.imp() + .fill_node(bounds, gradient_bounds, &self.imp().message_colors.borrow()) + } + fn ensure_shader(&self) { let imp = self.imp(); if imp.shader.borrow().is_none() { @@ -370,6 +450,23 @@ impl Background { } }; } + + fn refresh_theme(&self, dark: bool) { + if let Some(chat_theme) = &*self.imp().chat_theme.borrow() { + let theme = if dark { + &chat_theme.dark_settings + } else { + &chat_theme.light_settings + }; + + self.set_theme(theme); + + // For some reason tdlib tells that light theme is dark + self.imp().dark.set(dark); + } else { + self.set_theme(&hard_coded_themes(dark)); + } + } } impl Default for Background { @@ -378,8 +475,36 @@ impl Default for Background { } } +fn fill_colors(fill: &tdlib::enums::BackgroundFill) -> Vec { + match fill { + tdlib::enums::BackgroundFill::FreeformGradient(gradient) if gradient.colors.len() == 4 => { + gradient.colors.iter().map(int_color_to_vec3).collect() + } + tdlib::enums::BackgroundFill::Solid(solid) => vec![int_color_to_vec3(&solid.color)], + tdlib::enums::BackgroundFill::Gradient(gradient) => vec![ + int_color_to_vec3(&gradient.top_color), + int_color_to_vec3(&gradient.bottom_color), + ], + _ => unimplemented!("Unsupported background fill: {fill:?}"), + } +} + +fn int_color_to_vec3(color: &i32) -> graphene::Vec3 { + let [_, r, g, b] = color.to_be_bytes(); + graphene::Vec3::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0) +} + +fn vec3_to_rgba(vec3: &graphene::Vec3) -> gdk::RGBA { + let [red, green, blue] = vec3.to_float(); + gdk::RGBA::new(red, green, blue, 1.0) +} + fn hard_coded_themes(dark: bool) -> tdlib::types::ThemeSettings { - fn theme(dark: bool, colors: Vec) -> tdlib::types::ThemeSettings { + fn theme( + dark: bool, + bg_colors: Vec, + message_colors: Vec, + ) -> tdlib::types::ThemeSettings { use tdlib::enums::BackgroundFill::*; use tdlib::enums::BackgroundType::Fill; use tdlib::types::*; @@ -389,7 +514,7 @@ fn hard_coded_themes(dark: bool) -> tdlib::types::ThemeSettings { is_default: true, is_dark: dark, r#type: Fill(BackgroundTypeFill { - fill: FreeformGradient(BackgroundFillFreeformGradient { colors }), + fill: FreeformGradient(BackgroundFillFreeformGradient { colors: bg_colors }), }), id: 0, name: String::new(), @@ -398,13 +523,25 @@ fn hard_coded_themes(dark: bool) -> tdlib::types::ThemeSettings { accent_color: 0, animate_outgoing_message_fill: false, outgoing_message_accent_color: 0, - outgoing_message_fill: Solid(BackgroundFillSolid { color: 0 }), + outgoing_message_fill: FreeformGradient(BackgroundFillFreeformGradient { + colors: message_colors, + }), } } + // tr tl bl br + if dark { - theme(dark, vec![0xd6932e, 0xbc40db, 0x4280d7, 0x614ed5]) + theme( + dark, + vec![0xd6932e, 0xbc40db, 0x4280d7, 0x614ed5], + vec![0x2d52ab, 0x4036a1, 0x9f388d, 0x9d3941], + ) } else { - theme(dark, vec![0x94dae9, 0x9aeddb, 0x94c3f6, 0xac96f7]) + theme( + dark, + vec![0x94dae9, 0x9aeddb, 0x94c3f6, 0xac96f7], + vec![0xddecff, 0xe0ddfd, 0xdbffff, 0xddffdf], + ) } } diff --git a/src/ui/session/content/chat_history.rs b/src/ui/session/content/chat_history.rs index 28a706c6f..d52c280fe 100644 --- a/src/ui/session/content/chat_history.rs +++ b/src/ui/session/content/chat_history.rs @@ -25,7 +25,8 @@ mod imp { #[template(resource = "/app/drey/paper-plane/ui/session/content/chat_history.ui")] pub(crate) struct ChatHistory { pub(super) chat: glib::WeakRef, - pub(super) chat_handler: RefCell>, + pub(super) chat_handlers: RefCell>, + pub(super) session_handlers: RefCell>, pub(super) model: RefCell>, pub(super) message_menu: OnceCell, pub(super) is_auto_scrolling: Cell, @@ -354,13 +355,43 @@ impl ChatHistory { } })); - let handler = chat.connect_new_message(clone!(@weak self as obj => move |_, msg| { - if msg.is_outgoing() { - obj.imp().background.animate(); + self.imp().background.set_chat_theme(chat.chat_theme()); + + let new_message_handler = + chat.connect_new_message(clone!(@weak self as obj => move |_, msg| { + if msg.is_outgoing() { + obj.imp().background.animate(); + } + })); + + let chat_theme_handler = chat.connect_notify_local( + Some("theme-name"), + clone!(@weak self as obj => move |chat, _| { + obj.imp().background.set_chat_theme(chat.chat_theme()); + }), + ); + + for old_handler in self + .imp() + .chat_handlers + .replace(vec![new_message_handler, chat_theme_handler]) + { + if let Some(old_chat) = imp.chat.upgrade() { + old_chat.disconnect(old_handler); } - })); + } - if let Some(old_handler) = self.imp().chat_handler.replace(Some(handler)) { + let chat_themes_handler = chat.session_().connect_update_chat_themes( + clone!(@weak self as obj, @weak chat => move || { + obj.imp().background.set_chat_theme(chat.chat_theme()); + }), + ); + + for old_handler in self + .imp() + .session_handlers + .replace(vec![chat_themes_handler]) + { if let Some(old_chat) = imp.chat.upgrade() { old_chat.disconnect(old_handler); } diff --git a/src/ui/session/content/message_row/bubble.blp b/src/ui/session/content/message_row/bubble.blp index e6d8a1b56..27239c902 100644 --- a/src/ui/session/content/message_row/bubble.blp +++ b/src/ui/session/content/message_row/bubble.blp @@ -2,6 +2,8 @@ using Gtk 4.0; using Adw 1; template $PaplMessageBubble { + overflow: hidden; + Box box_ { orientation: vertical; diff --git a/src/ui/session/content/message_row/bubble.rs b/src/ui/session/content/message_row/bubble.rs index be012d7b4..304f33cd5 100644 --- a/src/ui/session/content/message_row/bubble.rs +++ b/src/ui/session/content/message_row/bubble.rs @@ -4,6 +4,7 @@ use std::hash::Hash; use std::hash::Hasher; use adw::prelude::*; +use glib::clone; use gtk::glib; use gtk::subclass::prelude::*; use gtk::CompositeTemplate; @@ -32,6 +33,8 @@ mod imp { pub(crate) struct MessageBubble { pub(super) sender_color_class: RefCell>, pub(super) sender_binding: RefCell>, + pub(super) parent_list_view: RefCell>, + pub(super) parent_background: RefCell>, #[template_child] pub(super) box_: TemplateChild, #[template_child] @@ -99,6 +102,54 @@ mod imp { } impl WidgetImpl for MessageBubble { + fn realize(&self) { + self.parent_realize(); + + let widget = self.obj(); + + if let Some(view) = widget.parent_list_view() { + self.parent_list_view.replace(view.downgrade()); + view.vadjustment() + .unwrap() + .connect_value_notify(clone!(@weak widget => move |_| { + widget.queue_draw(); + })); + } + + if let Some(background) = widget.parent_background() { + self.parent_background.replace(background.downgrade()); + background.subscribe_to_redraw(widget.upcast_ref()); + } + } + + fn snapshot(&self, snapshot: >k::Snapshot) { + let widget = self.obj(); + + if let Some(background) = self.parent_background.borrow().upgrade() { + if !background.has_css_class("fallback") { + let bounds = { + let width = widget.width() as f32; + let height = widget.height() as f32; + + gtk::graphene::Rect::new(0.0, 0.0, width, height) + }; + + let gradient_bounds = background.compute_bounds(self.obj().as_ref()).unwrap(); + + if widget.has_css_class("outgoing") { + snapshot + .append_node(&background.message_bg_node(&bounds, &gradient_bounds)); + } else { + snapshot.push_opacity(0.1); + snapshot.append_node(&background.bg_node(&bounds, &gradient_bounds)); + snapshot.pop(); + }; + } + } + + self.parent_snapshot(snapshot); + } + fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) { // Limit the widget width if orientation == gtk::Orientation::Horizontal { @@ -299,6 +350,16 @@ impl MessageBubble { .set_indicators(Some(imp.indicators.clone())); } } + + fn parent_list_view(&self) -> Option { + self.ancestor(gtk::ListView::static_type())?.downcast().ok() + } + + fn parent_background(&self) -> Option { + self.ancestor(ui::Background::static_type())? + .downcast() + .ok() + } } fn hash_label(label: &str) -> i64 {