flare: update patches and upstream source
This commit is contained in:
@@ -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>
|
||||
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
|
||||
<default>"firstname"</default>
|
||||
<summary>How to sort contacts, e.g with "firstname" or "surname"</summary>
|
||||
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<Channel> {
|
||||
@@ -737,14 +759,15 @@ impl Manager {
|
||||
@@ -742,14 +753,15 @@ impl Manager {
|
||||
pub(super) async fn send_message_to_group(
|
||||
&self,
|
||||
group_key: Vec<u8>,
|
||||
@@ -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::<TextMessage>);
|
||||
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 {
|
||||
|
||||
@@ -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>
|
||||
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<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
|
||||
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<S: AsRef<str>>(
|
||||
&self,
|
||||
@@ -462,6 +522,8 @@ mod imp {
|
||||
@@ -352,6 +411,8 @@ mod imp {
|
||||
pub(super) message_attributes: RefCell<AttrList>,
|
||||
#[property(get, set)]
|
||||
pub(super) is_deleted: RefCell<bool>,
|
||||
@@ -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<bool>,
|
||||
@@ -480,6 +496,13 @@ pub mod imp {
|
||||
@@ -487,6 +503,13 @@ pub mod imp {
|
||||
self.obj().set_reply_message(None::<TextMessage>);
|
||||
}
|
||||
|
||||
@@ -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::<gtk::ListItem>().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::<TextMessage>);
|
||||
@@ -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<gtk::Image>,
|
||||
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 {
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
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<bool>,
|
||||
#[property(get, set)]
|
||||
pub(super) error: RefCell<bool>,
|
||||
@@ -282,7 +282,7 @@ index f3a0537..eba08ec 100644
|
||||
pub(super) data: RefCell<Option<DataMessage>>,
|
||||
|
||||
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 {
|
||||
@@ -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>
|
||||
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 {
|
||||
@@ -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>
|
||||
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 {
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user