[fractal/split-appop-ui] Check more invariants through the type system in MessageBox



commit 238f16a6212dc236c32c7fc02a90b97cc1a8292d
Author: Alejandro Domínguez <adomu net-c com>
Date:   Fri Jan 15 11:49:44 2021 +0100

    Check more invariants through the type system in MessageBox
    
    This modification improves encapsulation and follows the mantra
    "make illegal state unrepresentable", while making the building
    process less stateful.

 fractal-gtk/src/appop/message.rs        |   4 +-
 fractal-gtk/src/widgets/message.rs      | 751 +++++++++++++++++---------------
 fractal-gtk/src/widgets/room_history.rs |  24 +-
 3 files changed, 418 insertions(+), 361 deletions(-)
---
diff --git a/fractal-gtk/src/appop/message.rs b/fractal-gtk/src/appop/message.rs
index 887f9661..3a780c99 100644
--- a/fractal-gtk/src/appop/message.rs
+++ b/fractal-gtk/src/appop/message.rs
@@ -74,7 +74,7 @@ impl AppOp {
                 self.user_info_cache.clone(),
                 &ui_msg,
             );
-            let m = &mb.root;
+            let m = mb.get_widget();
             messages.add(m);
 
             if let Some(w) = messages.get_children().iter().last() {
@@ -114,7 +114,7 @@ impl AppOp {
                     self.user_info_cache.clone(),
                     &ui_msg,
                 );
-                let m = &mb.root;
+                let m = mb.get_widget();
                 messages.add(m);
 
                 if let Some(w) = messages.get_children().iter().last() {
diff --git a/fractal-gtk/src/widgets/message.rs b/fractal-gtk/src/widgets/message.rs
index 3f40473d..766f72f5 100644
--- a/fractal-gtk/src/widgets/message.rs
+++ b/fractal-gtk/src/widgets/message.rs
@@ -19,40 +19,40 @@ 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 {
-    pub root: gtk::ListBoxRow,
-    username: gtk::Label,
-    pub username_event_box: gtk::EventBox,
+    root: gtk::ListBoxRow,
     eventbox: gtk::EventBox,
     gesture: gtk::GestureLongPress,
-    image: Option<gtk::DrawingArea>,
-    pub video_player: Option<Rc<VideoPlayerWidget>>,
-    pub header: bool,
+    pub media_widget: MessageBoxMedia,
+    header: Option<MessageBoxInfoHeader>,
 }
 
 impl MessageBox {
     fn new() -> Self {
-        let username = gtk::Label::new(None);
-        let username_event_box = gtk::EventBox::new();
         let eventbox = gtk::EventBox::new();
+
         let root = gtk::ListBoxRow::new();
-        let gesture = gtk::GestureLongPress::new(&eventbox);
+        root.add(&eventbox);
 
-        username.set_ellipsize(pango::EllipsizeMode::End);
+        let gesture = gtk::GestureLongPress::new(&eventbox);
         gesture.set_propagation_phase(gtk::PropagationPhase::Capture);
         gesture.set_touch_only(true);
 
         Self {
             root,
-            username,
-            username_event_box,
             eventbox,
             gesture,
-            image: None,
-            video_player: None,
-            header: true,
+            media_widget: MessageBoxMedia::None,
+            header: None,
         }
     }
 
@@ -71,7 +71,6 @@ impl MessageBox {
         let w = match msg.mtype {
             RowType::Emote => {
                 mb.root.set_margin_top(12);
-                mb.header = false;
                 mb.small_widget(session_client, msg)
             }
             RowType::Video if is_temp => {
@@ -95,17 +94,12 @@ impl MessageBox {
             }
             _ if has_header => {
                 mb.root.set_margin_top(12);
-                mb.header = true;
                 mb.widget(session_client, user_info_cache, msg)
             }
-            _ => {
-                mb.header = false;
-                mb.small_widget(session_client, msg)
-            }
+            _ => mb.small_widget(session_client, msg),
         };
 
         mb.eventbox.add(&w);
-        mb.root.add(&mb.eventbox);
         mb.root.show_all();
         mb.connect_right_click_menu(msg, None);
 
@@ -131,13 +125,11 @@ impl MessageBox {
     ) {
         let w = if has_header && msg.mtype != RowType::Emote {
             self.root.set_margin_top(12);
-            self.header = true;
             self.widget(session_client, user_info_cache, &msg)
         } else {
             if let RowType::Emote = msg.mtype {
                 self.root.set_margin_top(12);
             }
-            self.header = false;
             self.small_widget(session_client, &msg)
         };
         if let Some(eb) = self.eventbox.get_child() {
@@ -147,6 +139,21 @@ impl MessageBox {
         self.root.show_all();
     }
 
+    pub fn get_widget(&self) -> &gtk::ListBoxRow {
+        &self.root
+    }
+
+    pub fn get_video_player(&self) -> Option<&Rc<VideoPlayerWidget>> {
+        match self.media_widget {
+            MessageBoxMedia::VideoPlayer(ref player) => Some(player),
+            _ => None,
+        }
+    }
+
+    pub fn has_header(&self) -> bool {
+        self.header.is_some()
+    }
+
     fn widget(
         &mut self,
         session_client: MatrixClient,
@@ -158,9 +165,9 @@ impl MessageBox {
         // | avatar | content |
         // +--------+---------+
         let msg_widget = gtk::Box::new(gtk::Orientation::Horizontal, 10);
-        let content = self.build_room_msg_content(session_client.clone(), msg, false);
+        let content = self.build_room_msg_content(session_client.clone(), msg, true);
         // TODO: make build_room_msg_avatar() faster (currently ~1ms)
-        let avatar = self.build_room_msg_avatar(session_client, user_info_cache, msg);
+        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);
@@ -174,7 +181,7 @@ impl MessageBox {
         // |        | content |
         // +--------+---------+
         let msg_widget = gtk::Box::new(gtk::Orientation::Horizontal, 5);
-        let content = self.build_room_msg_content(session_client, msg, true);
+        let content = self.build_room_msg_content(session_client, msg, false);
         content.set_margin_start(50);
 
         msg_widget.pack_start(&content, true, true, 0);
@@ -186,7 +193,7 @@ impl MessageBox {
         &mut self,
         session_client: MatrixClient,
         msg: &Message,
-        small: bool,
+        info_header: bool,
     ) -> gtk::Box {
         // content
         // +---------+
@@ -196,12 +203,16 @@ impl MessageBox {
         // +---------+
         let content = gtk::Box::new(gtk::Orientation::Vertical, 0);
 
-        if !small {
-            let info = self.build_room_msg_info(msg);
-            info.set_margin_top(2);
-            info.set_margin_bottom(3);
-            content.pack_start(&info, false, false, 0);
-        }
+        self.header = if info_header {
+            let info = MessageBoxInfoHeader::from(msg);
+            info.root.set_margin_top(2);
+            info.root.set_margin_bottom(3);
+            content.pack_start(&info.root, false, false, 0);
+
+            Some(info)
+        } else {
+            None
+        };
 
         let body_bx = self.build_room_msg_body_bx(session_client, msg);
         content.pack_start(&body_bx, true, true, 0);
@@ -217,12 +228,34 @@ impl MessageBox {
         let body_bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
 
         let body = match msg.mtype {
-            RowType::Sticker => self.build_room_msg_sticker(session_client, msg),
-            RowType::Image => self.build_room_msg_image(session_client, msg),
-            RowType::Emote => self.build_room_msg_emote(msg),
-            RowType::Audio => self.build_room_audio_player(session_client, msg),
-            RowType::Video => self.build_room_video_player(session_client, msg),
-            RowType::File => self.build_room_msg_file(msg),
+            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);
+                }
+
+                image_box
+            }
+            RowType::Video => {
+                let (video_box, player) = build_room_video_player(session_client, msg);
+
+                if let Some(player) = player {
+                    self.media_widget = MessageBoxMedia::VideoPlayer(player);
+                    self.connect_media_viewer(msg);
+                }
+
+                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),
         };
 
@@ -239,48 +272,8 @@ impl MessageBox {
 
             body_bx.pack_start(&edit_mark, false, false, 0);
         }
-        body_bx
-    }
-
-    fn build_room_msg_avatar(
-        &self,
-        session_client: MatrixClient,
-        user_info_cache: UserInfoCache,
-        msg: &Message,
-    ) -> widgets::Avatar {
-        let uid = 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,
-        );
 
-        self.username
-            .set_text(alias.as_deref().unwrap_or(uid.as_str()));
-
-        download_to_cache(
-            session_client.clone(),
-            user_info_cache,
-            uid.clone(),
-            data.clone(),
-        );
-
-        avatar
-    }
-
-    fn build_room_msg_username(&self, uname: &str) -> gtk::Label {
-        self.username.set_text(uname);
-        self.username.set_justify(gtk::Justification::Left);
-        self.username.set_halign(gtk::Align::Start);
-        self.username.get_style_context().add_class("username");
-
-        self.username.clone()
+        body_bx
     }
 
     // Add classes to the widget based on message type
@@ -358,271 +351,6 @@ impl MessageBox {
         bx
     }
 
-    fn build_room_msg_image(&mut self, session_client: MatrixClient, msg: &Message) -> gtk::Box {
-        let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
-
-        // If the thumbnail is not a valid URL we use the msg.url
-        let img = msg
-            .thumb
-            .clone()
-            .filter(|m| m.scheme() == "mxc" || m.scheme().starts_with("http"))
-            .or_else(|| msg.url.clone())
-            .map(Either::Left)
-            .or_else(|| Some(Either::Right(msg.local_path.clone()?)));
-
-        if let Some(img_path) = img {
-            let image = widgets::image::Image::new(img_path)
-                .size(Some(globals::MAX_IMAGE_SIZE))
-                .build(session_client);
-
-            image.widget.get_style_context().add_class("image-widget");
-
-            bx.pack_start(&image.widget, true, true, 0);
-            bx.show_all();
-            self.image = Some(image.widget);
-            self.connect_media_viewer(msg);
-        }
-
-        bx
-    }
-
-    fn build_room_msg_sticker(&self, session_client: MatrixClient, msg: &Message) -> gtk::Box {
-        let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
-        if let Some(url) = msg.url.clone() {
-            let image = widgets::image::Image::new(Either::Left(url))
-                .size(Some(globals::MAX_STICKER_SIZE))
-                .build(session_client);
-            image.widget.set_tooltip_text(Some(&msg.body[..]));
-
-            bx.add(&image.widget);
-        }
-
-        bx
-    }
-
-    fn build_room_audio_player(&self, session_client: MatrixClient, msg: &Message) -> gtk::Box {
-        let bx = gtk::Box::new(gtk::Orientation::Horizontal, 6);
-
-        if let Some(url) = msg.url.clone() {
-            let player = AudioPlayerWidget::new();
-            let start_playing = false;
-            PlayerExt::initialize_stream(
-                player.clone(),
-                session_client,
-                url,
-                bx.clone(),
-                start_playing,
-            );
-
-            let control_box = PlayerExt::get_controls_container(&player)
-                .expect("Every AudioPlayer must have controls.");
-            bx.pack_start(&control_box, false, true, 0);
-        }
-
-        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
-            .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"));
-        bx.pack_start(&download_btn, false, false, 3);
-
-        let outer_box = gtk::Box::new(gtk::Orientation::Vertical, 6);
-        let file_name = gtk::Label::new(Some(&format!("<b>{}</b>", msg.body)));
-        file_name.set_use_markup(true);
-        file_name.set_xalign(0.0);
-        file_name.set_line_wrap(true);
-        file_name.set_line_wrap_mode(pango::WrapMode::WordChar);
-        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_video_player(&mut self, session_client: MatrixClient, msg: &Message) -> gtk::Box {
-        let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
-
-        if let Some(url) = msg.url.clone() {
-            let with_controls = false;
-            let player = VideoPlayerWidget::new(with_controls);
-            let start_playing = false;
-            PlayerExt::initialize_stream(
-                player.clone(),
-                session_client,
-                url,
-                bx.clone(),
-                start_playing,
-            );
-
-            let overlay = Overlay::new();
-            let video_widget = player.get_video_widget();
-            video_widget.set_size_request(-1, 390);
-            VideoPlayerWidget::auto_adjust_video_dimensions(&player);
-            overlay.add(&video_widget);
-
-            let play_button = gtk::Button::new();
-            let play_icon = gtk::Image::from_icon_name(
-                Some("media-playback-start-symbolic"),
-                gtk::IconSize::Dialog,
-            );
-            play_button.set_image(Some(&play_icon));
-            play_button.set_halign(gtk::Align::Center);
-            play_button.set_valign(gtk::Align::Center);
-            play_button.get_style_context().add_class("osd");
-            play_button.get_style_context().add_class("play-icon");
-            play_button.get_style_context().add_class("flat");
-            let evid = msg
-                .id
-                .as_ref()
-                .map(|evid| evid.to_string())
-                .unwrap_or_default();
-            let data = glib::Variant::from(evid);
-            play_button.set_action_name(Some("app.open-media-viewer"));
-            play_button.set_action_target_value(Some(&data));
-            overlay.add_overlay(&play_button);
-
-            let menu_button = gtk::MenuButton::new();
-            let three_dot_icon =
-                gtk::Image::from_icon_name(Some("view-more-symbolic"), gtk::IconSize::Button);
-            menu_button.set_image(Some(&three_dot_icon));
-            menu_button.get_style_context().add_class("osd");
-            menu_button.get_style_context().add_class("round-button");
-            menu_button.get_style_context().add_class("flat");
-            menu_button.set_margin_top(12);
-            menu_button.set_margin_end(12);
-            menu_button.set_opacity(0.8);
-            menu_button.set_halign(gtk::Align::End);
-            menu_button.set_valign(gtk::Align::Start);
-            menu_button.connect_size_allocate(|button, allocation| {
-                let diameter = max(allocation.width, allocation.height);
-                button.set_size_request(diameter, diameter);
-            });
-            overlay.add_overlay(&menu_button);
-
-            let evid = msg.id.as_ref();
-            let redactable = msg.redactable;
-            let menu = MessageMenu::new(evid, &RowType::Video, &redactable, None);
-            menu_button.set_popover(Some(&menu.get_popover()));
-
-            let clip_container = ClipContainer::new();
-            clip_container.add(&overlay);
-
-            bx.pack_start(&clip_container, true, true, 0);
-            self.connect_media_viewer(msg);
-            self.video_player = Some(player);
-        }
-        bx
-    }
-
-    fn build_room_msg_file(&self, 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.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
-            .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
-    }
-
-    fn build_room_msg_date(&self, dt: &DateTime<Local>) -> gtk::Label {
-        // TODO: get system preference for 12h/24h
-        let use_ampm = false;
-        let format = if use_ampm {
-            // Use 12h time format (AM/PM)
-            i18n("%l∶%M %p")
-        } else {
-            // Use 24 time format
-            i18n("%R")
-        };
-
-        let d = dt.format(&format).to_string();
-
-        let date = gtk::Label::new(None);
-        date.set_markup(&format!("<span alpha=\"60%\">{}</span>", d.trim()));
-        date.set_line_wrap(true);
-        date.set_justify(gtk::Justification::Right);
-        date.set_valign(gtk::Align::Start);
-        date.set_halign(gtk::Align::End);
-        date.get_style_context().add_class("timestamp");
-
-        date
-    }
-
-    fn build_room_msg_info(&self, msg: &Message) -> gtk::Box {
-        // info
-        // +----------+------+
-        // | username | date |
-        // +----------+------+
-        let info = gtk::Box::new(gtk::Orientation::Horizontal, 0);
-
-        let username =
-            self.build_room_msg_username(msg.sender_name.as_deref().unwrap_or(msg.sender.as_str()));
-        let date = self.build_room_msg_date(&msg.date);
-
-        self.username_event_box.add(&username);
-
-        info.pack_start(&self.username_event_box, true, true, 0);
-        info.pack_start(&date, false, false, 0);
-
-        info
-    }
-
-    fn build_room_msg_emote(&self, msg: &Message) -> gtk::Box {
-        let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
-        // Use MXID till we have a alias
-        let sname = msg
-            .sender_name
-            .clone()
-            .unwrap_or_else(|| msg.sender.to_string());
-        let msg_label = gtk::Label::new(None);
-        let markup = markup_text(&msg.body);
-
-        self.connect_right_click_menu(msg, Some(&msg_label));
-        msg_label.set_markup(&format!("<b>{}</b> {}", sname, markup));
-        set_label_styles(&msg_label);
-
-        bx.add(&msg_label);
-        bx
-    }
-
     fn connect_right_click_menu(&self, msg: &Message, label: Option<&gtk::Label>) -> Option<()> {
         let mtype = msg.mtype;
         let redactable = msg.redactable;
@@ -662,6 +390,267 @@ impl MessageBox {
     }
 }
 
+fn build_room_msg_avatar(
+    session_client: MatrixClient,
+    user_info_cache: UserInfoCache,
+    msg: &Message,
+) -> widgets::Avatar {
+    let uid = 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,
+    );
+
+    download_to_cache(
+        session_client.clone(),
+        user_info_cache,
+        uid.clone(),
+        data.clone(),
+    );
+
+    avatar
+}
+
+fn build_room_msg_sticker(session_client: MatrixClient, msg: &Message) -> gtk::Box {
+    let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
+    if let Some(url) = msg.url.clone() {
+        let image = widgets::image::Image::new(Either::Left(url))
+            .size(Some(globals::MAX_STICKER_SIZE))
+            .build(session_client);
+        image.widget.set_tooltip_text(Some(&msg.body[..]));
+
+        bx.add(&image.widget);
+    }
+
+    bx
+}
+
+fn build_room_audio_player(session_client: MatrixClient, msg: &Message) -> gtk::Box {
+    let bx = gtk::Box::new(gtk::Orientation::Horizontal, 6);
+
+    if let Some(url) = msg.url.clone() {
+        let player = AudioPlayerWidget::new();
+        let start_playing = false;
+        PlayerExt::initialize_stream(
+            player.clone(),
+            session_client,
+            url,
+            bx.clone(),
+            start_playing,
+        );
+
+        let control_box = PlayerExt::get_controls_container(&player)
+            .expect("Every AudioPlayer must have controls.");
+        bx.pack_start(&control_box, false, true, 0);
+    }
+
+    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
+        .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"));
+    bx.pack_start(&download_btn, false, false, 3);
+
+    let outer_box = gtk::Box::new(gtk::Orientation::Vertical, 6);
+    let file_name = gtk::Label::new(Some(&format!("<b>{}</b>", msg.body)));
+    file_name.set_use_markup(true);
+    file_name.set_xalign(0.0);
+    file_name.set_line_wrap(true);
+    file_name.set_line_wrap_mode(pango::WrapMode::WordChar);
+    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.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
+        .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
+}
+
+fn build_room_msg_image(
+    session_client: MatrixClient,
+    msg: &Message,
+) -> (gtk::Box, Option<widgets::image::Image>) {
+    let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
+
+    // If the thumbnail is not a valid URL we use the msg.url
+    let img = msg
+        .thumb
+        .clone()
+        .filter(|m| m.scheme() == "mxc" || m.scheme().starts_with("http"))
+        .or_else(|| msg.url.clone())
+        .map(Either::Left)
+        .or_else(|| Some(Either::Right(msg.local_path.clone()?)));
+
+    let image = if let Some(img_path) = img {
+        let image = widgets::image::Image::new(img_path)
+            .size(Some(globals::MAX_IMAGE_SIZE))
+            .build(session_client);
+
+        image.widget.get_style_context().add_class("image-widget");
+
+        bx.pack_start(&image.widget, true, true, 0);
+        bx.show_all();
+
+        Some(image)
+    } else {
+        None
+    };
+
+    (bx, image)
+}
+
+fn build_room_video_player(
+    session_client: MatrixClient,
+    msg: &Message,
+) -> (gtk::Box, Option<Rc<VideoPlayerWidget>>) {
+    let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
+
+    let player = if let Some(url) = msg.url.clone() {
+        let with_controls = false;
+        let player = VideoPlayerWidget::new(with_controls);
+        let start_playing = false;
+        PlayerExt::initialize_stream(
+            player.clone(),
+            session_client,
+            url,
+            bx.clone(),
+            start_playing,
+        );
+
+        let overlay = Overlay::new();
+        let video_widget = player.get_video_widget();
+        video_widget.set_size_request(-1, 390);
+        VideoPlayerWidget::auto_adjust_video_dimensions(&player);
+        overlay.add(&video_widget);
+
+        let play_button = gtk::Button::new();
+        let play_icon = gtk::Image::from_icon_name(
+            Some("media-playback-start-symbolic"),
+            gtk::IconSize::Dialog,
+        );
+        play_button.set_image(Some(&play_icon));
+        play_button.set_halign(gtk::Align::Center);
+        play_button.set_valign(gtk::Align::Center);
+        play_button.get_style_context().add_class("osd");
+        play_button.get_style_context().add_class("play-icon");
+        play_button.get_style_context().add_class("flat");
+        let evid = msg
+            .id
+            .as_ref()
+            .map(|evid| evid.to_string())
+            .unwrap_or_default();
+        let data = glib::Variant::from(evid);
+        play_button.set_action_name(Some("app.open-media-viewer"));
+        play_button.set_action_target_value(Some(&data));
+        overlay.add_overlay(&play_button);
+
+        let menu_button = gtk::MenuButton::new();
+        let three_dot_icon =
+            gtk::Image::from_icon_name(Some("view-more-symbolic"), gtk::IconSize::Button);
+        menu_button.set_image(Some(&three_dot_icon));
+        menu_button.get_style_context().add_class("osd");
+        menu_button.get_style_context().add_class("round-button");
+        menu_button.get_style_context().add_class("flat");
+        menu_button.set_margin_top(12);
+        menu_button.set_margin_end(12);
+        menu_button.set_opacity(0.8);
+        menu_button.set_halign(gtk::Align::End);
+        menu_button.set_valign(gtk::Align::Start);
+        menu_button.connect_size_allocate(|button, allocation| {
+            let diameter = max(allocation.width, allocation.height);
+            button.set_size_request(diameter, diameter);
+        });
+        overlay.add_overlay(&menu_button);
+
+        let evid = msg.id.as_ref();
+        let redactable = msg.redactable;
+        let menu = MessageMenu::new(evid, &RowType::Video, &redactable, None);
+        menu_button.set_popover(Some(&menu.get_popover()));
+
+        let clip_container = ClipContainer::new();
+        clip_container.add(&overlay);
+
+        bx.pack_start(&clip_container, true, true, 0);
+
+        Some(player)
+    } else {
+        None
+    };
+
+    (bx, player)
+}
+
+fn build_room_msg_emote(msg: &Message) -> (gtk::Box, gtk::Label) {
+    let bx = gtk::Box::new(gtk::Orientation::Horizontal, 0);
+    // Use MXID till we have a alias
+    let sname = msg
+        .sender_name
+        .clone()
+        .unwrap_or_else(|| msg.sender.to_string());
+    let msg_label = gtk::Label::new(None);
+    let markup = markup_text(&msg.body);
+
+    msg_label.set_markup(&format!("<b>{}</b> {}", sname, markup));
+    set_label_styles(&msg_label);
+
+    bx.add(&msg_label);
+
+    (bx, msg_label)
+}
+
 fn set_label_styles(w: &gtk::Label) {
     w.set_line_wrap(true);
     w.set_line_wrap_mode(pango::WrapMode::WordChar);
@@ -745,6 +734,76 @@ fn highlight_username(
     None
 }
 
+#[derive(Clone, Debug)]
+pub struct MessageBoxInfoHeader {
+    root: gtk::Box,
+    username_event_box: gtk::EventBox,
+    username: gtk::Label,
+    date: gtk::Label,
+}
+
+impl From<&Message> for MessageBoxInfoHeader {
+    fn from(msg: &Message) -> Self {
+        // info
+        // +----------+------+
+        // | username | date |
+        // +----------+------+
+        let root = gtk::Box::new(gtk::Orientation::Horizontal, 0);
+
+        let username =
+            build_room_msg_username(msg.sender_name.as_deref().unwrap_or(msg.sender.as_str()));
+        let date = build_room_msg_date(&msg.date);
+
+        let username_event_box = gtk::EventBox::new();
+        username_event_box.add(&username);
+
+        root.pack_start(&username_event_box, true, true, 0);
+        root.pack_start(&date, false, false, 0);
+
+        Self {
+            root,
+            username_event_box,
+            username,
+            date,
+        }
+    }
+}
+
+fn build_room_msg_username(uname: &str) -> gtk::Label {
+    let username = gtk::Label::new(Some(uname));
+
+    username.set_ellipsize(pango::EllipsizeMode::End);
+    username.set_justify(gtk::Justification::Left);
+    username.set_halign(gtk::Align::Start);
+    username.get_style_context().add_class("username");
+
+    username
+}
+
+fn build_room_msg_date(dt: &DateTime<Local>) -> gtk::Label {
+    // TODO: get system preference for 12h/24h
+    let use_ampm = false;
+    let format = if use_ampm {
+        // Use 12h time format (AM/PM)
+        i18n("%l∶%M %p")
+    } else {
+        // Use 24 time format
+        i18n("%R")
+    };
+
+    let d = dt.format(&format).to_string();
+
+    let date = gtk::Label::new(None);
+    date.set_markup(&format!("<span alpha=\"60%\">{}</span>", d.trim()));
+    date.set_line_wrap(true);
+    date.set_justify(gtk::Justification::Right);
+    date.set_valign(gtk::Align::Start);
+    date.set_halign(gtk::Align::End);
+    date.get_style_context().add_class("timestamp");
+
+    date
+}
+
 #[derive(PartialEq)]
 enum MsgPartType {
     Normal,
diff --git a/fractal-gtk/src/widgets/room_history.rs b/fractal-gtk/src/widgets/room_history.rs
index 5ccb8ff1..34b6135f 100644
--- a/fractal-gtk/src/widgets/room_history.rs
+++ b/fractal-gtk/src/widgets/room_history.rs
@@ -137,9 +137,9 @@ impl List {
                     Some(content
                         .widget
                         .as_ref()?
-                        .video_player
-                        .clone()
+                        .get_video_player()
                         .expect("The widget of every MessageContent, whose mtype is RowType::Video, must 
have a video_player.")
+                        .clone()
                     )
                 },
                 _ => None,
@@ -255,13 +255,11 @@ enum Element {
 impl Element {
     fn get_listbox_row(&self) -> &gtk::ListBoxRow {
         match self {
-            Element::Message(content) => {
-                &content
-                    .widget
-                    .as_ref()
-                    .expect("The content of every message element must have widget.")
-                    .root
-            }
+            Element::Message(content) => content
+                .widget
+                .as_ref()
+                .expect("The content of every message element must have widget.")
+                .get_widget(),
             Element::NewDivider(widgets) => widgets.get_widget(),
             Element::DayDivider(widget) => widget,
         }
@@ -626,7 +624,7 @@ impl RoomHistory {
             session_client,
             user_info_cache,
             item.clone(),
-            msg_widget.header,
+            msg_widget.has_header(),
             &self.rows,
         ));
         rows.replace_item(i, Element::Message(item));
@@ -658,7 +656,7 @@ impl RoomHistory {
 
         // If the redacted message was a header message let's set
         // the header on the next non-redacted message instead.
-        if msg_widget.header {
+        if msg_widget.has_header() {
             let rows_list_len = rows.list.len();
             if let Some((msg_next_cloned, msg_widget)) = rows
                 .list
@@ -752,8 +750,8 @@ fn create_row(
     if let RowType::Video = row.mtype {
         /* The followign callback requires `Send` but is handled by the gtk main loop */
         let fragile_rows = Fragile::new(Rc::downgrade(rows));
-        PlayerExt::get_player(mb.video_player
-                .as_ref()
+        PlayerExt::get_player(mb
+                .get_video_player()
                 .expect("The widget of every MessageContent, whose mtype is RowType::Video, must have a 
video_player."))
                 .connect_uri_loaded(move |player, _| {
                     if let Some(rows) = fragile_rows.get().upgrade() {


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