From e776c7432ba0b795068b1e3aee57435c2b688ef2 Mon Sep 17 00:00:00 2001 From: Marco Melorio Date: Mon, 13 Mar 2023 23:38:45 +0100 Subject: [PATCH 1/3] WIP: chat-history: Move the message list in a separate widget This a new major change to the chat history. We now have a new widget component, called MessageListView, which can display messages... in a list view. For now it's basically the same as the internal list view in ChatHistory, but in the future it will support showing messages from other contexts, like pinned messages history, reply threads, post comments, etc. --- data/resources/resources.gresource.xml | 1 + data/resources/style.css | 4 +- data/resources/ui/content-chat-history.ui | 91 +----- data/resources/ui/content-event-row.blp | 2 +- data/resources/ui/message-list-view.ui | 95 ++++++ data/resources/ui/message-menu.blp | 4 +- .../message_list_view}/event_row.rs | 20 +- .../message_list_view/item.rs} | 42 +-- .../message_list_view}/message_row/base.rs | 28 +- .../message_list_view}/message_row/bubble.rs | 2 +- .../message_row/document.rs | 5 +- .../message_row/indicators.rs | 0 .../message_list_view}/message_row/label.rs | 2 +- .../message_row/media_picture.rs | 0 .../message_list_view}/message_row/mod.rs | 46 +-- .../message_list_view}/message_row/photo.rs | 7 +- .../message_list_view}/message_row/reply.rs | 0 .../message_list_view}/message_row/sticker.rs | 7 +- .../message_list_view}/message_row/text.rs | 5 +- .../message_list_view}/message_row/video.rs | 7 +- src/components/message_list_view/mod.rs | 295 ++++++++++++++++++ .../message_list_view/model.rs} | 53 ++-- .../message_list_view/row.rs} | 37 ++- src/components/mod.rs | 2 + src/session/content/chat_history.rs | 186 +---------- src/session/content/mod.rs | 10 - 26 files changed, 514 insertions(+), 437 deletions(-) create mode 100644 data/resources/ui/message-list-view.ui rename src/{session/content => components/message_list_view}/event_row.rs (77%) rename src/{session/content/chat_history_item.rs => components/message_list_view/item.rs} (57%) rename src/{session/content => components/message_list_view}/message_row/base.rs (78%) rename src/{session/content => components/message_list_view}/message_row/bubble.rs (99%) rename src/{session/content => components/message_list_view}/message_row/document.rs (99%) rename src/{session/content => components/message_list_view}/message_row/indicators.rs (100%) rename src/{session/content => components/message_list_view}/message_row/label.rs (99%) rename src/{session/content => components/message_list_view}/message_row/media_picture.rs (100%) rename src/{session/content => components/message_list_view}/message_row/mod.rs (87%) rename src/{session/content => components/message_list_view}/message_row/photo.rs (98%) rename src/{session/content => components/message_list_view}/message_row/reply.rs (100%) rename src/{session/content => components/message_list_view}/message_row/sticker.rs (97%) rename src/{session/content => components/message_list_view}/message_row/text.rs (98%) rename src/{session/content => components/message_list_view}/message_row/video.rs (98%) create mode 100644 src/components/message_list_view/mod.rs rename src/{session/content/chat_history_model.rs => components/message_list_view/model.rs} (83%) rename src/{session/content/chat_history_row.rs => components/message_list_view/row.rs} (82%) diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml index 987ace68e..95761add8 100644 --- a/data/resources/resources.gresource.xml +++ b/data/resources/resources.gresource.xml @@ -25,6 +25,7 @@ ui/content-message-text.ui ui/content-send-photo-dialog.ui ui/login.ui + ui/message-list-view.ui ui/message-menu.ui ui/phone-number-input.ui ui/preferences-window.ui diff --git a/data/resources/style.css b/data/resources/style.css index 069429436..3720bd70c 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -115,12 +115,12 @@ sidebarsearchitemrow { border-spacing: 12px; } -listview.chat-history { +messagelistview listview.message { background: transparent; padding: 3px 0; } -listview.chat-history > row { +messagelistview listview.message > row { margin: 2px 4px; border-radius: 9px; } diff --git a/data/resources/ui/content-chat-history.ui b/data/resources/ui/content-chat-history.ui index f99acad88..bb1f37096 100644 --- a/data/resources/ui/content-chat-history.ui +++ b/data/resources/ui/content-chat-history.ui @@ -34,96 +34,7 @@ - - - - slide-up - - end - end - - - - - center - start - middle - - - ContentChatHistory - - - - - ContentChatHistory - - - - - - - - center - end - go-down-symbolic - chat-history.scroll-down - - Scroll to bottom - - - - - - - - - - - True - never - - - 800 - 600 - natural - - - True - - - - - - - - ]]> - - - - - - - - - + diff --git a/data/resources/ui/content-event-row.blp b/data/resources/ui/content-event-row.blp index 21828f538..d6b1a8c1b 100644 --- a/data/resources/ui/content-event-row.blp +++ b/data/resources/ui/content-event-row.blp @@ -1,7 +1,7 @@ using Gtk 4.0; using Adw 1; -template ContentEventRow : Adw.Bin { +template MessageListViewEventRow : Adw.Bin { styles ["event-row"] child: Label label { diff --git a/data/resources/ui/message-list-view.ui b/data/resources/ui/message-list-view.ui new file mode 100644 index 000000000..cbbceeb1d --- /dev/null +++ b/data/resources/ui/message-list-view.ui @@ -0,0 +1,95 @@ + + + + diff --git a/data/resources/ui/message-menu.blp b/data/resources/ui/message-menu.blp index 34ff60a1b..b52a3a251 100644 --- a/data/resources/ui/message-menu.blp +++ b/data/resources/ui/message-menu.blp @@ -16,13 +16,13 @@ menu model { item { label: _("Delete for Ever_yone"); - action: "message-row.revoke-delete"; + action: "message-list-view.revoke-delete"; hidden-when: "action-disabled"; } item { label: _("_Delete for Me"); - action: "message-row.delete"; + action: "message-list-view.delete"; hidden-when: "action-disabled"; } } diff --git a/src/session/content/event_row.rs b/src/components/message_list_view/event_row.rs similarity index 77% rename from src/session/content/event_row.rs rename to src/components/message_list_view/event_row.rs index 7f1262c43..9dbb38d88 100644 --- a/src/session/content/event_row.rs +++ b/src/components/message_list_view/event_row.rs @@ -9,15 +9,15 @@ mod imp { #[derive(Debug, Default, CompositeTemplate)] #[template(resource = "/com/github/melix99/telegrand/ui/content-event-row.ui")] - pub(crate) struct EventRow { + pub(crate) struct MessageListViewEventRow { #[template_child] pub(super) label: TemplateChild, } #[glib::object_subclass] - impl ObjectSubclass for EventRow { - const NAME: &'static str = "ContentEventRow"; - type Type = super::EventRow; + impl ObjectSubclass for MessageListViewEventRow { + const NAME: &'static str = "MessageListViewEventRow"; + type Type = super::MessageListViewEventRow; type ParentType = adw::Bin; fn class_init(klass: &mut Self::Class) { @@ -29,7 +29,7 @@ mod imp { } } - impl ObjectImpl for EventRow { + impl ObjectImpl for MessageListViewEventRow { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| vec![glib::ParamSpecString::builder("label").build()]); @@ -55,22 +55,22 @@ mod imp { } } - impl WidgetImpl for EventRow {} - impl BinImpl for EventRow {} + impl WidgetImpl for MessageListViewEventRow {} + impl BinImpl for MessageListViewEventRow {} } glib::wrapper! { - pub(crate) struct EventRow(ObjectSubclass) + pub(crate) struct MessageListViewEventRow(ObjectSubclass) @extends gtk::Widget, adw::Bin; } -impl Default for EventRow { +impl Default for MessageListViewEventRow { fn default() -> Self { Self::new() } } -impl EventRow { +impl MessageListViewEventRow { pub(crate) fn new() -> Self { glib::Object::new() } diff --git a/src/session/content/chat_history_item.rs b/src/components/message_list_view/item.rs similarity index 57% rename from src/session/content/chat_history_item.rs rename to src/components/message_list_view/item.rs index cf1682790..63f7b505c 100644 --- a/src/session/content/chat_history_item.rs +++ b/src/components/message_list_view/item.rs @@ -6,8 +6,8 @@ use gtk::subclass::prelude::*; use crate::tdlib::Message; #[derive(Clone, Debug, glib::Boxed)] -#[boxed_type(name = "ContentChatHistoryItemType")] -pub(crate) enum ChatHistoryItemType { +#[boxed_type(name = "MessageListViewItemType")] +pub(crate) enum MessageListViewItemType { Message(Message), DayDivider(DateTime), } @@ -17,23 +17,25 @@ mod imp { use once_cell::sync::{Lazy, OnceCell}; #[derive(Debug, Default)] - pub(crate) struct ChatHistoryItem { - pub(super) type_: OnceCell, + pub(crate) struct MessageListViewItem { + pub(super) type_: OnceCell, } #[glib::object_subclass] - impl ObjectSubclass for ChatHistoryItem { - const NAME: &'static str = "ContentChatHistoryItem"; - type Type = super::ChatHistoryItem; + impl ObjectSubclass for MessageListViewItem { + const NAME: &'static str = "MessageListViewItem"; + type Type = super::MessageListViewItem; } - impl ObjectImpl for ChatHistoryItem { + impl ObjectImpl for MessageListViewItem { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| { - vec![glib::ParamSpecBoxed::builder::("type") - .write_only() - .construct_only() - .build()] + vec![ + glib::ParamSpecBoxed::builder::("type") + .write_only() + .construct_only() + .build(), + ] }); PROPERTIES.as_ref() } @@ -41,7 +43,7 @@ mod imp { fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { match pspec.name() { "type" => { - let type_ = value.get::().unwrap(); + let type_ = value.get::().unwrap(); self.type_.set(type_).unwrap(); } _ => unimplemented!(), @@ -51,26 +53,26 @@ mod imp { } glib::wrapper! { - pub(crate) struct ChatHistoryItem(ObjectSubclass); + pub(crate) struct MessageListViewItem(ObjectSubclass); } -impl ChatHistoryItem { +impl MessageListViewItem { pub(crate) fn for_message(message: Message) -> Self { - let type_ = ChatHistoryItemType::Message(message); + let type_ = MessageListViewItemType::Message(message); glib::Object::builder().property("type", type_).build() } pub(crate) fn for_day_divider(day: DateTime) -> Self { - let type_ = ChatHistoryItemType::DayDivider(day); + let type_ = MessageListViewItemType::DayDivider(day); glib::Object::builder().property("type", type_).build() } - pub(crate) fn type_(&self) -> &ChatHistoryItemType { + pub(crate) fn type_(&self) -> &MessageListViewItemType { self.imp().type_.get().unwrap() } pub(crate) fn message(&self) -> Option<&Message> { - if let ChatHistoryItemType::Message(message) = self.type_() { + if let MessageListViewItemType::Message(message) = self.type_() { Some(message) } else { None @@ -78,7 +80,7 @@ impl ChatHistoryItem { } pub(crate) fn message_timestamp(&self) -> Option { - if let ChatHistoryItemType::Message(message) = self.type_() { + if let MessageListViewItemType::Message(message) = self.type_() { Some( glib::DateTime::from_unix_utc(message.date().into()) .and_then(|t| t.to_local()) diff --git a/src/session/content/message_row/base.rs b/src/components/message_list_view/message_row/base.rs similarity index 78% rename from src/session/content/message_row/base.rs rename to src/components/message_list_view/message_row/base.rs index 1cbaea860..5a00bc38f 100644 --- a/src/session/content/message_row/base.rs +++ b/src/components/message_list_view/message_row/base.rs @@ -1,11 +1,11 @@ use gtk::prelude::*; use gtk::subclass::prelude::*; -use gtk::{gdk, glib, CompositeTemplate}; +use gtk::{glib, CompositeTemplate}; mod imp { use super::*; - use crate::session::content::ChatHistory; + use crate::components::MessageListView; #[derive(Debug, Default, CompositeTemplate)] #[template(string = r#" @@ -44,26 +44,24 @@ mod imp { impl MessageBase { #[template_callback] fn on_pressed(&self, _n_press: i32, x: f64, y: f64) { - self.show_message_menu(x as i32, y as i32); + self.show_message_menu(x, y); } #[template_callback] fn on_long_pressed(&self, x: f64, y: f64) { - self.show_message_menu(x as i32, y as i32); + self.show_message_menu(x, y); } - fn show_message_menu(&self, x: i32, y: i32) { + fn show_message_menu(&self, x: f64, y: f64) { let obj = self.obj(); - let chat_history = obj.ancestor(ChatHistory::static_type()).unwrap(); - let menu = chat_history - .downcast_ref::() - .unwrap() - .message_menu(); - - menu.set_pointing_to(Some(&gdk::Rectangle::new(x, y, 0, 0))); - menu.unparent(); - menu.set_parent(&*obj); - menu.popup(); + let list_view = obj + .ancestor(MessageListView::static_type()) + .and_downcast::() + .unwrap(); + let (x, y) = obj.translate_coordinates(&list_view, x, y).unwrap(); + + obj.activate_action("message-list-view.show-message-menu", Some(&(x, y).into())) + .unwrap(); } } diff --git a/src/session/content/message_row/bubble.rs b/src/components/message_list_view/message_row/bubble.rs similarity index 99% rename from src/session/content/message_row/bubble.rs rename to src/components/message_list_view/message_row/bubble.rs index 3660f133c..7fb8b7109 100644 --- a/src/session/content/message_row/bubble.rs +++ b/src/components/message_list_view/message_row/bubble.rs @@ -4,7 +4,7 @@ use gtk::{glib, CompositeTemplate}; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; -use crate::session::content::message_row::{MessageIndicators, MessageLabel, MessageReply}; +use super::{MessageIndicators, MessageLabel, MessageReply}; use crate::tdlib::{Chat, ChatType, Message, MessageSender, SponsoredMessage}; const MAX_WIDTH: i32 = 400; diff --git a/src/session/content/message_row/document.rs b/src/components/message_list_view/message_row/document.rs similarity index 99% rename from src/session/content/message_row/document.rs rename to src/components/message_list_view/message_row/document.rs index 916897494..e84340fe2 100644 --- a/src/session/content/message_row/document.rs +++ b/src/components/message_list_view/message_row/document.rs @@ -5,13 +5,12 @@ use gtk::{gdk, gio, glib, CompositeTemplate}; use tdlib::enums::MessageContent; use tdlib::types::File; -use crate::session::content::message_row::{MessageBase, MessageBaseImpl, MessageBubble}; +use super::base::MessageBaseExt; +use super::{MessageBase, MessageBaseImpl, MessageBubble}; use crate::tdlib::Message; use crate::utils::{parse_formatted_text, spawn}; use crate::Session; -use super::base::MessageBaseExt; - mod imp { use super::*; use once_cell::sync::Lazy; diff --git a/src/session/content/message_row/indicators.rs b/src/components/message_list_view/message_row/indicators.rs similarity index 100% rename from src/session/content/message_row/indicators.rs rename to src/components/message_list_view/message_row/indicators.rs diff --git a/src/session/content/message_row/label.rs b/src/components/message_list_view/message_row/label.rs similarity index 99% rename from src/session/content/message_row/label.rs rename to src/components/message_list_view/message_row/label.rs index 6a7c38f8e..6bf8596f6 100644 --- a/src/session/content/message_row/label.rs +++ b/src/components/message_list_view/message_row/label.rs @@ -2,7 +2,7 @@ use gtk::prelude::*; use gtk::subclass::prelude::*; use gtk::{glib, pango, CompositeTemplate}; -use crate::session::content::message_row::MessageIndicators; +use super::MessageIndicators; const OBJECT_REPLACEMENT_CHARACTER: char = '\u{FFFC}'; const INDICATORS_SPACING: i32 = 6; diff --git a/src/session/content/message_row/media_picture.rs b/src/components/message_list_view/message_row/media_picture.rs similarity index 100% rename from src/session/content/message_row/media_picture.rs rename to src/components/message_list_view/message_row/media_picture.rs diff --git a/src/session/content/message_row/mod.rs b/src/components/message_list_view/message_row/mod.rs similarity index 87% rename from src/session/content/message_row/mod.rs rename to src/components/message_list_view/message_row/mod.rs index 6566be5c4..15d29674f 100644 --- a/src/session/content/message_row/mod.rs +++ b/src/components/message_list_view/message_row/mod.rs @@ -23,15 +23,12 @@ use self::text::MessageText; use self::video::MessageVideo; use adw::prelude::*; -use gettextrs::gettext; -use glib::clone; use gtk::subclass::prelude::*; -use gtk::{gio, glib, CompositeTemplate}; +use gtk::{glib, CompositeTemplate}; use tdlib::enums::{MessageContent, StickerFormat}; use crate::components::Avatar; use crate::tdlib::{Chat, ChatType, Message, MessageForwardOrigin, MessageSender}; -use crate::utils::spawn; const AVATAR_SIZE: i32 = 32; const SPACING: i32 = 6; @@ -71,12 +68,6 @@ mod imp { widget.reply() }); klass.install_action("message-row.edit", None, move |widget, _, _| widget.edit()); - klass.install_action("message-row.revoke-delete", None, move |widget, _, _| { - widget.show_delete_dialog(true) - }); - klass.install_action("message-row.delete", None, move |widget, _, _| { - widget.show_delete_dialog(false) - }); } fn instance_init(obj: &glib::subclass::InitializingObject) { @@ -162,41 +153,6 @@ impl MessageRow { } } - fn show_delete_dialog(&self, revoke: bool) { - let window: gtk::Window = self.root().and_then(|root| root.downcast().ok()).unwrap(); - - let message = if revoke { - gettext("Do you want to delete this message for everyone?") - } else { - gettext("Do you want to delete this message?") - }; - - let dialog = adw::MessageDialog::builder() - .heading(gettext("Confirm Message Deletion")) - .body_use_markup(true) - .body(message) - .transient_for(&window) - .build(); - - dialog.add_responses(&[("no", &gettext("_No")), ("yes", &gettext("_Yes"))]); - dialog.set_default_response(Some("no")); - dialog.set_response_appearance("yes", adw::ResponseAppearance::Destructive); - - dialog.choose( - gio::Cancellable::NONE, - clone!(@weak self as obj => move |response| { - if response == "yes" { - if let Ok(message) = obj.message().downcast::() { - spawn(async move { - if let Err(e) = message.delete(revoke).await { - log::warn!("Error deleting a message (revoke = {}): {:?}", revoke, e); - } - }); - } - } - })); - } - pub(crate) fn message(&self) -> glib::Object { self.imp().message.borrow().clone().unwrap() } diff --git a/src/session/content/message_row/photo.rs b/src/components/message_list_view/message_row/photo.rs similarity index 98% rename from src/session/content/message_row/photo.rs rename to src/components/message_list_view/message_row/photo.rs index 9122dfe6f..f2031264a 100644 --- a/src/session/content/message_row/photo.rs +++ b/src/components/message_list_view/message_row/photo.rs @@ -4,15 +4,12 @@ use gtk::subclass::prelude::*; use gtk::{gdk, gio, glib, CompositeTemplate}; use tdlib::enums::MessageContent; -use crate::session::content::message_row::{ - MediaPicture, MessageBase, MessageBaseImpl, MessageBubble, -}; +use super::base::MessageBaseExt; +use super::{MediaPicture, MessageBase, MessageBaseImpl, MessageBubble}; use crate::tdlib::{BoxedMessageContent, Message}; use crate::utils::{decode_image_from_path, parse_formatted_text, spawn}; use crate::Session; -use super::base::MessageBaseExt; - mod imp { use super::*; use once_cell::sync::Lazy; diff --git a/src/session/content/message_row/reply.rs b/src/components/message_list_view/message_row/reply.rs similarity index 100% rename from src/session/content/message_row/reply.rs rename to src/components/message_list_view/message_row/reply.rs diff --git a/src/session/content/message_row/sticker.rs b/src/components/message_list_view/message_row/sticker.rs similarity index 97% rename from src/session/content/message_row/sticker.rs rename to src/components/message_list_view/message_row/sticker.rs index bac6ff27d..988c19bd6 100644 --- a/src/session/content/message_row/sticker.rs +++ b/src/components/message_list_view/message_row/sticker.rs @@ -3,14 +3,11 @@ use gtk::subclass::prelude::*; use gtk::{glib, CompositeTemplate}; use tdlib::enums::{MessageContent, StickerFullType}; +use super::base::MessageBaseExt; +use super::{MessageBase, MessageBaseImpl, MessageIndicators, MessageReply}; use crate::components::Sticker; -use crate::session::content::message_row::{ - MessageBase, MessageBaseImpl, MessageIndicators, MessageReply, -}; use crate::tdlib::Message; -use super::base::MessageBaseExt; - const MAX_REPLY_CHAR_WIDTH: i32 = 18; const STICKER_SIZE: i32 = 176; diff --git a/src/session/content/message_row/text.rs b/src/components/message_list_view/message_row/text.rs similarity index 98% rename from src/session/content/message_row/text.rs rename to src/components/message_list_view/message_row/text.rs index 19a1e0cc1..de925b944 100644 --- a/src/session/content/message_row/text.rs +++ b/src/components/message_list_view/message_row/text.rs @@ -5,12 +5,11 @@ use gtk::subclass::prelude::*; use gtk::{glib, CompositeTemplate}; use tdlib::enums::MessageContent; -use crate::session::content::message_row::{MessageBase, MessageBaseImpl, MessageBubble}; +use super::base::MessageBaseExt; +use super::{MessageBase, MessageBaseImpl, MessageBubble}; use crate::tdlib::{BoxedMessageContent, Message, SponsoredMessage}; use crate::utils::parse_formatted_text; -use super::base::MessageBaseExt; - mod imp { use super::*; use once_cell::sync::Lazy; diff --git a/src/session/content/message_row/video.rs b/src/components/message_list_view/message_row/video.rs similarity index 98% rename from src/session/content/message_row/video.rs rename to src/components/message_list_view/message_row/video.rs index ec61162f8..d792adeb9 100644 --- a/src/session/content/message_row/video.rs +++ b/src/components/message_list_view/message_row/video.rs @@ -4,15 +4,12 @@ use gtk::subclass::prelude::*; use gtk::{gdk, glib, CompositeTemplate}; use tdlib::enums::MessageContent; -use crate::session::content::message_row::{ - MediaPicture, MessageBase, MessageBaseImpl, MessageBubble, -}; +use super::base::MessageBaseExt; +use super::{MediaPicture, MessageBase, MessageBaseImpl, MessageBubble}; use crate::tdlib::Message; use crate::utils::{parse_formatted_text, spawn}; use crate::Session; -use super::base::MessageBaseExt; - mod imp { use super::*; use once_cell::sync::Lazy; diff --git a/src/components/message_list_view/mod.rs b/src/components/message_list_view/mod.rs new file mode 100644 index 000000000..d66f44f64 --- /dev/null +++ b/src/components/message_list_view/mod.rs @@ -0,0 +1,295 @@ +mod event_row; +mod item; +mod message_row; +mod model; +mod row; + +use self::event_row::MessageListViewEventRow; +use self::item::{MessageListViewItem, MessageListViewItemType}; +use self::message_row::MessageRow; +use self::model::{MessageListViewModel, MessageListViewModelError}; +use self::row::MessageListViewRow; + +use adw::prelude::*; +use gettextrs::gettext; +use glib::clone; +use gtk::subclass::prelude::*; +use gtk::{gdk, gio, glib, CompositeTemplate}; + +use crate::tdlib::{Chat, ChatType, SponsoredMessage}; +use crate::utils::spawn; +use crate::Session; + +const MIN_N_ITEMS: u32 = 20; + +mod imp { + use super::*; + use once_cell::unsync::OnceCell; + use std::cell::{Cell, RefCell}; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/com/github/melix99/telegrand/ui/message-list-view.ui")] + pub(crate) struct MessageListView { + pub(super) is_auto_scrolling: Cell, + pub(super) model: RefCell>, + pub(super) message_menu: OnceCell, + #[template_child] + pub(super) scroll_to_bottom_revealer: TemplateChild, + #[template_child] + pub(super) scrolled_window: TemplateChild, + #[template_child] + pub(super) list_view: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for MessageListView { + const NAME: &'static str = "MessageListView"; + type Type = super::MessageListView; + type ParentType = gtk::Widget; + + fn class_init(klass: &mut Self::Class) { + MessageListViewRow::static_type(); + klass.bind_template(); + klass.set_layout_manager_type::(); + klass.set_css_name("messagelistview"); + + klass.install_action( + "message-list-view.show-message-menu", + Some("dd"), + |widget, _, variant| { + let (x, y) = variant.and_then(|v| v.get()).unwrap(); + widget.show_message_menu(x, y); + }, + ); + klass.install_action( + "message-list-view.scroll-to-bottom", + None, + |widget, _, _| { + widget.scroll_to_bottom(); + }, + ); + klass.install_action_async( + "message-list-view.revoke-delete", + None, + |widget, _, _| async move { + widget.show_delete_dialog(true).await; + }, + ); + klass.install_action_async( + "message-list-view.delete", + None, + |widget, _, _| async move { + widget.show_delete_dialog(false).await; + }, + ); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for MessageListView { + fn constructed(&self) { + self.parent_constructed(); + + let obj = self.obj(); + + let adj = self.list_view.vadjustment().unwrap(); + adj.connect_value_changed(clone!(@weak obj => move |adj| { + let imp = obj.imp(); + + imp.is_auto_scrolling.set(adj.value() + adj.page_size() >= adj.upper()); + imp.scroll_to_bottom_revealer.set_reveal_child(!imp.is_auto_scrolling.get()); + + if adj.value() < adj.page_size() * 2.0 || adj.upper() <= adj.page_size() * 2.0 { + spawn(clone!(@weak obj => async move { + obj.load_older_messages().await; + })); + } + })); + + adj.connect_upper_notify(clone!(@weak obj => move |_| { + if obj.imp().is_auto_scrolling.get() { + obj.scroll_to_bottom(); + } + })); + } + + fn dispose(&self) { + self.dispose_template(); + } + } + + impl WidgetImpl for MessageListView { + // fn direction_changed(&self, previous_direction: gtk::TextDirection) { + // let obj = self.obj(); + + // if obj.direction() == previous_direction { + // return; + // } + + // if let Some(menu) = self.message_menu.get() { + // menu.set_halign(if obj.direction() == gtk::TextDirection::Rtl { + // gtk::Align::End + // } else { + // gtk::Align::Start + // }); + // } + // } + } +} + +glib::wrapper! { + pub(crate) struct MessageListView(ObjectSubclass) + @extends gtk::Widget; +} + +impl MessageListView { + pub(crate) fn load_messages(&self, chat: &Chat) { + let imp = self.imp(); + let model = MessageListViewModel::new(chat); + + // Request sponsored message, if needed + let list_view_model: gio::ListModel = if matches!(chat.type_(), ChatType::Supergroup(supergroup) if supergroup.is_channel()) + { + let list = gio::ListStore::new(gio::ListModel::static_type()); + + // We need to create a list here so that we can append the sponsored message + // to the chat history in the GtkListView using a GtkFlattenListModel + let sponsored_message_list = gio::ListStore::new(SponsoredMessage::static_type()); + list.append(&sponsored_message_list); + + let chat_id = chat.id(); + let session = chat.session(); + spawn(clone!(@weak self as obj => async move { + obj.request_sponsored_message(&session, chat_id, &sponsored_message_list).await; + })); + + list.append(&model); + + gtk::FlattenListModel::new(Some(list)).upcast() + } else { + model.clone().upcast() + }; + + let selection = gtk::NoSelection::new(Some(list_view_model)); + imp.list_view.set_model(Some(&selection)); + + spawn(clone!(@weak self as obj, @weak model => async move { + obj.load_initial_messages(&model).await; + })); + + imp.model.replace(Some(model)); + } + + async fn load_initial_messages(&self, model: &MessageListViewModel) { + while model.n_items() < MIN_N_ITEMS { + let limit = MIN_N_ITEMS - model.n_items(); + + match model.load_older_messages(limit as i32).await { + Ok(can_load_more) => { + if !can_load_more { + break; + } + } + Err(e) => { + log::warn!("Couldn't load initial history messages: {}", e); + break; + } + } + } + } + + async fn request_sponsored_message( + &self, + session: &Session, + chat_id: i64, + list: &gio::ListStore, + ) { + match SponsoredMessage::request(chat_id, &session).await { + Ok(sponsored_message) => { + if let Some(sponsored_message) = sponsored_message { + list.append(&sponsored_message); + } + } + Err(e) => { + if e.code != 404 { + log::warn!("Failed to request a sponsored message: {:?}", e); + } + } + } + } + + async fn load_older_messages(&self) { + if let Some(model) = self.imp().model.borrow().as_ref() { + if let Err(MessageListViewModelError::Tdlib(e)) = model.load_older_messages(20).await { + log::warn!("Couldn't load more chat messages: {e:?}"); + } + } + } + + fn show_message_menu(&self, x: f64, y: f64) { + let menu = self.imp().message_menu.get_or_init(|| { + let menu = + gtk::Builder::from_resource("/com/github/melix99/telegrand/ui/message-menu.ui") + .object::("menu") + .unwrap(); + + menu.set_halign(if self.direction() == gtk::TextDirection::Rtl { + gtk::Align::End + } else { + gtk::Align::Start + }); + menu.set_parent(self); + + menu + }); + + menu.set_pointing_to(Some(&gdk::Rectangle::new(x as i32, y as i32, 1, 1))); + menu.popup(); + } + + fn scroll_to_bottom(&self) { + let imp = self.imp(); + + imp.is_auto_scrolling.set(true); + imp.scrolled_window + .emit_by_name::("scroll-child", &[>k::ScrollType::End, &false]); + } + + async fn show_delete_dialog(&self, revoke: bool) { + let parent = self.root().and_downcast::().unwrap(); + let body = if revoke { + gettext("Do you want to delete this message for everyone?") + } else { + gettext("Do you want to delete this message?") + }; + let dialog = adw::MessageDialog::new( + Some(&parent), + Some(&gettext("Confirm Message Deletion")), + Some(&body), + ); + + dialog.set_body_use_markup(true); + dialog.add_responses(&[("no", &gettext("_No")), ("yes", &gettext("_Yes"))]); + dialog.set_default_response(Some("no")); + dialog.set_response_appearance("yes", adw::ResponseAppearance::Destructive); + + dialog.choose_future().await; + + // dialog.choose( + // gio::Cancellable::NONE, + // clone!(@weak self as obj => move |response| { + // if response == "yes" { + // if let Ok(message) = obj.message().downcast::() { + // spawn(async move { + // if let Err(e) = message.delete(revoke).await { + // log::warn!("Error deleting a message (revoke = {}): {:?}", revoke, e); + // } + // }); + // } + // } + // })); + } +} diff --git a/src/session/content/chat_history_model.rs b/src/components/message_list_view/model.rs similarity index 83% rename from src/session/content/chat_history_model.rs rename to src/components/message_list_view/model.rs index 984f4f9f9..c6cd6da8d 100644 --- a/src/session/content/chat_history_model.rs +++ b/src/components/message_list_view/model.rs @@ -5,11 +5,11 @@ use gtk::{gio, glib}; use std::cmp::Ordering; use thiserror::Error; -use crate::session::content::{ChatHistoryItem, ChatHistoryItemType}; +use super::{MessageListViewItem, MessageListViewItemType}; use crate::tdlib::{Chat, Message}; #[derive(Error, Debug)] -pub(crate) enum ChatHistoryError { +pub(crate) enum MessageListViewModelError { #[error("The chat history is already loading messages")] AlreadyLoading, #[error("TDLib error: {0:?}")] @@ -24,20 +24,20 @@ mod imp { use std::collections::VecDeque; #[derive(Debug, Default)] - pub(crate) struct ChatHistoryModel { + pub(crate) struct MessageListViewModel { pub(super) chat: WeakRef, pub(super) is_loading: Cell, - pub(super) list: RefCell>, + pub(super) list: RefCell>, } #[glib::object_subclass] - impl ObjectSubclass for ChatHistoryModel { - const NAME: &'static str = "ContentChatHistoryModel"; - type Type = super::ChatHistoryModel; + impl ObjectSubclass for MessageListViewModel { + const NAME: &'static str = "MessageListViewModel"; + type Type = super::MessageListViewModel; type Interfaces = (gio::ListModel,); } - impl ObjectImpl for ChatHistoryModel { + impl ObjectImpl for MessageListViewModel { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| { vec![glib::ParamSpecObject::builder::("chat") @@ -57,9 +57,9 @@ mod imp { } } - impl ListModelImpl for ChatHistoryModel { + impl ListModelImpl for MessageListViewModel { fn item_type(&self) -> glib::Type { - ChatHistoryItem::static_type() + MessageListViewItem::static_type() } fn n_items(&self) -> u32 { @@ -77,13 +77,13 @@ mod imp { } glib::wrapper! { - pub(crate) struct ChatHistoryModel(ObjectSubclass) + pub(crate) struct MessageListViewModel(ObjectSubclass) @implements gio::ListModel; } -impl ChatHistoryModel { +impl MessageListViewModel { pub(crate) fn new(chat: &Chat) -> Self { - let obj: ChatHistoryModel = glib::Object::new(); + let obj: MessageListViewModel = glib::Object::new(); obj.imp().chat.set(Some(chat)); @@ -100,11 +100,14 @@ impl ChatHistoryModel { /// Loads older messages from this chat history. /// /// Returns `true` when more messages can be loaded. - pub(crate) async fn load_older_messages(&self, limit: i32) -> Result { + pub(crate) async fn load_older_messages( + &self, + limit: i32, + ) -> Result { let imp = self.imp(); if imp.is_loading.get() { - return Err(ChatHistoryError::AlreadyLoading); + return Err(MessageListViewModelError::AlreadyLoading); } let oldest_message_id = imp @@ -122,7 +125,7 @@ impl ChatHistoryModel { imp.is_loading.set(false); - let messages = result.map_err(ChatHistoryError::Tdlib)?; + let messages = result.map_err(MessageListViewModelError::Tdlib)?; if messages.is_empty() { return Ok(false); @@ -147,7 +150,7 @@ impl ChatHistoryModel { } else { None }; - let mut dividers: Vec<(usize, ChatHistoryItem)> = vec![]; + let mut dividers: Vec<(usize, MessageListViewItem)> = vec![]; for (index, current) in list.range(position..position + added).enumerate().rev() { if let Some(current_timestamp) = current.message_timestamp() { @@ -156,7 +159,7 @@ impl ChatHistoryModel { let divider_pos = position + index + 1; dividers.push(( divider_pos, - ChatHistoryItem::for_day_divider(current_timestamp.clone()), + MessageListViewItem::for_day_divider(current_timestamp.clone()), )); previous_timestamp = Some(current_timestamp); } @@ -180,7 +183,7 @@ impl ChatHistoryModel { let position = position as usize; let item_before_removed = list.get(position); - if let Some(ChatHistoryItemType::DayDivider(_)) = + if let Some(MessageListViewItemType::DayDivider(_)) = item_before_removed.map(|i| i.type_()) { let item_after_removed = if position > 0 { @@ -190,7 +193,7 @@ impl ChatHistoryModel { }; match item_after_removed.map(|item| item.type_()) { - None | Some(ChatHistoryItemType::DayDivider(_)) => { + None | Some(MessageListViewItemType::DayDivider(_)) => { list.remove(position + removed); removed += 1; @@ -213,7 +216,7 @@ impl ChatHistoryModel { let last_added_timestamp = list.get(position).unwrap().message_timestamp().unwrap(); let next_item = list.get(position - 1); - if let Some(ChatHistoryItemType::DayDivider(date)) = + if let Some(MessageListViewItemType::DayDivider(date)) = next_item.map(|item| item.type_()) { if date.ymd() == last_added_timestamp.ymd() { @@ -236,7 +239,7 @@ impl ChatHistoryModel { self.imp() .list .borrow_mut() - .push_front(ChatHistoryItem::for_message(message)); + .push_front(MessageListViewItem::for_message(message)); self.items_changed(0, 0, 1); } @@ -250,7 +253,7 @@ impl ChatHistoryModel { for message in messages { imp.list .borrow_mut() - .push_back(ChatHistoryItem::for_message(message)); + .push_back(MessageListViewItem::for_message(message)); } let index = imp.list.borrow().len() - added; @@ -270,10 +273,10 @@ impl ChatHistoryModel { // can exploit this by applying a binary search. let index = list .binary_search_by(|m| match m.type_() { - ChatHistoryItemType::Message(other_message) => { + MessageListViewItemType::Message(other_message) => { message.id().cmp(&other_message.id()) } - ChatHistoryItemType::DayDivider(date_time) => { + MessageListViewItemType::DayDivider(date_time) => { let ordering = glib::DateTime::from_unix_utc(message.date() as i64) .unwrap() .cmp(date_time); diff --git a/src/session/content/chat_history_row.rs b/src/components/message_list_view/row.rs similarity index 82% rename from src/session/content/chat_history_row.rs rename to src/components/message_list_view/row.rs index 6e15092ee..9a2910cb1 100644 --- a/src/session/content/chat_history_row.rs +++ b/src/components/message_list_view/row.rs @@ -6,7 +6,7 @@ use gtk::prelude::*; use gtk::subclass::prelude::*; use tdlib::enums::MessageContent; -use crate::session::content::{ChatHistoryItem, ChatHistoryItemType, EventRow, MessageRow}; +use super::{MessageListViewEventRow, MessageListViewItem, MessageListViewItemType, MessageRow}; use crate::strings; use crate::tdlib::SponsoredMessage; @@ -16,19 +16,19 @@ mod imp { use std::cell::RefCell; #[derive(Debug, Default)] - pub(crate) struct ChatHistoryRow { + pub(crate) struct MessageListViewRow { /// An `ChatHistoryItem` or `SponsoredMessage` pub(super) item: RefCell>, } #[glib::object_subclass] - impl ObjectSubclass for ChatHistoryRow { - const NAME: &'static str = "ContentChatHistoryRow"; - type Type = super::ChatHistoryRow; + impl ObjectSubclass for MessageListViewRow { + const NAME: &'static str = "MessageListViewRow"; + type Type = super::MessageListViewRow; type ParentType = adw::Bin; } - impl ObjectImpl for ChatHistoryRow { + impl ObjectImpl for MessageListViewRow { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| { vec![glib::ParamSpecObject::builder::("item") @@ -57,22 +57,22 @@ mod imp { } } - impl WidgetImpl for ChatHistoryRow {} - impl BinImpl for ChatHistoryRow {} + impl WidgetImpl for MessageListViewRow {} + impl BinImpl for MessageListViewRow {} } glib::wrapper! { - pub(crate) struct ChatHistoryRow(ObjectSubclass) + pub(crate) struct MessageListViewRow(ObjectSubclass) @extends gtk::Widget, adw::Bin; } -impl Default for ChatHistoryRow { +impl Default for MessageListViewRow { fn default() -> Self { Self::new() } } -impl ChatHistoryRow { +impl MessageListViewRow { pub(crate) fn new() -> Self { glib::Object::new() } @@ -87,9 +87,9 @@ impl ChatHistoryRow { } if let Some(ref item) = item { - if let Some(item) = item.downcast_ref::() { + if let Some(item) = item.downcast_ref::() { match item.type_() { - ChatHistoryItemType::Message(message) => { + MessageListViewItemType::Message(message) => { use tdlib::enums::MessageContent::*; match message.content().0 { @@ -115,7 +115,7 @@ impl ChatHistoryRow { _ => self.update_or_create_message_row(message.to_owned().upcast()), } } - ChatHistoryItemType::DayDivider(date) => { + MessageListViewItemType::DayDivider(date) => { let fmt = if date.year() == glib::DateTime::now_local().unwrap().year() { // Translators: This is a date format in the day divider without the year gettext("%B %e") @@ -155,11 +155,14 @@ impl ChatHistoryRow { } } - fn get_or_create_event_row(&self) -> EventRow { - if let Some(Ok(child)) = self.child().map(|w| w.downcast::()) { + fn get_or_create_event_row(&self) -> MessageListViewEventRow { + if let Some(Ok(child)) = self + .child() + .map(|w| w.downcast::()) + { child } else { - let child = EventRow::new(); + let child = MessageListViewEventRow::new(); self.set_child(Some(&child)); child } diff --git a/src/components/mod.rs b/src/components/mod.rs index f5efdd186..12dda1fcf 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,9 +1,11 @@ mod avatar; mod message_entry; +mod message_list_view; mod snow; mod sticker; pub(crate) use self::avatar::Avatar; pub(crate) use self::message_entry::MessageEntry; +pub(crate) use self::message_list_view::MessageListView; pub(crate) use self::snow::Snow; pub(crate) use self::sticker::Sticker; diff --git a/src/session/content/chat_history.rs b/src/session/content/chat_history.rs index f381da914..f7f253c9c 100644 --- a/src/session/content/chat_history.rs +++ b/src/session/content/chat_history.rs @@ -1,25 +1,19 @@ use adw::prelude::*; use gettextrs::gettext; -use glib::clone; use gtk::subclass::prelude::*; -use gtk::{gio, glib, CompositeTemplate}; +use gtk::{glib, CompositeTemplate}; use tdlib::enums::ChatMemberStatus; use tdlib::functions; -use crate::session::content::{ - ChatActionBar, ChatHistoryError, ChatHistoryModel, ChatHistoryRow, ChatInfoWindow, -}; -use crate::tdlib::{Chat, ChatType, SponsoredMessage}; -use crate::utils::spawn; -use crate::{expressions, Session}; - -const MIN_N_ITEMS: u32 = 20; +use super::{ChatActionBar, ChatInfoWindow}; +use crate::components::MessageListView; +use crate::expressions; +use crate::tdlib::{Chat, ChatType}; mod imp { use super::*; use adw::subclass::prelude::BinImpl; use once_cell::sync::Lazy; - use once_cell::unsync::OnceCell; use std::cell::{Cell, RefCell}; #[derive(Debug, Default, CompositeTemplate)] @@ -27,16 +21,10 @@ mod imp { pub(crate) struct ChatHistory { pub(super) compact: Cell, pub(super) chat: RefCell>, - pub(super) model: RefCell>, - pub(super) message_menu: OnceCell, - pub(super) is_auto_scrolling: Cell, - pub(super) sticky: Cell, #[template_child] pub(super) window_title: TemplateChild, #[template_child] - pub(super) scrolled_window: TemplateChild, - #[template_child] - pub(super) list_view: TemplateChild, + pub(super) message_list_view: TemplateChild, #[template_child] pub(super) chat_action_bar: TemplateChild, } @@ -48,15 +36,11 @@ mod imp { type ParentType = adw::Bin; fn class_init(klass: &mut Self::Class) { - ChatHistoryRow::static_type(); klass.bind_template(); klass.install_action("chat-history.view-info", None, move |widget, _, _| { widget.open_info_dialog(); }); - klass.install_action("chat-history.scroll-down", None, move |widget, _, _| { - widget.scroll_down(); - }); klass.install_action( "chat-history.reply", Some("x"), @@ -91,9 +75,6 @@ mod imp { glib::ParamSpecObject::builder::("chat") .explicit_notify() .build(), - glib::ParamSpecBoolean::builder("sticky") - .read_only() - .build(), ] }); PROPERTIES.as_ref() @@ -111,7 +92,6 @@ mod imp { let chat = value.get().unwrap(); obj.set_chat(chat); } - "sticky" => obj.set_sticky(value.get().unwrap()), _ => unimplemented!(), } } @@ -122,59 +102,17 @@ mod imp { match pspec.name() { "compact" => self.compact.get().to_value(), "chat" => obj.chat().to_value(), - "sticky" => obj.sticky().to_value(), _ => unimplemented!(), } } fn constructed(&self) { self.parent_constructed(); - - let obj = self.obj(); - - obj.setup_expressions(); - - let adj = self.list_view.vadjustment().unwrap(); - adj.connect_value_changed(clone!(@weak obj => move |adj| { - let imp = obj.imp(); - - if imp.is_auto_scrolling.get() { - if adj.value() + adj.page_size() >= adj.upper() { - imp.is_auto_scrolling.set(false); - obj.set_sticky(true); - } - } else { - obj.set_sticky(adj.value() + adj.page_size() >= adj.upper()); - obj.load_older_messages(adj); - } - })); - - adj.connect_upper_notify(clone!(@weak obj => move |_| { - if obj.sticky() || obj.imp().is_auto_scrolling.get() { - obj.scroll_down(); - } - })); - } - } - - impl WidgetImpl for ChatHistory { - fn direction_changed(&self, previous_direction: gtk::TextDirection) { - let obj = self.obj(); - - if obj.direction() == previous_direction { - return; - } - - if let Some(menu) = self.message_menu.get() { - menu.set_halign(if obj.direction() == gtk::TextDirection::Rtl { - gtk::Align::End - } else { - gtk::Align::Start - }); - } + self.obj().setup_expressions(); } } + impl WidgetImpl for ChatHistory {} impl BinImpl for ChatHistory {} } @@ -205,18 +143,6 @@ impl ChatHistory { ); } - fn load_older_messages(&self, adj: >k::Adjustment) { - if adj.value() < adj.page_size() * 2.0 || adj.upper() <= adj.page_size() * 2.0 { - if let Some(model) = self.imp().model.borrow().as_ref() { - spawn(clone!(@weak model => async move { - if let Err(ChatHistoryError::Tdlib(e)) = model.load_older_messages(20).await { - log::warn!("Couldn't load more chat messages: {:?}", e); - } - })); - } - } - } - fn open_info_dialog(&self) { if let Some(chat) = self.chat() { ChatInfoWindow::new(&self.parent_window(), &chat).present(); @@ -251,40 +177,6 @@ impl ChatHistory { self.root()?.downcast().ok() } - fn request_sponsored_message(&self, session: &Session, chat_id: i64, list: &gio::ListStore) { - spawn(clone!(@weak session, @weak list => async move { - match SponsoredMessage::request(chat_id, &session).await { - Ok(sponsored_message) => { - if let Some(sponsored_message) = sponsored_message { - list.append(&sponsored_message); - } - } - Err(e) => { - if e.code != 404 { - log::warn!("Failed to request a SponsoredMessage: {:?}", e); - } - } - } - })); - } - - pub(crate) fn message_menu(&self) -> >k::PopoverMenu { - self.imp().message_menu.get_or_init(|| { - let menu = - gtk::Builder::from_resource("/com/github/melix99/telegrand/ui/message-menu.ui") - .object::("menu") - .unwrap(); - - menu.set_halign(if self.direction() == gtk::TextDirection::Rtl { - gtk::Align::End - } else { - gtk::Align::Start - }); - - menu - }) - } - pub(crate) fn handle_paste_action(&self) { self.imp().chat_action_bar.handle_paste_action(); } @@ -310,70 +202,10 @@ impl ChatHistory { }, ); - let model = ChatHistoryModel::new(chat); - - // Request sponsored message, if needed - let list_view_model: gio::ListModel = if matches!(chat.type_(), ChatType::Supergroup(supergroup) if supergroup.is_channel()) - { - let list = gio::ListStore::new(gio::ListModel::static_type()); - - // We need to create a list here so that we can append the sponsored message - // to the chat history in the GtkListView using a GtkFlattenListModel - let sponsored_message_list = gio::ListStore::new(SponsoredMessage::static_type()); - list.append(&sponsored_message_list); - self.request_sponsored_message(&chat.session(), chat.id(), &sponsored_message_list); - - list.append(&model); - - gtk::FlattenListModel::new(Some(list)).upcast() - } else { - model.clone().upcast() - }; - - spawn(clone!(@weak model => async move { - while model.n_items() < MIN_N_ITEMS { - let limit = MIN_N_ITEMS - model.n_items(); - match model.load_older_messages(limit as i32).await { - Ok(can_load_more) => if !can_load_more { - break; - } - Err(e) => { - log::warn!("Couldn't load initial history messages: {}", e); - break; - } - } - } - })); - - let selection = gtk::NoSelection::new(Some(list_view_model)); - imp.list_view.set_model(Some(&selection)); - - imp.model.replace(Some(model)); + imp.message_list_view.load_messages(chat); } imp.chat.replace(chat); self.notify("chat"); } - - pub(crate) fn sticky(&self) -> bool { - self.imp().sticky.get() - } - - fn set_sticky(&self, sticky: bool) { - if self.sticky() == sticky { - return; - } - - self.imp().sticky.set(sticky); - self.notify("sticky"); - } - - fn scroll_down(&self) { - let imp = self.imp(); - - imp.is_auto_scrolling.set(true); - - imp.scrolled_window - .emit_by_name::("scroll-child", &[>k::ScrollType::End, &false]); - } } diff --git a/src/session/content/mod.rs b/src/session/content/mod.rs index e0904b1fe..550282e7d 100644 --- a/src/session/content/mod.rs +++ b/src/session/content/mod.rs @@ -1,21 +1,11 @@ mod chat_action_bar; mod chat_history; -mod chat_history_item; -mod chat_history_model; -mod chat_history_row; mod chat_info_window; -mod event_row; -mod message_row; mod send_photo_dialog; use self::chat_action_bar::ChatActionBar; use self::chat_history::ChatHistory; -use self::chat_history_item::{ChatHistoryItem, ChatHistoryItemType}; -use self::chat_history_model::{ChatHistoryError, ChatHistoryModel}; -use self::chat_history_row::ChatHistoryRow; use self::chat_info_window::ChatInfoWindow; -use self::event_row::EventRow; -use self::message_row::MessageRow; use self::send_photo_dialog::SendPhotoDialog; use gtk::glib; From 3b2cbed638882d95e85933b1ec2c913634947a4a Mon Sep 17 00:00:00 2001 From: Marco Melorio Date: Wed, 29 Mar 2023 18:47:18 +0200 Subject: [PATCH 2/3] session: Use correct prefix for back action "content" -> "session" --- data/resources/ui/content-chat-history.ui | 6 +++--- src/session/mod.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/data/resources/ui/content-chat-history.ui b/data/resources/ui/content-chat-history.ui index bb1f37096..14c72f8d9 100644 --- a/data/resources/ui/content-chat-history.ui +++ b/data/resources/ui/content-chat-history.ui @@ -10,9 +10,9 @@ - - go-previous-symbolic - content.go-back + + go-previous-symbolic + session.go-back diff --git a/src/session/mod.rs b/src/session/mod.rs index d1eac5652..3ae934ac3 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -72,7 +72,7 @@ mod imp { fn class_init(klass: &mut Self::Class) { klass.bind_template(); - klass.install_action("content.go-back", None, move |widget, _, _| { + klass.install_action("session.go-back", None, move |widget, _, _| { widget .imp() .leaflet From ffa58a2a735461c06a232109e5b77dc71ff360e7 Mon Sep 17 00:00:00 2001 From: Marco Melorio Date: Tue, 14 Mar 2023 00:56:54 +0100 Subject: [PATCH 3/3] WIP: content: Add pinned messages bar and view --- data/resources/ui/content-chat-history.ui | 3 + data/resources/ui/content.blp | 10 ++- src/components/message_list_view/mod.rs | 11 ++- src/components/message_list_view/model.rs | 23 ++++- src/components/mod.rs | 2 +- src/session/content/chat_history.rs | 9 +- src/session/content/mod.rs | 55 +++++++++++- src/session/content/pinned_messages_bar.rs | 66 ++++++++++++++ src/session/content/pinned_messages_view.rs | 96 +++++++++++++++++++++ src/tdlib/chat.rs | 39 ++++++++- 10 files changed, 299 insertions(+), 15 deletions(-) create mode 100644 src/session/content/pinned_messages_bar.rs create mode 100644 src/session/content/pinned_messages_view.rs diff --git a/data/resources/ui/content-chat-history.ui b/data/resources/ui/content-chat-history.ui index 14c72f8d9..1140d8bc2 100644 --- a/data/resources/ui/content-chat-history.ui +++ b/data/resources/ui/content-chat-history.ui @@ -33,6 +33,9 @@ + + + diff --git a/data/resources/ui/content.blp b/data/resources/ui/content.blp index d8be4ad74..0efd9cb48 100644 --- a/data/resources/ui/content.blp +++ b/data/resources/ui/content.blp @@ -32,9 +32,13 @@ template Content : Adw.Bin { }; } - .ContentChatHistory chat_history { - compact: bind Content.compact; - chat: bind Content.chat; + Adw.Leaflet chat_leaflet { + can-unfold: false; + + .ContentChatHistory chat_history { + compact: bind Content.compact; + chat: bind Content.chat; + } } } } diff --git a/src/components/message_list_view/mod.rs b/src/components/message_list_view/mod.rs index d66f44f64..681e728df 100644 --- a/src/components/message_list_view/mod.rs +++ b/src/components/message_list_view/mod.rs @@ -22,6 +22,13 @@ use crate::Session; const MIN_N_ITEMS: u32 = 20; +#[derive(Debug, Default, Clone, Copy)] +pub(crate) enum MessageListViewType { + #[default] + ChatHistory, + PinnedMessages, +} + mod imp { use super::*; use once_cell::unsync::OnceCell; @@ -146,9 +153,9 @@ glib::wrapper! { } impl MessageListView { - pub(crate) fn load_messages(&self, chat: &Chat) { + pub(crate) fn load_messages(&self, type_: MessageListViewType, chat: &Chat) { let imp = self.imp(); - let model = MessageListViewModel::new(chat); + let model = MessageListViewModel::new(type_, chat); // Request sponsored message, if needed let list_view_model: gio::ListModel = if matches!(chat.type_(), ChatType::Supergroup(supergroup) if supergroup.is_channel()) diff --git a/src/components/message_list_view/model.rs b/src/components/message_list_view/model.rs index c6cd6da8d..1c4407fe1 100644 --- a/src/components/message_list_view/model.rs +++ b/src/components/message_list_view/model.rs @@ -3,9 +3,10 @@ use gtk::prelude::*; use gtk::subclass::prelude::*; use gtk::{gio, glib}; use std::cmp::Ordering; +use tdlib::enums::SearchMessagesFilter; use thiserror::Error; -use super::{MessageListViewItem, MessageListViewItemType}; +use super::{MessageListViewItem, MessageListViewItemType, MessageListViewType}; use crate::tdlib::{Chat, Message}; #[derive(Error, Debug)] @@ -25,6 +26,7 @@ mod imp { #[derive(Debug, Default)] pub(crate) struct MessageListViewModel { + pub(super) type_: Cell, pub(super) chat: WeakRef, pub(super) is_loading: Cell, pub(super) list: RefCell>, @@ -82,9 +84,10 @@ glib::wrapper! { } impl MessageListViewModel { - pub(crate) fn new(chat: &Chat) -> Self { + pub(crate) fn new(type_: MessageListViewType, chat: &Chat) -> Self { let obj: MessageListViewModel = glib::Object::new(); + obj.imp().type_.set(type_); obj.imp().chat.set(Some(chat)); chat.connect_new_message(clone!(@weak obj => move |_, message| { @@ -121,7 +124,21 @@ impl MessageListViewModel { imp.is_loading.set(true); - let result = self.chat().get_chat_history(oldest_message_id, limit).await; + let result = match imp.type_.get() { + MessageListViewType::ChatHistory => { + self.chat().get_chat_history(oldest_message_id, limit).await + } + MessageListViewType::PinnedMessages => { + self.chat() + .search_messages( + String::new(), + oldest_message_id, + limit, + Some(SearchMessagesFilter::Pinned), + ) + .await + } + }; imp.is_loading.set(false); diff --git a/src/components/mod.rs b/src/components/mod.rs index 12dda1fcf..1b2fadded 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -6,6 +6,6 @@ mod sticker; pub(crate) use self::avatar::Avatar; pub(crate) use self::message_entry::MessageEntry; -pub(crate) use self::message_list_view::MessageListView; +pub(crate) use self::message_list_view::{MessageListView, MessageListViewType}; pub(crate) use self::snow::Snow; pub(crate) use self::sticker::Sticker; diff --git a/src/session/content/chat_history.rs b/src/session/content/chat_history.rs index f7f253c9c..230088419 100644 --- a/src/session/content/chat_history.rs +++ b/src/session/content/chat_history.rs @@ -5,8 +5,8 @@ use gtk::{glib, CompositeTemplate}; use tdlib::enums::ChatMemberStatus; use tdlib::functions; -use super::{ChatActionBar, ChatInfoWindow}; -use crate::components::MessageListView; +use super::{ChatActionBar, ChatInfoWindow, PinnedMessagesBar}; +use crate::components::{MessageListView, MessageListViewType}; use crate::expressions; use crate::tdlib::{Chat, ChatType}; @@ -24,6 +24,8 @@ mod imp { #[template_child] pub(super) window_title: TemplateChild, #[template_child] + pub(super) pinned_messages_bar: TemplateChild, + #[template_child] pub(super) message_list_view: TemplateChild, #[template_child] pub(super) chat_action_bar: TemplateChild, @@ -202,7 +204,8 @@ impl ChatHistory { }, ); - imp.message_list_view.load_messages(chat); + imp.message_list_view + .load_messages(MessageListViewType::ChatHistory, chat); } imp.chat.replace(chat); diff --git a/src/session/content/mod.rs b/src/session/content/mod.rs index 550282e7d..162ad35c3 100644 --- a/src/session/content/mod.rs +++ b/src/session/content/mod.rs @@ -1,11 +1,15 @@ mod chat_action_bar; mod chat_history; mod chat_info_window; +mod pinned_messages_bar; +mod pinned_messages_view; mod send_photo_dialog; use self::chat_action_bar::ChatActionBar; use self::chat_history::ChatHistory; use self::chat_info_window::ChatInfoWindow; +use self::pinned_messages_bar::PinnedMessagesBar; +use self::pinned_messages_view::PinnedMessagesView; use self::send_photo_dialog::SendPhotoDialog; use gtk::glib; @@ -31,6 +35,8 @@ mod imp { #[template_child] pub(super) unselected_chat_view: TemplateChild, #[template_child] + pub(super) chat_leaflet: TemplateChild, + #[template_child] pub(super) chat_history: TemplateChild, } @@ -41,8 +47,14 @@ mod imp { type ParentType = adw::Bin; fn class_init(klass: &mut Self::Class) { - ChatHistory::static_type(); klass.bind_template(); + + klass.install_action("content.go-back", None, move |widget, _, _| { + widget.go_back(); + }); + klass.install_action("content.show-pinned-messages", None, move |widget, _, _| { + widget.show_pinned_messages(); + }); } fn instance_init(obj: &glib::subclass::InitializingObject) { @@ -114,6 +126,36 @@ impl Content { self.imp().chat_history.handle_paste_action(); } + fn go_back(&self) { + self.imp() + .chat_leaflet + .navigate(adw::NavigationDirection::Back); + } + + fn show_pinned_messages(&self) { + if let Some(chat) = self.chat() { + let imp = self.imp(); + + let next_child = imp + .chat_leaflet + .adjacent_child(adw::NavigationDirection::Forward); + let cached = if let Some(pinned_messages_view) = + next_child.and_downcast::() + { + pinned_messages_view.chat() == chat + } else { + false + }; + + if !cached { + let pinned_messages = PinnedMessagesView::new(&chat); + imp.chat_leaflet.append(&pinned_messages); + } + + imp.chat_leaflet.navigate(adw::NavigationDirection::Forward); + } + } + pub(crate) fn chat(&self) -> Option { self.imp().chat.borrow().clone() } @@ -125,7 +167,16 @@ impl Content { let imp = self.imp(); if chat.is_some() { - imp.stack.set_visible_child(&imp.chat_history.get()); + // Remove every leaflet page except the first one (the first chat history) + imp.chat_leaflet + .pages() + .iter::() + .map(|p| p.unwrap()) + .enumerate() + .filter(|(i, _)| i > &0) + .for_each(|(_, p)| imp.chat_leaflet.remove(&p.child())); + + imp.stack.set_visible_child(&imp.chat_leaflet.get()); } else { imp.stack.set_visible_child(&imp.unselected_chat_view.get()); } diff --git a/src/session/content/pinned_messages_bar.rs b/src/session/content/pinned_messages_bar.rs new file mode 100644 index 000000000..27322ab11 --- /dev/null +++ b/src/session/content/pinned_messages_bar.rs @@ -0,0 +1,66 @@ +use gtk::subclass::prelude::*; +use gtk::{glib, CompositeTemplate}; + +mod imp { + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(string = r#" + template PinnedMessagesBar { + Box content_box { + styles ["toolbar"] + + Box { + orientation: vertical; + hexpand: true; + + Inscription { + + } + + Inscription { + + } + } + + Button { + icon-name: "view-list-symbolic"; + action-name: "content.show-pinned-messages"; + } + } + } + "#)] + pub(crate) struct PinnedMessagesBar { + #[template_child] + pub(super) content_box: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for PinnedMessagesBar { + const NAME: &'static str = "PinnedMessagesBar"; + type Type = super::PinnedMessagesBar; + type ParentType = gtk::Widget; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + klass.set_layout_manager_type::(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for PinnedMessagesBar { + fn dispose(&self) { + self.dispose_template(); + } + } + + impl WidgetImpl for PinnedMessagesBar {} +} + +glib::wrapper! { + pub(crate) struct PinnedMessagesBar(ObjectSubclass) + @extends gtk::Widget; +} diff --git a/src/session/content/pinned_messages_view.rs b/src/session/content/pinned_messages_view.rs new file mode 100644 index 000000000..866deb24d --- /dev/null +++ b/src/session/content/pinned_messages_view.rs @@ -0,0 +1,96 @@ +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::{glib, CompositeTemplate}; + +use crate::components::{MessageListView, MessageListViewType}; +use crate::tdlib::Chat; + +mod imp { + use super::*; + use glib::Properties; + use once_cell::unsync::OnceCell; + + #[derive(Debug, Default, Properties, CompositeTemplate)] + #[properties(wrapper_type = super::PinnedMessagesView)] + #[template(string = r#" + using Adw 1; + + template PinnedMessagesView { + Adw.ToolbarView toolbar_view { + [top] + HeaderBar { + [start] + Button { + icon-name: "go-previous-symbolic"; + action-name: "content.go-back"; + } + } + + content: .MessageListView message_list_view {}; + } + } + "#)] + pub(crate) struct PinnedMessagesView { + #[property(get, set, construct_only)] + pub(super) chat: OnceCell, + #[template_child] + pub(super) toolbar_view: TemplateChild, + #[template_child] + pub(super) message_list_view: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for PinnedMessagesView { + const NAME: &'static str = "PinnedMessagesView"; + type Type = super::PinnedMessagesView; + type ParentType = gtk::Widget; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + klass.set_layout_manager_type::(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for PinnedMessagesView { + fn properties() -> &'static [glib::ParamSpec] { + Self::derived_properties() + } + + fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { + self.derived_set_property(id, value, pspec) + } + + fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { + self.derived_property(id, pspec) + } + + fn constructed(&self) { + self.parent_constructed(); + + let chat = self.chat.get().unwrap(); + self.message_list_view + .load_messages(MessageListViewType::PinnedMessages, chat); + } + + fn dispose(&self) { + self.dispose_template(); + } + } + + impl WidgetImpl for PinnedMessagesView {} +} + +glib::wrapper! { + pub(crate) struct PinnedMessagesView(ObjectSubclass) + @extends gtk::Widget; +} + +impl PinnedMessagesView { + pub(crate) fn new(chat: &Chat) -> Self { + glib::Object::builder().property("chat", chat).build() + } +} diff --git a/src/tdlib/chat.rs b/src/tdlib/chat.rs index 41a054dcd..ed7b2e72b 100644 --- a/src/tdlib/chat.rs +++ b/src/tdlib/chat.rs @@ -1,7 +1,7 @@ use gtk::glib; use gtk::prelude::*; use gtk::subclass::prelude::*; -use tdlib::enums::{ChatType as TdChatType, Update}; +use tdlib::enums::{ChatType as TdChatType, SearchMessagesFilter, Update}; use tdlib::types::Chat as TelegramChat; use tdlib::{functions, types}; @@ -571,6 +571,43 @@ impl Chat { Ok(loaded_messages) } + pub(crate) async fn search_messages( + &self, + query: String, + from_message_id: i64, + limit: i32, + filter: Option, + ) -> Result, types::Error> { + let client_id = self.session().client_id(); + let result = functions::search_chat_messages( + self.id(), + query, + None, + from_message_id, + 0, + limit, + filter, + 0, + client_id, + ) + .await; + + let tdlib::enums::FoundChatMessages::FoundChatMessages(data) = result?; + + let mut messages = self.imp().messages.borrow_mut(); + let loaded_messages: Vec = data + .messages + .into_iter() + .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(