Repository: FedericoBruzzone/tgt
Pull request #147: Add the ability to sort the chat list window through a search string
Author: login: Mroik
Created: 2025-04-08T12:36:29Z | Merged: 2025-04-08T21:16:29Z

Adds the ability to sort chats based on a search string. To do this the PR introduces the `nucleo-matcher` crate which is used to rank chat names using fuzzy search algorithms.


Diff:
diff --git a/Cargo.lock b/Cargo.lock
index d4835da1..0842f462 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1804,6 +1804,16 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "nucleo-matcher"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85"
+dependencies = [
+ "memchr",
+ "unicode-segmentation",
+]
+
 [[package]]
 name = "num-bigint"
 version = "0.4.6"
@@ -3026,6 +3036,7 @@ dependencies = [
  "futures",
  "image",
  "lazy_static",
+ "nucleo-matcher",
  "ratatui",
  "ratatui-image",
  "reqwest",
diff --git a/Cargo.toml b/Cargo.toml
index 45f50e00..c5dbf537 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -60,6 +60,7 @@ ratatui-image = "5.0.0"
 image = "0.25.6"
 signal-hook = "0.3.17"
 clap = { version = "4.5.35", features = ["derive"] }
+nucleo-matcher = "0.3.1"
 
 [build-dependencies]
 dirs = "6.0.0"
@@ -67,8 +68,6 @@ reqwest = { version = "0.12.15", features = ["blocking"] }
 zip = { version = "2.3.0" }
 tdlib-rs = "1.0.5"
 
-[profile.dev]
-
 [profile.release]
 lto = true
 opt-level = 'z'
diff --git a/config/keymap.toml b/config/keymap.toml
index 6f241ab2..c204e1ae 100644
--- a/config/keymap.toml
+++ b/config/keymap.toml
@@ -47,6 +47,10 @@ keymap = [
   { keys = ["right"], command = "chat_list_open", description = "Open the selected chat"},
   # Open the selected chat
   { keys = ["enter"], command = "chat_list_open", description = "Open the selected chat"},
+  # Move focus to the prompt and set its state to receive a string used to order the entries in the chat list window.
+  { keys = ["alt+r"], command = "chat_list_search", description = "Focus on prompt to start searching"},
+  # Restore the default sorting in the chat list window.
+  { keys = ["alt+c"], command = "chat_list_restore_sort", description = "Restore the default ordering of the chat list"},
 ]
 
 # The chat key bindings are only usable in the chat component.
diff --git a/src/action.rs b/src/action.rs
index 7f84c198..68d16f08 100644
--- a/src/action.rs
+++ b/src/action.rs
@@ -143,6 +143,8 @@ pub enum Action {
     ChatListUnselect,
     /// ChatListOpen action.
     ChatListOpen,
+    /// ChatListSortWithString action.
+    ChatListSortWithString(String),
 
     /// ChatWindowNext action.
     ChatWindowNext,
@@ -169,6 +171,14 @@ pub enum Action {
     /// This event is used to reply to a message.
     /// The first parameter is the `message_id` and the second parameter is the `text`.
     ReplyMessage(i64, String),
+    /// ChatListSearch event.
+    /// This event is used to set the prompt to search to set the search string
+    /// for the ChatListWindow.
+    ChatListSearch,
+    /// ChatListRestoreSort event.
+    /// This event is used to restore the default ordering.
+    /// I.e. pinned first then chronological order.
+    ChatListRestoreSort,
 }
 /// Implement the `Action` enum.
 impl Action {
@@ -207,6 +217,8 @@ impl FromStr for Action {
             "chat_list_previous" => Ok(Action::ChatListPrevious),
             "chat_list_unselect" => Ok(Action::ChatListUnselect),
             "chat_list_open" => Ok(Action::ChatListOpen),
+            "chat_list_search" => Ok(Action::ChatListSearch),
+            "chat_list_restore_sort" => Ok(Action::ChatListRestoreSort),
             "chat_window_next" => Ok(Action::ChatWindowNext),
             "chat_window_previous" => Ok(Action::ChatWindowPrevious),
             "chat_window_unselect" => Ok(Action::ChatWindowUnselect),
diff --git a/src/components/chat_list_window.rs b/src/components/chat_list_window.rs
index d764b0cd..a0b3072f 100644
--- a/src/components/chat_list_window.rs
+++ b/src/components/chat_list_window.rs
@@ -4,6 +4,7 @@ use crate::component_name::ComponentName::Prompt;
 use crate::components::component_traits::{Component, HandleFocus};
 use crate::event::Event;
 use crate::tg::message_entry::MessageEntry;
+use nucleo_matcher::{Matcher, Utf32Str};
 use ratatui::layout::Rect;
 use ratatui::symbols::border::PLAIN;
 use ratatui::text::{Line, Span, Text};
@@ -140,6 +141,8 @@ pub struct ChatListWindow {
     chat_list_state: ListState,
     /// Indicates whether the `ChatListWindow` is focused or not.
     focused: bool,
+    /// String used to sort chats.
+    sort_string: Option<String>,
 }
 /// Implementation of the `ChatListWindow` struct.
 impl ChatListWindow {
@@ -156,6 +159,7 @@ impl ChatListWindow {
         let chat_list = vec![];
         let chat_list_state = ListState::default();
         let focused = false;
+        let sort_string = None;
 
         ChatListWindow {
             app_context,
@@ -164,6 +168,7 @@ impl ChatListWindow {
             chat_list,
             chat_list_state,
             focused,
+            sort_string,
         }
     }
     /// Set the name of the `ChatListWindow`.
@@ -242,6 +247,16 @@ impl ChatListWindow {
             }
         }
     }
+
+    /// Sets the string used to order the entries of the chat list.
+    fn sort(&mut self, s: String) {
+        self.sort_string = Some(s);
+    }
+
+    /// Unsets the string used to order the entries of the chat list.
+    fn default_sort(&mut self) {
+        self.sort_string = None;
+    }
 }
 
 /// Implement the `HandleFocus` trait for the `ChatListWindow` struct.
@@ -270,6 +285,8 @@ impl Component for ChatListWindow {
             Action::ChatListPrevious => self.previous(),
             Action::ChatListUnselect => self.unselect(),
             Action::ChatListOpen => self.confirm_selection(),
+            Action::ChatListSortWithString(s) => self.sort(s),
+            Action::ChatListRestoreSort => self.default_sort(),
             _ => {}
         }
     }
@@ -282,7 +299,36 @@ impl Component for ChatListWindow {
         };
         if let Ok(Some(items)) = self.app_context.tg_context().get_chats_index() {
             self.chat_list = items;
+
+            // Sort before drawing
+            if let Some(s) = self.sort_string.as_ref() {
+                let mut config = nucleo_matcher::Config::DEFAULT;
+                config.prefer_prefix = true;
+                let mut matcher = Matcher::new(config);
+                let s: Vec<char> = s.chars().collect();
+                self.chat_list.sort_by(|a, b| {
+                    let a: Vec<char> = a.chat_name.chars().collect();
+                    let b: Vec<char> = b.chat_name.chars().collect();
+                    let a_score = matcher
+                        .fuzzy_indices(
+                            Utf32Str::Unicode(&a),
+                            Utf32Str::Unicode(&s),
+                            &mut Vec::new(),
+                        )
+                        .unwrap_or(0);
+                    let b_score = matcher
+                        .fuzzy_indices(
+                            Utf32Str::Unicode(&b),
+                            Utf32Str::Unicode(&s),
+                            &mut Vec::new(),
+                        )
+                        .unwrap_or(0);
+                    a_score.cmp(&b_score)
+                });
+                self.chat_list.reverse();
+            }
         }
+
         let items = self
             .chat_list
             .iter()
diff --git a/src/components/core_window.rs b/src/components/core_window.rs
index ff8fd0f3..aefd961b 100644
--- a/src/components/core_window.rs
+++ b/src/components/core_window.rs
@@ -255,6 +255,17 @@ impl Component for CoreWindow {
             Action::HideChatWindowReply => {
                 self.show_reply_message = false;
             }
+            Action::ChatListSearch => {
+                self.component_focused = Some(ComponentName::Prompt);
+                self.components
+                    .get_mut(&ComponentName::Prompt)
+                    .unwrap_or_else(|| panic!("Failed to get component: {}", ComponentName::Prompt))
+                    .focus();
+                self.components
+                    .iter_mut()
+                    .filter(|(name, _)| *name != &ComponentName::Prompt)
+                    .for_each(|(_, component)| component.unfocus());
+            }
             _ => {}
         }
 
diff --git a/src/components/prompt_window.rs b/src/components/prompt_window.rs
index 57e06a37..9285fe7a 100755
--- a/src/components/prompt_window.rs
+++ b/src/components/prompt_window.rs
@@ -50,6 +50,9 @@ enum Mode {
     /// Usually, when the prompt is replying to a message.
     /// The parameter is the message id of the message that is being replied.
     Reply(i64),
+    /// The search mode of the prompt.
+    /// Used to sort the chat list window based on the search string.
+    SearchChatList,
 }
 /// `InputCell` is a struct that represents a cell of the input.
 /// It is responsible for managing the input cell of the prompt.
@@ -486,11 +489,32 @@ impl Input {
                     self.set_prompt_size_to_one_focused();
                     self.mode = Mode::Normal;
                 }
+                Mode::SearchChatList => {
+                    // NOTE: This should probably change in the future. Currently CoreWindow
+                    // only propagates updates to the other components if they are in focus.
+                    // FocusComponent is sent first to work around this problem.
+                    //
+                    // Ideally CoreWindow should propagate updates regardless of focus, that
+                    // way components can handle their own state by themselves.
+                    self.action_tx
+                        .as_ref()
+                        .unwrap()
+                        .send(Action::FocusComponent(ComponentName::ChatList))
+                        .unwrap();
+                    self.action_tx
+                        .as_ref()
+                        .unwrap()
+                        .send(Action::ChatListSortWithString(self.text_to_string()))
+                        .unwrap();
+                    self.text = vec![vec![]];
+                    self.mode = Mode::Normal;
+                    self.set_prompt_size_to_one_focused();
+                }
             }
         }
     }
     /// Convert the text of the `Input` struct to a string.
-    fn text_to_string(&mut self) -> String {
+    fn text_to_string(&self) -> String {
         // TODO: Parse into markdown
         let mut message = String::new();
         self.text.iter().for_each(|e| {
@@ -793,6 +817,7 @@ impl Component for PromptWindow {
             Action::ReplyMessage(message_id, _) => {
                 self.input.mode = Mode::Reply(message_id);
             }
+            Action::ChatListSearch => self.input.mode = Mode::SearchChatList,
             _ => {}
         }
     }
diff --git a/src/configs/custom/keymap_custom.rs b/src/configs/custom/keymap_custom.rs
index 5aaae4ee..5393adaa 100644
--- a/src/configs/custom/keymap_custom.rs
+++ b/src/configs/custom/keymap_custom.rs
@@ -439,7 +439,7 @@ mod tests {
     fn test_keymap_config_default() {
         let keymap_config = KeymapConfig::default();
         assert_eq!(keymap_config.core_window.len(), 15);
-        assert_eq!(keymap_config.chat_list.len(), 5);
+        assert_eq!(keymap_config.chat_list.len(), 7);
         assert_eq!(keymap_config.chat.len(), 9);
         assert_eq!(keymap_config.prompt.len(), 0);
     }
@@ -541,7 +541,7 @@ mod tests {
         };
         keymap_config = keymap_config.merge(Some(keymap_raw));
         assert_eq!(keymap_config.core_window.len(), 15);
-        assert_eq!(keymap_config.chat_list.len(), 5);
+        assert_eq!(keymap_config.chat_list.len(), 7);
         assert_eq!(keymap_config.chat.len(), 9);
         assert_eq!(keymap_config.prompt.len(), 0);
     }
diff --git a/src/event.rs b/src/event.rs
index f0fd103e..916ccd23 100644
--- a/src/event.rs
+++ b/src/event.rs
@@ -199,9 +199,7 @@ impl Display for Event {
             Event::ReplyMessage(message_id, text) => {
                 write!(f, "ReplyMessage({}, {})", message_id, text)
             }
-            Event::ViewAllMessages => {
-                write!(f, "ViewAllMessages")
-            }
+            Event::ViewAllMessages => write!(f, "ViewAllMessages"),
         }
     }
 }


Reviews:
user: login: FedericoBruzzone
body: After all great job @Mroik  
It was a much requested feature.
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-08T19:41:53Z
user: login: FedericoBruzzone
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-08T19:46:38Z
user: login: Mroik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T19:48:26Z
user: login: Mroik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T19:58:25Z
user: login: FedericoBruzzone
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-08T20:03:13Z
user: login: FedericoBruzzone
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-08T20:12:32Z
user: login: FedericoBruzzone
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-08T20:13:02Z
user: login: Mroik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T20:23:41Z
user: login: Mroik
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T20:57:35Z
user: login: FedericoBruzzone
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-08T21:00:17Z

Review comments:
diff_hunk: @@ -486,11 +489,31 @@ impl Input {
                     self.set_prompt_size_to_one_focused();
                     self.mode = Mode::Normal;
                 }
+                Mode::SearchChatList => {
+                    // Sending a focus first is required for the next action to propagate
+                    // to the chat list window.
+                    // This should probably change in the future, components should change
path: src/components/prompt_window.rs
user: login: FedericoBruzzone
body: Probably it should be better to put the disclaimer as the first or last line in the comment. 
However, if the changes are already known at this point in development  we should say what they are explicitly.
created_at: 2025-04-08T19:28:54Z
author_association: OWNER
diff_hunk: @@ -282,7 +299,40 @@ impl Component for ChatListWindow {
         };
         if let Ok(Some(items)) = self.app_context.tg_context().get_chats_index() {
             self.chat_list = items;
+
+            // Sort before drawing
+            if let Some(s) = self.sort_string.as_ref() {
+                let mut config = nucleo_matcher::Config::DEFAULT;
+                config.prefer_prefix = true;
+                let mut matcher = Matcher::new(config);
+                let s: Vec<char> = s.chars().collect();
+                self.chat_list.sort_by(|a, b| {
+                    let a: Vec<char> = a.chat_name.chars().collect();
+                    let b: Vec<char> = b.chat_name.chars().collect();
+                    let a_score = matcher
+                        .fuzzy_indices(
+                            Utf32Str::Unicode(&a),
+                            Utf32Str::Unicode(&s),
+                            &mut Vec::new(),
+                        )
+                        .unwrap_or(0);
+                    let b_score = matcher
+                        .fuzzy_indices(
+                            Utf32Str::Unicode(&b),
+                            Utf32Str::Unicode(&s),
+                            &mut Vec::new(),
+                        )
+                        .unwrap_or(0);
+                    match (a_score, b_score) {
+                        (a, b) if a < b => Ordering::Less,
+                        (a, b) if a > b => Ordering::Greater,
+                        _ => Ordering::Equal,
+                    }
path: src/components/chat_list_window.rs
user: login: FedericoBruzzone
body: We can write something like
```rust
a_score.cmp(&b_score)
```

Am I wrong?
created_at: 2025-04-08T19:33:35Z
author_association: OWNER
diff_hunk: @@ -242,6 +248,15 @@ impl ChatListWindow {
             }
         }
     }
+
+    fn sort(&mut self, s: String) {
+        // TODO Does not panic and
+        self.sort_string = Some(s);
+    }
+
+    fn default_sort(&mut self) {
path: src/components/chat_list_window.rs
user: login: FedericoBruzzone
body: I don't have a strong opinion about it but, an alternative  name for this funcion could be `default_order` (with a comment that explain what it does).
What you think about it? @Mroik  
created_at: 2025-04-08T19:37:33Z
author_association: OWNER
diff_hunk: @@ -242,6 +248,15 @@ impl ChatListWindow {
             }
         }
     }
+
+    fn sort(&mut self, s: String) {
+        // TODO Does not panic and
path: src/components/chat_list_window.rs
user: login: FedericoBruzzone
body: This comment does not seems relevent written in this way.
It probably could be, but it should be reworded.
created_at: 2025-04-08T19:38:46Z
author_association: OWNER
diff_hunk: @@ -47,6 +47,8 @@ keymap = [
   { keys = ["right"], command = "chat_list_open", description = "Open the selected chat"},
   # Open the selected chat
   { keys = ["enter"], command = "chat_list_open", description = "Open the selected chat"},
+  { keys = ["alt+r"], command = "chat_list_search", description = "Focus on prompt to start searching"},
+  { keys = ["alt+c"], command = "chat_list_restore_sort", description = "Restore the default ordering of the chat list"},
path: config/keymap.toml
user: login: FedericoBruzzone
body: I'm trying this PR locally (on MacOS x84_64) and I'm not able to use these bindings. I pretty sure that the problem is related to MacOS. I'll try to find an additional fix for this before merging this PR.
created_at: 2025-04-08T19:40:48Z
author_association: OWNER
diff_hunk: @@ -47,6 +47,8 @@ keymap = [
   { keys = ["right"], command = "chat_list_open", description = "Open the selected chat"},
   # Open the selected chat
   { keys = ["enter"], command = "chat_list_open", description = "Open the selected chat"},
+  { keys = ["alt+r"], command = "chat_list_search", description = "Focus on prompt to start searching"},
+  { keys = ["alt+c"], command = "chat_list_restore_sort", description = "Restore the default ordering of the chat list"},
path: config/keymap.toml
user: login: FedericoBruzzone
body: Anyway, in order to be consistent with the other bindings, a doc comment for each your binding should be added.
created_at: 2025-04-08T19:46:33Z
author_association: OWNER
diff_hunk: @@ -242,6 +248,15 @@ impl ChatListWindow {
             }
         }
     }
+
+    fn sort(&mut self, s: String) {
+        // TODO Does not panic and
+        self.sort_string = Some(s);
+    }
+
+    fn default_sort(&mut self) {
path: src/components/chat_list_window.rs
user: login: Mroik
body: I'd rather leave it as `default_sort` since the other function is called `sort`, makes looking for related functions easier.
created_at: 2025-04-08T19:48:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -486,11 +489,31 @@ impl Input {
                     self.set_prompt_size_to_one_focused();
                     self.mode = Mode::Normal;
                 }
+                Mode::SearchChatList => {
+                    // Sending a focus first is required for the next action to propagate
+                    // to the chat list window.
+                    // This should probably change in the future, components should change
path: src/components/prompt_window.rs
user: login: Mroik
body: I put it there because it doesn't affect the other modes. It refers to the fact that in `CoreWindow` the updates are propagated to the other components only if they're focused. Personally I dislike this since in some cases state handling of a component is done by the wrapper component.
An example can be seen here: https://github.com/FedericoBruzzone/tgt/blob/0d61aad519116af3a961c8f26ce75cc020dd2b16/src/components/core_window.rs#L210-L220

If the `Action` update were to propagate regardless of focus the components could handle their state themselves.
created_at: 2025-04-08T19:58:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -242,6 +248,15 @@ impl ChatListWindow {
             }
         }
     }
+
+    fn sort(&mut self, s: String) {
+        // TODO Does not panic and
+        self.sort_string = Some(s);
+    }
+
+    fn default_sort(&mut self) {
path: src/components/chat_list_window.rs
user: login: FedericoBruzzone
body: Let's go like this, all right!

created_at: 2025-04-08T20:03:08Z
author_association: OWNER
diff_hunk: @@ -486,11 +489,31 @@ impl Input {
                     self.set_prompt_size_to_one_focused();
                     self.mode = Mode::Normal;
                 }
+                Mode::SearchChatList => {
+                    // Sending a focus first is required for the next action to propagate
+                    // to the chat list window.
+                    // This should probably change in the future, components should change
path: src/components/prompt_window.rs
user: login: FedericoBruzzone
body: This implementation choice is questionable. I was very busy when I had written that code.

Anyway, what I meant is that we could  explain why "This should probably change in the future" and the comment should become something like:
```rust
// NOTE: This should probably change in the future because ...
// Sending a focus first is required ...
```
created_at: 2025-04-08T20:12:32Z
author_association: OWNER
diff_hunk: @@ -282,7 +299,40 @@ impl Component for ChatListWindow {
         };
         if let Ok(Some(items)) = self.app_context.tg_context().get_chats_index() {
             self.chat_list = items;
+
+            // Sort before drawing
+            if let Some(s) = self.sort_string.as_ref() {
+                let mut config = nucleo_matcher::Config::DEFAULT;
+                config.prefer_prefix = true;
+                let mut matcher = Matcher::new(config);
+                let s: Vec<char> = s.chars().collect();
+                self.chat_list.sort_by(|a, b| {
+                    let a: Vec<char> = a.chat_name.chars().collect();
+                    let b: Vec<char> = b.chat_name.chars().collect();
+                    let a_score = matcher
+                        .fuzzy_indices(
+                            Utf32Str::Unicode(&a),
+                            Utf32Str::Unicode(&s),
+                            &mut Vec::new(),
+                        )
+                        .unwrap_or(0);
+                    let b_score = matcher
+                        .fuzzy_indices(
+                            Utf32Str::Unicode(&b),
+                            Utf32Str::Unicode(&s),
+                            &mut Vec::new(),
+                        )
+                        .unwrap_or(0);
+                    match (a_score, b_score) {
+                        (a, b) if a < b => Ordering::Less,
+                        (a, b) if a > b => Ordering::Greater,
+                        _ => Ordering::Equal,
+                    }
path: src/components/chat_list_window.rs
user: login: FedericoBruzzone
body: @Mroik closed unexpectedly?
created_at: 2025-04-08T20:13:02Z
author_association: OWNER
diff_hunk: @@ -486,11 +489,31 @@ impl Input {
                     self.set_prompt_size_to_one_focused();
                     self.mode = Mode::Normal;
                 }
+                Mode::SearchChatList => {
+                    // Sending a focus first is required for the next action to propagate
+                    // to the chat list window.
+                    // This should probably change in the future, components should change
path: src/components/prompt_window.rs
user: login: Mroik
body: Hopefully it is clearer now :pray: 
https://github.com/FedericoBruzzone/tgt/blob/26fef9ab430138f8bd205c4a3590c31e42e0babe/src/components/prompt_window.rs#L493-L498
created_at: 2025-04-08T20:23:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -282,7 +299,40 @@ impl Component for ChatListWindow {
         };
         if let Ok(Some(items)) = self.app_context.tg_context().get_chats_index() {
             self.chat_list = items;
+
+            // Sort before drawing
+            if let Some(s) = self.sort_string.as_ref() {
+                let mut config = nucleo_matcher::Config::DEFAULT;
+                config.prefer_prefix = true;
+                let mut matcher = Matcher::new(config);
+                let s: Vec<char> = s.chars().collect();
+                self.chat_list.sort_by(|a, b| {
+                    let a: Vec<char> = a.chat_name.chars().collect();
+                    let b: Vec<char> = b.chat_name.chars().collect();
+                    let a_score = matcher
+                        .fuzzy_indices(
+                            Utf32Str::Unicode(&a),
+                            Utf32Str::Unicode(&s),
+                            &mut Vec::new(),
+                        )
+                        .unwrap_or(0);
+                    let b_score = matcher
+                        .fuzzy_indices(
+                            Utf32Str::Unicode(&b),
+                            Utf32Str::Unicode(&s),
+                            &mut Vec::new(),
+                        )
+                        .unwrap_or(0);
+                    match (a_score, b_score) {
+                        (a, b) if a < b => Ordering::Less,
+                        (a, b) if a > b => Ordering::Greater,
+                        _ => Ordering::Equal,
+                    }
path: src/components/chat_list_window.rs
user: login: Mroik
body: I resolved it since I applied your suggestion
created_at: 2025-04-08T20:57:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -282,7 +299,40 @@ impl Component for ChatListWindow {
         };
         if let Ok(Some(items)) = self.app_context.tg_context().get_chats_index() {
             self.chat_list = items;
+
+            // Sort before drawing
+            if let Some(s) = self.sort_string.as_ref() {
+                let mut config = nucleo_matcher::Config::DEFAULT;
+                config.prefer_prefix = true;
+                let mut matcher = Matcher::new(config);
+                let s: Vec<char> = s.chars().collect();
+                self.chat_list.sort_by(|a, b| {
+                    let a: Vec<char> = a.chat_name.chars().collect();
+                    let b: Vec<char> = b.chat_name.chars().collect();
+                    let a_score = matcher
+                        .fuzzy_indices(
+                            Utf32Str::Unicode(&a),
+                            Utf32Str::Unicode(&s),
+                            &mut Vec::new(),
+                        )
+                        .unwrap_or(0);
+                    let b_score = matcher
+                        .fuzzy_indices(
+                            Utf32Str::Unicode(&b),
+                            Utf32Str::Unicode(&s),
+                            &mut Vec::new(),
+                        )
+                        .unwrap_or(0);
+                    match (a_score, b_score) {
+                        (a, b) if a < b => Ordering::Less,
+                        (a, b) if a > b => Ordering::Greater,
+                        _ => Ordering::Equal,
+                    }
path: src/components/chat_list_window.rs
user: login: FedericoBruzzone
body: Really sorry, I had missed it
created_at: 2025-04-08T21:00:17Z
author_association: OWNER

PR comments:
user: login: FedericoBruzzone
created_at: 2025-04-08T15:48:41Z
body: Wow! Thanks a lot for adding this highly requested feature! 🤩

Also, thanks for splitting it into separate commits.

I’ll review it later this evening or tomorrow morning at the latest! 
author_association: OWNER
user: login: FedericoBruzzone
created_at: 2025-04-08T16:12:10Z
body: @Mroik clippy seems not to be happy.
Can you make it happy? (Look at any CI)
author_association: OWNER
user: login: Mroik
created_at: 2025-04-08T16:18:43Z
body: I propose to leave out formatting checks and clippy stuff in different workflow. There's no need to check for those in each one. It's also easier to see if it fails due to OS compatibility or if it's just due to coding standards.
author_association: CONTRIBUTOR
user: login: Andreal2000
created_at: 2025-04-08T16:35:53Z
body: @Mroik we didn't separated that checks because some features are enabled in some OS and in other not, so clippy can chack the code for each OS.
And another reason is because clippy needs the code to be built and in this way we avoid useless rebuild.
author_association: COLLABORATOR