Files
nixos/patches/flare/0007-feat-messages-Open-image-attachments-in-a-fullscreen.patch

271 lines
8.7 KiB
Diff

From c185703eef79a48dead219f6b41a7874994c7273 Mon Sep 17 00:00:00 2001
From: Simon Gardling <titaniumtown@proton.me>
Date: Tue, 12 May 2026 12:03:33 -0400
Subject: [PATCH] feat(messages): Open image attachments in a dialog
Mirrors the official Signal desktop application by allowing the user
to click on an image attachment and see it expanded in an Adw.Dialog
overlay with built-in close button, Escape handling, and mobile
swipe-down support.
---
CHANGELOG.md | 4 ++
data/resources/meson.build | 1 +
data/resources/resources.gresource.xml.in | 1 +
.../ui/components/attachment_image.blp | 5 ++
data/resources/ui/image_viewer.blp | 15 +++++
src/backend/dummy.rs | 3 +-
src/gui/components/attachment_image.rs | 28 ++++++++-
src/gui/image_viewer.rs | 57 +++++++++++++++++++
src/gui/mod.rs | 1 +
src/gui/window.rs | 7 +++
10 files changed, 120 insertions(+), 2 deletions(-)
create mode 100644 data/resources/ui/image_viewer.blp
create mode 100644 src/gui/image_viewer.rs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20dc578e..1e4649bc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- Click an image attachment to open it in a fullscreen-capable viewer with a close button, fullscreen toggle, and `Escape`-to-dismiss.
+
## [0.20.4] - 2026-04-22
### Fixed
diff --git a/data/resources/meson.build b/data/resources/meson.build
index 7fc00b7c..efd789b7 100644
--- a/data/resources/meson.build
+++ b/data/resources/meson.build
@@ -11,6 +11,7 @@ blueprints = custom_target('blueprints',
'ui/dialog_clear_messages.blp',
'ui/dialog_unlink.blp',
'ui/error_dialog.blp',
+ 'ui/image_viewer.blp',
'ui/setup_window.blp',
'ui/linked_devices_window.blp',
'ui/device_info_item.blp',
diff --git a/data/resources/resources.gresource.xml.in b/data/resources/resources.gresource.xml.in
index 8604fe24..7c958d02 100644
--- a/data/resources/resources.gresource.xml.in
+++ b/data/resources/resources.gresource.xml.in
@@ -13,6 +13,7 @@
<file preprocess="xml-stripblanks">ui/linked_devices_window.ui</file>
<file preprocess="xml-stripblanks">ui/device_info_item.ui</file>
<file preprocess="xml-stripblanks">ui/error_dialog.ui</file>
+ <file preprocess="xml-stripblanks">ui/image_viewer.ui</file>
<file preprocess="xml-stripblanks">ui/attachment.ui</file>
<file preprocess="xml-stripblanks">ui/preferences_window.ui</file>
<file preprocess="xml-stripblanks">ui/shortcuts.ui</file>
diff --git a/data/resources/ui/components/attachment_image.blp b/data/resources/ui/components/attachment_image.blp
index ec303a72..bcbdabe1 100644
--- a/data/resources/ui/components/attachment_image.blp
+++ b/data/resources/ui/components/attachment_image.blp
@@ -2,6 +2,11 @@ using Gtk 4.0;
template $FlAttachmentImage: $FlAttachmentBase {
Picture picture {
+ GestureClick {
+ button: 1;
+ pressed => $on_clicked() swapped;
+ }
+
styles [
"photo",
]
diff --git a/data/resources/ui/image_viewer.blp b/data/resources/ui/image_viewer.blp
new file mode 100644
index 00000000..19660f0b
--- /dev/null
+++ b/data/resources/ui/image_viewer.blp
@@ -0,0 +1,15 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $FlImageViewer: Adw.Bin {
+ hexpand: true;
+ vexpand: true;
+
+ Picture picture {
+ paintable: bind template.attachment as <$FlAttachmentObject>.image;
+ content-fit: contain;
+ can-shrink: true;
+ hexpand: true;
+ vexpand: true;
+ }
+}
diff --git a/src/backend/dummy.rs b/src/backend/dummy.rs
index e6824e80..1ebff6f6 100644
--- a/src/backend/dummy.rs
+++ b/src/backend/dummy.rs
@@ -382,6 +382,7 @@ impl super::Manager {
},
));
+
let msg_screenshot = msg!(self, "", 2, GROUP_ID, 19 + base_minute);
let screenshot_file = gtk::gio::File::for_uri("resource:///icon.svg");
let attachment = crate::backend::Attachment::from_file(screenshot_file, self);
@@ -393,7 +394,7 @@ impl super::Manager {
vec![
msg_replied,
- // msg_screenshot,
+ msg_screenshot,
msg_reply,
msg!(
self,
diff --git a/src/gui/components/attachment_image.rs b/src/gui/components/attachment_image.rs
index a91bf05b..9ca6e0c7 100644
--- a/src/gui/components/attachment_image.rs
+++ b/src/gui/components/attachment_image.rs
@@ -24,7 +24,10 @@ pub mod imp {
use glib::subclass::InitializingObject;
use gtk::{CompositeTemplate, Picture};
- use crate::gui::{attachment::Attachment, attachment::AttachmentImpl, utility::Utility};
+ use crate::gui::{
+ attachment::{Attachment, AttachmentImpl},
+ utility::Utility,
+ };
#[derive(CompositeTemplate, Default)]
#[template(resource = "/ui/components/attachment_image.ui")]
@@ -41,6 +44,7 @@ pub mod imp {
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
+ Self::bind_template_callbacks(klass);
Utility::bind_template_callbacks(klass);
}
@@ -49,6 +53,28 @@ pub mod imp {
}
}
+ #[gtk::template_callbacks]
+ impl AttachmentImage {
+ /// Open the image in a viewer presented inside the parent window.
+ /// Bails out silently if the attachment's texture hasn't loaded yet.
+ #[template_callback]
+ fn on_clicked(&self) {
+ let obj = self.obj();
+ let base = obj.upcast_ref::<Attachment>();
+ let attachment = base.attachment();
+ if attachment.image().is_none() {
+ return;
+ }
+ let Some(window) = obj
+ .root()
+ .and_then(|r| r.dynamic_cast::<crate::gui::Window>().ok())
+ else {
+ return;
+ };
+ window.present_image_viewer(&attachment);
+ }
+ }
+
impl ObjectImpl for AttachmentImage {
fn constructed(&self) {
self.parent_constructed();
diff --git a/src/gui/image_viewer.rs b/src/gui/image_viewer.rs
new file mode 100644
index 00000000..a667a5a5
--- /dev/null
+++ b/src/gui/image_viewer.rs
@@ -0,0 +1,57 @@
+use crate::prelude::*;
+
+use crate::backend::Attachment;
+
+glib::wrapper! {
+ pub struct ImageViewer(ObjectSubclass<imp::ImageViewer>)
+ @extends adw::Bin, gtk::Widget,
+ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+}
+
+impl ImageViewer {
+ pub fn new(attachment: &Attachment) -> Self {
+ Object::builder::<Self>()
+ .property("attachment", attachment)
+ .build()
+ }
+}
+
+pub mod imp {
+ use crate::prelude::*;
+
+ use gtk::{subclass::prelude::*, CompositeTemplate, Picture};
+
+ use crate::backend::Attachment;
+
+ #[derive(CompositeTemplate, Default, glib::Properties)]
+ #[properties(wrapper_type = super::ImageViewer)]
+ #[template(resource = "/ui/image_viewer.ui")]
+ pub struct ImageViewer {
+ #[property(get, set, construct_only)]
+ attachment: RefCell<Option<Attachment>>,
+
+ #[template_child]
+ pub(super) picture: TemplateChild<Picture>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for ImageViewer {
+ const NAME: &'static str = "FlImageViewer";
+ type Type = super::ImageViewer;
+ type ParentType = adw::Bin;
+
+ fn class_init(klass: &mut Self::Class) {
+ Self::bind_template(klass);
+ }
+
+ fn instance_init(obj: &InitializingObject<Self>) {
+ obj.init_template();
+ }
+ }
+
+ #[glib::derived_properties]
+ impl ObjectImpl for ImageViewer {}
+
+ impl WidgetImpl for ImageViewer {}
+ impl BinImpl for ImageViewer {}
+}
diff --git a/src/gui/mod.rs b/src/gui/mod.rs
index 6b5cfd19..d2e997ee 100644
--- a/src/gui/mod.rs
+++ b/src/gui/mod.rs
@@ -8,6 +8,7 @@ mod channel_messages;
mod components;
mod device_info_item;
mod error_dialog;
+mod image_viewer;
mod linked_devices_window;
mod message_item;
mod new_channel_dialog;
diff --git a/src/gui/window.rs b/src/gui/window.rs
index 6335f3d6..4f75e360 100644
--- a/src/gui/window.rs
+++ b/src/gui/window.rs
@@ -103,6 +103,13 @@ impl Window {
pub(crate) fn settings(&self) -> gio::Settings {
self.imp().settings.clone()
}
+
+ pub fn present_image_viewer(&self, attachment: &crate::backend::Attachment) {
+ let viewer = crate::gui::image_viewer::ImageViewer::new(attachment);
+ let dialog = adw::Dialog::new();
+ dialog.set_child(Some(&viewer));
+ dialog.present(Some(self.upcast_ref::<gtk::Widget>()));
+ }
}
pub mod imp {
--
2.53.0