Repository: zulip/zulip-terminal
Pull request #1481 (2024-04-14 10:17:58)

Use a common scroll prompt in popup headers
### What does this PR do, and why?
Uses a constant to store the scroll prompt, and reuses in all popup headers.

### How did you test this?
- [x] Manually - Visual changes


Patch:
diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py
index 119770e..b341993 100644
--- a/zulipterminal/api_types.py
+++ b/zulipterminal/api_types.py
@@ -34,6 +34,14 @@ MAX_TOPIC_NAME_LENGTH: Final = 60
 MAX_MESSAGE_LENGTH: Final = 10000
 
 
+###############################################################################
+# These values are in the register response from ZFL 164
+# Before this feature level, they had the listed default (fixed) values
+
+PRESENCE_OFFLINE_THRESHOLD_SECS: Final = 140
+PRESENCE_PING_INTERVAL_SECS: Final = 60
+
+
 ###############################################################################
 # Core message types (used in Composition and Message below)
 
diff --git a/zulipterminal/core.py b/zulipterminal/core.py
index 1e11354..99d6cac 100644
--- a/zulipterminal/core.py
+++ b/zulipterminal/core.py
@@ -51,6 +51,8 @@ from zulipterminal.version import ZT_VERSION
 
 ExceptionInfo = Tuple[Type[BaseException], BaseException, TracebackType]
 
+SCROLL_PROMPT = "(up/down scrolls)"
+
 
 class Controller:
     """
@@ -246,11 +248,11 @@ class Controller:
         self.loop.widget = self.view
 
     def show_help(self) -> None:
-        help_view = HelpView(self, "Help Menu (up/down scrolls)")
+        help_view = HelpView(self, f"Help Menu {SCROLL_PROMPT}")
         self.show_pop_up(help_view, "area:help")
 
     def show_markdown_help(self) -> None:
-        markdown_view = MarkdownHelpView(self, "Markdown Help Menu (up/down scrolls)")
+        markdown_view = MarkdownHelpView(self, f"Markdown Help Menu {SCROLL_PROMPT}")
         self.show_pop_up(markdown_view, "area:help")
 
     def show_topic_edit_mode(self, button: Any) -> None:
@@ -266,7 +268,7 @@ class Controller:
         msg_info_view = MsgInfoView(
             self,
             msg,
-            "Message Information (up/down scrolls)",
+            f"Message Information {SCROLL_PROMPT}",
             topic_links,
             message_links,
             time_mentions,
@@ -315,7 +317,10 @@ class Controller:
     def show_user_info(self, user_id: int) -> None:
         self.show_pop_up(
             UserInfoView(
-                self, user_id, "User Information (up/down scrolls)", "USER_INFO"
+                self,
+                user_id,
+                f"User Information {SCROLL_PROMPT}",
+                "USER_INFO",
             ),
             "area:user",
         )
@@ -325,7 +330,7 @@ class Controller:
             UserInfoView(
                 self,
                 user_id,
-                "Message Sender Information (up/down scrolls)",
+                f"Message Sender Information {SCROLL_PROMPT}",
                 "MSG_SENDER_INFO",
             ),
             "area:user",
@@ -345,7 +350,7 @@ class Controller:
                 topic_links,
                 message_links,
                 time_mentions,
-                "Full rendered message (up/down scrolls)",
+                f"Full rendered message {SCROLL_PROMPT}",
             ),
             "area:msg",
         )
@@ -364,7 +369,7 @@ class Controller:
                 topic_links,
                 message_links,
                 time_mentions,
-                "Full raw message (up/down scrolls)",
+                f"Full raw message {SCROLL_PROMPT}",
             ),
             "area:msg",
         )
@@ -383,7 +388,7 @@ class Controller:
                 topic_links,
                 message_links,
                 time_mentions,
-                "Edit History (up/down scrolls)",
+                f"Edit History {SCROLL_PROMPT}",
             ),
             "area:msg",
         )
diff --git a/zulipterminal/model.py b/zulipterminal/model.py
index af0b846..72215c8 100644
--- a/zulipterminal/model.py
+++ b/zulipterminal/model.py
@@ -34,6 +34,8 @@ from zulipterminal.api_types import (
     MAX_MESSAGE_LENGTH,
     MAX_STREAM_NAME_LENGTH,
     MAX_TOPIC_NAME_LENGTH,
+    PRESENCE_OFFLINE_THRESHOLD_SECS,
+    PRESENCE_PING_INTERVAL_SECS,
     TYPING_STARTED_EXPIRY_PERIOD,
     TYPING_STARTED_WAIT_PERIOD,
     TYPING_STOPPED_WAIT_PERIOD,
@@ -81,9 +83,6 @@ from zulipterminal.platform_code import notify
 from zulipterminal.ui_tools.utils import create_msg_box_list
 
 
-OFFLINE_THRESHOLD_SECS = 140
-
-
 class ServerConnectionFailure(Exception):
     pass
 
@@ -172,6 +171,8 @@ class Model:
         self.server_version = self.initial_data["zulip_version"]
         self.server_feature_level: int = self.initial_data.get("zulip_feature_level", 0)
 
+        self._store_server_presence_intervals()
+
         self.user_dict: Dict[str, MinimalUserData] = {}
         self.user_id_email_dict: Dict[int, str] = {}
         self._all_users_by_id: Dict[int, RealmUser] = {}
@@ -445,7 +446,7 @@ class Model:
                     view = self.controller.view
                     view.users_view.update_user_list(user_list=self.users)
                     view.middle_column.update_message_list_status_markers()
-            time.sleep(60)
+            time.sleep(self.server_presence_ping_interval_secs)
 
     @asynch
     def toggle_message_reaction(
@@ -814,6 +815,18 @@ class Model:
             TYPING_STARTED_EXPIRY_PERIOD,
         )
 
+    def _store_server_presence_intervals(self) -> None:
+        """
+        In ZFL 164, these values were added to the register response.
+        Uses default values if not received.
+        """
+        self.server_presence_offline_threshold_secs = self.initial_data.get(
+            "server_presence_offline_threshold_seconds", PRESENCE_OFFLINE_THRESHOLD_SECS
+        )
+        self.server_presence_ping_interval_secs = self.initial_data.get(
+            "server_presence_ping_interval_seconds", PRESENCE_PING_INTERVAL_SECS
+        )
+
     @staticmethod
     def modernize_message_response(message: Message) -> Message:
         """
