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

Add DM panel to the left side view (rebased #1416) #1532

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,15 @@ def clean_custom_profile_data_fixture() -> List[CustomProfileData]:
]


@pytest.fixture
def sorted_recent_dms_fixture() -> List[Dict[str, Any]]:
return [
{"max_message_id": 4, "user_ids": []},
{"max_message_id": 3, "user_ids": [2]},
{"max_message_id": 2, "user_ids": [1]},
]


@pytest.fixture
def initial_data(
logged_on_user: Dict[str, Any],
Expand Down Expand Up @@ -1050,6 +1059,11 @@ def initial_data(
"zulip_feature_level": MINIMUM_SUPPORTED_SERVER_VERSION[1],
"starred_messages": [1117554, 1117558, 1117574],
"custom_profile_fields": custom_profile_fields_fixture,
"recent_private_conversations": [
{"max_message_id": 4, "user_ids": []},
{"max_message_id": 2, "user_ids": [1]},
{"max_message_id": 3, "user_ids": [2]},
],
}


Expand Down Expand Up @@ -1469,6 +1483,7 @@ def classified_unread_counts() -> Dict[str, Any]:
return {
"all_msg": 12,
"all_pms": 8,
"all_stream_msg": 4,
"unread_topics": {
(1000, "Some general unread topic"): 3,
(99, "Some private unread topic"): 1,
Expand Down
14 changes: 14 additions & 0 deletions tests/helper/test_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ def test_sort_unread_topics(
[["Some general stream", "Some general unread topic"]],
{
"all_msg": 8,
"all_stream_msg": 0,
"streams": {99: 1},
"unread_topics": {(99, "Some private unread topic"): 1},
"all_mentions": 0,
Expand All @@ -298,17 +299,30 @@ def test_sort_unread_topics(
[["Secret stream", "Some private unread topic"]],
{
"all_msg": 8,
"all_stream_msg": 0,
"streams": {1000: 3},
"unread_topics": {(1000, "Some general unread topic"): 3},
"all_mentions": 0,
},
),
({1}, [], {"all_mentions": 0}),
(
{},
[["Some general stream", "Some general unread topic"]],
{
"all_msg": 9,
"all_stream_msg": 1,
"streams": {99: 1},
"unread_topics": {(99, "Some private unread topic"): 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",
"mute_general_stream_topic",
],
)
def test_classify_unread_counts(
Expand Down
7 changes: 7 additions & 0 deletions tests/model/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def test_init(
realm_emojis_data,
zulip_emoji,
stream_dict,
sorted_recent_dms_fixture,
):
assert hasattr(model, "controller")
assert hasattr(model, "client")
Expand All @@ -94,6 +95,7 @@ def test_init(
assert model.users == []
self.classify_unread_counts.assert_called_once_with(model)
assert model.unread_counts == []
assert model.recent_dms == sorted_recent_dms_fixture
assert model.active_emoji_data == OrderedDict(
sorted(
{**unicode_emojis, **realm_emojis_data, **zulip_emoji}.items(),
Expand Down Expand Up @@ -261,6 +263,7 @@ def test_register_initial_desired_events(self, mocker, initial_data):
"user_settings",
"realm_emoji",
"custom_profile_fields",
"recent_private_conversations",
"zulip_version",
]
model.client.register.assert_called_once_with(
Expand Down Expand Up @@ -1918,6 +1921,7 @@ def test__handle_message_event_with_Falsey_log(
)
model.notify_user = mocker.Mock()
event = {"type": "message", "message": message_fixture}
model.user_id = 5140

model._handle_message_event(event)

Expand All @@ -1937,6 +1941,7 @@ def test__handle_message_event_with_valid_log(self, mocker, model, message_fixtu
)
model.notify_user = mocker.Mock()
event = {"type": "message", "message": message_fixture}
model.user_id = 5140

model._handle_message_event(event)

Expand Down Expand Up @@ -1964,6 +1969,7 @@ def test__handle_message_event_with_flags(self, mocker, model, message_fixture):
"message": message_fixture,
"flags": ["read", "mentioned"],
}
model.user_id = 5140

model._handle_message_event(event)

Expand Down Expand Up @@ -2093,6 +2099,7 @@ def test__handle_message_event(
mocker.patch(MODULE + ".index_messages", return_value={})
mocker.patch(MODULE + ".create_msg_box_list", return_value=["msg_w"])
set_count = mocker.patch(MODULE + ".set_count")
mocker.patch(MODEL + "._update_recent_dms")
self.controller.view.message_view = mocker.Mock(log=[])
(
self.controller.view.left_panel.is_in_topic_view_with_stream_id.return_value
Expand Down
12 changes: 9 additions & 3 deletions tests/ui/test_ui_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ def test_init(self, mocker, stream_view):
assert stream_view.streams_btn_list == self.streams_btn_list
assert stream_view.stream_search_box
self.stream_search_box.assert_called_once_with(
stream_view, "SEARCH_STREAMS", stream_view.update_streams
stream_view, "SEARCH_STREAMS", stream_view.update_streams, label="streams"
)

@pytest.mark.parametrize(
Expand Down Expand Up @@ -605,7 +605,7 @@ def test_init(self, mocker, topic_view):
assert topic_view.view == self.view
assert topic_view.topic_search_box
self.topic_search_box.assert_called_once_with(
topic_view, "SEARCH_TOPICS", topic_view.update_topics
topic_view, "SEARCH_TOPICS", topic_view.update_topics, label="topics"
)
self.header_list.assert_called_once_with(
[
Expand Down Expand Up @@ -1156,6 +1156,7 @@ def test_menu_view(self, mocker):
starred_button = mocker.patch(VIEWS + ".StarredButton")
mocker.patch(VIEWS + ".urwid.ListBox")
mocker.patch(VIEWS + ".urwid.SimpleFocusListWalker")
mocker.patch(VIEWS + ".urwid.LineBox")
mocker.patch(VIEWS + ".StreamButton.mark_muted")
left_col_view = LeftColumnView(self.view)
home_button.assert_called_once_with(
Expand All @@ -1167,16 +1168,21 @@ def test_menu_view(self, mocker):
)

@pytest.mark.parametrize("pinned", powerset([1, 2, 99, 999, 1000]))
def test_streams_view(self, mocker, streams, pinned):
def test_stream_panel(self, mocker, streams, pinned):
self.view.unpinned_streams = [s for s in streams if s["id"] not in pinned]
self.view.pinned_streams = [s for s in streams if s["id"] in pinned]
stream_button = mocker.patch(VIEWS + ".StreamButton")
stream_panel_button = mocker.patch(VIEWS + ".StreamPanelButton")
mocker.patch(VIEWS + ".StreamsView")
mocker.patch(VIEWS + ".urwid.LineBox")
divider = mocker.patch(VIEWS + ".StreamsViewDivider")

LeftColumnView(self.view)

stream_panel_button.assert_called_once_with(
controller=self.view.controller, count=mocker.ANY
)

if pinned:
assert divider.called
else:
Expand Down
12 changes: 12 additions & 0 deletions tests/ui_tools/test_buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
PMButton,
StarredButton,
StreamButton,
StreamPanelButton,
TopButton,
TopicButton,
UserButton,
Expand Down Expand Up @@ -198,6 +199,17 @@ def test_count_style_init_argument_value(
assert starred_button.suffix_style == "starred_count"


class TestStreamPanelButton:
def test_button_text_length(self, mocker: MockerFixture, count: int = 10) -> None:
stream_panel_button = StreamPanelButton(controller=mocker.Mock(), count=count)
assert len(stream_panel_button.label_text) == 20

def test_button_text_title(self, mocker: MockerFixture, count: int = 10) -> None:
stream_panel_button = StreamPanelButton(controller=mocker.Mock(), count=count)
title_text = stream_panel_button.label_text[:-3].strip()
assert title_text == "Stream messages"


class TestStreamButton:
def test_mark_muted(
self, mocker: MockerFixture, stream_button: StreamButton
Expand Down
2 changes: 2 additions & 0 deletions zulipterminal/config/symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
MENTIONED_MESSAGES_MARKER = "@"
STARRED_MESSAGES_MARKER = "*"

# Used in View buttons, and short form of DMs (not for streams)
DIRECT_MESSAGE_MARKER = "§" # SECTION SIGN, U+00A7 (Latin-1 supplement)
STREAM_MESSAGE_MARKER = ">"

STREAM_MARKER_PRIVATE = "P"
STREAM_MARKER_PUBLIC = "#"
Expand Down
7 changes: 7 additions & 0 deletions zulipterminal/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ class Index(TypedDict):
class UnreadCounts(TypedDict):
all_msg: int
all_pms: int
all_stream_msg: int
all_mentions: int
unread_topics: Dict[Tuple[int, str], int] # stream_id, topic
unread_pms: Dict[int, int] # sender_id
Expand Down Expand Up @@ -239,6 +240,7 @@ def _set_count_in_view(
user_buttons_list = controller.view.user_w.users_btn_list
all_msg = controller.view.home_button
all_pm = controller.view.pm_button
all_stream = controller.view.stream_button
all_mentioned = controller.view.mentioned_button
for message in changed_messages:
user_id = message["sender_id"]
Expand Down Expand Up @@ -272,6 +274,9 @@ def _set_count_in_view(
for topic_button in topic_buttons_list:
if topic_button.topic_name == msg_topic:
topic_button.update_count(topic_button.count + new_count)
if add_to_counts:
unread_counts["all_stream_msg"] += new_count
all_stream.update_count(unread_counts["all_stream_msg"])
else:
for user_button in user_buttons_list:
if user_button.user_id == user_id:
Expand Down Expand Up @@ -488,6 +493,7 @@ def classify_unread_counts(model: Any) -> UnreadCounts:
unread_counts = UnreadCounts(
all_msg=0,
all_pms=0,
all_stream_msg=0,
all_mentions=0,
unread_topics=dict(),
unread_pms=dict(),
Expand Down Expand Up @@ -520,6 +526,7 @@ def classify_unread_counts(model: Any) -> UnreadCounts:
unread_counts["streams"][stream_id] += count
if stream_id not in model.muted_streams:
unread_counts["all_msg"] += count
unread_counts["all_stream_msg"] += count

# store unread count of group pms in `unread_huddles`
for group_pm in unread_msg_counts["huddles"]:
Expand Down
39 changes: 39 additions & 0 deletions zulipterminal/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ def __init__(self, controller: Any) -> None:
"user_settings",
"realm_emoji",
"custom_profile_fields",
"recent_private_conversations",
# zulip_version and zulip_feature_level are always returned in
# POST /register from Feature level 3.
"zulip_version",
Expand Down Expand Up @@ -180,6 +181,11 @@ def __init__(self, controller: Any) -> None:
self.users: List[MinimalUserData] = []
self._update_users_data_from_initial_data()

self.recent_dms: List[Dict[str, Any]] = self.initial_data[
"recent_private_conversations"
]
self._sort_recent_dms()

self.stream_dict: Dict[int, Any] = {}
self.muted_streams: Set[int] = set()
self.pinned_streams: List[StreamData] = []
Expand Down Expand Up @@ -1335,6 +1341,37 @@ def user_name_from_id(self, user_id: int) -> str:

return self.user_dict[user_email]["full_name"]

def _sort_recent_dms(self) -> None:
"""
Sorts the list of recent direct message conversations.
"""
self.recent_dms.sort(
key=lambda conversation: conversation["max_message_id"],
reverse=True,
)

def _update_recent_dms(self, message: Message) -> None:
"""
Updates the list of recent direct message conversations.
"""
msg_id = message["id"]
user_ids = [recipient["id"] for recipient in message["display_recipient"]]
user_ids.remove(self.user_id)
replaced = False
for dm in self.recent_dms:
if set(dm["user_ids"]) == set(user_ids) and msg_id > dm["max_message_id"]:
dm["max_message_id"] = msg_id
replaced = True
break
if not replaced:
self.recent_dms.append(
{
"user_ids": user_ids,
"max_message_id": msg_id,
}
)
self._sort_recent_dms()

def _subscribe_to_streams(self, subscriptions: List[Subscription]) -> None:
def make_reduced_stream_data(stream: Subscription) -> StreamData:
# stream_id has been changed to id.
Expand Down Expand Up @@ -1695,6 +1732,8 @@ def _handle_message_event(self, event: Event) -> None:
message["stream_id"], message["subject"], message["sender_id"]
)
self.controller.update_screen()
elif message["type"] == "private":
self._update_recent_dms(message)

# We can notify user regardless of whether UI is rendered or not,
# but depend upon the UI to indicate failures.
Expand Down
19 changes: 15 additions & 4 deletions zulipterminal/ui_tools/boxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -970,13 +970,24 @@ class PanelSearchBox(ReadlineEdit):
"""

def __init__(
self, panel_view: Any, search_command: str, update_function: Callable[..., None]
self,
panel_view: Any,
search_command: str,
update_function: Callable[..., None],
label: Optional[str] = None,
) -> None:
self.panel_view = panel_view
self.search_command = search_command
self.search_text = (
f" Search [{', '.join(display_keys_for_command(search_command))}]: "
)
if label:
self.search_text = (
f" Search {label} "
f"[{', '.join(display_keys_for_command(search_command))}]: "
)
else:
self.search_text = (
f" Search "
f"[{', '.join(display_keys_for_command(search_command))}]: "
)
self.search_error = urwid.AttrMap(
urwid.Text([" ", INVALID_MARKER, " No Results"]), "search_error"
)
Expand Down
Loading
Loading