From b056f25f1beb548b359afb8f62f9f5a0883c002d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Fri, 22 Jan 2021 22:13:56 +0100 Subject: [PATCH] Further componentize MessageBox --- fractal-gtk/src/appop/message.rs | 4 +- fractal-gtk/src/widgets/message.rs | 854 ++++++++++++++++------------- 2 files changed, 467 insertions(+), 391 deletions(-) diff --git a/fractal-gtk/src/appop/message.rs b/fractal-gtk/src/appop/message.rs index cd51cf77..3ae44e32 100644 --- a/fractal-gtk/src/appop/message.rs +++ b/fractal-gtk/src/appop/message.rs @@ -69,7 +69,7 @@ impl AppOp { let login_data = self.login_data.clone()?; let messages = self.ui.history.as_ref()?.get_listbox(); if let Some(ui_msg) = self.create_new_room_message(msg.clone()) { - let mb = widgets::MessageBox::tmpwidget( + let mb = widgets::MessageBox::create_tmp( login_data.session_client.clone(), self.user_info_cache.clone(), &ui_msg, @@ -109,7 +109,7 @@ impl AppOp { let mut widgets = vec![]; for t in self.msg_queue.iter().rev().filter(|m| m.msg.room == r.id) { if let Some(ui_msg) = self.create_new_room_message(t.msg.clone()) { - let mb = widgets::MessageBox::tmpwidget( + let mb = widgets::MessageBox::create_tmp( login_data.session_client.clone(), self.user_info_cache.clone(), &ui_msg, diff --git a/fractal-gtk/src/widgets/message.rs b/fractal-gtk/src/widgets/message.rs index f40a0c6f..2c89063a 100644 --- a/fractal-gtk/src/widgets/message.rs +++ b/fractal-gtk/src/widgets/message.rs @@ -19,43 +19,14 @@ use matrix_sdk::Client as MatrixClient; use std::cmp::max; use std::rc::Rc; -#[derive(Clone, Debug)] -pub enum MessageBoxMedia { - None, - Image(gtk::DrawingArea), - VideoPlayer(Rc), -} - // A message row in the room history #[derive(Clone, Debug)] pub struct MessageBox { - root: gtk::ListBoxRow, - eventbox: gtk::EventBox, - gesture: gtk::GestureLongPress, - pub media_widget: MessageBoxMedia, - header: Option, + container: MessageBoxContainer, + msg_widget: MessageBoxMsg, } impl MessageBox { - fn new() -> Self { - let eventbox = gtk::EventBox::new(); - - let root = gtk::ListBoxRow::new(); - root.add(&eventbox); - - let gesture = gtk::GestureLongPress::new(&eventbox); - gesture.set_propagation_phase(gtk::PropagationPhase::Capture); - gesture.set_touch_only(true); - - Self { - root, - eventbox, - gesture, - media_widget: MessageBoxMedia::None, - header: None, - } - } - // create the message row with or without a header pub fn create( session_client: MatrixClient, @@ -64,56 +35,45 @@ impl MessageBox { has_header: bool, is_temp: bool, ) -> Self { - let mut mb = Self::new(); - mb.set_msg_styles(msg); - mb.root.set_selectable(false); - let upload_attachment_msg = gtk::Box::new(gtk::Orientation::Horizontal, 10); - let w = match msg.mtype { + let container = MessageBoxContainer::new(); + + container.set_msg_styles(msg.mtype); + let msg_widget = match msg.mtype { + RowType::Video if is_temp => MessageBoxMsg::tmpwidget("Uploading video."), + RowType::Audio if is_temp => MessageBoxMsg::tmpwidget("Uploading audio."), + RowType::Image if is_temp => MessageBoxMsg::tmpwidget("Uploading image."), + RowType::File if is_temp => MessageBoxMsg::tmpwidget("Uploading file."), RowType::Emote => { - mb.root.set_margin_top(12); - mb.small_widget(session_client, msg) - } - RowType::Video if is_temp => { - upload_attachment_msg - .add(>k::Label::new(Some(i18n("Uploading video.").as_str()))); - upload_attachment_msg - } - RowType::Audio if is_temp => { - upload_attachment_msg - .add(>k::Label::new(Some(i18n("Uploading audio.").as_str()))); - upload_attachment_msg - } - RowType::Image if is_temp => { - upload_attachment_msg - .add(>k::Label::new(Some(i18n("Uploading image.").as_str()))); - upload_attachment_msg - } - RowType::File if is_temp => { - upload_attachment_msg.add(>k::Label::new(Some(i18n("Uploading file.").as_str()))); - upload_attachment_msg + container.root.set_margin_top(12); + MessageBoxMsg::small_widget(&container, session_client, msg) } _ if has_header => { - mb.root.set_margin_top(12); - mb.widget(session_client, user_info_cache, msg) + container.root.set_margin_top(12); + MessageBoxMsg::widget(&container, session_client, user_info_cache, msg) } - _ => mb.small_widget(session_client, msg), + _ => MessageBoxMsg::small_widget(&container, session_client, msg), }; - mb.eventbox.add(&w); - mb.root.show_all(); - mb.connect_right_click_menu(msg, None); + if is_temp { + container.root.get_style_context().add_class("msg-tmp"); + } + + container.eventbox.add(msg_widget.root()); + container.root.show_all(); + container.connect_right_click_menu(msg, None); - mb + Self { + container, + msg_widget, + } } - pub fn tmpwidget( + pub fn create_tmp( session_client: MatrixClient, user_info_cache: UserInfoCache, msg: &Message, ) -> Self { - let mb = Self::create(session_client, user_info_cache, msg, true, true); - mb.root.get_style_context().add_class("msg-tmp"); - mb + Self::create(session_client, user_info_cache, msg, true, true) } pub fn update_header( @@ -123,78 +83,210 @@ impl MessageBox { msg: Message, has_header: bool, ) { - let w = if has_header && msg.mtype != RowType::Emote { - self.root.set_margin_top(12); - self.widget(session_client, user_info_cache, &msg) + let msg_widget = if has_header && msg.mtype != RowType::Emote { + self.container.root.set_margin_top(12); + MessageBoxMsg::widget(&self.container, session_client, user_info_cache, &msg) } else { if let RowType::Emote = msg.mtype { - self.root.set_margin_top(12); + self.container.root.set_margin_top(12); } - self.small_widget(session_client, &msg) + MessageBoxMsg::small_widget(&self.container, session_client, &msg) }; - if let Some(eb) = self.eventbox.get_child() { - self.eventbox.remove(&eb); + if let Some(eb) = self.container.eventbox.get_child() { + self.container.eventbox.remove(&eb); } - self.eventbox.add(&w); - self.root.show_all(); + self.container.eventbox.add(msg_widget.root()); + self.container.root.show_all(); } pub fn get_widget(&self) -> >k::ListBoxRow { - &self.root + &self.container.root } pub fn get_video_player(&self) -> Option<&Rc> { - match self.media_widget { - MessageBoxMedia::VideoPlayer(ref player) => Some(player), + match &self.msg_widget { + MessageBoxMsg::Final { content, .. } => { + if let MessageBodyType::Video(ref player) = content.body_bx.type_extras { + player.as_ref() + } else { + None + } + } _ => None, } } pub fn has_header(&self) -> bool { - self.header.is_some() + match &self.msg_widget { + MessageBoxMsg::Final { content, .. } => content.info.is_some(), + _ => false, + } + } +} + +#[derive(Clone, Debug)] +struct MessageBoxContainer { + root: gtk::ListBoxRow, + eventbox: gtk::EventBox, + gesture: gtk::GestureLongPress, +} + +impl MessageBoxContainer { + fn new() -> Self { + let eventbox = gtk::EventBox::new(); + + let root = gtk::ListBoxRow::new(); + root.add(&eventbox); + root.set_selectable(false); + + let gesture = gtk::GestureLongPress::new(&eventbox); + gesture.set_propagation_phase(gtk::PropagationPhase::Capture); + gesture.set_touch_only(true); + + Self { + root, + eventbox, + gesture, + } + } + + // Add classes to the widget based on message type + fn set_msg_styles(&self, mtype: RowType) { + let style = self.root.get_style_context(); + match mtype { + RowType::Mention => style.add_class("msg-mention"), + RowType::Emote => style.add_class("msg-emote"), + RowType::Emoji => style.add_class("msg-emoji"), + _ => {} + } + } + + fn connect_media_viewer(&self, msg: &Message) -> Option<()> { + let evid = msg.msg.id.as_ref()?.to_string(); + let data = glib::Variant::from(evid); + self.root.set_action_name(Some("app.open-media-viewer")); + self.root.set_action_target_value(Some(&data)); + None + } + + fn connect_right_click_menu(&self, msg: &Message, label: Option<>k::Label>) -> Option<()> { + let mtype = msg.mtype; + let redactable = msg.redactable; + let widget = if let Some(l) = label { + l.upcast_ref::() + } else { + self.eventbox.upcast_ref::() + }; + + let id = msg.msg.id.clone(); + widget.connect_button_press_event(move |w, e| { + if e.triggers_context_menu() { + let menu = MessageMenu::new(id.as_ref(), &mtype, &redactable, Some(w)); + let coords = e.get_position(); + menu.show_at_coords(w, coords); + Inhibit(true) + } else { + Inhibit(false) + } + }); + + let id = msg.msg.id.clone(); + self.gesture + .connect_pressed(clone!(@weak widget => move |_, x, y| { + let menu = MessageMenu::new(id.as_ref(), &mtype, &redactable, Some(&widget)); + menu.show_at_coords(&widget, (x, y)); + })); + None + } +} + +#[derive(Clone, Debug)] +enum MessageBoxMsg { + Temp(gtk::Box), + Final { + root: gtk::Box, + avatar: Option, + content: MessageBoxContent, + }, +} + +impl MessageBoxMsg { + fn tmpwidget(label_content: &str) -> Self { + let upload_attachment_msg = gtk::Box::new(gtk::Orientation::Horizontal, 10); + upload_attachment_msg.add(>k::Label::new(Some(i18n(label_content).as_str()))); + + Self::Temp(upload_attachment_msg) } fn widget( - &mut self, + container: &MessageBoxContainer, session_client: MatrixClient, user_info_cache: UserInfoCache, msg: &Message, - ) -> gtk::Box { + ) -> Self { // msg // +--------+---------+ // | avatar | content | // +--------+---------+ let msg_widget = gtk::Box::new(gtk::Orientation::Horizontal, 10); - let content = self.build_room_msg_content(session_client.clone(), msg, true); + let content = MessageBoxContent::build(container, session_client.clone(), msg, true); // TODO: make build_room_msg_avatar() faster (currently ~1ms) let avatar = build_room_msg_avatar(session_client, user_info_cache, msg); msg_widget.pack_start(&avatar, false, false, 0); - msg_widget.pack_start(&content, true, true, 0); + msg_widget.pack_start(&content.root, true, true, 0); - msg_widget + Self::Final { + root: msg_widget, + avatar: Some(avatar), + content, + } } - fn small_widget(&mut self, session_client: MatrixClient, msg: &Message) -> gtk::Box { + fn small_widget( + container: &MessageBoxContainer, + session_client: MatrixClient, + msg: &Message, + ) -> Self { // msg // +--------+---------+ // | | content | // +--------+---------+ let msg_widget = gtk::Box::new(gtk::Orientation::Horizontal, 5); - let content = self.build_room_msg_content(session_client, msg, false); - content.set_margin_start(50); + let content = MessageBoxContent::build(container, session_client, msg, false); + content.root.set_margin_start(50); - msg_widget.pack_start(&content, true, true, 0); + msg_widget.pack_start(&content.root, true, true, 0); - msg_widget + Self::Final { + root: msg_widget, + avatar: None, + content, + } } - fn build_room_msg_content( - &mut self, + fn root(&self) -> >k::Box { + match self { + Self::Temp(root) => root, + Self::Final { root, .. } => root, + } + } +} + +#[derive(Clone, Debug)] +struct MessageBoxContent { + root: gtk::Box, + info: Option, + body_bx: MessageBodyBox, +} + +impl MessageBoxContent { + fn build( + container: &MessageBoxContainer, session_client: MatrixClient, msg: &Message, info_header: bool, - ) -> gtk::Box { + ) -> Self { // content // +---------+ // | info | @@ -203,7 +295,7 @@ impl MessageBox { // +---------+ let content = gtk::Box::new(gtk::Orientation::Vertical, 0); - self.header = if info_header { + let info = if info_header { let info = MessageBoxInfoHeader::from(msg); info.root.set_margin_top(2); info.root.set_margin_bottom(3); @@ -214,211 +306,218 @@ impl MessageBox { None }; - let body_bx = self.build_room_msg_body_bx(session_client, msg); - content.pack_start(&body_bx, true, true, 0); + let body_bx = MessageBodyBox::build(&container, session_client, msg); + content.pack_start(&body_bx.root, true, true, 0); - content + Self { + root: content, + info, + body_bx, + } } +} - fn build_room_msg_body_bx(&mut self, session_client: MatrixClient, msg: &Message) -> gtk::Box { - // body_bx - // +------+-----------+ - // | body | edit_mark | - // +------+-----------+ - let body_bx = gtk::Box::new(gtk::Orientation::Horizontal, 0); - - let body = match msg.mtype { - RowType::Sticker => build_room_msg_sticker(session_client, msg), - RowType::Audio => build_room_audio_player(session_client, msg), - RowType::Image => { - let (image_box, image) = build_room_msg_image(session_client, msg); - - if let Some(image) = image { - self.media_widget = MessageBoxMedia::Image(image.widget); - self.connect_media_viewer(msg); - } +fn build_room_msg_avatar( + session_client: MatrixClient, + user_info_cache: UserInfoCache, + msg: &Message, +) -> widgets::Avatar { + let uid = msg.msg.sender.clone(); + let alias = msg.sender_name.clone(); + let avatar = widgets::Avatar::avatar_new(Some(globals::MSG_ICON_SIZE)); + avatar.set_valign(gtk::Align::Start); - image_box - } - RowType::Video => { - let (video_box, player) = build_room_video_player(session_client, msg); + let data = avatar.circle( + uid.to_string(), + alias.clone(), + globals::MSG_ICON_SIZE, + None, + None, + ); - if let Some(player) = player { - self.media_widget = MessageBoxMedia::VideoPlayer(player); - self.connect_media_viewer(msg); - } + download_to_cache( + session_client.clone(), + user_info_cache, + uid.clone(), + data.clone(), + ); - video_box - } - RowType::Emote => { - let (emote_box, msg_label) = build_room_msg_emote(msg); - self.connect_right_click_menu(msg, Some(&msg_label)); - emote_box - } - RowType::File => build_room_msg_file(msg), - _ => self.build_room_msg_body(msg), - }; + avatar +} - body_bx.pack_start(&body, true, true, 0); +fn set_label_styles(w: >k::Label) { + w.set_line_wrap(true); + w.set_line_wrap_mode(pango::WrapMode::WordChar); + w.set_justify(gtk::Justification::Left); + w.set_xalign(0.0); + w.set_valign(gtk::Align::Start); + w.set_halign(gtk::Align::Fill); + w.set_selectable(true); +} - if let Some(replace_date) = msg.msg.replace_date() { - let edit_mark = - gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button); - edit_mark.get_style_context().add_class("edit-mark"); - edit_mark.set_valign(gtk::Align::End); +fn highlight_username( + label: gtk::Label, + attr: &pango::AttrList, + alias: &str, + input: String, +) -> Option<()> { + fn contains((start, end): (i32, i32), item: i32) -> bool { + if start <= end { + start <= item && end > item + } else { + start <= item || end > item + } + } - let edit_tooltip = replace_date.format(&i18n("Last edited %c")).to_string(); - edit_mark.set_tooltip_text(Some(&edit_tooltip)); + let mut input = input.to_lowercase(); + let bounds = label.get_selection_bounds(); + let context = label.get_style_context(); + let fg = context.lookup_color("theme_selected_bg_color")?; + let red = fg.red * 65535. + 0.5; + let green = fg.green * 65535. + 0.5; + let blue = fg.blue * 65535. + 0.5; + let color = pango::Attribute::new_foreground(red as u16, green as u16, blue as u16)?; - body_bx.pack_start(&edit_mark, false, false, 0); + let alias = &alias.to_lowercase(); + let mut removed_char = 0; + while input.contains(alias) { + let pos = { + let start = input.find(alias)? as i32; + (start, start + alias.len() as i32) + }; + let mut color = color.clone(); + let mark_start = removed_char as i32 + pos.0; + let mark_end = removed_char as i32 + pos.1; + let mut final_pos = Some((mark_start, mark_end)); + // exclude selected text + if let Some((bounds_start, bounds_end)) = bounds { + // If the selection is within the alias + if contains((mark_start, mark_end), bounds_start) + && contains((mark_start, mark_end), bounds_end) + { + final_pos = Some((mark_start, bounds_start)); + // Add blue color after a selection + let mut color = color.clone(); + color.set_start_index(bounds_end as u32); + color.set_end_index(mark_end as u32); + attr.insert(color); + } else { + // The alias starts inside a selection + if contains(bounds?, mark_start) { + final_pos = Some((bounds_end, final_pos?.1)); + } + // The alias ends inside a selection + if contains(bounds?, mark_end - 1) { + final_pos = Some((final_pos?.0, bounds_start)); + } + } } - body_bx - } - - // Add classes to the widget based on message type - fn set_msg_styles(&self, msg: &Message) { - let style = self.root.get_style_context(); - match msg.mtype { - RowType::Mention => style.add_class("msg-mention"), - RowType::Emote => style.add_class("msg-emote"), - RowType::Emoji => style.add_class("msg-emoji"), - _ => {} + if let Some((start, end)) = final_pos { + color.set_start_index(start as u32); + color.set_end_index(end as u32); + attr.insert(color); + } + { + let end = pos.1 as usize; + input.drain(0..end); } + removed_char += pos.1 as u32; } - fn build_room_msg_body(&self, msg: &Message) -> gtk::Box { - let bx = gtk::Box::new(gtk::Orientation::Vertical, 6); + None +} - let msgs_by_kind_of_line = msg.msg.body.lines().group_by(|&line| kind_of_line(line)); - let msg_parts = msgs_by_kind_of_line.into_iter().map(|(k, group)| { - let mut v: Vec<&str> = if k == MsgPartType::Quote { - group.map(trim_start_quote).collect() - } else { - group.collect() - }; - // We need to remove the first and last empty line (if any) because quotes use \n\n - if v.starts_with(&[""]) { - v.drain(..1); - } - if v.ends_with(&[""]) { - v.pop(); - } - let part = v.join("\n"); +#[derive(Clone, Debug)] +enum MessageBodyType { + Sticker, + Audio, + Image(Option), // gtk::DrawingArea + Video(Option>), + Emote(gtk::Label), + File, + Text, +} - let part_widget = gtk::Label::new(None); - part_widget.set_markup(&markup_text(&part)); - set_label_styles(&part_widget); +#[derive(Clone, Debug)] +struct MessageBodyBox { + root: gtk::Box, + body: gtk::Box, + edit_mark: Option, + type_extras: MessageBodyType, +} - if k == MsgPartType::Quote { - part_widget.get_style_context().add_class("quote"); - } +impl MessageBodyBox { + fn build(container: &MessageBoxContainer, session_client: MatrixClient, msg: &Message) -> Self { + // body_bx + // +------+-----------+ + // | body | edit_mark | + // +------+-----------+ + let body_bx = gtk::Box::new(gtk::Orientation::Horizontal, 0); - part_widget - }); + let (body, type_extras) = build_room_msg(container, session_client, msg); - for part in msg_parts { - if msg.mtype == RowType::Mention { - let highlights = msg.highlights.clone(); - part.connect_property_cursor_position_notify(move |w| { - let attr = pango::AttrList::new(); - for light in highlights.clone() { - highlight_username(w.clone(), &attr, &light, w.get_text().to_string()); - } - w.set_attributes(Some(&attr)); - }); - - let highlights = msg.highlights.clone(); - part.connect_property_selection_bound_notify(move |w| { - let attr = pango::AttrList::new(); - for light in highlights.clone() { - highlight_username(w.clone(), &attr, &light, w.get_text().to_string()); - } - w.set_attributes(Some(&attr)); - }); + body_bx.pack_start(&body, true, true, 0); - let attr = pango::AttrList::new(); - for light in msg.highlights.clone() { - highlight_username(part.clone(), &attr, &light, part.get_text().to_string()); - } - part.set_attributes(Some(&attr)); - } + let edit_mark = if let Some(replace_date) = msg.msg.replace_date() { + let edit_mark = + gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button); + edit_mark.get_style_context().add_class("edit-mark"); + edit_mark.set_valign(gtk::Align::End); - self.connect_right_click_menu(msg, Some(&part)); - bx.add(&part); - } + let edit_tooltip = replace_date.format(&i18n("Last edited %c")).to_string(); + edit_mark.set_tooltip_text(Some(&edit_tooltip)); - bx - } + body_bx.pack_start(&edit_mark, false, false, 0); - fn connect_right_click_menu(&self, msg: &Message, label: Option<>k::Label>) -> Option<()> { - let mtype = msg.mtype; - let redactable = msg.redactable; - let widget = if let Some(l) = label { - l.upcast_ref::() + Some(edit_mark) } else { - self.eventbox.upcast_ref::() + None }; - let id = msg.msg.id.clone(); - widget.connect_button_press_event(move |w, e| { - if e.triggers_context_menu() { - let menu = MessageMenu::new(id.as_ref(), &mtype, &redactable, Some(w)); - let coords = e.get_position(); - menu.show_at_coords(w, coords); - Inhibit(true) - } else { - Inhibit(false) - } - }); - - let id = msg.msg.id.clone(); - self.gesture - .connect_pressed(clone!(@weak widget => move |_, x, y| { - let menu = MessageMenu::new(id.as_ref(), &mtype, &redactable, Some(&widget)); - menu.show_at_coords(&widget, (x, y)); - })); - None - } - - fn connect_media_viewer(&self, msg: &Message) -> Option<()> { - let evid = msg.msg.id.as_ref()?.to_string(); - let data = glib::Variant::from(evid); - self.root.set_action_name(Some("app.open-media-viewer")); - self.root.set_action_target_value(Some(&data)); - None + Self { + root: body_bx, + body, + edit_mark, + type_extras, + } } } -fn build_room_msg_avatar( +type BodyAndType = (gtk::Box, MessageBodyType); + +fn build_room_msg( + container: &MessageBoxContainer, session_client: MatrixClient, - user_info_cache: UserInfoCache, msg: &Message, -) -> widgets::Avatar { - let uid = msg.msg.sender.clone(); - let alias = msg.sender_name.clone(); - let avatar = widgets::Avatar::avatar_new(Some(globals::MSG_ICON_SIZE)); - avatar.set_valign(gtk::Align::Start); - - let data = avatar.circle( - uid.to_string(), - alias.clone(), - globals::MSG_ICON_SIZE, - None, - None, - ); +) -> BodyAndType { + let (body, type_extras) = match msg.mtype { + RowType::Sticker => build_room_msg_sticker(session_client, msg), + RowType::Audio => build_room_audio_player(session_client, msg), + RowType::Image => build_room_msg_image(session_client, msg), + RowType::Video => build_room_video_player(session_client, msg), + RowType::Emote => build_room_msg_emote(msg), + RowType::File => build_room_msg_file(msg), + _ => build_room_msg_body(container, msg), + }; - download_to_cache( - session_client.clone(), - user_info_cache, - uid.clone(), - data.clone(), - ); + match type_extras { + MessageBodyType::Image(Some(_)) => { + container.connect_media_viewer(msg); + } + MessageBodyType::Video(Some(_)) => { + container.connect_media_viewer(msg); + } + MessageBodyType::Emote(ref msg_label) => { + container.connect_right_click_menu(msg, Some(msg_label)); + } + _ => {} + } - avatar + (body, type_extras) } -fn build_room_msg_sticker(session_client: MatrixClient, msg: &Message) -> gtk::Box { +fn build_room_msg_sticker(session_client: MatrixClient, msg: &Message) -> BodyAndType { let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0); if let Some(url) = msg.msg.url.clone() { let image = widgets::image::Image::new(Either::Left(url)) @@ -429,10 +528,10 @@ fn build_room_msg_sticker(session_client: MatrixClient, msg: &Message) -> gtk::B bx.add(&image.widget); } - bx + (bx, MessageBodyType::Sticker) } -fn build_room_audio_player(session_client: MatrixClient, msg: &Message) -> gtk::Box { +fn build_room_audio_player(session_client: MatrixClient, msg: &Message) -> BodyAndType { let bx = gtk::Box::new(gtk::Orientation::Horizontal, 6); if let Some(url) = msg.msg.url.clone() { @@ -475,56 +574,11 @@ fn build_room_audio_player(session_client: MatrixClient, msg: &Message) -> gtk:: outer_box.pack_start(&file_name, false, false, 0); outer_box.pack_start(&bx, false, false, 0); outer_box.get_style_context().add_class("audio-box"); - outer_box -} - -fn build_room_msg_file(msg: &Message) -> gtk::Box { - let bx = gtk::Box::new(gtk::Orientation::Horizontal, 12); - let btn_bx = gtk::Box::new(gtk::Orientation::Horizontal, 0); - - let name = msg.msg.body.as_str(); - let name_lbl = gtk::Label::new(Some(name)); - name_lbl.set_tooltip_text(Some(name)); - name_lbl.set_ellipsize(pango::EllipsizeMode::End); - - name_lbl.get_style_context().add_class("msg-highlighted"); - - let download_btn = - gtk::Button::from_icon_name(Some("document-save-symbolic"), gtk::IconSize::Button); - download_btn.set_tooltip_text(Some(i18n("Save").as_str())); - - let evid = msg - .msg - .id - .as_ref() - .map(|evid| evid.to_string()) - .unwrap_or_default(); - - let data = glib::Variant::from(&evid); - download_btn.set_action_target_value(Some(&data)); - download_btn.set_action_name(Some("message.save_as")); - - let open_btn = - gtk::Button::from_icon_name(Some("document-open-symbolic"), gtk::IconSize::Button); - open_btn.set_tooltip_text(Some(i18n("Open").as_str())); - - let data = glib::Variant::from(&evid); - open_btn.set_action_target_value(Some(&data)); - open_btn.set_action_name(Some("message.open_with")); - - btn_bx.pack_start(&open_btn, false, false, 0); - btn_bx.pack_start(&download_btn, false, false, 0); - btn_bx.get_style_context().add_class("linked"); - bx.pack_start(&name_lbl, false, false, 0); - bx.pack_start(&btn_bx, false, false, 0); - bx + (outer_box, MessageBodyType::Audio) } -fn build_room_msg_image( - session_client: MatrixClient, - msg: &Message, -) -> (gtk::Box, Option) { +fn build_room_msg_image(session_client: MatrixClient, msg: &Message) -> BodyAndType { let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0); // If the thumbnail is not a valid URL we use the msg.url @@ -552,13 +606,10 @@ fn build_room_msg_image( None }; - (bx, image) + (bx, MessageBodyType::Image(image)) } -fn build_room_video_player( - session_client: MatrixClient, - msg: &Message, -) -> (gtk::Box, Option>) { +fn build_room_video_player(session_client: MatrixClient, msg: &Message) -> BodyAndType { let bx = gtk::Box::new(gtk::Orientation::Vertical, 6); let player = if let Some(url) = msg.msg.url.clone() { @@ -634,10 +685,10 @@ fn build_room_video_player( None }; - (bx, player) + (bx, MessageBodyType::Video(player)) } -fn build_room_msg_emote(msg: &Message) -> (gtk::Box, gtk::Label) { +fn build_room_msg_emote(msg: &Message) -> BodyAndType { let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0); // Use MXID till we have a alias let sname = msg @@ -652,90 +703,115 @@ fn build_room_msg_emote(msg: &Message) -> (gtk::Box, gtk::Label) { bx.add(&msg_label); - (bx, msg_label) + (bx, MessageBodyType::Emote(msg_label)) } -fn set_label_styles(w: >k::Label) { - w.set_line_wrap(true); - w.set_line_wrap_mode(pango::WrapMode::WordChar); - w.set_justify(gtk::Justification::Left); - w.set_xalign(0.0); - w.set_valign(gtk::Align::Start); - w.set_halign(gtk::Align::Fill); - w.set_selectable(true); +fn build_room_msg_file(msg: &Message) -> BodyAndType { + let bx = gtk::Box::new(gtk::Orientation::Horizontal, 12); + let btn_bx = gtk::Box::new(gtk::Orientation::Horizontal, 0); + + let name = msg.msg.body.as_str(); + let name_lbl = gtk::Label::new(Some(name)); + name_lbl.set_tooltip_text(Some(name)); + name_lbl.set_ellipsize(pango::EllipsizeMode::End); + + name_lbl.get_style_context().add_class("msg-highlighted"); + + let download_btn = + gtk::Button::from_icon_name(Some("document-save-symbolic"), gtk::IconSize::Button); + download_btn.set_tooltip_text(Some(i18n("Save").as_str())); + + let evid = msg + .msg + .id + .as_ref() + .map(|evid| evid.to_string()) + .unwrap_or_default(); + + let data = glib::Variant::from(&evid); + download_btn.set_action_target_value(Some(&data)); + download_btn.set_action_name(Some("message.save_as")); + + let open_btn = + gtk::Button::from_icon_name(Some("document-open-symbolic"), gtk::IconSize::Button); + open_btn.set_tooltip_text(Some(i18n("Open").as_str())); + + let data = glib::Variant::from(&evid); + open_btn.set_action_target_value(Some(&data)); + open_btn.set_action_name(Some("message.open_with")); + + btn_bx.pack_start(&open_btn, false, false, 0); + btn_bx.pack_start(&download_btn, false, false, 0); + btn_bx.get_style_context().add_class("linked"); + + bx.pack_start(&name_lbl, false, false, 0); + bx.pack_start(&btn_bx, false, false, 0); + + (bx, MessageBodyType::File) } -fn highlight_username( - label: gtk::Label, - attr: &pango::AttrList, - alias: &str, - input: String, -) -> Option<()> { - fn contains((start, end): (i32, i32), item: i32) -> bool { - if start <= end { - start <= item && end > item +fn build_room_msg_body(container: &MessageBoxContainer, msg: &Message) -> BodyAndType { + let bx = gtk::Box::new(gtk::Orientation::Vertical, 6); + + let msgs_by_kind_of_line = msg.msg.body.lines().group_by(|&line| kind_of_line(line)); + let msg_parts = msgs_by_kind_of_line.into_iter().map(|(k, group)| { + let mut v: Vec<&str> = if k == MsgPartType::Quote { + group.map(trim_start_quote).collect() } else { - start <= item || end > item + group.collect() + }; + // We need to remove the first and last empty line (if any) because quotes use \n\n + if v.starts_with(&[""]) { + v.drain(..1); } - } + if v.ends_with(&[""]) { + v.pop(); + } + let part = v.join("\n"); - let mut input = input.to_lowercase(); - let bounds = label.get_selection_bounds(); - let context = label.get_style_context(); - let fg = context.lookup_color("theme_selected_bg_color")?; - let red = fg.red * 65535. + 0.5; - let green = fg.green * 65535. + 0.5; - let blue = fg.blue * 65535. + 0.5; - let color = pango::Attribute::new_foreground(red as u16, green as u16, blue as u16)?; + let part_widget = gtk::Label::new(None); + part_widget.set_markup(&markup_text(&part)); + set_label_styles(&part_widget); - let alias = &alias.to_lowercase(); - let mut removed_char = 0; - while input.contains(alias) { - let pos = { - let start = input.find(alias)? as i32; - (start, start + alias.len() as i32) - }; - let mut color = color.clone(); - let mark_start = removed_char as i32 + pos.0; - let mark_end = removed_char as i32 + pos.1; - let mut final_pos = Some((mark_start, mark_end)); - // exclude selected text - if let Some((bounds_start, bounds_end)) = bounds { - // If the selection is within the alias - if contains((mark_start, mark_end), bounds_start) - && contains((mark_start, mark_end), bounds_end) - { - final_pos = Some((mark_start, bounds_start)); - // Add blue color after a selection - let mut color = color.clone(); - color.set_start_index(bounds_end as u32); - color.set_end_index(mark_end as u32); - attr.insert(color); - } else { - // The alias starts inside a selection - if contains(bounds?, mark_start) { - final_pos = Some((bounds_end, final_pos?.1)); + if k == MsgPartType::Quote { + part_widget.get_style_context().add_class("quote"); + } + + part_widget + }); + + for part in msg_parts { + if msg.mtype == RowType::Mention { + let highlights = msg.highlights.clone(); + part.connect_property_cursor_position_notify(move |w| { + let attr = pango::AttrList::new(); + for light in highlights.clone() { + highlight_username(w.clone(), &attr, &light, w.get_text().to_string()); } - // The alias ends inside a selection - if contains(bounds?, mark_end - 1) { - final_pos = Some((final_pos?.0, bounds_start)); + w.set_attributes(Some(&attr)); + }); + + let highlights = msg.highlights.clone(); + part.connect_property_selection_bound_notify(move |w| { + let attr = pango::AttrList::new(); + for light in highlights.clone() { + highlight_username(w.clone(), &attr, &light, w.get_text().to_string()); } + w.set_attributes(Some(&attr)); + }); + + let attr = pango::AttrList::new(); + for light in msg.highlights.iter() { + highlight_username(part.clone(), &attr, light, part.get_text().to_string()); } + part.set_attributes(Some(&attr)); } - if let Some((start, end)) = final_pos { - color.set_start_index(start as u32); - color.set_end_index(end as u32); - attr.insert(color); - } - { - let end = pos.1 as usize; - input.drain(0..end); - } - removed_char += pos.1 as u32; + container.connect_right_click_menu(msg, Some(&part)); + bx.add(&part); } - None + (bx, MessageBodyType::Text) } #[derive(Clone, Debug)]