[fractal/split-appop-ui] Further componentize MessageBox




commit b056f25f1beb548b359afb8f62f9f5a0883c002d
Author: Alejandro Domínguez <adomu net-c com>
Date:   Fri Jan 22 22:13:56 2021 +0100

    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<VideoPlayerWidget>),
-}
-
 // 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<MessageBoxInfoHeader>,
+    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(&gtk::Label::new(Some(i18n("Uploading video.").as_str())));
-                upload_attachment_msg
-            }
-            RowType::Audio if is_temp => {
-                upload_attachment_msg
-                    .add(&gtk::Label::new(Some(i18n("Uploading audio.").as_str())));
-                upload_attachment_msg
-            }
-            RowType::Image if is_temp => {
-                upload_attachment_msg
-                    .add(&gtk::Label::new(Some(i18n("Uploading image.").as_str())));
-                upload_attachment_msg
-            }
-            RowType::File if is_temp => {
-                upload_attachment_msg.add(&gtk::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) -> &gtk::ListBoxRow {
-        &self.root
+        &self.container.root
     }
 
     pub fn get_video_player(&self) -> Option<&Rc<VideoPlayerWidget>> {
-        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<&gtk::Label>) -> Option<()> {
+        let mtype = msg.mtype;
+        let redactable = msg.redactable;
+        let widget = if let Some(l) = label {
+            l.upcast_ref::<gtk::Widget>()
+        } else {
+            self.eventbox.upcast_ref::<gtk::Widget>()
+        };
+
+        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<widgets::Avatar>,
+        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(&gtk::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) -> &gtk::Box {
+        match self {
+            Self::Temp(root) => root,
+            Self::Final { root, .. } => root,
+        }
+    }
+}
+
+#[derive(Clone, Debug)]
+struct MessageBoxContent {
+    root: gtk::Box,
+    info: Option<MessageBoxInfoHeader>,
+    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: &gtk::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<widgets::image::Image>), // gtk::DrawingArea
+    Video(Option<Rc<VideoPlayerWidget>>),
+    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<gtk::Image>,
+    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<&gtk::Label>) -> Option<()> {
-        let mtype = msg.mtype;
-        let redactable = msg.redactable;
-        let widget = if let Some(l) = label {
-            l.upcast_ref::<gtk::Widget>()
+            Some(edit_mark)
         } else {
-            self.eventbox.upcast_ref::<gtk::Widget>()
+            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<widgets::image::Image>) {
+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<Rc<VideoPlayerWidget>>) {
+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: &gtk::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)]


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]