Skip to content

Commit 11f8d9f

Browse files
rsashankneiljp
authored andcommitted
core/boxes: Improve handling of pressing Esc during message compose.
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. This specifically avoids prompting for - message editing, rather than new compositions - when the message content is identical to the saved draft This adds a call to the recently added _set_default_footer_after_autocomplete() in the exit_compose_box() method, to reset the footer in case autocomplete was in use. However, the footer is currently always reset near the top of the keypress() method, before that point (see added TODO comment). This is since autocomplete does not correctly fully resume even if compose itself is resumed with the appropriate EXIT_COMPOSE exclusion near that TODO comment - the autocomplete state itself must also somehow be resumed for this to work. Tests added and updated. Fixes #1342.
1 parent f1e6c6d commit 11f8d9f

File tree

3 files changed

+105
-9
lines changed

3 files changed

+105
-9
lines changed

tests/ui_tools/test_boxes.py

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@
2525
)
2626
from zulipterminal.config.ui_mappings import StreamAccessType
2727
from zulipterminal.helper import Index, MinimalUserData
28-
from zulipterminal.ui_tools.boxes import PanelSearchBox, WriteBox, _MessageEditState
28+
from zulipterminal.ui_tools.boxes import (
29+
MAX_MESSAGE_LENGTH_CONFIRMATION_POPUP,
30+
PanelSearchBox,
31+
WriteBox,
32+
_MessageEditState,
33+
)
2934
from zulipterminal.urwid_types import urwid_Size
3035

3136

