[fractal/split-appop-ui] Further componentize MessageBox
- From: Alejandro Domínguez <aledomu src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/split-appop-ui] Further componentize MessageBox
- Date: Sat, 23 Jan 2021 00:39:45 +0000 (UTC)
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(>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<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<>k::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(>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<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: >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<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<>k::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: >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)]
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]