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(&gtk::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(&gtk::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(&gtk::Label::new(Some(i18n("Uploading image.").as_str())));
101                 upload_attachment_msg
102             }
103             RowType::File if is_temp => {
104                 upload_attachment_msg.add(&gtk::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) -> &gtk::ListBoxRow124     pub fn get_listbox_row(&self) -> &gtk::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: &gtk::ListBoxRow)316     fn set_msg_styles(&self, msg: &Message, w: &gtk::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: &gtk::Label)326     fn set_label_styles(&self, w: &gtk::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<&gtk::Label>) -> Option<()>699     fn connect_right_click_menu(&self, msg: &Message, label: Option<&gtk::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