Repository: zulip/zulip-terminal (python)

Issue:
Respect server typing notification duration settings
Prior to Feature level 204 (Zulip 8.0) typing notification values were expected to be fixed values, as listed in `api_types.py`.

From this feature level (https://zulip.com/api/changelog), the server expects clients to use various `server_typing_*_milliseconds` values returned from `register` instead.

Note that we need to fall back to the fixed values for earlier server versions, so those versions should be retained in the current source file, and tests should ensure that the values are correct.

Code which uses these values should not directly access these values, but now query the model instead.

Pull request description:
Feat: Respect Server Typing Duration Settings & Tests
<!-- See README for documentation, or ask in #zulip-terminal if unclear -->
### What does this PR do, and why?
As introduced in ZFL 204/Zulip 8.0, the typing duration values should no longer be hardcoded into the clients, and the server will provide those values. In case the values are not present, the previous defaults are used. I have also added tests to check for this behaviour.

### Outstanding aspect(s)    <!-- DELETE SECTION IF EMPTY -->
<!-- In what ways is this not fully implemented/functioning? Compared to a discussion/issue? -->
<!-- Do you not understand something? Are you unsure about a certain approach? Want feedback? -->
- [ ] 

### External discussion & connections
<!-- [x] all that apply, specifying topic and adding numbers after # for issues/PRs -->
- [x] Discussed in **#zulip-terminal** in `Respect server typing notification settings #T1448 #T1445`
- [x] Fully fixes #1445
- [ ] Partially fixes issue #
- [ ] Builds upon previous unmerged work in PR #
- [ ] Is a follow-up to work in PR #
- [ ] Requires merge of PR #
- [ ] Merge will enable work on #

### How did you test this?
<!-- [x] all that apply -->
- [ ] Manually - Behavioral changes
- [ ] Manually - Visual changes
- [x] Adapting existing automated tests
- [x] Adding automated tests for new behavior (or missing tests)
- [ ] Existing automated tests should already cover this (*only a refactor of tested code*)

### Self-review checklist for each commit
- [x] It is a [minimal coherent idea](https://github.com/zulip/zulip-terminal#structuring-commits---speeding-up-reviews-merging--development)
- [x] It has a commit summary following the [documented style](https://github.com/zulip/zulip-terminal#structuring-commits---speeding-up-reviews-merging--development) (title & body)
- [x] It has a commit summary describing the  motivation and reasoning for the change
- [x] It individually passes linting and tests
- [x] It contains test additions for any new behavior
- [x] It flows clearly from a previous branch commit, and/or prepares for the next commit


<!-- NOTE: Attached videos/images will be clearer from smaller terminal windows -->
<img width="935" alt="image" src="https://github.com/zulip/zulip-terminal/assets/40538506/db6166f6-bc54-4b1b-a0c1-d0ae8b452f5e">


Patch:
diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py
index 8acf4d0..9bf3b5b 100644
--- a/zulipterminal/api_types.py
+++ b/zulipterminal/api_types.py
@@ -50,10 +50,12 @@ MessageType = Union[DirectMessageString, StreamMessageString]
 #
 # NOTE: `to` field could be email until ZFL 11/3.0; ids were possible from 2.0+
 
-# Timing parameters for when notifications should occur
-TYPING_STARTED_WAIT_PERIOD: Final = 10
-TYPING_STOPPED_WAIT_PERIOD: Final = 5
-TYPING_STARTED_EXPIRY_PERIOD: Final = 15  # TODO: Needs implementation in ZT
+# In ZFL 204, these values were made server-configurable
+# Before this feature level, these values were fixed as follows:
+# Timing parameters for when notifications should occur (in milliseconds)
+TYPING_STARTED_WAIT_PERIOD: Final = 10000
+TYPING_STOPPED_WAIT_PERIOD: Final = 5000
+TYPING_STARTED_EXPIRY_PERIOD: Final = 15000  # TODO: Needs implementation in ZT
 
 TypingStatusChange = Literal["start", "stop"]
 
diff --git a/zulipterminal/model.py b/zulipterminal/model.py
index 9c6fce6..22988c6 100644
--- a/zulipterminal/model.py
+++ b/zulipterminal/model.py
@@ -34,6 +34,9 @@ from zulipterminal.api_types import (
     MAX_MESSAGE_LENGTH,
     MAX_STREAM_NAME_LENGTH,
     MAX_TOPIC_NAME_LENGTH,
+    TYPING_STARTED_EXPIRY_PERIOD,
+    TYPING_STARTED_WAIT_PERIOD,
+    TYPING_STOPPED_WAIT_PERIOD,
     Composition,
     CustomFieldValue,
     DirectTypingNotification,
@@ -214,6 +217,7 @@ class Model:
         self._draft: Optional[Composition] = None
 
         self._store_content_length_restrictions()
+        self._store_typing_duration_settings()
 
         self.active_emoji_data, self.all_emoji_names = self.generate_all_emoji_data(
             self.initial_data["realm_emoji"]
@@ -791,6 +795,25 @@ class Model:
             "max_message_length", MAX_MESSAGE_LENGTH
         )
 
+    def _store_typing_duration_settings(self) -> None:
+        """
+        Store typing duration fields in model.
+        In ZFL 204, these values were made server-configurable.
+        Uses default values if not received from server.
+        """
+        self.typing_started_wait_period = self.initial_data.get(
+            "server_typing_started_wait_period_milliseconds",
+            TYPING_STARTED_WAIT_PERIOD,
+        )
+        self.typing_stopped_wait_period = self.initial_data.get(
+            "server_typing_stopped_wait_period_milliseconds",
+            TYPING_STOPPED_WAIT_PERIOD,
+        )
+        self.typing_started_expiry_period = self.initial_data.get(
+            "server_typing_started_expiry_period_milliseconds",
+            TYPING_STARTED_EXPIRY_PERIOD,
+        )
+
     @staticmethod
     def modernize_message_response(message: Message) -> Message:
         """
diff --git a/zulipterminal/ui_tools/boxes.py b/zulipterminal/ui_tools/boxes.py
index db5f396..6867d5a 100644
--- a/zulipterminal/ui_tools/boxes.py
+++ b/zulipterminal/ui_tools/boxes.py
@@ -13,13 +13,7 @@ import urwid
 from typing_extensions import Literal
 from urwid_readline import ReadlineEdit
 
-from zulipterminal.api_types import (
-    TYPING_STARTED_WAIT_PERIOD,
-    TYPING_STOPPED_WAIT_PERIOD,
-    Composition,
-    PrivateComposition,
-    StreamComposition,
-)
+from zulipterminal.api_types import Composition, PrivateComposition, StreamComposition
 from zulipterminal.config.keys import (
     is_command_key,
     keys_for_command,
@@ -237,8 +231,12 @@ class WriteBox(urwid.Pile):
         ]
         self.focus_position = self.FOCUS_CONTAINER_MESSAGE
 
-        start_period_delta = timedelta(seconds=TYPING_STARTED_WAIT_PERIOD)
-        stop_period_delta = timedelta(seconds=TYPING_STOPPED_WAIT_PERIOD)
+        start_period_delta = timedelta(
+            milliseconds=self.model.typing_started_wait_period
+        )
+        stop_period_delta = timedelta(
+            milliseconds=self.model.typing_stopped_wait_period
+        )
 
         def on_type_send_status(edit: object, new_edit_text: str) -> None:
             if new_edit_text and self.typing_recipient_user_ids:


Test patch:
diff --git a/tests/model/test_model.py b/tests/model/test_model.py
index 915621f..51c71eb 100644
--- a/tests/model/test_model.py
+++ b/tests/model/test_model.py
@@ -14,6 +14,9 @@ from zulipterminal.model import (
     MAX_MESSAGE_LENGTH,
     MAX_STREAM_NAME_LENGTH,
     MAX_TOPIC_NAME_LENGTH,
+    TYPING_STARTED_EXPIRY_PERIOD,
+    TYPING_STARTED_WAIT_PERIOD,
+    TYPING_STOPPED_WAIT_PERIOD,
     Model,
     ServerConnectionFailure,
     UserSettings,
@@ -1391,6 +1394,45 @@ class TestModel:
             assert model.max_topic_length == MAX_TOPIC_NAME_LENGTH
             assert model.max_message_length == MAX_MESSAGE_LENGTH
 
+    def test__store_typing_duration_settings__default_values(self, model, initial_data):
+        model.initial_data = initial_data
+
+        model._store_typing_duration_settings()
+
+        assert model.typing_started_wait_period == TYPING_STARTED_WAIT_PERIOD
+        assert model.typing_stopped_wait_period == TYPING_STOPPED_WAIT_PERIOD
+        assert model.typing_started_expiry_period == TYPING_STARTED_EXPIRY_PERIOD
+
+    def test__store_typing_duration_settings__with_values(
+        self,
+        model,
+        initial_data,
+        feature_level=204,
+        typing_started_wait=7500,
+        typing_stopped_wait=3000,
+        typing_started_expiry=10000,
+    ):
+        # Ensure inputs are not the defaults, to avoid the test accidentally passing
+        assert typing_started_wait != TYPING_STARTED_WAIT_PERIOD
+        assert typing_stopped_wait != TYPING_STOPPED_WAIT_PERIOD
+        assert typing_started_expiry != TYPING_STARTED_EXPIRY_PERIOD
+
+        to_vary_in_initial_data = {
+            "server_typing_started_wait_period_milliseconds": typing_started_wait,
+            "server_typing_stopped_wait_period_milliseconds": typing_stopped_wait,
+            "server_typing_started_expiry_period_milliseconds": typing_started_expiry,
+        }
+
+        initial_data.update(to_vary_in_initial_data)
+        model.initial_data = initial_data
+        model.server_feature_level = feature_level
+
+        model._store_typing_duration_settings()
+
+        assert model.typing_started_wait_period == typing_started_wait
+        assert model.typing_stopped_wait_period == typing_stopped_wait
+        assert model.typing_started_expiry_period == typing_started_expiry
+
     def test_get_message_false_first_anchor(
         self,
         mocker,
diff --git a/tests/ui_tools/test_boxes.py b/tests/ui_tools/test_boxes.py
index 869cc98..dbdd082 100644
--- a/tests/ui_tools/test_boxes.py
+++ b/tests/ui_tools/test_boxes.py
@@ -7,6 +7,11 @@ from pytest import param as case
 from pytest_mock import MockerFixture
 from urwid import Widget
 
+from zulipterminal.api_types import (
+    TYPING_STARTED_EXPIRY_PERIOD,
+    TYPING_STARTED_WAIT_PERIOD,
+    TYPING_STOPPED_WAIT_PERIOD,
+)
 from zulipterminal.config.keys import keys_for_command, primary_key_for_command
 from zulipterminal.config.symbols import (
     INVALID_MARKER,
@@ -50,6 +55,9 @@ class TestWriteBox:
         write_box.model.max_stream_name_length = 60
         write_box.model.max_topic_length = 60
         write_box.model.max_message_length = 10000
+        write_box.model.typing_started_wait_period = TYPING_STARTED_WAIT_PERIOD
+        write_box.model.typing_stopped_wait_period = TYPING_STOPPED_WAIT_PERIOD
+        write_box.model.typing_started_expiry_period = TYPING_STARTED_EXPIRY_PERIOD
         write_box.model.user_group_names = [
             groups["name"] for groups in user_groups_fixture
         ]