Skip to content

Views/BuddyView: Introduce BuddyView to show Recent PMs and Group PMs. #315

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ autohide=autohide
| Narrow to all starred messages | <kbd>f</kbd> |
| Next Unread Topic | <kbd>n</kbd> |
| Next Unread private message | <kbd>p</kbd> |
| Search Users | <kbd>w</kbd> |
| Toggle Buddy List (includes Recent PMs and Group PMs) | <kbd>b</kbd> |
| Search People | <kbd>w</kbd> |
| Search Messages | <kbd>/</kbd> |
| Search Streams | <kbd>q</kbd> |
| Mute/unmute Streams | <kbd>m</kbd> |
Expand Down
109 changes: 96 additions & 13 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,64 @@ def initial_data(logged_on_user, users_fixture, streams_fixture):
}],
'result': 'success',
'queue_id': '1522420755:786',
<<<<<<< HEAD
'realm_users': users_fixture,
=======
'realm_users': [{
'bot_type': None,
'is_bot': False,
'is_admin': False,
'email': logged_on_user['email'],
'full_name': logged_on_user['full_name'],
'user_id': logged_on_user['user_id'],
'avatar_url': None,
'is_active': True
}, {
'full_name': 'Boo Boo',
'email': 'boo@zulip.com',
'user_id': 5179,
'avatar_url': None,
'is_active': True,
'bot_type': None,
'is_bot': False,
'is_admin': False,
}, {
'full_name': 'Foo Foo',
'user_id': 5140,
'avatar_url': None,
'is_active': True,
'bot_type': None,
'is_bot': False,
'is_admin': False,
'email': 'foo@zulip.com',
}, {
'full_name': 'Bar Bar',
'user_id': 5180,
'avatar_url': None,
'is_active': True,
'bot_type': None,
'is_bot': False,
'is_admin': False,
'email': 'bar@zulip.com',
}, {
'full_name': 'Jari Winberg',
'user_id': 6086,
'avatar_url': None,
'is_active': True,
'bot_type': None,
'is_bot': False,
'is_admin': False,
'email': 'nyan.salmon+sns@gmail.com',
}, {
'bot_type': None,
'is_bot': False,
'is_admin': False,
'email': 'cloudserver2@hotmail.de',
'full_name': 'Test Account',
'user_id': 6085,
'is_active': True
}],
>>>>>>> BuddyView: Implement v1 of buddy list.
'cross_realm_bots': [{
'full_name': 'Notification Bot',
'timezone': '',
Expand Down Expand Up @@ -608,6 +665,24 @@ def user_dict(logged_on_user):
'status': 'inactive',
'user_id': 4
},
'bar@zulip.com': {
'email': 'bar@zulip.com',
'full_name': 'Bar Bar',
'status': 'inactive',
'user_id': 5180
},
'boo@zulip.com': {
'email': 'boo@zulip.com',
'full_name': 'Boo Boo',
'status': 'inactive',
'user_id': 5179
},
'foo@zulip.com': {
'email': 'foo@zulip.com',
'full_name': 'Foo Foo',
'status': 'inactive',
'user_id': 5140
},
}


