Skip to content

Commit c766f8a

Browse files
committed
buttons: Add support for PM narrow links in MessageLinkButton.
Currently webapp supports several url types, such as: narrow/pm-with/1,2-pm narrow/pm-with/1,2-group narrow/pm-with/1,2,3-pm narrow/pm-with/1,2,3-group narrow/pm-with/1-user1 narrow/pm-with/1-bot-name (near parameter supported for each) This commit adds support for narrow using above mentioned url types. Helper method added: _decode_pm_data(). TypeDict added: DecodedPM(Literal, List) Tests added and amended.
1 parent b7434b5 commit c766f8a

File tree

2 files changed

+213
-6
lines changed

2 files changed

+213
-6
lines changed

tests/ui_tools/test_buttons.py

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44
from pytest import param as case
5+
from typing_extensions import Literal
56
from urwid import AttrMap, Overlay
67

78
from zulipterminal.config.keys import keys_for_command
@@ -364,6 +365,34 @@ def test__decode_stream_data(self, stream_data, expected_response):
364365

365366
assert return_value == expected_response
366367

368+
@pytest.mark.parametrize(
369+
"pm_data, expected_response",
370+
[
371+
("1,2-pm", dict(recipient_ids=[1, 2], type=Literal["pm"])),
372+
("1,2-group", dict(recipient_ids=[1, 2], type=Literal["pm"])),
373+
("1,2,3-pm", dict(recipient_ids=[1, 2, 3], type=Literal["group"])),
374+
("1,2,3-group", dict(recipient_ids=[1, 2, 3], type=Literal["group"])),
375+
("1-user1", dict(recipient_ids=[1], type=Literal["pm"])),
376+
("1-user2", dict(recipient_ids=[1], type=Literal["pm"])),
377+
("1-bot-name", dict(recipient_ids=[1], type=Literal["pm"])),
378+
("1-bot;name", dict(recipient_ids=[1], type=Literal["pm"])),
379+
],
380+
ids=[
381+
"pm_with_two_recipients",
382+
"group_pm_with_two_recipients",
383+
"pm_with_more_than_two_recipients",
384+
"group_pm_with_more_than_two_recipients",
385+
"pm_exposed_format_1_ordinary",
386+
"pm_exposed_format_1_ambigous",
387+
"pm_with_bot_exposed_format_1_ordinary",
388+
"pm_with_bot_exposed_format_1_ambigous",
389+
],
390+
)
391+
def test__decode_pm_data(self, pm_data, expected_response):
392+
return_value = MessageLinkButton._decode_pm_data(pm_data, 2)
393+
394+
assert return_value == expected_response
395+
367396
@pytest.mark.parametrize(
368397
"message_id, expected_return_value",
369398
[
@@ -415,6 +444,56 @@ def test__decode_message_id(self, message_id, expected_return_value):
415444
"stream": {"stream_id": 1, "stream_name": None},
416445
},
417446
),
447+
(
448+
SERVER_URL + "/#narrow/pm-with/1,2-pm",
449+
{
450+
"narrow": "pm-with",
451+
"pm_with": {"type": Literal["pm"], "recipient_ids": [1, 2]},
452+
},
453+
),
454+
(
455+
SERVER_URL + "/#narrow/pm-with/1,2-group",
456+
{
457+
"narrow": "pm-with",
458+
"pm_with": {"type": Literal["pm"], "recipient_ids": [1, 2]},
459+
},
460+
),
461+
(
462+
SERVER_URL + "/#narrow/pm-with/1-user1",
463+
{
464+
"narrow": "pm-with",
465+
"pm_with": {"type": Literal["pm"], "recipient_ids": [1]},
466+
},
467+
),
468+
(
469+
SERVER_URL + "/#narrow/pm-with/1-bot-name",
470+
{
471+
"narrow": "pm-with",
472+
"pm_with": {"type": Literal["pm"], "recipient_ids": [1]},
473+
},
474+
),
475+
(
476+
SERVER_URL + "/#narrow/pm-with/1,2-pm/near/1",
477+
{
478+
"narrow": "pm-with:near",
479+
"message_id": 1,
480+
"pm_with": {"type": Literal["pm"], "recipient_ids": [1, 2]},
481+
},
482+
),
483+
(
484+
SERVER_URL + "/#narrow/pm-with/1,2,3-pm",
485+
{
486+
"narrow": "pm-with",
487+
"pm_with": {"type": Literal["group"], "recipient_ids": [1, 2, 3]},
488+
},
489+
),
490+
(
491+
SERVER_URL + "/#narrow/pm-with/1,2,3-group",
492+
{
493+
"narrow": "pm-with",
494+
"pm_with": {"type": Literal["group"], "recipient_ids": [1, 2, 3]},
495+
},
496+
),
418497
(SERVER_URL + "/#narrow/foo", {}),
419498
(SERVER_URL + "/#narrow/stream/", {}),
420499
(SERVER_URL + "/#narrow/stream/1-Stream-1/topic/", {}),
@@ -427,6 +506,13 @@ def test__decode_message_id(self, message_id, expected_return_value):
427506
"topic_narrow_link",
428507
"stream_near_narrow_link",
429508
"topic_near_narrow_link",
509+
"pm_with_two_recipients_narrow_link",
510+
"group_pm_with_two_recipients_narrow_link",
511+
"pm_exposed_format_1_narrow_link",
512+
"pm_with_bot_exposed_format_1_narrow_link",
513+
"common_pm_near_narrow_link",
514+
"pm_with_more_than_two_recipients_narrow_link",
515+
"group_pm_with_more_than_two_recipients_narrow_link",
430516
"invalid_narrow_link_1",
431517
"invalid_narrow_link_2",
432518
"invalid_narrow_link_3",
@@ -435,7 +521,7 @@ def test__decode_message_id(self, message_id, expected_return_value):
435521
],
436522
)
437523
def test__parse_narrow_link(self, link, expected_parsed_link):
438-
return_value = MessageLinkButton._parse_narrow_link(link)
524+
return_value = MessageLinkButton._parse_narrow_link(link, 1)
439525