@@ -236,7 +241,7 @@ def test_not_calling_send_private_message_without_recipients(
236241
assert not write_box.model.send_private_message.called
237242

238243
@pytest.mark.parametrize("key", keys_for_command("EXIT_COMPOSE"))
239-
def test__compose_attributes_reset_for_private_compose(
244+
def test__compose_attributes_reset_for_private_compose__no_popup(
240245
self,
241246
key: str,
242247
mocker: MockerFixture,
@@ -247,17 +252,41 @@ def test__compose_attributes_reset_for_private_compose(
247252
mocker.patch("urwid.connect_signal")
248253
write_box.model.user_id_email_dict = user_id_email_dict
249254
write_box.private_box_view(recipient_user_ids=[11])
250-
write_box.msg_write_box.edit_text = "random text"
255+
256+
write_box.msg_write_box.edit_text = "." * (
257+
MAX_MESSAGE_LENGTH_CONFIRMATION_POPUP - 1
258+
)
251259

252260
size = widget_size(write_box)
253261
write_box.keypress(size, key)
254262

263+
write_box.view.controller.exit_compose_confirmation_popup.assert_not_called()
255264
assert write_box.to_write_box is None
256265
assert write_box.msg_write_box.edit_text == ""
257266
assert write_box.compose_box_status == "closed"
258267

259268
@pytest.mark.parametrize("key", keys_for_command("EXIT_COMPOSE"))
260-
def test__compose_attributes_reset_for_stream_compose(
269+
def test__compose_attributes_reset_for_private_compose__popup(
270+
self,
271+
key: str,
272+
mocker: MockerFixture,
273+
write_box: WriteBox,
274+
widget_size: Callable[[Widget], urwid_Size],
275+
user_id_email_dict: Dict[int, str],
276+
) -> None:
277+
mocker.patch("urwid.connect_signal")
278+
write_box.model.user_id_email_dict = user_id_email_dict
279+
write_box.private_box_view(recipient_user_ids=[11])
280+
281+
write_box.msg_write_box.edit_text = "." * MAX_MESSAGE_LENGTH_CONFIRMATION_POPUP
282+
283+
size = widget_size(write_box)
284+
write_box.keypress(size, key)
285+
286+
write_box.view.controller.exit_compose_confirmation_popup.assert_called_once()
287+
288+
@pytest.mark.parametrize("key", keys_for_command("EXIT_COMPOSE"))
289+
def test__compose_attributes_reset_for_stream_compose__no_popup(
261290
self,
262291
key: str,
263292
mocker: MockerFixture,
@@ -266,15 +295,37 @@ def test__compose_attributes_reset_for_stream_compose(
266295
) -> None:
267296
mocker.patch(WRITEBOX + "._set_stream_write_box_style")
268297
write_box.stream_box_view(stream_id=1)
269-
write_box.msg_write_box.edit_text = "random text"
298+
299+
write_box.msg_write_box.edit_text = "." * (
300+
MAX_MESSAGE_LENGTH_CONFIRMATION_POPUP - 1
301+
)
270302

271303
size = widget_size(write_box)
272304
write_box.keypress(size, key)
273305

306+
write_box.view.controller.exit_compose_confirmation_popup.assert_not_called()
274307
assert write_box.stream_id is None
275308
assert write_box.msg_write_box.edit_text == ""
276309
assert write_box.compose_box_status == "closed"
277310

311+
@pytest.mark.parametrize("key", keys_for_command("EXIT_COMPOSE"))
312+
def test__compose_attributes_reset_for_stream_compose__popup(
313+
self,
314+
key: str,
315+
mocker: MockerFixture,
316+
write_box: WriteBox,
317+
widget_size: Callable[[Widget], urwid_Size],
318+
) -> None:
319+
mocker.patch(WRITEBOX + "._set_stream_write_box_style")
320+
write_box.stream_box_view(stream_id=1)
321+
322+
write_box.msg_write_box.edit_text = "." * MAX_MESSAGE_LENGTH_CONFIRMATION_POPUP
323+
324+
size = widget_size(write_box)
325+
write_box.keypress(size, key)
326+
327+
write_box.view.controller.exit_compose_confirmation_popup.assert_called_once_with()
328+
278329
@pytest.mark.parametrize(
279330
["raw_recipients", "tidied_recipients"],
280331
[
@@ -1523,23 +1574,26 @@ def test_keypress_SEND_MESSAGE_no_topic(
15231574
)
15241575
def test_keypress_typeahead_mode_autocomplete_key(
15251576
self,
1577+
mocker: MockerFixture,
15261578
write_box: WriteBox,
15271579
widget_size: Callable[[Widget], urwid_Size],
15281580
current_typeahead_mode: bool,
15291581
expected_typeahead_mode: bool,
15301582
expect_footer_was_reset: bool,
15311583
key: str,
15321584
) -> None:
1585+
write_box.msg_write_box = mocker.Mock(edit_text="")
15331586
write_box.is_in_typeahead_mode = current_typeahead_mode
15341587
size = widget_size(write_box)
15351588

15361589
write_box.keypress(size, key)
15371590

15381591
assert write_box.is_in_typeahead_mode == expected_typeahead_mode
15391592
if expect_footer_was_reset:
1540-
self.view.set_footer_text.assert_called_once_with()
1593+
# We may prefer called-once in future, but the key part is that we do reset
1594+
assert self.view.set_footer_text.called
15411595
else:
1542-
self.view.set_footer_text.assert_not_called()
1596+
assert not self.view.set_footer_text.called
15431597

15441598
@pytest.mark.parametrize(
15451599
[

zulipterminal/core.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,21 @@ def stream_muting_confirmation_popup(
532532
mute_this_stream = partial(self.model.toggle_stream_muted_status, stream_id)
533533
self.loop.widget = PopUpConfirmationView(self, question, mute_this_stream)
534534

535+
def exit_compose_confirmation_popup(self) -> None:
536+
question = urwid.Text(
537+
(
538+
"bold",
539+
"Please confirm that you wish to exit the compose box.\n"
540+
"(You can save the message as a draft upon returning to compose)",
541+
),
542+
"center",
543+
)
544+
write_box = self.view.write_box
545+
popup_view = PopUpConfirmationView(
546+
self, question, write_box.exit_compose_box, location="center"
547+
)
548+
self.loop.widget = popup_view
549+
535550
def copy_to_clipboard(self, text: str, text_category: str) -> None:
536551
try:
537552
pyperclip.copy(text)

zulipterminal/ui_tools/boxes.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple
1111

1212
import urwid
13-
from typing_extensions import Literal
13+
from typing_extensions import Final, Literal
1414
from urwid_readline import ReadlineEdit
1515

1616
from zulipterminal.api_types import Composition, PrivateComposition, StreamComposition
@@ -49,6 +49,11 @@
4949
from zulipterminal.urwid_types import urwid_Size
5050

5151

52+
# This constant defines the maximum character length of a message
53+
# in the compose box that does not trigger a confirmation popup.
54+
MAX_MESSAGE_LENGTH_CONFIRMATION_POPUP: Final = 15
55+
56+
5257
class _MessageEditState(NamedTuple):
5358
message_id: int
5459
old_topic: str
@@ -710,6 +715,7 @@ def autocomplete_emojis(
710715
return emoji_typeahead, emojis
711716

712717
def exit_compose_box(self) -> None:
718+
self._set_default_footer_after_autocomplete()
713719
self._set_compose_attributes_to_defaults()
714720
self.view.controller.exit_editor_mode()
715721
self.main_view(False)
@@ -724,6 +730,11 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
724730
is_command_key("AUTOCOMPLETE", key)
725731
or is_command_key("AUTOCOMPLETE_REVERSE", key)
726732
):
733+
# As is, this exits autocomplete even if the user chooses to resume compose.
734+
# Including a check for "EXIT_COMPOSE" in the above logic would avoid
735+
# resetting the footer until actually exiting compose, but autocomplete
736+
# itself does not continue on resume with such a solution.
737+
# TODO: Fully implement resuming of autocomplete upon resuming compose.
727738
self._set_default_footer_after_autocomplete()
728739

729740
if is_command_key("SEND_MESSAGE", key):
@@ -807,8 +818,24 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
807818
"Cannot narrow to message without specifying recipients."
808819
)
809820
elif is_command_key("EXIT_COMPOSE", key):
821+
saved_draft = self.model.session_draft_message()
810822
self.send_stop_typing_status()
811-
self.exit_compose_box()
823+
824+
compose_not_in_edit_mode = self.msg_edit_state is None
825+
compose_box_content = self.msg_write_box.edit_text
826+
saved_draft_content = saved_draft.get("content") if saved_draft else None
827+
828+
exceeds_max_length = (
829+
len(compose_box_content) >= MAX_MESSAGE_LENGTH_CONFIRMATION_POPUP
830+
)
831+
not_saved_as_draft = (
832+
saved_draft is None or compose_box_content != saved_draft_content
833+
)
834+
835+
if compose_not_in_edit_mode and exceeds_max_length and not_saved_as_draft:
836+
self.view.controller.exit_compose_confirmation_popup()
837+
else:
838+
self.exit_compose_box()
812839
elif is_command_key("MARKDOWN_HELP", key):
813840
self.view.controller.show_markdown_help()
814841
return key

0 commit comments

Comments
 (0)