Skip to content

Commit

Permalink
core/boxes: Improve handling of pressing Esc during message compose.
Browse files Browse the repository at this point in the history
Introduces variable MAX_MESSAGE_LENGTH_CONFIRMATION_POPUP to set
a threshold for maximum length of message in compose box beyond
which it prompts a confirmation popup instead of the current
instant exit when Esc is pressed.

Fixes zulip#1342.
  • Loading branch information
rsashank committed Apr 15, 2024
1 parent 764fb72 commit 246354d
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 10 deletions.
63 changes: 58 additions & 5 deletions tests/ui_tools/test_boxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
)
from zulipterminal.config.ui_mappings import StreamAccessType
from zulipterminal.helper import Index, MinimalUserData
from zulipterminal.ui_tools.boxes import PanelSearchBox, WriteBox, _MessageEditState
from zulipterminal.ui_tools.boxes import (
MAX_MESSAGE_LENGTH_CONFIRMATION_POPUP,
PanelSearchBox,
WriteBox,
_MessageEditState,
)
from zulipterminal.urwid_types import urwid_Size


Expand Down Expand Up @@ -232,7 +237,7 @@ def test_not_calling_send_private_message_without_recipients(
assert not write_box.model.send_private_message.called

@pytest.mark.parametrize("key", keys_for_command("GO_BACK"))
def test__compose_attributes_reset_for_private_compose(
def test__compose_attributes_reset_for_private_compose__no_popup(
self,
key: str,
mocker: MockerFixture,
Expand All @@ -243,17 +248,41 @@ def test__compose_attributes_reset_for_private_compose(
mocker.patch("urwid.connect_signal")
write_box.model.user_id_email_dict = user_id_email_dict
write_box.private_box_view(recipient_user_ids=[11])
write_box.msg_write_box.edit_text = "random text"

write_box.msg_write_box.edit_text = "." * (
MAX_MESSAGE_LENGTH_CONFIRMATION_POPUP - 1
)

size = widget_size(write_box)
write_box.keypress(size, key)

write_box.view.controller.exit_compose_confirmation_popup.assert_not_called()
assert write_box.to_write_box is None
assert write_box.msg_write_box.edit_text == ""
assert write_box.compose_box_status == "closed"

@pytest.mark.parametrize("key", keys_for_command("GO_BACK"))
def test__compose_attributes_reset_for_stream_compose(
def test__compose_attributes_reset_for_private_compose__popup(
self,
key: str,
mocker: MockerFixture,
write_box: WriteBox,
widget_size: Callable[[Widget], urwid_Size],
user_id_email_dict: Dict[int, str],
) -> None:
mocker.patch("urwid.connect_signal")
write_box.model.user_id_email_dict = user_id_email_dict
write_box.private_box_view(recipient_user_ids=[11])

write_box.msg_write_box.edit_text = "." * MAX_MESSAGE_LENGTH_CONFIRMATION_POPUP

size = widget_size(write_box)
write_box.keypress(size, key)

write_box.view.controller.exit_compose_confirmation_popup.assert_called_once()

@pytest.mark.parametrize("key", keys_for_command("GO_BACK"))
def test__compose_attributes_reset_for_stream_compose__no_popup(
self,
key: str,
mocker: MockerFixture,
Expand All @@ -262,15 +291,37 @@ def test__compose_attributes_reset_for_stream_compose(
) -> None:
mocker.patch(WRITEBOX + "._set_stream_write_box_style")
write_box.stream_box_view(stream_id=1)
write_box.msg_write_box.edit_text = "random text"

write_box.msg_write_box.edit_text = "." * (
MAX_MESSAGE_LENGTH_CONFIRMATION_POPUP - 1
)

size = widget_size(write_box)
write_box.keypress(size, key)

write_box.view.controller.exit_compose_confirmation_popup.assert_not_called()
assert write_box.stream_id is None
assert write_box.msg_write_box.edit_text == ""
assert write_box.compose_box_status == "closed"

@pytest.mark.parametrize("key", keys_for_command("GO_BACK"))
def test__compose_attributes_reset_for_stream_compose__popup(
self,
key: str,
mocker: MockerFixture,
write_box: WriteBox,
widget_size: Callable[[Widget], urwid_Size],
) -> None:
mocker.patch(WRITEBOX + "._set_stream_write_box_style")
write_box.stream_box_view(stream_id=1)

write_box.msg_write_box.edit_text = "." * MAX_MESSAGE_LENGTH_CONFIRMATION_POPUP

size = widget_size(write_box)
write_box.keypress(size, key)

write_box.view.controller.exit_compose_confirmation_popup.assert_called_once_with()

@pytest.mark.parametrize(
["raw_recipients", "tidied_recipients"],
[
Expand Down Expand Up @@ -1516,13 +1567,15 @@ def test_keypress_SEND_MESSAGE_no_topic(
)
def test_keypress_typeahead_mode_autocomplete_key(
self,
mocker: MockerFixture,
write_box: WriteBox,
widget_size: Callable[[Widget], urwid_Size],
current_typeahead_mode: bool,
expected_typeahead_mode: bool,
expect_footer_was_reset: bool,
key: str,
) -> None:
write_box.msg_write_box = mocker.Mock(edit_text="")
write_box.is_in_typeahead_mode = current_typeahead_mode
size = widget_size(write_box)

Expand Down
15 changes: 15 additions & 0 deletions zulipterminal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,21 @@ def stream_muting_confirmation_popup(
mute_this_stream = partial(self.model.toggle_stream_muted_status, stream_id)
self.loop.widget = PopUpConfirmationView(self, question, mute_this_stream)

def exit_compose_confirmation_popup(self) -> None:
question = urwid.Text(
(
"bold",
"Please confirm that you wish to exit the compose box.\n"
"(You can save the message as a draft upon returning to compose)",
),
"center",
)
write_box = self.view.write_box
popup_view = PopUpConfirmationView(
self, question, write_box.exit_compose_box, location="center"
)
self.loop.widget = popup_view

def copy_to_clipboard(self, text: str, text_category: str) -> None:
try:
pyperclip.copy(text)
Expand Down
36 changes: 31 additions & 5 deletions zulipterminal/ui_tools/boxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple

import urwid
from typing_extensions import Literal
from typing_extensions import Final, Literal
from urwid_readline import ReadlineEdit

from zulipterminal.api_types import Composition, PrivateComposition, StreamComposition
Expand Down Expand Up @@ -48,6 +48,11 @@
from zulipterminal.urwid_types import urwid_Size


# This constant defines the maximum character length of a message
# in the compose box that does not trigger a confirmation popup.
MAX_MESSAGE_LENGTH_CONFIRMATION_POPUP: Final = 15


class _MessageEditState(NamedTuple):
message_id: int
old_topic: str
Expand Down Expand Up @@ -708,10 +713,19 @@ def autocomplete_emojis(

return emoji_typeahead, emojis

def exit_compose_box(self) -> None:
self.is_in_typeahead_mode = False
self.view.set_footer_text()
self._set_compose_attributes_to_defaults()
self.view.controller.exit_editor_mode()
self.main_view(False)
self.view.middle_column.set_focus("body")

def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
if self.is_in_typeahead_mode and not (
is_command_key("AUTOCOMPLETE", key)
or is_command_key("AUTOCOMPLETE_REVERSE", key)
or is_command_key("GO_BACK", key)
):
# set default footer when done with autocomplete
self.is_in_typeahead_mode = False
Expand Down Expand Up @@ -798,11 +812,23 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
"Cannot narrow to message without specifying recipients."
)
elif is_command_key("GO_BACK", key):
saved_draft = self.model.session_draft_message()
self.send_stop_typing_status()
self._set_compose_attributes_to_defaults()
self.view.controller.exit_editor_mode()
self.main_view(False)
self.view.middle_column.set_focus("body")

if (
self.msg_edit_state is None
and len(self.msg_write_box.edit_text)
>= MAX_MESSAGE_LENGTH_CONFIRMATION_POPUP
):
if (
saved_draft is None
or self.msg_write_box.edit_text != saved_draft.get("content")
):
self.view.controller.exit_compose_confirmation_popup()
else:
self.exit_compose_box()
else:
self.exit_compose_box()
elif is_command_key("MARKDOWN_HELP", key):
self.view.controller.show_markdown_help()
return key
Expand Down

0 comments on commit 246354d

Please sign in to comment.