1 use crate::i18n::i18n;
2 use itertools::Itertools;
3
4 use crate::appop::UserInfoCache;
5 use crate::backend::ThreadPool;
6 use chrono::prelude::*;
7 use either::Either;
8 use fractal_api::r0::AccessToken;
9 use fractal_api::url::Url;
10 use glib::clone;
11 use gtk::{prelude::*, ButtonExt, ContainerExt, LabelExt, Overlay, WidgetExt};
12 use std::cmp::max;
13 use std::rc::Rc;
14
15 use crate::util::markup_text;
16
17 use crate::cache::download_to_cache;
18 use crate::cache::download_to_cache_username;
19 use crate::cache::download_to_cache_username_emote;
20
21 use crate::globals;
22 use crate::uitypes::MessageContent as Message;
23 use crate::uitypes::RowType;
24 use crate::widgets;
25 use crate::widgets::message_menu::MessageMenu;
26 use crate::widgets::AvatarExt;
27 use crate::widgets::{AudioPlayerWidget, PlayerExt, VideoPlayerWidget};
28
29 /* A message row in the room history */
30 #[derive(Clone, Debug)]
31 pub struct MessageBox {
32 access_token: AccessToken,
33 server_url: Url,
34 username: gtk::Label,
35 pub username_event_box: gtk::EventBox,
36 eventbox: gtk::EventBox,
37 gesture: gtk::GestureLongPress,
38 row: gtk::ListBoxRow,
39 image: Option<gtk::DrawingArea>,
40 video_player: Option<Rc<VideoPlayerWidget>>,
41 pub header: bool,
42 }
43
44 impl MessageBox {
new(server_url: Url, access_token: AccessToken) -> MessageBox45 pub fn new(server_url: Url, access_token: AccessToken) -> MessageBox {
46 let username = gtk::Label::new(None);
47 let eb = gtk::EventBox::new();
48 let eventbox = gtk::EventBox::new();
49 let row = gtk::ListBoxRow::new();
50 let gesture = gtk::GestureLongPress::new(&eventbox);
51
52 username.set_ellipsize(pango::EllipsizeMode::End);
53 gesture.set_propagation_phase(gtk::PropagationPhase::Capture);
54 gesture.set_touch_only(true);
55
56 MessageBox {
57 access_token,
58 server_url,
59 username,
60 username_event_box: eb,
61 eventbox,
62 gesture,
63 row,
64 image: None,
65 video_player: None,
66 header: true,
67 }
68 }
69
70 /* create the message row with or without a header */
create( &mut self, thread_pool: ThreadPool, user_info_cache: UserInfoCache, msg: &Message, has_header: bool, is_temp: bool, )71 pub fn create(
72 &mut self,
73 thread_pool: ThreadPool,
74 user_info_cache: UserInfoCache,
75 msg: &Message,
76 has_header: bool,
77 is_temp: bool,
78 ) {
79 self.set_msg_styles(msg, &self.row);
80 self.row.set_selectable(false);
81 let upload_attachment_msg = gtk::Box::new(gtk::Orientation::Horizontal, 10);
82 let w = match msg.mtype {
83 RowType::Emote => {
84 self.row.set_margin_top(12);
85 self.header = false;
86 self.small_widget(thread_pool, msg)
87 }
88 RowType::Video if is_temp => {
89 upload_attachment_msg
90 .add(>k::Label::new(Some(i18n("Uploading video.").as_str())));
91 upload_attachment_msg
92 }
93 RowType::Audio if is_temp => {
94 upload_attachment_msg
95 .add(>k::Label::new(Some(i18n("Uploading audio.").as_str())));
96 upload_attachment_msg
97 }
98 RowType::Image if is_temp => {
99 upload_attachment_msg
100 .add(>k::Label::new(Some(i18n("Uploading image.").as_str())));
101 upload_attachment_msg
102 }
103 RowType::File if is_temp => {
104 upload_attachment_msg.add(>k::Label::new(Some(i18n("Uploading file.").as_str())));
105 upload_attachment_msg
106 }
107 _ if has_header => {
108 self.row.set_margin_top(12);
109 self.header = true;
110 self.widget(thread_pool, user_info_cache, msg)
111 }
112 _ => {
113 self.header = false;
114 self.small_widget(thread_pool, msg)
115 }
116 };
117
118 self.eventbox.add(&w);
119 self.row.add(&self.eventbox);
120 self.row.show_all();
121 self.connect_right_click_menu(msg, None);
122 }
123
get_listbox_row(&self) -> >k::ListBoxRow124 pub fn get_listbox_row(&self) -> >k::ListBoxRow {
125 &self.row
126 }
127
tmpwidget( mut self, thread_pool: ThreadPool, user_info_cache: UserInfoCache, msg: &Message, ) -> MessageBox128 pub fn tmpwidget(
129 mut self,
130 thread_pool: ThreadPool,
131 user_info_cache: UserInfoCache,
132 msg: &Message,
133 ) -> MessageBox {
134 self.create(thread_pool, user_info_cache, msg, true, true);
135 {
136 let w = self.get_listbox_row();
137 w.get_style_context().add_class("msg-tmp");
138 }
139 self
140 }
141
update_header( &mut self, thread_pool: ThreadPool, user_info_cache: UserInfoCache, msg: Message, has_header: bool, )142 pub fn update_header(
143 &mut self,
144 thread_pool: ThreadPool,
145 user_info_cache: UserInfoCache,
146 msg: Message,
147 has_header: bool,
148 ) {
149 let w = if has_header && msg.mtype != RowType::Emote {
150 self.row.set_margin_top(12);
151 self.header = true;
152 self.widget(thread_pool, user_info_cache, &msg)
153 } else {
154 if let RowType::Emote = msg.mtype {
155 self.row.set_margin_top(12);
156 }
157 self.header = false;
158 self.small_widget(thread_pool, &msg)
159 };
160 if let Some(eb) = self.eventbox.get_child() {
161 eb.destroy(); // clean the eventbox
162 }
163 self.eventbox.add(&w);
164 self.row.show_all();
165 }
166
widget( &mut self, thread_pool: ThreadPool, user_info_cache: UserInfoCache, msg: &Message, ) -> gtk::Box167 fn widget(
168 &mut self,
169 thread_pool: ThreadPool,
170 user_info_cache: UserInfoCache,
171 msg: &Message,
172 ) -> gtk::Box {
173 // msg
174 // +--------+---------+
175 // | avatar | content |
176 // +--------+---------+
177 let msg_widget = gtk::Box::new(gtk::Orientation::Horizontal, 10);
178 let content = self.build_room_msg_content(thread_pool.clone(), msg, false);
179 /* Todo: make build_room_msg_avatar() faster (currently ~1ms) */
180 let avatar = self.build_room_msg_avatar(thread_pool, user_info_cache, msg);
181
182 msg_widget.pack_start(&avatar, false, false, 0);
183 msg_widget.pack_start(&content, true, true, 0);
184
185 msg_widget
186 }
187
small_widget(&mut self, thread_pool: ThreadPool, msg: &Message) -> gtk::Box188 fn small_widget(&mut self, thread_pool: ThreadPool, msg: &Message) -> gtk::Box {
189 // msg
190 // +--------+---------+
191 // | | content |
192 // +--------+---------+
193 let msg_widget = gtk::Box::new(gtk::Orientation::Horizontal, 5);
194 let content = self.build_room_msg_content(thread_pool, msg, true);
195 content.set_margin_start(50);
196
197 msg_widget.pack_start(&content, true, true, 0);
198
199 msg_widget
200 }
201
build_room_msg_content( &mut self, thread_pool: ThreadPool, msg: &Message, small: bool, ) -> gtk::Box202 fn build_room_msg_content(
203 &mut self,
204 thread_pool: ThreadPool,
205 msg: &Message,
206 small: bool,
207 ) -> gtk::Box {
208 // content
209 // +---------+
210 // | info |
211 // +---------+
212 // | body_bx |
213 // +---------+
214 let content = gtk::Box::new(gtk::Orientation::Vertical, 0);
215
216 if !small {
217 let info = self.build_room_msg_info(msg);
218 info.set_margin_top(2);
219 info.set_margin_bottom(3);
220 content.pack_start(&info, false, false, 0);
221 }
222
223 let body_bx = self.build_room_msg_body_bx(thread_pool, msg);
224 content.pack_start(&body_bx, true, true, 0);
225
226 content
227 }
228
build_room_msg_body_bx(&mut self, thread_pool: ThreadPool, msg: &Message) -> gtk::Box229 fn build_room_msg_body_bx(&mut self, thread_pool: ThreadPool, msg: &Message) -> gtk::Box {
230 // body_bx
231 // +------+-----------+
232 // | body | edit_mark |
233 // +------+-----------+
234 let body_bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
235
236 let body = match msg.mtype {
237 RowType::Sticker => self.build_room_msg_sticker(thread_pool, msg),
238 RowType::Image => self.build_room_msg_image(thread_pool, msg),
239 RowType::Emote => self.build_room_msg_emote(msg),
240 RowType::Audio => self.build_room_audio_player(thread_pool, msg),
241 RowType::Video => self.build_room_video_player(thread_pool, msg),
242 RowType::File => self.build_room_msg_file(msg),
243 _ => self.build_room_msg_body(msg),
244 };
245
246 body_bx.pack_start(&body, true, true, 0);
247
248 if let Some(replace_date) = msg.replace_date {
249 let edit_mark = gtk::Image::new_from_icon_name(
250 Some("document-edit-symbolic"),
251 gtk::IconSize::Button,
252 );
253 edit_mark.get_style_context().add_class("edit-mark");
254 edit_mark.set_valign(gtk::Align::End);
255
256 let edit_tooltip = replace_date.format(&i18n("Last edited %c")).to_string();
257 edit_mark.set_tooltip_text(Some(&edit_tooltip));
258
259 body_bx.pack_start(&edit_mark, false, false, 0);
260 }
261 body_bx
262 }
263
build_room_msg_avatar( &self, thread_pool: ThreadPool, user_info_cache: UserInfoCache, msg: &Message, ) -> widgets::Avatar264 fn build_room_msg_avatar(
265 &self,
266 thread_pool: ThreadPool,
267 user_info_cache: UserInfoCache,
268 msg: &Message,
269 ) -> widgets::Avatar {
270 let uid = msg.sender.clone();
271 let alias = msg.sender_name.clone();
272 let avatar = widgets::Avatar::avatar_new(Some(globals::MSG_ICON_SIZE));
273
274 let data = avatar.circle(
275 uid.to_string(),
276 alias.clone(),
277 globals::MSG_ICON_SIZE,
278 None,
279 None,
280 );
281 if let Some(name) = alias {
282 self.username.set_text(&name);
283 } else {
284 self.username.set_text(&uid.to_string());
285 }
286
287 download_to_cache(
288 thread_pool,
289 user_info_cache,
290 self.server_url.clone(),
291 self.access_token.clone(),
292 uid.clone(),
293 data.clone(),
294 );
295 download_to_cache_username(
296 self.server_url.clone(),
297 self.access_token.clone(),
298 uid,
299 self.username.clone(),
300 Some(data),
301 );
302
303 avatar
304 }
305
build_room_msg_username(&self, uname: String) -> gtk::Label306 fn build_room_msg_username(&self, uname: String) -> gtk::Label {
307 self.username.set_text(&uname);
308 self.username.set_justify(gtk::Justification::Left);
309 self.username.set_halign(gtk::Align::Start);
310 self.username.get_style_context().add_class("username");
311
312 self.username.clone()
313 }
314
315 /* Add classes to the widget based on message type */
set_msg_styles(&self, msg: &Message, w: >k::ListBoxRow)316 fn set_msg_styles(&self, msg: &Message, w: >k::ListBoxRow) {
317 let style = w.get_style_context();
318 match msg.mtype {
319 RowType::Mention => style.add_class("msg-mention"),
320 RowType::Emote => style.add_class("msg-emote"),
321 RowType::Emoji => style.add_class("msg-emoji"),
322 _ => {}
323 }
324 }
325
set_label_styles(&self, w: >k::Label)326 fn set_label_styles(&self, w: >k::Label) {
327 w.set_line_wrap(true);
328 w.set_line_wrap_mode(pango::WrapMode::WordChar);
329 w.set_justify(gtk::Justification::Left);
330 w.set_xalign(0.0);
331 w.set_valign(gtk::Align::Start);
332 w.set_halign(gtk::Align::Fill);
333 w.set_selectable(true);
334 }
335
build_room_msg_body(&self, msg: &Message) -> gtk::Box336 fn build_room_msg_body(&self, msg: &Message) -> gtk::Box {
337 let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
338
339 let msg_parts = self.create_msg_parts(&msg.body);
340
341 if msg.mtype == RowType::Mention {
342 for part in msg_parts.iter() {
343 let highlights = msg.highlights.clone();
344 part.connect_property_cursor_position_notify(move |w| {
345 if let Some(text) = w.get_text() {
346 let attr = pango::AttrList::new();
347 for light in highlights.clone() {
348 highlight_username(w.clone(), &attr, &light, text.to_string());
349 }
350 w.set_attributes(Some(&attr));
351 }
352 });
353
354 let highlights = msg.highlights.clone();
355 part.connect_property_selection_bound_notify(move |w| {
356 if let Some(text) = w.get_text() {
357 let attr = pango::AttrList::new();
358 for light in highlights.clone() {
359 highlight_username(w.clone(), &attr, &light, text.to_string());
360 }
361 w.set_attributes(Some(&attr));
362 }
363 });
364
365 if let Some(text) = part.get_text() {
366 let attr = pango::AttrList::new();
367 for light in msg.highlights.clone() {
368 highlight_username(part.clone(), &attr, &light, text.to_string());
369 }
370 part.set_attributes(Some(&attr));
371 }
372 }
373 }
374
375 for part in msg_parts {
376 self.connect_right_click_menu(msg, Some(&part));
377 bx.add(&part);
378 }
379 bx
380 }
381
create_msg_parts(&self, body: &str) -> Vec<gtk::Label>382 fn create_msg_parts(&self, body: &str) -> Vec<gtk::Label> {
383 let mut parts_labels: Vec<gtk::Label> = vec![];
384
385 for (k, group) in body.lines().group_by(kind_of_line).into_iter() {
386 let mut v: Vec<&str> = if k == MsgPartType::Quote {
387 group.map(|l| trim_start_quote(l)).collect()
388 } else {
389 group.collect()
390 };
391 /* We need to remove the first and last empty line (if any) because quotes use /n/n */
392 if v.starts_with(&[""]) {
393 v.drain(..1);
394 }
395 if v.ends_with(&[""]) {
396 v.pop();
397 }
398 let part = v.join("\n");
399
400 parts_labels.push(self.create_msg(part.as_str(), k));
401 }
402
403 parts_labels
404 }
405
create_msg(&self, body: &str, k: MsgPartType) -> gtk::Label406 fn create_msg(&self, body: &str, k: MsgPartType) -> gtk::Label {
407 let msg_part = gtk::Label::new(None);
408 msg_part.set_markup(&markup_text(body));
409 self.set_label_styles(&msg_part);
410
411 if k == MsgPartType::Quote {
412 msg_part.get_style_context().add_class("quote");
413 }
414 msg_part
415 }
416
build_room_msg_image(&mut self, thread_pool: ThreadPool, msg: &Message) -> gtk::Box417 fn build_room_msg_image(&mut self, thread_pool: ThreadPool, msg: &Message) -> gtk::Box {
418 let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
419
420 // If the thumbnail is not a valid URL we use the msg.url
421 let img = msg
422 .thumb
423 .clone()
424 .filter(|m| m.scheme() == "mxc" || m.scheme().starts_with("http"))
425 .or_else(|| msg.url.clone())
426 .map(Either::Left)
427 .or_else(|| Some(Either::Right(msg.local_path.clone()?)));
428
429 if let Some(img_path) = img {
430 let image = widgets::image::Image::new(self.server_url.clone(), img_path)
431 .size(Some(globals::MAX_IMAGE_SIZE))
432 .build(thread_pool);
433
434 image.widget.get_style_context().add_class("image-widget");
435
436 bx.pack_start(&image.widget, true, true, 0);
437 bx.show_all();
438 self.image = Some(image.widget);
439 self.connect_media_viewer(msg);
440 }
441
442 bx
443 }
444
build_room_msg_sticker(&self, thread_pool: ThreadPool, msg: &Message) -> gtk::Box445 fn build_room_msg_sticker(&self, thread_pool: ThreadPool, msg: &Message) -> gtk::Box {
446 let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
447 if let Some(url) = msg.url.clone() {
448 let image = widgets::image::Image::new(self.server_url.clone(), Either::Left(url))
449 .size(Some(globals::MAX_STICKER_SIZE))
450 .build(thread_pool);
451 image.widget.set_tooltip_text(Some(&msg.body[..]));
452
453 bx.add(&image.widget);
454 }
455
456 bx
457 }
458
build_room_audio_player(&self, thread_pool: ThreadPool, msg: &Message) -> gtk::Box459 fn build_room_audio_player(&self, thread_pool: ThreadPool, msg: &Message) -> gtk::Box {
460 let bx = gtk::Box::new(gtk::Orientation::Horizontal, 6);
461
462 if let Some(url) = msg.url.clone() {
463 let player = AudioPlayerWidget::new();
464 let start_playing = false;
465 PlayerExt::initialize_stream(
466 &player,
467 url,
468 self.server_url.clone(),
469 thread_pool,
470 &bx,
471 start_playing,
472 );
473
474 let control_box = PlayerExt::get_controls_container(&player)
475 .expect("Every AudioPlayer must have controls.");
476 bx.pack_start(&control_box, false, true, 0);
477 }
478
479 let download_btn =
480 gtk::Button::new_from_icon_name(Some("document-save-symbolic"), gtk::IconSize::Button);
481 download_btn.set_tooltip_text(Some(i18n("Save").as_str()));
482
483 let evid = msg
484 .id
485 .as_ref()
486 .map(|evid| evid.to_string())
487 .unwrap_or_default();
488 let data = glib::Variant::from(evid);
489 download_btn.set_action_target_value(Some(&data));
490 download_btn.set_action_name(Some("message.save_as"));
491 bx.pack_start(&download_btn, false, false, 3);
492
493 let outer_box = gtk::Box::new(gtk::Orientation::Vertical, 6);
494 let file_name = gtk::Label::new(Some(&format!("<b>{}</b>", msg.body)));
495 file_name.set_use_markup(true);
496 file_name.set_xalign(0.0);
497 file_name.set_line_wrap(true);
498 file_name.set_line_wrap_mode(pango::WrapMode::WordChar);
499 outer_box.pack_start(&file_name, false, false, 0);
500 outer_box.pack_start(&bx, false, false, 0);
501 outer_box.get_style_context().add_class("audio-box");
502 outer_box
503 }
504
build_room_video_player(&mut self, thread_pool: ThreadPool, msg: &Message) -> gtk::Box505 fn build_room_video_player(&mut self, thread_pool: ThreadPool, msg: &Message) -> gtk::Box {
506 let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
507
508 if let Some(url) = msg.url.clone() {
509 let with_controls = false;
510 let player = VideoPlayerWidget::new(with_controls);
511 let start_playing = false;
512 PlayerExt::initialize_stream(
513 &player,
514 url,
515 self.server_url.clone(),
516 thread_pool,
517 &bx,
518 start_playing,
519 );
520
521 let overlay = Overlay::new();
522 let video_widget = player.get_video_widget();
523 video_widget.set_size_request(-1, 390);
524 VideoPlayerWidget::auto_adjust_video_dimensions(&player);
525 overlay.add(&video_widget);
526
527 let play_button = gtk::Button::new();
528 let play_icon = gtk::Image::new_from_icon_name(
529 Some("media-playback-start-symbolic"),
530 gtk::IconSize::Dialog,
531 );
532 play_button.set_image(Some(&play_icon));
533 play_button.set_halign(gtk::Align::Center);
534 play_button.set_valign(gtk::Align::Center);
535 play_button.get_style_context().add_class("osd");
536 play_button.get_style_context().add_class("play-icon");
537 play_button.get_style_context().add_class("flat");
538 let evid = msg
539 .id
540 .as_ref()
541 .map(|evid| evid.to_string())
542 .unwrap_or_default();
543 let data = glib::Variant::from(evid);
544 play_button.set_action_name(Some("app.open-media-viewer"));
545 play_button.set_action_target_value(Some(&data));
546 overlay.add_overlay(&play_button);
547
548 let menu_button = gtk::MenuButton::new();
549 let three_dot_icon =
550 gtk::Image::new_from_icon_name(Some("view-more-symbolic"), gtk::IconSize::Button);
551 menu_button.set_image(Some(&three_dot_icon));
552 menu_button.get_style_context().add_class("osd");
553 menu_button.get_style_context().add_class("round-button");
554 menu_button.get_style_context().add_class("flat");
555 menu_button.set_margin_top(12);
556 menu_button.set_margin_end(12);
557 menu_button.set_opacity(0.8);
558 menu_button.set_halign(gtk::Align::End);
559 menu_button.set_valign(gtk::Align::Start);
560 menu_button.connect_size_allocate(|button, allocation| {
561 let diameter = max(allocation.width, allocation.height);
562 button.set_size_request(diameter, diameter);
563 });
564 overlay.add_overlay(&menu_button);
565
566 let evid = msg.id.as_ref();
567 let redactable = msg.redactable;
568 let menu = MessageMenu::new(evid, &RowType::Video, &redactable, None, None);
569 menu_button.set_popover(Some(&menu.get_popover()));
570
571 bx.pack_start(&overlay, true, true, 0);
572 self.connect_media_viewer(msg);
573 self.video_player = Some(player);
574 }
575
576 bx
577 }
578
get_video_widget(&self) -> Option<Rc<VideoPlayerWidget>>579 pub fn get_video_widget(&self) -> Option<Rc<VideoPlayerWidget>> {
580 self.video_player.clone()
581 }
582
build_room_msg_file(&self, msg: &Message) -> gtk::Box583 fn build_room_msg_file(&self, msg: &Message) -> gtk::Box {
584 let bx = gtk::Box::new(gtk::Orientation::Horizontal, 12);
585 let btn_bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
586
587 let name = msg.body.as_str();
588 let name_lbl = gtk::Label::new(Some(name));
589 name_lbl.set_tooltip_text(Some(name));
590 name_lbl.set_ellipsize(pango::EllipsizeMode::End);
591
592 name_lbl.get_style_context().add_class("msg-highlighted");
593
594 let download_btn =
595 gtk::Button::new_from_icon_name(Some("document-save-symbolic"), gtk::IconSize::Button);
596 download_btn.set_tooltip_text(Some(i18n("Save").as_str()));
597
598 let evid = msg
599 .id
600 .as_ref()
601 .map(|evid| evid.to_string())
602 .unwrap_or_default();
603
604 let data = glib::Variant::from(&evid);
605 download_btn.set_action_target_value(Some(&data));
606 download_btn.set_action_name(Some("message.save_as"));
607
608 let open_btn =
609 gtk::Button::new_from_icon_name(Some("document-open-symbolic"), gtk::IconSize::Button);
610 open_btn.set_tooltip_text(Some(i18n("Open").as_str()));
611
612 let data = glib::Variant::from(&evid);
613 open_btn.set_action_target_value(Some(&data));
614 open_btn.set_action_name(Some("message.open_with"));
615
616 btn_bx.pack_start(&open_btn, false, false, 0);
617 btn_bx.pack_start(&download_btn, false, false, 0);
618 btn_bx.get_style_context().add_class("linked");
619
620 bx.pack_start(&name_lbl, false, false, 0);
621 bx.pack_start(&btn_bx, false, false, 0);
622 bx
623 }
624
build_room_msg_date(&self, dt: &DateTime<Local>) -> gtk::Label625 fn build_room_msg_date(&self, dt: &DateTime<Local>) -> gtk::Label {
626 /* TODO: get system preference for 12h/24h */
627 let use_ampm = false;
628 let format = if use_ampm {
629 /* Use 12h time format (AM/PM) */
630 i18n("%l∶%M %p")
631 } else {
632 /* Use 24 time format */
633 i18n("%R")
634 };
635
636 let d = dt.format(&format).to_string();
637
638 let date = gtk::Label::new(None);
639 date.set_markup(&format!("<span alpha=\"60%\">{}</span>", d.trim()));
640 date.set_line_wrap(true);
641 date.set_justify(gtk::Justification::Right);
642 date.set_valign(gtk::Align::Start);
643 date.set_halign(gtk::Align::End);
644 date.get_style_context().add_class("timestamp");
645
646 date
647 }
648
build_room_msg_info(&self, msg: &Message) -> gtk::Box649 fn build_room_msg_info(&self, msg: &Message) -> gtk::Box {
650 // info
651 // +----------+------+
652 // | username | date |
653 // +----------+------+
654 let info = gtk::Box::new(gtk::Orientation::Horizontal, 0);
655
656 let username = self.build_room_msg_username(
657 msg.sender_name
658 .clone()
659 .unwrap_or_else(|| msg.sender.to_string()),
660 );
661 let date = self.build_room_msg_date(&msg.date);
662
663 self.username_event_box.add(&username);
664
665 info.pack_start(&self.username_event_box, true, true, 0);
666 info.pack_start(&date, false, false, 0);
667
668 info
669 }
670
build_room_msg_emote(&self, msg: &Message) -> gtk::Box671 fn build_room_msg_emote(&self, msg: &Message) -> gtk::Box {
672 let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
673 /* Use MXID till we have a alias */
674 let sname = msg
675 .sender_name
676 .clone()
677 .unwrap_or_else(|| msg.sender.to_string());
678 let msg_label = gtk::Label::new(None);
679 let body: &str = &msg.body;
680 let markup = markup_text(body);
681
682 download_to_cache_username_emote(
683 self.server_url.clone(),
684 self.access_token.clone(),
685 msg.sender.clone(),
686 &markup,
687 msg_label.clone(),
688 None,
689 );
690
691 self.connect_right_click_menu(msg, Some(&msg_label));
692 msg_label.set_markup(&format!("<b>{}</b> {}", sname, markup));
693 self.set_label_styles(&msg_label);
694
695 bx.add(&msg_label);
696 bx
697 }
698
connect_right_click_menu(&self, msg: &Message, label: Option<>k::Label>) -> Option<()>699 fn connect_right_click_menu(&self, msg: &Message, label: Option<>k::Label>) -> Option<()> {
700 let mtype = msg.mtype;
701 let redactable = msg.redactable;
702 let widget = if let Some(l) = label {
703 l.upcast_ref::<gtk::Widget>()
704 } else {
705 self.eventbox.upcast_ref::<gtk::Widget>()
706 };
707
708 let eventbox = &self.eventbox;
709 let id = msg.id.clone();
710 widget.connect_button_press_event(
711 clone!(@weak eventbox => @default-return Inhibit(false), move |w, e| {
712 if e.get_button() == 3 {
713 MessageMenu::new(id.as_ref(), &mtype, &redactable, Some(&eventbox), Some(w));
714 Inhibit(true)
715 } else {
716 Inhibit(false)
717 }
718 }),
719 );
720
721 let id = msg.id.clone();
722 self.gesture
723 .connect_pressed(clone!(@weak eventbox, @weak widget => move |_, _, _| {
724 MessageMenu::new(
725 id.as_ref(),
726 &mtype,
727 &redactable,
728 Some(&eventbox),
729 Some(&widget),
730 );
731 }));
732 None
733 }
734
connect_media_viewer(&self, msg: &Message) -> Option<()>735 fn connect_media_viewer(&self, msg: &Message) -> Option<()> {
736 let evid = msg.id.as_ref()?.to_string();
737 let data = glib::Variant::from(evid);
738 self.row.set_action_name(Some("app.open-media-viewer"));
739 self.row.set_action_target_value(Some(&data));
740 None
741 }
742 }
743
highlight_username( label: gtk::Label, attr: &pango::AttrList, alias: &str, input: String, ) -> Option<()>744 fn highlight_username(
745 label: gtk::Label,
746 attr: &pango::AttrList,
747 alias: &str,
748 input: String,
749 ) -> Option<()> {
750 fn contains((start, end): (i32, i32), item: i32) -> bool {
751 if start <= end {
752 start <= item && end > item
753 } else {
754 start <= item || end > item
755 }
756 }
757
758 let mut input = input.to_lowercase();
759 let bounds = label.get_selection_bounds();
760 let context = label.get_style_context();
761 let fg = context.lookup_color("theme_selected_bg_color")?;
762 let red = fg.red * 65535. + 0.5;
763 let green = fg.green * 65535. + 0.5;
764 let blue = fg.blue * 65535. + 0.5;
765 let color = pango::Attribute::new_foreground(red as u16, green as u16, blue as u16)?;
766
767 let alias = &alias.to_lowercase();
768 let mut removed_char = 0;
769 while input.contains(alias) {
770 let pos = {
771 let start = input.find(alias)? as i32;
772 (start, start + alias.len() as i32)
773 };
774 let mut color = color.clone();
775 let mark_start = removed_char as i32 + pos.0;
776 let mark_end = removed_char as i32 + pos.1;
777 let mut final_pos = Some((mark_start, mark_end));
778 /* exclude selected text */
779 if let Some((bounds_start, bounds_end)) = bounds {
780 /* If the selection is within the alias */
781 if contains((mark_start, mark_end), bounds_start)
782 && contains((mark_start, mark_end), bounds_end)
783 {
784 final_pos = Some((mark_start, bounds_start));
785 /* Add blue color after a selection */
786 let mut color = color.clone();
787 color.set_start_index(bounds_end as u32);
788 color.set_end_index(mark_end as u32);
789 attr.insert(color);
790 } else {
791 /* The alias starts inside a selection */
792 if contains(bounds?, mark_start) {
793 final_pos = Some((bounds_end, final_pos?.1));
794 }
795 /* The alias ends inside a selection */
796 if contains(bounds?, mark_end - 1) {
797 final_pos = Some((final_pos?.0, bounds_start));
798 }
799 }
800 }
801
802 if let Some((start, end)) = final_pos {
803 color.set_start_index(start as u32);
804 color.set_end_index(end as u32);
805 attr.insert(color);
806 }
807 {
808 let end = pos.1 as usize;
809 input.drain(0..end);
810 }
811 removed_char += pos.1 as u32;
812 }
813
814 None
815 }
816
817 #[derive(PartialEq)]
818 enum MsgPartType {
819 Normal,
820 Quote,
821 }
822
kind_of_line(line: &&str) -> MsgPartType823 fn kind_of_line(line: &&str) -> MsgPartType {
824 if line.trim_start().starts_with('>') {
825 MsgPartType::Quote
826 } else {
827 MsgPartType::Normal
828 }
829 }
830
trim_start_quote(line: &str) -> &str831 fn trim_start_quote(line: &str) -> &str {
832 line.trim_start().get(1..).unwrap_or(line).trim_start()
833 }
834