Skip to content

Commit 9e92459

Browse files
authored
Merge pull request #246 from just-carlod/reply-to-msg
message-row: Introduce MessageReply
2 parents 83ed95f + dd81eee commit 9e92459

File tree

8 files changed

+317
-6
lines changed

8 files changed

+317
-6
lines changed

data/resources/style.css

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ messagebubble.outgoing:dir(ltr), messagebubble:not(.outgoing):dir(rtl) {
152152
margin-left: 38px;
153153
}
154154

155-
messagebubble.outgoing link {
155+
messagebubble.outgoing link,
156+
messagebubble.outgoing messagereply label.message {
156157
color: @accent_fg_color;
157158
}
158159

@@ -233,6 +234,33 @@ messageindicators,
233234
font-feature-settings: "tnum";
234235
}
235236

237+
messagebubble messagereply {
238+
margin: 3px 0;
239+
}
240+
241+
messagesticker messagereply {
242+
background: alpha(currentColor, 0.08);
243+
border-radius: 6px;
244+
padding: 3px 6px;
245+
}
246+
247+
messagereply {
248+
border-spacing: 6px;
249+
}
250+
251+
messagereply separator {
252+
background-color: currentColor;
253+
}
254+
255+
/* Do not use the sender color */
256+
messagereply label.message {
257+
color: @window_fg_color;
258+
}
259+
260+
messagesticker {
261+
border-spacing: 6px;
262+
}
263+
236264
.event-row {
237265
font-size: smaller;
238266
font-weight: bold;

data/resources/ui/content-message-sticker.blp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
using Gtk 4.0;
22

33
template MessageSticker : .MessageBase {
4-
layout-manager: BinLayout {};
4+
layout-manager: BoxLayout {};
55

6-
Overlay {
6+
Overlay sticker_overlay {
77
.MessageStickerPicture picture {}
88

99
[overlay]

src/session/content/message_row/bubble.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use gtk::{glib, CompositeTemplate};
44
use std::collections::hash_map::DefaultHasher;
55
use std::hash::{Hash, Hasher};
66

7-
use crate::session::content::message_row::{MessageIndicators, MessageLabel};
7+
use crate::session::content::message_row::{MessageIndicators, MessageLabel, MessageReply};
88
use crate::tdlib::{Chat, ChatType, Message, MessageSender, SponsoredMessage};
99

1010
const MAX_WIDTH: i32 = 400;
@@ -40,6 +40,8 @@ mod imp {
4040
visible: false;
4141
}
4242
43+
Adw.Bin message_reply_bin {}
44+
4345
Adw.Bin prefix_bin {}
4446
4547
.MessageLabel message_label {
@@ -63,6 +65,8 @@ mod imp {
6365
#[template_child]
6466
pub(super) sender_label: TemplateChild<gtk::Label>,
6567
#[template_child]
68+
pub(super) message_reply_bin: TemplateChild<adw::Bin>,
69+
#[template_child]
6670
pub(super) prefix_bin: TemplateChild<adw::Bin>,
6771
#[template_child]
6872
pub(super) message_label: TemplateChild<MessageLabel>,
@@ -193,6 +197,20 @@ impl MessageBubble {
193197
None
194198
};
195199

200+
// Handle MessageReply
201+
if message.reply_to_message_id() != 0 {
202+
let reply = MessageReply::new(
203+
message.chat(),
204+
message.reply_to_message_id(),
205+
message.is_outgoing(),
206+
);
207+
208+
// FIXME: Do not show message reply when message is being deleted
209+
imp.message_reply_bin.set_child(Some(&reply));
210+
} else {
211+
imp.message_reply_bin.set_child(gtk::Widget::NONE);
212+
}
213+
196214
// Show sender label, if needed
197215
if let Some(maybe_id) = show_sender {
198216
let sender_name_expression = message.sender_display_name_expression();

src/session/content/message_row/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod indicators;
55
mod label;
66
mod media_picture;
77
mod photo;
8+
mod reply;
89
mod sticker;
910
mod sticker_picture;
1011
mod text;
@@ -17,6 +18,7 @@ use self::indicators::MessageIndicators;
1718
use self::label::MessageLabel;
1819
use self::media_picture::MediaPicture;
1920
use self::photo::MessagePhoto;
21+
use self::reply::MessageReply;
2022
use self::sticker::MessageSticker;
2123
use self::sticker_picture::StickerPicture;
2224
use self::text::MessageText;
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
use gtk::glib::{self, clone};
2+
use gtk::prelude::*;
3+
use gtk::subclass::prelude::*;
4+
use gtk::CompositeTemplate;
5+
6+
use crate::strings;
7+
use crate::tdlib::{Chat, ChatType, Message, MessageSender};
8+
use crate::utils::spawn;
9+
10+
mod imp {
11+
use gtk::glib::{ParamSpec, Properties, Value};
12+
13+
use super::*;
14+
use std::cell::{Cell, RefCell};
15+
16+
#[derive(Debug, Default, Properties, CompositeTemplate)]
17+
#[properties(wrapper_type = super::MessageReply)]
18+
#[template(string = r#"
19+
template MessageReply : Widget {
20+
Separator separator {
21+
width-request: 2;
22+
}
23+
24+
Box labels_box {
25+
orientation: vertical;
26+
27+
Label sender_label {
28+
ellipsize: end;
29+
xalign: 0;
30+
31+
styles ["caption-heading"]
32+
}
33+
34+
Label message_label {
35+
ellipsize: end;
36+
xalign: 0;
37+
single-line-mode: true;
38+
39+
styles [
40+
"message",
41+
"small-body",
42+
]
43+
}
44+
}
45+
}
46+
"#)]
47+
pub(crate) struct MessageReply {
48+
pub(super) sender_color_class: RefCell<Option<String>>,
49+
pub(super) bindings: RefCell<Vec<gtk::ExpressionWatch>>,
50+
pub(super) is_loading: Cell<bool>,
51+
52+
#[property(get, set, construct_only)]
53+
pub(super) chat: RefCell<Option<Chat>>,
54+
#[property(get, set, construct_only)]
55+
pub(super) reply_id: Cell<i64>,
56+
#[property(get, set, construct_only)]
57+
pub(super) is_outgoing: Cell<bool>,
58+
59+
#[template_child]
60+
pub(super) separator: TemplateChild<gtk::Separator>,
61+
#[template_child]
62+
pub(super) labels_box: TemplateChild<gtk::Box>,
63+
#[template_child]
64+
pub(super) sender_label: TemplateChild<gtk::Label>,
65+
#[template_child]
66+
pub(super) message_label: TemplateChild<gtk::Label>,
67+
}
68+
69+
#[glib::object_subclass]
70+
impl ObjectSubclass for MessageReply {
71+
const NAME: &'static str = "MessageReply";
72+
type Type = super::MessageReply;
73+
type ParentType = gtk::Widget;
74+
75+
fn class_init(klass: &mut Self::Class) {
76+
Self::bind_template(klass);
77+
klass.set_layout_manager_type::<gtk::BoxLayout>();
78+
klass.set_css_name("messagereply");
79+
}
80+
81+
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
82+
obj.init_template();
83+
}
84+
}
85+
86+
impl ObjectImpl for MessageReply {
87+
fn properties() -> &'static [ParamSpec] {
88+
Self::derived_properties()
89+
}
90+
91+
fn set_property(&self, id: usize, value: &Value, pspec: &ParamSpec) {
92+
self.derived_set_property(id, value, pspec)
93+
}
94+
95+
fn property(&self, id: usize, pspec: &ParamSpec) -> Value {
96+
self.derived_property(id, pspec)
97+
}
98+
99+
fn constructed(&self) {
100+
self.is_loading.set(true);
101+
self.message_label
102+
.set_label(&gettextrs::gettext("Loading ..."));
103+
self.obj().load_message();
104+
}
105+
106+
fn dispose(&self) {
107+
self.dispose_template();
108+
}
109+
}
110+
111+
impl WidgetImpl for MessageReply {}
112+
}
113+
114+
glib::wrapper! {
115+
pub(crate) struct MessageReply(ObjectSubclass<imp::MessageReply>)
116+
@extends gtk::Widget;
117+
}
118+
119+
impl MessageReply {
120+
pub(crate) fn new(chat: Chat, reply_id: i64, is_outgoing: bool) -> Self {
121+
glib::Object::builder()
122+
.property("chat", chat)
123+
.property("reply-id", reply_id)
124+
.property("is-outgoing", is_outgoing)
125+
.build()
126+
}
127+
128+
fn load_message(&self) {
129+
let imp = self.imp();
130+
131+
let reply_id = imp.reply_id.get();
132+
let chat = self.chat().unwrap();
133+
134+
if let Some(message) = chat.message(reply_id) {
135+
self.update_from_message(&message);
136+
} else {
137+
spawn(clone!(@weak self as obj => async move {
138+
let chat = obj.imp().chat.borrow().clone().unwrap();
139+
if let Ok(message) = chat.fetch_message(reply_id).await {
140+
obj.update_from_message(&message);
141+
} else {
142+
// Message doesn't exist, so we should remove "Loading..." caption
143+
// TODO: Impelent it properly using signals
144+
obj.imp().message_label.set_label("Deleted message");
145+
}
146+
}));
147+
}
148+
149+
imp.is_loading.set(false);
150+
}
151+
152+
pub(crate) fn set_max_char_width(&self, n_chars: i32) {
153+
self.imp().message_label.set_max_width_chars(n_chars);
154+
self.imp().sender_label.set_max_width_chars(n_chars);
155+
}
156+
157+
fn update_from_message(&self, message: &Message) {
158+
let imp = self.imp();
159+
let mut bindings = imp.bindings.borrow_mut();
160+
while let Some(binding) = bindings.pop() {
161+
binding.unwatch();
162+
}
163+
164+
// Remove the previous color css class
165+
let mut sender_color_class = imp.sender_color_class.borrow_mut();
166+
if let Some(class) = sender_color_class.as_ref() {
167+
self.remove_css_class(class);
168+
}
169+
// Show sender label, if needed
170+
let show_sender = !matches!(
171+
message.chat().type_(),
172+
ChatType::Supergroup(data) if data.is_channel()
173+
);
174+
if show_sender {
175+
let sender_name_expression = message.sender_name_expression();
176+
let sender_binding =
177+
sender_name_expression.bind(&*imp.sender_label, "label", glib::Object::NONE);
178+
179+
bindings.push(sender_binding);
180+
181+
if !imp.is_outgoing.get() {
182+
// Color sender label
183+
if let MessageSender::User(user) = message.sender() {
184+
let classes = vec![
185+
"sender-text-red",
186+
"sender-text-orange",
187+
"sender-text-violet",
188+
"sender-text-green",
189+
"sender-text-cyan",
190+
"sender-text-blue",
191+
"sender-text-pink",
192+
];
193+
194+
let color_class = classes[user.id() as usize % classes.len()];
195+
self.add_css_class(color_class);
196+
197+
*sender_color_class = Some(color_class.into());
198+
}
199+
}
200+
imp.sender_label.set_visible(true);
201+
}
202+
203+
// Set content label expression
204+
205+
let caption = strings::message_content(message.clone().as_ref());
206+
imp.message_label.set_label(&caption);
207+
208+
self.imp().is_loading.set(false);
209+
}
210+
}

src/session/content/message_row/sticker.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1+
use adw::prelude::*;
12
use glib::clone;
2-
use gtk::prelude::*;
33
use gtk::subclass::prelude::*;
44
use gtk::{gio, glib, CompositeTemplate};
55
use tdlib::enums::MessageContent;
66

77
use crate::session::content::message_row::{
8-
MessageBase, MessageBaseImpl, MessageIndicators, StickerPicture,
8+
MessageBase, MessageBaseImpl, MessageIndicators, MessageReply, StickerPicture,
99
};
1010
use crate::tdlib::Message;
1111
use crate::utils::{decode_image_from_path, spawn};
1212
use crate::Session;
1313

1414
use super::base::MessageBaseExt;
1515

16+
const MAX_REPLY_CHAR_WIDTH: i32 = 18;
17+
1618
mod imp {
1719
use super::*;
1820
use once_cell::sync::Lazy;
@@ -25,6 +27,8 @@ mod imp {
2527
#[template_child]
2628
pub(super) picture: TemplateChild<StickerPicture>,
2729
#[template_child]
30+
pub(super) sticker_overlay: TemplateChild<gtk::Overlay>,
31+
#[template_child]
2832
pub(super) indicators: TemplateChild<MessageIndicators>,
2933
}
3034

@@ -97,6 +101,23 @@ impl MessageBaseExt for MessageSticker {
97101

98102
imp.indicators.set_message(message.clone().upcast());
99103

104+
if message.reply_to_message_id() != 0 {
105+
let reply = MessageReply::new(
106+
message.chat(),
107+
message.reply_to_message_id(),
108+
message.is_outgoing(),
109+
);
110+
reply.set_valign(gtk::Align::Start);
111+
reply.set_max_char_width(MAX_REPLY_CHAR_WIDTH);
112+
113+
// FIXME: Do not show message reply when message is being deleted
114+
// Sticker and the reply should be at the opposite sides of the box
115+
if message.is_outgoing() {
116+
reply.insert_before(self, Some(&imp.sticker_overlay.get()));
117+
} else {
118+
reply.insert_after(self, Some(&imp.sticker_overlay.get()));
119+
}
120+
}
100121
imp.picture.set_texture(None);
101122

102123
if let MessageContent::MessageSticker(data) = message.content().0 {

0 commit comments

Comments
 (0)