Repository: zulip/zulip-terminal
Pull request #1277 (2022-12-27 08:00:11)

[WIP] Maintain position in topic list when toggling topic list
<!-- Please see https://github.com/zulip/zulip-terminal#contributor-guidelines ! -->

**What does this PR do?**  <!-- Overall description goes here -->
-> This PR is based on the idea that just like a topic is always coupled with a stream, a stream can also be coupled with a particular a topic that was last focussed.
-> it solves the maintaining of focus of the topic list of a stream( say stream 1) from where the t button was pressed.
`api_types`: added new field "curr_topic" to Subscriptions
`buttons`: fill the `curr_topic` with topic button's name when `t` is pressed
`views`: In` TopicsView` when the view is created make the `"curr_topic"` focussed. (This would make the implementation work even if the topic list is rearranged as it depends on topic_name not the pos on the list)
`test_ui_tools`: change `self.view` to be a `MagicMock `object as `Mock `object is not subscriptable.
<!-- If fixing a filed bug or new feature, add 'Fixes #<issue>' or 'Partial fix for #<issue>' -->
https://github.com/zulip/zulip-terminal/issues/617
<!-- Add a link to a discussion on chat.zulip.org, if relevant -->

**Tested?** <!-- Fine to leave some of these unchecked if this is a draft/work-in-progress -->
- [x] Manually
- [x] Existing tests (adapted, if necessary)
- [x] New tests added (for any new behaviour)
- [x] Passed linting & tests (each commit)
<!-- Code must pass CI (GitHub Actions) before merging - look for the green tick! -->

<!-- See https://github.com/zulip/zulip-terminal#commit-style -->
**Commit flow** <!-- if more than one commit; add/delete/fill-in as appropriate -->
<!-- For example:
- first, commit to doing something
- maybe multiple commits doing similar things
-->
3rd and last commit is not related to the issue





Subhasish-Behera: @zulipbot add "PR needs review"
yuktasarode: I see that this issue still needs to be updated after the PR review. Can I pick this up if no one is currently working on this?
Subhasish-Behera: @yuktasarode Thanks for showing interest in ZT. I've made substantial progress and am almost finished. Given the progress I've made, I think it would be best for me to wrap this up, and you could work on a different issue.
Also if you want to work on an issue, commenting on an issue(not the linked PR) is a better thing to do :)
Subhasish-Behera: @zulipbot add "PR needs review"

Patch:
diff --git a/zulipterminal/core.py b/zulipterminal/core.py
index 6ac1ee4..f93d89e 100644
--- a/zulipterminal/core.py
+++ b/zulipterminal/core.py
@@ -619,10 +619,18 @@ class Controller:
     def deregister_client(self) -> None:
         queue_id = self.model.queue_id
         self.client.deregister(queue_id, 1.0)
+        sys.exit(0)
 
     def exit_handler(self, signum: int, frame: Any) -> None:
-        self.deregister_client()
-        sys.exit(0)
+        question = urwid.Text(
+            ("bold", " Please confirm that you wish to exit Zulip-Terminal "),
+            "center",
+        )
+        popup_view = PopUpConfirmationView(
+            self, question, self.deregister_client, location="center"
+        )
+        self.loop.widget = popup_view
+        self.loop.run()
 
     def _raise_exception(self, *args: Any, **kwargs: Any) -> Literal[True]:
         if self._exception_info is not None:
diff --git a/zulipterminal/ui.py b/zulipterminal/ui.py
index a8ffdf3..3507f72 100644
--- a/zulipterminal/ui.py
+++ b/zulipterminal/ui.py
@@ -5,7 +5,7 @@ Defines the `View`, and controls where each component is displayed
 import random
 import re
 import time
-from typing import Any, List, Optional
+from typing import Any, Dict, List, Optional
 
 import urwid
 
@@ -44,12 +44,19 @@ class View(urwid.WidgetWrap):
         self.unpinned_streams = self.model.unpinned_streams
         self.write_box = WriteBox(self)
         self.search_box = MessageSearchBox(self.controller)
+        self.stream_topic_map: Dict[int, str] = {}
 
         self.message_view: Any = None
         self.displaying_selection_hint = False
 
         super().__init__(self.main_window())
 
