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..1140d8bc2 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 @@ -33,97 +33,11 @@ + + + - - - - 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/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/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..681e728df --- /dev/null +++ b/src/components/message_list_view/mod.rs @@ -0,0 +1,302 @@ +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; + +#[derive(Debug, Default, Clone, Copy)] +pub(crate) enum MessageListViewType { + #[default] + ChatHistory, + PinnedMessages, +} + +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, type_: MessageListViewType, chat: &Chat) { + let imp = self.imp(); + 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()) + { + 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 76% rename from src/session/content/chat_history_model.rs rename to src/components/message_list_view/model.rs index 984f4f9f9..1c4407fe1 100644 --- a/src/session/content/chat_history_model.rs +++ b/src/components/message_list_view/model.rs @@ -3,13 +3,14 @@ use gtk::prelude::*; use gtk::subclass::prelude::*; use gtk::{gio, glib}; use std::cmp::Ordering; +use tdlib::enums::SearchMessagesFilter; use thiserror::Error; -use crate::session::content::{ChatHistoryItem, ChatHistoryItemType}; +use super::{MessageListViewItem, MessageListViewItemType, MessageListViewType}; 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 +25,21 @@ mod imp { use std::collections::VecDeque; #[derive(Debug, Default)] - pub(crate) struct ChatHistoryModel { + pub(crate) struct MessageListViewModel { + pub(super) type_: Cell, 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 +59,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,14 +79,15 @@ mod imp { } glib::wrapper! { - pub(crate) struct ChatHistoryModel(ObjectSubclass) + pub(crate) struct MessageListViewModel(ObjectSubclass) @implements gio::ListModel; } -impl ChatHistoryModel { - pub(crate) fn new(chat: &Chat) -> Self { - let obj: ChatHistoryModel = glib::Object::new(); +impl MessageListViewModel { + 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| { @@ -100,11 +103,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 @@ -118,11 +124,25 @@ impl ChatHistoryModel { 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); - let messages = result.map_err(ChatHistoryError::Tdlib)?; + let messages = result.map_err(MessageListViewModelError::Tdlib)?; if messages.is_empty() { return Ok(false); @@ -147,7 +167,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 +176,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 +200,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 +210,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 +233,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 +256,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 +270,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 +290,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..1b2fadded 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, 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 f381da914..230088419 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, PinnedMessagesBar}; +use crate::components::{MessageListView, MessageListViewType}; +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,12 @@ 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, + pub(super) pinned_messages_bar: TemplateChild, #[template_child] - pub(super) list_view: TemplateChild, + pub(super) message_list_view: TemplateChild, #[template_child] pub(super) chat_action_bar: TemplateChild, } @@ -48,15 +38,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 +77,6 @@ mod imp { glib::ParamSpecObject::builder::("chat") .explicit_notify() .build(), - glib::ParamSpecBoolean::builder("sticky") - .read_only() - .build(), ] }); PROPERTIES.as_ref() @@ -111,7 +94,6 @@ mod imp { let chat = value.get().unwrap(); obj.set_chat(chat); } - "sticky" => obj.set_sticky(value.get().unwrap()), _ => unimplemented!(), } } @@ -122,59 +104,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 +145,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 +179,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 +204,11 @@ 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(MessageListViewType::ChatHistory, 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..162ad35c3 100644 --- a/src/session/content/mod.rs +++ b/src/session/content/mod.rs @@ -1,21 +1,15 @@ 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 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_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::pinned_messages_bar::PinnedMessagesBar; +use self::pinned_messages_view::PinnedMessagesView; use self::send_photo_dialog::SendPhotoDialog; use gtk::glib; @@ -41,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, } @@ -51,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) { @@ -124,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() } @@ -135,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/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 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(