From 51935b1fc25e5b47158a6a45ea8ea1aacfac4845 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Sun, 30 Nov 2025 21:33:52 +0100 Subject: [PATCH 1/7] Threadmenu now supports submenus --- core/thread.py | 71 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/core/thread.py b/core/thread.py index bf77180f8c..2b8148ceec 100644 --- a/core/thread.py +++ b/core/thread.py @@ -849,11 +849,9 @@ async def send_genesis_message(): if getattr(self, "_selected_thread_creation_menu_option", None) and self.bot.config.get( "thread_creation_menu_selection_log" ): - opt = self._selected_thread_creation_menu_option + path = self._selected_thread_creation_menu_option try: - log_txt = f"Selected menu option: {opt.get('label')} ({opt.get('type')})" - if opt.get("type") == "command": - log_txt += f" -> {opt.get('callback')}" + log_txt = f"Selected menu path: {' -> '.join(path)}" await channel.send(embed=discord.Embed(description=log_txt, color=self.bot.mod_color)) except Exception: logger.warning( @@ -2659,29 +2657,32 @@ async def create( placeholder = "Select an option to contact the staff team." timeout = 20 - options = self.bot.config.get("thread_creation_menu_options") or {} - submenus = self.bot.config.get("thread_creation_menu_submenus") or {} - # Minimal inline view implementation (avoid importing plugin code) thread.ready = False # not ready yet class _ThreadCreationMenuSelect(discord.ui.Select): - def __init__(self, outer_thread: Thread): + def __init__(self, bot, outer_thread: Thread, option_data: dict, menu_msg: discord.Message, path: list, is_home: bool = True): + self.bot = bot self.outer_thread = outer_thread - opts = [ + self.option_data = option_data + self.menu_msg = menu_msg + self.path = path + options = [ discord.SelectOption( label=o["label"], description=o["description"], emoji=o["emoji"], ) - for o in options.values() + for o in option_data.values() ] + if not is_home: + options.append(discord.SelectOption(label="main menu", description="Return to the main menu", emoji="🏠")) super().__init__( placeholder=placeholder, min_values=1, max_values=1, - options=opts, + options=options, ) async def callback(self, interaction: discord.Interaction): @@ -2694,10 +2695,37 @@ async def callback(self, interaction: discord.Interaction): except Exception: logger.warning("Failed unsnoozing thread prior to menu selection; continuing.") chosen_label = self.values[0] + self.path.append(chosen_label) # Resolve option key key = chosen_label.lower().replace(" ", "_") - selected = options.get(key) - self.outer_thread._selected_thread_creation_menu_option = selected + if key == "main_menu": + # From a submenu, go back to main menu. This is achieved by rebuilding the view. + option_data = self.bot.config.get("thread_creation_menu_options") or {} + new_view = _ThreadCreationMenuView( + self.bot, + self.outer_thread, + option_data, + self.menu_msg, + path=[], + is_home=True, + ) + return await self.menu_msg.edit(view=new_view) + selected: dict = self.option_data.get(key, {}) + if selected["type"] == "submenu": + # Build new view for submenu + submenu_data = self.bot.config.get("thread_creation_menu_submenus") or {} + option_data = submenu_data.get(key, {}) + new_view = _ThreadCreationMenuView( + self.bot, + self.outer_thread, + option_data, + self.menu_msg, + path=self.path, + is_home=False, + ) + return await self.menu_msg.edit(view=new_view) + + self.outer_thread._selected_thread_creation_menu_option = self.path # Reflect the selection in the original DM by editing the embed/body try: msg = getattr(interaction, "message", None) @@ -2936,10 +2964,13 @@ async def callback(self, interaction: discord.Interaction): ctx_.command.checks = old_checks class _ThreadCreationMenuView(discord.ui.View): - def __init__(self, outer_thread: Thread): + def __init__(self, bot, outer_thread: Thread, option_data: dict, menu_msg: discord.Message, path: list = [], is_home: bool = True): super().__init__(timeout=timeout) self.outer_thread = outer_thread - self.add_item(_ThreadCreationMenuSelect(outer_thread)) + self.path = [] if is_home else path + self.menu_msg = menu_msg + self.option_data = option_data + self.add_item(_ThreadCreationMenuSelect(bot, outer_thread, option_data=option_data, menu_msg=menu_msg, path=self.path, is_home=is_home)) async def on_timeout(self): # Timeout -> abort thread creation @@ -3061,8 +3092,10 @@ async def on_timeout(self): embed.set_thumbnail(url=embed_thumb) except Exception as e: logger.debug("Thumbnail set failed (ignored): %s", e) - menu_view = _ThreadCreationMenuView(thread) - menu_msg = await recipient.send(embed=embed, view=menu_view) + menu_msg = await recipient.send(embed=embed) + option_data = self.bot.config.get("thread_creation_menu_options") or {} + menu_view = _ThreadCreationMenuView(self.bot, thread, option_data, menu_msg) + menu_msg = await menu_msg.edit(view=menu_view) # mark thread as pending menu selection thread._pending_menu = True # Explicitly attach the message to the view for safety in callbacks @@ -3135,10 +3168,6 @@ async def callback(self, interaction: discord.Interaction): "Failed unsnoozing thread prior to precreate menu selection; continuing.", exc_info=True, ) - chosen_label = self.values[0] - key = chosen_label.lower().replace(" ", "_") - selected = options.get(key) - self.outer_thread._selected_thread_creation_menu_option = selected # Remove the view try: msg = getattr(interaction, "message", None) From 55471ee5c14a477fb8369272744423d385cbf7d6 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Sun, 30 Nov 2025 21:36:30 +0100 Subject: [PATCH 2/7] Fix a small issue with path not resetting after main menu. --- core/thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/thread.py b/core/thread.py index 2b8148ceec..08b993be40 100644 --- a/core/thread.py +++ b/core/thread.py @@ -2967,7 +2967,7 @@ class _ThreadCreationMenuView(discord.ui.View): def __init__(self, bot, outer_thread: Thread, option_data: dict, menu_msg: discord.Message, path: list = [], is_home: bool = True): super().__init__(timeout=timeout) self.outer_thread = outer_thread - self.path = [] if is_home else path + self.path = path self.menu_msg = menu_msg self.option_data = option_data self.add_item(_ThreadCreationMenuSelect(bot, outer_thread, option_data=option_data, menu_msg=menu_msg, path=self.path, is_home=is_home)) From 224afd516b12c917cfe8465ea3cbe6946c7249b4 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Sun, 30 Nov 2025 21:44:15 +0100 Subject: [PATCH 3/7] Fix copilot suggestions --- core/thread.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/thread.py b/core/thread.py index 08b993be40..a3836911ca 100644 --- a/core/thread.py +++ b/core/thread.py @@ -2695,7 +2695,6 @@ async def callback(self, interaction: discord.Interaction): except Exception: logger.warning("Failed unsnoozing thread prior to menu selection; continuing.") chosen_label = self.values[0] - self.path.append(chosen_label) # Resolve option key key = chosen_label.lower().replace(" ", "_") if key == "main_menu": @@ -2710,8 +2709,9 @@ async def callback(self, interaction: discord.Interaction): is_home=True, ) return await self.menu_msg.edit(view=new_view) + self.path.append(chosen_label) selected: dict = self.option_data.get(key, {}) - if selected["type"] == "submenu": + if selected.get("type", "command") == "submenu": # Build new view for submenu submenu_data = self.bot.config.get("thread_creation_menu_submenus") or {} option_data = submenu_data.get(key, {}) @@ -2964,7 +2964,7 @@ async def callback(self, interaction: discord.Interaction): ctx_.command.checks = old_checks class _ThreadCreationMenuView(discord.ui.View): - def __init__(self, bot, outer_thread: Thread, option_data: dict, menu_msg: discord.Message, path: list = [], is_home: bool = True): + def __init__(self, bot, outer_thread: Thread, option_data: dict, menu_msg: discord.Message, path: list, is_home: bool = True): super().__init__(timeout=timeout) self.outer_thread = outer_thread self.path = path @@ -3094,7 +3094,7 @@ async def on_timeout(self): logger.debug("Thumbnail set failed (ignored): %s", e) menu_msg = await recipient.send(embed=embed) option_data = self.bot.config.get("thread_creation_menu_options") or {} - menu_view = _ThreadCreationMenuView(self.bot, thread, option_data, menu_msg) + menu_view = _ThreadCreationMenuView(self.bot, thread, option_data, menu_msg, path=[], is_home=True) menu_msg = await menu_msg.edit(view=menu_view) # mark thread as pending menu selection thread._pending_menu = True From 86ab3a7385fc8f8fa03ccaa6c68012820b2d8d2b Mon Sep 17 00:00:00 2001 From: sebkuip Date: Sun, 30 Nov 2025 21:51:00 +0100 Subject: [PATCH 4/7] Black formatting --- core/thread.py | 51 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/core/thread.py b/core/thread.py index a3836911ca..f7a0c1169f 100644 --- a/core/thread.py +++ b/core/thread.py @@ -224,16 +224,12 @@ async def snooze(self, moderator=None, command_used=None, snooze_for=None): "author_name": ( getattr(m.embeds[0].author, "name", "").split(" (")[0] if m.embeds and m.embeds[0].author and m.author == self.bot.user - else getattr(m.author, "name", None) - if m.author != self.bot.user - else None + else getattr(m.author, "name", None) if m.author != self.bot.user else None ), "author_avatar": ( getattr(m.embeds[0].author, "icon_url", None) if m.embeds and m.embeds[0].author and m.author == self.bot.user - else m.author.display_avatar.url - if m.author != self.bot.user - else None + else m.author.display_avatar.url if m.author != self.bot.user else None ), } async for m in channel.history(limit=None, oldest_first=True) @@ -2662,7 +2658,15 @@ async def create( thread.ready = False # not ready yet class _ThreadCreationMenuSelect(discord.ui.Select): - def __init__(self, bot, outer_thread: Thread, option_data: dict, menu_msg: discord.Message, path: list, is_home: bool = True): + def __init__( + self, + bot, + outer_thread: Thread, + option_data: dict, + menu_msg: discord.Message, + path: list, + is_home: bool = True, + ): self.bot = bot self.outer_thread = outer_thread self.option_data = option_data @@ -2677,7 +2681,11 @@ def __init__(self, bot, outer_thread: Thread, option_data: dict, menu_msg: disco for o in option_data.values() ] if not is_home: - options.append(discord.SelectOption(label="main menu", description="Return to the main menu", emoji="🏠")) + options.append( + discord.SelectOption( + label="main menu", description="Return to the main menu", emoji="🏠" + ) + ) super().__init__( placeholder=placeholder, min_values=1, @@ -2724,7 +2732,7 @@ async def callback(self, interaction: discord.Interaction): is_home=False, ) return await self.menu_msg.edit(view=new_view) - + self.outer_thread._selected_thread_creation_menu_option = self.path # Reflect the selection in the original DM by editing the embed/body try: @@ -2964,13 +2972,30 @@ async def callback(self, interaction: discord.Interaction): ctx_.command.checks = old_checks class _ThreadCreationMenuView(discord.ui.View): - def __init__(self, bot, outer_thread: Thread, option_data: dict, menu_msg: discord.Message, path: list, is_home: bool = True): + def __init__( + self, + bot, + outer_thread: Thread, + option_data: dict, + menu_msg: discord.Message, + path: list, + is_home: bool = True, + ): super().__init__(timeout=timeout) self.outer_thread = outer_thread self.path = path self.menu_msg = menu_msg self.option_data = option_data - self.add_item(_ThreadCreationMenuSelect(bot, outer_thread, option_data=option_data, menu_msg=menu_msg, path=self.path, is_home=is_home)) + self.add_item( + _ThreadCreationMenuSelect( + bot, + outer_thread, + option_data=option_data, + menu_msg=menu_msg, + path=self.path, + is_home=is_home, + ) + ) async def on_timeout(self): # Timeout -> abort thread creation @@ -3094,7 +3119,9 @@ async def on_timeout(self): logger.debug("Thumbnail set failed (ignored): %s", e) menu_msg = await recipient.send(embed=embed) option_data = self.bot.config.get("thread_creation_menu_options") or {} - menu_view = _ThreadCreationMenuView(self.bot, thread, option_data, menu_msg, path=[], is_home=True) + menu_view = _ThreadCreationMenuView( + self.bot, thread, option_data, menu_msg, path=[], is_home=True + ) menu_msg = await menu_msg.edit(view=menu_view) # mark thread as pending menu selection thread._pending_menu = True From ddc18142f3a7bc9a614c13ad80b55a4100e86ed6 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Sun, 30 Nov 2025 21:56:07 +0100 Subject: [PATCH 5/7] Fix undeclared vars --- core/thread.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/thread.py b/core/thread.py index f7a0c1169f..983b52aedb 100644 --- a/core/thread.py +++ b/core/thread.py @@ -3195,6 +3195,10 @@ async def callback(self, interaction: discord.Interaction): "Failed unsnoozing thread prior to precreate menu selection; continuing.", exc_info=True, ) + # Define selection variables + chosen_label = self.values[0] + key = chosen_label.lower().replace(" ", "_") + selected = options.get(key) # Remove the view try: msg = getattr(interaction, "message", None) From febf14bcbbbd64e6dbc226562a180499380b7ed5 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:05:57 +0100 Subject: [PATCH 6/7] threadmenu: submenu navigation fixes Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> --- cogs/threadmenu.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cogs/threadmenu.py b/cogs/threadmenu.py index 7f9e193844..0e225d527f 100644 --- a/cogs/threadmenu.py +++ b/cogs/threadmenu.py @@ -178,6 +178,9 @@ def typecheck(m): if label.lower() == "cancel": return await ctx.send("Cancelled.") + if label.lower() == "main menu": + return await ctx.send("You cannot use that label.") + if sanitized_label in conf["options"]: await ctx.send("That option already exists. Use `threadmenu edit` to edit it.") return From d511c3a45bb7c6e30be2a5aefac259528aa67176 Mon Sep 17 00:00:00 2001 From: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:06:46 +0100 Subject: [PATCH 7/7] threadmenu: submenu navigation fixes Refactor thread creation menu handling to improve path management and submenu navigation. Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com> --- core/thread.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/core/thread.py b/core/thread.py index 983b52aedb..d792c582d9 100644 --- a/core/thread.py +++ b/core/thread.py @@ -2706,7 +2706,6 @@ async def callback(self, interaction: discord.Interaction): # Resolve option key key = chosen_label.lower().replace(" ", "_") if key == "main_menu": - # From a submenu, go back to main menu. This is achieved by rebuilding the view. option_data = self.bot.config.get("thread_creation_menu_options") or {} new_view = _ThreadCreationMenuView( self.bot, @@ -2717,23 +2716,34 @@ async def callback(self, interaction: discord.Interaction): is_home=True, ) return await self.menu_msg.edit(view=new_view) - self.path.append(chosen_label) selected: dict = self.option_data.get(key, {}) + next_path = [*self.path, chosen_label] if selected.get("type", "command") == "submenu": - # Build new view for submenu submenu_data = self.bot.config.get("thread_creation_menu_submenus") or {} - option_data = submenu_data.get(key, {}) + submenu_key = selected.get("callback", key) + option_data = submenu_data.get(submenu_key, {}) + if not option_data: + home_options = self.bot.config.get("thread_creation_menu_options") or {} + new_view = _ThreadCreationMenuView( + self.bot, + self.outer_thread, + home_options, + self.menu_msg, + path=[], + is_home=True, + ) + return await self.menu_msg.edit(view=new_view) new_view = _ThreadCreationMenuView( self.bot, self.outer_thread, option_data, self.menu_msg, - path=self.path, + path=next_path, is_home=False, ) return await self.menu_msg.edit(view=new_view) - self.outer_thread._selected_thread_creation_menu_option = self.path + self.outer_thread._selected_thread_creation_menu_option = next_path # Reflect the selection in the original DM by editing the embed/body try: msg = getattr(interaction, "message", None) @@ -3195,10 +3205,10 @@ async def callback(self, interaction: discord.Interaction): "Failed unsnoozing thread prior to precreate menu selection; continuing.", exc_info=True, ) - # Define selection variables chosen_label = self.values[0] key = chosen_label.lower().replace(" ", "_") selected = options.get(key) + self.outer_thread._selected_thread_creation_menu_option = selected # Remove the view try: msg = getattr(interaction, "message", None)