""" Provides a means of presenting long messages by paging them. Paging requires the source message to insert `PAGE_BREAK` characters at meaningful breaks, preferably at fairly uniform intervals. """ from typing import Optional from discord import Interaction from discord.ui import LayoutView, TextDisplay, ActionRow, Button from rocketbot.utils import dump_stacktrace PAGE_BREAK = '\f' def paginate(text: str) -> list[str]: """ Breaks long message text into one or more pages, using page break markers as potential clean break points. """ max_page_size = 2000 chunks = text.split(PAGE_BREAK) pages = [ '' ] for chunk in chunks: if len(chunk) > max_page_size: raise ValueError('Help content needs more page breaks! One chunk is too big for message.') if len(pages[-1] + chunk) < max_page_size: pages[-1] += chunk else: pages.append(chunk) page_count = len(pages) if page_count == 1: return pages # Do another pass and try to even out the page lengths indices = [ i * len(chunks) // page_count for i in range(page_count + 1) ] even_pages = [ ''.join(chunks[indices[i]:indices[i + 1]]) for i in range(page_count) ] for page in even_pages: if len(page) > max_page_size: # We made a page too big. Give up. return pages return even_pages async def update_paged_content( interaction: Interaction, original_interaction: Optional[Interaction], current_page: int, pages: list[str], **send_args, ) -> None: """ Posts and/or updates the content of a message from paged content. Parameters ---------- interaction : Interaction the current interaction, either the initial one or from a button press original_interaction : Interaction the first interaction that triggered presentation of the content, or None if this already is the first interaction current_page : int page index to show (assumed to be in bounds) pages : list[str] array of page content **send_args additional arguments to pass to the send_message method """ if len(pages) == 1: # No paging needed await interaction.response.send_message( pages[0], ephemeral=True, ) return try: view = _PagingLayoutView(current_page, pages, original_interaction or interaction) resolved = interaction if original_interaction is not None: # We have an original interaction from the initial command and a # new one from the button press. Use the original to swap in the # new page in place, then acknowledge the new one to satisfy the # API that we didn't fail. await original_interaction.edit_original_response( view=view, ) if interaction is not original_interaction: await interaction.response.defer(ephemeral=True, thinking=False) else: # Initial send await resolved.response.send_message( view=view, ephemeral=True, **send_args, ) except BaseException as e: dump_stacktrace(e) class _PagingLayoutView(LayoutView): def __init__( self, current_page: int, pages: list[str], original_interaction: Optional[Interaction], **send_args, ) -> None: super().__init__() self.current_page: int = current_page self.pages: list[str] = pages self.text.content = self.pages[self.current_page] + f'\n\n_Page {self.current_page + 1} of {len(self.pages)}_' self.original_interaction = original_interaction if current_page <= 0: self.handle_prev_button.disabled = True if current_page >= len(self.pages) - 1: self.handle_next_button.disabled = True self.send_args = send_args text = TextDisplay('') row = ActionRow() @row.button(label='< Prev') async def handle_prev_button(self, interaction: Interaction, button: Button) -> None: new_page = max(0, self.current_page - 1) await update_paged_content(interaction, self.original_interaction, new_page, self.pages, **self.send_args) @row.button(label='Next >') async def handle_next_button(self, interaction: Interaction, button: Button) -> None: new_page = min(len(self.pages) - 1, self.current_page + 1) await update_paged_content(interaction, self.original_interaction, new_page, self.pages, **self.send_args)