440526
assert return_value == expected_parsed_link
441527

@@ -667,7 +753,12 @@ def test__validate_and_patch_stream_data(
667753
assert error == expected_error
668754

669755
@pytest.mark.parametrize(
670-
"parsed_link, narrow_to_stream_called, narrow_to_topic_called",
756+
[
757+
"parsed_link",
758+
"narrow_to_stream_called",
759+
"narrow_to_topic_called",
760+
"narrow_to_user_called",
761+
],
671762
[
672763
(
673764
{
@@ -676,6 +767,7 @@ def test__validate_and_patch_stream_data(
676767
},
677768
True,
678769
False,
770+
False,
679771
),
680772
(
681773
{
@@ -685,6 +777,7 @@ def test__validate_and_patch_stream_data(
685777
},
686778
False,
687779
True,
780+
False,
688781
),
689782
(
690783
{
@@ -694,6 +787,7 @@ def test__validate_and_patch_stream_data(
694787
},
695788
True,
696789
False,
790+
False,
697791
),
698792
(
699793
{
@@ -704,29 +798,80 @@ def test__validate_and_patch_stream_data(
704798
},
705799
False,
706800
True,
801+
False,
802+
),
803+
(
804+
{
805+
"narrow": "pm-with",
806+
"pm_with": {"type": Literal["pm"], "recipient_ids": [1, 2]},
807+
},
808+
False,
809+
False,
810+
True,
811+
),
812+
(
813+
{
814+
"narrow": "pm-with",
815+
"pm_with": {"type": Literal["group"], "recipient_ids": [1, 2, 3]},
816+
},
817+
False,
818+
False,
819+
True,
820+
),
821+
(
822+
{
823+
"narrow": "pm-with:near",
824+
"message_id": 1,
825+
"pm_with": {"type": Literal["pm"], "recipient_ids": [1, 2]},
826+
},
827+
False,
828+
False,
829+
True,
830+
),
831+
(
832+
{
833+
"narrow": "pm-with:near",
834+
"message_id": 1,
835+
"pm_with": {"type": Literal["group"], "recipient_ids": [1, 2, 3]},
836+
},
837+
False,
838+
False,
839+
True,
707840
),
708841
],
709842
ids=[
710843
"stream_narrow",
711844
"topic_narrow",
712845
"stream_near_narrow",
713846
"topic_near_narrow",
847+
"pm_narrow",
848+
"group_pm_narrow",
849+
"pm_near_narrow",
850+
"group_pm_near_narrow",
714851
],
715852
)
716853
def test__switch_narrow_to(
717854
self,
718855
parsed_link,
719856
narrow_to_stream_called,
720857
narrow_to_topic_called,
858+
narrow_to_user_called,
721859
):
722860
mocked_button = self.message_link_button()
861+
# For PM narrow switch
862+
mocked_button.model.user_id_email_dict = {
863+
864+
865+
866+
}
723867

724868
mocked_button._switch_narrow_to(parsed_link)
725869

726870
assert (
727871
mocked_button.controller.narrow_to_stream.called == narrow_to_stream_called
728872
)
729873
assert mocked_button.controller.narrow_to_topic.called == narrow_to_topic_called
874+
assert mocked_button.controller.narrow_to_user.called == narrow_to_user_called
730875

731876
@pytest.mark.parametrize(
732877
"error, report_error_called, _switch_narrow_to_called, exit_popup_called",

zulipterminal/ui_tools/buttons.py

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import re
22
from functools import partial
3-
from typing import Any, Callable, Dict, Optional, Tuple, Union, cast
3+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
44
from urllib.parse import urljoin, urlparse
55

66
import urwid
7-
from typing_extensions import TypedDict
7+
from typing_extensions import Literal, TypedDict
88

99
from zulipterminal.api_types import EditPropagateMode
1010
from zulipterminal.config.keys import is_command_key, primary_key_for_command
@@ -322,9 +322,15 @@ class DecodedStream(TypedDict):
322322
stream_name: Optional[str]
323323

324324

325+
class DecodedPM(TypedDict):
326+
type: Literal["pm", "group"]
327+
recipient_ids: List[int]
328+
329+
325330
class ParsedNarrowLink(TypedDict, total=False):
326331
narrow: str
327332
stream: DecodedStream
333+
pm_with: DecodedPM
328334
topic_name: str
329335
message_id: Optional[int]
330336

@@ -375,6 +381,27 @@ def _decode_stream_data(encoded_stream_data: str) -> DecodedStream:
375381
stream_name = hash_util_decode(encoded_stream_data)
376382
return DecodedStream(stream_id=None, stream_name=stream_name)
377383

384+
@staticmethod
385+
def _decode_pm_data(encoded_pm_data: str, user_id: int) -> DecodedPM:
386+
"""
387+
Returns a dict with PM type and IDs of PM recipients.
388+
"""
389+
# Recipient ids (seperated by `,`) are of prime interest.
390+
recipient_ids, *_ = encoded_pm_data.split("-")
391+
recipient_ids_list = list(map(int, recipient_ids.split(",")))
392+
393+
no_of_recipients = len(recipient_ids_list)
394+
# Bump no. of recipients tp include current user_id if not already
395+
# present
396+
if user_id not in recipient_ids_list:
397+
no_of_recipients += 1
398+
399+
# Currently webapp uses `pm` and `group` suffix interchangeably.
400+
# Treat more-than-2 pms to group pms to avoid confusion.
401+
pm_type = "pm" if no_of_recipients < 3 else "group"
402+
403+
return DecodedPM(type=Literal[pm_type], recipient_ids=recipient_ids_list)
404+
378405
@staticmethod
379406
def _decode_message_id(message_id: str) -> Optional[int]:
380407
"""
@@ -386,7 +413,7 @@ def _decode_message_id(message_id: str) -> Optional[int]:
386413
return None
387414

388415
@classmethod
389-
def _parse_narrow_link(cls, link: str) -> ParsedNarrowLink:
416+
def _parse_narrow_link(cls, link: str, user_id: int) -> ParsedNarrowLink:
390417
"""
391418
Returns either a dict with narrow parameters for supported links or an
392419
empty dict.
@@ -400,6 +427,12 @@ def _parse_narrow_link(cls, link: str) -> ParsedNarrowLink:
400427
# {encoded.20topic.20name}
401428
# d. narrow/stream/[{stream_id}-]{stream-name}/topic/
402429
# {encoded.20topic.20name}/near/{message_id}
430+
# e. narrow/pm-with/[{recipient_ids},]-{pm-type}
431+
# f. narrow/pm-with/[{recipient_ids},]-{pm-type}/near/{message_id}
432+
# g. narrow/pm-with/{user_id}-user{user_id}
433+
# h. narrow/pm-with/{user_id}-user{user_id}/near/{message_id}
434+
# i. narrow/pm-with/{bot_id}-{bot-name}
435+
# j. narrow/pm-with/{bot_id}-{bot-name}/near/{message_id}
403436
fragments = urlparse(link.rstrip("/")).fragment.split("/")
404437
len_fragments = len(fragments)
405438
parsed_link = ParsedNarrowLink()
@@ -440,6 +473,19 @@ def _parse_narrow_link(cls, link: str) -> ParsedNarrowLink:
440473
message_id=message_id,
441474
)
442475

476+
elif (
477+
len_fragments == 5 and fragments[1] == "pm-with" and fragments[3] == "near"
478+
):
479+
pm_data = cls._decode_pm_data(fragments[2], user_id)
480+
message_id = cls._decode_message_id(fragments[4])
481+
parsed_link = dict(
482+
narrow="pm-with:near", pm_with=pm_data, message_id=message_id
483+
)
484+
485+
elif len_fragments == 3 and fragments[1] == "pm-with":
486+
pm_data = cls._decode_pm_data(fragments[2], user_id)
487+
parsed_link = dict(narrow="pm-with", pm_with=pm_data)
488+
443489
return parsed_link
444490

445491
def _validate_and_patch_stream_data(self, parsed_link: ParsedNarrowLink) -> str:
@@ -528,13 +574,29 @@ def _switch_narrow_to(self, parsed_link: ParsedNarrowLink) -> None:
528574
topic_name=parsed_link["topic_name"],
529575
contextual_message_id=parsed_link["message_id"],
530576
)
577+
elif "pm-with:near" == narrow:
578+
emails = list(
579+
self.model.user_id_email_dict.get(user_id)
580+
for user_id in parsed_link["pm_with"]["recipient_ids"]
581+
)
582+
self.controller.narrow_to_user(
583+
recipient_emails=emails,
584+
contextual_message_id=parsed_link["message_id"],
585+
)
586+
587+
elif "pm-with" == narrow:
588+
emails = list(
589+
self.model.user_id_email_dict.get(user_id)
590+
for user_id in parsed_link["pm_with"]["recipient_ids"]
591+
)
592+
self.controller.narrow_to_user(recipient_emails=emails)
531593

532594
def handle_narrow_link(self) -> None:
533595
"""
534596
Narrows to the respective narrow if the narrow link is valid or updates
535597
the footer with an appropriate validation error message.
536598
"""
537-
parsed_link = self._parse_narrow_link(self.link)
599+
parsed_link = self._parse_narrow_link(self.link, self.model.user_id)
538600
error = self._validate_narrow_link(parsed_link)
539601

540602
if error:

0 commit comments

Comments
 (0)