From 0bf31b78df1aaa28750893661e21272a606f7626 Mon Sep 17 00:00:00 2001
From: Niloth-p <20315308+Niloth-p@users.noreply.github.com>
Date: Thu, 6 Jun 2024 06:04:27 +0530
Subject: [PATCH 1/3] refactor: ui_tools/helper: Add new enum Search Status.

The existing empty_search boolean property of views with search boxes is
replaced by an enum property search_status that supports the states -
DEFAULT, FILTERED and EMPTY.
This allows tracking whether the results are filtered or not as well,
without introducing a separate boolean property for that.

Updated tests.
---
 tests/ui/test_ui_tools.py       |  5 +++-
 tests/ui_tools/test_boxes.py    |  6 ++---
 zulipterminal/helper.py         |  7 ++++++
 zulipterminal/ui_tools/boxes.py |  6 ++++-
 zulipterminal/ui_tools/views.py | 43 ++++++++++++++++++++++++---------
 5 files changed, 50 insertions(+), 17 deletions(-)

diff --git a/tests/ui/test_ui_tools.py b/tests/ui/test_ui_tools.py
index 0a4d9ec030..17968c1cdc 100644
--- a/tests/ui/test_ui_tools.py
+++ b/tests/ui/test_ui_tools.py
@@ -7,7 +7,7 @@
 
 from zulipterminal.config.keys import keys_for_command, primary_key_for_command
 from zulipterminal.config.symbols import STATUS_ACTIVE
-from zulipterminal.helper import powerset
+from zulipterminal.helper import SearchStatus, powerset
 from zulipterminal.ui_tools.views import (
     SIDE_PANELS_MOUSE_SCROLL_LINES,
     LeftColumnView,
@@ -565,6 +565,7 @@ def test_keypress_CLEAR_SEARCH(self, mocker, stream_view, key, widget_size):
         mocker.patch.object(stream_view, "set_focus")
         mocker.patch(VIEWS + ".urwid.Frame.keypress")
         mocker.patch.object(stream_view.stream_search_box, "reset_search_text")
+        stream_view.search_status = SearchStatus.FILTERED
         stream_view.streams_btn_list = ["FOO", "foo", "fan", "boo", "BOO"]
         stream_view.focus_index_before_search = 3
 
@@ -731,6 +732,7 @@ def test_keypress_CLEAR_SEARCH(self, mocker, topic_view, key, widget_size):
         mocker.patch(VIEWS + ".TopicsView.set_focus")
         mocker.patch(VIEWS + ".urwid.Frame.keypress")
         mocker.patch.object(topic_view.topic_search_box, "reset_search_text")
+        topic_view.search_status = SearchStatus.FILTERED
         topic_view.topics_btn_list = ["FOO", "foo", "fan", "boo", "BOO"]
         topic_view.focus_index_before_search = 3
 
@@ -1112,6 +1114,7 @@ def test_keypress_CLEAR_SEARCH(self, right_col_view, mocker, key, widget_size):
         mocker.patch(VIEWS + ".RightColumnView.set_focus")
         mocker.patch(VIEWS + ".RightColumnView.set_body")
         mocker.patch.object(right_col_view.user_search, "reset_search_text")
+        right_col_view.search_status = SearchStatus.FILTERED
         right_col_view.users_btn_list = []
 
         right_col_view.keypress(size, key)
diff --git a/tests/ui_tools/test_boxes.py b/tests/ui_tools/test_boxes.py
index c4e89a2b58..729efeee8c 100644
--- a/tests/ui_tools/test_boxes.py
+++ b/tests/ui_tools/test_boxes.py
@@ -24,7 +24,7 @@
     STREAM_MARKER_WEB_PUBLIC,
 )
 from zulipterminal.config.ui_mappings import StreamAccessType
