diff --git a/home/profiles/gui.nix b/home/profiles/gui.nix index 94fb665..c09fa2c 100644 --- a/home/profiles/gui.nix +++ b/home/profiles/gui.nix @@ -89,7 +89,7 @@ # alternative GTK signal client; carries local feature patches under # patches/flare/ on top of upstream master (typing indicators, edited # messages, multi-select with delete-for-me, in-channel message search, - # the deleted-message placeholder, the image-viewer overlay, and the + # the deleted-message placeholder, the image-viewer dialog, and the # not-yet-merged init_channels cache-miss fix). (pkgs.flare-signal.overrideAttrs (old: { src = inputs.flare-upstream; @@ -106,15 +106,6 @@ ../../patches/flare/0006-fix-backend-Refresh-cached-channels-on-init_channels.patch ../../patches/flare/0007-feat-messages-Open-image-attachments-in-a-fullscreen.patch ]; - # The image-viewer demo patch (0007) references - # data/screenshots/dummy_apple.jpg, but git binary diffs are not - # supported by the standard `patch` tool nixpkgs uses for the - # patchPhase. Ship the JPEG alongside the patches and copy it into - # the source tree before the build picks it up. - postPatch = (old.postPatch or "") + '' - install -m 0644 ${./../../patches/flare/dummy_apple.jpg} \ - data/screenshots/dummy_apple.jpg - ''; })) # accounting diff --git a/patches/flare/0007-feat-messages-Open-image-attachments-in-a-fullscreen.patch b/patches/flare/0007-feat-messages-Open-image-attachments-in-a-fullscreen.patch index 9d30341..e6f1fa9 100644 --- a/patches/flare/0007-feat-messages-Open-image-attachments-in-a-fullscreen.patch +++ b/patches/flare/0007-feat-messages-Open-image-attachments-in-a-fullscreen.patch @@ -1,60 +1,42 @@ -From e13d8cbf15a2eea323397accb2183e2362dae106 Mon Sep 17 00:00:00 2001 +From c185703eef79a48dead219f6b41a7874994c7273 Mon Sep 17 00:00:00 2001 From: Simon Gardling -Date: Wed, 6 May 2026 18:41:15 -0400 -Subject: [PATCH 7/7] feat(messages): Open image attachments in a - fullscreen-capable viewer -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit +Date: Tue, 12 May 2026 12:03:33 -0400 +Subject: [PATCH] feat(messages): Open image attachments in a dialog -Clicking an image bubble currently does nothing — the existing -"pressed" gesture is right-click-only and routes to the context menu. -To actually look at an image at full resolution the user has to -right-click and choose "Open", which dispatches the file through the -xdg-open portal to the system image viewer. That is the wrong -affordance for a chat client; every other Signal frontend opens the -image inline on tap. - -Add an `ImageViewer` (Adw.Window subclass) that hosts the attachment's -texture in a `gtk::Picture` filling the window over a near-black -backdrop, with two `osd circular` buttons floating in the top-right -corner: a fullscreen toggle and a close button. Closing happens on -`Escape` or via the close button — clicks on the picture and on the -letterboxed area do nothing, leaving the gesture stack open for a -future pan/zoom implementation. - -Wire `AttachmentImage` to open the viewer on left-click. Bails out -silently if the texture has not loaded yet or if the row is not -currently parented in a `gtk::Window`. +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 | 1 + - data/resources/meson.build | 1 + - data/resources/resources.gresource.xml.in | 5 + - data/resources/style.css | 8 +- - .../ui/components/attachment_image.blp | 5 + - data/resources/ui/image_viewer.blp | 64 +++++ - data/resources/ui/window.blp | 224 +++++++++--------- - src/backend/dummy.rs | 36 ++- - src/gui/components/attachment_image.rs | 33 ++- - src/gui/image_viewer.rs | 120 ++++++++++ - src/gui/mod.rs | 1 + - src/gui/window.rs | 30 +++ - 13 files changed, 411 insertions(+), 117 deletions(-) + 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 e221f555..8ebca71c 100644 +index 20dc578e..1e4649bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md -@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - Display incoming edited messages with an `edited` indicator and edit your own sent messages from their context menu. - - Multi-select messages from their context menu and delete the selection locally with a single action. - - In-channel message search (Ctrl+Shift+F) over the loaded timeline with prev/next navigation and a match counter. -+- Click an image attachment to open it in a fullscreen-capable viewer with a close button, fullscreen toggle, and `Escape`-to-dismiss. +@@ -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 @@ -68,21 +50,10 @@ index 7fc00b7c..efd789b7 100644 '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..863564b3 100644 +index 8604fe24..7c958d02 100644 --- a/data/resources/resources.gresource.xml.in +++ b/data/resources/resources.gresource.xml.in -@@ -2,6 +2,10 @@ - - - ../icons/de.schmidhuberj.Flare.svg -+ -+ -+ -+ ../screenshots/dummy_apple.jpg - - ui/window.ui - ui/channel_list.ui -@@ -13,6 +17,7 @@ +@@ -13,6 +13,7 @@ ui/linked_devices_window.ui ui/device_info_item.ui ui/error_dialog.ui @@ -90,23 +61,6 @@ index 8604fe24..863564b3 100644 ui/attachment.ui ui/preferences_window.ui ui/shortcuts.ui -diff --git a/data/resources/style.css b/data/resources/style.css -index b3a517f9..0c95a55a 100644 ---- a/data/resources/style.css -+++ b/data/resources/style.css -@@ -309,4 +309,10 @@ unread-indicator { - background: alpha(@accent_bg_color, 0.35); - outline: 2px solid alpha(@accent_color, 0.7); - outline-offset: -2px; --} -\ No newline at end of file -+} -+ -+/* Image attachment viewer: a near-black backdrop so the picture appears to -+ float and the floating OSD close button has enough contrast. */ -+.image-viewer-backdrop { -+ background-color: rgb(15, 15, 15); -+} 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 @@ -125,386 +79,48 @@ index ec303a72..bcbdabe1 100644 ] diff --git a/data/resources/ui/image_viewer.blp b/data/resources/ui/image_viewer.blp new file mode 100644 -index 00000000..1b27cc09 +index 00000000..19660f0b --- /dev/null +++ b/data/resources/ui/image_viewer.blp -@@ -0,0 +1,64 @@ +@@ -0,0 +1,15 @@ +using Gtk 4.0; +using Adw 1; + +template $FlImageViewer: Adw.Bin { + hexpand: true; + vexpand: true; -+ focusable: true; + -+ EventControllerKey { -+ key-pressed => $on_key_pressed() swapped; -+ } -+ -+ // Box paints the dark backdrop. Overlay does not draw a background, so -+ // the CSS class lives on the Box. -+ Box { ++ Picture picture { ++ paintable: bind template.attachment as <$FlAttachmentObject>.image; ++ content-fit: contain; ++ can-shrink: true; + hexpand: true; + vexpand: true; -+ -+ styles [ -+ "image-viewer-backdrop", -+ ] -+ -+ Overlay { -+ hexpand: true; -+ vexpand: true; -+ -+ // Any left-click in the viewer dismisses — on the image, on the -+ // letterboxed dark area, anywhere. The close button absorbs its own -+ // click before it reaches us via standard GTK click-bubbling so the -+ // button still works. -+ GestureClick { -+ button: 1; -+ pressed => $on_backdrop_pressed() swapped; -+ } -+ -+ Picture picture { -+ paintable: bind template.attachment as <$FlAttachmentObject>.image; -+ content-fit: contain; -+ can-shrink: true; -+ hexpand: true; -+ vexpand: true; -+ } -+ -+ [overlay] -+ Box { -+ halign: end; -+ valign: start; -+ margin-top: 12; -+ margin-end: 12; -+ -+ Button close_button { -+ icon-name: "window-close-symbolic"; -+ tooltip-text: _("Close"); -+ clicked => $on_close_clicked() swapped; -+ -+ styles [ -+ "osd", -+ "circular", -+ ] -+ } -+ } -+ } + } +} -diff --git a/data/resources/ui/window.blp b/data/resources/ui/window.blp -index 82de6af3..3706121a 100644 ---- a/data/resources/ui/window.blp -+++ b/data/resources/ui/window.blp -@@ -52,130 +52,136 @@ template $FlWindow: Adw.ApplicationWindow { - Gtk.StackPage { - name: "main"; - -- child: Adw.NavigationSplitView split_view { -- min-sidebar-width: 300; -- max-sidebar-width: 400; -- sidebar-width-fraction: 0.25; -- notify::show-content => $handle_show_content() swapped; -- -- sidebar: Adw.NavigationPage { -- title: _("Channel list"); -- tag: "channel-list"; -- -- Adw.ToolbarView { -- [top] -- Adw.HeaderBar { -- title-widget: Adw.WindowTitle { -- title: "Flare"; -- }; -+ child: Gtk.Overlay image_viewer_overlay { -+ // Wraps the navigation split view so an image viewer pushed in via -+ // `Window::present_image_viewer` covers the entire window content -+ // (sidebar + chat) rather than being constrained to the split -+ // view's content pane like an Adw.Dialog would be. -+ Adw.NavigationSplitView split_view { -+ min-sidebar-width: 300; -+ max-sidebar-width: 400; -+ sidebar-width-fraction: 0.25; -+ notify::show-content => $handle_show_content() swapped; -+ -+ sidebar: Adw.NavigationPage { -+ title: _("Channel list"); -+ tag: "channel-list"; -+ -+ Adw.ToolbarView { -+ [top] -+ Adw.HeaderBar { -+ title-widget: Adw.WindowTitle { -+ title: "Flare"; -+ }; -+ -+ [start] -+ Gtk.Button { -+ accessibility { -+ label: C_("accessibility", "Add Conversation"); -+ } - -- [start] -- Gtk.Button { -- accessibility { -- label: C_("accessibility", "Add Conversation"); -+ tooltip-text: "Add Conversation"; -+ icon-name: "chat-message-new-symbolic"; -+ clicked => $handle_add_conversation_clicked() swapped; - } - -- tooltip-text: "Add Conversation"; -- icon-name: "chat-message-new-symbolic"; -- clicked => $handle_add_conversation_clicked() swapped; -- } -+ [end] -+ MenuButton { -+ accessibility { -+ label: C_("accessibility", "Menu"); -+ } - -- [end] -- MenuButton { -- accessibility { -- label: C_("accessibility", "Menu"); -+ tooltip-text: "Menu"; -+ menu-model: menubar; -+ primary: true; -+ icon-name: "open-menu-symbolic"; - } - -- tooltip-text: "Menu"; -- menu-model: menubar; -- primary: true; -- icon-name: "open-menu-symbolic"; -- } -+ [end] -+ ToggleButton { -+ accessibility { -+ label: C_("accessibility", "Search"); -+ } - -- [end] -- ToggleButton { -- accessibility { -- label: C_("accessibility", "Search"); -+ tooltip-text: "Search"; -+ icon-name: "system-search-symbolic"; -+ active: bind channel_list.search-enabled no-sync-create; -+ clicked => $handle_search_clicked() swapped; - } -- -- tooltip-text: "Search"; -- icon-name: "system-search-symbolic"; -- active: bind channel_list.search-enabled no-sync-create; -- clicked => $handle_search_clicked() swapped; - } -- } - -- content: $FlChannelList channel_list { -- notify::active-channel => $handle_go_forward() swapped; -- manager: bind template.manager; -- }; -- } -- }; -- -- content: Adw.NavigationPage { -- title: _("Chat"); -- tag: "chat"; -- -- Adw.ToolbarView { -- [top] -- Adw.HeaderBar { -- title-widget: Button { -- styles [ -- "flat", -- ] -- -- sensitive: bind $is_some(channel_list.active-channel) as ; -- visible: bind $is_some(channel_list.active-channel) as ; -- action-name: "win.channel-information"; -- -- child: Box { -- name: "room_title"; -- orientation: vertical; -- halign: center; -- valign: center; -- -- Label title_label { -- focusable: true; -- ellipsize: end; -- halign: center; -- wrap: false; -- single-line-mode: true; -- use-markup: false; -- width-chars: 5; -- label: bind channel_list.active-channel as <$FlChannel>.title; -- -- styles [ -- "title", -- ] -- } -+ content: $FlChannelList channel_list { -+ notify::active-channel => $handle_go_forward() swapped; -+ manager: bind template.manager; -+ }; -+ } -+ }; - -- Label subtitle_label { -- focusable: true; -- ellipsize: end; -+ content: Adw.NavigationPage { -+ title: _("Chat"); -+ tag: "chat"; -+ -+ Adw.ToolbarView { -+ [top] -+ Adw.HeaderBar { -+ title-widget: Button { -+ styles [ -+ "flat", -+ ] -+ -+ sensitive: bind $is_some(channel_list.active-channel) as ; -+ visible: bind $is_some(channel_list.active-channel) as ; -+ action-name: "win.channel-information"; -+ -+ child: Box { -+ name: "room_title"; -+ orientation: vertical; - halign: center; -- wrap: false; -- single-line-mode: true; -- use-markup: true; -- visible: bind channel_list.active-channel as <$FlChannel>.is-typing; -- label: bind channel_list.active-channel as <$FlChannel>.typing-label; -- tooltip-markup: bind channel_list.active-channel as <$FlChannel>.description; -- -- styles [ -- "subtitle-room", -- "accent", -- ] -- } -+ valign: center; -+ -+ Label title_label { -+ focusable: true; -+ ellipsize: end; -+ halign: center; -+ wrap: false; -+ single-line-mode: true; -+ use-markup: false; -+ width-chars: 5; -+ label: bind channel_list.active-channel as <$FlChannel>.title; -+ -+ styles [ -+ "title", -+ ] -+ } -+ -+ Label subtitle_label { -+ focusable: true; -+ ellipsize: end; -+ halign: center; -+ wrap: false; -+ single-line-mode: true; -+ use-markup: true; -+ visible: bind channel_list.active-channel as <$FlChannel>.is-typing; -+ label: bind channel_list.active-channel as <$FlChannel>.typing-label; -+ tooltip-markup: bind channel_list.active-channel as <$FlChannel>.description; -+ -+ styles [ -+ "subtitle-room", -+ "accent", -+ ] -+ } -+ }; - }; -+ } -+ -+ content: $FlChannelMessages channel_messages { -+ manager: bind template.manager; -+ active-channel: bind channel_list.active-channel; -+ has-channels: bind channel_list.has-channels; - }; - } -- -- content: $FlChannelMessages channel_messages { -- manager: bind template.manager; -- active-channel: bind channel_list.active-channel; -- has-channels: bind channel_list.has-channels; -- }; -- } -- }; -+ }; -+ } - }; - } - } diff --git a/src/backend/dummy.rs b/src/backend/dummy.rs -index e6824e80..c1aae764 100644 +index e6824e80..1ebff6f6 100644 --- a/src/backend/dummy.rs +++ b/src/backend/dummy.rs -@@ -382,18 +382,40 @@ impl super::Manager { +@@ -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); -- msg_screenshot -+ // A short question-and-answer in the 1-on-1 chat with contact 2 -+ // ("Developer") demonstrating the click-to-open image viewer -+ // (`gui::image_viewer`). The contact sends a photo of an apple -+ // (bundled into the screenshot-mode build via -+ // `data/resources/resources.gresource.xml.in` as `dummy_apple.jpg`, -+ // public domain "Big red apple.jpg" by Paolo Neo via Wikimedia -+ // Commons) along with a -+ // text question, and the local user answers with the obvious -+ // one-word reply. -+ let msg_fruit_question = msg!( -+ self, -+ "what is the name of this fruit?", -+ 2, -+ 2, -+ 30 + base_minute -+ ); -+ // `Attachment::from_file` only loads images via filesystem paths; for -+ // `resource://` URIs `GFile::path()` returns None, so the image -+ // would never reach the Picture. Load the bytes directly out of the -+ // gresource bundle into a Texture instead, and build the attachment -+ // from that. -+ let fruit_pixbuf = gtk::gdk_pixbuf::Pixbuf::from_resource("/dummy_apple.jpg") -+ .expect("apple photo resource to load"); -+ let fruit_texture = gtk::gdk::Texture::for_pixbuf(&fruit_pixbuf); -+ let fruit_photo = crate::backend::Attachment::from_texture(fruit_texture, self); -+ msg_fruit_question - .clone() - .downcast::() - .unwrap() -- .add_attachment(attachment); -+ .add_attachment(fruit_photo); -+ let msg_fruit_answer = msg!(self, "apple", 0, 2, 32 + base_minute); ++ + 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, -@@ -405,6 +427,8 @@ impl super::Manager { - msg!(self, "Glad you like it.", 2, GROUP_ID, 25 + base_minute), - msg!(self, "YAY!", 0, GROUP_ID, 27 + base_minute), - msg!(self, "Greetings", 2, 2, base_minute - 100), -+ msg_fruit_question, -+ msg_fruit_answer, - msg!(self, "It's a trap!", 3, 3, 1 + base_minute), - msg!(self, "Hello, Mr. Anderson", 4, 4, 2 + base_minute), - ] diff --git a/src/gui/components/attachment_image.rs b/src/gui/components/attachment_image.rs -index a91bf05b..4ddd29b6 100644 +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 { @@ -527,7 +143,7 @@ index a91bf05b..4ddd29b6 100644 Utility::bind_template_callbacks(klass); } -@@ -49,6 +53,33 @@ pub mod imp { +@@ -49,6 +53,28 @@ pub mod imp { } } @@ -543,11 +159,6 @@ index a91bf05b..4ddd29b6 100644 + if attachment.image().is_none() { + return; + } -+ // Walk up to the top-level `crate::gui::Window` and ask it to -+ // host the viewer in its window-level overlay so the viewer -+ // covers the entire window content (sidebar + chat) rather -+ // than being constrained to the navigation split view's -+ // content pane like an Adw.Dialog would be. + let Some(window) = obj + .root() + .and_then(|r| r.dynamic_cast::().ok()) @@ -563,25 +174,15 @@ index a91bf05b..4ddd29b6 100644 self.parent_constructed(); diff --git a/src/gui/image_viewer.rs b/src/gui/image_viewer.rs new file mode 100644 -index 00000000..b3e9d720 +index 00000000..a667a5a5 --- /dev/null +++ b/src/gui/image_viewer.rs -@@ -0,0 +1,120 @@ +@@ -0,0 +1,57 @@ +use crate::prelude::*; + +use crate::backend::Attachment; + +glib::wrapper! { -+ /// A widget that displays an image attachment over a dark backdrop. It is -+ /// designed to be inserted into a top-level [`gtk::Overlay`] in -+ /// [`crate::gui::Window`] (see [`crate::gui::Window::present_image_viewer`]) -+ /// so that it covers every other widget in the window — sidebar, chat -+ /// pane, header bar — rather than being constrained to the content area -+ /// of the surrounding navigation split view (which is what `Adw.Dialog` -+ /// would force). -+ /// -+ /// Emits `closed` when the user dismisses the viewer. Consumers should -+ /// remove the widget from the overlay in response. + pub struct ImageViewer(ObjectSubclass) + @extends adw::Bin, gtk::Widget, + @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; @@ -589,7 +190,6 @@ index 00000000..b3e9d720 + +impl ImageViewer { + pub fn new(attachment: &Attachment) -> Self { -+ log::trace!("Initializing `ImageViewer`"); + Object::builder::() + .property("attachment", attachment) + .build() @@ -599,8 +199,7 @@ index 00000000..b3e9d720 +pub mod imp { + use crate::prelude::*; + -+ use glib::subclass::{InitializingObject, Signal}; -+ use gtk::{CompositeTemplate, Picture}; ++ use gtk::{subclass::prelude::*, CompositeTemplate, Picture}; + + use crate::backend::Attachment; + @@ -615,41 +214,6 @@ index 00000000..b3e9d720 + pub(super) picture: TemplateChild, + } + -+ #[gtk::template_callbacks] -+ impl ImageViewer { -+ /// Click anywhere in the viewer (image or backdrop) closes. The -+ /// close button stops its own click before it reaches us via the -+ /// usual GTK click-bubbling, so the button still works. -+ #[template_callback] -+ fn on_backdrop_pressed(&self) { -+ self.obj().emit_by_name::<()>("closed", &[]); -+ } -+ -+ #[template_callback] -+ fn on_close_clicked(&self) { -+ self.obj().emit_by_name::<()>("closed", &[]); -+ } -+ -+ /// Close on `Escape`. The viewer grabs focus on construction so the -+ /// key controller actually sees keystrokes; without that, focus -+ /// would still be on whatever widget the user clicked on (typically -+ /// the message row) and `Escape` would route there instead. -+ #[template_callback] -+ fn on_key_pressed( -+ &self, -+ keyval: gdk::Key, -+ _keycode: u32, -+ _state: gdk::ModifierType, -+ ) -> glib::Propagation { -+ if keyval == gdk::Key::Escape { -+ self.obj().emit_by_name::<()>("closed", &[]); -+ glib::Propagation::Stop -+ } else { -+ glib::Propagation::Proceed -+ } -+ } -+ } -+ + #[glib::object_subclass] + impl ObjectSubclass for ImageViewer { + const NAME: &'static str = "FlImageViewer"; @@ -658,7 +222,6 @@ index 00000000..b3e9d720 + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); -+ Self::bind_template_callbacks(klass); + } + + fn instance_init(obj: &InitializingObject) { @@ -667,22 +230,7 @@ index 00000000..b3e9d720 + } + + #[glib::derived_properties] -+ impl ObjectImpl for ImageViewer { -+ fn signals() -> &'static [Signal] { -+ static SIGNALS: Lazy> = -+ Lazy::new(|| vec![Signal::builder("closed").build()]); -+ SIGNALS.as_ref() -+ } -+ -+ fn constructed(&self) { -+ self.parent_constructed(); -+ // Make the widget focusable so the key controller below is -+ // actually allowed to receive keystrokes; Adw.Bin defaults to -+ // not-focusable. -+ self.obj().set_focusable(true); -+ self.obj().set_can_focus(true); -+ } -+ } ++ impl ObjectImpl for ImageViewer {} + + impl WidgetImpl for ImageViewer {} + impl BinImpl for ImageViewer {} @@ -700,60 +248,23 @@ index 6b5cfd19..d2e997ee 100644 mod message_item; mod new_channel_dialog; diff --git a/src/gui/window.rs b/src/gui/window.rs -index ce097ce1..bd9474e2 100644 +index 6335f3d6..4f75e360 100644 --- a/src/gui/window.rs +++ b/src/gui/window.rs -@@ -104,6 +104,33 @@ impl Window { +@@ -103,6 +103,13 @@ impl Window { pub(crate) fn settings(&self) -> gio::Settings { self.imp().settings.clone() } + -+ /// Show an image viewer overlay covering the whole window content area -+ /// for `attachment`. Removes itself from the overlay when the viewer -+ /// emits `closed` (on `Escape`, click anywhere inside the viewer, or -+ /// the close button). + pub fn present_image_viewer(&self, attachment: &crate::backend::Attachment) { -+ use crate::gui::image_viewer::ImageViewer; -+ let viewer = ImageViewer::new(attachment); -+ let overlay = self.imp().image_viewer_overlay.clone(); -+ overlay.add_overlay(&viewer); -+ viewer.grab_focus(); -+ viewer.connect_local( -+ "closed", -+ false, -+ clone!( -+ #[weak] -+ overlay, -+ #[weak] -+ viewer, -+ #[upgrade_or_default] -+ move |_| { -+ overlay.remove_overlay(&viewer); -+ None -+ } -+ ), -+ ); ++ 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::())); + } } pub mod imp { -@@ -142,6 +169,8 @@ pub mod imp { - #[template_child] - split_view: TemplateChild, - #[template_child] -+ pub(super) image_viewer_overlay: TemplateChild, -+ #[template_child] - pub(super) channel_list: TemplateChild, - #[template_child] - subtitle_label: TemplateChild, -@@ -165,6 +194,7 @@ pub mod imp { - channel_list: Default::default(), - subtitle_label: Default::default(), - channel_messages: Default::default(), -+ image_viewer_overlay: Default::default(), - new_channel_dialog: Default::default(), - manager: Default::default(), - settings: Settings::new(BASE_ID), -- 2.53.0 diff --git a/patches/flare/dummy_apple.jpg b/patches/flare/dummy_apple.jpg deleted file mode 100644 index da0c8e4..0000000 Binary files a/patches/flare/dummy_apple.jpg and /dev/null differ