diff --git a/Cargo.lock b/Cargo.lock index f70a8f2cca4fc4a52aba4d32a658fbf306451f7f..37b0ee69f4a8d85899151eb4667453b0e3804042 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,9 +117,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.34" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf8dcb5b4bbaa28653b647d8c77bd4ed40183b48882e130c1f1ffb73de069fd7" +checksum = "ee67c11feeac938fae061b232e38e0b6d94f97a9df10e6271319325ac4c56a86" [[package]] name = "arrayref" @@ -1536,9 +1536,9 @@ dependencies = [ [[package]] name = "html2pango" -version = "0.3.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469b284033c3c93e7df758d316cb2e0da4408e4138d5add903b8be356a841738" +checksum = "a2a7f65103a4da1b629f519474a51ae89077c61f88954eb9e6df7b22e1a7fd98" dependencies = [ "ammonia", "anyhow", diff --git a/fractal-gtk/Cargo.toml b/fractal-gtk/Cargo.toml index 937265d50cb9edd5bbb1b80c69e223de1fdfbd20..44d6ec928a0c4f36283fda6424acf286c05a3855 100644 --- a/fractal-gtk/Cargo.toml +++ b/fractal-gtk/Cargo.toml @@ -6,7 +6,7 @@ workspace = "../" edition = "2018" [dependencies] -anyhow = "1.0.32" +anyhow = "1.0.37" async-trait = "0.1.40" clap = "2.33.0" chrono = "0.4.10" @@ -19,7 +19,7 @@ gdk = "0.13.0" gdk-pixbuf = "0.9.0" gstreamer-pbutils = "0.16.0" glib = "0.10.1" -html2pango = "0.3.2" +html2pango = "0.4.1" http = "0.2.1" itertools = "0.8.2" lazy_static = "1.4.0" diff --git a/fractal-gtk/res/app.css b/fractal-gtk/res/app.css index 3a6c598dfa16dc502c9d5b61177f7076cb1e6c59..287913f4c0f9ad979d156756280967c59c094aaf 100644 --- a/fractal-gtk/res/app.css +++ b/fractal-gtk/res/app.css @@ -412,6 +412,34 @@ box.folded-history padding-right: 6px; } +.h1, .h2, .h3, .h4, .h5, .h6 { + font-weight: bold; +} + +.h1 { + font-size: xx-large; +} + +.h2 { + font-size: x-large; +} + +.h3 { + font-size: large; +} + +.codeview { + border-radius: 5px; + padding: 6px; + font-family: monospace; + background-color: @text_view_bg; + color: @theme_text_color; +} + +.codeview > text { + background: none; +} + #clip-container { border-radius: 6px; } diff --git a/fractal-gtk/src/widgets/message.rs b/fractal-gtk/src/widgets/message.rs index 2c89063a71503650a8b64c4a4c71a0e0aca605f3..f3429a75ba4a94c618d86ca8214483978d633a76 100644 --- a/fractal-gtk/src/widgets/message.rs +++ b/fractal-gtk/src/widgets/message.rs @@ -10,12 +10,15 @@ use crate::widgets::message_menu::MessageMenu; use crate::widgets::AvatarExt; use crate::widgets::ClipContainer; use crate::widgets::{AudioPlayerWidget, PlayerExt, VideoPlayerWidget}; +use anyhow::Context; use chrono::prelude::*; use either::Either; use glib::clone; use gtk::{prelude::*, ButtonExt, ContainerExt, LabelExt, Overlay, WidgetExt}; +use html2pango::block::{markup_html, HtmlBlock}; use itertools::Itertools; use matrix_sdk::Client as MatrixClient; +use sourceview4::BufferExt; use std::cmp::max; use std::rc::Rc; @@ -169,11 +172,11 @@ impl MessageBoxContainer { None } - fn connect_right_click_menu(&self, msg: &Message, label: Option<>k::Label>) -> Option<()> { + fn connect_right_click_menu(&self, msg: &Message, w: Option<>k::Widget>) -> Option<()> { let mtype = msg.mtype; let redactable = msg.redactable; - let widget = if let Some(l) = label { - l.upcast_ref::() + let widget = if let Some(l) = w { + l } else { self.eventbox.upcast_ref::() }; @@ -509,7 +512,7 @@ fn build_room_msg( container.connect_media_viewer(msg); } MessageBodyType::Emote(ref msg_label) => { - container.connect_right_click_menu(msg, Some(msg_label)); + container.connect_right_click_menu(msg, Some(msg_label.upcast_ref::())); } _ => {} } @@ -517,6 +520,113 @@ fn build_room_msg( (body, type_extras) } +fn build_room_msg_body_html( + container: &MessageBoxContainer, + msg: &Message, +) -> anyhow::Result { + let raw = msg.msg.formatted_body.clone().unwrap_or_default(); + + if raw.contains("") { + anyhow::bail!("Empty message omited: , using plain text instead."); + } + + let blocks = + markup_html(&raw).with_context(|| format!("Could not render message: {}", &raw))?; + let bx = gtk::Box::new(gtk::Orientation::Vertical, 6); + for b in blocks { + let widget = render_html_block(container, msg, &b); + bx.add(&widget); + } + Ok(bx) +} + +fn render_html_block( + container: &MessageBoxContainer, + msg: &Message, + block: &HtmlBlock, +) -> gtk::Widget { + let widget = match block { + HtmlBlock::Heading(n, s) => { + let w = gtk::Label::new(None); + set_label_styles(&w); + w.set_markup(&s); + w.get_style_context().add_class(&format!("h{}", n)); + container.connect_right_click_menu(msg, Some(&w.upcast_ref::())); + w.upcast::() + } + HtmlBlock::UList(elements) => { + let bx = gtk::Box::new(gtk::Orientation::Vertical, 6); + bx.set_margin_end(6); + bx.set_margin_start(6); + + for li in elements.iter() { + let h_box = gtk::Box::new(gtk::Orientation::Horizontal, 6); + let bullet = gtk::Label::new(Some("•")); + bullet.set_valign(gtk::Align::Start); + let w = gtk::Label::new(None); + set_label_styles(&w); + h_box.add(&bullet); + h_box.add(&w); + w.set_markup(&li); + container.connect_right_click_menu(msg, Some(&w.upcast_ref::())); + bx.add(&h_box); + } + + bx.upcast::() + } + HtmlBlock::OList(elements) => { + let bx = gtk::Box::new(gtk::Orientation::Vertical, 6); + bx.set_margin_end(6); + bx.set_margin_start(6); + + for (i, ol) in elements.iter().enumerate() { + let h_box = gtk::Box::new(gtk::Orientation::Horizontal, 6); + let bullet = gtk::Label::new(Some(&format!("{}.", i + 1))); + bullet.set_valign(gtk::Align::Start); + let w = gtk::Label::new(None); + set_label_styles(&w); + h_box.add(&bullet); + h_box.add(&w); + w.set_markup(&ol); + bx.add(&h_box); + container.connect_right_click_menu(msg, Some(&w.upcast_ref::())); + } + + bx.upcast::() + } + HtmlBlock::Code(s) => { + let scrolled = gtk::ScrolledWindow::new(gtk::NONE_ADJUSTMENT, gtk::NONE_ADJUSTMENT); + scrolled.set_policy(gtk::PolicyType::Automatic, gtk::PolicyType::Never); + let buffer = sourceview4::Buffer::new::(None); + buffer.set_highlight_matching_brackets(false); + buffer.set_text(&s); + let view = sourceview4::View::with_buffer(&buffer); + view.set_editable(false); + view.get_style_context().add_class("codeview"); + container.connect_right_click_menu(msg, Some(&view.upcast_ref::())); + scrolled.add(&view); + scrolled.upcast::() + } + HtmlBlock::Quote(blocks) => { + let bx = gtk::Box::new(gtk::Orientation::Vertical, 6); + bx.get_style_context().add_class("quote"); + for b in blocks.iter() { + let w = render_html_block(container, msg, &b); + bx.add(&w); + } + bx.upcast::() + } + HtmlBlock::Text(s) => { + let w = gtk::Label::new(None); + set_label_styles(&w); + w.set_markup(&s); + container.connect_right_click_menu(msg, Some(&w.upcast_ref::())); + w.upcast::() + } + }; + widget +} + 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() { @@ -751,6 +861,15 @@ fn build_room_msg_file(msg: &Message) -> BodyAndType { } fn build_room_msg_body(container: &MessageBoxContainer, msg: &Message) -> BodyAndType { + let bx = match msg.msg.format.as_deref() { + Some("org.matrix.custom.html") => build_room_msg_body_html(container, &msg) + .unwrap_or_else(|_err| build_room_msg_body_text(container, &msg)), + _ => build_room_msg_body_text(container, &msg), + }; + (bx, MessageBodyType::Text) +} + +fn build_room_msg_body_text(container: &MessageBoxContainer, msg: &Message) -> gtk::Box { 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)); @@ -807,11 +926,11 @@ fn build_room_msg_body(container: &MessageBoxContainer, msg: &Message) -> BodyAn part.set_attributes(Some(&attr)); } - container.connect_right_click_menu(msg, Some(&part)); + container.connect_right_click_menu(msg, Some(&part.upcast_ref::())); bx.add(&part); } - (bx, MessageBodyType::Text) + bx } #[derive(Clone, Debug)]