-from zulipterminal.helper import Index, MinimalUserData
+from zulipterminal.helper import Index, MinimalUserData, SearchStatus
 from zulipterminal.ui_tools.boxes import (
     MAX_MESSAGE_LENGTH_CONFIRMATION_POPUP,
     PanelSearchBox,
@@ -1903,8 +1903,8 @@ def test_keypress_ENTER(
         size = widget_size(panel_search_box)
         panel_search_box.panel_view.view.controller.is_in_editor_mode = lambda: True
         panel_search_box.panel_view.log = log
-        empty_search = not log
-        panel_search_box.panel_view.empty_search = empty_search
+        search_status = SearchStatus.FILTERED if log else SearchStatus.EMPTY
+        panel_search_box.panel_view.search_status = search_status
         panel_search_box.set_caption("")
         panel_search_box.edit_text = "key words"
 
diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py
index a71d055b74..733e1159fe 100644
--- a/zulipterminal/helper.py
+++ b/zulipterminal/helper.py
@@ -7,6 +7,7 @@
 import time
 from collections import defaultdict
 from contextlib import contextmanager
+from enum import Enum
 from functools import partial, wraps
 from itertools import chain, combinations
 from re import ASCII, MULTILINE, findall, match
@@ -49,6 +50,12 @@
 StreamAccessType = Literal["public", "private", "web-public"]
 
 
+class SearchStatus(Enum):
+    DEFAULT = 0
+    FILTERED = 1
+    EMPTY = 2
+
+
 class StreamData(TypedDict):
     name: str
     id: int
diff --git a/zulipterminal/ui_tools/boxes.py b/zulipterminal/ui_tools/boxes.py
index 9856685336..b2ca3f3f02 100644
--- a/zulipterminal/ui_tools/boxes.py
+++ b/zulipterminal/ui_tools/boxes.py
@@ -36,6 +36,7 @@
 )
 from zulipterminal.config.ui_mappings import STREAM_ACCESS_TYPE
 from zulipterminal.helper import (
+    SearchStatus,
     asynch,
     format_string,
     match_emoji,
@@ -1043,7 +1044,10 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
             # Don't call 'Esc' when inside a popup search-box.
             if not self.panel_view.view.controller.is_any_popup_open():
                 self.panel_view.keypress(size, primary_key_for_command("CLEAR_SEARCH"))
-        elif is_command_key("EXECUTE_SEARCH", key) and not self.panel_view.empty_search:
+        elif (
+            is_command_key("EXECUTE_SEARCH", key)
+            and self.panel_view.search_status != SearchStatus.EMPTY
+        ):
             self.panel_view.view.controller.exit_editor_mode()
             self.set_caption([("filter_results", " Search Results "), " "])
             self.panel_view.set_focus("body")
diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py
index c2034b3ef7..d77ecb7dde 100644
--- a/zulipterminal/ui_tools/views.py
+++ b/zulipterminal/ui_tools/views.py
@@ -36,6 +36,7 @@
 )
 from zulipterminal.config.ui_sizes import LEFT_WIDTH
 from zulipterminal.helper import (
+    SearchStatus,
     TidiedUserInfo,
     asynch,
     match_emoji,
@@ -335,7 +336,7 @@ def __init__(self, streams_btn_list: List[Any], view: Any) -> None:
             ),
         )
         self.search_lock = threading.Lock()
-        self.empty_search = False
+        self.search_status = SearchStatus.DEFAULT
 
     @asynch
     def update_streams(self, search_box: Any, new_text: str) -> None:
@@ -352,7 +353,11 @@ def update_streams(self, search_box: Any, new_text: str) -> None:
             )[0]
 
             streams_display_num = len(streams_display)
