flare: update patches and upstream source

This commit is contained in:
2026-05-06 15:21:53 -04:00
parent f1321d2223
commit 78d6aa01d5
10 changed files with 201 additions and 757 deletions

17
flake.lock generated
View File

@@ -491,6 +491,22 @@
"type": "github" "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": { "gitignore": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -1102,6 +1118,7 @@
"disko": "disko", "disko": "disko",
"emacs-overlay": "emacs-overlay", "emacs-overlay": "emacs-overlay",
"firefox-addons": "firefox-addons", "firefox-addons": "firefox-addons",
"flare-upstream": "flare-upstream",
"home-manager": "home-manager", "home-manager": "home-manager",
"home-manager-stable": "home-manager-stable", "home-manager-stable": "home-manager-stable",
"impermanence": "impermanence", "impermanence": "impermanence",

View File

@@ -95,6 +95,14 @@
flake = false; 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 # Server (muffin) — follows nixpkgs-stable
nix-minecraft = { nix-minecraft = {
url = "github:Infinidoge/nix-minecraft"; url = "github:Infinidoge/nix-minecraft";

View File

@@ -86,18 +86,24 @@
signal-desktop signal-desktop
# alternative GTK signal client; carries five local feature patches # alternative GTK signal client; carries local feature patches under
# under patches/flare/ on top of upstream 0.20.4 (typing indicators, # patches/flare/ on top of upstream master (typing indicators, edited
# formatted messages, edited messages, multi-select with delete-for-me, # messages, multi-select with delete-for-me, in-channel message search,
# and in-channel message search). # the deleted-message placeholder, and the not-yet-merged init_channels
# cache-miss fix).
(pkgs.flare-signal.overrideAttrs (old: { (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 = (old.patches or [ ]) ++ [
../../patches/flare/0001-feat-typing-Implement-typing-indicators.patch ../../patches/flare/0001-feat-typing-Implement-typing-indicators.patch
../../patches/flare/0002-feat-messages-Implement-formatted-messages.patch ../../patches/flare/0002-feat-messages-Implement-edited-messages.patch
../../patches/flare/0003-feat-messages-Implement-edited-messages.patch ../../patches/flare/0003-feat-messages-Multi-select-messages-and-delete-for-m.patch
../../patches/flare/0004-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-In-channel-message-search.patch ../../patches/flare/0005-feat-messages-Show-This-message-was-deleted.-placeho.patch
../../patches/flare/0006-feat-messages-Show-This-message-was-deleted.-placeho.patch ../../patches/flare/0006-fix-backend-Refresh-cached-channels-on-init_channels.patch
]; ];
})) }))

View File

