diff --git a/tests/conftest.py b/tests/conftest.py index ad92c4cc71..2075b0dda0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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], @@ -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]}, + ], } @@ -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, diff --git a/tests/helper/test_helper.py b/tests/helper/test_helper.py index 2e32e5c10f..eb1cd7952b 100644 --- a/tests/helper/test_helper.py +++ b/tests/helper/test_helper.py @@ -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, @@ -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( diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 119b3aecab..9a6ca40f92 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -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") @@ -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(), @@ -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( @@ -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) @@ -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) @@ -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) @@ -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 diff --git a/tests/ui/test_ui_tools.py b/tests/ui/test_ui_tools.py index 0a4d9ec030..2084a4c9c6 100644 --- a/tests/ui/test_ui_tools.py +++ b/tests/ui/test_ui_tools.py @@ -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( @@ -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( [ @@ -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( @@ -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: diff --git a/tests/ui_tools/test_buttons.py b/tests/ui_tools/test_buttons.py index adc2faa127..9efe034cce 100644 --- a/tests/ui_tools/test_buttons.py +++ b/tests/ui_tools/test_buttons.py @@ -16,6 +16,7 @@ PMButton, StarredButton, StreamButton, + StreamPanelButton, TopButton, TopicButton, UserButton, @@ -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 diff --git a/zulipterminal/config/symbols.py b/zulipterminal/config/symbols.py index f52dd85f7b..22da4d1f82 100644 --- a/zulipterminal/config/symbols.py +++ b/zulipterminal/config/symbols.py @@ -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 = "#" diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py index a71d055b74..c5af90d104 100644 --- a/zulipterminal/helper.py +++ b/zulipterminal/helper.py @@ -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 @@ -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"] @@ -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: @@ -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(), @@ -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"]: diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 1d7688cdeb..7b2c724a62 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -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", @@ -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] = [] @@ -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. @@ -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. diff --git a/zulipterminal/ui_tools/boxes.py b/zulipterminal/ui_tools/boxes.py index fa3dc1ffd6..024b15653e 100644 --- a/zulipterminal/ui_tools/boxes.py +++ b/zulipterminal/ui_tools/boxes.py @@ -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" ) diff --git a/zulipterminal/ui_tools/buttons.py b/zulipterminal/ui_tools/buttons.py index 91c428a2b7..25fe241b03 100644 --- a/zulipterminal/ui_tools/buttons.py +++ b/zulipterminal/ui_tools/buttons.py @@ -21,11 +21,16 @@ ALL_MESSAGES_MARKER, CHECK_MARK, DIRECT_MESSAGE_MARKER, + STREAM_MESSAGE_MARKER, MENTIONED_MESSAGES_MARKER, MUTE_MARKER, STARRED_MESSAGES_MARKER, ) -from zulipterminal.config.ui_mappings import EDIT_MODE_CAPTIONS, STREAM_ACCESS_TYPE +from zulipterminal.config.ui_mappings import ( + EDIT_MODE_CAPTIONS, + STATE_ICON, + STREAM_ACCESS_TYPE, +) from zulipterminal.helper import StreamData, hash_util_decode, process_media from zulipterminal.urwid_types import urwid_MarkupTuple, urwid_Size @@ -143,8 +148,10 @@ def __init__(self, *, controller: Any, count: int) -> None: ) -class PMButton(TopButton): - def __init__(self, *, controller: Any, count: int) -> None: +class DMPanelButton(TopButton): + def __init__( + self, *, controller: Any, count: int, show_function: Callable[[], Any] + ) -> None: button_text = f"Direct messages [{primary_display_key_for_command('ALL_PM')}]" super().__init__( @@ -152,10 +159,37 @@ def __init__(self, *, controller: Any, count: int) -> None: label_markup=(None, button_text), prefix_markup=("title", DIRECT_MESSAGE_MARKER), suffix_markup=("unread_count", ""), - show_function=controller.narrow_to_all_pm, + show_function=show_function, count=count, ) + def selectable(self) -> bool: + return False + + def activate(self, key: Any) -> None: + self.show_function() + + +class StreamPanelButton(TopButton): + def __init__( + self, *, controller: Any, count: int, show_function: Callable[[], Any] + ) -> None: + button_text = "Stream messages [S]" + super().__init__( + controller=controller, + label_markup=(None, button_text), + prefix_markup=("title", STREAM_MESSAGE_MARKER), + suffix_markup=("unread_count", ""), + show_function=show_function, + count=count, + ) + + def selectable(self) -> bool: + return False + + def activate(self, key: Any) -> None: + self.show_function() + class MentionedButton(TopButton): def __init__(self, *, controller: Any, count: int) -> None: @@ -268,6 +302,38 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: return super().keypress(size, key) +class DMButton(TopButton): + def __init__( + self, + *, + dm_data: Dict[str, Any], + controller: Any, + view: Any, + state_marker: str, + color: Optional[str] = None, + count: int, + ) -> None: + self.model = controller.model + self.count = count + self.view = view + self.users: str = dm_data["users"] + self.user_emails: List[str] = dm_data["emails"] + self.dm_type: str = dm_data["type"] + + narrow_function = partial( + controller.narrow_to_user, + recipient_emails=self.user_emails, + ) + super().__init__( + controller=controller, + prefix_markup=(color, state_marker), + label_markup=(None, self.users), + suffix_markup=("unread_count", count), + show_function=narrow_function, + count=count, + ) + + class UserButton(TopButton): def __init__( self, diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index c2034b3ef7..ab56958479 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -46,13 +46,15 @@ from zulipterminal.server_url import near_message_url from zulipterminal.ui_tools.boxes import PanelSearchBox from zulipterminal.ui_tools.buttons import ( + DMButton, + DMPanelButton, EmojiButton, HomeButton, MentionedButton, MessageLinkButton, - PMButton, StarredButton, StreamButton, + StreamPanelButton, TopicButton, UserButton, ) @@ -305,6 +307,45 @@ def read_message(self, index: int = -1) -> None: self.model.mark_message_ids_as_read(read_msg_ids) +class DMPanel(urwid.Pile): + def __init__( + self, + submenu_view: Optional[List[Any]], + view: Any, + show_function: Callable[[], Any], + ) -> None: + self.view = view + count = self.view.model.unread_counts.get("all_pms", 0) + self.view.pm_button = DMPanelButton( + controller=self.view.controller, count=count, show_function=show_function + ) + + if submenu_view: + self._contents = [ + ("pack", self.view.pm_button), + ("pack", urwid.Divider(div_char=SECTION_DIVIDER_LINE)), + submenu_view, + ] + focus_item = 2 + else: + self._contents = [ + ("pack", self.view.pm_button), + ] + focus_item = 0 + + super().__init__(self.contents, focus_item=focus_item) + + +class DMView(urwid.Frame): + def __init__(self, dm_btn_list: List[Any], view: Any) -> None: + self.view = view + self.log = urwid.SimpleFocusListWalker(dm_btn_list) + self.dm_btn_list = dm_btn_list + self.focus_index_before_search = 0 + list_box = urwid.ListBox(self.log) + super().__init__(list_box) + + class StreamsViewDivider(urwid.Divider): """ A custom urwid.Divider to visually separate pinned and unpinned streams. @@ -318,6 +359,38 @@ def __init__(self) -> None: super().__init__(div_char=PINNED_STREAMS_DIVIDER) +class StreamPanel(urwid.Pile): + def __init__( + self, + submenu_view: Optional[List[Any]], + view: Any, + show_function: Callable[[], Any], + ) -> None: + self.view = view + count = self.view.model.unread_counts.get("all_stream_msg", 0) + self.view.stream_button = StreamPanelButton( + controller=self.view.controller, + count=count, + show_function=show_function, + ) + + if submenu_view: + self._contents = [ + ("pack", self.view.stream_button), + ("pack", urwid.Divider(div_char=SECTION_DIVIDER_LINE)), + submenu_view, + ] + focus_item = 2 + else: + self._contents = [ + ("pack", self.view.stream_button), + ("pack", urwid.Divider(div_char=SECTION_DIVIDER_LINE)), + ] + focus_item = 0 + + super().__init__(self.contents, focus_item=focus_item) + + class StreamsView(urwid.Frame): def __init__(self, streams_btn_list: List[Any], view: Any) -> None: self.view = view @@ -326,7 +399,10 @@ def __init__(self, streams_btn_list: List[Any], view: Any) -> None: self.focus_index_before_search = 0 list_box = urwid.ListBox(self.log) self.stream_search_box = PanelSearchBox( - self, "SEARCH_STREAMS", self.update_streams + self, + "SEARCH_STREAMS", + self.update_streams, + label="streams", ) super().__init__( list_box, @@ -420,7 +496,10 @@ def __init__( self.focus_index_before_search = 0 self.list_box = urwid.ListBox(self.log) self.topic_search_box = PanelSearchBox( - self, "SEARCH_TOPICS", self.update_topics + self, + "SEARCH_TOPICS", + self.update_topics, + label="topics", ) self.header_list = urwid.Pile( [ @@ -782,19 +861,25 @@ def __init__(self, view: Any) -> None: self.view = view self.controller = view.controller self.menu_v = self.menu_view() + self.dm_v = self.dms_view() + self.dm_panel = self.dms_panel(None) self.stream_v = self.streams_view() - + self.stream_panel = self.streams_panel(self.stream_v) self.is_in_topic_view = False - contents = [(4, self.menu_v), self.stream_v] + self.is_in_dm_panel_view = False + contents = [ + (3, self.menu_v), + ("pack", urwid.Divider(COLUMN_TITLE_BAR_LINE)), + ("pack", self.dm_panel), + ("pack", urwid.Divider(COLUMN_TITLE_BAR_LINE)), + self.stream_panel, + ] super().__init__(contents) def menu_view(self) -> Any: count = self.model.unread_counts.get("all_msg", 0) self.view.home_button = HomeButton(controller=self.controller, count=count) - count = self.model.unread_counts.get("all_pms", 0) - self.view.pm_button = PMButton(controller=self.controller, count=count) - self.view.mentioned_button = MentionedButton( controller=self.controller, count=self.model.unread_counts["all_mentions"], @@ -807,13 +892,84 @@ def menu_view(self) -> Any: ) menu_btn_list = [ self.view.home_button, - self.view.pm_button, self.view.mentioned_button, self.view.starred_button, ] w = urwid.ListBox(urwid.SimpleFocusListWalker(menu_btn_list)) return w + def streams_panel(self, submenu_view: Any) -> Any: + self.view.stream_p = StreamPanel( + submenu_view, self.view, show_function=self.show_stream_panel + ) + return self.view.stream_p + + def dms_panel(self, submenu_view: Any) -> Any: + self.view.dm_p = DMPanel( + submenu_view, self.view, show_function=self.show_dm_panel + ) + return self.view.dm_p + + def dms_view(self) -> Any: + + def get_dm_unread_count(user_ids): + if len(user_ids) == 1: + count = self.model.unread_counts["unread_pms"].get( + user_ids[0], 0 + ) + else: + user_ids.append(self.model.user_id) + count = self.model.unread_counts["unread_huddles"].get( + frozenset(user_ids), 0 + ) + return count + + def get_dm_state_marker_and_color(user_emails): + if len(user_emails) == 1: + user = user_emails[0] + user_dict = self.model.user_dict + status = user_dict[user]["status"] + else: + status = "offline" + + state_marker = STATE_ICON[status] + color = f"user_{status}" + return state_marker, color + + dm_btn_list = [] + dm_list = self.model.recent_dms + for dm in dm_list: + user_emails = [] + user_names = [] + non_existing_user = False + for user_id in dm["user_ids"]: + try: + user_names.append(str(self.model.user_name_from_id(user_id))) + user_emails.append(self.model.user_id_email_dict[user_id]) + except RuntimeError: + non_existing_user = True + if non_existing_user is False: + users = ", ".join(user_names) + dm_data = { + "users": users, + "emails": user_emails, + "type": "dm" if len(dm["user_ids"]) == 1 else "group_dm", + } + count = get_dm_unread_count(dm["user_ids"]) + state_marker, color = get_dm_state_marker_and_color(user_emails) + dm_btn_list.append( + DMButton( + dm_data=dm_data, + controller=self.controller, + view=self.view, + state_marker=state_marker, + color=color, + count=count, + ) + ) + self.view.dm_w = DMView(dm_btn_list, self.view) + return self.view.dm_w + def streams_view(self) -> Any: streams_btn_list = [ StreamButton( @@ -845,20 +1001,7 @@ def streams_view(self) -> Any: } self.view.stream_w = StreamsView(streams_btn_list, self.view) - w = urwid.LineBox( - self.view.stream_w, - title="Streams", - title_attr="column_title", - tlcorner=COLUMN_TITLE_BAR_LINE, - tline=COLUMN_TITLE_BAR_LINE, - trcorner=COLUMN_TITLE_BAR_LINE, - blcorner="", - rline="", - lline="", - bline="", - brcorner="", - ) - return w + return self.view.stream_w def topics_view(self, stream_button: Any) -> Any: stream_id = stream_button.stream_id @@ -877,20 +1020,7 @@ def topics_view(self, stream_button: Any) -> Any: ] self.view.topic_w = TopicsView(topics_btn_list, self.view, stream_button) - w = urwid.LineBox( - self.view.topic_w, - title="Topics", - title_attr="column_title", - tlcorner=COLUMN_TITLE_BAR_LINE, - tline=COLUMN_TITLE_BAR_LINE, - trcorner=COLUMN_TITLE_BAR_LINE, - blcorner="", - rline="", - lline="", - bline="", - brcorner="", - ) - return w + return self.view.topic_w def is_in_topic_view_with_stream_id(self, stream_id: int) -> bool: return ( @@ -905,20 +1035,39 @@ def update_stream_view(self) -> None: def show_stream_view(self) -> None: self.is_in_topic_view = False - self.contents[1] = (self.stream_v, self.options(height_type="weight")) + self.stream_panel = self.streams_panel(self.stream_v) + self.contents[4] = (self.stream_panel, self.options(height_type="weight")) def show_topic_view(self, stream_button: Any) -> None: self.is_in_topic_view = True - self.contents[1] = ( - self.topics_view(stream_button), + self.stream_panel = self.streams_panel(self.topics_view(stream_button)) + self.contents[4] = ( + self.stream_panel, self.options(height_type="weight"), ) + def show_dm_panel(self) -> None: + self.dm_panel = self.dms_panel(self.dm_v) + self.contents[2] = (self.dm_panel, self.options(height_type="weight")) + self.stream_panel = self.streams_panel(None) + self.contents[4] = (self.stream_panel, self.options(height_type="pack")) + self.focus_position = 2 + self.is_in_dm_panel_view = True + + def show_stream_panel(self) -> None: + self.stream_panel = self.streams_panel(self.stream_v) + self.contents[4] = (self.stream_panel, self.options(height_type="weight")) + self.dm_panel = self.dms_panel(None) + self.contents[2] = (self.dm_panel, self.options(height_type="pack")) + self.focus_position = 4 + self.is_in_dm_panel_view = False + def keypress(self, size: urwid_Size, key: str) -> Optional[str]: - if is_command_key("SEARCH_STREAMS", key) or is_command_key( + if (is_command_key("SEARCH_STREAMS", key) or is_command_key( "SEARCH_TOPICS", key - ): - self.focus_position = 1 + )) and not self.is_in_dm_panel_view: + self.focus_position = 4 + self.view.stream_p.focus_position = 2 if self.is_in_topic_view: self.view.topic_w.keypress(size, key) else: @@ -926,6 +1075,10 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: return key elif is_command_key("GO_RIGHT", key): self.view.show_left_panel(visible=False) + elif is_command_key("ALL_PM", key): + self.show_dm_panel() + elif key == "S": + self.show_stream_panel() return super().keypress(size, key)