flare: update patches
This commit is contained in:
@@ -89,8 +89,8 @@
|
|||||||
# alternative GTK signal client; carries local feature patches under
|
# alternative GTK signal client; carries local feature patches under
|
||||||
# patches/flare/ on top of upstream master (typing indicators, edited
|
# patches/flare/ on top of upstream master (typing indicators, edited
|
||||||
# messages, multi-select with delete-for-me, in-channel message search,
|
# messages, multi-select with delete-for-me, in-channel message search,
|
||||||
# the deleted-message placeholder, and the not-yet-merged init_channels
|
# the deleted-message placeholder, the image-viewer overlay, and the
|
||||||
# cache-miss fix).
|
# not-yet-merged init_channels cache-miss fix).
|
||||||
(pkgs.flare-signal.overrideAttrs (old: {
|
(pkgs.flare-signal.overrideAttrs (old: {
|
||||||
src = inputs.flare-upstream;
|
src = inputs.flare-upstream;
|
||||||
cargoDeps = pkgs.rustPlatform.importCargoLock {
|
cargoDeps = pkgs.rustPlatform.importCargoLock {
|
||||||
@@ -104,7 +104,17 @@
|
|||||||
../../patches/flare/0004-feat-messages-In-channel-message-search.patch
|
../../patches/flare/0004-feat-messages-In-channel-message-search.patch
|
||||||
../../patches/flare/0005-feat-messages-Show-This-message-was-deleted.-placeho.patch
|
../../patches/flare/0005-feat-messages-Show-This-message-was-deleted.-placeho.patch
|
||||||
../../patches/flare/0006-fix-backend-Refresh-cached-channels-on-init_channels.patch
|
../../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
|
# accounting
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
From 7ab41203098dd868ee70249fcd78c8444438d80c Mon Sep 17 00:00:00 2001
|
From 7ab41203098dd868ee70249fcd78c8444438d80c Mon Sep 17 00:00:00 2001
|
||||||
From: Simon Gardling <titaniumtown@proton.me>
|
From: Simon Gardling <titaniumtown@proton.me>
|
||||||
Date: Wed, 29 Apr 2026 19:00:12 -0400
|
Date: Wed, 29 Apr 2026 19:00:12 -0400
|
||||||
Subject: [PATCH 1/6] feat(typing): Implement typing indicators
|
Subject: [PATCH 1/7] feat(typing): Implement typing indicators
|
||||||
|
|
||||||
- Send TypingMessage Started/Stopped events as the user composes a
|
- Send TypingMessage Started/Stopped events as the user composes a
|
||||||
message, including a periodic refresh and an idle-stop timer so the
|
message, including a periodic refresh and an idle-stop timer so the
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
From f198aa5720bbc953fd72ebfffcec4f62f794fb19 Mon Sep 17 00:00:00 2001
|
From f198aa5720bbc953fd72ebfffcec4f62f794fb19 Mon Sep 17 00:00:00 2001
|
||||||
From: Simon Gardling <titaniumtown@proton.me>
|
From: Simon Gardling <titaniumtown@proton.me>
|
||||||
Date: Wed, 29 Apr 2026 19:33:06 -0400
|
Date: Wed, 29 Apr 2026 19:33:06 -0400
|
||||||
Subject: [PATCH 2/6] feat(messages): Implement edited messages
|
Subject: [PATCH 2/7] feat(messages): Implement edited messages
|
||||||
|
|
||||||
- Receive incoming EditMessage (1-1 and sync) and replace the body,
|
- Receive incoming EditMessage (1-1 and sync) and replace the body,
|
||||||
body_ranges, and attachments of the targeted message in place. The
|
body_ranges, and attachments of the targeted message in place. The
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
From e6db982a5be5ca392538259dfbb66d38ca0d158d Mon Sep 17 00:00:00 2001
|
From e6db982a5be5ca392538259dfbb66d38ca0d158d Mon Sep 17 00:00:00 2001
|
||||||
From: Simon Gardling <titaniumtown@proton.me>
|
From: Simon Gardling <titaniumtown@proton.me>
|
||||||
Date: Wed, 29 Apr 2026 19:53:22 -0400
|
Date: Wed, 29 Apr 2026 19:53:22 -0400
|
||||||
Subject: [PATCH 3/6] feat(messages): Multi-select messages and delete for me
|
Subject: [PATCH 3/7] feat(messages): Multi-select messages and delete for me
|
||||||
|
|
||||||
- Add a 'Select' action to the message context menu that puts the
|
- Add a 'Select' action to the message context menu that puts the
|
||||||
channel into selection mode and pre-selects the message. While in
|
channel into selection mode and pre-selects the message. While in
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
From 6272aee14e9ece866751b94b7ebd823f147f9531 Mon Sep 17 00:00:00 2001
|
From 6272aee14e9ece866751b94b7ebd823f147f9531 Mon Sep 17 00:00:00 2001
|
||||||
From: Simon Gardling <titaniumtown@proton.me>
|
From: Simon Gardling <titaniumtown@proton.me>
|
||||||
Date: Wed, 29 Apr 2026 19:58:54 -0400
|
Date: Wed, 29 Apr 2026 19:58:54 -0400
|
||||||
Subject: [PATCH 4/6] feat(messages): In-channel message search
|
Subject: [PATCH 4/7] feat(messages): In-channel message search
|
||||||
|
|
||||||
- Add a SearchBar above the message list that searches the
|
- Add a SearchBar above the message list that searches the
|
||||||
currently-loaded timeline using a case-insensitive substring match
|
currently-loaded timeline using a case-insensitive substring match
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
From b8227bc71123c5ddb473a07884d33353e7078bf4 Mon Sep 17 00:00:00 2001
|
From b8227bc71123c5ddb473a07884d33353e7078bf4 Mon Sep 17 00:00:00 2001
|
||||||
From: Simon Gardling <titaniumtown@proton.me>
|
From: Simon Gardling <titaniumtown@proton.me>
|
||||||
Date: Thu, 30 Apr 2026 04:25:07 -0400
|
Date: Thu, 30 Apr 2026 04:25:07 -0400
|
||||||
Subject: [PATCH 5/6] feat(messages): Show 'This message was deleted.'
|
Subject: [PATCH 5/7] feat(messages): Show 'This message was deleted.'
|
||||||
placeholder
|
placeholder
|
||||||
|
|
||||||
Upstream hides the whole MessageItem when is-deleted is true via a
|
Upstream hides the whole MessageItem when is-deleted is true via a
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
From 1546f95d8b6666a7f0e9575ddfa85bcb7ec53e3d Mon Sep 17 00:00:00 2001
|
From 1546f95d8b6666a7f0e9575ddfa85bcb7ec53e3d Mon Sep 17 00:00:00 2001
|
||||||
From: Simon Gardling <titaniumtown@proton.me>
|
From: Simon Gardling <titaniumtown@proton.me>
|
||||||
Date: Wed, 6 May 2026 14:38:07 -0400
|
Date: Wed, 6 May 2026 14:38:07 -0400
|
||||||
Subject: [PATCH 6/6] fix(backend): Refresh cached channels on init_channels
|
Subject: [PATCH 6/7] fix(backend): Refresh cached channels on init_channels
|
||||||
re-runs
|
re-runs
|
||||||
|
|
||||||
init_channels populates the channel cache via or_insert_with, then
|
init_channels populates the channel cache via or_insert_with, then
|
||||||
|
|||||||
@@ -0,0 +1,759 @@
|
|||||||
|
From e13d8cbf15a2eea323397accb2183e2362dae106 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Simon Gardling <titaniumtown@proton.me>
|
||||||
|
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
|
||||||
|
|
||||||
|
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`.
|
||||||
|
---
|
||||||
|
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(-)
|
||||||
|
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
|
||||||
|
--- 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.
|
||||||
|
|
||||||
|
## [0.20.4] - 2026-04-22
|
||||||
|
|
||||||
|
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..863564b3 100644
|
||||||
|
--- a/data/resources/resources.gresource.xml.in
|
||||||
|
+++ b/data/resources/resources.gresource.xml.in
|
||||||
|
@@ -2,6 +2,10 @@
|
||||||
|
<gresources>
|
||||||
|
<gresource prefix="/">
|
||||||
|
<file alias="icon.svg">../icons/de.schmidhuberj.Flare.svg</file>
|
||||||
|
+ <!-- Photo bundled into the screenshot-mode build only as the demo image-->
|
||||||
|
+ <!-- attachment in `src/backend/dummy.rs`. "Big red apple.jpg" by-->
|
||||||
|
+ <!-- Paolo Neo, public domain, from Wikimedia Commons.-->
|
||||||
|
+ <file alias="dummy_apple.jpg">../screenshots/dummy_apple.jpg</file>
|
||||||
|
|
||||||
|
<file preprocess="xml-stripblanks">ui/window.ui</file>
|
||||||
|
<file preprocess="xml-stripblanks">ui/channel_list.ui</file>
|
||||||
|
@@ -13,6 +17,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/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
|
||||||
|
+++ 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..1b27cc09
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/data/resources/ui/image_viewer.blp
|
||||||
|
@@ -0,0 +1,64 @@
|
||||||
|
+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 {
|
||||||
|
+ 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 <bool>;
|
||||||
|
- visible: bind $is_some(channel_list.active-channel) as <bool>;
|
||||||
|
- 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 <bool>;
|
||||||
|
+ visible: bind $is_some(channel_list.active-channel) as <bool>;
|
||||||
|
+ 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
|
||||||
|
--- a/src/backend/dummy.rs
|
||||||
|
+++ b/src/backend/dummy.rs
|
||||||
|
@@ -382,18 +382,40 @@ 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::<TextMessage>()
|
||||||
|
.unwrap()
|
||||||
|
- .add_attachment(attachment);
|
||||||
|
+ .add_attachment(fruit_photo);
|
||||||
|
+ let msg_fruit_answer = msg!(self, "apple", 0, 2, 32 + base_minute);
|
||||||
|
|
||||||
|
vec![
|
||||||
|
msg_replied,
|
||||||
|
- // 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
|
||||||
|
--- 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,33 @@ 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;
|
||||||
|
+ }
|
||||||
|
+ // 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::<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..b3e9d720
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/src/gui/image_viewer.rs
|
||||||
|
@@ -0,0 +1,120 @@
|
||||||
|
+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<imp::ImageViewer>)
|
||||||
|
+ @extends adw::Bin, gtk::Widget,
|
||||||
|
+ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+impl ImageViewer {
|
||||||
|
+ pub fn new(attachment: &Attachment) -> Self {
|
||||||
|
+ log::trace!("Initializing `ImageViewer`");
|
||||||
|
+ Object::builder::<Self>()
|
||||||
|
+ .property("attachment", attachment)
|
||||||
|
+ .build()
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+pub mod imp {
|
||||||
|
+ use crate::prelude::*;
|
||||||
|
+
|
||||||
|
+ use glib::subclass::{InitializingObject, Signal};
|
||||||
|
+ use gtk::{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>,
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ #[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";
|
||||||
|
+ type Type = super::ImageViewer;
|
||||||
|
+ type ParentType = adw::Bin;
|
||||||
|
+
|
||||||
|
+ fn class_init(klass: &mut Self::Class) {
|
||||||
|
+ Self::bind_template(klass);
|
||||||
|
+ Self::bind_template_callbacks(klass);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ fn instance_init(obj: &InitializingObject<Self>) {
|
||||||
|
+ obj.init_template();
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ #[glib::derived_properties]
|
||||||
|
+ impl ObjectImpl for ImageViewer {
|
||||||
|
+ fn signals() -> &'static [Signal] {
|
||||||
|
+ static SIGNALS: Lazy<Vec<Signal>> =
|
||||||
|
+ 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 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 ce097ce1..bd9474e2 100644
|
||||||
|
--- a/src/gui/window.rs
|
||||||
|
+++ b/src/gui/window.rs
|
||||||
|
@@ -104,6 +104,33 @@ 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
|
||||||
|
+ }
|
||||||
|
+ ),
|
||||||
|
+ );
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod imp {
|
||||||
|
@@ -142,6 +169,8 @@ pub mod imp {
|
||||||
|
#[template_child]
|
||||||
|
split_view: TemplateChild<adw::NavigationSplitView>,
|
||||||
|
#[template_child]
|
||||||
|
+ pub(super) image_viewer_overlay: TemplateChild<gtk::Overlay>,
|
||||||
|
+ #[template_child]
|
||||||
|
pub(super) channel_list: TemplateChild<ChannelList>,
|
||||||
|
#[template_child]
|
||||||
|
subtitle_label: TemplateChild<gtk::Label>,
|
||||||
|
@@ -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
|
||||||
|
|
||||||
BIN
patches/flare/dummy_apple.jpg
Normal file
BIN
patches/flare/dummy_apple.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 276 KiB |
Reference in New Issue
Block a user