@@ -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 <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/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/channel_messages.blp | 33 +++
data/resources/ui/preferences_window.blp | 15 ++ data/resources/ui/preferences_window.blp | 15 ++
src/backend/channel.rs | 59 +++++- src/backend/channel.rs | 59 +++++-
src/backend/manager.rs | 43 +++- src/backend/manager.rs | 32 ++-
src/backend/manager_thread.rs | 8 +- src/backend/manager_thread.rs | 8 +-
src/backend/message/mod.rs | 12 +- src/backend/message/mod.rs | 12 +-
src/gui/channel_messages.rs | 249 ++++++++++++++++++++++- src/gui/channel_messages.rs | 249 ++++++++++++++++++++++-
src/gui/preferences_window.rs | 23 +++ 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 diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20dc578..2bde927 100644 index 20dc578e..2bde927c 100644
--- a/CHANGELOG.md --- a/CHANGELOG.md
+++ b/CHANGELOG.md +++ b/CHANGELOG.md
@@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 @@ -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 ### Fixed
diff --git a/data/de.schmidhuberj.Flare.gschema.xml b/data/de.schmidhuberj.Flare.gschema.xml 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 --- a/data/de.schmidhuberj.Flare.gschema.xml
+++ b/data/de.schmidhuberj.Flare.gschema.xml +++ b/data/de.schmidhuberj.Flare.gschema.xml
@@ -58,6 +58,15 @@ @@ -58,6 +58,15 @@
@@ -64,7 +64,7 @@ index 8a58415..0705a73 100644
<default>"firstname"</default> <default>"firstname"</default>
<summary>How to sort contacts, e.g with "firstname" or "surname"</summary> <summary>How to sort contacts, e.g with "firstname" or "surname"</summary>
diff --git a/data/resources/style.css b/data/resources/style.css 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 --- a/data/resources/style.css
+++ b/data/resources/style.css +++ b/data/resources/style.css
@@ -13,6 +13,12 @@ @@ -13,6 +13,12 @@
@@ -81,7 +81,7 @@ index dcd0569..00e4783 100644
padding:0; padding:0;
} }
diff --git a/data/resources/ui/channel_messages.blp b/data/resources/ui/channel_messages.blp 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 --- a/data/resources/ui/channel_messages.blp
+++ b/data/resources/ui/channel_messages.blp +++ b/data/resources/ui/channel_messages.blp
@@ -102,6 +102,39 @@ template $FlChannelMessages: Box { @@ -102,6 +102,39 @@ template $FlChannelMessages: Box {
@@ -125,7 +125,7 @@ index 53be7ab..7f438e4 100644
styles [ styles [
"toolbar", "toolbar",
diff --git a/data/resources/ui/preferences_window.blp b/data/resources/ui/preferences_window.blp 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 --- a/data/resources/ui/preferences_window.blp
+++ b/data/resources/ui/preferences_window.blp +++ b/data/resources/ui/preferences_window.blp
@@ -66,6 +66,21 @@ template $FlPreferencesWindow: Adw.PreferencesDialog { @@ -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 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 --- a/src/backend/channel.rs
+++ b/src/backend/channel.rs +++ b/src/backend/channel.rs
@@ -15,8 +15,9 @@ use glib::Bytes; @@ -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): /// This does the following (based on the type of message):
/// - Add a quote to the message if needed. /// - Add a quote to the message if needed.
diff --git a/src/backend/manager.rs b/src/backend/manager.rs 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 --- a/src/backend/manager.rs
+++ b/src/backend/manager.rs +++ b/src/backend/manager.rs
@@ -8,7 +8,7 @@ use libsignal_service::protocol::DeviceId; @@ -8,7 +8,7 @@ use libsignal_service::protocol::DeviceId;
@@ -241,22 +241,7 @@ index c25fba0..eaa41e0 100644
protocol::ServiceId, protocol::ServiceId,
sender::{AttachmentSpec, AttachmentUploadError}, sender::{AttachmentSpec, AttachmentUploadError},
websocket::account::DeviceInfo, websocket::account::DeviceInfo,
@@ -490,20 +490,42 @@ impl Manager { @@ -499,16 +499,27 @@ 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;
let channel = Channel::from_contact_or_group(contact, group, self).await; let channel = Channel::from_contact_or_group(contact, group, self).await;
channel.initialize_avatar().await; channel.initialize_avatar().await;
@@ -291,7 +276,7 @@ index c25fba0..eaa41e0 100644
} }
pub fn channel_from_thread(&self, thread: Thread) -> Option<Channel> { pub fn channel_from_thread(&self, thread: Thread) -> Option<Channel> {
@@ -737,14 +759,15 @@ impl Manager { @@ -742,14 +753,15 @@ impl Manager {
pub(super) async fn send_message_to_group( pub(super) async fn send_message_to_group(
&self, &self,
group_key: Vec<u8>, group_key: Vec<u8>,
@@ -310,7 +295,7 @@ index c25fba0..eaa41e0 100644
}) })
.await .await
diff --git a/src/backend/manager_thread.rs b/src/backend/manager_thread.rs 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 --- a/src/backend/manager_thread.rs
+++ b/src/backend/manager_thread.rs +++ b/src/backend/manager_thread.rs
@@ -21,7 +21,7 @@ use libsignal_service::{ @@ -21,7 +21,7 @@ use libsignal_service::{
@@ -350,7 +335,7 @@ index 1f6a885..cba62ae 100644
sender, sender,
)) ))
diff --git a/src/backend/message/mod.rs b/src/backend/message/mod.rs 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 --- a/src/backend/message/mod.rs
+++ b/src/backend/message/mod.rs +++ b/src/backend/message/mod.rs
@@ -270,14 +270,16 @@ impl Message { @@ -270,14 +270,16 @@ impl Message {
@@ -376,7 +361,7 @@ index 11ccd7c..74952ac 100644
let Some(channel) = channel else { let Some(channel) = channel else {
diff --git a/src/gui/channel_messages.rs b/src/gui/channel_messages.rs 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 --- a/src/gui/channel_messages.rs
+++ b/src/gui/channel_messages.rs +++ b/src/gui/channel_messages.rs
@@ -5,6 +5,16 @@ use crate::ApplicationError; @@ -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() { if let Some(active_chan) = self.active_channel.borrow().as_ref() {
active_chan.set_property("draft", self.text_entry.text()); 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(); self.obj().focus_input();
@@ -658,7 +643,7 @@ index 0e8ae4e..831fc25 100644
} }
#[template_callback(function)] #[template_callback(function)]
@@ -501,7 +737,18 @@ pub mod imp { @@ -508,7 +744,18 @@ pub mod imp {
s.obj().set_reply_message(None::<TextMessage>); s.obj().set_reply_message(None::<TextMessage>);
if let Some(channel) = s.active_channel.borrow().as_ref() { if let Some(channel) = s.active_channel.borrow().as_ref() {
let draft = channel.property("draft"); 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 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 --- a/src/gui/preferences_window.rs
+++ b/src/gui/preferences_window.rs +++ b/src/gui/preferences_window.rs
@@ -78,6 +78,11 @@ pub mod imp { @@ -78,6 +78,11 @@ pub mod imp {

View File

@@ -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 <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 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, - 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
@@ -22,30 +22,29 @@ Subject: [PATCH 3/6] feat(messages): Implement edited messages
data/resources/ui/message_item.blp | 10 +++ data/resources/ui/message_item.blp | 10 +++
src/backend/channel.rs | 92 ++++++++++++++++++++- src/backend/channel.rs | 92 ++++++++++++++++++++-
src/backend/message/edit_message_item.rs | 66 +++++++++++++++ src/backend/message/edit_message_item.rs | 66 +++++++++++++++
src/backend/message/formatting.rs | 11 ++-
src/backend/message/mod.rs | 71 ++++++++++++++++ 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/channel_messages.rs | 67 +++++++++++++++
src/gui/components/indicators.rs | 2 + src/gui/components/indicators.rs | 2 +
src/gui/components/item_row.rs | 58 ++++++------- src/gui/components/item_row.rs | 58 ++++++-------
src/gui/message_item.rs | 27 ++++++ 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 create mode 100644 src/backend/message/edit_message_item.rs
diff --git a/CHANGELOG.md b/CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md
index 50cd5f5..0338ed8 100644 index 2bde927c..7caa7296 100644
--- a/CHANGELOG.md --- a/CHANGELOG.md
+++ b/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. - 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. +- Display incoming edited messages with an `edited` indicator and edit your own sent messages from their context menu.
## [0.20.4] - 2026-04-22 ## [0.20.4] - 2026-04-22
diff --git a/data/resources/ui/channel_messages.blp b/data/resources/ui/channel_messages.blp 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 --- a/data/resources/ui/channel_messages.blp
+++ b/data/resources/ui/channel_messages.blp +++ b/data/resources/ui/channel_messages.blp
@@ -238,6 +238,95 @@ template $FlChannelMessages: Box { @@ -238,6 +238,95 @@ template $FlChannelMessages: Box {
@@ -145,7 +144,7 @@ index 6c3948f..f3d2348 100644
Box { Box {
vexpand-set: true; vexpand-set: true;
diff --git a/data/resources/ui/components/indicators.blp b/data/resources/ui/components/indicators.blp 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 --- a/data/resources/ui/components/indicators.blp
+++ b/data/resources/ui/components/indicators.blp +++ b/data/resources/ui/components/indicators.blp
@@ -8,6 +8,16 @@ template $FlMessageIndicators { @@ -8,6 +8,16 @@ template $FlMessageIndicators {
@@ -166,7 +165,7 @@ index f6c51f6..977f1c4 100644
styles [ styles [
"dim-label", "dim-label",
diff --git a/data/resources/ui/message_item.blp b/data/resources/ui/message_item.blp 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 --- a/data/resources/ui/message_item.blp
+++ b/data/resources/ui/message_item.blp +++ b/data/resources/ui/message_item.blp
@@ -16,6 +16,14 @@ menu message-menu { @@ -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 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 --- a/src/backend/channel.rs
+++ b/src/backend/channel.rs +++ b/src/backend/channel.rs
@@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
@@ -340,7 +339,7 @@ index 4bb1d38..711c92c 100644
#[property(name = "avatar", get = Self::avatar)] #[property(name = "avatar", get = Self::avatar)]
diff --git a/src/backend/message/edit_message_item.rs b/src/backend/message/edit_message_item.rs diff --git a/src/backend/message/edit_message_item.rs b/src/backend/message/edit_message_item.rs
new file mode 100644 new file mode 100644
index 0000000..9655f50 index 00000000..9655f50e
--- /dev/null --- /dev/null
+++ b/src/backend/message/edit_message_item.rs +++ b/src/backend/message/edit_message_item.rs
@@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
@@ -410,50 +409,26 @@ index 0000000..9655f50
+ impl MessageImpl for EditMessageItem {} + impl MessageImpl for EditMessageItem {}
+ impl ObjectImpl 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<BodyRange>) {
// 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 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 --- a/src/backend/message/mod.rs
+++ b/src/backend/message/mod.rs +++ b/src/backend/message/mod.rs
@@ -1,6 +1,7 @@ @@ -1,12 +1,14 @@
mod call_message; mod call_message;
mod deletion_message; mod deletion_message;
mod display_message; mod display_message;
+mod edit_message_item; +mod edit_message_item;
mod formatting;
mod reaction_message; mod reaction_message;
mod text_message; mod text_message;
@@ -8,6 +9,7 @@ mod text_message;
pub use call_message::{CallMessage, CallMessageType}; pub use call_message::{CallMessage, CallMessageType};
pub use deletion_message::DeletionMessage; pub use deletion_message::DeletionMessage;
pub use display_message::{DisplayMessage, DisplayMessageExt}; pub use display_message::{DisplayMessage, DisplayMessageExt};
+pub use edit_message_item::EditMessageItem; +pub use edit_message_item::EditMessageItem;
pub use formatting::parse_formatting;
pub use reaction_message::ReactionMessage; pub use reaction_message::ReactionMessage;
pub use text_message::TextMessage; pub use text_message::TextMessage;
@@ -253,6 +255,75 @@ impl Message {
@@ -251,6 +253,75 @@ impl Message {
.upcast(), .upcast(),
) )
} }
@@ -530,10 +505,10 @@ index 4e0f584..f3a0537 100644
ContentBody::CallMessage(c) => { ContentBody::CallMessage(c) => {
// TODO: Group calls? // TODO: Group calls?
diff --git a/src/backend/message/text_message.rs b/src/backend/message/text_message.rs 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 --- a/src/backend/message/text_message.rs
+++ b/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); self.set_property("is-deleted", true);
} }
@@ -569,8 +544,7 @@ index c06bcfa..ff7aaaa 100644
+ let (body, body_ranges) = if cleaned.is_empty() { + let (body, body_ranges) = if cleaned.is_empty() {
+ (None, Vec::new()) + (None, Vec::new())
+ } else { + } else {
+ let (body, ranges) = super::parse_formatting(&cleaned); + (Some(cleaned), Vec::new())
+ (Some(body), ranges)
+ }; + };
+ +
+ // Carry forward the original message's structural fields (quote, + // 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. /// Send a reaction for a message and apply it.
pub async fn send_reaction<S: AsRef<str>>( pub async fn send_reaction<S: AsRef<str>>(
&self, &self,
@@ -462,6 +522,8 @@ mod imp { @@ -352,6 +411,8 @@ mod imp {
pub(super) message_attributes: RefCell<AttrList>, pub(super) message_attributes: RefCell<AttrList>,
#[property(get, set)] #[property(get, set)]
pub(super) is_deleted: RefCell<bool>, pub(super) is_deleted: RefCell<bool>,
@@ -610,7 +584,7 @@ index c06bcfa..ff7aaaa 100644
impl TextMessage { impl TextMessage {
diff --git a/src/gui/channel_messages.rs b/src/gui/channel_messages.rs 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 --- a/src/gui/channel_messages.rs
+++ b/src/gui/channel_messages.rs +++ b/src/gui/channel_messages.rs
@@ -2,6 +2,7 @@ use crate::prelude::*; @@ -2,6 +2,7 @@ use crate::prelude::*;
@@ -650,7 +624,7 @@ index 831fc25..9187bee 100644
#[property(get, set, default = true)] #[property(get, set, default = true)]
sticky: Cell<bool>, sticky: Cell<bool>,
@@ -480,6 +496,13 @@ pub mod imp { @@ -487,6 +503,13 @@ pub mod imp {
self.obj().set_reply_message(None::<TextMessage>); self.obj().set_reply_message(None::<TextMessage>);
} }
@@ -664,7 +638,7 @@ index 831fc25..9187bee 100644
#[template_callback] #[template_callback]
fn remove_attachments(&self) { fn remove_attachments(&self) {
log::trace!("Unsetting attachments"); log::trace!("Unsetting attachments");
@@ -585,6 +608,33 @@ pub mod imp { @@ -592,6 +615,33 @@ pub mod imp {
}; };
self.obj().notify("has-attachments"); self.obj().notify("has-attachments");
@@ -698,7 +672,7 @@ index 831fc25..9187bee 100644
if text.is_empty() && attachments.is_empty() { if text.is_empty() && attachments.is_empty() {
log::warn!("Got requested to send empty message, skipping"); 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::<gtk::ListItem>().unwrap(); let list_item = object.downcast_ref::<gtk::ListItem>().unwrap();
list_item.set_activatable(false); list_item.set_activatable(false);
list_item.set_selectable(false); list_item.set_selectable(false);
@@ -735,6 +801,7 @@ pub mod imp { @@ -742,6 +808,7 @@ pub mod imp {
self, self,
move |_, _| { move |_, _| {
s.obj().set_reply_message(None::<TextMessage>); s.obj().set_reply_message(None::<TextMessage>);
@@ -730,7 +704,7 @@ index 831fc25..9187bee 100644
let draft = channel.property("draft"); let draft = channel.property("draft");
// Block the typing buffer-changed handler so // Block the typing buffer-changed handler so
diff --git a/src/gui/components/indicators.rs b/src/gui/components/indicators.rs 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 --- a/src/gui/components/indicators.rs
+++ b/src/gui/components/indicators.rs +++ b/src/gui/components/indicators.rs
@@ -26,6 +26,8 @@ mod imp { @@ -26,6 +26,8 @@ mod imp {
@@ -743,7 +717,7 @@ index ce38221..4356607 100644
//#[template_child] //#[template_child]
//pub(super) sending_state_icon: TemplateChild<gtk::Image>, //pub(super) sending_state_icon: TemplateChild<gtk::Image>,
diff --git a/src/gui/components/item_row.rs b/src/gui/components/item_row.rs 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 --- a/src/gui/components/item_row.rs
+++ b/src/gui/components/item_row.rs +++ b/src/gui/components/item_row.rs
@@ -1,5 +1,3 @@ @@ -1,5 +1,3 @@
@@ -847,7 +821,7 @@ index b2c20d3..538b1bb 100644
}); });
SIGNALS.as_ref() SIGNALS.as_ref()
diff --git a/src/gui/message_item.rs b/src/gui/message_item.rs 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 --- a/src/gui/message_item.rs
+++ b/src/gui/message_item.rs +++ b/src/gui/message_item.rs
@@ -94,6 +94,14 @@ impl MessageItem { @@ -94,6 +94,14 @@ impl MessageItem {

View File

@@ -1,622 +0,0 @@
From 200461c0abe0399ae4b5b0bfd3848fcc226ba308 Mon Sep 17 00:00:00 2001
From: Simon Gardling <titaniumtown@proton.me>
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<MatchedSpan> {
+ let mut open: HashMap<Marker, (usize, usize)> = HashMap::new();
+ let mut spans: Vec<MatchedSpan> = 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<BodyRange>) {
+ let chars: Vec<char> = 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<BodyRange> = 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<pango::Attribute> {
+ fn span<A: Into<pango::Attribute>>(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<S: AsRef<str>>(
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<String>, 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<u16> = 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<u16> = 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<usize> = 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

View File

@@ -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 <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 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 - 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
@@ -30,19 +30,19 @@ Subject: [PATCH 4/6] feat(messages): Multi-select messages and delete for me
10 files changed, 416 insertions(+), 1 deletion(-) 10 files changed, 416 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0338ed8..47ec77a 100644 index 7caa7296..e287fc73 100644
--- a/CHANGELOG.md --- a/CHANGELOG.md
+++ b/CHANGELOG.md +++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 @@ -11,6 +11,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 typing indicators while composing a message and display them above the message input.
- Send formatted messages with markdown-style markers (`**bold**`, `*italic*`, `~~strike~~`, `||spoiler||`, `` `monospace` ``). - 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. - 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. +- Multi-select messages from their context menu and delete the selection locally with a single action.
## [0.20.4] - 2026-04-22 ## [0.20.4] - 2026-04-22
diff --git a/data/resources/style.css b/data/resources/style.css 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 --- a/data/resources/style.css
+++ b/data/resources/style.css +++ b/data/resources/style.css
@@ -19,6 +19,20 @@ @@ -19,6 +19,20 @@
@@ -67,7 +67,7 @@ index 00e4783..1c0cdfd 100644
padding:0; padding:0;
} }
diff --git a/data/resources/ui/channel_messages.blp b/data/resources/ui/channel_messages.blp 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 --- a/data/resources/ui/channel_messages.blp
+++ b/data/resources/ui/channel_messages.blp +++ b/data/resources/ui/channel_messages.blp
@@ -135,6 +135,66 @@ template $FlChannelMessages: Box { @@ -135,6 +135,66 @@ template $FlChannelMessages: Box {
@@ -138,7 +138,7 @@ index f3d2348..eb927f8 100644
styles [ styles [
"toolbar", "toolbar",
diff --git a/data/resources/ui/message_item.blp b/data/resources/ui/message_item.blp 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 --- a/data/resources/ui/message_item.blp
+++ b/data/resources/ui/message_item.blp +++ b/data/resources/ui/message_item.blp
@@ -24,6 +24,13 @@ menu message-menu { @@ -24,6 +24,13 @@ menu message-menu {
@@ -176,7 +176,7 @@ index 2c21b8b..ba3fd23 100644
visible: bind template.message as <$FlTextMessage>.pending; visible: bind template.message as <$FlTextMessage>.pending;
tooltip-text: _("This message is currently being sent"); tooltip-text: _("This message is currently being sent");
diff --git a/src/backend/channel.rs b/src/backend/channel.rs 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 --- a/src/backend/channel.rs
+++ b/src/backend/channel.rs +++ b/src/backend/channel.rs
@@ -199,6 +199,28 @@ impl Channel { @@ -199,6 +199,28 @@ impl Channel {
@@ -226,7 +226,7 @@ index 711c92c..f07ce96 100644
}); });
SIGNALS.as_ref() SIGNALS.as_ref()
diff --git a/src/backend/manager.rs b/src/backend/manager.rs 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 --- a/src/backend/manager.rs
+++ b/src/backend/manager.rs +++ b/src/backend/manager.rs
@@ -210,6 +210,38 @@ impl Manager { @@ -210,6 +210,38 @@ impl Manager {
@@ -269,10 +269,10 @@ index eaa41e0..0964681 100644
&self, &self,
token: S, token: S,
diff --git a/src/backend/message/mod.rs b/src/backend/message/mod.rs 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 --- a/src/backend/message/mod.rs
+++ b/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<bool>, pub(super) pending: RefCell<bool>,
#[property(get, set)] #[property(get, set)]
pub(super) error: RefCell<bool>, pub(super) error: RefCell<bool>,
@@ -282,7 +282,7 @@ index f3a0537..eba08ec 100644
pub(super) data: RefCell<Option<DataMessage>>, pub(super) data: RefCell<Option<DataMessage>>,
diff --git a/src/backend/timeline/mod.rs b/src/backend/timeline/mod.rs 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 --- a/src/backend/timeline/mod.rs
+++ b/src/backend/timeline/mod.rs +++ b/src/backend/timeline/mod.rs
@@ -44,6 +44,22 @@ impl Timeline { @@ -44,6 +44,22 @@ impl Timeline {
@@ -309,7 +309,7 @@ index 1ce6a24..18dd436 100644
let current_items = self.imp().list.borrow(); let current_items = self.imp().list.borrow();
let index = current_items.binary_search_by_key(&timestamp, |i| i.timestamp()); let index = current_items.binary_search_by_key(&timestamp, |i| i.timestamp());
diff --git a/src/gui/channel_messages.rs b/src/gui/channel_messages.rs 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 --- a/src/gui/channel_messages.rs
+++ b/src/gui/channel_messages.rs +++ b/src/gui/channel_messages.rs
@@ -2,7 +2,8 @@ use crate::prelude::*; @@ -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() { if let Some(active_chan) = self.active_channel.borrow().as_ref() {
active_chan.set_property("draft", self.text_entry.text()); 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().focus_input();
self.obj().setup_typing_indicator(); self.obj().setup_typing_indicator();
@@ -448,7 +448,7 @@ index 9187bee..d6d1826 100644
} }
#[template_callback(function)] #[template_callback(function)]
@@ -503,6 +595,41 @@ pub mod imp { @@ -510,6 +602,41 @@ pub mod imp {
self.text_entry.clear(); self.text_entry.clear();
} }
@@ -491,7 +491,7 @@ index 9187bee..d6d1826 100644
fn remove_attachments(&self) { fn remove_attachments(&self) {
log::trace!("Unsetting attachments"); log::trace!("Unsetting attachments");
diff --git a/src/gui/message_item.rs b/src/gui/message_item.rs 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 --- a/src/gui/message_item.rs
+++ b/src/gui/message_item.rs +++ b/src/gui/message_item.rs
@@ -34,6 +34,7 @@ impl MessageItem { @@ -34,6 +34,7 @@ impl MessageItem {

View File

@@ -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 <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 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 - 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
@@ -24,11 +24,11 @@ Subject: [PATCH 5/6] feat(messages): In-channel message search
9 files changed, 374 insertions(+), 1 deletion(-) 9 files changed, 374 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47ec77a..16880cd 100644 index e287fc73..e221f555 100644
--- a/CHANGELOG.md --- a/CHANGELOG.md
+++ b/CHANGELOG.md +++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 @@ -12,6 +12,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` ``). - 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. - 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. - 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. +- 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 ## [0.20.4] - 2026-04-22
diff --git a/data/resources/style.css b/data/resources/style.css 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 --- a/data/resources/style.css
+++ b/data/resources/style.css +++ b/data/resources/style.css
@@ -294,3 +294,13 @@ unread-indicator { @@ -294,3 +294,13 @@ unread-indicator {
@@ -55,7 +55,7 @@ index 1c0cdfd..e00e789 100644
+} +}
\ No newline at end of file \ No newline at end of file
diff --git a/data/resources/ui/channel_messages.blp b/data/resources/ui/channel_messages.blp 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 --- a/data/resources/ui/channel_messages.blp
+++ b/data/resources/ui/channel_messages.blp +++ b/data/resources/ui/channel_messages.blp
@@ -43,6 +43,61 @@ template $FlChannelMessages: Box { @@ -43,6 +43,61 @@ template $FlChannelMessages: Box {
@@ -121,7 +121,7 @@ index eb927f8..a166f7b 100644
[overlay] [overlay]
Adw.Spinner { Adw.Spinner {
diff --git a/data/resources/ui/shortcuts.blp b/data/resources/ui/shortcuts.blp 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 --- a/data/resources/ui/shortcuts.blp
+++ b/data/resources/ui/shortcuts.blp +++ b/data/resources/ui/shortcuts.blp
@@ -58,5 +58,10 @@ Adw.ShortcutsDialog help_overlay { @@ -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 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 --- a/src/backend/message/display_message.rs
+++ b/src/backend/message/display_message.rs +++ b/src/backend/message/display_message.rs
@@ -137,6 +137,7 @@ mod imp { @@ -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 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 --- a/src/backend/timeline/mod.rs
+++ b/src/backend/timeline/mod.rs +++ b/src/backend/timeline/mod.rs
@@ -69,6 +69,16 @@ impl Timeline { @@ -69,6 +69,16 @@ impl Timeline {
@@ -197,7 +197,7 @@ index 18dd436..40fe324 100644
let current_items = self.imp().list.borrow(); let current_items = self.imp().list.borrow();
current_items.clone().into_iter() current_items.clone().into_iter()
diff --git a/src/gui/channel_messages.rs b/src/gui/channel_messages.rs 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 --- a/src/gui/channel_messages.rs
+++ b/src/gui/channel_messages.rs +++ b/src/gui/channel_messages.rs
@@ -85,6 +85,204 @@ impl ChannelMessages { @@ -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() { if let Some(active_chan) = self.active_channel.borrow().as_ref() {
active_chan.set_property("draft", self.text_entry.text()); 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) { fn handle_row_activated(&self, row: gtk::ListBoxRow) {
if let Ok(msg) = row if let Ok(msg) = row
diff --git a/src/gui/message_item.rs b/src/gui/message_item.rs 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 --- a/src/gui/message_item.rs
+++ b/src/gui/message_item.rs +++ b/src/gui/message_item.rs
@@ -33,6 +33,7 @@ impl MessageItem { @@ -33,6 +33,7 @@ impl MessageItem {
@@ -543,7 +543,7 @@ index f2d98e2..1f62d50 100644
let message = self.message(); let message = self.message();
message.connect_notify_local( message.connect_notify_local(
diff --git a/src/gui/window.rs b/src/gui/window.rs 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 --- a/src/gui/window.rs
+++ b/src/gui/window.rs +++ b/src/gui/window.rs
@@ -20,6 +20,7 @@ impl Window { @@ -20,6 +20,7 @@ impl Window {

View File

@@ -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 <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 6/6] feat(messages): Show 'This message was deleted.' Subject: [PATCH 5/6] 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
@@ -22,7 +22,7 @@ they are not reachable through bindings.
3 files changed, 65 insertions(+), 4 deletions(-) 3 files changed, 65 insertions(+), 4 deletions(-)
diff --git a/data/resources/style.css b/data/resources/style.css 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 --- a/data/resources/style.css
+++ b/data/resources/style.css +++ b/data/resources/style.css
@@ -9,6 +9,12 @@ @@ -9,6 +9,12 @@
@@ -39,7 +39,7 @@ index e00e789..b3a517f 100644
border-top: 1px solid @borders; border-top: 1px solid @borders;
} }
diff --git a/data/resources/ui/message_item.blp b/data/resources/ui/message_item.blp 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 --- a/data/resources/ui/message_item.blp
+++ b/data/resources/ui/message_item.blp +++ b/data/resources/ui/message_item.blp
@@ -71,8 +71,6 @@ template $FlMessageItem: $ContextMenuBin { @@ -71,8 +71,6 @@ template $FlMessageItem: $ContextMenuBin {
@@ -119,7 +119,7 @@ index ba3fd23..49002a5 100644
justify: left; justify: left;
vexpand: false; vexpand: false;
diff --git a/src/gui/message_item.rs b/src/gui/message_item.rs 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 --- a/src/gui/message_item.rs
+++ b/src/gui/message_item.rs +++ b/src/gui/message_item.rs
@@ -36,6 +36,7 @@ impl MessageItem { @@ -36,6 +36,7 @@ impl MessageItem {

View File

@@ -0,0 +1,76 @@
From 1546f95d8b6666a7f0e9575ddfa85bcb7ec53e3d Mon Sep 17 00:00:00 2001
From: Simon Gardling <titaniumtown@proton.me>
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