@@ -1202,7 +1215,7 @@ class Model:
                 *
                 * Out of the ClientPresence objects found in `presence`, we
                 * consider only those with a timestamp newer than
-                * OFFLINE_THRESHOLD_SECS; then of
+                * self.server_presence_offline_threshold_secs; then of
                 * those, return the one that has the greatest UserStatus, where
                 * `active` > `idle` > `offline`.
                 *
@@ -1216,7 +1229,9 @@ class Model:
                     timestamp = client[1]["timestamp"]
                     if client_name == "aggregated":
                         continue
-                    elif (time.time() - timestamp) < OFFLINE_THRESHOLD_SECS:
+                    elif (
+                        time.time() - timestamp
+                    ) < self.server_presence_offline_threshold_secs:
                         if status == "active":
                             aggregate_status = "active"
                         if status == "idle" and aggregate_status != "active":


Test patch:
diff --git a/tests/model/test_model.py b/tests/model/test_model.py
index 83ce9de..6b5a18e 100644
--- a/tests/model/test_model.py
+++ b/tests/model/test_model.py
@@ -14,6 +14,8 @@ from zulipterminal.model import (
     MAX_MESSAGE_LENGTH,
     MAX_STREAM_NAME_LENGTH,
     MAX_TOPIC_NAME_LENGTH,
+    PRESENCE_OFFLINE_THRESHOLD_SECS,
+    PRESENCE_PING_INTERVAL_SECS,
     TYPING_STARTED_EXPIRY_PERIOD,
     TYPING_STARTED_WAIT_PERIOD,
     TYPING_STOPPED_WAIT_PERIOD,
@@ -1440,6 +1442,60 @@ class TestModel:
         assert model.typing_stopped_wait_period == typing_stopped_wait
         assert model.typing_started_expiry_period == typing_started_expiry
 
+    @pytest.mark.parametrize(
+        "feature_level, to_vary_in_initial_data, "
+        "expected_offline_threshold, expected_presence_ping_interval",
+        [
+            (0, {}, PRESENCE_OFFLINE_THRESHOLD_SECS, PRESENCE_PING_INTERVAL_SECS),
+            (157, {}, PRESENCE_OFFLINE_THRESHOLD_SECS, PRESENCE_PING_INTERVAL_SECS),
+            (
+                164,
+                {
+                    "server_presence_offline_threshold_seconds": 200,
+                    "server_presence_ping_interval_seconds": 100,
+                },
+                200,
+                100,
+            ),
+        ],
+        ids=[
+            "Zulip_2.1_ZFL_0_hard_coded",
+            "Zulip_6.2_ZFL_157_hard_coded",
+            "Zulip_7.0_ZFL_164_server_provided",
+        ],
+    )
+    def test__store_server_presence_intervals(
+        self,
+        model,
+        initial_data,
+        feature_level,
+        to_vary_in_initial_data,
+        expected_offline_threshold,
+        expected_presence_ping_interval,
+    ):
+        # Ensure inputs are not the defaults, to avoid the test accidentally passing
+        assert (
+            to_vary_in_initial_data.get("server_presence_offline_threshold_seconds")
+            != PRESENCE_OFFLINE_THRESHOLD_SECS
+        )
+        assert (
+            to_vary_in_initial_data.get("server_presence_ping_interval_seconds")
+            != PRESENCE_PING_INTERVAL_SECS
+        )
+
+        initial_data.update(to_vary_in_initial_data)
+        model.initial_data = initial_data
+        model.server_feature_level = feature_level
+
+        model._store_server_presence_intervals()
+
+        assert (
+            model.server_presence_offline_threshold_secs == expected_offline_threshold
+        )
+        assert (
+            model.server_presence_ping_interval_secs == expected_presence_ping_interval
+        )
+
     def test_get_message_false_first_anchor(
         self,
         mocker,