[fractal] room-history: Replace mentions by Pills
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal] room-history: Replace mentions by Pills
- Date: Tue, 31 May 2022 16:15:54 +0000 (UTC)
commit 65a4b6a89d6f3dc899291012d1643c6feafbe26f
Author: Kévin Commaille <zecakeh tedomum fr>
Date: Sun May 15 10:08:23 2022 +0200
room-history: Replace mentions by Pills
src/components/label_with_widgets.rs | 44 +++-
src/components/toast.rs | 6 +-
.../content/room_history/message_row/mod.rs | 11 +-
.../content/room_history/message_row/text.rs | 290 ++++++++++-----------
4 files changed, 190 insertions(+), 161 deletions(-)
---
diff --git a/src/components/label_with_widgets.rs b/src/components/label_with_widgets.rs
index ae3dea646..db3dcaa55 100644
--- a/src/components/label_with_widgets.rs
+++ b/src/components/label_with_widgets.rs
@@ -50,6 +50,13 @@ mod imp {
None,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
+ glib::ParamSpecString::new(
+ "use-markup",
+ "Use Markup",
+ "Whether the label's text is interpreted as Pango markup.",
+ None,
+ glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+ ),
]
});
@@ -66,6 +73,7 @@ mod imp {
match pspec.name() {
"label" => obj.set_label(value.get().unwrap()),
"placeholder" => obj.set_placeholder(value.get().unwrap()),
+ "use-markup" => obj.set_use_markup(value.get().unwrap()),
_ => unimplemented!(),
}
}
@@ -74,15 +82,20 @@ mod imp {
match pspec.name() {
"label" => obj.label().to_value(),
"placeholder" => obj.placeholder().to_value(),
+ "use-markup" => obj.uses_markup().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
- self.label.set_parent(obj);
- self.label.set_wrap(true);
- self.label.connect_notify_local(
+
+ let label = &self.label;
+ label.set_parent(obj);
+ label.set_wrap(true);
+ label.set_xalign(0.0);
+ label.set_valign(gtk::Align::Start);
+ label.connect_notify_local(
Some("label"),
clone!(@weak obj => move |_, _| {
obj.invalidate_child_widgets();
@@ -170,7 +183,13 @@ glib::wrapper! {
}
impl LabelWithWidgets {
- pub fn new<P: IsA<gtk::Widget>>(label: &str, widgets: Vec<P>) -> Self {
+ /// Create an empty `LabelWithWidget`.
+ pub fn new() -> Self {
+ glib::Object::new(&[]).expect("Failed to create LabelWithWidgets")
+ }
+
+ /// Create a `LabelWithWidget` with the given label and widgets.
+ pub fn with_label_and_widgets<P: IsA<gtk::Widget>>(label: &str, widgets: Vec<P>) -> Self {
let obj: Self =
glib::Object::new(&[("label", &label)]).expect("Failed to create LabelWithWidgets");
// FIXME: use a property for widgets
@@ -214,7 +233,7 @@ impl LabelWithWidgets {
let placeholder = priv_.placeholder.borrow();
let placeholder = placeholder.as_deref().unwrap_or(DEFAULT_PLACEHOLDER);
let label = label.replace(placeholder, OBJECT_REPLACEMENT_CHARACTER);
- priv_.label.set_text(&label);
+ priv_.label.set_label(&label);
}
priv_.text.replace(label);
@@ -343,4 +362,19 @@ impl LabelWithWidgets {
}
}
}
+
+ pub fn uses_markup(&self) -> bool {
+ self.imp().label.uses_markup()
+ }
+
+ /// Sets whether the text of the label contains markup.
+ pub fn set_use_markup(&self, use_markup: bool) {
+ self.imp().label.set_use_markup(use_markup);
+ }
+}
+
+impl Default for LabelWithWidgets {
+ fn default() -> Self {
+ Self::new()
+ }
}
diff --git a/src/components/toast.rs b/src/components/toast.rs
index e7b2c6223..b1698a147 100644
--- a/src/components/toast.rs
+++ b/src/components/toast.rs
@@ -104,7 +104,11 @@ impl Toast {
.build()
.upcast()
} else {
- LabelWithWidgets::new(&self.title().unwrap_or_default(), self.widgets()).upcast()
+ LabelWithWidgets::with_label_and_widgets(
+ &self.title().unwrap_or_default(),
+ self.widgets(),
+ )
+ .upcast()
}
}
}
diff --git a/src/session/content/room_history/message_row/mod.rs
b/src/session/content/room_history/message_row/mod.rs
index 235b52fc9..7c46596f6 100644
--- a/src/session/content/room_history/message_row/mod.rs
+++ b/src/session/content/room_history/message_row/mod.rs
@@ -261,7 +261,12 @@ fn build_content(parent: &adw::Bin, event: &Event, compact: bool) {
parent.set_child(Some(&child));
child
};
- child.emote(message.formatted, message.body, event.sender());
+ child.emote(
+ message.formatted,
+ message.body,
+ event.sender(),
+ &event.room(),
+ );
}
MessageType::File(message) => {
let info = message.info.as_ref();
@@ -320,7 +325,7 @@ fn build_content(parent: &adw::Bin, event: &Event, compact: bool) {
parent.set_child(Some(&child));
child
};
- child.markup(message.formatted, message.body);
+ child.markup(message.formatted, message.body, &event.room());
}
MessageType::ServerNotice(message) => {
let child = if let Some(Ok(child)) =
@@ -344,7 +349,7 @@ fn build_content(parent: &adw::Bin, event: &Event, compact: bool) {
parent.set_child(Some(&child));
child
};
- child.markup(message.formatted, message.body);
+ child.markup(message.formatted, message.body, &event.room());
}
MessageType::Video(message) => {
let child = if let Some(Ok(child)) =
diff --git a/src/session/content/room_history/message_row/text.rs
b/src/session/content/room_history/message_row/text.rs
index a11dd5bbf..4921985d6 100644
--- a/src/session/content/room_history/message_row/text.rs
+++ b/src/session/content/room_history/message_row/text.rs
@@ -1,16 +1,23 @@
use adw::{prelude::BinExt, subclass::prelude::*};
-use gtk::{glib, pango, prelude::*, subclass::prelude::*};
+use gtk::{glib, prelude::*, subclass::prelude::*};
use html2pango::{
block::{markup_html, HtmlBlock},
html_escape, markup_links,
};
use log::warn;
-use matrix_sdk::ruma::events::room::message::{FormattedBody, MessageFormat};
+use matrix_sdk::ruma::{
+ events::room::message::{FormattedBody, MessageFormat},
+ matrix_uri::MatrixId,
+ MatrixToUri, MatrixUri,
+};
use once_cell::sync::Lazy;
use regex::Regex;
use sourceview::prelude::*;
-use crate::session::{room::Member, UserExt};
+use crate::{
+ components::{LabelWithWidgets, Pill, DEFAULT_PLACEHOLDER},
+ session::{room::Member, Room, UserExt},
+};
static EMOJI_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
@@ -27,20 +34,16 @@ static EMOJI_REGEX: Lazy<Regex> = Lazy::new(|| {
.unwrap()
});
-mod imp {
- use std::cell::RefCell;
-
- use once_cell::sync::Lazy;
+enum WithMentions<'a> {
+ Yes(&'a Room),
+ No,
+}
+mod imp {
use super::*;
#[derive(Debug, Default)]
- pub struct MessageText {
- /// 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>>,
- }
+ pub struct MessageText {}
#[glib::object_subclass]
impl ObjectSubclass for MessageText {
@@ -49,56 +52,7 @@ mod imp {
type ParentType = adw::Bin;
}
- impl ObjectImpl for MessageText {
- fn properties() -> &'static [glib::ParamSpec] {
- static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
- vec![
- glib::ParamSpecString::new(
- "body",
- "Body",
- "The displayed content of the message",
- None,
- glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
- ),
- glib::ParamSpecObject::new(
- "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() {
- "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() {
- "body" => obj.body().to_value(),
- "sender" => obj.sender().to_value(),
- _ => unimplemented!(),
- }
- }
-
- fn constructed(&self, obj: &Self::Type) {
- self.parent_constructed(obj);
- }
- }
+ impl ObjectImpl for MessageText {}
impl WidgetImpl for MessageText {}
@@ -107,6 +61,8 @@ mod imp {
glib::wrapper! {
/// A widget displaying the content of a text message.
+ // FIXME: We have to be able to allow text selection and override popover
+ // menu. See https://gitlab.gnome.org/GNOME/gtk/-/issues/4606
pub struct MessageText(ObjectSubclass<imp::MessageText>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
@@ -119,64 +75,34 @@ impl MessageText {
/// Display the given plain text.
pub fn text(&self, body: String) {
- self.build_text(&body, false);
- self.set_body(Some(body));
+ self.build_text(body, WithMentions::No);
}
/// Display the given text with markup.
///
/// It will detect if it should display the body or the formatted body.
- pub fn markup(&self, formatted: Option<FormattedBody>, body: String) {
- if let Some((html_blocks, body)) =
- formatted
- .filter(is_valid_formatted_body)
- .and_then(|formatted| {
- parse_formatted_body(strip_reply(&formatted.body))
- .map(|blocks| (blocks, formatted.body))
- })
+ pub fn markup(&self, formatted: Option<FormattedBody>, body: String, room: &Room) {
+ if let Some(html_blocks) = formatted
+ .filter(is_valid_formatted_body)
+ .and_then(|formatted| parse_formatted_body(strip_reply(&formatted.body)))
{
- self.build_html(html_blocks);
- self.set_body(Some(body));
+ self.build_html(html_blocks, room);
} else {
let body = linkify(strip_reply(&body));
- self.build_text(&body, true);
- self.set_body(Some(body));
+ self.build_text(body, WithMentions::Yes(room));
}
}
- pub fn set_body(&self, body: Option<String>) {
- let priv_ = self.imp();
-
- if body.as_ref() == priv_.body.borrow().as_ref() {
- return;
- }
-
- priv_.body.replace(body);
- }
-
- pub fn body(&self) -> Option<String> {
- self.imp().body.borrow().to_owned()
- }
-
- pub fn set_sender(&self, sender: Option<Member>) {
- let priv_ = self.imp();
-
- if sender.as_ref() == priv_.sender.borrow().as_ref() {
- return;
- }
-
- priv_.sender.replace(sender);
- self.notify("sender");
- }
-
- pub fn sender(&self) -> Option<Member> {
- self.imp().sender.borrow().to_owned()
- }
-
/// Display the given emote for `sender`.
///
/// It will detect if it should display the body or the formatted body.
- pub fn emote(&self, formatted: Option<FormattedBody>, body: String, sender: Member) {
+ pub fn emote(
+ &self,
+ formatted: Option<FormattedBody>,
+ body: String,
+ sender: Member,
+ room: &Room,
+ ) {
if let Some(body) = formatted
.filter(is_valid_formatted_body)
.and_then(|formatted| {
@@ -185,54 +111,55 @@ impl MessageText {
parse_formatted_body(&body).map(|_| formatted.body)
})
{
- // TODO: we need to bind the display name to the sender
let formatted = FormattedBody {
body: format!("<b>{}</b> {}", sender.display_name(), strip_reply(&body)),
format: MessageFormat::Html,
};
let html = parse_formatted_body(&formatted.body).unwrap();
- self.build_html(html);
- self.set_body(Some(body));
- self.set_sender(Some(sender));
+ self.build_html(html, room);
} else {
- // TODO: we need to bind the display name to the sender
- let body = linkify(&body);
- self.build_text(&format!("<b>{}</b> {}", sender.display_name(), &body), true);
- self.set_body(Some(body));
- self.set_sender(Some(sender));
+ self.build_text(
+ format!("<b>{}</b> {}", sender.display_name(), linkify(&body)),
+ WithMentions::Yes(room),
+ );
}
}
- fn build_text(&self, text: &str, use_markup: bool) {
- let child = if let Some(Ok(child)) = self.child().map(|w| w.downcast::<gtk::Label>()) {
+ fn build_text(&self, text: String, with_mentions: WithMentions) {
+ let child = if let Some(Ok(child)) = self.child().map(|w| w.downcast::<LabelWithWidgets>())
+ {
child
} else {
- let child = gtk::Label::new(None);
- set_label_styles(&child);
+ let child = LabelWithWidgets::new();
self.set_child(Some(&child));
child
};
- if EMOJI_REGEX.is_match(text) {
+ if EMOJI_REGEX.is_match(&text) {
child.add_css_class("emoji");
} else {
child.remove_css_class("emoji");
}
- if use_markup {
- child.set_markup(text);
+ if let WithMentions::Yes(room) = with_mentions {
+ let (label, widgets) = extract_mentions(&text, room);
+ child.set_use_markup(true);
+ child.set_label(Some(label));
+ child.set_widgets(widgets);
} else {
- child.set_text(text);
+ child.set_use_markup(false);
+ child.set_widgets(Vec::<gtk::Widget>::new());
+ child.set_label(Some(text));
}
}
- fn build_html(&self, blocks: Vec<HtmlBlock>) {
+ fn build_html(&self, blocks: Vec<HtmlBlock>, room: &Room) {
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);
+ let widget = create_widget_for_html_block(&block, room);
child.append(&widget);
}
}
@@ -250,24 +177,12 @@ 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);
- // FIXME: We have to be able to allow text selection and override popover
- // menu. See https://gitlab.gnome.org/GNOME/gtk/-/issues/4606
- // w.set_selectable(true);
-}
-
-fn create_widget_for_html_block(block: &HtmlBlock) -> gtk::Widget {
+fn create_widget_for_html_block(block: &HtmlBlock, room: &Room) -> gtk::Widget {
match block {
HtmlBlock::Heading(n, s) => {
- let w = gtk::Label::new(None);
- set_label_styles(&w);
- w.set_markup(s);
+ let (label, widgets) = extract_mentions(s, room);
+ let w = LabelWithWidgets::with_label_and_widgets(&label, widgets);
+ w.set_use_markup(true);
w.add_css_class(&format!("h{}", n));
w.upcast::<gtk::Widget>()
}
@@ -280,11 +195,11 @@ fn create_widget_for_html_block(block: &HtmlBlock) -> gtk::Widget {
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);
+ let (label, widgets) = extract_mentions(li, room);
+ let w = LabelWithWidgets::with_label_and_widgets(&label, widgets);
+ w.set_use_markup(true);
h_box.append(&bullet);
h_box.append(&w);
- w.set_markup(li);
bx.append(&h_box);
}
@@ -299,11 +214,11 @@ fn create_widget_for_html_block(block: &HtmlBlock) -> gtk::Widget {
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);
+ let (label, widgets) = extract_mentions(ol, room);
+ let w = LabelWithWidgets::with_label_and_widgets(&label, widgets);
+ w.set_use_markup(true);
h_box.append(&bullet);
h_box.append(&w);
- w.set_markup(ol);
bx.append(&h_box);
}
@@ -326,15 +241,15 @@ fn create_widget_for_html_block(block: &HtmlBlock) -> gtk::Widget {
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);
+ let w = create_widget_for_html_block(block, room);
bx.append(&w);
}
bx.upcast::<gtk::Widget>()
}
HtmlBlock::Text(s) => {
- let w = gtk::Label::new(None);
- set_label_styles(&w);
- w.set_markup(s);
+ let (label, widgets) = extract_mentions(s, room);
+ let w = LabelWithWidgets::with_label_and_widgets(&label, widgets);
+ w.set_use_markup(true);
w.upcast::<gtk::Widget>()
}
}
@@ -355,6 +270,77 @@ fn strip_reply(text: &str) -> &str {
}
}
+/// Extract mentions from the given string.
+///
+/// Returns a new string with placeholders and the corresponding widgets.
+fn extract_mentions(s: &str, room: &Room) -> (String, Vec<Pill>) {
+ let session = room.session();
+ let mut label = s.to_owned();
+ let mut widgets = vec![];
+
+ // The markup has been normalized by html2pango so we are sure of the format of
+ // links.
+ for (start, _) in s.rmatch_indices("<a href=") {
+ let uri_start = start + 9;
+ let link = &s[uri_start..];
+
+ let uri_end = if let Some(end) = link.find('"') {
+ end
+ } else {
+ continue;
+ };
+
+ let uri = &link[..uri_end];
+
+ let id = if let Ok(mx_uri) = MatrixUri::parse(uri) {
+ mx_uri.id().to_owned()
+ } else if let Ok(mx_to_uri) = MatrixToUri::parse(uri) {
+ mx_to_uri.id().to_owned()
+ } else {
+ continue;
+ };
+
+ let pill = match id {
+ MatrixId::Room(room_id) => {
+ if let Some(room) = session.room_list().get(&room_id) {
+ Pill::for_room(&room)
+ } else {
+ continue;
+ }
+ }
+ MatrixId::RoomAlias(room_alias) => {
+ // TODO: Handle non-canonical aliases.
+ if let Some(room) = session.client().rooms().iter().find_map(|matrix_room| {
+ matrix_room
+ .canonical_alias()
+ .filter(|alias| alias == &room_alias)
+ .and_then(|_| session.room_list().get(matrix_room.room_id()))
+ }) {
+ Pill::for_room(&room)
+ } else {
+ continue;
+ }
+ }
+ MatrixId::User(user_id) => {
+ let user = room.members().member_by_id(user_id).upcast();
+ Pill::for_user(&user)
+ }
+ _ => continue,
+ };
+
+ let end = if let Some(end) = link.find("</a>") {
+ uri_start + end + 4
+ } else {
+ continue;
+ };
+
+ label.replace_range(start..end, DEFAULT_PLACEHOLDER);
+ widgets.insert(0, pill);
+ }
+
+ (label, widgets)
+}
+
impl Default for MessageText {
fn default() -> Self {
Self::new()
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]