From 78d6aa01d5050f4f6c8d251a152dd4e86f6e3bed Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Wed, 6 May 2026 15:21:53 -0400 Subject: [PATCH] flare: update patches and upstream source --- flake.lock | 17 + flake.nix | 8 + home/profiles/gui.nix | 24 +- ...t-typing-Implement-typing-indicators.patch | 51 +- ...-messages-Implement-edited-messages.patch} | 86 +-- ...essages-Implement-formatted-messages.patch | 622 ------------------ ...ti-select-messages-and-delete-for-m.patch} | 36 +- ...-messages-In-channel-message-search.patch} | 28 +- ...w-This-message-was-deleted.-placeho.patch} | 10 +- ...esh-cached-channels-on-init_channels.patch | 76 +++ 10 files changed, 201 insertions(+), 757 deletions(-) rename patches/flare/{0003-feat-messages-Implement-edited-messages.patch => 0002-feat-messages-Implement-edited-messages.patch} (92%) delete mode 100644 patches/flare/0002-feat-messages-Implement-formatted-messages.patch rename patches/flare/{0004-feat-messages-Multi-select-messages-and-delete-for-m.patch => 0003-feat-messages-Multi-select-messages-and-delete-for-m.patch} (96%) rename patches/flare/{0005-feat-messages-In-channel-message-search.patch => 0004-feat-messages-In-channel-message-search.patch} (97%) rename patches/flare/{0006-feat-messages-Show-This-message-was-deleted.-placeho.patch => 0005-feat-messages-Show-This-message-was-deleted.-placeho.patch} (96%) create mode 100644 patches/flare/0006-fix-backend-Refresh-cached-channels-on-init_channels.patch diff --git a/flake.lock b/flake.lock index 229e9ed..ab92321 100644 --- a/flake.lock +++ b/flake.lock @@ -491,6 +491,22 @@ "type": "github" } }, + "flare-upstream": { + "flake": false, + "locked": { + "lastModified": 1778090949, + "narHash": "sha256-YLT3gZtkU4cYwlXC3nWdRSPgAi/5wc3kffA+uvla2aQ=", + "owner": "schmiddi-on-mobile", + "repo": "flare", + "rev": "5ec4d87c394709e859d745dfb19e4f097040858f", + "type": "gitlab" + }, + "original": { + "owner": "schmiddi-on-mobile", + "repo": "flare", + "type": "gitlab" + } + }, "gitignore": { "inputs": { "nixpkgs": [ @@ -1102,6 +1118,7 @@ "disko": "disko", "emacs-overlay": "emacs-overlay", "firefox-addons": "firefox-addons", + "flare-upstream": "flare-upstream", "home-manager": "home-manager", "home-manager-stable": "home-manager-stable", "impermanence": "impermanence", diff --git a/flake.nix b/flake.nix index 82b3031..9d4142f 100644 --- a/flake.nix +++ b/flake.nix @@ -95,6 +95,14 @@ flake = false; }; + # Upstream Flare Signal source — overridden into pkgs.flare-signal in + # home/profiles/gui.nix so the patches under patches/flare/ apply against + # current master rather than whatever stale snapshot nixpkgs is shipping. + flare-upstream = { + url = "gitlab:schmiddi-on-mobile/flare"; + flake = false; + }; + # Server (muffin) — follows nixpkgs-stable nix-minecraft = { url = "github:Infinidoge/nix-minecraft"; diff --git a/home/profiles/gui.nix b/home/profiles/gui.nix index d703f06..57c5af3 100644 --- a/home/profiles/gui.nix +++ b/home/profiles/gui.nix @@ -86,18 +86,24 @@ signal-desktop - # alternative GTK signal client; carries five local feature patches - # under patches/flare/ on top of upstream 0.20.4 (typing indicators, - # formatted messages, edited messages, multi-select with delete-for-me, - # and in-channel message search). + # 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, and the not-yet-merged init_channels + # cache-miss fix). (pkgs.flare-signal.overrideAttrs (old: { + src = inputs.flare-upstream; + cargoDeps = pkgs.rustPlatform.importCargoLock { + lockFile = "${inputs.flare-upstream}/Cargo.lock"; + allowBuiltinFetchGit = true; + }; patches = (old.patches or [ ]) ++ [ ../../patches/flare/0001-feat-typing-Implement-typing-indicators.patch - ../../patches/flare/0002-feat-messages-Implement-formatted-messages.patch - ../../patches/flare/0003-feat-messages-Implement-edited-messages.patch - ../../patches/flare/0004-feat-messages-Multi-select-messages-and-delete-for-m.patch - ../../patches/flare/0005-feat-messages-In-channel-message-search.patch - ../../patches/flare/0006-feat-messages-Show-This-message-was-deleted.-placeho.patch + ../../patches/flare/0002-feat-messages-Implement-edited-messages.patch + ../../patches/flare/0003-feat-messages-Multi-select-messages-and-delete-for-m.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/0006-fix-backend-Refresh-cached-channels-on-init_channels.patch ]; })) diff --git a/patches/flare/0001-feat-typing-Implement-typing-indicators.patch b/patches/flare/0001-feat-typing-Implement-typing-indicators.patch index fe95f75..d423c13 100644 --- a/patches/flare/0001-feat-typing-Implement-typing-indicators.patch +++ b/patches/flare/0001-feat-typing-Implement-typing-indicators.patch @@ -1,4 +1,4 @@ -From 9ec9203bd47b7369e2a97fee2d6896576da23da0 Mon Sep 17 00:00:00 2001 +From 7ab41203098dd868ee70249fcd78c8444438d80c Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Wed, 29 Apr 2026 19:00:12 -0400 Subject: [PATCH 1/6] feat(typing): Implement typing indicators @@ -20,15 +20,15 @@ Subject: [PATCH 1/6] feat(typing): Implement typing indicators data/resources/ui/channel_messages.blp | 33 +++ data/resources/ui/preferences_window.blp | 15 ++ src/backend/channel.rs | 59 +++++- - src/backend/manager.rs | 43 +++- + src/backend/manager.rs | 32 ++- src/backend/manager_thread.rs | 8 +- src/backend/message/mod.rs | 12 +- src/gui/channel_messages.rs | 249 ++++++++++++++++++++++- src/gui/preferences_window.rs | 23 +++ - 11 files changed, 441 insertions(+), 21 deletions(-) + 11 files changed, 430 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md -index 20dc578..2bde927 100644 +index 20dc578e..2bde927c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 @@ -44,7 +44,7 @@ index 20dc578..2bde927 100644 ### Fixed diff --git a/data/de.schmidhuberj.Flare.gschema.xml b/data/de.schmidhuberj.Flare.gschema.xml -index 8a58415..0705a73 100644 +index 8a584152..0705a73e 100644 --- a/data/de.schmidhuberj.Flare.gschema.xml +++ b/data/de.schmidhuberj.Flare.gschema.xml @@ -58,6 +58,15 @@ @@ -64,7 +64,7 @@ index 8a58415..0705a73 100644 "firstname" How to sort contacts, e.g with "firstname" or "surname" diff --git a/data/resources/style.css b/data/resources/style.css -index dcd0569..00e4783 100644 +index dcd05695..00e47833 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -13,6 +13,12 @@ @@ -81,7 +81,7 @@ index dcd0569..00e4783 100644 padding:0; } diff --git a/data/resources/ui/channel_messages.blp b/data/resources/ui/channel_messages.blp -index 53be7ab..7f438e4 100644 +index 53be7ab0..7f438e44 100644 --- a/data/resources/ui/channel_messages.blp +++ b/data/resources/ui/channel_messages.blp @@ -102,6 +102,39 @@ template $FlChannelMessages: Box { @@ -125,7 +125,7 @@ index 53be7ab..7f438e4 100644 styles [ "toolbar", diff --git a/data/resources/ui/preferences_window.blp b/data/resources/ui/preferences_window.blp -index dd84f74..2068cab 100644 +index dd84f748..2068cab1 100644 --- a/data/resources/ui/preferences_window.blp +++ b/data/resources/ui/preferences_window.blp @@ -66,6 +66,21 @@ template $FlPreferencesWindow: Adw.PreferencesDialog { @@ -151,7 +151,7 @@ index dd84f74..2068cab 100644 } diff --git a/src/backend/channel.rs b/src/backend/channel.rs -index 73e82f3..4bb1d38 100644 +index 73e82f31..4bb1d385 100644 --- a/src/backend/channel.rs +++ b/src/backend/channel.rs @@ -15,8 +15,9 @@ use glib::Bytes; @@ -229,7 +229,7 @@ index 73e82f3..4bb1d38 100644 /// This does the following (based on the type of message): /// - Add a quote to the message if needed. diff --git a/src/backend/manager.rs b/src/backend/manager.rs -index c25fba0..eaa41e0 100644 +index 47c3dd42..c9079612 100644 --- a/src/backend/manager.rs +++ b/src/backend/manager.rs @@ -8,7 +8,7 @@ use libsignal_service::protocol::DeviceId; @@ -241,22 +241,7 @@ index c25fba0..eaa41e0 100644 protocol::ServiceId, sender::{AttachmentSpec, AttachmentUploadError}, websocket::account::DeviceInfo, -@@ -490,20 +490,42 @@ impl Manager { - Thread::Contact(uuid) - }; - -+ // Fast path: return the cached channel if we already know it. -+ // Without this, callers that arrive after initial channel discovery -+ // (incoming TypingMessage routing, in particular) would receive a -+ // freshly-built Channel object whose property notifications never -+ // reach widgets bound to the cached one in the UI — typing -+ // indicators on both the header bar and the channel-messages view -+ // would silently never light up. -+ if let Some(cached) = self.imp().channels.borrow().get(&thread).cloned() { -+ return cached; -+ } -+ - let contact = Contact::from_service_address(&uuid, self).await; +@@ -499,16 +499,27 @@ impl Manager { let channel = Channel::from_contact_or_group(contact, group, self).await; channel.initialize_avatar().await; @@ -291,7 +276,7 @@ index c25fba0..eaa41e0 100644 } pub fn channel_from_thread(&self, thread: Thread) -> Option { -@@ -737,14 +759,15 @@ impl Manager { +@@ -742,14 +753,15 @@ impl Manager { pub(super) async fn send_message_to_group( &self, group_key: Vec, @@ -310,7 +295,7 @@ index c25fba0..eaa41e0 100644 }) .await diff --git a/src/backend/manager_thread.rs b/src/backend/manager_thread.rs -index 1f6a885..cba62ae 100644 +index 1f6a8854..cba62ae5 100644 --- a/src/backend/manager_thread.rs +++ b/src/backend/manager_thread.rs @@ -21,7 +21,7 @@ use libsignal_service::{ @@ -350,7 +335,7 @@ index 1f6a885..cba62ae 100644 sender, )) diff --git a/src/backend/message/mod.rs b/src/backend/message/mod.rs -index 11ccd7c..74952ac 100644 +index 11ccd7ca..74952acc 100644 --- a/src/backend/message/mod.rs +++ b/src/backend/message/mod.rs @@ -270,14 +270,16 @@ impl Message { @@ -376,7 +361,7 @@ index 11ccd7c..74952ac 100644 let Some(channel) = channel else { diff --git a/src/gui/channel_messages.rs b/src/gui/channel_messages.rs -index 0e8ae4e..831fc25 100644 +index 0d64d25d..2b494a61 100644 --- a/src/gui/channel_messages.rs +++ b/src/gui/channel_messages.rs @@ -5,6 +5,16 @@ use crate::ApplicationError; @@ -650,7 +635,7 @@ index 0e8ae4e..831fc25 100644 if let Some(active_chan) = self.active_channel.borrow().as_ref() { active_chan.set_property("draft", self.text_entry.text()); } -@@ -195,6 +430,7 @@ pub mod imp { +@@ -202,6 +437,7 @@ pub mod imp { } self.obj().focus_input(); @@ -658,7 +643,7 @@ index 0e8ae4e..831fc25 100644 } #[template_callback(function)] -@@ -501,7 +737,18 @@ pub mod imp { +@@ -508,7 +744,18 @@ pub mod imp { s.obj().set_reply_message(None::); if let Some(channel) = s.active_channel.borrow().as_ref() { let draft = channel.property("draft"); @@ -678,7 +663,7 @@ index 0e8ae4e..831fc25 100644 } ), diff --git a/src/gui/preferences_window.rs b/src/gui/preferences_window.rs -index 8137af7..b2b6405 100644 +index 8137af7e..b2b64053 100644 --- a/src/gui/preferences_window.rs +++ b/src/gui/preferences_window.rs @@ -78,6 +78,11 @@ pub mod imp { diff --git a/patches/flare/0003-feat-messages-Implement-edited-messages.patch b/patches/flare/0002-feat-messages-Implement-edited-messages.patch similarity index 92% rename from patches/flare/0003-feat-messages-Implement-edited-messages.patch rename to patches/flare/0002-feat-messages-Implement-edited-messages.patch index 1c072e9..ee84369 100644 --- a/patches/flare/0003-feat-messages-Implement-edited-messages.patch +++ b/patches/flare/0002-feat-messages-Implement-edited-messages.patch @@ -1,7 +1,7 @@ -From 96cabe9e786b4ca8ba89064dfd90e71222e919af Mon Sep 17 00:00:00 2001 +From f198aa5720bbc953fd72ebfffcec4f62f794fb19 Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Wed, 29 Apr 2026 19:33:06 -0400 -Subject: [PATCH 3/6] feat(messages): Implement edited messages +Subject: [PATCH 2/6] feat(messages): Implement edited messages - Receive incoming EditMessage (1-1 and sync) and replace the body, body_ranges, and attachments of the targeted message in place. The @@ -22,30 +22,29 @@ Subject: [PATCH 3/6] feat(messages): Implement edited messages data/resources/ui/message_item.blp | 10 +++ src/backend/channel.rs | 92 ++++++++++++++++++++- src/backend/message/edit_message_item.rs | 66 +++++++++++++++ - src/backend/message/formatting.rs | 11 ++- src/backend/message/mod.rs | 71 ++++++++++++++++ - src/backend/message/text_message.rs | 62 ++++++++++++++ + src/backend/message/text_message.rs | 61 ++++++++++++++ src/gui/channel_messages.rs | 67 +++++++++++++++ src/gui/components/indicators.rs | 2 + src/gui/components/item_row.rs | 58 ++++++------- src/gui/message_item.rs | 27 ++++++ - 13 files changed, 528 insertions(+), 38 deletions(-) + 12 files changed, 522 insertions(+), 32 deletions(-) create mode 100644 src/backend/message/edit_message_item.rs diff --git a/CHANGELOG.md b/CHANGELOG.md -index 50cd5f5..0338ed8 100644 +index 2bde927c..7caa7296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md -@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 + + - Send typing indicators while composing a message and display them above the message input. - Settings to enable or disable sending and showing typing indicators. - - Render formatted message styles (bold, italic, strikethrough, spoiler, monospace) on incoming messages. - - 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. ## [0.20.4] - 2026-04-22 diff --git a/data/resources/ui/channel_messages.blp b/data/resources/ui/channel_messages.blp -index 6c3948f..f3d2348 100644 +index 7f438e44..0a49e2ba 100644 --- a/data/resources/ui/channel_messages.blp +++ b/data/resources/ui/channel_messages.blp @@ -238,6 +238,95 @@ template $FlChannelMessages: Box { @@ -145,7 +144,7 @@ index 6c3948f..f3d2348 100644 Box { vexpand-set: true; diff --git a/data/resources/ui/components/indicators.blp b/data/resources/ui/components/indicators.blp -index f6c51f6..977f1c4 100644 +index f6c51f6f..977f1c4f 100644 --- a/data/resources/ui/components/indicators.blp +++ b/data/resources/ui/components/indicators.blp @@ -8,6 +8,16 @@ template $FlMessageIndicators { @@ -166,7 +165,7 @@ index f6c51f6..977f1c4 100644 styles [ "dim-label", diff --git a/data/resources/ui/message_item.blp b/data/resources/ui/message_item.blp -index 82c018b..2c21b8b 100644 +index 82c018bf..2c21b8bf 100644 --- a/data/resources/ui/message_item.blp +++ b/data/resources/ui/message_item.blp @@ -16,6 +16,14 @@ menu message-menu { @@ -201,7 +200,7 @@ index 82c018b..2c21b8b 100644 } diff --git a/src/backend/channel.rs b/src/backend/channel.rs -index 4bb1d38..711c92c 100644 +index 4bb1d385..711c92cc 100644 --- a/src/backend/channel.rs +++ b/src/backend/channel.rs @@ -1,8 +1,8 @@ @@ -340,7 +339,7 @@ index 4bb1d38..711c92c 100644 #[property(name = "avatar", get = Self::avatar)] diff --git a/src/backend/message/edit_message_item.rs b/src/backend/message/edit_message_item.rs new file mode 100644 -index 0000000..9655f50 +index 00000000..9655f50e --- /dev/null +++ b/src/backend/message/edit_message_item.rs @@ -0,0 +1,66 @@ @@ -410,50 +409,26 @@ index 0000000..9655f50 + impl MessageImpl for EditMessageItem {} + impl ObjectImpl for EditMessageItem {} +} -diff --git a/src/backend/message/formatting.rs b/src/backend/message/formatting.rs -index 5a1d596..ed12a85 100644 ---- a/src/backend/message/formatting.rs -+++ b/src/backend/message/formatting.rs -@@ -108,13 +108,12 @@ pub fn parse_formatting(input: &str) -> (String, Vec) { - // Mark which character positions are part of a matched marker token and - // therefore must be removed from the cleaned output. - let mut skip = vec![false; chars.len()]; -+ let total = chars.len(); - for sp in &spans { -- for k in sp.open_pos..(sp.open_pos + sp.marker_len).min(chars.len()) { -- skip[k] = true; -- } -- for k in sp.close_pos..(sp.close_pos + sp.marker_len).min(chars.len()) { -- skip[k] = true; -- } -+ let open_end = (sp.open_pos + sp.marker_len).min(total); -+ skip[sp.open_pos..open_end].fill(true); -+ let close_end = (sp.close_pos + sp.marker_len).min(total); -+ skip[sp.close_pos..close_end].fill(true); - } - - // Build the cleaned output and a per-input-char map into the output's diff --git a/src/backend/message/mod.rs b/src/backend/message/mod.rs -index 4e0f584..f3a0537 100644 +index 74952acc..da7a715b 100644 --- a/src/backend/message/mod.rs +++ b/src/backend/message/mod.rs -@@ -1,6 +1,7 @@ +@@ -1,12 +1,14 @@ mod call_message; mod deletion_message; mod display_message; +mod edit_message_item; - mod formatting; mod reaction_message; mod text_message; -@@ -8,6 +9,7 @@ mod text_message; + pub use call_message::{CallMessage, CallMessageType}; pub use deletion_message::DeletionMessage; pub use display_message::{DisplayMessage, DisplayMessageExt}; +pub use edit_message_item::EditMessageItem; - pub use formatting::parse_formatting; pub use reaction_message::ReactionMessage; pub use text_message::TextMessage; -@@ -253,6 +255,75 @@ impl Message { + +@@ -251,6 +253,75 @@ impl Message { .upcast(), ) } @@ -530,10 +505,10 @@ index 4e0f584..f3a0537 100644 ContentBody::CallMessage(c) => { // TODO: Group calls? diff --git a/src/backend/message/text_message.rs b/src/backend/message/text_message.rs -index c06bcfa..ff7aaaa 100644 +index a9adb04d..ba901d02 100644 --- a/src/backend/message/text_message.rs +++ b/src/backend/message/text_message.rs -@@ -199,6 +199,66 @@ impl TextMessage { +@@ -155,6 +155,65 @@ impl TextMessage { self.set_property("is-deleted", true); } @@ -569,8 +544,7 @@ index c06bcfa..ff7aaaa 100644 + let (body, body_ranges) = if cleaned.is_empty() { + (None, Vec::new()) + } else { -+ let (body, ranges) = super::parse_formatting(&cleaned); -+ (Some(body), ranges) ++ (Some(cleaned), Vec::new()) + }; + + // Carry forward the original message's structural fields (quote, @@ -600,7 +574,7 @@ index c06bcfa..ff7aaaa 100644 /// Send a reaction for a message and apply it. pub async fn send_reaction>( &self, -@@ -462,6 +522,8 @@ mod imp { +@@ -352,6 +411,8 @@ mod imp { pub(super) message_attributes: RefCell, #[property(get, set)] pub(super) is_deleted: RefCell, @@ -610,7 +584,7 @@ index c06bcfa..ff7aaaa 100644 impl TextMessage { diff --git a/src/gui/channel_messages.rs b/src/gui/channel_messages.rs -index 831fc25..9187bee 100644 +index 2b494a61..0653d7fa 100644 --- a/src/gui/channel_messages.rs +++ b/src/gui/channel_messages.rs @@ -2,6 +2,7 @@ use crate::prelude::*; @@ -650,7 +624,7 @@ index 831fc25..9187bee 100644 #[property(get, set, default = true)] sticky: Cell, -@@ -480,6 +496,13 @@ pub mod imp { +@@ -487,6 +503,13 @@ pub mod imp { self.obj().set_reply_message(None::); } @@ -664,7 +638,7 @@ index 831fc25..9187bee 100644 #[template_callback] fn remove_attachments(&self) { log::trace!("Unsetting attachments"); -@@ -585,6 +608,33 @@ pub mod imp { +@@ -592,6 +615,33 @@ pub mod imp { }; self.obj().notify("has-attachments"); @@ -698,7 +672,7 @@ index 831fc25..9187bee 100644 if text.is_empty() && attachments.is_empty() { log::warn!("Got requested to send empty message, skipping"); } -@@ -683,6 +733,22 @@ pub mod imp { +@@ -690,6 +740,22 @@ pub mod imp { } ), ); @@ -721,7 +695,7 @@ index 831fc25..9187bee 100644 let list_item = object.downcast_ref::().unwrap(); list_item.set_activatable(false); list_item.set_selectable(false); -@@ -735,6 +801,7 @@ pub mod imp { +@@ -742,6 +808,7 @@ pub mod imp { self, move |_, _| { s.obj().set_reply_message(None::); @@ -730,7 +704,7 @@ index 831fc25..9187bee 100644 let draft = channel.property("draft"); // Block the typing buffer-changed handler so diff --git a/src/gui/components/indicators.rs b/src/gui/components/indicators.rs -index ce38221..4356607 100644 +index ce382217..43566075 100644 --- a/src/gui/components/indicators.rs +++ b/src/gui/components/indicators.rs @@ -26,6 +26,8 @@ mod imp { @@ -743,7 +717,7 @@ index ce38221..4356607 100644 //#[template_child] //pub(super) sending_state_icon: TemplateChild, diff --git a/src/gui/components/item_row.rs b/src/gui/components/item_row.rs -index b2c20d3..538b1bb 100644 +index b2c20d3a..538b1bb2 100644 --- a/src/gui/components/item_row.rs +++ b/src/gui/components/item_row.rs @@ -1,5 +1,3 @@ @@ -847,7 +821,7 @@ index b2c20d3..538b1bb 100644 }); SIGNALS.as_ref() diff --git a/src/gui/message_item.rs b/src/gui/message_item.rs -index 21f504a..59d2778 100644 +index 21f504a3..59d2778d 100644 --- a/src/gui/message_item.rs +++ b/src/gui/message_item.rs @@ -94,6 +94,14 @@ impl MessageItem { diff --git a/patches/flare/0002-feat-messages-Implement-formatted-messages.patch b/patches/flare/0002-feat-messages-Implement-formatted-messages.patch deleted file mode 100644 index 795d0d9..0000000 --- a/patches/flare/0002-feat-messages-Implement-formatted-messages.patch +++ /dev/null @@ -1,622 +0,0 @@ -From 200461c0abe0399ae4b5b0bfd3848fcc226ba308 Mon Sep 17 00:00:00 2001 -From: Simon Gardling -Date: Wed, 29 Apr 2026 19:13:52 -0400 -Subject: [PATCH 2/6] feat(messages): Implement formatted messages - -- Display Signal BodyRange styles (bold, italic, strikethrough, - spoiler, monospace) on incoming messages by translating them into - pango attributes alongside the existing mention rendering, making - the offset accounting work for mention substitutions and - surrogate-pair text alike. -- Parse a markdown-style formatting syntax on outbound messages and - send the resulting BodyRanges with the cleaned body text. The - parser lives in its own module with unit tests covering the - supported markers, nesting, unmatched markers, and non-BMP UTF-16 - offsets. -- Update the message-input tooltip to surface the supported markers. ---- - CHANGELOG.md | 2 + - data/resources/ui/channel_messages.blp | 2 +- - src/backend/message/formatting.rs | 287 +++++++++++++++++++++++++ - src/backend/message/mod.rs | 2 + - src/backend/message/text_message.rs | 200 +++++++++++++---- - 5 files changed, 447 insertions(+), 46 deletions(-) - create mode 100644 src/backend/message/formatting.rs - -diff --git a/CHANGELOG.md b/CHANGELOG.md -index 2bde927..50cd5f5 100644 ---- a/CHANGELOG.md -+++ b/CHANGELOG.md -@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - - Send typing indicators while composing a message and display them above the message input. - - Settings to enable or disable sending and showing typing indicators. -+- Render formatted message styles (bold, italic, strikethrough, spoiler, monospace) on incoming messages. -+- Send formatted messages with markdown-style markers (`**bold**`, `*italic*`, `~~strike~~`, `||spoiler||`, `` `monospace` ``). - - ## [0.20.4] - 2026-04-22 - -diff --git a/data/resources/ui/channel_messages.blp b/data/resources/ui/channel_messages.blp -index 7f438e4..6c3948f 100644 ---- a/data/resources/ui/channel_messages.blp -+++ b/data/resources/ui/channel_messages.blp -@@ -301,7 +301,7 @@ template $FlChannelMessages: Box { - activate => $send_message() swapped; - paste-file => $paste_file() swapped; - paste-texture => $paste_texture() swapped; -- tooltip-text: C_("tooltip", "Message input"); -+ tooltip-text: C_("tooltip", "Message input. Use **bold**, *italic*, ~~strike~~, ||spoiler|| or `monospace` to format text."); - } - - Button button_send { -diff --git a/src/backend/message/formatting.rs b/src/backend/message/formatting.rs -new file mode 100644 -index 0000000..5a1d596 ---- /dev/null -+++ b/src/backend/message/formatting.rs -@@ -0,0 +1,287 @@ -+//! Lightweight markdown-style formatting parser for outgoing messages. -+//! -+//! Supported syntax (mirroring the way Signal Desktop and iOS render -+//! formatted messages): -+//! -+//! - `**text**` for bold -+//! - `*text*` or `_text_` for italic -+//! - `~~text~~` for strikethrough -+//! - `||text||` for spoiler -+//! - `` `text` `` for monospace -+//! -+//! Parsing is forgiving: any marker without a matching counterpart is left -+//! verbatim in the resulting text. Markers may nest as long as the inner -+//! marker is a different kind from the outer one. -+//! -+//! The function returns the cleaned message body plus the corresponding -+//! `BodyRange`s with offsets in UTF-16 code units, as required by the -+//! Signal protocol. -+ -+use std::collections::HashMap; -+ -+use libsignal_service::proto::BodyRange; -+use libsignal_service::proto::body_range::{AssociatedValue, Style as BodyRangeStyle}; -+ -+#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)] -+enum Marker { -+ Bold, -+ Italic, -+ Strikethrough, -+ Spoiler, -+ Monospace, -+} -+ -+impl Marker { -+ fn style(self) -> BodyRangeStyle { -+ match self { -+ Marker::Bold => BodyRangeStyle::Bold, -+ Marker::Italic => BodyRangeStyle::Italic, -+ Marker::Strikethrough => BodyRangeStyle::Strikethrough, -+ Marker::Spoiler => BodyRangeStyle::Spoiler, -+ Marker::Monospace => BodyRangeStyle::Monospace, -+ } -+ } -+} -+ -+/// Try to consume a marker starting at `chars[i]` and return its kind plus -+/// the number of characters that make up the marker token. -+fn detect_marker(chars: &[char], i: usize) -> Option<(Marker, usize)> { -+ let cur = *chars.get(i)?; -+ let next = chars.get(i + 1).copied(); -+ match (cur, next) { -+ ('*', Some('*')) => Some((Marker::Bold, 2)), -+ ('~', Some('~')) => Some((Marker::Strikethrough, 2)), -+ ('|', Some('|')) => Some((Marker::Spoiler, 2)), -+ ('*', _) | ('_', _) => Some((Marker::Italic, 1)), -+ ('`', _) => Some((Marker::Monospace, 1)), -+ _ => None, -+ } -+} -+ -+#[derive(Debug, Clone, Copy)] -+struct MatchedSpan { -+ marker: Marker, -+ open_pos: usize, -+ close_pos: usize, -+ marker_len: usize, -+} -+ -+/// Walk the character stream left-to-right and pair markers of the same -+/// kind. The first occurrence opens a span, the next occurrence of the same -+/// kind closes it; markers without a partner are simply ignored. -+fn detect_matched_markers(chars: &[char]) -> Vec { -+ let mut open: HashMap = HashMap::new(); -+ let mut spans: Vec = Vec::new(); -+ let mut i = 0; -+ while i < chars.len() { -+ if let Some((marker, len)) = detect_marker(chars, i) { -+ if let Some((open_pos, marker_len)) = open.remove(&marker) { -+ spans.push(MatchedSpan { -+ marker, -+ open_pos, -+ close_pos: i, -+ marker_len, -+ }); -+ } else { -+ open.insert(marker, (i, len)); -+ } -+ i += len; -+ } else { -+ i += 1; -+ } -+ } -+ spans -+} -+ -+/// Parse markdown-style formatting markers in `input` and produce the cleaned -+/// text plus the corresponding Signal [BodyRange]s with UTF-16 offsets. -+/// -+/// Empty matched spans (e.g. `**` followed immediately by `**`) are dropped. -+pub fn parse_formatting(input: &str) -> (String, Vec) { -+ let chars: Vec = input.chars().collect(); -+ let spans = detect_matched_markers(&chars); -+ -+ if spans.is_empty() { -+ return (input.to_owned(), Vec::new()); -+ } -+ -+ // Mark which character positions are part of a matched marker token and -+ // therefore must be removed from the cleaned output. -+ let mut skip = vec![false; chars.len()]; -+ for sp in &spans { -+ for k in sp.open_pos..(sp.open_pos + sp.marker_len).min(chars.len()) { -+ skip[k] = true; -+ } -+ for k in sp.close_pos..(sp.close_pos + sp.marker_len).min(chars.len()) { -+ skip[k] = true; -+ } -+ } -+ -+ // Build the cleaned output and a per-input-char map into the output's -+ // UTF-16 code-unit offset. -+ let mut output = String::with_capacity(input.len()); -+ let mut input_to_output_utf16 = vec![0u32; chars.len() + 1]; -+ let mut utf16_count: u32 = 0; -+ for (i, c) in chars.iter().enumerate() { -+ input_to_output_utf16[i] = utf16_count; -+ if !skip[i] { -+ output.push(*c); -+ utf16_count += c.len_utf16() as u32; -+ } -+ } -+ input_to_output_utf16[chars.len()] = utf16_count; -+ -+ let mut ranges: Vec = Vec::with_capacity(spans.len()); -+ for sp in spans { -+ let start = input_to_output_utf16[sp.open_pos + sp.marker_len]; -+ let end = input_to_output_utf16[sp.close_pos]; -+ if end <= start { -+ continue; -+ } -+ ranges.push(BodyRange { -+ start: Some(start), -+ length: Some(end - start), -+ associated_value: Some(AssociatedValue::Style(sp.marker.style() as i32)), -+ }); -+ } -+ -+ // Sort by start so the final ranges are stable for tests and for -+ // downstream consumers that expect ordered ranges. -+ ranges.sort_by_key(|r| r.start); -+ -+ (output, ranges) -+} -+ -+#[cfg(test)] -+mod tests { -+ use super::*; -+ -+ fn ranges_summary(ranges: &[BodyRange]) -> Vec<(u32, u32, BodyRangeStyle)> { -+ ranges -+ .iter() -+ .map(|r| { -+ let style = match r.associated_value { -+ Some(AssociatedValue::Style(s)) => { -+ BodyRangeStyle::try_from(s).unwrap_or(BodyRangeStyle::None) -+ } -+ _ => BodyRangeStyle::None, -+ }; -+ (r.start.unwrap_or(0), r.length.unwrap_or(0), style) -+ }) -+ .collect() -+ } -+ -+ #[test] -+ fn no_markers() { -+ let (text, ranges) = parse_formatting("hello world"); -+ assert_eq!(text, "hello world"); -+ assert!(ranges.is_empty()); -+ } -+ -+ #[test] -+ fn bold() { -+ let (text, ranges) = parse_formatting("**bold**"); -+ assert_eq!(text, "bold"); -+ assert_eq!(ranges_summary(&ranges), vec![(0, 4, BodyRangeStyle::Bold)]); -+ } -+ -+ #[test] -+ fn italic_asterisk() { -+ let (text, ranges) = parse_formatting("*italic*"); -+ assert_eq!(text, "italic"); -+ assert_eq!( -+ ranges_summary(&ranges), -+ vec![(0, 6, BodyRangeStyle::Italic)] -+ ); -+ } -+ -+ #[test] -+ fn italic_underscore() { -+ let (text, ranges) = parse_formatting("_italic_"); -+ assert_eq!(text, "italic"); -+ assert_eq!( -+ ranges_summary(&ranges), -+ vec![(0, 6, BodyRangeStyle::Italic)] -+ ); -+ } -+ -+ #[test] -+ fn strikethrough() { -+ let (text, ranges) = parse_formatting("~~strike~~"); -+ assert_eq!(text, "strike"); -+ assert_eq!( -+ ranges_summary(&ranges), -+ vec![(0, 6, BodyRangeStyle::Strikethrough)] -+ ); -+ } -+ -+ #[test] -+ fn spoiler() { -+ let (text, ranges) = parse_formatting("||hidden||"); -+ assert_eq!(text, "hidden"); -+ assert_eq!( -+ ranges_summary(&ranges), -+ vec![(0, 6, BodyRangeStyle::Spoiler)] -+ ); -+ } -+ -+ #[test] -+ fn monospace() { -+ let (text, ranges) = parse_formatting("`code`"); -+ assert_eq!(text, "code"); -+ assert_eq!( -+ ranges_summary(&ranges), -+ vec![(0, 4, BodyRangeStyle::Monospace)] -+ ); -+ } -+ -+ #[test] -+ fn bold_and_italic_nested() { -+ let (text, ranges) = parse_formatting("**bold *italic***"); -+ assert_eq!(text, "bold italic"); -+ let summary = ranges_summary(&ranges); -+ assert!(summary.contains(&(0, 11, BodyRangeStyle::Bold))); -+ assert!(summary.contains(&(5, 6, BodyRangeStyle::Italic))); -+ } -+ -+ #[test] -+ fn unmatched_open_left_literal() { -+ let (text, ranges) = parse_formatting("**only one start"); -+ assert_eq!(text, "**only one start"); -+ assert!(ranges.is_empty()); -+ } -+ -+ #[test] -+ fn surrounding_text_preserved() { -+ let (text, ranges) = parse_formatting("hello **world**!"); -+ assert_eq!(text, "hello world!"); -+ assert_eq!(ranges_summary(&ranges), vec![(6, 5, BodyRangeStyle::Bold)]); -+ } -+ -+ #[test] -+ fn multiple_pairs() { -+ let (text, ranges) = parse_formatting("**a**b**c**"); -+ assert_eq!(text, "abc"); -+ let summary = ranges_summary(&ranges); -+ assert_eq!(summary.len(), 2); -+ assert_eq!(summary[0], (0, 1, BodyRangeStyle::Bold)); -+ assert_eq!(summary[1], (2, 1, BodyRangeStyle::Bold)); -+ } -+ -+ #[test] -+ fn empty_pair_dropped() { -+ let (text, ranges) = parse_formatting("****"); -+ assert_eq!(text, ""); -+ assert!(ranges.is_empty()); -+ } -+ -+ #[test] -+ fn utf16_offsets_for_non_bmp() { -+ // Character "𝟚" (U+1D7DA) is a non-BMP codepoint occupying two -+ // UTF-16 code units, so a Bold range over a string containing it -+ // must reflect that in its `length`. -+ let (text, ranges) = parse_formatting("**𝟚**"); -+ assert_eq!(text, "𝟚"); -+ assert_eq!(ranges_summary(&ranges), vec![(0, 2, BodyRangeStyle::Bold)]); -+ } -+} -diff --git a/src/backend/message/mod.rs b/src/backend/message/mod.rs -index 74952ac..4e0f584 100644 ---- a/src/backend/message/mod.rs -+++ b/src/backend/message/mod.rs -@@ -1,12 +1,14 @@ - mod call_message; - mod deletion_message; - mod display_message; -+mod formatting; - mod reaction_message; - mod text_message; - - pub use call_message::{CallMessage, CallMessageType}; - pub use deletion_message::DeletionMessage; - pub use display_message::{DisplayMessage, DisplayMessageExt}; -+pub use formatting::parse_formatting; - pub use reaction_message::ReactionMessage; - pub use text_message::TextMessage; - -diff --git a/src/backend/message/text_message.rs b/src/backend/message/text_message.rs -index a9adb04..c06bcfa 100644 ---- a/src/backend/message/text_message.rs -+++ b/src/backend/message/text_message.rs -@@ -2,9 +2,9 @@ use crate::prelude::*; - - use libsignal_service::content::Reaction; - use libsignal_service::proto::DataMessage; --use libsignal_service::proto::body_range::AssociatedValue; -+use libsignal_service::proto::body_range::{AssociatedValue, Style as BodyRangeStyle}; - use libsignal_service::proto::data_message::Delete; --use pango::{AttrColor, AttrList}; -+use pango::{AttrColor, AttrInt, AttrList, AttrString, Style as PangoStyle, Weight}; - - use crate::backend::timeline::{TimelineItem, TimelineItemExt}; - use crate::backend::{Attachment, Channel, Contact}; -@@ -19,6 +19,48 @@ gtk::glib::wrapper! { - const MENTION_CHAR: char = '@'; - const MENTION_COLOR: (u16, u16, u16) = (0, 0, u16::MAX); - -+/// Convert a Signal [BodyRangeStyle] into the pango attributes that render -+/// the same visual style. Spoilers are approximated as a black-on-black -+/// span as pango has no native spoiler primitive. -+fn style_to_pango_attrs( -+ style: BodyRangeStyle, -+ start_byte: u32, -+ end_byte: u32, -+) -> Vec { -+ fn span>(attr: A, start: u32, end: u32) -> pango::Attribute { -+ let mut attr: pango::Attribute = attr.into(); -+ attr.set_start_index(start); -+ attr.set_end_index(end); -+ attr -+ } -+ -+ match style { -+ BodyRangeStyle::Bold => vec![span( -+ AttrInt::new_weight(Weight::Bold), -+ start_byte, -+ end_byte, -+ )], -+ BodyRangeStyle::Italic => vec![span( -+ AttrInt::new_style(PangoStyle::Italic), -+ start_byte, -+ end_byte, -+ )], -+ BodyRangeStyle::Strikethrough => { -+ vec![span(AttrInt::new_strikethrough(true), start_byte, end_byte)] -+ } -+ BodyRangeStyle::Monospace => vec![span( -+ AttrString::new_family("monospace"), -+ start_byte, -+ end_byte, -+ )], -+ BodyRangeStyle::Spoiler => vec![ -+ span(AttrColor::new_foreground(0, 0, 0), start_byte, end_byte), -+ span(AttrColor::new_background(0, 0, 0), start_byte, end_byte), -+ ], -+ BodyRangeStyle::None => Vec::new(), -+ } -+} -+ - impl TextMessage { - pub fn from_text_channel_sender>( - text: S, -@@ -65,14 +107,16 @@ impl TextMessage { - .build(); - - let text_owned = text.as_ref().to_owned(); -- let body = if text_owned.is_empty() { -- None -+ let (body, body_ranges) = if text_owned.is_empty() { -+ (None, Vec::new()) - } else { -- Some(text_owned) -+ let (cleaned, ranges) = super::parse_formatting(&text_owned); -+ (Some(cleaned), ranges) - }; - - let message = DataMessage { - body, -+ body_ranges, - timestamp: Some(timestamp), - ..Default::default() - }; -@@ -245,10 +289,17 @@ impl TextMessage { - self.notify_body(); - } - -- /// Formats the message body based on its ranges, e.g. to insert mention names. -+ /// Format the message body based on its body ranges. -+ /// -+ /// This both substitutes mentions with the resolved participant name and -+ /// applies styling (bold, italic, monospace, strikethrough, spoiler) as -+ /// pango attributes on the resulting text. - /// -- /// Returns the resulting strings and an [AttrList] that can be used in labels to highlight areas. -- /// Be carefull when editing this function and note that Signal uses UTF-16 byte offsets, while Rust uses UTF-8 byte offsets. -+ /// Note that Signal uses UTF-16 byte offsets, while Rust strings use -+ /// UTF-8. The implementation maintains an explicit per-utf16-index -+ /// mapping into the resulting UTF-8 string so that styles applied to a -+ /// range that survives a mention substitution still land on the right -+ /// bytes. - async fn format_body(&self) -> (Option, AttrList) { - let Some(body) = self.internal_data().and_then(|m| m.body) else { - return (None, AttrList::new()); -@@ -264,53 +315,112 @@ impl TextMessage { - - let channel = self.channel(); - -- // Sort by growing start index -+ // Sort by growing start index so mention substitutions happen left-to-right. - ranges.sort_unstable_by_key(|r| r.start()); - -- let attrs = AttrList::new(); -- -- // Signal (Java) uses UTF-16 body and therefore also UTF-16 offsets, while Flare (Rust) uses UTF-8. Need to convert. -- let body_utf16: Vec = body.encode_utf16().collect(); -- -- let mut result_utf8 = String::new(); -- let mut index_utf16 = 0; -- let mut index_utf8 = 0; -- for r in ranges { -- let start = r.start() as usize; -- let end = start + r.length() as usize; -- let uuid = match r.associated_value { -+ // Resolve mention names asynchronously up front so the rest of the -+ // formatting can be a synchronous walk. -+ let mut mentions: Vec<(usize, usize, String)> = Vec::new(); -+ for r in &ranges { -+ let uuid = match &r.associated_value { - Some(AssociatedValue::MentionAci(u)) => u.parse().ok(), - Some(AssociatedValue::MentionAciBinary(u)) => { -- u.try_into().ok().map(Uuid::from_bytes) -+ u.clone().try_into().ok().map(Uuid::from_bytes) - } - _ => None, - }; -- let Some(uuid) = uuid else { -+ if let Some(uuid) = uuid { -+ let start = r.start() as usize; -+ let end = (r.start() + r.length()) as usize; -+ let name = format!( -+ "{}{}", -+ MENTION_CHAR, -+ channel.participant_by_uuid(uuid).await.title() -+ ); -+ mentions.push((start, end, name)); -+ } -+ } -+ // Mentions cannot overlap each other; ensure the iterator order is stable. -+ mentions.sort_unstable_by_key(|(s, _, _)| *s); -+ -+ let body_utf16: Vec = body.encode_utf16().collect(); -+ let attrs = AttrList::new(); -+ -+ // Build the result string while constructing a per-utf16-index map -+ // into the resulting UTF-8 byte offsets. -+ let mut byte_at: Vec = Vec::with_capacity(body_utf16.len() + 1); -+ let mut result_utf8 = String::new(); -+ let mut mention_iter = mentions.into_iter().peekable(); -+ -+ let mut i = 0; -+ while i < body_utf16.len() { -+ // Inject mention substitutions at their start position. -+ if mention_iter -+ .peek() -+ .is_some_and(|(m_start, _, _)| *m_start == i) -+ { -+ let (m_start, m_end, name) = mention_iter.next().expect("peeked entry to exist"); -+ let mention_byte_start = result_utf8.len(); -+ // Mark every UTF-16 index inside the mention span as the start -+ // of the substituted text. Indices >= m_end will be filled by -+ // subsequent iterations. -+ for _ in m_start..m_end { -+ byte_at.push(mention_byte_start); -+ } -+ result_utf8.push_str(&name); -+ -+ let mut highlight = -+ AttrColor::new_foreground(MENTION_COLOR.0, MENTION_COLOR.1, MENTION_COLOR.2); -+ highlight.set_start_index(mention_byte_start as u32); -+ highlight.set_end_index(result_utf8.len() as u32); -+ attrs.insert(highlight); -+ -+ i = m_end.min(body_utf16.len()); - continue; -- }; -- let name = format!( -- "{}{}", -- MENTION_CHAR, -- channel.participant_by_uuid(uuid).await.title() -- ); -- let to_add_body = String::from_utf16_lossy(&body_utf16[index_utf16..start]); -- result_utf8.push_str(&to_add_body); -- result_utf8.push_str(&name); -- index_utf16 = end; -- -- let index_start_highlight = index_utf8 + to_add_body.len(); -- index_utf8 += to_add_body.len() + name.len(); -- let index_end_highlight = index_utf8; -- -- let (red, green, blue) = MENTION_COLOR; -- let mut highlight = AttrColor::new_foreground(red, green, blue); -- highlight.set_start_index(index_start_highlight as u32); -- highlight.set_end_index(index_end_highlight as u32); -- attrs.insert(highlight); -+ } -+ -+ byte_at.push(result_utf8.len()); -+ let unit = body_utf16[i]; -+ if (0xD800..=0xDBFF).contains(&unit) && i + 1 < body_utf16.len() { -+ // High surrogate: consume the pair as one codepoint. -+ let pair = [unit, body_utf16[i + 1]]; -+ let decoded = char::decode_utf16(pair.iter().copied()) -+ .next() -+ .and_then(|r| r.ok()) -+ .unwrap_or('\u{FFFD}'); -+ result_utf8.push(decoded); -+ byte_at.push(result_utf8.len()); -+ i += 2; -+ } else { -+ let decoded = char::decode_utf16([unit].iter().copied()) -+ .next() -+ .and_then(|r| r.ok()) -+ .unwrap_or('\u{FFFD}'); -+ result_utf8.push(decoded); -+ i += 1; -+ } - } -+ byte_at.push(result_utf8.len()); - -- if index_utf16 < body_utf16.len() { -- result_utf8.push_str(&String::from_utf16_lossy(&body_utf16[index_utf16..])) -+ // Apply style ranges using the byte-offset map. -+ for r in ranges { -+ let Some(AssociatedValue::Style(s)) = r.associated_value else { -+ continue; -+ }; -+ let style = match BodyRangeStyle::try_from(s) { -+ Ok(BodyRangeStyle::None) | Err(_) => continue, -+ Ok(other) => other, -+ }; -+ let start_utf16 = (r.start() as usize).min(byte_at.len() - 1); -+ let end_utf16 = ((r.start() + r.length()) as usize).min(byte_at.len() - 1); -+ if start_utf16 >= end_utf16 { -+ continue; -+ } -+ let start_byte = byte_at[start_utf16] as u32; -+ let end_byte = byte_at[end_utf16] as u32; -+ for attr in style_to_pango_attrs(style, start_byte, end_byte) { -+ attrs.insert(attr); -+ } - } - - (Some(result_utf8), attrs) --- -2.53.0 - diff --git a/patches/flare/0004-feat-messages-Multi-select-messages-and-delete-for-m.patch b/patches/flare/0003-feat-messages-Multi-select-messages-and-delete-for-m.patch similarity index 96% rename from patches/flare/0004-feat-messages-Multi-select-messages-and-delete-for-m.patch rename to patches/flare/0003-feat-messages-Multi-select-messages-and-delete-for-m.patch index bbb64f8..efaccea 100644 --- a/patches/flare/0004-feat-messages-Multi-select-messages-and-delete-for-m.patch +++ b/patches/flare/0003-feat-messages-Multi-select-messages-and-delete-for-m.patch @@ -1,7 +1,7 @@ -From 86088503e4acb398aff50c4bfdc603c2518370f2 Mon Sep 17 00:00:00 2001 +From e6db982a5be5ca392538259dfbb66d38ca0d158d Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Wed, 29 Apr 2026 19:53:22 -0400 -Subject: [PATCH 4/6] feat(messages): Multi-select messages and delete for me +Subject: [PATCH 3/6] feat(messages): Multi-select messages and delete for me - Add a 'Select' action to the message context menu that puts the channel into selection mode and pre-selects the message. While in @@ -30,19 +30,19 @@ Subject: [PATCH 4/6] feat(messages): Multi-select messages and delete for me 10 files changed, 416 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md -index 0338ed8..47ec77a 100644 +index 7caa7296..e287fc73 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 - - Render formatted message styles (bold, italic, strikethrough, spoiler, monospace) on incoming messages. - - Send formatted messages with markdown-style markers (`**bold**`, `*italic*`, `~~strike~~`, `||spoiler||`, `` `monospace` ``). +@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 + - Send typing indicators while composing a message and display them above the message input. + - Settings to enable or disable sending and showing typing indicators. - 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. ## [0.20.4] - 2026-04-22 diff --git a/data/resources/style.css b/data/resources/style.css -index 00e4783..1c0cdfd 100644 +index 00e47833..1c0cdfdb 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -19,6 +19,20 @@ @@ -67,7 +67,7 @@ index 00e4783..1c0cdfd 100644 padding:0; } diff --git a/data/resources/ui/channel_messages.blp b/data/resources/ui/channel_messages.blp -index f3d2348..eb927f8 100644 +index 0a49e2ba..9bfedd64 100644 --- a/data/resources/ui/channel_messages.blp +++ b/data/resources/ui/channel_messages.blp @@ -135,6 +135,66 @@ template $FlChannelMessages: Box { @@ -138,7 +138,7 @@ index f3d2348..eb927f8 100644 styles [ "toolbar", diff --git a/data/resources/ui/message_item.blp b/data/resources/ui/message_item.blp -index 2c21b8b..ba3fd23 100644 +index 2c21b8bf..ba3fd23c 100644 --- a/data/resources/ui/message_item.blp +++ b/data/resources/ui/message_item.blp @@ -24,6 +24,13 @@ menu message-menu { @@ -176,7 +176,7 @@ index 2c21b8b..ba3fd23 100644 visible: bind template.message as <$FlTextMessage>.pending; tooltip-text: _("This message is currently being sent"); diff --git a/src/backend/channel.rs b/src/backend/channel.rs -index 711c92c..f07ce96 100644 +index 711c92cc..f07ce96d 100644 --- a/src/backend/channel.rs +++ b/src/backend/channel.rs @@ -199,6 +199,28 @@ impl Channel { @@ -226,7 +226,7 @@ index 711c92c..f07ce96 100644 }); SIGNALS.as_ref() diff --git a/src/backend/manager.rs b/src/backend/manager.rs -index eaa41e0..0964681 100644 +index c9079612..2e9bd761 100644 --- a/src/backend/manager.rs +++ b/src/backend/manager.rs @@ -210,6 +210,38 @@ impl Manager { @@ -269,10 +269,10 @@ index eaa41e0..0964681 100644 &self, token: S, diff --git a/src/backend/message/mod.rs b/src/backend/message/mod.rs -index f3a0537..eba08ec 100644 +index da7a715b..45e7f7d8 100644 --- a/src/backend/message/mod.rs +++ b/src/backend/message/mod.rs -@@ -518,6 +518,8 @@ mod imp { +@@ -516,6 +516,8 @@ mod imp { pub(super) pending: RefCell, #[property(get, set)] pub(super) error: RefCell, @@ -282,7 +282,7 @@ index f3a0537..eba08ec 100644 pub(super) data: RefCell>, diff --git a/src/backend/timeline/mod.rs b/src/backend/timeline/mod.rs -index 1ce6a24..18dd436 100644 +index 1ce6a24d..18dd4367 100644 --- a/src/backend/timeline/mod.rs +++ b/src/backend/timeline/mod.rs @@ -44,6 +44,22 @@ impl Timeline { @@ -309,7 +309,7 @@ index 1ce6a24..18dd436 100644 let current_items = self.imp().list.borrow(); let index = current_items.binary_search_by_key(×tamp, |i| i.timestamp()); diff --git a/src/gui/channel_messages.rs b/src/gui/channel_messages.rs -index 9187bee..d6d1826 100644 +index 0653d7fa..6af4c0ce 100644 --- a/src/gui/channel_messages.rs +++ b/src/gui/channel_messages.rs @@ -2,7 +2,8 @@ use crate::prelude::*; @@ -440,7 +440,7 @@ index 9187bee..d6d1826 100644 if let Some(active_chan) = self.active_channel.borrow().as_ref() { active_chan.set_property("draft", self.text_entry.text()); -@@ -447,6 +538,7 @@ pub mod imp { +@@ -454,6 +545,7 @@ pub mod imp { self.obj().focus_input(); self.obj().setup_typing_indicator(); @@ -448,7 +448,7 @@ index 9187bee..d6d1826 100644 } #[template_callback(function)] -@@ -503,6 +595,41 @@ pub mod imp { +@@ -510,6 +602,41 @@ pub mod imp { self.text_entry.clear(); } @@ -491,7 +491,7 @@ index 9187bee..d6d1826 100644 fn remove_attachments(&self) { log::trace!("Unsetting attachments"); diff --git a/src/gui/message_item.rs b/src/gui/message_item.rs -index 59d2778..f2d98e2 100644 +index 59d2778d..f2d98e2d 100644 --- a/src/gui/message_item.rs +++ b/src/gui/message_item.rs @@ -34,6 +34,7 @@ impl MessageItem { diff --git a/patches/flare/0005-feat-messages-In-channel-message-search.patch b/patches/flare/0004-feat-messages-In-channel-message-search.patch similarity index 97% rename from patches/flare/0005-feat-messages-In-channel-message-search.patch rename to patches/flare/0004-feat-messages-In-channel-message-search.patch index e53acd3..12d6584 100644 --- a/patches/flare/0005-feat-messages-In-channel-message-search.patch +++ b/patches/flare/0004-feat-messages-In-channel-message-search.patch @@ -1,7 +1,7 @@ -From 00d9d9f0b8770453eb3f124db0c79d0bc4bacb39 Mon Sep 17 00:00:00 2001 +From 6272aee14e9ece866751b94b7ebd823f147f9531 Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Wed, 29 Apr 2026 19:58:54 -0400 -Subject: [PATCH 5/6] feat(messages): In-channel message search +Subject: [PATCH 4/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 @@ -24,11 +24,11 @@ Subject: [PATCH 5/6] feat(messages): In-channel message search 9 files changed, 374 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md -index 47ec77a..16880cd 100644 +index e287fc73..e221f555 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` ``). +@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 + - Settings to enable or disable sending and showing typing indicators. - 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. @@ -36,7 +36,7 @@ index 47ec77a..16880cd 100644 ## [0.20.4] - 2026-04-22 diff --git a/data/resources/style.css b/data/resources/style.css -index 1c0cdfd..e00e789 100644 +index 1c0cdfdb..e00e7898 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -294,3 +294,13 @@ unread-indicator { @@ -55,7 +55,7 @@ index 1c0cdfd..e00e789 100644 +} \ 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 +index 9bfedd64..4e4be943 100644 --- a/data/resources/ui/channel_messages.blp +++ b/data/resources/ui/channel_messages.blp @@ -43,6 +43,61 @@ template $FlChannelMessages: Box { @@ -121,7 +121,7 @@ index eb927f8..a166f7b 100644 [overlay] Adw.Spinner { diff --git a/data/resources/ui/shortcuts.blp b/data/resources/ui/shortcuts.blp -index ed2a959..79339cc 100644 +index ed2a9596..79339ccf 100644 --- a/data/resources/ui/shortcuts.blp +++ b/data/resources/ui/shortcuts.blp @@ -58,5 +58,10 @@ Adw.ShortcutsDialog help_overlay { @@ -136,7 +136,7 @@ index ed2a959..79339cc 100644 } } diff --git a/src/backend/message/display_message.rs b/src/backend/message/display_message.rs -index 4cd5e7f..4ccb763 100644 +index 4cd5e7f3..4ccb7631 100644 --- a/src/backend/message/display_message.rs +++ b/src/backend/message/display_message.rs @@ -137,6 +137,7 @@ mod imp { @@ -176,7 +176,7 @@ index 4cd5e7f..4ccb763 100644 } } diff --git a/src/backend/timeline/mod.rs b/src/backend/timeline/mod.rs -index 18dd436..40fe324 100644 +index 18dd4367..40fe3244 100644 --- a/src/backend/timeline/mod.rs +++ b/src/backend/timeline/mod.rs @@ -69,6 +69,16 @@ impl Timeline { @@ -197,7 +197,7 @@ index 18dd436..40fe324 100644 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 d6d1826..53e341f 100644 +index 6af4c0ce..a4547acc 100644 --- a/src/gui/channel_messages.rs +++ b/src/gui/channel_messages.rs @@ -85,6 +85,204 @@ impl ChannelMessages { @@ -471,7 +471,7 @@ index d6d1826..53e341f 100644 if let Some(active_chan) = self.active_channel.borrow().as_ref() { active_chan.set_property("draft", self.text_entry.text()); -@@ -818,6 +1045,27 @@ pub mod imp { +@@ -825,6 +1052,27 @@ pub mod imp { } } @@ -500,7 +500,7 @@ index d6d1826..53e341f 100644 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 f2d98e2..1f62d50 100644 +index f2d98e2d..1f62d509 100644 --- a/src/gui/message_item.rs +++ b/src/gui/message_item.rs @@ -33,6 +33,7 @@ impl MessageItem { @@ -543,7 +543,7 @@ index f2d98e2..1f62d50 100644 let message = self.message(); message.connect_notify_local( diff --git a/src/gui/window.rs b/src/gui/window.rs -index 6335f3d..ce097ce 100644 +index 6335f3d6..ce097ce1 100644 --- a/src/gui/window.rs +++ b/src/gui/window.rs @@ -20,6 +20,7 @@ impl Window { diff --git a/patches/flare/0006-feat-messages-Show-This-message-was-deleted.-placeho.patch b/patches/flare/0005-feat-messages-Show-This-message-was-deleted.-placeho.patch similarity index 96% rename from patches/flare/0006-feat-messages-Show-This-message-was-deleted.-placeho.patch rename to patches/flare/0005-feat-messages-Show-This-message-was-deleted.-placeho.patch index 6329423..7f7fc46 100644 --- a/patches/flare/0006-feat-messages-Show-This-message-was-deleted.-placeho.patch +++ b/patches/flare/0005-feat-messages-Show-This-message-was-deleted.-placeho.patch @@ -1,7 +1,7 @@ -From 68d9dee5a3345c35197968e158d20cbc3e85e1b3 Mon Sep 17 00:00:00 2001 +From b8227bc71123c5ddb473a07884d33353e7078bf4 Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Thu, 30 Apr 2026 04:25:07 -0400 -Subject: [PATCH 6/6] feat(messages): Show 'This message was deleted.' +Subject: [PATCH 5/6] feat(messages): Show 'This message was deleted.' placeholder Upstream hides the whole MessageItem when is-deleted is true via a @@ -22,7 +22,7 @@ they are not reachable through bindings. 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/data/resources/style.css b/data/resources/style.css -index e00e789..b3a517f 100644 +index e00e7898..b3a517f9 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -9,6 +9,12 @@ @@ -39,7 +39,7 @@ index e00e789..b3a517f 100644 border-top: 1px solid @borders; } diff --git a/data/resources/ui/message_item.blp b/data/resources/ui/message_item.blp -index ba3fd23..49002a5 100644 +index ba3fd23c..49002a57 100644 --- a/data/resources/ui/message_item.blp +++ b/data/resources/ui/message_item.blp @@ -71,8 +71,6 @@ template $FlMessageItem: $ContextMenuBin { @@ -119,7 +119,7 @@ index ba3fd23..49002a5 100644 justify: left; vexpand: false; diff --git a/src/gui/message_item.rs b/src/gui/message_item.rs -index 1f62d50..9936680 100644 +index 1f62d509..99366802 100644 --- a/src/gui/message_item.rs +++ b/src/gui/message_item.rs @@ -36,6 +36,7 @@ impl MessageItem { diff --git a/patches/flare/0006-fix-backend-Refresh-cached-channels-on-init_channels.patch b/patches/flare/0006-fix-backend-Refresh-cached-channels-on-init_channels.patch new file mode 100644 index 0000000..5d97c8b --- /dev/null +++ b/patches/flare/0006-fix-backend-Refresh-cached-channels-on-init_channels.patch @@ -0,0 +1,76 @@ +From 1546f95d8b6666a7f0e9575ddfa85bcb7ec53e3d Mon Sep 17 00:00:00 2001 +From: Simon Gardling +Date: Wed, 6 May 2026 14:38:07 -0400 +Subject: [PATCH 6/6] fix(backend): Refresh cached channels on init_channels + re-runs + +init_channels populates the channel cache via or_insert_with, then +hands every freshly-built Channel (orphan or just-inserted) to the +load_last + initialize_avatar pass. For the first-run case this is +fine because the freshly-built Channel is also the cached one. On +re-runs (Received::Contacts after the user adds a contact in another +client), the existing entry wins the cache and the freshly-built +Channel is dropped on the floor, but initialize_avatar still runs +on the orphan rather than the cached instance the UI is bound to. + +The orphan's Contact gets its profile name and avatar populated; +the cached Contact does not, and the channel-list row is stuck on +"Unknown contact" until the next restart. + +Use the result of `entry().or_insert_with()` so to_load contains +the actually-cached Channels. +--- + src/backend/manager.rs | 28 +++++++++++++++++----------- + 1 file changed, 17 insertions(+), 11 deletions(-) + +diff --git a/src/backend/manager.rs b/src/backend/manager.rs +index 2e9bd761..38d355f4 100644 +--- a/src/backend/manager.rs ++++ b/src/backend/manager.rs +@@ -625,13 +625,16 @@ impl Manager { + // Storing loaded cannels. Extra block around to drop `known_channels` before `await`. + { + let mut known_channels = self.imp().channels.borrow_mut(); +- to_load.extend(loaded_channels.clone()); + for channel in loaded_channels { +- known_channels.entry(channel.thread()).or_insert_with(|| { +- log::trace!("Got a contact from the storage"); +- self.emit_by_name::<()>("channel", &[&channel]); +- channel +- }); ++ let stored = known_channels ++ .entry(channel.thread()) ++ .or_insert_with(|| { ++ log::trace!("Got a contact from the storage"); ++ self.emit_by_name::<()>("channel", &[&channel]); ++ channel.clone() ++ }) ++ .clone(); ++ to_load.push(stored); + } + } + +@@ -665,12 +668,15 @@ impl Manager { + // Store loaded channels. Extra block around to drop `known_channels` before `await`. + { + let mut known_channels = self.imp().channels.borrow_mut(); +- to_load.extend(loaded_channels.clone()); + for channel in loaded_channels { +- known_channels.entry(channel.thread()).or_insert_with(|| { +- self.emit_by_name::<()>("channel", &[&channel]); +- channel +- }); ++ let stored = known_channels ++ .entry(channel.thread()) ++ .or_insert_with(|| { ++ self.emit_by_name::<()>("channel", &[&channel]); ++ channel.clone() ++ }) ++ .clone(); ++ to_load.push(stored); + } + } + } +-- +2.53.0 +