[fractal] message-row: Allow to embed messages content preview
- From: Kévin Commaille <kcommaille src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal] message-row: Allow to embed messages content preview
- Date: Thu, 15 Sep 2022 16:56:39 +0000 (UTC)
commit 8127a52199bf94edbff31629c58f76b3e3159953
Author: Kévin Commaille <zecakeh tedomum fr>
Date: Fri Jul 15 12:42:11 2022 +0200
message-row: Allow to embed messages content preview
Provide a more compact format for message content.
data/resources/ui/content-message-row.ui | 2 +-
po/POTFILES.in | 2 +-
src/components/label_with_widgets.rs | 78 ++++-
src/components/location_viewer.rs | 59 ++++
.../content/room_history/message_row/audio.rs | 5 +-
.../content/room_history/message_row/content.rs | 370 +++++++++++++++++++++
.../content/room_history/message_row/file.rs | 9 +
.../content/room_history/message_row/location.rs | 27 +-
.../content/room_history/message_row/media.rs | 10 +-
.../content/room_history/message_row/mod.rs | 275 ++-------------
.../content/room_history/message_row/text.rs | 129 +++++--
11 files changed, 661 insertions(+), 305 deletions(-)
---
diff --git a/data/resources/ui/content-message-row.ui b/data/resources/ui/content-message-row.ui
index 17c679553..05addb775 100644
--- a/data/resources/ui/content-message-row.ui
+++ b/data/resources/ui/content-message-row.ui
@@ -41,7 +41,7 @@
</object>
</child>
<child>
- <object class="AdwBin" id="content">
+ <object class="ContentMessageContent" id="content">
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<style>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index b58010e62..41468cb53 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -62,8 +62,8 @@ src/session/content/room_details/member_page/mod.rs
src/session/content/room_details/mod.rs
src/session/content/room_history/item_row.rs
src/session/content/room_history/message_row/audio.rs
+src/session/content/room_history/message_row/content.rs
src/session/content/room_history/message_row/media.rs
-src/session/content/room_history/message_row/mod.rs
src/session/content/room_history/mod.rs
src/session/content/room_history/state_row/creation.rs
src/session/content/room_history/state_row/mod.rs
diff --git a/src/components/label_with_widgets.rs b/src/components/label_with_widgets.rs
index b13dd95e2..4052f4a99 100644
--- a/src/components/label_with_widgets.rs
+++ b/src/components/label_with_widgets.rs
@@ -10,7 +10,7 @@ fn pango_pixels(d: i32) -> i32 {
}
mod imp {
- use std::cell::RefCell;
+ use std::cell::{Cell, RefCell};
use super::*;
@@ -21,6 +21,7 @@ mod imp {
pub label: gtk::Label,
pub placeholder: RefCell<Option<String>>,
pub text: RefCell<Option<String>>,
+ pub ellipsize: Cell<bool>,
}
#[glib::object_subclass]
@@ -57,6 +58,13 @@ mod imp {
None,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
+ glib::ParamSpecString::new(
+ "ellipsize",
+ "Ellipsize",
+ "Whether the label's text should be ellipsized.",
+ None,
+ glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+ ),
]
});
@@ -74,6 +82,7 @@ mod imp {
"label" => obj.set_label(value.get().unwrap()),
"placeholder" => obj.set_placeholder(value.get().unwrap()),
"use-markup" => obj.set_use_markup(value.get().unwrap()),
+ "ellipsize" => obj.set_ellipsize(value.get().unwrap()),
_ => unimplemented!(),
}
}
@@ -83,6 +92,7 @@ mod imp {
"label" => obj.label().to_value(),
"placeholder" => obj.placeholder().to_value(),
"use-markup" => obj.uses_markup().to_value(),
+ "ellipsize" => obj.ellipsize().to_value(),
_ => unimplemented!(),
}
}
@@ -234,15 +244,8 @@ impl LabelWithWidgets {
return;
}
- if let Some(ref label) = label {
- 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_label(&label);
- }
-
priv_.text.replace(label);
- self.invalidate_child_widgets();
+ self.update_label();
self.notify("label");
}
@@ -257,14 +260,8 @@ impl LabelWithWidgets {
return;
}
- if let Some(text) = &*priv_.text.borrow() {
- let placeholder = placeholder.as_deref().unwrap_or(DEFAULT_PLACEHOLDER);
- let label = text.replace(placeholder, OBJECT_REPLACEMENT_CHARACTER);
- priv_.label.set_text(&label);
- }
-
priv_.placeholder.replace(placeholder);
- self.invalidate_child_widgets();
+ self.update_label();
self.notify("placeholder");
}
@@ -376,6 +373,55 @@ impl LabelWithWidgets {
pub fn set_use_markup(&self, use_markup: bool) {
self.imp().label.set_use_markup(use_markup);
}
+
+ /// Whether the text of the label is ellipsized.
+ pub fn ellipsize(&self) -> bool {
+ self.imp().ellipsize.get()
+ }
+
+ /// Sets whether the text of the label should be ellipsized.
+ pub fn set_ellipsize(&self, ellipsize: bool) {
+ if self.ellipsize() == ellipsize {
+ return;
+ }
+
+ self.imp().ellipsize.set(true);
+ self.update_label();
+ self.notify("ellipsize");
+ }
+
+ fn update_label(&self) {
+ let priv_ = self.imp();
+ if self.ellipsize() {
+ // Workaround: if both wrap and ellipsize are set, and there are
+ // widgets inserted, GtkLabel reports an erroneous minimum width.
+ priv_.label.set_wrap(false);
+ priv_.label.set_ellipsize(pango::EllipsizeMode::End);
+
+ if let Some(label) = priv_.text.borrow().as_ref() {
+ let placeholder = priv_.placeholder.borrow();
+ let placeholder = placeholder.as_deref().unwrap_or(DEFAULT_PLACEHOLDER);
+ let label = label.replace(placeholder, OBJECT_REPLACEMENT_CHARACTER);
+ let label = if let Some(pos) = label.find('\n') {
+ format!("{}…", &label[0..pos])
+ } else {
+ label
+ };
+ priv_.label.set_label(&label);
+ }
+ } else {
+ priv_.label.set_wrap(true);
+ priv_.label.set_ellipsize(pango::EllipsizeMode::None);
+
+ if let Some(label) = priv_.text.borrow().as_ref() {
+ 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_label(&label);
+ }
+ }
+ self.invalidate_child_widgets();
+ }
}
impl Default for LabelWithWidgets {
diff --git a/src/components/location_viewer.rs b/src/components/location_viewer.rs
index ce7468490..4159f8f61 100644
--- a/src/components/location_viewer.rs
+++ b/src/components/location_viewer.rs
@@ -5,7 +5,10 @@ use shumate::prelude::*;
use crate::i18n::gettext_f;
mod imp {
+ use std::cell::Cell;
+
use glib::subclass::InitializingObject;
+ use once_cell::sync::Lazy;
use super::*;
@@ -17,6 +20,7 @@ mod imp {
#[template_child]
pub marker_img: TemplateChild<gtk::Image>,
pub marker: shumate::Marker,
+ pub compact: Cell<bool>,
}
#[glib::object_subclass]
@@ -36,6 +40,40 @@ mod imp {
}
impl ObjectImpl for LocationViewer {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpecBoolean::new(
+ "compact",
+ "Compact",
+ "Whether to display this location in a compact format",
+ false,
+ 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() {
+ "compact" => obj.set_compact(value.get().unwrap()),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "compact" => obj.compact().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+
fn constructed(&self, obj: &Self::Type) {
self.marker.set_child(Some(&*self.marker_img));
@@ -72,6 +110,27 @@ impl LocationViewer {
glib::Object::new(&[]).expect("Failed to create LocationViewer")
}
+ /// Whether to display this location in a compact format.
+ pub fn compact(&self) -> bool {
+ self.imp().compact.get()
+ }
+
+ /// Set the compact format of this location.
+ pub fn set_compact(&self, compact: bool) {
+ if self.compact() == compact {
+ return;
+ }
+
+ let map = &self.imp().map;
+ map.set_show_zoom_buttons(!compact);
+ if let Some(license) = map.license() {
+ license.set_visible(!compact);
+ }
+
+ self.imp().compact.set(compact);
+ self.notify("compact");
+ }
+
pub fn set_geo_uri(&self, uri: &str) {
let imp = self.imp();
diff --git a/src/session/content/room_history/message_row/audio.rs
b/src/session/content/room_history/message_row/audio.rs
index 9069765fd..64bd32d24 100644
--- a/src/session/content/room_history/message_row/audio.rs
+++ b/src/session/content/room_history/message_row/audio.rs
@@ -8,7 +8,7 @@ use gtk::{
use log::warn;
use matrix_sdk::{media::MediaEventContent, ruma::events::room::message::AudioMessageEventContent};
-use super::media::MediaState;
+use super::{media::MediaState, ContentFormat};
use crate::{components::AudioPlayer, session::Session, spawn, spawn_tokio, utils::media_type_uid};
mod imp {
@@ -198,9 +198,10 @@ impl MessageAudio {
}
/// Display the given `audio` message.
- pub fn audio(&self, audio: AudioMessageEventContent, session: &Session, compact: bool) {
+ pub fn audio(&self, audio: AudioMessageEventContent, session: &Session, format: ContentFormat) {
self.set_body(Some(audio.body.clone()));
+ let compact = matches!(format, ContentFormat::Compact | ContentFormat::Ellipsized);
self.set_compact(compact);
if compact {
self.set_state(MediaState::Ready);
diff --git a/src/session/content/room_history/message_row/content.rs
b/src/session/content/room_history/message_row/content.rs
new file mode 100644
index 000000000..48e7f8215
--- /dev/null
+++ b/src/session/content/room_history/message_row/content.rs
@@ -0,0 +1,370 @@
+use adw::{prelude::*, subclass::prelude::*};
+use gettextrs::gettext;
+use gtk::{glib, glib::clone};
+use log::warn;
+use matrix_sdk::ruma::events::{
+ room::message::{MessageType, Relation},
+ AnyMessageLikeEventContent,
+};
+
+use super::{
+ audio::MessageAudio, file::MessageFile, location::MessageLocation, media::MessageMedia,
+ reply::MessageReply, text::MessageText,
+};
+use crate::{prelude::*, session::room::SupportedEvent, spawn, utils::filename_for_mime};
+
+#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
+#[repr(i32)]
+#[enum_type(name = "ContentFormat")]
+pub enum ContentFormat {
+ /// The content should appear at its natural size.
+ Natural = 0,
+
+ /// The content should appear in a smaller format without interactions, if
+ /// possible.
+ ///
+ /// This has no effect on text replies.
+ ///
+ /// The related events of replies are not displayed.
+ Compact = 1,
+
+ /// Like `Compact`, but the content should be ellipsized if possible to show
+ /// only a single line.
+ Ellipsized = 2,
+}
+
+impl Default for ContentFormat {
+ fn default() -> Self {
+ Self::Natural
+ }
+}
+
+mod imp {
+ use std::cell::Cell;
+
+ use once_cell::sync::Lazy;
+
+ use super::*;
+
+ #[derive(Debug, Default)]
+ pub struct MessageContent {
+ pub format: Cell<ContentFormat>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for MessageContent {
+ const NAME: &'static str = "ContentMessageContent";
+ type Type = super::MessageContent;
+ type ParentType = adw::Bin;
+ }
+
+ impl ObjectImpl for MessageContent {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpecEnum::new(
+ "format",
+ "Format",
+ "The displayed format of the message",
+ ContentFormat::static_type(),
+ ContentFormat::default() as i32,
+ 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()),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "format" => obj.format().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+
+ impl WidgetImpl for MessageContent {}
+ impl BinImpl for MessageContent {}
+}
+
+glib::wrapper! {
+ pub struct MessageContent(ObjectSubclass<imp::MessageContent>)
+ @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl MessageContent {
+ pub fn new() -> Self {
+ glib::Object::new(&[]).expect("Failed to create MessageContent")
+ }
+
+ pub fn format(&self) -> ContentFormat {
+ self.imp().format.get()
+ }
+
+ pub fn set_format(&self, format: ContentFormat) {
+ if self.format() == format {
+ return;
+ }
+
+ self.imp().format.set(format);
+ self.notify("format");
+ }
+
+ pub fn update_for_event(&self, event: &SupportedEvent) {
+ let format = self.format();
+ if format == ContentFormat::Natural && event.is_reply() {
+ spawn!(
+ glib::PRIORITY_HIGH,
+ clone!(@weak self as obj, @weak event => async move {
+ if let Some(related_event) = event
+ .reply_to_event()
+ .await
+ .ok()
+ .flatten()
+ .and_then(|event| event.downcast::<SupportedEvent>().ok())
+ {
+ let reply = MessageReply::new();
+ reply.set_related_content_sender(related_event.sender().upcast());
+ build_content(reply.related_content(), &related_event, ContentFormat::Compact);
+ build_content(reply.content(), &event, ContentFormat::Natural);
+ obj.set_child(Some(&reply));
+ } else {
+ build_content(&obj, &event, format);
+ }
+ })
+ );
+ } else {
+ build_content(self, event, format);
+ }
+ }
+}
+
+impl Default for MessageContent {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+/// Build the content widget of `event` as a child of `parent`.
+fn build_content(parent: &impl IsA<adw::Bin>, event: &SupportedEvent, format: ContentFormat) {
+ let parent = parent.upcast_ref();
+ match event.content() {
+ Some(AnyMessageLikeEventContent::RoomMessage(message)) => {
+ let msgtype = if let Some(Relation::Replacement(replacement)) = message.relates_to {
+ replacement.new_content.msgtype
+ } else {
+ message.msgtype
+ };
+ match msgtype {
+ MessageType::Audio(message) => {
+ let child = if let Some(Ok(child)) =
+ parent.child().map(|w| w.downcast::<MessageAudio>())
+ {
+ child
+ } else {
+ let child = MessageAudio::new();
+ parent.set_child(Some(&child));
+ child
+ };
+ child.audio(message, &event.room().session(), format);
+ }
+ MessageType::Emote(message) => {
+ let child = if let Some(Ok(child)) =
+ parent.child().map(|w| w.downcast::<MessageText>())
+ {
+ child
+ } else {
+ let child = MessageText::new();
+ parent.set_child(Some(&child));
+ child
+ };
+ child.emote(
+ message.formatted,
+ message.body,
+ event.sender(),
+ &event.room(),
+ format,
+ );
+ }
+ MessageType::File(message) => {
+ let info = message.info.as_ref();
+ let filename = message
+ .filename
+ .filter(|name| !name.is_empty())
+ .or(Some(message.body))
+ .filter(|name| !name.is_empty())
+ .unwrap_or_else(|| {
+ filename_for_mime(info.and_then(|info| info.mimetype.as_deref()), None)
+ });
+
+ let child = if let Some(Ok(child)) =
+ parent.child().map(|w| w.downcast::<MessageFile>())
+ {
+ child
+ } else {
+ let child = MessageFile::new();
+ parent.set_child(Some(&child));
+ child
+ };
+ child.set_filename(Some(filename));
+ child.set_format(format);
+ }
+ MessageType::Image(message) => {
+ let child = if let Some(Ok(child)) =
+ parent.child().map(|w| w.downcast::<MessageMedia>())
+ {
+ child
+ } else {
+ let child = MessageMedia::new();
+ parent.set_child(Some(&child));
+ child
+ };
+ child.image(message, &event.room().session(), format);
+ }
+ MessageType::Location(message) => {
+ let child = if let Some(Ok(child)) =
+ parent.child().map(|w| w.downcast::<MessageLocation>())
+ {
+ child
+ } else {
+ let child = MessageLocation::new();
+ parent.set_child(Some(&child));
+ child
+ };
+ child.set_geo_uri(&message.geo_uri, format);
+ }
+ MessageType::Notice(message) => {
+ let child = if let Some(Ok(child)) =
+ parent.child().map(|w| w.downcast::<MessageText>())
+ {
+ child
+ } else {
+ let child = MessageText::new();
+ parent.set_child(Some(&child));
+ child
+ };
+ child.markup(message.formatted, message.body, &event.room(), format);
+ }
+ MessageType::ServerNotice(message) => {
+ let child = if let Some(Ok(child)) =
+ parent.child().map(|w| w.downcast::<MessageText>())
+ {
+ child
+ } else {
+ let child = MessageText::new();
+ parent.set_child(Some(&child));
+ child
+ };
+ child.text(message.body, format);
+ }
+ MessageType::Text(message) => {
+ let child = if let Some(Ok(child)) =
+ parent.child().map(|w| w.downcast::<MessageText>())
+ {
+ child
+ } else {
+ let child = MessageText::new();
+ parent.set_child(Some(&child));
+ child
+ };
+ child.markup(message.formatted, message.body, &event.room(), format);
+ }
+ MessageType::Video(message) => {
+ let child = if let Some(Ok(child)) =
+ parent.child().map(|w| w.downcast::<MessageMedia>())
+ {
+ child
+ } else {
+ let child = MessageMedia::new();
+ parent.set_child(Some(&child));
+ child
+ };
+ child.video(message, &event.room().session(), format);
+ }
+ MessageType::VerificationRequest(_) => {
+ // TODO: show more information about the verification
+ let child = if let Some(Ok(child)) =
+ parent.child().map(|w| w.downcast::<MessageText>())
+ {
+ child
+ } else {
+ let child = MessageText::new();
+ parent.set_child(Some(&child));
+ child
+ };
+ child.text(gettext("Identity verification was started"), format);
+ }
+ _ => {
+ warn!("Event not supported: {:?}", msgtype);
+ let child = if let Some(Ok(child)) =
+ parent.child().map(|w| w.downcast::<MessageText>())
+ {
+ child
+ } else {
+ let child = MessageText::new();
+ parent.set_child(Some(&child));
+ child
+ };
+ child.text(gettext("Unsupported event"), format);
+ }
+ }
+ }
+ Some(AnyMessageLikeEventContent::Sticker(content)) => {
+ let child =
+ if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageMedia>()) {
+ child
+ } else {
+ let child = MessageMedia::new();
+ parent.set_child(Some(&child));
+ child
+ };
+ child.sticker(content, &event.room().session(), format);
+ }
+ Some(AnyMessageLikeEventContent::RoomEncrypted(_)) => {
+ let child = if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageText>())
+ {
+ child
+ } else {
+ let child = MessageText::new();
+ parent.set_child(Some(&child));
+ child
+ };
+ child.text(gettext("Unable to decrypt this message, decryption will be retried once the keys are
available."), format);
+ }
+ Some(AnyMessageLikeEventContent::RoomRedaction(_)) => {
+ let child = if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageText>())
+ {
+ child
+ } else {
+ let child = MessageText::new();
+ parent.set_child(Some(&child));
+ child
+ };
+ child.text(gettext("This message was removed."), format);
+ }
+ _ => {
+ warn!("Unsupported event: {:?}", event.content());
+ let child = if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageText>())
+ {
+ child
+ } else {
+ let child = MessageText::new();
+ parent.set_child(Some(&child));
+ child
+ };
+ child.text(gettext("Unsupported event"), format);
+ }
+ }
+}
diff --git a/src/session/content/room_history/message_row/file.rs
b/src/session/content/room_history/message_row/file.rs
index b27110f37..75d6b3390 100644
--- a/src/session/content/room_history/message_row/file.rs
+++ b/src/session/content/room_history/message_row/file.rs
@@ -1,6 +1,8 @@
use adw::subclass::prelude::*;
use gtk::{glib, prelude::*, CompositeTemplate};
+use super::ContentFormat;
+
mod imp {
use std::cell::{Cell, RefCell};
@@ -125,6 +127,13 @@ impl MessageFile {
pub fn compact(&self) -> bool {
self.imp().compact.get()
}
+
+ pub fn set_format(&self, format: ContentFormat) {
+ self.set_compact(matches!(
+ format,
+ ContentFormat::Compact | ContentFormat::Ellipsized
+ ));
+ }
}
impl Default for MessageFile {
diff --git a/src/session/content/room_history/message_row/location.rs
b/src/session/content/room_history/message_row/location.rs
index 12dc03cfe..ab903ce78 100644
--- a/src/session/content/room_history/message_row/location.rs
+++ b/src/session/content/room_history/message_row/location.rs
@@ -1,6 +1,7 @@
use adw::{prelude::*, subclass::prelude::*};
use gtk::{glib, CompositeTemplate};
+use super::ContentFormat;
use crate::components::LocationViewer;
mod imp {
@@ -40,13 +41,26 @@ mod imp {
fn measure(
&self,
_widget: &Self::Type,
- _orientation: gtk::Orientation,
+ orientation: gtk::Orientation,
_for_size: i32,
) -> (i32, i32, i32, i32) {
- (300, 300, -1, -1)
+ if self.location.compact() {
+ if orientation == gtk::Orientation::Horizontal {
+ (75, 75, -1, -1)
+ } else {
+ (50, 50, -1, -1)
+ }
+ } else {
+ (300, 300, -1, -1)
+ }
}
fn size_allocate(&self, _widget: &Self::Type, width: i32, height: i32, baseline: i32) {
+ let width = if self.location.compact() {
+ width.min(75)
+ } else {
+ width
+ };
self.location
.size_allocate(>k::Allocation::new(0, 0, width, height), baseline)
}
@@ -66,7 +80,12 @@ impl MessageLocation {
glib::Object::new(&[]).expect("Failed to create MessageLocation")
}
- pub fn set_geo_uri(&self, uri: &str) {
- self.imp().location.set_geo_uri(uri);
+ pub fn set_geo_uri(&self, uri: &str, format: ContentFormat) {
+ let location = &self.imp().location;
+ location.set_geo_uri(uri);
+ location.set_compact(matches!(
+ format,
+ ContentFormat::Compact | ContentFormat::Ellipsized
+ ))
}
}
diff --git a/src/session/content/room_history/message_row/media.rs
b/src/session/content/room_history/message_row/media.rs
index cc7abf286..a5d854345 100644
--- a/src/session/content/room_history/message_row/media.rs
+++ b/src/session/content/room_history/message_row/media.rs
@@ -18,6 +18,7 @@ use matrix_sdk::{
},
};
+use super::ContentFormat;
use crate::{
components::VideoPlayer,
session::Session,
@@ -336,10 +337,11 @@ impl MessageMedia {
}
/// Display the given `image`, in a `compact` format or not.
- pub fn image(&self, image: ImageMessageEventContent, session: &Session, compact: bool) {
+ pub fn image(&self, image: ImageMessageEventContent, session: &Session, format: ContentFormat) {
let info = image.info.as_deref();
let width = uint_to_i32(info.and_then(|info| info.width));
let height = uint_to_i32(info.and_then(|info| info.height));
+ let compact = matches!(format, ContentFormat::Compact | ContentFormat::Ellipsized);
self.set_width(width);
self.set_height(height);
@@ -348,11 +350,12 @@ impl MessageMedia {
}
/// Display the given `sticker`, in a `compact` format or not.
- pub fn sticker(&self, sticker: StickerEventContent, session: &Session, compact: bool) {
+ pub fn sticker(&self, sticker: StickerEventContent, session: &Session, format: ContentFormat) {
let info = &sticker.info;
let width = uint_to_i32(info.width);
let height = uint_to_i32(info.height);
let body = Some(sticker.body.clone());
+ let compact = matches!(format, ContentFormat::Compact | ContentFormat::Ellipsized);
self.set_width(width);
self.set_height(height);
@@ -361,11 +364,12 @@ impl MessageMedia {
}
/// Display the given `video`, in a `compact` format or not.
- pub fn video(&self, video: VideoMessageEventContent, session: &Session, compact: bool) {
+ pub fn video(&self, video: VideoMessageEventContent, session: &Session, format: ContentFormat) {
let info = &video.info.as_deref();
let width = uint_to_i32(info.and_then(|info| info.width));
let height = uint_to_i32(info.and_then(|info| info.height));
let body = Some(video.body.clone());
+ let compact = matches!(format, ContentFormat::Compact | ContentFormat::Ellipsized);
self.set_width(width);
self.set_height(height);
diff --git a/src/session/content/room_history/message_row/mod.rs
b/src/session/content/room_history/message_row/mod.rs
index 85b925b6e..23c2e8c6f 100644
--- a/src/session/content/room_history/message_row/mod.rs
+++ b/src/session/content/room_history/message_row/mod.rs
@@ -1,4 +1,5 @@
mod audio;
+pub mod content;
mod file;
mod location;
mod media;
@@ -8,25 +9,15 @@ mod reply;
mod text;
use adw::{prelude::*, subclass::prelude::*};
-use gettextrs::gettext;
use gtk::{
glib,
glib::{clone, signal::SignalHandlerId},
CompositeTemplate,
};
-use log::warn;
-use matrix_sdk::ruma::events::{
- room::message::{MessageType, Relation},
- AnyMessageLikeEventContent,
-};
-use self::{
- audio::MessageAudio, file::MessageFile, location::MessageLocation, media::MessageMedia,
- reaction_list::MessageReactionList, reply::MessageReply, text::MessageText,
-};
-use crate::{
- components::Avatar, prelude::*, session::room::SupportedEvent, spawn, utils::filename_for_mime,
-};
+pub use self::content::ContentFormat;
+use self::{content::MessageContent, reaction_list::MessageReactionList};
+use crate::{components::Avatar, prelude::*, session::room::SupportedEvent};
mod imp {
use std::cell::RefCell;
@@ -48,7 +39,7 @@ mod imp {
#[template_child]
pub timestamp: TemplateChild<gtk::Label>,
#[template_child]
- pub content: TemplateChild<adw::Bin>,
+ pub content: TemplateChild<MessageContent>,
#[template_child]
pub reactions: TemplateChild<MessageReactionList>,
pub source_changed_handler: RefCell<Option<SignalHandlerId>>,
@@ -108,7 +99,20 @@ mod imp {
_ => unimplemented!(),
}
}
+
+ fn constructed(&self, obj: &Self::Type) {
+ self.content.connect_notify_local(
+ Some("format"),
+ clone!(@weak obj => move |content, _|
+ obj.imp().reactions.set_visible(!matches!(
+ content.format(),
+ ContentFormat::Compact | ContentFormat::Ellipsized
+ ));
+ ),
+ );
+ }
}
+
impl WidgetImpl for MessageRow {}
impl BinImpl for MessageRow {}
}
@@ -144,6 +148,10 @@ impl MessageRow {
self.notify("show-header");
}
+ pub fn set_content_format(&self, format: ContentFormat) {
+ self.imp().content.set_format(format);
+ }
+
pub fn set_event(&self, event: SupportedEvent) {
let priv_ = self.imp();
// Remove signals and bindings from the previous event
@@ -196,32 +204,7 @@ impl MessageRow {
}
fn update_content(&self, event: &SupportedEvent) {
- if event.is_reply() {
- spawn!(
- glib::PRIORITY_HIGH,
- clone!(@weak self as obj, @weak event => async move {
- let priv_ = obj.imp();
-
- if let Some(related_event) = event
- .reply_to_event()
- .await
- .ok()
- .flatten()
- .and_then(|event| event.downcast::<SupportedEvent>().ok())
- {
- let reply = MessageReply::new();
- reply.set_related_content_sender(related_event.sender().upcast());
- build_content(reply.related_content(), &related_event, true);
- build_content(reply.content(), &event, false);
- priv_.content.set_child(Some(&reply));
- } else {
- build_content(&priv_.content, &event, false);
- }
- })
- );
- } else {
- build_content(&self.imp().content, event, false);
- }
+ self.imp().content.update_for_event(event);
}
}
@@ -230,215 +213,3 @@ impl Default for MessageRow {
Self::new()
}
}
-
-/// Build the content widget of `event` as a child of `parent`.
-///
-/// If `compact` is true, the content should appear in a smaller format without
-/// interactions, if possible.
-fn build_content(parent: &adw::Bin, event: &SupportedEvent, compact: bool) {
- match event.content() {
- Some(AnyMessageLikeEventContent::RoomMessage(message)) => {
- let msgtype = if let Some(Relation::Replacement(replacement)) = message.relates_to {
- replacement.new_content.msgtype
- } else {
- message.msgtype
- };
- match msgtype {
- MessageType::Audio(message) => {
- let child = if let Some(Ok(child)) =
- parent.child().map(|w| w.downcast::<MessageAudio>())
- {
- child
- } else {
- let child = MessageAudio::new();
- parent.set_child(Some(&child));
- child
- };
- child.audio(message, &event.room().session(), compact);
- }
- MessageType::Emote(message) => {
- let child = if let Some(Ok(child)) =
- parent.child().map(|w| w.downcast::<MessageText>())
- {
- child
- } else {
- let child = MessageText::new();
- parent.set_child(Some(&child));
- child
- };
- child.emote(
- message.formatted,
- message.body,
- event.sender(),
- &event.room(),
- );
- }
- MessageType::File(message) => {
- let info = message.info.as_ref();
- let filename = message
- .filename
- .filter(|name| !name.is_empty())
- .or(Some(message.body))
- .filter(|name| !name.is_empty())
- .unwrap_or_else(|| {
- filename_for_mime(info.and_then(|info| info.mimetype.as_deref()), None)
- });
-
- let child = if let Some(Ok(child)) =
- parent.child().map(|w| w.downcast::<MessageFile>())
- {
- child
- } else {
- let child = MessageFile::new();
- parent.set_child(Some(&child));
- child
- };
- child.set_filename(Some(filename));
- child.set_compact(compact);
- }
- MessageType::Image(message) => {
- let child = if let Some(Ok(child)) =
- parent.child().map(|w| w.downcast::<MessageMedia>())
- {
- child
- } else {
- let child = MessageMedia::new();
- parent.set_child(Some(&child));
- child
- };
- child.image(message, &event.room().session(), compact);
- }
- MessageType::Location(message) => {
- let child = if let Some(Ok(child)) =
- parent.child().map(|w| w.downcast::<MessageLocation>())
- {
- child
- } else {
- let child = MessageLocation::new();
- parent.set_child(Some(&child));
- child
- };
- child.set_geo_uri(&message.geo_uri);
- }
- MessageType::Notice(message) => {
- let child = if let Some(Ok(child)) =
- parent.child().map(|w| w.downcast::<MessageText>())
- {
- child
- } else {
- let child = MessageText::new();
- parent.set_child(Some(&child));
- child
- };
- child.markup(message.formatted, message.body, &event.room());
- }
- MessageType::ServerNotice(message) => {
- let child = if let Some(Ok(child)) =
- parent.child().map(|w| w.downcast::<MessageText>())
- {
- child
- } else {
- let child = MessageText::new();
- parent.set_child(Some(&child));
- child
- };
- child.text(message.body);
- }
- MessageType::Text(message) => {
- let child = if let Some(Ok(child)) =
- parent.child().map(|w| w.downcast::<MessageText>())
- {
- child
- } else {
- let child = MessageText::new();
- parent.set_child(Some(&child));
- child
- };
- child.markup(message.formatted, message.body, &event.room());
- }
- MessageType::Video(message) => {
- let child = if let Some(Ok(child)) =
- parent.child().map(|w| w.downcast::<MessageMedia>())
- {
- child
- } else {
- let child = MessageMedia::new();
- parent.set_child(Some(&child));
- child
- };
- child.video(message, &event.room().session(), compact);
- }
- MessageType::VerificationRequest(_) => {
- // TODO: show more information about the verification
- let child = if let Some(Ok(child)) =
- parent.child().map(|w| w.downcast::<MessageText>())
- {
- child
- } else {
- let child = MessageText::new();
- parent.set_child(Some(&child));
- child
- };
- child.text(gettext("Identity verification was started"));
- }
- _ => {
- warn!("Event not supported: {:?}", msgtype);
- let child = if let Some(Ok(child)) =
- parent.child().map(|w| w.downcast::<MessageText>())
- {
- child
- } else {
- let child = MessageText::new();
- parent.set_child(Some(&child));
- child
- };
- child.text(gettext("Unsupported event"));
- }
- }
- }
- Some(AnyMessageLikeEventContent::Sticker(content)) => {
- let child =
- if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageMedia>()) {
- child
- } else {
- let child = MessageMedia::new();
- parent.set_child(Some(&child));
- child
- };
- child.sticker(content, &event.room().session(), compact);
- }
- Some(AnyMessageLikeEventContent::RoomEncrypted(_)) => {
- let child = if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageText>())
- {
- child
- } else {
- let child = MessageText::new();
- parent.set_child(Some(&child));
- child
- };
- child.text(gettext("Unable to decrypt this message, decryption will be retried once the keys are
available."));
- }
- Some(AnyMessageLikeEventContent::RoomRedaction(_)) => {
- let child = if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageText>())
- {
- child
- } else {
- let child = MessageText::new();
- parent.set_child(Some(&child));
- child
- };
- child.text(gettext("This message was removed."));
- }
- _ => {
- let child = if let Some(Ok(child)) = parent.child().map(|w| w.downcast::<MessageText>())
- {
- child
- } else {
- let child = MessageText::new();
- parent.set_child(Some(&child));
- child
- };
- child.text(gettext("Unsupported event"));
- }
- }
-}
diff --git a/src/session/content/room_history/message_row/text.rs
b/src/session/content/room_history/message_row/text.rs
index f799f814b..0a3446e7a 100644
--- a/src/session/content/room_history/message_row/text.rs
+++ b/src/session/content/room_history/message_row/text.rs
@@ -14,6 +14,7 @@ use matrix_sdk::ruma::{
};
use sourceview::prelude::*;
+use super::ContentFormat;
use crate::{
components::{LabelWithWidgets, Pill, DEFAULT_PLACEHOLDER},
session::{room::Member, Room, UserExt},
@@ -60,22 +61,28 @@ impl MessageText {
}
/// Display the given plain text.
- pub fn text(&self, body: String) {
- self.build_text(body, WithMentions::No);
+ pub fn text(&self, body: String, format: ContentFormat) {
+ self.build_text(body, WithMentions::No, format);
}
/// 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, room: &Room) {
+ pub fn markup(
+ &self,
+ formatted: Option<FormattedBody>,
+ body: String,
+ room: &Room,
+ format: ContentFormat,
+ ) {
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, room);
+ self.build_html(html_blocks, room, format);
} else {
let body = linkify(strip_reply(&body));
- self.build_text(body, WithMentions::Yes(room));
+ self.build_text(body, WithMentions::Yes(room), format);
}
}
@@ -88,6 +95,7 @@ impl MessageText {
body: String,
sender: Member,
room: &Room,
+ format: ContentFormat,
) {
if let Some(body) = formatted
.filter(is_valid_formatted_body)
@@ -99,16 +107,17 @@ impl MessageText {
};
let html = parse_formatted_body(&formatted.body).unwrap();
- self.build_html(html, room);
+ self.build_html(html, room, format);
} else {
self.build_text(
format!("{} {}", sender.html_mention(), linkify(&body)),
WithMentions::Yes(room),
+ format,
);
}
}
- fn build_text(&self, text: String, with_mentions: WithMentions) {
+ fn build_text(&self, text: String, with_mentions: WithMentions, format: ContentFormat) {
let child = if let Some(Ok(child)) = self.child().map(|w| w.downcast::<LabelWithWidgets>())
{
child
@@ -134,15 +143,23 @@ impl MessageText {
child.set_widgets(Vec::<gtk::Widget>::new());
child.set_label(Some(text));
}
+
+ child.set_ellipsize(format == ContentFormat::Ellipsized);
}
- fn build_html(&self, blocks: Vec<HtmlBlock>, room: &Room) {
+ fn build_html(&self, blocks: Vec<HtmlBlock>, room: &Room, format: ContentFormat) {
let child = gtk::Box::new(gtk::Orientation::Vertical, 6);
self.set_child(Some(&child));
+ let ellipsize = format == ContentFormat::Ellipsized;
+ let len = blocks.len();
for block in blocks {
- let widget = create_widget_for_html_block(&block, room);
+ let widget = create_widget_for_html_block(&block, room, ellipsize, len > 1);
child.append(&widget);
+
+ if ellipsize {
+ break;
+ }
}
}
}
@@ -176,14 +193,23 @@ fn parse_formatted_body(formatted: &str) -> Option<Vec<HtmlBlock>> {
markup_html(formatted).ok()
}
-fn create_widget_for_html_block(block: &HtmlBlock, room: &Room) -> gtk::Widget {
+fn create_widget_for_html_block(
+ block: &HtmlBlock,
+ room: &Room,
+ ellipsize: bool,
+ has_more: bool,
+) -> gtk::Widget {
match block {
HtmlBlock::Heading(n, s) => {
let (label, widgets) = extract_mentions(s, room);
- let label = hoverify_links(&label);
+ let mut label = hoverify_links(&label);
+ if ellipsize && has_more && !label.ends_with('…') && !label.ends_with("...") {
+ label.push('…');
+ }
let w = LabelWithWidgets::with_label_and_widgets(&label, widgets);
w.set_use_markup(true);
w.add_css_class(&format!("h{}", n));
+ w.set_ellipsize(ellipsize);
w.upcast::<gtk::Widget>()
}
HtmlBlock::UList(elements) => {
@@ -196,12 +222,24 @@ fn create_widget_for_html_block(block: &HtmlBlock, room: &Room) -> gtk::Widget {
let bullet = gtk::Label::new(Some("•"));
bullet.set_valign(gtk::Align::Start);
let (label, widgets) = extract_mentions(li, room);
- let label = hoverify_links(&label);
+ let mut label = hoverify_links(&label);
+ if ellipsize
+ && (has_more || elements.len() > 1)
+ && !label.ends_with('…')
+ && !label.ends_with("...")
+ {
+ label.push('…');
+ }
let w = LabelWithWidgets::with_label_and_widgets(&label, widgets);
w.set_use_markup(true);
+ w.set_ellipsize(ellipsize);
h_box.append(&bullet);
h_box.append(&w);
bx.append(&h_box);
+
+ if ellipsize {
+ break;
+ }
}
bx.upcast::<gtk::Widget>()
@@ -216,43 +254,82 @@ fn create_widget_for_html_block(block: &HtmlBlock, room: &Room) -> gtk::Widget {
let bullet = gtk::Label::new(Some(&format!("{}.", i + 1)));
bullet.set_valign(gtk::Align::Start);
let (label, widgets) = extract_mentions(ol, room);
- let label = hoverify_links(&label);
+ let mut label = hoverify_links(&label);
+ if ellipsize
+ && (has_more || elements.len() > 1)
+ && !label.ends_with('…')
+ && !label.ends_with("...")
+ {
+ label.push('…');
+ }
let w = LabelWithWidgets::with_label_and_widgets(&label, widgets);
w.set_use_markup(true);
+ w.set_ellipsize(ellipsize);
h_box.append(&bullet);
h_box.append(&w);
bx.append(&h_box);
+
+ if ellipsize {
+ break;
+ }
}
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);
- crate::utils::setup_style_scheme(&buffer);
- 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>()
+ if ellipsize {
+ let label = if let Some(pos) = s.find('\n') {
+ format!("<tt>{}…</tt>", &s[0..pos])
+ } else if has_more {
+ format!("<tt>{s}…</tt>")
+ } else {
+ format!("<tt>{s}</tt>")
+ };
+ let w = LabelWithWidgets::with_label_and_widgets(&label, Vec::<gtk::Widget>::new());
+ w.set_use_markup(true);
+ w.set_ellipsize(ellipsize);
+ w.upcast::<gtk::Widget>()
+ } else {
+ 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);
+ crate::utils::setup_style_scheme(&buffer);
+ 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, room);
+ let w = create_widget_for_html_block(
+ block,
+ room,
+ ellipsize,
+ has_more || blocks.len() > 1,
+ );
bx.append(&w);
+
+ if ellipsize {
+ break;
+ }
}
bx.upcast::<gtk::Widget>()
}
HtmlBlock::Text(s) => {
let (label, widgets) = extract_mentions(s, room);
- let label = hoverify_links(&label);
+ let mut label = hoverify_links(&label);
+ if ellipsize && has_more && !label.ends_with('…') && !label.ends_with("...") {
+ label.push('…');
+ }
let w = LabelWithWidgets::with_label_and_widgets(&label, widgets);
w.set_use_markup(true);
+ w.set_ellipsize(ellipsize);
w.upcast::<gtk::Widget>()
}
}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]