Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handling Empty Narrows #1543

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
58 changes: 56 additions & 2 deletions tests/core/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,8 +518,8 @@ def test_stream_muting_confirmation_popup(
"search_within_topic_narrow",
],
)
@pytest.mark.parametrize("msg_ids", [({200, 300, 400}), (set()), ({100})])
def test_search_message(
@pytest.mark.parametrize("msg_ids", [({200, 300, 400}), ({100})])
def test_search_message__hits(
self,
initial_narrow: List[Any],
final_narrow: List[Any],
Expand Down Expand Up @@ -550,6 +550,60 @@ def set_msg_ids(*args: Any, **kwargs: Any) -> None:
create_msg.assert_called_once_with(controller.model, msg_ids)
assert controller.model.index == dict(index_search_messages, search=msg_ids)

@pytest.mark.parametrize(
"initial_narrow, final_narrow",
[
([], [["search", "FOO"]]),
([["search", "BOO"]], [["search", "FOO"]]),
([["stream", "PTEST"]], [["stream", "PTEST"], ["search", "FOO"]]),
(
[["pm-with", "[email protected]"], ["search", "BOO"]],
[["pm-with", "[email protected]"], ["search", "FOO"]],
),
(
[["stream", "PTEST"], ["topic", "RDS"]],
[["stream", "PTEST"], ["topic", "RDS"], ["search", "FOO"]],
),
],
ids=[
"Default_all_msg_search",
"redo_default_search",
"search_within_stream",
"pm_search_again",
"search_within_topic_narrow",
],
)
def test_search_message__no_hits(
self,
initial_narrow: List[Any],
final_narrow: List[Any],
controller: Controller,
mocker: MockerFixture,
index_search_messages: Index,
msg_ids: Set[int] = set(),
) -> None:
get_message = mocker.patch(MODEL + ".get_messages")
create_msg = mocker.patch(MODULE + ".create_msg_box_list")
mocker.patch(MODEL + ".get_message_ids_in_current_narrow", return_value=msg_ids)
controller.model.index = index_search_messages # Any initial search index
controller.view.message_view = mocker.patch("urwid.ListBox")
controller.model.narrow = initial_narrow

def set_msg_ids(*args: Any, **kwargs: Any) -> None:
controller.model.index["search"].update(msg_ids)

get_message.side_effect = set_msg_ids
assert controller.model.index["search"] == {500}

controller.search_messages("FOO")

assert controller.model.narrow == final_narrow
get_message.assert_called_once_with(
num_after=0, num_before=30, anchor=10000000000
)
create_msg.assert_not_called()
assert controller.model.index == dict(index_search_messages, search=msg_ids)

@pytest.mark.parametrize(
"screen_size, expected_popup_size",
[
Expand Down
4 changes: 4 additions & 0 deletions tests/model/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1913,6 +1913,7 @@ def test__handle_message_event_with_Falsey_log(
mocker.patch(MODEL + "._update_topic_index")
mocker.patch(MODULE + ".index_messages", return_value={})
self.controller.view.message_view = mocker.Mock(log=[])
self.controller.is_in_empty_narrow = False
create_msg_box_list = mocker.patch(
MODULE + ".create_msg_box_list", return_value=["msg_w"]
)
Expand All @@ -1932,6 +1933,7 @@ def test__handle_message_event_with_valid_log(self, mocker, model, message_fixtu
mocker.patch(MODEL + "._update_topic_index")
mocker.patch(MODULE + ".index_messages", return_value={})
self.controller.view.message_view = mocker.Mock(log=[mocker.Mock()])
self.controller.is_in_empty_narrow = False
create_msg_box_list = mocker.patch(
MODULE + ".create_msg_box_list", return_value=["msg_w"]
)
Expand All @@ -1954,6 +1956,7 @@ def test__handle_message_event_with_flags(self, mocker, model, message_fixture):
mocker.patch(MODEL + "._update_topic_index")
mocker.patch(MODULE + ".index_messages", return_value={})
self.controller.view.message_view = mocker.Mock(log=[mocker.Mock()])
self.controller.is_in_empty_narrow = False
mocker.patch(MODULE + ".create_msg_box_list", return_value=["msg_w"])
model.notify_user = mocker.Mock()
set_count = mocker.patch(MODULE + ".set_count")
Expand Down Expand Up @@ -2097,6 +2100,7 @@ def test__handle_message_event(
(
self.controller.view.left_panel.is_in_topic_view_with_stream_id.return_value
) = False
self.controller.is_in_empty_narrow = False
model.notify_user = mocker.Mock()
model.narrow = narrow
model.recipients = recipients
Expand Down
4 changes: 4 additions & 0 deletions tests/ui/test_ui_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ def test_mouse_event(self, mocker, msg_view, mouse_scroll_event, widget_size):
@pytest.mark.parametrize("key", keys_for_command("GO_DOWN"))
def test_keypress_GO_DOWN(self, mocker, msg_view, key, widget_size):
size = widget_size(msg_view)
msg_view.model.controller.is_in_empty_narrow = False
msg_view.new_loading = False
mocker.patch(MESSAGEVIEW + ".focus_position", return_value=0)
mocker.patch(MESSAGEVIEW + ".set_focus_valign")
Expand All @@ -291,6 +292,7 @@ def test_keypress_GO_DOWN_exception(
self, mocker, msg_view, key, widget_size, view_is_focused
):
size = widget_size(msg_view)
msg_view.model.controller.is_in_empty_narrow = False
msg_view.new_loading = False
mocker.patch(MESSAGEVIEW + ".focus_position", return_value=0)
mocker.patch(MESSAGEVIEW + ".set_focus_valign")
Expand All @@ -315,6 +317,7 @@ def test_keypress_GO_DOWN_exception(
@pytest.mark.parametrize("key", keys_for_command("GO_UP"))
def test_keypress_GO_UP(self, mocker, msg_view, key, widget_size):
size = widget_size(msg_view)
msg_view.model.controller.is_in_empty_narrow = False
mocker.patch(MESSAGEVIEW + ".focus_position", return_value=0)
mocker.patch(MESSAGEVIEW + ".set_focus_valign")
msg_view.old_loading = False
Expand All @@ -330,6 +333,7 @@ def test_keypress_GO_UP_exception(
self, mocker, msg_view, key, widget_size, view_is_focused
):
size = widget_size(msg_view)
msg_view.model.controller.is_in_empty_narrow = False
msg_view.old_loading = False
mocker.patch(MESSAGEVIEW + ".focus_position", return_value=0)
mocker.patch(MESSAGEVIEW + ".set_focus_valign")
Expand Down
24 changes: 18 additions & 6 deletions tests/ui_tools/test_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def test_init(self, mocker, message_type, set_fields):
display_recipient=[
{"id": 7, "email": "[email protected]", "full_name": "Boo is awesome"}
],
id=457823,
stream_id=5,
subject="hi",
sender_email="[email protected]",
Expand All @@ -65,18 +66,13 @@ def test_init(self, mocker, message_type, set_fields):
assert msg_box.message_links == OrderedDict()
assert msg_box.time_mentions == list()

def test_init_fails_with_bad_message_type(self):
message = dict(type="BLAH")

with pytest.raises(RuntimeError):
MessageBox(message, self.model, None)

def test_private_message_to_self(self, mocker):
message = dict(
type="private",
display_recipient=[
{"full_name": "Foo Foo", "email": "[email protected]", "id": None}
],
id=457823,
sender_id=9,
content="<p> self message. </p>",
sender_full_name="Foo Foo",
Expand All @@ -88,6 +84,7 @@ def test_private_message_to_self(self, mocker):
MODULE + ".MessageBox._is_private_message_to_self", return_value=True
)
mocker.patch.object(MessageBox, "main_view")

msg_box = MessageBox(message, self.model, None)

assert msg_box.recipient_emails == ["[email protected]"]
Expand Down Expand Up @@ -759,6 +756,7 @@ def test_main_view(self, mocker, message, last_message):
"color": "#bd6",
},
}
self.model.controller.is_in_empty_narrow = False
MessageBox(message, self.model, last_message)

@pytest.mark.parametrize(
Expand Down Expand Up @@ -838,6 +836,7 @@ def test_main_view_generates_stream_header(
"color": "#bd6",
},
}
self.model.controller.is_in_empty_narrow = False
last_message = dict(message, **to_vary_in_last_message)
msg_box = MessageBox(message, self.model, last_message)
view_components = msg_box.main_view()
Expand Down Expand Up @@ -890,6 +889,7 @@ def test_main_view_generates_stream_header(
def test_main_view_generates_PM_header(
self, mocker, message, to_vary_in_last_message
):
self.model.controller.is_in_empty_narrow = False
last_message = dict(message, **to_vary_in_last_message)
msg_box = MessageBox(message, self.model, last_message)
view_components = msg_box.main_view()
Expand Down Expand Up @@ -1032,9 +1032,11 @@ def test_msg_generates_search_and_header_bar(
},
}
self.model.narrow = msg_narrow
self.model.controller.is_in_empty_narrow = False
messages = messages_successful_response["messages"]
current_message = messages[msg_type]
msg_box = MessageBox(current_message, self.model, messages[0])

search_bar = msg_box.top_search_bar()
header_bar = msg_box.recipient_header()

Expand Down Expand Up @@ -1151,8 +1153,11 @@ def test_main_view_compact_output(
):
message_fixture.update({"id": 4})
varied_message = dict(message_fixture, **to_vary_in_each_message)
self.model.controller.is_in_empty_narrow = False
msg_box = MessageBox(varied_message, self.model, varied_message)

view_components = msg_box.main_view()

assert len(view_components) == 1
assert isinstance(view_components[0], Padding)

Expand All @@ -1162,7 +1167,9 @@ def test_main_view_generates_EDITED_label(
messages = messages_successful_response["messages"]
for message in messages:
self.model.index["edited_messages"].add(message["id"])
self.model.controller.is_in_empty_narrow = False
msg_box = MessageBox(message, self.model, message)

view_components = msg_box.main_view()

label = view_components[0].original_widget.contents[0]
Expand All @@ -1188,6 +1195,7 @@ def test_update_message_author_status(
):
message = message_fixture
last_msg = dict(message, **to_vary_in_last_message)
self.model.controller.is_in_empty_narrow = False

msg_box = MessageBox(message, self.model, last_msg)

Expand Down Expand Up @@ -1391,6 +1399,7 @@ def test_keypress_EDIT_MESSAGE(
to_vary_in_each_message["subject"] = ""
varied_message = dict(message_fixture, **to_vary_in_each_message)
message_type = varied_message["type"]
self.model.controller.is_in_empty_narrow = False
msg_box = MessageBox(varied_message, self.model, message_fixture)
size = widget_size(msg_box)
msg_box.model.user_id = 1
Expand All @@ -1405,6 +1414,7 @@ def test_keypress_EDIT_MESSAGE(
report_error = msg_box.model.controller.report_error
report_warning = msg_box.model.controller.report_warning
mocker.patch(MODULE + ".time", return_value=100)
msg_box.model.controller.is_in_empty_narrow = False

msg_box.keypress(size, key)

Expand Down Expand Up @@ -1820,6 +1830,7 @@ def test_reactions_view(
varied_message = dict(message_fixture, **to_vary_in_each_message)
msg_box = MessageBox(varied_message, self.model, None)
reactions = to_vary_in_each_message["reactions"]
msg_box.model.controller.is_in_empty_narrow = False

reactions_view = msg_box.reactions_view(reactions)

Expand Down Expand Up @@ -1976,6 +1987,7 @@ def test_mouse_event_left_click(
mocker.patch.object(msg_box, "keypress")
msg_box.model = mocker.Mock()
msg_box.model.controller.is_in_editor_mode.return_value = compose_box_is_open
msg_box.model.controller.is_in_empty_narrow = False

msg_box.mouse_event(size, "mouse press", 1, col, row, focus)

Expand Down
7 changes: 6 additions & 1 deletion zulipterminal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def __init__(

self.active_conversation_info: Dict[str, Any] = {}
self.is_typing_notification_in_progress = False
self.is_in_empty_narrow = False

self.show_loading()
client_identifier = f"ZulipTerminal/{ZT_VERSION} {platform()}"
Expand Down Expand Up @@ -503,20 +504,23 @@ def show_media_confirmation_popup(
self, question, callback, location="center"
)

def search_messages(self, text: str) -> None:
def search_messages(self, text: str) -> bool:
# Search for a text in messages
self.model.index["search"].clear()
self.model.set_search_narrow(text)

self.model.get_messages(num_after=0, num_before=30, anchor=10000000000)
msg_id_list = self.model.get_message_ids_in_current_narrow()
if len(msg_id_list) == 0:
return False

w_list = create_msg_box_list(self.model, msg_id_list)
self.view.message_view.log.clear()
self.view.message_view.log.extend(w_list)
focus_position = 0
if 0 <= focus_position < len(w_list):
self.view.message_view.set_focus(focus_position)
return True

def save_draft_confirmation_popup(self, draft: Composition) -> None:
question = urwid.Text(
Expand Down Expand Up @@ -598,6 +602,7 @@ def _narrow_to(self, anchor: Optional[int], **narrow: Any) -> None:
if len(msg_id_list) == 0 or (anchor is not None and anchor not in msg_id_list):
self.model.get_messages(num_before=30, num_after=10, anchor=anchor)
msg_id_list = self.model.get_message_ids_in_current_narrow()
self.is_in_empty_narrow = bool(len(msg_id_list) == 0)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this small commit factored out, so that it would cover Sumanth's original process here? Oh, I think you said that it was where the inspiration came from?

(I'm torn whether this belongs in the model or controller here, but that's probably a refactoring to consider how to structure the whole narrowing behavior)


w_list = create_msg_box_list(self.model, msg_id_list, focus_msg_id=anchor)

Expand Down
3 changes: 3 additions & 0 deletions zulipterminal/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1742,6 +1742,9 @@ def _handle_message_event(self, event: Event) -> None:
msg_w = msg_w_list[0]

if self.current_narrow_contains_message(message):
if self.controller.is_in_empty_narrow:
del msg_log[0]
self.controller.is_in_empty_narrow = False
Comment on lines +1745 to +1747
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the part that is not working properly, in your 'Next steps'?
I would guess that this is blocked by some earlier conditional?

It looks like this commit should come later in the sequencce, at or after where we add an empty message, or else the del is going to trigger an exception?

As per another comment, this isn't updated at all in main right now, so we could handle this incrementally, ie. handle updates as a later part or follow-up. However, it would be good to get this as a base feature if we can, of course :)

Note that we will also want this to be triggered for star changes as it stands, and may be a lot simpler to test in that way, ie. to make a narrow empty using 0/1 starred messages - though of course is a different function under test in that case.
(a quick scan suggests we need to handle mention changes too, among possibly other flags)

msg_log.append(msg_w)

self.controller.update_screen()
Expand Down
10 changes: 6 additions & 4 deletions zulipterminal/ui_tools/boxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1014,7 +1014,7 @@ def main_view(self) -> Any:
self.text_box,
]
)
self.msg_narrow = urwid.Text("DONT HIDE")
self.msg_narrow = urwid.Text("")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said in the channel, it's great to remove this line, though I think this was associated with the comment some lines above, which we could also remove? (look through the git log via git blame?)

In terms of this commit, it would be helpful to clarify under which conditions the application would display an empty recipient bar, since I very rarely actually see this odd text. This doesn't need to be via an exact analysis, but notes would be helpful, since looking back to this change in the future would help clarify what happens here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to suggest taking advantage of this small commit to eg. refactor the attribute naming to be private. However, this is one of those coupled classes/objects, so many of these names are actually public, so this likely would be best settled with a separate refactoring effort.

In the absence of that, you could add comments to clarify each attribute for now; would that help you next time? If so, it'd help others who next read the code :)

self.recipient_bar = urwid.LineBox(
self.msg_narrow,
title="Current message recipients",
Expand All @@ -1032,9 +1032,11 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
return key

elif is_command_key("EXECUTE_SEARCH", key):
self.controller.exit_editor_mode()
self.controller.search_messages(self.text_box.edit_text)
self.controller.view.middle_column.set_focus("body")
if self.controller.search_messages(self.text_box.edit_text):
self.controller.exit_editor_mode()
self.controller.view.middle_column.set_focus("body")
else:
self.controller.report_error(["No results found."])
Comment on lines -1035 to +1039
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we want to take this approach rather than just have the empty result like the rest of this branch is pursuing, but it's an interesting extra change - I assume this is extra to the original version?

return key

key = super().keypress(size, key)
Expand Down
Loading
Loading