Repository: zulip/zulip-terminal

Issue:
Migrate to subscription update events with `is_muted` property
In [Zulip 2.1.0](https://zulip.com/api/changelog#changes-in-zulip-21), the `is_muted` stream subscription property was added and replaced the `in_home_view` property. But the server had still only been sending [subscription update events](https://zulip.com/api/get-events#subscription-update) for the `in_home_view` property when a user muted/unmuted a subscribed stream.

In https://github.com/zulip/zulip/pull/22695 (API feature level 139), we updated `do_change_subscription_property` so that the server sends a subscription update event for both `is_muted` and `in_home_view` when the subscription setting is updated by the user.

While we have preserved the `in_home_view` event/property for backwards-compatibility, we likely want to delete it once clients have migrated to using the `is_muted` event/property.

[Relevant CZO conversation](https://chat.zulip.org/#narrow/stream/378-api-design/topic/in_home_view.20vs.20is_muted.20.28events.20only.3F.29/near/1411886)

Discussion:
neiljp: @mounilKshah Thanks for following through with this :+1: As per my message in the stream, this looks good and I just made textual changes in the commit, comments and a variable name before pushing back here.

Tests passed, so merging now 🎉 

Patch:
diff --git a/zulipterminal/model.py b/zulipterminal/model.py
index 8fabfb8..36507cc 100644
--- a/zulipterminal/model.py
+++ b/zulipterminal/model.py
@@ -1287,23 +1287,35 @@ class Model:
 
         if event["op"] == "update":
             if hasattr(self.controller, "view"):
-                # NOTE: Currently, the response from the API still contains in_home_view
-                # and not is_muted, hence, the conditional is left unaltered, for now.
-                if event.get("property", None) == "in_home_view":
+                # NOTE: As per ZFL 139, is_muted is supported now, but the server
+                # also sends event with in_home_view to support older versions
+                if (
+                    event.get("property", None) == "in_home_view"
+                    or event.get("property", None) == "is_muted"
+                ):
+                    # Account for in_home_view and is_muted having opposite boolean values
+                    event_stream_muted_value: bool
+                    if event.get("property", None) == "in_home_view":
+                        event_stream_muted_value = not event["value"]
+                    else:
+                        event_stream_muted_value = event["value"]
+
                     stream_id = event["stream_id"]
 
                     # FIXME: Does this always contain the stream_id?
                     stream_button = self.controller.view.stream_id_to_button[stream_id]
 
                     unread_count = self.unread_counts["streams"][stream_id]
-                    if event["value"]:  # Unmuting streams
-                        self.muted_streams.remove(stream_id)
-                        self.unread_counts["all_msg"] += unread_count
-                        stream_button.mark_unmuted(unread_count)
+                    if not event_stream_muted_value:  # Unmuting streams
+                        if stream_id in self.muted_streams:
+                            self.muted_streams.remove(stream_id)
+                            self.unread_counts["all_msg"] += unread_count
+                            stream_button.mark_unmuted(unread_count)
                     else:  # Muting streams
-                        self.muted_streams.add(stream_id)
-                        self.unread_counts["all_msg"] -= unread_count
-                        stream_button.mark_muted()
+                        if stream_id not in self.muted_streams:
+                            self.muted_streams.add(stream_id)
+                            self.unread_counts["all_msg"] -= unread_count
+                            stream_button.mark_muted()
                     self.controller.update_screen()
                 elif event.get("property", None) == "pin_to_top":
                     stream_id = event["stream_id"]


Test patch:
diff --git a/tests/model/test_model.py b/tests/model/test_model.py
index 1e15c11..5ab797e 100644
--- a/tests/model/test_model.py
+++ b/tests/model/test_model.py
@@ -3031,8 +3031,31 @@ class TestModel:
                 },
                 {15, 19, 30},
             ),
+            (
+                {
+                    "property": "is_muted",
+                    "op": "update",
+                    "stream_id": 19,
+                    "value": False,
+                },
+                {15},
+            ),
+            (
+                {
+                    "property": "is_muted",
+                    "op": "update",
+                    "stream_id": 30,
+                    "value": True,
+                },
+                {15, 19, 30},
+            ),
+        ],
+        ids=[
+            "remove_19_in_home_view:ZFLNone",
+            "add_30_in_home_view:ZFLNone",
+            "remove_19_is_muted:ZFL139",
+            "add_30_is_muted:ZFL139",
         ],
-        ids=["remove_19", "add_30"],
     )
     def test__handle_subscription_event_mute_streams(
         self, model, mocker, stream_button, event, final_muted_streams
@@ -3054,7 +3077,10 @@ class TestModel:
         model._handle_subscription_event(event)
 
         assert model.muted_streams == final_muted_streams
-        if event["value"]:
+        # This condition is to check if the stream is unmuted or not
+        if (event["value"] and event["property"] == "in_home_view") or (
+            not event["value"] and event["property"] == "is_muted"
+        ):
             mark_unmuted.assert_called_once_with(99)
             assert model.unread_counts["all_msg"] == 399
         else: