content: Add MessageText widget
Start to separate code for MessageRow, to avoid having every message type implementation in the same file.
This commit is contained in:
parent
622850cb21
commit
b653ca7933
4 changed files with 434 additions and 189 deletions
|
@ -75,6 +75,7 @@ src/session/content/item_row.rs
|
||||||
src/session/content/invite.rs
|
src/session/content/invite.rs
|
||||||
src/session/content/markdown_popover.rs
|
src/session/content/markdown_popover.rs
|
||||||
src/session/content/message_row/mod.rs
|
src/session/content/message_row/mod.rs
|
||||||
|
src/session/content/message_row/text.rs
|
||||||
src/session/content/mod.rs
|
src/session/content/mod.rs
|
||||||
src/session/content/room_details/mod.rs
|
src/session/content/room_details/mod.rs
|
||||||
src/session/content/room_history.rs
|
src/session/content/room_history.rs
|
||||||
|
|
|
@ -58,6 +58,7 @@ sources = files(
|
||||||
'session/content/invite.rs',
|
'session/content/invite.rs',
|
||||||
'session/content/markdown_popover.rs',
|
'session/content/markdown_popover.rs',
|
||||||
'session/content/message_row/mod.rs',
|
'session/content/message_row/mod.rs',
|
||||||
|
'session/content/message_row/text.rs',
|
||||||
'session/content/mod.rs',
|
'session/content/mod.rs',
|
||||||
'session/content/room_history.rs',
|
'session/content/room_history.rs',
|
||||||
'session/content/room_details/mod.rs',
|
'session/content/room_details/mod.rs',
|
||||||
|
|
|
@ -1,22 +1,19 @@
|
||||||
|
mod text;
|
||||||
|
|
||||||
use crate::components::Avatar;
|
use crate::components::Avatar;
|
||||||
use adw::{prelude::*, subclass::prelude::*};
|
use adw::{prelude::*, subclass::prelude::*};
|
||||||
use gettextrs::gettext;
|
use gettextrs::gettext;
|
||||||
use gtk::{
|
use gtk::{
|
||||||
gio, glib, glib::clone, glib::signal::SignalHandlerId, pango, subclass::prelude::*,
|
glib, glib::clone, glib::signal::SignalHandlerId, subclass::prelude::*, CompositeTemplate,
|
||||||
CompositeTemplate,
|
|
||||||
};
|
|
||||||
use html2pango::{
|
|
||||||
block::{markup_html, HtmlBlock},
|
|
||||||
html_escape, markup_links,
|
|
||||||
};
|
};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use matrix_sdk::ruma::events::{
|
use matrix_sdk::ruma::events::{
|
||||||
room::message::{FormattedBody, MessageFormat, MessageType, Relation},
|
room::message::{MessageType, Relation},
|
||||||
room::redaction::RoomRedactionEventContent,
|
room::redaction::RoomRedactionEventContent,
|
||||||
AnyMessageEventContent, AnySyncMessageEvent, AnySyncRoomEvent,
|
AnyMessageEventContent, AnySyncMessageEvent, AnySyncRoomEvent,
|
||||||
};
|
};
|
||||||
use sourceview::prelude::*;
|
|
||||||
|
|
||||||
|
use self::text::MessageText;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::session::room::Event;
|
use crate::session::room::Event;
|
||||||
|
|
||||||
|
@ -243,10 +240,12 @@ impl MessageRow {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_content(&self, event: &Event) {
|
fn update_content(&self, event: &Event) {
|
||||||
|
let priv_ = imp::MessageRow::from_instance(self);
|
||||||
let content = self.find_content(event);
|
let content = self.find_content(event);
|
||||||
|
|
||||||
// TODO: create widgets for all event types
|
// TODO: create widgets for all event types
|
||||||
// TODO: display reaction events from event.relates_to()
|
// TODO: display reaction events from event.relates_to()
|
||||||
|
// TODO: we should reuse the already present child widgets when possible
|
||||||
|
|
||||||
match content {
|
match content {
|
||||||
Some(AnyMessageEventContent::RoomMessage(message)) => {
|
Some(AnyMessageEventContent::RoomMessage(message)) => {
|
||||||
|
@ -258,58 +257,24 @@ impl MessageRow {
|
||||||
match msgtype {
|
match msgtype {
|
||||||
MessageType::Audio(_message) => {}
|
MessageType::Audio(_message) => {}
|
||||||
MessageType::Emote(message) => {
|
MessageType::Emote(message) => {
|
||||||
// TODO we need to bind the display name to the sender
|
let child =
|
||||||
if let Some(html_blocks) = message
|
MessageText::emote(message.formatted, message.body, event.sender());
|
||||||
.formatted
|
priv_.content.set_child(Some(&child));
|
||||||
.filter(|formatted| is_valid_formatted_body(formatted))
|
|
||||||
.and_then(|formatted| {
|
|
||||||
let body = FormattedBody {
|
|
||||||
body: format!(
|
|
||||||
"<b>{}</b> {}",
|
|
||||||
event.sender().display_name(),
|
|
||||||
formatted.body
|
|
||||||
),
|
|
||||||
format: MessageFormat::Html,
|
|
||||||
};
|
|
||||||
|
|
||||||
parse_formatted_body(Some(&body))
|
|
||||||
})
|
|
||||||
{
|
|
||||||
self.show_html(html_blocks);
|
|
||||||
} else {
|
|
||||||
self.show_text(
|
|
||||||
&format!(
|
|
||||||
"<b>{}</b> {}",
|
|
||||||
event.sender().display_name(),
|
|
||||||
linkify(&message.body)
|
|
||||||
),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
MessageType::File(_message) => {}
|
MessageType::File(_message) => {}
|
||||||
MessageType::Image(_message) => {}
|
MessageType::Image(_message) => {}
|
||||||
MessageType::Location(_message) => {}
|
MessageType::Location(_message) => {}
|
||||||
MessageType::Notice(message) => {
|
MessageType::Notice(message) => {
|
||||||
// TODO: we should reuse the already present child widgets when possible
|
let child = MessageText::markup(message.formatted, message.body);
|
||||||
if let Some(html_blocks) = parse_formatted_body(message.formatted.as_ref())
|
priv_.content.set_child(Some(&child));
|
||||||
{
|
|
||||||
self.show_html(html_blocks);
|
|
||||||
} else {
|
|
||||||
self.show_text(&linkify(&message.body), true)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
MessageType::ServerNotice(message) => {
|
MessageType::ServerNotice(message) => {
|
||||||
self.show_text(&message.body, false);
|
let child = MessageText::text(message.body);
|
||||||
|
priv_.content.set_child(Some(&child));
|
||||||
}
|
}
|
||||||
MessageType::Text(message) => {
|
MessageType::Text(message) => {
|
||||||
// TODO: we should reuse the already present child widgets when possible
|
let child = MessageText::markup(message.formatted, message.body);
|
||||||
if let Some(html_blocks) = parse_formatted_body(message.formatted.as_ref())
|
priv_.content.set_child(Some(&child));
|
||||||
{
|
|
||||||
self.show_html(html_blocks);
|
|
||||||
} else {
|
|
||||||
self.show_text(&linkify(&message.body), true)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
MessageType::Video(_message) => {}
|
MessageType::Video(_message) => {}
|
||||||
MessageType::VerificationRequest(_message) => {}
|
MessageType::VerificationRequest(_message) => {}
|
||||||
|
@ -320,151 +285,19 @@ impl MessageRow {
|
||||||
}
|
}
|
||||||
Some(AnyMessageEventContent::RoomEncrypted(content)) => {
|
Some(AnyMessageEventContent::RoomEncrypted(content)) => {
|
||||||
warn!("Couldn't decrypt event {:?}", content);
|
warn!("Couldn't decrypt event {:?}", content);
|
||||||
self.show_text(&gettext("Fractal couldn't decrypt this message."), false)
|
let child = MessageText::text(gettext("Fractal couldn't decrypt this message."));
|
||||||
|
priv_.content.set_child(Some(&child));
|
||||||
}
|
}
|
||||||
Some(AnyMessageEventContent::RoomRedaction(_)) => {
|
Some(AnyMessageEventContent::RoomRedaction(_)) => {
|
||||||
self.show_text(&gettext("This message was removed."), false)
|
let child = MessageText::text(gettext("This message was removed."));
|
||||||
}
|
|
||||||
_ => self.show_text(&gettext("Unsupported event"), false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_text(&self, text: &str, use_markup: bool) {
|
|
||||||
let priv_ = imp::MessageRow::from_instance(self);
|
|
||||||
|
|
||||||
let child =
|
|
||||||
if let Some(Ok(child)) = priv_.content.child().map(|w| w.downcast::<gtk::Label>()) {
|
|
||||||
child
|
|
||||||
} else {
|
|
||||||
let child = gtk::Label::new(None);
|
|
||||||
set_label_styles(&child);
|
|
||||||
priv_.content.set_child(Some(&child));
|
priv_.content.set_child(Some(&child));
|
||||||
child
|
|
||||||
};
|
|
||||||
|
|
||||||
if use_markup {
|
|
||||||
child.set_markup(text);
|
|
||||||
} else {
|
|
||||||
child.set_text(text);
|
|
||||||
}
|
}
|
||||||
}
|
_ => {
|
||||||
|
let child = MessageText::text(gettext("Unsupported event"));
|
||||||
fn show_html(&self, blocks: Vec<HtmlBlock>) {
|
|
||||||
let priv_ = imp::MessageRow::from_instance(self);
|
|
||||||
|
|
||||||
let child = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
|
||||||
priv_.content.set_child(Some(&child));
|
priv_.content.set_child(Some(&child));
|
||||||
|
|
||||||
for block in blocks {
|
|
||||||
let widget = create_widget_for_html_block(&block);
|
|
||||||
child.append(&widget);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn linkify(text: &str) -> String {
|
|
||||||
markup_links(&html_escape(text))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_valid_formatted_body(formatted: &FormattedBody) -> bool {
|
|
||||||
formatted.format == MessageFormat::Html && !formatted.body.contains("<!-- raw HTML omitted -->")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_formatted_body(formatted: Option<&FormattedBody>) -> Option<Vec<HtmlBlock>> {
|
|
||||||
formatted
|
|
||||||
.filter(|formatted| is_valid_formatted_body(formatted))
|
|
||||||
.and_then(|formatted| markup_html(&formatted.body).ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_label_styles(w: >k::Label) {
|
|
||||||
w.set_wrap(true);
|
|
||||||
w.set_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);
|
|
||||||
let menu_model: Option<gio::MenuModel> =
|
|
||||||
gtk::Builder::from_resource("/org/gnome/FractalNext/content-item-row-menu.ui")
|
|
||||||
.object("menu_model");
|
|
||||||
w.set_extra_menu(menu_model.as_ref());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_widget_for_html_block(block: &HtmlBlock) -> gtk::Widget {
|
|
||||||
match block {
|
|
||||||
HtmlBlock::Heading(n, s) => {
|
|
||||||
let w = gtk::Label::new(None);
|
|
||||||
set_label_styles(&w);
|
|
||||||
w.set_markup(s);
|
|
||||||
w.add_css_class(&format!("h{}", n));
|
|
||||||
w.upcast::<gtk::Widget>()
|
|
||||||
}
|
|
||||||
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.append(&bullet);
|
|
||||||
h_box.append(&w);
|
|
||||||
w.set_markup(li);
|
|
||||||
bx.append(&h_box);
|
|
||||||
}
|
|
||||||
|
|
||||||
bx.upcast::<gtk::Widget>()
|
|
||||||
}
|
|
||||||
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.append(&bullet);
|
|
||||||
h_box.append(&w);
|
|
||||||
w.set_markup(ol);
|
|
||||||
bx.append(&h_box);
|
|
||||||
}
|
|
||||||
|
|
||||||
bx.upcast::<gtk::Widget>()
|
|
||||||
}
|
|
||||||
HtmlBlock::Code(s) => {
|
|
||||||
let scrolled = gtk::ScrolledWindow::new();
|
|
||||||
scrolled.set_policy(gtk::PolicyType::Automatic, gtk::PolicyType::Never);
|
|
||||||
let buffer = sourceview::Buffer::new(None);
|
|
||||||
buffer.set_highlight_matching_brackets(false);
|
|
||||||
buffer.set_text(s);
|
|
||||||
let view = sourceview::View::with_buffer(&buffer);
|
|
||||||
view.set_editable(false);
|
|
||||||
view.add_css_class("codeview");
|
|
||||||
scrolled.set_child(Some(&view));
|
|
||||||
scrolled.upcast::<gtk::Widget>()
|
|
||||||
}
|
|
||||||
HtmlBlock::Quote(blocks) => {
|
|
||||||
let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
|
||||||
bx.add_css_class("quote");
|
|
||||||
for block in blocks.iter() {
|
|
||||||
let w = create_widget_for_html_block(block);
|
|
||||||
bx.append(&w);
|
|
||||||
}
|
|
||||||
bx.upcast::<gtk::Widget>()
|
|
||||||
}
|
|
||||||
HtmlBlock::Text(s) => {
|
|
||||||
let w = gtk::Label::new(None);
|
|
||||||
set_label_styles(&w);
|
|
||||||
w.set_markup(s);
|
|
||||||
w.upcast::<gtk::Widget>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MessageRow {
|
impl Default for MessageRow {
|
||||||
|
|
410
src/session/content/message_row/text.rs
Normal file
410
src/session/content/message_row/text.rs
Normal file
|
@ -0,0 +1,410 @@
|
||||||
|
use adw::{prelude::BinExt, subclass::prelude::*};
|
||||||
|
use gtk::{gio, glib, pango, prelude::*, subclass::prelude::*};
|
||||||
|
use html2pango::{
|
||||||
|
block::{markup_html, HtmlBlock},
|
||||||
|
html_escape, markup_links,
|
||||||
|
};
|
||||||
|
use matrix_sdk::ruma::events::room::message::{FormattedBody, MessageFormat};
|
||||||
|
use sourceview::prelude::*;
|
||||||
|
|
||||||
|
use crate::session::{room::Member, UserExt};
|
||||||
|
|
||||||
|
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::GEnum)]
|
||||||
|
#[repr(u32)]
|
||||||
|
#[genum(type_name = "TextFormat")]
|
||||||
|
pub enum TextFormat {
|
||||||
|
Text = 0,
|
||||||
|
Markup = 1,
|
||||||
|
Html = 2,
|
||||||
|
Emote = 3,
|
||||||
|
HtmlEmote = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TextFormat {
|
||||||
|
fn default() -> Self {
|
||||||
|
TextFormat::Text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod imp {
|
||||||
|
use super::*;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use std::cell::{Cell, RefCell};
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct MessageText {
|
||||||
|
/// The format of the text message.
|
||||||
|
pub format: Cell<TextFormat>,
|
||||||
|
/// The displayed content of the message.
|
||||||
|
pub body: RefCell<Option<String>>,
|
||||||
|
/// The sender of the message(only used for emotes).
|
||||||
|
pub sender: RefCell<Option<Member>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for MessageText {
|
||||||
|
const NAME: &'static str = "ContentMessageText";
|
||||||
|
type Type = super::MessageText;
|
||||||
|
type ParentType = adw::Bin;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for MessageText {
|
||||||
|
fn properties() -> &'static [glib::ParamSpec] {
|
||||||
|
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||||
|
vec![
|
||||||
|
glib::ParamSpec::new_enum(
|
||||||
|
"format",
|
||||||
|
"Format",
|
||||||
|
"The format of the text message",
|
||||||
|
TextFormat::static_type(),
|
||||||
|
TextFormat::default() as i32,
|
||||||
|
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
|
||||||
|
),
|
||||||
|
glib::ParamSpec::new_string(
|
||||||
|
"body",
|
||||||
|
"Body",
|
||||||
|
"The displayed content of the message",
|
||||||
|
None,
|
||||||
|
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
|
||||||
|
),
|
||||||
|
glib::ParamSpec::new_object(
|
||||||
|
"sender",
|
||||||
|
"Sender",
|
||||||
|
"The sender of the message",
|
||||||
|
Member::static_type(),
|
||||||
|
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
PROPERTIES.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_property(
|
||||||
|
&self,
|
||||||
|
obj: &Self::Type,
|
||||||
|
_id: usize,
|
||||||
|
value: &glib::Value,
|
||||||
|
pspec: &glib::ParamSpec,
|
||||||
|
) {
|
||||||
|
match pspec.name() {
|
||||||
|
"format" => obj.set_format(value.get().unwrap()),
|
||||||
|
"body" => obj.set_body(value.get().unwrap()),
|
||||||
|
"sender" => obj.set_sender(value.get().unwrap()),
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||||
|
match pspec.name() {
|
||||||
|
"format" => obj.format().to_value(),
|
||||||
|
"body" => obj.body().to_value(),
|
||||||
|
"sender" => obj.sender().to_value(),
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn constructed(&self, obj: &Self::Type) {
|
||||||
|
self.parent_constructed(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetImpl for MessageText {}
|
||||||
|
|
||||||
|
impl BinImpl for MessageText {}
|
||||||
|
}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
/// A widget displaying the content of a text message.
|
||||||
|
pub struct MessageText(ObjectSubclass<imp::MessageText>)
|
||||||
|
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageText {
|
||||||
|
// Creates a widget that displays plain text.
|
||||||
|
pub fn text(body: String) -> Self {
|
||||||
|
glib::Object::new(&[("body", &body)]).expect("Failed to create MessageText")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a widget that displays text with markup. It will detect if it should display the body or the formatted body.
|
||||||
|
pub fn markup(formatted: Option<FormattedBody>, body: String) -> Self {
|
||||||
|
if let Some((html_blocks, body)) = formatted
|
||||||
|
.filter(|formatted| is_valid_formatted_body(formatted))
|
||||||
|
.and_then(|formatted| {
|
||||||
|
parse_formatted_body(&formatted.body)
|
||||||
|
.and_then(|blocks| Some((blocks, formatted.body)))
|
||||||
|
})
|
||||||
|
{
|
||||||
|
let self_: Self = glib::Object::new(&[("format", &TextFormat::Html), ("body", &body)])
|
||||||
|
.expect("Failed to create MessageText");
|
||||||
|
|
||||||
|
self_.build_html(html_blocks);
|
||||||
|
self_
|
||||||
|
} else {
|
||||||
|
let self_: Self =
|
||||||
|
glib::Object::new(&[("format", &TextFormat::Markup), ("body", &linkify(&body))])
|
||||||
|
.expect("Failed to create MessageText");
|
||||||
|
|
||||||
|
self_.build();
|
||||||
|
self_
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a widget that displays an emote. It will detect if it should display the body or the formatted body.
|
||||||
|
pub fn emote(formatted: Option<FormattedBody>, body: String, sender: Member) -> Self {
|
||||||
|
if let Some(body) = formatted
|
||||||
|
.filter(|formatted| is_valid_formatted_body(formatted))
|
||||||
|
.and_then(|formatted| {
|
||||||
|
let body = format!("<b>{}</b> {}", sender.display_name(), formatted.body);
|
||||||
|
|
||||||
|
parse_formatted_body(&body).and_then(|_| Some(formatted.body))
|
||||||
|
})
|
||||||
|
{
|
||||||
|
glib::Object::new(&[
|
||||||
|
("format", &TextFormat::HtmlEmote),
|
||||||
|
("body", &body),
|
||||||
|
("sender", &Some(sender)),
|
||||||
|
])
|
||||||
|
.expect("Failed to create MessageText")
|
||||||
|
} else {
|
||||||
|
glib::Object::new(&[
|
||||||
|
("format", &TextFormat::Emote),
|
||||||
|
("body", &linkify(&body)),
|
||||||
|
("sender", &Some(sender)),
|
||||||
|
])
|
||||||
|
.expect("Failed to create MessageText")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_format(&self, format: TextFormat) {
|
||||||
|
let priv_ = imp::MessageText::from_instance(self);
|
||||||
|
|
||||||
|
if format == priv_.format.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
priv_.format.set(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(&self) -> TextFormat {
|
||||||
|
let priv_ = imp::MessageText::from_instance(self);
|
||||||
|
priv_.format.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_body(&self, body: Option<String>) {
|
||||||
|
let priv_ = imp::MessageText::from_instance(self);
|
||||||
|
|
||||||
|
if body.as_ref() == priv_.body.borrow().as_ref() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
priv_.body.replace(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn body(&self) -> Option<String> {
|
||||||
|
let priv_ = imp::MessageText::from_instance(self);
|
||||||
|
priv_.body.borrow().to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_sender(&self, sender: Option<Member>) {
|
||||||
|
let priv_ = imp::MessageText::from_instance(self);
|
||||||
|
|
||||||
|
if sender.as_ref() == priv_.sender.borrow().as_ref() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
priv_.sender.replace(sender);
|
||||||
|
|
||||||
|
if self.format() == TextFormat::Emote || self.format() == TextFormat::HtmlEmote {
|
||||||
|
self.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.notify("sender");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sender(&self) -> Option<Member> {
|
||||||
|
let priv_ = imp::MessageText::from_instance(self);
|
||||||
|
priv_.sender.borrow().to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(&self) {
|
||||||
|
match self.format() {
|
||||||
|
TextFormat::Text => {
|
||||||
|
self.build_text(&self.body().unwrap(), false);
|
||||||
|
}
|
||||||
|
TextFormat::Markup => {
|
||||||
|
self.build_text(&self.body().unwrap(), true);
|
||||||
|
}
|
||||||
|
TextFormat::Html => {
|
||||||
|
let formatted = FormattedBody {
|
||||||
|
body: self.body().unwrap(),
|
||||||
|
format: MessageFormat::Html,
|
||||||
|
};
|
||||||
|
|
||||||
|
let html = parse_formatted_body(&formatted.body).unwrap();
|
||||||
|
self.build_html(html);
|
||||||
|
}
|
||||||
|
TextFormat::Emote => {
|
||||||
|
// TODO: we need to bind the display name to the sender
|
||||||
|
self.build_text(
|
||||||
|
&format!(
|
||||||
|
"<b>{}</b> {}",
|
||||||
|
self.sender().unwrap().display_name(),
|
||||||
|
&self.body().unwrap()
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
TextFormat::HtmlEmote => {
|
||||||
|
// TODO: we need to bind the display name to the sender
|
||||||
|
let formatted = FormattedBody {
|
||||||
|
body: format!(
|
||||||
|
"<b>{}</b> {}",
|
||||||
|
self.sender().unwrap().display_name(),
|
||||||
|
self.body().unwrap()
|
||||||
|
),
|
||||||
|
format: MessageFormat::Html,
|
||||||
|
};
|
||||||
|
|
||||||
|
let html = parse_formatted_body(&formatted.body).unwrap();
|
||||||
|
self.build_html(html);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_text(&self, text: &str, use_markup: bool) {
|
||||||
|
let child = if let Some(Ok(child)) = self.child().map(|w| w.downcast::<gtk::Label>()) {
|
||||||
|
child
|
||||||
|
} else {
|
||||||
|
let child = gtk::Label::new(None);
|
||||||
|
set_label_styles(&child);
|
||||||
|
self.set_child(Some(&child));
|
||||||
|
child
|
||||||
|
};
|
||||||
|
|
||||||
|
if use_markup {
|
||||||
|
child.set_markup(text);
|
||||||
|
} else {
|
||||||
|
child.set_text(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_html(&self, blocks: Vec<HtmlBlock>) {
|
||||||
|
let child = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
||||||
|
self.set_child(Some(&child));
|
||||||
|
|
||||||
|
for block in blocks {
|
||||||
|
let widget = create_widget_for_html_block(&block);
|
||||||
|
child.append(&widget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn linkify(text: &str) -> String {
|
||||||
|
markup_links(&html_escape(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid_formatted_body(formatted: &FormattedBody) -> bool {
|
||||||
|
formatted.format == MessageFormat::Html && !formatted.body.contains("<!-- raw HTML omitted -->")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_formatted_body(formatted: &str) -> Option<Vec<HtmlBlock>> {
|
||||||
|
markup_html(formatted).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_label_styles(w: >k::Label) {
|
||||||
|
w.set_wrap(true);
|
||||||
|
w.set_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);
|
||||||
|
let menu_model: Option<gio::MenuModel> =
|
||||||
|
gtk::Builder::from_resource("/org/gnome/FractalNext/content-item-row-menu.ui")
|
||||||
|
.object("menu_model");
|
||||||
|
w.set_extra_menu(menu_model.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_widget_for_html_block(block: &HtmlBlock) -> gtk::Widget {
|
||||||
|
match block {
|
||||||
|
HtmlBlock::Heading(n, s) => {
|
||||||
|
let w = gtk::Label::new(None);
|
||||||
|
set_label_styles(&w);
|
||||||
|
w.set_markup(s);
|
||||||
|
w.add_css_class(&format!("h{}", n));
|
||||||
|
w.upcast::<gtk::Widget>()
|
||||||
|
}
|
||||||
|
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.append(&bullet);
|
||||||
|
h_box.append(&w);
|
||||||
|
w.set_markup(li);
|
||||||
|
bx.append(&h_box);
|
||||||
|
}
|
||||||
|
|
||||||
|
bx.upcast::<gtk::Widget>()
|
||||||
|
}
|
||||||
|
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.append(&bullet);
|
||||||
|
h_box.append(&w);
|
||||||
|
w.set_markup(ol);
|
||||||
|
bx.append(&h_box);
|
||||||
|
}
|
||||||
|
|
||||||
|
bx.upcast::<gtk::Widget>()
|
||||||
|
}
|
||||||
|
HtmlBlock::Code(s) => {
|
||||||
|
let scrolled = gtk::ScrolledWindow::new();
|
||||||
|
scrolled.set_policy(gtk::PolicyType::Automatic, gtk::PolicyType::Never);
|
||||||
|
let buffer = sourceview::Buffer::new(None);
|
||||||
|
buffer.set_highlight_matching_brackets(false);
|
||||||
|
buffer.set_text(s);
|
||||||
|
let view = sourceview::View::with_buffer(&buffer);
|
||||||
|
view.set_editable(false);
|
||||||
|
view.add_css_class("codeview");
|
||||||
|
scrolled.set_child(Some(&view));
|
||||||
|
scrolled.upcast::<gtk::Widget>()
|
||||||
|
}
|
||||||
|
HtmlBlock::Quote(blocks) => {
|
||||||
|
let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
||||||
|
bx.add_css_class("quote");
|
||||||
|
for block in blocks.iter() {
|
||||||
|
let w = create_widget_for_html_block(block);
|
||||||
|
bx.append(&w);
|
||||||
|
}
|
||||||
|
bx.upcast::<gtk::Widget>()
|
||||||
|
}
|
||||||
|
HtmlBlock::Text(s) => {
|
||||||
|
let w = gtk::Label::new(None);
|
||||||
|
set_label_styles(&w);
|
||||||
|
w.set_markup(s);
|
||||||
|
w.upcast::<gtk::Widget>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MessageText {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::text(format!(""))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue