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 @@
@@ -33,97 +33,11 @@
+
+
+
-
+
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 @@
+
+
+
+
+
+
+
+ slide-up
+ end
+ end
+
+
+
+
+ center
+ start
+ middle
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ center
+ end
+ go-down-symbolic
+ message-list-view.scroll-to-bottom
+
+ Scroll to bottom
+
+
+
+
+
+
+
+
+
+
+ never
+
+
+ 800
+ 600
+ natural
+
+
+ True
+
+
+
+
+
+
+ False
+
+
+
+ GtkListItem
+
+
+
+
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
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