diff --git a/docs/hotkeys.md b/docs/hotkeys.md index 43bbf6125a..61433c8d14 100644 --- a/docs/hotkeys.md +++ b/docs/hotkeys.md @@ -136,4 +136,5 @@ |View current message in browser|v| |Show/hide full rendered message|f| |Show/hide full raw message|r| +|Show/hide poll voter list|M| diff --git a/zulipterminal/config/keys.py b/zulipterminal/config/keys.py index a86420a364..77e8cb639a 100644 --- a/zulipterminal/config/keys.py +++ b/zulipterminal/config/keys.py @@ -457,6 +457,11 @@ class KeyBinding(TypedDict): 'help_text': 'Show/hide full raw message', 'key_category': 'msg_info', }, + 'SHOW_POLL_VOTES': { + 'keys': ['M'], + 'help_text': 'Show/hide poll voter list', + 'key_category': 'msg_info', + }, 'NEW_HINT': { 'keys': ['tab'], 'help_text': 'New footer hotkey hint', diff --git a/zulipterminal/core.py b/zulipterminal/core.py index 61c5f79922..3540f527cb 100644 --- a/zulipterminal/core.py +++ b/zulipterminal/core.py @@ -43,6 +43,7 @@ MarkdownHelpView, MsgInfoView, NoticeView, + PollResultsView, PopUpConfirmationView, StreamInfoView, StreamMembersView, @@ -281,6 +282,34 @@ def show_msg_info( ) self.show_pop_up(msg_info_view, "area:msg") + def show_poll_vote( + self, + poll_question: str, + options: Dict[str, Dict[str, Any]], + ) -> None: + options_with_names = {} + for option_key, option_data in options.items(): + option_text = option_data["option"] + voter_ids = option_data["votes"] + + voter_names = [] + for voter_id in voter_ids: + voter_names.append(self.model.user_name_from_id(voter_id)) + + options_with_names[option_key] = { + "option": option_text, + "votes": voter_names if voter_names else [], + } + + self.show_pop_up( + PollResultsView( + self, + poll_question, + options_with_names, + ), + "area:msg", + ) + def show_emoji_picker(self, message: Message) -> None: all_emoji_units = [ (emoji_name, emoji["code"], emoji["aliases"]) diff --git a/zulipterminal/ui_tools/messages.py b/zulipterminal/ui_tools/messages.py index 8fe2bb8f5c..f2b6785245 100644 --- a/zulipterminal/ui_tools/messages.py +++ b/zulipterminal/ui_tools/messages.py @@ -69,6 +69,7 @@ def __init__(self, message: Message, model: "Model", last_message: Any) -> None: self.topic_links: Dict[str, Tuple[str, int, bool]] = dict() self.time_mentions: List[Tuple[str, str]] = list() self.last_message = last_message + self.widget_type: str = "" # if this is the first message if self.last_message is None: self.last_message = defaultdict(dict) @@ -733,9 +734,9 @@ def main_view(self) -> List[Any]: ) if self.message.get("submessages"): - widget_type = find_widget_type(self.message.get("submessages", [])) + self.widget_type = find_widget_type(self.message.get("submessages", [])) - if widget_type == "todo": + if self.widget_type == "todo": title, tasks = process_todo_widget(self.message.get("submessages", [])) todo_widget = "To-do\n" + f"{title}" @@ -757,28 +758,28 @@ def main_view(self) -> List[Any]: # though it's not very useful. self.message["content"] = todo_widget - elif widget_type == "poll": - poll_question, poll_options = process_poll_widget( + elif self.widget_type == "poll": + self.poll_question, self.poll_options = process_poll_widget( self.message.get("submessages", []) ) # TODO: ZT doesn't yet support adding poll questions after the # creation of the poll. So, if the poll question is not provided, # we show a message to add one via the web app. - if not poll_question: - poll_question = ( + if not self.poll_question: + self.poll_question = ( "No poll question is provided. Please add one via the web app." ) - poll_widget = f"Poll\n{poll_question}" + poll_widget = f"Poll\n{self.poll_question}" - if poll_options: + if self.poll_options: max_votes_len = max( len(str(len(option["votes"]))) - for option in poll_options.values() + for option in self.poll_options.values() ) - for option_info in poll_options.values(): + for option_info in self.poll_options.values(): padded_votes = f"{len(option_info['votes']):>{max_votes_len}}" poll_widget += f"\n[ {padded_votes} ] {option_info['option']}" else: @@ -1188,4 +1189,6 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: self.model.controller.show_emoji_picker(self.message) elif is_command_key("MSG_SENDER_INFO", key): self.model.controller.show_msg_sender_info(self.message["sender_id"]) + elif is_command_key("SHOW_POLL_VOTES", key) and self.widget_type == "poll": + self.model.controller.show_poll_vote(self.poll_question, self.poll_options) return key diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 02b3afbd0b..effba3b7f5 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -23,6 +23,7 @@ from zulipterminal.config.symbols import ( CHECK_MARK, COLUMN_TITLE_BAR_LINE, + INVALID_MARKER, PINNED_STREAMS_DIVIDER, SECTION_DIVIDER_LINE, ) @@ -2176,3 +2177,37 @@ def keypress(self, size: urwid_Size, key: str) -> str: self.controller.exit_popup() return key return super().keypress(size, key) + + +class PollResultsView(PopUpView): + def __init__( + self, + controller: Any, + poll_question: str, + poll_options: Dict[str, Dict[str, Any]], + ) -> None: + poll_results_content: List[Tuple[str, List[Tuple[str, str]]]] = [("", [])] + + for option_key, option_data in poll_options.items(): + option_text = option_data["option"] + if len(option_text) >= 13: + option_text = option_text[:10] + "…" + voter_names = option_data["votes"] + + voters_display = ( + "\n".join(map(str, voter_names)) + if voter_names + else f"{INVALID_MARKER} No votes yet" + ) + + poll_results_content[0][1].append((option_text, voters_display)) + + popup_width, column_widths = self.calculate_table_widths( + poll_results_content, len(poll_question) + ) + + widgets = self.make_table_with_categories(poll_results_content, column_widths) + + super().__init__( + controller, widgets, "SHOW_POLL_VOTES", popup_width, poll_question + )