From 1dca8e956a38b647c28531e35650e296a5f2fa3f Mon Sep 17 00:00:00 2001 From: sahithch5 Date: Sun, 23 Mar 2025 21:57:40 +0530 Subject: [PATCH] added recent conversations --- zulipterminal/cli/run.py | 1 + zulipterminal/config/keys.py | 10 + zulipterminal/config/ui_sizes.py | 2 +- zulipterminal/core.py | 4 +- zulipterminal/model.py | 56 ++++- zulipterminal/ui.py | 3 + zulipterminal/ui_tools/buttons.py | 25 +- zulipterminal/ui_tools/views.py | 368 +++++++++++++++++++++++++----- 8 files changed, 402 insertions(+), 67 deletions(-) diff --git a/zulipterminal/cli/run.py b/zulipterminal/cli/run.py index a5cce2cf2d..4a0f45e90c 100755 --- a/zulipterminal/cli/run.py +++ b/zulipterminal/cli/run.py @@ -696,3 +696,4 @@ def print_setting(setting: str, data: SettingData, suffix: str = "") -> None: if __name__ == "__main__": main() + \ No newline at end of file diff --git a/zulipterminal/config/keys.py b/zulipterminal/config/keys.py index a86420a364..db7c5c49fa 100644 --- a/zulipterminal/config/keys.py +++ b/zulipterminal/config/keys.py @@ -152,6 +152,11 @@ class KeyBinding(TypedDict): 'help_text': 'Send a message', 'key_category': 'compose_box', }, + 'OPEN_RECENT_CONVERSATIONS': { + 'keys': ['^'], + 'help_text': 'Open recent conversations', + 'key_category': 'navigation', + }, 'SAVE_AS_DRAFT': { 'keys': ['meta s'], 'help_text': 'Save current message as a draft', @@ -209,6 +214,11 @@ class KeyBinding(TypedDict): 'help_text': 'Toggle topics in a stream', 'key_category': 'stream_list', }, + "SEARCH_RECENT_CONVERSATIONS": { + "keys": ["ctrl+f"], + "help_text": "Search recent conversations", + "key_category": "navigation" + }, 'ALL_MESSAGES': { 'keys': ['a', 'esc'], 'help_text': 'View all messages', diff --git a/zulipterminal/config/ui_sizes.py b/zulipterminal/config/ui_sizes.py index cdc95a7433..9459885c21 100644 --- a/zulipterminal/config/ui_sizes.py +++ b/zulipterminal/config/ui_sizes.py @@ -3,7 +3,7 @@ """ TAB_WIDTH = 3 -LEFT_WIDTH = 31 +LEFT_WIDTH = 32 RIGHT_WIDTH = 23 # These affect popup width-scaling, dependent upon window width diff --git a/zulipterminal/core.py b/zulipterminal/core.py index 61c5f79922..5a783cf04a 100644 --- a/zulipterminal/core.py +++ b/zulipterminal/core.py @@ -47,6 +47,7 @@ StreamInfoView, StreamMembersView, UserInfoView, + ) from zulipterminal.version import ZT_VERSION @@ -593,10 +594,11 @@ def copy_to_clipboard(self, text: str, text_category: str) -> None: def _narrow_to(self, anchor: Optional[int], **narrow: Any) -> None: already_narrowed = self.model.set_narrow(**narrow) + self.view.middle_column.set_view("messages") if already_narrowed and anchor is None: return - + msg_id_list = self.model.get_message_ids_in_current_narrow() # If no messages are found in the current narrow diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 1b8064fa87..c474161833 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -8,7 +8,7 @@ from collections import defaultdict from concurrent.futures import Future, ThreadPoolExecutor, wait from copy import deepcopy -from datetime import datetime +from datetime import datetime,timezone from typing import ( Any, Callable, @@ -116,7 +116,6 @@ def __init__(self, controller: Any) -> None: self.recipients: FrozenSet[Any] = frozenset() self.index = initial_index self.last_unread_pm = None - self.user_id = -1 self.user_email = "" self.user_full_name = "" @@ -315,6 +314,7 @@ def set_narrow( frozenset(["pm_with"]): [["pm-with", pm_with]], frozenset(["starred"]): [["is", "starred"]], frozenset(["mentioned"]): [["is", "mentioned"]], + } for narrow_param, narrow in valid_narrows.items(): if narrow_param == selected_params: @@ -856,6 +856,7 @@ def modernize_message_response(message: Message) -> Message: message["topic_links"] = topic_links return message + def fetch_message_history( self, message_id: int @@ -1094,7 +1095,56 @@ def get_other_subscribers_in_stream( if stream["name"] == stream_name if sub != self.user_id ] + def group_recent_conversations(self) -> List[Dict[str, Any]]: + """Return the 10 most recent stream conversations.""" + # Filter for stream messages + stream_msgs = [m for m in self.index["messages"].values() if m["type"] == "stream"] + if not stream_msgs: + return [] + + # Sort messages by timestamp (most recent first) + stream_msgs.sort(key=lambda x: x["timestamp"], reverse=True) + + # Group messages by stream and topic + convos = defaultdict(list) + for msg in stream_msgs[:50]: # Limit to 50 recent messages + convos[(msg["stream_id"], msg["subject"])].append(msg) + + # Process conversations into the desired format + processed_conversations = [] + now = datetime.now(timezone.utc) + for (stream_id, topic), msg_list in sorted( + convos.items(), key=lambda x: max(m["timestamp"] for m in x[1]), reverse=True + )[:10]: + # Map stream_id to stream name + + stream_name=self.stream_name_from_id(stream_id) + topic_name = topic if topic else "(no topic)" + + # Extract participants + participants = set() + for msg in msg_list: + participants.add(msg["sender_full_name"]) + + # Format timestamp (using the most recent message in the conversation) + most_recent_msg = max(msg_list, key=lambda x: x["timestamp"]) + timestamp = most_recent_msg["timestamp"] + conv_time = datetime.fromtimestamp(timestamp, tz=timezone.utc) + delta = now - conv_time + if delta.days > 0: + time_str = f"{delta.days} days ago" + else: + hours = delta.seconds // 3600 + time_str = f"{hours} hours ago" if hours > 0 else "just now" + + processed_conversations.append({ + "stream": stream_name, + "topic": topic_name, + "participants": list(participants), + "time": time_str, + }) + return processed_conversations def _clean_and_order_custom_profile_data( self, custom_profile_data: Dict[str, CustomFieldValue] ) -> List[CustomProfileData]: @@ -1417,6 +1467,8 @@ def stream_id_from_name(self, stream_name: str) -> int: if stream["name"] == stream_name: return stream_id raise RuntimeError("Invalid stream name.") + def stream_name_from_id(self,stream_id:int)->str: + return self.stream_dict[stream_id]["name"] def stream_access_type(self, stream_id: int) -> StreamAccessType: if stream_id not in self.stream_dict: diff --git a/zulipterminal/ui.py b/zulipterminal/ui.py index d0785bd928..a8dcfbf72a 100644 --- a/zulipterminal/ui.py +++ b/zulipterminal/ui.py @@ -72,6 +72,7 @@ def middle_column_view(self) -> Any: self.middle_column = MiddleColumnView( self, self.model, self.write_box, self.search_box ) + return urwid.LineBox( self.middle_column, title="Messages", @@ -273,6 +274,8 @@ def keypress(self, size: urwid_Box, key: str) -> Optional[str]: self.pm_button.activate(key) elif is_command_key("ALL_STARRED", key): self.starred_button.activate(key) + elif is_command_key("OPEN_RECENT_CONVERSATIONS", key): + self.time_button.activate(key) elif is_command_key("ALL_MENTIONS", key): self.mentioned_button.activate(key) elif is_command_key("SEARCH_PEOPLE", key): diff --git a/zulipterminal/ui_tools/buttons.py b/zulipterminal/ui_tools/buttons.py index 8e67d79adf..99ba7b580c 100644 --- a/zulipterminal/ui_tools/buttons.py +++ b/zulipterminal/ui_tools/buttons.py @@ -24,6 +24,7 @@ MENTIONED_MESSAGES_MARKER, MUTE_MARKER, STARRED_MESSAGES_MARKER, + TIME_MENTION_MARKER ) from zulipterminal.config.ui_mappings import EDIT_MODE_CAPTIONS, STREAM_ACCESS_TYPE from zulipterminal.helper import StreamData, hash_util_decode, process_media @@ -130,7 +131,7 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: class HomeButton(TopButton): def __init__(self, *, controller: Any, count: int) -> None: button_text = ( - f"All messages [{primary_display_key_for_command('ALL_MESSAGES')}]" + f"All messages [{primary_display_key_for_command('ALL_MESSAGES')}]" ) super().__init__( @@ -145,7 +146,7 @@ def __init__(self, *, controller: Any, count: int) -> None: class PMButton(TopButton): def __init__(self, *, controller: Any, count: int) -> None: - button_text = f"Direct messages [{primary_display_key_for_command('ALL_PM')}]" + button_text = f"Direct messages [{primary_display_key_for_command('ALL_PM')}]" super().__init__( controller=controller, @@ -160,7 +161,7 @@ def __init__(self, *, controller: Any, count: int) -> None: class MentionedButton(TopButton): def __init__(self, *, controller: Any, count: int) -> None: button_text = ( - f"Mentions [{primary_display_key_for_command('ALL_MENTIONS')}]" + f"Mentions [{primary_display_key_for_command('ALL_MENTIONS')}]" ) super().__init__( @@ -171,12 +172,28 @@ def __init__(self, *, controller: Any, count: int) -> None: show_function=controller.narrow_to_all_mentions, count=count, ) +class TimeMentionedButton(TopButton): + def __init__(self, *, controller: Any, count: int) -> None: + button_text = ( + f"Recent Conversations [{primary_display_key_for_command('OPEN_RECENT_CONVERSATIONS')}]" + ) + super().__init__( + controller=controller, + prefix_markup=("title", TIME_MENTION_MARKER), + label_markup=(None, button_text), + suffix_markup=("unread_count", f" ({count})" if count > 0 else ""), + show_function=self.show_recent_conversations, + count=count, + ) + + def show_recent_conversations(self) -> None: + self.controller.view.middle_column.set_view("recent") class StarredButton(TopButton): def __init__(self, *, controller: Any, count: int) -> None: button_text = ( - f"Starred messages [{primary_display_key_for_command('ALL_STARRED')}]" + f"Starred messages [{primary_display_key_for_command('ALL_STARRED')}]" ) super().__init__( diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 02b3afbd0b..4dca55df89 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -54,6 +54,7 @@ StarredButton, StreamButton, TopicButton, + TimeMentionedButton, UserButton, ) from zulipterminal.ui_tools.messages import MessageBox @@ -80,6 +81,7 @@ def set_focus(self, position: int) -> None: def _set_focus(self, index: int) -> None: # This method is called when directly setting focus via # self.focus = focus_position + if not self: # type: ignore[truthy-bool] # Implemented in base class self._focus = 0 return @@ -113,7 +115,6 @@ def __init__(self, model: Any, view: Any) -> None: # Initialize for reference self.focus_msg = 0 self.log = ModListWalker(contents=self.main_view(), action=self.read_message) - super().__init__(self.log) self.set_focus(self.focus_msg) # if loading new/old messages - True @@ -303,7 +304,149 @@ def read_message(self, index: int = -1) -> None: break self.model.mark_message_ids_as_read(read_msg_ids) +class RecentConversationsView(urwid.Frame): + def __init__(self, controller: Any) -> None: + self.controller = controller + self.model = controller.model + self.conversations = self.model.group_recent_conversations() + self.all_conversations = self.conversations.copy() + self.search_lock = threading.Lock() + self.empty_search = False + + self.search_box = PanelSearchBox(self, "SEARCH_RECENT_CONVERSATIONS", self.update_conversations) + search_header = urwid.Pile([self.search_box, urwid.Divider(SECTION_DIVIDER_LINE)]) + + self.log = urwid.SimpleFocusListWalker(self._build_body_contents(self.conversations)) + list_box = urwid.ListBox(self.log) + + super().__init__(list_box, header=search_header) + if len(self.log) > 1: + self.body.set_focus_valign("middle") # Fix: Call on self.body (the ListBox) + self.log.set_focus(1) # Focus on first conversation row + + def _build_body_contents(self, conversations: List[Dict[str, Any]]) -> List[urwid.Widget]: + contents = [] + header = self._build_header_row() + contents.append(header) + + for idx, conv in enumerate(conversations): + row = self._build_conversation_row(conv, idx) + contents.append(row) + + return contents + + def _build_header_row(self) -> urwid.Widget: + columns = [ + ("weight", 1, urwid.Text(("header", "Channel"))), + ("weight", 2, urwid.Text(("header", "Topic"))), + ("weight", 1, urwid.Text(("header", "Participants"))), + ("weight", 1, urwid.Text(("header", "Time"))), + ] + return urwid.Columns(columns, dividechars=1) + + def _build_conversation_row(self, conv: Dict[str, Any], idx: int) -> urwid.Widget: + stream = conv["stream"] + topic = conv["topic"] + participants = conv["participants"] + time = conv["time"] + + participant_text = f"{len(participants)} users" if len(participants) > 3 else ", ".join(participants) + + columns = [ + ("weight", 1, urwid.Text(f"#{stream}")), + ("weight", 2, urwid.Text(topic)), + ("weight", 1, urwid.Text(participant_text)), + ("weight", 1, urwid.Text(time)), + ] + row = urwid.Columns(columns, dividechars=1) + + button = urwid.Button("", on_press=self._on_row_click, user_data=conv) + button._label = row + button._w = urwid.AttrMap(row, None, "highlight") + + return button + def _on_row_click(self, button: urwid.Button, conv: Dict[str, Any]) -> None: + stream = conv["stream"] + topic = conv["topic"] + self.controller.narrow_to_topic(stream_name=stream, topic_name=topic) + self.controller.view.middle_column.set_view("messages") + + @asynch + def update_conversations(self, search_box: Any, new_text: str) -> None: + if not self.controller.is_in_editor_mode(): + return + + with self.search_lock: + new_text = new_text.lower() + filtered_conversations = [ + conv for conv in self.all_conversations + if (new_text in conv["stream"].lower() or + new_text in conv["topic"].lower() or + any(new_text in p.lower() for p in conv["participants"])) + ] + + self.empty_search = len(filtered_conversations) == 0 + + + self.log.clear() + if not self.empty_search: + self.log.extend(self._build_body_contents(filtered_conversations)) + else: + self.log.extend([self.search_box.search_error]) + + if len(self.log) > 1: + self.log.set_focus(1) + self.controller.update_screen() + + def mouse_event( + self, size: tuple[int, int], event: str, button: int, col: int, row: int, focus: bool + ) -> bool: + if event == "mouse press": + if button == 4: + for _ in range(5): + self.keypress(size, primary_key_for_command("GO_UP")) + return True + elif button == 5: + for _ in range(5): + self.keypress(size, primary_key_for_command("GO_DOWN")) + return True + return super().mouse_event(size, event, button, col, row, focus) + + def keypress(self, size: tuple[int, int], key: str) -> Optional[str]: + if is_command_key("SEARCH_RECENT_CONVERSATIONS", key): + self.set_focus("header") + self.search_box.set_caption(" ") + self.controller.enter_editor_mode_with(self.search_box) + return None + elif is_command_key("CLEAR_SEARCH", key): + self.search_box.reset_search_text() + self.log.clear() + self.log.extend(self._build_body_contents(self.all_conversations)) + self.set_focus("body") + if len(self.log) > 1: + self.log.set_focus(1) + self.controller.update_screen() + return None + elif is_command_key("GO_DOWN", key): + focused_widget, focused_position = self.log.get_focus() + if focused_position < len(self.log) - 1: + self.log.set_focus(focused_position + 1) + return None + elif is_command_key("GO_UP", key): + focused_widget, focused_position = self.log.get_focus() + if focused_position > 1: + self.log.set_focus(focused_position - 1) + return None + elif key == "enter": + focused_widget, focused_position = self.log.get_focus() + if focused_position > 0: + focused_widget._emit("click") + return None + elif is_command_key("ALL_MESSAGES", key): + self.controller.view.middle_column.set_view("messages") + return None + return super().keypress(size, key) class StreamsViewDivider(urwid.Divider): """ A custom urwid.Divider to visually separate pinned and unpinned streams. @@ -550,86 +693,120 @@ def mouse_event( self.keypress(size, primary_key_for_command("GO_DOWN")) return super().mouse_event(size, event, button, col, row, focus) - class MiddleColumnView(urwid.Frame): def __init__(self, view: Any, model: Any, write_box: Any, search_box: Any) -> None: - message_view = MessageView(model, view) self.model = model self.controller = model.controller self.view = view self.search_box = search_box - view.message_view = message_view - super().__init__(message_view, header=search_box, footer=write_box) + self.write_box = write_box + + self.message_view = MessageView(model, view) + view.message_view = self.message_view + self.recent_convo_view = RecentConversationsView(self.controller) + self.current_view = self.message_view + self.last_narrow = self.model.narrow + super().__init__(self.message_view, header=search_box, footer=write_box) + + def set_view(self, view_name: str) -> None: + if view_name == "recent": + self.current_view = self.recent_convo_view + header = None + else: + self.current_view = self.message_view + header = self.search_box + self.set_body(self.current_view) + self.set_header(header) + self.set_footer(self.write_box) + self.set_focus("body") + self.controller.update_screen() def update_message_list_status_markers(self) -> None: - for message_w in self.body.log: - message_box = message_w.original_widget + if isinstance(self.current_view, MessageView): + for message_w in self.body.log: + message_box = message_w.original_widget + message_box.update_message_author_status() + self.controller.update_screen() - message_box.update_message_author_status() + def check_narrow_and_switch_view(self) -> None: + """ + Check if the model's narrow has changed and switch to MessageView if necessary. + """ + current_narrow = self.model.narrow + if current_narrow != self.last_narrow and self.current_view != self.message_view: + self.set_view("messages") + self.last_narrow = current_narrow - self.controller.update_screen() + def keypress(self, size: tuple[int, int], key: str) -> Optional[str]: + self.check_narrow_and_switch_view() - def keypress(self, size: urwid_Size, key: str) -> Optional[str]: if self.focus_position in ["footer", "header"]: return super().keypress(size, key) - elif is_command_key("SEARCH_MESSAGES", key): + elif is_command_key("SEARCH_MESSAGES", key): self.controller.enter_editor_mode_with(self.search_box) self.set_focus("header") - return key + return None - elif is_command_key("REPLY_MESSAGE", key): - self.body.keypress(size, key) - if self.footer.focus is not None: - self.set_focus("footer") - self.footer.focus_position = 1 - return key + elif is_command_key("OPEN_RECENT_CONVERSATIONS", key): + self.set_view("recent") + return None - elif is_command_key("STREAM_MESSAGE", key): - self.body.keypress(size, key) - # For new streams with no previous conversation. - if self.footer.focus is None: - stream_id = self.model.stream_id - stream_dict = self.model.stream_dict - if stream_id is None: - self.footer.stream_box_view(0) - else: - self.footer.stream_box_view(caption=stream_dict[stream_id]["name"]) + elif is_command_key("ALL_MESSAGES", key): + self.controller.narrow_to_all_messages() + self.set_view("messages") + return None + + elif is_command_key("ALL_PM", key): + self.controller.narrow_to_all_pm() + self.set_view("messages") + return None + + elif is_command_key("ALL_STARRED", key): + self.controller.narrow_to_all_starred() + self.set_view("messages") + return None + + elif is_command_key("ALL_MENTIONS", key): + self.controller.narrow_to_all_mentions() + self.set_view("messages") + return None + + elif is_command_key("PRIVATE_MESSAGE", key): + self.footer.private_box_view() self.set_focus("footer") self.footer.focus_position = 0 - return key + return None - elif is_command_key("REPLY_AUTHOR", key): - self.body.keypress(size, key) - if self.footer.focus is not None: - self.set_focus("footer") - self.footer.focus_position = 1 - return key + elif is_command_key("GO_LEFT", key): + self.view.show_left_panel(visible=True) + return None - elif is_command_key("NEXT_UNREAD_TOPIC", key): - # narrow to next unread topic - focus = self.view.message_view.focus + elif is_command_key("GO_RIGHT", key): + self.view.show_right_panel(visible=True) + return None + + elif is_command_key("NEXT_UNREAD_TOPIC", key): narrow = self.model.narrow - if focus: - current_msg_id = focus.original_widget.message["id"] - stream_topic = self.model.next_unread_topic_from_message_id( - current_msg_id - ) - if stream_topic is None: - return key + if self.current_view == self.message_view and self.view.message_view.focus: + current_msg_id = self.view.message_view.focus.original_widget.message["id"] + stream_topic = self.model.next_unread_topic_from_message_id(current_msg_id) elif narrow[0][0] == "stream" and narrow[1][0] == "topic": stream_topic = self.model.next_unread_topic_from_message_id(None) else: - return key + stream_topic = self.model.next_unread_topic_from_message_id(None) + if stream_topic is None: + return key stream_id, topic = stream_topic self.controller.narrow_to_topic( stream_name=self.model.stream_dict[stream_id]["name"], topic_name=topic, ) - return key - elif is_command_key("NEXT_UNREAD_PM", key): - # narrow to next unread pm + self.set_view("messages") + return None + + elif is_command_key("NEXT_UNREAD_PM", key): pm = self.model.get_next_unread_pm() if pm is None: return key @@ -638,18 +815,87 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: recipient_emails=[email], contextual_message_id=pm, ) - elif is_command_key("PRIVATE_MESSAGE", key): - # Create new PM message - self.footer.private_box_view() + self.set_view("messages") + return None + + if hasattr(self.current_view, "keypress"): + result = self.current_view.keypress(size, key) + if result is None: + return None + + if is_command_key("REPLY_MESSAGE", key) or is_command_key("MENTION_REPLY", key) or is_command_key("QUOTE_REPLY", key) or is_command_key("REPLY_AUTHOR", key): # 'r', 'enter', '@', '>', 'R' + if self.current_view != self.message_view: + self.set_view("messages") + if self.message_view.log: + self.message_view.set_focus(len(self.message_view.log) - 1) + self.current_view.keypress(size, key) + if self.footer.focus is not None: + self.set_focus("footer") + self.footer.focus_position = 1 + return None + + elif is_command_key("STREAM_MESSAGE", key): + if self.current_view != self.message_view: + self.set_view("messages") + self.current_view.keypress(size, key) + if self.footer.focus is None: + stream_id = self.model.stream_id + stream_dict = self.model.stream_dict + if stream_id is None: + self.footer.stream_box_view(0) + else: + self.footer.stream_box_view(caption=stream_dict[stream_id]["name"]) self.set_focus("footer") self.footer.focus_position = 0 - return key - elif is_command_key("GO_LEFT", key): - self.view.show_left_panel(visible=True) - elif is_command_key("GO_RIGHT", key): - self.view.show_right_panel(visible=True) - return super().keypress(size, key) + return None + elif is_command_key("STREAM_NARROW", key): + if self.current_view != self.message_view or not self.view.message_view.focus: + return key + message = self.view.message_view.focus.original_widget.message + if message["type"] != "stream": + return key + self.controller.narrow_to_stream(stream_name=message["stream"]) + self.set_view("messages") + return None + + elif is_command_key("TOPIC_NARROW", key): + if self.current_view != self.message_view or not self.view.message_view.focus: + return key + message = self.view.message_view.focus.original_widget.message + if message["type"] != "stream": + return key + self.controller.narrow_to_topic( + stream_name=message["stream"], + topic_name=message["subject"], + ) + self.set_view("messages") + return None + + elif is_command_key("THUMBS_UP", key): + if self.current_view != self.message_view or not self.view.message_view.focus: + return key + message = self.view.message_view.focus.original_widget.message + self.controller.toggle_message_reaction(message["id"], "thumbs_up") + self.controller.update_screen() + return None + + elif is_command_key("TOGGLE_STAR_STATUS", key): + if self.current_view != self.message_view or not self.view.message_view.focus: + return key + message = self.view.message_view.focus.original_widget.message + self.controller.toggle_message_star_status(message["id"]) + self.controller.update_screen() + return None + + elif is_command_key("ADD_REACTION", key): + if self.current_view != self.message_view or not self.view.message_view.focus: + return key + message = self.view.message_view.focus.original_widget.message + self.controller.show_emoji_picker(message["id"]) + return None + + return super().keypress(size, key) class RightColumnView(urwid.Frame): """ @@ -784,7 +1030,7 @@ def __init__(self, view: Any) -> None: self.stream_v = self.streams_view() self.is_in_topic_view = False - contents = [(4, self.menu_v), self.stream_v] + contents = [(5, self.menu_v), self.stream_v] super().__init__(contents) def menu_view(self) -> Any: @@ -794,6 +1040,8 @@ def menu_view(self) -> Any: count = self.model.unread_counts.get("all_pms", 0) self.view.pm_button = PMButton(controller=self.controller, count=count) + self.view.time_button = TimeMentionedButton(controller=self.controller, count=count) + self.view.mentioned_button = MentionedButton( controller=self.controller, count=self.model.unread_counts["all_mentions"], @@ -807,9 +1055,11 @@ def menu_view(self) -> Any: menu_btn_list = [ self.view.home_button, self.view.pm_button, + self.view.time_button, self.view.mentioned_button, self.view.starred_button, ] + w = urwid.ListBox(urwid.SimpleFocusListWalker(menu_btn_list)) return w