Expand All @@ -619,15 +694,21 @@ def user_list(logged_on_user):
"""
# NOTE These are sorted active > idle, then according to full_name
return [{
'full_name': logged_on_user['full_name'],
'email': logged_on_user['email'],
'status': 'active',
'user_id': logged_on_user['user_id'],
'full_name': 'Tomás Farías',
'email': 'FOOBOO@gmail.com',
'user_id': 5827,
'status': 'active'
}, {
'email': 'emailgateway@zulip.com',
'full_name': 'Email Gateway',
'status': 'inactive',
'user_id': 6
'email': 'emailgateway@zulip.com',
'user_id': 6,
'status': 'inactive'
}, {
'full_name': 'Foo Foo',
'email': 'foo@zulip.com',
'user_id': 5140,
'status': 'inactive'
}, {
'full_name': 'Human 1',
'email': 'person1@example.com',
Expand All @@ -641,18 +722,20 @@ def user_list(logged_on_user):
}, {
'email': 'notification-bot@zulip.com',
'full_name': 'Notification Bot',
'status': 'inactive',
'user_id': 5
'email': 'notification-bot@zulip.com',
'user_id': 5,
'status': 'inactive'
}, {
'email': 'welcome-bot@zulip.com',
'full_name': 'Welcome Bot',
'status': 'inactive',
'user_id': 4
'email': 'welcome-bot@zulip.com',
'user_id': 4,
'status': 'inactive'
}, {
'email': 'feedback@zulip.com',
'full_name': 'Zulip Feedback Bot',
'status': 'inactive',
'user_id': 1
'email': 'feedback@zulip.com',
'user_id': 1,
'status': 'inactive'
}]


Expand Down
1 change: 1 addition & 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_register_initial_desired_events(self, mocker, initial_data):
mocker.patch('zulipterminal.model.Model.get_messages')
mocker.patch('zulipterminal.model.Model.get_all_users')
mocker.patch('zulipterminal.model.Model.fetch_all_topics')
mocker.patch('zulipterminal.model.classify_unread_counts')
self.client.register.return_value = initial_data

model = Model(self.controller)
Expand Down
139 changes: 139 additions & 0 deletions tests/ui/test_ui_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
ModListWalker,
PopUpConfirmationView,
MsgInfoView,
BuddyView,
)
from zulipterminal.ui_tools.boxes import MessageBox
from zulipterminal.ui_tools.buttons import (
TopButton,
StreamButton,
UserButton,
TopicButton,
GroupPMButton,
)
from zulipterminal.config.keys import keys_for_command
from zulipterminal.helper import powerset
Expand Down Expand Up @@ -1676,6 +1678,35 @@ def test_text_content(self, mocker,
count_str)
assert len(text[0]) == len(expected_text) == (width - 1)
assert text[0] == expected_text
assert top_button._w._original_widget._wrap_mode == 'any'
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is wrapping on any character better than wrapping on spaces?

Copy link
Member Author

Choose a reason for hiding this comment

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

Primarily, to keep computation of showing unread count at the right position simple. Otherwise, we will have to take wrapping of words into account which is currently not included in the code.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I can see that this could confuse the placement of the unread counts. That said, the styling of the words looks pretty bad if the text wraps mid-word.

I would put this 'why' into the commit title/summary in any case.

Copy link
Member Author

Choose a reason for hiding this comment

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

Of course! done 👍


@pytest.mark.parametrize('width, count, short_text', [
(15, 0, 'caption'),
(15, 1000, 'caption'),
])
def test_shrink_false(self, mocker,
width, count, short_text, caption='caption'):
show_function = mocker.Mock()
prefix = '\N{BULLET}'
mocker.patch(TOPBUTTON + ".set_muted_streams")
top_button = TopButton(controller=mocker.Mock(),
caption=caption,
show_function=show_function,
prefix_character=prefix,
width=width,
count=count,
shrink=False)

text = top_button._w._original_widget.get_text()
count_str = '' if count == 0 else str(count)
if count < 0:
count_str = 'M'
expected_text = ' {} {}{}{}'.format(
prefix, short_text,
(width - 4 - len(short_text) - len(count_str))*' ',
count_str)
assert len(text[0]) == len(expected_text) == (width - 1)
assert text[0] == expected_text


class TestStreamButton:
Expand Down Expand Up @@ -1872,3 +1903,111 @@ def test_init_calls_top_button(self, mocker, width, count, title,
assert topic_button.stream_name == stream_name
assert topic_button.stream_id == stream_id
assert topic_button.topic_name == title

class TestGroupPMButton:
@pytest.mark.parametrize('width, count, expected_text', [
(20, 0, ' • Foo Surname, Boo Surname '),
(20, 10, ' • Foo Surname, Boo Surname 10'),
(20, 999, ' • Foo Surname, Boo Surname 999'),
(40, 0, ' • Foo Surname, Boo Surname '),
(40, 10, ' • Foo Surname, Boo Surname 10'),
(40, 999, ' • Foo Surname, Boo Surname 999'),
])
def test_text_content(self, mocker, width, count, expected_text):
users = [{
'email': 'some_email',
'user_id': 5,
'full_name': "Foo Surname",
}, {
'email': 'some_other_email',
'user_id': 6,
'full_name': "Boo Surname",
}]
controller = mocker.Mock()
controller.model.user_id = 1
controller.model.unread_counts = {
'unread_pms': {
frozenset({1, 5, 6}): count,
}
}
mocker.patch(TOPBUTTON + ".set_muted_streams")
group_pm_btn = GroupPMButton(users,
controller=controller,
bw_width=width,
view=mocker.Mock(),
color=None)

text = group_pm_btn._w._original_widget.get_text()
assert text[0] == expected_text


class TestBuddyView:
@pytest.fixture(autouse=True)
def mock_external_classes(self, mocker):
self.controller = mocker.Mock()
mocker.patch(VIEWS + ".urwid.SimpleFocusListWalker", return_value=[])

@pytest.fixture
def buddy_view(self, mocker):
mocker.patch(VIEWS + ".BuddyView.update_users_to_display")
mocker.patch(VIEWS + ".BuddyView.get_button", return_value="BUTTON")
return BuddyView(self.controller)

@pytest.fixture
def group_pms_to_display(self):
return [[{
'email': 'boo@zulip.com',
'full_name': 'Boo Boo',
'status': 'inactive',
'user_id': 5179
}, {
'email': 'foo@zulip.com',
'full_name': 'Foo Foo',
'status': 'inactive',
'user_id': 5140
}, {
'email': 'bar@zulip.com',
'full_name': 'Bar Bar',
'status': 'inactive',
'user_id': 5180
}], [{
'email': 'boo@zulip.com',
'full_name': 'Boo Boo',
'status': 'inactive',
'user_id': 5179
}, {
'email': 'foo@zulip.com',
'full_name': 'Foo Foo',
'status': 'inactive',
'user_id': 5140
}]]

def test_update_users_to_display(self, mocker, user_dict,
group_pms_to_display,
user_id_email_dict, empty_index):
mocker.patch(VIEWS + ".BuddyView.get_button", return_value="BUTTON")
mocker.patch(VIEWS + ".BuddyView.fetch_more_pms")
self.controller.model.index = empty_index
self.controller.model.user_dict = user_dict
self.controller.model.user_id_email_dict = user_id_email_dict
buddy_view = BuddyView(self.controller)
assert buddy_view.fetch_more_pms.called
assert buddy_view.users_to_display == [] # FIXME: Add users
# We just want to see if the list contents are same
# FIXME: Test for 'active', 'idle' and 'offline' users.
for i, group_pm in enumerate(buddy_view.group_pms_to_display):
assert sorted(group_pm, key=lambda x: x['user_id']
) == sorted(group_pms_to_display[i],
key=lambda x: x['user_id'])

def test_keypress_any_key(self, buddy_view):
key = "a"
size = (200, 20)
buddy_view.keypress(size, key)
assert buddy_view.controller.loop.widget != buddy_view.controller.view

@pytest.mark.parametrize('key', keys_for_command("GO_BACK"))
def test_keypress_goback(self, buddy_view, key):
size = (200, 20)
buddy_view.keypress(size, key)
assert buddy_view.controller.loop.widget == buddy_view.controller.view
4 changes: 4 additions & 0 deletions zulipterminal/config/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
'help_text': 'Show/hide help menu',
'excluded_from_random_tips': True,
}),
('BUDDY_LIST', {
'keys': {'b'},
'help_text': 'Show/hide buddy list',
}),
('GO_BACK', {
'keys': {'esc'},
'help_text': 'Go Back',
Expand Down
24 changes: 23 additions & 1 deletion zulipterminal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@
from zulipterminal.model import Model, GetMessagesArgs, ServerConnectionFailure
from zulipterminal.ui import View, Screen
from zulipterminal.ui_tools.utils import create_msg_box_list
from zulipterminal.ui_tools.views import HelpView, MsgInfoView
from zulipterminal.ui_tools.views import (
HelpView,
MsgInfoView,
BuddyView,
BUDDY_VIEW_WIDTH
)
from zulipterminal.ui_tools.buttons import GroupPMButton
from zulipterminal.config.themes import ThemeSpec
from zulipterminal.ui_tools.views import PopUpConfirmationView

Expand Down Expand Up @@ -114,6 +120,17 @@ def show_msg_info(self, msg: Any) -> None:
msg_info_view = MsgInfoView(self, msg)
self.show_pop_up(msg_info_view,
"Message Information (up/down scrolls)")
def show_buddy_list(self) -> None:
cols, rows = self.loop.screen.get_cols_rows()
buddy_view = BuddyView(self)
self.loop.widget = urwid.Overlay(
urwid.LineBox(buddy_view),
self.view,
align='center',
valign='middle',
width=BUDDY_VIEW_WIDTH,
height=3*rows//4 + 2
)

def search_messages(self, text: str) -> None:
# Search for a text in messages
Expand Down Expand Up @@ -203,6 +220,11 @@ def narrow_to_user(self, button: Any) -> None:
if not emails and len(button.message['display_recipient']) == 1:
emails = [self.model.user_email]
user_emails = ', '.join(emails)
user_ids = {user['id']
for user in button.message['display_recipient']}
elif isinstance(button, GroupPMButton):
user_emails = button.emails
user_ids = button.user_ids
else:
user_emails = button.email

Expand Down
Loading