+    def associate_stream_with_topic(self, stream_id: int, topic_name: str) -> None:
+        self.stream_topic_map[stream_id] = topic_name
+
+    def saved_topic_in_stream_id(self, stream_id: int) -> Optional[str]:
+        return self.stream_topic_map.get(stream_id, None)
+
     def left_column_view(self) -> Any:
         tab = TabView(
             f"{AUTOHIDE_TAB_LEFT_ARROW} STREAMS & TOPICS {AUTOHIDE_TAB_LEFT_ARROW}"
diff --git a/zulipterminal/ui_tools/buttons.py b/zulipterminal/ui_tools/buttons.py
index 3fd8b97..3e3ea24 100644
--- a/zulipterminal/ui_tools/buttons.py
+++ b/zulipterminal/ui_tools/buttons.py
@@ -348,6 +348,7 @@ class TopicButton(TopButton):
     def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
         if is_command_key("TOGGLE_TOPIC", key):
             # Exit topic view
+            self.view.associate_stream_with_topic(self.stream_id, self.topic_name)
             self.view.left_panel.show_stream_view()
         return super().keypress(size, key)
 
diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py
index 1622114..ef59d19 100644
--- a/zulipterminal/ui_tools/views.py
+++ b/zulipterminal/ui_tools/views.py
@@ -428,6 +428,7 @@ class TopicsView(urwid.Frame):
                 urwid.Divider(SECTION_DIVIDER_LINE),
             ]
         )
+        self.list_box.focus_position = self._focus_position_for_topic_name()
         super().__init__(
             self.list_box,
             header=self.header_list,
@@ -435,6 +436,16 @@ class TopicsView(urwid.Frame):
         self.search_lock = threading.Lock()
         self.empty_search = False
 
+    def _focus_position_for_topic_name(self) -> int:
+        saved_topic_state = self.view.saved_topic_in_stream_id(
+            self.stream_button.stream_id
+        )
+        if saved_topic_state is not None:
+            for index, topic in enumerate(self.log):
+                if topic.topic_name == saved_topic_state:
+                    return index
+        return 0
+
     @asynch
     def update_topics(self, search_box: Any, new_text: str) -> None:
         if not self.view.controller.is_in_editor_mode():


Test patch:
diff --git a/tests/ui/test_ui_tools.py b/tests/ui/test_ui_tools.py
index 324ee04..18d2ef2 100644
--- a/tests/ui/test_ui_tools.py
+++ b/tests/ui/test_ui_tools.py
@@ -1,6 +1,8 @@
 from collections import OrderedDict
 
 import pytest
+import urwid
+from pytest import param as case
 from urwid import Divider
 
 from zulipterminal.config.keys import keys_for_command, primary_key_for_command
@@ -614,6 +616,33 @@ class TestTopicsView:
             ]
         )
 
+    @pytest.mark.parametrize(
+        "stream_id, saved_topic_state, expected_focus_index",
+        [
+            case(1, None, 0, id="initial_condition_no_topic_is_stored"),
+            case(2, "Topic 3", 2, id="topic_is_stored_and_present_in_topic_list"),
+            case(3, "Topic 4", 0, id="topic_is_stored_but_not_present_in_topic_list"),
+        ],
+    )
+    def test__focus_position_for_topic_name(
+        self, mocker, stream_id, saved_topic_state, topic_view, expected_focus_index
+    ):
+        topic_view.stream_button.stream_id = stream_id
+        topic_view.list_box = mocker.MagicMock(spec=urwid.ListBox)
+        topic_view.list_box.body = [
+            mocker.Mock(topic_name="Topic 1"),
+            mocker.Mock(topic_name="Topic 2"),
+            mocker.Mock(topic_name="Topic 3"),
+        ]
+        topic_view.log = urwid.SimpleFocusListWalker(topic_view.list_box.body)
+        mocker.patch.object(
+            topic_view.view, "saved_topic_in_stream_id", return_value=saved_topic_state
+        )
+
+        new_focus_index = topic_view._focus_position_for_topic_name()
+
+        assert new_focus_index == expected_focus_index
+
     @pytest.mark.parametrize(
         "new_text, expected_log",
         [