From f96389adc3049db33b5f6d8df3efcc9bed0246ee Mon Sep 17 00:00:00 2001 From: Sumanth V Rao Date: Sun, 18 Aug 2019 23:36:49 +0530 Subject: [PATCH 1/7] model: Send toggle_muted_topic status request to mute_topic. This handles updating muting/unmuting topics in a stream by sending the request to the server with the help of an API call. Tests added. --- tests/model/test_model.py | 28 ++++++++++++++++++++++++++++ zulipterminal/model.py | 12 ++++++++++++ 2 files changed, 40 insertions(+) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 0eeb583ea8..678a98379d 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -706,6 +706,34 @@ def test_toggle_stream_muted_status(self, mocker, model, self.display_error_if_present.assert_called_once_with(response, self.controller) + @pytest.mark.parametrize(['initial_muted_topics', 'op', + 'topic_to_be_toggled'], [ + ({ + ('Stream 1', 'delhi'): 1 + }, 'add', ('Stream 1', 'party')), + ({ + ('Stream 1', 'delhi'): 1, + ('Stream 1', 'party'): 2 + }, 'remove', ('Stream 1', 'delhi')) + ], ids=['muting_party', 'unmuting_delhi']) + def test_toggle_topic_muted_status(self, mocker, model, + stream_dict, topic_to_be_toggled, + initial_muted_topics, op, + response={'result': 'success'}): + model._muted_topics = initial_muted_topics + model.stream_dict = stream_dict + model.client.mute_topic.return_value = response + model.toggle_topic_muted_status(1, topic_to_be_toggled[0], + topic_to_be_toggled[1]) + request = { + 'stream': topic_to_be_toggled[0], + 'topic': topic_to_be_toggled[1], + 'op': op + } + model.client.mute_topic.assert_called_once_with(request) + self.display_error_if_present.assert_called_once_with(response, + self.controller) + @pytest.mark.parametrize('flags_before, expected_operator', [ ([], 'add'), (['starred'], 'remove'), diff --git a/zulipterminal/model.py b/zulipterminal/model.py index e8a59e5212..c836145c5f 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -848,6 +848,18 @@ def toggle_stream_muted_status(self, stream_id: int) -> None: response = self.client.update_subscription_settings(request) display_error_if_present(response, self.controller) + def toggle_topic_muted_status(self, stream_id: int, + stream_name: str, topic_name: str) -> None: + request = { + 'stream': stream_name, + 'topic': topic_name, + 'op': 'remove' + if self.is_muted_topic(stream_id, topic_name) + else 'add' + } + response = self.client.mute_topic(request) + display_error_if_present(response, self.controller) + def stream_id_from_name(self, stream_name: str) -> int: for stream_id, stream in self.stream_dict.items(): if stream['name'] == stream_name: From a7b552c8ac00416d1de1041b92cccd554016adc6 Mon Sep 17 00:00:00 2001 From: Sumanth V Rao Date: Sun, 18 Aug 2019 23:26:19 +0530 Subject: [PATCH 2/7] core: Add topic_muting confirmation method using PopUpConfirmationView. We make use of the PopUpConfirmation for confirmation if the user wishes to continue performing muting/unmuting action of the displayed topic. The TopicButton's stream_name and topic_name attribute allow us to display the affected stream name and topic name in the view. Tests added. --- tests/conftest.py | 21 ++++++++++++++++++++- tests/core/test_core.py | 28 ++++++++++++++++++++++++++++ zulipterminal/core.py | 16 ++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index b93b2c90df..0326355a3a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,11 @@ from zulipterminal.config.keys import keys_for_command from zulipterminal.helper import initial_index as helper_initial_index from zulipterminal.ui_tools.boxes import MessageBox -from zulipterminal.ui_tools.buttons import StreamButton, UserButton +from zulipterminal.ui_tools.buttons import ( + StreamButton, + TopicButton, + UserButton, +) from zulipterminal.version import ( MINIMUM_SUPPORTED_SERVER_VERSION, SUPPORTED_SERVER_VERSIONS, @@ -51,6 +55,21 @@ def stream_button(mocker): return button +@pytest.fixture +def topic_button(mocker): + """ + Mocked topic button. + """ + button = TopicButton( + stream_id=1, + topic='party', + controller=mocker.patch('zulipterminal.core.Controller'), + width=27, + count=30 + ) + return button + + @pytest.fixture def user_button(mocker, width=38): """ diff --git a/tests/core/test_core.py b/tests/core/test_core.py index 7c18c19330..e73da0c6d2 100644 --- a/tests/core/test_core.py +++ b/tests/core/test_core.py @@ -302,6 +302,34 @@ def test_stream_muting_confirmation_popup(self, mocker, controller, + "' ?"), "center") pop_up.assert_called_once_with(controller, text(), partial()) + @pytest.mark.parametrize('muted_topics, action', [ + ({ + ('Stream 1', 'delhi'): 1 + }, 'muting'), + ({ + ('Stream 1', 'delhi'): 1, + ('Stream 1', 'party'): 2 + }, 'unmuting'), + ]) + def test_topic_muting_confirmation_popup(self, mocker, controller, + stream_dict, topic_button, + muted_topics, action): + pop_up = mocker.patch(CORE + '.PopUpConfirmationView') + text = mocker.patch(CORE + '.urwid.Text') + partial = mocker.patch(CORE + '.partial') + controller.model._muted_topics = muted_topics + controller.model.stream_dict = stream_dict + controller.loop = mocker.Mock() + + controller.topic_muting_confirmation_popup(topic_button) + text.assert_called_with( + ("", + f"Confirm {action} of topic '{topic_button.topic_name}' " + f"under the '{topic_button.stream_name}' stream ?"), + "center" + ) + pop_up.assert_called_once_with(controller, text(), partial()) + @pytest.mark.parametrize('initial_narrow, final_narrow', [ ([], [['search', 'FOO']]), ([['search', 'BOO']], [['search', 'FOO']]), diff --git a/zulipterminal/core.py b/zulipterminal/core.py index 346e8a8b42..df4e457421 100644 --- a/zulipterminal/core.py +++ b/zulipterminal/core.py @@ -287,6 +287,22 @@ def stream_muting_confirmation_popup(self, button: Any) -> None: self.loop.widget = PopUpConfirmationView(self, question, mute_this_stream) + def topic_muting_confirmation_popup(self, button: Any) -> None: + currently_muted = self.model.is_muted_topic(button.stream_id, + button.topic_name) + type_of_action = "unmuting" if currently_muted else "muting" + question = urwid.Text( + ("", + f"Confirm {type_of_action} of topic '{button.topic_name}' under " + f"the '{button.stream_name}' stream ?"), + "center" + ) + mute_this_topic = partial(self.model.toggle_topic_muted_status, + button.stream_id, button.stream_name, + button.topic_name) + self.loop.widget = PopUpConfirmationView(self, question, + mute_this_topic) + def _narrow_to(self, anchor: Optional[int], **narrow: Any) -> None: already_narrowed = self.model.set_narrow(**narrow) if already_narrowed: From ee04db56a3e9eab7e6c385633db541ed31915870 Mon Sep 17 00:00:00 2001 From: Manu_K_Paul Date: Tue, 2 Feb 2021 17:12:46 +0530 Subject: [PATCH 3/7] model/buttons/views: Event handler for muted_topic event for (un)muting. We register the event_type 'muted_topics' to the event queue on startup, along with a handler method which responds to these events appropriately. Depending on whether the topic is already muted or not, the handler method calls the `mark_muted` or `mark_unmuted`. These two functions will update the data structures which keeps track of message counts in a later commit. Tests added --- tests/model/test_model.py | 76 +++++++++++++++++++++++++++++++ zulipterminal/api_types.py | 6 +++ zulipterminal/model.py | 49 +++++++++++++++++++- zulipterminal/ui_tools/buttons.py | 3 ++ zulipterminal/ui_tools/views.py | 2 + 5 files changed, 135 insertions(+), 1 deletion(-) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 678a98379d..f8ffef2ad2 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -167,6 +167,7 @@ def test_register_initial_desired_events(self, mocker, initial_data): 'message', 'update_message', 'reaction', + 'muted_topics', 'subscription', 'typing', 'update_message_flags', @@ -993,6 +994,55 @@ def test__handle_message_event(self, mocker, user_profile, response, # LOG REMAINS THE SAME IF UPDATE IS FALSE assert self.controller.view.message_view.log == log + @pytest.mark.parametrize(['event', 'initial_muted_topics', + 'toggled_topic_status'], [ + ({ + "type": 'muted_topics', + "muted_topics": [ + ['Stream 1', 'delhi', 3], + ['Stream 1', 'kerala', 2], + ['Stream 2', 'bangalore', 1] + ] + }, + { + ('Stream 1', 'kerala'): 2, + ('Stream 2', 'bangalore'): 1 + }, ('delhi', True)), + ({ + "type": 'muted_topics', + "muted_topics": [ + ['Stream 1', 'delhi', 3], + ['Stream 2', 'bangalore', 1] + ] + }, + { + ('Stream 1', 'delhi'): 3, + ('Stream 1', 'kerala'): 2, + ('Stream 2', 'bangalore'): 1 + }, ('kerala', False)) + ], ids=['muting_topic_delhi', 'unmuting_topic_kerala']) + def test__handle_topic_muting_event(self, mocker, model, + event, initial_muted_topics, + toggled_topic_status): + model._muted_topics = initial_muted_topics + model._get_muted_topic = ( + mocker.Mock(return_value=toggled_topic_status) + ) + mark_muted = mocker.patch( + 'zulipterminal.ui_tools.buttons.TopicButton.mark_muted') + mark_unmuted = mocker.patch( + 'zulipterminal.ui_tools.buttons.TopicButton.mark_unmuted') + model.handle_topic_muting_event(event) + + if toggled_topic_status[1]: + assert ('Stream 1', 'delhi') in model._muted_topics + mark_muted.called + else: + assert ('Stream 1', 'kerala') not in model._muted_topics + mark_unmuted.called + model._get_muted_topic.assert_called + assert model.controller.update_screen.called + @pytest.mark.parametrize(['topic_name', 'topic_order_initial', 'topic_order_final'], [ ('TOPIC3', ['TOPIC2', 'TOPIC3', 'TOPIC1'], @@ -2040,6 +2090,32 @@ def test_is_muted_topic(self, topic, is_muted, stream_dict, model, assert return_value == is_muted + @pytest.mark.parametrize('muted_topics', [ + [ + ['Stream 1', 'delhi', 4], + ['Stream 1', 'kerala', 2], + ['Stream 2', 'bangalore', 1] + ] + ]) + @pytest.mark.parametrize(['initial_muted_topics', 'toggle_topic_status'], [ + ({ + ('Stream 1', 'kerala'): 2, + ('Stream 2', 'bangalore'): 1 + }, (['Stream 1', 'delhi', 4], True)), + ({ + ('Stream 1', 'delhi'): 4, + ('Stream 1', 'chennai'): 3, + ('Stream 1', 'kerala'): 2, + ('Stream 2', 'bangalore'): 1 + }, (['Stream 1', 'chennai', 3], False)), + ], ids=['muting_delhi', 'unmuting_chennai']) + def test__get_muted_topic(self, model, initial_muted_topics, + muted_topics, toggle_topic_status): + model._muted_topics = initial_muted_topics + return_value = model._get_muted_topic(muted_topics) + + assert return_value == toggle_topic_status + @pytest.mark.parametrize('stream_id, expected_response', [ (1, True), (462, False), diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index 6ca30b5540..7712c2987b 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -112,6 +112,11 @@ class ReactionEvent(TypedDict): message_id: int +class TopicMutingEvent(TypedDict): + type: Literal['muted_topics'] + muted_topics: List[List[str]] + + class SubscriptionEvent(TypedDict): type: Literal['subscription'] op: str @@ -152,6 +157,7 @@ class UpdateDisplaySettings(TypedDict): MessageEvent, UpdateMessageEvent, ReactionEvent, + TopicMutingEvent, SubscriptionEvent, TypingEvent, UpdateMessageFlagsEvent, diff --git a/zulipterminal/model.py b/zulipterminal/model.py index c836145c5f..c550fe6eb9 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -110,6 +110,7 @@ def __init__(self, controller: Any) -> None: ('message', self._handle_message_event), ('update_message', self._handle_update_message_event), ('reaction', self._handle_reaction_event), + ('muted_topics', self.handle_topic_muting_event), ('subscription', self._handle_subscription_event), ('typing', self._handle_typing_event), ('update_message_flags', @@ -144,7 +145,7 @@ def __init__(self, controller: Any) -> None: # feature level 1, server version 3.0. muted_topics = self.initial_data['muted_topics'] assert set(map(len, muted_topics)) in (set(), {2}, {3}) - self._muted_topics: Dict[Tuple[str, str], Optional[int]] = { + self._muted_topics: Dict[Tuple[str, str], Any] = { (stream_name, topic): (None if self.server_feature_level is None else date_muted[0]) for stream_name, topic, *date_muted in muted_topics @@ -838,6 +839,52 @@ def _group_info_from_realm_user_groups(self, user_group_names.sort(key=str.lower) return user_group_names + def handle_topic_muting_event(self, event: Event) -> None: + """ + Handle Topic muting events + """ + assert event['type'] == "muted_topics" + if hasattr(self.controller, 'view'): + if 'muted_topics' in event: + new_muted_topics = event['muted_topics'] + added_topic, is_mute_topic = self._get_muted_topic( + new_muted_topics) + self._muted_topics = { + (stream_name, topic): (None + if self.server_feature_level is None + else date_muted) + for stream_name, topic, date_muted in new_muted_topics + } + if (self.controller.view.left_panel.is_in_topic_view + and added_topic[0] == self.controller.view. + topic_w.stream_button.stream_name): + topic_button = self.controller.view.topic_name_to_button[ + added_topic[1]] + if is_mute_topic: + topic_button.mark_muted() + else: + topic_button.mark_unmuted() + self.controller.update_screen() + + def _get_muted_topic(self, + muted_topics: List[List[str]]) -> Tuple[List[str], + bool]: + """ + We figure out which topic has been muted/unmuted and return the extra + topic and whether it is muting/unmuting. + """ + for topic in muted_topics: + if (topic[0], topic[1]) not in self._muted_topics.keys(): + return (topic, True) + # If it reaches here, then we must have unmuted a topic. + for unmuted_topic in self._muted_topics.keys(): + muting_timestamp = self._muted_topics[unmuted_topic] + formatted_topic = list(unmuted_topic) + formatted_topic.append(muting_timestamp) + if formatted_topic not in muted_topics: + return (formatted_topic, False) + return ([], True) + def toggle_stream_muted_status(self, stream_id: int) -> None: request = [{ 'stream_id': stream_id, diff --git a/zulipterminal/ui_tools/buttons.py b/zulipterminal/ui_tools/buttons.py index f21af7754d..5438df21b7 100644 --- a/zulipterminal/ui_tools/buttons.py +++ b/zulipterminal/ui_tools/buttons.py @@ -300,6 +300,9 @@ def mark_muted(self) -> None: self.update_widget(('muted', MUTE_MARKER), 'muted') # TODO: Handle event-based approach for topic-muting. + def mark_unmuted(self) -> None: + pass + class DecodedStream(TypedDict): stream_id: Optional[int] diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 8851a0edfb..83a604d882 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -834,6 +834,8 @@ def topics_view(self, stream_button: Any) -> Any: for topic in topics ] + self.view.topic_name_to_button = {topic.topic_name: topic + for topic in topics_btn_list} self.view.topic_w = TopicsView(topics_btn_list, self.view, stream_button) w = urwid.LineBox( From e91fcc88aed6aa0b58606fbd87329a35c5cc8e33 Mon Sep 17 00:00:00 2001 From: Sumanth V Rao Date: Sun, 18 Aug 2019 23:38:34 +0530 Subject: [PATCH 4/7] buttons/helper: Add functions to change button counts during (un)muting. `model.unread_counts` and button counts are updated based on user action of muting or unmuting. During muting of a topic: * We decrease All_msg count by the unread count of topic. * If there are unread messages in the stream, we decrease its count by the same amount as well. During unmuting of a topic: * If there are unread messages in the stream, we replace 'M' with the correct unread count. * If there are no unread messages, we remove the marked 'M'. Tests to be added for mark_(un)muted. --- tests/helper/test_helper.py | 7 ++++- tests/ui_tools/test_buttons.py | 2 +- zulipterminal/helper.py | 5 +++- zulipterminal/ui_tools/buttons.py | 45 ++++++++++++++++++++++++++++--- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/tests/helper/test_helper.py b/tests/helper/test_helper.py index 785c8dbbd0..62754c961a 100644 --- a/tests/helper/test_helper.py +++ b/tests/helper/test_helper.py @@ -200,15 +200,20 @@ def test_powerset(iterable, map_func, expected_powerset): 'all_msg': 8, 'streams': {99: 1}, 'unread_topics': {(99, 'Some private unread topic'): 1}, + 'unread_muted_topics': {(1000, 'Some general unread topic'): 3}, 'all_mentions': 0, }), ([1000], [['Secret stream', 'Some private unread topic']], { 'all_msg': 8, 'streams': {1000: 3}, 'unread_topics': {(1000, 'Some general unread topic'): 3}, + 'unread_muted_topics': {(99, 'Some private unread topic'): 1}, + 'all_mentions': 0, + }), + ([1], [], { + 'unread_muted_topics': {}, 'all_mentions': 0, }), - ([1], [], {'all_mentions': 0}) ], ids=['mute_private_stream_mute_general_stream_topic', 'mute_general_stream_mute_private_stream_topic', 'no_mute_some_other_stream_muted'] diff --git a/tests/ui_tools/test_buttons.py b/tests/ui_tools/test_buttons.py index 9f1419a090..9c6847e33f 100644 --- a/tests/ui_tools/test_buttons.py +++ b/tests/ui_tools/test_buttons.py @@ -314,7 +314,7 @@ def test_init_calls_mark_muted(self, mocker, stream_name, title, topic=title, controller=controller, width=40, count=0) if is_muted_called: - mark_muted.assert_called_once_with() + mark_muted.called else: mark_muted.assert_not_called() diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py index 821cc63b3f..43dc91546b 100644 --- a/zulipterminal/helper.py +++ b/zulipterminal/helper.py @@ -89,6 +89,7 @@ class UnreadCounts(TypedDict): all_pms: int all_mentions: int unread_topics: Dict[Tuple[int, str], int] # stream_id, topic + unread_muted_topics: Dict[Tuple[int, str], int] unread_pms: Dict[int, int] # sender_id unread_huddles: Dict[FrozenSet[int], int] # Group pms streams: Dict[int, int] # stream_id @@ -429,6 +430,7 @@ def classify_unread_counts(model: Any) -> UnreadCounts: all_pms=0, all_mentions=0, unread_topics=dict(), + unread_muted_topics=dict(), unread_pms=dict(), unread_huddles=dict(), streams=defaultdict(int), @@ -446,12 +448,13 @@ def classify_unread_counts(model: Any) -> UnreadCounts: for stream in unread_msg_counts['streams']: count = len(stream['unread_message_ids']) stream_id = stream['stream_id'] + stream_topic = (stream_id, stream['topic']) # unsubscribed streams may be in raw unreads, but are not tracked if not model.is_user_subscribed_to_stream(stream_id): continue if model.is_muted_topic(stream_id, stream['topic']): + unread_counts['unread_muted_topics'][stream_topic] = count continue - stream_topic = (stream_id, stream['topic']) unread_counts['unread_topics'][stream_topic] = count if not unread_counts['streams'].get(stream_id): unread_counts['streams'][stream_id] = count diff --git a/zulipterminal/ui_tools/buttons.py b/zulipterminal/ui_tools/buttons.py index 5438df21b7..2f24c6d1da 100644 --- a/zulipterminal/ui_tools/buttons.py +++ b/zulipterminal/ui_tools/buttons.py @@ -278,6 +278,7 @@ def __init__(self, stream_id: int, topic: str, self.topic_name = topic self.stream_id = stream_id self.model = controller.model + self.view = controller.view narrow_function = partial( controller.narrow_to_topic, @@ -294,14 +295,52 @@ def __init__(self, stream_id: int, topic: str, ) if controller.model.is_muted_topic(self.stream_id, self.topic_name): - self.mark_muted() + self.update_widget(('muted', MUTE_MARKER), 'muted') + # TODO: Handle event-based approach for topic-muting. + + def set_button_counts(self, unmuted_count: int) -> None: + self.update_count(unmuted_count) + self.model.unread_counts['streams'][self.stream_id] += unmuted_count + stream_button = self.view.stream_id_to_button[self.stream_id] + stream_button.update_count( + self.model.unread_counts['streams'][self.stream_id]) + self.model.unread_counts['all_msg'] += unmuted_count + self.view.home_button.update_count( + self.model.unread_counts['all_msg']) def mark_muted(self) -> None: + """ + We do not decrease the count of topic button (or + unread_counts['unread_topics']) but only mark it as M since + we would require the correct count while unmuting. + However, we decrease the all msg count and the topic's stream + count by the unread counts. + """ self.update_widget(('muted', MUTE_MARKER), 'muted') - # TODO: Handle event-based approach for topic-muting. + if self.stream_id in self.model.unread_counts['streams']: + self.model.unread_counts['streams'][self.stream_id] -= self.count + stream_button = self.view.stream_id_to_button[self.stream_id] + stream_button.update_count( + self.model.unread_counts['streams'][self.stream_id]) + self.model.unread_counts['unread_muted_topics'][( + self.stream_id, self.topic_name)] = self.count + self.model.unread_counts['unread_topics'].pop( + (self.stream_id, self.topic_name), None) + self.model.unread_counts['all_msg'] -= self.count + self.view.home_button.update_count( + self.model.unread_counts['all_msg']) def mark_unmuted(self) -> None: - pass + key = (self.stream_id, self.topic_name) + if key in self.model.unread_counts['unread_muted_topics']: + unmuted_count = self.model.unread_counts[ + 'unread_muted_topics'][key] + self.model.unread_counts['unread_muted_topics'].pop(key, None) + self.model.unread_counts['unread_topics'][key] = unmuted_count + self.set_button_counts(unmuted_count) + else: + # All messages in this topic are read. + self.set_button_counts(0) class DecodedStream(TypedDict): From 5b08a604f94483f136549c799aa925a60bba6cdb Mon Sep 17 00:00:00 2001 From: Manu_K_Paul Date: Sat, 24 Apr 2021 14:31:01 +0530 Subject: [PATCH 5/7] helper: Update unread counts for muted topics. Handles setting count of `model.unread_counts`. Functions `_set_count_in_model` and `_set_count_in_view` are modified to keep track of unread counts of muted topics when a new message is recieved or an existing message is read. This ensures that the count is in sync when the topic is unmuted later on. --- zulipterminal/helper.py | 53 +++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py index 43dc91546b..972b67ad24 100644 --- a/zulipterminal/helper.py +++ b/zulipterminal/helper.py @@ -119,7 +119,8 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: return wrapper -def _set_count_in_model(new_count: int, changed_messages: List[Message], +def _set_count_in_model(controller: Any, new_count: int, + changed_messages: List[Message], unread_counts: UnreadCounts) -> None: """ This function doesn't explicitly set counts in model, @@ -141,9 +142,16 @@ def update_unreads(unreads: Dict[KeyT, int], key: KeyT) -> None: for message in changed_messages: if message['type'] == 'stream': stream_id = message['stream_id'] - update_unreads(unread_counts['unread_topics'], - (stream_id, message['subject'])) - update_unreads(unread_counts['streams'], stream_id) + topic = message['subject'] + # If topic is muted, update unread_counts['unread_muted_topics'] + # Don't need to change stream unread count + if controller.model.is_muted_topic(stream_id, topic): + update_unreads(unread_counts['unread_muted_topics'], + (stream_id, topic)) + else: + update_unreads(unread_counts['unread_topics'], + (stream_id, topic)) + update_unreads(unread_counts['streams'], stream_id) # self-pm has only one display_recipient # 1-1 pms have 2 display_recipient elif len(message['display_recipient']) <= 2: @@ -188,24 +196,27 @@ def _set_count_in_view(controller: Any, new_count: int, if msg_type == 'stream': stream_id = message['stream_id'] msg_topic = message['subject'] - if controller.model.is_muted_stream(stream_id): - add_to_counts = False # if muted, don't add to eg. all_msg - else: - for stream_button in stream_buttons_log: - if stream_button.stream_id == stream_id: - stream_button.update_count(stream_button.count - + new_count) - break - # FIXME: Update unread_counts['unread_topics']? + # If the topic has been muted, we don't need to change + # stream/topic button count. if controller.model.is_muted_topic(stream_id, msg_topic): add_to_counts = False - if is_open_topic_view and stream_id == toggled_stream_id: - # If topic_view is open for incoming messages's stream, - # We update the respective TopicButton count accordingly. - for topic_button in topic_buttons_log: - if topic_button.topic_name == msg_topic: - topic_button.update_count(topic_button.count - + new_count) + else: + if controller.model.is_muted_stream(stream_id): + add_to_counts = False # if muted, don't add to eg. all_msg + else: + for stream_button in stream_buttons_log: + if stream_button.stream_id == stream_id: + stream_button.update_count(stream_button.count + + new_count) + break + + if is_open_topic_view and stream_id == toggled_stream_id: + # If topic_view is open for incoming messages's stream, + # We update the respective TopicButton count accordingly. + for topic_button in topic_buttons_log: + if topic_button.topic_name == msg_topic: + topic_button.update_count(topic_button.count + + new_count) else: for user_button in user_buttons_log: if user_button.user_id == user_id: @@ -226,7 +237,7 @@ def set_count(id_list: List[int], controller: Any, new_count: int) -> None: messages = controller.model.index['messages'] unread_counts: UnreadCounts = controller.model.unread_counts changed_messages = [messages[id] for id in id_list] - _set_count_in_model(new_count, changed_messages, unread_counts) + _set_count_in_model(controller, new_count, changed_messages, unread_counts) # if view is not yet loaded. Usually the case when first message is read. while not hasattr(controller, 'view'): From ecc30c930284b07148bdcf9e323b6ad95c103d87 Mon Sep 17 00:00:00 2001 From: Sumanth V Rao Date: Sun, 18 Aug 2019 17:27:13 +0530 Subject: [PATCH 6/7] hotkeys/keys: Add 'TOGGLE_MUTE_TOPIC' hotkey to 'mute/unmute' topics. New section 'Topic list actions' created in hotkeys.md. New 'topic_list' category created in keys for mapping keys belonging to topic list actions. --- docs/hotkeys.md | 5 +++++ zulipterminal/config/keys.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/docs/hotkeys.md b/docs/hotkeys.md index f9c8b9fd8a..a70ca572e9 100644 --- a/docs/hotkeys.md +++ b/docs/hotkeys.md @@ -64,6 +64,11 @@ |Show/hide stream information & modify settings|i| |Show/hide stream members (from stream information)|m| +## Topic list actions +|Command|Key Combination| +| :--- | :---: | +|Mute/unmute Topics|M| + ## Composing a Message |Command|Key Combination| | :--- | :---: | diff --git a/zulipterminal/config/keys.py b/zulipterminal/config/keys.py index 887fa8f107..3f3c4bf285 100644 --- a/zulipterminal/config/keys.py +++ b/zulipterminal/config/keys.py @@ -209,6 +209,11 @@ class KeyBinding(TypedDict, total=False): 'help_text': 'Mute/unmute Streams', 'key_category': 'stream_list', }), + ('TOGGLE_MUTE_TOPIC', { + 'keys': ['M'], + 'help_text': 'Mute/unmute Topics', + 'key_category': 'topic_list', + }), ('ENTER', { 'keys': ['enter'], 'help_text': 'Perform current action', @@ -339,6 +344,7 @@ class KeyBinding(TypedDict, total=False): ('searching', 'Searching'), ('msg_actions', 'Message actions'), ('stream_list', 'Stream list actions'), + ('topic_list', 'Topic list actions'), ('msg_compose', 'Composing a Message'), ]) From 02bd05cddff3ac654ff4df86022f597f08d083ab Mon Sep 17 00:00:00 2001 From: Sumanth V Rao Date: Wed, 3 Feb 2021 14:10:17 +0530 Subject: [PATCH 7/7] buttons: Connect PopUp confirmation display for topics via keypress. On pressing TOGGLE_MUTE_TOPIC, we now ask for confirmation before performing the action of muting/unmuting topics. Tests added. --- tests/ui_tools/test_buttons.py | 9 +++++++++ zulipterminal/ui_tools/buttons.py | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/tests/ui_tools/test_buttons.py b/tests/ui_tools/test_buttons.py index 9c6847e33f..4c33d82b94 100644 --- a/tests/ui_tools/test_buttons.py +++ b/tests/ui_tools/test_buttons.py @@ -318,6 +318,15 @@ def test_init_calls_mark_muted(self, mocker, stream_name, title, else: mark_muted.assert_not_called() + @pytest.mark.parametrize('key', keys_for_command('TOGGLE_MUTE_TOPIC')) + def test_keypress_TOGGLE_MUTE_TOPIC(self, mocker, topic_button, key, + widget_size): + size = widget_size(topic_button) + pop_up = mocker.patch( + 'zulipterminal.core.Controller.topic_muting_confirmation_popup') + topic_button.keypress(size, key) + pop_up.assert_called_once_with(topic_button) + class TestMessageLinkButton: @pytest.fixture(autouse=True) diff --git a/zulipterminal/ui_tools/buttons.py b/zulipterminal/ui_tools/buttons.py index 2f24c6d1da..6c7a1dea9d 100644 --- a/zulipterminal/ui_tools/buttons.py +++ b/zulipterminal/ui_tools/buttons.py @@ -342,6 +342,11 @@ def mark_unmuted(self) -> None: # All messages in this topic are read. self.set_button_counts(0) + def keypress(self, size: urwid_Size, key: str) -> Optional[str]: + if is_command_key('TOGGLE_MUTE_TOPIC', key): + self.controller.topic_muting_confirmation_popup(self) + return super().keypress(size, key) + class DecodedStream(TypedDict): stream_id: Optional[int]