From 68ab15e75ca5a7d60210e7058555ed343a21447b Mon Sep 17 00:00:00 2001 From: Sashank Ravipati Date: Sun, 30 Jul 2023 13:23:41 +0530 Subject: [PATCH 1/5] model: Add muted_users to intial_data_to_fetch. The muted users are stored in muted_users list of Model class. Tests adapted. Co-authored by: Subhasish-Behera --- tests/conftest.py | 1 + tests/model/test_model.py | 31 +++++++++++++++++++++++++++++++ zulipterminal/model.py | 9 +++++++++ 3 files changed, 41 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index ad92c4cc71..8d3e784124 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -903,6 +903,7 @@ def initial_data( } ], "result": "success", + "muted_users": {}, "queue_id": "1522420755:786", "realm_users": users_fixture, "cross_realm_bots": [ diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 119b3aecab..82005fe8f6 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -190,6 +190,36 @@ def test_init_muted_topics( assert model._muted_topics == locally_processed_data + @pytest.mark.parametrize( + "server_response, ids, zulip_feature_level", + [ + ( + [ + {"id": 32323, "timestamp": 1726810359}, + {"id": 37372, "timestamp": 214214214}, + ], + {32323, 37372}, + 48, + ), + ([], set(), 0), + ], + ids=[ + "zulip_feature_level:48", + "zulip_feature_level:0", + ], + ) + def test_init_muted_users( + self, mocker, initial_data, server_response, ids, zulip_feature_level + ): + mocker.patch(MODEL + ".get_messages", return_value="") + initial_data["zulip_feature_level"] = zulip_feature_level + initial_data["muted_users"] = server_response + self.client.register = mocker.Mock(return_value=initial_data) + + model = Model(self.controller) + + assert model._muted_users == ids + def test_init_InvalidAPIKey_response(self, mocker, initial_data): # Both network calls indicate the same response mocker.patch(MODEL + ".get_messages", return_value="Invalid API key") @@ -262,6 +292,7 @@ def test_register_initial_desired_events(self, mocker, initial_data): "realm_emoji", "custom_profile_fields", "zulip_version", + "muted_users", ] model.client.register.assert_called_once_with( event_types=event_types, diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 1d7688cdeb..e80b77dcae 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -145,6 +145,7 @@ def __init__(self, controller: Any) -> None: # zulip_version and zulip_feature_level are always returned in # POST /register from Feature level 3. "zulip_version", + "muted_users", ] # Events desired with their corresponding callback @@ -208,6 +209,11 @@ def __init__(self, controller: Any) -> None: ) for stream_name, topic, *date_muted in muted_topics } + # NOTE: muted_users also contains timestamps, but we only store the user IDs + # muted_users was added in ZFL 48, Zulip 4.0 + self._muted_users: Set[int] = set() + if self.server_feature_level >= 48: + self._update_muted_users(self.initial_data["muted_users"]) groups = self.initial_data["realm_user_groups"] self.user_group_by_id: Dict[int, Dict[str, Any]] = {} @@ -1204,6 +1210,9 @@ def get_user_info(self, user_id: int) -> Optional[TidiedUserInfo]: return user_info + def _update_muted_users(self, muted_users: List[Dict[int, int]]) -> None: + self._muted_users = {muted_user["id"] for muted_user in muted_users} + def _update_users_data_from_initial_data(self) -> None: # Dict which stores the active/idle status of users (by email) presences = self.initial_data["presences"] From 6f1fcce307c83204d262191caf5d90807a579002 Mon Sep 17 00:00:00 2001 From: Sashank Ravipati Date: Wed, 9 Aug 2023 23:21:14 +0530 Subject: [PATCH 2/5] model/api_types: Handle muted_user event. Update the muted_user list after a muted_user event. Add MutedUserEvent to api_types. Co-authored by: Subhasish-Behera --- tests/model/test_model.py | 1 + zulipterminal/api_types.py | 11 +++++++++++ zulipterminal/model.py | 8 ++++++++ 3 files changed, 20 insertions(+) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 82005fe8f6..0bb426d4de 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -275,6 +275,7 @@ def test_register_initial_desired_events(self, mocker, initial_data): "user_settings", "realm_emoji", "realm_user", + "muted_users", ] fetch_event_types = [ "realm", diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index 319dc4733a..b9684684a1 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -613,6 +613,16 @@ class UpdateDisplaySettingsEvent(TypedDict): setting: bool +class MutedUser(TypedDict): + user_id: int + timestamp: int + + +class MutedUserEvent(TypedDict): + type: Literal["muted_users"] + muted_users: List[MutedUser] + + # ----------------------------------------------------------------------------- Event = Union[ MessageEvent, @@ -628,6 +638,7 @@ class UpdateDisplaySettingsEvent(TypedDict): UpdateUserSettingsEvent, UpdateGlobalNotificationsEvent, RealmUserEvent, + MutedUserEvent, ] ############################################################################### diff --git a/zulipterminal/model.py b/zulipterminal/model.py index e80b77dcae..1b64451025 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -161,6 +161,7 @@ def __init__(self, controller: Any) -> None: "user_settings": self._handle_user_settings_event, "realm_emoji": self._handle_update_emoji_event, "realm_user": self._handle_realm_user_event, + "muted_users": self._handle_muted_users_event, } self.initial_data: Dict[str, Any] = {} @@ -1675,6 +1676,13 @@ def notify_user(self, message: Message) -> str: ) return "" + def _handle_muted_users_event(self, event: Event) -> None: + """ + Handle muting/unmuting of users + """ + assert event["type"] == "muted_users" + self._update_muted_users(event["muted_users"]) + def _handle_message_event(self, event: Event) -> None: """ Handle new messages (eg. add message to the end of the view) From a7d0d10a9249e24562cdc8edfa754b745637dae4 Mon Sep 17 00:00:00 2001 From: rsashank Date: Fri, 20 Sep 2024 14:28:24 +0530 Subject: [PATCH 3/5] model: Add is_user_muted method. Test added. --- tests/model/test_model.py | 11 +++++++++++ zulipterminal/model.py | 3 +++ 2 files changed, 14 insertions(+) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 0bb426d4de..e2aed5dd4c 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -4112,6 +4112,17 @@ def test_is_muted_topic( assert return_value == is_muted + @pytest.mark.parametrize( + "user, is_muted", + [ + case(1, False, id="unmuted_user"), + case(2, True, id="muted_user"), + ], + ) + def test_is_muted_user(self, user, is_muted, model): + model._muted_users = {2} + assert model.is_muted_user(user) == is_muted + @pytest.mark.parametrize( "unread_topics, current_topic, next_unread_topic", [ diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 1b64451025..230ccce2ca 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -957,6 +957,9 @@ def is_muted_topic(self, stream_id: int, topic: str) -> bool: topic_to_search = (stream_name, topic) return topic_to_search in self._muted_topics + def is_muted_user(self, user_id: int) -> bool: + return user_id in self._muted_users + def stream_topic_from_message_id( self, message_id: int ) -> Optional[Tuple[int, str]]: From 3937ee2686b12c9f4a4b6eea5b1cbddfbabcd4a8 Mon Sep 17 00:00:00 2001 From: rsashank Date: Fri, 20 Sep 2024 16:34:22 +0530 Subject: [PATCH 4/5] messages: Changes made in MessageBox for muted_messages. The content shown for the message is changed. The recepient header shows muted_user instead of original name. The name of the author is also changed to muted_user. Tests Adapted. Co-authored by: Subhasish-Behera --- tests/core/test_core.py | 5 +++++ tests/ui_tools/test_messages.py | 6 ++++++ zulipterminal/ui_tools/messages.py | 17 +++++++++++++++-- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/core/test_core.py b/tests/core/test_core.py index ff6701a41d..0fd1023bb9 100644 --- a/tests/core/test_core.py +++ b/tests/core/test_core.py @@ -229,6 +229,7 @@ def test_narrow_to_user( controller.view.message_view = mocker.patch("urwid.ListBox") controller.model.user_id = 5140 controller.model.user_email = "some@email" + controller.model._muted_users = set() controller.model.user_dict = { user_email: { "user_id": user_id, @@ -267,6 +268,7 @@ def test_narrow_to_all_messages( controller.view.message_view = mocker.patch("urwid.ListBox") controller.model.user_email = "some@email" controller.model.user_id = 1 + controller.model._muted_users = set() controller.model.stream_dict = { 205: { "color": "#ffffff", @@ -295,6 +297,7 @@ def test_narrow_to_all_pm( controller.view.message_view = mocker.patch("urwid.ListBox") controller.model.user_id = 1 controller.model.user_email = "some@email" + controller.model._muted_users = set() controller.narrow_to_all_pm() # FIXME: Add id narrowing test @@ -316,6 +319,7 @@ def test_narrow_to_all_starred( # FIXME: Expand upon is_muted_topic(). mocker.patch(MODEL + ".is_muted_topic", return_value=False) controller.model.user_email = "some@email" + controller.model._muted_users = set() controller.model.stream_dict = { 205: { "color": "#ffffff", @@ -343,6 +347,7 @@ def test_narrow_to_all_mentions( mocker.patch(MODEL + ".is_muted_topic", return_value=False) controller.model.user_email = "some@email" controller.model.user_id = 1 + controller.model._muted_users = set() controller.model.stream_dict = { 205: { "color": "#ffffff", diff --git a/tests/ui_tools/test_messages.py b/tests/ui_tools/test_messages.py index 08ba7d0661..d3ebc63c5b 100644 --- a/tests/ui_tools/test_messages.py +++ b/tests/ui_tools/test_messages.py @@ -776,6 +776,7 @@ def test_main_view(self, mocker, message, last_message): "reactions": [], "sender_full_name": "Alice", "timestamp": 1532103879, + "sender_id": 32900, } ], ) @@ -814,6 +815,7 @@ def test_main_view_renders_slash_me(self, mocker, message, content, is_me_messag "reactions": [], "sender_full_name": "Alice", "timestamp": 1532103879, + "sender_id": 32900, } ], ) @@ -867,6 +869,7 @@ def test_main_view_generates_stream_header( "reactions": [], "sender_full_name": "Alice", "timestamp": 1532103879, + "sender_id": 32900, }, ], ) @@ -1057,6 +1060,7 @@ def test_msg_generates_search_and_header_bar( "reactions": [], "sender_full_name": "alice", "timestamp": 1532103879, + "sender_id": 32900, } ], ) @@ -1117,6 +1121,8 @@ def test_main_view_content_header_without_header( msg_box = MessageBox(this_msg, self.model, last_msg) + self.model.is_muted_user.return_value = False + expected_header[2] = output_date_time if current_year > 2018: expected_header[2] = "2018 - " + expected_header[2] diff --git a/zulipterminal/ui_tools/messages.py b/zulipterminal/ui_tools/messages.py index b8552fdc92..212a52e785 100644 --- a/zulipterminal/ui_tools/messages.py +++ b/zulipterminal/ui_tools/messages.py @@ -96,7 +96,9 @@ def __init__(self, message: Message, model: "Model", last_message: Any) -> None: else: self.recipients_names = ", ".join( [ - recipient["full_name"] + "Muted user" + if self.model.is_muted_user(recipient["id"]) + else recipient["full_name"] for recipient in self.message["display_recipient"] if recipient["email"] != self.model.user_email ] @@ -687,7 +689,12 @@ def main_view(self) -> List[Any]: text: Dict[str, urwid_MarkupTuple] = {key: (None, " ") for key in text_keys} if any(different[key] for key in ("recipients", "author", "24h")): - text["author"] = ("msg_sender", message["this"]["author"]) + text["author"] = ( + "msg_sender", + "Muted user" + if self.model.is_muted_user(self.message["sender_id"]) + else message["this"]["author"], + ) # TODO: Refactor to use user ids for look up instead of emails. email = self.message.get("sender_email", "") @@ -729,10 +736,16 @@ def main_view(self) -> List[Any]: "/me", f"{self.message['sender_full_name']}", 1 ) + muted_message_text = "This message was hidden because you have muted the sender" + # Transform raw message content into markup (As needed by urwid.Text) content, self.message_links, self.time_mentions = self.transform_content( self.message["content"], self.model.server_url ) + + if self.model.is_muted_user(self.message["sender_id"]): + content = (None, muted_message_text) + self.content.set_text(content) if self.message["id"] in self.model.index["edited_messages"]: From 10ad48b8c737e83a2861aab8542c71ff160d7588 Mon Sep 17 00:00:00 2001 From: rsashank Date: Fri, 20 Sep 2024 19:29:54 +0530 Subject: [PATCH 5/5] views: Remove muted_users from user_list. Skip user from showing in user list if user is muted. Test updated. Co-authored by: Subhasish-Behera --- tests/ui/test_ui_tools.py | 2 ++ zulipterminal/ui_tools/views.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/ui/test_ui_tools.py b/tests/ui/test_ui_tools.py index 0a4d9ec030..419838d2dd 100644 --- a/tests/ui/test_ui_tools.py +++ b/tests/ui/test_ui_tools.py @@ -1078,6 +1078,8 @@ def test_users_view(self, users, users_btn_len, editor_mode, status, mocker): user_btn = mocker.patch(VIEWS + ".UserButton") users_view = mocker.patch(VIEWS + ".UsersView") right_col_view = RightColumnView(self.view) + mocker.patch("zulipterminal.model.Model.is_muted_user", return_value=False) + self.view.model.is_muted_user.return_value = False if status != "inactive": user_btn.assert_called_once_with( user=self.view.users[0], diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 6d01a82566..2285f72c1f 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -726,6 +726,8 @@ def users_view(self, users: Any = None) -> Any: users_btn_list = list() for user in users: + if self.view.model.is_muted_user(user["user_id"]): + continue status = user["status"] # Only include `inactive` users in search result. if status == "inactive" and not self.view.controller.is_in_editor_mode():