-            self.empty_search = streams_display_num == 0
+            self.search_status = (
+                SearchStatus.EMPTY
+                if streams_display_num == 0
+                else SearchStatus.FILTERED
+            )
 
             # Add a divider to separate pinned streams from the rest.
             pinned_stream_names = [
@@ -371,7 +376,7 @@ def update_streams(self, search_box: Any, new_text: str) -> None:
                 streams_display.insert(first_unpinned_index, StreamsViewDivider())
 
             self.log.clear()
-            if not self.empty_search:
+            if self.search_status == SearchStatus.FILTERED:
                 self.log.extend(streams_display)
             else:
                 self.log.extend([self.stream_search_box.search_error])
@@ -404,6 +409,7 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
             self.log.extend(self.streams_btn_list)
             self.set_focus("body")
             self.log.set_focus(self.focus_index_before_search)
+            self.search_status = SearchStatus.DEFAULT
             self.view.controller.update_screen()
             return key
         return super().keypress(size, key)
@@ -436,7 +442,7 @@ def __init__(
             header=self.header_list,
         )
         self.search_lock = threading.Lock()
-        self.empty_search = False
+        self.search_status = SearchStatus.DEFAULT
 
     def _focus_position_for_topic_name(self) -> int:
         saved_topic_state = self.view.saved_topic_in_stream_id(
@@ -461,10 +467,14 @@ def update_topics(self, search_box: Any, new_text: str) -> None:
                 for topic in self.topics_btn_list.copy()
                 if new_text in topic.topic_name.lower()
             ]
-            self.empty_search = len(topics_to_display) == 0
+            self.search_status = (
+                SearchStatus.EMPTY
+                if len(topics_to_display) == 0
+                else SearchStatus.FILTERED
+            )
 
             self.log.clear()
-            if not self.empty_search:
+            if self.search_status == SearchStatus.FILTERED:
                 self.log.extend(topics_to_display)
             else:
                 self.log.extend([self.topic_search_box.search_error])
@@ -524,6 +534,7 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
             self.log.extend(self.topics_btn_list)
             self.set_focus("body")
             self.log.set_focus(self.focus_index_before_search)
+            self.search_status = SearchStatus.DEFAULT
             self.view.controller.update_screen()
             return key
         return super().keypress(size, key)
@@ -665,7 +676,7 @@ def __init__(self, view: Any) -> None:
 
         self.allow_update_user_list = True
         self.search_lock = threading.Lock()
-        self.empty_search = False
+        self.search_status = SearchStatus.DEFAULT
         super().__init__(self.users_view(), header=search_box)
 
     @asynch
@@ -706,10 +717,12 @@ def update_user_list(
             else:
                 users_display = users
 
-            self.empty_search = len(users_display) == 0
+            self.search_status = (
+                SearchStatus.EMPTY if len(users_display) == 0 else SearchStatus.FILTERED
+            )
 
             # FIXME Update log directly?
-            if not self.empty_search:
+            if self.search_status != SearchStatus.EMPTY:
                 self.body = self.users_view(users_display)
             else:
                 self.body = UsersView(
@@ -765,6 +778,7 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
             self.body = UsersView(self.view.controller, self.users_btn_list)
             self.set_body(self.body)
             self.set_focus("body")
+            self.search_status = SearchStatus.DEFAULT
             self.view.controller.update_screen()
             return key
         elif is_command_key("GO_LEFT", key):
@@ -2027,7 +2041,7 @@ def __init__(
         search_box = urwid.Pile(
             [self.emoji_search, urwid.Divider(SECTION_DIVIDER_LINE)]
         )
-        self.empty_search = False
+        self.search_status = SearchStatus.DEFAULT
         self.search_lock = threading.Lock()
         super().__init__(
             controller,
@@ -2073,10 +2087,14 @@ def update_emoji_list(
             else:
                 self.emojis_display = self.emoji_buttons
 
-            self.empty_search = len(self.emojis_display) == 0
+            self.search_status = (
+                SearchStatus.EMPTY
+                if len(self.emojis_display) == 0
+                else SearchStatus.FILTERED
+            )
 
             body_content = self.emojis_display
-            if self.empty_search:
+            if self.search_status == SearchStatus.EMPTY:
                 body_content = [self.emoji_search.search_error]
 
             self.contents["body"] = (
@@ -2150,5 +2168,6 @@ def keypress(self, size: urwid_Size, key: str) -> str:
             self.emoji_search.reset_search_text()
             self.controller.exit_editor_mode()
             self.controller.exit_popup()
+            self.search_status = SearchStatus.DEFAULT
             return key
         return super().keypress(size, key)

From 4f15cacce157beb3765b4d78e701ed06b9867b82 Mon Sep 17 00:00:00 2001
From: Niloth-p <20315308+Niloth-p@users.noreply.github.com>
Date: Thu, 6 Jun 2024 06:22:07 +0530
Subject: [PATCH 2/3] views/boxes: Reset search only after verifying search
 status.

Added conditional checks to ensure that a search was previously
performed before resetting search, using the newly added search_status.
---
 zulipterminal/ui_tools/boxes.py |  5 ++++-
 zulipterminal/ui_tools/views.py | 15 ++++++++++++---
 2 files changed, 16 insertions(+), 4 deletions(-)

diff --git a/zulipterminal/ui_tools/boxes.py b/zulipterminal/ui_tools/boxes.py
index b2ca3f3f02..da0a5acfc7 100644
--- a/zulipterminal/ui_tools/boxes.py
+++ b/zulipterminal/ui_tools/boxes.py
@@ -1042,7 +1042,10 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
             self.reset_search_text()
             self.panel_view.set_focus("body")
             # Don't call 'Esc' when inside a popup search-box.
-            if not self.panel_view.view.controller.is_any_popup_open():
+            if (
+                not self.panel_view.view.controller.is_any_popup_open()
+                and self.panel_view.search_status != SearchStatus.DEFAULT
+            ):
                 self.panel_view.keypress(size, primary_key_for_command("CLEAR_SEARCH"))
         elif (
             is_command_key("EXECUTE_SEARCH", key)
diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py
index d77ecb7dde..6db731e016 100644
--- a/zulipterminal/ui_tools/views.py
+++ b/zulipterminal/ui_tools/views.py
@@ -403,7 +403,10 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
             self.stream_search_box.set_caption(" ")
             self.view.controller.enter_editor_mode_with(self.stream_search_box)
             return key
-        elif is_command_key("CLEAR_SEARCH", key):
+        elif (
+            is_command_key("CLEAR_SEARCH", key)
+            and self.search_status != SearchStatus.DEFAULT
+        ):
             self.stream_search_box.reset_search_text()
             self.log.clear()
             self.log.extend(self.streams_btn_list)
@@ -528,7 +531,10 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
             self.topic_search_box.set_caption(" ")
             self.view.controller.enter_editor_mode_with(self.topic_search_box)
             return key
-        elif is_command_key("CLEAR_SEARCH", key):
+        elif (
+            is_command_key("CLEAR_SEARCH", key)
+            and self.search_status != SearchStatus.DEFAULT
+        ):
             self.topic_search_box.reset_search_text()
             self.log.clear()
             self.log.extend(self.topics_btn_list)
@@ -772,7 +778,10 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
             self.user_search.set_caption(" ")
             self.view.controller.enter_editor_mode_with(self.user_search)
             return key
-        elif is_command_key("CLEAR_SEARCH", key):
+        elif (
+            is_command_key("CLEAR_SEARCH", key)
+            and self.search_status != SearchStatus.DEFAULT
+        ):
             self.user_search.reset_search_text()
             self.allow_update_user_list = True
             self.body = UsersView(self.view.controller, self.users_btn_list)

From 2525a798f4b485d17e0512493ba362cf1cbc1e98 Mon Sep 17 00:00:00 2001
From: Niloth-p <20315308+Niloth-p@users.noreply.github.com>
Date: Thu, 6 Jun 2024 06:53:08 +0530
Subject: [PATCH 3/3] views: Make the ALL_MESSAGES command work globally.

This command worked only when a message was selected, using it as
an anchor to fetch messages.
Now, it has been made consistent with the other narrow commands,
to work from any panel.

This could not be implemented in ui.py along with other narrow commands,
because of the conflict caused by the Esc key also being assigned to
reset search in the side panels (GO_BACK command).

When both operations,
1. reset search in the current panel view
2. narrow to all messages
are possible, pressing `Esc` is set to trigger only
the reset search operation and not the ALL_MESSAGES command.
The next keypress of `Esc` will go to the home view once the
current panel view is restored to its default state.

Fixes #1505.
---
 zulipterminal/ui_tools/views.py | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py
index 6db731e016..776cdaa387 100644
--- a/zulipterminal/ui_tools/views.py
+++ b/zulipterminal/ui_tools/views.py
@@ -415,6 +415,8 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
             self.search_status = SearchStatus.DEFAULT
             self.view.controller.update_screen()
             return key
+        elif is_command_key("ALL_MESSAGES", key):
+            self.view.home_button.activate(key)
         return super().keypress(size, key)
 
 
@@ -543,6 +545,8 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
             self.search_status = SearchStatus.DEFAULT
             self.view.controller.update_screen()
             return key
+        elif is_command_key("ALL_MESSAGES", key):
+            self.view.home_button.activate(key)
         return super().keypress(size, key)
 
 
@@ -790,6 +794,8 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
             self.search_status = SearchStatus.DEFAULT
             self.view.controller.update_screen()
             return key
+        elif is_command_key("ALL_MESSAGES", key):
+            self.view.home_button.activate(key)
         elif is_command_key("GO_LEFT", key):
             self.view.show_right_panel(visible=False)
         return super().keypress(size, key)
@@ -949,6 +955,8 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
             return key
         elif is_command_key("GO_RIGHT", key):
             self.view.show_left_panel(visible=False)
+        elif is_command_key("ALL_MESSAGES", key) and self.get_focus() is self.menu_v:
+            self.view.home_button.activate(key)
         return super().keypress(size, key)