flare: add patched flare-signal with five local feature patches
- patches/flare/000{1..5}-*.patch: typing indicators, formatted
messages, edited messages, multi-select with delete-for-me, and
in-channel message search. Mirror the matching commits in
~/projects/forks/flare and apply cleanly on top of upstream 0.20.4
(which is what nixpkgs ships).
- home/profiles/gui.nix: include a flare-signal override that appends
the patches via overrideAttrs. None of them touch Cargo.lock so the
cargoDeps hash stays valid; signal-desktop stays alongside it.
This commit is contained in:
578
patches/flare/0005-feat-messages-In-channel-message-search.patch
Normal file
578
patches/flare/0005-feat-messages-In-channel-message-search.patch
Normal file
@@ -0,0 +1,578 @@
|
||||
From 7066327c28717de189dbc38209d3a4847eb7e838 Mon Sep 17 00:00:00 2001
|
||||
From: Simon Gardling <titaniumtown@proton.me>
|
||||
Date: Wed, 29 Apr 2026 19:58:54 -0400
|
||||
Subject: [PATCH 5/6] feat(messages): In-channel message search
|
||||
|
||||
- Add a SearchBar above the message list that searches the
|
||||
currently-loaded timeline using a case-insensitive substring match
|
||||
against the message body. Bind it to a new
|
||||
channel-messages.toggle-search action wired to Ctrl+Shift+F.
|
||||
- Surface a match counter (current/total) and previous/next buttons
|
||||
next to the entry, plus reuse the existing flash_requires_attention
|
||||
helper to scroll to and briefly highlight the focused match.
|
||||
- Reset matches when the bar closes or the active channel changes.
|
||||
---
|
||||
CHANGELOG.md | 1 +
|
||||
data/resources/style.css | 10 +
|
||||
data/resources/ui/channel_messages.blp | 55 ++++++
|
||||
data/resources/ui/shortcuts.blp | 5 +
|
||||
src/backend/message/display_message.rs | 8 +
|
||||
src/backend/timeline/mod.rs | 10 +
|
||||
src/gui/channel_messages.rs | 248 ++++++++++++++++++++++++-
|
||||
src/gui/message_item.rs | 25 +++
|
||||
src/gui/window.rs | 11 ++
|
||||
9 files changed, 372 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/CHANGELOG.md b/CHANGELOG.md
|
||||
index 47ec77a..16880cd 100644
|
||||
--- a/CHANGELOG.md
|
||||
+++ b/CHANGELOG.md
|
||||
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Send formatted messages with markdown-style markers (`**bold**`, `*italic*`, `~~strike~~`, `||spoiler||`, `` `monospace` ``).
|
||||
- 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.
|
||||
|
||||
## [0.20.4] - 2026-04-22
|
||||
|
||||
diff --git a/data/resources/style.css b/data/resources/style.css
|
||||
index 1c0cdfd..e00e789 100644
|
||||
--- a/data/resources/style.css
|
||||
+++ b/data/resources/style.css
|
||||
@@ -294,3 +294,13 @@ unread-indicator {
|
||||
background: alpha(@accent_bg_color, 0.7);
|
||||
}
|
||||
}
|
||||
+
|
||||
+
|
||||
+/* Persistent highlight for the message currently focused by the in-channel
|
||||
+ search bar. Stays applied while the user is parked on this match;
|
||||
+ cleared when navigating to a different match or closing the search. */
|
||||
+.search-match .message-bubble {
|
||||
+ background: alpha(@accent_bg_color, 0.35);
|
||||
+ outline: 2px solid alpha(@accent_color, 0.7);
|
||||
+ outline-offset: -2px;
|
||||
+}
|
||||
\ No newline at end of file
|
||||
diff --git a/data/resources/ui/channel_messages.blp b/data/resources/ui/channel_messages.blp
|
||||
index eb927f8..a166f7b 100644
|
||||
--- a/data/resources/ui/channel_messages.blp
|
||||
+++ b/data/resources/ui/channel_messages.blp
|
||||
@@ -43,6 +43,61 @@ template $FlChannelMessages: Box {
|
||||
hexpand: true;
|
||||
orientation: vertical;
|
||||
|
||||
+ // In-channel message search.
|
||||
+ SearchBar search_bar {
|
||||
+ key-capture-widget: scrolled_window;
|
||||
+ search-mode-enabled: bind template.search-active bidirectional;
|
||||
+
|
||||
+ Adw.Clamp {
|
||||
+ maximum-size: 600;
|
||||
+
|
||||
+ Box {
|
||||
+ orientation: horizontal;
|
||||
+ spacing: 6;
|
||||
+
|
||||
+ SearchEntry search_entry {
|
||||
+ hexpand: true;
|
||||
+ placeholder-text: _("Search loaded messages");
|
||||
+ search-changed => $on_search_query_changed() swapped;
|
||||
+ previous-match => $on_search_previous() swapped;
|
||||
+ next-match => $on_search_next() swapped;
|
||||
+ stop-search => $on_search_stop() swapped;
|
||||
+ }
|
||||
+
|
||||
+ Label {
|
||||
+ styles [
|
||||
+ "caption",
|
||||
+ "dim-label",
|
||||
+ ]
|
||||
+
|
||||
+ label: bind template.search-summary;
|
||||
+ }
|
||||
+
|
||||
+ Button {
|
||||
+ icon-name: "go-up-symbolic";
|
||||
+ tooltip-text: C_("tooltip", "Previous match");
|
||||
+ sensitive: bind template.has-matches;
|
||||
+ clicked => $on_search_previous() swapped;
|
||||
+
|
||||
+ styles [
|
||||
+ "flat",
|
||||
+ ]
|
||||
+ }
|
||||
+
|
||||
+ Button {
|
||||
+ icon-name: "go-down-symbolic";
|
||||
+ tooltip-text: C_("tooltip", "Next match");
|
||||
+ sensitive: bind template.has-matches;
|
||||
+ clicked => $on_search_next() swapped;
|
||||
+
|
||||
+ styles [
|
||||
+ "flat",
|
||||
+ ]
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
Overlay {
|
||||
[overlay]
|
||||
Adw.Spinner {
|
||||
diff --git a/data/resources/ui/shortcuts.blp b/data/resources/ui/shortcuts.blp
|
||||
index ed2a959..79339cc 100644
|
||||
--- a/data/resources/ui/shortcuts.blp
|
||||
+++ b/data/resources/ui/shortcuts.blp
|
||||
@@ -58,5 +58,10 @@ Adw.ShortcutsDialog help_overlay {
|
||||
title: C_("shortcut window", "Load more messages");
|
||||
accelerator: "<Ctrl>&l";
|
||||
}
|
||||
+
|
||||
+ Adw.ShortcutsItem {
|
||||
+ title: C_("shortcut window", "Search messages in current channel");
|
||||
+ accelerator: "<Ctrl><Shift>&f";
|
||||
+ }
|
||||
}
|
||||
}
|
||||
diff --git a/src/backend/message/display_message.rs b/src/backend/message/display_message.rs
|
||||
index 4cd5e7f..4ccb763 100644
|
||||
--- a/src/backend/message/display_message.rs
|
||||
+++ b/src/backend/message/display_message.rs
|
||||
@@ -137,6 +137,7 @@ mod imp {
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DisplayMessage {
|
||||
pub(super) requires_attention: Cell<bool>,
|
||||
+ pub(super) is_search_match: Cell<bool>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
@@ -155,6 +156,7 @@ mod imp {
|
||||
.read_only()
|
||||
.build(),
|
||||
ParamSpecBoolean::builder("requires-attention").build(),
|
||||
+ ParamSpecBoolean::builder("is-search-match").build(),
|
||||
]
|
||||
});
|
||||
|
||||
@@ -168,6 +170,11 @@ mod imp {
|
||||
.get()
|
||||
.expect("requires-attention parameter to be boolean"),
|
||||
),
|
||||
+ "is-search-match" => self.is_search_match.set(
|
||||
+ value
|
||||
+ .get()
|
||||
+ .expect("is-search-match parameter to be boolean"),
|
||||
+ ),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
@@ -176,6 +183,7 @@ mod imp {
|
||||
match pspec.name() {
|
||||
"textual-description" => self.obj().textual_description().to_value(),
|
||||
"requires-attention" => self.requires_attention.get().to_value(),
|
||||
+ "is-search-match" => self.is_search_match.get().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
diff --git a/src/backend/timeline/mod.rs b/src/backend/timeline/mod.rs
|
||||
index 18dd436..40fe324 100644
|
||||
--- a/src/backend/timeline/mod.rs
|
||||
+++ b/src/backend/timeline/mod.rs
|
||||
@@ -69,6 +69,16 @@ impl Timeline {
|
||||
}
|
||||
}
|
||||
|
||||
+ /// Returns the index of the item with the given timestamp, if any.
|
||||
+ /// Used by the in-channel search to scroll the list view to the
|
||||
+ /// matched row even when it has not yet been materialized.
|
||||
+ pub fn position_of(&self, timestamp: u64) -> Option<u32> {
|
||||
+ let list = self.imp().list.borrow();
|
||||
+ list.binary_search_by_key(×tamp, |i| i.timestamp())
|
||||
+ .ok()
|
||||
+ .map(|i| i as u32)
|
||||
+ }
|
||||
+
|
||||
pub fn iter_forwards(&self) -> impl Iterator<Item = TimelineItem> + 'static {
|
||||
let current_items = self.imp().list.borrow();
|
||||
current_items.clone().into_iter()
|
||||
diff --git a/src/gui/channel_messages.rs b/src/gui/channel_messages.rs
|
||||
index 747b36a..4f00b73 100644
|
||||
--- a/src/gui/channel_messages.rs
|
||||
+++ b/src/gui/channel_messages.rs
|
||||
@@ -85,6 +85,204 @@ impl ChannelMessages {
|
||||
self.set_has_selection(count > 0);
|
||||
}
|
||||
|
||||
+ /// Connect the `search-active` property so the entry is focused when
|
||||
+ /// the bar opens and the matches are cleared when it closes.
|
||||
+ fn setup_search(&self) {
|
||||
+ self.connect_notify_local(
|
||||
+ Some("search-active"),
|
||||
+ clone!(
|
||||
+ #[weak(rename_to = s)]
|
||||
+ self,
|
||||
+ move |_, _| {
|
||||
+ if s.search_active() {
|
||||
+ s.imp().search_entry.grab_focus();
|
||||
+ s.attach_search_message_listener();
|
||||
+ } else {
|
||||
+ s.detach_search_message_listener();
|
||||
+ s.clear_current_search_match();
|
||||
+ s.imp().search_entry.set_text("");
|
||||
+ s.imp().search_matches.replace(Vec::new());
|
||||
+ s.imp().search_index.set(0);
|
||||
+ s.set_has_matches(false);
|
||||
+ s.set_search_summary(String::new());
|
||||
+ }
|
||||
+ }
|
||||
+ ),
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
+ /// While the search bar is open, watch the active channel for new
|
||||
+ /// messages so the result set stays in sync with the timeline. Only
|
||||
+ /// one handler is alive at a time; `detach_search_message_listener` or
|
||||
+ /// the next `attach` call clears the previous one.
|
||||
+ fn attach_search_message_listener(&self) {
|
||||
+ self.detach_search_message_listener();
|
||||
+ let Some(channel) = self.active_channel() else {
|
||||
+ return;
|
||||
+ };
|
||||
+ let handler = channel.connect_local(
|
||||
+ "message",
|
||||
+ false,
|
||||
+ clone!(
|
||||
+ #[weak(rename_to = s)]
|
||||
+ self,
|
||||
+ #[upgrade_or_default]
|
||||
+ move |_| {
|
||||
+ let query = s.imp().search_entry.text().to_string();
|
||||
+ s.refresh_search(&query);
|
||||
+ None
|
||||
+ }
|
||||
+ ),
|
||||
+ );
|
||||
+ self.imp()
|
||||
+ .search_message_handler
|
||||
+ .replace(Some((channel, handler)));
|
||||
+ }
|
||||
+
|
||||
+ fn detach_search_message_listener(&self) {
|
||||
+ if let Some((channel, handler)) = self.imp().search_message_handler.take() {
|
||||
+ channel.disconnect(handler);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ /// Re-run the loaded-message search using `query`. The search is
|
||||
+ /// case-insensitive substring match against the message body.
|
||||
+ pub fn refresh_search(&self, query: &str) {
|
||||
+ let imp = self.imp();
|
||||
+ let Some(channel) = self.active_channel() else {
|
||||
+ self.clear_current_search_match();
|
||||
+ imp.search_matches.replace(Vec::new());
|
||||
+ imp.search_index.set(0);
|
||||
+ self.set_has_matches(false);
|
||||
+ self.set_search_summary(String::new());
|
||||
+ return;
|
||||
+ };
|
||||
+
|
||||
+ let trimmed = query.trim();
|
||||
+ if trimmed.is_empty() {
|
||||
+ imp.search_matches.replace(Vec::new());
|
||||
+ self.clear_current_search_match();
|
||||
+ imp.search_index.set(0);
|
||||
+ self.set_has_matches(false);
|
||||
+ self.set_search_summary(String::new());
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ let needle = trimmed.to_lowercase();
|
||||
+ let matches: Vec<u64> = channel
|
||||
+ .timeline()
|
||||
+ .iter_forwards()
|
||||
+ .filter_map(|i| i.dynamic_cast::<TextMessage>().ok())
|
||||
+ .filter(|m| {
|
||||
+ m.body()
|
||||
+ .map(|b| b.to_lowercase().contains(&needle))
|
||||
+ .unwrap_or(false)
|
||||
+ })
|
||||
+ .map(|m| m.timestamp())
|
||||
+ .collect();
|
||||
+
|
||||
+ let total = matches.len();
|
||||
+ imp.search_matches.replace(matches);
|
||||
+ // Snap to the latest match (most recent in time) by default so the
|
||||
+ // user lands at the bottom of the conversation, matching how the
|
||||
+ // existing scroll-to-unread heuristic works.
|
||||
+ imp.search_index.set(total.saturating_sub(1));
|
||||
+ self.set_has_matches(total > 0);
|
||||
+ self.update_search_summary();
|
||||
+ self.focus_current_search_match();
|
||||
+ }
|
||||
+
|
||||
+ /// Move the search cursor to the next or previous match and flash that
|
||||
+ /// message.
|
||||
+ pub fn goto_search_match(&self, forwards: bool) {
|
||||
+ let imp = self.imp();
|
||||
+ let total = imp.search_matches.borrow().len();
|
||||
+ if total == 0 {
|
||||
+ return;
|
||||
+ }
|
||||
+ let current = imp.search_index.get();
|
||||
+ let next = if forwards {
|
||||
+ (current + 1) % total
|
||||
+ } else if current == 0 {
|
||||
+ total - 1
|
||||
+ } else {
|
||||
+ current - 1
|
||||
+ };
|
||||
+ imp.search_index.set(next);
|
||||
+ self.update_search_summary();
|
||||
+ self.focus_current_search_match();
|
||||
+ }
|
||||
+
|
||||
+ fn update_search_summary(&self) {
|
||||
+ let imp = self.imp();
|
||||
+ let total = imp.search_matches.borrow().len();
|
||||
+ let summary = if total == 0 {
|
||||
+ String::new()
|
||||
+ } else {
|
||||
+ // Translators: e.g. "3 of 12" indicating the focused match
|
||||
+ // index out of total search matches.
|
||||
+ gettextrs::gettext("{current} of {total}")
|
||||
+ .replace("{current}", &(imp.search_index.get() + 1).to_string())
|
||||
+ .replace("{total}", &total.to_string())
|
||||
+ };
|
||||
+ self.set_search_summary(summary);
|
||||
+ }
|
||||
+
|
||||
+ /// Drop the persistent highlight from the previously-focused match,
|
||||
+ /// if any. Used both when navigating to a new match and when the
|
||||
+ /// search bar closes.
|
||||
+ fn clear_current_search_match(&self) {
|
||||
+ if let Some(prev) = self.imp().current_search_match.take() {
|
||||
+ prev.set_property("is-search-match", false);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ /// Mark the currently-indexed match with the persistent `search-match`
|
||||
+ /// highlight and scroll the list view so the row is visible (and
|
||||
+ /// materialized as a `MessageItem` that can react to the property).
|
||||
+ fn focus_current_search_match(&self) {
|
||||
+ let imp = self.imp();
|
||||
+ let Some(timestamp) = imp
|
||||
+ .search_matches
|
||||
+ .borrow()
|
||||
+ .get(imp.search_index.get())
|
||||
+ .copied()
|
||||
+ else {
|
||||
+ self.clear_current_search_match();
|
||||
+ return;
|
||||
+ };
|
||||
+ let Some(channel) = self.active_channel() else {
|
||||
+ self.clear_current_search_match();
|
||||
+ return;
|
||||
+ };
|
||||
+ let timeline = channel.timeline();
|
||||
+ let Some(item) = timeline.get_by_timestamp(timestamp) else {
|
||||
+ self.clear_current_search_match();
|
||||
+ return;
|
||||
+ };
|
||||
+ let Some(msg) = item.dynamic_cast::<DisplayMessage>().ok() else {
|
||||
+ self.clear_current_search_match();
|
||||
+ return;
|
||||
+ };
|
||||
+ // Skip the property churn if the focused match has not changed
|
||||
+ // (e.g. typing more characters while the latest match remains the
|
||||
+ // best hit).
|
||||
+ let already_focused = imp
|
||||
+ .current_search_match
|
||||
+ .borrow()
|
||||
+ .as_ref()
|
||||
+ .is_some_and(|prev| prev == &msg);
|
||||
+ if !already_focused {
|
||||
+ self.clear_current_search_match();
|
||||
+ msg.set_property("is-search-match", true);
|
||||
+ imp.current_search_match.replace(Some(msg));
|
||||
+ }
|
||||
+ if let Some(position) = timeline.position_of(timestamp) {
|
||||
+ imp.list_view
|
||||
+ .scroll_to(position, gtk::ListScrollFlags::NONE, None);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
pub fn focus_input(&self) {
|
||||
self.imp().text_entry.grab_focus();
|
||||
}
|
||||
@@ -431,7 +629,7 @@ pub mod imp {
|
||||
use crate::gui::components::ItemRow;
|
||||
use crate::gui::components::time_divider::TimeDivider;
|
||||
use crate::{
|
||||
- backend::{Channel, Manager, message::TextMessage},
|
||||
+ backend::{Channel, Manager, message::{DisplayMessage, TextMessage}},
|
||||
gui::{error_dialog::ErrorDialog, message_item::MessageItem, text_entry::TextEntry},
|
||||
};
|
||||
|
||||
@@ -446,6 +644,8 @@ pub mod imp {
|
||||
#[template_child]
|
||||
pub(super) text_entry: TemplateChild<TextEntry>,
|
||||
#[template_child]
|
||||
+ pub(super) search_entry: TemplateChild<gtk::SearchEntry>,
|
||||
+ #[template_child]
|
||||
pub(super) list_view: TemplateChild<gtk::ListView>,
|
||||
#[template_child]
|
||||
no_channels_page: TemplateChild<adw::StatusPage>,
|
||||
@@ -478,6 +678,29 @@ pub mod imp {
|
||||
#[property(get, set)]
|
||||
has_selection: Cell<bool>,
|
||||
|
||||
+ #[property(get, set)]
|
||||
+ search_active: Cell<bool>,
|
||||
+ #[property(get, set)]
|
||||
+ search_summary: RefCell<String>,
|
||||
+ #[property(get, set)]
|
||||
+ has_matches: Cell<bool>,
|
||||
+
|
||||
+ /// Cached timestamps of every message currently matching the search
|
||||
+ /// query, in chronological order.
|
||||
+ pub(super) search_matches: RefCell<Vec<u64>>,
|
||||
+ /// Index into `search_matches` of the currently focused match.
|
||||
+ pub(super) search_index: Cell<usize>,
|
||||
+ /// Handler installed on the active channel's `message` signal
|
||||
+ /// while the search bar is open, so newly-arrived messages are
|
||||
+ /// folded into the match set without the user re-running the
|
||||
+ /// search by hand.
|
||||
+ pub(super) search_message_handler:
|
||||
+ RefCell<Option<(Channel, glib::SignalHandlerId)>>,
|
||||
+ /// The match the user is currently parked on. Held so we can clear
|
||||
+ /// its `is-search-match` flag when navigating to a different match
|
||||
+ /// or when the search bar closes.
|
||||
+ pub(super) current_search_match: RefCell<Option<DisplayMessage>>,
|
||||
+
|
||||
/// Whether we currently believe the user is composing a message in the
|
||||
/// active channel and have informed the peer with a `Started` event.
|
||||
pub(super) sending_typing: Cell<bool>,
|
||||
@@ -520,6 +743,7 @@ pub mod imp {
|
||||
self.obj().setup_send_on_enter();
|
||||
self.obj().setup_typing_settings();
|
||||
self.obj().setup_typing_send();
|
||||
+ self.obj().setup_search();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,6 +752,7 @@ pub mod imp {
|
||||
// forget about it.
|
||||
self.obj().send_typing_stopped();
|
||||
self.obj().exit_selection_mode();
|
||||
+ self.obj().set_search_active(false);
|
||||
|
||||
if let Some(active_chan) = self.active_channel.borrow().as_ref() {
|
||||
active_chan.set_property("draft", self.text_entry.text());
|
||||
@@ -820,6 +1045,27 @@ pub mod imp {
|
||||
}
|
||||
}
|
||||
|
||||
+ #[template_callback]
|
||||
+ fn on_search_query_changed(&self) {
|
||||
+ let query = self.search_entry.text().to_string();
|
||||
+ self.obj().refresh_search(&query);
|
||||
+ }
|
||||
+
|
||||
+ #[template_callback]
|
||||
+ fn on_search_previous(&self) {
|
||||
+ self.obj().goto_search_match(false);
|
||||
+ }
|
||||
+
|
||||
+ #[template_callback]
|
||||
+ fn on_search_next(&self) {
|
||||
+ self.obj().goto_search_match(true);
|
||||
+ }
|
||||
+
|
||||
+ #[template_callback]
|
||||
+ fn on_search_stop(&self) {
|
||||
+ self.obj().set_search_active(false);
|
||||
+ }
|
||||
+
|
||||
#[template_callback]
|
||||
fn handle_row_activated(&self, row: gtk::ListBoxRow) {
|
||||
if let Ok(msg) = row
|
||||
diff --git a/src/gui/message_item.rs b/src/gui/message_item.rs
|
||||
index d88306f..dbf64a7 100644
|
||||
--- a/src/gui/message_item.rs
|
||||
+++ b/src/gui/message_item.rs
|
||||
@@ -33,6 +33,7 @@ impl MessageItem {
|
||||
s.setup_loaded();
|
||||
s.setup_text();
|
||||
s.setup_requires_attention();
|
||||
+ s.setup_search_match();
|
||||
s.setup_pending_and_error();
|
||||
s.setup_selection();
|
||||
s
|
||||
@@ -325,6 +326,30 @@ impl MessageItem {
|
||||
message.notify("requires-attention");
|
||||
}
|
||||
|
||||
+ /// Reflect the message's `is-search-match` state by toggling the
|
||||
+ /// `search-match` CSS class. Unlike `requires-attention`, this state
|
||||
+ /// is not auto-cleared after a delay; it stays set until the channel
|
||||
+ /// search navigates to a different match or closes.
|
||||
+ pub fn setup_search_match(&self) {
|
||||
+ let message = self.message();
|
||||
+ self.track_notify_local(&message, "is-search-match", {
|
||||
+ let s = self.downgrade();
|
||||
+ move |m, _| {
|
||||
+ let Some(s) = s.upgrade() else {
|
||||
+ return;
|
||||
+ };
|
||||
+ if m.property("is-search-match") {
|
||||
+ s.add_css_class("search-match");
|
||||
+ } else {
|
||||
+ s.remove_css_class("search-match");
|
||||
+ }
|
||||
+ }
|
||||
+ });
|
||||
+ if message.property("is-search-match") {
|
||||
+ self.add_css_class("search-match");
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
pub fn setup_pending_and_error(&self) {
|
||||
let message = self.message();
|
||||
message.connect_notify_local(
|
||||
diff --git a/src/gui/window.rs b/src/gui/window.rs
|
||||
index 6335f3d..ce097ce 100644
|
||||
--- a/src/gui/window.rs
|
||||
+++ b/src/gui/window.rs
|
||||
@@ -20,6 +20,7 @@ impl Window {
|
||||
app.set_accels_for_action("window.close", &["<Control>q"]);
|
||||
app.set_accels_for_action("channel-messages.activate-input", &["<Control>i"]);
|
||||
app.set_accels_for_action("channel-messages.load-more", &["<Control>l"]);
|
||||
+ app.set_accels_for_action("channel-messages.toggle-search", &["<Control><Shift>f"]);
|
||||
for i in 1..=9 {
|
||||
app.set_accels_for_action(
|
||||
&format!("channel-list.activate-channel({i})"),
|
||||
@@ -531,10 +532,20 @@ pub mod imp {
|
||||
channel_messages.load_more();
|
||||
}
|
||||
));
|
||||
+ let action_toggle_search = SimpleAction::new("toggle-search", None);
|
||||
+ action_toggle_search.connect_activate(clone!(
|
||||
+ #[strong(rename_to = channel_messages)]
|
||||
+ self.channel_messages,
|
||||
+ move |_, _| {
|
||||
+ let active = !channel_messages.search_active();
|
||||
+ channel_messages.set_search_active(active);
|
||||
+ }
|
||||
+ ));
|
||||
let actions = SimpleActionGroup::new();
|
||||
obj.insert_action_group("channel-messages", Some(&actions));
|
||||
actions.add_action(&action_activate_input);
|
||||
actions.add_action(&action_load_more);
|
||||
+ actions.add_action(&action_toggle_search);
|
||||
|
||||
// Channel list actions.
|
||||
|
||||
--
|
||||
2.53.0
|
||||
|
||||
Reference in New Issue
Block a user